SDD(Sleep-Driven Development)

睡眠の重要性!!睡眠の重要性!!

Linuxに移行した話

この前macOSをアップデートしたところOSが飛びました。

まあOSが飛ぶのは別にいいのですが(よくない)、そのころはRootでログインできたりデグレってたりしていてmacOSに対する信仰心が薄れていたのと、ちょうどLinuxについての記事を多数見ていたので思い切ってLinuxに移行してみました。

その環境がようやく落ち着いてきたので備忘録代わりに。

環境

  • MacBookPro mid 2015(Core i7, RAM16GB, 512GB PCIeベースフラッシュストレージ)
  • Ubuntu 17.10

Ubuntu17.10はBIOSを破壊する致命的な不具合がありましたが、幸いにもMacBookProだったので免れた。

主にインストールしたアプリは以下。

i3-gaps

screenshot from 2018-01-03 17-34-05

いくつかのLinux記事で気になっていたタイル型のウィンドウマネージャであるi3、の改良バージョンであるi3-gapsを使っています。

タイル型ウィンドウマネージャはいくつかありますが、最近の記事は大体i3という印象だったのでi3、その中でもウィンドウ間の隙間を開けられるほうが個人的に好きなのでi3-gapsを選びました。

ウィンドウが重ならないので便利ですが世の中がウィンドウが重なる前提なのでたまにバグっぽい挙動をして辛い。

Compton

コンポジット型のウィンドウマネージャ、らしい。

i3のデフォルトだとウィンドウの透過ができなくて透け透け好き派としては微妙だったので導入しました。

アプリケーション毎にウィンドウの透過設定を変えられてめっちゃ便利です。

screenshot from 2018-01-03 18-16-26 ↑が導入前

screenshot from 2018-01-03 18-16-58 ↑導入後

(Google Cromeを透けさせたらさすがに見づらかったので透過設定をしてない) 透け透け好きには最高。

Polybar

screenshot from 2018-01-03 18-22-29

トップバー。i3にはデフォルトでバーは付いてくるのですが、こっちのほうが自由度が高い?らしいので導入。

シェルスクリプトとか使えるらしいですがまだ良くわかってないのでこれで放置。 どんなに充電しても95%止まりだったりボリュームが明らかにずれていても放置。

rofi

peek 2018-01-03 18-29 macOSで言うSpotlight。これもまた自由度が高くてシェルスクリプトとか使えるので重宝しています。

i3wm-themer

カラースキーマ。結構種類があって私はその中で暖色系のColorsを使っています。

環境構築用のシェルスクリプトもあるのですが、中途半端に不発だったのが残念。

ただGtk+のメニューバーの色やウィンドウの閉じるボタンのアイコンまで変更できるのは本当に便利。

ちなみにVimのテーマはなかったのでDespacioを使っています。

Terminal

ターミナルはUbuntuをインストールしたときにくっついてきたGnome-terminalを使っていて、エディタはVim、ShellはFishを使っています。

特にこだわりはないのでVimとかは(テーマを適用したくらいで)ほぼプレーン、、、なはずです。

JetBrains Toolbox

私はJetBrains信者なのでLinuxでもJetBrainsのIDEソースコードをがしがし書いています。

ぶっちゃけあまりタイル型のウィンドウマネージャと相性よくないですが、起動が心なしか早いのと、Linuxでも使える高機能IDEなのでずっと使い続けると思います(信者並感想)

Visual Studio Code

主にざっと確認したいけどVimだとちょっと辛いときに使っています。

軽い割に無駄に対応言語が多く、シンタックスハイライトのおかげで見やすいので重宝しています。

また、卒論でLatex書いてたときは大変お世話になりました。

Latex Workshop Extensionがなければ諦めていたと思います。卒論。

Typora

peek 2018-01-03 18-36

マルチプラットフォームなマークダウンエディタ。主にメモ用途に使っています。

2018年1月現在はβ版でその間は無料で使えるらしい?です。

テキスト+プレビューではなく、書いていくそばから変換されていく形式なので横幅をあまり使いたくない自分には便利。あとcssで見た目を変えられるので周りの色に合わせた感じにしています。

また、rofiと合わせて新規作成したりメモったファイルを開けるようにしています。

まとめ

最近「Linuxええで」という声をたまに聞くので思い切って移行してみたのですが、最大の学びは「WindowsmacOSはすごかった」でした。

i3wmは必要最低限の機能しかなく、Ubuntuにくっついてきたgnome-xxxが大活躍しました。それでもいくつか辛い部分があり、特に

が致命傷な感じです。

ただこれらも設定していけば解消するはず(トラックパッドは微妙だけど)であり、そこいらへんがLinuxの楽しいところだと思いました。

これからもLinuxを改造していき、どんどん便利(カオス)にしていきたいです。

最後に、設定ファイルもろもろをgithubに上げました。

ubuntu-i3wmというディレクトリが悪戦苦闘のあと設定ファイルの数々です。

誰かの参考になれば幸いです。

ただし、環境構築しながら書いたせいで何かおかしな部分があるかもしれません(現時点でいくつか見つけたのであとで修正します。あとで・・・)

また、INSTALL.mdにあるシェルスクリプトは適当に書きなぐっただけでテストもしてないので絶対にそのまま実行しないでください。途中でエラーで止まって地獄をみます。

では、楽しいLinuxライフを。

Xamarin.iOSにおける循環参照

この記事は[初心者さん・学生さん大歓迎!] Xamarin その1 Advent Calendar 2017の12日目の記事です。
昨日は@kmz_kappaさんでXamarinでも難読化のすゝめでした。

