web-dev-qa-db-ja.com

SwiftUI ScrollView:.content.offset別名ページングを変更する方法?

問題

ScrollViewのスクロールターゲットを変更するにはどうすればよいですか? 「クラシック」なscrollViewデリゲートメソッドの代替品を探しています

override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)

...たとえば、ターゲットの_scrollView.contentOffset_を_targetContentOffset.pointee_を介して変更して、カスタムページング動作を作成できます。

または言い換えると、(水平の)scrollViewでページング効果を作成したいのです。

私が試したもの、すなわち。このようなものです:

_    ScrollView(.horizontal, showsIndicators: true, content: {
            HStack(alignment: VerticalAlignment.top, spacing: 0, content: {
                card(title: "1")
                card(title: "2")
                card(title: "3")
                card(title: "4")
            })
     })
    // 3.
     .content.offset(x: self.dragState.isDragging == true ? self.originalOffset : self.modifiedOffset, y: 0)
    // 4.
     .animation(self.dragState.isDragging == true ? nil : Animation.spring())
    // 5.
    .gesture(horizontalDragGest)
_

試行

これは私が試したものです(カスタムのscrollViewアプローチ以外に):

  1. ScrollViewには、画面領域よりも大きいコンテンツ領域があり、スクロールをまったく有効にできます。

  2. ドラッグが行われているかどうかを検出するDragGesture()を作成しました。 .onChangedおよび.onEndedクロージャーで、_@State_値を変更して、目的のscrollTargetを作成しました。

  3. 元の変更されていない値と新しい変更された値の両方を条件付きで.content.offset(x:y :)修飾子にフィードします-dragStateに応じて、不足しているscrollDelegateメソッドの代わりとして使用します。

  4. ドラッグが終了したときにのみ条件付きで動作するアニメーションを追加しました。

  5. ジェスチャーをscrollViewにアタッチしました。

短い話です。動作しません。私は私の問題が何であるかを理解したことを願っています。

そこに解決策はありますか?入力を楽しみにしています。ありがとう!

17
HelloTimo

@Bindingインデックスを使用してページング動作を実現できました。ソリューションが汚れているように見える可能性があります。回避策を説明します。

私が最初に間違ったのは、デフォルトの.leadingではなく.centerに位置合わせすることでした。それ以外の場合、オフセットは異常に機能します。次に、バインディングとローカルオフセット状態を組み合わせました。この方法は「真実の単一ソース」の原則に反しますが、それ以外の場合は、外部インデックスの変更を処理してオフセットを変更する方法がわかりませんでした。

だから、私のコードは次のとおりです

struct SwiftUIPagerView<Content: View & Identifiable>: View {

    @Binding var index: Int
    @State private var offset: CGFloat = 0
    @State private var isGestureActive: Bool = false

    // 1
    var pages: [Content]

    var body: some View {
        GeometryReader { geometry in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .center, spacing: 0) {
                    ForEach(self.pages) { page in
                        page
                            .frame(width: geometry.size.width, height: nil)
                    }
                }
            }
            // 2
            .content.offset(x: self.isGestureActive ? self.offset : -geometry.size.width * CGFloat(self.index))
            // 3
            .frame(width: geometry.size.width, height: nil, alignment: .leading)
            .gesture(DragGesture().onChanged({ value in
                // 4
                self.isGestureActive = true
                // 5
                self.offset = value.translation.width + -geometry.size.width * CGFloat(self.index)
            }).onEnded({ value in
                if -value.predictedEndTranslation.width > geometry.size.width / 2, self.index < self.pages.endIndex - 1 {
                    self.index += 1
                }
                if value.predictedEndTranslation.width > geometry.size.width / 2, self.index > 0 {
                    self.index -= 1
                }
                // 6
                withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
                // 7
                DispatchQueue.main.async { self.isGestureActive = false }
            }))
        }
    }
}
  1. コンテンツをラップするだけでよいので、「チュートリアルビュー」に使用しました。
  2. これは、外部と内部の状態変化を切り替えるトリックです
  3. すべてのオフセットを中央に変換したくない場合は、.leadingは必須です。
  4. 状態をローカル状態変更に設定する
  5. ジェスチャデルタ(* -1)と前のインデックス状態からの完全なオフセットを計算します
  6. 最後に、予測されたジェスチャーの終了に基づいて最終的なインデックスを設定し、オフセットを上下に丸めます
  7. インデックスへの外部変更を処理するために状態をリセットする

次のコンテキストでテストしました

    struct WrapperView: View {

        @State var index: Int = 0

        var body: some View {
            VStack {
                SwiftUIPagerView(index: $index, pages: (0..<4).map { index in TODOView(extraInfo: "\(index + 1)") })

                Picker(selection: self.$index.animation(.easeInOut), label: Text("")) {
                    ForEach(0..<4) { page in Text("\(page + 1)").tag(page) }
                }
                .pickerStyle(SegmentedPickerStyle())
                .padding()
            }
        }
    }

ここで、TODOViewは、実装するビューを示すカスタムビューです。

質問が正解になることを願っています。そうでない場合は、どの部分に焦点を当てるべきかを指定してください。また、isGestureActive状態を削除するための提案を歓迎します。

9
gujci

