-
-
Save mecid/e0d4d6652ccc8b5737449a01ee8cbc6f to your computer and use it in GitHub Desktop.
// | |
// PagerView.swift | |
// | |
// Created by Majid Jabrayilov on 12/5/19. | |
// Copyright © 2019 Majid Jabrayilov. All rights reserved. | |
// | |
import SwiftUI | |
struct PagerView<Content: View>: View { | |
let pageCount: Int | |
@Binding var currentIndex: Int | |
let content: Content | |
@GestureState private var translation: CGFloat = 0 | |
init(pageCount: Int, currentIndex: Binding<Int>, @ViewBuilder content: () -> Content) { | |
self.pageCount = pageCount | |
self._currentIndex = currentIndex | |
self.content = content() | |
} | |
var body: some View { | |
GeometryReader { geometry in | |
HStack(spacing: 0) { | |
self.content.frame(width: geometry.size.width) | |
} | |
.frame(width: geometry.size.width, alignment: .leading) | |
.offset(x: -CGFloat(self.currentIndex) * geometry.size.width) | |
.offset(x: self.translation) | |
.animation(.interactiveSpring()) | |
.gesture( | |
DragGesture().updating(self.$translation) { value, state, _ in | |
state = value.translation.width | |
}.onEnded { value in | |
let offset = value.translation.width / geometry.size.width | |
let newIndex = (CGFloat(self.currentIndex) - offset).rounded() | |
self.currentIndex = min(max(Int(newIndex), 0), self.pageCount - 1) | |
} | |
) | |
} | |
} | |
} |
@wtpalexander yes it seems that if the PagerView is embedded in a ScrollView then the sub views of the PagerView are not scrollable. Again my view hierarchy is Master View -> PagerView -> Subviews. For now I have the subviews having ScrollViews, which works to scroll vertically to see their content, however the large title the nav bar doesn't change to inline. Ideally I would like to remove the ScrollView from the subviews and place it in the MasterView so I can get this functionality where the large title changes to inline.
@mecid / @d03090 / @gesabo
Just my two cents, as often as possible I prefer to use the real-thing given Apple usually does a far more complete implementation for other things like accessibility. In addition, the animations and gesture implementations are more consistent across iOS versions. In that vein, I've created an implementation that just wraps TabView
and provides auto-sizing based on the content (height).
Since it wraps TabView
the API is identical and it behaves exactly as expected. Feel free to grab it here if it works for your needs:
https://gist.github.com/shaps80/65d9d58480469bfe8eff6542f1dd16c4
@wtpalexander I tried your solution but I found the gestures to be very 'sticky' and difficult to work with, maybe this works for you too? 👍
My team recently delivered PagerTabStripView, a new Open Source for Pager Tab views. This is the GitHub repo. You can read an introduction in this post.
Here is an example gif!
We'll appreciate it if you can take a look! If you like the library, want to contribute to the project, or need some help using it, please reach out to us, open an issue or a pull request.
This will more smoothly
import Foundation
import SwiftUI
struct PagerView<Content: View>: View {
let pageCount: Int
@State var ignore: Bool = false
@Binding var currentIndex: Int {
didSet {
if (!ignore) {
currentFloatIndex = CGFloat(currentIndex)
}
}
}
@State var currentFloatIndex: CGFloat = 0 {
didSet {
ignore = true
currentIndex = min(max(Int(currentFloatIndex.rounded()), 0), self.pageCount - 1)
ignore = false
}
}
let content: Content
@GestureState private var offsetX: CGFloat = 0
init(pageCount: Int, currentIndex: Binding<Int>, @ViewBuilder content: () -> Content) {
self.pageCount = pageCount
self._currentIndex = currentIndex
self.content = content()
}
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
self.content.frame(width: geometry.size.width)
}
.frame(width: geometry.size.width, alignment: .leading)
.offset(x: -CGFloat(self.currentFloatIndex) * geometry.size.width)
.offset(x: self.offsetX)
.animation(.linear, value:offsetX)
.highPriorityGesture(
DragGesture().updating(self.$offsetX) { value, state, _ in
state = value.translation.width
}
.onEnded({ (value) in
let offset = value.translation.width / geometry.size.width
let offsetPredicted = value.predictedEndTranslation.width / geometry.size.width
let newIndex = CGFloat(self.currentFloatIndex) - offset
self.currentFloatIndex = newIndex
withAnimation(.easeOut) {
if(offsetPredicted < -0.5 && offset > -0.5) {
self.currentFloatIndex = CGFloat(min(max(Int(newIndex.rounded() + 1), 0), self.pageCount - 1))
} else if (offsetPredicted > 0.5 && offset < 0.5) {
self.currentFloatIndex = CGFloat(min(max(Int(newIndex.rounded() - 1), 0), self.pageCount - 1))
} else {
self.currentFloatIndex = CGFloat(min(max(Int(newIndex.rounded()), 0), self.pageCount - 1))
}
}
})
)
}
.onChange(of: currentIndex, perform: { value in
print("index changed")
// this is probably animated twice, if the tab change occurs because of the drag gesture
withAnimation(.easeOut) {
currentFloatIndex = CGFloat(value)
}
})
}
}
Is there any way to load the views lazily?
@samilao101 did you try to use LazyHStack instead of HStack?
@mecid I did, but the issue is that in order for a lazyhstack to become lazy it needs to be imbedded in a scrollview, and if you do that, you essentially lose the pagination feature.
@samilao101 oh, you're right. Maybe TabView with Page style will work for you better?
So isn't the lazy this approach....Load all contents and onAppear
@pjcau sorry I am not sure I understand what you mean
@pjcau sorry I am not sure I understand what you mean
IN this way, if you want load a array of DetalView for example, so load in one step all init and onAppear(). So it's not good. But fix it with TabView with Page style.
@pjcau did you manage to integrate ScrollView in order to achieve lazy loading?
@gesabo Apologies - I miss understood your requirements! So you'd like a scroll view that snaps to each page, but when scrolled to the second or third nested view, the NavBar changes from
LargeTitle
toInline
?