20210107のiOSに関する記事は7件です。

【Flutter】build() でやってはいけない 3 つのこと

Flutter で仕事したい人のための Widget 入門 で説明した通り、Flutter では基本的に StatelessWidgetStatefulWidget を継承したクラスで build() をオーバーライドし、そこに UI を構築する処理を書いていきます。 (厳密には、 StatefulWidget の場合は State クラスの build()

例↓

login_page.dart
class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(32),
      child: Column(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          TextFormField(
            decoration: InputDecoration(
              hintText: 'chooyan@example.com',
            ),
          ),
          const SizedBox(height: 32),
          ElevatedButton(
            onPressed: () {},
            child: Text('ログインメールを送る'),
          ),
        ],
      ),
    );
  }
}

初めて Flutter を触る人にとっては、この build()そのページの「初期化処理」と捉えてしまいがち なのではないかと思います。自分もそうでした。

しかし、build() を「初期化処理」だと考えて「やってはいけないこと」を書いてしまうと、思わぬパフォーマンスの低下や不具合を引き起こしてしまう可能性があります。

この記事では、そんな build() でやってはいけない 3 つのパターン を理由つきで説明していきます。

build() は何度でも呼ばれる

まず知っておかなければならないのは、 build() は何度でも呼ばれる、ということです。最初の1回だけではありません。

そもそも Flutter は、「状態」が変わるたびに何度も Widget を破棄し、新しい「状態」を使って Widget を作り直すことで画面を変化させる 設計になっています。

例えば Flutter プロジェクトを新規作成した時にテンプレートとして作られる「カウンターアプリ」では、 FloatingActionButton として配置されたカウントアップボタンをタップするたびに _counter(状態を表す変数)をインクリメントして _MyHomePageState の中の build() メソッドが呼ばれます。

_MyHomePageStatebuild() では、以下のように _counter 変数の値を Text で画面に表示するようにコーディングされていますので、

Text(
  '$_counter',
  style: Theme.of(context).textTheme.headline4,
),

build() が呼ばれるたびにその時点での _counter の値を持った Text が作り直され、画面の表示内容が変化する仕組みになっています。

build() が呼ばれるタイミング

そんな build() が呼ばれるタイミングは様々です。

一番分かりやすいのは、 StatefulWidget(が生成する State)で setState() が呼ばれたときです。

カウンターアプリでも、 FloatingActionButton がタップされた時に呼び出される _incrementCounter メソッドには

void _incrementCounter() {
  setState(() {
    _counter++;
  });
}

とコーディングされていて、 setState() を呼び出すことと、 _counter をインクリメントすることが書かれています。

Flutter では、この setState() 内に書いた処理を実行したあとに build() を呼び出す1作りになっているため、新しく build() で作られる Text にはインクリメントした後の _counter の値がセットされる、というわけです。

他にも、 MediaQueryProvider など、 InheritedWidget が保持する値が更新された場合や、同様のことが Widget ツリーの先祖で発生して build() が伝播してきた場合など、いろいろなタイミングがあります。2

特にアニメーションが発生する場合、60 fps、つまり 16 ミリ秒に 1 回という速度(参考)で毎フレームこの build() が呼ばれる場合もあります。

とにかく、 build() は様々なタイミングで、高速に何度でも呼ばれる可能性がある 、ということを忘れてはいけません。

build() でやってはいけない 3 つのこと

build() が高速に何度でも呼ばれる可能性がある ということを踏まえると、以下のような処理はそこに書いてはいけないことが見えてきます。

1. 初期化処理

build() は画面が出ている間(場合によっては画面に見えていない間も)何度でも呼ばれる可能性があります。そのため、状態を管理する変数を初期化するようなコードは書いてはいけません。

int _count;

@override
Widget build(BuildContext context) {
  _count = 0; // 0で初期化
  return Container(
    child: (省略)
  );
}

このように書いてしまうと、何かのきっかけで build() が呼ばれ直した瞬間にそれまでのカウントが 0 に戻ってしまいます。

また、 Firebase やローカルのデータベースとの接続処理であったり、表示するデータの取得処理などもここに書いてしまうと繰り返し呼び出されてバグの原因につながります。サーバーにアクセスする処理であればサーバーの負荷にも影響が出てしまうでしょう。

このような初期化処理は、基本的に State クラスが持っている initState() をオーバーライドするなど、確実にその画面が初めて生成されるタイミングに 1 回だけ呼ばれる場所に書くのが良いでしょう。

2. ひとつだけあれば良いオブジェクトの生成

