Skip to content

Commit 515efd2

Browse files
committed
Version 1.0.0 commit
Code here with at least one basic test, and a full README.MD file.
1 parent 132cc96 commit 515efd2

10 files changed

+700
-1
lines changed

.DS_Store

6 KB
Binary file not shown.

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,7 @@ fastlane/test_output
8888
# https://github.com/johnno1962/injectionforxcode
8989

9090
iOSInjectionProject/
91+
.swiftpm/.DS_Store
92+
.swiftpm/xcode/.DS_Store
93+
Sources/.DS_Store
94+
Tests/.DS_Store

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// swift-tools-version: 5.6
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "Observable",
8+
platforms: [
9+
.macOS(.v10_15),
10+
.iOS(.v13),
11+
.tvOS(.v13),
12+
.watchOS(.v6)
13+
],
14+
products: [
15+
// Products define the executables and libraries a package produces, and make them visible to other packages.
16+
.library(
17+
name: "Observable",
18+
targets: ["Observable"]),
19+
],
20+
dependencies: [
21+
// Dependencies declare other packages that this package depends on.
22+
.package(url: "https://github.com/Flowduino/ThreadSafeSwift.git", from: "1.0.0"),
23+
],
24+
targets: [
25+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
26+
// Targets can depend on other targets in this package, and on products in packages this package depends on.
27+
.target(
28+
name: "Observable",
29+
dependencies: [
30+
.product(name: "ThreadSafeSwift", package: "ThreadSafeSwift"),
31+
]),
32+
.testTarget(
33+
name: "ObservableTests",
34+
dependencies: [
35+
"Observable",
36+
.product(name: "ThreadSafeSwift", package: "ThreadSafeSwift"),
37+
]),
38+
]
39+
)

README.md

