20210410のSwiftに関する記事は8件です。

【Swift】SOLID原則って?

はじめに 有名な設計の原則にSOLID原則というものがあります。SOLID原則とはいったいなんなのかを今回は解説したいと思います。 「S」ingle Responsibility Principle(単一責任原則) 「クラスを変更する理由はふたつ以上存在してはならない」と言う原則です。 以下のクラスはこの原則に違反しているでしょうか? User class User { let name: String let age: Int init(name: String, age: Int) { self.name = name self.age = age } func introduce() { print("\(self.name)です、\(self.age)歳です。") } } これは違反していないと言えますね。責任が一つしかないからです。では、違反している例を見てみましょう。 User class User { let name: String let age: Int init(name: String, age: Int) { self.name = name self.age = age } func introduce() { print("\(self.name)です、\(self.age)歳です。") } func nameAlert() { print("\(name)アラート") } } これは単一責任原則に違反しています。これでは、ユーザー名を保持する役割とユーザー名に応じてアラートを表示する役割を持つことになります。つまり、クラスを変更する理由がふたつ存在してしまいます。以下のように修正してみましょう。 User class User { let name: String let age: Int init(name: String, age: Int) { self.name = name self.age = age } func introduce() { print("\(self.name)です、\(self.age)歳です。") } } class NameAlert { static func present(name: String) { print("\(name)アラート") } } let user = User(name: "REON", age: 20) NameAlert.present(name: user.name) // REONアラート Userクラスは単一責任になっていますね。このようにすることで、以下のようにAnimalクラスにもこのアラートを使い回しすることができ、再利用性が高くなります。 Animal class Animal { let name: String let owner: String init(name: String, owner: String) { self.name = name self.owner = owner } } let animal = Animal(name: "HARINEZUMI", owner: "REON") NameAlert.present(name: animal.name) // HARINEZUMIアラート Userクラスに書かれたままになっていれば、同じアラートの処理をAnimalクラスにも書かなければいけませんでした。 「O」pen/Closed Principle(開放閉鎖の原則) 「クラスは拡張に対して開いていて、修正に対して閉じていなければならない」という原則です。 この原則は、オブジェクト指向設計の核心だと言われています。 もう少し詳しくみていくと、以下のようなことをこの原則は表現しています。 ・拡張に対して開いている(Open): 仕様が変更されても、モジュールに新たな振る舞いを追加することで変更に対処することができる。 ・修正に対して閉じている(Closed): モジュールの振る舞いを拡張してもそのソースコードやバイナリコードは影響を受けない これでも正直わかりづらいです。簡単に言うと、「変わらない部分」と「変わりやすい部分」を分離しましょうと言うことです。 以下のコードをみてください。 struct Language { let name: String } let languages = [ Language(name: "Swift"), Language(name: "Kotlin"), ] func printLanguageDescription(languages: [Language]) { for language in languages { if language.name == "Swift" { print("Swiftの由来はアマツバメだよ!") } else if language.name == "Kotlin" { print("Kotlinの由来は開発された場所の近くにあったコトリン島だよ!") } } } printLanguageDescription(languages: languages) //Swiftの由来はアマツバメだよ! //Kotlinの由来は開発された場所の近くにあったコトリン島だよ! このような実装では、新しく言語が追加されたときにprintLanguageDescriptionメソッドを修正しないといけなくなります。 struct Language { let name: String } let languages = [ Language(name: "Swift"), Language(name: "Kotlin"), Language(name: "Go"), ] func printLanguageDescription(languages: [Language]) { for language in languages { if language.name == "Swift" { print("Swiftの由来はアマツバメだよ!") } else if language.name == "Kotlin" { print("Kotlinの由来は開発された場所の近くにあったコトリン島だよ!") } else if language.name == "Go" { print("Goの由来は開発したのがGoogleだからだよ!") } } } printLanguageDescription(languages: languages) //Swiftの由来はアマツバメだよ! //Kotlinの由来は開発された場所の近くにあったコトリン島だよ! //Goの由来は開発したのがGoogleだからだよ! このように、言語を追加することでこのコードでは変更に硬く、扱いづらいのがわかります。 以下のように修正してみます。 protocol Language { var name: String { get set } func printLanguageDescription() } struct Swift: Language { var name: String func printLanguageDescription() { print("Swiftの由来はアマツバメだよ!") } } struct Kotlin: Language { var name: String func printLanguageDescription() { print("Kotlinの由来は開発された場所の近くにあったコトリン島だよ!") } } struct Go: Language { var name: String func printLanguageDescription() { print("Goの由来は開発したのがGoogleだからだよ!") } } let languages: [Language] = [ Swift(name: "Swift"), Kotlin(name: "Kotlin"), Go(name: "Go"), ] for language in languages { language.printLanguageDescription() } //Swiftの由来はアマツバメだよ! //Kotlinの由来は開発された場所の近くにあったコトリン島だよ! //Goの由来は開発したのがGoogleだからだよ! コード量は多くなってしまいますが、このようにすることで、今後言語が増えたときもLanguageプロトコルを準拠させることで問題なさそうです。 「L」iskov Substitution Principle(リスコフの置換原則) 「継承よりプロトコルコンポジション」という金言もあるので、この原則については詳しく解説しませんが、簡単に言うと、 「サブクラスは、そのスーパークラスで代用可能でなければならない」という原則です。 つまり、スーパークラスにできてサブクラスにできないことがあってはいけません。 「I」nterface Segregation Principle(インターフェース分離の原則) 「クライアントに、クライアントが利用しないメソッドへの依存を強制してはならない」という原則です。 例えば、以下のようなコードはこの原則に違反しています。 protocol AnimalProtocol { func fly() func eat() func sleep() } class Bird: AnimalProtocol { func fly() { print("飛ぶ") } func eat() { print("食べる") } func sleep() { print("寝る") } } class Human: AnimalProtocol { func fly() { print("飛ぶ") } func eat() { print("食べる") } func sleep() { print("寝る") } } Humanクラスがflyメソッドを実装することを強制されています。これは、fly,sleep,eatメソッドを宣言したAnimalProtocolをHumanクラスが準拠しているためです。これではAnimalProtocolの使える範囲が限られてしまいますし、不要なメソッドを実装しないといけなくなります。 以下のように修正してみましょう。  protocol SkyAnimalProtocol { func fly() } protocol AnimalProtocol { func eat() func sleep() } class Bird: SkyAnimalProtocol, AnimalProtocol { func fly() { print("飛ぶ") } func eat() { print("食べる") } func sleep() { print("寝る") } } class Human: AnimalProtocol { func eat() { print("食べる") } func sleep() { print("寝る") } } こうすることで、Humanクラスでflyメソッドを書かなくてすむようになりました。 「D」ependency Inversion Principle(依存関係逆転の原則) 「上位レベルのモジュールは下位レベルのモジュールに依存するべきではない、抽象に依存するべきである」、 「抽象は詳細に依存してはならない。詳細が抽象に依存すべきである」という原則です。 まず、一つ目の「上位レベルのモジュールは下位レベルのモジュールに依存するべきではない、抽象に依存するべきである」からみていきましょう。 あるモジュールは具体的な型ではなく、抽象(インターフェース=プロトコル)に依存していれば、その依存対象の具体型は差し替え可能です。 さらに、抽象化をすることにより、再利用性が高くなります。 上位レベルのモジュールは下位レベルのモジュールに依存しているパターンは以下のようなものです。 class BlackCore { let color = "black" func printColor() { print(self.color) } } class BallPen { let core: BlackCore init(core: BlackCore) { self.core = core } func printColor() { core.printColor() } } let blackCore = BlackCore() let pen = BallPen(core: blackCore) pen.printColor() // black これでは、上位モジュールであるBallPenクラスが下位モジュールであるBlackCoreクラスに依存してしまっています。黒以外の芯に変えたいときにBallPenクラスに修正が必要になります。 これを以下のように修正します。 protocol Core { func printColor() } class BlackCore: Core { let color = "black" func printColor() { print(self.color) } } class RedCore: Core { let color = "red" func printColor() { print(self.color) } } class BallPen { let core: Core init(core: Core) { self.core = core } func printColor() { core.printColor() } } let blackCore = BlackCore() let redCore = RedCore() let blackBallPen = BallPen(core: blackCore) blackBallPen.printColor() // black let redBallPen = BallPen(core: redCore) redBallPen.printColor() // red BallPenクラスは特定の芯に依存していないため、新しい色の芯クラスを作成してもBallPenクラスを変更する必要は無くなりました。 おわりに SOLID原則を意識することで、より良い設計になりそうですね! 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はじめてのAR 〜自分の部屋にARでポスター貼ってみた〜

