ディーバ Blog

大阪発 C#の会社、株式会社ディーバの Blog です。

iOS 11 対応でアプリを修正したところ

開発している iOS 10 アプリをそのまま iOS 11.0 で動かすと不具合があったので修正したところです。

※ Xamarin.iOS + MvvmCross およびその他ライブラリを使っている環境です。

Document picker のデザインが異なる

iOS 11 では Document picker の navigation bar が白に、status bar の style が黒文字前提のデザインになっていました。

インポート時 (UIDocumentPickerMode.Import) の UIDocumentPickerViewController を表示したところ。

f:id:jz5_diva:20170927071101p:plain

左: iOS 11 右: iOS 10

iOS 10 アプリでしていたこと

  • アプリ全体の navigation bar の背景色・テキストの色を UINavigationBar.Appearance.BarTintColorUINavigationBar.Appearance.TintColor とで指定
  • アプリ全体の status bar の style を info.plist で指定
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>

iOS 11 アプリ用に修正したところ

  • iOS 10 で行なっていたアプリ全体の navigation bar の色、status bar の style 指定をやめ、ViewController ごとに毎回指定するようにしました。
NavigationController.NavigationBar.BarTintColor = UIColor.FromRGB(0x3f, 0x51, 0xb5);
NavigationController.NavigationBar.TintColor = UIColor.White;
NavigationController.NavigationBar.BarStyle = UIBarStyle.Black;

修正後の画面

f:id:jz5_diva:20170927071648p:plain

左: iOS 11 右: iOS 10

Document picker で選んだファイルを開けない

肝心なことが動かなくなっていました。DidPickDocument イベントが発生せず、iOS 11 からの DidPickDocumentAtUrls で処理する必要があるようです。

iOS 10 アプリのコード

var picker = new UIDocumentPickerViewController(new[] { "public.item" }, UIDocumentPickerMode.Import);
picker.DidPickDocument += (sender, args) =>
{
    // Do something
};
PresentViewController(picker, true, null);

iOS 11 アプリ用に修正したコード

var picker = new UIDocumentPickerViewController(new[] { "public.item" }, UIDocumentPickerMode.Import);

if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0))
{
    picker.DidPickDocumentAtUrls += (sender, args) =>
    {
        // Do something
    };
}
else
{
    picker.DidPickDocument += (sender, args) =>
    {
        // Do something
    };
}
PresentViewController(picker, true, null);

UIActivity で リソースファイルを共有できない

BundleResource としてアプリに組み込んでいるファイルを直接 BundlePath から URL 文字列を作成し UIActivityViewController へ渡すと「〜にコピー」というようなアプリへはファイルを渡せません。

f:id:jz5_diva:20170927082106p:plain

iOS 10 アプリのコード

var path = Path.Combine(NSBundle.MainBundle.BundlePath, "help.pdf");
var url = new NSUrl(path, false); // filepath to url (file://)

var activityItems = new NSObject[] { url };
var activityController = new UIActivityViewController(activityItems, null);
if (activityController.PopoverPresentationController != null)
{
        activityController.PopoverPresentationController.SourceView = View;
        activityController.PopoverPresentationController.BarButtonItem = _shareButton;
}
PresentViewController(activityController, true, null);

iOS 11 アプリように修正したコード

対象のファイルを一時フォルダーにコピーするようにしました。

var path = Path.Combine(NSBundle.MainBundle.BundlePath, "help.pdf");
var tempPath = Path.Combine(Path.GetTempPath(), "help.pdf");
File.Copy(path, tempPath, true);
var url = new NSUrl(tempPath, false); // filepath to url (file://)

var activityItems = new NSObject[] { url };
var activityController = new UIActivityViewController(activityItems, null);
if (activityController.PopoverPresentationController != null)
{
    activityController.PopoverPresentationController.SourceView = View;
    activityController.PopoverPresentationController.BarButtonItem = _shareButton;
}
PresentViewController(activityController, true, null);

UIToolbar 上の UIButton が押せない

トリッキーなコードですが、UIToobar の Subview に追加した UIButton がタップできません。

このコードがあるのが BTProgressHUD 。loading/progress 画面のキャンセルボタンが反応しません。ACR User Dialogs で参照しているライブラリなので ACR User Dialogs を使っていても問題が起こります。

