20210118のiOSに関する記事は4件です。

Flutter で仕事したい人のための Widget ツリー入門

ご存知の通り、 Flutter は Widget を入れ子の構造で記述することで UI を構築します。

Center(
  child: Column(
    children: const <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      Text('This is my first app.'),
    ],
  ),
),

Flutter の Widget 同士の関係は、このようなソースコードの形状から「入れ子」の構造をイメージしがちですが、 Flutter の仕組みを理解する上では、このソースコードから Widget の ツリー構造 がイメージできるようになっておくと何かと役に立ちます。

この記事では、Flutter の仕組みを説明する上でよく使われる 「Widget ツリー」 について、初学者向けに基本から解説していきたいと思います。

なぜ Widget ツリーを理解するのか

例えば「中央寄せしたければ Center で囲う」のように簡単な UI を構築するだけであれば Flutter が用意してくれている色々な Widget とそのプロパティの使い方を理解すれば作れてしまうのが Flutter の良いところです。

しかし一方で、 Snackbar を表示しようと思ったら Scaffold.of() called with a context that does not contain a Scaffold エラーが発生した、複数の画面でデータを共有する方法を調べたら InheritedWidgetProvider といった知らない仕組みが出てきて混乱した、など、 少し込み入ったアプリを作ろうと思ったら「こう書けば、こう表示される」の対応表だけではやりたいことを実現できない 場合も少なくありません。

これは、Flutter が提供するほとんどの Widget が「Widget のツリー構造」を前提に設計されているためです。

逆に言うと、この Widget のツリー構造がイメージできるようになれば、より効率的に Flutter に対する理解を深めることができ、「Flutter の考え方」に則って安全で無駄のないアプリが開発できるようになる はずです。

Widget ツリーを組み立てる

では、先ほどのサンプルコードを使いながら Widget ツリーがどのように組み立てられるのかを見ていきましょう。

基本は child / children プロパティに指定した通り

TextImage といった一部の Widget を除き、 Flutter が提供するほとんどの Widget には child または children プロパティが用意されていて、 Widget ツリーの構造もそれに従った形になります。

例えば、以下のようにサンプルコード(再掲)があったとき

Center(
  child: Column(
    children: const <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      Text('This is my first app.'),
    ],
  ),
),

以下の図のような Widget ツリーができあがります。

image.png

  • ソースコードの先頭に Center があるためツリーの先頭は Center が配置され、
  • Centerchild プロパティに Column が渡されているため、ツリー上も Center の下(子 Widget)に Column が配置され、
  • 以下同様に Column の子 Widget として children プロパティの Text, SizedBox, Text が配置され、

という具合です。

まずはこれが Widget ツリーのイメージの基本となります。ソースコードと見比べて Widget ツリーのイメージを脳内で変換できるようになると良いでしょう。

StatelessWidget が入る場合

さて、以下のように child(もしくは children) に StatelessWidget が渡された場合はどうなるでしょうか。

Center(
  child: Column(
    children: <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      UserCard(),  // 自作の StatelessWidget
    ],
  ),
),

.. 省略..

// ユーザーのアイコンと名前を横並びにした Widget
class UserCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Icon(Icons.people),
        Text('User name'),
      ],
    );
  }
}

このソースコードを Widget ツリーで表すと以下のようになります。(前の例と変わらない部分は半透明にしてあります)

image.png

見ての通り、自分で作った UserIcon もひとつの Widget としてツリーに組み込まれ、その子 Widget には build() メソッドで return した Widget(この例では Row)が続く形になっています。

Widget ツリーにおいて、 子 Widget は必ずしも child / children で指定したものとは限らない という点は覚えておいてください。

StatefulWidget をはさんだ場合

では、 StatefulWidget はどうでしょうか。以下のコードの場合を考えてみましょう。

Center(
  child: Column(
    children: <Widget>[
      Text('Hello, Flutter!'),
      SizedBox(height: 16),
      Counter(),  // 自作 StatefulWidget
    ],
  ),
),

.. 省略..

