SwiftData ordered many-to-many relationship

72 Views Asked by At

How to have Watchlist.movies be ordered like whatever array I assign? I cannot add an order property to Movie because it can appear in multiple Watchlists.

@Model
final class Watchlist {
    var movies = [Movie]()
    
    init() {}
}

@Model
final class Movie {
    @Relationship(inverse: \Watchlist.movies)
    var watchlists = [Watchlist]()
    
    init() {}
}

Not a duplicate of SwiftData IOS 17 Array in random order? because that deals with the "why" and doesn't address many-to-many relationships.

Considered solutions

This seems a like a hack and requires saving because PersistentIdentifier is temporary until the first save:

@Model
final class Watchlist {
    var movies: [Movie] {
        get {
            moviesOrder.compactMap{ modelContext?.model(for: $0) as? Movie }
        }
        set {
            moviesOrder = newValue.map{ $0.persistentModelID }
        }
    }
    private var moviesOrder = [PersistentIdentifier]()
    
    init() {}
}

But it's simpler than creating a separate MovieResult model with an order property and sorting on every get. Which is what the OrderedRelationship lib seems to do.

1

There are 1 best solutions below

0
John On

Here's the solution with an intermediary model. It seems like the best type of solution so far but I don't like it much because it requires an additional model and sort on every get.

@Model
final class Watchlist {
    @Relationship(deleteRule: .cascade, inverse: \WatchlistMovie.watchlist)
    private var moviesUnordered = [WatchlistMovie]()
    
    var movies: [Movie] {
        get {
            moviesUnordered
                .sorted(using: SortDescriptor(\.order))
                .map{ $0.movie }
        }
        set {
            moviesUnordered.forEach{ modelContext!.delete($0) }
            moviesUnordered = newValue.enumerated().map{ offset, movie in
                WatchlistMovie(self, movie, order: offset)
            }
        }
    }
    
    init() {}
}

/// An intermediary between `Watchlist` and `Movie` used to retain insertion
/// order.
@Model
final class WatchlistMovie {
    var watchlist: Watchlist!
    var movie: Movie!
    let order: Int
    
    init(_ watchlist: Watchlist, _ movie: Movie, order: Int) {
        self.order = order
        
        watchlist.modelContext!.insert(self)
        self.movie = movie
        self.watchlist = watchlist
    }
}


@Model
final class Movie {
    let name: String
    @Relationship(inverse: \WatchlistMovie.movie)
    var watchlistItems = [WatchlistMovie]()
    
    init(_ name: String) {
        self.name = name
    }
}