Run ViewModel Method off the mainActor

I have a ViewModel and I added the @MainActor to it, but there is a function in the viewModel where it starts to fetch data. I wanted to run this method on the background thread to keep the mainActor available, after trying the only way where I managed to remove the warning in the strict mode and also run function in the background is by conforming the viewModel to sendable type. My question is the following is there a way to run a method in a class annotated with @MainActor off the mainThread ?

@Observable
@MainActor
final class LinkPreviewViewModel {
    var isLoading: Bool = true
    var isThereError: Bool = false
    var isShareSheetPresented: Bool = false
    var linkMetadata: LPLinkMetadata?
    var url: URL?

    init(url: URL?) {
        self.url = url
    }
}

extension LinkPreviewViewModel {
    func loadMetadata() async {
        do {
            guard let url = url else { return }
            isLoading.toggle()
            isThereError = false
            let metadataProvider = LPMetadataProvider()
            let metaData = try await metadataProvider.startFetchingMetadata(for: url) // runs // on the mainActor
            linkMetadata = metaData
            isLoading.toggle()
        } catch {
            isLoading = false
            isThereError = true
        }
    }
}

sendable solution :

@Observable
//@MainActor
final class LinkPreviewViewModel: @unchecked Sendable {
    var isLoading: Bool = true
    var isThereError: Bool = false
    var isShareSheetPresented: Bool = false
    var linkMetadata: LPLinkMetadata?
    var url: URL?

    init(url: URL?) {
        self.url = url
    }
}

extension LinkPreviewViewModel {
    func loadMetadata() async {
        do {
            guard let url = url else { return }
            await isLoading.asyncToggle()
            await isThereError.asyncSet(false)

            let metadataProvider = LPMetadataProvider()
            let metaData = try await metadataProvider.startFetchingMetadata(for: url) // runs // in the background

            await setLinkMetadata(metaData)
            await isLoading.asyncToggle()
        } catch {
            await isLoading.asyncSet(false)
            await isThereError.asyncSet(false)
        }
    }
}

@MainActor
extension LinkPreviewViewModel {
    func setLinkMetadata(_ metadata: LPLinkMetadata) async {
        linkMetadata = metadata
    }
}

You can use nonisolated but you need to safely access properties then

@MainActor
class Test {
    var a = 0

    func bar() {
        a = 2
    }
}

extension Test {
    nonisolated func foo() async {
        await bar()
    }
}

I would suggest extracting

 let metadataProvider = LPMetadataProvider()
            let metaData = try await metadataProvider.startFetchingMetadata(for: url) // runs // on the mainActor

to some other method (nonisolated or some other type) and just awaiting on this one part

1 Like

it worked thanks man.

extension LinkPreviewViewModel {
    func loadMetadata() async {
        do {
            guard let url = url else { return }
            isLoading.toggle()
            isThereError = false

            let metaData = try await fetchMetaData(url)

            linkMetadata = metaData
            isLoading.toggle()
        } catch {
            isLoading = false
            isThereError = true
        }
    }

    nonisolated private func fetchMetaData(_ url: URL) async throws -> LPLinkMetadata {
        let metadataProvider = LPMetadataProvider()
        return try await metadataProvider.startFetchingMetadata(for: url)
    }
}

I had a similar confusion while using MapKit. I needed to add an implementation of the MKLocalSearchCompleterDelegate protocol to the ViewModel. Later I found out that Sendable is basically unchanged or works on Actors.
So my solution was to put @MainActor before the init method and put @MainActor before each var (if it needs to be updated in the main thread).
I hope Swift gets better and better.