今回はXamarin.iOSをやる上で気をつけたほうがいいと思ったメモリリークのお話です。
基本的にはXamarin.iOS特有のお話でXamarin.FormsやXamarin.Androidは大丈夫なはず。。。
ただし、Xamarin.FormsでCustomRenderer等を用いてNSObjectを触る場合は注意が必要です。

また、もし書いてあることに誤りがあったときはご指摘をよろしくお願いします🙇

環境

循環参照とは

循環参照とは、以下のコードのようにHogeというオブジェクトとFooというオブジェクトがお互いに参照を持ってしまっている状態です。

class Hoge 
{
    Foo foo;

    public Hoge() 
    {
       foo = new Foo(this);
    }
}

class Foo 
{
    Hoge hoge;

    public Foo(Hoge hoge) 
    {
        this.hoge = hoge;
    }
}

.NET Frameworkでは特に問題ないコードですが、Xamarin.iOSでは特定条件下でオブジェクトが破棄されなくなる可能性があります。

iOSにおける循環参照問題

.NET Frameworkなどのオブジェクトを追跡して破棄するGCを採用している場合は上記コードでも問題は起きないのですが、代入などでオブジェクトの参照が増えたときにカウントを増やし、0になったら破棄をする参照カウント方式のGCを使っているiOSでは問題が発生します。

たとえば

void NewHoge() 
{
    var hoge = new Hoge();
}

としたとき、hogeの参照カウントは+1されます。 また、コンストラクタ内でFooをHogeのフィールドfooに代入してfooの参照カウントは+1されます。 そしてFooにthisを渡しているのでhogeの参照はまた+1されます。

このとき、上記例でメソッドを抜けた場合hogeは破棄されることを期待します。 が、メソッドを抜けたことでhogeの参照カウントは-1されますが、まだhogeのフィールドfooが持っている参照が残っています。 参照カウント方式では参照カウントが0にならないとオブジェクトは破棄されないので0になっていないhogeは破棄されません。 また、foohogeが参照を持っているため破棄されません。

よって永遠にhogefooの参照カウントは0にならず、NewHoge()というメソッドを抜けたあとも生き続け、メモリリークとなるのです。

Xamarin.iOSにおける循環参照問題

上の例ではすぐに循環参照になりそうですが、Monoは優秀で実は上記のようにただフィールドで循環参照した場合ではきちんとオブジェクトは破棄されます。

しかしNSObjectが関わってきた場合は注意が必要です。 たとえば、

public class Hoge : UIView
{
    public Foo Foo { get; set; }
    public Hoge() : base(CGRect.Empty)
    {
        Foo = new Foo {Hoge = this};
    }
}

public class Foo : UIView
{
    public Hoge Hoge { get; set; }
}

上記コードは先程のとおりオブジェクトが使われなくなったときに破棄されます。

しかし、

public class Hoge : UIView
{
    public Hoge() : base(CGRect.Empty)
    {
        var foo = new Foo {Hoge = this};
        AddSubview(foo);
    }
    ...
}
...

のようにUIViewのメソッドであるAddSubviewに循環参照しているfooオブジェクトを渡すとオブジェクトが破棄されなくなります

これはメソッドにかぎらず、プロパティやイベントに循環参照したオブジェクトを渡しても発生します。

特にイベントなどでラムダ式を使うと、注意していても循環参照するときがあります。

たとえば、

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

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();
        // Perform any additional setup after loading the view, typically from a nib.

        var button = new UIButton();
        button.TouchUpInside += (sender, args) =>
        {
            var viewController = this.Storyboard.InstantiateInitialViewController();
            this.NavigationController.PushViewController(viewController, true);

        };

        View.AddSubview(button);
    }
}

上のようなコードは.NETではよく見そうなコードですが、ViewControllerとbuttonが循環参照してオブジェクトが破棄されません。

これはbutton.TouchUpInsideに渡しているラムダ式内でthisを使うことによってViewControllerの参照がキャプチャされbuttonオブジェクトが持ってしまい、循環参照になってしまうのです。

循環参照の解決方法

循環参照はお互いに参照を持っていることが問題なので適切なタイミングで開放するようにすれば問題ありません。

いくつか方法はあるのですが、ここでは2つ書きます。

WeakReference

1つ目の方法は、オブジェクト同士がお互いに強く参照していることが問題なので片方を弱参照にすれば大丈夫です。

public class Hoge : UIView
{
    public Foo Foo { get; set; }

    public Hoge() : base(CGRect.Empty)
    {
        var weakRef = new WeakReference<Hoge>(this);
        Foo = new Foo(weakRef);
        AddSubview(Foo);
    }
}

public class Foo : UIView
{
    WeakReference<Hoge> weakHoge;

    public Hoge Hoge
    {
        get
        {
            if (weakHoge.TryGetTarget(out var hoge))
            {
                return hoge;
            }

            return null;
        }
    }

    public Foo(WeakReference<Hoge> weakHoge) : base(CGRect.Empty)
    {
        this.weakHoge = weakHoge;
    }
}

AddSubViewをしていますが、上記コードは問題なくオブジェクトが破棄されます。

手動で開放

2つ目はオブジェクトが破棄されるタイミングでも循環参照が残っているのが問題なので、自分で開放してあげます。

public class Hoge : UIView
{
    public Foo Foo { get; set; }

    public Hoge() : base(CGRect.Empty)
    {
        Foo = new Foo(this);
        AddSubview(Foo);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            Foo.RemoveFromSuperview();
        }
        base.Dispose(disposing);
    }
}

