vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Xamarin.iOS】Xibで作ったTableViewCellを動的に追加したい

はじめに

以前書きましたが、 iPhone X 買いました。

マスクしてると顔認証してくれないとか不満が完全に無いわけではありませんが、
まぁ良好、といったところです。

で、せっかくなので何かアプリを作りたいですよね?

ということで、まずは目覚ましアプリに挑戦してみることにしました。

環境と仕様

開発言語

言語ですが、通常なら Swift となるわけですが、今回は Xamarin.iOS を使ってみることにしました。

ここ最近 Xamarin.Forms を使っていて Xamarin.iOS にも興味があった、ということが大きな理由です。

また、お仕事でもごくまれに Swift を使ってアプリを作る時があるのですが (3D を使わず、かつ BLE などネイティブの機能が必要な場合) 、
ほとんど触ったことのない Swift を言語仕様を調べながら書くほうが良いのか、
曲がりなりにも日常的に触れている C# を使って、 Xamarin.iOS の仕様を調べながら書くほうが良いのかは調べてみたいと思っていました。

なおXamarin.Forms を選ばなかった理由は、単に Xamarin.iOS を使ってみたかったからですw

Swift は Swift で注目すべき言語の一つだとは思いますので、別途追いかけたいところです。

開発環境

WindowsMacVisual Studio を使って開発しています。

本当は Windows からリモート接続で開発をしたいところなのですが、
ネットワーク状況からかなかなか接続できない時があったりするため、
Mac と接続していないとできない Storyboard 操作の部分は Mac でやっちゃうようにしています。

Mac だけにしない理由は家の Mac がラップトップでない (Mac mini) ことと、
2012 年モデルなのでさすがに動きがちょっともっさりしているというのが理由です。

仕様

まずはできるだけシンプルに作ることにします。

  • セットした時間になったら目覚ましが鳴る
  • 目覚ましが鳴る時間を追加、編集、削除できる
  • 目覚ましを曜日で指定できる
  • 目覚ましの On/Off ができる

とりあえず日本時間のみを考慮し、サマータイムなどは考えないことにします。
(うるう年とかは考慮が必要)

あと、次の 24 時間だけ目覚ましをキャンセルする機能も追加したいと思います。

Android 標準の目覚ましではこの機能があり、便利に使っていたためです。
(朝にあんまり強くないので、目覚ましを複数回設定するので)

現状、とりあえずの見た目はこんな感じです。

f:id:mslGt:20171230074435p:plain

親となる View にヘッダーと UITableView を設置し、
UITableView に UITableViewCell を追加しています。

StoryboardとViewController

Swift や Objective-C でアプリを作る場合、
Storyboard にボタンなどを追加し、 Outline で ViewController に紐づけます。

Xamarin.iOS も同じかな?と思ったのですが、実は Xamarin.Forms の XAML とコードビハインドの関係と同じように、
Storyboard で Identity の Name に名前を設定しておくと、
ViewController からその名前でアクセスすることができます。

すごい!

コードでボタンを追加する

追加ボタンを押したときに、動的に要素を追加したい。

ということで、まずは試しに ViewController からボタンを追加してみます。

ViewController.cs

using System;
using UIKit;

namespace XamarinIOsSample
{
    public partial class ViewController : UIViewController
    {
        private UIButton button;
        
        public ViewController(IntPtr handle) : base(handle)
        {
            // 親となるViewに対する設定.
            View.BackgroundColor = UIColor.White;
            Title = "My Custom View Controller";
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            
            // UITableViewの背景色をセット.
            MainView.BackgroundColor = UIColor.Red;

            // ボタンを作成して TableView に追加する.
            button = UIButton.FromType(UIButtonType.System);
            button.Frame = new CoreGraphics.CGRect(20, 20, 280, 44);
            button.SetTitle("Click Me", UIControlState.Normal);
            MainView.AddSubview(button);

            button.TouchUpInside += (sender, e) => {
                // ボタン押下時の処理.
            };
        }
        public override void DidReceiveMemoryWarning()
        {
            base.DidReceiveMemoryWarning();
            // Release any cached data, images, etc that aren't in use.
        }
    }
}

UITableView への背景色の設定やボタンの追加は ViewDidLoad() で行っています。

これは、コンストラクターで実行してしまうとそのあと上書きされてしまい、
設定した内容が反映されない(ように見える)ためです。

なお公式ページにはコードだけで Window から追加していく方法が紹介されています。
この充実ぶりもすごいです。

コードでXibを追加する

さてそれでは実際にアイテムを追加できるようにしてみます。

ただ、先ほどのようにボタンやラベルを一つずつ追加して位置を指定していくのは、
コード量も多くなるしあまりに大変。

Unity の Prefab のように、一緒に扱う UI をグループ化して取り扱う方法として、
Xib を使う、というものがあります。
(TableView にセットする UITableViewCell の場合)

と、ここまでは良かったのですが。

Xibを作成する

追加 > クラス から UITableViewCell のテンプレートを選択してファイル作成すると、
Xib ファイルとコードビハインドが生成されます。

この時「Cell」という名前にするのが良さそうです。(理由は後述)

