- 投稿日:2020-12-01T23:42:47+09:00
iOSの証明書周りのお話
はじめまして、Link-UでiOSエンジニアをしている野田佑樹です。
本日はiOSアプリ開発における証明書周りのお話を少ししたいと思います。概要
私は2020年度にLink-Uに入社しまして、iOSについての諸々は入社後から勉強を始めました。
たくさんある中でなかなか中身について理解するのが難しかったのが証明書の話です。なので今回は
証明書周りのお話
について話したいと思います。具体的にこのファイルがあれば開発ができる。というのはわかるのですが、結局そのファイルって何?みたいなことがもう全然わかりませんでした。
これを機に勉強し、証明書周りで焦ることがないように努めていきます。
あと完全に備忘録目的
多分知っている人からすると「何言ってんだこいつ。そんなの常識だろ」と言われそうですが、生暖かい目で見守ってくれれば幸いです。
それではさっそく行きましょうiOSの開発フローと証明書について
iOS開発でのフローを大雑把ですが以下に示します。
- とりあえず開発する
- 随所随所で企画側にテストを送って、バグをつぶしていく
- App storeにアプリを申請して審査に通れば晴れてリリース
という流れでアプリがリリースされることになるのですが、これらのフロー全てで証明書類がないと詰むようにできています。
たとえば
- 開発証明書およびプロビジョニングプロファイルに開発用の端末を登録しないと端末にアプリをビルドできない(ただしシミュレータは除く)
- testflight等でテストする際にも証明書およびプロビジョニングファイルがないとテスト配信ができない(ただしその他のサービスだとこの限りではないかもしれない)
- もちろん証明書がないと配信(アプリのリリース)ができない
こんなかんじです。
証明書の説明
ここからは前章で出てきた証明書類について詳しく見ていきます。
ちなみに今回登場する証明書類が大雑把に
- 開発証明書(cerファイル)
- 開発証明書(p12ファイル)
- プロビジョニングファイル
上記の3つです。これらが正しく対応されたものを使用しないとビルドができなくなります。
あとは、iOSアプリでよくみる上から出てくる通知。その通知を送るときに必要な通知証明書等もありますが、それはとりあえずここでは置いておきます。
開発証明書(.cer)
開発証明書と呼ばれる証明書は2種類あります。
体感ですがこれがややこしい一因な気もします。で、このcerファイルですが
こいつの中身は電子署名された公開鍵です通常、アプリ開発をする際にまず最初に開発証明書を作成するための要求書(CSRファイル)を作成します。CSRファイルを生成する途中で端末側では秘密鍵と公開鍵が生成されます。CSRファイル内にはその公開鍵が付いており、AppleはそのCSRファイルに署名をしてそれを開発証明書としています。
これらはPCをローカルで特定するための証明書です。
中身の公開鍵を見ることで、秘密鍵を持っている人(CSRファイルを送った人)の公開鍵だとわかります。電子署名に関してはここでは説明をしません。
電子署名をすることで何がわかるのかというと、「開発証明書はAppleから確かに送られた証明書で、改竄もされてない」ことがわかります
気になる方は調べてみるといいかもしれません。
ちなみに、私は学生時代もっとセキュリティについて勉強しておけばよかったと今になって切に思います。開発証明書(.p12)
上記と一緒で証明書とか呼ばれています。
ややこしいこちらの中身は開発証明書(.cer)+秘密鍵です。
上述のcerファイルはCSRを作成したPCでしか認識しません。しかしチームで開発を行っていると不便です。Aさんが書いたコードはAさんのPCでしか実行できないことになります(上図の公開鍵Aと秘密鍵Aがペアになっているため)。またPCが変わればまたCSRの件からやり直さなければいけません。それでは非常に面倒です。
なので、必要な証明書と秘密鍵をセットにしたものを配布することにしました。
それがp12ファイルです。
このファイルによってチームで開発等をする際にいちいちCSRファイルから作るなんてことをしなくて済みます。
雑な絵で(ryプロビジョニングプロファイル
こちらは
- 開発証明書
- AppID
- UDID(実行可能端末)
をまとめたものです。
これらは、
- 実行する端末がプロビジョニングプロファイル内のUDIDに登録されており、
- Xcodeで設定しているBundleIDとAppID内のBundleIDが一致しており、
- キーチェーンに保存してある秘密鍵とプロビジョニングプロファイル内の開発証明書
が一致している(ペアになっている)場合のみ端末でビルドできます。
AppID
AppIDはアプリを特定する任意のIDです。
BundleIDとチームIDとかがくっついたものでアプリごとに違うIDが割り振られており、かぶることはありませんUDID
Apple側から端末に対して一意に割り振られているIDのこと。
プロビジョニングプロファイル内のUDIDリストに実行したい端末のUDIDを登録しておきます。このプロビジョニングプロファイルを雑に説明をするなら
アプリがきちんとしたところのきちんとした人によってビルドされてます
といった証明になるプロファイルです。最後にバイナリとプロビジョニングプロファイルをまとめた物を秘密鍵で署名をしてアプリは晴れて世に出ます。
まとめ
ざっくりとしたおさらいです。
- 開発証明書(.cer)はAppleによって電子署名された公開鍵
- 開発証明書(.p12)は開発証明書(.cer)と対応する秘密鍵のセットになったもの
- プロビジョニングファイルは開発証明書(.cer)とAppIDとUDIDがセットになったもの
なぜこんな
面倒くさいことをするのかというと一言で言って変なアプリを端末に入れさせないためだと思っています。
つまりこの証明書によってユーザーは信頼できる開発元から出ているアプリのみを入れられることになります。リリース前になると証明書類で慌てそうなので今回学んだことを思い出して慌てないようにしていきたいですね。
最後まで読んでいただきありがとうございました。
- 投稿日:2020-12-01T23:34:27+09:00
Unity as a LibraryによるUIネイティブ化
こんにちは、mizogucheです。2015年9月にクラスターに入ったのでもう6年目です。
この記事はクラスター Advent Calendar 2020 - Qiitaの2日目の記事です。
Unity UIでの課題
UnityでUIを実装すると普段使っているネイティブアプリの操作性と違った触り心地になります。
ネイティブアプリで簡単に実装できるUIの操作性をUnityで再現するコストはかなり高く、たとえばiOSにおける戻るジェスチャ、Androidにおけるバックボタンなど、OS固有の操作はネイティブUIだと何も実装しなくても実現できる1一方で、Unityではそれぞれのプラットフォームごとに独自に実装していく必要があります。
感覚的なものでいうと、スクロールビューのスクロールの感触なんかもUnity、iOS、Androidでそれぞれ違うので、Unity製のアプリだと他のネイティブアプリと比較して触り心地がよくないのが正直なところです。
Unityのランタイムの起動に時間がかかる分、アプリの起動時間が遅くなってしまうというのも大きな問題でした。
Unity as a Library
Unityでビルドしたアプリを起動したらすぐにUnityのランタイムが実行されますが、Unity as a Library(= UaaL)という技術で、アプリの一部分だけでUnityを利用することができます。
これによりUnityでの再現が難しいネイティブアプリのUIの操作感を実現しつつ、必要なところでUnityの表現力を発揮することができます。
メンテナンスコストの増加
すべてがUnity製のアプリであれば1つのコードからiOS/Android両プラットフォームのアプリをビルドすることができます。
しかし、UaaLを使うことでiOS/Android両プラットフォームのネイティブUI部分を実装する必要が生まれます。
つまり、UaaLの採用によって実質的に2つのアプリを開発・メンテナンスする必要が生まれます。
clusterではUaaLをどう利用したか
ネイティブUIのoutroom/Unityのinroom
ワールド・イベントに入るまでの世界をoutroom、ワールド・イベントに入った後のUnityの世界をinroomと呼んでいます2。
モバイルアプリ版clusterで、outroomをネイティブ、inroomでUnityを使うようアップデートしたものが11月にリリースした v1.85です。
これにより、Unity UIでの課題を解決してネイティブの触り心地を実現つつ、ワールド・イベントのクリエイティブな領域ではUnityの表現力を使うことができるようになりました。
この動画をご覧いただくと、普通にネイティブUIで実装し直すだけでどれだけ改善されたかがおわかりいただけるかと思います。
今後
outroomをネイティブUIにして体験を改善することができました。
しかしまだまだ改善することは山積みなため、クラスターではiOS/Androidエンジニアを募集しています。
というわけで2日目の記事は以上です。
明日は__0xyさんがなんかかくそうです。どんなことが書かれるんやろ…… ? お楽しみに!!
クラスター Advent Calendar 2020 - Qiita
参考リンク
- 投稿日:2020-12-01T23:20:27+09:00
TODOアプリで比較するAndroid(MVVM)とFlutter(Provider)の違い
最近Flutterを初めて、Android(Kotlin)の書き方よりもずいぶん素早く開発できることに驚いたので、TODOアプリを通して比較してみました。
筆者はJavaもKotlinもFlutterも実務経験はなく、半年程度の経験しかありません。
なので対象読者としては今からスマホのアプリを開発してみたいけど、ネイティブ(Java/Kotlin,Objective-C/Swift)を勉強するかFlutterのようなクロスプラットフォーム開発を勉強しているか悩んでいる人を想定しています。
先にこの記事の主張を述べると、要するに以下のような理由で、FlutterのProviderパターン最高って話です。
- iOS/Android同時に開発できて嬉しい
- Kotlinのdata bindingのようなものがnotifyListener()だけでできる。直感的
- 特にリストのバインドが面倒(アダプターを作る必要がある)
作ったアプリはこんなものです。
Kotlinバージョン
GitHubリポジトリ: https://github.com/tokku5552/TODOAppSample-KotlinFlutterバージョン
GitHubリポジトリ: https://github.com/tokku5552/TODOAppSample-Flutterアーキテクチャ概要
5分くらいで書いたアーキテクチャメモをもとにざっくり説明します。
Kotlin - MVVM
MainActivityでは画面遷移のみを行い、2つのフラグメントでTodoリスト一覧と、詳細画面を表示を行っています。
それぞれのフラグメントはMainActivityViewModelを参照していて、画面遷移が伴うアクションの時はMainActivityViewModelを通して画面遷移します。一覧のTodoItemをクリックすると、MainActivityのclickedItemが呼ばれて詳細画面に各値を入れた状態で表示させます。
もう少し詳しくこちらのブログで解説しています。https://tokku-engineer.tech/todomvvmkotlin/
バージョン
- Realm 2.1.1
Flutter - Provider
main.dartで一覧を表示し、TodoItemDetailPageで詳細画面を表示しています。
本当はRealmを使いたかったのですが、FlutterではRealmのいい感じのパッケージが見つからなかったので、
しょうがなくSQLiteを使っています。
基本的に非同期で描画しておいて、MainModel(ViewModelにあたる)で値をとってきたり更新したりした後に更新をnotifyListenerで通知します。
こちらでもう少し詳しく解説しています。
https://tokku-engineer.tech/todoapp-provider-flutter/パッケージのバージョン
- provider: 4.3.2+2
- sqflite: 1.3.2+1
- shared_preferences: 0.5.12+4
それでは私がなぜFlutterのProviderパターンを推してるかを以下で説明します。
iOS/Android同時に開発できて嬉しい
いきなりそもそもの話になりますが、Flutterはクロスプラットフォーム開発のためのフレームワークなので、1つコードを書くだけで、AndroidとiOSのアプリを同時に作ることが出来ます。
Androidだと、画面を作る際にレイアウト用のxmlと、View(Activity or Fragment)を用意する必要がありますが、Flutterでは1つのdartファイルを書けばよいので非常に効率的です。
また、ホットリロード出来る点も、すぐに結果が確認できてスピードが上がった気がします。KotlinのDataBindingのようなものがnotifyListener()だけでできる
Androidではdata bindingというjetpack1ライブラリを使って、ViewとViewModelの依存関係の方向を整理しています。
例えば詳細画面のコードを比べてみます。TodoItemDetailFragment.kt//詳細画面の表示 private fun showTask(todoItem: TodoItem) { binding.editTitle.setText(todoItem.title, TextView.BufferType.EDITABLE) binding.editDetail.setText(todoItem.detail, TextView.BufferType.EDITABLE) binding.editCreate.text = todoItem.createDate.toString("yyyy/MM/dd") binding.createDate.isVisible = true binding.editCreate.isVisible = true if (todoItem.createDate != todoItem.updateDate) { binding.editUpdate.text = todoItem.updateDate.toString("yyyy/MM/dd") binding.editUpdate.isVisible = true binding.update.isVisible = true } binding.buttonLeft.text = "更新" binding.buttonLeft.setOnClickListener { todoItemDetailFragmentViewModel.updateTask( todoItem.id, binding.editTitle.text.toString(), binding.editDetail.text.toString() ) closeFragment() } }Kotlinの場合です。
上記のようにそれぞれのTextViewにbindしているのに加えて、
xmlファイルで以下のようにdatabindingのためのmodelの指定を記載する必要があります。todo_item_detail_fragment.xml<data> <import type="android.view.View" /> <import type="androidx.core.content.ContextCompat" /> <variable name="viewmodel" type="tech.tokku_engineer.todoappsample_kotlin.viewmodels.TodoItemDetailFragmentViewModel" /> </data>Flutterの場合はこんな感じになります。
todo_item_detail_page.dartbody: Consumer<TodoItemDetailModel>(builder: (context, model, child) { model.todoTitle = todoItem?.title; model.todoBody = todoItem?.body; return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( decoration: InputDecoration( labelText: "タイトル", hintText: "やること", ), onChanged: (title) { model.todoTitle = title; }, controller: titleEditingController, ), SizedBox( height: 16, ), TextField( decoration: InputDecoration( labelText: "詳細", hintText: "やることの詳細", ), onChanged: (body) { model.todoBody = body; }, controller: detailEditingController, ), SizedBox( height: 16, ), RaisedButton( child: Text(isUpdate ? "更新する" : "追加する"), onPressed: () async { try { isUpdate ? await model.update(todoItem.id) : await model.add(); Navigator.pop(context); } catch (e) { //なんかエラー処理 ) ], ); }, ); } }, ) ], ), ); }),私は個人的に、この括弧をいくつも改行して表示するスタイルはあまり好きではないですが、ほとんどがレイアウト用のコードになっていて、データを直接modelのフィールドに代入したり取得したりできます。
変更を伝えるには、model側でnotifyListenerするだけです。todo_item_detail_model.dartFuture<void> update(int id) async { await TodoItemRepository.updateTodoItem( id: id, title: (todoTitle.isEmpty) ? todoTitle = "" : todoTitle, body: (todoBody.isEmpty) ? "" : todoBody, ); notifyListeners(); }上記の例はDBで更新をかけている例ですが、フィールドに値を代入した後、notifyListeners();とするだけでUI側に更新が通知されます。
非同期処理もasync:awaitで書けて、私にとってはわかりやすかったです。(KotlinもCoroutine使うとずいぶん直感的に書けますが・・・)特にリストのバインドが面倒(アダプターを作る必要がある)
特に分かりやすさに差が出たのがリストの表示です。Kotlinの場合ListViewAdapterというクラスを作成して、ViewHolderを定義して、onCreateViewHolderをとonBindViewHolderをそれぞれオーバライドして生成⇒bindしてあげて、、、
date bindingしてる場合はexecutePendingBindings()とかを呼ばないとうまくバインドされなくて・・・
と、結構初学者殺しな感じがします。ですが、Flutterだと、listをmapするだけ、、、なんと直感的
main.dartbody: Consumer<MainModel>(builder: (context, model, child) { final todoList = model.list; return ListView( children: todoList ?.map( (todo) => ListTile( leading: Checkbox( value: todo.isDone, onChanged: (bool value) { //なんか処理 }, ), title: //いい感じの文字 ), onTap: () { pushWithReload(context, model, todoItem: todo); }, ), ) ?.toList() ?? [ ListTile( title: Text(""), ) ], ); }),リストがnullだった時の処理を三項演算で書いてしまっているので少しわかりずらいですが、listをmodelに入れて、UI側は単にmapでしこしこウィジェットを表示していくだけで良いです。
もちろん状態が変わってもmodel側でnotifyListenersしてあげれば即座に更新されます。まとめ
おそらく私の経験や知識が不足していることもあり、いろいろなことが考慮できていなかったり設計が甘かったりするのだと思いますが、それでもFlutterはかなり直感的に書けるという印象でした。
AndroidにしてもFlutterにしても(おそらくiOSも)このようなパターンが生まれるまでにGoogleやAppleの優秀な開発者たちの紆余曲折があったんだと思います。
ちなみに筆者はまだKotlinなんて登場していなくて、Eclipseを使って開発することが主流だった時代に一度Androidアプリの開発にチャレンジしたことがありますが、エミュレータはまともに動かないし、Javaで毎回findViewByIdしてidをとりまくらないといけないしで途中で挫折してしまいました・・・これからもどんどん書き方が変わっていくだろうと思いますので、素早く優れたパターンを設計に取り入れられるかが、スマホアプリの開発において重要だと感じました。
[参考]
- データ バインディング ライブラリ:(https://developer.android.com/topic/libraries/data-binding?hl=ja)
- DataBindingを使っていてexecutePendingBindingsを呼び出さないとどうなるか:(https://android.gcreate.jp/358/)
- 64. FlutterのProviderパターンを3分で理解する:(https://tamappe.com/2020/06/09/2020-06-09-200000/)
Android Jetpack(https://developer.android.com/jetpack?hl=ja) ↩
- 投稿日:2020-12-01T21:39:22+09:00
XCUITest の最初のテスト
dwango2 Advent Calendar の記事です。四日目は iOS の話です。
こんにちは、 @daichiro です。
ドワンゴには iOS 四天王というのがおり[要出典]、私もその一人でしたが、四天王と揶揄されるのが恥ずかしいので今はその地位を譲りました。
若いエンジニアでも活躍できる、そして四天王が作られるほど iOS エンジニアがいる良い環境であると言えます。さて、みなさん UITest は書いているでしょうか。
アプリの振る舞いをプログラムで制御し、画面の要素や画面繊維が期待した通りになっているかを調べることができます。
利用されるシーンとしては、アプリのバージョンアップ前に既存機能が壊れていないか調べる リグレッションテスト が多いかと思います。リグレッションテストは多くのプロジェクトで行われていると信じていますが、それが自動化されているかというと、数えるほどになってしまうという感覚です。
今回は簡単なサンプルアプリに対して UITest をサクッと書くという記事です。
皆さんのプロジェクトの導入のきっかけになれば幸いです。導入
新しく Xcode Project を作成する際は、 Test を作るチェックマークを入れておけば自動的に作られます。
既存のプロジェクトに追加する場合はいくつか手順が必要です。
これは Qiita に様々な記事があるので参照されると良いと思います。ここまでできたと仮定して、テストを書いてみましょう。
最初のテスト
UITest で最も初歩的なのは、表示を確認するものだと思います。
- 画面の要素を確認する
- 画面遷移を確認する
ログインしているかどうか、プレミアム会員かどうか、というお客様の状態によって画面の要素が変化するというのはよくあるので、画面にある要素の確認のテストは必要ですね。
しかし画面の要素に対して何か操作を行うものの方が UITest をやっている感があるでしょうか。
ということで、画面遷移のテストを書いてみましょう。コード
import SwiftUI struct ContentView: View { @State private var toDetailView = false var body: some View { NavigationView { VStack { Button("Go to detail") { toDetailView = true } NavigationLink( destination: DetailView(), isActive: $toDetailView, label: { EmptyView() }) } .navigationBarTitle("Content") } } } struct DetailView: View { var body: some View { VStack { Text("This is detail view") } .navigationBarTitle("Detail") } }
ContentView
の中心に存在するGo to detail
と書かれたボタンをタップすると、DetailView
に遷移するはずです。テスト
import XCTest final class ContentViewUITests: XCTestCase { func testMoveToDetailView() { let app = XCUIApplication() XCTContext.runActivity(named: "Launch app") { _ in app.launch() } XCTContext.runActivity(named: "Check content view") { _ in XCTAssertTrue(app.navigationBars["Content"].exists) } XCTContext.runActivity(named: "Tap button and show detail view") { _ in app.buttons["Go to detail"].firstMatch.tap() XCTAssertTrue(app.navigationBars["Detail"].exists) XCTAssertTrue(app.staticTexts["This is detail view"].exists) } } }これを実行すると、シミュレータが起動し、テストが実行されます。
アプリが起動して画面遷移を一瞬のうちに行い、テストは成功です。
XCUIApplication
let app = XCUIApplication() app.launch()XCUITest においては
XCUIApplication
を中心としてコトが進んでいきます。
これは起動するアプリそのものと言っていいでしょう。
navigationBars
buttons
staticTexts
images
などのプロパティが生えていますが、これは 現在表示されているスクリーン に対してアクセスできるものが取得できます。
これらはXCUIElementQuery
という型になっていて、Label や Identifier を使って特定の要素にアクセスできます。例えば、Content View が開かれている時には
app.navigationBars["Content"]
は一件ヒットします。
逆に言えば、Detail View が開かれている時にはヒットしないので、現在の画面が何であるか、要素が存在するかということのチェックに使えるというわけです。より詳しく知りたい場合は公式ドキュメントを探ってみてください。
変更に強くする
さて、先ほどのテストで今はうまくいっていますが、そのうち動かなくなることは明白です。
なぜなら、要素に直にアクセスしているからです。app.buttons["Go to detail"].firstMatch.tap()この文を例にとると、
Go to detail
という文字列を直に検索しているので、ここが何かの変更によって書き換えられるとたちまちテストは通らなくなります。
文字列自体に意味があれば別ですが、ここでは Detail View に遷移するボタンさえあれば、ボタンの様子は気にしません。
また、他にGo to detail
と書かれたボタンが置かれたら想定しないボタンをタップしてしまうこともあります。ではどうすれば良いかというと、 AccessibilityIdentifier を使います。
AccessibilityIdentifier
アクセシビリティ?多言語対応?ボイスオーバー?などと思うかもしれませんが、別物です。
ユーザーに見えるaccessibilityLabel
とは違って、こちらは開発者向けのプロパティです。(多分)これを各要素に設定してあげることにより、UITest はその要素の見た目に関わらず同じ振る舞いをすることができます。
ではやってみましょう。コード
struct ContentView: View { @State private var toDetailView = false var body: some View { NavigationView { VStack { Button("Go to detail") { toDetailView = true } .accessibility(identifier: "cloud.mokumoku.uitestsample.contentview.button") NavigationLink( destination: DetailView(), isActive: $toDetailView, label: { EmptyView() }) } .navigationBarTitle("Content") } } } struct DetailView: View { var body: some View { VStack { Text("This is detail view") .accessibility(identifier: "cloud.mokumoku.uitestsample.detail.text") } .navigationBarTitle("Detail") } }テスト
final class ContentViewUITests: XCTestCase { func testMoveToDetailView() { let app = XCUIApplication() XCTContext.runActivity(named: "Launch app") { _ in app.launch() } XCTContext.runActivity(named: "Check content view") { _ in XCTAssertTrue(app.navigationBars["Content"].exists) } XCTContext.runActivity(named: "Tap button and show detail view") { _ in app.buttons["cloud.mokumoku.uitestsample.contentview.button"].tap() XCTAssertTrue(app.navigationBars["Detail"].exists) XCTAssertTrue(app.staticTexts["cloud.mokumoku.uitestsample.detail.text"].exists) } } }SwiftUI はわかりやすいですね。
このようにして AccessibilityIdentifier を設定してあげると文言修正や場所変更に捉われず安定したテストを作ることができます。
そうは言っても
いきなり Identifier を設定するなんて、どこからやったらいいかわからないし、既存のコードがそういう作りになってなくてやりづらいんですけど…
というのが現実かと思われます。
大丈夫です。 Identifier はなくてもいいので、まずは書くこと、カバレッジを 1% でも上げることが大事だと思います。
by 半年 UITest を書いている人より
次回予告
実際のアプリはもっと要素が多くて複雑です。
このままだと一つ一つのテストが長くなり、理解に時間がかかります。
次回はもうちょっと踏み込んで体系的に UITest を書けるようにしていきましょう。明日は @daichiro です。
- 投稿日:2020-12-01T21:35:11+09:00
FigmaのデザインをSwiftUIのコードに変換してくれる無料プラグインがあるらしい???
GitHubで便利そうなライブラリを探していたらこんなものを見つけました。
FigmaToCode
Figmaのデザインを
Flutter
、tailwidcss
、SwiftUI
にコンバートしてくれるらしい。
FigmaはUIKitしかサポートしてくれてないので、使い物になればかなり有り難い...試しに使ってみた
今回はこちらのデザインをコンバートしてみようと思います。
ニューモーフィズムのサンプルデザインです。コンバート結果
import SwiftUI struct ContentView: View { var body: some View { VStack(spacing: 43) { Text("Neumorphism UI Kit") .fontWeight(.medium) .font(.largeTitle) .padding(.vertical, 49) .padding(.leading, 116) .padding(.trailing, 117) .frame(width: 693, height: 155) .foregroundColor(Color.init("text")) .background(Color.init("background")) .cornerRadius(98) .shadow(radius: 30) Text("Button / Switch / Progress / Pagination/ Selector ... and more") .font(.title) } .background(Color.init("background")) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }使ってみて
結構、再現度の高いものに仕上がっているのではないでしょうか?
AutoLayoutを再現するにはこのようなポイントに注意しなければいけないようです。
個人的には画面全体ではなく各パーツを1つずつ変換していく形になると思うので、あまり関係ないかなと。
絶賛開発中のプラグインなので期待していいのでは?SwiftUIのアプリを来年から新たに開発予定なので、そのタイミングでもっと使ってみようかなって思います!
- 投稿日:2020-12-01T19:51:05+09:00
開発者ごとでなく、プロジェクトごとにFlutterのバージョンを管理する
Flutterのアップデート、まだまだ激しいですよね。
以前チーム開発していた時も、
- 他の開発者とのFlutterのバージョン違いによりAPI定義が異なりエラーの嵐→無駄なコミュニケーションの発生
- 自分のPCのFlutterのバージョンを上げてしまったところ、ビルドできなくなったといった報告の対応
など、プロジェクトごとでなく開発者ごとにFlutterのバージョンを管理しているために起きてしまう問題がいくつかありました。また、OSSや個人開発などでも使用するFlutter SDKのバージョンを固定したいこともあるかと思われます。
本記事では、開発者ごとでなく、プロジェクトごとにFlutterのバージョンを管理する方法を紹介します。
FVMを用いる場合
FVMはFlutter SDKバージョン管理ツールです。
https://github.com/leoafarias/fvm
導入についてはこちらの記事が日本語で分かりやすいので、説明は割愛します。
https://qiita.com/Kurusu_ima/items/2dfd067f6e79f520198f
バージョンをプロジェクトごとに固定する方法
ターミナルを開いてプロジェクトのルートに移動しておいてください。
インストール可能なリリース済みのバージョン一覧を確認しましょう。
$ fvm releases次に現在インストールされているバージョンの一覧を確認してみましょう。
$ fvm listお目当てのバージョンがなければ、以下のコマンドでバージョン名またはチャンネル名からFlutter SDKが手に入ります。
$ fvm install <バージョン名またはチャンネル名>例えばstableを使いたい人は以下のコマンドです。
$ fvm install stable最後に、以下のコマンドで、プロジェクトのFlutter SDKのバージョンを固定することができます。
$ fvm use <バージョン名またはチャンネル名>そうすると、プロジェクト内に
.fvm
が作成されます。.fvm
内には以下が入っています。
- プロジェクトで固定されるFlutter SDKバージョン情報が記載された
.fvm/fvm_config.json
~/fvm/versions/<バージョン名>
のシンボリックリンクとなっている.fvm/flutter_sdk
すでにFVMによりバージョン固定されているプロジェクトで開発する時
バージョンを指定せず以下のコマンドを実行すると、
.fvm/fvm_config.json
のバージョンをインストールできます。$ fvm install固定されたバージョンのFlutter SDKを使用する
flutter
コマンドの前にfvm
をつけるだけです。例としてflutter run
したい場合は以下のようになります。$ fvm flutter runIDEの設定
IDEの操作でも固定したFlutter SDK下で実行できるようにします。デバッガーなどIDEの機能を使いたい時のために必要です。
Android Studioの場合
Preferences > Languages & Frameworks > Flutter
のFlutter SDK pathを{対象プロジェクトのルート}/.fvm/flutter_sdk
に変更してください。VSCodeの場合
{対象プロジェクトのルート}/.vscode/settings.json
に以下を追加してください。{ "dart.flutterSdkPath": [".fvm/flutter_sdk"], }VSCodeを再起動し、コマンドパレットで
Flutter: Change SDK
と入力してバージョンを選んでください。Flutter Wrapperを用いる場合
更新が一年以上止まっているのですが、特定のバージョンのFlutter SDKをダウンロード&実行するシェルスクリプトによってFlutterのバージョンをプロジェクトで固定できるFlutter Wrapperを用いる方法もあります。
https://github.com/passsy/flutter_wrapper
導入
ターミナルでバージョンを固定したいプロジェクトのルートに移動した後、以下を実行するだけです。
$ sh -c "$(curl -fsSL https://raw.githubusercontent.com/passsy/flutter_wrapper/master/install.sh)"以降は
flutter
の代わりに./flutterw
のコマンドを用いることで、固定されたFlutter SDK下でコマンドを実行することができます。アップグレード
特に新しいコマンドを覚える必要はありません。これも
flutter
の代わりに./flutterw
になるだけで、いつものFlutterのアップグレードと同じです。IDEの設定
各IDEで設定されているFlutter SDKのパスを変更しておくことで、FVMの時と同様、IDEの操作でも固定したFlutter SDK下で実行できるようにします。
Android Studioの場合
Preferences > Languages & Frameworks > Flutter
のFlutter SDK pathを{対象プロジェクトのルート}/.flutter
に変更してください。VSCodeの場合
{対象プロジェクトのルート}/.vscode/settings.json
に以下を追加してください。{ "dart.flutterSdkPath": ".flutter", }まとめ
Flutter Wrapperは更新があまりされていないので、現在のところFVMがスタンダードなのかなという感じです。FVMの方がバージョン切り替えもスムーズかと思います。強いて言うなら、Flutter WrapperはFVMのインストールを必要としないので、コマンド一発で使えるというところがメリットでしょうか。
参考
https://qiita.com/tetsufe/items/8ffa296c22a2dc8b9b51
https://qiita.com/Slowhand0309/items/0767abee120fcb3ba0b4
- 投稿日:2020-12-01T19:27:14+09:00
SwiftUI学習メモ
『1人でアプリを作る人を支えるSwiftUI開発レシピ』などを読み、SwiftUIを軽く触ったときのメモ。(※理解度がまだ浅いので、間違っている記載がある可能性があります)
出典 : Xcode - SwiftUI - Apple Developer
概要
- WWDC2019で発表
- iOS13以上で利用可能
- iOS14 SDKからさらにAPI追加されてる。実質使える状態なのはiOS14からかな...
- iOS14から、フルSwiftUIでアプリ作れるようになった
- iOS14から登場したWidget機能を実装するには、SwiftUIがマストで必要
※チュートリアルが充実してる
特徴
- 宣言的UI
- データバインディング (ステートドリブンなシステム)
- リアルタイムのViewレイアウトプレビュー
ReactやFlutterやってる人には馴染みやすいはず
今までのビュー作成との違い
従来は、命令型 (宣言型の逆) プログラミングでビューを作っていた
let imageView = UIImageView(image: UIImage(named: "unko")) imageView.contentMode = .scaleAspectFit imageView.layer.cornerRadius = 10 imageView.clipsToBounds = true view.addSubview(imageView)↑こんな感じのコードが、↓以下のように。
宣言的に書いて、View修飾子 (ex..cornerRadius
) をチェーンさせて新しいViewを返していく。Image("unko") .cornerRadius(10)プレビューもXcodeで出せる!
※ただし、すぐ止まったりする(謎また、ビューの重ねていき方のパラダイムも異なる。
SwiftUIでは、親Viewが表示可能領域・子の配置位置を決め、子Viewは自身のView領域を決める。つまり、親は子のサイズ決定には基本的に関与しない。子が自身のサイズを決めるコンポーネント
例えば [Swift] SwiftUIのチートシート - Qiita にUIKitとの対応がけっこう書いてある。
こういうの見て、あとは実際に書いて理解してくしかないかな...
アプリの作成
プロジェクトを新しく作成すると、以下のようなファイルが自動でできる。
import SwiftUI @main struct SwiftUISampleApp: App { var body: some Scene { WindowGroup { ContentView() } } }
@main
がエントリポイントを表す。
- App -> Scene (WindowGroup) -> View という構成で作る
- 今のところiPhoneアプリの場合はSceneが1つ
- iPadアプリではマルチウィンドウ機能があるのでSceneが2つとかになる
- アプリの状態検知は、
scenePhase
つかう (↓のようなイメージ)@main struct SwiftUISampleApp: App { @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { ContentView() .onChange(of: scenePhase) { newScenePhase in switch newScenePhase { case .background: ... } } } } }データ管理
'Property Wrapper'というやつでデータをViewにバインディングする機能が標準で備わってる
例えばシンプルな例。
struct ToggleView: View { @State private var isOn = false var body: some View { VStack { Toggle("スイッチON", isOn: $isOn) Text("\(isOn ? "ON" : "OFF")") } .frame(width: 160, height: 64) } }
@State
というPropertyWrapperをつけたプロパティに$
をつけることで、データがバインディング対象になるProperty Wrapper8種類
データは何か、データはどのように処理するか、データはどこからくるか、で用途を分ける
@State
- 扱うデータが値型 and View自身でデータを保持して更新する場合に使う
@Binding
- 扱うデータが値型 and 親Viewなど外部からデータを渡され更新する場合に使う
@State
のデータの先頭に$
マークをつけることで、@Binding
のデータに変換可能@Environment
- 環境値を読み取る場合に使う。KeyPathを指定して読み込み
@StateObject
- ※iOS14以降
- 扱うデータが参照型 and View自身でデータを保持して更新する場合に使う
- データ更新→再レンダリングの際も、Viewインスタンスは破棄されない(=最初の一度しか作成されない)
@ObservedObject
- 扱うデータが参照型 and 親Viewからデータを渡され更新する場合に使う
- こっちはViewインスタンスが再レンダリング時に破棄される
@EnvironmentObject
- 階層を飛び越えてViewにデータオブジェクトを渡せる
@ObservedObject
のバケツリレーを避けられる- ※
ObservableObject
プロトコル
- 参照型のデータを監視する場合、対象のオブジェクトはObservableObjectプロトコルに準拠しないといけない
@AppStorage
- 値型のデータを格納できる
- 格納先はUserDefaults。ライフサイクルがアプリが消されるまで
@SceneStorage
- マルチウインドウをサポートするアプリで、シーンごとに値型のデータを格納できる
- 使い方は、
@AppStorage
とほぼ同じCombine
こちらもWWDC19で発表されたフレームワーク。
- データ処理を宣言的に扱えるようになり、非同期イベントのハンドリングがしやすくなる
- RxSwiftやReactiveSwiftなど、Reactive系のフレームワークに馴染みのある人には扱いやすい?
- ※まだ慣れてないので調べきれてないが、異なる部分や足りない部分も当然あるはず
- SwiftUIの備えるデータバインディング機構と相性がいいので、SwiftUIにAPI通信処理etc.を組み込む際はCombineの利用が前提になるはず
登場人物はざっくり3つ
- Publisher
- Subscriber
- Operator
役割分担はこんな感じで、RxSwiftとかの経験あるなら概念は理解しやすいと思う
- Publisherが発行するデータストリームをSubscriberが受け取る。
- = 雑にいうとイベント駆動で処理を書ける
- Operatorは、両者の間でデータストリームの値の変換を行う。
例えば以下のようにCombineを使う。(他にもやり方はある)
import Combine class Unko { var touched = PassthroughSubject<String, Never>() } let unko = Unko() let subscriber = unko.touched.sink { str in print("Unko touch: \(str)") } unko.touched.send("1回目") unko.touched.send("2回目") subscriber.cancel() unko.touched.send("3回目はない")出力されるのは、
Unko touch: 1回目 Unko touch: 2回目※Unkoクラスのtouchプロパティに、
@Published
をつけるやり方もある。
これをつけとくと、変数の値が更新されたタイミングで、監視してるSubscriberに更新が伝わる。RxSwift, RxCocoaとのAPI対応表みたいのがあるので、Rxに慣れてる人はそれ見ながら覚えてくのが良さそう
CombineCommunity/rxswift-to-combine-cheatsheet: RxSwift to Apple’s Combine Cheat SheetWidget
- iOS14の新機能。ホーム画面におけるようになったアレ
- Widgetに配置できるのはLabelやImageなど表示系のみ。スクロールとかスイッチは無理
- WidgetのUIイベントはタップのみ
- アプリを起動できる
- DeepLinkが設定できるので、特定の画面に遷移させられる
実装
アプリ本体のエクステンション (追加ターゲット) として、作成する
- エントリポイントで、
Widget
プロトコルに準拠
WidgetConfiguration
プロトコルのインスタンスをbodyとして返すStaticConfiguration
: ユーザが設定を編集できないIntentConfiguration
: ユーザが設定を編集できる- 表示 (更新) のロジックを
TimelineProvider
プロトコルが担う
- WidgetはタイムラインにそってViewを更新
- 3つのメソッドの実装が必要
placeholder(in:)
- 初期表示
getSnapshot(in:completion:)
- ホーム画面に追加されたときや、Widget Gallleryで表示されたとき
getTimeline(in:completion:)
- タイムラインに沿った更新時など?
- タイムラインの更新ポリシーにも種類がある :
TimelineReloadPolicy
.atEnd
,.after(_:)
,.never
- ※ホストアプリからWidgetKitを通して、更新処理をキックすることももちろんできる
WidgetCenter
を使うタップ時の処理は、DeepLinkの仕組みでゴニョゴニョする。
.widgetURL
修飾子を使う
- Widget側に
.widgetURL
を追加して、特定のURLセット- ユーザがタップすると、ホストアプリで
.onOpenURL
が呼ばれURLを受け取る- ホスト側でURLをハンドリング
雑にまとめ
- 動きの少ないビューであれば、UIKitで作るよりもサクッと作れそう
- 応用が効くかどうか。HIGにのっとって作るなら大丈夫かな...?
- TableViewやCollectionViewのときにメモリを効率的につかってくれるのかがちょい不安
- iOS14でAPI増えて改善したようなので、たぶん大丈夫...
- Combineとの組み合わせで、必然的にデータバインディングを利用したリアクティブプログラミングで作ることになる
- ちゃんと作れば、状態の複雑さに起因するバグが確実に減る
- (たぶん)デファクトにある程度なってるRxSwift系の流れを汲みつつ、RxSwift依存から離れられる
- アプリがiOS14以上サポートになるまで(あと1-2年くらい?)は、既存アプリに組み込むのは面倒が増えるだけかも
- 練習的に、OSバージョンで条件分岐させて簡単な画面作ってみるのはあり。シンプルな設定画面とか
- 新規アプリで採用するどうかはもう少し使ってみて判断したい。...が、UIKitとのハイブリッド構成もいけるので、困ったらそっちに逃げれる?
- 既存アプリに突っ込むなら、Widget機能つくるタイミングが一番な気がする
- そもそもSwiftUI使わないとリリースできないので
とりあえずもっと使ってみたい! (あと、RxSwiftをCombineでリプレイスしたい)
- 投稿日:2020-12-01T18:47:17+09:00
SwiftUI App ライフサイクル で Watch Connectivity
はじめに
Xcode 12 から、プロジェクト作成時に Lifecycle を選択できるようになり、従来の "UIKit App Delegate" だけではなく、"SwiftUI App" を設定できるようになりました。
このようにして作成したプロジェクトには、AppDelegate.swift と SceneDelegate.swift がありません。
これまで、アプリの初期設定等をここで行っていましたが、"SwiftUI App" でアプリを作成した場合は、初期設定をどこでどのように行えば良いのでしょうか?ここでは、SwiftUI App に iPhone と Apple Watch 間で通信を行うための、Watch Connectvity を使うための設定方法を説明します。
おそらく、Watch Connectvity 以外にも、AppDelegate.swift で初期設定を行なっていたアプリ全般に適用できるテクニックだと思います。なお、本稿では、SwiftUI ならびに Watch Connectivity についての基本的な説明は割愛します。
開発環境等
- MacBook (Retina, 12inch, Early 2015)
- macOS Catalina Version 10.15.7
- Xcode Version 12.2(12B45b)
- iOS 14.2.1(18B121)
- watchOS 7.1(18R590)
従来の(App Delegate による)設定方法
"UIKit App Delegate" ライフサイクルでは、AppDelegate.swift でこんなふうに設定します(iPhone側)。
AppDelegate.swiftimport UIKit import WatchConnectivity @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate { ...<中略>... func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Watch Connectivity 初期設定 if WCSession.isSupported() { let defaultSession = WCSession.default defaultSession.delegate = self defaultSession.activate() } return true }Lifecycle として "SwiftUI App" を選択して作成したプロジェクトでは、AppDelegate.swift がありません。では、Watch Connectivity 周りの初期設定はどこでどう行えば良いのでしょうか?
やはり AppDelegate ?
実は SwiftUI App でも AppDelegate を使うことができます。
@UIApplicationDelegateAdaptor() を使います。※ここでは、SwiftUIWCTest という名前のアプリを作成しているものとします。SwiftUIWCTestApp.swift というファイルが自動生成されますので、そこに以下のように記述します。
SwiftUIWCTestApp.swift@main struct TestApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Watch Connectivity 初期設定 if WCSession.isSupported() { let defaultSession = WCSession.default defaultSession.delegate = self defaultSession.activate() } return true } }ただ、せっかく SwiftUI で AppDelegate を葬り去ったのに、またまた AppDelegate のお世話になるのはあんまり美しくありません。
加えて、Watch Connetctivity を使うには、Apple Watch 側でも設定が必要で、そちらではもともと AppDelegate はありません。どうすれば良いのでしょうか?SwiftUI App ライフサイクルでの実装
まずは、WCSessionDelegate プロトコルを実装したクラスを用意します(ここでは、クラス名を WCManager とします)。
WCManager.swiftimport Foundation import WatchConnectivity class WCManager: NSObject, WCSessionDelegate { static var shared = WCManager() /// 初期化〜有効化 func activate() { // Watch Connectivity if (WCSession.isSupported()) { let session = WCSession.default session.delegate = self session.activate() } else { print("** \(device): WC is NOT Supported") } } } // メッセージ送受信メソッドなどを実装 ...<中略>... }このクラスには、他にメッセージ送受のためのメソッドなどもまとめて実装します。
この辺は、SwiftUI App に限った話ではないのでここでは割愛します。このうえで、SwiftUIWCTestApp の init() で activate すればOKです。
SwiftUIWCTestApp.swift@main struct SwiftUIWCTestApp: App { init() { // Watch Connectivity 初期設定 WCManager.shared.activate() } var body: some Scene { WindowGroup { ContentView() } } }Apple Watch 側でも同様の方法で WCSession の初期設定を行います。
WCManager.swift は、iPhone アプリ側と、WatchKit Extension 側の両方からソースコードを共有できるように設定しておきます(Target Membership で設定)。Apple Watfch側では、SwiftUIWCTest WatchKit Extension グループ下に、同じく SwiftUIWCTestApp.swift というファイルがあるので、SwiftUIWCTestApp の init() で初期設定します。
SwiftUIWCTestApp.swift@main struct SwiftUIWCTestApp: App { init() { // Watch Connectivity 初期設定 WCManager.shared.activate() } @SceneBuilder var body: some Scene { WindowGroup { ContentView() } WKNotificationScene(controller: NotificationController.self, category: "myCategory") } }おわりに
Apple Watch 開発に関する情報はまだまだ少ないように思います。本稿で説明したのは簡単なことですが、個人的に試行錯誤して発見したことですので記録しておきます。
どなたかの参考になれば幸いです。参考
- 投稿日:2020-12-01T18:08:19+09:00
Swiftにおける基本的な型の紹介〜Optional型〜
Optional<Wrapped>型 とは
Optional<Wrapped>型とは、値が存在するか空かのいずれかを表す型です。
swiftでは基本的にnilを許容しないですが、
nilを許容する必要がある場合はOptional<Wrapped>型を利用します。Optional<Wrapped>型のWrappedとは
プレースホルダ型と言い実際にはWrappedを具体的なInt型やString型に置き換えて使用します。Optional<Wrapped>のように<>の中にプレースホルダ型を持つ型をジェネリック型と言います。
ジェネリック型については別記事て記載します。nilを許容する必要がある場合とは、どのような状況か・・・。
twitterのアイコンなどでnilが使えるのではないかなと思います。twitterのアカウントを作成する際に、
ユーザ名は必須ですが、アイコンは任意だと思います。その時に、アイコンがnilを許容しない設計だと、
アイコン画像を設定しないといけなくなると思います。個人的にはそういう時に使うんだろうなーと思いました、
Optional<Wrapped>型の作成方法
Optional型の作成方法は二通りあるらしいのですが、
一般的に使用される方しか覚えてないのでそちらを共有します。Optional型には、Optional型と、非Optional型があります。
Optional<Wrapped>型を作成する方法はものすごく簡単で、!や?を付けるだけです。var a: Int // 非Optional型 -> nilを許容しない var b: Int? // Optional<Int>型 -> nilを許容する変数aは、いつも通りの宣言ですね。
変数aには何か値を代入して初期化しないとコンパイルエラーがおきます。しかし、変数bはOptional型でnilを許容するので
初期化しなくてもコンパイルエラーは起きません。つまり、変数aはコンパイルエラー、変数bはnilという状態になります。
!や?を語尾に付けるだけでnilを許容するようになるなんてOptionalは便利だ!
と思いますが、実は面倒な部分があります。var a: Int? // Optional型 //print関数でログ出力 print(a) // nil //type関数で変数aの型を表示 type(of:a) // Optional<Int>まだ何も代入していないので、print() でnilが返されるのは想定内ですが、
変数aは、Int型かと思いきやOptional<Int>型になります。var a: Int? // Optional型 a = 1 //print関数でログ出力 print(a) // Optional(1) //type関数で変数aの型を表示 type(of:a) // Optional<Int>さらに、変数aに1を代入すると、
print()関数の結果がOptional(1)
type()関数の結果がOptional<Int>Optionalってなんぞや!!
当時は訳が分からず5回ぐらい叫びました(笑)
まず、Optional型というのは、nilを許容する型です。
なので1を代入したとしてもまたどこかでnilが代入される可能性があります。そのような不確かなものをswift様が許す訳もなく、
Int型と同じくくりにせずOptional<Int>としているっぽいです。なので、Int型とOptional<Int>型の計算は、もちろん出来ません。
var a: Int = 10 // Int型 var b: Int? = 100 // Optional<Int>型 var c = a + b // 10 + Optional(100) -> コンパイルエラーnilを許容出来るようにするのは便利ですが、
このようにOptional型は通常では使えなくなっております。では、どのようにしてOptional型を扱うのか。
Optional型で宣言した変数はずっと使えないのでは不便です。しかし解決策があります。
それは、Optional型のアンラップです。Optional型の変数をアンラップすることにより、
通常のInt型やString型と同じように利用できます。今回はOptional型の紹介ですので、
アンラップについては別の記事に記載します。そちらをご覧いただけると幸いです。
以上、ありがとうございました!
- 投稿日:2020-12-01T16:13:39+09:00
FlutterでFirebaseを使わずにSign in with Appleを実装する
この記事はFlutter #2 Advent Calendar 2020の6日目の記事です。
アドベントカレンダーの参加は今年が初めてです。
いつもは個人ブログで記事を投稿していますので、Qiitaへの投稿は久しぶりです。Flutterはクロスプラットフォーム開発ができる便利なツールですが、まだまだ実務で導入するには見えていない部分があり会社で導入するためにはハードルがあったりします。
例えば、どれだけOS依存の課題に対応できるか、そしてどれだけサーバー連携が絡んだ時に柔軟にカスタムできるかだったりですね。
今回はその企業でも導入する際に検討事項に入りそうな「アカウント周りの情報」にういて深堀りしてみます。
企業としては「ただユーザーがSNSアカウントでログインできたら良いや」というのは絶対ありえなく、ログインしたらユーザー情報を社内のデータベースに保存してセキュアに取り扱いたいというのが本音です。
また絶対にユーザー情報が外部に漏れても駄目ですね。
そんなアカウント周りの情報ですが、Flutterでのログイン機構だとまだまだ見えていない部分の方が多いです。そこで、今回はAppleIDを使って認証するシステムである
Sign in with Apple
における振る舞いについて見ていきます。iOSエンジニアがFlutterでSign in with Apple
Flutterを導入できるプロジェクトの場合はだいたいは相性がいいFirebaseも導入してそこら辺はFirebaseが担ってくれる場面が多いのですが、プロジェクトによっては
Firebase Authentication
の機能が使えずFirebase Authentication
で連携できない画面もあるかもしれません。その場合にはFlutterでSign in with Appleでの認証が可能かどうかやっぱり気になります。
なので、今回はFirebase Authentication
が使えない場合を想定してみました。今回はFlutterでのSign in with Apple認証をするために使うパッケージに
sign_in_with_apple
をチョイスしてみます。非常にpopularにパッケージになっています。sign_in_with_apple
URL: https://pub.dev/packages/sign_in_with_appleこのパッケージを使ってFlutterでSign in with Appleを実装してみます。
開発環境
Flutter 1.22.4 • channel stable • https://github.com/flutter/flutter.git Framework • revision 1aafb3a8b9 (2 weeks ago) • 2020-11-13 09:59:28 -0800 Engine • revision 2c956a31c0 Tools • Dart 2.10.4
- Xcode 12.1
- Android Studio 4.0
- iOS 14.3 (Sign in with Apple は実機で開発した方がスムーズのため)
発生したエラー
Xcodeのバージョンが古い
実機のiOSのバージョンに対してXcodeのバージョンが足りなかった時に発生したエラー。
═══════════════════════════════════════════════════════════════════════════════════ Error launching app. Try launching from within Xcode via: open ios/Runner.xcworkspace Your Xcode version may be too old for your iOS version. ═══════════════════════════════════════════════════════════════════════════════════ 2020-11-29 18:23:16.711 ios-deploy[2686:17758351] [ !! ] Error 0xe8000022: The service is invalid. AMDeviceSecureStartService(device, serviceName, NULL, &dbgServiceConnection) Could not run build/ios/iphoneos/Runner.app on 00008030-000508D21A83802E. Try launching Xcode and selecting "Product > Run" to fix the problem: open ios/Runner.xcworkspace Error launching application on XXXXX.おそらくXcodeのバージョンを上げたらいいのかと思いバージョンを上げてみる。
Xcode 12.2 でアプリが起動してくれました?
Flutter で Sign in with Apple の実装まで
それではFlutterでの本実装の解説に入ります。
Xcode側で「Capability」の設定を行う
Xcode側でSign in with Appleの設定を活性化させておきます。
これを設定していないと、Apple認証が正しく動作しません。
pubspec.yamlのソースコード
sign_in_with_appleをインストールするためpubspec.yamlファイルを編集します。
pubspec.yamldependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.0 sign_in_with_apple: ^2.5.4 # 追加するこれで
Pub get
してインポートします。ソースコードについて
今回は味気ないですが、単純に初期コードにSign in with Appleのボタンを追加するだけにします。
main.dartimport 'package:flutter/material.dart'; import 'package:sign_in_with_apple/sign_in_with_apple.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), SignInWithAppleButton( onPressed: () async { final credential = await SignInWithApple.getAppleIDCredential( scopes: [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, ], ); print(credential); }, ) ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } }Sampleにある通りにSign in with Appleのウィジェットは
SignInWithAppleButton
だそうです。
これを使って実装しました。これでソースコードをビルドすると次のような画面が表示されます。
個人的にボタンのデザインをカスタムにできたら嬉しいなと思っています。ま、多分カスタマイズできると思っています。黒色の「Sign in with Apple」をタップするとApple認証のやつが下から表示されます。
Apple認証はAppleのサーバーにリクエストしてレスポンスとしてUser情報を受け取りますので、非同期処理のために
async/await
で対応します。
getAppleIDCredential
を叩いた時にscopes
引数があるのはUserのAppleIdに紐付いている
- 姓名
- メールアドレス
は任意でリクエストを送らないと取得できないようになっているからです。
ですので、名前とメールアドレスの取得が必要であればこのscopes
にAppleIDAuthorizationScopes.email
とAppleIDAuthorizationScopes.fullName
をセットしないといけません。受け取れるユーザー情報
で、ここから本題になりますが、この受け取ったUser情報がどれくらいネイティブアプリと比べて取得できるのかを調べます。
今回のソースコードでは、
print(credential);この部分の調査になります。まず、credentialは
getAppleIDCredential
を叩いたときに返ってくるものです。このメソッドをググると、static Future<AuthorizationCredentialAppleID> getAppleIDCredential({ @required List<AppleIDAuthorizationScopes> scopes, /// Optional parameters for web-based authentication flows on non-Apple platforms /// /// This parameter is required on Android. WebAuthenticationOptions webAuthenticationOptions, /// Optional string which, if set, will be be embedded in the resulting `identityToken` field on the [AuthorizationCredentialAppleID]. /// /// This can be used to mitigate replay attacks by using a unique argument per sign-in attempt. /// /// Can be `null`, in which case no nonce will be passed to the request. String nonce, /// Data that’s returned to you unmodified in the corresponding [AuthorizationCredentialAppleID.state] after a successful authentication. /// /// Can be `null`, in which case no state will be passed to the request. String state, }) async {というふうに
Future<AuthorizationCredentialAppleID>
が返ります。AuthorizationCredentialAppleID
はSign in with Appleを実装したiOSエンジニアならご存知ですがこれがApple認証が成功した時に受け取れるUser情報になります。以下が
credential
の情報になります。
プロパティ 役割 userIdentifier 一番重要なApple認証後のUser情報 givenName 名 familyName 性 メールアドレス authorizationCode 実はよくわかりません identityToken JSON Web Token (JWTです、後述します) state 状態 (よくわかりません) print(credential.userIdentifier); print(credential.givenName); print(credential.familyName); print(credential.email); print(credential.authorizationCode); print(credential.identityToken); print(credential.state);ちなみに2回目以降のApple認証で取得できるUser情報はこちらになります。
givenName、familyName、emailが2回目以降取得できないのが再現されています。(それはそう。)
これら3つの情報を何度も取得したい場合は端末のApple認証ステータスをログアウトする必要があります。「設定アプリ」から「パスワードとセキュリティ」「Apple IDを使用中のApp」の項目へ進んで「Apple IDの使用を停止する」を選択すればAppleIDの使用が停止され再度上記3つのデータを取得できるようになっているはずです。
identityToken の説明
そして、ここからはいつもの
Sign in with Apple
の使い方ですが、identityToken
というのはJWTというもので、これは暗号化された文字列になっています。この情報を解析するためには、
へアクセスして、
の「Encoded」の部分に
identityToken
をそのままコピーペーストすればデコードされた情報が確認できます。
そして、デコードされた情報の中にcredential.userIdentifier
と同じ情報が含まれています。そのため、例えば、独自のAPIリクエストを使って社内のデータベースと認証して同じユーザーかどうかを確認する場合はこの
userIdentifier
を使えば良さそうです。そんな感じでSwift/iOSでアレだけ面倒だったSign in with Appleがなんと
SignInWithAppleButton( onPressed: () async { final credential = await SignInWithApple.getAppleIDCredential( scopes: [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, ], ); print(credential); }, )とこれだけでSign in with Appleの実装ができるのですね。
ここの部分をSwiftで書くとしたら下のようになります。
@objc func handleAuthorizationAppleIDButtonPress() { let appleIDProvider = ASAuthorizationAppleIDProvider() let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email] let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.presentationContextProvider = self authorizationController.performRequests() }しかも動作や受け取れるユーザー情報もネイティブのときと同じです。
ただ、ちょっと気になるのがXcode 12.2じゃないとビルドできなかった点ぐらいでしょうか。
iOSが最新バージョンだった影響もあるかもしれません。(なので、既存のプロジェクトでSign in with Appleを導入する場合は、OSのバージョンに注意したほうが良いかもしれません。)
ネイティブ実装
それではおまけ程度にネイティブでSign in with Appleを実装する方法を解説します。
とはいうもののそんなたいそうな話ではなく既にAppleがサンプルのアプリを用意してくれています。Implementing User Authentication with Sign in with Apple
ここの「Download」からサンプルプロジェクトをダウンロードできます。
それを見て実装方法を調査すればできます。ネイティブの場合はリクエストを送信するとDelegateでコールバックでUser情報が返ってきます。
細かいハマリポイントは会社のテックブログでまとめましたので良かったらこちらのページから確認してください。iOS 版レアジョブアプリが Sign in with Apple に対応した話
こちらの記事では、本記事で取り上げなかった
- メールアドレスの取り扱い(メールを非公開、にした場合に得られるApple側のアドレスの内容)
- メール送信機能がある場合の対応方法
- iOS 13未満のOSに対する取り扱い
について解説しています。
ということで僕からは以上になります。
- 投稿日:2020-12-01T12:51:29+09:00
最新のiOSのバージョンを取得する方法
はじめに
ソフトウェアテストの業界向けに役に立つ話、番外編です。
最新のiOSがリリースされた時に、いちはやくリリースされたことをシェアしたいと思い、botを作りました。今回はチャットサービスに送信する部分は省略して、iOSのバージョン取得部分を紹介します。
1. アップデートxmlの取得
iOSはアップデートを確認する際、下記アドレスに確認しにいくようです。
http://mesu.apple.com/assets/com_apple_MobileAsset_SoftwareUpdate/com_apple_MobileAsset_SoftwareUpdate.xmlimport requests url = "http://mesu.apple.com/assets/com_apple_MobileAsset_SoftwareUpdate/com_apple_MobileAsset_SoftwareUpdate.xml" res = requests.get(url)まずはリクエストを送信し、xmlを変数に格納しましょう。
2. xmlをパースする
import xml.etree.ElementTree as ET root = ET.fromstring(res.text)次にxml.etree.ElementTree モジュールを使用し、XML データを扱えるようにします。
3. 調べたいデバイスが何番目にあるか
<plist version="1.0"> <dict> <array> <dict> ... <key>OSVersion</key> <string>9.9.14.2</string> <array> ... <array> <string>iPhone11,6</string> <dict> ...本xmlでは、dict > array > dict の中にデバイス毎のアップデート情報が記載されていますが、デバイスの機種毎に提供されている最新OSは異なる為、「iPhone11,6」(iPhone12 Pro Max) など最新OSが提供されてそうなデバイスを指定します。
devices = root.find('dict').find('array').findall('dict') TARGET_DEVICES = "iPhone11,6" for number, device in enumerate(devices): for keys in device.findall('array'): for key in keys: if key.text == TARGET_DEVICES: return numberひとまず、「iPhone12 Pro Max」はリストの何番目なのか探します。
4. バージョンを取り出す
device = devices[number] for i, key in enumerate(device): if key.text == 'OSVersion': version = device[i + 1].text return version<key>OSVersion</key> <string>9.9.14.2</string>前述の節でリストの何番目のデバイス情報を取り出せばいいかわかったので、次は
OSVersion
というキーの下にある要素を見つけます。9.9.14.2これでバージョンを取り出せましたが、謎の9.9表記がついています。
5. 不要な表記を削除する
if version[:4] == '9.9.': version = version.replace('9.9.', '')バージョン情報には必ず頭に「9.9」という表記がついています。これが何の意味があるかはわかりませんが、今回は不要なので取り除きます。
6. 結果
14.2無事バージョンを綺麗に取得できました。
さいごに
このバージョンをpickleに保存しておけば、前回のバージョンと差があった際にチャットサービスにお知らせするbotが実現できるかと思います。
※ Appleのサーバに負担がかからないように定期実行の頻度は気をつけましょう。
コード全文
import requests import xml.etree.ElementTree as ET def get_ios_version() -> str: """iOSの最新バージョンを表示するメソッド 特定デバイスに提供されている最新のiOSのバージョンを表示します。 Returns: str: iOSのバージョン """ def get_version(response_text: str) -> str: """バージョンを取り出す為の一連の動作を制御するメソッド Args: response_text (str): xmlのレスポンスのテキスト Returns: str: バージョン """ root = ET.fromstring(response_text) devices = root.find('dict').find('array').findall('dict') number = get_match_number(devices) if number is not None: version = get_version_text(devices[number]) if version: return format_version(version) def format_version(version: str) -> str: """バージョンに含まれる不要な表記を取り除くメソッド Args: version (str): バージョンのテキスト Returns: str: 通常、不要な表記である「9.9.」が頭に含まれる為、除去 Example: 9.9.14.2 -> 14.2 """ if version[:4] == '9.9.': return version.replace('9.9.', '') else: return version def get_version_text(device: ET.Element) -> str: """バージョンを実際に取り出すメソッド Args: device (ET.Element): xml内にある、対象デバイスのElement Returns: str: バージョンのテキスト """ for i, key in enumerate(device): if key.text == 'OSVersion': version = device[i + 1].text return version def get_match_number(devices: list) -> int: """ターゲットのデバイスを探し、何番目に存在したか番号を返すメソッド TARGET_DEVICESの「iPhone11,6」は「iPhone 12 Pro Max」のことです。 Args: devices (list): Returns: int: 一致したデバイスのリスト番号 """ TARGET_DEVICES = "iPhone11,6" for number, device in enumerate(devices): for keys in device.findall('array'): for key in keys: if key.text == TARGET_DEVICES: return number url = "http://mesu.apple.com/assets/com_apple_MobileAsset_SoftwareUpdate/com_apple_MobileAsset_SoftwareUpdate.xml" try: res = requests.get(url) except Exception as e: print(e) return if res: version = get_version(res.text) return version if __name__ == '__main__': version = get_ios_version() print(version)
- 投稿日:2020-12-01T11:01:13+09:00
the Composable Architecture の始め方
iOSDCでのyimajoさんの発表など、the Composable Architecture(以下、TCA)が良さそうという評判を聞いて調べてみました。どこから始めていいのか少し迷ったので、公式レポジトリのREADMEにあるBasic UsageをベースにTCAの始め方を解説してみます。
TCAどころかSwiftUIすら勉強し始めなので、間違いなどあるかもしれません。コメントで教えて頂けると嬉しいです!TCAって何?
TCAはiOSなどのAppleプラットフォームのアプリケーション開発のためのフレームワークです。
Combineを前提としているため、iOSだとiOS13以上が対象となる制約があります。(ちなみにiOS13未満向けにRxSwift版のforkもあるみたいです)
SwiftUIとの親和性が高く、SwiftUIをより使いやすくする機能が充実しています。TCAが提供する機能については、READMEのWhat is the Composable Architecture?に以下が挙げられています。
State Management
シンプルな値型によるアプリケーションの状態管理の手段を提供します。複数の画面にまたがって状態は共有され、1つの画面での状態の更新はただちに他の画面にも反映されます。Composition
巨大な機能を小さなコンポーネントにブレイクダウンして独立したモジュールに分離し、それらを簡単にまとめ直して1つの機能に組み上げる手段を提供します。Side Effects
可能な限り最もテスタビリティが高く、理解しやすい方法で、副作用を伴うアプリケーションの外側の世界とやり取りする手段を提供します。Testing
TCAを使って実装した機能をテストするだけでなく、多くの部品で構成された機能の統合テストを書いたり、副作用がアプリケーションにどのように影響するかを把握するためのエンドツーエンドのテストを書く手段を提供します。これにより、ビジネスロジックが期待通りに動作していることを強力に保証することができます。Ergonomics
コンセプトと部品を可能な限り少なくしたシンプルなAPIにより、上記のすべてを達成する手段を提供します。アーキテクチャ概要
コードを書き始める前に簡単にTCAのアーキテクチャとしての概要を説明したいと思います。
TCAはReduxやFluxといったアーキテクチャと近い構造を持っていて、Store/State/Action/Reducerという型を持っています。またこれらに加えてEffectとEnvironmentという型も定義しています。
まずState/Action/Reducerについて解説します。
- State: 1つの機能がそのロジックを実行したり、UIを描画したりするために、必要となるデータを定義する。
- Action: 1つの機能で発生するすべてのアクション(ユーザからのUI操作やユーザへの通知やデータ層からのデータ受け取りなど)を表現する。
- Reducer: 受け取ったActionに応じてStateを更新するファンクション。Stateを更新するために発生するあらゆる作用(副作用を含む)に対して処理を行う責務をもち、作用からは
Effect
型で値が返却されてくることになる。上記で出てきたEffectがTCAに特有の考え方になっています。
- Effect
- 副作用(Side Effect)を含む作用を表現する型
- Reduxでは副作用はStore/State/Action/Reducerの枠組みの外で扱うべきとなっている(ActionDispatcherなどが担当することになる)が、TCAではReducerの中で扱うことができるようになっている。
ReducerがEffectを扱うことができることによって、プレゼンテーション層/ドメイン層/データ層の各層で発生するすべてのアクションを単なるActionとして扱うことができるようになっていることが、TCAの特徴の一つだと思います。
次にEnvironmentです。
- Environment: 1つの機能において必要となる依存を保持する。
いわゆるDI(Dependency Injection)を提供します。
Basic Usageの中ではあまり本質的な使い方がされてなくてよくわかっていない部分があるのですが、Environmentによってロジックの入れ替えが実現可能となり、テスタビリティ向上やState/Action/Reducerの再利用の促進(よりComposableにできる)に繋がっているのかなーと思っています。最後にStoreです。
- Store: State/Action/Reducer/Environmentを一つにまとめ、それらの窓口となる。すべてのActionはStoreに対して送信され、それを受けてStoreはReducerを動かす。Reducerの処理結果で発生したStateの変更は、Storeを経由してViewで監視できるようになっている。
StoreをViewにバインドする時は
WithViewStore
を使い、ViewStoreというViewにバインドできる型になります。これらを図にまとめると以下のような関係性になっています。
Viewでの操作はすべてActionに変換されます。
ActionをReducerで処理してStateに反映する単方向のデータ制御を実現しており、ViewStoreを経由してViewにStateの変更が反映されるようになっています。
データ層へのアクセスについては、ReducerまたはEnvironmentを経由してEffectを発行し、非同期でデータ操作を行います。結果はEffectからActionを投げることで、Stateに反映されます。Basic Usage
ここからはTCAを使ってコードを書いていきます。
Basic Usageに記載されている、数字をインクリメント/デクリメントする画面を作成します。
「+」ボタンでカウンタがインクリメントされ、「-」ボタンでデクリメントされます。
「Number fact」ボタンを押すとアラートを表示します。アラートではカウンタを含んだ文字列が表示されるようにします。プロジェクト作成からインストール
まずはXcodeで新規プロジェクトを作成します。「Interface」には「SwiftUI」を選択します。
TCAをインストールします。
TCAはライブラリとして提供されており、Swift Package Managerを使ってインストールします。
State/Action/Environment/Reducer
TCAの根幹となるState/Action/Environment/Reducerを作ります。
State
import ComposableArchitecture struct AppState: Equatable { var count = 0 var numberFactAlert: String? }
count
が画面に表示するカウンタの値です。
numberFactAlert
には画面の「Number fact」ボタンを押下した時にアラートで表示する文字列が格納されます。Action
enum AppAction: Equatable { case factAlertDismissed case decrementButtonTapped case incrementButtonTapped case numberFactButtonTapped case numberFactResponse(Result<String, ApiError>) } struct ApiError: Error, Equatable {}enumでActionを定義します。各ケースは以下のアクションに対応しています。
factAlertDismissed
: アラートのボタンを押した時のアクションdecrementButtonTapped
: マイナスボタンを押した時のアクションincrementButtonTapped
: プラスボタンを押した時のアクションnumberFactButtonTapped
: 「Number fact」ボタンを押した時のアクションnumberFactResponse
: 「Number fact」ボタンにより発生するEffectの戻りのアクション
factAlertDismissed
からnumberFactButtonTapped
までがプレゼンテーション層から発生するアクションです。
numberFactResponse
はドメイン層/データ層から発生するアクションです。Basic Usageではデータ層へのアクセスはないため、ドメイン層からのアクションになりますが、データ層にアクセスした場合も似たような形になると思います。成功/失敗に対応できるようにResult
をAssociated Valueとして受け取れるようになっています。Envrionment
struct AppEnvironment { var mainQueue: AnySchedulerOf<DispatchQueue> var numberFact: (Int) -> Effect<String, ApiError> }依存対象を切り出しています。
numberFact
はカウンタの値を引数にアラートに表示する文字列を作るクロージャです。Reducer
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in switch action { case .factAlertDismissed: state.numberFactAlert = nil return .none case .decrementButtonTapped: state.count -= 1 return .none case .incrementButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return environment.numberFact(state.count) .receive(on: environment.mainQueue) .catchToEffect() .map(AppAction.numberFactResponse) case let .numberFactResponse(.success(fact)): state.numberFactAlert = fact return .none case .numberFactResponse(.failure): state.numberFactAlert = "Could not load a number fact :(" return .none } }Reducerでは各Actionに対応する「Stateへの処理」と「実行するべきEffect」について記述します。
ケース文の最後にreturnしている.none
はEmptyのEffectです。これによりそのケースでは実行されるべきEffectがないことを示しています。つまりReducerはState, Action, Environmentを引数にしてEffectを返すクロージャと認識すればよいかなと思います。(正確にはクロージャを引数にした.initを持つstructです)Effectを使っている
case .numberFactButtonTapped:
について取り上げます。
「Number fact」ボタンが押された際はEffectで非同期的な処理を実行するようになっています。
AppEnvironment
で定義したnumberFact
クロージャを動かし、戻り値のEffectを最終的にはActionに変換しています。Actionに変換することにより再度Reducerが呼び出されます。ドメイン層/データ層からの戻り値を再びReducerで処理することができるようになっています。
(途中のreceive(on:)
は実行スレッドの指定、catchToEffect()
はreceive(on:)
でPublisher
に変換された型を再びEffect
に戻しています。)View
SwiftUIのViewへTCAを組み込みます。プロジェクト新規作成時にデフォルトで存在する
ContentView
を使うように原本から少し改変しています。import SwiftUI import ComposableArchitecture struct ContentView: View { let store: Store<AppState, AppAction> var body: some View { WithViewStore(self.store) { viewStore in VStack { HStack { Button("−") { viewStore.send(.decrementButtonTapped) } Text("\(viewStore.count)") Button("+") { viewStore.send(.incrementButtonTapped) } } Button("Number fact") { viewStore.send(.numberFactButtonTapped) } } .alert( item: viewStore.binding( get: { $0.numberFactAlert.map(FactAlert.init(title:)) }, send: .factAlertDismissed ), content: { Alert(title: Text($0.title)) } ) } } } struct FactAlert: Identifiable { var title: String var id: String { self.title } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView( store: Store( initialState: AppState(), reducer: appReducer, environment: AppEnvironment( mainQueue: DispatchQueue.main.eraseToAnyScheduler(), numberFact: { number in Effect(value: "\(number) is a good number Brent") } ) ) ) } }通常のSwiftUIで出てこないTCA特有の要素について解説していきます。
WithViewStore(self.store) { viewStore in }
WithViewStore
でViewにStoreを組み込みます。
WithViewStore
はSwiftUIのView
を返すので、var body: some View {}
の中で使うことができます。
Button("−") { viewStore.send(.decrementButtonTapped) }
viewStore.send()
でActionを送ります。
ここでは「-」ボタンを押した時に.decrementButtonTapped
Actionを送るように設定しています。
Text("\(viewStore.count)")
viewStore
のプロパティとしてStateのプロパティにアクセスできます。
Stateが更新されれば自動的にViewが更新されます。
SwiftUIだと値をバインドする時にはプロパティラッパーを使うことになると思うのですが、単なるViewStoreのプロパティとして書けるので簡単ですね。
(StoreからStateにプロパティが置き換わっているのがどうやって実現しているのかはまだよくわかっていません...? )
viewStore.binding(get:send:)
viewStore.binding(get:send:)
でSwiftUIのBinding
を提供して、ViewとViewStoreの間に双方向のバインディングを実現します。ViewStore→View方向のバインディングはget:
で指定してStateが引数として渡され、Viewへの出力内容を設定できます。View→ViewStore方向のバインディングはsend:
で指定し、Viewの操作をトリガーにActionを送ることができます。ここでは
alert(item:content:)
のitem: Binding<FactAlert?>
を作るために使われています。
ViewStore→View方向では、StateのnumberFactAlert
をAlertで使用するFactAlert
struct に変換しています。Stateで保持しているドメイン層のモデルをプレゼンテーション層のモデル(FactAlert
)に変換しているイメージかなと思います。
View→ViewStore方向では、アラートでボタンを押した時のアクションを設定しています。
Store(initialState:reducer:environment:)
ContentView_Previews
でプレビューできるようにStoreを作っています。environment:
で指定しているAppEnvironment(mainQueue:numberFact)
のnumberFact:
でEffect<String>
を返すクロージャを指定し、ようやくここでアラートにどういった文字を表示するのかが決まります。App
最後にAppでの
ContentView
生成箇所を修正して完了です。import SwiftUI import ComposableArchitecture @main struct TCABasicUsageApp: App { var body: some Scene { WindowGroup { ContentView( store: Store( initialState: AppState(), reducer: appReducer, environment: AppEnvironment( mainQueue: DispatchQueue.main.eraseToAnyScheduler(), numberFact: { number in Effect(value: "\(number) is a good number Brent") } ) ) ) } } }やっていることはViewの
ContentView_Previews
と同じです。これで実行すれば画面が表示できると思います。お疲れ様でした!?
感想
TCA触ってみた感想です。
- 単方向制御がTCAの中で完結している
- プレゼンテーション層からの入力も、副作用を伴うデータ層からの入力もActionに変換されるので単方向制御が実現しやすい。
- ReducerからもEffectを介してActionを発行できるので、ドメイン層内のアクションにも対応できる。
- 非同期の処理をEffectで表現して、CombineとState/Reducer/Actionの世界をうまく繋げている。
- SwiftUIとの親和性が高い
- Viewでの値監視についてプロパティラッパーを意識しないで済むので$アクセスが不要になる。単なるViewStoreのプロパティとしてアクセスすればよくなるのでSwiftUIがより簡単になる。
煩雑になりがちな処理・データの流れが明快で、すごく洗練されたアーキテクチャになっているなと思いました。iOS13以上の制約がクリアできるならぜひ使いたいです。
あと、the Composable ArchitechtureのComposableたる所以である
combine
/pullBack
についてはBasic Usageでは使われておらず、この記事では書いていません。時間があればまたどこかで書きたいなと思っています。最後までご覧頂きありがとうございました?
参考リンク
- 投稿日:2020-12-01T09:45:22+09:00
iOS 14からMultipeer ConnectivityがErrorCode -72008で繋がらなくなったときの解決方法
はじめに
LiDARセンサー付きの iPhone がどうしても欲しくて、最近自身のスマホを iPhone 12 Pro に機種変更しました。
早速、LiDARセンサーの性能を試すべく、Multipeer Conectivity で連携できる自作のARアプリをiPhone 12 ProにXcodeでビルドし、あらかじめ用意してあったもう一台のiPhoneとともにアプリを立ち上げたのですが、あれ?デバイス同士の連携ができない......?
と異変に気づき、Xcode でコンソールを確認してみると、
2020-12-01 00:34:28.593776+0900 MCSample[28078:11026553] [MCNearbyServiceBrowser] NSNetServiceBrowser did not search with error dict [{ NSNetServicesErrorCode = "-72008"; NSNetServicesErrorDomain = 10; }]. 2020-12-01 00:34:28.593778+0900 MCSample[28078:11026556] [MCNearbyServiceAdvertiser] Server did not publish: errorDict [{ NSNetServicesErrorCode = "-72008"; NSNetServicesErrorDomain = 10; }].という今まで見たことのないエラーログが出ているではありませんか...!
「なぁにこれぇ」って武藤遊戯みたいな反応 になったのですが、調べまくってなんとか解決できたので、ここにその方法を残しておこうと思います。エラーの原因
今回上記のエラーが発生した iPhone 12 Pro は初期搭載OSバージョンが iOS 14.1でした。
手元でさまざまなOSバージョンの端末で試してみたのですが、そもそも上記のエラーは iOS 14から発生していることがわかりました。
そのことから、iOS 14から追加された新機能に絞って調べてみると、WWDC 2020 の Support local network privacy in your app のセッション で、iOS 14からローカルネットワークへの通信にユーザの許可が必要になったことがわかり、そのための追加実装が抜けていたことが今回のエラーの原因だったとわかりました。解決する方法
このエラーを解決する方法はApple公式ドキュメントである Discovering Peers with MultipeerConnectivity | Apple Developer Documentation の「Add Bonjour Services Plist Keys」の章にちゃんと記載されていました。
が、ドキュメントを読んでいて、解釈が少々わかりづらいポイントがあったので、その点については後述します。① ローカルネットワークの利用許可ダイアログの説明文を追加する
まず、今回の対応を行うことで表示されるようになる以下のダイアログの説明文を
Info.plist
に追加します。Property Listから追加する場合は、
Privacy - Local Network Usage Description
の値に 利用許可を求める説明文 を追加しましょう。Source Codeから追加する場合は、以下のように追記しましょう。
<key>NSLocalNetworkUsageDescription</key> <string>利用許可を求める説明文</string>② Bonjour サービスで検索するサービス名の追加
Bonjour(ボンジュール)って急にフランス語が出てきて草生えたのですが、
Bonjour とは、Apple社が開発・提供し、Mac/iPhoneに標準装備されているネットワーク機器を何の設定も行わず簡単に接続するための技術 の名称でした。
https://developer.apple.com/bonjour/iOS 14以降の Multipeer Connectivity では、Bonjour サービスで検索するために、
Info.plist
に検出してもらうサービス名を追加する必要があります。ここで、公式ドキュメントである Discovering Peers with MultipeerConnectivity | Apple Developer Documentation の「Add Bonjour Services Plist Keys」の章を見直してみると、
NSBonjourServices
に追加する._tcp
と._udp
のプレフィックスはそれぞれ_myAppName
と記載されています。ここが大きなハマりポイントでした。最初ドキュメントを見たときに、
_myAppName
って Product Name もしくは Display Name のことかな? と思って試してみたのですが、全然違いました ?
困り果ててググってみると、 Apple Developer Forums で同じ問題に対するQ&Aの回答 を発見し、完全に理解しました。
_myAppName
とは、MCNearbyServiceAdvertiser と MCNearbyServiceBrowser の生成時に共通で使用する serviceType: String の先頭に_
を加えた文字列のことだったのです。なので、
Info.plist
には、Property Listから追加する場合は、以下のように
Bonjour services
の Array に
_serviceType._tcp
と_serviceType._udp
の Item をそれぞれ追加しましょう。Source Codeから追加する場合は、以下のように追記しましょう。
<key>NSBonjourServices</key> <array> <string>_serviceType._tcp</string> <string>_serviceType._udp</string> </array>以上の①、②の対応を行い、再度ビルドすることで無事にエラーが出なくなり、連携できるようになりました。
まとめ
- iOS 14以降で Multipeer Conectivity を利用するには、ローカルネットワークの利用許可ダイアログの説明文とBonjour サービスで検索するサービス名の
Info.plist
への追加が必要になった- 対応方法が記載されている Discovering Peers with MultipeerConnectivity | Apple Developer Documentation の「Add Bonjour Services Plist Keys」の章の
_myAppName
には MCNearbyServiceAdvertiser と MCNearbyServiceBrowser の生成時に共通で使用する serviceType: String の先頭に_
を加えた文字列を使うことが正解参考リンク一覧
- 投稿日:2020-12-01T03:29:22+09:00
【SwiftUI】AppデザインのためにGeometryReaderを理解する
はじめに
SwiftUIでそれぞれのViewのレイアウトをデザインする時に必要なGeometryReaderについて整理することを目的とする
開発環境
OSX 10.15.7 (Catalina)
Xcode 12.2.0
CocoaPods 1.10.0SwiftUIのレイアウトシステム
Appをデザインするための基本的な考え方はHStack、VStack、ZStackを組み合わせです。複雑なレイアウトを実装していくためにそれぞれの特徴を理解する必要があります。
HStack
childViewを横一列に並べることができる。
"H": a horizontal lineVStack
childViewを縦方向に並べることができる。
"V": a vertical lineZStack
childViewを重ねることができる。
"Z": overlap参考文献
How to use stacks: HStack, VStack and ZStack in SwiftUI (Equivalent of UIStackView in UIKit)import SwiftUI struct ContentView: View { var body: some View { // a vertical line VStack { // a vertical line VStack { // overlap ZStack { // a horizontal line HStack{ Image(systemName: "doc") Text("First One") } } // overlap ZStack { // a horizontal line HStack{ Image(systemName: "doc") Text("Two One") } } } } } }GeometryReaderの考え方
参考文献
GeometryReaderはViewの一種であり、親のViewのサイズと自身の親のViewに対する位置を知るためのもの
実装の要点
struct ContentView: View { var body: some View { // a vertical line // parent View VStack { // クロージャーの"g"は任意で設定可能 GeometryReader { g in // a vertical line VStack { // overlap ZStack { // a horizontal line HStack{ Image(systemName: "doc") Text("First One") } // parentViewの幅いっぱい、縦は全体の1/2に設定。 }.frame(width: g.size.width, height: g.size.height/2, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) .background(Color.red) // overlap ZStack { // a horizontal line HStack{ Image(systemName: "doc") Text("Two One") } // parentViewの幅いっぱい、縦は全体の1/2に設定。 }.frame(width: g.size.width, height: g.size.height/2, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) .background(Color.yellow) } } }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) } }現在、IPhoneは様々な画面サイズのデバイスが生産されています。それらのサイズに適したデザインをするためにはGeometryReaderでサイズを取得し、配置していく必要があります。
以下のイメージは画面サイズを1/3に設定したものです。
height: g.size.height/3
の"3"は整数だけでなく、小数点で表記しても認識されます。これにより、さらに細かいレイアウトのデザインが可能です。実装のイメージ
ログイン・サインアップViewのレイアウトイメージです。
全体の比率を計算して、レイアウトを決めていきます。