public class Foo : UIView
{
    Hoge hoge;
    public Foo(Hoge hoge) : base(CGRect.Empty)
    {
        this.hoge = hoge;
    }
}

ここで注意なのが、Dispose(bool)メソッドは正しく実装されていた場合オブジェクトが破棄されるタイミングで呼び出されますが、Finalizerからの呼び出しなのでdisposingはfalseになります。 なので上記コードの場合はHogeが必要なくなったタイミングでDispose()メソッドを呼び出して上げる必要があります。

また、イベントの場合、

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

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();
        // Perform any additional setup after loading the view, typically from a nib.

        var button = new UIButton();
        button.TouchUpInside += (sender, args) =>
        {
            var viewController = Storyboard.InstantiateInitialViewController();
            NavigationController.PushViewController(viewController, true);
        };

        View.AddSubview(button);
    }
}

のような書き方だとラムダ式なのでイベントの購読を解除できず、またViewDidUnloadは非推奨で使用すべきではないので解除するタイミングがありません。

なので、

public class ViewController : UIViewController
{
    UIButton button;
    
    protected ViewController(IntPtr handle) : base(handle)
    {
        // Note: this .ctor should not contain any initialization logic.
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();
        // Perform any additional setup after loading the view, typically from a nib.

        button = new UIButton();
        View.AddSubview(button);
    }

    public override void ViewWillAppear(bool animated)
    {
        base.ViewWillAppear(animated);
        
        button.TouchUpInside += OnTouchUpInside;
    }

    public override void ViewDidDisappear(bool animated)
    {
        base.ViewDidDisappear(animated);

        button.TouchUpInside -= OnTouchUpInside;
    }

    private void OnTouchUpInside(object sender, EventArgs args)
    {
        var viewController = Storyboard.InstantiateInitialViewController();
        NavigationController.PushViewController(viewController, true);
    }
}

のようにViewWillAppearでイベントを購読、ViewDidDisappearでイベント購読解除すれば大丈夫です。

循環参照の調べ方

Xamarin Profilerが便利そうなのですが、Enterpriseライセンスを持ってないので詳細はわかりません。

Xamarin Profiler について - Xamarin 日本語情報

一番簡単な方法は、NSObjectならDispose(bool)メソッドをoverrideしてConsole.WriteLine()なりなんなりする方法です。

循環参照をするとオブジェクトが破棄されなくなるのでFinalizerから呼び出されるDispose(bool)メソッドも呼び出されなくなります。

ただし、iOS Simulatorだと結構頻繁にDisposeメソッドが呼ばれていることが確認できるのですが、iOS実機だとなかなかDisposeメソッドが呼ばれません。

これはiOS SimulatorだとGC.Collect()?が頻繁に呼び出されているからだそうです。
memory management - the garbage collector in Xamarin Ios it not working on devices - Stack Overflow

なので実機ではどこかにGC.Collectを仕込むか、リソースを食う処理を頻繁に起こすか、ひたすら待つかして確認する必要がありそうです。

もっといい方法があれば教えてください。。

まとめ

ここではiOSで初めて開発する人が嵌りそうな循環参照によるメモリリークについて書きました。

C#の世界にいる限り、循環参照によるメモリリークは大丈夫なはずですが、UIView.AddSubviewのようなメソッドやプロパティを通してC#の世界の外に出ると注意しなければメモリリークになってしまいます。

ここいら辺かなりややこしいのでまだまだ罠がありそうですが、

  • 循環しそうなフィールドやプロパティを持つときはWeakRefenrenceを使用する
  • イベントはしっかり購読解除する

などを心がけていれば大丈夫だと思います。

明日は@DevTakasさんです。
よろしくお願いします!

Kotlin/NativeのiOSサンプルを動かしてみる

Kotlin/Nativeが頑張っているっぽいので、iOSのサンプルを実行してみました。

・・・実行しただけです。

環境

ちなみに

私はビルドし終わったあとに気づきましたが、GitHubReleaseにおそらく各種プラットフォーム向けにビルドされたやつがあるっぽいです。。

なのでそこから持ってきた場合はiOSで実行できるようにするまで飛ばせるはずです。
時間が有り余っている人はCloneからお読み下さい。

kotlin-nativeリポジトリのClone

今回はKotlin/Nativeのリポジトリに含まれているsamples/uikitを実行してみたいと思います。 まずはリポジトリをClone。

$ git clone https://github.com/JetBrains/kotlin-native.git

次にkotlin-nativeのディレクトリに移動し、依存関連をダウンロード。

$ cd kotlin-native
$ ./gradlew dependencies:update

ちょっと時間がかかるかもしれません。軽く筋トレしていましょう。

そしてコンパイラや、各種プラットフォームのライブラリをコンパイル

$ ./gradlew bundle

ただし、README.mdに

The build can take about an hour on a Macbook Pro.

と書かれている通り、ビルドに1時間近くかかります(私の環境で46分でした)。 待っている間は筋トレでもしてましょう。

ビルドが終わったらKotlin/Native側の準備は完了です。
さっそくiOSプロジェクトを見ていきましょう!

iOSで実行できるようにする

samples/uikitに移動し、.xcodeprojをオープン。

$ cd samples/uikit
$ open konan.xcodeproj

早速実行、、、といきたいところですが、いくつか修正が必要です。

まず最初にBundle Identifierを変更します。
初期状態だとorg.jetbrains.konanになっているのでここを適当に変更します。

f:id:crocus7724:20171107021121p:plain

私は今回com.example.konanにしました。

また、SigningTeamを自分のに指定します。

