christophriepe
christophriepe

Reputation: 1673

Swift • How can I initiate a Background Task from a View's Initializer?

I am working on a SwiftUI App and encountered a problem.

I have a View (File Icon). A Data object is passed to this View. To create the File Icon, the Data must be converted into a PDFDocument and an Image must be rendered from the first Document Page.

Idea: Since this is a heavy task and there are Views that display multiple File Icons (e.g. 20 File Icons are created at the same time), the plan is to create the File Icon with some sort of placeholder, render the image in a Background Queue and, once its finished, change the displayed Image.

Code:

struct FileIcon: View {
    let data: Data
    @State private var image: UIImage?

    init(for data: Data) {
        self.data = data

        renderImage() // Initiate the Background Task
    }

    var body: some View {
        Group {
            if let safeImage = image {
                // Some Placeholder View
            } else {
                Image(uiImage: safeImage)
            }
        }
    }

    private func renderImage() {
        DispatchQueue.global(qos: .userInitiated).async {
            // Heavy Logic

            DispatchQueue.main.async {
                image = renderedImage
            }
        }
    }
}

Problem: As you can see, I am initiating the Background Task inside the View's Initializer. Xcode does not give any Error. However, when running this Code, the Placeholder View keeps being displayed and there is no switch to the actual rendered Image. when calling renderImage() inside .onAppear(), the placeholder get replaced by the rendered image after some time.

Question: How can I initiate a Background Task from a View's Initializer?

Upvotes: 0

Views: 1461

Answers (1)

Asperi
Asperi

Reputation: 257563

Here is a simple demo (as you want to do it in init, however, since I don't know your scenario, probably it is better to move it into separated method and call explicitly)

Tested with Xcode 12.5 / iOS 14.5

struct DemoBackgroundTaskView: View {

    class ViewModel: ObservableObject {
        @Published var finished = false

        init() {
            print("Init") // test called once

            // set up once performed background task
            DispatchQueue.global(qos: .background).async {

                sleep(10) // do your long task here

                DispatchQueue.main.async { [weak self] in
                    self?.finished = true
                }
            }
        }
    }

    @StateObject private var vm = ViewModel()

    var body: some View {
        Text(vm.finished ? "Done" : "Loading...")
    }
}

Upvotes: 3

Related Questions