ValueObservation

public struct ValueObservation<Reducer> where Reducer : ValueReducer

ValueObservation tracks changes in the results of database requests, and notifies fresh values whenever the database changes.

For example:

let observation = ValueObservation.tracking { db in
    try Player.fetchAll(db)
}

let cancellable = try observation.start(
    in: dbQueue,
    onError: { error in ... },
    onChange: { players: [Player] in
        print("Players have changed.")
    })
  • Default is false. Set this property to true when the observation requires write access in order to fetch fresh values. Fetches are then wrapped inside a savepoint.

    Don’t set this flag to true unless you really need it. A read/write observation is less efficient than a read-only observation.

    Declaration

    Swift

    public var requiresWriteAccess: Bool
  • Returns a shared value observation that shares a single underlying database observation for all subscriptions, and thus spares database resources.

    Experimental

    For example:

    let sharedObservation = ValueObservation
        .tracking { db in try Player.fetchAll(db) }
        .shared(in: dbQueue)
    

    The sharing only applies if you start observing the database from the same SharedValueObservation instance:

    // NOT shared
    let cancellable1 = ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...)
    let cancellable2 = ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...)
    
    // Shared
    let sharedObservation = ValueObservation.tracking { db in ... }.shared(in: dbQueue)
    let cancellable1 = sharedObservation.start(...)
    let cancellable2 = sharedObservation.start(...)
    

    By default, fresh values are dispatched asynchronously on the main queue. You can change this behavior by providing a scheduler. For example, .immediate notifies all values on the main queue as well, and the first one is immediately notified when the start() method is called:

    let sharedObservation = ValueObservation
        .tracking { db in try Player.fetchAll(db) }
        .shared(
            in: dbQueue,
            scheduling: .immediate) // <-
    
    let cancellable = try sharedObservation.start(
        onError: { error in ... },
        onChange: { players: [Player] in
            print("fresh players: \(players)")
        })
    // <- here "fresh players" is already printed.
    

    Note that the .immediate scheduler requires that the observation is subscribed from the main thread. It raises a fatal error otherwise.

    A shared observation starts observing the database as soon as it is subscribed. You can choose if database observation should stop, or not, when its number of subscriptions drops down to zero, with the extent parameter. See SharedValueObservationExtent for available options.

    Declaration

    Swift

    public func shared(
        in reader: DatabaseReader,
        scheduling scheduler: ValueObservationScheduler = .async(onQueue: .main),
        extent: SharedValueObservationExtent = .whileObserved)
    -> SharedValueObservation<Reducer.Value>

    Parameters

    reader

    A DatabaseReader.

    scheduler

    A Scheduler. By default, fresh values are dispatched asynchronously on the main queue.

    extent

    The extent of the shared database observation.

    Return Value

    A SharedValueObservation

Starting Observation

  • Starts the value observation in the provided database reader (such as a database queue or database pool).

    The observation lasts until the returned cancellable is cancelled or deallocated.

    For example:

    let observation = ValueObservation.tracking { db in
        try Player.fetchAll(db)
    }
    
    let cancellable = try observation.start(
        in: dbQueue,
        onError: { error in ... },
        onChange: { players: [Player] in
            print("fresh players: \(players)")
        })
    

    By default, fresh values are dispatched asynchronously on the main queue. You can change this behavior by providing a scheduler. For example, .immediate notifies all values on the main queue as well, and the first one is immediately notified when the start() method is called:

    let cancellable = try observation.start(
        in: dbQueue,
        scheduling: .immediate, // <-
        onError: { error in ... },
        onChange: { players: [Player] in
            print("fresh players: \(players)")
        })
    // <- here "fresh players" is already printed.
    

    Note that the .immediate scheduler requires that the observation is subscribed from the main thread. It raises a fatal error otherwise.

    Declaration

    Swift

    public func start(
        in reader: DatabaseReader,
        scheduling scheduler: ValueObservationScheduler = .async(onQueue: .main),
        onError: @escaping (Error) -> Void,
        onChange: @escaping (Reducer.Value) -> Void) -> DatabaseCancellable

    Parameters

    reader

    A DatabaseReader.

    scheduler

    A Scheduler. By default, fresh values are dispatched asynchronously on the main queue.

    onError

    A closure that is provided eventual errors that happen during observation

    onChange

    A closure that is provided fresh values

    Return Value

    a DatabaseCancellable

