Problème
Comment puis-je modifier la cible de défilement d'une scrollView? Je cherche une sorte de remplacement pour la méthode déléguée scrollView "classique"
override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
... où nous pouvons modifier le scrollView.contentOffset
ciblé via targetContentOffset.pointee
par exemple pour créer un comportement de pagination personnalisé.
Ou en d'autres termes: je veux créer un effet de pagination dans un scrollView (horizontal).
Ce que j'ai essayé c'est à dire. est quelque chose comme ça:
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)
Tentative
Voici ce que j'ai essayé (en plus d'une approche scrollView personnalisée):
Un scrollView a une zone de contenu plus grande que l'espace d'écran pour permettre le défilement.
J'ai créé une DragGesture()
pour détecter s'il y a un glissement en cours. Dans les fermetures .onChanged et .onEnded, j'ai modifié mes valeurs @State
Pour créer le scrollTarget souhaité.
Alimentation conditionnelle à la fois des valeurs d'origine inchangées et des nouvelles valeurs modifiées dans le modificateur .content.offset (x: y :) - en fonction du dragState en remplacement des méthodes scrollDelegate manquantes.
Ajout d'une animation agissant conditionnellement uniquement lorsque le glissement est terminé.
Attaché le geste au scrollView.
Longue histoire courte. Ça ne marche pas. J'espère avoir compris quel est mon problème.
Des solutions? Dans l'attente de toute entrée. Merci!
J'ai réussi à obtenir un comportement de pagination avec un @Binding
index. La solution peut sembler sale, je vais vous expliquer mes solutions de contournement.
La première chose que je me suis trompée, c'est d'avoir un alignement sur .leading
au lieu de la valeur par défaut .center
, sinon le décalage fonctionne de manière inhabituelle. J'ai ensuite combiné la liaison et un état de décalage local. Cela va un peu à l'encontre du principe de la "source unique de vérité", mais sinon je n'avais aucune idée de la façon de gérer les changements d'index externes et de modifier mon décalage.
Donc, mon code est le suivant
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
est obligatoire si vous ne voulez pas traduire tous les décalages au centre.Je l'ai testé dans le contexte suivant
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()
}
}
}
où TODOView
est ma vue personnalisée qui indique une vue à implémenter.
J'espère avoir bien répondu à la question, sinon veuillez préciser sur quelle partie je dois me concentrer. Je souhaite également toute suggestion de suppression de l'état isGestureActive
.
@gujci, merci pour cet exemple intéressant. J'ai joué avec et j'ai supprimé l'état isGestureActive
. Un exemple complet peut être trouvé dans mon 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) }
})
)
}
}
}
Une solution alternative serait d'intégrer UIKit dans SwiftUI en utilisant UIViewRepresentative qui relie les composants UIKit à SwiftUI. Pour des pistes et des ressources supplémentaires, voyez comment Apple vous suggère d'interfacer avec UIKit: Interfaçage avec UIKit . Ils ont un bon exemple qui montre à la page entre les images et l'index de sélection de piste .
Edit: jusqu'à ce qu'ils (Apple) implémentent une sorte de décalage de contenu qui affecte le défilement au lieu de la vue entière, c'est leur solution suggérée car ils savaient que la version initiale de SwiftUI ne comprendrait pas toutes les fonctionnalités d'UIKit.
Pour autant que je sache, les parchemins dans swiftUI
ne prennent pas encore en charge quoi que ce soit d'utile comme scrollViewDidScroll
ou scrollViewWillEndDragging
. Je suggère d'utiliser des vues UIKit classiques pour créer un comportement très personnalisé et des vues SwiftUI sympas pour tout ce qui est plus facile. J'ai beaucoup essayé et ça marche! Jetez un oeil à ce guide . J'espère que cela pourra aider
@gujci, votre solution est parfaite, pour une utilisation plus générale, faites-la accepter les modèles et le générateur de vue comme dans (notez que je passe la taille de la géométrie dans le générateur):
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 }
}))
}
}
}
et peut être utilisé comme:
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)
}
}
avec l'aide de certains utilitaires:
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) }
}
}