20210621のSwiftに関する記事は10件です。

自作パッケージをSwift Package Indexに追加する方法

「Swift Packagesによるライブラリの作成方法」は4部構成です。 記事を順番に読み進めると、Swift Packagesでライブラリを自作して公開できるようになります。 第1部: Swift Packagesでライブラリを自作する 第2部: 自作パッケージをSwift Package Indexに追加する ←イマココ 第3部: 自作パッケージをCocoaPodsに追加する 第4部: 自作パッケージをCarthageに対応する はじめに 自作パッケージをSwift Package Indexに追加する方法を紹介します。 「Swift Package Index」とは? 公式サイトのFAQを直訳すると「Swift Package Managerをサポートする、パッケージの検索エンジン」です。 パッケージを検索して、そのパッケージに関する様々な情報を得ることができるサイトです。 環境 OS: macOS Big Sur 11.4 Xcode:12.4 (12D4e) Swift:5.3.2 swift-tools:5.3 Swift Package Indexへの追加方法 Swift Package IndexへはPRを作成して手動で追加を依頼する必要があるので、その手順を紹介します。 パッケージを自作する Swift Package Indexへは他人が作成したパッケージを追加することもできるようですが、やはり自作パッケージがなければ始まりません。 以下の記事を参考にパッケージを自作します。 PackageListリポジトリをフォークする パッケージを自作したら、以下のリポジトリをフォークします。 私はフォーク元のリモートリポジトリを origin とし、フォークしたリモートリポジトリには作者(今回は自分)の名前を付けるのが好みなので、そのようにします。 $ git clone https://github.com/SwiftPackageIndex/PackageList.git $ cd PackageList $ git remote add uhooi https://github.com/uhooi/PackageList.git main ブランチから作業ブランチを切ります。 $ git switch -c feature/add_package JSONファイルに自作パッケージを追加する Package.json へ自作パッケージのURLを アルファベット順 に追加します。 Package.json [ ... "https://github.com/ucotta/brillianthtml5parser.git", + "https://github.com/uhooi/swift-string-transform.git", "https://github.com/uias/Pageboy.git", ... ] 追加したら swift ./validate.swift を実行し、JSONファイルに問題がないか確認します。 $ swift ./validate.swift Warning: Using anonymous authentication -- may run into rate limiting issues Processing package list ... + github.com/uhooi/swift-string-transform.git ✅ validation succeeded 追加したパッケージのURLと「✅ validation succeeded」が出力されたらバリデーション成功です。 PRを作成する バリデーションに成功したら、変更をコミットしてプッシュします。 $ git add Package.json $ git commit -m 'Add package' $ git push uhooi feature/add_package フォーク元のリモートリポジトリにPRを作成します。 タイトルや概要などは以下のPRを参考にしてください。 問題なければ数時間でメンバーの方がマージしてくださります。 Swift Package Indexでできること PRがマージされたらSwift Package Indexに自作パッケージのページができます Swift Package Indexでできることを抜粋して紹介します。 様々な環境でビルドしてバッジ化される 様々なプラットフォーム、Swiftのバージョン、ソース(最新タグとデフォルトブランチ)の組み合わせでビルドし、成否とログを確認できます。 CIが成功してからパッケージをリリースすることが多いとは思いますが、すべての組み合わせを網羅することはなかなかないので、非常にありがたいです。 ビルドに成功した環境がバッジ化されるので、READMEに付けるとどの環境で使えるかがすぐにわかります。 ちなみに新しいバージョンをリリースしたら自動で再ビルドしてくれます。 手動でリリースを伝える必要はありません。 同じ作者で他のパッケージを見れる Xcode 13で追加された「Package Collections」という仕組みがあり、複数のパッケージをJSONファイルにまとめることでパッケージを探しやすくなります。 Swift Package Indexでは同じ作者のパッケージをまとめたコレクションのJSONファイルを自動で作成してくれます。 その場でパッケージを試せる 「Try in a Playground」をクリックすることで、パッケージがセットアップされた状態でPlaygroundが起動し、すぐにパッケージを試すことができます。 spi-playgrounds://open?dependencies=uhooi/swift-string-transform 事前にSwift Package Index Playgrounds appをインストールする必要があります。 おわりに 自作パッケージをSwift Package Indexに追加することができました! 自分のパッケージが素晴らしいサイトに載るのはとても嬉しいです Swift Package Indexは非常に便利なサイトなので、パッケージを自作していない人もぜひ活用してみてください! 参考リンク FAQ – Swift Package Index Add a Package – Swift Package Index https://twitter.com/the_uhooi/status/1404723147509690379?s=20 https://twitter.com/the_uhooi/status/1404784236050096129?s=20
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[swift] Snippetを作成する方法

はじめに この記事は自分のメモとして書いてあります。 snippetを使うことで便利になること コードを打つ手間が省ける スペルを忘れても補完でなんとかなる ドキュメントコメントや特殊タグを書く時に便利 ドキュメントコメント参考文献 : https://qiita.com/y-some/items/8bc06567eee18dfeafbb Snippet作成 snippetとして登録したいコードを選択 今回は // MARK: - <#comment#> を選択しています。 // MARK: - <#comment#> Create Code Snippetを選択 コードを選択して、右クリックで Create Code Snippet を選択します。 登録画面から登録 登録画面が表示されます 登録画面のTittleとCompletionを入力して登録します。 Tittleは補完で表示される内容を登録します。 Completionは何を打ったら補完されるのかを登録します。 今回はTittleに// MARK:をCompletionに markを登録しました。 完成 以上で完成です。 markと入力されるだけで補完されます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] 引数の位置のsomeは本当にジェネリック引数なのか?

