C#
WPF
Xamarin
Xamarin.Forms

Windows、Android、iOSアプリを同時開発してみたおはなし

More than 1 year has passed since last update.

自己紹介

image

あすかです。
どこにても:point_up::rolling_eyes::point_left:よくいる:disappointed:、こくふつう:scream:の男性:walking_tone1:会社員:vulcan::laughing::vulcan:です。あと、おみみ:hand_splayed::dark_sunglasses::hand_splayed:聞こえません。:hot_pepper::sleeping::dizzy_face::ok_woman::pig:
きょねん、かいしゃにはいりました。2ねんめです!しようしょ、つくったり、てすとしたり、かいはつしたり、してます!
趣味は、プログラミングと、おええをかくことです☆

image

新都社というところで漫画を描いているのですが、ストーリーを考えるのが苦手で、いつも苦労しています。
面白い話を描きたいけど、面白くてかつそれなりにまとまった物語を描くためには、話をまとめるためのソフトが必要だと思いまして、作りました
今回は、そのアプリを作ったときのお話をちょっと。。

アプリの概要

image

今回制作したアプリは「StoryCanvas」
Windows、Android、iOS上で動作する、ストーリーに出てくる登場人物、シーンなどの設計に特化したアプリです。
公式サイト:https://storycanvas.kmycode.net/
Android:https://play.google.com/store/apps/details?id=net.kmycode.storycanvas
iOS:https://itunes.apple.com/jp/app/storycanvas/id1141722768?&mt=8

言語はC#、開発環境はVisual Studio 2015、フレームワークは以下を使っています。

エディション FW
Windows WPF
Android、iOS Xamarin.Forms

ちなみに、Xamarin歴=StoryCanvas開発期間=半年くらいです。
WPFも、仕事で自動テスト作る時にたまーーーーーーーーーに読んでた程度で、本格的に書くのはStoryCanvasが初めてです。
C#は、Unityでちょっと使った程度で、.NET Frameworkもほとんど知りません。むしろJavaやQt派です。
.NET Frameworkしょしんしゃです!WPFしょしんしゃです!Xamarinしょしんしゃです!Xamarin.Formsしょしんしゃです!MVVMしょしんしゃです!M$すばらちい!大丈夫かこれ。

この記事の構成