次にRunボタンをクリックしてみるとわかるのですが、ビルドが

cp: /~/kotlin-native/samples/uikit/build/konan/bin/app.kexe: No such file or directory

でこけます。

どうやらkotlinのコードをビルドしたとき、成果物がuikit/build/konan/binに吐き出される想定のはずなのですが、ないっぽいです。

実は成果物はuikit/build/konan/bin/iphoneに吐き出されるようなので単純にミスみたいです()

ということで、Build PhasesReplace Binaryを以下のように修正。

cp "$SRCROOT/build/konan/bin/iphone/app.kexe" "$TARGET_BUILD_DIR/$EXECUTABLE_PATH"

f:id:crocus7724:20171107021339p:plain

さあエラーも消えたし実行!!してみるとビルドは通りますが、実行されません。

実はTargetをシミュレータにしていたのですが、どうやらシミュレータ用のバイナリを吐いてくれないらしく、再生ボタンを何回か押すと以下のようなメッセージが表示されます。

f:id:crocus7724:20171107021356p:plain

なので、MacbookiPhoneを繋いで実機ビルドしましょう!

実行ボタンを押すとビルドされ、実機デプロイされるのですが、以下のメッセージが表示され、初回は時間がかかります。

f:id:crocus7724:20171107021417p:plain

筋トレしていましょう。

うまくいけば実行できているはずです!!

f:id:crocus7724:20171107021511j:plain

お疲れ様でした!!

まとめ

仕組みはどうなっているのかなーと思ったのですが、.xcodeprojのBuild Phasesを見た感じ、

  • kotlinコードをkonanc?を使ってネイティブコンパイル
  • 成果物を$EXECUTABLE_PATH(konan.app/konan)にReplace

しているっぽいですね。

なのでobjcコードは完全にダミーでIBホニャララするくらいしか使わない感じ?

あと、現状唯一Kotlin/Nativeに対応しているIDEがCLionなのですが、開いてみた感じまだ対応されていないので補完なしで頑張るしかないっぽいですね(非常に辛い)。

また、Common moduleがない?(みつからなかった)のでネイティブAPIで頑張るしかないみたいです(非常に辛い)。

現状iOS開発するにはとてもとてもとても辛いですが、将来Common moduleが出たりIDE対応が進んだりすればきっと選択肢の中に一瞬入るようになると期待しています。

Visual Studio for Mac Extensionで独自Padを作る

Visual Studio for Macでの独自Padの実装方法です。 ちなみにPadは以下のようなやつです。間違ってたらごめんなさい。

f:id:crocus7724:20170520015028p:plain

今回はサンプルとしてビルドした回数とデバッグした回数を記録しておくPadを作ります。
(本当はRunした回数を取りたかったけど取得の仕方がわからなかった・・・)

PadContentの作成

まず最初にPadContentを継承したクラスを作成します。これはiOS/AndroidでいうUIViewController/Activityみたいな役割で、表示するViewを組み立てたりします。たぶん

実際にコードを見てみましょう。

using System;
using Gtk;
using MonoDevelop.Components;
using MonoDevelop.Debugger;
using MonoDevelop.Ide;
using MonoDevelop.Ide.Gui;
using MonoDevelop.Projects;

namespace SamplePadExtension
{
    /// <summary>
    /// ビルドした回数とデバッグ実行した回数を表示するPad
    /// </summary>
    public class SamplePadExtensionPad : PadContent
    {
        // デバッグした回数
        private int debugCount;

        // ビルドした回数
        private int buildCount;

        // デバッグした回数を表示するラベル
        private Label debugCountLabel;

        // ビルドした回数を表示するラベル
        private Label buildCountLabel;

        // 実際に表示するものを指定するバッキングフィールド
        // overrideのControlプロパティがget onlyなので
        private Control control;

        /// <summary>
        /// 実際に表示するControl(View)を格納するプロパティ
        /// </summary>
        public override Control Control => control;

        /// <summary>
        /// 実際に表示されるときに呼ばれる?
        /// </summary>
        /// <param name="window">Padのウィンドウ. Toolbarとかとれる</param>
        protected override void Initialize(IPadWindow window)
        {
            buildCountLabel = new Label {Text = $"Build count: {buildCount}"};
            debugCountLabel = new Label {Text = $"Debug count: {debugCount}"};

            // 縦積みするレイアウト
            var vBox = new VBox {buildCountLabel, debugCountLabel};
            // 表示
            vBox.ShowAll();
            control = vBox;

            IdeApp.ProjectOperations.StartBuild += OnStartBuild;
            DebuggingService.DebugSessionStarted += OnDebugSessionStarted;
        }

        // ビルドが始まったときに呼ばれる
        private void OnStartBuild(object sender, BuildEventArgs args)
            => buildCountLabel.Text = $"Build count: {++buildCount}";

        // デバッグ実行が始まったときに呼ばれる
        private void OnDebugSessionStarted(object sender, EventArgs eventArgs)
            => debugCountLabel.Text = $"Debug count: {++debugCount}";

        public override void Dispose()
        {
            IdeApp.ProjectOperations.StartBuild -= OnStartBuild;
            DebuggingService.DebugSessionStarted -= OnDebugSessionStarted;

            base.Dispose();
        }
    }
}

注意点なのですが、デバッグの開始を取得するためにMonodevelop.Debuggerを使っています。
デフォルトだと参照できないのでVisual Studio for Mac ExtensionプロジェクトのAddin Reference(下の画像の赤枠のやつ)をダブルクリックして表示されるポップアップからMonodevelop.Debuggerを選択してください。