背景 Swift5.1でOpaque Result Typeという種類の型が追加されました。これは「制約を満たすなんらかの特定の型」を関数が返せるようにする仕組みです。その動作は「リバースジェネリクス」という概念で説明されます。 Swiftのジェネリクスの方向性についての議論がなされたForumのスレッド「ジェネリクスのUIの改善(Improving the UI of generics)」では、以下の構文が提案されています。 // この2つは同じ動作 func concatenate(a: some Collection, b: some Collection) -> some Collection func concatenate<T: Collection, U: Collection>(a: T, b: U) -> some Collection また、Opaque Result Typeの導入のプロポーザルであるSE-0244でもこの点が言及されています。 This some Protocol sugar can be generalized to generic arguments and structural positions in return types in the future 端的にいうと、ここではsomeというキーワードが次のように働きます。 引数の位置ではジェネリックな型 戻り値の位置ではOpaque Result Type この構文は「ジェネリクスの表記の軽量化」という目標を達成します。既存の構文は「威圧感が強い(intimidating)」うえに、慣習的に1文字の名前が利用されるため理解もしづらい傾向にあります。新しい構文は制約を流暢に表現するため威圧感が弱く、無理に名前をつけていないため(匿名型であるため)理解にも支障がありません。 また、someの導入時点から意図されてきたanyというキーワードとの対応も同時に目指されています。any PとはプロトコルPを用いたExistential Type、すなわち現在Pと表記される型の新しい表記で、Pに準拠する任意(any)の型の値を代入できることを示唆する表記です。any Pへの移行は正式な提案には至っていませんが、過去複数のプロポーザルで言及されるなど、半ば既定路線となっています。このany Pは引数位置でも用いることが出来るので、some Pも引数位置で用いられると対照的で綺麗です。 この記事では、引数の位置で使われるsomeが本当にジェネリック引数であるのかどうか検討します。 根拠付け 戻り値の位置ではOpaque Result Typeを示すキーワードであるsomeが引数の位置ではジェネリックな型を表すのは何故でしょうか。非常に直感的に言えば、Opaque Result Typeとジェネリック引数の振る舞いがかなり類似しているからです。 let collection: some Collection = "Hello" print(collection.hasPrefix("H")) // valueの型がStringなのは分からないのでエラー print(collection.prefix(3)) // Collectionに準拠した型はprefixを実装しているので呼び出せる func hoge<T: Collection>(_ collection: T) { print(collection.hasPrefix("H")) // valueの型がStringなのは分からないのでエラー print(collection.prefix(3)) // Collectionに準拠した型はprefixを実装しているので呼び出せる } このため、ジェネリクスを用いたコードをsomeを使って以下のように書くこともできそうです。 func hoge(_ collection: some Collection) { print(collection.hasPrefix("H")) print(collection.prefix(3)) } ここではsomeが引数の位置にあります。この引数の位置に置かれたsome Pを指してOpaque Argument Typeと呼ぶ場合があります。 「Opaque」という言葉は、このような状態を形容しています。つまり、ある型が実際に何なのかが不透明である、ということです。そのように考えると、冒頭で紹介したジェネリクスの役割をも担うsomeをもっと単純に表現することが出来ます。つまり「Opaque」であることを標識するものがsomeだということです[^1]。 Rustとの関連 私はRustがほとんど分からないので誤りを含む可能性があります。 これはRustのimplの挙動に倣った提案であると考えられます。 Swiftにおけるプロトコルにあたる機能がRustのトレイトです。しかしSwiftと異なり、トレイトをそのまま型として扱うことはできません。Box<dyn Trait>として提供されるのがSwiftのProtocolにあたり、impl Traitによって提供されるのがSwiftのOpaque Result Typeとほぼ同様の働きを持つ機能です[^5]。つまり、impl TraitはTraitを実装した何らかの決まった型を表していて、その実際の型は分からず、コンパイル時には静的に解決されます。 Rustでは引数の位置にimpl Traitが現れた場合どうなるのでしょうか。ピッチ段階にあったOpaque Result Typeのスレッドでこれについての言及があります。 We're not even going to mention the use of opaque in argument position, because it's a distraction for the purposes of this proposal; see Rust RFC 1951. ここで言及されているRFC 1951という文章で、ちょうど上記のような内容が主張されています。つまり、引数の位置のimpl Traitは、ジェネリクスと同様に振る舞う、ということです[^2]。 なお、ちょっとややこしいのですが、Rustではimpl TraitがExistential Typeと呼ばれ、SwiftではProtocolがExistential Typeと呼ばれています。この記事で以降Existential Typeという場合はSwiftの意味です。 問題 発想 さて、現在は許されませんが、そのうち次のような表現が可能になるでしょう。型の構成要素として用いられるsomeです。 let someArray: [some Numeric] = [0, 1, 2] // someArray: [Int] let someClosure: (some Numeric) -> () = { (value: Int) in print(value) } // 実際は (Int) -> () どちらも特に不思議なことはありません。someArrayはElementがOpaque Result Typeであるような配列で、someClosureはNumericに準拠した何らかの型の値を引数に取り、その値をprintするクロージャです。 脳内でsomeClosureを実行してみましょう。きっと引数の型がOpaqueなので、こんな感じで動くはずです。 let number: Int = 42 someClosure(number) // 引数の型がIntとはわからないのでエラー someClosure(.zero) // (some Numeric).zeroは存在するのでエラーにならない とはいえやはり実際に動作しているところを見たいものです。実例を持ってきましょう。 [some Numeric]と書けないからといって、そういう型を作ることが出来ないわけではありません。 let value: some Numeric = 42 let someArray = [value] // someArray: [some Numeric] 同じことを(some Numeric) -> ()でもやってみましょう。Numericだと上手くいかないので、今回はBinaryIntegerというプロトコルでやってみます。このプロトコルはisMultiple(of: Self)というメソッドの実装を要求します。 let value: some BinaryInteger = 42 let someClosure = value.isMultiple // someClosure: (some BinaryInteger) -> Bool このクロージャは、脳内で実行したのと全く同じように、引数がOpaqueになっているように動きます。 let number: Int = 42 someClosure(number) // 引数の型がIntとはわからないのでエラー someClosure(.zero) // (some BinaryInteger).zeroは存在するのでエラーにならない 困惑 以上で、someを引数の位置に含むクロージャが構成できることを確認し、その挙動は「引数の型をOpaqueにする」と形容できることがわかりました。 ところが、関数ではどうでしょうか。最初に確認したとおり、関数ではsomeを引数の位置におくとジェネリックな型を表すのでした。つまり、こういうことが起きます。 // 引数の型がOpaqueなクロージャ let someClosure: (some Numeric) -> () = { (value: Int) in print(value) } // 引数の型がジェネリックな関数 func someFunction(_ value: some Numeric) { print(value) } let number: Int = 42 someClosure(number) // エラー someFunction(number) // 問題なく動作 関数でもクロージャでも、表記が同一であれば型も同じだというのが自然な推測です。しかしここでは、クロージャであるか、関数であるかによって、引数の型が真逆に変わってしまうのです。 うまく理屈をつければこの動作を正当化することはできるでしょう[^3]。ただ、無理に理屈をつけないと説明できない言語仕様は妥当なのでしょうか。 別案 (some Numeric) -> ()が表すのは、実はジェネリックなクロージャだったかもしれません。こうすることで関数の表現と一致します。 let someClosure: (some Numeric) -> () = { (value: some Numeric) in print(value) } someClosure(42 as Int) // 42 someClosure(42.0 as Double) // 42.0 残念ながら、こうしてもやはり不自然な事態が起こります。 以下は全て有効な宣言です。動作に若干の違いはありますが、全て同じように「Opaque Result Type」として動作することが期待されます。 let x: some HogeProtocol = Hoge() var x: some HogeProtocol { return Hoge() } func x() -> some HogeProtocol { return Hoge() } もちろん以下も同様です。 // これはジェネリックなクロージャ let x: (some Numeric) -> () = /* ... */ // なのでこれも var x: (some Numeric) -> () { /* ... */ } // これも、ジェネリックなクロージャを返すべき func x() -> (some Numeric) -> () { /* ... */ } しかし、3つ目はよく考えると奇妙です。下のようにsome Numericの位置をずらす事を考えると、1つ目はジェネリックな関数、2つ目はジェネリックなクロージャ(Rank2型として)を返す関数、3つ目はOpaque Result Typeを含む関数型になります。 func x(some Numeric) -> () -> () { /* ... */ } func x() -> (some Numeric) -> () { /* ... */ } func x() -> () -> (some Numeric) { /* ... */ } 複雑に考えれば、この振る舞いを理解することは出来ます[^4]。ただ、それではそもそもの目標であった「軽量で読みやすい構文」から程遠いものになってしまいます。 まとめ 以上をまとめると、引数の位置のsomeをジェネリック引数と考えた場合、次のような問題が生じることになります。 (some P) -> ()型のクロージャをジェネリックでないクロージャと考えた場合 関数宣言と動作が一致せず、非直感的な振る舞いをすることになる (some P) -> ()型のクロージャをジェネリックなクロージャと考えた場合 振る舞いが非常に複雑になり、理解に支障を来す どちらにしても、some Pに関する直感的な理解を諦め、複雑な理由付けや場合分けを用いて納得しなければなりません。コードを書く際にも読む際にも重くのしかかる負担となり、「ジェネリクスのUIの改善」となるどころか、UIが悪化してしまうことになります。 解決策 リバースジェネリクス リバースジェネリクスという概念がOpaque Result Typeを理解するために提案されています。Opaque Result Typeはリバースジェネリックな戻り値です。 リバースジェネリクスを「実装者が型を決めるジェネリクス」と表現することがありますが、より簡潔には「外側へのジェネリクス」と表現することが出来ます。通常のジェネリクスが、外部によって決定される型を内部で利用するという意味で「内側へのジェネリクス」であるのに対し、リバースジェネリクスは内部で決定される型を外部で利用するという意味です。こう表現することで「リバース=裏返し」という言葉がよりうまく当てはまります。 こう考えると、以下のようなコードが多少わかりやすくなります。「実装者」と考えると少し混乱しそうです。 // ここから上の行は外側 let value: some Numeric = 42 // この行が内側 // ここから下の行は外側 print(value) someの役割 「内側で見るジェネリックな型」と「外側で見るリバースジェネリックな型」は同一物に見えます。どちらも実際の型は抽象化されているからです。別の言い方にすると「内側で見るジェネリックな型と外側で見るリバースジェネリックな型は共にOpaqueだ」と言えます。「Opaque」と「リバースジェネリック」が同じ意味ではないことに注意してください。ジェネリックな引数も内側から見てOpaqueですが、決してリバースジェネリックではありません。 someをジェネリクスに使おう、という提案は「内側で見るジェネリックな型」と「外側で見るリバースジェネリックな型」が同一物に見えることに注目し、これらをsomeで統一しようとするものです。 ところが既に確認したとおり、このようなsomeの利用には無理があります。UIの改善とは程遠い複雑性を孕み、解釈もかなり難しくなります。そこでsomeをジェネリクスに使うのを諦め、次のようにすることで無矛盾な統一が得られます。Opaqueか否かに関係なく、リバースジェネリクスにあたるものをsomeで表すのです。 ジェネリクス リバースジェネリクス 内側から見る Opaque some(Visible) 外側から見る Visible some(Opaque) この場合どうなるのでしょうか。some Pを引数に取る関数を考えるとき、この引数はジェネリックではなく、リバースジェネリックな型です。some Numericは外部に向かって抽象化された型なので、内部では決まった型として扱えます。この発想を即席の構文で表現すると、下のようになります。 // 引数の型がリバースジェネリックな関数 func someFunction(_ value: Int as some Numeric) { print(value) } そして、この関数の挙動はクロージャと全く同じになるはずです。つまり以下が成り立ちます。非常に直感的ではないでしょうか。 // この2つは同じ動作 let someClosure: (some Numeric) -> () = { (value: Int) in print(value) } func someFunction(_ value: Int as some Numeric) { print(value) } このとき、someの役割はリバースジェネリクスの標識であり、外部への抽象化の宣言です。そして、Opaqueであることの標識ではありません。 ジェネリクスのショートハンド しかしこれでは冒頭で挙げたsome Pをジェネリクスに導入することのモチベーションであった「ジェネリクスの軽量化」や「anyとの対応」といった目標が達成できなくなってしまいます。そこで発想を転換することでこれまで以上に綺麗な対応関係を持った構文体系を得ることが出来ます。ジェネリクスの標識にanyを用いるのです。 ジェネリクス リバースジェネリクス 内側から見る any some 外側から見る any some つまり、こうです。 // この2つは同じ動作 func concatenate(a: any Collection, b: any Collection) -> some Collection func concatenate<T: Collection, U: Collection>(a: T, b: U) -> some Collection この構文の最大の利点は、ジェネリクスとリバースジェネリクスの表記に直接対応していることです。特に、今度は戻り値のジェネリクスにもショートハンドができたことになります。 // ジェネリックな型パラメータは常にanyになる // リバースジェネリックな型パラメータは常にsomeになる func x<A: P, B: P, ^C: P = Q , ^D: P = Q>(a: A, c: C) -> (b: B, d: D) { /* ... */ } func x(a: any P, c: Q as some P) -> (b: any P, d: Q as some P) { /* ... */ } こうすることで「軽量化」「直感的な構文」「anyとの対応」「矛盾の解決」といった私たちの欲しかったものが一挙に手に入ります。 結論 引数の位置のsomeはジェネリック引数ではありません。 someはOpaqueではなくリバースジェネリクスの標識に用いた方がいいでしょう。 anyはExistential Typeではなくジェネリクスの標識に用いた方がいいでしょう。 someをOpaqueの標識と考えてジェネリック引数に用いれば、大きな混乱を招くことになるでしょう。そこでsomeをリバースジェネリクスの標識と考えることで理解しづらい挙動を取り除くことが出来ます。さらにanyをジェネリクスの標識として導入することによって綺麗な対応関係、軽量な構文、さらには混乱のない体系という欲しかったものを得られるのです。 補足 これまでの言及 実際のところ、リバースジェネリックな引数という発想は以前にも出てはいます。 例えばChris Lattner氏によるOpaque Type Aliasという提案[^7]ではリバースジェネリックな引数が書ける可能性が提示されていました。 You can take opaque types as arguments as well, because the compiler knows the identity of types on the caller side: public func extractField(a : OpaqueReturn1) -> Int { // I'm defined in the same module as OpaqueReturn1, so I know it is a string. return a.count } これに関連して、Joe Groff氏は以下のように述べています。(時系列は上の提案より前です)。 Opaque typealiases are completely independent of "opaque argument types". There is perhaps an alternative factoring of these features, where we have opaque typealiases, and then the "some" sugar is introduced uniformly for arguments (to introduce anonymous generic arguments) and returns (to introduce an anonymous opaque return typealias). 面白いことに、長い議論の中でリバースジェネリックな引数についての言及はこれくらいです。需要のなさを物語っているとも思います。 RFC 1951での言及 記事を書いている最中に気付いて大変驚いたのですが、Opaque Result Typeのピッチやプロポーザルで言及されていたRFC 1951では、私の主張と同様に、someがリバースジェネリクス、anyがジェネリクスに充てられています。 In any case, one longstanding proposal for impl Trait is to split it into two distinct features: some Trait and any Trait. Then you'd have: // These two are equivalent fn foo<T: MyTrait>(t: T) fn foo(t: any MyTrait) // These two are equivalent fn foo() -> impl Iterator fn foo() -> some Iterator // These two are equivalent fn foo<T: Default>() -> T fn foo() -> any Default impl Traitを引数位置に用いてジェネリクスとして使おうと主張するRFC 1951の議論では、someとanyが一貫してリバースジェネリクスとジェネリクスの意味で用いられています。Swiftはジェネリクスのanyとリバースジェネリクスのsomeの両方の特徴を持った概念として説明されていたimpl Traitをsome Protocolとして導入し、それをジェネリック引数として使おうとしています。なぜ? ちなみに、その後のセクションでは以下のようにも書かれています。 it's possible to make sense of some Trait and any Trait in arbitrary positions in a function signature. But experience with the language strongly suggests that some Trait semantics is virtually never wanted in argument position, and any Trait semantics is rarely used in return position. 「リバースジェネリックな引数」「ジェネリックな戻り値」のショートハンドはほとんど需要がない、というこの指摘は実際その通りだと思います。だからこそRustはsomeとanyではなくimplを導入するだけで済ませたのです。しかしimpl Protocolでもopaque Protocolでもなくsome Protocolを導入したSwiftでは、もはやこの道しかないのではないでしょうか。 this RFC also proposes to disallow use of impl Trait within Fn trait sugar or higher-ranked bounds, i.e. to disallow examples like the following: fn foo(f: impl Fn(impl SomeTrait) -> impl OtherTrait) fn bar() -> (impl Fn(impl SomeTrait) -> impl OtherTrait) While we will eventually want to allow such uses, it's likely that we'll want to introduce nested universal quantifications (i.e., higher-ranked bounds) in at least some cases; we don't yet have the ability to do so. We can revisit this question later on, once higher-ranked bounds have gained full expressiveness. 面白いことに、Fn(クロージャを示すトレイト)内部でのimpl Traitの利用は禁止され、これを高ランクの型に当てるかもしれないということになっています。Swiftで言えばany ClosureProtocol<.Argument == any P, .Result == Int>のような書き方ができるという話なので、確かに高ランクの型を意味してもおかしくないかもしれません。 Swiftでもクロージャ内でのsomeの利用を禁止できるかもしれません。そうすることで上で私が問題として挙げた不自然な振る舞いは考えずに済むでしょう。ただ、今度はクロージャ内でのsomeの利用が禁止されていること自体が理解しづらい例外になってしまいます。 Protocolのためのany Existential Typeにつけるためのキーワードがなくなってしまうのが気になるかもしれません。これについてはexistなどのキーワードがつけばいいのではないでしょうか[^6]。 不安点として型消去に用いられるAnyHogehogeやトップ型のAnyが挙げられます。someとの対応を目指すのであれば多少被ってしまったとしてもanyをジェネリクスに使った方が良さそうだと思います。 定数に用いるany someが定数の型にも使えることから、anyも定数の型で使えた方が良さそうです。これに当たる概念は既にGenerics Manifestoで言及されています。 Generic constants let constants could be allowed to have generic parameters, such that they produce differently-typed values depending on how they are used. For example, this is particularly useful for named literal values, e.g., let π<T : ExpressibleByFloatLiteral>: T = 3.141592653589793238462643383279502884197169399 anyはちょうどこの宣言と同じ意味になるはずです。つまり以下のように動くでしょう。 // この2つは同じ動作 let π<T : ExpressibleByFloatLiteral>: T = 3.141592653589793238462643383279502884197169399 let π: any ExpressibleByFloatLiteral> = 3.141592653589793238462643383279502884197169399 これが実現されれば、someとanyの構文上の対称関係がより明確になるはずです。 [^1]: 厳密には「値の主な利用者にとってOpaque」とsomeの役割を説明することができます。引数を主に利用するのは関数の内部、戻り値を主に利用するのは関数の外部だからです。 [^2]: ただし、導入には反発も強かったようです。Add RFC undo-universal-impl-trait. by phaazon · Pull Request #2444 · rust-lang/rfcs [^3]: 例えば「戻り値の位置のsomeはOpaque Result Typeで捉え、純粋に引数の位置にあるsomeのみジェネリクスで捉える」と考えれば辻褄が合います。ただ、かなり複雑です。 [^4]: 例えば「純粋に戻り値の位置のsomeのみOpaque Result Typeで捉え、引数位置にあるsomeはその場でジェネリクスと捉える」と考えれば良さそうです。ただ、高ランク型の実装が必要になりますし、直感的とはとても言えない考え方です。 [^5]: そもそも歴史的にはRustのimpl Traitを参考に導入されたのがSwiftのOpaque Result Typeです。 [^6]: 具体的なキーワードの提案ではありません。 [^7]: Opaque Result Typeの議論の中で、Alternativeとして提案されたものです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Index out of rangeをこの世から無くす方法

