Swift UI （アノテーション・データバインディング・UI部品Tips) ひとまとめ

### AnyView


- AnyView とは
        - https://developer.apple.com/documentation/swiftui/anyview
        - https://www.swiftbysundell.com/articles/avoiding-anyview-in-swiftui/
        - https://qiita.com/noppefoxwolf/items/22a243b28d39b97c8ccb
        - https://www.paveway.info/entry/2022/03/02/swiftui_anyview
        - どんなViewが来ても大丈夫なように型消去したViewがAnyViewである。ただパフォーマンスは悪くなるので、可能なら@ViewBuilderを利用するなどして使用を避けたほうが良いかもしれない。


### @ViewBuilder

- 以下、引用

> 複数画面で共通のナビゲーションバーやボタンを使う場合、毎回同じViewやmodifierを書くのは煩雑なので、共通のViewとして定義しておけば簡単に流用できる。そこでViewBuilderを使えば、HStackやVStackのようにsubviewsを構築するViewを定義できる。
ViewBuilderとは、クロージャーから複数のViewを構築するカスタムパラメータ属性で、複数のViewをTupleViewという型にまとめて返却する。

```swift:.swift
/// 共通のNavigationStack
struct CommonNavigationStack<Content: View>: View {
    let content: Content
    let toolBarColor = Color.pink
    
    // イニシャライザのパラメータに@ViewBuilderを付けることで、複数のViewから構築できるようになる
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        // 共通のNavigationStackに各種設定を適用
        NavigationStack {
            content
                .navigationBarTitleDisplayMode(.inline)
                .toolbarBackground(toolBarColor, for: .navigationBar)
                .toolbarBackground(.visible, for: .navigationBar)
                .toolbarColorScheme(.dark, for: .navigationBar)
        }
    }
}

struct FirstView: View {
    var body: some View {
        // 共通のNavigationStackの中にViewを記述すれば、上で定義した各種設定が反映される
        CommonNavigationStack {
            VStack {
                Text("First View")

                NavigationLink {
                    SecondView()
                } label: {
                    Text("Show Second")
                }
                .padding()
            }
            .navigationTitle("First")
        }
    }
}

struct SecondView: View {
    var body: some View {
        // ここでも共通のNavigationStackを使えば、簡単に共通のナビゲーションバーを適用できる
        CommonNavigationStack {
            Text("Second View")
                .navigationTitle("Second")
        }
    }
}

```


