Skip to content

NSManagedObject Mapping

Alexis Bridoux edited this page Jan 22, 2021 · 6 revisions

Reading

To provide relevant examples, here are two entities to work with

class PlayerEntity: NSManagedObject {
    @NSManaged var name: String?
    @NSManaged var score: Int32
    @NSManaged var avatar: Data?

    @NSManaged var games: NSSet?
}

class GameEntity: NSManagedObject {
    @NSManaged var duration: Double
    @NSManaged var player: PlayerEntity?
}

And their associated DatabaseModels

extension PlayerEntity: DatabaseEntity {}
extension GameEntity: DatabaseEntity {}

struct Player: DatabaseModel {
    let name = Field(\.name)
    let score = Field(\.score)
    let avatar = UIImage(\.avatar, as: UIImage.self)
    
    let games = Children(\.games, as: Game.self)
    // ... 
}

struct Game: DatabaseModel {
    let duration = Field(\.duration)
    let player = Parent(\.player, as: Player.self)
    // ... 
}

Current value

To read a current value in the database model, you can call its current(:) function. This allows to read a field, a parent value or a child value when exposed by the related model.

let player: Player
let game: Game

player.current(\.name) // String?
player.current(\.score) // Double
player.current(\.avatar) // UIImage?
player.current(\.games) // [Game]

game.current(\.player) // Player?
game.current(\.player, \.name) // String?

Map current functions

The library offers map functions using those current functions.

var players: [Players]

// Fields
players.mapCurrent(\.name) // [String?]
players.compactMapCurrent(\.name) // [String]

// Children
players.flatMapCurrent(\.games) // [Games]

Published value

You can also subscribe to a value rather than reading it.

let player: Player

func setupSubscriptions() {
    player.publisher(for: \.name)
        .assign(to: name.label.text, on self)
        .store(in: &subscriptions)
        
    player.publisher(for: \.avatar)
        .assign(to: avatarView.image, on: self)
        .store(in: &subscriptions)
        
    player.publisher(for: \.games, sortedBy: .ascending(\.duration))
        .assign(to: avatarView.image, on: self)
        .store(in: &subscriptions)
}

Writing

To set the Player value, you can either use the assign(:to:) function or the publisher assign(to:on:) when using a publisher that emits the value to assign.

player.assign(newName, to: \.name)

Just(newName)
    .assign(to: \.name, on: player)
    .store(in: &subscriptions)

Validating

Before assigning a value, you can validate it with the validate(:for:) function or the publisher tryValidate(for:on).

try player.validate(newName, for: \.name)

Just(newName)
    .tryValidate(for: \.name, on: player)
    .sink(receiveCompletion:  { completion in // can finish with a failure
    //...
    }, receiveValue: { _ in })
    .store(in: &subscriptions)

Optionally, it's possible to use validateAndAssign(to:) and to chain both tryValidate(for:on:) and assign(to:on:receiveCompletion:) publishers.

try player.validateAndAssign(newName, to: \.name)

Just(newName)
    .tryValidate(for: \.name, on: player)
    .assign(to: \.name, on: player) { completion in
        if case let .failure(error) = completion {
            // display a relevant error message to the user
    }
    .store(in: &subscriptions)
        

You can learn more about the validation in the Validation section.

Relationships

To map a relationship between two entities, you can use:

  • Children to map a 1:N relationship (from the parent point of view)
  • OrderedChildren to map an ordered 1:N relationship (from the parent point of view)
  • Parent to map a N:1 relationship (from the child point of view)
  • Sibling to map a 1:1 relationship

For example, if the player entity has a 1:N relationship with a GameEntity entity mapped with a Game model, you can declare

let games = Children(\.games, as: Game.self)

It's then possible to use the publisher player.publisher(for: \.games) which will emit a Game array, or to call relevant modifications CRUD functions.

// Subscribe to the player games, sorted by their date then by their duration
player.publisher(for: \.games, sortedBy: .ascending(\.date), .descending(\.duration))

// Add a `Game` model to the player `games` relationship
let game = Game(...)
try player.add(game, to: \.games)

NSFetchedResultsController

Once a DatabaseModel is declared to map an entity, you can subscribe to its fetched results updates with the static updatePublisher(sortingBy:) function. The subscription will hold a NSFetchedResultsController and the emitted values will be an array of DatabaseModel.

Player.updatePublisher(sortedBy: \.ascending(.name)) // emits [Player]

When working with a diffable data source, this can be really useful, especially as a DatabaseModel is hashable.