目的 自分の部屋にARでポスターを貼って、iPhone(iPad)カメラを通して眺めて楽しむ ※もし誤りなどありましたら、ご指摘くださればありがたいです。 必要な環境やツール(自分で動作確認した環境) MacBookPro(Appleシリコン) XCodeを動作させるのにマスト XCode(12.4) iPhone(iPad)アプリを開発する ARKit4 現実世界を認識する RealityComposer ARコンテンツ(ポスター)を制作する usdzファイルを読み込む RealityKit 製作したARコンテンツを現実世界と重ねてレンダリングする Blender(v2.92.0) 3DCG(ポスターを貼る直方体)を製作する glbファイルで出力する RealityConverter glbファイルをusdzファイルに変換する レシピ(作り方) Blenderでポスターを作成する。 Blenderの画面左上あたりにある[Add]から、[Mesh]-[Plane]とボタンを押していき、ポスターを貼るための直方体(Plane)を配置する。 直方体(Plane)の[MaterialProperties]-[BaseColor]-[Texture]-[ImageTexture]で、ポスター用画像(jpg等)をテクスチャとして指定する。 上のメニューバー[File]-[Export]-[glTF2.0]をボタンを押していき、作成したポスターをglbファイルで出力する。 XCodeでARアプリを作成するためにファイルを変換する。 XCodeでのポスター利用に向け、RealityConverterを用いて、Blenderで出力したglbファイルをusdzファイルに変換する。 XCodeでARアプリを作成する。 XCodeにて、[New]-[Project]-[Augumented Reality App]でARアプリ開発を開始する。 Experience.rcproject-[Open in RealityComposer]を押して、ポスター(usdz)を取り込む。 RealityComposer左側Scenesの[+]-[Vertical]でシーンを追加する。 シーンの名前を[Poster]と変更する。 RealityComposerメニュー[File]-[Import]から、usdzファイルをインポートする。 ContentView.swiftのmakeUIView関数を編集する func makeUIView(context: Context) -> ARView { let arView = ARView(frame: .zero) let posterAnchor = try! Experience.loadPoster() arView.scene.anchors.append(posterAnchor) return arView } XDCodeでiPhone(iPad)を指定して実行する。 関連リンク Expolore ARKit4 https://developer.apple.com/videos/play/wwdc2020/10611 What's New in RealityKit https://developer.apple.com/videos/play/wwdc2020/10612/ The artist's AR toolkit https://developer.apple.com/videos/play/wwdc2020/10601
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[問題]スマホアプリにお試し期間の概念は必要か?

