How to animate a sequence of images using SwiftUI?

Viewed 5138

How can I animate a sequence of images (say Frame-1.png all the to Frame-6) using the SwiftUI framework?

I've tried creating an array of "images". Then I assigned the UIImage.animatedImage(with: images, duration: 1.0) method to a variable called "animatedImage" finally I tried Image(uiImage: animatedImage) in "body" of "ContentView.swift"


var images: [UIImage]! = [UIImage(named: "Sequence/frame-1")!,
                          UIImage(named: "Sequence/frame-2")!,
                          UIImage(named: "Sequence/frame-3")!,
                          UIImage(named: "Sequence/frame-4")!,
                          UIImage(named: "Sequence/frame-5")!,
                          UIImage(named: "Sequence/frame-6")!
]

let animatedImage : UIImage! = UIImage.animatedImage(with: images, duration: 1.0)

//////Then in the ContentView.swift I've tried this code:

struct ContentView : View {
    var body: some View {

        Image(uiImage: animatedImage)

    }
}

when I run the program it just shows the firs frame, but I expected an animation of the frames

4 Answers

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.

Version without Combine

import SwiftUI

struct AnimatingImage: View {
    let images: [Image]

    @ObservedObject private var counter = Counter(interval: 0.05)
        
    var body: some View {
        images[counter.value % images.count]
    }
}

private class Counter: ObservableObject {
    private var timer: Timer?

    @Published var value: Int = 0
    
    init(interval: Double) {
        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in self.value += 1 }
    }
}

You can use it

struct LoadingView: View {
    private let images = (1...20).map { String(format: "i_%02d", $0) }.map { Image($0) }
    
    var body: some View {
        AnimatingImage(images: images)
    }
}

I have three image assets that I want to animate sequentially and repeatedly named: thinking1, thinking2 and thinking3. Based on nightwill's answer this works in iOS 14.4

// images[counter.value % images.count] mod ensures counter never exceeds images.count

import SwiftUI

struct ThinkingView: View {
    
    let images: [Image] = (1...3).map { String(format: "thinking%01d", $0) }.map { Image($0) }
    
    @ObservedObject private var counter = Counter(interval: 0.1)
    
    var body: some View {
        images[counter.value % images.count]
    }
}

private class Counter: ObservableObject {
    private var timer: Timer?

    @Published var value: Int = 0
    
    init(interval: Double) {
        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in self.value += 1 }
    }
}

struct ThinkingView_Previews: PreviewProvider {
    static var previews: some View {
        ThinkingView()
    }
}

I figured out a pure SwftUI animation from image array : It is using an animatable modifier that handle the images to be displayed in place of a view (here an button with an image). When you tap a button its image animates. If you tap again image stop animating and default button image is displayed:

//
//  ImageArrayAnimationView
//
//  Created by Pitt Xav
//
import SwiftUI

struct ImageArrayAnimationView: View {
    // List of images
    private let images: [Image] = [
        Image(systemName: "icloud.and.arrow.up"),
        Image(systemName: "icloud.and.arrow.down"),
        Image(systemName: "cloud"),
        Image(systemName: "cloud.drizzle"),
        Image(systemName: "cloud.rain"),
        Image(systemName: "cloud.heavyrain")
    ]
    // Images and percent variable use for animating rain down
    private let imagesOrderDown = [2, 4, 5]
    @State var percentDown: Float = 0

    // Images and percent variable use for animating rain up
    @State var percentUp: Float = 0
    private let imagesOrderUp = [5, 3, 2]
    var body: some View {
        VStack {
            Spacer()
            HStack {
                Spacer()
                // tapping a button set percent to :
                // 0 = no animation (Animation.default)
                // 100 = animation with images
                Button {percentDown = percentDown < 50 ? 100 : 0} label: {
                    Image(systemName: "icloud.and.arrow.down")
                        .resizable()
                        .frame(width: 150, height: 150, alignment: .top)
                    .imageArrayAnimation(images: images, imagesOrder: imagesOrderDown, percent: percentDown)
                    .animation(percentDown <= 0 ? Animation.default : .linear(duration: 1).repeatForever(autoreverses: false), value: percentDown)
                    .border(.foreground, width: 2)
                    Spacer()
                }

                Button {percentUp = percentUp < 50 ? 100 : 0} label: {
                    Image(systemName: "icloud.and.arrow.up")
                        .resizable()
                        .frame(width: 150, height: 150, alignment: .top)
                    .imageArrayAnimation(images: images, imagesOrder: imagesOrderUp, percent: percentUp)
                    .animation(percentUp <= 0 ? Animation.default : .linear(duration: 1).repeatForever(autoreverses: false), value: percentUp)
                    .border(.foreground, width: 2)
                    Spacer()
                }
            }
            .frame(width: 100, height: 100, alignment: .center)
            Spacer()
        }
        
    }
}

struct ImageAnimation: AnimatableModifier {
    // Animation through th percent property
    // percent is converted into a position in an index array
    // this index value enable to choose whch image in the array
    // to display
    var animatableData: Float {
        get {
            percent
        }
        set {
            percent = newValue
        }
    }
    var images: [Image]
    var imgesOrder: [Int]
    var percent: Float
    var currentImageIndex: Int {
        if percent > 0 {
            let index = Int((percent / 100) *  Float(imgesOrder.count))
            print("% \(percent) \(index)")
            return imgesOrder[min(index, imgesOrder.count-1)]
        } else {
            return 0
        }
    }
    
    func body(content: Content) -> some View {
        ZStack(alignment: .top) {
            // for the animation to work content and image
            // needs to always be there
            // opacity enable to choose what to actually display
            content
                .opacity(percent == 0 ? 1 : 0)
            images[currentImageIndex]
                .resizable()
                .scaledToFit()
                .opacity(percent == 0 ? 0 : 1)
            
        }
    }
}

// extension to have mnamed modifier
extension View {
    func imageArrayAnimation(images: [Image], imagesOrder: [Int], percent: Float) -> some View {
        self.modifier(ImageAnimation(images: images, imgesOrder: imagesOrder, percent: percent))
    }
}

struct ImageArrayAnimationView_Previews: PreviewProvider {
    static var previews: some View {
        ImageArrayAnimationView()
    }
}
Related