SwiftUI页面控件实现

标签 swiftui

我需要实现类似动画页面控件的东西。如果可能的话,我不想使用与 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。如果我使用调试器检查, currentIndexnextIndex 值是正确的 - 12,但是之后显示的页面动画始终是我开始的动画(索引为 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/

相关文章:

swift - 解决包依赖 swiftUI 项目时出错

swift - 使用行和列构建惰性网格 - SwiftUI

swift - NavigationView SwiftUI 在 iPad 中显示 Split View

ios - SwiftUI:如何将初始化程序分配给特定对象?

SwiftUI 线程 1 : Fatal error: each layout item may only occur once

swift - 带有 `isActive=true` 的嵌套导航链接未正确显示

animation - 全屏封面/模态的替代动画 - iOS 14

arrays - 如何将字符串数组转换为列表

swift - SwiftUI 列表的性能问题

SwiftUI:var 和 let 作为列表行中的参数有什么区别?