- 投稿日:2020-12-01T22:49:35+09:00
[iOS]UIImageViewがタップされた時にイベントをつける
UIImageViewがタップされた時にイベントを発火させたかった。
バージョン
Swift 5.3
Xcode 12.1実装
UIViewController.swiftimport UIKit class UIImageViewEventViewController: UIViewController { let imageView: UIImageView = UIImageView(image: UIImage(named: "cat")) override func viewDidLoad() { super.viewDidLoad() imageView.frame.size = CGSize(width: 100, height: 100) imageView.center = view.center view.addSubview(imageView) imageView.isUserInteractionEnabled = true imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped))) } @objc func tapped() { imageView.center = CGPoint(x: Double.random(in: 100...200), y: Double.random(in: 100...300)) } }結果
画像にイベントをつけることができました。猫がかわいいですね
解説
今回の肝になる部分はここです
imageView.isUserInteractionEnabled = true imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped)))まず、
isUserInteractionEnabled
はUIViewクラスが持っているプロパティで、ユーザーが行う操作(タップ、長押しなど)を無視するかどうかを決めるプロパティみたいです。
addGestureRecognizer
の部分はGestureRecognizerを追加するメソッドです。GestureRecognizerとは
ユーザーからのタップなどの操作を認識し、何かしらの処理を行うために用意されたクラスです。
上のコードで指定しているUITapGestureRecognizer
もUIGestureRecognizer
のサブクラスみたいですね。
その他にピンチされた時(UIPinchGestureRecognizer)やスワイプ(UISwipeGestureRecognizer)された時などにイベントを発火させたり、普段自分たちが使っているアプリのUIはこのクラスをこねくり回して元に作られてそうですね。しらんけど終わりに
GestureRecognizerを使ったら色々面白いことができそうなので、今後も触っていきたいです。
参考文献
- 投稿日:2020-12-01T21:35:49+09:00
[Swift]Firestoreのdocument取得を見やすく書けるようにしてみる
アドベントカレンダー1日目! 一番いい場所いただきました!
内容
Firestoreのドキュメントでは、データの取得の際に以下のようなコードが紹介されています.
ドキュメントはとってもわかりやすいですが、データ取得の際、毎回こう書くのは面倒だし見にくいなーと思ったので、
Firestoreのdocument取得をみやすく書けるようにしてみました。方針: 簡単なメソッドチェーンで、自分で定義したモデルクラスに落とし込まれた状態のデータを取得できるようにする
具体的にいうと、このコードを
// 単一データの取得 db.collection("cities").document("SF").getDocument { (document, error) in if let document = document, document.exists { let data = document.data() let city = self.parse(data) // 何らかの方法でparseする self.hoge(city) // 取得したデータで何かする } else { print("Document does not exist") } } // 複数データの取得 db.collection("cities").getDocuments() { (querySnapshot, err) in if let err = err { print("Error") } else { let cities = for document in querySnapshot!.documents { let data = document.data() return self.parse(data) } self.hoge(cities) } }こういう風に書けるようにします。
db.collection("cities").document("SF").fetch(as: City.self) { city in if let city = city { self.hoge(city) } else { print("error") } } // 複数データの取得 db.collection("cities").fetch(as: City.self) { cities in if let cities = cities { self.hoge(cities) } else { print("Error") } }どうやって?
Codable
に適合させたカスタムオブジェクトを作成する。- Firestoreに実装されているクラスを
拡張
する。やりかた
1. Codableに適合させたカスタムオブジェクトを作成する。
Codableについては、この記事がとても分かりやすいです(これより良く書けないし、丸投げします )
Firestoreに登録するドキュメントと、モデルクラスを対応させることで、
data(as:)
でFirestoreのデータ構造からモデルオブジェクトに自動的に変換させられます。// 単一データ取得。documentからデータのparseのところがちょっとわかりやすくなる。 db.collection("cities").document("SF").getDocument() { document, err in if let document = document, document.exists { do { let city = try document.data(as: City.self) // こういう風に書けるようになる self.hoge(city) } catch let error { print("error") } } else { print("error") } } // 複数データ取得。 db.collection("cities").getDocuments() { document, err in if let err = err print("Error") } else { let cities = querySnapshot!.documents.flatMap { document in do { return try document.data(as: City.self) // これもこう書けるようになる。 } catch let error { print("error") } } self.hoge(cities) } }
data(as: City.self)
を用いることで、コードが直感的に理解し安くなりました。
ここで気になるのは、結局毎回documentをオブジェクトに落とし混む処理を記述する必要があることです。
getDocument
やgetDocuments
で返されるものがすでに処理済みのものだったらもっと楽に書けるのに。。。(複数取得の方、長いし)2. Firestoreに実装されているクラスを拡張する。
なので、
DocumentReference
とQuery
を拡張して、いい感じに描けるようにしてみます。
それぞれ、#getDocument(as:)
と#getDocuments(as:)
をラップしただけの関数です。
fetch(as:)
を以下のように実装しました。コード!ドーン!!!import Foundation import FirebaseFirestore extension DocumentReference { func fetch<T : Decodable> (as type: T.Type, _ completion: @escaping (T?) -> Void) { getDocument { (snapshot, error) in if let snapshot = snapshot, snapshot.exists { do { let obj = try snapshot.data(as: type) completion(obj) } catch let error { print("[Error] #data(as:) throw error: \(error)") } } else if let error = error { print("[Error] #getDocument return error response: \(error)") completion(nil) } else { // !snapshot.existsのとき completion(nil) } } } } extension Query { func fetch<T : Decodable> (as type: T.Type, _ completion: @escaping ([T]?) -> Void) { getDocuments() { (snapshot, error) in if let error = error { print("[Error] #getDocument return error response: \(error)") completion(nil) } else { let docs = snapshot!.documents let objs:[T] = docs.compactMap { doc in do { return try doc.data(as: type) } catch let error { print("[Error] #data(as:) throw error: \(error)") return nil } } completion(objs) } } } }シンプルなメソッドチェーンでカスタムオブジェクトを返すことで、
以下のようにみやすく書くことができるようになりました!!!// (再掲) // 単一データの取得 db.collection("cities").document("SF").fetch(as: City.self) { city in if let city = city { self.hoge(city) } else { print("error") // } } // 複数データの取得。 db.collection("cities").fetch(as: City.self) { cities in if let cities = cities { self.hoge(cities) } else { responseError() } }注意点
注意点として、自分の紹介したコードでは、ラッパーメソッド
fetch(as:)
内でエラーを食べてしまっています。
自分はエラー種類によるロジック分岐が不要そうだったのでこのようにしていますが、必要な場合はerror戻すように修正いただければ...おわり!!
趣味で触っているFirestore と趣味で触っているSwiftで記事を書きました...
他者に指摘されない環境で学んだため、変な記述があったかもしれません...
万が一、「その書き方おかしいよ、いかんだろ」とかあったら優しく教えてくださいありがとうございました!!!
- 投稿日: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-01T20:27:43+09:00
強制ダークモード
画面を強制的にダークモードにしたい場合
iOS13以降のみ ViewController 内でこうするoverride func viewDidLoad() { super.viewDidLoad() if #available(iOS 13.0, *) { overrideUserInterfaceStyle = .dark } else { // Fallback on earlier versions } }
- 投稿日: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:42:39+09:00
Kotlin どうでしょう
Kotlin コトリン
Kotlin は JET BRAINS社が開発したオブジェクト型指向言語。
2017年に Google がAndroid開発言語として正式サポート。
元々、Android開発環境である「Android Studio」は、JET BRAINS社の IntelliJ ベース。
開発環境の言語サポートが充実している!言語仕様について
Javaと相互運用可能!
JavaからKotlin のクラス使える。
逆に、KotlinからJavaのクラスも使用可能。
(Kotlin が JVM上で動作する言語のため。)JavaコードをAndroid Studio上で 貼り付けすると、
Kotlinコードに変換できる。
(ただし、完璧ではない。ビルドが通らないことも有。)・総評
記述が簡潔にできるので、積極的に使用したい。
Swiftの文法に近いため、
iOS - Android 開発者のスキルトランスファーの敷居は下がると思われる。
(設計の思想は違う。)Android開発には、Javaを知った上でのKotlin知識が必要なため、
Javaを捨てることはまだできない。
- 投稿日: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-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-01T09:21:29+09:00
Web エンジニアが始める Server-side Swift "Vapor"
はじめに
2014年の WWDC で電撃発表された Swift は、もはや iOS アプリ開発では定番となっていますが、Web 開発界隈のエンジニアにとっては「何それ美味しいの?」という感じであり、ましてや「サーバサイドで Swift が動く!」とか言われても、どうせ最低限の事しかできず「プレステで Linux が動く!」くらいのお遊びだと思われている方も一定数いらっしゃるのでは無いでしょうか?(シランケド)とか言う私も、当時の WWDC の Swift 発表は興奮して朝まで眠れなかったくらいですが、最近は Swift からは少し離れて Web アプリ開発にどっぷりです。
そんなわけで、今回は、Web 開発者の目線で、Swift の Web アプリケーションフレームワーク Vapor を触ってみたいと思います。
Swift ってどんな言語?
2014年以前は、iOS や macOS のアプリは Objective-C という
変態言語での開発が主流でした。WWDC での突然の発表で世に出た Swift ですが、普及するには4、5年はかかると見られていた予想とは裏腹に、あっと言う間に多くのプロジェクトが Swift に移行していきました。一つの iOS プロジェクト内で Objective-C と Swift を混在して書けるというバイナリ互換が後押ししたというのもあるでしょう。さらに翌年にはオープンソース化して GitHub で公開したことによって、他のプラットフォームへの移植のハードルが下がりました。今では Linux だけでなく Windows でも動作するようになっています。
じゃあ Linux や Windows でも iOS のようなアプリが作れるのかというとそうではなく、あくまでも言語の基礎部分だけがオープンソース化しているに過ぎず、スマートフォンの画面を構成する UiKit や、マルチメディアな機能を扱う AVFoundation など iPhone 環境に特化した様々な非公開フレームワーク群が備わった環境が iOS SDK としてデベロッパーにのみ提供されており、Xcode でのアプリ開発を可能にしているわけです。
そんな Swift さんですが、Objective-C という技術的負債をバッサリ捨てたことにより、何の障壁もなく様々なプログラムバラダイムのトレンドを実装できたという背景もあって、クロージャーやジェネリクス、Optional 型、タイプセーフな構文などは他のナウい言語と共通する部分もあって学びやすい言語だと思います。
Vapor とは?
Vapor はそんな Swift で書かれた Web アプリケーションフレームワークです。Laravel (Lumen) にインスパイアされて作ったとか作らなかったとか(すみません以前公式に書かれてた気がしましたがソースが見つかりませんでした)。Web アプリケーション開発では一般的な MVC の構成で、DB接続 (PostgreSQL / MySQL / SQLite / MongoDB) はもちろん、ORM やテンプレートエンジンなどの Web に必須となる機能もエコシステムとして提供されています。
- Vapor 公式
- Vapor GitHub
Vapor は今のところ Mac と Linux で利用可能です。Mac だと Homebrew、Linux では yum や apt などでパッケージが提供されています。あくまでも実行環境ということなので docker や Homestead を使えば Windows でも開発は可能です。
余談ですが、Swift 製の Web アプリケーションフレームワークと言えば当初は IBM が開発する Kitura が有名でしたが、調べたら2020年1月に(IBMとして)開発を終了しているようでした。
- Kitura 公式
- Kitura GitHub
サーバサイドを Swift で書くメリット
Swift で iOS アプリを開発しているエンジニアにとっては、馴染みの無い PHP や Ruby に手を出してサーバサイドの実装を行うことは大きなハードルになりますが、サーバサイドも Swift で書けるということで、システム全体を得意言語でカバーできることになります。何より頭の中で言語のコンテキストをスイッチすることが無くなると言うメリットは大きいでしょう。Web エンジニア風に言えば、シングルページアプリケーションを JavaScript で書きつつ、サーバ API は node.js で実装するような「JavaScript ボーダレス」な開発環境と言えば分かりやすいんじゃないでしょうか。
では、PHP などの非コンパイル言語での開発を生業としているエンジニアにとって、Swift でサーバサイドのプログラムを実装するメリットは何でしょうか?
Swift を使ったことがなければ答えは「メリットなし」でしょう。餅は餅屋。得意言語で実装するに越したことはないです。ただ、少しでも Swift を触ったことがあれば、(もしくはこれから学ぼうという意欲があれば)、読みやすい、書きやすいで定評のある Swift を使うメリットはあると思います。個人的には Xcode での開発ができる点もメリットだと思います。Web 開発の IDE は Electron や Javas ベースのものがほとんどで、Web 開発者の多くは、メモリ食い過ぎ問題や動作もっさり問題に悩まされています。 macOS ネイティブで快適に安定動作する IDE は皆無でした。賛否はありますが、Xcode が使えるのは開発効率的にもメリットだと思います。
環境準備
言語比較やIDEの批評をすると石が飛んでくるらしいので早速 Vapor のインストールに入りましょう。
まずは事前に下記をインストール。Xcode は初回立ち上げ時に License Agreement に同意しないと Vapor のインストールで怒られるので同意しておいてください。Developer Program への登録は不要なので、AppStore からインストールしてください。
インストール
Vapor 本体は Homebrew でインストールします。下記コマンドで一発です。
$ brew install vapor完了したら vapor コマンドが使用できるので、バージョンが表示されればOKです。
$ vapor --version framework: 4.36.0プロジェクトの作成
では早速 Web アプリケーションを作成しましょう。ここでは ApiTest というプロジェクトを作成します。vapor new コマンドでプロジェクト名を渡します。
$ vapor new ApiTest Cloning template... name: ApiTest Would you like to use Fluent? (--fluent/--no-fluent) y/n>Fluent を使用するかどうかを聞かれるので、y か n を入力。
Fluent は Laravel で言うところの Eloquent のようなライブラリみたいです。とりあえず y で入れておきます。Which database would you like to use? (--fluent.db) 1: Postgres (Recommended) 2: MySQL 3: SQLite 4: MongoFluent を使用すると言うことはその後ろにデータベースがあるわけですので、次でデータベース製品を選択します。PostgreSQL が Recomended なので、1 を入力。
Would you like to use Leaf? (--leaf/--no-leaf) y/n>次は Leaf の追加。こちらは Laravel で言うところの Blade テンプレートシステムです。もちろん y を入力。
すると完了メッセージと共に下記のようなアスキーアートが表示されます。
アスキーアートと言えば、昔アドベントカレンダーで書いた自分の記事を思い出しました。全然関係ないけど。
- 【アスキーアート職人歓喜?】画像からアスキーアートを生成
Xcode の起動
プロジェクトディレクトリに移動して下記コマンドで Xcode が起動します。
$ cd cd ApiTest $ vapor xcode初回起動時は依存パッケージがダウンロードされるのでしばらく待って、左上のビルドボタン(再生ボタン)がアクティブになるまで待ってください。
依存バッケージのダウンロードが完了したら、ビルドを実行してみましょう。ショートカットキー「⌘ + R」で実行できます。
初回は時間がかかりますが、完了すると Xcode のコンソールに下記のようなメッセージが表示されます。[ WARNING ] No custom working directory set for this scheme, using /Users/shigeta/Library/Developer/Xcode/DerivedData/ApiTest-awtfxnyehqlqwxclpfyylflkbavb/Build/Products/Debug [ NOTICE ] Server starting on http://127.0.0.1:8080ちょうど npm run dev したような感じですね。ではブラウザでアクセスしましょう。
軽く構成を眺める
初期構成のファイル構成を軽く見てみましょう。
既視感があると言うかなんと言うか、もうフォルダ名のファイル名で大体わかっちゃいますよね。主要なファイルをさくっと紹介。Resources/Views/index.leaf
Leafのテンプレートファイルです。Webエンジニアであれば説明は不要でしょう。
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>#(title)</title> </head> <body> <h1>#(title)</h1> </body> </html>Sources/App/Controllers/TodoController.swift
こちらはコントローラーです。
予め TODO の CRUD 遷移が実装されています。import Fluent import Vapor struct TodoController: RouteCollection { func boot(routes: RoutesBuilder) throws { let todos = routes.grouped("todos") todos.get(use: index) todos.post(use: create) todos.group(":todoID") { todo in todo.delete(use: delete) } } func index(req: Request) throws -> EventLoopFuture<[Todo]> { return Todo.query(on: req.db).all() } func create(req: Request) throws -> EventLoopFuture<Todo> { let todo = try req.content.decode(Todo.self) return todo.save(on: req.db).map { todo } } func delete(req: Request) throws -> EventLoopFuture<HTTPStatus> { return Todo.find(req.parameters.get("todoID"), on: req.db) .unwrap(or: Abort(.notFound)) .flatMap { $0.delete(on: req.db) } .transform(to: .ok) } }Migrations/CreateTodo.swift
マイグレーションファイルもありますね。Swift 知らなくても Laravel や Rails 使ってる人であれば直感的に理解できるんじゃないでしょうか。
import Fluent struct CreateTodo: Migration { func prepare(on database: Database) -> EventLoopFuture<Void> { return database.schema("todos") .id() .field("title", .string, .required) .create() } func revert(on database: Database) -> EventLoopFuture<Void> { return database.schema("todos").delete() } }Models/Todo.swift
モデルクラスです。
import Fluent import Vapor final class Todo: Model, Content { static let schema = "todos" @ID(key: .id) var id: UUID? @Field(key: "title") var title: String init() { } init(id: UUID? = nil, title: String) { self.id = id self.title = title } }Sources/App/routes.swift
URLルート定義ファイル。URLのマッピングをここに記載。これも Web アプリケーションフレームワークではおなじみですね。
import Fluent import Vapor func routes(_ app: Application) throws { app.get { req in return req.view.render("index", ["title": "Hello Vapor!"]) } app.get("hello") { req -> String in return "Hello, world!" } try app.register(collection: TodoController()) }まとめ
Vapor はざっと見た感じ Rails や Laravel に近い構成なので、Web アプリケーションエンジニアにとっては取っ付きやすいのでは無いでしょうか。
Swift は言語的にもとてもスマートで、本当に洗練された無駄の無い言語なので、iOS アプリだけで使うのは勿体無いですが、とは言え、PHP などの軽量言語のエンジニアにとってはまだまだ敷居が高いです。特にブラウザ上で動作するメイン言語がまだまだ JavaScript 全盛なので、Web アプリエンジニアにとってはあまりメリットが感じられないかもしれません。
そうなると願いは一つですね。
いつの日か、Swift がブラウザ上で動作する時代が来ますように!
告知
猫会社 猫会社で有名な qnote は今年もアドベントカレンダーに参加しています!
応援よろしくお願いします!
qnote Advent Calendar 2020
過去の猫会社アドベントカレンダー