f:id:jz5_diva:20170927074358p:plain

※ iOS 10 では動いているので view の UserInteractionEnabled を true する話とは違います。

調べていると UIToolbar にひとつ以上 UIBarButtonItem があると UIButton も押せるという回避策を見つけました。

iOS 11 アプリでは、とりあえずライブラリのコードを修正して UIToolbar を使わず UIView に置き換えました。

MvvmCross で UITableViewCell の要素の binding で表示不具合

ニッチなのでさらっと紹介すると、MvvmCross を使って ViewModel 側でリスト項目のテキストやアイコン画像を cell の view(ImageVIew など)に binding していました。

iOS 11 では UITableView の表示と ViewModel に値を設定するタイミングで、ImageView.Image への binding している画像が表示されない現象が起きました。

View 側:

public class CustomStyleCell : MvxTableViewCell
{
    [Export("initWithStyle:reuseIdentifier:")]
    public CustomStyleCell(UITableViewCellStyle style, NSString cellIdentifier) : base("", UITableViewCellStyle.Value1, cellIdentifier)
    {
        var set = this.CreateBindingSet<CustomStyleCell, ListItemViewModel>();
        set.Bind(TextLabel).To(vm => vm.Text);
        set.Bind(DetailTextLabel).To(vm => vm.DetailText);
        set.Bind(ImageView).For(v => v.Image).To(vm => vm.Image).WithConversion(new NameToUIImageValueConverter());
        set.Apply();
    }
}
public class NameToUIImageValueConverter : MvxValueConverter<string, UIImage>
{
    protected override UIImage Convert(string value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value == null)  ? null : UIImage.FromBundle(value);
    }
}

ViewModel:

public class ListItemViewModel : MvxNotifyPropertyChanged
{
    public string Text
    {
        get => _text;
        set => SetProperty(ref _text, value);
    }
    private string _text;

    public string DetailText
    {
        get => _detailText;
        set => SetProperty(ref _detailText, value);
    }
    private string _detailText;

    public string Image
    {
        get => _image;
        set => SetProperty(ref _image, value);
    }
    private string _image;
}

iOS 11 アプリ用に修正したコード

iOS 11 アプリでは MvxTableViewSource の GetOrCreateCellFor 内で生成した cell の ImageView.Image へ直接 ViewModel の値を設定するように「も」しました。

public class CustomTableViewSource<TCell> : MvxTableViewSource where TCell : MvxTableViewCell
{
    protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
    {
        var cell = tableView.DequeueReusableCell("cellIdentifier");
        var vm = (ListItemViewModel)item;
        if (vm != null)
        {
            // workaround
            if (!string.IsNullOrWhiteSpace(vm.Image))
                cell.ImageView.Image = UIImage.FromBundle(vm.Image);
        }
        return cell;
    }
}

「Xamarinネイティブによるモバイルアプリ開発 C#によるAndroid/iOS UI 制御の基礎」が出版されました

青柳です。

先週、9月15日に私が執筆した Xamarinネイティブによるモバイルアプリ開発 CによるAndroid/iOS UI制御の基礎 が出版されました。

CodeZineに紹介記事があります。
C#でアプリ開発できるXamarinを体験!『Xamarinネイティブによるモバイルアプリ開発』
キャンペーンページ もあります。

ちなみに、こういうのは私も全然知らなくて、Twitterとかでつぶやかれているのを見てこういうページがあることを知りました。アマゾンの表紙写真にもあるようにちょまどさんのコメントが帯に載る(しかも写真入り)というのもちょまどさんのツイートで知ったくらいだったりします。事前に編集さんから「マイクロソフトの方にコメントをもらえないかと思っている」ということは聞いてはいましたがそれがこういう風になるとは。ちょまどさんは私はきちんとはお会いしたことはない(去年のde:codeのときに遠くからお見かけしたことはある)のですがマイクロソフトのエバンジェリストの方で、Xamarin界隈でもとても有名ですね。そんな方に「良本」なんて言っていただけて嬉しい限りです。