たとえば GoogleMaps やアニメーション関係の Controller であったり、一度生成してキャッシュしておく Widget の生成処理などはここに書かいてはいけません。

// 良くない例
class MapSampleState extends State<MapSample> {

  Completer<GoogleMapController> _controller;

  @override
  Widget build(BuildContext context) {
    // build ごとに mapController を生成してはいけない
    _controller = Completer(); 

    return GoogleMap(
      (省略)
      onMapCreated: (GoogleMapController controller) {
        _controller.complete(controller);
      },
    );
  }
}
// 良くない例
class FixedComponentState extends State<FixedComponent> {

  List<Widget> cachedMenu;

  @override
  Widget build(BuildContext context) {
    // 使い回す Widget をここで生成してはいけない
    cachedMenu = [
      Text('メニューその1'),
      Text('メニューその2'),
      Text('メニューその3'),
    ];
    return Column(
      children: cachedMenu,
    );
  }
}

Controller が複数生成されてしまってその Controller 自体の初期化処理が何度も無駄に実行されてしまったり、せっかくキャッシュしておいた Widget が何度も再生成 & 上書きされてキャッシュの意味がなくなってしまいます。

このような場合は、例えば以下のようにフィールドの宣言と同時にインスタンスの生成までやってしまうと良いでしょう。

// 改善例 (google_maps_flutter 公式のサンプルコード通り)
class MapSampleState extends State<MapSample> {
  Completer<GoogleMapController> _controller = Completer();

  @override
  Widget build(BuildContext context) {
    return GoogleMap(
      (省略)
      onMapCreated: (GoogleMapController controller) {
        _controller.complete(controller);
      },
    );
  }
}
// 改善例
class FixedComponentState extends State<FixedComponent> {

  // FYI: 可能な限り const もつけるとなお良い
  List<Widget> cachedMenu = [
    Text('メニューその1'),
    Text('メニューその2'),
    Text('メニューその3'),
  ];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: cachedMenu,
    );
  }
}

3. その他重い処理

その他、多量な計算や通信処理、ファイルの読み書きなど、重い処理を書けば書くほど画面の変化を端末に反映させるのが滞ってしまいます。なるべく build() には Widget の生成以外の処理を書かないようにしましょう。

もし重い処理を書く必要があるのであれば、やはり initState() など適切な場所で async つきのメソッドにまとめて処理が終わり次第 setState() で結果だけを反映させるような工夫を入れます。処理が終わるまでは CircularProgressIndicator でクルクルを表示するなどすると良いでしょう。

class ArticleListState extends State<FixedComponent> {

  List<Article> articles;

  Future<void> _loadArticles() async {
    final result = await ArticleLoader().all(); // 記事を全件取得する処理
    setState(() {
      articles = result;
    }
  }

  @override
  void initState() {
    _loadArticles();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // articles が null ならクルクルを表示、あれば ArticleList を表示
    return articles == null ? CircularProgressIndicator() : ArticleList(articles);
  }
}

まとめ

Flutter を入門すると、まずソースコードを書き始めるのが build() の中(StatelessWidget でも StatefulWidget でも)な関係上、どうしてもここが「最初に1回」やる処理を書く場所だと誤解してしまいがちです。

しかし実際は、 Flutter の仕組みとして Widget は使い捨てであり、 build() は何度でも呼び出されます。このことを初学者はまず知る必要があるでしょう。公式ドキュメントにも、以下のように記述されています。

This method can potentially be called in every frame and should not have any side effects beyond building a widget.
訳) このメソッドは毎フレーム呼び出される可能性があり、Widget を構築する以外の副作用を発生させてはいけません。

これは Flutter の根本的な設計の都合ですので、 Provider などの状態管理パッケージを使う場合でも同様の発想が必要になります。3

「1回だけやれば良い処理や重い処理は build() に書いてはいけない、なぜなら build() は高速に連続で呼び出される可能性があるから」 ということを頭に置いておくだけでも、 Flutter アプリ開発をする上で発生する多くの不具合を回避できるでしょう。

(2021.1.10 追記)
この記事の内容を気をつけていても、アプリの規模が大きくなるのにしたがって build() による画面の更新が重くなってしまう場合があります。そんな時に使えるテクニックと考え方を以下の記事に書いてみましたので、読んでみてください。

【Flutter】 無駄なリビルドを防ぐたった1つの方法
(追記ここまで)

併せて読みたい

この記事の内容をさらに詳しく理解する手助けになる記事です。それぞれの記事には公式サイトも参照先に挙げられていますので、そこまで読めればだいぶ理解を深められるでしょう。