- 以上、引用　[ViewBuilderやViewModifierでSwiftUIのViewを分割する](https://note.com/taatn0te/n/nfcf517fd98d4) より


- その他参考
    - [ViewBuilder](https://developer.apple.com/documentation/swiftui/viewbuilder)
    - [ViewBuilder Tutorial: Creating Reusable SwiftUI Views](https://www.kodeco.com/30070603-viewbuilder-tutorial-creating-reusable-swiftui-views)



### カスタムViewModifier

- 簡単なものならViewのextensionに関数を追加する形でカスタムViewModifierを作れる。ただ状態保持が必要なものなど、複雑なものの場合は、カスタムViewModifierを利用する。ただし、[公式ドキュメント](https://developer.apple.com/documentation/swiftui/viewmodifier)でもあるように、カスタムViewModifierを利用する場合でも、extensionでラップするとよい。
- [ViewBuilderやViewModifierでSwiftUIのViewを分割する](https://note.com/taatn0te/n/nfcf517fd98d4)


### SwiftUIからUIkitを使う <-> UIKitからSwiftUIを使う

#### SwiftUIからUIkitを使う
[UIViewControllerRepresentable](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable), [UIViewRepresentable](https://developer.apple.com/documentation/swiftui/uiviewrepresentable)を使う。

UIView(Controller)Representable プロトコルを批准する構造体を使うことで、UIKitの部品をラップして、SwiftUIで使えるようにしてくれる。ぱっと見ゴチャゴチャしているのだが、以下を覚えておきたい。

- UIKit部品を最初に生成する処理
    - UIView(Controller)Representable プロトコルを批准した構造体の`func makeUIView(context: Context) -> T`メソッドで行う。
- 後からUIKit部品を更新したり、UIKit部品に何らかの処理を行わせるための指示を出す場合
    - UIView(Controller)Representable プロトコルを批准した構造体の`func updateUIView(_ uiView: T, context: Context)`メソッドで行う。私見だが、SwiftUIと絡めて使うなら`@Binding`を使えば指示を出しやすかった。
- UIKit側からの更新を受け取る場合
　　 - Coordinatorというクラスを別に用意し、UIKitからの何かの更新があった際はdelegateメソッドが呼ばれるので、そこで受け取ることができる。

実装例：

```swift:WebView.swift
import SwiftUI
import WebKit

public struct WebKitView: UIViewRepresentable {
    public let url: URL
    public let loadFinished: (() -> Void)?
    @Binding var shouldReload: Bool

    public init(
        url: URL,
        loadFinished: (() -> Void)? = nil,
        shouldReload: Binding<Bool> = .init(get: { false }, set: { _ in })
    ) {
        self.url = url
        self.loadFinished = loadFinished
        self._shouldReload = shouldReload
    }

    public func makeUIView(context: Context) -> WKWebView {
        let view = WKWebView()
        view.navigationDelegate = context.coordinator
        view.load(URLRequest(url: url))
        return view
    }

    public func updateUIView(_ uiView: WKWebView, context: Context) {
        if shouldReload {
            uiView.reload()
            shouldReload = false
        }
    }

    public func makeCoordinator() -> Coordinator {
        .init(parent: self)
    }

    public final class Coordinator: NSObject, WKNavigationDelegate, UIScrollViewDelegate {
        let parent: WebKitView
        init(parent: WebKitView) {
            self.parent = parent
        }

        // MARK: - WKNavigationDelegate
        public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            parent.loadFinished?()
        }
    }
}

// 利用側
struct UserView: View {
    @State private var loadFinished = false
    @State private var shouldReload = false
    var body: some View {
        WebView(
            url: URL("https://example.com")!,
            loadFinished: {
                loadFinished = true
            },
            shouldReload: $shouldReload
        )
    }
}

```

参考: [Pass state/binding to UIViewRepresentable](https://stackoverflow.com/questions/61446026/pass-state-binding-to-uiviewrepresentable)


#### UIKitからSwiftUIを使う
- [UIHostingController](https://developer.apple.com/documentation/swiftui/uihostingcontroller)を使う。

    -> UIHosingControllerに何らかの状態を持たせたいときは

    - たとえば、UIHosingControllerのインスタンスをあるViewControllerで保持しておき、ViewController上で何か状態の変更があったら、UIHosingControllerのインスタンスの`rootView`プロパティを更新することで、UIHosingControllerが保持しているSwiftUI Viewの中身を変更することができる。
    - あるいは、UIHostingControllerに保持させるSwiftUI ViewへObservableObject(後述)を渡しておけば、何らかの状態が変更されたときにその変更がSwiftUI Viewへ伝わるし、逆にSwiftUI View側で状態を変更したときは、それが外部にも伝わるということになる。
    - あるいは、ObservableObjectではなく、CombineのPublisherを渡すこともできる。

    - 以下、引用

```swift:MessageView.swift
struct MessageView: View {
    let publisher: AnyPublisher<String, Never>

    @State private var message: String = ""

    var body: some View {
        Text(message)
            .onReceive(publisher) { message = $0 }


    }
}
```

-　以上、引用 [SwiftUIとUIKitを仲良くさせる](https://speakerdeck.com/auramagi/iosdc-2022-swiftui-uikit?slide=17)より


 - 参考: [SwiftUIとUIKitを仲良くさせる](https://speakerdeck.com/auramagi/iosdc-2022-swiftui-uikit?slide=17)


### @Binding


[Binding](https://developer.apple.com/documentation/swiftui/binding)

- @Bindingとは、別のViewでそのViewの@State変数を変更するための、カスタム属性です。@Stateを@Bindingに渡すには、$をつけて渡します。
    - [【SwiftUI】@Bindingの使い方を徹底解説](https://ios-docs.dev/swiftui-binding/)

#### Local Binding (Custom Binding)

Bindingとは構造体であって、以下のようにイニシャライザも用意されている。これを利用することで、値のget, setの際に行うべき動作をカスタマイズさせることもできる。

##### [init(get: @escaping () -> Value, set: @escaping (Value) -> Void)](https://developer.apple.com/documentation/swiftui/binding/init(get:set:)-7ufcp)



- 以下、引用

> アプリの開発をすすめると 実際にはもう少し複雑なケースがでてきます。
例えば、実際には Int で管理している数値を スライダーUIを使用して変更したいときです。
Slider が受け取る Binding は、Double 等の BinaryFloatingPoint に準拠している必要があります。
Double や Float は BinaryFloatingPoint に準拠していますが、Int は、準拠していません。つまり、Int の Binding は渡すことができないということです。
そのほかにも、実際には指定された値を調整してからデータとして保持したいとか、表示する時に特定の加工を行ったものを表示したい等があります。そんな時には、ローカルに Binding を作って渡すことができます。Apple のドキュメントを見てみると、Binding は、struct であることもわかります。
initializer を見てみると、Binding を受け取るものや projectedValue を受け取るものと並んで、get/set を指定する initializer が見つかります。この initializer を使って、Binding を作成することになります。

```swift:ContentView.swift
//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/03/16
//  © 2022  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State private var amount: Amount = .medium

    var body: some View {
        VStack {
            Text("Selection: \(amount.description)")
            Slider(value: Binding(get: {
                return amount.representValue
            }, set: { newValue in
                if Amount.smallRange.contains(newValue) {
                    amount = .small
                } else if Amount.mediumRange.contains(newValue) {
                    amount = .medium
                } else {
                    amount = .large
                }
            }))
        }
        .padding()
    }

    enum Amount: CustomStringConvertible {
        static let smallRange = 0..<0.25
        static let mediumRange = 0.25..<0.75
        static let largeRange = 0.75...1
        case small, medium, large
        var description: String {
            switch self {
            case .small:
                return "Small"
            case .medium:
                return "Medium"
            case .large:
                return "Large"
            }
        }
        var representValue: Double {
            switch self {
            case .small:
                return 0.15
            case .medium:
                return 0.5
            case .large:
                return 0.85
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

```

- 以上、引用　[[SwiftUI] Binding のおさらいと Local Binding](https://software.small-desk.com/development/2022/03/25/swiftui-binding-local-binding/)　より

- その他参考：
    - [How to create custom bindings](https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-custom-bindings)

##### [init(get: @escaping () -> Value, set: @escaping (Value, Transaction) -> Void)](https://developer.apple.com/documentation/swiftui/binding/init(get:set:)-6g3d5)

こちらのイニシャライザは一つ上のやつと違って、setterのところにTransactionという引数が追加されている。

#### animation(_:)

Bindingの変更がUIに変更される際に、アニメーションを伴って変更してほしい場合は、[animation(_:)](https://developer.apple.com/documentation/swiftui/binding/animation(_:))メソッドを用いる。


### @ObservedObject, ObservableObject, @Published

@Stateと同様、データバインディングの仕組みの一つで、データクラスの更新を監視する。

- 以下、引用

> ＠StateではViewのプロパティに対してその仕組みを実現しますが、データクラスに対しては＠ObservedObjectを使用します。

> ポイントは、次の３点です。
データクラスはObservableObjectプロトコル準拠とする。
監視対象とするプロパティに@Published属性を付加する。
データクラスのインスタンスは@ObservedObject属性を付加してViewの中で宣言する。

```swift:UserView.swift
/// ユーザークラスの定義
class User: ObservableObject {
    @Published var name = ""
    @Published var level = 1
}
 
struct ContentView: View {
    @ObservedObject var user = User()   // インスタンスを生成
    var body: some View {
        VStack {
            Text("\(user.name)さんのレベルは\(user.level)です")
            
            /// 入力フォーム
            TextField("名前", text: $user.name)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(width: 300)
                .padding()
            Button("レベルアップ") {
                user.level += 1
            }
        }
    }
}

```
- 以上、引用　[【SwiftUI】@ObservedObjectの使い方](https://capibara1969.com/2508/) より


#### @Bindingとの違い

@ObservedObjectを丸々子Viewに渡し、子ビューの側でも@ObservedObjectマークをつければ、子ビューの側ともバインディングすることができる。

@Bindingも同様の機能を果たすが、@BindingはObservableObjectに準拠していないような型（例えば、String, Int, その他自作クラスなど）を子ビューに渡してバインディングさせたいような時に使うので、@ObservedObjectとはこの点で使い分ける。

[【SwiftUI】@ObservedObjectの使い方を徹底解説](https://ios-docs.dev/swiftui-observerdobjects/)


### @State, @Binding, @StateObject, @ObservedObject 使い分け

- @State: データが値型で、データの発生源がView自身の場合
- @Binding: 値型のデータで、データの発生源は親Viewなど外から渡される場合
- @StateObject: 参照型データオブジェクトを扱い、データの発生源はView自身の場合
- @ObservedObject: 参照型データオブジェクトを扱い、データの発生源は親Viewなど外から渡される場合 
    - ObservedObjectとStateObjectの違いについて混乱するかと思いますが、ObservedObjectは該当ViewのBodyに何らかの変更が加わるたびにリセット・再生成されます。そのため子View自身が保持してほしいデータについてObservedObjectとしてしまうと、親ViewのBodyに何か変更があるたびに子Viewのボディも変更されるため、そのデータがリセットされてしまいます。そのため、子View自身の管理下に置いておきたいデータはStateObjectとすべきです。StateObjectであれば、データが保持されます。子View自身の管理するデータであるため、Stateと同様、privateマークをつけておくべきです。

- [SwiftUIのデータ管理 Property Wrapper編](https://blog.personal-factory.com/2021/01/23/how-to-use-propertywrapper-in-swiftui/)

#### @Stateと@StateObjectの使い分け　補足

上に述べたように基本的に@Stateは値型、@StateObjectは参照型の変数につけていくことになる。公式リファレンスにもそう書いてある。

- [Apple Developer - State](https://developer.apple.com/documentation/swiftui/state)
- [Apple Developer - StateObject](https://developer.apple.com/documentation/swiftui/stateobject)

また少し特別な場合として、[Observable](https://developer.apple.com/documentation/observation/observable)プロトコルに準拠するような変数につける場合は(値型であっても)@State, [ObservableObject](https://developer.apple.com/documentation/Combine/ObservableObject)プロトコルに準拠する様な変数につける場合は@StateObjectとも述べられている。ObservableObjectはiOS13の頃からあるが、ObservableはiOS17から追加された比較的新しいものである。

補足としては、@Stateは、変数の参照が丸ごと入れ替わったような時にのみ反応し、変数であるクラス、構造体の中身の変数に何か変更があっただけでは何も反応しないとしている。これとは対照的に、@StateObjectは双方の場合に反応するとしている。だからこそ、クラス、構造体の中身の変数に何か変更があった時に変更の合図を出せる[ObservableObject](https://developer.apple.com/documentation/Combine/ObservableObject)に準拠している型の場合は、@StateObjectを用いろということだろう。

### @EnvironmentObject


機能としては@ObservedObjectと同じく、子Viewに情報をBindingする時に親Viewから渡すものですが、間に何個も子Viewが挟まると目的の孫ビューまでいちいち伝言ゲームのように渡していくのはいかにも無駄なことがあります。その場合、孫ビューから直接参照できるように親ビューで設定しておくことができます。

親ビュー側では、親ビューを初期化する際に親ビューインスタンスの[environmentObject(_)](https://developer.apple.com/documentation/swiftui/view/environmentobject(_:))メソッドを呼び、目的のオブジェクトを渡しておく必要があります。渡していないとランタイムエラーとなります。

孫ビュー側では、[@EnvironmentObject](https://developer.apple.com/documentation/swiftui/environmentobject)マークをつけた上で、目的のオブジェクトに直接アクセスできます。

### @AppStorage

UserDefaultに直接アクセス＆データに変更があればViewに反映される便利なマーク。
[AppStorage](https://developer.apple.com/documentation/swiftui/appstorage)
[What is the @AppStorage property wrapper?](https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper)

@AppStorage の変数について、初期値 を設定すると、UserDefaultに値がない時のみ初期値として使われる。すでにUserDefaultsに値がある場合は、それが使われ、初期値は無視される。例えば、以下の「10」は、すでにUserDefaultsに値がある場合は無視される。

```swift:test.swift
    @AppStorage("hoge") private var hoge = 10
```

### @SceneStorage

Scene内部で、軽いデータを共有するために用いることができるマーク。Scene外にはデータを伝えることはできない。Sceneを破棄するとこのデータも破棄される。

[SceneStorage](https://developer.apple.com/documentation/swiftui/scenestorage)
[Introducing @SceneStorage in SwiftUI](https://crystalminds.medium.com/introducing-scenestorage-in-swiftui-5a4ec1a90ca3)

### PropertyWrapper 全部　使い分け


- 画像引用:
![スクリーンショット 2023-01-13 18.38.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392332/72b56aae-bbc6-b068-c8f1-06dd05e4cc4c.png)
[SwiftUIのデータ管理 Property Wrapper編](https://blog.personal-factory.com/2021/01/23/how-to-use-propertywrapper-in-swiftui/)

### [@UIApplicationDelegateAdaptor](https://developer.apple.com/documentation/swiftui/uiapplicationdelegateadaptor)

SwiftUI AppからAppDelegateを使いたいときに利用する。

```swift:Main.swift
@main
struct MainApp: App {
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

```

### @GestureState 

@Stateと似たような立ち位置だが、ドラッグなどジェスチャーの位置により何かUIを変化させたいときに用いる。例えば、ドラッグの位置にあるViewを追随させるなど。@Stateよりパフォーマンスが良く、またジェスチャーが終わった場合は自動で元の位置に戻るという利点がある。

[What is the @GestureState property wrapper?](https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-gesturestate-property-wrapper)

使用時は[updating(_:body:)](https://developer.apple.com/documentation/swiftui/gesture/updating(_:body:))というメソッドと組み合わせて用いる。

```swift:example.swift
struct SimpleLongPressGestureView: View {
    @GestureState private var isDetectingLongPress = false


    var longPress: some Gesture {
        LongPressGesture(minimumDuration: 3)
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }
    }


    var body: some View {
        Circle()
            .fill(self.isDetectingLongPress ? Color.red : Color.green)
            .frame(width: 100, height: 100, alignment: .center)
            .gesture(longPress)
    }
```
- Apple [GestureState](https://developer.apple.com/documentation/swiftui/gesturestate)より引用


### @FocusState

@Stateの派生系で、テキストフィールドなどが画面上に複数ある場合、どのテキストフィールドにフォーカスを合わせキーボードを出していくのかを制御するためのもの。

テキストフィールドなどが２つしかない場合は、単にBool型の変数に@FocusStateをつけることになる。

逆に３つ以上の場合は、ケースが3つ以上ある専用のenumを作り、そのenumを型とする変数に@FocusStateをつけることになる。

- 以下、引用

```swift:ContentView.swift
struct ContentView: View {
    @FocusState private var isUsernameFocused: Bool
    @State private var username = "Anonymous"

    var body: some View {
        VStack {
            TextField("Enter your username", text: $username)
                .focused($isUsernameFocused)

            Button("Toggle Focus") {
                isUsernameFocused.toggle()
            }
        }
    }
}
```

```swift:ContentView.swift
struct ContentView: View {
    enum FocusedField {
        case username, password
    }

    @FocusState private var focusedField: FocusedField?
    @State private var username = "Anonymous"
    @State private var password = "sekrit"

    var body: some View {
        VStack {
            TextField("Enter your username", text: $username)
                .focused($focusedField, equals: .username)

            SecureField("Enter your password", text: $password)
                .focused($focusedField, equals: .password)
        }
        .onSubmit {
            if focusedField == .username {
                focusedField = .password
            } else {
                focusedField = nil
            }
        }
    }
}

```

- 以上、引用 [Hacking with Swift - What is the @FocusState property wrapper?](https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-focusstate-property-wrapper)




### 上記を踏まえて、いろいろなViewから使いまわせる部品のような子Viewを作るとすると:

```swift:ParentView.swift
struct ParentView<Presenter>: View where Presenter: ParentPresenterProtocol {
    @StateObject var presenter: Presenter

    var body: some View {
        VStack(spacing: 0) {
            title
            childABody
            childBBody
        }
    }

    @ViewBuilder
    var title: some View {
        Text("title")
    }

    @ViewBuilder
    var childABody: some View {
        Button {
            presenter.sendEvent(.childA)
        } label: {
            Text("ChildA")
        }
    }

    @ViewBuilder
    var chilBABody: some View {
        Button {
            presenter.sendEvent(.childB)
        } label: {
            Text("ChildB")
        }
    }
}
```

### Observation系列
上記長々解説してきたところ申し訳ないが、2023年以降(iOS17以降)はObservedObjectではなく、新登場の[Observation(@Observableマクロ)](https://developer.apple.com/documentation/observation)を用いた方が簡単かつパフォーマンスが良さそう。

注意点としては

- @Observableマクロをつける対象はclass(なので、@ObservableObjectの代替となる)。プロパティに@Publishedなどと言うマークはいらない。プロパティをただ宣言するだけでView側からの購読対象とできる。

```swift:ContentViewModel.swift
@Observable
final class ContentViewModel {
    var model: ContentModel = .init()
    // do something
}
```

- @Observableマクロをつけた対象のクラスの中で複数プロパティがある場合、あるプロパティが更新された場合はそこに関連するSwift UI ビューの部品のみが更新され、別のプロパティに基づく部品は更新されないので、パフォーマンスに良い。

- View側では、「@StateObject」ではなく「@State」をつけてプロパティとして宣言する。それだけで購読できる。

なお、iOS17から追加された[「@Bindable」](https://developer.apple.com/documentation/swiftui/bindable)をつけて宣言すれば、双方向バインディングとできる。

```swift:ContentView.swift
@MainActor
struct ContentView: View {
    // これだけでOK
    @State private var viewModel: ContentViewModel = ContentViewModel()
    
    var body: some View {
        VStack {
            Image(viewModel.model.handState.imageName ?? "", bundle: nil)
                .resizable()
                .layoutPriority(1)
                .padding(.bottom, 5)
            Text(viewModel.model.handState.text)
                .frame(height: 60)
                .padding(.bottom, 5)
            Button(action: {
                viewModel.onTapButton()
            }, label: {
                Text("Please tap")
                    .frame(height: 60)
            })
        }
        .padding()
    }
}
```

詳細は:

[さようなら、Combine。そしてこんにちは、Observation。](https://qiita.com/lovee/items/4909d0166bba973b3bc0)

### PropertyWrapperとは

getter や setterを いちいち書かずに済むよう、アノテーションマークをいっこ書くだけで済むようにするための仕組み。SwiftUIの@Stateなど数々のマークで使われている。 

もちろんPropertyWrapperとは自体は@Stateなどのマークとは別個の仕組みであるので、自分でオリジナルのPropertyWrapperを作ることもできる。 例えばUserDegfaultsで毎回getterやsetterを書いていたところを簡単に書けるようにするなどの目的で利用できる。その他、変数のセットやゲットの時に何かの制御をさせたいようなあらゆる場面で利用できる。

利用時には、class, struct, enumに@PropertyWrapperマークをつけると同時に、任意の型の`wrappedValue`という変数を設ける。

projectedValueは、PropertyWrapperの値と関連した何らかの任意の型の値を返させるために使う。使わなくても良い。$マークを変数につけてアクセスできる。

[Properties - Swift.org](https://docs.swift.org/swift-book/LanguageGuide/Properties.html)

### DynamicMemberLookUp とは

定義されていない名前の変数でもあたかも存在するかのように動的に値を返せる仕組み。SwiftUIの@Stateなど数々のマークで使われている。

DynamicMemberLookUp自体は@Stateなどのマークとは別個の仕組みである。そのため自分でオリジナルのDynamicMemberLookUpに準拠した型を作ることもできる。が、自分で利用する機会はそれほどないかもしれない。ライブラリなどを作成するとき、内部の仕様を利用者に意識させないよう隠蔽するために使うなどか。

利用時には、class, struct, enumに@DynamicMemberLookUpマークをつけると同時に、`subscript(dynamicMember: )`というメソッドを作ることで、DynamicMemberLookUpへの適合が完了する。

[[Swift]Dynamic Member LookupからSwift5.1で追加されたKeyPath Member Lookupまで](https://dev.classmethod.jp/articles/playground-dynamic-member-lookup-dynamiccallable/)

### PreferenceKey

子ビューから親ビューへ情報を上げていく仕組みについてBindingを紹介したが、妙な振る舞いになることがあるので複雑なユースケースでは代わりに[PreferenceKey](https://developer.apple.com/documentation/swiftui/preferencekey)という仕組みを利用する。

[SwiftOnTap - PreferenceKey](https://swiftontap.com/preferencekey)


- 以下、引用

```swift:ContentView.swift
struct ContentView: View {
    var body: some View {
        VStack {
            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle1")

            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle2")

            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle3")

            Color.clear
                .preference(key: StringPreferenceKey.self, value: "Rectangle4")
        }
        .onPreferenceChange(StringPreferenceKey.self) { value in
            print("onPreferenceChange() value: \(value)")
        }
    }
}

struct StringPreferenceKey: PreferenceKey {
    typealias Value = String
    static var defaultValue: Value = "Default"

    static func reduce(value: inout Value, nextValue: () -> Value) {
        let next = nextValue()
        print("reduce() value: \(value), nextValue: \(next)")
        value = next
    }
}

このビューを画面に表示したとき、コンソールには以下のように出力されます。
reduce() value: Rectangle1, nextValue: Rectangle2
reduce() value: Rectangle2, nextValue: Rectangle3
reduce() value: Rectangle3, nextValue: Rectangle4
onPreferenceChange() value: Rectangle4

```


- 以上、引用 [[SwiftUI] PreferenceKeyの基本的な仕組みを知る](https://qiita.com/takehilo/items/2499c632c2e0e5cdcb06) より
[What is PreferenceKey in SwiftUI](https://medium.com/@fatihcyln/what-is-preferencekey-in-swiftui-3a3f0056b147)

## ローカライズ

### String Catalog
String Catalogというファイルを用いて、各言語での文字列のローカライズを行うことが推奨されている。(2023年より)
[Localizing and varying text with a string catalog](https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog)

[Evolution of Localization in Swift: From Strings to String Catalogs](https://hackernoon.com/evolution-of-localization-in-swift-from-strings-to-string-catalogs)

Xcodeより`Localizable.xcstrings`ファイルを追加するだけで、ビルド時にコード上の文字列リテラルを自動で走査し、`Localizable.xcstrings`にまとめてくれるようになるので、かなり便利ではある。

その場合、`Localizable.xcstrings`の該当キーをXcodeの右ペインで見ると`Managed`プロパティが`Automatically`になっているのがわかるだろう。逆に`Localizable.xcstrings`に自ら手動で追加したものは`Manually`となる。`Manually`としたものは、`Localizable.xcstrings`にてプログラマ自らキーやバリューを変えることも可能。

`Manually`,`Automatically`はXcode右ペインから相互に変換できる。そのため、一旦`Automatically`で自動生成してもらい、その後`Manually`に変更して明示的に管理していくこともできる。これはプロジェクトに途中から導入するときに便利だろう。

利用時は以下のように使える:

```swift:Example.swift
import SwiftUI

Text("foo")//キー名fooを入力するだけで、翻訳後の文字列が自動で表示され便利ではある。

// 中略

Text(String(localizable: "foo"))//このように翻訳されていることを明示的に示すことも可能。
// 一見面倒のようだが、可読性は上がるのでこちらを推奨。
```

デフォルトの言語は英語となっているので、日本語のみにする場合は:
- Xcodeのproject>Info>LocalizationsよりEnglishを削除し、Japaneseのみとし、Japaneseにチェックがついていることを確認。
- `Localizable.xcstrings`ファイルを右クリック>Open As...>SourceCodeをクリックしソースコードとして開き、`"sourceLanguage" : "en",`を`sourceLanguage" : "ja",`に変更する。

欠点としては、利用時にキー名を指定する際に`"foo"`のような文字列リテラルをハードコーディングで使わざるを得ない。有名ライブラリ`R.swift`や`SwiftGen`はStringCatalogにまだ対応しておらず
(2025/2時点)、タイポを防ぐために文字列リテラルを使わないようにしたい場合は、新し目のライブラリである[liamnichols/xcstrings-tool](https://github.com/liamnichols/xcstrings-tool)を使うと良いかもしれない。

このライブラリを使うと利用法は以下のようになる:
- ビルドを行う。すると、xcstrings-toolが文字列利用のためのstaticプロパティを自動生成。`DerivedData`内に生成される。

```swift:Example.swift
        String(localizable: .foo) // 「フー」
```

## UI部品

### ボタン

かなり頻繁に使う部品と言える。

イニシャライザの`label`プロパティから外観を変更しても良い。が、同じような見た目のボタンを使い回すなら`.buttonStyle`モディファイアに自分で作ったカスタムボタンスタイルを渡すと便利である。

```swift:Test.swift
struct ContentView: View {
    var body: some View {
        Button(action: {
            print("Hello!")
        }) {
            Text("FooBar")
        }
        .buttonStyle(MyButtonStyle())
    }
}


struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding()
            .background(.green)
            .cornerRadius(4)
            .padding()
    }
}

```


[ButtonStyle](https://developer.apple.com/documentation/swiftui/buttonstyle)

### ラベル

アイコンと文字列からなるUI部品である。

そのまま使っても良いが、同じモディファイアをつけたラベルを様々なところで使い回すような場合、`.labelStyle`モディファイアを使うと良い。クロージャの中で、`LabelStyleConfiguration`という引数が用意されている。`Configuration`の中の`icon`と`title` に様々なモディファイアをつけておくことで、そのスタイルをあちこちで使い回すことができ、可読性が上がる。

[labelStyle(_:)](https://developer.apple.com/documentation/swiftui/view/labelstyle(_:))
[LabelStyleConfiguration](https://developer.apple.com/documentation/swiftui/labelstyleconfiguration)

公式使用例:

[apple/sample-food-truck](https://github.com/apple/sample-food-truck/blob/820dd9fd2165debc6fd6d29f18169ebb45787dde/App/Truck/Cards/CardNavigationHeaderLabelStyle.swift)

### アラート

- iOS15以上: [alert(_:isPresented:actions:message:)](https://developer.apple.com/documentation/swiftui/view/alert(_:ispresented:actions:message:)-6awwp)


- iOS14以下: [alert(isPresented:content:) deprecated](https://developer.apple.com/documentation/swiftui/view/alert(ispresented:content:))

- 実装例

```swift: .swift
            if #available(iOS 15.0 *) {
                self.view
                    .alert(
                        "Logout",
                        isPresented: isPresented,
                        actions: {},
                        message: { 
                            let msg = "Logout"
                            Text(msg) 
                        }
                    )
            } else {
                self.view
                    .alert(isPresented: isPresented) {
                        Alert(
                            title: Text("Logout"),
                            dismissButton: .default(Text("OK"))
                        )
                    }
            }

```

### プログレスバー・ローディング表示

- [ProgressView](https://developer.apple.com/documentation/swiftui/progressview)

```swift:.swift
ProgressView()

```

### [List](https://developer.apple.com/documentation/swiftui/list)

上記ドキュメントによれば以下の形式がある。

- 静的に子を配置していくタイプ
- Identifiableに準拠したデータにより動的に子を生成するタイプ
- Identifiableに準拠していないデータにより動的に子を生成するタイプ（Identifiableの代わりに、KeyPassを用いる）
- ファイルシステムのように階層構造を持ったリストを生成するタイプ
- 各子部品に編集動作を指定できるタイプ

プルトゥリフレッシュ操作をさせたい場合は、[refreshable](https://developer.apple.com/documentation/swiftui/view/refreshable(action:))モディファイアを使えば良い。

### ウェブ上にある画像をダウンロードして表示したい。

[AsyncImage](https://developer.apple.com/documentation/swiftui/asyncimage)が使える。（iOS15以上から。）

```swift:Example.swift
AsyncImage(
    url: utl
) { image in
    image
        .resizable()
        .scaledToFit()
        .frame(height: 50)
} placeholder: {
    ProgressView()
}
```


### 何らかの計算などを伴う複雑なレイアウト

大抵、GeometryReaderを用いると解決できる。該当ビューの親ビューのサイズやフレームを教えてくれる。GeometryReaderが一番外側にある場合は画面サイズも取得できる。

[SwiftUIの肝となるGeometryReaderについて理解を深める](https://qiita.com/masa7351/items/0567969f93cc88d714ac)

### 親ビューの中のある特定の子ビューが想定よりも小さく潰れて表示されてしまっている。

GeometryReaderを使う、`.frame()`モディファイアを使うなどが考えられるが、`.layoutPriority()`モディファイアでも簡単に解決する場合があるので選択肢に入れておきたい。これは、特定のViewにDouble型の引数を指定することで、そのViewの大きさの優先度を設定するようなものである。

[layoutPriority(_:)](https://developer.apple.com/documentation/swiftui/view/layoutpriority(_:))
[How to control layout priority using layoutPriority()](https://www.hackingwithswift.com/quick-start/swiftui/how-to-control-layout-priority-using-layoutpriority)
[【Swift】SwiftUIのListでダイナミックな高さを実現する方法](https://qiita.com/shiz/items/acabed1af1331bb2a966)


#### ScrollViewで、検索バーのような部分はスクロールしても画面上部に固定されるようにしたい。

子ビューにおいてGeometryReaderを使い、親ビューの`frame`を監視し、特定の領域までスクロールされた場合は、特定の部品を`.offset`モディファイアで固定するなどが考えられる。

```swift:MainScreen.swift
import SwiftUI

struct MainScreen: View {

    @State var searchQuery: String = ""

    let headerHeight: CGFloat = 30
    let searchBarHeight: CGFloat = 44

    @State var startOffset: CGFloat = 0
    // 現在のスクロール位置。
    @State var offset: CGFloat = 0

    private func safeAreaInsetsTop() -> CGFloat {
        guard let window = UIApplication.shared.windows.first(where: \.isKeyWindow) else { return 0.0 }
        return window.safeAreaInsets.top
    }

    private func getYOffset() -> CGFloat {
        // searchbarが下にスクロールされているケース: 特にsearchbarをずらす必要はない。
        guard offset < 0 else { return 0 }

        // searchbarが上にスクロールされているケース:
        return offset <= -headerHeight ?
            -(offset + headerHeight) :// searchbarが画面外に出てしまうケース:画面外に出ない位置で固定する。
            0// searchbarがまだ画面内にあるケース:まだ画面内にあるので特にsearchbarをずらす必要はない。
    }

    var body: some View {
        ScrollView {
            ZStack(alignment: .top) {
                contents// スクロールビューの中身
                    .padding(.top, headerHeight + searchBarHeight + 10)

                // ヘッダ類
                VStack(spacing: 0) {
                    Text("Some Header")
                        .frame(width: .infinity, height: headerHeight)
                    searchbar
                }
                .frame(width: .infinity)

            }
            .overlay(

                // ScrollViewの中身(ZStack)が上下にどの程度スクロールされているのかを監視。
                GeometryReader { proxy -> Color in
                    let minY = proxy.frame(in: .global).minY
                    if startOffset == 0 {
                        startOffset = offset
                    }

                    // 現在のスクロール位置を記録
                    offset = minY - startOffset
                    print(offset)

                    return Color.clear
                }
                .frame(width: 0, height: 0)

                , alignment: .top
            )

        }
        .clipped()
    }

    private var contents: some View {
        VStack {
            Text("offset: \(offset) safeAreaInsetsTop: \(safeAreaInsetsTop())")
            ForEach(0..<100) { num in
                Text("Number \(num)")
            }
        }
    }

    private var searchbar: some View {
        TextField("Search", text: $searchQuery)// 検索バー
            .frame(width: .infinity, height: searchBarHeight)
            .background(Color.white)
            .offset(y: getYOffset())
    }
}

struct MainScreen_Previews: PreviewProvider {
    static var previews: some View {
        MainScreen()
    }
}

```

以下の動画も参考になる。
[SwiftUI 2.0 Custom ScrollView With Sticky Top Search Bar - Navigation Search Bar - SwiftUI Tutorials](https://www.youtube.com/watch?v=Iv-or6kTNIE)

### 地図

MapKitにて`Map`が用意されており、SwiftUIで使える。地図上に現在地を表示したり、マークや経路図などさまざまなアイテムを貼り付けられる。

https://developer.apple.com/documentation/mapkit/map

典型的には、Mapのクロージャの中にMarkerを入れ、該当地点にマーカーをつける、およびMapのイニシャライザに渡すposition変数により地図の位置を操作するといった処理をよく行う。

[`.mapStyle()`モディファイア](https://developer.apple.com/documentation/mapkit/map/4231685-mapstyle)で衛星写真かイラスト描画かを変更できる。

### ヘルプ

ユーザーのために操作のヒントなどを表示するためのUI部品。

[`TipKit`](https://developer.apple.com/documentation/TipKit)があり、SwiftUIの中で[`TipView`](https://developer.apple.com/documentation/tipkit/tipview)を使うことができる。(iOS17以上)吹き出し状にテキスト、画像を出すことができる。

visionOS, macOSにおいてはポインタを重ねる(or 視線を合わせる)だけでヘルプマークが出るが、これは[help](https://developer.apple.com/documentation/SwiftUI/View/help(_:)-6oiyb)モディファイヤが使える。

詳細な実装方針については

[Human Interface Guideline - Offering help](https://developer.apple.com/design/human-interface-guidelines/offering-help)
[SwiftUIでTipKitを使用してユーザーにヒントを表示（iOS 17、WWDC 2023）](https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7)

を参照

### 画像などをSNSや他アプリにシェアしたい

[ShareLink](https://developer.apple.com/documentation/SwiftUI/ShareLink)を用いる。

### タブバー

- [TabView](https://developer.apple.com/documentation/swiftui/tabview/)

- 以下、引用
- タブのタップ時にアニメーションをさせたい場合 (`onChange`):

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {

    @State private var selectionType: SelectionType = .circle
    /// アニメーション実行用フラグ
    @State private var shouldStartAnimation = false

    var body: some View {
        TabView(selection: $selectionType) {
            ForEach(SelectionType.allCases) { type in
                Image(systemName: type.rawValue)
                    .font(.system(size: 300))
                    .tag(type)
                    .tabItem {
                        Image(systemName: type.rawValue)
                    }
                // アニメーションフラグによってscaleを変更
                    .scaleEffect(shouldStartAnimation ? 1.5 : 1.0)
            }
        }
        .onChange(of: selectionType) { _ in

            // アニメーションを実行
            withAnimation {
                shouldStartAnimation = true
            }

            // 0.5秒後にアニメーションを終了
            Timer.scheduledTimer(withTimeInterval: 0.5,
                                 repeats: false) { _ in

                withAnimation {
                    shouldStartAnimation = false
                }
            }
        }
    }
}
```

- 選択中のタブをクリックしても処理を実行する(`onReceive`)

```swift:ContentView.swift
struct ContentView: View {

    @StateObject private var viewModel = ViewModel()

    var body: some View {
        TabView(selection: $viewModel.selectionType) {
            ForEach(SelectionType.allCases) { type in
                Image(systemName: type.rawValue)
                    .font(.system(size: 300))
                    .tag(type)
                    .tabItem {
                        Image(systemName: type.rawValue)
                    }
                    .scaleEffect(viewModel.scaleEffectValue)
            }
        }
        .onReceive(viewModel.$selectionType) { _ in
            viewModel.startAnimation()
        }
    }
}
```

```swift:ViewModel.swift
class ViewModel: ObservableObject {
    @Published var selectionType: SelectionType = .circle
    @Published var shouldStartAnimation = false

    var scaleEffectValue: CGFloat {
        return shouldStartAnimation ? 1.5 : 1.0
    }

    func startAnimation() {
        withAnimation {
            shouldStartAnimation = true
        }

        Timer.scheduledTimer(withTimeInterval: 0.5,
                             repeats: false) { _ in

            withAnimation {
                self.shouldStartAnimation = false
            }
        }
    }
}

```

- 以上、引用[【SwiftUI】TabViewのタブが選択された時に処理を実行する](https://dev.classmethod.jp/articles/swiftui-tabview-select/)

### ツールバー

[toolbar(content:)](https://developer.apple.com/documentation/swiftui/view/toolbar(content:)-5w0tj)モディファイアを使うと、ツールバーを設置できる（ナビゲーションバーの上、キーボードの上等）

[【SwiftUI】toolbarの使い方！アイテムにボタンを増やす方法](https://tech.amefure.com/swift-toolbar)

何らかのボタンはもちろん、いわゆるタイトルも設定できる。

```swift:Example.swift
        .toolbar {
            ToolbarItem(placement: .principal) {
                Text(L10n.MyScreen.title)
            }
        }

```


### ColectionViewライクなView

#### iOS13以下

ScrollViewの中にVStackやHStackを入れる。

#### iOS14以上

ScrollViewの中にLazyVGridやLazyHGridを入れる。パフォーマンス面でこちらの方が良い。

[The SwiftUI Equivalents to UICollectionView](https://betterprogramming.pub/the-swiftui-equivalents-to-uicollectionview-60415e3c1bbe)
[SwiftUIでLazyなGridとStackの使い方を学ぶよ](https://qiita.com/akky-tys/items/c644f55f1eb847b2546c)

LazyVGridなどの中にZStackを入れるとスクロールビューのコンテンツがが画面外（横方向）にはみ出してしまうことがあった。`.frame(width: UIScreen.main.bounds.width)`として、ScrollViewの中身のVStackの横の長さを端末サイズに制限するとうまくいった。

LazyVGrid, LazyHGrid を使うときは[GridItem](https://developer.apple.com/documentation/swiftui/griditem)を使うことになる。GridItemのサイズには三種類あり、Fixed, Flexible, Adaptiveである。Fixedはプログラムで指定したGridItemの個数を絶対に守り、スクリーンのサイズが足りなくてもそれをはみ出す。Flexibleは可能な限りプログラムで指定したGridItemの個数を守ろうとするが、スクリーンサイズが足りない場合はスクリーンサイズに合わせて個数を減らす。Adaptiveは、プログラムで指定したGridItemの個数を一切守らず、スクリーン幅に照らしてちょうどいいサイズにまで広がる。Adaptiveの場合、GridItemを何個追加しようと無意味であるため、GridItemは一個書けば良いこととなる。
参考: [SwiftUI Grid: fixed vs flexible vs adaptive](https://paigeshin1991.medium.com/swiftui-grid-fixed-vs-flexible-vs-adaptive-253e9b12da34)

### 画像をいじる

Imageクラス。

```swift:test.swift
            Image(uiImage: image)
                .resizable()
                .hueRotation(Angle(degrees: viewStore.hue * 360))
                .saturation(viewStore.saturation)
                .brightness(viewStore.brightness)
                .contrast(viewStore.contrast)
```

- [huerotation()](https://developer.apple.com/documentation/swiftui/image/huerotation(_:))
    - 色相を変換する。色相とは円で表される色のレパートリーであって、引数として角度（Angle）を指定し、色相の円をどの程度移動するか決められる。
- [saturation()](https://developer.apple.com/documentation/swiftui/view/saturation(_:))
    - 色の彩度、0.0~1.0を指定。
- [brightness()](https://developer.apple.com/documentation/swiftui/view/brightness(_:))
    - 明度。0.0~1.0を指定。
- [contrast()](https://developer.apple.com/documentation/swiftui/form/contrast(_:))
    - コントラスト。-1.0~1.0を指定。


### マテリアル

背景色として色を指定する代わりに、マテリアルを指定して、不透明感のあるエフェクトをかけて背景を見にくくするようなエフェクトを出すことができる。

特にVisionOSでは背景に色を指定するのは推奨されていない。周囲の環境の明るさの変化に対応するため、マテリアルを代わりに使用することが推奨されている。

直接初期化するのではなく、ultraThin, thin, regular, thick, ultraThick, bar といった定義済みのものを利用する。


[Materials](https://developer.apple.com/design/human-interface-guidelines/materials#visionOS)
[SwiftUI - Material](https://developer.apple.com/documentation/swiftui/material)


### 影をつける

[.shadow](https://developer.apple.com/documentation/swiftui/form/shadow(color:radius:x:y:))モディファイアで簡単につけることができる。

もし特定の方向（上下左右）のみ影を出したい時は、[mask(alignment:_:)](https://developer.apple.com/documentation/swiftui/view/mask(alignment:_:))モディファイアで特定の方向にマスクをかけ、マスクがかかっていない方向にのみ影を見せるということになるだろう。

[SwiftUI - How to show shadow only on top side?](https://stackoverflow.com/questions/68765679/swiftui-how-to-show-shadow-only-on-top-side)

### Identifiable

ListyやForEachに渡すデータは通常Identifiableに適合している必要がある。

```swift:TestData.swift
let id = UUID()
```

以上のようにUUIDを設ければ、適合できる。

### 子ビューのサイズを知りたい場合

デフォルトで手軽にはできないので、自分で拡張するのをおすすめ。以下は一例です。

```swift:View+Extensions.swift
    func sizeReader(size: Binding<CGSize>) -> some View {
        background(
            GeometryReader { proxy -> Color in
                DispatchQueue.main.async {
                    size.wrappedValue = proxy.size
                }
                return Color.clear
            }
        )
    }
```

```swift:ScreenA.swift
struct: ScreenA: View {
    @State private var childViewSize: CGSize = .zero

    var body: some View {
        VStack(spacing: 0) {
            OtherView()
            ChildView()
                .sizeReader(size: $childViewSize)
        }        
        .frame(height: childViewSize.height)
        .padding(20)
    }
}
```

参考: [SwiftUI - Get size of child?](https://stackoverflow.com/questions/56573373/swiftui-get-size-of-child)


### ジェスチャー

[gesture(_:including:)](https://developer.apple.com/documentation/swiftui/view/gesture(_:including:))モディファイアを利用すると良い。

[How to add a gesture recognizer to a view](https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-gesture-recognizer-to-a-view)


タップ、ロングプレスの場合は専用のモディファイアもあるので、それでも良い。

- [onTapGesture(count:perform:)](https://developer.apple.com/documentation/swiftui/view/ontapgesture(count:perform:))
- [onLongPressGesture(minimumDuration:maximumDistance:perform:onPressingChanged:)](https://developer.apple.com/documentation/swiftui/view/onlongpressgesture(minimumduration:maximumdistance:perform:onpressingchanged:))
- [[SwiftUI] タップや長押しの認識、onTapGestureとonLongPressGesture](https://i-app-tec.com/swiftui/tap-gesture-recognize.html)

### `.frame`などでViewの大きさを制限しているにも関わらずTextの文字などのコンテンツがViewの大きさをはみ出してしまう。

[`.clipped`](https://developer.apple.com/documentation/swiftui/view/clipped(antialiased:))モディファイアを使うと良い。

```swift:test.swift
ScrollView {
    ZStack {
    // 何か
    }
}
.clipped()
```

### セーフエリアにもコンテンツを表示したい。

[`edgesIgnoringSafeArea(_:)`](https://developer.apple.com/documentation/swiftui/view/edgesignoringsafearea(_:))というのもあったが、deprecatedになったようなので、[ignoresSafeArea(_:edges:)](https://developer.apple.com/documentation/swiftui/view/ignoressafearea(_:edges:))を使おう。

### Viewの表示位置を本来の位置からずらしたい。

- [`offset(x:y:)`](https://developer.apple.com/documentation/swiftui/view/offset(x:y:))

指定したx, yの分だけ本来の位置から横、縦にずれる。

- [`position(x:y:)`](https://developer.apple.com/documentation/swiftui/view/position(x:y:))

親Viewの座標系の中で(x, y)の位置にViewを移動する。

以下が参考になる。

- 以下、引用
![スクリーンショット 2023-03-13 11.57.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392332/288ed85d-e1c4-0e0e-0332-3b5d0940f872.png)
- 以上、引用 [[SwiftUI] .offset と .position の違い](https://1plion.blog.fc2.com/blog-entry-75.html)より

### カメラ・写真の利用

カメラロールの写真について、iOS16以降はSwiftUI準拠の[PhotosPicker](https://developer.apple.com/documentation/photokit/photospicker)が利用できる。

カメラ機能については、SwiftUI純正で使えるものがない。UIKitのUIImagePickerControllerをUIViewControllerRepresentableでラップして使うしかない。

参考: [【SwiftUI】カメラを使いたい](https://qiita.com/SNQ-2001/items/2cc6e7e35ab98ba02397)

### ナビゲーションバー

#### iOS15まで

[NavigationView](https://developer.apple.com/documentation/swiftui/navigationview)を用います。この中に[NavigationLink](https://developer.apple.com/documentation/swiftui/navigationlink)を入れることで、次のページへのリンクを設定できます。

#### iOS16以降

[NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack)の中に[NavigationLink](https://developer.apple.com/documentation/swiftui/navigationlink)を入れましょう。詳細なパターンについてはいろいろあります。

[SwiftUI4, iOS16以降で使用できる NavigationStackについて軽く調査した](https://qiita.com/YOSUKE8080/items/009543937ec180990132#case-3-routerをenumにして遷移する例)

#### 共通

- 画面遷移についてはUIKitを使用する例もあります。
- SwiftUIも最近は使いやすくなりました。iOS16以降の、[NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack)と、[NavigationLink](https://developer.apple.com/documentation/swiftui/navigationlink)を用いる形態です。ベタ書きすると以下のようになります。

```swift:
        NavigationStack {
           Text("hoge")
            .toolbar {
                ToolbarItem(
                    placement: .topBarTrailing) {
                        NavigationLink {
                            SecondView()
                        } label: {
                            Text("SecondView")
                        }
                    }
            }
```
- 大量の画面遷移の場合はPathを独自に定義し、NavigationStack, NavigationLinkで用いる形態をお勧めします: [NavigationStackを一番活かす人（SwiftUI）](https://qiita.com/yoshi-eng/items/91666637cd7cdd8edf88)。注意点としては、NavigationStackは通常ルートの画面に一つ設ければよく、子画面に設ける必要はありません。またパスを用いる場合ルートの画面に画面遷移のロジックを全て集約させれば、小画面に書く必要はなくなりますので、大規模なアプリで便利です: [【SwiftUI】NavigationLinkとNavigationPathを使った遷移機能の実装](https://qiita.com/Harx02663971/items/88df35d749bbb8753ac0)

- iOS16以降、どのような型のパスでも受け入れられる独自の型である[NavigationPath](https://developer.apple.com/documentation/swiftui/navigationpath)が設けられており、複数の型のパスを一つのArrayで扱いたい場合はこれを用いるのも良いです。
    - NavigationPath型にはHashable(とcodable)に準拠した型ならなんでも入れられます。NavigationPath型には`isEmpty`, `count`, `append(_:)`, `removeLest`そして`codable`というプロパティ・メソッドが定義されています。基本的な操作はできますがArrayのような複雑な操作はできなくなっています。NavigationPathに入れたものの中身は`codable`で取得し、ストレージに保存して復活させることもできるようになっています。ただし中身を検索したりいじったりということは必ずしも想定されていないらしく、複雑な作りになっており、中身として入れたものの型の情報と値の情報が書かれた文字列のようになっており、いじるのは困難を伴います。:[Reverse Engineering SwiftUI's navigationPath codablity](https://www.pointfree.co/blog/posts/78-reverse-engineering-swiftui-s-navigationpath-codability)


- タイトル は[navigationTitle(_:)](https://developer.apple.com/documentation/swiftui/view/navigationtitle(_:)-43srq)モディファイアで設定します。そのままでは、画面左上にタイトルが大きく表示されます。従来のようにナビゲーションバーの中心部に小さめに表示したい場合は、[navigationBarTitleDisplayMode(_:)](https://developer.apple.com/documentation/swiftui/menu/navigationbartitledisplaymode(_:))で[`.inline`](https://developer.apple.com/documentation/swiftui/navigationbaritem/titledisplaymode/inline)を指定しましょう。

- ナビゲーションバー上のボタンについては[toolbar(content:)](https://developer.apple.com/documentation/swiftui/view/toolbar(content:)-5w0tj)を使いましょう。

- 以下、引用

```swift:ContentView.swift
struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("要素1")
                Text("要素2")
            } .navigationTitle("タイトル")
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button(action: {
                            print("設定ボタンです")
                        }) {
                            Image(systemName: "gearshape.fill")
                        }
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button(action: {
                            print("マイページです")
                        }){
                            HStack {
                                Image(systemName: "person.fill")
                            }
                        }
                    }
                }
        }
    }
}

```

- 以上、引用 [【SwiftUI】toolbarの使い方！アイテムにボタンを増やす方法](https://tech.amefure.com/swift-toolbar)より

### スプリットビュー

`NavigationSplitView`が利用できる。端末の横方向が広いiPadでは画面左側に目次を出し、画面中央に詳細画面を出すことができる。横方向が狭いiPhoneでは、普通のナビゲーションバーのルートと別画面への遷移になるため、iPhoneで使っても安心。

```swift:test.swift
        NavigationSplitView {
            List(selection: $selectedContentsType) {
                Foo
            }
            .navigationTitle("Contents")
            .toolbar {
                yourToolbarItems
            }
        } detail: {
                if let selectedContentsType {
                    selectedContentsType.detailView
                } else {
                    Text("Please selecta something")
                }
            }
        }
```

## ケーススタディ

### あるViewの背景に別のViewを設定したい

[background(alignment:content:)](https://developer.apple.com/documentation/swiftui/view/background(alignment:content:))モディファイアを使うと設定できる。あるいは、あるViewそのものよりも大きい背景を作りたい場合は、[ZStack](https://developer.apple.com/documentation/swiftui/zstack)
も利用できる。もし画面全体を背景で覆いたいなどであればZStackがすぐできる選択肢となる。safeareaを無視させたいときはZStackに`.ignoresSafeArea()`モディファイアをつければ良い。
[Adding a background to your view](https://developer.apple.com/documentation/swiftui/adding-a-background-to-your-view)


backgroundモディファイアに関して、alignmentとしてcenter, leading, trailingなどが設定可能。背景に設定したい複数のViewに別々のalignmentを設定したいときは、backgroundモディファイアを2回以上使えばいい。あるいはbackgroundモディファイアの中に ZStackを設けることも考えられる。
[background(alignment:content:)](https://developer.apple.com/documentation/swiftui/view/background(alignment:content:))


### VStack, HStack などを幅いっぱいに表示させたい。

VStack, HStackの子としてSpacer()を使うことができます。Spacerは親のVstackまたはHstackが画面一杯になるまで膨らみます。どのように膨らむかは下記ドキュメントを参照。
例えば、HStackの左側にSpacerを置いたときは、HStackが可能な限り最大の横幅に拡大し、右端にコンテンツが並び、左側には空白が広がる。
HStackの右側にSpacerを置いたときは、HStackが可能な限り最大の横幅に拡大し、左端にコンテンツが並び、右側には空白が広がる。
HStackの左右の端にSpacerを１個ずつ置いたときは、HStackが可能な限り最大の横幅に拡大し、中央にコンテンツが並び、左右に同じ大きさの空白が広がる。

[Spacer](https://developer.apple.com/documentation/swiftui/spacer)
[SwiftUIのSpacerの使い方](https://programming-sansho.com/swift/swiftui-spacer/)


### VStack, HStackの子をVStack、HStackの中でいっぱいになるまで広げたい

子に`.frame(maxWidth: .infinity)`モディファイアを使うと可能。

[SwiftUI HStack fill whole width with equal spacing](https://stackoverflow.com/questions/58887526/swiftui-hstack-fill-whole-width-with-equal-spacing)

VStack, HStackの中では、代わりに[layoutPriority](https://developer.apple.com/documentation/swiftui/view/layoutpriority(_:))をつける方法でも動作した:

```
.layoutPriority(1)
```
[Stack Over Flow - In SwiftUI, how do you get a view to take as much space as possible?](https://stackoverflow.com/questions/75416821/in-swiftui-how-do-you-get-a-view-to-take-as-much-space-as-possible)

### Listのセレクションタップが動作しない。

色々ググりまくったがうまく原因がわからなかったので、ListではなくLazyVStackの中で
ForEachを使うようにした。ForEachの中の要素にonTapGestureモディファイアを使えばListのセレクションタップのようなことができる。

この場合LazyVStackの直下にForEachを置く必要がある。他の何か(VStackとか)の下にForEachをおくと、ForEachの要素が全て毎回再描画されるので、LazyVStackを使っている意味がなくなってしまう。
[LazyVStackを使用すると描画処理が何度も走ってしまう](https://zenn.dev/ueshun/articles/a687c470c7b903)

またUICollectionViewをUIViewRepresentableでラップして使ってもいいと思う。


### Listの上下左右に何故か最初から余白が存在する。


Listに以下のようなものをつけることで解消する。

```swift.HomeView.swift
.listRowInsets(EdgeInsets())
```


マイナスのpaddingを自分でつけることでも解消する。

```swift:HomeView.swift
List()
   .padding(.horizontal, -20)
   .padding(.vertical, -42)
```

### List の見た目の種類について

[`listStyle(_:)`](https://developer.apple.com/documentation/swiftui/view/liststyle(_:))モディファイアをListにつけることによって何種類かの見た目の変更が可能。

- 以下、引用

![スクリーンショット 2023-04-02 17.21.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392332/e14c92fd-b151-1b3d-17d1-c90516870378.png)


- 以上、引用 [[SwiftUI] List View with ListStyle. ListStyle別List Viewの表示差分](https://www.harubears.com/ja/tech-ja/liststyles/) より

- Watch OS専用の`CarouselListStyle`, `ElipticalListStyle`もある。

- 以下、引用

![スクリーンショット 2023-04-02 17.27.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392332/081c4f12-55dd-37b7-c528-101a877f64cd.png)
![スクリーンショット 2023-04-02 17.27.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392332/5f7538f0-fc2d-62be-bb17-00bf7cdda468.png)

- 以上、引用 [ListStyles. SwiftUI](https://tonidevblog.com/posts/liststyles_swiftui/)より

### Listの背景色を灰色から他に変えたい。

`.scrollContentBackground(.hidden)`というモディファイアを使い、デフォルトの背景を隠してから色を変える必要あり。

```swift:Home.swift
List() {
// ...
}
    .background(Asset.white1.swiftUIColor)
    .scrollContentBackground(.hidden)
```

[How to change SwiftUI list background color](https://sarunw.com/posts/swiftui-list-background-color/)

### ユーザーによりテキストを入力させたい

主に1行のテキストフィールドの場合は[TextField](https://developer.apple.com/documentation/swiftui/textfield), TextFieldと似ているがパスワードなど機密情報を入力させる場合は[SecureField](https://developer.apple.com/documentation/swiftui/securefield), 何行もの入力をさせる場合はスクロール可能な[TextEditor](https://developer.apple.com/documentation/swiftui/texteditor)を選ぶと良いだろう。

[Apple Developer - Text input and output](https://developer.apple.com/documentation/swiftui/text-input-and-output)

### TextEditor にプレースホルダーを表示させたい。

デフォルトではやる方法がない。TextEditorのテキストを監視し、空文字の時は、TextEditorとZStackや.overlayで重ねたTextにプレースホルダーを表示させるなどが考えられる。

[Stack Over Flow - How to add placeholder text to TextEditor in SwiftUI?](https://stackoverflow.com/questions/62741851/how-to-add-placeholder-text-to-texteditor-in-swiftui)
[Developers io - 【SwiftUI】プレースホルダー付きのTextEditorを自作してみた](https://dev.classmethod.jp/articles/swiftui-texteditor-with-placeholder/)

### TextFieldで、キーボードのreturnキーが押されたことを検知したい。

[onSubmit](https://developer.apple.com/documentation/swiftui/view/onsubmit(of:_:))モディファイアを使う。

### TextFieldで、横幅サイズいっぱいまで文字が入力されたら、自動で改行し縦に長くなっていくようにしたい。

[TextFieldを初期化する](https://developer.apple.com/documentation/swiftui/textfield/init(_:text:axis:)-7n1bm)際、`axis`引数に[vertical](https://developer.apple.com/documentation/swiftui/axis/vertical)を指定する。

### TextFieldで、ユーザーによる編集を禁止したい。

`.disabled(true)`モディファイアを使えば可能である。

例：
```swift:test.swift
 TextField(
     text: Binding<String>(
         get: { presenter.state.model.translatedText ?? "" },
         set: { _, _ in }// read only
     ),
     axis: .vertical,
     label: {}
 )
    .disabled(true)
```

### Viewの角を丸くしたい。

`.cornerRadius`モディファイアを使う。

または,
`.background`などで色をつけたりした後、`.clipShape(Capsule())`,`.clipShape(RoundedRectangle(cornerRadius: 50))`, `.clipShape(Circle())`などを使う。

```swift:test.swift
Text("Test")
    .background(Color.white)
    .clipShape(Circle())
```

### Viewの特定の角だけ丸くしたい。

ただ`.cornerRadius`モディファイアを使うだけでは、四つの角全てが丸くなってしまう。色々やりようはあるが、`.cornerRadius`を使う前に丸くさせたくない方向にマイナスの値で`.padding`モディファイアを使っておき、`.cornerRadius`を使った後にプラスの値で`.padding`モディファイアを使って角丸に戻しておくというような簡単なテクニックがおすすめ。

```swift:HomeView.swift
    @ViewBuilder
    var dummyInputArea: some View {
        VStack {
            HStack {
                Button(action: {
                    presenter.tapEventSubject.send(.tappedInputButton)
                }) {
                    Text(L10n.homeDummyInputButton)
                        .font(.largeTitle)
                        .foregroundColor(Color.gray)
                }
                    .frame(height: 60)
                Spacer()
                Button(action: {
                    presenter.tapEventSubject.send(.tappedHandWritingInputButton)
                }) {
                    Image(systemName: "pencil.and.outline")
                        .foregroundStyle(.black)
                }
                .frame(width: 44, height: 44)
            }
                .frame(height: 66)
            Spacer()
                .frame(maxHeight: .infinity)
        }
        .padding(.horizontal, 20)
        .background(.white)
        .padding(.top, 40.0)
        .cornerRadius(40.0)
        .padding(.top, -40.0)
        .background(Color(uiColor: UIColor(hex: "#EDEBEBFF")!))
    }

```

これで、Viewの下側だけ角を丸くしている。


![截屏2023-07-28 15.56.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/392332/08b349d1-a59d-2197-1c4c-5bab2eec5780.png)

参考: [Stack Over Flow - Round Specific Corners SwiftUI](https://stackoverflow.com/a/72435691)

または、`Shape`プロトコルを継承するクラスを自分で作成し、`.clipShape`モディファイアを使うという手もある。こちらも使いやすい。

- 以下、引用

```swift:View+RoundedCorner.swift
import SwiftUI

struct RoundedCorner: Shape {
    var radius: CGFloat = .infinity
    var corners: UIRectCorner = .allCorners
    
    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        return Path(path.cgPath)
    }
}

extension View {
    func roundedCorner(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape(RoundedCorner(radius: radius, corners: corners) )
    }
}
```

- 以上、引用 [Round Specific Corners in SwiftUI](https://www.devtechie.com/community/public/posts/151937-round-specific-corners-in-swiftui)

### Textのフォントを変えたい

こちらに一蘭がまとまっている。

[【SwiftUI】フォント(font)の指定方法を総まとめ！](https://terupro.net/swiftui-font/)

[Font](https://developer.apple.com/documentation/swiftui/font)構造体はプログラマーが直接初期化することはなく、staticな定数を経由したりしてFont構造体のオブジェクトを得ることが多いようだ。

### Divider を横線にしたいのに、縦線になってしまう。

DividerをHStackの中に入れている場合は、[minor axisすなわち縦軸に沿って線が引かれる](https://developer.apple.com/documentation/swiftui/divider)ので、縦線になってしまう。横線にしたいのであればVStackの中に入れる。

```swift:test.swift
HStack {
    Divider()
}

// ↓

HStack {
    VStack {
        Divider()
    }
}

```

### Dividerの色を変えたいのに変えられない

`.foregroundColor`モディファイアではなく、`.background`モディファイアを使うと色を変えられる。

```swift:test.swift
Divider()
    .foregroundColor(Color.white)// Meaningless


Divider()
    .background(Color.white) // Great!
```

### Image(systemName: )で作ったSF symbolの画像について、シンボルの部分の大きさを変えたい。

`.font()`モディファイアで変えられる。

```swift:HomeView.swift
Image(systemName: "mic")
    .font(.largeTitle)
```

[How do I set the size of a SF Symbol in SwiftUI?](https://stackoverflow.com/questions/57342170/how-do-i-set-the-size-of-a-sf-symbol-in-swiftui)

### .onTapGesture()で、タップがSpacer()で作った隙間の領域では反応しない。

`.contentShape(_:eoFill:)`モディファイアを使い、タップ領域を変更することで対応できる。

[SwiftUI can't tap in Spacer of HStack](https://stackoverflow.com/questions/57191013/swiftui-cant-tap-in-spacer-of-hstack)

### キーボードが開いているか否かを知りたい。

`NotificationCenter`を用いて知ることができます。SwiftUIのクラスでキャッチしたい場合はCombineを用いて通知を受けることができます。

参考:

[How to detect if keyboard is present in swiftui](https://stackoverflow.com/questions/65784294/how-to-detect-if-keyboard-is-present-in-swiftui)

別のアプローチとして、[`focused(_:equals:)`](https://developer.apple.com/documentation/swiftui/view/focused(_:equals:))モディファイアを使うことでも検知できます。このモディファイアは複数のテキストフィールドなどのうちどれがフォーカスされているかをenum型で知ることができます。何もFocusされていない時はnilが設定されるようになるので、渡したfocusedに渡したenum型がnilであればキーボードが閉じていることになります。

### キーボードの上に閉じるボタンをつけたい。

まずはBool値を渡す[`focused(_:)`](https://developer.apple.com/documentation/swiftui/view/focused(_:)), enumを渡す[`focused(_:equals:)`](https://developer.apple.com/documentation/swiftui/view/focused(_:equals:))のどちらかを使って、キーボードを開いたり閉じたりするための
@FocusStateの変数を用意しましょう。例としては

```swift:
@FocusState private var isFocused: Bool = false
```

```swift:
@FocusState private var focusedTextFieldType: MyTextFieldType? 
```

といった具合です。

[toolbar](https://developer.apple.com/documentation/swiftui/view/toolbar(content:)-5w0tj)モディファイアの中で、`.keyboard`のタイプを持つボタンを設置します。そのボタンが押されたら、上のisFocusedにfalseを設定、focusedTextFieldTypeにnilを設定すると、キーボードを閉じることがあります。

iOS16のシミュレータだと、うまくツールバー上のボタンが表示されない不具合がありましたが、実機なら大丈夫でした。

-> iOS17, 18でこれだといくつか不具合が生じるようです。

- iOS17.0~17.1: キーボードが閉じない https://forums.developer.apple.com/forums/thread/737399?answerId=768966022
    - 結論的には、SwiftUIのバグなのでUIKitで閉じるしかない。例：

```swift:
        if #available(iOS 17.0, *) {
            if #unavailable(iOS 17.1) {
                UIApplication.shared.connectedScenes
                    .compactMap({ $0 as? UIWindowScene })
                    .flatMap({ $0.windows })
                    .first(where: { $0.isKeyWindow })?.endEditing(true)
            }
        }
```


- iOS17, 18~でモーダル表示されたビュー上で、閉じるボタンが出現しない https://forums.developer.apple.com/forums/thread/736040
    - こちらもSwiftUIのバグなので、UIKitを一部使うしかない。フォーラムに回避策が２つほど投稿されていました。下記の回避策は自分でも試しましたが、よく動いています。この例では、`toolbar`モディファイアではなく、`safeAreaInset`でキーボードと画面の間にツールバー領域を追加しているようです。： https://gist.github.com/JamesSedlacek/2d0425319e2a854da8c51f4b05c9842a

### 画面のいずれかの空白エリアをタップしたらキーボードが閉じるようにしたい。

画面をZStackで囲い、ZStackの中身の一番上にColor.clearを用意しましょう。Color.clearをタップしたらテキストフィールドが閉じるようにすれば実現可能です。以下のような具合です。透明なViewはタップなどの動作がそのままではできないので、`.contentShape`を使ってあげることでタップ領域を設定するのがポイントです。

```swift:

@FocusState private var focusedTextFieldType: MyTextFieldType? 

var body: some View {
    ZStack {
        Color.clear
            .contentShape(Rectangle())
            .onTapGesture {
                focusedTextFieldType = nil
            }
        root
    }
}
```

[SwiftUI - dismissing keyboard on tapping anywhere in the view - issues with other interactive elements](https://stackoverflow.com/questions/60348988/swiftui-dismissing-keyboard-on-tapping-anywhere-in-the-view-issues-with-othe)

### Viewを回転させる。

[rotationEffect(_:anchor:)](https://developer.apple.com/documentation/swiftui/view/rotationeffect(_:anchor:))を、3D空間で回転させたいなら[rotation3DEffect(_:axis:anchor:anchorZ:perspective:)](https://developer.apple.com/documentation/swiftui/view/rotation3deffect(_:axis:anchor:anchorz:perspective:))を用いる。visionOSにおいては代わりに[perspectiveRotationEffect(_:axis:anchor:anchorZ:perspective:)](https://developer.apple.com/documentation/swiftui/view/perspectiverotationeffect(_:axis:anchor:anchorz:perspective:))も使える。

### ForEach 初期化でCannot convert value of type .. to expected argument type 'Binding<C>'というエラーが出る。

ForEach初期化の部分で出ているが、実際のエラーの原因はそこではなく、ForEachのクロージャ内の各要素のViewを記載するところで、実際には存在しないはずの変数名の変数を利用してしまっていた（要はタイポ）ためにこのエラーが出ていた。

参考: [SwiftUI: ForEachでCannot convert value of type .. to expected argument type 'Binding<C>'と出るケース](https://zenn.dev/kabeya/scraps/efd6be76abf082)

### View出現時・消失時に何らかの処理をさせたい。

- [onAppear](https://developer.apple.com/documentation/SwiftUI/View/onAppear(perform:))
- [task](https://developer.apple.com/documentation/swiftui/view/task(priority:_:)): iOS15以上でしか使えない。asyncのメソッドをawaitをつけることなく非同期で実行できる。該当Viewが消えたり変更されたりした場合は処理がキャンセルされる。
- [onDissappear](https://developer.apple.com/documentation/swiftui/view/ondisappear(perform:))

これらが使える。

使い分けについては：
[https://developer.apple.com/documentation/swiftui/view/ondisappear(perform:)](https://qiita.com/uhooi/items/54c4bbdf80f1551cd319)

### Modifying state during view update, this will cause undefined behavior.

bodyなどViewの描画中にViewのStateを変更するような処理を直接書くと、このエラーが出て、予測不能な振る舞いになる可能性がある。

ViewのStateの変更は、ジェスチャー発動時や、View出現時など、あくまでViewの描画とは異なるタイミングに記述するべきである。

本件では、RealitiyViewのupdateクロージャはViewのStateが変更されたことをきっかけに呼ばれるのだが、その中でさらにViewのStateをコードで変更してしまっており、この警告が出た。

参考 [How to fix “Modifying state during view update, this will cause undefined behavior”](https://www.hackingwithswift.com/quick-start/swiftui/how-to-fix-modifying-state-during-view-update-this-will-cause-undefined-behavior)

### UI部品が多すぎて覚えきれない

Xcodeで「Command + Shift + L」または右上の➕ボタンを押下で、UI部品カタログが表示され、ワンクリックでコード上に追加できる。

### Invalid frame dimension

```swift
.frame(width: .infinity)
```

などとしていた時に発生した。こうではなく、代わりに以下のように設定する。

```swift
.frame(maxWidth: .infinity)
```

参考: [SOLVED: Invalid frame dimension](https://www.hackingwithswift.com/forums/100-days-of-swiftui/invalid-frame-dimension/15353)

### 共通要素遷移(遷移前の画面の部品が、遷移後の画面の部品にシームレスに変化していくようなアニメーションを伴う画面遷移)をしたい

iOS18以降では、[navigationTransition(_:)](https://developer.apple.com/documentation/swiftui/view/navigationtransition(_:)), [NavigationTransition.zoom](https://developer.apple.com/documentation/swiftui/navigationtransition/zoom(sourceid:in:)), [matchedTransitionSource(id:in:configuration:)](https://developer.apple.com/documentation/swiftui/view/matchedtransitionsource(id:in:configuration:))を利用することで比較的容易に実現できる。

[Using the zoom navigation transition in SwiftUI](https://www.createwithswift.com/using-the-zoom-navigation-transition-in-swiftui/)

### テキストフィールドにおいて、キーボードの改行ボタンの文言を変えたい。

[.submitLabel](https://developer.apple.com/documentation/swiftui/submitlabel)モディファイアを使うことで可能となる。

[[SwiftUI]SubmitLabelの種類ごとの日本語訳まとめ](https://zenn.dev/beryu/articles/b473c8076876d3)

### SFSafariを表示したい。

SFSafariViewControllerをUIViewControllerRepresentableでラップするしかない。

```swift:SafariView.swift
import SwiftUI
import SafariServices

struct SafariView: UIViewControllerRepresentable {
    let url: URL
    
    func makeUIViewController(context: Context) -> some UIViewController {
        return SFSafariViewController(url: url)
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        // Do nothing
    }
}

#Preview {
    SafariView(url: URL(fileURLWithPath: ""))
}
```

### システム画像のImageの全体の色を変更したいのに、画像の一部だけ変更できない。

`.renderingMode()`モディファイアを使ってレンダリングモードを変更してみる。`.template`を設定すると、`.foregroundColor`で設定した色で全体が塗り替えられる。
ちなみに`.original`だと画像の一部は元の色となる。何も指定しない場合、ダークモードの時は白、通常モードの時は真っ黒となる。

- [renderingMode(_:)に.templateを設定すると何が変わる？](https://qiita.com/h-taro/items/991d8dcd362d9cb8b819)
- https://developer.apple.com/documentation/swiftui/image/renderingmode(_:)

### なぜか、UI部品の上に隙間が生じてしまう。

SwiftUIのデフォルトでは、Alignment（配置）がCenterになっている模様: [frame(width:height:alignment:)](https://developer.apple.com/documentation/swiftui/view/frame(width:height:alignment:))

例えば、以下のように.topを指定すれば解消されることがある。
```swift:
VStack {
    // 省略
}
.frame(alignment: .top)
```

### .background, .buttonStyleなどでButtonに背景やスタイルを設定したが、設定した背景やスタイルがボタンのサイズまで拡大してくれない

Buttonに.frameモディファイアなどでサイズをつけた後、.background, .buttonStyleをつけることで、背景やスタイルがButtonのサイズに合わせて変化してくれるようになります。要はモディファイアの順番という基本的な問題です。

参考: [How to make SwiftUI button with buttonStyle expand to full width](https://sarunw.com/posts/swiftui-button-style-width/)

### アプリがバックグラウンド化、フォアグラウンドかを判定したい。

SwiftUIでは[ScenePhase](https://developer.apple.com/documentation/swiftui/scenephase)が使える。`@Environment`アノテーションで受け取ることができる。

### 参考書籍

- たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode15/iOS17/Swift5.9対応, 藤治仁ほか, ソシム, 2023