あれ使ったよこれ使ったよの紹介だけで、具体的な使い方(コーディングとか)の説明はしません。
言いたいことが多すぎて記事長くなっちゃうこ(´・ω・`)

あと、XamarinのカレンダーなのにWPFの話も混ざってます。ご了承くださいまし

WPFとXamarin.Formsの同時開発

つまるところ、PC上で動作するソフトを、スマホでも動作させようという話です。
一見難しそうですが、実は比較的簡単です。

皆様は、オブジェクト指向の勉強をされていて、一度でも「MVC」という言葉を聞く機会があったと思います。クラスに役割を付けるやつです。
「MVVM」はその仲間で、StoryCanvasは基本的にMVVMを採用しました。
なぜMVVMかというと、WPFとXamarin.Formsで共通して利用することができるXAMLと親和性が高く、画面表示とビジネスロジックの疎結合を実現することで、プラットフォーム間の移植を容易にしているからです。それだけでなく、ListBoxのItemTemplateなど、XAMLには、MVVMでないと使いこなすことができない機能がいくつかあります。

XAMLは一般的なXMLやJavaFXのFXMLなどと異なり、画面表示以外にも、Bindingやイベントへの対応など、数多くの動的な機能を持ちます。XMLとかは基本的に静的ですよね!

ただし、タイムラインなど、MVVMでの表現が難しい画面では、MVCを採用しています。(MVCVMっていうのかなこの場合?)

WPF/XF開発において、MVVMを採用するメリット

なにより、ビジネスロジックが共有できることです!
いや、それだけじゃMVCと大差ないですよね。XAMLを利用すると、画面表示のコードが書きやすいです!(こなみかん)

MVCというか、今は拡張MVCでやってる方も多いと思うんですけど、このようなプログラムを書かれた方も多いと思います。

SampleView.cs
public class SampleView : ContentPage
{
    private ほむほむModel _model = new ほむほむModel();    // Qiitaじゃエラー扱いかよ!!
    private Button _runButton;
    public SampleView()
    {
        this._runButton = new Button();
        this._runButton.Text = _model.Text;
        this._runButton.OnClick += (sender, e) => Model.Run();
        this.Content = this._runButton;
    }
}

見ての通り、ViewとModelが密結合していますね!

ボタン1つ定義するだけで、これくらいのコードが必要になりました。
これはこれくらいの規模だからいいんですけど、もっと複雑な画面を作るときを考えます。
ユーザがあるボタンを押すと、いろんなデータが更新されます。Viewちゃんは、それをいちいちModelくんのところから持ってこなければいけないんです。
これだけでも気が重くなりそうです。中央線止めたくなります。

結果的に、「Modelへの参照」、「Viewの定義」、2種類のコードが混ざって、こんがらかったコードになることが予想されます。これはいけない。
そこで、MVVMです。「Modelへの参照」を、そっくりそのままViewModelへうつしてしまいます!

SampleView.xaml
<Button Text="{Binding Text}" Command="{Binding RunCommand}"/>
SampleView.xaml.cs
public class SampleView : ContentPage
{
    public SampleView()
    {
        this.BindingContext = new SampleViewModel();
    }
}
SampleViewModel.cs
public class SampleViewModel : ViewModelBase
{
    private ほむほむModel _model = new ほむほむModel();
    public SampleViewModel()
    {
        this._model.PropertyChanged += base.RaisePropertyChanged;
    }
    public string Text => this._model.Text;
    private RelayCommand _runCommand;
    public RelayCommand RunCommand
    {
        get
        {
            return this._runCommand = this._runCommand ?? new RelayCommand((obj) => this._model.Run());
        }
    }
}

うっひょおい!!!
コード全体は長くなりましたが、Viewから「Modelへの参照」を省くことで、こういうメリットができてしまいます。

  • Viewは、画面の表示に集中することができます。Modelへの参照コード、もといModelの仕様をあまり知らなくてもいいので、プログラミングに詳しくないデザイナに書いてもらえるXAMLの量が増えます。やったねたえちゃん!
  • 作成したプログラムを別のプラットフォームに移植する時、変更する部分は基本的にViewだけで大丈夫です。それどころか、ViewからModelを参照する部分のコードを新たに書き直す必要がありません。そのコードは、ViewModelのところへ行ったからです。
  • ViewModelはPCLプロジェクトに含めることができます。つまり、MVVMは、テスタビリティの向上に貢献します。Modelのメソッドやプロパティを直接呼び出すMVCと比べて、MVVMはUI操作がなくてもかなりの範囲のテストを自動化することができます。

MVVMではなくOOPの話も混ざってるかもしれませんが。。

正直、MVC使いたい病 患者でなければ、MVVMを検討すべきです。
こういうところがあるから、WPFの画面をXamarin.Formsへ移植するときも、ほとんど時間かからないです。

問題点ですが、中途半端に設計した状態でMVVM書くとシングルトンの山になりそうな予感がすることです(´・ω・`)

プロジェクト構成

WPFとXFを同時開発するということで気になるプロジェクト構成!ですが、
実はXamarin.Forms をガチで使う時のプロジェクト構成(2016冬Ver)(amay077さん)とそんなにかわらないです。WPFプロジェクトを1つ追加しただけです。

image

灰色で塗りつぶしてるとこは、UWPとかWPとかのプロジェクトなのですが、この記事ではノイズなので消してます。

プロジェクト名 説明
StoryCanvas PCL。Xamarin.FormsのViewとかを書きます。先述の記事のHoge.ViewCoreと同じです
StoryCanvas.Droid Android用のプロジェクトです。先述の記事のHoge.Droidと同じです
StoryCanvas.iOS iOS用のプロジェクトです。先述の記事のHoge.iOSと同じです
StoryCanvas.Shared 共有プロジェクトです。VMとMをここに書きます。先述の記事のHoge.Coreと同じです。ちなみにStoryCanvas.WPFからも参照されます
StoryCanvas.Shared.Test 共有プロジェクトのテストプロジェクトです。最近全然使ってないぞ
StoryCanvas.WPF WPFのViewとかを書きます

