Today、わかりました:SwitUI FlashCardの作成


今日は、SwitUIでFlashCardを作成し、これらの概念をまとめます.
(プロジェクト完了リンク:https://github.com/kipsong133/TIL/tree/main/2022/02/22/SwiftUI_FlashCardExample)
インプリメンテーション
UI->観測可能オブジェクトの構成->Gesture接続を実装します.

  • UI実装
    フラッシュメモリカードなので、カードUIがあるはずです.以下のUIを構成するSwitUIViewを作成しました.
  • struct CardView: View {
        var body: some View {
            ZStack {
                // setup BackgroundColor
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: 320, height: 210)
                    .cornerRadius(12)
                
                VStack {
                    Spacer()
                    Group {
                        Text("Flash Card Title")
                            .font(.largeTitle)
                        
                        Text("Answer")
                            .font(.headline)
                    }
                    .foregroundColor(.white)
                    Spacer()
                }
            }
            .shadow(radius: 8)
            .frame(width: 320, height: 210)
        }
    }

    今までに実現したのは1枚のカードで、使う時には何枚ものカードがあるはずです.ビューを再作成し、「DeckView」と名前を付けます.
    struct DeckView: View {
        var body: some View {
            ZStack {
                CardView()
                CardView()
            }
        }
    }
    今ではZStackにすぎませんが、ここではデータをバインドし、ジェスチャーを追加します.
    UI実装はこれで終了する.次に、ObserverObjectまたはタイプを定義してバインドします.
  • 観測対象配置
  • まずstructを使用してカードのデータ型を定義します.
    Quizタイプを定義します.
    struct Quiz {
        let question: String
        let answer: String
    }
    次にCardを定義します.
    struct Card: Identifiable {
        var card: Quiz
        var id = UUID()
    }
    
    
    extension Card: Equatable {
        static func == (lhs: Card, rhs: Card) -> Bool {
            return lhs.card.question == rhs.card.question
            && lhs.card.answer == rhs.card.answer
        }
    }
    Identifiableは一意性を与えた.
    Equatableによる整合性の検証が可能な構造体を定義します.
    最後に、すべてのカードを含むDeckクラスを定義します.
    class Deck: ObservableObject {
        @Published var cards: [Card]
        
        init(from cards: [Quiz]) {
            self.cards = cards.map { Card(card: $0) }
        }
    }
    ビューで観察できるオブジェクトプロトコルを使用します.
    @Publishedを使用してプロパティを囲み、UIの更新時にオブジェクトがカードであることを確認します.
    初期化方法では、タイプを入力した「質問」->「カード」に変更します.
    DeckView->CardViewでカードごとのメッセージを伝えましょうだからカードビューからデータを受信するつもりです
    まず、Cardをメンバー変数として定義します.
    struct CardView: View {
        var card: Card
      ... 
    }
    
    テキストのStringProtocolパラメータ部分をbody property内部の変数に変更します.
    Text(card.quiz.question)
    ...
    Text(card.quiz.answer)
    必ずやるわけではありませんが、間違いを見たくないので、Previewも以下のように修正しました.
    struct CardView_Previews: PreviewProvider {
        @State static var card = Card(quiz: quiz01)
        
        static var previews: some View {
            CardView(card: card)
                .previewLayout(.device)
        }
    }
    これで、DeckViewが初期化時にcard値をポイントに渡すと、データはCardViewに渡されます.
    struct DeckView: View {
        
        @StateObject var deck = Deck(from: quizBundle)
        
        var body: some View {
            ZStack {
                ForEach(deck.cards) { card in
                    CardView(card: card)
                }
            }
        }
    }
    StateObject全体を伝えるのではなく、@Publishedで包まれたPropertyカードを渡すことに注意してください!
    これで、ユーザーインタフェースの操作とデータ接続が完了しました.ZStackなので見えないカードを見てみましょうFlashCard機能の実装を完了するためにジェスチャーを追加します.
  • Gesture
  • カードを左または右に移動する場合は、カードが右または左に消えるように移動位置を決定します.そのためには、以下の事項を実施する必要があります.
    ジェスチャー認識
    ジェスチャーを区別(左または右)
    Dragによるオブジェクト位置の移動(Transition)
    Dragを終了したら、移動カード(DragGesture+onEnded)
    ひとつずつ追加しよう
    まず、enumを定義して、左に移動するか右に移動するかを決定します.
    enum DismissCardDirection {
        case left
        case right
    }
    
    ドラッグしたビューはCardViewです.ドラッグに関連するジェスチャーとアニメーションを追加します.
    struct CardView: View {
      ...
      @State var offset: CGSize = .zero
      
      var body: some View {
        let drag = DragGesture()
          .onChanged { offset = $0.translation }
        
        return ZStack {
          ...
        }
        ...
        .animation(.spring(), value: offset)
        .offset(offset)
        .gesture(drag)
      }
    }
    offsetでカードの位置を移動するたびに、移動する位置に変更されます.
    パンプロファイルは、開始位置と変更位置の差を持つプロファイルです.ドラッグオブジェクトの移動距離をoffsetに指定して位置を移動します.
    アニメーションはspring効果を生成し、アニメーションの値はoffsetです.
    ビューの位置はoffsetによって変更されます.
    最後にジェスチャーを追加してみんなをインタラクティブにします
    ここでは、次の操作を行います.

    今、左に移動するか右に移動するかによって、カードを消したいと思っています.
    まず、CardViewはドラッグに論理を追加し、値に応じてカードの位置を変更します.
    let drag = DragGesture()
        .onChanged { offset = $0.translation }
        .onEnded {
            // move left
            if $0.translation.width < -100 {
                // dismiss left
                offset = .init(width: -1000, height: 0)
                // memorized card
                dragged(card, .left)
                
            // move right
            } else if $0.translation.width > 100 {
                // dismiss right
                offset = .init(width: 1000, height: 0)
                // memorized card
                dragged(card, .right)
            
            // move in the middle
            } else {
                // move base
                offset = .zero
            }
        }
    次に、追加されていないPropertyとtypealiasを追加します.
        typealias CardDrag = (_ card: Card,
                              _ direction: DismissCardDirection) -> Void
        let dragged: CardDrag
    今回の機能実装では、記憶されたカードと記憶されていないカードの論理は実装されませんが、それらの間のデータ交換を確保するために処理します.
    initコードを変更します.
       init(
            card: Card,
            onDrag dragged: @escaping CardDrag = {_, _ in }) {
                self.card = card
                self.dragged = dragged
            }
    最後に、DeckViewでカードビューの宣言を変更します.
    struct DeckView: View {
        
        @StateObject var deck = Deck(from: quizBundle)
        
        let onMemorized: (Card) -> Void = { _ in }
        
        var body: some View {
            ZStack {
                ForEach(deck.cards) { card in
                    CardView(card: card) { card, direction in
                        if direction == .left {
                            onMemorized(card)
                        } else {
                            // do something
                        }
                    }
                }
            }
        }
    }
    次に、次の操作を行います.

    整理する
    今回使ったコンセプトを整理してみます.
    外部オブジェクトのプロパティからビューのUIを更新するためにObservableObjectが構成されています.
    DragGestureとTranslation Propertyを使用して、オブジェクトの位置を変更するかどうかを決定します.
    確認した場所にアニメーションを送信しました.
    最後にoffsetとgeserを追加し、上で構成したドラッグと移動アニメーションを有効にします.
    参考資料
  • https://www.raywenderlich.com/books/swiftui-by-tutorials/v4.0/chapters/11-gestures#toc-chapter-015-anchor-001
  • 読んでくれてありがとう.