キャンペーンページやちょまどさん効果といったことも多分にあると思いますが、8月下旬からアマゾンの「モバイルプログラミング」のジャンルで1~5位くらいのあたりをうろうろしています。毎日見ていたわけではありませんがだいたいそれくらいみたいです。「プログラミング」のジャンルでは20位ちょっとくらいでした。このジャンルだと上位はディープラーニングとかPythonとかExcel VBAなどのいかにも売れ筋の本たちですのでかなり健闘していると言っていいんじゃないかと思います。うれしいなぁ

ちょっと思い出話を

もともと今回の執筆の話をいただいたのは去年の7月くらいです。もう1年以上経つんですね。shoeisha.co.jpドメインの知らない方からメールで「Xamarin書籍の企画があるんだけど興味ありませんか?」と来たのが最初だったと思います。もしかしたらTwitterやFacebook Messengerだったかもしれませんが。まぁ、そんな感じで始まりました。
本業があるので執筆は夜や休日が中心になります。なので、執筆を生業としている人に比べるとどうしても時間はかかると思います。そんな話をしてそれでOKということでやらせていただきました。
ちなみに本業は 株式会社ディーバ の代表取締役です。経営業だけでなくプログラマーとしてガシガシコードも書いてます。むしろそちらの方が比重としては高いくらいです。ほんとは経営業に専念した方がいいというのはわかってるんですがなにぶん人手不足でして。(ちなみに求人中です。興味のある方いたら連絡ください。XamarinだけでなくUWP、WPF、ASP.NET MVCといろいろやってます。どれもC#です)
話がそれましたがこういう立場なのでサラリーマンの方と違い調整さえつけば時間は自由になります。業務時間中に執筆することもできます。けど、日中はなんだかんだと予定が入ってやっぱり執筆は自宅に帰ってからの夜や休日が中心でした。そうするとどうしても途切れがちになるし、忙しいときには睡眠時間を削るしかなくなって、思った以上に大変でした。

本当ならもうちょっと執筆に時間を取りたかったです。技術的に間違ったことを書くことがないように、その面についてしっかり時間を取ってやったつもりです。けど、わかりやすい文章になるように吟味するとか、図表を追加するとか、そういった面に関してはちょっと時間不足だったと感じています。個人のブログとかWeb上の解説記事とかと違い、これだけのボリュームがあると書いてる途中で「この間書いたあそこはこう変えよう。あっちも直した方がいいな」といろいろ出てきて、けどそれを全部やっていたら時間足りないし、とどんどんとっちらかっていって収拾がつかなくなってしまうんです。プロの執筆家の人ってどうしてるんだろう?
分量も難しかったです。最初の時点で「350ページくらい」というのは決まっていました。以前に WindowsPhoneビジネスアプリケーション開発ガイド という書籍を秀和システムさんから出したんですが、これも340ページと同じくらいの分量でした(ちなみに、この本も編集さんの方から「こういう企画があるんですが興味ありませんか?」とメールが来たのが最初でした)。けど、2回目だからと言って感がつかめているわけでもなく書きたいように書いていったら思ったより多くなってしまいました。実は半分くらいの時点では「このままだとページ数足りないかもしれませんね」と言われてたんです。けど、その後書き続けていったら450ページくらいになってしまいました。それでも書きたいことをすべて書ききったというわけではなかったんですが。結局、なくても支障はないというところを削って本文380ページくらい、削った部分だけどせっかくなら読んでもらえるといいよね、というところを購入者特典としてPDFで提供するという形にすることになりました。

がんばって書いたので興味のある方は手にとっていただけると幸いです。

Xamarin ネイティブによるモバイルアプリ開発 C# による Android/iOS UI 制御の基礎

株式会社ディーバ 代表取締役 青柳臣一が、Xamarinネイティブによるモバイルアプリ開発 CによるAndroid/iOS UI制御の基礎 (翔泳社)を執筆しました。

2017年9月15日発売です。

AndroidもiOSも。 C#を使ったクロスプラットフォーム開発に挑戦!

これまで別々の言語や環境で開発していたAndroid/iOSアプリを 共通の言語(C#)や環境(.NET Framework)で作成する 注目の開発環境Xamarin。

マイクロソフトMVP受賞経験のある著者が、豊富なアプリケーション開発の 経験を活かし、すべてのモバイルアプリ開発者に向けてXamarinネイティブ によるクロスプラットフォーム開発を徹底解説しました。

本書は「Xamarinとは何か」にはじまり、汎用ライブラリMVVMCrossを 使って、本格的なクロスプラットフォームを開発するノウハウまで、 ミニサンプルアプリを作りながらわかりやすく学ぶことができます。

また、特にAndroid/iOS開発経験のある読者のために、 JavaやObjective-C、Swiftと比較しながらの解説も行っています。

現場の開発者のための、Xamarinを使ったクロスプラットフォーム開発を スムーズに学べる一冊です。

Xamarin.Android SfDataGrid でセルの背景色を変える

Syncfusion SfDataGrid でセルの背景色を変える方法です。

※ この記事の投稿時点の SfDataGrid のバージョンは v15.2.0.46 です。

f:id:jz5_diva:20170722011745j:plain

セルの背景色の変更は GridCell クラスを継承したクラスを GridColumn クラス UserCellType プロパティに指定します。背景色以外も自由にカスタマイズできます。

ちなみに、行全体の背景色を変えるには DataGridStyle クラスが使えます(Styles 参照)。

次のようにユーザー定義のセルを作ります。

public class ColoredCell : GridCell
{
    private readonly TextView _textView;
    private readonly View _view;

    public ColoredCell(Context context) : base(context)
    {
        _view = new View(context);
        _textView = new TextView(context)
        {
            Gravity = GravityFlags.Center
        };
        _textView.SetTextColor(Color.Black);

        AddView(_view);
        AddView(_textView);
        CanRenderUnLoad = false;
    }

    protected override void UnLoad()
    {
        (Parent as VirtualizingCellsControl)?.RemoveView(this);
    }

    protected override void OnLayout(bool change, int l, int t, int r, int b)
    {
        _view.Layout(1, 1, Width - 1, Height - 1);
        _textView.Layout(1, (int)(10 * Resources.DisplayMetrics.Density), Width, Height);
    }

    protected override void OnDraw(Canvas canvas)
    {
        base.OnDraw(canvas);

        var valueText = DataColumn.CellValue.ToString();
        _textView.Text = valueText;

        // セルの値が true のとき背景色を変える
        if (bool.TryParse(valueText, out var result) && result)
        {
            _view.SetBackgroundColor(Color.Orange);
        }
    }
}

GridColumn.UserCellType に設定します。

var col = new GridTextColumn {
    // ...
};
col.UserCellType = typeof(ColoredCell);
_dataGrid.Columns.Add(col);

Xamarin.iOS SfDataGrid でセルの背景色を変える

Syncfusion SfDataGrid でセルの背景色を変える方法です。

※ この記事の投稿時点の SfDataGrid のバージョンは v15.2.0.46 です。

f:id:jz5_diva:20170722013749p:plain

セルの背景色の変更は GridCell クラスを継承したクラスを GridColumn クラス UserCellType プロパティに指定します。背景色以外も自由にカスタマイズできます。

ちなみに、行全体の背景色を変えるには DataGridStyle クラスが使えます(Styles 参照)。

次のようにユーザー定義のセルを作ります。

public class ColoredCell : GridCell
{
    UILabel _label;

    public ColoredCell()
    {
        _label = new UILabel
        {
            Font = UIFont.SystemFontOfSize(20)
        };
        Add(_label);
        CanRenderUnLoad = false;
    }

    protected override void UnLoad()
    {
        RemoveFromSuperview();
    }

    public override void Draw(CGRect rect)
    {
        _label.Font = DataColumn.GridColumn.RecordFont;
        _label.TextColor = DataColumn.Renderer.DataGrid.GridStyle.GetRecordForegroundColor();
        _label.TextAlignment = UITextAlignment.Center;
        base.Draw(rect);
    }

    public override void LayoutSubviews()
    {
        base.LayoutSubviews();

        var valueText = DataColumn.CellValue.ToString();

        _label.Frame = new CGRect(0, Bounds.Top, Bounds.Width, Bounds.Height);
        _label.Font = DataColumn.GridColumn.RecordFont;
        _label.TextAlignment = UITextAlignment.Center;
        _label.Text = valueText;

        // セルの値が true のとき背景色を変える
        if (bool.TryParse(valueText, out var result) && result)
        {
            _label.BackgroundColor = UIColor.Orange;
        }
    }

    protected override void Dispose(bool disposing)
    {
        if (_label != null)
        {
            _label.Dispose();
            _label = null;
        }

        base.Dispose(disposing);
    }
}

GridColumn.UserCellType に設定します。

var col = new GridTextColumn {
    // ...
};
col.UserCellType = typeof(ColoredCell);
_dataGrid.Columns.Add(col);

Xamarin.iOS SfDataGrid 編集時のキーボードに完了ボタンを追加する

Syncfusion SfDataGrid で編集する際に キーボードの種類(KeyboardType)を変更 した上で、完了ボタン(Done ボタン)を追加して、編集の確定とキーボードを閉じる方法です。

※ この記事の投稿時点の SfDataGrid のバージョンは v15.2.0.46 です。

f:id:jz5_diva:20170721222320p:plain

Editing 機能で Excel のセルのように編集できます。キーボードの種類を指定するには、カスタム GridCellTextViewRenderer クラスを使います。

さらに、数字のキーパッドの場合、入力を確定する方法・キーボードを閉じる方法がわかりにくいので「完了ボタン」を追加します。このアイデアは「(UITextViewの)キーボードに「閉じる」ボタンを追加する - ながいものには、まかれたくない」から。

public class GridCellTextViewRendererExt : GridCellTextViewRenderer
{
    public override void OnInitializeEditView(DataColumnBase dataColumn, UITextField view)
    {
        base.OnInitializeEditView(dataColumn, view);

        // KeyboardType 変更
        view.KeyboardType = UIKeyboardType.NumberPad;

        // Toolbar 追加
        var toolbar = new UIToolbar(new CGRect(0, 0, 60, 44))
        {
            BarStyle = UIBarStyle.Default,
            Translucent = true
        };
        toolbar.SizeToFit();
        var spacer = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace);
        var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, (o, e) =>
        {
            view.ResignFirstResponder();
        });
        doneButton.Clicked += (o, e) =>
        {
            // 編集を確定する
            dataColumn.Renderer.DataGrid.EndEdit();
        };

        toolbar.Items = new[] { spacer, doneButton };
        view.InputAccessoryView = toolbar;
    }
}

