How to animate a sequence of images using SwiftUI?
Asked Answered
K

5

10

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

Kara answered 23/6, 2019 at 9:44 Comment(0)
C
7

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.

Cornelius answered 1/9, 2019 at 21:27 Comment(4)
Nice answer, loved two things in your answer: 1- use of .map method, 2- use of Publisher from Combine framework. btw ,I didn't get it why did you write private var image = Image(uiImage: images[index]) (It's not needed I guess)Kara
Ah you’re right. It’s from the previous tries and forgot to remove it. I’ll update the answer.Conant
This seems promising. I am getting the error Cannot find type 'Cancellable' in scope - anything library that needs to be loaded for this?Cleopatra
You need to import combine.Conant
C
8

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)
    }
}
Cchaddie answered 3/7, 2020 at 17:53 Comment(3)
Isn't ObservableObject part of Combine?Calculation
In this case ObservableObject is a part of SwiftUI, because @ObservedObject should be ObservableObject.Cchaddie
@ObservedObject should be @StateObject. In the case of @ObservedObject, whenever the counter value updates, the view is redrawn and reinitializes the Counter object making the value 0 again so the image never animates and stays on the first frame.Juliannjulianna
C
7

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.

Cornelius answered 1/9, 2019 at 21:27 Comment(4)
Nice answer, loved two things in your answer: 1- use of .map method, 2- use of Publisher from Combine framework. btw ,I didn't get it why did you write private var image = Image(uiImage: images[index]) (It's not needed I guess)Kara
Ah you’re right. It’s from the previous tries and forgot to remove it. I’ll update the answer.Conant
This seems promising. I am getting the error Cannot find type 'Cancellable' in scope - anything library that needs to be loaded for this?Cleopatra
You need to import combine.Conant
P
1

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()
    }
}
Prolusion answered 15/2, 2022 at 16:20 Comment(0)
J
0

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()
    }
}
Jog answered 25/2, 2021 at 16:21 Comment(0)
F
0

A simple pure SwiftUI version using just concurrency

struct LoadingView: View {
    let images: [UIImage]
    let interval: Float // Interval in seconds
    let width: CGFloat?
    let height: CGFloat?
    @State var currentIndex = 0
    @State var task: Task<Void, Never>?

    var body: some View {
        Image(uiImage: images[currentIndex])
            .resizable()
            .frame(width: width, height: height)
            .onAppear { next() }
            .onDisappear { task?.cancel() }
    }

    @MainActor
    func next() {
        task?.cancel()
        task = Task {
            try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000))
            if !Task.isCancelled {
                currentIndex += 1
                if currentIndex >= images.count {
                    currentIndex = 0
                }
                next()
            }
        }
    }
}

Usage:

LoadingView(
    images: [
        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")!
    ],
    interval: 0.03,
    width: 100,
    height: 100
)
Forthright answered 15/7, 2024 at 14:54 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.