【问题标题】:How can I make my subview with a binding to mainview disappear and cleanly reappear?如何使绑定到主视图的子视图消失并干净地重新出现?
【发布时间】:2022-07-30 02:27:45
【问题描述】:

我预计会发生什么

我有一个冥想视图,它有一个带有绑定属性 inhaling 的动画子视图,当按下按钮时应该会出现。

当动画子视图出现时,它应该从头开始动画。它基本上是苹果冥想呼吸动画:它从一个小球开始,当吸气为真时变大,然后当吸气为假时变小。

当用户再次按下按钮时,动画应该会消失。

当用户再次按下按钮,第二次,它应该启动动画子视图与绑定清洁。这意味着子视图是一个小球,然后又变大了。和第一次一样。

struct Meditation: View {
    @State var startBreathingAnimation = false
    @State var inhaling = false
    @State var infoText = "Start a mediation"

    var body: some View {
        VStack(spacing: 20) {
            ZStack {
                if startBreathingAnimation {
                    BreathAnimation(inhaling: $inhaling)
                        .onChange(of: inhaling) { newValue in
                            if newValue {
                                infoText = "Breath in..."
                            } else {
                                infoText = "Breath out..."
                            }                                }
                        .onDisappear {
                            infoText = "Start your meditation" // Never executed?
                        }
                } else {
                    Circle()
                        .frame(height: 100)
                        .foregroundColor(.blue)
                }
            }
            Text(infoText)
            Button("Toggle") {
                startBreathingAnimation.toggle()
            }
        }
        .padding()
    }
}

实际发生的情况

具有绑定的动画子视图不会重置,而是重新初始化,而是在按下按钮“关闭”后从中断处开始。

当我没有在子视图中添加绑定属性时,它实际上按预期工作:它每次都会重置并给我一个“新鲜”的子视图。但我确实需要观察动画子视图属性inhaling 的变化,以便更新主视图中的infoText 属性。

可重现的示例代码,准备复制到 Xcode 中

非常感谢任何帮助!

// Can be copied to Xcode directly
struct Meditation: View {
    @State var startBreathingAnimation = false
    @State var inhaling = false
    @State var infoText = "Start a mediation"

    var body: some View {
        VStack(spacing: 20) {
            ZStack {
                if startBreathingAnimation {
                    BreathAnimation(inhaling: $inhaling)
                        .onChange(of: inhaling) { newValue in
                            if newValue {
                                infoText = "Breath in..."
                            } else {
                                infoText = "Breath out..."
                            }                                }
                        .onDisappear {
                            infoText = "Start your meditation" // Never executed?
                        }
                } else {
                    Circle()
                        .frame(height: 100)
                        .foregroundColor(.blue)
                }
            }
            Text(infoText)
            Button("Toggle") {
                startBreathingAnimation.toggle()
            }
        }
        .padding()
    }
}


private let gradientStart = Color.accentColor.opacity(0.9)
private let gradientEnd = Color.accentColor.opacity(1.0)


private let gradient = LinearGradient(gradient: Gradient(colors: [gradientStart, gradientEnd]), startPoint: .top, endPoint: .bottom)
private let maskGradient = LinearGradient(gradient: Gradient(colors: [.black]), startPoint: .top, endPoint: .bottom)

private let maxSize: CGFloat = 150
private let minSize: CGFloat = 30
private let inhaleTime: Double = 8
private let exhaleTime: Double = 8
private let pauseTime: Double = 1.5

private let numberOfPetals = 4
private let bigAngle = 360 / numberOfPetals
private let smallAngle = bigAngle / 2

private let ghostMaxSize: CGFloat = maxSize * 0.99
private let ghostMinSize: CGFloat = maxSize * 0.95

private struct Petals: View {
    let size: CGFloat
    let inhaling: Bool

    var isMask = false

    var body: some View {
        let petalsGradient = isMask ? maskGradient : gradient

        ZStack {
            ForEach(0..<numberOfPetals) { index in
                petalsGradient
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .mask(
                        Circle()
                            .frame(width: size, height: size)
                            .offset(x: inhaling ? size * 0.5 : 0)
                            .rotationEffect(.degrees(Double(bigAngle * index)))
                    )
                    .blendMode(isMask ? .normal : .screen)
            }
        }
    }
}

struct BreathAnimation: View {
    @State private var size = minSize
    @Binding var inhaling: Bool

    @State private var ghostSize = ghostMaxSize
    @State private var ghostBlur: CGFloat = 0
    @State private var ghostOpacity: Double = 0