とりあえず下記のようにアイテムを並べて、それぞれ名前を付けておきます。

f:id:mslGt:20171230074519p:plain

で、これを UITableView に追加してみます。

UITableViewSource

UITableView を使う場合、そのデータのソースとなる UITableViewDataSource と、
ふるまいを指定する UITableViewDelegate を継承したクラスを作成し、セットする必要があります。

Xamarin.iOS における UITableViewSource は、 UITableViewDataSource と UITableViewDelegate を1つのクラスにまとめたもの、
ということなので、これを継承したクラスを作ってあげれば OK です。

C# ではクラスの多重継承ができない、という理由もあるのでしょうが、
ただ Swift や Objective-C の代わりに C# で書ける、
という以外にも細かい部分で改善されている部分があるのが良いですね。

DataSource.cs

using System;
using Foundation;
using UIKit;

using System.Collections.Generic;
using XamarinIOsSample.Value;

namespace XamarinIOsSample
{
    public class DataSource : UITableViewSource
    {
        private readonly List< AlarmItem> objects = new List< AlarmItem>();
        
        public IList< AlarmItem> Objects => objects;

        public override nint RowsInSection(UITableView tableview, nint section)
        {
            return objects.Count;
        }
        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            // UITableViewCellを作成したXibのクラスとして取得する.
            if (!(tableView.DequeueReusableCell(Cell.Key) is Cell cell))
            {
                return null;
            }
            // ラベルやボタンに対する設定を行う.
            cell.TimeLabelText = objects[indexPath.Row].TimeSetText;
            cell.DayOfTheWeekLabelText = objects[indexPath.Row].DayOfTheWeekText;
            cell.OnOffSwitchStatus = objects[indexPath.Row].OnOffSwitchStatus;

            return cell;
        }

        public override bool CanEditRow(UITableView tableView, NSIndexPath indexPath)
        {
            // Return false if you do not want the specified item to be editable.
            return true;
        }
    }
}

とりあえず必要そうなメソッドだけで作成してみました。

ラベルのテキストなどに設定する値を格納するクラスの変数をリスト化して Cell の数ぶん保持しています。

AlarmItem.cs

namespace XamarinIOsSample.Value
{
    public class AlarmItem
    {
        public string TimeSetText { get; set; }
        public string DayOfTheWeekText { get; set; }
        public bool OnOffSwitchStatus { get; set; }
    }
}

あとは Xib のコードビハインドでこれらの値をセットできるようにします。

Cell.cs

using System;

using Foundation;
using UIKit;

namespace XamarinIOsSample
{
    public partial class Cell : UITableViewCell
    {
        public static readonly NSString Key = new NSString("Cell");
        public static readonly UINib Nib;

        public string TimeLabelText { 
            get => TimeLabel.Text;
            set => TimeLabel.Text = value;
        }

        public string DayOfTheWeekLabelText
        {
            get => DayOfTheWeekLabel.Text;
            set => DayOfTheWeekLabel.Text = value;
        }
        public bool OnOffSwitchStatus
        {
            get => OnOffSwitch.On;
            set => OnOffSwitch.On = value;
        }

        static Cell()
        {
            Nib = UINib.FromName("Cell", NSBundle.MainBundle);
        }

        protected Cell(IntPtr handle) : base(handle)
        {
            // Note: this .ctor should not contain any initialization logic.
        }
    }
}

直接ラベルなどを返すようにした方が良いのかな?とも思いますが、
まず今回はこのような形にしました。

最後に ViewController から UITableView への追加や Table 行の高さなどを設定してみます。

ViewController.cs

using System;
using UIKit;
using XamarinIOsSample.Value;

namespace XamarinIOsSample
{
    public partial class ViewController : UIViewController
    {
        private DataSource dataSource;
~省略~
        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            // Xibの読み込み.
            MainView.RegisterNibForCellReuse(Cell.Nib, Cell.Key);
            MainView.Source = dataSource = new DataSource();
            
            // UITableView.AutomaticDimensionだと正しく設定されなかったのでいったんベタ打ち.
            MainView.RowHeight = 124;

            var newItem = new AlarmItem();
            newItem.TimeSetText = "06:30";

            dataSource.Objects.Insert(0, newItem);

            var newItem2 = new AlarmItem();
            newItem2.TimeSetText = "07:00";

            dataSource.Objects.Insert(1, newItem2);

        }
~省略~
    }
}

NSUnknownKeyException

上記で UITableViewCell を作るときに、名前を「Cell」にした理由です。

なぜか別の名前で作成してしまうと、
ViewController や DataSource での名前を変更しても NSUnknownKeyException が発生しました。

どこかに「Cell」という名前で紐づけがあるのだと思うのですが、
それがどこかはわからず。。。

これについては解決しましたら追記か別途書くようにしたいと思います。

おわりに

Storyboard や Xib など iOS アプリ開発に関する知識や Xamarin.iOS についての知識はともかく、
日常的に使い慣れた言語で書けるというのは良いですね。

もう少し作りこんだら、 Swift で書くとどうなるの?というのも試してみたいと思います。

参照