はじめに 本題に入る前に、マネタイズについてお話します。 スマホアプリでマネタイズするには、大きく2つパターンがあります。 1. 利用料無料で、広告代で得る 2. 有料(買い切り、サブスクリプション、追加課金制) (自分が知らないだけで、他にもあるかも知れませんが、ここでは触れません) 1. の利用料無料の場合は特に問題ありません。ずっと無料ですから、飽きたり気に入らなかったら、アプリを削除すれば問題無いと思います。 問題は、2.です。 2. の有料の場合、買わないとそのアプリが遊べない・利用できないわけですから、自ずと買わざるを得ません。ですが、買ってつまらなかった・想像していたものと違うなど理由によってはお金を出し惜しみして買ってくれない人も少なくありません。そこで登場するのが、お試し期間です。 お試し期間とは? 有料アプリを使わせるために、無料のお試し期間を設け、その間のみ有料時のサービスが利用できる仕組みのこと。 ※ただし、ここで無料期間としていましたが、無料ポイント、初回のみチケット無料のようなものも含まれます。 何が問題か ここまでの話だけでは、特に問題ないのではないか? と思う人も居るでしょう。 アプリ運用する側(メーカー)が困ること・・・ それは何か・・・ 無料期間(無料ポイント、初回のみ無料、1週間無料)が何度も利用できてしまう ってことです。 ????? 何を言っているのか、分からない人もいると思います。 ゲームアプリでよくあるのですが、いわゆるリセマラ(リセットマラソン)を指します。 リセマラとは リセットマラソン(Reset Marathon)とは、主にソーシャルゲームにおいて、ソフトのインストールとアンインストールを何度も繰り返すこと、及びそれにより自分が目的とするアイテムを入手する方法のこと (Wikipediaより) ゲームアプリだとなじみのある行為だと思いますが、一般的なツールアプリ・ライフスタイルアプリなどにおいては、ガチャではなく、無料期間(無料ポイント、初回のみ無料、1週間無料)を言います。そして、企業側からすると無料期間(無料ポイント、初回のみ無料、1週間無料)が何度も利用できてしまうと、損失につながります。 そして、この「無料期間(無料ポイント、初回のみ無料、1週間無料)が何度も利用できてしまう」問題をエンジニアは解決しなくてはいけません。 どうやって? 実際にアプリを制作してみるとぶち当たる壁が存在します。 このアプリをアンインストールしたとしても、過去にインストールされた経歴があることを判断しなくてはいけません。 ですが、端末内データに何かしらのフラグをキャッシュとして保存したとしてもアプリ削除したら消えてしまいます。また、メールアドレスで判断しようとしても複製できてしまうため、意味がありません。 そこで登場するのが、端末固有IDです! 端末固有IDとは? 端末それぞれに不変のIDとして存在する識別子のことを指します。そして、その識別子を元にインストール経歴があるのか判断します。 一般的に多くのAndroid/iOSアプリにおいては以下の識別子を利用します。 Android ANDROID_ID iOS AdvertisingIdentifier ただし、これらのIDは端末を継続して利用している間のみ値が変更されないです。 各詳細について見てみましょう。 ANDROID_ID The value may change if a factory reset is performed on the device or if an APK signing key changes. デバイスで出荷時設定へのリセットが実行された場合、またはAPK署名鍵が変更された場合、値は変更される可能性があります。 AdvertisingIdentifier Unlike the identifierForVendor property of the UIDevice, the same value is returned to all vendors. This identifier may change. For example, if the user erases the device. For this reason you should not cache it. UIDeviceのidentifierForVendorプロパティとは異なり、すべてのベンダーに同じ値が返されます。 この識別子は変更される可能性があります。 たとえば、ユーザーがデバイスを消去した場合などです。 このため、キャッシュしないでください。 と記載されています。 つまり、端末を工場出荷状態(端末リセット)に戻すことで、これらのIDが初期化されてしまいます。 では、工場出荷状態に戻しても変わらないIDを利用すればよいのではないか・・・? と思う方が居ると思います。 残念ながら、それはできません。 Androidに関しては、永続的なデバイス ID(IMEI / MEID、IMSI、SIM、ビルドシリアル)はJava/Kotlinから取得できない制限がかかっています。そのため利用できないのです。 また、iOSも同様でMACアドレスは取得できません。 こんなものも・・・ 工場出荷状態に戻して、端末固有IDをリセットするのがめんどうである時にこんなアプリもあります。 同じ端末で2つのLINEアカウントが使い分けられるようにできる禁断のアプリ「並行世界」の使い方 このアプリを利用するとこのアプリ自体が仮想OSの役割を果たし、同じアプリを2つ起動することができます。 つまり、並行世界を起動した上でLINEを起動すると、1端末で2つのアカウントを利用することができます。 これは実質、端末固有IDを複製しています。 このアプリを利用すると簡単に端末固有IDを複製できてしまうので、何かしら対策しないといけません。 では、どうするか? 対策として考えられることが、Android/iOSアプリで決済する時のカード情報をサーバー側で管理しておき、同じカード情報であった場合、無料期間(無料ポイント、初回のみ無料、1週間無料)を利用できなくすれば問題ないと思います。 ですが、無料期間(無料ポイント、初回のみ無料、1週間無料)リセマラのために、カード情報を入力するアプリは多くありません。 そもそもの問題として、お試し期間という概念は必要でしょうか。という問題定義に戻ります。 無料期間(無料ポイント、初回のみ無料、1週間無料)の機能を追加したが故に、無限に悪用できてしまい、本来有料ユーザーを増やしたいがためにやった行為なのに、逆に減らしてしまうのではないか。と懸念されます。 そこまでのリスクを追うくらいであれば、無料期間(無料ポイント、初回のみ無料、1週間無料)は要らないのではないか。と私は思います。 以上です。 参考 iOS/Androidで端末を識別するIDまとめ https://iridge.jp/blog/201404/4836/ デバイス ID https://source.android.google.cn/devices/tech/config/device-identifiers?hl=ja 一意の識別子に関するベスト プラクティス https://developer.android.com/training/articles/user-data-ids?hl=ja ANDROID IDがどのように生成されているかざっくり調べた https://qiita.com/operandoOS/items/b41fa4a1c6c009c64a10 SSAID取得 https://gist.github.com/Koronaa/d204bc29a122f9adef857d8935fd7cbc
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIでListの上にButtonを置く場合List自体にイベントを奪われないようにする