Debugging

  • Performs the specified closures when ValueObservation events occur.

    Declaration

    Swift

    public func handleEvents(
        willStart: (() -> Void)? = nil,
        willFetch: (() -> Void)? = nil,
        willTrackRegion: ((DatabaseRegion) -> Void)? = nil,
        databaseDidChange: (() -> Void)? = nil,
        didReceiveValue: ((Reducer.Value) -> Void)? = nil,
        didFail: ((Error) -> Void)? = nil,
        didCancel: (() -> Void)? = nil)
    -> ValueObservation<ValueReducers.Trace<Reducer>>

    Parameters

    willStart

    A closure that executes when the observation starts. Defaults to nil.

    willFetch

    A closure that executes when the observed value is about to be fetched. Defaults to nil.

    willTrackRegion

    A closure that executes when the observation starts tracking a database region. Defaults to nil.

    databaseDidChange

    A closure that executes after the observation was impacted by a database change. Defaults to nil.

    didReceiveValue

    A closure that executes on fresh values. Defaults to nil.

    didFail

    A closure that executes when the observation fails. Defaults to nil.

    didCancel

    A closure that executes when the observation is cancelled. Defaults to nil.

    Return Value

    A ValueObservation that performs the specified closures when ValueObservation events occur.

  • Prints log messages for all ValueObservation events.

    Declaration

    Swift

    public func print(
        _ prefix: String = "",
        to stream: TextOutputStream? = nil)
    -> ValueObservation<ValueReducers.Trace<Reducer>>

Asynchronous Observation

Publishing Observed Values

  • Creates a publisher which tracks changes in database values.

    For example:

    let observation = ValueObservation.tracking { db in
        try Player.fetchAll(db)
    }
    let cancellable = observation
        .publisher(in: dbQueue)
        .sink(
            receiveCompletion: { completion in ... },
            receiveValue: { players: [Player] in
                print("fresh players: \(players)")
            })
    

    By default, fresh values are dispatched asynchronously on the main queue. You can change this behavior by by providing a scheduler.

    For example, .immediate notifies all values on the main queue as well, and the first one is immediately notified when the publisher is subscribed:

    let cancellable = observation
        .publisher(
            in: dbQueue,
            scheduling: .immediate) // <-
        .sink(
            receiveCompletion: { completion in ... },
            receiveValue: { players: [Player] in
                print("fresh players: \(players)")
            })
    // <- here "fresh players" is already printed.
    

    Note that the .immediate scheduler requires that the publisher is subscribed from the main thread. It raises a fatal error otherwise.

    Declaration

    Swift

    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
    public func publisher(
        in reader: DatabaseReader,
        scheduling scheduler: ValueObservationScheduler = .async(onQueue: .main))
    -> DatabasePublishers.Value<Reducer.Value>

    Parameters

    reader

    A DatabaseReader.

    scheduler

    A Scheduler. By default, fresh values are dispatched asynchronously on the main queue.

    Return Value

    A Combine publisher

  • Returns a ValueObservation which notifies the results of calling the given transformation which each element notified by this value observation.

    Declaration

    Swift

    public func map<T>(_ transform: @escaping (Reducer.Value) -> T)
    -> ValueObservation<ValueReducers.Map<Reducer, T>>
  • Returns a ValueObservation which only publishes elements that don’t match the previous element, as evaluated by a provided closure.

    Declaration

    Swift

    public func removeDuplicates(by predicate: @escaping (Reducer.Value, Reducer.Value) -> Bool)
    -> ValueObservation<ValueReducers.RemoveDuplicates<Reducer>>

