Xamarin.Mac First Step (Hard Mode)

#あらまし

さてさて始まりました[Xamarin Advent Calendar 2018](https://qiita.com/advent-calendar/2018/xamarin)の6日目として、生**Xamarin.Mac**でのアプリケーション作成をお送りしたいと思います。
ただ、**Storyboard**や**Interface Builder**を使用した例は、Web検索すればいくらでも見つかります。
なのでハードモードと称して、Web検索してもあまりサンプルが見つからない、コードだけでの作成(**Nibless Application**)をお送りしたいかと思います。

#前提

**Xamarin.Mac**によるアプリケーション作成のため、MacOS環境での.NET上の開発となります。
あと私は日和っているので、**Visual Studio for Mac**での解説となりますがご了承をば。

ここでは、**Xamarin.Mac**込みで**Visual Studio for Mac**がインストール済みで話を進めます。
記憶が定かではないですが、インストーラー回せばいい感じに勝手にいい感じにセットアップしてくれたように思えます。

#プロジェクト準備

##プロジェクト作成

プロジェクトテンプレートとして、

> Mac > アプリ > Cocoa アプリ

の順に選択し、プロジェクトを作成します。

![2-project.png](https://qiita-image-store.s3.amazonaws.com/0/11233/a3761169-28dd-0b3a-73e0-07fd5e7f85f1.png)

#不要ファイルの削除

**Nibless**アプリを作成するための準備として、

* **Main.storyboard**を削除します。
* **ViewController.cs**を削除します。

##info.plistの編集

info.plistを開き、ソースを選択します。

![3-info_plist.png](https://qiita-image-store.s3.amazonaws.com/0/11233/6aa86cbc-ddf5-ff3c-409e-192830f9cef2.png)

NSApplicationStoryBoardFileの行を削除します。

#空ウインドウ表示まで

*MainWindowController.cs*を新規ファイルとして追加します。

アイテムテンプレートとして、

> General > 空のクラス

の順に選択します。名前は、**MainWindowController**とします。![5-new-container.png](https://qiita-image-store.s3.amazonaws.com/0/11233/60fae17e-75dd-e153-8050-f06155b94ab8.png)

##MainWindowController.csの編集

親クラスとして、**AppKit.NSWindowController**を指定します。

```csharp
public partial class MainWindow : AppKit.NSWindowController {
    // 省略...
}
```

次いで、コンストラクタを作成します。
サイズを指定してウインドウを初期表示したいため、コンストラクタでサイズを受け取れるようにします。
以降、*using*指定は、適宜追加してください。

```csharp
public partial class MainWindowController : AppKit.NSWindowController {
    public MainWindowController(CGRect rect) {
        var w = new NSWindow();

        w.Title = "Nibless App";
        w.StyleMask |= NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Resizable;
        w.BackingType = NSBackingStore.Buffered;
        w.SetFrame(rect, true);
        this.Window = w;
    }    
}
```

##AppDelegate.csの編集

ウインドウコントローラーのインスタンス化、および表示するコードを追加します。

```csharp
[Register("AppDelegate")]
public class AppDelegate : NSApplicationDelegate {
    MainWindowController controller;

    public AppDelegate() {
    }

    //
    // メインウインドウのインスタンスを作成
    //
    public override void WillFinishLaunching(NSNotification notification) {
        var contentSize = new CGRect(0, 0, 800, 600);
        this.controller = new MainWindowController(contentSize);
    }

    //
    // メインウインドウの表示
    //        
    public override void DidFinishLaunching(NSNotification notification) {
        this.controller.ShowWindow(null);
    }

    public override void WillTerminate(NSNotification notification) {
        // Insert code here to tear down your application
    }
}
```

##Main.csの編集

明示的に**AppDelidate**をインスタンス化し引き渡します。

```csharp
static class MainClass {
    static void Main(string[] args) {
        NSApplication.Init();

        using (var pool = new NSAutoreleasePool()) {
            NSApplication.SharedApplication.Delegate = new AppDelegate();
            NSApplication.Main(args);
        }
    }
}
```

##実行

とりあえず、空のウインドウが表示されたかと思います。

![11-empty-window.png](https://qiita-image-store.s3.amazonaws.com/0/11233/13cf5212-dce7-2151-787c-685ba20953bd.png)

ただ、MacOS村に旧くから受け継がれている慣習により、メインウインドウを閉じてもアプリケーションは終了しません[^1]。
あえて風習に逆らうのもどうかと思いますので、次章で、メインメニューからの終了の選択でアプリケーションを終わらせるよう修正します。

[^1]: **NSApplicationDelegate#ApplicationShouldTerminateAfterLastWindowClosed()**メソッドをオーバーライドし、**true**を返させることで、**Windows**っぽい挙動にできます。

#メインメニューの実装

##AppDelegate.csの編集

**AppDelegate#WillFinishLaunching**にメインメニュー初期化のコードを追加します。

```csharp
    public override void WillFinishLaunching(NSNotification notification) {
        var contentSize = new CGRect(0, 0, 800, 600);
        this.controller = new MainWindowController(contentSize);
        this.PopulateMainMenu(); // 追加
    }

    private void PopulateMainMenu() {
        var mainMenu = new NSMenu();
        var appMenu = new NSMenu();
        appMenu.Title = "whatever";
        appMenu.AutoEnablesItems = false; 

        var appMenuMi = new NSMenuItem(appMenu.Title);
        appMenuMi.Submenu = appMenu;
        mainMenu.AddItem(appMenuMi);

        var exitSubMenuMi = new NSMenuItem("Quit");
        exitSubMenuMi.Activated += this.NotifyOnQuitMenuClicked;

        appMenu.AddItem(exitSubMenuMi);

        NSApplication.SharedApplication.Menu = mainMenu;
    }

    private void NotifyOnQuitMenuClicked(object sender, EventArgs e) {
        NSApplication.SharedApplication.Terminate(this);
    }
```

**Quit**メニューアイテムに選択された場合のイベントハンドラを追加しています。
このイベントハンドラで、有無を言わさずアプリを終了させています。

##実行

**メニュー > Quit**の選択で、アプリケーションの終了が確認できます。

空のウインドウを表示しただけでは面白くないので、次章で何か部品を置けるようにしてみたいと思います。

#コンテナの準備

**NSWindow**はただの枠なので、部品を置けるようにするためにはコンテナを用意してあげる必要があります。

*ContainerViweController.cs*を追加します。

アイテムテンプレートとして、

> General > 空のクラス

の順に選択します。名前は、**ContainerViweController**とします。

![5-new-container.png](https://qiita-image-store.s3.amazonaws.com/0/11233/cca7cdd7-5578-60d4-0102-211bfd18e3b5.png)

親クラスとして、**AppKit.NSViewController**を指定します。

```csharp
public class ContainerViewController: AppKit.NSViewController {
    public ContainerViewController() {
    }
}
```

Macアプリは、nibリソースが存在することを前提としている節があります。
そのため、nibのないViewControllerは、実行時にエラー吐いて落ちます。

niblessでコンテナを扱えるようにするため、**NSViewController#LoadView**メソッドが用意されています。
これをオーバーライドすることで、niblessなコンテナを作成することができます。

```csharp
public class ContainerViewController: AppKit.NSViewController {
    // 省略...

    public override void LoadView() {
        this.View = new NSView();
    }

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

        this.View.WantsLayer = true;
        this.View.Layer.BackgroundColor = new CGColor(0, 0, 255, 0.8);
    }
}
```

コンテナが割り当てられたことがわかるよう背景色をつけています。

2. **MainWindowController**のコンストラクタで、コンテナをインスタンス化し、ウインドウに割り当てます。

```csharp
public class MainWindowController: AppKit.NSWindowController {
    public MainWindowController(CGRect rect) {
        var w = new NSWindow();

        w.Title = "Nibless App";
        w.StyleMask |= NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Resizable;
        w.BackingType = NSBackingStore.Buffered;
        w.ContentViewController = new ContainerViewController(); // 追加
        w.SetFrame(rect, true);
        this.Window = w;
    }
}
```

##実行

領域がうっすら色づいたウインドウが表示されます。

![6-with-conteiner.png](https://qiita-image-store.s3.amazonaws.com/0/11233/70194114-6b76-129d-3a0d-978cc5ca463b.png)

次章で、このコンテナにボタンを貼り付け、クリックでメッセージを表示させます。

#コンテナアイテムの割り当て

**ContainerViewController#ViewDidLoad**で、ボタンをインスタンス化し、コンテナに追加します。
ボタンには、クリックされた場合のイベントハンドラも割り当てておきます。

```csharp
public class ContainerViewController: AppKit.NSViewController {
    private NSButton button;

    // 省略...

    public override void ViewDidLoad() {
        // 省略...

        //
        // 以下のコードを追加する
        //
        this.button = new NSButton(new CGRect(0, 0, 80, 30));
        this.button.Title = "Click ME !!";
        this.button.Activated += this.NotifyOnBittonClicked;
        this.View.AddSubview(this.button);
    }

    //
    // イベントハンドラで、メッセージ表示
    //
    private void NotifyOnBittonClicked(object sender, EventArgs e) {
        var alert = new NSAlert();
        alert.MessageText = "Hello Nibless World !";
        alert.RunSheetModal(this.View.Window);
    }
}
```

実行すると、ウインドウ内にボタンが表示され、クリックするとメッセージが表示されます。

![7-show-message.png](https://qiita-image-store.s3.amazonaws.com/0/11233/8a18f302-c35b-4e88-651c-b9367766acc0.png)

ただ、場所がなんというか・・・。
使う側視点として、上から下に配置するため、できれば座標も上から下に向けて大きくなって欲しいところ。

左上が原点に指定する方法が**Xamarin.mac (cocoa)**には用意されてはいます。
がしかし、実はそんなことをしなくても、別の方法で解決することができます。
次章で、その方法を説明します。

#左上原点で部品を配置する

**Xamarin.mac (cocoa)**は、**Mac は OS X Lion**以降、**AutoLayout**というレイアウトシステムが用意されています。
これは、親子間の相対的な位置や、もしくはサイズ指定により柔軟に配置を行えるようになるものです。

日本語で書かれた解説やチュートリアルが既に多く存在しているため、ここでは詳細については述べません。

**AutoLayout**による配置を行わせるために、**NSViewController#UpdateViewConstraints**メソッドをオーバーライドします。

```csharp
public class ContainerViewController: AppKit.NSViewController {
    // 省略...

    public override void UpdateViewConstraints() {
        base.UpdateViewConstraints();
    }
}
```

このメソッド内で、コンテナとボタンとの相対位置関係を定義します。
ここでは、ボタンをコンテナの左上から(100, 50)の位置に配置させます。

```csharp
public class ContainerViewController: AppKit.NSViewController {
    // 省略...

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

        this.View.AddConstraints(
            this.CreateConstraint(this.View, this.button).ToArray()
        );
    }

    private IEnumerable<NSLayoutConstraint> CreateConstraint(NSView parent, NSView child) {
        yield return NSLayoutConstraint.Create(
            child, NSLayoutAttribute.Left, NSLayoutRelation.Equal,
            parent, NSLayoutAttribute.Left, 1.0f, new nfloat(100)
        );
        yield return NSLayoutConstraint.Create(
            child, NSLayoutAttribute.Top, NSLayoutRelation.Equal,
            parent, NSLayoutAttribute.Top, 1.0f, new nfloat(50)
        );
        yield return NSLayoutConstraint.Create(
            child, NSLayoutAttribute.Width, NSLayoutRelation.Equal,
            null, NSLayoutAttribute.Width, 1.0f, new nfloat(80)
        );
        yield return NSLayoutConstraint.Create(
            child, NSLayoutAttribute.Height, NSLayoutRelation.Equal,
            null, NSLayoutAttribute.Height, 1.0f, new nfloat(30)
        );
    }
}
```

実行すると・・・・、あれボタンが表示されません＞＜

![8-lost-button.png](https://qiita-image-store.s3.amazonaws.com/0/11233/88291958-82a9-d86c-7429-2dbc72568a83.png)

実は**AutoLayout**を有効にするのに、これだけでは不十分で、以下の2点を行う必要があります。

* コンテナを含むすべての部品に対して、**NSView#TranslatesAutoresizingMaskIntoConstraints**プロパティの値を**false**に変更する。

* **AutoLayout**ベースで、配置することを宣言する。

前者は、単にプロパティを変更するだけです。
**AutoLayout**ベースを予定しているため、ボタンのインスタンス化の際のサイズ指定も取り除いています。

```csharp
public class ContainerViewController: AppKit.NSViewController {
    // 省略...

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

        this.View.TranslatesAutoresizingMaskIntoConstraints = false; // 追加
        this.View.WantsLayer = true;
        this.View.Layer.BackgroundColor = new CGColor(0, 0, 255, 0.8f);

        this.button = new NSButton(); // サイズの指定はもはや不要
        this.button.TranslatesAutoresizingMaskIntoConstraints = false; // 追加
        this.button.Title = "Click ME !!";
        this.button.Activated += this.NotifyOnBittonClicked;
        this.View.AddSubview(this.button);
    }
}
```

後者は、**NSView#RequiresConstraintBasedLayout()**メソッドの戻り値が**true**の場合、**AutoLayout**が有効になります。
**static**メソッドであるため、**new**修飾子をつけて、メソッドを置き換えます。
また、**ExportAttribute**で置き換えの意思があることも宣言しておきます。忘れると有効になりません(1敗)。

今回は、*ContainerViweController.cs*に**NSView**の派生クラスを用意します。

```csharp
    class ContainerView: NSView {
        [Export("requiresConstraintBasedLayout")]
        public static new bool RequiresConstraintBasedLayout() {
            return true;
        }
    }
```

**ContainerViewController#ViewDidLoad**で、利用するViewを差し替えます。

```csharp
public class ContainerViewController: AppKit.NSViewController {
    // 省略...

    public override void LoadView() {
        // this.View = new NSView();
        this.View = new ContainerView(); // 差し替える
    }

    // 省略...
}
```

実行すると、ボタンの配置が変わったかと思います。

![9-autolayout-aware.png](https://qiita-image-store.s3.amazonaws.com/0/11233/8f291fd5-71d9-c788-ebc8-4bb2876c3867.png)

ボタンをコンテナ中央に配置したい場合は、以下のようにします。

```csharp
public class ContainerViewController: AppKit.NSViewController {
    // 省略...

    private IEnumerable<NSLayoutConstraint> CreateConstraint(NSView parent, NSView child) {
        // X座標を親の中心を軸に配置
        yield return NSLayoutConstraint.Create(
            child, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal,
            parent, NSLayoutAttribute.CenterX, 1.0f, new nfloat(0)
        );
        // Y座標を親の中心を軸に配置
        yield return NSLayoutConstraint.Create(
            child, NSLayoutAttribute.CenterY, NSLayoutRelation.Equal,
            parent, NSLayoutAttribute.CenterY, 1.0f, new nfloat(0)
        );
        yield return NSLayoutConstraint.Create(
            child, NSLayoutAttribute.Width, NSLayoutRelation.Equal,
            null, NSLayoutAttribute.Width, 1.0f, new nfloat(80)
        );
        yield return NSLayoutConstraint.Create(
            child, NSLayoutAttribute.Height, NSLayoutRelation.Equal,
            null, NSLayoutAttribute.Height, 1.0f, new nfloat(30)
        );
    }
}
```

実行すると、ボタンがウインドウの中央に配置されます。

![10-autolayout-center.png](https://qiita-image-store.s3.amazonaws.com/0/11233/621ab3fb-22eb-322f-b6a6-150e47f8fc93.png)

#まとめ

**Xamarin.Mac**を用いた**Nibless Application**の作成について、プロジェクトの作成から、またハマりどころについて解説しました。

もっとも、全宇宙の99.999999999%の人類は、**Storyboard**を用いて、アプリケーション作成を行うと思われますので、この記事が役に立つこときっとないでしょう（なぜ書いたし）。