我需要实现类似动画页面控件的东西。如果可能的话,我不想使用与 UIKit 的集成。我有 pages
数组,其中包含需要在 4 个 View 之间切换的 View 。我通过使用计时器更改 progress
变量的值来创建动画本身。我现在有以下代码
@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))))
}
}
它的动画很棒 - 当前页面向左移动直到消失,然后下一页从右侧显示取代它的位置。在动画结束时,我将 1
添加到两个索引,并将 progress
重置为 0
。但是,一旦动画(不完全是动画 - 我只是使用计时器更改进度值,并手动生成每个状态)结束,索引为 1
的页面将交换回索引为 0
。如果我使用调试器检查, currentIndex
和 nextIndex
值是正确的 - 1
和 2
,但是之后显示的页面动画始终是我开始的动画(索引为 0
)。有谁知道为什么会发生这种情况吗?
整个代码如下
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"
]
}
在场景委托(delegate)中:
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()
}
最佳答案
以下解决方案有效。我认为问题是在 SwiftUI 尝试比较和更新 View 时切换 View ,这并不是 SwiftUI 所擅长的。
因此,只需使用相同的两个 PageView View 并根据当前索引交换其内容即可。
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"
]
}
关于SwiftUI页面控件实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57391615/