+224-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,225 @@
11
# Observable
2-
The most flexible and easiest to implement Observer Pattern platform for the Swift language (includes fully-functional Observable Thread!)
2+
3+
<p>
4+
<img src="https://img.shields.io/badge/Swift-5.1%2B-yellowgreen.svg?style=flat" />
5+
<img src="https://img.shields.io/badge/iOS-13.0+-865EFC.svg" />
6+
<img src="https://img.shields.io/badge/iPadOS-13.0+-F65EFC.svg" />
7+
<img src="https://img.shields.io/badge/macOS-10.15+-179AC8.svg" />
8+
<img src="https://img.shields.io/badge/tvOS-13.0+-41465B.svg" />
9+
<img src="https://img.shields.io/badge/watchOS-6.0+-1FD67A.svg" />
10+
<img src="https://img.shields.io/badge/License-MIT-blue.svg" />
11+
<a href="https://github.com/apple/swift-package-manager">
12+
<img src="https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat" />
13+
</a>
14+
</p>
15+
16+
Collection of carefully-prepared Classes and Protocols designed to imbue your inheriting Object Types with efficient, protocol-driven Observer Pattern Behaviour.
17+
18+
## Installation
19+
### Xcode Projects
20+
Select `File` -> `Swift Packages` -> `Add Package Dependency` and enter `https://github.com/Flowduino/Observable.git`
21+
22+
### Swift Package Manager Projects
23+
You can use `Observable` as a Package Dependency in your own Packages' `Package.swift` file:
24+
```swift
25+
let package = Package(
26+
//...
27+
dependencies: [
28+
.package(
29+
url: "https://github.com/Flowduino/Observable.git",
30+
.upToNextMajor(from: "1.0.0")
31+
),
32+
],
33+
//...
34+
)
35+
```
36+
37+
From there, refer to `Observable` as a "target dependency" in any of _your_ package's targets that need it.
38+
39+
```swift
40+
targets: [
41+
.target(
42+
name: "YourLibrary",
43+
dependencies: [
44+
"Observable",
45+
],
46+
//...
47+
),
48+
//...
49+
]
50+
```
51+
You can then do `import Observable` in any code that requires it.
52+
53+
## Usage
54+
55+
Here are some quick and easy usage examples for the features provided by `Observable`:
56+
57+
### ObservableClass
58+
You can inherit from `ObservableClass` in your *own* Class Types to provide out-of-the-box Observer Pattern support.
59+
This not only works for `@ObservedObject` decorated Variables in a SwiftUI `View`, but also between your Classes (e.g. between Services, or Repositories etc.)
60+
61+
First, you would define a Protocol describing the Methods implemented in your *Observer* Class Type that your *Observable* Class can invoke:
62+
```swift
63+
/// Protocol defining what Methods the Obverable Class can invoke on any Observer
64+
protocol DummyObserver: AnyObject { // It's extremely important that this Protocol be constrained to AnyObject
65+
func onFooChanged(oldValue: String, newValue: String)
66+
func onBarChanged(oldValue: String, newValue: String)
67+
}
68+
```
69+
**Note** - It is important that our Protocol define the `AnyObject` conformity-constraint as shown above.
70+
71+
Now, we can define our *Observable*, inheriting from `ObservableClass`:
72+
```swift
73+
/// Class that can be Observed
74+
class Dummy: ObservableClass {
75+
private var _foo: String = "Hello"
76+
public var foo: String {
77+
get {
78+
return _foo
79+
}
80+
set {
81+
// Invoke onFooChanged for all current Observers
82+
withObservers { (observer: DummyObserver) in
83+
observer.onFooChanged(oldValue: _foo, newValue: newValue)
84+
}
85+
_foo = newValue
86+
objectWillChange.send() // This is for the standard ObservableObject behaviour (both are supported together)
87+
}
88+
}
89+
90+
private var _bar: String = "World"
91+
public var bar: String {
92+
get {
93+
return _bar
94+
}
95+
set {
96+
// Invoke onBarChanged for all current Observers
97+
withObservers { (observer: DummyObserver) in
98+
observer.onBarChanged(oldValue: _bar, newValue: newValue)
99+
}
100+
_bar = newValue
101+
objectWillChange.send() // This is for the standard ObservableObject behaviour (both are supported together)
102+
}
103+
}
104+
}
105+
```
106+
107+
We can now define an *Observer* to register with the *Observable*, ensuring that we specify that it implements our `DummyObserver` protocol:
108+
```swift
109+
class DummyObserver: DummyObserver {
110+
/* Implementations for DummyObserver */
111+
func onFooChanged(oldValue: String, newValue: String) {
112+
print("Foo Changed from \(oldValue) to \(newValue)")
113+
}
114+
115+
func onBarChanged(oldValue: String, newValue: String) {
116+
print("Bar Changed from \(oldValue) to \(newValue)")
117+
}
118+
}
119+
```
120+
121+
We can now produce some simple code (such as in a Playground) to put it all together:
122+
```swift
123+
// Playground Code to use the above
124+
var observable = Dummy() // This is the Object that we can Observe
125+
var observer = DummyObserver() // This is an Object that will Observe the Observable
126+
127+
observable.addObserver(observer) // This is what registers the Observer with the Observable!
128+
observable.foo = "Test 1"
129+
observable.bar = "Test 2"
130+
```
131+
132+
### ObservableThreadSafeClass
133+
`ObservableThreadSafeClass` works exactly the same way as `ObservableClass`. The internal implementation simply encapsulates the `Observer` collections behind a `DispatchSemaphore`, and provides a *Revolving Door* mechanism to ensure unobstructed access is available to `addObserver` and `removeObserver`, even when `withObservers` is in execution.
134+
135+
Its usage is exactly as shown above in `ObservableClass`, only you would substitute the inheritence of `ObservableClass` to instead inherit from `ObservableThreadSafeClass`.
136+
137+
### ObservableThread
138+
`ObservableThread` provides you with a Base Type for any Thread Types you would want to Observe.
139+
140+
**Note** - `ObservableThread` does implement the `ObservableObject` protocol, and is *technically* compatible with the `@ObservedObject` property decorator in a SwiftUI `View`. However, to use it in this way, anywhere you would invoke `objectWillUpdate.send()` you must instead use `notifyChange()`. Internally, `ObservableThread` will execute `objectWillChange.send()` **but** enforce that it must execute on the `MainActor` (as required by Swift)
141+
142+
Let's now begin taking a look at how we can use `ObservableThread` in your code.
143+
The example is intentionally simplistic, and simply generates a random number every 60 seconds within an endless loop in the Thread.
144+
145+
Let's begin by defining our Observation Protocol:
146+
```swift
147+
protocol RandomNumberObserver: AnyObject {
148+
func onRandomNumber(_ randomNumber: Int)
149+
}
150+
```
151+
Any Observer for our Thread will need to conform to the RandomNumberObserver protocol above.
152+
153+
Now, let's define our RandomNumberObservableThread class:
154+
```swift
155+
class RandomNumberObservableThread: ObservableThread {
156+
init() {
157+
self.start() // This will start the thread on creation. You aren't required to do it this way, I'm just choosing to!
158+
}
159+
160+
public override func main() { // We must override this method
161+
while self.isExecuting { // This creates a loop that will continue for as long as the Thread is running!
162+
let randomNumber = Int.random(in: -9000..<9001) // We'll generate a random number between -9000 and +9000
163+
// Now let's notify all of our Observers!
164+
withObservers { (observer: RandomNumberObserver) in
165+
observer.onRandomNumber(randomNumber)
166+
}
167+
Self.sleep(forTimeInterval: 60.00) // This will cause our Thread to sleep for 60 seconds
168+
}
169+
}
170+
}
171+
```
172+
173+
So, we now have a Thread that can be Observed, and will notify all Observers every minute when it generates a random Integer.
174+
175+
Let's now implement a Class intended to Observe this Thread:
176+
```swift
177+
class RandomNumberObserverClass: RandomNumberObserver {
178+
public func onRandomNumber(_ randomNumber: Int) {
179+
print("Random Number is: \(randomNumber)")
180+
}
181+
```
182+
We can now tie this all together in a simple Playground:
183+
```swift
184+
var myThread = RandomNumberObservableThread()
185+
var myObserver = RandomNumberObserverClass()
186+
myThread.addObserver(myObserver)
187+
```
188+
189+
That's it! The Playground program will now simply print out the new Random Number notice message into the console output every 60 seconds.
190+
191+
You can adopt this approach for any Observation-Based Thread Behaviour you require, because `ObservableThread` will always invoke the Observer callback methods in the execution context their own threads! This means that, for example, you can safely instantiate an Observer class on the UI Thread, while the code execution being observed resides in its own threads (one or many, per your requirements).
192+
193+
## Additional Useful Hints
194+
There are a few additional useful things you should know about this Package.
195+
### A single *Observable* can invoke `withObservers` for any number of *Observer Protocols*
196+
This library intentionally performs run-time type checks against each registered *Observer* to ensure that it conforms to the explicitly-defined *Observer Protocol* being requested by your `withObservers` Closure method.
197+
198+
Simple example protocols:
199+
```swift
200+
protocol ObserverProtocolA: AnyObject {
201+
func doSomethingForProtocolA()
202+
}
203+
204+
protocol ObserverProtocolB: AnyObject {
205+
func doSomethingForProtocolB()
206+
}
207+
```
208+
209+
Which can then both be used by the same `ObservableClass`, `ObservableThreadSafeClass`, or `ObservableThread` descendant:
210+
211+
```swift
212+
withObservers { (observer: ObserverProtocolA) in
213+
observer.doSomethingForProtocolA()
214+
}
215+
216+
withObservers { (observer: ObserverProtocolB) in
217+
observer.doSomethingForProtocolB()
218+
}
219+
```
220+
221+
Any number of *Observer Protocols* can be marshalled by any of our *Observable* types, and only *Observers* conforming to the explicitly-specified *Observer Protocol* will be passed into your `withObservers` Closure method.
222+
223+
## License
224+
225+
`ThreadSafeSwift` is available under the MIT license. See the [LICENSE file](./LICENSE) for more info.