作成したカスタムレンダラーに差し替えます。

_dataGrid = new SfDataGrid(this);

// Add custom renderer    
_dataGrid.CellRenderers.Remove("TextView");
_dataGrid.CellRenderers.Add("TextView", new GridCellTextViewRendererExt());

Xamarin.Android SfDataGrid で編集時にキーボードの種類を変える

Syncfusion SfDataGrid で編集する際にキーボードの種類(InputType)を変更する方法です。

※ この記事の投稿時点の SfDataGrid のバージョンは v15.2.0.46 です。

f:id:jz5_diva:20170722010425j:plain

Editing 機能で Excel のセルのように編集できます。キーボードの種類を指定するには、カスタム GridCellTextViewRenderer クラスを使います。

セルに入力できる値を制限をする場合、Column Types の GridNumericColumn 等も使えます。

public class GridCellTextViewRendererExt : GridCellTextViewRenderer
{
    protected override void OnEnteredEditMode(DataColumnBase dataColumn, View currentRendererElement)
    {
        base.OnEnteredEditMode(dataColumn, currentRendererElement);

        // MappingName により分岐させる場合は次のように参照
        if (dataColumn.GridColumn.MappingName != "ProductNo") return;
        
        // 行のデータにより分岐させる場合は次のように RowData を参照
        // var item = dataColumn.RowData as MyViewModel;

        var editText = currentRendererElement as EditText;
        if (editText == null) return;

        // InputTypes を変更
        editText.InputType = InputTypes.ClassNumber;
    }
}

作成したカスタムレンダラーに差し替えます。

_dataGrid = new SfDataGrid(this);

// Add custom renderer    
_dataGrid.CellRenderers.Remove("TextView");
_dataGrid.CellRenderers.Add("TextView", new GridCellTextViewRendererExt());