How to import large data from Server and save it to Swift Data

Here’s the situation: • You’re downloading a huge list of data from iCloud. • You’re saving it one by one (sequentially) into SwiftData. • You don’t want the SwiftUI view to refresh until all the data is imported. • After all the import is finished, SwiftUI should show the new data.

The Problem

If you insert into the same ModelContext that SwiftUI’s @Environment(.modelContext) is watching, each insert may cause SwiftUI to start reloading immediately.

That will make the UI feel slow, and glitchy, because SwiftUI will keep trying to re-render while you’re still importing.

How to achieve this in Swift Data ?

Do you know how much data is expected to be downloaded? If so, you could ensure the view doesn't update until the right amount of data is received and stored.

If you don't know the quantity of data, you could add a Bool and toggle it when the data loading starts, then toggle it when the data has finished loading, so the View is only refreshed when the last of the data is stored.

Another way would be to disable the UI so the user can't interact with it until the data has finished loading. The UI would update in the background, but the user wouldn't experience a slow UI as they can't interact with it anyway. You could put a partially-transparent View with a ProgressView in it at the top of a ZStack (which would be at the bottom of the ZStack in the code...) and hide it when you're done. I'll show you how I do it below.

There's a few ways of doing it, but I don't think there's a built-in way to achieve this. But then, I'm not a genius on SwiftData, so...

Using a blocking view:

@State private var loadingData: Bool = false

var body: some View {
    ZStack {
        // ... UI goes here
        // ...
        BlockingView()
            .additional().hidden(loadingData)  // Hide the view when loading data == true
    }
}

struct BlockingView: View {
	var body: some View {
		ZStack {
			Rectangle()
				.fill(Color.black.opacity(0.2))  // Partially-transparent, but doesn't have to be

			ProgressView(label: {
				Label(title: { Text("Please Wait") }, icon: { Image(systemName: "exclamationmark.circle") })
			})
			.frame(width: 240, height: 100)
			.background(.ultraThickMaterial)
			.clipShape(RoundedRectangle(cornerRadius: 24))
			.shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 0)
		}
		.ignoresSafeArea()
	}
}

/*
This modifier is required because you can't conditionally hide a view with `.hidden()` but on the plus side, you can use this in tons of places, and add new modifiers like conditional shadows etc.
*/

public struct Additional<Content> {
	public let content: Content

	public init(_ content: Content) {
		self.content = content
	}
}

extension View {
	var additional: Additional<Self> { Additional(self) }
}

extension Additional where Content: View {
	@ViewBuilder func hidden(_ hide: Bool) -> some View {
		if(hide) {
			content.hidden()

		} else {
			content
		}
	}
}

One way would be to perform the import in the background in a @ModelActor. Something like this:

@ModelActor
actor ImportService {
    func import(data: [Data]) throws {
        for date in data {
            let model = Model(/* ... */)
            modelContext.insert(model)
        }
        try modelContext.save()
    }
}

This way the import will not block the UI, and the imported data will only be visible in the UI after modelContext.save() is called.

I haven't used SwiftData in a real-world app yet, however, so do be careful. For example I don't know if SwiftData will keep the models in memory until save() is called, so you might have to save more frequently. But even then, the UI will not update for every newly inserted model but only when you choose.

How to import large data from Server and save it to Swift Data
 
 
Q