UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Image lag

Forums > SwiftUI

1) I'm a Beginner so for you my code can be full of bad practice and things done in wrong way. 2) My language is Italian so i try to explain my problem better i can

I'm trying to create a simple Image Slideshow in SwiftUI and i found on stackoverflow this code that work very well.

This is my model

import SwiftUI

class AnimalModel: ObservableObject, Codable {

    var id = UUID()
    var name: String
    var image: Data?

    var realImg : UIImage {
        let realImg = UIImage(data: image ?? Data())
        return realImg ?? #imageLiteral(resourceName: "catImage")
    }

    init(name: String, image: Data?) {
        self.name = name
        self.image = image
    }
}

extension AnimalModel: Equatable {
    static func == (lhs: AnimalModel, rhs: AnimalModel) -> Bool {
        lhs === rhs
    }
}

extension AnimalModel: Hashable, Identifiable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

This where i store all the entry and manage the data

import SwiftUI

class DataManager: ObservableObject {

    static let shared = DataManager()

    var storage: [AnimalModel] = [] {
        didSet {
            objectWillChange.send()
        }
    }

    typealias Storage = [AnimalModel]

    var filePath : String = ""

    init() { loadData() }

    func loadData() {

        filePath = documentsFolder() + "/animal.plist"

        if FileManager.default.fileExists(atPath: filePath) {

            do {

                let data = try Data(contentsOf: URL(fileURLWithPath: filePath))

                let decoder = PropertyListDecoder()

                storage = try decoder.decode(Storage.self, from: data)
            } catch {

                debugPrint(error.localizedDescription)
            }

        } else {

            let harry = AnimalModel(name: "Harry", image: #imageLiteral(resourceName: "img1").pngData())
            let paco = AnimalModel(name: "Paco", image: #imageLiteral(resourceName: "img3").pngData())

            storage = [harry, paco]

            save()
        }
    }

    func save() {

        let encoder = PropertyListEncoder()
        encoder.outputFormat = .xml

        do {

            let data = try encoder.encode(storage)

            try data.write(to: URL(fileURLWithPath: filePath))
        } catch {

            debugPrint(error.localizedDescription)
        }
    }

    func addNewAnimal(name: String, image: UIImage?) {
        let newAnimal = AnimalModel(name: name, image: image?.pngData())
        storage.append(newAnimal)
        print(storage)
    }

    func documentsFolder() -> String {
        let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        debugPrint(paths[0])
        return paths[0]
    }

    let example = AnimalModel(name: "Harry", image: #imageLiteral(resourceName: "img2").pngData())

}

In this view, the forEach loop the storage array and found all images.

import SwiftUI

struct AnimalSelection: View {

    @State var index = 0

    @State var isAddPresented: Bool = false
    @ObservedObject var dataManager: DataManager = DataManager.shared

    // 5 images stored in the assetts catalog work fine
    var images = ["img1","img2","img3","img4","img5"]

    var body: some View {

            VStack(spacing: 20) {
                PagingView(index: $index, maxIndex: dataManager.storage.count - 1) {
                    ForEach(dataManager.storage , id: \.self) { animal in
                        Image(uiImage: animal.realImg)
                            .resizable()
                            .scaledToFill()

                    }
                }
                .aspectRatio(3/2, contentMode: .fit)
                .clipShape(RoundedRectangle(cornerRadius: 20))
                Button(action: { self.isAddPresented = true }) {
                    Image(systemName: "plus.circle")
                        .resizable()
                        .foregroundColor(Color(red: 91 / 255, green: 185 / 255, blue: 171 / 255))
                        .frame(width: 30, height: 30)
                }.sheet(isPresented: $isAddPresented, content: {
                    AddScreen(isAddShow: self.$isAddPresented)
                })
            }
            .padding(.leading, 15)
            .padding(.trailing, 15)
        }
    }

There are two problems 1) When i swipe from one image to another, the transition lag. 2) Some images are upside down.

Check this video on youtube where i show the problem: https://youtu.be/k8d_RPEVNDo

I've tried to pass in the forEach loop an array of 5 images (stored in the assets catalog) and all work fine.

Can anyone explain how can i solve the problem?

3      

Hi!

I wasn't able to reproduce the amount of lag you showed on my phone, but I believe your issue is caused by this property:

var realImg : UIImage {
    let realImg = UIImage(data: image ?? Data())
    return realImg ?? #imageLiteral(resourceName: "catImage")
}

The reason for that is that every time you start to swipe, this property computes the image from the data again. That is a very expensive operation, and running it repeatedly every time you start viewing the image is what slows the app down. So the solution is caching. There are actually two ways you could go about doing it.

The first one is simple. Just change your computed realImg property into a lazy closure-based var. This loads the image once when the user first sees it, and then when they swipe back to it it uses the previously loaded version:

class AnimalModel: ObservableObject, Codable {
    var id = UUID()
    let name: String
    let image: Data?

    lazy var realImg: UIImage = {
        return UIImage(data: image ?? Data()) ?? #imageLiteral(resourceName: "catImage")
    }()

    init(name: String, image: Data?) {
        self.name = name
        self.image = image
    }
}

The issue with this approach is that since it's loading the image when the user first swipes to it there might be a stutter when the user first opens the image.


The second option is to compute your UIImage when you create the model class. This means the heavy work is done at the start, and then when swiping all is smooth. This is a bit more complex because UIImage doesn't conform to Codable, so I wrote a wrapper that does work with Codable and that holds both the Data and the UIImage but only encodes the Data.

class AnimalModel: ObservableObject, Codable {
    var id = UUID()
    let name: String
    let imageData: ImageData

    init(name: String, image: Data?) {
        self.name = name
        self.imageData = ImageData(image)
    }
}

struct ImageData: Codable {
    let data: Data?
    let image: UIImage

    init(_ data: Data?) {
        self.data = data
        image = Self.getImage(from: data)
    }

    static func getImage(from data: Data?) -> UIImage {
        return UIImage(data: data ?? Data()) ?? #imageLiteral(resourceName: "catImage")
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        data = try container.decode((Data?).self)
        image = Self.getImage(from: data)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(data)
    }
}

You'll also need to change the Image inside your PageView to

Image(uiImage: animal.imageData.image)

This approach also has a big drawback though, and that is that if you plan on having a lot of images, loading them all at the start will cause a lag on app launch, which you don't want. So for large numbers of images I suggest using the first solution.

4      

@jakcharvat Thank you, your solution solve my problem.

4      

Awesome, happy to help :)

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.