I have a legacy UIImageView
that animates using a CABasicAnimation
. I wrapped that in a UIViewRepresentable
in order to embed it inside a SwiftUI view. It works great for the main view, but in a view that is presented using a .sheet
modifier, it doesn't animate. My entire code is below (compiled with Xcode 15.3).
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Microphone: UIViewRepresentable {
func updateUIView(_ uiView: UIImageView, context: Context) {
}
func makeUIView(context: Context) -> UIImageView {
let view = UIImageView(
image:
UIImage(
systemName: "mic.circle.fill",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 50,weight: .bold, scale: .large)
)?
.withRenderingMode(.alwaysTemplate)
)
view.translatesAutoresizingMaskIntoConstraints = false
view.tintColor = .green
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 1.3
animation.repeatCount = .greatestFiniteMagnitude
animation.autoreverses = true
animation.fromValue = 0.9
animation.toValue = 1.1
view.layer.add(animation, forKey: "scale")
return view
}
}
struct ContentView: View {
@State var showSheet = false
var body: some View {
VStack {
Microphone() // <-- animates perfectly
.frame(width: 50, height: 50)
Button("Show Sheet") { showSheet = true }
.padding(.top)
}
.sheet(isPresented: $showSheet) {
VStack {
Microphone() // <-- does not animate
.frame(width: 50, height: 50)
}
}
}
}
#Preview {
ContentView()
}
The reason I'm going with UIViewRepresentable
is
Checking the memory graph debugger, the CABasicAnimation
created for the image view in the sheet is deallocated for some reason. From my experiments, I also found that the animation does get added if layer.add(animation)
is called after some delay. I think this is either a SwiftUI bug or perhaps the view somehow isn't "ready" for an animation to be added.
Adding the animation in layoutSubviews
works, at least:
class ImageViewSubclass: UIImageView {
override func layoutSubviews() {
super.layoutSubviews()
// optionally, check layer.animation(forKey: "scale") == nil first
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 1.3
animation.repeatCount = .greatestFiniteMagnitude
animation.autoreverses = true
animation.fromValue = 0.9
animation.toValue = 1.1
layer.add(animation, forKey: "scale")
}
}
// in makeUIView:
let view = ImageViewSubclass(...)
...
return view
There are probably other life cycle methods you can do this in.
A pure SwiftUI way to do such an animation would be:
struct Microphone: View {
@State private var scale = 0.9
var body: some View {
Image(systemName: "mic.circle.fill")
.imageScale(.large)
.font(.system(size: 50, weight: .bold))
.foregroundStyle(.green)
.scaleEffect(scale)
.onAppear {
withAnimation(.linear(duration: 1.3).repeatForever(autoreverses: true)) {
scale = 1.1
}
}
}
}