はじめに こんにちは@kaneko77です。 大それた記事のタイトルに今回はなりましたが、 配列の数よりも大きい数を指定すると出るあいつをもう駆除しちゃおうぜって話になります。 今回はIndex out of rangeのエラーをもう見なくて良い方法を共有していきたいと思います。 マエフリ Index out of range ってなに? 念の為の説明になります。 なんの工夫も無いただの配列があります。 let test = ["マチュピチュ", "モンサンミッシェル", "サグラダファミリア", "アンコールワット"] 配列の中身は見ての通りですが4つになります。 print(test.count) // 4 以下をやってしまうと当然...エラーになります。 要素数以上に指定してしまってるからですね。 これがIndex out of rangeが起きる原因です。 配列はこの場合だと要素は0番目から3番目までです。 print(test[4]) <以下エラー画面> 原因について 先ほど説明したように いうまでもなく配列の要素数より上の数を指定するから「そんな要素ないわ」って 怒られますよね。 これがシンプルな原因ですが、しかし頭でわかっていてもやっぱりミスってあるし プログラム的に要素の指定するってなると 余計気を配らなければいけないですよね。 解決策 ということでやっと紹介できます。 定義した要素を超えた数をどうにかしなければいけないです。 方法として何点かありますが、手っ取り早いのが要素より超えた数を指定した場合nilを 返す方法が一番楽だと思います。 コード nilを吐き出してくれるチェック用のextension作成 まず以下のコードが必要になります。 以下のコードはこちらを参考にしました。 extension Array { subscript (element index: Index) -> Element? { // MARK: 配列の要素以上を指定していたらnilを返すようにする indices.contains(index) ? self[index] : nil } } 呼び出し側 以下でnilが返すようになりました。 let test = ["マチュピチュ", "モンサンミッシェル", "サグラダファミリア", "アンコールワット"] print(test[element: 4]) // nil 上記だとnilの制御しなければいけないので以下のようにしました。 こんな感じです。 これでnilチェックが入ったコードになりました。 let test = ["マチュピチュ", "モンサンミッシェル", "サグラダファミリア", "アンコールワット"] guard let list = test[element: 4] else { print("nilだったみたいですよ。") return } // 現状はここに入ります print("受け取れたみたいです。", list) 終わりに 今回は大それたタイトルでご紹介しましたが, エラー潰しのストレスを少しは消せる技術ではないかと思います。 ここ前で見ていただきありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift で簡単多言語プレビュー Extension

はじめに Swift UI 最高ですよね! 非常に直観的で、コードとUIが直結しています Swift UI での開発時、多言語(と言っても英語と日本語だけだけど)でプレビューが表示したくなったので、簡単に書けるようにしました 通常のプレビュー こんな感じのコードのとき import SwiftUI struct ExampleView: View { var body: some View { Text("hello") .font(.title) } } struct ExampleView_Previews: PreviewProvider { static var previews: some View { ExampleView() } } 以下のように言語設定していたとして Localizable.strings (English) "hello" = "Hello"; Localizable.strings (Japanese) "hello" = "こんにちは"; プレビューは以下のようになります これだと英語のときは分かっても、日本語のときどうなるか分かりません ちゃんと .strings に設定しているか、折り返しなどでおかしくなっていないか、など心配です Extension 毎回言語を指定すると大変なので、以下のような Extension を用意します import SwiftUI extension View { func en() -> some View { self.environment(\.locale, .init(identifier: "en")) } func ja() -> some View { self.environment(\.locale, .init(identifier: "ja")) } } そして、プレビューを以下のように変更します struct ExampleView_Previews: PreviewProvider { static var previews: some View { ExampleView().en() ExampleView().ja() } } そうするとプレビューで各言語版が縦に並んで表示されます モジュール化 言語が増えてきたりすると、毎回 .en() と .ja() を書くのも面倒だし漏れそうです というわけで、以下のようにモジュール化します import SwiftUI struct MultiLanguagePreview<Content>: View where Content: View { var content: () -> Content init(@ViewBuilder content: @escaping () -> Content) { self.content = content } var body: some View { SwiftUI.Group { VStack(content: self.content).en() VStack(content: self.content).ja() } } } struct MultiLanguagePreview_Previews: PreviewProvider { static var previews: some View { MultiLanguagePreview { Text("hello") } } } プレビューを以下のように変えれば、それだけで多言語プレビューができるようになります struct ExampleView_Previews: PreviewProvider { static var previews: some View { MultiLanguagePreview { ExampleView() } } } まとめ Extension を使うことで、プレビューの記述をスッキリさせることができました 今後も余裕のあるときに便利な Extension を紹介していきたいと思います
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【社内勉強会資料】iOSアプリ開発のナレッジ全般�〜管理・設計・テスト・リリース・運用のポイント〜

