Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ Released on TKTKTK
* Requires Swift 5.7
* Protocols define their primary associated types
* JSON literal arrays and dictionaries now must be strongly typed via the `JSONConvertible` protocol
* Annotate many methods as @MainActor
* All delegate methods
* All code with assert(Thread.isMainThread)
* Faulting an association when you're off the main thread will have different characteristics
* If the association already exists, nothing will change
* If the association does not already exist, it will always return nil and hit the main thread to batch fetch the associations
* More eventing supports occurring off of the main thread
* If needed, it will async bounce to the main thread to actually perform the change
* Newly allowed Events:
* Associated Value creation events
* Entity creation events
* Data reset events
* Note any changes to your model still must occur on the main thread
* data
* isDeleted
* NSSortDescriptor keyPaths
* Association keyPaths

## [4.0.4](https://github.com/square/FetchRequests/releases/tag/4.0.4)
Released on 2022-08-30
Expand Down
10 changes: 6 additions & 4 deletions Example/iOS-Example/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,14 @@ extension Model {
// MARK: - FetchableObjectProtocol

extension Model: FetchableObjectProtocol {
func observeDataChanges(_ handler: @escaping () -> Void) -> InvalidatableToken {
return _data.observeChanges { change in handler() }
func observeDataChanges(_ handler: @escaping @MainActor () -> Void) -> InvalidatableToken {
return _data.observeChanges { change in
handler()
}
}

func observeIsDeletedChanges(_ handler: @escaping () -> Void) -> InvalidatableToken {
return self.observe(\.isDeleted, options: [.old, .new]) { object, change in
func observeIsDeletedChanges(_ handler: @escaping @MainActor () -> Void) -> InvalidatableToken {
return self.observe(\.isDeleted, options: [.old, .new]) { @MainActor(unsafe) object, change in
guard let old = change.oldValue, let new = change.newValue, old != new else {
return
}
Expand Down
9 changes: 6 additions & 3 deletions Example/iOS-Example/Observable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ struct Change<Value> {

@propertyWrapper
class Observable<Value> {
fileprivate var observers: Atomic<[UUID: (Change<Value>) -> Void]> = Atomic(wrappedValue: [:])
typealias Handler = @MainActor (Change<Value>) -> Void

fileprivate var observers: Atomic<[UUID: Handler]> = Atomic(wrappedValue: [:])

var wrappedValue: Value {
@MainActor(unsafe)
didSet {
assert(Thread.isMainThread)

Expand All @@ -34,7 +37,7 @@ class Observable<Value> {
self.wrappedValue = wrappedValue
}

func observe(handler: @escaping (Change<Value>) -> Void) -> InvalidatableToken {
func observe(handler: @escaping Handler) -> InvalidatableToken {
let token = Token(parent: self)
observers.mutate { value in
value[token.uuid] = handler
Expand All @@ -44,7 +47,7 @@ class Observable<Value> {
}

extension Observable where Value: Equatable {
func observeChanges(handler: @escaping (Change<Value>) -> Void) -> InvalidatableToken {
func observeChanges(handler: @escaping Handler) -> InvalidatableToken {
return observe { change in
guard change.oldValue != change.newValue else {
return
Expand Down
2 changes: 2 additions & 0 deletions FetchRequests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = targeted;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs "clean".
Full concurrency checking would fail in many ways (notably @MainActor(unsafe) does not work).

SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "";
Expand Down Expand Up @@ -1100,6 +1101,7 @@
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = targeted;
SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "";
Expand Down
20 changes: 14 additions & 6 deletions FetchRequests/Sources/Associations/AssociatedValueReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class FetchableAssociatedValueReference<Entity: FetchableObject>: AssociatedValu
return [dataObserver, isDeletedObserver]
}

@MainActor
private func observedDeletionEvent(with entity: Entity) {
var invalidate = false
if let value = value as? Entity, value == entity {
Expand All @@ -71,19 +72,22 @@ class FetchableAssociatedValueReference<Entity: FetchableObject>: AssociatedValu
}

class AssociatedValueReference: NSObject {
typealias CreationObserved = @MainActor (_ value: Any?, _ entity: Any) -> AssociationReplacement<Any>
typealias ChangeHandler = @MainActor (_ invalidate: Bool) -> Void

private let creationObserver: FetchRequestObservableToken<Any>?
private let creationObserved: (Any?, Any) -> AssociationReplacement<Any>
private let creationObserved: CreationObserved

fileprivate(set) var value: Any?
fileprivate var changeHandler: ((_ invalidate: Bool) -> Void)?
fileprivate var changeHandler: ChangeHandler?

var canObserveCreation: Bool {
return creationObserver != nil
}

init(
creationObserver: FetchRequestObservableToken<Any>? = nil,
creationObserved: @escaping (Any?, Any) -> AssociationReplacement<Any> = { _, _ in .same },
creationObserved: @escaping CreationObserved = { _, _ in .same },
value: Any? = nil
) {
self.creationObserver = creationObserver
Expand All @@ -107,16 +111,17 @@ extension AssociatedValueReference {
self.value = value
}

func observeChanges(_ changeHandler: @escaping (_ invalidate: Bool) -> Void) {
func observeChanges(_ changeHandler: @escaping ChangeHandler) {
stopObserving()

self.changeHandler = changeHandler

startObservingValue()

creationObserver?.observeIfNeeded { [weak self] entity in
assert(Thread.isMainThread)
self?.observedCreationEvent(with: entity)
performOnMainThread {
self?.observedCreationEvent(with: entity)
}
}
}

Expand All @@ -132,7 +137,10 @@ extension AssociatedValueReference {
changeHandler = nil
}

@MainActor
private func observedCreationEvent(with entity: Any) {
assert(Thread.isMainThread)

// We just received a notification about an entity being created

switch creationObserved(value, entity) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ public enum AssociationReplacement<T> {
/// Map an associated value's key to object
public class FetchRequestAssociation<FetchedObject: FetchableObject> {
/// Fetch associated values given a list of parent objects
public typealias AssocationRequestByParent<AssociatedEntity> = (_ objects: [FetchedObject], _ completion: @escaping ([FetchedObject.ID: AssociatedEntity]) -> Void) -> Void
public typealias AssocationRequestByParent<AssociatedEntity> = @MainActor (_ objects: [FetchedObject], _ completion: @escaping ([FetchedObject.ID: AssociatedEntity]) -> Void) -> Void
/// Fetch associated values given a list of associated IDs
public typealias AssocationRequestByID<AssociatedEntityID: Hashable, AssociatedEntity> = (_ objects: [AssociatedEntityID], _ completion: @escaping ([AssociatedEntity]) -> Void) -> Void
public typealias AssocationRequestByID<AssociatedEntityID: Hashable, AssociatedEntity> = @MainActor (_ objects: [AssociatedEntityID], _ completion: @escaping ([AssociatedEntity]) -> Void) -> Void
/// Event that represents the creation of an associated value object
public typealias CreationObserved<Value, Comparison> = (Value?, Comparison) -> AssociationReplacement<Value>
/// Start observing a source object
Expand Down
53 changes: 43 additions & 10 deletions FetchRequests/Sources/Associations/ObservableToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,26 @@ internal class LegacyKeyValueObserving<Object: NSObject, Value: Any>: NSObject,

private var unsafeIsObserving = true

convenience init(object: Object, keyPath: AnyKeyPath, type: Value.Type, handler: @escaping Handler) {
self.init(object: object, keyPath: keyPath._kvcKeyPathString!, type: type, handler: handler)
convenience init(
object: Object,
keyPath: AnyKeyPath,
type: Value.Type,
handler: @escaping Handler
) {
self.init(
object: object,
keyPath: keyPath._kvcKeyPathString!,
type: type,
handler: handler
)
}

init(object: Object, keyPath: String, type: Value.Type, handler: @escaping Handler) {
init(
object: Object,
keyPath: String,
type: Value.Type,
handler: @escaping Handler
) {
self.object = object
self.keyPath = keyPath
self.handler = handler
Expand Down Expand Up @@ -107,9 +122,22 @@ internal class LegacyKeyValueObserving<Object: NSObject, Value: Any>: NSObject,
}

// swiftlint:disable:next block_based_kvo
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard let typedObject = object as? Object, typedObject == self.object, keyPath == self.keyPath else {
return super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?
) {
guard let typedObject = object as? Object,
typedObject == self.object,
keyPath == self.keyPath
else {
return super.observeValue(
forKeyPath: keyPath,
of: object,
change: change,
context: context
)
}

let oldValue = change?[.oldKey] as? Value
Expand All @@ -120,7 +148,9 @@ internal class LegacyKeyValueObserving<Object: NSObject, Value: Any>: NSObject,
}

internal class FetchRequestObservableToken<Parameter>: ObservableToken {
private let _observe: (_ handler: @escaping (Parameter) -> Void) -> Void
typealias Handler = (Parameter) -> Void
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this @mainactor (which requires making ObservableToken's handler @mainactor) was a giant pain and not worth it since I could do the simple main-thread bouncing behavior.


private let _observe: (_ handler: @escaping Handler) -> Void
private let _invalidate: () -> Void

var isObserving: Bool {
Expand All @@ -131,7 +161,10 @@ internal class FetchRequestObservableToken<Parameter>: ObservableToken {

private var unsafeIsObserving = false

private init(observe: @escaping (_ handler: @escaping (Parameter) -> Void) -> Void, invalidate: @escaping () -> Void) {
private init(
observe: @escaping (_ handler: @escaping Handler) -> Void,
invalidate: @escaping () -> Void
) {
_observe = observe
_invalidate = invalidate
}
Expand All @@ -141,7 +174,7 @@ internal class FetchRequestObservableToken<Parameter>: ObservableToken {
_invalidate = { token.invalidate() }
}

func observeIfNeeded(handler: @escaping (Parameter) -> Void) {
func observeIfNeeded(handler: @escaping Handler) {
synchronized(self) {
guard !unsafeIsObserving else {
return
Expand All @@ -154,7 +187,7 @@ internal class FetchRequestObservableToken<Parameter>: ObservableToken {
}
}

func observe(handler: @escaping (Parameter) -> Void) {
func observe(handler: @escaping Handler) {
synchronized(self) {
_observe(handler)
}
Expand Down
Loading