Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

project: add the Observed property wrapper #9

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions Burritos.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ A collection of well tested Swift Property Wrappers.
* @Trimmed
* @UndoRedo
* @UserDefault
* @Observed
* More coming ...
DESC

Expand Down Expand Up @@ -104,4 +105,10 @@ A collection of well tested Swift Property Wrappers.
sp.source_files = 'Sources/UserDefault/*'
sp.framework = 'Foundation'
end

## @Observed
s.subspec 'Observed' do |sp|
sp.source_files = 'Sources/Observed/*'
sp.framework = 'Foundation'
end
end
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ let package = Package(
"Trimmed",
"UndoRedo",
"UserDefault",
"Observed",
]),
],
dependencies: [], // No dependencies
Expand Down Expand Up @@ -58,5 +59,7 @@ let package = Package(
.testTarget(name: "UndoRedoTests", dependencies: ["UndoRedo"]),
.target(name: "UserDefault", dependencies: []),
.testTarget(name: "UserDefaultTests", dependencies: ["UserDefault"]),
.target(name: "Observed", dependencies: []),
.testTarget(name: "ObservedTests", dependencies: ["Observed"]),
]
)
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ A collection of well tested Swift Property Wrappers.
- [@Trimmed](#Trimmed)
- [@UndoRedo](#UndoRedo)
- [@UserDefault](#UserDefault)
- [@Observed](#Observed)
- More coming ...

## 🚧 Beta Software: 🚧
Expand Down Expand Up @@ -306,6 +307,33 @@ let userDefaults = UserDefaults(suiteName: "your.app.group")
var test: String
```

## @Observed

Implementation of the Observer pattern. The annotated property becomes a `Subject` that can be observed by `Observers`.

```swift

class IndexObserver: Observer {
func update(value: Int) {
print ("New value received: \(value)")
}
}

@Observed
var index = 1

let indexObserver = IndexObserver().toAnyObserver()
$index.add(observer: indexObserver)

index = 2

// will make indexObserver print: New value received: 2

// later the observer can be removed like this:
$index.remove(observer: indexObserver)

```

## @Cached
TODO

Expand Down
100 changes: 100 additions & 0 deletions Sources/Observed/Observed.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Observed.swift
// Burritos-Framework
//
// Created by Thibault Wittemberg on 2019-07-14.
// Copyright © 2019 Thibault Wittemberg. All rights reserved.
//

/// A property wrapper to transform any property into an observable property.
/// Step 1: Implement a class conforming to Observer (the associatedtype must be of the same type as the wrapped value)
/// Step 2: Register the observer to the property wrapper
/// Step 3: All the property mutations will trigger the update function of the Observer
///
/// ```
/// struct User {
/// var name: String
/// var age: Int
/// }
///
/// class UserObserver: Observer {
/// func update(value: User) {
/// print ("New user received: \(user)")
/// }
/// }
///
/// @Observed
/// var user = User(name: "john doe", age: 20)
///
/// let userObserver = UserObserver().toAnyObserver()
/// $user.add(observer: userObserver)
///
/// user.name = "jane doe"
/// user.age = 30
///
/// will print:
/// New user received: User(name: "jane doe", age: 20)
/// New user received: User(name: "jane doe", age: 30)
/// ```

public protocol Observer: AnyObject {
associatedtype Value
func update(value: Value)
}

public class AnyObserver<Value>: Observer {
private let updateClosure: (Value) -> Void

init<ObserverType: Observer>(with observer: ObserverType) where ObserverType.Value == Value {
self.updateClosure = observer.update
}

public func update(value: Value) {
self.updateClosure(value)
}
}

extension Observer {
func toAnyObserver () -> AnyObserver<Value> {
return AnyObserver<Value>(with: self)
}
}

public protocol Subject {
associatedtype Value
mutating func add(observer: AnyObserver<Value>)
mutating func remove(observer: AnyObserver<Value>)
}

@propertyWrapper
public struct Observed<Value>: Subject {

public init (initialValue: Value) {
self.wrappedValue = initialValue
}

init (initialValue: Value, by observer: AnyObserver<Value>) {
self.wrappedValue = initialValue
self.observers.append(observer)
}

public var wrappedValue: Value {
didSet {
self.fireNotification()
}
}

private var observers = [AnyObserver<Value>]()

public mutating func add(observer: AnyObserver<Value>) {
self.observers.append(observer)
}

public mutating func remove(observer: AnyObserver<Value>) {
self.observers.removeAll { $0 === observer }
}

private func fireNotification() {
self.observers.forEach { $0.update(value: self.wrappedValue) }
}
}
2 changes: 2 additions & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LazyTests
import TrimmedTests
import UndoRedoTests
import UserDefaultTests
import ObservedTests

var tests = [XCTestCaseEntry]()
tests += AtomicWriteTests.allTests()
Expand All @@ -26,5 +27,6 @@ tests += LazyConstantTests.allTests()
tests += Trimmed.allTests()
tests += UndoRedoTests.allTests()
tests += UserDefaultTests.allTests()
tests += ObservedTests.allTests()

XCTMain(tests)
113 changes: 113 additions & 0 deletions Tests/ObservedTests/ObservedTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// ObservedTests.swift
// Burritos-FrameworkTests
//
// Created by Thibault Wittemberg on 2019-07-14.
// Copyright © 2019 Thibault Wittemberg. All rights reserved.
//

import XCTest
@testable import Observed

private struct User: Equatable {
var name: String
var age: Int
}

private class IntObserver: Observer {

var hasUpdated = false
var value = 0

func update(value: Int) {
self.hasUpdated = true
self.value = value
}
}

private class UserObserver: Observer {

var numberOfUpdateCalls = 0
var value: User?

func update(value: User) {
self.numberOfUpdateCalls += 1
self.value = value
}
}

final class ObservedTests: XCTestCase {

@Observed
private var index = 0

@Observed
fileprivate var user = User(name: "john doe", age: 20)

override func setUp() {
super.setUp()
self.index = 0
self.user = User(name: "john doe", age: 20)
}

override func tearDown() {
self.index = 0
self.user = User(name: "john doe", age: 20)
super.tearDown()
}

func testObserver_isCalled() {

// Given: a IntObserver, observing a int property
let intObserver = IntObserver()
$index.add(observer: intObserver.toAnyObserver())

// When: mutating the int property
self.index = 10

// Then: the observer has been notified
XCTAssertTrue(intObserver.hasUpdated)
XCTAssertEqual(10, intObserver.value)
XCTAssertEqual(10, self.index)
}

func testObserver_isNotCalled_whenObserverHasBeenRemoved() {

// Given: a IntObserver, observing a int property and then unregistered from observation
let intObserver = IntObserver()
let anyObserver = intObserver.toAnyObserver()
$index.add(observer: anyObserver)
$index.remove(observer: anyObserver)

// When: mutating the int property
self.index = 10

// Then: the observer has not been notified
XCTAssertFalse(intObserver.hasUpdated)
XCTAssertEqual(0, intObserver.value)
XCTAssertEqual(10, self.index)
}

func testObserver_isCalled_forComplexTypes() {

// Given: a UserObserver, observing a user property
let userObserver = UserObserver()
$user.add(observer: userObserver.toAnyObserver())

// When: mutating the user property
self.user.name = "jane doe"
self.user.age = 30

// Then: the observer has been notified 2 times
XCTAssertEqual(userObserver.numberOfUpdateCalls, 2)
XCTAssertEqual(User(name: "jane doe", age: 30), userObserver.value)
XCTAssertEqual("jane doe", self.user.name)
XCTAssertEqual(30, self.user.age)
}

static var allTests = [
("testObserver_isCalled", testObserver_isCalled),
("testObserver_isNotCalled_whenObserverHasBeenRemoved", testObserver_isNotCalled_whenObserverHasBeenRemoved),
("testObserver_isCalled_forComplexTypes", testObserver_isCalled_forComplexTypes)
]
}
1 change: 1 addition & 0 deletions Tests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public func allTests() -> [XCTestCaseEntry] {
testCase(Trimmed.allTests),
testCase(UndoRedoTests.allTests),
testCase(UserDefaultTests.allTests),
testCase(ObservedTests.allTests),
]
}
#endif