Skip to content

Commit

Permalink
Merge pull request #488 from groue/feature/ValueObservationCleanup
Browse files Browse the repository at this point in the history
ValueObservation Cleanup
  • Loading branch information
groue authored Mar 1, 2019
2 parents 03fafa4 + 4ee2523 commit 661cca9
Show file tree
Hide file tree
Showing 19 changed files with 684 additions and 754 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one expection:
- [#478](https://github.com/groue/GRDB.swift/pull/478): Swift 5: SQL interpolation
- [#484](https://github.com/groue/GRDB.swift/pull/484): SE-0193 Cross-module inlining and specialization
- [#486](https://github.com/groue/GRDB.swift/pull/486): Refactor PersistenceError.recordNotFound
- [#488](https://github.com/groue/GRDB.swift/pull/488): ValueObservation Cleanup

### Breaking Changes

- Swift 4.0 and Swift 4.1 are no longer supported
- iOS 8 is no longer supported. Minimum deployment target is now iOS 9.0
- Deprecated APIs are no longer available.
- `SQLRequest.arguments` is no longer optional

### Documentation Diff

Expand Down
2 changes: 1 addition & 1 deletion GRDB/Core/DatabaseSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ extension DatabaseSnapshot {
}
}
}
case let .onQueue(queue, startImmediately: startImmediately):
case let .async(onQueue: queue, startImmediately: startImmediately):
if startImmediately {
if let value = try unsafeReentrantRead(observation.initialValue) {
queue.async {
Expand Down
4 changes: 2 additions & 2 deletions GRDB/Core/DatabaseWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ extension DatabaseWriter {
DispatchQueue.main.async { onChange(value) }
}
}
case let .onQueue(queue, startImmediately: startImmediately):
case let .async(onQueue: queue, startImmediately: startImmediately):
if startImmediately {
if let value = try reducer.initialValue(db, requiresWriteAccess: observation.requiresWriteAccess) {
queue.async { onChange(value) }
Expand All @@ -231,7 +231,7 @@ extension DatabaseWriter {
notificationQueue: observation.notificationQueue,
onError: onError,
onChange: onChange)
db.add(transactionObserver: valueObserver, extent: observation.extent)
db.add(transactionObserver: valueObserver, extent: .observerLifetime)

return valueObserver
}
Expand Down
13 changes: 13 additions & 0 deletions GRDB/Fixit/GRDB-4.0.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,16 @@ extension DatabaseValue {
@available(*, unavailable)
public func losslessConvert<T>(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T: DatabaseValueConvertible { preconditionFailure() }
}

extension ValueScheduling {
@available(*, unavailable, renamed: "async(onQueue:startImmediately:)")
public static func onQueue(_ queue: DispatchQueue, startImmediately: Bool) -> ValueScheduling { preconditionFailure() }
}

extension ValueObservation {
@available(*, unavailable, message: "Observation extent is controlled by the lifetime of observers returned by the start() method.")
public var extent: Database.TransactionObservationExtent {
get { preconditionFailure() }
set { preconditionFailure() }
}
}
1 change: 0 additions & 1 deletion GRDB/ValueObservation/ValueObservation+MapReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ extension ValueObservation {
var observation = ValueObservation<R>(
tracking: observedRegion,
reducer: { db in try transform(db, makeReducer(db)) })
observation.extent = extent
observation.scheduling = scheduling
observation.requiresWriteAccess = requiresWriteAccess
return observation
Expand Down
11 changes: 2 additions & 9 deletions GRDB/ValueObservation/ValueObservation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public enum ValueScheduling {
///
/// An initial value is fetched and notified if `startImmediately`
/// is true.
case onQueue(DispatchQueue, startImmediately: Bool)
case async(onQueue: DispatchQueue, startImmediately: Bool)

/// Values are not all notified on the same dispatch queue.
///
Expand Down Expand Up @@ -89,13 +89,6 @@ public struct ValueObservation<Reducer> {
/// observation is less efficient than a read-only observation.
public var requiresWriteAccess: Bool = false

// TODO: replace ValueObservation.extent
/// The extent of the database observation. The default is
/// `.observerLifetime`: the observation lasts until the
/// observer returned by the `start(in:onError:onChange:)` method
/// is deallocated.
public var extent = Database.TransactionObservationExtent.observerLifetime

/// `scheduling` controls how fresh values are notified. Default
/// is `.mainQueue`.
///
Expand Down Expand Up @@ -158,7 +151,7 @@ public struct ValueObservation<Reducer> {
switch scheduling {
case .mainQueue:
return DispatchQueue.main
case .onQueue(let queue, startImmediately: _):
case let .async(onQueue: queue, startImmediately: _):
return queue
case .unsafe:
return nil
Expand Down
63 changes: 37 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6073,7 +6073,36 @@ An initial fetch is performed as soon as the observation starts: the view is set
The observer returned by the `start` method is stored in a property of the view controller. This allows the view controller to control the duration of the observation. When the observer is deallocated, the observation stops. Meanwhile, all transactions that modify the observed player are notified, and the `nameLabel` is kept up-to-date.

> :bulb: **Tip**: see the [Demo Application](DemoApps/GRDBDemoiOS/README.md) for a sample app that uses ValueObservation.

>
> :bulb: **Tip**: When fetching initial values is slow, and should not block the main queue, opt in for async notifications:
>
> ```swift
> override func viewWillAppear(_ animated: Bool) {
> super.viewWillAppear(animated)
>
> // Define a ValueObservation which tracks a player
> let request = Player.filter(key: 42)
> var observation = ValueObservation.trackingOne(request)
>
> // Observation is asynchronous
> observation.scheduling = .async(onQueue: .main, startImmediately: true)
>
> // Start observing the database
> observer = try! observation.start(
> in: dbQueue,
> onChange: { [unowned self] (player: Player?) in
> // Player has changed: update view
> self.activityIndicator.stopAnimating()
> self.nameLabel.text = player?.name
> })
>
> // Wait for player
> activityIndicator.startAnimating()
> nameLabel.text = nil
> }
> ```
>
> See [ValueObservation.scheduling](#valueobservationscheduling) for more information.

### ValueObservation.trackingCount, trackingOne, trackingAll

Expand Down Expand Up @@ -6399,29 +6428,10 @@ let observer = try observation.start(

Some behaviors of value observations can be configured:

- [ValueObservation.extent](#valueobservationextent): Precise control of the observation duration.
- [ValueObservation.scheduling](#valueobservationscheduling): Control the dispatching of notified values.
- [ValueObservation.requiresWriteAccess](#valueobservationrequireswriteaccess): Allow observations to write in the database.


#### ValueObservation.extent

The `extent` property lets you specify the duration of the observation.

The default extent is `.observerLifetime`: the observation stops when the observer returned by `start` is deallocated.

You can use the `.databaseLifetime` extent to specify that the observation lasts until the database connection is closed:

```swift
// No need to retain the observer returned by the start method:
var observation = ValueObservation...
observation.extent = .databaseLifetime
_ = observation.start(in: dbQueue) { newValue in ... }
```

> :warning: **Warning**: Don't use the `.nextTransaction` lifetime, because it produces unreliable results. A future version of GRDB will deprecate `ValueObservation.extent`, and provide a better API.


#### ValueObservation.scheduling

The `scheduling` property lets you control how fresh values are notified:
Expand Down Expand Up @@ -6462,18 +6472,21 @@ The `scheduling` property lets you control how fresh values are notified:
}
```

- `.onQueue(_:startImmediately:)`: all values are asychronously notified on the specified queue.
- `.async(onQueue:startImmediately:)`: all values are asychronously notified on the specified queue.

An initial value is fetched and notified if `startImmediately` is true.

For example:

```swift
let customQueue = DispatchQueue(label: "customQueue")
// On main queue
var observation = ValueObservation.trackingAll(Player.all())
observation.scheduling = .onQueue(customQueue, startImmediately: true)
observation.scheduling = .async(onQueue: .main, startImmediately: true)
let observer = try observation.start(in: dbQueue) { (players: [Player]) in
// On customQueue
// On main queue
print("fresh players: \(players)")s
}
// <- here "fresh players" is not printed yet.
```

- `unsafe(startImmediately:)`: values are not all notified on the same dispatch queue.
Expand Down Expand Up @@ -7183,8 +7196,6 @@ dbQueue.inDatabase { db in
}
```

> :point_up: **Note**: removing a transaction observer makes it sure it won't be notified of any future transaction - there is no race condition. However, this does not stop eventual concurrent processing of previous transactions. For example, `remove(transactionObserver:)` is not a correct way to stop observers started by [ValueObservation]. In this case, the correct way is deallocating the observer.

Alternatively, use the `extent` parameter of the `add(transactionObserver:extent:)` method:

```swift
Expand Down
Loading

0 comments on commit 661cca9

Please sign in to comment.