f:id:crocus7724:20170520020303p:plain

Manifest.addin.xmlの編集

次に上で書いたPadContentを継承したクラスをVisual Studio for Mac Extensionが認識できるようにします。 Propertiesフォルダの中にあるManifest.addin.xmlファイルを以下のように編集します。

<?xml version="1.0" encoding="UTF-8"?>
<ExtensionModel>
    <!-- MainMenu→View→PadsにPadを追加する -->
    <Extension path="/MonoDevelop/Ide/Pads">
        <!-- 実際に追加するPad -->
        <!-- idは固有識別子 -->
        <!-- defaultPlacementは表示させたときのデフォルトの位置 -->
        <!-- classはPadContentを継承したクラスの完全修飾名 -->
        <!-- _labelはPadsに追加されたときに表示される文字 -->
        <Pad id="SamplePadExtension.SamplePadExtensionPad"
             defaultPlacement="Bottom"
             class="SamplePadExtension.SamplePadExtensionPad"
             _label="Sample Extension Pad"/>
    </Extension>
</ExtensionModel>

これで準備は完了です!

実行

それでは実際に実行してみましょう。

f:id:crocus7724:20170520021013p:plain

実行すると上のようなのが追加されていて、ビルドしたりデバッグしたりすると下のようにカウントが増えていくはずです。

f:id:crocus7724:20170520021057p:plain

Xamarin StudioまではファイルテンプレートにGtkがあったのですが、VS for MacからGtkテンプレートが消失してしまい、Xamarin StudioはVS for Macに置き換わる予定ですからGtkファイル手作業作成しか残されてないかなーと思うのですが辛すぎるので手抜きViewです。
もし凝ったレイアウトを作成したい場合は覚悟してください私はやりたくない

もうちょっと詳しく

もう少し凝ったレイアウトやよくあるリスト形式のPadを作成したい場合はおそらくBreakpointのPad実装である

monodevelop/BreakpointPad.cs at master · mono/monodevelop · GitHub

が参考になると思います。

まとめ

Visual Studio for Mac ExtensionならPadの作成も見た目を気にしなくていいならとても簡単にできます。
Padが作成できるとやれることの幅が広がるのでぜひ作ってみてください。

Visual Studio for Mac Extensionの作り方

Visual Studio for Macが正式リリースされたっぽい?のでVisual Studio for Macの拡張の作り方を調べてみました。(Xamarin Studio Addinと変わってなかったけど)

Addin Makerインストー

最初にVS for Mac ExtensionのテンプレートやデバッグサポートをしてくれるAddin Makerを入れます。

VS for Macを開き、Main MenuのVisual Studio→Extensions…を選択します。

f:id:crocus7724:20170513161535p:plain

そして開いたウィンドウのGallery→Addin Development→Addin Makerを選択しインストールします。

f:id:crocus7724:20170513161923p:plain

いつものようにFile→New Solution…を開き、Other→Miscellaneous→Generalに以下のようなものが追加されていたらインストール成功です。

f:id:crocus7724:20170513162226p:plain

_人人人人人人人人人人人人人人_
> Xamarin Studio Addin <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

Extension作成

ここではサンプルとしてメニューから選択するとConsole.WriteLine("Hello World");を挿入するExtensionを作りたいと思います。(誰得だけど)

まずNew SolutionでXamarin Studio Addinを選択し、プロジェクトを作成します。
なおAddin Makerをインストールして最初の数回はよくアセンブリが見つからなかったりするエラーが発生しますが、VS for Macを再起動したりビルドしたりするとそのうちなおります。

次にメニュー等から選択されたときに実行されるCommandHandlerを作成し以下のように記述します。

using MonoDevelop.Components.Commands;
using MonoDevelop.Ide;

namespace SampleVSExtension
{
    /// <summary>
    /// コマンドが選択されると現在のキャレット位置に
    /// <code>Console.WriteLine("Hello World");</code>が挿入される
    /// コマンドハンドラー
    /// </summary>
    public class HelloWorldCommandHandler : CommandHandler
    {
        /// <summary>
        /// メインメニューなどが開かれるときなどに実行される
        /// ここで可視性や非活性、動的な文字の変更などができる
        /// </summary>
        /// <param name="info">コマンドのステータス</param>
        protected override void Update(CommandInfo info)
        {
            // ActiveDocument(現在開いているファイル)がnullではない(なにかしらのファイルを開いている)
            // ときにコマンドを表示
            info.Visible = IdeApp.Workbench.ActiveDocument != null;
        }

        /// <summary>
        /// コマンドが実際に選択されたときに実行される
        /// </summary>
        protected override void Run()
        {
            // 現在開いているエディタの情報を取得
            var editor = IdeApp.Workbench.ActiveDocument.Editor;
            // 現在のキャレット位置にConsole.WriteLine("Hello World");
            // を表示
            editor.InsertAtCaret("Console.WriteLine(\"Hello World\");");
        }
    }
}

もしusing MonoDevelopとかでエラーが表示されたらVS for Macを一回閉じてもう一度起動してみてください。

細かい説明などはソースコードに書いたので省略して次にCommandの種別を決めるSampleVSExtensionCommandsを作成します。

namespace SampleVSExtension
{
    public enum SampleVSExtensionCommands
    {
        InsertHelloWorld,
    }
}

これはただ単に種別を表すだけなのでとくに説明はありません。

最後に実際にどのような拡張をするかをVS for Macに知らせるManifest.addin.xmlを編集しましょう。
Manifest.addin.xmlAssemblyInfo.csなどがあるPropertiesフォルダの中にあります。