記事中に出てきた InheritedWidget の役割や使い方を解説した記事です。 InheritedWidget は実際にアプリ開発者が直接使うことは少ない Widget のため参考資料も少なめですが、この記事は実際に動作するコードつきで段階的に説明してくれているので、 InheritedWidget というものを理解する上でとても勉強になると思います。

InheritedWidget の中身にフォーカスした記事です。Flutter の実装なども読みながらその仕組みを追っていきます。なお、↑の記事とは違い「どうやって使うのか」には一切触れていません。

setState() の実装を追いながらその仕組みを説明した記事です。フレームワーク内部のソースコードに興味のある方は読んでみると、 Flutter のソースコードの追い方などが見えてくるかもしれません。

Flutter の Widget が画面に表示されるまでの Flutter フレームワークの流れを詳しく解説した記事です。ちょっと長くて内容も「慣れてきた人」向けではありますが、いつかは読んでおくととても良い勉強になります。


  1. 実際には build() を直接呼び出すのではなく、 Flutter フレームワークが定期的にチェックしている「更新が必要な Widget リスト」に登録し、次のフレームでの更新対象に入れています。 

  2. このあたりの内容は Flutter の学び初めの段階から知っている必要はありませんが、興味がある場合は 併せて読みたい の参考資料を読んでみてください。 

  3. 対処方法やベストプラクティスは状態管理パッケージごとに異なると思いますので、利用するものに合わせてさらに研究してみてください。 

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

[Swift] スタティックプロパティ(static)を使ってみて分かったこと

はじめに

今回は、スタティックプロパティを理解するためにインスタンスプロパティも一緒に動かしてみたので、
比較しながら理解を深めていけたらなと思います。

試してみたコードの説明

比較のためにインスタンスプロパティaとスタティックプロパティbを用意します。
関数getは、aとbの値の変更内容を表示するためのものです。

struct Sample {
    var a = 0
    static var b = 0

    func get() -> String {
        return "aが\(a)、bが\(Sample.b)"
    }
}

インスタンスプロパティaを変更してみた

//3種類のインスタンスを作成します。
var S1 = Sample()
var S2 = Sample()
var S3 = Sample()

//もちろん結果は同じです。
S1.get()  //"aが0、bが0"
S2.get()  //"aが0、bが0"
S3.get()  //"aが0、bが0"

//S2だけ変更を加えてみます。
S2.a = 3   

//S2.aの部分のみが変更されています。
S1.get()  //"aが0、bが0"
S2.get()  //"aが3、bが0"
S3.get()  //"aが0、bが0"
//変数aにアクセスしようとすると、S1.aかS2.aかS3.aになる。
//インスタンス化したものに紐づいてアクセスするので、値の変更があった場合はそのインスタンスごとに変更になる。
//結果を見てもS2.aだけ変更されている事がわかる。

スタティックプロパティbを変更してみた

Sample.b = 6  

//全てのbが変更されている。
S1.get()  //"aが0、bが6"
S2.get()  //"aが3、bが6"
S3.get()  //"aが0、bが6"
//staticプロパティのbにアスセスするには、Sample.bになる。S1.bとアクセスする事はできない。
//インスタンスではなく、元の型自身に紐づいているプロパティである事がわかる。
//変更結果をみても、元になる型の内容を変更していることになるので、全ての値が変更されている事がわかる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コピペで乗り切るエンジニアがSwiftUIを勉強してみた(環境構築編)

こんにちは!

転職をして会社で扱うPCがMacだったので、
社畜の私はMacBook Proを買いました。

この時期、M1搭載のMacがYoutubeで色々話題ですが、
何か分からないけど使えないソフトとかあったらどうしよう。。。と
Intel CoreのMacを買いました。

さて、せっかくMacを買ったということで、
MacでできてWindowsでできないことといえば、

そう! iOSアプリの開発!

何も分かりませんが、やってみないと分からないということで、
まずは環境構築をやってみます。

環境構築

Swift UIを使えるように実装していきます。

Xcodeのインストール

「App Store」より「Xcode」をインストールしていきます。
スクリーンショット 2021-01-07 19.36.46.png
スクリーンショット 2021-01-07 19.35.29.png

これでバッチリです!!(たぶん)

テストコードを作る

さて、実行環境ができたので
テストコードといえば、「Hello world」の実行ですね。
(「Hello world」と画面に表示するだけ)

「Create a new Xcode project」をクリックします。

次に「iOS」->「App」を選択し、「Next」をクリックします。
スクリーンショット 2021-01-07 19.56.38.png