なんかXFのViewを書くほうのプロジェクト名が分かりづらいですね(´・ω・`)
以前はちゃんと「StoryCanvas.Xamarin」という名前でやってたんですけど、プロジェクトを壊したことがあって、その時に適当に作ったプロジェクトをそのまま使い続けてる感じです。

コード共有率

気になる(?)コード共有率です。一説では40~60%くらい共有できればいいほうという記事もありましたが、StoryCanvasではどうなんでしょう。。
VisualStudioのコードメトリクスで表示されるコード行を基準に、v2.0.5時点の数字を計算してみます!

image

プロジェクト名 コード行 割合
StoryCanvas 1508 19.1%
StoryCanvas.Droid 201 2.5%
StoryCanvas.iOS 29 0.4%
StoryCanvas.Shared 5237 66.4%
StoryCanvas.WPF 914 11.6%
合計 7889 100%

プロジェクト全体の共有コードは66.4%、Xamarin.Formsの部分に限れば85.5%ですね。
後述する通りカスタムレンダラをnugetパッケージにまとめちゃったこともあって、高い共有率になってます。

Androidプロジェクトのコード行が多めなのは、以前Androidでソート可能なリストを作ろうとして挫折した痕跡です(´・ω・`) iOSだと一発でできる(らしい)んですけどねぇ‥‥jarをbindingでもするか‥‥

ItemTemplateとItemsControlはいいぞ

コレクションをListViewのItemsSourceとして登録してItemTemplateを使うと、複雑なことができます。

これはWPFの画面ですが、「章と脚本」という画面は、選択した章に関連付けられたシーンの一覧と、それぞれのシーンに関連付けられた人物・場所の要素を表示するだけでなく、脚本の内容も編集することができます。

image

シーンや関連人物・場所を並べているだけです。こういう画面、ItemTemplateで作るのに向いているやつです。実際、ItemTemplate使ってます。

また、Listbox#ItemTemplateより強力なものとして、ItemsControlというものがWPFにはありまして、例えばこれとWrapLayoutを組み合わせると、ストーリー設定画面の「カスタム色」のところもXAMLとBindingだけでできてしまいます。

image

このカスタム色のところ、Gridで並べただけだと思うでしょ?実はItemsControlとWrapLayout組み合わせたんです。

Xamarin.FormsにItemsControlは残念ながらありませんが、なんと作った方がおられます。Xamarin.Formsでは、ここのコードを使わせていただきました。ご快諾いただいたmatatabi_ux様に感謝を。ニャーン

ItemTemplateや、ItemsControlを利用すると、ただの配列なのに複雑な表現をすることが可能です。他にも色々な応用パターンがあると思います!
MVVMとXAMLを組み合わせると、この部分のWPFからXFへの移植も簡単ですね!

MVVMではまったところ

カスタムコントロール作ろうと思って、この2つを同時にやろうとしたんです。

  • カスタムコントロールに依存プロパティ持たせて、外側からBindingする
  • カスタムコントロールにDataContextを設定する

つまり、依存プロパティを持つカスタムコントロールをMVVMで作ろうとしたんです。
でもだめでした。カスタムコントロールの依存プロパティの値を変更しても、外側の値は変更されません。カスタムコントロールに設定されたDataContextのプロパティの値のほうが変わってしまいます。
これ1日くらいはまって、結局カスタムコントロールをMVVMでやるのを諦めて、メッセージとMVCでやり取りすることにしました。
もしかしたらMVVM初心者がはまりやすい失敗かもしれないのでメモ。

StoryCanvasの各機能(技術的な視点で)

StoryCanvasには、本当にたくさんの機能がありますが、そのうち技術的に面白そうなものをいくつか取り上げます。

ネットワーク通信

StoryCanvasでは、自宅Wi-Fiを利用して、作成したストーリーをお互いの端末に送信することができます。
これは、「家のパソコンで作成したストーリーの続きを、外出先でスマホで編集する。家に帰ったら、パソコンでまた続きを書く」という使い方を想定したものです。
あまり需要はなさそうですが、自分が必要だということで、最初のバージョン1.0から搭載しています。

