問題
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アプローチ以外に):
ScrollViewには、画面領域よりも大きいコンテンツ領域があり、スクロールをまったく有効にできます。
ドラッグが行われているかどうかを検出するDragGesture()
を作成しました。 .onChangedおよび.onEndedクロージャーで、_@State
_値を変更して、目的のscrollTargetを作成しました。
元の変更されていない値と新しい変更された値の両方を条件付きで.content.offset(x:y :)修飾子にフィードします-dragStateに応じて、不足しているscrollDelegateメソッドの代わりとして使用します。
ドラッグが終了したときにのみ条件付きで動作するアニメーションを追加しました。
ジェスチャーをscrollViewにアタッチしました。
短い話です。動作しません。私は私の問題が何であるかを理解したことを願っています。
そこに解決策はありますか?入力を楽しみにしています。ありがとう!
@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 }
}))
}
}
}
.leading
は必須です。次のコンテキストでテストしました
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
状態を削除するための提案を歓迎します。
@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) }
})
)
}
}
}
代替ソリューションは、UIKitコンポーネントをSwiftUIにリンクするUIViewRepresentativeを使用して、UIKitをSwiftUIに統合することです。その他のリードとリソースについては、AppleがUIKitとのインターフェースを提案している方法をご覧ください: IKitとのインターフェース 。画像間のページングとトラック選択インデックスを示す良い例があります。 。
編集:(Apple)がビュー全体ではなくスクロールに影響を与えるある種のコンテンツオフセットを実装するまで、SwiftUIの最初のリリースにはUIKitのすべての機能が含まれるわけではないことを知っていたため、これが推奨されるソリューションです。
私が知る限り、swiftUI
のスクロールは、scrollViewDidScroll
やscrollViewWillEndDragging
などの潜在的に有用なものをまだサポートしていません。非常にカスタムの動作を作成するにはクラシックUIKitビューを使用し、より簡単なものにはクールなSwiftUIビューを使用することをお勧めします。私はそれをたくさん試しました、そしてそれは実際に働きます!これを見てください ガイド 。それが役に立てば幸い
@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) }
}
}