@gujci、面白い例をありがとう。私はそれで遊んで、isGestureActive状態を削除しました。完全な例はmy Gist にあります。

struct SwiftUIPagerView<Content: View & Identifiable>: View {

    @State private var index: Int = 0
    @State private var offset: CGFloat = 0

    var pages: [Content]

    var body: some View {
        GeometryReader { geometry in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .center, spacing: 0) {
                    ForEach(self.pages) { page in
                        page
                            .frame(width: geometry.size.width, height: nil)
                    }
                }
            }
            .content.offset(x: self.offset)
            .frame(width: geometry.size.width, height: nil, alignment: .leading)
            .gesture(DragGesture()
                .onChanged({ value in
                    self.offset = value.translation.width - geometry.size.width * CGFloat(self.index)
                })
                .onEnded({ value in
                    if abs(value.predictedEndTranslation.width) >= geometry.size.width / 2 {
                        var nextIndex: Int = (value.predictedEndTranslation.width < 0) ? 1 : -1
                        nextIndex += self.index
                        self.index = nextIndex.keepIndexInRange(min: 0, max: self.pages.endIndex - 1)
                    }
                    withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
                })
            )
        }
    }
}
1
Alex.O

代替ソリューションは、UIKitコンポーネントをSwiftUIにリンクするUIViewRepresentativeを使用して、UIKitをSwiftUIに統合することです。その他のリードとリソースについては、AppleがUIKitとのインターフェースを提案している方法をご覧ください: IKitとのインターフェース 。画像間のページングとトラック選択インデックスを示す良い例があります。 。

編集:(Apple)がビュー全体ではなくスクロールに影響を与えるある種のコンテンツオフセットを実装するまで、SwiftUIの最初のリリースにはUIKitのすべての機能が含まれるわけではないことを知っていたため、これが推奨されるソリューションです。

0
Kyle Beard

私が知る限り、swiftUIのスクロールは、scrollViewDidScrollscrollViewWillEndDraggingなどの潜在的に有用なものをまだサポートしていません。非常にカスタムの動作を作成するにはクラシックUIKitビューを使用し、より簡単なものにはクールなSwiftUIビューを使用することをお勧めします。私はそれをたくさん試しました、そしてそれは実際に働きます!これを見てください ガイド 。それが役に立てば幸い

0
glassomoss

@gujciあなたのソリューションは完璧です、より一般的な使用法では、モデルを受け入れ、ビルダーを表示するようにします(ビルダーにジオメトリサイズを渡すことに注意してください)。

struct SwiftUIPagerView<TModel: Identifiable ,TView: View >: View {

    @Binding var index: Int
    @State private var offset: CGFloat = 0
    @State private var isGestureActive: Bool = false

    // 1
    var pages: [TModel]
    var builder : (CGSize, TModel) -> TView

    var body: some View {
        GeometryReader { geometry in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .center, spacing: 0) {
                    ForEach(self.pages) { page in
                        self.builder(geometry.size, page)
                    }
                }
            }
            // 2
            .content.offset(x: self.isGestureActive ? self.offset : -geometry.size.width * CGFloat(self.index))
            // 3
            .frame(width: geometry.size.width, height: nil, alignment: .leading)
            .gesture(DragGesture().onChanged({ value in
                // 4
                self.isGestureActive = true
                // 5
                self.offset = value.translation.width + -geometry.size.width * CGFloat(self.index)
            }).onEnded({ value in
                if -value.predictedEndTranslation.width > geometry.size.width / 2, self.index < self.pages.endIndex - 1 {
                    self.index += 1
                }
                if value.predictedEndTranslation.width > geometry.size.width / 2, self.index > 0 {
                    self.index -= 1
                }
                // 6
                withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
                // 7
                DispatchQueue.main.async { self.isGestureActive = false }
            }))
        }
    }
}

そしてとして使用することができます:

    struct WrapperView: View {

        @State var index: Int = 0
        @State var items : [(color:Color,name:String)] = [
            (.red,"Red"),
            (.green,"Green"),
            (.yellow,"Yellow"),
            (.blue,"Blue")
        ]
        var body: some View {
            VStack(spacing: 0) {

                SwiftUIPagerView(index: $index, pages: self.items.identify { $0.name }) { size, item in
                    TODOView(extraInfo: item.model.name)
                        .frame(width: size.width, height: size.height)
                        .background(item.model.color)
                }

                Picker(selection: self.$index.animation(.easeInOut), label: Text("")) {
                    ForEach(0..<4) { page in Text("\(page + 1)").tag(page) }
                }
                .pickerStyle(SegmentedPickerStyle())

            }.edgesIgnoringSafeArea(.all)
        }
    }

いくつかのユーティリティの助けを借りて:

    struct MakeIdentifiable<TModel,TID:Hashable> :  Identifiable {
        var id : TID {
            return idetifier(model)
        }
        let model : TModel
        let idetifier : (TModel) -> TID
    }
    extension Array {
        func identify<TID: Hashable>(by: @escaping (Element)->TID) -> [MakeIdentifiable<Element, TID>]
        {
            return self.map { MakeIdentifiable.init(model: $0, idetifier: by) }
        }
    }
0
Ala'a Al Hallaq