次に以下を指定して「Next」をクリックします。
* Product Names
プロジェクトの名前。アプリ名の初期値にもなる。
* Team
Apple Developer Web サイトを利用できるユーザーや組織名。
(勉強用ということで適当でー)
* Organization Identifier
任意の値でOK。ただし、iOS アプリの配布を想定した名前。
(こちらも適当でー)
* Interface
「SwiftUI App」を指定する。
* Language
「Swift」を指定する。
スクリーンショット 2021-01-07 19.44.13.png

そのまま「Create」をクリックします。
スクリーンショット 2021-01-07 19.44.29.png

そうすると。。。
プロジェクトが立ち上がるので、そのまま「▶️」をクリックします。
スクリーンショット 2021-01-07 19.45.13.png

しばらく待っていると、シミュレーターの画面に「Hello, world!」が!
おぉーー!これでOK!!(きっと)

次回は「Hello world」のソースについて
ふわっと理解で触れていきます。

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

iOS Simulator 実物大にする方法

はじめに

シミュレータのサイズを実物大にする方法を調べてみても、
情報が古くて指定するメニューがなかったりしたので、
お助けになればと思います。

手順

上部メニューのWindow > Physical Sizeにチェックを入れるだけです。
スクリーンショット 2021-01-07 16.19.55.png

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

iOS14.3のiOSシミュレータでNavigationBarが黄色くなってしまう場合の対処方法

問題点

iOS14.3のiOSシミュレータでiOSアプリをビルドして起動すると、NavigationBarなどの表示が本来は半透明のところが黄色くなってしまいます。
全体の雰囲気がわからなくなるので元にもどしたい。

スクリーンショット

iOSSimulator

対処法

  1. iOSシミュレータのメニューからGPUの設定を変更する
    • 変更前 [File] -> [GPU Selection] -> [Automatic]
    • 変更後 [File] -> [GPU Selection] -> [Prefer Integrated GPU]
  2. iOSシミュレータを再起動する

スクリーンショット 2021-01-07 14.35.20.png

変更後

  • 下記のスクリーンショットのように元の半透明に戻っていることが確認できます

スクリーンショット

iOSSimulator

参考URL
https://stackoverflow.com/questions/65299337/xcode-12-3-ios-14-3-simulators-yellow-dock-yellow-translucent-issues

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

[Swift5]コードでダークモード(暗い外観)の適用を回避する方法

実装方法

//ダークモードの適用を回避
self.overrideUserInterfaceStyle = .light

これで常にライトモード(明るい外観)になります。

まとめ

この方法で簡単にダークモードの適用を回避することができます。

しかし、AppleはiOSアプリのダークモード適用を推奨しているので、どこかのタイミングでアプリにダークモード設定を適用させる必要があります。

この方法は、一時的な対応として処理しましょう!

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

iOS14でtrackingAuthorizationStatusのメモ

環境

  • Xcode12.3
  • iOS 14.2 iPhone SE

条件

  1. 端末: 設定>プライバシー>トラッキング>Appからのトラッキング要求を許可 IMG_9760666317E9-1.jpeg
  2. アプリ: トラッキング要求ダイアログ IMG_2264.PNG

トラッキング要求の動き

端末 トラッキング要求 アプリ status
許可 - notDetermined
許可 authorized
拒否 denied
拒否 - denied
許可 denied
拒否 denied

想定通り。
トラッキング要求を出していない状態で、端末設定を拒否から許可に戻すとちゃんとnotDeterminedに戻ります

トラッキング要求を許可後の動き

アプリでトラッキング要求を許可した後に端末設定を拒否にした場合、こんなダイアログが出てくる
トラッキング要求をしていない場合や拒否した場合はダイアログがでない
IMG_729819AB963E-1.jpeg

「Appがトラッキングを続けることを許可する」を選ぶと、こんな画面なので端末で拒否しても取れるのかなと思ったらそんなことはなかった。
IMG_6D17ED26E52E-1.jpeg

再表示するとちゃんとオフになっています
IMG_8F6BE63E2509-1.jpeg

端末設定で再許可時のstatusをそのままにしてくれる動きのようです

ダイアログ status 端末設定で再許可時のstatus
Appがトラッキングを続けることを許可する denied authorized
「Appにトラッキングの停止を要求」 denied denied

端末拒否時のトラッキング要求の動き

また、端末で拒否時にトラッキング要求をするとdeniedが設定されて端末設定を許可に戻してもdeniedのままになります。
トラッキング要求を出していなければnotDeterminedに戻ります

端末拒否時

トラッキング要求 status 端末許可に変更後のstatus
denied notDetermined
denied denied
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む