3

I have a MovieData class that conforms to SwiftData model as @Model such as:

@Model
class MovieData: Codable{
   var ....
}

We also have a web service struct that returns a MovieData array from an API:

struct WebService {
    
    func getMoviesArr() async -> [MovieData]{
        do {
            let data = try await getDataFromURL(callType: .movies)
            return try JSONDecoder().decode([MovieData].self, from: data)
        } catch {
            print("Failed to fetch movies arr: \(error)")
            return []
        }
    }
}

The issue I am facing is, while importing the movies list to save them using SwiftData on a background actor created by @ModelActor the error is obviously Non-sendable type '[MovieData]' returned by implicitly asynchronous call to nonisolated function cannot cross actor boundary

As SwiftData models cannot conform to the Sendable protocol I cannot use a seperate class/struct to better organize the code and make more modular.

Calling the struct and the method from my actor:

@ModelActor
actor UpdateActor: Sendable{

    func downloadAndWriteMovies(context: ModelContext) async -> [MovieData]{
        let webService = WebService()

        let moviesArr = await webService.getMoviesArr() \\ <- Creating the error
            
        context.insert(moviesArr)

        return moviesArr
    }

}

Any ideas how to go around it instead of putting all the code in our @ModelActor or create extensions for that actor?

4
  • One solution is to just save the data and not return it and then fetch it afterwards. Just saw the edit, why is the actor sendable? Commented Aug 4, 2024 at 20:40
  • @JoakimDanielson the issue with that approach is that I would like to modify the data before inserting it, which would be possible with that approach but seems very redundant, especially when working with very large lists/data structures Commented Aug 4, 2024 at 20:42
  • Well you can’t pass model objects outside of an actor’s boundaries as the message says. Two options, merge the actor and the api functionality into one type (actor) so nothing needs to be passed across any boundaries or decode into a separate struct type that can be safely passed around. Personally I have opted for the latter and I am working with struct types and none of my (relevant) model classes conforms to Codable. Commented Aug 4, 2024 at 20:50
  • 2
    As Joakim Danielson says, don't decode to your @Model class. Write some Sendable structs that contains all the information you need to create your @Model class, and decode to those structs instead. These are not "redundant" - this is how you make your code thread safe. Commented Aug 5, 2024 at 1:20

1 Answer 1

2

Sweeper's comment is good advice: create a separate "data transfer object" struct for tasks like encoding/decoding, and define convenience constructors to swap between the DTO structs and SwiftData models.

@Model
final class MovieData {
    var title: String
    var length: Int

    init(_ dto: MovieDTO) {
        title = dto.title
        length = dto.length
    }
}

struct MovieDTO: Sendable, Codable {
    var title: String
    var length: Int

    init(_ movie: MovieData) {
        title = movie.title
        length = movie.length
    }
}

When you decode in getMoviesArr, decode to MovieDTO and return an array of those objects:

struct WebService {
    func getMoviesArr() async -> [MovieDTO] {
        ...
        return try JSONDecoder().decode([MovieDTO].self, from: data)
        ...
    }
}

And convert them to Models within the actor:

@ModelActor
actor UpdateActor: Sendable {
    func downloadAndWriteMovies(context: ModelContext) async -> [MovieDTO] {
        let webService = WebService()

        let moviesArr = await webService.getMoviesArr()

        let movieModels = moviesArr.map(MovieData.init)
        context.insert(movieModels)

        return moviesArr // No longer have direct access to models...
    }
}

Mapping DTOs to Existing Models

The above strategy works when creating new models, but matching the DTO structs to existing models down the line is tricky since (again) SwiftData Models are non-Sendable and can't be passed between actors.

In my hobby project, I went the route of tracking the Model's PersistentIdentifier in a field on the DTO. The persistent identifier is a struct (Sendable!), and it can be used to unambiguously fetch the matching Model from the database later on.

For the movie example, you could do:

// Removed Codable for brevity.
// Use CodingKeys if needed to ignore the `id` field when decoding.
struct MovieDTO: Sendable {
    var id: PersistentIdentifier

    var title: String
    var length: Int

    init(_ movie: MovieData) {
        id = movie.persistentModelID

        title = movie.title
        length = movie.length
    }
}

And in the UpdateActor, call context.save() after insertion (to ensure the persistent ID is set) and add them to the DTOs before returning them.

There are certainly more robust ways to formalize this Persistent ID pattern, but the snippets above should get you started at minimizing redundancy between your models and DTOs.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.