20210105のiOSに関する記事は10件です。

[Swift] 遷移先で変更した配列を遷移元に反映させ、Label等に表示させる方法

はじめに

今回はNavigationControllerを用いて、画面遷移を行っています。そのため、遷移先から遷移元に戻る時に呼ばれるnavigationControllerのwillShowを使っています。また、collectionViewのCell内にLabelを置き、そこに配列(String)の値を表示させています。

開発環境

macOS 11.0.1
Xcode version 12.2
Swift version 5.3.1

コード

親画面にはString型で変数aを与え、初期値として5個の何の値も入っていない値を与えておきます。これを、collectionViewを用いて、Cell内に設置したLabelに先程の値を与えています。遷移先で変更された配列を遷移元に渡した時にプロパティ・オブザーバを設定してあげることで変更が起こった時の処理を書くことができるようになります。オブザーバは、プロパティ変更前に呼ばれる willSet と、変更後に呼ばれる didSet があります。今回は、変更後に呼ばれるdidsetを使います。

FirstViewContoroller
import UIKit

class ViewController:
                  //collectionViewを用いるため、下の2つを書いておく
UIViewController,UICollectionViewDelegate,UICollectionViewDataSource {
   //上と同じ理由で書いておく
    @IBOutlet weak var collectionView: UICollectionView!

    var a = ["","","","",""] {didSet{

       //didset内に書くことで、変数aに変更が起こった場合にreloadDataし、label内の表示           される値も変更することができる
        collectionView.reloadData()

        }

    }

    override func viewDidLoad() {
        super.viewDidLoad()        

    } 

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

        return a.count

    }     

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for:indexPath)
        cell.backgroundColor = .lightGray

        //タグつけしたlabelに変数aの値を順に表示
        var aLabel = cell.contentView.viewWithTag(1) as! UILabel
        aLabel.text = a[indexPath.row]

        return cell
    }

//ほかには、何かしらの方法で遷移するコード書く

}

NextViewController
import UIKit


class NextViewController: UIViewController,UINavigationControllerDelegate {