ネットワーク通信では、少し複雑なやり方をしています。

  1. UDPマルチキャストで、送信側と受信側を繋げる。送信側が受信側のIPアドレスを手に入れる
  2. 送信側がTCPを使って受信側にデータを送る

UDPとTCPを同時に使っています。
UDPではデータの信頼性は保証されないので、今回のストーリーデータのように漏れがあったら困るデータはできるだけTCPで送りたいところです。
しかし、TCPでデータを送るときには、送信先のIPアドレスを知っている必要があります。まさかプログラミングのことをよく知らないユーザに「IPアドレス調べて手打ちしてねっ☆えっ、IPアドレスが分からない?そこから丁寧に説明しますね!ええとなになに」と、長々説明するわけにも行かないです。
そこで、IPアドレスの交換は、UDPマルチキャストを使って自動化しました。これが、TCPとUDP両方使っている理由です。

クラウド使ったほうが早い?維持費なんぽする思てんねん!クラウドも絶賛検討中です。

ライブラリは、SocketsForPCLを使いました。

図形描画

StoryCanvasには、タイムラインという画面があります。シーンを時系列に確認することができる画面です。

image

実はこのタイムライン部分、BoxViewではなく、ネイティブの機能を呼び出して使っています。
図形描画は他のアプリでも頻繁に使うだろうということで、カスタムレンダラをnugetパッケージにまとめて、そこから参照することにしました。PCLThinCanvasのLineView、SquareViewです。
今回は使用しませんでしたが、SquareViewでは角丸四角形を描画できたりします。他にも、EllipseViewで楕円を描画することとかができます。今後、それらの機能をフルに使おうと妄想中です。

Androidの図形描画ではまったところ

自分の作ったライブラリではまるもなにもないですが、はまりました。
このタイムラインを拡大表示しようと思って、拡大ボタンをつけてみたことがありました。押すだけでアプリがクラッシュしました。
一体なぜかと思って調べたら、ログに次の一文を発見。

image

Layer exceeds max. dimensions supported by the GPU (240x7992, max=4096x4096)

Androidにはハードウェア描画、ソフトウェア描画の2種類の描画がありまして、ハードウェア描画だと、な・ん・と!4096x4096までしか描画できないようです。
ソフトウェア描画に切り替えたら動作しましたが、なんとなく画面が重かったり、表示されているはずの図形が勝手に消えたりで、いろいろ心配なのでタイムラインの拡大表示機能の実装は見送りました。

ちなみにAndroidのBoxViewの実装調べてみましたが、矩形を描画しているのではなくて、領域の背景色を変えるだけで済ましてるとのこと。それだと4096x4096よりも広い領域が描画できるわけ。そ、そんな~!

縦書き表示

image

WebBrowserとWebView使いました。おわり。

WebBrowserはTrident、WebViewはWebKitですね、Tridentのバージョンが古くてCSSのwriting-modeの指定方法がWebKitと違っているのですが、Trident向けにwriting-mode:tb-rl、WebKit向けに-webkit-writing-mode:vertical-rl、両方のプロパティを同時に指定したら、同一HTMLでWPFとXF両方いけました。

HockeyApp

v2.0.1から、クラッシュレポートが手軽に管理できるHockeyAppを導入しました。
アプリがクラッシュすると、次にアプリを起動した時に、「クラッシュレポートを送信しますか?」と聞いてくれます。送信されたレポートは、HockeyAppの管理画面から参照できます。

image

image

Androidではローカライズされていないみたいですが、それでも親切な方がクラッシュレポート送ってくれました。感謝。

クラッシュレポートは、このように分かりやすいスタックトレースで表示されます。

image

このアプリ動かないと言ったユーザの住所特定してスマホ盗んでデバッグするわけにもまいりませんので、これは助かります。大いに助かります。死ぬほど助かります。死んでも助かります。
実際にHockeyAppのクラッシュレポートを手がかりにしてバグ修正した例もあります。(v2.0.4)

