2

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() } 
9
  • How are progress, currentIndex and nextIndex defined? Commented Aug 7, 2019 at 9:59
  • Edited my question to include declarations, thank you Commented Aug 8, 2019 at 5:34
  • This still doesn't show any error. Can you share the update and initialization code too please. Commented Aug 8, 2019 at 6:26
  • Initialization is combined with declaration in my case and I added update code - and that's pretty much my whole program already Commented Aug 8, 2019 at 7:41
  • Please post a full example so we can replicate it. Thanks! I tried 10min to replicate it but didn't get it done. Commented Aug 8, 2019 at 11:03

1 Answer 1

3

The following solution works. I think the problem was switching out views while SwiftUI tries to diff and update them is not something SwiftUI is good at.

So just use the same two PageView views and swap out their content based on the current index.

import Foundation import SwiftUI import Combine struct PagesView : View { let limit: Double = 15 let step: Double = 0.3 @State var pages: [Page] = (0...3).map { i in Page(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i]) } @State var currentIndex = 0 @State var nextIndex = 1 @State var progress: Double = 0 @State var isAnimating = false static let timerSpeed: Double = 0.01 @State var timer = Timer.publish(every: timerSpeed, on: .current, in: .common).autoconnect() @State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center)) var body: some View { ZStack { Button(action: { self.isAnimating.toggle() self.timer = Timer.publish(every: Self.timerSpeed, on: .current, in: .common).autoconnect() }) { self.shape }.offset(y: 300) PageView(page: pages[currentIndex]) .offset(x: -CGFloat(pow(2, self.progress))) PageView(page: pages[nextIndex]) .offset(x: CGFloat(pow(2, (self.limit - self.progress)))) }.edgesIgnoringSafeArea(.vertical) .onReceive(self.timer) { _ in if !self.isAnimating { return } self.refreshAnimatingViews() } } 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 Page { var title: String var imageName: String var content: String let imageWidth: CGFloat = 150 } struct PageView: View { var page: Page var body: some View { VStack(alignment: .center, spacing: 15) { Text(page.title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil) Image(page.imageName) .resizable() .frame(width: page.imageWidth, height: page.imageWidth) .cornerRadius(page.imageWidth/2) .clipped() Text(page.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" ] } 
Sign up to request clarification or add additional context in comments.

1 Comment

I tried your solution and found out that the step that did it was not using only two page views. Just removing @State from PageView's variables declaration solves the problem

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.