SwiftUIでListの上にButtonを置くと、タップイベントがListのほうで実行されてしまう。そのため、ButtonのスタイルをBorderlessButtonStyleかPlainButtonStyleにする必要がある。 (この記事はXcode 12.4でのSwiftUIでの話です) 本題 解決方法 ButtonにBorderlessButtonStyleかPlainButtonStyleのスタイルを設定する //: A UIKit based Playground for presenting user interface import UIKit import SwiftUI import PlaygroundSupport struct ContentView: View { var body: some View { List { HStack { Text("Cell 1") Spacer() Button(action: { print("B_1押したね") }, label: { Text("Border") }) .buttonStyle(BorderlessButtonStyle()) Button(action: { print("P_1押したね") }, label: { Text("Plain") }) .buttonStyle(PlainButtonStyle()) } } } } PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView()) これで解決。タップイベントはListではなくボタンで反応するようになったデザイン的なものはButtonでなく内部のTextに適用していけばいい。 蛇足: なぜButtonのスタイルで判断するのかを考える 全てのスタイルを適用してみる 他にもDefaultButtonStyleというのがあるのでそれも適用してみる。結果はListがタップできるようになってしまう。つまりこれがデフォルトか。 //: A UIKit based Playground for presenting user interface import UIKit import SwiftUI import PlaygroundSupport struct ContentView: View { var body: some View { List { HStack { Text("Cell 1") Spacer() Button(action: { print("B_1押したね") }, label: { Text("Border") }) .buttonStyle(BorderlessButtonStyle()) Button(action: { print("P_1押したね") }, label: { Text("Plain") }) .buttonStyle(PlainButtonStyle()) Button(action: { print("D_1押したね") }, label: { Text("Default") }) .buttonStyle(DefaultButtonStyle()) } } } } PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView()) なぜButtonのスタイルで判断するのか Listには複数のSectionやView(Cell/Item)が必要なためListのスタイルで統一的に指定しづらい 1行目とか指定する形にしてしまえばできるとは思うがそれも複雑でどうかと思う 内部のViewの状態で判断しようとしている 具体的にはListが2行になっている場合、1行目はCellにはタップできないが、2行目はCellにもタップできる。 //: A UIKit based Playground for presenting user interface import UIKit import SwiftUI import PlaygroundSupport struct ContentView: View { var body: some View { List { HStack { Text("Cell 1") Spacer() Button(action: { print("B_1押したね") }, label: { Text("Border") }) .buttonStyle(BorderlessButtonStyle()) Button(action: { print("P_1押したね") }, label: { Text("Plain") }) .buttonStyle(PlainButtonStyle()) } HStack { Text("Cell 2") Spacer() Button(action: { print("D_2押したね") }, label: { Text("Default") }) .buttonStyle(DefaultButtonStyle()) } } } } PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SwiftUI] $hoge.fugaは$(hoge.fuga)ではない