HockeyAppは、クラッシュレポート以外にも、フィードバック送信、イベントといった機能が利用できます。まだ使っていませんが。。

iOSのHockeyApp SDKは、普通のSDKと、Crash Onlyの2種類が用意されているようです。
iOS10から、写真を使うアプリではなぜ写真を使うか説明しなさいってAppStore申請時に叱られるようになりました。HockeyAppのフィードバック送信では写真を使うようですが、StoryCanvasではフィードバック機能を利用しないので、Crash Onlyのほうを採用しました。

実は将来的なクラウド機能の導入を見据えて、Firebaseへの乗り換えを検討していたりしますが。。
クラッシュレポートだけなら、HockeyAppでも十分使えると思います。

Firebaseは、Xamarin用とWPF用にそれぞれWrappingするnugetパッケージが出てます。
誰かが記事書いてくれないかなー!
書いてくれるー!きっと書いてくれるー!
書いてくれるってよー!(人任せ)

と思ったらXamarin版の記事はもうありました。参考になりそう。
XamarinでFirebaseを使ったリアルタイムチャットの原形を作ってみた with FireSharp(mah_lab氏)

データの保存

Gzipと併せて、データコントラクトシリアライズってのを使ってます。アプリのバージョンが上がって、保存するプロパティが増えても、そのまま対応できたりします。C#ってべんり(`・ω・´)

その他にはまったこととか

Androidのページ遷移バグ

NavigationPageを使ったAndroidのページ遷移は、遅い!重い!のろい!の三重奏でした。
それだけならまだいいんですけど、バグるんです

あるページに、他のページへ遷移するボタンを2つ設置しますよね。
片方のボタンを押しますよね。
ページ遷移に時間かかりますよね。
遷移待っている間にもう1つのボタンを押しますよね。
アプリがクラッシュするんですね。

System.InvalidOperationException: Page must not already have a parent.

こんな簡単なことでクラッシュする問題、長い間放置していたのですが、v1.5.0でやっと対処できました。
どうやって対処したかというと、

  1. ページ遷移ボタンを押す
  2. Navigation.PushAsyncを呼び出して、ページ遷移を開始する
  3. ページ遷移開始と同時に、ページ遷移のコマンド(ICommand)を無効にする(CanExecuteChangedイベントを呼び出し、CanExecuteでfalseを返す)
  4. 呼び出し先のページでDisappearingイベントが呼び出されたら、2.と同様のやり方でページ遷移のコマンドを有効に戻す

呼び出し先のページのAppearingイベントは、ページ遷移するより前に呼び出されてしまうようなので、Appearingのタイミングでコマンドを有効に戻しちゃうと意味ないみたいです。

ちなみにこの問題、iOSでもボタンを素早く押せば、たまに発生することがあるみたいです。

InvalidateMeasureのバグ?

Xamarin.Formsでも、WPFみたいな、テキストの長さに応じて自動で高さを変えるテキストボックス、作りたいと思いました。
で、作ってみました。

public class DynamicHeightEditor : Editor
{
    public DynamicHeightEditor()
    {
        this.TextChanged += (sender, e) => { this.InvalidateMeasure(); };
    }
}

でも、これ、うまく動かなかったです。
ScrollViewの中に入れたStackLayoutの一番最後にDynamicHeightEditorを配置すると、テキストボックスの最後の行が表示されなくなったりしました。
調べてみたら、自動折り返しで、かつテキストの長さが短い行が存在する時に、ScrollViewの中にあるInvalidateMeasureがうまく計算できてないようでした。

image

スクロールできる領域が、その分狭くなってしまいます。
まだまだうまく対応できていないところがあるかもしれませんね。

image

DynamicHeightEditorを、ScrollView>StackLayoutの下ではなく、Grid>ScrollViewの中に配置したら問題なく動きましたので、謎です。

