使用 UIView.animate 对 SwiftUI 视图进行动画处理

nic*_*ng2 4 animation uikit uiview swift swiftui

我注意到,与使用SwiftUI 中的通用动画类UIView.animate相比,它不那么古怪,更“平滑”,延迟也更少。withAnimation { }话虽这么说,我有一些Flashcards正在翻转。问题是,当我使用 时withAnimation { },会出现一些滞后,有时会让卡片看起来甚至没有翻转(看起来只是卡片内的内容立即发生变化)。我在 ScrollView 中同时渲染了 5 个抽认卡。我如何使用UIView.animate()动画来实现变化?

struct Flashcard<Front, Back>: View where Front: View, Back: View {
    var front: () -> Front
    var back: () -> Back
    
    @State var flipped: Bool = false
    
    @State var flashcardRotation = 0.0
    @State var contentRotation = 0.0
    
    init(@ViewBuilder front: @escaping () -> Front, @ViewBuilder back: @escaping () -> Back) {
        self.front = front
        self.back = back
    }
    
    var body: some View {
        ZStack {
            if flipped {
                ZStack {
                    back()
                    VStack {
                        HStack {
                            Spacer()
                            Button(action: {
                                flipFlashcard()
                            }, label: {
                                Text("Flipper")
                            }).padding(5)
                        }
                        Spacer()
                    }
                }
            } else {
                ZStack {
                    front()
                    VStack {
                        HStack {
                            Spacer()
                            Button(action: {
                                flipFlashcard()
                            }, label: {
                                Text("Flipper")
                            }).padding(5)
                        }
                        Spacer()
                    }
                }
            }
        }
        .rotation3DEffect(.degrees(contentRotation), axis: (x: 0, y: 1, z: 0))
        .frame(height: 150)
        .frame(maxWidth: .infinity)
        .padding(.horizontal)
        .rotation3DEffect(.degrees(flashcardRotation), axis: (x: 0, y: 1, z: 0))
    }
    