Sources/Observable/Observable.swift

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// Observable.swift
3+
// Copyright (c) 2022, Flowduino
4+
// Authored by Simon J. Stuart on 8th July 2022
5+
//
6+
// Subject to terms, restrictions, and liability waiver of the MIT License
7+
//
8+
9+
/**
10+
Describes any Type (Class or Thread) to which Observers can Subscribe.
11+
- Author: Simon J. Stuart
12+
- Version: 1.0
13+
- Important: This Protocol can only be applied to Class and Thread types.
14+
15+
See the Base Implementations `ObservableClass` and `ObservableThread`
16+
*/
17+
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
18+
public protocol Observable: AnyObject {
19+
/**
20+
Registers an Observer against this Observable Type
21+
- Author: Simon J. Stuart
22+
- Version: 1.0
23+
- Parameters:
24+
- observer: A reference to a Class (or Thread) conforming to the desired Observer Protocol, inferred from Generics as `TObservationProtocol`
25+
26+
Call this every time you need to register an Observer with your Observable.
27+
*/
28+
func addObserver<TObservationProtocol: AnyObject>(_ observer: TObservationProtocol)
29+
30+
/**
31+
Removes an Observer from this Observable Type
32+
- Author: Simon J. Stuart
33+
- Version: 1.0
34+
- Parameters:
35+
- observer: A reference to a Class (or Thread) conforming to the desired Observer Protocol, inferred from Generics as `TObservationProtocol`
36+
37+
Call this if you need to explicitly unregister an Observer from your Observable.
38+
*/
39+
func removeObserver<TObservationProtocol: AnyObject>(_ observer: TObservationProtocol)
40+
41+
/**
42+
Iterates all of the registered Observers for this Observable and invokes against them your defined Closure Method.
43+
- Author: Simon J. Stuart
44+
- Version: 1.0
45+
- Parameters:
46+
- code: The Closure you wish to invoke for each Observer
47+
- Important: You must explicitly define the Observation Protocol to which the Observer must conform. This is inferred from Generics as `TObservationProtocol`
48+
- Example: Here is a usage example with a hypothetical Observation Protocol named `MyObservationProtocol`
49+
````
50+
withObservers { (observer: MyObservationProtocol) in
51+
observer.myObservationMethod()
52+
}
53+
````
54+
Where `MyObservationProtocol` would look something like this:
55+
````
56+
protocol MyObservationProtocol: AnyObject {
57+
func myObservationMethod()
58+
}
59+
````
60+
*/
61+
func withObservers<TObservationProtocol>(_ code: @escaping (_ observer: TObservationProtocol) -> ())
62+
}

0 commit comments

Comments
 (0)