この記事は何 先日行った社内勉強会のスライドを元に書き起こしたものです。 iOSアプリ開発のマネジメント、設計、テスト、リリース後の運用(維持保守)において考慮すべきポイントについて解説しました。 開発者だけでなく、マネージャやソリューションアーキテクトも意識した内容になっていますので、実装に関する細かい話は出てきません。 認識違いなどがありましたら、コメント欄で教えていただけるとありがたいです。 XcodeとiOSのバージョン追従について Xcodeの概要 Xcodeは、Apple純正で、Appleプラットフォーム※のアプリを開発するための統合開発環境。(※今回はiOSのみに話を絞る) XcodeはMacにしか入れられないのでMacは必須。 iOS SDKが内包されており、XcodeのバージョンとiOS SDKのバージョンは対応している(後述)。 iOSシミュレータが内包されており、実機がなくてもある程度のデバッグは可能。ただし加速度センサー、カメラ、マイクなど、一部のセンサー/デバイスを使うテストはできない。 SwiftとObjective-Cコンパイラが内包されており選択可能だが、新規アプリでは生産性やNull安全性によってSwift一択。 Xcodeのバージョンアップ追従 iOSがバージョンアップされると、そのSDKを含むXcodeがリリースされる。→Xcodeを上げないと新機能のAPIが使えない Xcodeのメジャーアップデートが毎年9月にあり、翌年4月以降、最新メジャーVerのXcodeでビルドしないとビルドをAppleのサーバーにアップロードできないことが慣例化。 Xcodeのバージョンアップに伴い、Swiftコンパイラのバージョンが上がって、アプリのコードを修正しなければならないことがある。また、ライブラリの対応状況の事前調査が必要。 iOSの新バージョンへの追従 似たような話題として、リリース済みアプリが、新OSバージョン下ではユーザーが期待しない挙動となってしまう場合がある。 iOS 14リリース時の事例: プライバシー保護のため、アプリがコピペ内容を読み取るとバナーで通知される機能が追加された。この影響を受けてしまい、一部のライブラリのアップデートが必要となった。 デフォルトのブラウザをユーザーが設定できる機能が追加された。 Safari以外に設定された場合に、アプリからブラウザが開けなくなってしまい、アプリ改修が必要となった。 アプリ運用保守の年間スケジュール例 開発ライセンスについて Apple Developer Program (ADP) アプリをApp Storeで配布するための開発ライセンス。 App審査あり。 個人、法人ともにライセンス取得可能。 -法人の場合、実在を証明する「D-U-N-S Number」の取得が必要。(東京商工リサーチに申請) 法人の場合、アプリを「所有する」法人自身がライセンスを取得する必要あり。受託業者は開発者としてチームに招待される形。 99 US$/年 内部テストのためのアプリ配布については後述 Apple Developer Enterprise Program (ADEP) 社内用アプリを配布するための開発ライセンス。 逆にApp Storeでの配布はできない。(配布用サーバーが必要) App審査なし。 従業員100名以上であることが条件。 「D-U-N-S Number」の取得が必要。 社外にアプリを配布することは規約違反となる。 現状、ADEPを取得するのは非常に難しい模様。この規約を守らない事例が多いため? 399 US$/年 Human Interface Guideline (HIG) Apple公式のUI/UXデザインガイド。 デザイナー向けのガイドではあるが、HIG違反はApp審査リジェクトの可能性があるため、実質「開発ルール」でもある。 このため、iOS Developerは(できればManagerやArchitectも)HIGを十分理解する必要があり、また頻繁にアップデートされるため最新情報のキャッチアップが必要。 ライブラリについて よほど機能がシンプルなアプリでない限り、OSSライブラリの利用は避けられない。一から全て自作するには工数も技術力も必要なので… FirebaseなどmBaaSのSDK(※Firebaseの紹介は後述) 実装を簡易にするためのライブラリ アニメーション、ネットワーキング、DB、Keychainアクセスなど ただし、Xcodeの項で触れた通り、ライブラリ起因で「Xcodeのバージョンアップができない」等の運用トラブルが起こりがちなので、利用ライブラリの種類とバージョンの管理が重要。 ライブラリ管理ツール CocoaPods 最も歴史が長く、対応しているライブラリが多いため無難な選択肢。 Xcodeのプロジェクト定義ファイル(XML)を直接自動編集してしまう仕様のためトラブルが発生すると解決が大変。 Carthage(カルタゴ/カーセッジ) ビルドが高速なことが売り。 対応していないライブラリもちらほらある。 Swift Package Manager Xcode 9で同梱された比較的新しめの公式ツール。 徐々に対応しているライブラリが増えてきたので新規アプリの選択肢としては大いにアリ。 Firebaseについて モバイルアプリの開発や運用のための便利なクラウドサービス。 無料(2021年6月現在)で使える鉄板サービス Analytics: アプリでのユーザー行動をデータ化し分析できる機能。 Cloud Messaging (FCM): Apple Push Notificationサービス(APNs)との中継を行なってくれるサービス(詳細は後述)。ユーザー属性と行動に合わせて作成したセグメントに対して、メッセージを送信するような使い方が可能。 Crashlytics: クラッシュレポーティング。Xcodeの標準機能よりも反映が早くて見やすい。 認証サービスやリアルタイムDBなどの便利なサービスもあるが、有料である プッシュ通知 全体的な仕組みおよびFCMを使うメリット FCMを使わない場合のバックエンドの煩雑さ ・iOSはAPNs、AndroidはFCMにそれぞれ異なるAPIでリクエストを送らなければならない。 ・バックエンドサーバーにAPNs用の証明書または認証キーを登録しなければならない。 ↓ FCMを使うことで解消される点 ・iOS、Androidとも同じAPIでリクエストを送れる。 ・APNs用の証明書または認証キーはFCMの管理コンソールで登録すれば良い。 最も基本的なペイロード 規定項目はiOSが勝手に処理するためアプリが取得する必要はない。 それ以外にアプリとして自由にカスタム項目を追加できる。 それを利用して、「この値のプッシュ通知を受け取ったら画面Xに遷移する」などの機能を実現できる。 リッチな通知 iOS 10以降では、画像・動画・音声、カスタムアクションが使える。 iOS 12以降の機能 通知センター上でのグルーピングが可能となった。 ユーザー許諾を初回の通知受信時に行うことが可能になった。 サイレントプッシュ通知 特徴 画面上には何も表示されない通知。 iOSは通知を受け取ると該当アプリをバックグラウンドでWakeupする。 アプリはこれを受けたタイミングで何かしらのバックグラウンド処理を起動することができる(サーバーから最新データを取得する等)。 通知を受けた際のフック処理 「何かしらの処理」は最大30秒間しか実行できない。 実行タイミングはiOSが判断するため(充電状態や通信状態等?) 、全く実行されない場合や、遅延される可能性がある。 バックグラウンド処理 全体感 iOSではアプリのバックグラウンド処理について制約が大きい。 実行タイミングがシステムに依存している。 処理時間が制限されている。 実行時点での環境(バッテリー状況、通信状況など)によって実行されない場合もある。 ユーザーが「設定」appにてオプトアウトできる。 定型的なバックグラウンド処理 オーディオ再生、AirPlay、Picture in Picture 通話、通話用のプッシュ通知 サイレントプッシュ通知(※) 位置情報の取得(※) 外部アクセサリやBluetooth機器との通信(※) (※)ユーザーの許諾が必要なもの 任意のバックグラウンド処理 iOS 12以前 Background Task Completion フォアグラウンドでやり残した作業をバックグラウンド状態でも引き継いで実行させることが可能。 実行可能時間は不明(最大30秒?)。 Background Fetch SNSアプリのタイムライン事前取得等を想定した機能。 ユーザによるオプトアウトが可能。「設定」app>対象アプリ>「Appのバックグラウンド更新」 OSによってスケジューリングされ、遅延されたり実行されない場合も。 端末や該当アプリの利用傾向をOSが自動判定してタイミングを決めているらしい。 実行可能時間は30秒。 iOS 13以降 iOS 13で“BackgroundTasks Framework”が登場。 Background App Refresh Tasks Background Fetchの後継であり、基本は変わらず。 Background Processing Tasks iOS 13から利用可能な新機能 比較的重たい処理を想定し、数分間の実行時間が許されている。 例えば、Core MLを利用した機械学習のトレーニング処理などを想定。 アプリ側で条件設定が可能(Wi-Fi接続時とか、充電中とか) ただし、条件を満たしても確実に実行される保証はない。 ローカルデータストア Key-Value Store UserDefaults アプリ内部に保存される アプリを削除すると値が消失する 平文で保存される →センシティブ情報の保存には向かない Keychain デバイスのiCloud Keychainに保存される アプリを削除しても値は無くならない(再インストールしたら復活する) 暗号化され保存される+プロビジョニング・プロファイル(※)によって読み出し保護されている。 (※)開発者証明書とアプリIDによって正規のアプリであることを担保するためのプロファイル Database CoreData iOS SDK同梱 SQLiteのwrapper 暗号化したければOSSライブラリが必要。 Realm(レルム) →個人的におすすめ 鉄板のOSS オブジェクトデータベースで、エンティティをSwiftの型で定義できる。 永続化するかオンメモリーだけで保持するかを簡単に切り替えられる。 暗号化機能がある。 Webブラウジング SFSafariViewController ブラウザアプリに飛ばすよりもアプリと一体感がある。 見た目を変える、ボタンを追加する等のカスタマイズはほとんどできない。 イベントのフックなどもほとんどできない。 アプリからSFSafariViewControllerに認証情報は渡せない(IFがない)。つまり、アプリ側でサービスにログインしても、Web側のコンテンツを開くときに再ログインが必要になるので設計上の工夫が必要。 ドメイン名が表示されるのでオープンリダイレクト対策効果がある。 WKWebView 見た目を変える、ボタンを追加する等のカスタマイズが可能。逆にデフォルトはノッペラボーなので、閉じるボタン、ローディングインジケーター等、全て実装が必要。 URLやドメイン名は見えない。必要なら実装が必要。 Viewの一部に埋め込むこともできる。 イベントのフックが可能(リクエスト時、ロード開始時、ロード終了時などなど)。 Swiftコードで要素を拾うことやJSを注入することも可能。 認証Cookieを渡すことも可能。 ただし、iOSに内包されているWebKitの影響を受けて、iOSのバージョンアップ後に以前とは違った挙動をしたりする等のトラブルがある。 ご利用は計画的に。 テスト TestFlight ADPライセンスを取得すると、“App Store Connect”に、アプリの公開のための情報を登録することができるようになる。 App Store Connectにはテストのための“TestFlight”機能がある。 テスターのApple IDを登録できる。 開発者がアプリのビルドをアップロードすると、テスターのiOSデバイスのTestFlightアプリからテストアプリをダウンロード可能になる。 Jenkins, Bitrise, CircleCIなどを利用してCI/CDも可能。 テスターの種類 内部テスター App Store ConnectのロールはAccount Holder(アカウントホルダー)、Admin(管理者)、App Manager(App管理者)、Developer(デベロッパ)、Marketing(マーケティング)で、最大100人まで。 内部テスターは、上記のメンバーを設定できる。 各自最大30台のデバイスをテスト用デバイスとして登録できる。 外部テスター 最大10,000人まで、Eメールやリンクによって招待できる。 未リリースのアプリは、簡易的な審査を受けなければ、外部テストはできない。 テスト用ビルドは90日で無効になり、起動できなくなる。 テストのTips 本番用のアプリとはBundle ID(アプリID)を変えて、テスト用のアプリを別に作り、開発中はそちらを使ってテストした方が良い。 プッシュ通知など、間違えて本番向きに飛ばしてしまうと事故になってしまう。 クラッシュレポートに開発中/本番のログが混在してしまう。 テスト用アプリのみ、サーバーの接続先変更機能を持たせる。 リリース直前の最終検証フェーズから本番アプリに切り替える、などの運用が良い。 ただし、本番/テストビルドの切り替えを自動化するために、設定ファイルによるDI、ビルドスクリプト、あるいはブランチ運用などの手法の検討が必要。 自動テスト XCTest ロジックのユニットテスト。 Swiftでテストコードを記述する。要はxUnit。 XCUITest 自動シナリオテスト。Swiftで「実行ボタンをtapする」「アラートに◯◯という文字列が表示されること」というようなテストコードを書くと、シミュレータ上で実際に動作してテストが行われる。 テストコードからテストシナリオを読み取ることが難しく、メンテナンスがおざなりになってテストが陳腐化しがち。 実行時間が長い。テストシナリオが100を超えると1〜2時間以上は掛かる。 マネジメント層は自動テストに夢を抱きがちだが、現実はそんなに簡単ではない。 自動テストの導入には、戦略、計画、合意が重要。 App審査提出 大まかな流れ App Store Connectにアプリの基本情報(アプリID、説明文、検索キーワード、個人情報収集の説明etc)を登録する。 App Store上に表示させるアイコン画像を作成し登録する。 同、スクリーンショット画像(または動画)を作成し登録する。 プライバシーポリシー用Webページを作成し、リンクを登録する。 サポート用Webページを作成し、リンクを登録する。 アプリのビルドをアップロードしビルド番号を指定してsubmit。 主にプロダクトオーナーとデザイナーに負荷が掛かるフェーズ。 App審査の注意点 App Store Reviewガイドラインは一度目を通したほうが良い。 審査は結局レビューアの主観なので、今まで指摘されなかった問題を急に指摘されリジェクトを受けることもある。 審査にかかる日数は以前と比較すると短縮される傾向がある。 2020年から、バグ修正の場合は法的問題以外はガイドライン違反があってもリジェクトされず公開できることになった。 リリース計画には、審査リジェクトのリスクを考慮する必要あり。 実際に遭遇したリジェクト事例 非公開APIを使用している。 使用禁止となった旧APIを使用している。 “Android”のような、他のモバイルプラットフォームに関連する単語を表示している。 「ボタンtapでSafariに飛ばす」は“Minimum Functionality”なのでSFSafariViewControllerを使え。 App Store Connect上で入力したバージョンアップ説明文にて未発表のApple製品名に言及している。 機能を見られずレビューできないので動画を添付せよ。 おわりに iOSアプリ開発に携わる方にとって大事なこと Appleのポリシーを知ること。 Appleの最新動向をキャッチアップすること。 問題に突き当たったときは、まず公式ドキュメントを調べること。 公式ではないWeb記事等を参考にするときは、記事の執筆日時をチェックするクセをつけること(3年以上前の記事は当てにならないことが多い)。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(3/3 ライブラリ使い方編)

iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(1/3 考え方編) iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(2/3 実装方針編) iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(3/3 ライブラリ使い方編) ← いまここ 前置き 前回、前々回とモバイルアプリにおける状態管理の一つの考え方と、コードに落とし込むための実装方針、そしてなぜこのライブラリを作成したのかについてお話してきました。 今回は純粋に作成したStoreFlowableライブラリでどういった機能を提供しているかと、その使い方について紹介したいと思います。 Dart版も同等の機能を提供していますが、まだドキュメントやサンプルコード、テストなどが準備できていないので本記事ではKotlin版とSwift版を元に紹介します。 サンプルコード それぞれのリポジトリにはサンプルコードが含まれています。Kotlin版 / Swift版 下記の使い方と合わせて見ていただくと良いかと思います。 導入 Kotlin版はMavenCentralで配信しているので、Gradleの依存に以下を追加して下さい。 Kotlin dependencies { implementation("com.kazakago.storeflowable:storeflowable:x.x.x") } Swift版はSwift Package Managerでの導入が可能です。 Swift dependencies: [ .package(url: "https://github.com/KazaKago/StoreFlowable.swift.git", from: "x.x.x"), ], 基本的な使い方 1. FlowableDataStateManagerを継承したシングルトンクラスを作成する まず、FlowableDataStateManager<KEY>継承クラスをシングルトンクラスとして作成して下さい。 このクラスが前回の記事でも解説している、状態を通知できる仕組みを内包しています。 Kotlin object UserStateManager : FlowableDataStateManager<UserId>() Swift class UserStateManager: FlowableDataStateManager<UserId> { static let shared = UserStateManager() private override init() {} } 2. StoreFlowableFactory<KEY, DATA>を実装したクラスを作る まず、リモートからデータを取得するApiクラスとローカルキャッシュへのデータ入出力Cacheクラスを用意してください。 ここでは便宜上UserApiクラスとUserCacheクラスとします。 次にStoreFlowableFactory<KEY, DATA>の実装クラスを作ります。 このクラスがデータごとに処理が変わる、共通化部分としてまとめることが出来ない部分を記述したクラスとなります。 以下に例を示します。 Kotlin // StoreFlowableFactoryを実装したクラスを作成してください。 // 同一のデータが複数存在する場合はジェネリクスのKEYにその区別となる型を指定して下さい。データが一つの場合はUnitでOKです。 // DATAジェネリクスには扱うデータの型を指定して下さい。 class UserFlowableFactory(userId: UserId) : StoreFlowableFactory<UserId, UserData> { private val userApi = UserApi() private val userCache = UserCache() // データが複数存在する場合はその区別となるデータを渡して下さい。 override val key: UserId = userId // 作成したFlowableStateManagerのシングルトンインスタンスを指定して下さい。 override val flowableDataStateManager: FlowableDataStateManager<UserId> = UserStateManager // ローカルキャッシュからの取得処理を実装して下さい override suspend fun loadDataFromCache(): UserData? { return userCache.load(key) } // ローカルキャッシュへの保存処理を実装して下さい override suspend fun saveDataToCache(data: UserData?) { userCache.save(key, data) } // リモートからの取得処理を実装して下さい override suspend fun fetchDataFromOrigin(): FetchingResult<UserData> { val data = userApi.fetch(key) return FetchingResult(data = data) } // キャッシュが有効かどうかを判断する処理を実装して下さい。キャッシュが期限切れする必要がなければ常にfalseを返してしまってもOKです。 override suspend fun needRefresh(cachedData: UserData): Boolean { return cachedData.isExpired() } } Swift // StoreFlowableFactoryを実装したクラスを作成してください。 // 同一のデータが複数存在する場合はassosiatedTypeのKEYにその区別となる型を指定して下さい。データが一つの場合はUnitHashを指定して下さい。 // DATA assosiatedTypeには扱うデータの型を指定して下さい。 struct UserFlowableFactory : StoreFlowableFactory { typealias KEY = UserId typealias DATA = UserData private let userApi = UserApi() private let userCache = UserCache() init(userId: UserId) { key = userId } // データが複数存在する場合はその区別となるデータを渡して下さい。 let key: UserId // 作成したFlowableStateManagerのシングルトンインスタンスを指定して下さい。 let flowableDataStateManager: FlowableDataStateManager<UserId> = UserStateManager.shared // ローカルキャッシュからの取得処理を実装して下さい func loadDataFromCache() -> AnyPublisher<UserData?, Never> { userCache.load(userId: key) } // ローカルキャッシュへの保存処理を実装して下さい func saveDataToCache(newData: UserData?) -> AnyPublisher<Void, Never> { userCache.save(userId: key, data: newData) } // リモートからの取得処理を実装して下さい func fetchDataFromOrigin() -> AnyPublisher<UserData, Error> { userApi.fetch(userId: key).map { data in FetchingResult(data: data) }.eraseToAnyPublisher() } // キャッシュが有効かどうかを判断する処理を実装して下さい。キャッシュが期限切れする必要がなければ常にfalseを返してしまってもOKです。 func needRefresh(cachedData: UserData) -> AnyPublisher<Bool, Never> { cachedData.isExpired() } } <KEY>の利用するシーンとしては、例えばGET /users/{user_id}/reposのようなREST APIがある場合などに、UserIdごとにキャッシュを保持しておきたいケースなどに使用して下さい。 このような場合分けが不要な場合は<KEY>にKotlin版ではUnitを指定して下さい。SwiftではUnitHashというstructを作成してあるのでそちらを指定して下さい。 3. Repositoryクラスを作成する ここまででStoreFlowableを利用するための準備は整っているので、Repositoryパターンを体現したクラスを作成します。 2.で作成したクラスのインスタンスに生えているcreate()メソッドからStoreFlowableクラスを作成できます。 このクラスが本ライブラリの本体となり、データの監視や入出力を司るメソッドが生えています。 Kotlin class UserRepository { fun followUserData(userId: UserId): Flow<State<UserData>> { val userFlowable: StoreFlowable<UserId, UserData> = UserFlowableFactory(userId).create() return userFlowable.publish() } suspend fun updateUserData(userData: UserData) { val userFlowable: StoreFlowable<UserId, UserData> = UserFlowableFactory(userData.userId).create() userFlowable.update(userData) } } Swift struct UserRepository { func followUserData(userId: UserId) -> AnyPublisher<State<UserData>, Never> { let userFlowable: AnyStoreFlowable<UserId, UserData> = UserFlowableFactory(userId: userId).create() return userFlowable.publish() } func updateUserData(userData: UserData) -> AnyPublisher<Void, Never> { let userFlowable: AnyStoreFlowable<UserId, UserData> = UserFlowableFactory(userId: userId).create() return userFlowable.update(newData: userData) } } データの入出力を行う際には必ずこのStoreFlowableを経由して行って下さい。 データの実体を直接書き換えてしまうと変更通知が行われません。 データの監視を行いたい場合はpublish()を用いて下さい。 このメソッドを通すことで考え方編で当初目指していたRepositoryクラスのインターフェースを提供することが出来ます。 4. 作成したRepositoryクラスを利用する 作成したリポジトリクラスを利用するには利用側(ActivityやViewController、ViewModelなど)で監視するメソッドを実行します。 Kotlin版であればFlowによる通知なのでcollect {}, Swift版であればCombineによる通知なので sink {}を利用してデータ監視を開始できます。 また、返却されるStateクラスやStateContentクラスはdoActionメソッドで状態の分岐が可能です。 データの状態(停止状態・取得中状態・エラー状態)とデータの有無(存在する・存在しない)の組み合わせの最大6パターンの分岐を表示上網羅することができれば、データが将来的にいかなる状態になってもカバーできます。 Kotlin private fun subscribe(userId: UserId) = viewModelScope.launch { userRepository.followUserData(userId).collect { it.doAction( onFixed = { ... // 停止状態 }, onLoading = { ... // 取得中状態 }, onError = { exception -> ... // エラー状態 } ) it.content.doAction( onExist = { userData -> ... // データが存在する }, onNotExist = { ... // データが存在しない } ) } } Swift private func subscribe(userId: UserId) { userRepository.followUserData(userId: userId) .receive(on: DispatchQueue.main) .sink { state in state.doAction( onFixed: { ... // 停止状態 }, onLoading: { ... // 取得中状態 }, onError: { error in ... // エラー状態 } ) state.content.doAction( onExist: { userData in ... // データが存在する }, onNotExist: { ... // データが存在しない } ) } .store(in: &cancellableSet) } ここまでが基本的な使い方となります。 適切に使えば表示の不整合を解消しつつ、リモートとキャッシュの抽象化が実現できているはずです。 その他の機能 State<T>が不要な一度きりのデータ取得を行いたい場合 ここまでデータを監視して変化を受け取れることを前提にお話してきましたが、常にデータ監視が適切とは限りません。 その瞬間のデータが一度だけ必要で継続的な監視を必要としない場合はgetData()あるいはrequiredData()メソッドを使って下さい。 Kotlin interface StoreFlowable<KEY, DATA> { suspend fun getData(from: GettingFrom = GettingFrom.Both): DATA? suspend fun requireData(from: GettingFrom = GettingFrom.Both): DATA } Swift public extension StoreFlowable { func getData(from: GettingFrom = .both) -> AnyPublisher<DATA?, Never> func requireData(from: GettingFrom = .both) -> AnyPublisher<DATA, Error> } requiredData()は有効なキャッシュが存在せず、リモートからのデータ取得にも失敗した場合は例外を投げます。 getData()では例外の代わりにnull, nilを返します。 引数のGettingFromはどこからデータを取得するかを指定します。 デフォルトは両方からよしなに取得する.Bothですが、キャッシュからのみ取得する.Cacheと、リモートからのみ取得する.Originを指定することも出来ます。 enum class GettingFrom { Both, Origin, Cache, } 画面上でデータの取得から表示を行う場合は基本的にはpublish()によるデータ監視の仕組みを利用して下さい。 requiredData(), getData()についてはデータ監視と相性の悪い場合のみ使用して下さい。 データを強制的に更新する 通常の使い方ではキャッシュが無効にならない限りはリモートから新しいデータは取得しませんが、要件によっては監視を開始するタイミング(画面を開いたときなど)でデータの更新を強制的に行いたい場面もあると思います。 その場合はpublish()メソッドのforceRefresh引数にtrueを指定して下さい。 Kotlin interface StoreFlowable<KEY, DATA> { fun publish(forceRefresh: Boolean = false): Flow<State<<DATA>> } Swift public extension StoreFlowable { func publish(forceRefresh: Bool = false) -> AnyPublisher<State<DATA>, Never> } また、監視開始のタイミングではなく任意のタイミングでデータの更新をしたい場合はrefresh()を用いることも可能です。 引っ張って更新の機能などを提供する場合に利用して下さい。 Kotlin interface StoreFlowable<KEY, DATA> { suspend fun refresh(clearCacheWhenFetchFails: Boolean = true, continueWhenError: Boolean = true) } Swift public extension StoreFlowable { func refresh(clearCacheWhenFetchFails: Bool = true, continueWhenError: Bool = true) -> AnyPublisher<Void, Never> } clearCacheWhenFetchFailsはリモートからのデータ取得失敗時にローカルキャッシュを消去するか、continueWhenErrorはすでにエラー状態のときにデータ取得を継続するかどうかを変更できます。 いずれもtrueがデフォルトの振る舞いとなります。 キャッシュデータが有効か検証する 現時点で保持するデータが有効か検証したい場合はvalidate()が利用できます。 キャッシュが無効であればリモートからの再取得処理が実行されます。キャッシュが有効であれば何もしません。 Kotlin interface StoreFlowable<KEY, DATA> { suspend fun validate() } Swift public protocol StoreFlowable { func validate() -> AnyPublisher<Void, Never> } キャッシュデータを更新する なんらかの処理の都合でキャッシュデータを更新する必要がある場合はupdate()メソッドが使用できます。 null, nilを指定することでキャッシュデータの削除も可能です。 このメソッドからキャッシュを更新することで、データの変更通知が発火しすべてのデータ監視者にデータの変更が反映されます。 Kotlin interface StoreFlowable<KEY, DATA> { suspend fun update(newData: DATA?) } Swift public protocol StoreFlowable { func update(newData: DATA?) -> AnyPublisher<Void, Never> } State<T> 関連オペレーター State<T>に内包されるデータをストリーム内で触りたい場合に便利なオペレータをいくつか用意しています。 Flow<State<T>>、AnyPublisher<State<T>, Never>の変換 Flow<State<T>>、AnyPublisher<State<T>, Never>のストリーム内でデータを別のデータに置き換えたい場合にはmapContent()という関数を利用できます。 Kotlin val flowState: Flow<State<Int>> = ... val flowMergedState: Flow<State<String>> = flowState1.mapContent { value -> value.toString() } Swift let statePublisher: AnyPublisher<State<Int>, Never> = .. let mergedStatePublisher: AnyPublisher<State<String>, Never> = statePublisher1.mapContent { value in String(value) } Flow<State<T>>、AnyPublisher<State<T>, Never>の統合 複数のFlow<State<T>>>、AnyPublisher<State<Int>, Never>を統合したい場合は、Kotlinの場合はcombineState()、Swiftの場合はzipState()が利用できます。 Kotlin val flowState1: Flow<State<Int>> = ... val flowState2: Flow<State<Int>> = ... val flowMergedState: Flow<State<Int>> = flowState1.combineState(flowState2) { value1, value2 -> value1 + value2 } Swift let statePublisher1: AnyPublisher<State<Int>, Never> = .. let statePublisher2: AnyPublisher<State<Int>, Never> = .. let mergedStatePublisher: AnyPublisher<State<Int>, Never> = statePublisher1.zipState(statePublisher2) { value1, value2 in value1 + value2 } 片方のステータスがLoadingやErrorだった場合は全体がLoadingやErrorとして扱われるのでご注意下さい。 状態の優先度は Error > Loading > Fixed となります。 ページネーションサポート APIから取得したリモートのデータとローカルキャッシュのデータをうまくやりくりしないといけない一般的なユースケースの一つとしてページネーションがあります。 下記のようにリストの最下部に達したときに追加をAPIから読み込んで繋ぎ合わせるような仕組みです。 これに関してもRepositoryよりも外側でキャッシュを意識せずに利用するための追加クラスを提供しています。 1. PaginatingStoreFlowableFactoryを実装する この機能を使うにはStoreFlowableFactoryの代わりにPaginatingStoreFlowableFactoryを実装したクラスを作成して下さい。 基本的には同じですがsaveAdditionalDataToCache()とfetchAdditionalDataFromOrigin()を追加で実装する必要があるという部分が違いとしてあります。以下に実装例を示します。 Kotlin class UserListFlowableFactory : PaginatingStoreFlowableFactory<Unit, List<UserData>> { private val userListApi = UserListApi() private val userListCache = UserListCache() override val key: Unit = Unit override val flowableDataStateManager: FlowableDataStateManager<Unit> = UserListStateManager override suspend fun loadDataFromCache(): List<UserData>? { return userListCache.load() } override suspend fun saveDataToCache(newData: List<UserData>?) { userListCache.save(newData) } // 追加読み込みを行ったデータをキャッシュデータとどう繋ぎ合わせるかを定義して下さい override suspend fun saveAdditionalDataToCache(cachedData: List<UserData>?, newData: List<UserData>) { val mergedData = (cachedData ?: emptyList()) + newData userListCache.save(mergedData) } override suspend fun fetchDataFromOrigin(): FetchingResult<List<UserData>> { val fetchedData = userListApi.fetch(1) return FetchingResult(data = fetchedData, noMoreAdditionalData = fetchedData.isEmpty()) } // 追加読み込みを行う際のリモートからのデータの取得処理を記載して下さい。 // これ以上追加のデータが存在しないことがわかっている場合は戻り値のnoMoreAdditionalDataにtrueを指定して下さい override suspend fun fetchAdditionalDataFromOrigin(cachedData: List<GithubOrg>?): FetchingResult<List<GithubOrg>> { val page = (cachedData?.size ?: 0) / 10 + 1 val fetchedData = userListApi.fetch(page) return FetchingResult(data = fetchedData, noMoreAdditionalData = fetchedData.isEmpty()) } override suspend fun needRefresh(cachedData: List<UserData>): Boolean { return cachedData.last().isExpired() } } Swift struct UserListFlowableFactory : PaginatingStoreFlowableFactory { typealias KEY = UnitHash typealias DATA = [UserData] private let userListApi = UserListApi() private let userListCache = UserListCache() let key: UnitHash = UnitHash() let flowableDataStateManager: FlowableDataStateManager<UnitHash> = UserListStateManager.shared func loadDataFromCache() -> AnyPublisher<[UserData]?, Never> { userListCache.load() } func saveDataToCache(newData: [UserData]?) -> AnyPublisher<Void, Never> { userListCache.save(data: newData) } // 追加読み込みを行ったデータをキャッシュデータとどう繋ぎ合わせるかを定義して下さい // これ以上追加のデータが存在しないことがわかっている場合は戻り値のnoMoreAdditionalDataにtrueを指定して下さい func saveAdditionalDataToCache(cachedData: [UserData]?, newData: [UserData]) -> AnyPublisher<Void, Never> { let mergedData = (cachedData ?? []) + newData return userListCache.save(data: mergedData).map { data in FetchingResult(data: data, noMoreAdditionalData: data.isEmpty) }.eraseToAnyPublisher() } func fetchDataFromOrigin() -> AnyPublisher<FetchingResult<[UserData]>, Error> { userListApi.fetch(page: 1) } // 追加読み込みを行う際のリモートからのデータの取得処理を記載して下さい。 func fetchAdditionalDataFromOrigin(cachedData: [UserData]?) -> AnyPublisher<FetchingResult<[UserData]>, Error> { let page = ((cachedData?.count ?? 0) / 10 + 1) return userListApi.fetch(page: page).map { data in FetchingResult(data: data, noMoreAdditionalData: data.isEmpty) }.eraseToAnyPublisher() } func needRefresh(cachedData: [UserData]) -> AnyPublisher<Bool, Never> { cachedData.last.isExpired() } } このFactoryクラスから、create()で本体となるPaginatingStoreFlowableクラスを作成することができます。 2. requestAdditionalData()で追加読み込みを行う PaginatingStoreFlowableも通常のStoreFlowableクラスと使い方にほとんど変わりはありませんが、追加読み込みを行うためのメソッドが追加されています。 Kotlin interface PaginatingStoreFlowable<KEY, DATA> { suspend fun requestAdditionalData(continueWhenError: Boolean = true) } Swift public extension PaginatingStoreFlowable { func requestAdditionalData(continueWhenError: Bool = true) -> AnyPublisher<Void, Never> } このメソッドを呼ぶことで自動的にデータがつなぎ合わされた状態で通知されます。 すでに読込中の状態で連続でこのメソッドが呼ばれても、前回解説したとおり多重にAPIがリクエストされてしまうことはありません。 ゆえに画面側で読込中かどうかを判定して、メソッドを呼ぶかどうかを分岐させる処理は不要です。 繋ぎ合わされたデータは毎回まとめて通知されるため、実際に画面上にリスト表示する際にはRecyclerViewやUITableViewなどに対応する差分更新機能(DiffUtilやUITableViewDiffableDataSource)などを用いて描画更新してあげて下さい。 一連の記事のまとめ ここまで様々な視点から長々と解説とライブラリの紹介をしてきました。 もちろん今回紹介したライブラリを使って頂いても構いませんが、最も大事なのは状態管理に対する考え方であり、アプリ内におけるデータの不整合が出づらく、ローカルキャッシュに振り回されない仕組みが用意できていることです。 それが達成されてさえいればどのような仕組みのでも構わないと思います。 データ取得処理のインターフェースをなるべく早く安定させて、変わらないようにしておくこともとても大事です。 その際に技術的詳細を隠蔽したり、取得先を抽象化することを意識してみてください。 一年後〜数年後に、きっと状態管理が少しだけ楽になっているはずです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(2/3 実装方針編)

iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(1/3 考え方編) iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(2/3 実装方針編) ← いまここ iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(3/3 ライブラリ使い方編) 前置き 前回「iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(1/3 考え方編)」という記事を書きました。 今回はその考え方をどうやって実際のコードに落とし込んでいくのかを整理しながら紹介するとともに、実際にSwift/Kotlin/Dartそれぞれ向けにリファレンスライブラリを作成したので合わせて紹介します。 記事内のコードはKotlinとSwiftの2つで記載します。 作成したリファレンスライブラリの紹介 前回・今回の記事の思想を元に実装したライブラリをKotlin版、Swift版、Dart版にわけて作成しました。 いずれもApache License 2.0にて提供しています。 上記ライブラリ群の提供する機能やインターフェースはほとんど同一です。 このことからも記事でお話する内容は特定の言語やフレームワークに依存するものではないことがわかるかと思います。 リファレンスとはいえ、Kotlin版については私が担当する数十万人が利用するAndroidアプリで実際に運用しているライブラリでもあります。 最初に申し上げておくと弊社プロダクトに必要な要件向けにライブラリまで落とし込んでみた一例であり、あらゆるシーンに対応するものではありません。 しかしながら、API通信とアプリ内キャッシングを行う一般的なアプリのユースケースでは十分だと考えています。 細かい思想や実装方針はいいからライブラリの使い方と使い勝手が知りたいんだと言う方は3/3 ライブラリ使い方編記事を御覧ください。 複雑さの軽減に必要な要素 前回の記事にてデータの取得先の抽象化や通知の仕組みが状態管理の複雑さの軽減につながるという話をしました。 ここで整理した内容を再度書き出してみます。 データの状態を表現できる構造が存在すること データに変更があった場合は教えてくれる仕組みを用意する(Observerパターンの概念) 使う側からキャッシュかAPIかなどどこから取ってきているかを意識させず整合性の取れた値を返す(Repositoryパターンの概念) 出来る限り早く値を返却する 使う側でデータを変数などで保持せず、取得先を常に1箇所に絞る(Single Source of Truthの概念) 最終的なインターフェース データ取得のための最終的な出力としては以下のような継続的に変更を受け取れる形がよさそうだという話も前回しました。 今回はこの形を吐き出すコードを実装を目指します。 Kotlin+Coroutines interface MyRepository { fun followUser(): Flow<State<User>> } Swift+Combine protocol MyRepository { func followUser() -> AnyPublisher<State<User>, Never> } ※今回はState自体がErrorの情報を持ちうるため、AnyPublisherの第2ジェネリクスはNeverを指定しています。 1. データの「状態」を扱う まずは整理した要素の「1. データの状態を表現できる構造が存在すること」をコードで表現してみます。 これは前回の記事に記載した通りですが、データの有無とは別にデータの状態を取り扱えるようにしておいたほうが良いです。 具体的には「停止状態(Fixed)・取得中状態(Loading)・エラー状態(Error)」の3つあればモバイルアプリの要件としては十分だと思います。 以下のようにデータの状態とデータの実体を組み合わせたState<T>というデータの箱を考えてみます。 Kotlin sealed interface State<out T> { val content: StateContent<T> data class Fixed<out T>(override val content: StateContent<T>) : State<T> data class Loading<out T>(override val content: StateContent<T>) : State<T> data class Error<out T>(override val content: StateContent<T>, val exception: Exception) : State<T> } sealed interface StateContent<out T> { data class Exist<out T>(val rawContent: T) : StateContent<T> class NotExist<out T> : StateContent<T> } Swift enum State<T> { case fixed(stateContent: StateContent<T>) case loading(stateContent: StateContent<T>) case error(stateContent: StateContent<T>, error: Error) } enum StateContent<T> { case exist(rawContent: T) case notExist } この実装例でデータの有無を?(Null Safety)ではなくsealed classやenumで表現しているのはわざとで、データが無いケースが存在するというのを強く意識してUIを実装してほしいがためです。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) 2. Observerパターンの構築 次に「2. データに変更があった場合は教えてくれる仕組みを用意する」について実装を考えます。 データを保持して変化したときに通知できる仕組み、いわゆるObserverパターンを構築します。 KotlinではCoroutinesに含まれるStateFlowを、SwiftではCombineに含まれるCurrentValueSubjectを用いることでこの仕組みは簡単に実現できます。 RxJava,RxSwiftではBehaviorSubjectと呼ばれているものがこれに相当します。 Kotlin val observableData = MutableStateFlow<String>("initial_data") observableData.collect { data -> println("Data is changed: $data") } observableData.emit("new_data") Swift let observableData = CurrentValueSubject<String, Never>("initial_data") observableData.sink { data in print("Data is changed: \(data)") } observableData.send("new_data") 色々必要なコードは省いていますが、Swift・Kotlinともにこのようなコードで通知を記述するとデータを購読したタイミングで"initial_data"が通知され、emit,sendした時点で"new_data"を購読者に伝えることが出来ます。 通知の仕組みとデータの保存処理の分離 データを通知する仕組み自体は比較的簡単ですが、今回実装したライブラリのコードではデータの実体をこのような仕組みでは保持していません。 これはデータの実体の保持の仕組みをStateFlowやCurrentValueSubjectの振る舞いにロックインさせてしまうことを避けるためです。 StateFlowやCurrentValueSubjectに直接データをもたせるのは直感的ですが、これではアプリのキル時にデータが揮発してしまいます。 ではこの通知の仕組みを維持したままデータを永続化したい場合はどうすればよいでしょう? すでにAndroidのSharedPreferencesやiOSのCoreDataを使っていた場合は?これから使いたい場合は? それぞれのアプリの性質や歴史によってデータをどういった形式でどこに保存するかは様々です。 ゆえに今回は汎用的な用途を前提どこに保存するかは問わずに、データの変更通知の仕組みと分けて考えてみます。 データの実体と状態を別に管理する そこで着目するのが先の項目で話題に上げた「データの状態」です。 データの状態は一時的なものでアプリキル後まで永続化する必要は基本的にはありません。いかなる場合に揮発しても構わないはずです。 なのでこの要素を監視対象としてライブラリ内に組み込みStateFlowやCurrentValueSubjectにセットします。 データの実体はどこに保存してもよく、状態の変化の通知に合わせてついでに取ってきて一緒に返すくらいの考え方です。 これを満たすために、まず以下のように状態だけを取り扱う箱(DataState)を用意します。 エラー状態におけるのエラーの内容については基本的には揮発しても良いことが大半なので状態の一部として取り扱ってしまってよいでしょう。 Kotlin sealed interface DataState { class Fixed : DataState // 停止状態 class Loading : DataState // 取得中状態 class Error(val exception: Exception) : DataState // エラー状態 } Swift public enum DataState { case fixed // 停止状態 case loading // 取得中状態 case error(rawError: Error) // エラー状態 } これを内包するStateFlowやCurrentValueSubjectをシングルトンクラスで維持しつつ、通知される状態の流れをmap関数を使って変化させることで状態の変化に応じてデータを渡すことが出来るようになります。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) また、以下のコードはDataStateを通知のトリガーとして機能させつつキャッシュデータの実体と統合して前述したState<T>として返却する一例です。(※解説用に実際のコードより簡略化しています) Kotlin val dataStateObserver = MutableStateFlow<DataState>(DataState.Fixed()) // 状態を監視できるObserver fun observeData(): Flow<State<RawData>> { return dataStateObserver .map { dataState -> val data = loadCacheData() // 状態が変化した際に対象のデータの実体をキャッシュから取得する convertToState(data, dataState) // 最終的な出力形式である`State<T>`の形式に加工する } } Swift let dataStateObserver = CurrentValueSubject<DataState, Never>(.fixed()) // 状態を監視できるObserver func observeData() -> AnyPublisher<State<RawData>, Never> { dataStateObserver .map { dataState in let data = loadCacheData() // 状態が変化した際に対象のデータの実体をキャッシュから取得する return convertToState(data, dataState) // 最終的な出力形式である`State<T>`の形式に加工する } .eraseToAnyPublisher() } このようなアプローチを取ることで、データの保存先や保存形式に左右されずにデータ通知の仕組みを構築することが可能です。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) この手法を取る上で一つ注意すべきなのは、状態が変化したときしか通知されないためデータの実体を直接書き換えてしまうと通知されません。 データの更新を行う際は実体と状態を必ずセットで更新して上げる必要があります。 これらは手動で行おうとすると大変ですが、この2つの処理をセットにして共通化した仕組みを用意して必ずそこから更新処理を行うように徹底することでこの問題に対処することは可能です。作成したライブラリでもそのような仕組みを用意しています。 3&4, データ取得先の抽象化 次に考えてみるのは「3. 使う側からキャッシュかAPIかなどどこから取ってきているかを意識させず整合性の取れた値を返す」と「4. 出来る限り早く値を返却する」についてです。 これについては実際にコードに落とし込むにあたりどういった処理が必要なのか簡単にフローチャートを書いてみます。 上記の手順を踏んでデータを返すことで、ローカルキャッシュとリモートからのデータの取得を抽象化したデータを取り出すことが可能です。 さらにデータを要求者に返す仕組みを前述したObserverパターンと組み合わせることで、要求時のデータのみならず将来的な更新や状態の変化まで監視することが出来ます。 これを限界まで簡略化した上で愚直にコードで表現してみるとこんな感じかと思います。 データの入出力や状態の入出力については外部へ切り出して抽象化処理の共通部分のみ表現しています。 Kotlin fun process() { val currentState = loadState() // データの状態を取り出す if (currentState is DataState.Loading) return // 状態がLoadingなら何もしない val cacheData = loadDataFromCache() // キャッシュデータの実体を取り出す if (!needRefresh(cacheData)) return // キャッシュが有効なら何もしない saveState(DataState.Loading()) // データの状態をLoadingに変える (データが通知される) val response = fetchDataFromOrigin() // リモートから最新データを取得する if (response.isSuccess) { // データの取得に成功 saveDataToCache(response.data) // 取得したデータをキャッシュへ保存する saveState(DataState.Fixed()) //データの状態をFixedに変える (データが通知される) } else { // データの取得に失敗 saveState(DataState.Error()) //データの状態をErrorに変える (データが通知される) } } Swift func process() { let currentState = loadState() // データの状態を取り出す if case .loading = currentState { return } // 状態がLoadingなら何もしない let cacheData = loadDataFromCache() // キャッシュデータの実体を取り出す if !needRefresh(cacheData) { return } // キャッシュが有効なら何もしない saveState(.loading) // データの状態をLoadingに変える (データが通知される) let response = fetchDataFromOrigin() // リモートから最新データを取得する if response.isSuccess { // データの取得に成功 saveDataToCache(response.data) // 取得したデータをキャッシュへ保存する saveState(.fixed) //データの状態をFixedに変える (データが通知される) } else { // データの取得に失敗 saveState(.error) //データの状態をErrorに変える (データが通知される) } } 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) 実際には非同期処理やパラメータによる制御処理を多数入れているので上記で示したコードとはかなり異なります。 5. Single Source of Truthを守る 最後に「5. 使う側でデータを変数などで保持せず、取得先を常に1箇所に絞る」について考えてみます。 とはいえ、これに関してはライブラリ等でどうにかすることは出来ないので利用者に頑張ってルールを守ってもらうしかありません。 今回紹介している仕組みは前回の記事からお話している通り、擬似的なSingle Source of Truthを実現していますが あくまでこの仕組みを通した場合に限定されるので、仕組みを介さずにデータを取得・更新した場合やこの仕組みの外側でデータを保持してしまった場合はこの前提は崩れてしまいます。 この仕組みを採用する以上は、「4. 出来る限り早く値を返却する」に基づくため、この仕組みの外側でデータをメンバー変数などで保持する必要はなく、常にこの仕組みを経由して取得・更新を行うことが十分可能な作りになっているはずです。 共通化出来ない部分を切り分ける ここまでデータの保存先や保存形式に影響を受けない部分の共通化処理を考えてきました。 ここからは前回の記事でもお話しましたが、共通化出来ない部分についてもう一度書き出してみます。 データの状態を保持する機構 キャッシュからの取得処理 キャッシュへの保存処理 API等のプライマリデータからの取得処理 キャッシュが有効か否かの判断処理(時間、個数、etc..) これらに関してはデータによって処理が変わる部分なので、それぞれの実装時に処理を記述を変えられるように枠組みだけを提供してみます。 具体的には以下のようなインターフェースの提供を検討します。(※解説用に実際のコードより簡略化しています) Kotlin interface StoreFlowableFactory<DATA> { fun loadDataFromCache(): DATA? // ローカルキャッシュからの取得処理 fun saveDataToCache(newData: DATA?) // ローカルキャッシュへの保存処理 suspend fun fetchDataFromOrigin(): DATA // リモートデータからの取得処理 fun needRefresh(cachedData: DATA): Boolean // ローカルキャッシュが有効かの判定処理 } Swift protocol StoreFlowableFactory { associatedtype DATA func loadDataFromCache() -> DATA? // ローカルキャッシュからの取得処理 func saveDataToCache(newData: DATA?) // ローカルキャッシュへの保存処理 func fetchDataFromOrigin() -> AnyPublisher<DATA, Error> // リモートデータからの取得処理 func needRefresh(cachedData: DATA) -> Bool // ローカルキャッシュが有効かの判定処理 } 上記のインターフェースに準じた実装をデータごとに用意してあげることで保存先や保存形式を実装した部分に任せつつ、その他の抽象化のための共通処理をまとめることが出来ます。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) また、これに加えて前述したデータの状態のみを取り扱う箱DataStateを通知の仕組みに乗せたシングルトンクラスも必要になります。 こちらについても共通化して隠蔽してしまうと柔軟性が損なわれるので今回のライブラリでは切り出していますが場合によっては共通化してしまっても良いかもしれません。 Kotlin abstract class FlowableDataStateManager { val dataState = MutableStateFlow<DataState>(DataState.Fixed()) } Swift open class FlowableDataStateManager { let dataState = CurrentValueSubject<DataState, Never>(.fixed()) } これらを継承して作成したクラスをシングルトンで保持する部分は隠蔽せず、実装側に任せます。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) 複雑さを軽減する5つの要素をまとめる ここまでで、以下の5つの要素を実装に落とし込むための実装のパーツを紹介してきました。 今回はデータ通知の仕組みにKotlin Coroutines FlowやCombine Frameworkを使って表現しましたが、RxJavaやRxSwift、Stream APIやReactiveSwiftなど他の近しい技術を使っても実現できるはずです。 また、データの取得先を抽象化する処理についても解説したフローチャートのような分岐処理を記述することはさほど難しくはないと思います。 これらのパーツを組み合わせつつ、パラメータによる細かい調整を可能にしつつ共通化出来る部分を整えたのが今回紹介したStoreFlowableとなります。 ライブラリ内では様々なパターンを考慮して抽象化している部分が多々ありますが、特定のプロダクトに特化すればもっと簡易な仕組みを自作するだけでも十分機能すると思います。 次回記事に続く 今回の記事では考え方をコードに表現するにあたっての実装のキモとなる部分を作成したライブラリを元に紹介しました。 次回はライブラリの具体的な使い方について解説してみます。 次回記事: iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(3/3 ライブラリ使い方編)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

APIKitでapplication/urlencodedでbodyを送る方法

自分自身APIKITは普段使わないのですが、今回使う機会があり、application/urlencodedでbodyを送る方法で少し詰まったのでここにまとめておきます。 bodyParametersを使う 基本的にSwiftでAPIを叩くときはrequestやresponseといった風にファイルを分けるのですが、APIKITを使う場合は以下のような感じでrequestを作ります。 struct HogehogeAPI { struct hogeRequest: Requset { var headerFields: [String : String] { return [ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" ] } typealias Response = Hogehoge.Response var baseURL: URL { return URL(string: "/hogehoge")! } var method: HTTPMethod { return .post } var path: String { return /hogehoge } var parameters: Any? { return [ "user_id": 1 ] } } } こんな感じでRequestを継承してheaderFieldsやbodyであるparametersを設定していきます。 content-type: application/jsonだとおそらくこれで問題なかったのですが、今回の application/x-www-form-urlencodedではparametersを使うだけだとbodyを送れませんでした。 そこでbodyParametersを設定します。   var parameters: Any? { return [ "user_id": 1 ] } var bodyParameters: BodyParameters? { guard let parameters = parameters as? [String: Any] else { return nil } return FormURLEncodedBodyParameters(formObject: parameters) } こんな感じでparametersに設定した値をFormURLEncodedBodyParametersに変換してあげてbodyParametersに設定してあげると、上手く送信できました。 APIKITを普段から使っている人にとっては当たり前のことかもしれませんが、少しつまづいたので共有しておきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI Tutorialsに登場したProperty Wrappersまとめ

はじめに 自分がApple公式のSwiftUI Tutorialsを通して学んだ時に登場したProperty Wrappersがそれぞれどういう役割かとかを忘れちゃいそうなので備忘のために書き残すことにしました。 watchOSやmacOSのAppを作る予定は現状なかったので、Chapter3までに登場したProperty Wrappersの紹介になります。 これから同じくSwiftUIを始めて勉強する方々の参考になればと思います。 Property Wrappersとは Swift5.1で実装された機能です。 プロパティのget/setに関わる制御を共通化するような仕組みです。 この後紹介する@StateやEnvironmentObjectはProperty Wrappersですし、自分で新たなものを定義することが可能です。 SwiftUI Tutorialsに登場したProperty Wrappers @￰State State | Apple Developer Documentation 通常SwiftUIのViewはstructで定義していくためプロパティの更新ができないが、@Stateをつけて宣言することでそのプロパティの値の読み書きができる。 @Stateで宣言したプロパティはViewのbodyもしくはViewから呼び出されるメソッドからのみアクセスする必要があるとAppleから推奨されているので、基本的にはprivateをつけて宣言するのが一般的のようだ。 Viewにプロパティの値を渡したい時は$を変数名につけることで実現できる。 struct ToggleView: View { /// トグルのスイッチの状態(初期値: false) @State private var isOn = false var body: some View { // isOnの値を監視($をつける!) Toggle(isOn: $isOn) { Text("スイッチを切り替える") } } } これで下記画像のようにスイッチの状態を切り替えることができるようになる。 @￰Publishedと@￰EnvironmentObject Published | Apple Developer Documentation EnvironmentObject | Apple Developer Documentation チュートリアルでは、ObservableObjectを準拠しているクラスのプロパティに@Publishedを付けることで監視側がデータの変更を取得できるようにしている。 @EnvironmentObjectをつけてプロパティを宣言することで、複数のViewに共通のインスタンスを渡して値を監視させられます。 下記コードでは親のContentView内で.environmentObject(User())でインスタンスを渡して子ビューであるTextViewとButtonsViewで値を参照しています。 final class User: ObservableObject { @Published var name = "Taro" @Published var age = 18 } struct ContentView: View { var body: some View { VStack { TextView() ButtonsView() } .environmentObject(User()) } } struct TextView: View { @EnvironmentObject var user: User var body: some View { Text("I'm \(user.name).") Text("I'm \(user.age) years old.") } } struct ButtonsView: View { @EnvironmentObject var user: User var body: some View { HStack { Button(action: { self.user.age += 1 }) { Text("歳をとる") } Button(action: { self.user.age -= 1 }) { Text("若返る") } } } } 実行したアプリでは、ボタンを押したときのユーザの年齢が動的に変更される。 これで一応動くのですが、environmentObjectはアプリ全体で共通使用するデータのやりとりをするので、正しくはContentViewから.environmentObject(User())を削除し、代わりにXXXApp.swift内に下記のようにして記述するのが適切なようです。 そうでないと、print文で確認すると更新はされているがアプリの見た目は変わらないというような、正常に動作しないことがありました。 XXXApp.swift struct SampleSwiftUIApp: App { var body: some Scene { WindowGroup { ContentView().environmentObject(User()) //ここで共通する } } } @￰StateObject StateObject | Apple Developer Documentation 一度初期化すると、フレームワーク側で@stateObjectがついたプロパティの値が保持され続けるのでビューの再描画が発生しても値が初期化されなくなります。 @ObservedObjectを使用していると初期化されてしまうので、再描画されても変わってほしくない場合は@stateObjectを使うと良さそうです。 チュートリアルを見る感じだと、こちらも初期化してそのままプロパティを保持して欲しいことを考えると起動直後に宣言して.environmentObjectで渡すのが適しているという感じでしょうか。 XXXApp.swift struct SampleSwiftUIApp: App { @StateObject var hogeData = HogeData() // 任意の保持され続けて欲しいデータ var body: some Scene { WindowGroup { ContentView().environmentObject(hogeData) // そのデータを渡す } } } @￰Binding Binding | Apple Developer Documentation データを格納するプロパティと、そのデータを変更/更新するビューを双方向に接続します。 ということは、@Bindingで設定したプロパティが更新されれば、それと接続している他のビューも更新されるということになるようです。 final class ToggleState: ObservableObject { @Published var isOn = false } struct ContentView: View { @EnvironmentObject var toggle: ToggleState var body: some View { VStack { ToggleAView(isOn: $user.isOn) ToggleBView(isOn: $user.isOn) } } } struct ToggleAView:View { @Binding var isOn: Bool var body: some View { Toggle("スイッチA", isOn: $isOn) } } struct ToggleBView:View { @Binding var isOn: Bool var body: some View { Toggle("スイッチB", isOn: $isOn) } } 上記のコードは同じプロパティをBindingしているので、片方のトグルを切り替えると、その状態がもう片方の状態にも同期されて切り替わるようになります。 @￰Environment Environment | Apple Developer Documentation EnvironmentValuesに定義されているビューの環境の設定値を取得/更新したいときに使います。 下記コードは取得の例です。 struct ContentView: View { @Environment(\.timeZone) var timeZone @Environment(\.calendar) var calendar @Environment(\.locale) var locale var body: some View { VStack { Button(action: { print(timeZone) // Asia/Tokyo (current) print(calendar) // gregorian (current) print(locale) //en (current) }) { Text("button") } } } } EnvironmentValuesは上記のようにtimeZoneやcalendar等の値意外にも豊富に定義されています。 また、自分でEnvironmentValuesの値を新たに定義することもできるそうです。 おわりに ひとまず、SwiftUI Tutorialsに登場するProperty Wrappersをそれぞれ調べてみて、それぞれがどういうものなのかはある程度把握できました。 他の記事を見る感じだと、今回紹介したものがやはりよく紹介されているのを見かけるので頻繁に活用していくのだと思われます。 これから実際にSwiftUIを使ったコーディングをしていくと思うので、これらを駆使しながら実装していきます。 Property Wrappersを複数種類組み合わせたときの挙動とかのケーススタディができていないのと、@ObservedObjectや@AppStorage等の他のProperty Wrappersのまとめができていなかったりするので、こういうときどれを使うのが良いかがまだ不明な状態ですが、また色々調べながら進めて行けたらと思います。 参照  【Swift 5.1】Property Wrappersとは? | 2速で歩くヒト SwiftUIの機能 @State, @ObservedObject, @EnvironmentObjectの違いとは SwiftUIのProperty Wrappersとデータへのアクセス方法 EnvironmentValuesを制するものはSwiftUIを制する ※チュートリアルと各Property Wrappersのリンクは既に上記内で共有しているので省略させていただいています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む