// シンプルなカウンター Widget
class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  var _counter = 0;
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('$_counter'),
        IconButton(
          icon: Icon(Icons.add),
          onPressed: () => setState(() => _counter++),
        ),
      ],
    );
  }
}

このコードを Widget ツリーで表すと以下のようになります。

image.png

ということで、 StatelessWidget の場合と特に変わらないことが分かるかと思います。

一点だけ注意しなければならないのは、 StatefulWidget の場合は build() メソッドが Widget 自身(この例では Counter)ではなく State(この例では _CounterState)のに定義されている、という点です。

childchildren でも Widget の build() でもなく、StatefulWidget の場合は Statebuild() メソッドが return した Widget が自身の子 Widget となります。

Widget ツリーを構築する Element (一歩踏み込んだ話)

というわけで、Flutter のソースコードからどのように Widget ツリーが構築されるかをざっと見てみました。

特に、ある Widget の子 Widget がどう決まるかは

  • child / children プロパティに指定した Widget
  • Widget の build() メソッドで返却した Widget (StatelessWidget の場合)
  • State の build() メソッドで返却した Widget (StatefulWidget の場合)

などいくつかのパターンはあるものの、概ね Flutter で書いた Widget の入れ子がそのままツリー構造になっていることがイメージできたのではないかと思います。

さて、ここにリストアップした「この場合は、この Widget を子として配置する」の判断はどこでやっているのでしょうか。

その答えは、 Flutter においてとても重要な役割を持っている Element というクラスのソースコードに書かれています。

実はここまで説明してきた「Widget ツリー」というのはあくまで「イメージ」であり、必ずしも Widget 同士が親(もしくは子) Widget を参照しているわけではありません。

実際は、 Widget 1つひとつが Element という別のクラスと紐づいており、 Element 同士が親子の参照を保持する「Element ツリー」を構築することで Flutter における「ツリー構造」が成り立っています。1 その様子を表したのが以下のイメージです。

image.png

この「Elementツリー」を理解することは Flutter のより深い理解につながりますが、一方で Flutter は "Everything is a Widget" のスローガンの通り Widget さえ理解していればある程度のアプリが開発できるようにデザインされたフレームワークですので、今の段階で Element まで踏み込む必要はないでしょう。

もしここまでの内容が問題なく理解できて、さらに Element についても理解したいという方向は、Element ツリーについて詳しく解説した以下の記事(と、その参考記事)を読んでみてください。

【Flutter】Navigator.of(context) から理解する 3つのツリー | Zenn

実は Text は StatelessWidget

さて、これで Widget ツリーの基本を一通り説明してきましたが、最後に1つ、 Text は StatelessWidget のサブクラスである という点について説明します。

詳しくは別記事 【Flutter】Text とは何か を読んでいただければと思いますが、 Text も StatelessWidget のサブクラスですので、 StatelessWidget をはさんだ場合 で説明した通り build() メソッドの中で他のいくつかの Widget を組み立てて return し、それが Widget ツリーに反映されています。

そのことを考慮して最初の Widget ツリーの図を正確に書くと以下のようになります。

image.png

図の通り、 Text の下に SemanticsExcludeSemantics、さらに RichText といった別の Widget がつながっていることが読み取れます。

また、 SemanticsExcludeSemantics は Text の semanticsLabel プロパティに何も指定しなかった場合は省略するような記述も、 Text の build() メソッドの処理として書かれています。

このように、一見ひとつの Widget でも、実はその Widget が build() メソッドを持っていて(つまり StatelessWidget / StatefulWidget のサブクラスで)、私たちアプリ開発者が作る StatelessWidget / StatefulWidget と同様、他のいくつもの Widget を組み合わせてツリー上に配置する場合が少なからず存在します。例えば MaterialAppScaffold などは、大量の Widget を build() で返却する Widget の例です。

ただし、ここまで正確に Widget ツリーを把握しなければならない場面はそれほどありません。開発ツール等で実際の Widget ツリーを細かく確認するようになったときに、「Flutter 標準の Widget は他の Widget の組み合わせでできている場合がある」ことを思い出す程度で良いでしょう。