Creating ValueObservation

  • Creates an optimized ValueObservation that notifies the values returned by the fetch function whenever a database transaction changes them.

    The optimization only kicks in when the observation is started from a DatabasePool: fresh values are fetched concurrently, and do not block database writes.

    Precondition

    The fetch function must perform requests that fetch from a single and constant database region. The tracked region is made of tables, columns, and, when possible, rowids of individual rows. All changes that happen outside of this region do not impact the observation.

    For example:

    // Tracks the full 'player' table
    let observation = ValueObservation.trackingConstantRegion { db -> [Player] in
        try Player.fetchAll(db)
    }
    
    // Tracks the row with id 42 in the 'player' table
    let observation = ValueObservation.trackingConstantRegion { db -> Player? in
        try Player.fetchOne(db, key: 42)
    }
    
    // Tracks the 'score' column in the 'player' table
    let observation = ValueObservation.trackingConstantRegion { db -> Int? in
        try Player.select(max(Column("score"))).fetchOne(db)
    }
    
    // Tracks both the 'player' and 'team' tables
    let observation = ValueObservation.trackingConstantRegion { db -> ([Team], [Player]) in
        let teams = try Team.fetchAll(db)
        let players = try Player.fetchAll(db)
        return (teams, players)
    }
    

    When you want to observe a varying database region, make sure you use the ValueObservation.tracking(_:) method instead, or else some changes will not be notified.

    For example, consider those three observations below that depend on some user preference. They all track a varying region, and must use ValueObservation.tracking(_:):

    // Does not always track the same row in the player table.
    let observation = ValueObservation.tracking { db -> Player? in
        let pref = try Preference.fetchOne(db) ?? .default
        return try Player.fetchOne(db, key: pref.favoritePlayerId)
    }
    
    // Only tracks the 'user' table if there are some blocked emails.
    let observation = ValueObservation.tracking { db -> [User] in
        let pref = try Preference.fetchOne(db) ?? .default
        let blockedEmails = pref.blockedEmails
        return try User.filter(blockedEmails.contains(Column("email"))).fetchAll(db)
    }
    
    // Sometimes tracks the 'food' table, and sometimes the 'beverage' table.
    let observation = ValueObservation.tracking { db -> Int in
        let pref = try Preference.fetchOne(db) ?? .default
        switch pref.selection {
        case .food: return try Food.fetchCount(db)
        case .beverage: return try Beverage.fetchCount(db)
        }
    }
    

    Declaration

    Swift

    public static func trackingConstantRegion<Value>(
        _ fetch: @escaping (Database) throws -> Value)
    -> ValueObservation<ValueReducers.Fetch<Value>>

    Parameters

    fetch

    A function that fetches the observed value from the database.

  • Creates a ValueObservation that notifies the values returned by the fetch function whenever a database transaction has an impact on the given regions.

    The tracked region is not automatically inferred from the requests performed in the fetch function.

    For example:

    // Tracks the full database
    let observation = ValueObservation.tracking
        region: .fullDatabase,
        fetch: { db in ... })
    
    // Tracks the full 'player' table
    let observation = ValueObservation.tracking
        region: Player.all(),
        fetch: { db in ... })
    
    // Tracks the row with id 42 in the 'player' table
    let observation = ValueObservation.tracking
        region: Player.filter(id: 42),
        fetch: { db in ... })
    
    // Tracks the 'score' column in the 'player' table
    let observation = ValueObservation.tracking
        region: Player.select(max(Column("score")),
        fetch: { db in ... })
    
    // Tracks both the 'player' and 'team' tables
    let observation = ValueObservation.tracking
        region: Player.all(), Team.all(),
        fetch: { db in ... })
    

    Declaration

    Swift

    public static func tracking<Value>(
        region: DatabaseRegionConvertible...,
        fetch: @escaping (Database) throws -> Value)
    -> ValueObservation<ValueReducers.Fetch<Value>>

    Parameters

    region

    A list of observed regions.

    fetch

    A function that fetches the observed value from the database.

  • Creates a ValueObservation that notifies the values returned by the fetch function whenever a database transaction has an impact on the given regions.

    The tracked region is not automatically inferred from the requests performed in the fetch function.

    For example:

    // Tracks the full database
    let observation = ValueObservation.tracking
        regions: [.fullDatabase],
        fetch: { db in ... })
    
    // Tracks the full 'player' table
    let observation = ValueObservation.tracking
        regions: [Player.all()],
        fetch: { db in ... })
    
    // Tracks the row with id 42 in the 'player' table
    let observation = ValueObservation.tracking
        regions: [Player.filter(id: 42)],
        fetch: { db in ... })
    
    // Tracks the 'score' column in the 'player' table
    let observation = ValueObservation.tracking
        regions: [Player.select(max(Column("score"))],
        fetch: { db in ... })
    
    // Tracks both the 'player' and 'team' tables
    let observation = ValueObservation.tracking
        regions: [Player.all(), Team.all()],
        fetch: { db in ... })
    

    Declaration

    Swift

    public static func tracking<Value>(
        regions: [DatabaseRegionConvertible],
        fetch: @escaping (Database) throws -> Value)
    -> ValueObservation<ValueReducers.Fetch<Value>>

    Parameters

    regions

    A list of observed regions.

    fetch

    A function that fetches the observed value from the database.

  • Creates a ValueObservation that notifies the values returned by the fetch function whenever a database transaction changes them.

    For example:

    let observation = ValueObservation.tracking { db in
        try Player.fetchAll(db)
    }
    
    let cancellable = try observation.start(
        in: dbQueue,
        onError: { error in ... },
        onChange: { players: [Player] in
            print("Players have changed")
        })
    

    Declaration

    Swift

    public static func tracking<Value>(
        _ fetch: @escaping (Database) throws -> Value)
    -> ValueObservation<ValueReducers.Fetch<Value>>

    Parameters

    fetch

    A function that fetches the observed value from the database.

  • Creates a ValueObservation that notifies the values returned by the fetch function whenever a database transaction changes them.

    For example:

    let observation = ValueObservation.tracking { db in
        try Player.fetchAll(db)
    }
    
    let cancellable = try observation.start(
        in: dbQueue,
        onError: { error in ... },
        onChange: { players: [Player] in
            print("Players have changed")
        })
    

    Declaration

    Swift

    @available(*, deprecated, renamed: "tracking(_:﹚")
    public static func trackingVaryingRegion<Value>(
        _ fetch: @escaping (Database) throws -> Value)
    -> ValueObservation<ValueReducers.Fetch<Value>>

    Parameters

    fetch

    A function that fetches the observed value from the database.

Available where Reducer.Value: Equatable