    var body: some View {
        ZStack {
//            Color.black
//                .edgesIgnoringSafeArea(.all)

            ZStack {
                // ghosting for exhaling
                Petals(size: ghostSize, inhaling: inhaling)
                    .blur(radius: ghostBlur)
                    .opacity(ghostOpacity)

                // the mask is important, otherwise there is a color
                // 'jump' when exhaling
                Petals(size: size, inhaling: inhaling, isMask: true)

                // overlapping petals
                Petals(size: size, inhaling: inhaling)
                Petals(size: size, inhaling: inhaling)
                    .rotationEffect(.degrees(Double(smallAngle)))
                    .opacity(inhaling ? 0.8 : 0.6)
            }
            .rotationEffect(.degrees(Double(inhaling ? bigAngle : -smallAngle)))
            .drawingGroup()
        }
        .onAppear {
            performAnimations()
        }
        .onDisappear {
            size = minSize
            inhaling = false
            ghostSize = ghostMaxSize
            ghostBlur = 0
            ghostOpacity = 0
        }
    }
    func performAnimations() {
        withAnimation(.easeInOut(duration: inhaleTime)) {
            inhaling = true
            size = maxSize
        }
        Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime, repeats: false) { _ in
            ghostSize = ghostMaxSize
            ghostBlur = 0
            ghostOpacity = 0.8

            Timer.scheduledTimer(withTimeInterval: exhaleTime * 0.2, repeats: false) { _ in
                withAnimation(.easeOut(duration: exhaleTime * 0.6)) {
                    ghostBlur = 30
                    ghostOpacity = 0
                }
            }

            withAnimation(.easeInOut(duration: exhaleTime)) {
                inhaling = false
                size = minSize
                ghostSize = ghostMinSize
            }
        }

        Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime + exhaleTime + pauseTime, repeats: false) { _ in

            // endless animation!
            performAnimations()
        }
    }

    private func performAnimations2() {
        withAnimation(.easeInOut(duration: inhaleTime)) {
            inhaling = true
            size = maxSize
        }
        Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime, repeats: false) { _ in
            ghostSize = ghostMaxSize
            ghostBlur = 0
            ghostOpacity = 0.8

            Timer.scheduledTimer(withTimeInterval: exhaleTime * 0.2, repeats: false) { _ in
                withAnimation(.easeOut(duration: exhaleTime * 0.6)) {
                    ghostBlur = 30
                    ghostOpacity = 0
                }
            }

            withAnimation(.easeInOut(duration: exhaleTime)) {
                inhaling = false
                size = minSize
                ghostSize = ghostMinSize
            }
        }

        Timer.scheduledTimer(withTimeInterval: inhaleTime + pauseTime + exhaleTime + pauseTime, repeats: false) { _ in

            // endless animation!
            performAnimations()
        }
    }
}

struct MainView_Previews: PreviewProvider {

    static var previews: some View {
        Meditation()
    }
}

【问题讨论】:

  • Xcode 13.4 / iOS 15.5 未观察到问题,即。如果在关闭动画从初始状态重新开始后打开。你是什​​么环境?还是我错过了什么?
  • 或者这取决于你把冥想视图放在哪里。我将它直接放入 ContentView 正文中(周围什么都没有)。
  • 顺便说一句,以前的帖子stackoverflow.com/questions/73144662/… 确实有可重现的问题,但我在当前变体中看不到它。 (最好去掉之前的帖子,不要有同样的问题帖子,可能会混淆以后搜索的人。)
  • 我的环境是 Xcode 14 beta 3 和 iOS 16 beta 3。我也将它直接放到 ContentView 中。周围什么都没有。所以也许它与测试版有关。虽然下面的答案似乎和我一样。
  • 好吧,我认为这一切都与计时器有关 - 两个许多不受管理且未明确停止,因此它们可以在不幸的中断时刻重叠。您必须保留对计时器的引用并在停止活动时明确取消它们。

标签: swiftui


【解决方案1】:

这是一种可能的方法,通过为视图设置特定的 .id 并在重置时更改它,强制重绘子视图:

struct ContentView: View {
    @State var startBreathingAnimation = false
    @State var inhaling = false
    @State var infoText = "Start a mediation"
    @State var viewID = UUID()

    var body: some View {
        VStack(spacing: 20) {
            ZStack {
                if startBreathingAnimation {
                    BreathAnimation(inhaling: $inhaling)
                        .id(viewID) // here
                        .onChange(of: inhaling) { newValue in
                            if newValue {
                                infoText = "Breath in..."
                            } else {
                                infoText = "Breath out..."
                            }
                        }
                } else {
                    Circle()
                        .frame(height: 100)
                        .foregroundColor(.blue)
                }
            }
            Text(infoText)
            Button("Toggle") {
                if startBreathingAnimation {
                    startBreathingAnimation = false
                    infoText = "Start your meditation" 
                    inhaling = false
                } else {
                    startBreathingAnimation = true
                    viewID = UUID()
                }
            }
        }
        .padding()
    }
}

【讨论】:

  • 感谢您的建议!我尝试过这个。它奏效了。但是现在一个新的错误已经进入了代码。动画不同步。如果我反复切换动画,吸气和呼气部分会更长、更短、更奇怪。你也见过这个吗?知道为什么吗?
【解决方案2】:

作为对 ChrisR 答案的扩展,这有助于给我一个新的子视图,但产生了动画属性值不同步的问题,我使用了 PreferenceKeys 的帮助。 PreferenceKeys 显然在许多中级 SwiftUI 开发人员中并不为人所知,所以我想在这里简要分享一下。

Swiftful Thinking 有一个很棒的视频:link to video

绑定到子视图及其父视图为我的案例创建了一种强连接方式。我只想在 MainView() 上观察 BreathAnimation() 的吸入属性。

这就是 PreferenceKeys 发挥作用的时候。

这是帮助我解决问题的代码。

如果需要,创建一个可以从所有视图访问的属性:

struct InhalingPreferenceKey: PreferenceKey {
    
    static var defaultValue: Bool = false
    
    static func reduce(value: inout Bool, nextValue: () -> Bool) {
        value = nextValue()
    }
}

// Housekeeping that lets us update the preference key in our childview
extension View {
    
    func updateInhalingPreferenceKey(_ isInhaling: Bool) -> some View {
        preference(key: InhalingPreferenceKey.self, value: isInhaling)
    }
    
}

将此添加到子视图并将其连接到 BreathAnimation 属性吸入:

        .updateInhalingPreferenceKey(inhaling)

最后,我们可以通过以下方式访问 childview 属性:

                                    .onPreferenceChange(InhalingPreferenceKey.self, perform: { inhaling in
                                        self.inhaling = inhaling
                                    }) // self.inhaling is the parentview property

这与 ChrisR 的新儿童视图解决方案一起帮助我实现了我想要的。希望这对其他人也有帮助!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多