細かいことですが、SwiftUIなどで時折書く$hoge.fugaという式は$(hoge.fuga)ではないのだ、という話をします。 例 まずは記述例を挙げます。 struct Hoge { var fuga: String = "" } struct MyContentView: View { @State private var hoge = Hoge() var body: some View { TextField("入力してね", text: $hoge.fuga) .textFieldStyle(RoundedBorderTextFieldStyle()) } } 非常にシンプルです。 では、次のように書いてみましょう。エラーが出ます。 //Error: '$' is not an identifier; use backticks to escape it TextField("入力してね", text: $(hoge.fuga)) それからこれもダメです。 //Error: Referencing subscript 'subscript(dynamicMember:)' requires wrapper 'Binding<Hoge>' //Error: Value of type 'Hoge' has no dynamic member '$fuga' using key path from root type 'Hoge' TextField("入力してね", text: hoge.$fuga) これでひとまず$hoge.fugaが$(hoge.fuga)でないことはおわかりいただけたと思います。しかしでは$hoge.fugaとはなんなのでしょう。 正体 そもそも$を用いたアクセスはpropertyWrapperの機能によるものです。State<T>のprojectedValueはBinding<T>となっています。ですから$hogeとはBinding<Hoge>の値です。 しかし、そうするとBinding<Hoge>にプロパティfugaが存在するのが不思議に思えるはずです。これを実現しているのがdynamicMemberLookupです。Swift5.1以降でKeyPathを使ったLookupができるようになったため、おそらくBindingが次のように定義されています。 @dynamicMemberLookup struct Binding<T> { subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> Binding<U> { //処理 } } これだけでは少しわかりにくいので、もっと単純なサンプルを見てみましょう。 @dynamicMemberLookup private struct Wrapper<T> { init(wrappedValue: T) { self.wrappedValue = wrappedValue } private var wrappedValue: T subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> Wrapper<U> { get { return Wrapper<U>(wrappedValue: wrappedValue[keyPath: keyPath]) } } } こうすると、valueはWrapper型の値であるにもかかわらず、Stringが持っていたはずのプロパティにアクセスできるようになっています。もちろん定義した通りWrapped<Int>の値です。 以上で$hoge.fugaの正体は説明がつきます。$hogeがStateというpropertyWrapperのprojectedValueであり、その値はBinding<Hoge>型です。Binding<Hoge>型はdynamicMemberLookupをサポートしており、これを通してhogeのプロパティであるfugaをBinding<String>に変換して返すのです。 補足 以上だけだとひょっとすると$hoge[0]のような記法が可能なことが奇妙に思えるかもしれません。実際以下は全く正しく動きます。 struct MyContentView: View { @State private var hoge = ["A", "B", "C"] var body: some View { TextField("入力してね", text: $hoge[0]) .textFieldStyle(RoundedBorderTextFieldStyle()) } } 実はこれもdynamicMemberLookupで実現されています。びっくりするほど気持ち悪いですが、subscriptへのKeyPathというものが存在するおかげです。 let keyPath: WritableKeyPath<[Int], Int> = \.[0] 以上でだいたい網羅できたのではないかと思います。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者】Swift UIを勉強する その⑩ ーーー disabledとDispatchQueueアニメーションをもっと自由に