同様のケースとして。
ストーリー設定画面でWrapLayoutを導入したのですが、これ、WrapLayoutの最後のほうが画面の外に隠れて使えないと、ユーザから言われました。(正確に言うと、WrapLayoutの後ろに配置していたはずのトグルスイッチが画面の外に隠れて操作できないとのこと)
でも私の端末ではうまく動くしなんのことだかチンプンカンプン:laughing:
念のため調べてみると、WrapLayoutも内部でInvalidateMeasureを使っているようで、先のテキストボックスと同じような理由で高さの計算がうまくできていないということで、端末の画面の横幅によっては表示の一部が画面の外に隠れてスクロールできなくなるみたいでした。
ここは、WrapLayoutやめて代わりにStackLayout使うことにしました。

ちなみにメイン画面ではWrapLayout使ったままですが、絶対表示量が少ないのでこのままにしています。

一番苦労したこと

オートセーブです。
オートセーブです。
オートセーブです!!!

実は1.4.3まで、オートセーブは一切ありませんでした。
そこでv1.4.4でオートセーブを実装したのですが、その後、アプリが落ちて再起動したらデータが壊れてた、などという報告が頻繁にあがるようになりました。
これについてアンケートを取る、オートセーブの間隔を2秒ごとにして保存ボタンを連打するなどしてデバッグした結果、以下のことが分かりました。

  • 保存ボタンのタップによる保存はGUIスレッド内、オートセーブによる保存は別のスレッドで処理されるのだが、排他制御がうまくできていなかった
  • 新規作成直後でまだ保存先が指定されていないデータではオートセーブが動かないが、その仕様のユーザへの周知が足りなかった可能性がある

2.0.1では、バグ修正だけでなく、メイン画面に「オートセーブできないなう」と表示することでも対処しました。メイン画面が少し汚くなっちゃいましたが仕方ありません。

オートセーブ周りを修正したv2.0.1を出してからすでに3週間経過していますが、データ消えたなどという声はめっきり減りました。
いやまだ何か問題あることは確かですけど、どこ調べればいいかわからないのでもうクラウド導入かな。Windows持ってる人は当分の間Windowsでバックアップとってねってお願いするしかないです・・・

作ってみて

たいへんでした。(こなみかん)

ロジックにバグがあっても、WPFでお手軽デバッグできるのでらくちんです!!!スマホだと配置遅いですよねエミュレータも実機も。
スマホの画面表示にバグがあったら時間かかりますけれど。

ツイッターとかで自分のアプリが紹介されてるのを見ると恥ずかしい/////
拡散されてるの見るともっと恥ずかしい//////////
なんだよ150RTとか1600RTとか!ありえねえよ!ありがとうだよ!
沢山の人が使ってるってのを感じています!

今後の展望

StoryCanvasはv2.1(来春?)で、新しい要素「アイテム」を実装する予定です。v2.2あたりで香盤表を考えています。あとはOxyPlotの導入とかかな。ArtOfWordsデータのインポート・エクスポートも、近いうちに考えています!

Macへの移植も考えています。経緯など詳しいことはXamarin初心者その2のカレンダー(9日、10日)の記事に書きましたので、気になる方はそちらへ!!(しれっと宣伝)

v3台の話になりますが、かねてより要望のあった、画像取り込み機能を実装しようと思います。
画像を取り込むってことは、むろんAndroid、iOSネイティブの実装を呼ばなくちゃいけないってことですよね。Xamarin.Forms LabsのCamera Serviceが使えないか考えているところです。
また、画像を含めたデータの保存方法をどうしようかについても考えているところです。課題は多いですが、要望が非常に多いので、ぜひ作りたいです!

あとは、UIが使いづらいという指摘を多数頂いているので、時間を見つけてUIのおべんきょ!‥‥できたらいいな。

あすかのアプリは現在ver 2.0.5ですが、それなりに機能が揃ってきたものの、まだほかと比べて実用的ではなさそうなので、めんどいなー誰かが代わりに作ってくれないかなーと思いつつ開発続けてます。誰か作れよ!!
ArtOfWordsを勝手に目標にしてます。スマホと連携できる、人物の編集項目を自分で作れる、印刷できるなど、すでにいくつかの点でアドバンテージはありますが、それ以外ではまだまだなところも多いので、頑張らなくちゃですね!

_(:3」∠)_