使用 UIView.animate 动画化 SwiftUI 视图
Animating a SwiftUI view with UIView.animate
我注意到 UIView.animate
比在 SwiftUI 中使用 withAnimation { }
和一般动画 class 更不古怪,'smoother' 的延迟也更少。也就是说,我有一些 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()
}
}
}
您可以使用 UIViewRepresentable
和 ViewModifier
来做到这一点,以使其对用户友好。
这是通过影响整个 SwiftUI View
的 UIView
版本来完成的。您不会在 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)
}
}
然后为了方便使用,将包装器放在 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)
}
}
https://developer.apple.com/documentation/swiftui/viewmodifier
现在使用它只需访问
.flipAnimation(flipped: Binding<Bool>)
附在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)
}
}
如果你想更“喜欢”它一点,你可以将 transition/animation 参数传递给 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
}
}
}
然后您可以向 UIView
添加不同的配置更改,这样它们就可以像 animations
那样完成
.flipAnimation(flipped: $flipped, animationConfiguration: {
view in
view.alpha = Double.random(in: 0.25...1)
view.backgroundColor = [.red,.gray,.green].randomElement()!
})
我注意到 UIView.animate
比在 SwiftUI 中使用 withAnimation { }
和一般动画 class 更不古怪,'smoother' 的延迟也更少。也就是说,我有一些 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()
}
}
}
您可以使用 UIViewRepresentable
和 ViewModifier
来做到这一点,以使其对用户友好。
这是通过影响整个 SwiftUI View
的 UIView
版本来完成的。您不会在 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)
}
}
然后为了方便使用,将包装器放在 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)
}
}
https://developer.apple.com/documentation/swiftui/viewmodifier
现在使用它只需访问
.flipAnimation(flipped: Binding<Bool>)
附在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)
}
}
如果你想更“喜欢”它一点,你可以将 transition/animation 参数传递给 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
}
}
}
然后您可以向 UIView
添加不同的配置更改,这样它们就可以像 animations
.flipAnimation(flipped: $flipped, animationConfiguration: {
view in
view.alpha = Double.random(in: 0.25...1)
view.backgroundColor = [.red,.gray,.green].randomElement()!
})