I need to implement something like an animated page control. And I don't want to use integration with UIKit if possible. I have pages array containing 4 views I need to switch between. I create the animation itself by changing the value of progress variable using timer. And I have the following code right now
@State var pages: [PageView] @State var currentIndex = 0 @State var nextIndex = 1 @State var progress: Double = 0 var body: some View { ZStack { Button(action: { self.isAnimating = true }) { shape.onReceive(timer) { _ in if !self.isAnimating { return } self.refreshAnimatingViews() } }.offset(y: 300) pages[currentIndex] .offset(x: -CGFloat(pow(2, self.progress))) pages[nextIndex] .offset(x: CGFloat(pow(2, (limit - progress)))) } } It is animating great - current page is moved to the left until it disappears, and the next page is revealed from the right taking its place. At the end of animation I add 1 to both indices and reset progress to 0. But once the animation (well not exactly an animation - I just change the value of progress using timer, and generate every state manually) is over, the page with index 1 is swapped back to page with index 0. If I check with debugger, currentIndex and nextIndex values are correct - 1 and 2, but the page displayed after animation is always the one I started with (with index 0). Does anybody know why this is happening?
The whole code follows
struct ContentView : View { let limit: Double = 15 let step: Double = 0.3 let timer = Timer.publish(every: 0.01, on: .current, in: .common).autoconnect() @State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center)) @State var pages: [PageView] @State var currentIndex = 0 @State var nextIndex = 1 @State var progress: Double = 0 @State var isAnimating = false var body: some View { ZStack { Button(action: { self.isAnimating = true }) { shape.onReceive(timer) { _ in if !self.isAnimating { return } self.refreshAnimatingViews() } }.offset(y: 300) pages[currentIndex] .offset(x: -CGFloat(pow(2, self.progress))) pages[nextIndex] .offset(x: CGFloat(pow(2, (limit - progress)))) }.edgesIgnoringSafeArea(.vertical) } func refreshAnimatingViews() { progress += step if progress > 2*limit { isAnimating = false progress = 0 currentIndex = nextIndex if nextIndex + 1 < pages.count { nextIndex += 1 } else { nextIndex = 0 } } } } struct PageView: View { @State var title: String @State var imageName: String @State var content: String let imageWidth: Length = 150 var body: some View { VStack(alignment: .center, spacing: 15) { Text(title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil) Image(imageName) .resizable() .frame(width: imageWidth, height: imageWidth) .cornerRadius(imageWidth/2) .clipped() Text(content).font(.body).lineLimit(nil) }.padding(60) } } struct MockData { static let title = "Eating grapes 101" static let contentStrings = [ "Step 1. Break off a branch holding a few grapes and lay it on your plate.", "Step 2. Put a grape in your mouth whole.", "Step 3. Deposit the seeds into your thumb and first two fingers.", "Step 4. Place the seeds on your plate." ] static let imageNames = [ "screen 1", "screen 2", "screen 3", "screen 4" ] } in SceneDelegate:
if let windowScene = scene as? UIWindowScene { let pages = (0...3).map { i in PageView(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i]) } let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: ContentView(pages: pages)) self.window = window window.makeKeyAndVisible() }