SwiftUI: Unable to animate images
Here is possible approach. I simplified it for a demo purpose and less posting code, but idea should be clear and easy transferrable to your real code
Result demo (really it is much fluent than on gif):
Modified model
enum SpeakerSymbol: Int, CaseIterable { // Inherited from Int for convenient below
case speakerEmpty, speaker1, speaker2, speaker3
var image: some View {
var name: String
switch self {
case .speakerEmpty: name = "speaker.slash.fill"
case .speaker1: name = "speaker.1.fill"
case .speaker2: name = "speaker.2.fill"
case .speaker3: name = "speaker.3.fill"
}
return Image(systemName: name).font(.largeTitle)
}
}
Animatable modifier for SpeakerSymbol, required to let SwiftUI animation know that changed SpeakerSymbol value is able to animate
struct SpeakerModifier: AnimatableModifier {
var symbol: SpeakerSymbol
init(symbol: SpeakerSymbol) {
self.symbol = symbol
self.animating = Double(symbol.rawValue) // enum to Double
}
private var animating: Double // Double supports Animatable
var animatableData: Double { // required part of Animatable protocol
get { animating }
set { animating = newValue }
}
func body(content: Content) -> some View {
return SpeakerSymbol(rawValue: Int(animating))!.image // Double -> enum
}
}
Demo of usage
struct TestSpeakerModifier: View {
@State private var speaker: SpeakerSymbol = .speakerEmpty
var body: some View {
VStack {
Color.clear // << just holder area
.modifier(SpeakerModifier(symbol: speaker))
.frame(width: 60, height: 60, alignment: .leading)
Divider()
Button("Toggle") {
withAnimation { // animates between enum states
self.speaker =
(self.speaker == .speakerEmpty ? .speaker3 : .speakerEmpty)
}
}
}
}
}
SwiftUI Can't Animate Image Transitions
The current approach with SwiftUI is to use .transition()
, because .animation()
is being deprecated.
What is important to understand is that .transition()
is triggered when a view appears or disappears. Your view will not be completely re-drawn just because you change a @State
variable: in your code, the Image
changes but it always stays in the view.
One solution is to trigger the image to completely disappear and make a new one re-appear. The code below does that, depending on the state of showAnimation
. See that I only used .transition()
, but for a nice effect:
- it is asymmetric
- the
withAnimation()
closure wraps also the changing of thecurrentIndex
struct Example: View {
let photos = ["gear", "person", "person.2", "car", "leaf"]
@State private var currentIndex: Int = 0
@State private var showAnimation: Bool = true
private var scalingFactor: CGFloat = 0.5
var body: some View {
VStack {
Text("Image Detection")
.font(.system(size: 30))
.fontWeight(.heavy)
.padding(.top, 30)
Spacer()
if showAnimation {
image
} else {
image
}
Spacer()
HStack {
Button(action: {
withAnimation {
showAnimation.toggle()
if self.currentIndex >= self.photos.count {
self.currentIndex = self.currentIndex - 1
} else {
self.currentIndex = 0
}
}
}, label: {
Image(systemName: "arrowtriangle.backward.fill")
})
.padding()
.foregroundColor(Color.blue)
.font(.largeTitle)
Spacer()
Button(action: {
withAnimation {
showAnimation.toggle()
if self.currentIndex < self.photos.count - 1 {
self.currentIndex = self.currentIndex + 1
} else {
self.currentIndex = self.photos.count - 1
}
}
}, label: {
Image(systemName: "arrowtriangle.forward.fill")
})
.padding()
.foregroundColor(Color.blue)
.font(.largeTitle)
}
.padding(.horizontal, 50)
Spacer()
}//v
}//body
private var image: some View {
Image(systemName: photos[currentIndex])
.resizable()
.frame(width: 250, height: 250)
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
}
}
Transition animation not working in SwiftUI
The problem is that when views come and go in a ZStack, their "zIndex" doesn't stay the same. What is happening is that the when "showMessage" goes from true to false, the VStack with the "Hello World" text is put at the bottom of the stack and the yellow color is immediately drawn over top of it. It is actually fading out but it's doing so behind the yellow color so you can't see it.
To fix it you need to explicitly specify the "zIndex" for each view in the stack so they always stay the same - like so:
struct ContentView: View {
@State private var showMessage = false
var body: some View {
ZStack {
Color.yellow.zIndex(0)
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}.zIndex(1)
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
.zIndex(2)
}
}
}
}
Animation of image through layoutIfNeeded stopped working
I finally found a solution by adding a override func viewDidAppear
to the class and then adding the animation to that function. I guess all the images need to be properly loaded and placed before they can be animated or something like that?
How to animate a sequence of images using SwiftUI?
The accepted answer works very well, with the unfortunate issues mentioned by bhagyash ingale which makes it very hard to use. It would be useful if the specific methods of Image
could be reused via protocols or something. I have a very poor and maybe huge cannon for a fly solution for this, maybe it'll be easier in time but for now...
class LoadingTimer {
let publisher = Timer.publish(every: 0.1, on: .main, in: .default)
private var timerCancellable: Cancellable?
func start() {
self.timerCancellable = publisher.connect()
}
func cancel() {
self.timerCancellable?.cancel()
}
}
struct LoadingView: View {
@State private var index = 0
private let images = (0...7).map { UIImage(named: "Image-\($0).jpg")! }
private var timer = LoadingTimer()
var body: some View {
return Image(uiImage: images[index])
.resizable()
.frame(width: 100, height: 100, alignment: .center)
.onReceive(
timer.publisher,
perform: { _ in
self.index = self.index + 1
if self.index >= 7 { self.index = 0 }
}
)
.onAppear { self.timer.start() }
.onDisappear { self.timer.cancel() }
}
}
I don't like this but it gets the job done and relies on a Image
.
Related Topics
How to Convert String to Date Without Time in Swift 3
How to Build a Recursive Function in Swift to Return a String
Linking Pages in Swift Playgrounds [Xcode]
Switch Statement Where Value Is Int But Case Can Contain Array
Swiftsupport Folder Not Included in The IPA When Generating Build from Script
Uiview.Animatewithduration Not Animating
Swift Casting Generic to Optional with a Nil Value Causes Fatalerror
Firestore - Creating a Copy of a Collection
Leaks in Navigationview/List/Foreach with Dynamically Generated Views
Implementing Undo and Redo in a UItextview with Attributedtext
How to Enable a Button in Different Cases in Swift
Load a Spritekit Scene from Another Bundle
How to Convert an Array to List in Realm
Uitableviewcell Subclass Wrong Image in Cell or Old Image Bug
Swift Flatmap on Array with Elements Are Optional Has Different Behavior