まとめ

多くの初学者にとって Flutter で UI の構築方法を学ぶ際、まずやるのは「どの Widget をどのように書けばどう表示されるか」を体験することだと思います。

一方で、Flutter の仕組みを少し詳しく知ろうと思ったとき、公式ドキュメントを含めほとんどの記事は Widget がツリー構造であることを前提に説明されています。また、 Provider を使ったり Navigator で画面遷移する仕組みなどもこの Widget ツリーについてのイメージがあるかどうかで理解度が大きく変わります。

まずは自分の書いたプログラムがどのような Widget ツリーを構築するのかイメージできるようになることで、そのような「一歩踏み込んだ」説明を理解する土台を作ると、学習効率の観点からも良いと思います。


  1. 話が複雑になるため省略していますが、Element にもいくつかの種類があります。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存のWebコンテンツをCapacitorを使ってiOSアプリ化する

Zennと同時投稿→ https://zenn.dev/k_u_0615/articles/ece25c2b0a36f3

概要

Capacitorを使ったガワネイティブアプリに注目しています。
Capacitorとは、Webコンテンツをガワネイティブアプリとしてビルドしてくれるフレームワークで、Ionic Teamが開発しています。
既存のWebコンテンツにCapacitorを使用して、アプリ化したらどのような感覚で実現できるのか気になったので、試してみました。

アプリ化の対象

今回アプリ化するのは、Ionic Frameworkの公式ドキュメントの右に表示されているデモです。

スクリーンショット 2021-01-18 12.10.21.png

Ionic FrameworkもIonic Teamが開発しており、こちらはネイティブっぽいUIや画面遷移をhtml/css/jsで再現しているフレームワークです。
このデモ自体はiframeで表示されており、以下のURLでデモ単体を表示できます。

対象 URL
iOS https://ionic-docs-demo.herokuapp.com/?ionic:mode=ios
Android https://ionic-docs-demo.herokuapp.com/?ionic:mode=md

上記のリポジトリをforkして、デモのアプリ化を試みます。

forkしたリポジトリはこちら→ https://github.com/katsuyaU/ionic-docs-demo-for-ios

手順

初回のみ

① Capacitor関連をインストール

npm install -D @capacitor/core @capacitor/cli

npx cap initを実行

  • アプリ名と識別子(bundle identifier)とnpm/yarnが聞かれるので、答えます。

③ capacitor.config.jsonが作成されるので、webDirを適宜編集する

  • 例えば、create-react-appで作成したプロジェクトならbuildに、Vue CLIで作成したプロジェクトならならdistになると思います。
capacitor.config.json
{
  "appId": "com.example.app",
  "appName": "Ionic Demo",
  "bundledWebRuntime": false,
  "npmClient": "npm",
  "webDir": "www", // ←ここ
  "plugins": {
    "SplashScreen": {
      "launchShowDuration": 0
    }
  },
  "cordova": {}
}

npx cap add iosを実行する

  • iosディレクトリにネイティブコードなどが自動生成されます。

スクリーンショット 2021-01-18 13.27.13.png

npx cap open iosを実行し、XCodeの設定を行う

  • 実機で動作確認したいのでTeamやBundle Identifierを入力します。(ここでは詳しく解説しません。)

毎回

① ビルドコマンドを実行、ビルド先のディレクトリにビルドされているか確認する

npx cap copyを実行する

  • capacitor.config.jsonのwebDirがここで参照され、[プロジェクト]/ios/App/publicにコピーされます。

npx cap open iosを実行する

  • XCodeが開きます。

④ XCodeで実行ボタンをクリックする

これで、既存のWebコンテンツをアプリ化することに成功しました!
上記のコマンドを毎回入力するのは面倒なので、npm scriptsに登録します。

package.json
{
  "scripts": {
    "build:ios": "npm run build && cap copy && cap open ios && echo 'Please click ▶️ in XCode!'",
  },
}

感想