はじめに 前回は選択されたCardをタップしたら、リスト一覧を出せるようにしました。 しかし、選択されたcoursesは固定のままで、今回はそれぞれのCardをそれぞれのリストを出せるようにします。 完成品↓    目次 disabledとDispatchQueue まとめ 参考文献 disabledとDispatchQueue ・まずはCourse.swiftCourseを新たに作ったModelGroupに移動します。(MVVM) ・現状では、showがtrueかflaseかを条件としてCardの状態を管理しています。 全てのCardは自分が持つリストを認識させるように、まずはseletedItemを作ります。 デフォルトでnil(Optional)をつめて、Cardがまだ選択されていない状態を意味します。 CoursesView.swift @State var show = false @Namespace var namespace @State var selectedItem: Course? = nil ・タップジェスチャーはまだループの外にあるので、Cardの中に移動します。 また、どのCardをタップしたのかをわかってもらうために、クロージャのitemをselectedItemにつめていきます。 CoursesView.swift ForEach(courses) { item in CourseItem(course: item) .matchedGeometryEffect( id: item.id, in: namespace, isSource: !show ) .frame(width: 335, height: 250) .onTapGesture { withAnimation(.spring()) { show.toggle() selectedItem = item } } } ・Card中のListは↑上からselectedItemをもらって、自分専属のリストゲットできるようになりました。 ・ただ、今はCardを開くことしかできなくて、閉じる場合はCardと同じく、ListもonTapGesture()を使います。また、Cardを閉じることによって、selectedItemもいらなくなるので、nilをつめていきます。 CoursesView.swift if selectedItem != nil { ScrollView { CourseItem(course: selectedItem!) .matchedGeometryEffect( id: selectedItem!.id, in: namespace ) .frame(height: 300) .onTapGesture { withAnimation(.spring()) { show.toggle() selectedItem = nil } } .......   ・これで9割ができましたが、タップするスピードが早すぎですと、CardとCardが重なってしまいます。 解決するロジックもシンプルですが、Card①のアニメーションが終わるまで他のCardをdisableにします。 CoursesView.swift @State var isDisabled = false ForEach(courses) { item in CourseItem(course: item) .matchedGeometryEffect( id: item.id, in: namespace, isSource: !show ) .frame(width: 335, height: 250) .onTapGesture { withAnimation(.spring()) { show.toggle() selectedItem = item isDisabled = true } } .disabled(isDisabled) } ・同様に、Cardの中でdisableにしたため、Listでdisableを外します。 DispatchQueueを使ってアニメーションを自由に遅らせることができるので、0.5秒を遅らせば綺麗にできました。 CoursesView.swift if selectedItem != nil { ScrollView { CourseItem(course: selectedItem!) .matchedGeometryEffect( id: selectedItem!.id, in: namespace ) .frame(height: 300) .onTapGesture { withAnimation(.spring()) { show.toggle() selectedItem = nil DispatchQueue.main.asyncAfter( deadline: .now() + 0.5 ) { isDisabled = false } } } ....... まとめ ソースコードGithub 参考文献 Design Code --- SwiftUI for iOS 14
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TCAでsheet表示されたView自身からsheetを閉じる方法

はじめに The Composable Archtecture (TCA) でsheet表示したViewを表示した側から閉じる方法について、とりあえず1パターンだけ示しておきます。 結論 SwiftUIのおさらい SwiftUIではsheetとして表示したViewを閉じる方法としてEnvironmentValuesとしてPresentationModeがある PresentationModeはdismiss()メソッドでViewを閉じれる Viewのみに限定したロジックでViewを閉じたいときはこれだけでいい TCAでReducerの処理からViewを閉じたい場合どうするか Stateの変数が変わった場合、SwiftUIのonChange(of:)メソッドが検知できる onChange(of:)メソッドのクロージャでPresentationModeのdismiss()を呼び出す 詳細 SwiftUIではsheetとして表示したViewを閉じる方法としてEnvironmentValuesとしてPresentationModeがある サンプルコードとしてContentViewがChildViewをsheetで開き、ChildViewのPresentationModeはdismiss()メソッドで自身を閉じれる。 )` //: A UIKit based Playground for presenting user interface import UIKit import SwiftUI import PlaygroundSupport struct ContentView: View { @State var isPresented = false var body: some View { NavigationView { Button( action: { print("action:", isPresented) // => false isPresented = true }, label: { Text("Present View") } ) .sheet(isPresented: $isPresented) { ChildView() } } } } struct ChildView: View { @Environment(\.presentationMode) var presentationMode var body: some View { Button( action: { presentationMode.wrappedValue.dismiss() }, label: { Image(systemName: "xmark.square") Text("Close") } ) } } PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView()) Viewのみに限定したロジックでViewを閉じたいときはこれだけでいい。 余談としてContentViewのisPresentedはChildViewを閉じればfalseになってる。Bindingじゃないのに不思議。誰がisPresentedをfalseにしているんだろうか。onAppearなどで見張ろうとするとPlaygroundが循環参照してる警告を出してくる。SwiftUIは難しい...。 TCAでReducerの処理からViewを閉じたい場合どうするか StateのBoolを用意し、onChange(of:)でviewStoreを見張ってその変化時にpresentationModeを操作すればいい。 struct ChildSttate { var isPresented = true } struct ChildView: View { @Environment(\.presentationMode) var presentationMode let store: Store<ChildState, ChildAction> var body: some View { WithViewStore(store) { viewStore in Button( action: { presentationMode.wrappedValue.dismiss() }, label: { Image(systemName: "xmark.square") Text("Close") } ) .onChange(of: viewStore.isPresented) { value in if !value { presentationMode.dismiss() } } } } } 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS] alphaが0のUIViewのタップイベントを検知する方法

概要 alphaの制御で点滅させたUIViewのタップイベントを拾おうとした時に、alphaが0の時にイベントが拾えなくて困ったのでその対処法を記載します。 原因 UIViewではalphaが0.01以下の場合タップイベントが発生しなくなる為 ※公式では0.01未満と記載されていますが0.01以下でイベントが発生しなくなります。 対処法 alphaが0.01以下の場合でもイベントを返すようhitTestを拡張したUIViewのカスタムクラスを作成する。 HogeView.kt final class HogeView: UIView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if alpha <= 0.01 { return self } return super.hitTest(point, with: event) } } 参考 公式ドキュメント(hitTest(_:with:)): https://developer.apple.com/documentation/uikit/uiview/1622469-hittest
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む