以下のように定義してみましょう。

<?xml version="1.0" encoding="UTF-8"?>
<ExtensionModel>
    <!-- Editという種類のコマンドを定義する -->
    <Extension path="/MonoDevelop/Ide/Commands/Edit">
        <!-- idは用意した種別を表すenumを完全修飾名({namespace}.{enum}.{value})で指定 -->
        <!--_labelはコマンドが表示される文字 -->
        <!-- defaultHandlerはCommandHandlerを拡張したクラスを完全修飾名で指定 -->
        <Command id="SampleVSExtension.SampleVSExtensionCommands.InsertHelloWorld"
                 _label="Insert Hello World"
                 defaultHandler="SampleVSExtension.HelloWorldCommandHandler"/>
    </Extension>
    <!-- MainMenuのEditに表示するコマンドを定義する -->
    <Extension path="/MonoDevelop/Ide/MainMenu/Edit">
        <!-- 上で定義したコマンドのidを指定 -->
        <CommandItem id="SampleVSExtension.SampleVSExtensionCommands.InsertHelloWorld"/>
    </Extension>
</ExtensionModel>

これで準備は完了です。実際に実行してみましょう!

MainMenuのEditにInsert Hello Worldが追加されていて

f:id:crocus7724:20170513171210p:plain

選択したら現在の行に挿入されたら成功です!

f:id:crocus7724:20170513171321p:plain

何故かキャレット位置ではなく行の頭に挿入されているのが謎というかバグですが気にしたら負けです。これで無事やりたいことはできました!!

他の機能の作り方

Visual Studio for Mac Extensionは参考になるものがたくさん公開されています。

私の場合はEditなどのソースコードを編集する機能などは以下を参考にしましたし、

monodevelop/main/src/addins/CSharpBinding at master · mono/monodevelop · GitHub

プロジェクトテンプレートなどの作り方は以下が参考になりました。

monodevelop/main/src/addins/AspNet at master · mono/monodevelop · GitHub

そしてこれらは以下にまとまっています。

monodevelop/main/src/addins at master · mono/monodevelop · GitHub