    @IBOutlet weak var textFiled: UITextField! 

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    //ナビゲーションバーの戻るボタンを押した時に呼ばれる処理。下記を書くことで遷移先で遷移元の値   をいじることができる
     func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {

                                              //親画面のファイル名を記入
        if let controller = viewController as? FirstViewContoroller{

       //今回は変数aの1番目の値を変更する
       //遷移先画面のテキストフィールドに打たれた文字が遷移元の変数aの1番目の値を変更する
            controller.a[0] = textfiled.text!
        }

まとめ

今回のコードで重要なことは、プロパティ・オブザーバの一つであるdidsetです。この中でcollectionView.reloadData()を書くことで、変数である配列に変更が起きた場合に、
collectionView.reloadData()が呼ばれ、cell内のlabelに表示される値を変更毎に更新することができます。

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

【iOS/Android】ネイティブアプリをAPIと繋ぎ込む時に便利なツール

iOS/AndroidでAPIをつなぐ時に便利なツールを紹介します!

Chrome拡張のTalend API TesterでAPIテスト - Qiita

APIが正常に動いているか、これでチェックします!
この記事わかりやすくて神ですよ!

Text Escaping and Unescaping in JavaScript

JSONに入っているUniCode、読みたいですよね?
そんな時はこれに打ち込めば!日本語に変換してくれて便利!!!

JSON Schema Tool

あとは最後に、これ
JSONを入れるだけでスキーマを作ってくれます!楽チンですね

まとめ

この記事で少しでも開発が捗ったら嬉しいです!

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

InheritedWidgetの目的と使い方【Flutter】

まえがき

InheritedWidgetの使い方の基本をまとめます!

Flutterを勉強していてよくわからなくなるポイントの一つがこのInheritedWidgetだと思います。

筆者自身、これを理解するのにかなり時間がかかったので、「こういう説明があったらよかったな」と思う説明を書きます。

なお、以下では、InheritedWidgetクラスを継承したクラスのことを単にInheritedWidgetと呼びますね。他のStatefulWidgetなども同様です。

前提知識

FlutterアプリがWidgetのツリー構造でできていることを知っていて、StatefulWidgetsetStateは使えるものとします。

BuildContextの知識も少しだけ要りますが、「Widgetツリー内での位置を表現するもの」程度の理解で十分です。

それについてはこちらをお読みください。

Flutterのcontextの「お気持ち」を理解する

記事内の例の実行方法

該当WidgetをMaterialAppで包んでrunAppに渡すか、GitHubのrepositoryからプロジェクトをダウンロードして実行してください。

それぞれの例がページ別にわかれたアプリになっています。

https://github.com/agajo/learn_inherited_widget

InheritedWidgetの存在理由

InheritedWidgetの目的は、次の2つです。

  • Widgetツリーの下層のBuildContextから、プロパティにO(1)でアクセスすること
  • プロパティが変化した時に下層のBuildContextに更新を伝えてリビルドさせること

一つずつ説明します。

Widgetツリーの下層のBuildContextから、プロパティにO(1)でアクセスする

このOはランダウの記号と言います。ゼロではなくてオーです。

O(1)というのはアクセスにかかる時間が何に依存して増えるかを表す表現なのですが、O(1)ということは、そのアクセス時間が何にも依存しない(つまり増えない。常に最速でアクセスできる)ことになります。

ランダウの記号#無限大における漸近挙動と計算量の見積り - Wikipedia

dependOnInheritedWidgetOfExactTypeを使え!

BuildContextにはdependOnInheritedWidgetOfExactTypeというメソッドがあります。

context.dependOnInheritedWidgetOfExactType<Hoge>()のように書くと、そのcontextからWidgetツリーをさかのぼった時に一番近くにあるHogeというInheritedWidgetを返します。

その時の計算時間はO(1)です。つまり最速です。

基本的にfindAncestorStateOfTypeは使うな!

BuildContextにはfindAncestorStateOfTypeというメソッドもあり、同じようにBuildContextからWidgetツリーをさかのぼって一番近くにある、指定した型のStateを返すのですが、その時の計算時間はO(n)です。

このnBuildContextから実際に返るStateまでの、Widgetツリーの深さを表します。つまりWidgetツリーが深くなると、それに比例して計算に時間がかかるようになり、アプリが重くなっていくのです。

使用例

InheritedWidgetは、StatelessWidgetStatefulWidgetと同様に、InheritedWidgetクラスをextendsしたクラスを自分で作って使います。

この例ではMessageDataというInheritedWidgetを作っています。

updateShouldNotifyについては後ほど説明しますので今はスルーしてください。

下記のMainWidget()を、MaterialAppで覆ってrunAppに渡します。

just_access.dart
import 'package:flutter/material.dart';

// InheritedWidgetを作ります。
class MessageData extends InheritedWidget {
  const MessageData({Key key, @required Widget child})
      : assert(child != null),
        super(key: key, child: child);

  // InheritedWidgetのプロパティは変更不可能なので必ずfinal
  final String message = 'InheritedWidgetが保持してるメッセージだよ〜〜〜〜';

  // updateShouldNotifyについては後で説明します。
  @override
  bool updateShouldNotify(MessageData old) {
    return true;
  }
}

class MainWidget extends StatelessWidget {
  const MainWidget();
  @override
  Widget build(BuildContext context) {
    // Widgetツリーの上層部に、作ったInheritedWidgetを配置
    return MessageData(
        child: Scaffold(
      appBar: AppBar(),
      body: WidgetA(),
    ));
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA();
  @override
  Widget build(BuildContext context) {
    // InheritedWidgetからO(1)でmessageを取得!!
    final message =
        context.dependOnInheritedWidgetOfExactType<MessageData>().message;
    return Text(message);
  }
}

WidgetA内で、上層のInheritedWidgetからmessageが取得され、表示されます。

スクリーンショット 2021-01-03 0.18.30.png

Widgetツリー内でInheritedWidgetを上書きする

Widgetツリー内の一部でだけ異なるMessageDataを使いたい時は、もう一度MessageDataWidgetを配置すればいいです。

Theme, MediaQuery, Directionalityなど多くのFlutter Widgetがこの性質を持っていますね。実はそこでもInheritedWidgetが利用されています。

次の例を見てください。

MessageDataは2箇所配置されていますが、より呼び出し元のBuildContextに近い方からmessageが取得されていることを確認してください。

MessageDataのプロパティmessageはコンストラクタで外から受け取れるようにしました。

override_message.dart
import 'package:flutter/material.dart';

// InheritedWidgetを作ります。
class MessageData extends InheritedWidget {
  const MessageData({
    Key key,
    @required Widget child,
    @required this.message,
  })  : assert(child != null),
        super(key: key, child: child);

  // 今回はmessageをコンストラクタで受け取ります。
  final String message;

  @override
  bool updateShouldNotify(MessageData old) {
    return true;
  }
}

class MainWidget extends StatelessWidget {
  const MainWidget();
  @override
  Widget build(BuildContext context) {
    return MessageData(
        message: 'メッセージその1だよ',
        child: Scaffold(
          appBar: AppBar(),
          // 内容を変えたMessageDataウィジェットを挟み込みます
          body: MessageData(
            message: 'メッセージその2ですよ〜〜〜',
            child: WidgetA(),
          ),
        ));
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA();
  @override
  Widget build(BuildContext context) {
    final message =
        context.dependOnInheritedWidgetOfExactType<MessageData>().message;
    return Text(message);
  }
}

スクリーンショット 2021-01-03 0.33.16.png

プロパティが変化した時に下層のBuildContextに更新を伝えてリビルドさせる

InheritedWidgetのインスタンスは、中身を変更できません。

にもかかわらず更新を伝播するとは、一体どういうことなのでしょう?変更不可能なのに更新するとは??

筆者は以前、この部分で混乱していました。

答えはこうです。

InheritedWidgetのインスタンスを何度も作り直して置き換える

状態を更新可能なStatefulWidgetなどと組み合わせて、Widgetツリー内にあるInheritedWidgetのインスタンスを作り直して再配置します。

プロパティの異なるInheritedWidgetを作って置き直せば、プロパティの変更が、それに依存するBuildContextに伝播され、リビルドされるわけです。

使用例

実際の例で見てみましょう。

StatefulWidgetを使って、1秒に1回、プロパティを1ずつ増やしたInheritedWidgetを作って再配置します。作るInheritedWidgetの名前はCountDataとしています。

InheritedWidgetchildに渡すWidgetは、変更せず、同じインスタンスを毎回渡すことにします。

update_count.dart
import 'dart:async';

import 'package:flutter/material.dart';

// InheritedWidgetはmessageではなくcountを保持することにします。
class CountData extends InheritedWidget {
  const CountData({Key key, @required Widget child, @required this.count})
      : assert(child != null),
        super(key: key, child: child);

  final int count;

  @override
  bool updateShouldNotify(CountData old) {
    return true;
  }
}

// StatefulWidgetを使います。
class MainWidget extends StatefulWidget {
  MainWidget();
  // InheritedWidgetのchildには、毎回このインスタンスを渡します。
  final Widget child = Scaffold(
    appBar: AppBar(),
    body: WidgetA(),
  );

  @override
  _MainWidgetState createState() => _MainWidgetState();
}

class _MainWidgetState extends State<MainWidget> {
  int timeCount = 0;
  Timer timer;

  @override
  void initState() {
    super.initState();
    // Stateの初期化時に、毎秒setStateを呼ぶタイマーを起動します。
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      setState(() {
        timeCount++;
      });
    });
  }

  @override
  void dispose() {
    // initStateでタイマーを起動・disposeでタイマーを破棄。この2つはセットです。
    timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // タイマーにより毎秒setStateが呼ばれるので、このbuildメソッドが毎秒呼ばれます。
    // その際、このCountDataコンストラクタでCountDataのインスタンスが毎回作り直されます。
    // 渡す引数が異なるので、作られるCountDataのプロパティは毎回異なります。
    return CountData(
      count: timeCount,
      // StateではなくStatefulWidgetが保持しているchildを渡します。
      // つまり、毎秒変更されるインスタンスはCountDataだけであり、他は変化しません。
      child: widget.child,
    );
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA();
  @override
  Widget build(BuildContext context) {
    // ここでこのWidgetAのBuildContextがCountDataの変更通知対象として登録されるので、
    // CountDataのインスタンスが置き換わるたびにこのbuildメソッドが呼ばれます。
    final count = context.dependOnInheritedWidgetOfExactType<CountData>().count;
    return Text('count: $count');
  }
}

Jan-03-2021 20-36-01

updateShouldNotifyで更新を間引きする

InheritedWidgetのよくわからないポイントの一つ、updateShouldNotifyです。

ここまでの説明で、InheritedWidgetのインスタンスの置き換えが行われる際に更新が伝播されるようなことを言いましたが、

実は、実際に更新が伝播されるのはupdateShouldNotifytrueを返した時だけです。

インスタンスの置き換えの際、古い方のインスタンスも手元に手に入るので、比較して、更新通知するか制御可能です。

例えば先程の例で、更新を2回に1回にする(つまり、countを2で割った商が変化した時だけ更新する)場合、updateShouldNotifyは次のように書けます。

  @override
  bool updateShouldNotify(CountData old) {
    return count ~/ 2 != old.count ~/ 2;
  }

他は先程の例と全く同じようにすると、次のような動きになります。

Jan-03-2021 20-40-54

updateShouldNotifyの中身を、場合によって都合のいいように実装すればいいわけですね!

慣例として、ofを実装する

ここまで、動きの理解を重視して、BuildContext.dependOnInheritedWidgetOfExactTypeを直接呼んでいましたが、

Flutterの慣例として、このメソッドは直接呼ばず、InheritedWidgetofというstaticメソッドを実装して、そこから呼ぶのが普通です。

最初のMessageDataの例ですと、このようになります。

implement_of.dart
import 'package:flutter/material.dart';

class MessageData extends InheritedWidget {
  const MessageData({Key key, @required Widget child})
      : assert(child != null),
        super(key: key, child: child);

  final String message = 'InheritedWidgetが保持してるメッセージだよ〜〜〜〜';

  @override
  bool updateShouldNotify(MessageData old) {
    return true;
  }

  // ofを実装します!!!
  static MessageData of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<MessageData>();
}

class MainWidget extends StatelessWidget {
  const MainWidget();
  @override
  Widget build(BuildContext context) {
    return MessageData(
        child: Scaffold(
      appBar: AppBar(),
      body: WidgetA(),
    ));
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA();
  @override
  Widget build(BuildContext context) {
    // of経由で呼びます!!!
    // final message = context.dependOnInheritedWidgetOfExactType<MessageData>().message;
    final message = MessageData.of(context).message;
    return Text(message);
  }
}

こうすると、Themeなどで見かけたTheme.of(context)という見た目にそっくりになりましたね。

以上でInheritedWidgetの基本的な使い方の説明を終わります。

(応用)下層から更新する場合はどうするの?

InheritedWidgetを、アプリの状態管理にガッツリ使うことを考えます。

先程のタイマーを使った例では、更新がStatefulWidgetState内部から自発的に発生していました。

一方実際のアプリでは、Widgetツリー下層にあるRaisedButtonが押されたら、上層で保持している状態を更新する、という動きがよくあります。

となると、Widgetツリーの下層から上層の状態を更新するメソッドを呼ぶ必要があるわけです。

下層から更新用メソッドにアクセスできるようにする方法は、例えば

  • その更新用メソッドを、毎回Widgetのコンストラクタに渡して下層まで持っていく

という方法がありますが、これは中間層すべてのコンストラクタに記述が必要で、イマイチです。

せっかくInheritedWidgetがあるので、更新用メソッドにも下層からO(1)でアクセスしましょう!

今回は、公式のList of state management approachesからリンクされている

これらを参考に、一つ方法を紹介します。

InheritedWidgetState自体を下層に流せ!

StatefulWidgetを使う場合、プロパティも更新用メソッドもStateが持っていることが多いです。更新時にsetStateを呼ばないといけないので、更新用メソッドもStateが持たないといけない。

ならば、State自体をInheritedWidgetで下層に流してしまえば、そのプロパティにアクセスするのも更新用メソッドにアクセスするのも簡単ですね!

その方法を簡単にまとめるとこうです。

  • StatefulWidgetInheritedWidgetのペアで使う
  • InheritedWidgetprivateにする
  • Statepublicにする
  • InheritedWidgetdataというプロパティを持ち、Stateを格納する
  • ofメソッドはStatefulWidgetが持ち、State(InheritedWidgetdataプロパティ)を返す
  • StatefulWidgetchildを受け取り、StatebuildInheritedWidgetの直下に入れて返す
  • メソッドだけ欲しい場合はBuildContext.getElementForInheritedWidgetOfExactTypeを使うと、更新伝播の対象にならない

実例でコメントと一緒に見るのが早いと思いますので、次をご覧ください。

使用例

ボタンが押された回数をテキストで表示するだけのアプリを考えます。

ただし、状態管理WidgetはWidgetツリーの上層にあり、ボタンとテキストはツリーの違う枝に置きます。

今回は状態更新用WidgetとアプリUI用Widgetでファイルを分けます。

countを管理するStatefulWidget+InheritedWidgetのセット

まず、状態管理用のWidgetを用意します。

count_manager.dart
import 'package:flutter/material.dart';

// InheritedWidgetはStateを保持するだけの役。
// privateにします。
class _StateContainer extends InheritedWidget {
  const _StateContainer({Key key, @required Widget child, this.data})
      : assert(child != null),
        super(key: key, child: child);

  // CountManagerStateのインスタンスを保持し、下層に流します。
  final CountManagerState data;

  @override
  bool updateShouldNotify(_StateContainer old) {
    return true;
  }
}

// 状態管理用Widgetです。
class CountManager extends StatefulWidget {
  const CountManager({this.child});

  final Widget child;

  // ofメソッドはStatefulWidgetが持ちます。
  // ofにlisten引数を追加します。デフォルトはtrueです。
  static CountManagerState of(BuildContext context, {bool listen = true}) =>
      listen
          ? context.dependOnInheritedWidgetOfExactType<_StateContainer>().data
          // listen==falseの場合、このcontextが更新の対象にならないようにします。
          : (context
                  .getElementForInheritedWidgetOfExactType<_StateContainer>()
                  .widget as _StateContainer)
              .data;
  @override
  CountManagerState createState() => CountManagerState();
}

class CountManagerState extends State<CountManager> {
  // 下層からアクセスしたいプロパティです。
  int count = 0;

  // 下層からアクセスしたい状態更新用メソッドです。
  void increment() {
    setState(() {
      count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return _StateContainer(
      // InheritedWidgetのdataに、このStateインスタンス自体を渡す。
      data: this,
      child: widget.child,
    );
  }
}

BuildContextgetElementForInheritedWidgetOfExactTypeは、InheritedWidgetの中身にアクセスしつつも、BuildContextを状態更新時のリビルド対象として登録しません

状態更新用メソッドだけを取得して、リビルド対象にしたくない場合、こちらを使います。

(BuildContextgetElementForInheritedWidgetOfExactTypeの返り値はInheritedWidgetではなくInheritedElementなので、返り値の処理が若干変わってます)

UI構築

update_method.dart
import 'package:flutter/material.dart';
import 'package:learn_inherited_widget/count_manager.dart';

class MainWidget extends StatelessWidget {
  const MainWidget();
  @override
  Widget build(BuildContext context) {
    // CountManagerの傘下に、ボタンと表示用テキストを配置します。
    return CountManager(
      child: Scaffold(
        appBar: AppBar(),
        body: Column(
          children: [
            const ButtonWidget(),
            const ShowCount(),
          ],
        ),
      ),
    );
  }
}

class ButtonWidget extends StatelessWidget {
  const ButtonWidget();
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
        child: const Text('increment!!'),
        onPressed: () {
          // ofメソッドでCountManagerの状態更新用メソッドにアクセスします!
          // ボタンをリビルドする必要はないので、listen:falseとします。
          CountManager.of(context, listen: false).increment();
        });
  }
}

class ShowCount extends StatelessWidget {
  const ShowCount();
  @override
  Widget build(BuildContext context) {
    // ofメソッドでCountManagerのプロパティにアクセスします!
    // 更新を反映する必要があるので、listenはデフォルト(true)にします。
    final count = CountManager.of(context).count;
    return Text('count: $count');
  }
}

Jan-05-2021 22-26-59

Providerパッケージについて

先程の例のcount_manager.dartは50行近くあるのですが、countプロパティとincrementメソッド以外は、毎回ほぼ同じ内容を書くことになります。

そこで、それをパッケージ化すると便利そうだなと思うわけなのですが、

そうして出来上がったパッケージがProviderパッケージです。

(実際には、他にもいろいろな機能が追加されているので単純にそうとは言えませんが)

ということで、基本的にはProviderパッケージを使えばいいと思います。

同じ作者によってさらなる改良がなされたRiverpodもありますね。

もちろん、他にもお好みの状態管理手法を使ってもいいです。

この記事で紹介した方法を使ってもいいと思います。

この記事で紹介した方法は、Flutter以外のパッケージに一切依存しないところがメリットですね。

おわり

おわりです!これでInheritedWidgetを理解してもらえたら嬉しいです!

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

Alamofireでインターネット接続状態を確認する

はじめに

AlamofireでAPIリクエストをする際に端末がインターネット接続されていなかった場合エラーを流す必要がありました。
その時に調べたことのまとめになります。

NetworkReachabilityManagerを使う

Alamofireには標準でNetworkReachabilityManagerというクラスがあります。
今回はそちらを使用して接続状態を確認します。
参照

使い方

その①
ベタがき
コード量が少ないがパッと見読みにくい

import Alamofire
if let isConnected = NetworkReachabilityManager()?.isReachable, !isConnected {
     print("Disconnect")
 }

その②
クラスを定義する
見やすい

import Alamofire
class ConnectCheck {
    func isConnectedNetwork() -> Bool {
        return NetworkReachabilityManager()?.isReachable ?? false
    }
}

使う時

if ConnectCheck.isConnectedNetwork {
    print("Connect")
} else {
    print("Disconnect")
}

こんな感じでした。

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

INRestaurantReservation: ユーザーの予約情報をSiriに共有して、イベントを自動的に提案する

あなたがレストラン予約アプリケーション、またはユーザーのために予約を入れるアプリケーション(フライト、列車、チケット)を開発している場合、予約の詳細についてSiriに通知することができます。

この記事では、予約アプリのデモを作成します。ユーザーが予約を行うと、アプリが予約情報をSiriに送信し、Siriは予約情報をカレンダーに追加するようユーザーに促します。予約が閲覧されると、アプリが予約時間を更新します。システムカレンダーアプリが予約情報の更新について、ユーザーに通知するのが確認できます。

利点

Siriに通知することで、ユーザーはイベントをカレンダーに追加するための通知を即座に受け取ります。

イベント会場の詳細情報もSiriによって自動的に入力されます。

また、Siriは予約前に関連情報をユーザーの画面に表示します。

もう一つのメリットは、(予約IDに基づいて)特定の予約イベントに関する最新情報がアプリ上で更新されると、Siriがイベントの詳細を自動的に更新することです。アプリ上で既存の予約に変更があると、システムカレンダーアプリに通知バッジが表示されます。

import intents

Github: https://github.com/mszmagic/SiriReservationSample

ステップ 1. 期間を定める

まず、予約が続行する期間を定める関数を書きます。実演アプリケーションでは、レストランの予約を2時間に設定しました。

private func getReservationPromoteDateRange(_ item: Reservation) -> INDateComponentsRange {
    let calendar = Calendar.autoupdatingCurrent
    // 予約時間の2時間後まで、予約の詳細を閲覧するようユーザーに宣伝しましょう
    let promoteDate_end = calendar.date(byAdding: .hour, value: 2, to: item.bookedTime) ?? item.bookedTime
    let promoteDate_end_components = calendar.dateComponents(in: TimeZone.autoupdatingCurrent, from: promoteDate_end)
    let promoteDate_start = item.bookedTime
    let promoteDate_start_components = calendar.dateComponents(in: TimeZone.autoupdatingCurrent, from: promoteDate_start)
    //
    return .init(start: promoteDate_start_components, end: promoteDate_end_components)
}

ステップ 2. 予約名を定める

予約項目に対して固有の識別名を作成する必要があります。

let reservation_item = INSpeakableString(vocabularyIdentifier: item.reservationID, spokenPhrase: "\(item.restaurantName)の予約", pronunciationHint: nil)

ステップ 3. 予約を見る時に実行するアクションを定める

それから、ユーザーが予約を見たい時に実行するアクションを定めます。URLを提供することもできます。これらの情報は、ユーザーが特定の予約項目を見たい時に、どのようなアクションを実行するかをSiriが認識するために使用されます:

private func getViewReservationDetailsAction(_ item: Reservation) -> INReservationAction {
    let viewDetailsAction = NSUserActivity(activityType: "com.example.SiriReservationSample.viewReservationDetails")
    let reservationDateStr = DateFormatter.localizedString(from: item.bookedTime, dateStyle: .short, timeStyle: .short)
    viewDetailsAction.title = "\(item.restaurantName)における\(reservationDateStr)での予約状況の詳細を表示します"
    viewDetailsAction.userInfo = ["reservationID" : item.reservationID]
    viewDetailsAction.requiredUserInfoKeys = ["reservationID"]
    viewDetailsAction.webpageURL = generateReservationURL(item)
    return .init(type: .checkIn,
                 validDuration: getReservationPromoteDateRange(item),
                 userActivity: viewDetailsAction)
}

ステップ4. 予約オブジェクトを決めてください

let reservation = INRestaurantReservation(itemReference: reservation_item,
                                          reservationNumber: item.reservationID,
                                          bookingTime: item.bookedTime,
                                          reservationStatus: item.reservation_status,
                                          reservationHolderName: item.personName,
                                          actions: reservation_actions,
                                          url: generateReservationURL(item),
                                          reservationDuration: getReservationPromoteDateRange(item),
                                          partySize: item.reservation_partySize,
                                          restaurantLocation: item.restruantLocation)
Variable name Explanation
itemReference 参照IDオブジェクト reservation_item
reservationNumber 予約ID
bookingTime ユーザーがこの予約を入れる時間
reservationStatus 予約状況
reservationHolderName この予約を行なった人物の名前
actions この例における関数 getViewReservationDetailsAction() のアウトプット
url ユーザーが予約詳細へアクセスするためのURLアドレス(ウェブページ)
reservationDuration 予約の開始と終了を定めてください。getReservationPromoteDateRange() 機能の出力
partySize この予約の対象人数
restaurantLocation レストランの所在地 CLPlacemark

ステップ5.予約についてシステムに入力してください。

ここで、予約についてシステムに入力する必要があります。

let intent = INGetReservationDetailsIntent(reservationContainerReference: reservation_item, reservationItemReferences: nil)
let response = INGetReservationDetailsIntentResponse(code: .success, userActivity: nil)
response.reservations = [reservation]
let interaction = INInteraction(intent: intent, response: response)
interaction.donate(completion: completionHandler)

これで、カレンダーのイベントに追加するためのSiriのメッセージがユーザーに表示されます。

関数を呼び出すタイミング

ユーザーが予約を閲覧するたびに(情報が変更されていなくても)上記の関数を呼び出す必要があります。予約の詳細が変更された場合は、Siriがカレンダーアプリを通じてユーザーに通知します。

なお、1つの予約に対してご自分の予約IDが同じであることを確認することが重要です。

ステップ6. NSUserActivityTypes

プロジェクトの設定から Info タブに移動し、サポートされているアクティビティタイプの情報を追加します。上のコードでは、アクティビティタイプとして com.example.SiriReservationSample.viewReservationDetails を使用しています。

ステップ 7. Siriでのアプリケーションの起動を処理

ユーザーが予約の閲覧を希望する場合、Siriでアプリケーションを起動する事ができます。

SceneDelegate.swift ファイル内に次の関数を追加してください:

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    if userActivity.activityType == "INGetReservationDetailsIntent" {
        //
    }
}

こちらが予約IDをフェッチする方法です:

if let userActivity = notificationObject.object as? NSUserActivity,
   let intentObject = userActivity.interaction?.intent as? INGetReservationDetailsIntent,
   let reservationName = intentObject.reservationItemReferences?.first,
   let reservationID = reservationName.vocabularyIdentifier {
    DispatchQueue.main.async {
        self.viewingReservationID = .init(reservationID: reservationID)
    }
}

userInfo プロパティの利用を試みましたが、そこには、予約IDは含まれていないようです。つきましては、代わりに上記のコードを使って予約IDを取得してください

以前のコードの設定:

let reservation_item = INSpeakableString(vocabularyIdentifier: item.reservationID, spokenPhrase: "\(item.restaurantName)の予約", pronunciationHint: nil)

従って、変数 vocabularyIdentifier にアクセスすると、item.reservationID が得られます。


:relaxed: Twitter @MaShunzhe

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

[Swift5]特定の時間を比較して前なのか後なのかを判定する方法

特定の時間の比較

例えばアプリを起動したときに、現在時刻と開発者側で予め設定している時刻を比較して、その状況にあった処理をおこないたい場合があったとします。(何時より前なら処理A, 後ならBなど)

そういった状況で使えるのが.compare( )メソッドです。
今回はこちらのメソッドの紹介をします!

記述方法

if '現在時刻'.compare('こちらで設定した時刻') == .orderedDescending {
  //ここで処理
  //.orderedDescendingは〜より後
}

if '現在時刻'.compare('こちらで設定した時刻') == .orderedAscending {
  //ここで処理
  //.orderedAscendingは〜より前
}

このように記述すればOK!
ふたつの時刻を.compare( )メソッドで比較して、以降なのか以前なのかを判定しております。
また、双方の時刻が同じかどうかを判定したい場合は.orderedSameを使えばいい。

参考にしてください!

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

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

この記事について

Flutter の入門記事の中には、初学者にもわかりやすく説明しようとするあまり実際のアプリ開発で重要になる内容まで抜け落ちてしまったり、意訳や曲解によって誤解を与えてしまう内容が含まれることもあったりします。

この記事は、Flutter で本格的にアプリ開発を進める上で僕が先に知りたかったこと、詳しい説明がほしかった部分を、過去の自分と同じような Flutter 初学者向けに説明する記事です。

本来は公式ドキュメントを読みながら学習を進められるのが理想ではありますが、Flutter のドキュメントは全て英語であること、またある程度のアプリ開発経験があることを想定した難易度になっている(ように感じる)ことから、まずは日本語の記事を頼りに学習を進める方も多いと思います。この記事は、そんな方の役に立つ内容を目指しています。

この記事では、中でも Flutter の Widget に焦点を当てて書いています。

本文

Everything is a Widget

Flutter には "Everything is a Widget" というスローガンがあります。

https://flutter.dev/docs/resources/inside-flutter#conclusion

これは、UI を構築する全ての要素を Widget として表現する(つまり、 Widget クラスのサブクラスとして定義する)ことを目指した設計で、 Flutter 最大の特徴の1つに上げられることも多い考え方です。

例えば、画面にテキストを表示する Text は Widget のサブクラスですし、その周りのパディングを設定する Padding もやはり Widget です。ほかにも、「この時は Text を出す、この時は出さない」という設定は Visibility で実現でき、これもやはり Widget です。

宣言的プログラミング

そんな Widget を使って実際に画面を作るソースコードは以下のような書き方をします。

return Visibility(
  visible: _isVisible,
  child: const Padding(
    padding: const EdgeInsets.all(16),
    child: Text('Hello, Flutter!'),
  ),
);

これは、 _isVisible 変数が true であれば、四方を 16 のパディングで囲まれた Text で 'Hello, Flutter' を画面に表示する 、という内容です。

もし Kotlin や Swift でのアプリ開発経験がある方であれば、画面を作るための考え方が全く違うことに気が付くのではないかと思います。

おそらく同じようなことを Kotlin でしたければ、以下のようなコードになるでしょう。

if (isVisible) {
    val text = TextView(context);
    text.text = "Hello, Flutter!";
    text.setPadding(16, 16, 16, 16);

    view.addView(text);
}

(ちょっと Kotlin を忘れ気味なので) 細かなコードの正しさや内容は置いておいて、プログラムの作りが全く異なることがひと目で分かるのではないでしょうか。

Kotlin のコードは、

  • もし isVisibletrue であれば
  • TextView インスタンスを生成して
  • text フィールドに文字列を代入して
  • setPadding() でパディングを設定して
  • 親の View に追加する

という、手順を踏まえて1行1行処理を進めていく形で UI を構築します。とても「プログラミング的」と言えると思います。

一方で Flutter は、

  • Visibility を配置する
  • 表示 / 非表示の判定には _isVisible の値を使用する
  • Visibility の childPadding である
  • Padding の値は padding プロパティにセットした通りである
  • Padding の childText である
  • Text が表示する文字列は "Hello, Flutter!" である

と、まるで設定ファイルを記述するようにコーディングします。少なくとも、1行1行でメソッドを呼び出したりフィールドに値をセットしたり、といった作業ではないことは明らかです。

このようなコーディングスタイルを、「宣言的」(declarative) なプログラミングと呼びます。1行ずつ処理を呼び出したり条件分岐するのではなく、「これを使う」「この値はこうである」という「宣言」であるように読めることからこのような名称がつけられています。1

注意したいのは、これも立派なプログラムである、ということです。

return Visibility();

は Visibility クラスのインスタンスを生成して返却するコードで、そこに引数(プロパティ)の visiblechild を渡すコードを追加すると

return Visibility(visible: _isVisible, child: Padding(),);

となり、ここに改行を入れると

return Visibility(
  visible: _isVisible,
  child: Padding(),
);

となります。さらに、Padding も同様に paddingchild といったプロパティを渡して改行を入れていくと、先ほどのコードのように

return Visibility(
  visible: _isVisible,
  child: const Padding(
    padding: const EdgeInsets.all(16),
    child: Text('Hello, Flutter!'),
  ),
);

というような「宣言的」なコードが完成する、というわけです。

言い換えると、 return するためのインスタンスを生成して、コンストラクタに渡す値もその場でインスタンス生成したもので、さらにそのコンストラクタに渡す値もその場でインスタンス生成したもので、ということを繰り返すと上記のようなコードになりますが、あくまでプログラムです。そのため、以下のような書き方も一応可能です。

final text = Text('Hello, Flutter!');

final padding = const Padding(
  padding: const EdgeInsets.all(16),
  child: text,
),

return Visibility(
  visible: _isVisible,
  child: padding,
);

しかし、 child の中身がコード的に上の行に遡らなければ読めなくなってしまったり、どの Widget とどの Widget がどう関連しているのかがぱっと見で読みづらくなってしまう問題があることから、基本的には最初のコードのようにネストを深くしていく書き方をするのが主流です。2

1画面分のプログラム

では、以上を踏まえて画面を1つ作ってみたいと思います。

main.dart
import 'package:flutter/material.dart';

void main() => runApp(LoginPage());

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('ログイン画面'),
        ),
        body: 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('ログインメールを送る'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

このプログラムを実行すると、↓ のような画面が表示されます。

image.png

ソースコードの全てを読み解く必要はありませんが、先述した通り、ネストが深くなっていること、「命令的」ではなく「宣言的」にコーディングされていること、またマテリアルな UI を構築するための MaterialApp やパディングを設定する Padding、縦に Widget を並べる Column といったものが全て Widget で表されてることに注目してみてください。

Widget を構築する Widget

先ほどのログインページを構築するためのコードは build() メソッドに書いていましたが、この build() メソッドを持つクラスもまた Widget です。

class LoginPage extends StatelessWidget {

Widget ですので、このクラス自体のインスタンスを先ほど出てきた Paddingchild プロパティに渡すようなこともできますし、 AppBartitle のような、通常 Text を渡すような場所にも渡すことができます。

LoginPage のような StatelessWidget (または StatefulWidget) のサブクラスが Text や Column などと違うのは、それが 「Widget を構築する Widget である」 という点です。公式ドキュメントにも次のように書かれています。

A stateless widget is a widget that describes part of the user interface by building a constellation of other widgets that describe the user interface more concretely.
訳: StatelessWidget は他のより具体的な Widget を集めて UI の一部を表現する Widget です。

https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html

「他のより具体的な Widget」とは、 Padding や Container といった子 Widget の配置方法を決める Widget や、 Text や Image のような画面に何かを描画するための Widget を指します。3 4 これらの Widget は RenderObjectWidget という、描画のための RenderObject を生成する役割を持ったクラスで、複数の Widget を組み立てる build() のようなメソッドは持っていません。

一方で StatelessWidget や StatefulWidget は、具体的な何かを描画しない代わりに、 build() メソッドを実装することで「どの Widget をどのように配置するか」を指定する役割を持っています。そしてそれは Text や Image と違ってアプリごと、画面ごとに様々ですので、われわれアプリ開発者はこの 「Widget を構築する Widget」 をコーディングしていく のが Flutter でアプリ開発する上での主な仕事のひとつになる、というわけです。

もちろん、ひとつの StatelessWidget / StatefulWidget にどれだけのまとまりで Widget を集めるかは設計次第です。 1, 2個の Widget を汎用的に使えるよう配置して再利用するものもあれば、 LoginPage のように 1 ページ丸々記述する場合もあります。

どれだけ長くなったらどのように分割するかは実際にコーディングしながら試行錯誤すると良いでしょう。その点では通常のコーディングでメソッドの分割具合を考える感覚と変わりありません。正解もありません。

まとめ

Flutter は "Everything is a Widget" のスローガンの通り、アプリ開発者は Widget の使い方さえ知っていればアプリの UI が作れてしまうように API がデザインされています。

しかし、その「Widget の使い方」を知るためには、まず「宣言的」に書くことや、 Widget を自作すること、 Widget にもその役割によって種類があること、などを押さえておくことが、 Flutter を「雰囲気で」書くのをやめるためには重要です。

それを理解するための第一歩として、この記事に書いたことが役に立てれば嬉しいです。

なお、 Flutter は 標準搭載されている Widget の種類がとても豊富 なことも特徴のひとつです。もしかしたら、何かの共通パーツとして Text や Container などを駆使して Widget を自作したら、もうすでに同じような(場合によってはよりクオリティの高い) Widget が公式から提供されている、ということも珍しくありません。

すでに用意されている Widget を効率的に使ってより良いアプリを楽に作れるようになることを目指して、 Widget of the WeekWidget Catalogue などを日常的に眺める習慣をつけてみるのも良いでしょう。5


宣伝

"Everything is a Widget" とは言うものの、それはアプリ開発者向けの外向きな方針であって、実際の仕組みはもう少し複雑です。Widget の他にも ElementRenderObject というものがあり、それぞれの Element が親子の参照を保持してツリー構造を形成することで Flutter の仕組みの大部分が実現されていたりと、中身に目を向ければさらに Flutter に詳しくなり、開発や学習の効率も向上します。

そのあたりに興味のある方はぜひ 「【Flutter】Navigator.of(context) から理解する 3つのツリー」 も読んでみてください。少し Flutter に慣れた方向けの記事ですが、じっくり読めば(参照先の公式サイトなども含めて)さらにたくさんの知見を得られるはずです。


  1. ちなみに Kotlin のような、1つ1つ処理を呼び出すスタイルは「命令的」(imperative) と呼ばれています。 

  2. 当然、限度はあります。あまりにネストが深くなる場合は、適宜メソッドや別クラスに切り出したりして読みやすさを調整します。 

  3. 厳密には、 Text は StatelessWidget、 Image は StatefulWidget で、これらの Widget も同じく「他のより具体的な Widget を集めて UI の一部を表現する Widget」です。 Text は build() の中で RichText という「具体的な Widget」を、 Image は同じく build() の中で RawImage という「具体的な Widget」をそれぞれ生成して返却しています。 

  4. Text について、詳しく知りたい場合は 【Flutter】Text とは何か も読んでみてください。 

  5. 僕も Zenn の方に 1 つひとつの Widget の仕組みに着目した記事をいくつか書いていますので、何かの参考に読んでみていただければ嬉しいです。 

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

パスワードを入力する時に使用するTextFieldについて

問題

アカウント作成画面を作成していた時に、パスワード入力のViewにTextFieldを使用していたのだが、よく見るパスワード入力は入力文字が見えないようになっている。
SwiftUIを使用している場合、このようなTextFieldはどうやって実装するか分からなかったので調べた。

解決方法

TextFieldにオプションをつけるのかと思っていたが違うらしい。
SecureFieldというViewを使用すれば良いみたい。

https://developer.apple.com/documentation/swiftui/securefield

スクリーンショット 2021-01-05 0.35.11.png

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

非同期処理中にユーザー操作を無効にする方法

問題

アカウント作成という非同期処理があるのだが、作成中にはユーザーの操作を無効化したい。
ただ、調べてもユーザー操作を無効にする方法はないっぽい。
そこで、ZStackを使用して、上にViewを重ねることで解決できないか検討した。

解決方法

以下のコードのように、isBusyがtrueの時にのみ、Color.whiteを重ねてみたところ、下のViewを操作できないことを確認できたので、これで良さそう。
もし、より良い方法があればコメント頂けると助かります・・・。

ZStack{
    VStack{
        TextField("ユーザー名", text: $vm.userName)
            .autocapitalization(.none)
            .textFieldStyle(RoundedBorderTextFieldStyle())
        TextField("メールアドレス", text: $vm.emailAddress)
            .autocapitalization(.none)
            .textFieldStyle(RoundedBorderTextFieldStyle())
        SecureField("パスワード", text: $vm.password)
            .autocapitalization(.none)
            .textFieldStyle(RoundedBorderTextFieldStyle())
        SecureField("パスワード確認", text: $vm.passwordConfirm)
            .autocapitalization(.none)
            .textFieldStyle(RoundedBorderTextFieldStyle())
        if vm.validationText != "" {
            Text(vm.validationText)
                .foregroundColor(.red)
                .font(.footnote)
        }
        Button(
            action: {
                vm.createAccount()
            }
        ){
            Text("アカウント作成")
                .padding(4)
                .frame(maxWidth: .infinity)
                .foregroundColor(Color.white)
                .background(Color.gray)
                .cornerRadius(8)            
        }
    }
    .padding(.horizontal)

    if vm.isBusy {
        Color.white
            .opacity(0.7)
            .edgesIgnoringSafeArea(.all)
            .overlay(
                ProgressView("アカウント作成中...")
                    .foregroundColor(.black)
            )
    }
}

ezgif.com-gif-maker (2).gif

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

TextFieldにてアルファベット入力時に先頭文字が大文字になってしまうことを防ぐ

問題

TextFieldを用いた画面を作成した際に入力時に先頭文字が自動的に大文字になってしまうことがある。
特に、Canvasを使用して画面の動作を確認する際に、キーボードからの入力だと、大文字を小文字に変換する術がなく、辛い。
ezgif.com-gif-maker.gif

解決方法

TextFieldのModifiersである、autocapitalization(_:)を使用して、大文字への自動変換を制御すれば良い。

https://developer.apple.com/documentation/swiftui/textfield/autocapitalization(_:)

TextField("ユーザー名", text: $vm.userName)
    .autocapitalization(.none)
TextField("メールアドレス", text: $vm.emailAddress)
    .autocapitalization(.none)

ezgif.com-gif-maker (1).gif

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