思いのほか簡単にアプリ化することができました!
ガワネイティブアプリといえば、Cordovaはやたらビルドエラーする印象だったり、以前の職場ではObjCでローカルサーバーを自作していたりなど、実際にアプリとして動かすまですごく面倒という印象がありました。
しかし、Capacitorは公式docsの通りにすれば簡単に実行することができました。

またIonic Frameworkの話になりますが、webなのにネイティブのようなUIをハイレベルで再現できていることにも感動しました。
この手のネイティブっぽいUIフレームワークといえばOnsen UIが頭に浮かびますが、Ionic Frameworkの方が完成度が高いと思います。(コンポーネント自体はOnsen UIのほうが充実していますが)
最近クロスプラットフォームのアプリ開発ではFlutterが話題で、自分もサンプルを動かしてみたりしていますが、Ionic+Capacitorも良い選択なのではないかと思ってます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[超個人的メモ] iosシュミレーターのディレクトリ構造を覗きたい時

iOSシミュレーターのディレクトリ構造を覗くには
~/Library/Developer/CoreSimulator/Devices
をひらけば良い。

参考
https://stackoverflow.com/questions/6480607/is-there-any-way-to-see-the-file-system-on-the-ios-simulator

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIで作るしりとり地図アプリ

iosアプリ開発を学ぶために、位置情報を組み合わせたしりとりアプリを作成しました。
https://apps.apple.com/app/id1537582064#?platform=iphone

なんで作ろうと思ったか

モバイルアプリ開発のスキルを身につけるために、すぐに作れるチュートリアル的なものを作ろうと思って作りました。しりとりをするだけのアプリなら1ヶ月くらいで終わるだろうと思って始めたのですが、半年もかかる壮大なプロジェクトになってしまいました。

利用技術

  • フロント: SwiftUI
  • バックエンド: Firebase  

発表されたばかりで目新しかったというだけの理由で、SwiftUIを利用しました。
Swiftそのものを触ったことがない、かつ成熟していない技術のため調べるのが難しいので大変でした。バックエンドはFirebaseのFirestoreとFunctionを活用しました。

完成図

回答画面

しりとりを回答して送信する画面。
しりとりになっていなかったり、英数字が入力されたりするとバリデーションがかかる。

地図画面

しりとりの入力場所を地図上に表示する画面。本アプリの目玉。回答地点のピン間をアニメーションで繋いだのがポイント。

履歴画面

これまでのしりとり経歴を表示する。自分の回答のみフィルタすることも可能。

工夫したこと

非同期通信

DBにFirestoreを利用することで、しりとりデータの回答送受信を非同期通信で実現することができました。回答を送信した後、更新ボタンなど押すことなく地図画面、履歴画面の内容が更新されます更新されます。
(一方トランザクションは考慮されていないので、複数人が同時に送信した時の挙動が不安定...)

しりとり選択時の地図表示

地図表示にはSwiftUIではなく、SwiftのMapkitを利用しています。ピッカーで選択されたしりとりを地図上に表示させるために、MapkitとSwiftUIの状態変数をバインディングさせてるのが大変でした。

位置情報データ利用におけるプライバシー配慮

位置情報データを利用するにあたってユーザーからきちんと利用同意をとる必要があり、きちんとできていないとAppStoreのレビューで信じられないくらい怒られます。
実はここが一番苦労したところで、下記の機能を実装することでクリアしました。
1. 利用規約同意
2. 投稿時に位置情報を非公開にする機能
3. 既に投稿したしりとりの位置情報を非公開に変更する機能
アプリにとってはいわば非機能部分の実装ですが、何気にかなり時間がかかりました。

感想

開発に行き詰まるたびに、「このアプリは何のためにあるのだろう」という気持ちが頭をよぎり、モチベーションを維持するのが大変でしたが、なんとかアプリリリースまで漕ぎ着けてよかったです。
技術を身につけるために個人開発をするのはとてもためになりますが、役に立つもの、モチベーションが上がるものを作ることが何よりも大事だなあとしみじみ思います。
一緒に開発していただいた@antyuntyuntyun さんにこの場を借りて感謝申し上げます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む