- 投稿日:2021-01-05T23:30:06+09:00
[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を使います。
FirstViewContorollerimport 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 } //ほかには、何かしらの方法で遷移するコード書く }NextViewControllerimport 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に表示される値を変更毎に更新することができます。
- 投稿日:2021-01-05T23:25:20+09:00
【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を入れるだけでスキーマを作ってくれます!楽チンですねまとめ
この記事で少しでも開発が捗ったら嬉しいです!
- 投稿日:2021-01-05T22:54:03+09:00
InheritedWidgetの目的と使い方【Flutter】
まえがき
InheritedWidget
の使い方の基本をまとめます!Flutterを勉強していてよくわからなくなるポイントの一つがこの
InheritedWidget
だと思います。筆者自身、これを理解するのにかなり時間がかかったので、「こういう説明があったらよかったな」と思う説明を書きます。
なお、以下では、
InheritedWidget
クラスを継承したクラスのことを単にInheritedWidget
と呼びますね。他のStatefulWidget
なども同様です。前提知識
FlutterアプリがWidgetのツリー構造でできていることを知っていて、
StatefulWidget
とsetState
は使えるものとします。
BuildContext
の知識も少しだけ要りますが、「Widgetツリー内での位置を表現するもの」程度の理解で十分です。それについてはこちらをお読みください。
記事内の例の実行方法
該当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)
です。この
n
はBuildContext
から実際に返るState
までの、Widgetツリーの深さを表します。つまりWidgetツリーが深くなると、それに比例して計算に時間がかかるようになり、アプリが重くなっていくのです。使用例
InheritedWidget
は、StatelessWidget
やStatefulWidget
と同様に、InheritedWidget
クラスをextends
したクラスを自分で作って使います。この例では
MessageData
というInheritedWidget
を作っています。
updateShouldNotify
については後ほど説明しますので今はスルーしてください。下記の
MainWidget()
を、MaterialApp
で覆ってrunApp
に渡します。just_access.dartimport '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
が取得され、表示されます。Widgetツリー内で
InheritedWidget
を上書きするWidgetツリー内の一部でだけ異なる
MessageData
を使いたい時は、もう一度MessageData
Widgetを配置すればいいです。Theme, MediaQuery, Directionalityなど多くのFlutter Widgetがこの性質を持っていますね。実はそこでも
InheritedWidget
が利用されています。次の例を見てください。
MessageData
は2箇所配置されていますが、より呼び出し元のBuildContext
に近い方からmessage
が取得されていることを確認してください。
MessageData
のプロパティmessage
はコンストラクタで外から受け取れるようにしました。override_message.dartimport '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); } }プロパティが変化した時に下層の
BuildContext
に更新を伝えてリビルドさせる
InheritedWidget
のインスタンスは、中身を変更できません。にもかかわらず更新を伝播するとは、一体どういうことなのでしょう?変更不可能なのに更新するとは??
筆者は以前、この部分で混乱していました。
答えはこうです。
InheritedWidget
のインスタンスを何度も作り直して置き換える状態を更新可能な
StatefulWidget
などと組み合わせて、Widgetツリー内にあるInheritedWidget
のインスタンスを作り直して再配置します。プロパティの異なる
InheritedWidget
を作って置き直せば、プロパティの変更が、それに依存するBuildContext
に伝播され、リビルドされるわけです。使用例
実際の例で見てみましょう。
StatefulWidget
を使って、1秒に1回、プロパティを1ずつ増やしたInheritedWidget
を作って再配置します。作るInheritedWidget
の名前はCountData
としています。
InheritedWidget
のchild
に渡すWidgetは、変更せず、同じインスタンスを毎回渡すことにします。update_count.dartimport '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'); } }
updateShouldNotify
で更新を間引きする
InheritedWidget
のよくわからないポイントの一つ、updateShouldNotify
です。ここまでの説明で、
InheritedWidget
のインスタンスの置き換えが行われる際に更新が伝播されるようなことを言いましたが、実は、実際に更新が伝播されるのは
updateShouldNotify
がtrue
を返した時だけです。インスタンスの置き換えの際、古い方のインスタンスも手元に手に入るので、比較して、更新通知するか制御可能です。
例えば先程の例で、更新を2回に1回にする(つまり、
count
を2で割った商が変化した時だけ更新する)場合、updateShouldNotify
は次のように書けます。@override bool updateShouldNotify(CountData old) { return count ~/ 2 != old.count ~/ 2; }他は先程の例と全く同じようにすると、次のような動きになります。
updateShouldNotify
の中身を、場合によって都合のいいように実装すればいいわけですね!慣例として、
of
を実装するここまで、動きの理解を重視して、BuildContext.dependOnInheritedWidgetOfExactTypeを直接呼んでいましたが、
Flutterの慣例として、このメソッドは直接呼ばず、
InheritedWidget
にof
というstaticメソッドを実装して、そこから呼ぶのが普通です。例
最初の
MessageData
の例ですと、このようになります。implement_of.dartimport '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
を、アプリの状態管理にガッツリ使うことを考えます。先程のタイマーを使った例では、更新が
StatefulWidget
のState
内部から自発的に発生していました。一方実際のアプリでは、Widgetツリー下層にある
RaisedButton
が押されたら、上層で保持している状態を更新する、という動きがよくあります。となると、Widgetツリーの下層から上層の状態を更新するメソッドを呼ぶ必要があるわけです。
下層から更新用メソッドにアクセスできるようにする方法は、例えば
- その更新用メソッドを、毎回Widgetのコンストラクタに渡して下層まで持っていく
という方法がありますが、これは中間層すべてのコンストラクタに記述が必要で、イマイチです。
せっかく
InheritedWidget
があるので、更新用メソッドにも下層からO(1)
でアクセスしましょう!今回は、公式のList of state management approachesからリンクされている
これらを参考に、一つ方法を紹介します。
InheritedWidget
でState
自体を下層に流せ!
StatefulWidget
を使う場合、プロパティも更新用メソッドもState
が持っていることが多いです。更新時にsetState
を呼ばないといけないので、更新用メソッドもState
が持たないといけない。ならば、
State
自体をInheritedWidget
で下層に流してしまえば、そのプロパティにアクセスするのも更新用メソッドにアクセスするのも簡単ですね!その方法を簡単にまとめるとこうです。
StatefulWidget
とInheritedWidget
のペアで使うInheritedWidget
はprivate
にするState
をpublic
にするInheritedWidget
はdata
というプロパティを持ち、State
を格納するof
メソッドはStatefulWidget
が持ち、State
(InheritedWidget
のdata
プロパティ)を返すStatefulWidget
はchild
を受け取り、State
のbuild
でInheritedWidget
の直下に入れて返す- メソッドだけ欲しい場合は
BuildContext.getElementForInheritedWidgetOfExactType
を使うと、更新伝播の対象にならない実例でコメントと一緒に見るのが早いと思いますので、次をご覧ください。
使用例
ボタンが押された回数をテキストで表示するだけのアプリを考えます。
ただし、状態管理WidgetはWidgetツリーの上層にあり、ボタンとテキストはツリーの違う枝に置きます。
今回は状態更新用WidgetとアプリUI用Widgetでファイルを分けます。
count
を管理するStatefulWidget
+InheritedWidget
のセットまず、状態管理用のWidgetを用意します。
count_manager.dartimport '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, ); } }
BuildContext
のgetElementForInheritedWidgetOfExactTypeは、InheritedWidget
の中身にアクセスしつつも、BuildContext
を状態更新時のリビルド対象として登録しません。状態更新用メソッドだけを取得して、リビルド対象にしたくない場合、こちらを使います。
(
BuildContext
のgetElementForInheritedWidgetOfExactTypeの返り値はInheritedWidget
ではなくInheritedElementなので、返り値の処理が若干変わってます)UI構築
update_method.dartimport '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'); } }Providerパッケージについて
先程の例の
count_manager.dart
は50行近くあるのですが、count
プロパティとincrement
メソッド以外は、毎回ほぼ同じ内容を書くことになります。そこで、それをパッケージ化すると便利そうだなと思うわけなのですが、
そうして出来上がったパッケージがProviderパッケージです。
(実際には、他にもいろいろな機能が追加されているので単純にそうとは言えませんが)
ということで、基本的には
Provider
パッケージを使えばいいと思います。同じ作者によってさらなる改良がなされたRiverpodもありますね。
もちろん、他にもお好みの状態管理手法を使ってもいいです。
この記事で紹介した方法を使ってもいいと思います。
この記事で紹介した方法は、Flutter以外のパッケージに一切依存しないところがメリットですね。
おわり
おわりです!これで
InheritedWidget
を理解してもらえたら嬉しいです!
- 投稿日:2021-01-05T18:29:29+09:00
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") }こんな感じでした。
- 投稿日:2021-01-05T13:50:28+09:00
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
が得られます。
- 投稿日:2021-01-05T07:09:14+09:00
[Swift5]特定の時間を比較して前なのか後なのかを判定する方法
特定の時間の比較
例えばアプリを起動したときに、現在時刻と開発者側で予め設定している時刻を比較して、その状況にあった処理をおこないたい場合があったとします。(何時より前なら処理A, 後ならBなど)
そういった状況で使えるのが
.compare( )メソッド
です。
今回はこちらのメソッドの紹介をします!記述方法
if '現在時刻'.compare('こちらで設定した時刻') == .orderedDescending { //ここで処理 //.orderedDescendingは〜より後 } if '現在時刻'.compare('こちらで設定した時刻') == .orderedAscending { //ここで処理 //.orderedAscendingは〜より前 }このように記述すればOK!
ふたつの時刻を.compare( )メソッド
で比較して、以降なのか以前なのかを判定しております。
また、双方の時刻が同じかどうかを判定したい場合は.orderedSame
を使えばいい。参考にしてください!
- 投稿日:2021-01-05T01:50:41+09:00
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 のコードは、
- もし
isVisible
がtrue
であればTextView
インスタンスを生成してtext
フィールドに文字列を代入してsetPadding()
でパディングを設定して- 親の View に追加する
という、手順を踏まえて1行1行処理を進めていく形で UI を構築します。とても「プログラミング的」と言えると思います。
一方で Flutter は、
Visibility
を配置する- 表示 / 非表示の判定には
_isVisible
の値を使用する- Visibility の
child
はPadding
である- Padding の値は
padding
プロパティにセットした通りである- Padding の
child
はText
であるText
が表示する文字列は "Hello, Flutter!" であると、まるで設定ファイルを記述するようにコーディングします。少なくとも、1行1行でメソッドを呼び出したりフィールドに値をセットしたり、といった作業ではないことは明らかです。
このようなコーディングスタイルを、「宣言的」(declarative) なプログラミングと呼びます。1行ずつ処理を呼び出したり条件分岐するのではなく、「これを使う」「この値はこうである」という「宣言」であるように読めることからこのような名称がつけられています。1
注意したいのは、これも立派なプログラムである、ということです。
return Visibility();は Visibility クラスのインスタンスを生成して返却するコードで、そこに引数(プロパティ)の
visible
とchild
を渡すコードを追加するとreturn Visibility(visible: _isVisible, child: Padding(),);となり、ここに改行を入れると
return Visibility( visible: _isVisible, child: Padding(), );となります。さらに、Padding も同様に
padding
やchild
といったプロパティを渡して改行を入れていくと、先ほどのコードのように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 がどう関連しているのかがぱっと見で読みづらくなってしまう問題があることから、基本的には最初のコードのようにネストを深くしていく書き方をするのが主流です。21画面分のプログラム
では、以上を踏まえて画面を1つ作ってみたいと思います。
main.dartimport '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('ログインメールを送る'), ), ], ), ), ), ); } }このプログラムを実行すると、↓ のような画面が表示されます。
ソースコードの全てを読み解く必要はありませんが、先述した通り、ネストが深くなっていること、「命令的」ではなく「宣言的」にコーディングされていること、またマテリアルな UI を構築するための
MaterialApp
やパディングを設定するPadding
、縦に Widget を並べるColumn
といったものが全て Widget で表されてることに注目してみてください。Widget を構築する Widget
先ほどのログインページを構築するためのコードは
build()
メソッドに書いていましたが、このbuild()
メソッドを持つクラスもまた Widget です。class LoginPage extends StatelessWidget {Widget ですので、このクラス自体のインスタンスを先ほど出てきた
Padding
のchild
プロパティに渡すようなこともできますし、AppBar
のtitle
のような、通常 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 Week や Widget Catalogue などを日常的に眺める習慣をつけてみるのも良いでしょう。5
宣伝
"Everything is a Widget" とは言うものの、それはアプリ開発者向けの外向きな方針であって、実際の仕組みはもう少し複雑です。Widget の他にも Element や RenderObject というものがあり、それぞれの Element が親子の参照を保持してツリー構造を形成することで Flutter の仕組みの大部分が実現されていたりと、中身に目を向ければさらに Flutter に詳しくなり、開発や学習の効率も向上します。
そのあたりに興味のある方はぜひ 「【Flutter】Navigator.of(context) から理解する 3つのツリー」 も読んでみてください。少し Flutter に慣れた方向けの記事ですが、じっくり読めば(参照先の公式サイトなども含めて)さらにたくさんの知見を得られるはずです。
ちなみに Kotlin のような、1つ1つ処理を呼び出すスタイルは「命令的」(imperative) と呼ばれています。 ↩
当然、限度はあります。あまりにネストが深くなる場合は、適宜メソッドや別クラスに切り出したりして読みやすさを調整します。 ↩
厳密には、 Text は StatelessWidget、 Image は StatefulWidget で、これらの Widget も同じく「他のより具体的な Widget を集めて UI の一部を表現する Widget」です。 Text は
build()
の中でRichText
という「具体的な Widget」を、 Image は同じくbuild()
の中でRawImage
という「具体的な Widget」をそれぞれ生成して返却しています。 ↩Text について、詳しく知りたい場合は 【Flutter】Text とは何か も読んでみてください。 ↩
僕も Zenn の方に 1 つひとつの Widget の仕組みに着目した記事をいくつか書いていますので、何かの参考に読んでみていただければ嬉しいです。 ↩
- 投稿日:2021-01-05T00:35:50+09:00
パスワードを入力する時に使用するTextFieldについて
問題
アカウント作成画面を作成していた時に、パスワード入力のViewにTextFieldを使用していたのだが、よく見るパスワード入力は入力文字が見えないようになっている。
SwiftUIを使用している場合、このようなTextFieldはどうやって実装するか分からなかったので調べた。解決方法
TextFieldにオプションをつけるのかと思っていたが違うらしい。
SecureFieldというViewを使用すれば良いみたい。https://developer.apple.com/documentation/swiftui/securefield
- 投稿日:2021-01-05T00:27:35+09:00
非同期処理中にユーザー操作を無効にする方法
問題
アカウント作成という非同期処理があるのだが、作成中にはユーザーの操作を無効化したい。
ただ、調べてもユーザー操作を無効にする方法はないっぽい。
そこで、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) ) } }
- 投稿日:2021-01-05T00:05:39+09:00
TextFieldにてアルファベット入力時に先頭文字が大文字になってしまうことを防ぐ
問題
TextFieldを用いた画面を作成した際に入力時に先頭文字が自動的に大文字になってしまうことがある。
特に、Canvasを使用して画面の動作を確認する際に、キーボードからの入力だと、大文字を小文字に変換する術がなく、辛い。
解決方法
TextFieldのModifiersである、autocapitalization(_:)を使用して、大文字への自動変換を制御すれば良い。
https://developer.apple.com/documentation/swiftui/textfield/autocapitalization(_:)
TextField("ユーザー名", text: $vm.userName) .autocapitalization(.none) TextField("メールアドレス", text: $vm.emailAddress) .autocapitalization(.none)