    func flipFlashcard() {
        let animationTime = 0.25
//        My attempt at using UIView.animate()
//        UIView.animate(withDuration: 5, animations: {
//            flashcardRotation += 180
//        })

        withAnimation(Animation.linear(duration: animationTime)) {
            flashcardRotation += 180
        }

//        My attempt at using UIView.animate()
//        UIView.animate(withDuration: 0.001, delay: animationTime / 2, animations: {
//            contentRotation += 180
//            flipped.toggle()
//        }, completion: nil)
        
        withAnimation(Animation.linear(duration: 0.001).delay(animationTime / 2)) {
            contentRotation += 180
            flipped.toggle()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

lor*_*sum 5

您可以使用 aUIViewRepresentable和 a来完成此操作ViewModifier,以使其用户友好。

这是通过影响整个UIViewSwiftUI 的版本来完成的。View您不会更改 SwiftUI 中的变量View

UIViewRepresentable为了让 SwiftUI 兼容 UIKit

struct FlipWrapper<Content: View>: UIViewRepresentable{
    //The content to be flipped
    @ViewBuilder let content: () -> Content
    //Triggers the animation
    @Binding var flip: Bool
    func makeUIView(context: Context) -> UIView {
        //Convert SwiftUI View to UIKit UIView
        UIHostingController(rootView: content()).view
    }
    func updateUIView(_ uiView: UIView, context: Context) {
        //Variable must be used somewhere or it wont trigger
        print(flip)
        //Transition is a much easier way to Flip in UIKit
        UIView.transition(with: uiView, duration: 0.25, options: .transitionFlipFromRight, animations: nil, completion: nil)
    }
}
Run Code Online (Sandbox Code Playgroud)

然后为了使其易于使用,将包装器放在 a 中ViewModifier,以便可以像所有其他修饰符一样访问它

extension View {
    func flipAnimation( flipped: Binding<Bool>) -> some View {
        modifier(FlipAnimationViewModifier(flipped: flipped))
    }
}
struct FlipAnimationViewModifier: ViewModifier {
    @Binding var flipped: Bool
    func body(content: Content) -> some View {
        FlipWrapper(content: {
            content
        }, flip: $flipped)
    }
}
Run Code Online (Sandbox Code Playgroud)

https://developer.apple.com/documentation/swiftui/viewmodifier

现在使用它只需访问

.flipAnimation(flipped: Binding<Bool>)
Run Code Online (Sandbox Code Playgroud)

附加到View你想要在变量改变时翻转。

示例使用如下。在Flashcard你会flipped.toggle()而不是打电话flipFlashcard()

struct Flashcard<Front, Back>: View where Front: View, Back: View {
    @ViewBuilder var front: () -> Front
    @ViewBuilder var back: () -> Back
    
    @State var flipped: Bool = false
    var body: some View {
        ZStack {
            if flipped{
                back()
            }else{
                front()
            }
            VStack {
                HStack {
                    Spacer()
                    Button(action: {
                        flipped.toggle()
                    }, label: {
                        Text("Flipper")
                    }).padding(5)
                }
                Spacer()
            }
        }.flipAnimation(flipped: $flipped)
            .frame(height: 150)
            .frame(maxWidth: .infinity)
            .padding(.horizontal)
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你想更“花哨”一点,你可以将过渡/动画参数传递给 SwiftUI 来获取值。

extension View {
    func flipAnimation(flipped: Binding<Bool>, duration: TimeInterval = 0.25, options: UIView.AnimationOptions = .transitionFlipFromRight, animationConfiguration: ((UIView) -> Void)? = nil, onComplete: ((Bool) -> Void)? = nil)-> some View {
        modifier(FlipAnimationViewModifier(flipped: flipped, duration: duration, options: options, animationConfiguration: animationConfiguration, onComplete: onComplete))
    }
}
struct FlipAnimationViewModifier: ViewModifier {
    @Binding var flipped: Bool
    var duration: TimeInterval
    var options: UIView.AnimationOptions
    var animationConfiguration: ((UIView) -> Void)?
    var onComplete: ((Bool) -> Void)?
    func body(content: Content) -> some View {
        FlipWrapper(content: {
            content
        }, flip: $flipped, duration: duration, options: options, animationConfiguration: animationConfiguration, onComplete: onComplete)
    }
}
struct FlipWrapper<Content: View>: UIViewRepresentable{
    //The content to be flipped
    @ViewBuilder let content: () -> Content
    //Triggers the animation
    @Binding var flip: Bool
    var duration: TimeInterval
    var options: UIView.AnimationOptions
    var animationConfiguration: ((UIView) -> Void)?
    var onComplete: ((Bool) -> Void)?
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIView(context: Context) -> UIView {
        //Convert SwiftUI View to UIKit UIView
        UIHostingController(rootView: content()).view
    }
    func updateUIView(_ uiView: UIView, context: Context) {
        //Vaariable must be used somewhere or it wont trigger
        print(flip)
        //update gets called after make, if you only want to trigger the animation when you change flip you have to lock it for the first time
        //using the corrdinator to store the state makes it easy
        if !context.coordinator.initAnim{
            //Transition is a much simpler way to flip using UIKit
            UIView.transition(with: uiView, duration: duration, options: options, animations: {
                if animationConfiguration != nil{
                    animationConfiguration!(uiView)
                }
            }, completion: onComplete)
        }else{
            context.coordinator.initAnim = false
        }
    }
    class Coordinator{
        //Variable to lock animation for initial run
        var initAnim: Bool = true
        var parent: FlipWrapper
        init(_ parent:FlipWrapper){
            self.parent = parent
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你可以添加不同的配置更改,UIView这样它们就可以完成animations

.flipAnimation(flipped: $flipped, animationConfiguration: {
    view in
    view.alpha = Double.random(in: 0.25...1)
    view.backgroundColor = [.red,.gray,.green].randomElement()!
})
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述