素晴らしいですね(((

まとめ

VS for MacのPreviewが外れ、.NET CoreやC#7対応を含めてXamarin Studioから徐々に移行してくると思います。
VS for MacのExtensionもXamarin Studio addinとあまり変わってない(たぶん)と思うのでぜひ作ってみてください!!

RiderでXamarin.Androidがサポートされました!!

RiderのPublic EAP20でXamarin.Androidがサポートされました 🎉🎉🎉🎉🎉
(ついでにT4テンプレートとNode.js)

さっそく試してみましょう。

Androidプロジェクト作成

Riderが起動したらNew solution or projectを選択します。

f:id:crocus7724:20170415001848p:plain

Bindings Libraryプロジェクトまである・・・!!
ネイティブバインディングまでRiderだけでできるかはあとで調べるとして、とりあえずBlank Appを選択してみます。

f:id:crocus7724:20170415002343p:plain

初期状態はこんな感じ。

さっそくデバッグしてみます。

f:id:crocus7724:20170415002535p:plain

シミュレータを適当に選んで・・・

f:id:crocus7724:20170415002638p:plain

画面真っ黒(´・ω・`)
無事に実行できました!!

f:id:crocus7724:20170415003555p:plain

ちなみにブレークポイントでもちゃんと止まります。

f:id:crocus7724:20170415014234p:plain また、Xamarin.FormsのAndroidプロジェクトも実行できました!!
(Xamarin.iOSがloadしてある状態だとビルドエラーになったのでわざとunloadしてます)

これは捗りますね!!

注意点

ただRider自体がまだPublic EAPな上にXamarin.Androidに対応したばっかりなので結構怪しいところがあります。

Riderで作成したXamarin.AndroidプロジェクトがXamarin Studioで開けない

開けません。

f:id:crocus7724:20170415005225p:plain

ただエラー内容はただ単にXamarin StudioはそんなProject Type GUIDなんて知らないよって言っているだけなので書き換えればOKです。

Xamarin.Androidの.csprojの

<ProjectTypeGuids>{02CD1F06-00EA-420C-944B-AD8BE8AD9848}</ProjectTypeGuids>

<ProjectTypeGuids>{EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>

にすれば

f:id:crocus7724:20170415005627p:plain

Xamarin Studioでも開けました!!

ただ個人的にはまだXamarin Studioで作成してからRiderで開く運用のほうがいいと思います。(書き換えよろしくないと思いますし)

C#7使えない?

RiderはC#7に対応しているのですが、Xamarin Studioで作成したプロジェクトだと残念ながらコンパイル時にエラーになってしまいます。

ただ、Visual Studio for Macで作成したプロジェクトはRiderでもC#7で実行できるので、ヒトバシラーにはおすすめです() f:id:crocus7724:20170415022758p:plain




今回のバージョンアップでRiderでもXamarin.Androidプロジェクトが実行できるようになりました!
今回は適当なサンプルプロジェクトを実行しただけでこれが実用に耐えうるかは未知数ですが、これは大きな前進だと思います。また、Xamarin.iOSに関しても

ということで期待ですね・・・!!

Xamarin.Forms上でHello World

これは[学生さん・初心者さん大歓迎!]Xamarin Advent Calendar 2016 - Qiitaの23日目の記事です。

前日は

普段Swift書いてますがXamarinに入門してみました | Developers.IO

でした。

今回、本当はXamarin Studio Addinについて書こうとしたのですが、Xamarin Studioにブチ切れた諸々の諸事情により書く時間が足りなかったので思いっきりネタに走りました。本当に誰得レベル。

最近Xamarin.Forms書いてないし(専らNativeのほう)、そういえば私も1本もアプリ作れてないので初心者だと思います。なので、入門に向けてXamarin.Forms上でHello Worldをやってみました。
目指すのは以下のような感じ。

f:id:crocus7724:20161223225732p:plain

大事なことなのでもう一度いいます。ネタです。

環境

  • macOS Sierra
  • Xamarin Studio 6.1.2
  • Xamarin Forms 2.3.1.114

Hello World!!

とりあえずXamarin StudioでちゃちゃっとXamarin FormsのPortableでプロジェクトを作成します。(今考えるとSharedのほうが良かった)

作成したらぱぱっとHello Worldを実行したいのですが、PortableライブラリではNuGetを追加できませんでした。なので実行するためのインターフェースとして以下をPortableライブラリに追加します。

using System.Threading.Tasks;

namespace HelloForms
{
    public interface IScripting
    {
        Task<T> RunAsync<T>(string code);
    }
}

またNative側のNuGetパッケージにMicrosoft.CodeAnalytics.CSharp.Scriptingを追加します。 そして先程のインターフェースを実装したクラスを作成します。

using System.Threading.Tasks;
using HelloForms.iOS;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Xamarin.Forms;

[assembly: Dependency(typeof(Scripting))]

namespace HelloForms.iOS
{
    public class Scripting : IScripting
    {
        public async Task<T> RunAsync<T>(string code)
        {
            var result = await CSharpScript.RunAsync<T>(code);

            return result.ReturnValue;
        }
    }
}

上の[assembly:Dependency(typeof(Scripting))]はPortableライブラリ側でこのクラスを使うために必要です。 Tで戻り値の型を指定できるのですが今回全く使ってませんでした。(なくてもよかった)

これで準備は整いました。あとはPortableライブラリ側でViewとなんちゃってViewModelを作り・・・

  • HelloFormsPage.xaml
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:HelloForms"
             x:Class="HelloForms.HelloFormsPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0,20,0,0"
                    Android="0"
                    WinPhone="0" />
    </ContentPage.Padding>
    <ContentPage.BindingContext>
        <local:HelloFormsPageViewModel />
    </ContentPage.BindingContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="5*" />
        </Grid.RowDefinitions>

        <Editor Text="{Binding Code,Mode=OneWayToSource}" />
        <Button Grid.Row="1" Text="Run Script"
                Command="{Binding ExecuteCommand}" />
        <Editor Grid.Row="2" Text="{Binding Result}" />
    </Grid>
</ContentPage>
  • HelloFormsPageViewModel
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;

namespace HelloForms
{
    public class HelloFormsPageViewModel : INotifyPropertyChanged
    {
        private string _code;
        private string _result;

        public string Code
        {
            get { return _code; }
            set
            {
                _code = value;
                OnPropertyChanged();
            }
        }

        public Command ExecuteCommand { get; }

        public string Result
        {
            get { return _result; }
            set
            {
                _result = value;
                OnPropertyChanged();
            }
        }

        public HelloFormsPageViewModel()
        {
            ExecuteCommand = new Command(async () =>
            {
                try
                {
                    var result = await DependencyService.Get<IScripting>().RunAsync<object>(Code);
                    Result = result?.ToString() ?? "";
                }
                catch (Exception e)
                {
                    Result = e.Message;
                }
            });
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

実行すれば・・・

f:id:crocus7724:20161220000511g:plain

Hello Worldができました!!さらにIDE側のApplication OutputにもHello Worldできます。
これで無事私も入門できました!

なにか違う

思えば確かにHello Worldと出力されたけどあれは真のHello Worldでは無い気がします。私が、いや私達が目指す真のHello Worldはまっさらな画面の真ん中にシステム標準の大きさで表示されているHello Worldなはずです。

というわけで改造。各プラットフォーム毎のScripting.RunAsync<T>の中身を以下のように変更

public async Task<T> RunAsync<T>(string code)
{
    var result = await CSharpScript.RunAsync<T>(code,ScriptOptions.Default
        .AddReferences(Assembly.Load("Xamarin.Forms.Core"),
            Assembly.Load("Xamarin.Forms.Platform"),
            Assembly.Load("Xamarin.Forms.Platform.iOS"),
            Assembly.Load("Xamarin.Forms.Xaml"))
        .AddImports("Xamarin.Forms"));
    return result.ReturnValue;
}

Xamarin.Forms関連のアセンブリを片っ端から読み込ませます。ここいらへんヤケクソ これで実行時にXamarin.Forms関連のアセンブリが参照と、using Xamarin.Formsの省略ができます。

これで完成。

いちいち入力するの怠いので以下のコードをクリップボードにコピーしといて・・・

Application.Current.MainPage.Navigation.PushAsync(new ContentPage
{
    Content = new StackLayout
    {
        VerticalOptions = LayoutOptions.Center,
        HorizontalOptions = LayoutOptions.Center,
        Children = { new Label
        {
            Text = "Hello World!"
        }}
    }
})

実行時貼り付けてやれば・・・

f:id:crocus7724:20161221222342g:plain

やりました!!真のHello Worldができました!!

違う

よくよく考えなくてもXamarin.FormsのViewはXAMLで組み立てるのが可読性的な意味でベストです。
なので正真正銘のHello WorldXAMLを使って表示させるべきなのではないでしょうか??

というわけでまたまた改造していきます。
まず、XAMLで書かれた文字列をインスタンスに変えなければならないのですが、自力で実装するのは骨が折れます。
幸いなことに、文字列からインスタンスに変換してくれるメソッドはXamarin.Formsにはあります。

https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Xaml/ViewExtensions.cs

このExtensionsクラスのLoadFromXamlメソッドの第2引数がstringのものがまさに求めていたものなのですが、非常に残念なことにアクセシビリティinternalです。

なので通常の方法で呼び出すことができません。。。非常に残念なので

using System;
using System.Linq.Expressions;

namespace HelloForms
{
    public static class Extensions
    {
        private static readonly Func<object, string, object> loadFromXamlDelegate;

        static Extensions()
        {
            // object view;
            var view = Expression.Parameter(typeof(object), "view");
            // string xaml;
            var xaml = Expression.Parameter(typeof(string), "xaml");

            // (view, xaml) => Xamarin.Forms.Xaml.Extensions.LoadFromXaml<view.Type>(view, xaml)
            var lambda = Expression.Lambda<Func<object, string, object>>(
                Expression.Call(typeof(Xamarin.Forms.Xaml.Extensions),
                    nameof(Xamarin.Forms.Xaml.Extensions.LoadFromXaml),
                    new[] {view.Type},view, xaml),
                view, xaml);

            loadFromXamlDelegate = lambda.Compile();
        }

        public static TXaml LoadFromXaml<TXaml>(this TXaml view, string xaml)
            => (TXaml) loadXamlDelegate.Invoke(view, xaml);
    }
}

リフレクションで呼び出すことにしました。
毎回リフレクション叩くのは芸がないなーということと通常のリフレクションだとうまくいかなかったので式木を使ってデリゲートを生成しています。 すごい便利ですね。式木。なおこれ式木でやる意味がない気がしますが、気がするだけで気のせいです。

あとはこいつを呼び出してあげればいいわけです。

RunAsyncHelloFormsの参照を追加してあげて

public async Task<T> RunAsync<T>(string code)
{
    var result = await CSharpScript.RunAsync<T>(code, ScriptOptions.Default
            .AddReferences(Assembly.Load("Xamarin.Forms.Core"),
                Assembly.Load("Xamarin.Forms.Platform"),
                Assembly.Load("Xamarin.Forms.Platform.iOS"),
                Assembly.Load("Xamarin.Forms.Xaml"),
                Assembly.Load("HelloForms"))
            .AddImports("Xamarin.Forms","HelloForms"));
    return result.ReturnValue;
}

実行時にいちいち書くのは面倒なので以下のコードをクリップボードにあらかじめコピーしといて

var page = new ContentPage().LoadFromXaml(@"<?xml version=""1.0"" encoding=""utf-8""?>
<ContentPage xmlns=""http://xamarin.com/schemas/2014/forms""
             xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml""
             xmlns:local=""clr-namespace:HelloForms""
             x:Class=""HelloForms.HelloFormsPage"">
    <StackLayout VerticalOptions=""Center"" HorizontalOptions=""Center"">
        <Label Text=""Hello World!!"" />
    </StackLayout>
</ContentPage>");

実行してあげれば

f:id:crocus7724:20161223191825g:plain

やりました!XAMLでHelloWorldに成功しました!!

今回は時間がなかったのでnew ContentPage().LoadFromXaml("{XAML}")インスタンス化しましたが、ちゃんとXAML用のエディタを用意してあげればいつもと同じような感じでアプリケーション上でXamarin.Formsを使ってMVVMが試せますね。凄い。補完がないので地獄ですが。

ようするに

Roslynです。 詳しくは以下が参考になります。

C#スクリプト実行 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

Roslyn for Scriptingで、あなたのアプリケーションにもC#スクリプトを!! – kekyoの丼

アプリケーションのマクロ的な言語にC#が使えます。
これでどこでもC#が書ける!ということで詳しく説明する記事を書こうと思ったのですが...断念。

理由の1つ目がNuGet。追加する前はほとんどない(実際Xamarin.Formsだけ)NuGetパッケージが

f:id:crocus7724:20161220003553p:plain

Microsoft.CodeAnalytics.CSharp.Scripting追加後・・・

f:id:crocus7724:20161220003626p:plain

こうなりました。すごい(小並感)

また、ScriptingはiOSでは使えません。シミュレータ動いてんじゃんと思うかもしれませんが、

f:id:crocus7724:20161221065702p:plain

ハイ。JITコンパイルじゃないと使えないらしいです(シミュレータはJITコンパイル、実機はAOTコンパイル)。なのでJITコンパイルが使えないiOSと、最近はAndroidもAOTですかね??まあモバイル勢はいろいろ制約がキツそうです。無念。

じゃあXamarin.Macは?と思うかもしれませんが、Target FrameworkがMobileだとSystem.Runtime.LoaderSystem.NotImplementedExceptionが出てScriptが実行できず、.NET4.5だとそもそもインストールできませんでした。

そもそもSystem.Runtime.Loaderが謎い。iPhoneシミュレータだとSystem.Runtime.Loaderがなくても実行できる(むしろあるとNotImplemented)のですが、iPhone実機で実行しようとするとコンパイルエラー。うーん、コンパイルの仕方で必要なアセンブリが変わるんですかね。。ここいらへんの仕組みがよくわかってないです。

というわけでXamarinだと今のところUWP?とAndroid(JIT)?とiPhoneシミュレータ上だけです。おしい、惜しい。
ただ前までは式木もできなかったらしいので今後Scriptingもできるようになることを期待してます。

以上、ネタでした。

明日の担当はnobukuma - Qiitaさんになります。よろしくお願いします!