├── .github └── ISSUE_TEMPLATE │ ├── i-found-a-bug-.md │ └── i-have-an-idea-.md ├── .gitignore ├── .spi.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Code ├── CombineObserver │ └── SwiftObserver+Combine.swift └── SwiftObserver │ ├── AnyAuthor.swift │ ├── Connection.swift │ ├── ObservableObject │ ├── Messenger.swift │ ├── ObservableCache.swift │ ├── ObservableObject+StopObservation.swift │ ├── ObservableObject.swift │ ├── ReceiverPool.swift │ └── Update.swift │ ├── Observer │ ├── ObservableObject+Observer.swift │ ├── Observer.swift │ └── Receiver.swift │ ├── Transforms │ ├── ObservableTransforms │ │ ├── AuthorFilter.swift │ │ ├── Cache.swift │ │ ├── CacheOnOptionalMessage.swift │ │ ├── Filter.swift │ │ ├── Mapper+Cache.swift │ │ ├── Mapper.swift │ │ ├── ObservableObject+Transforms │ │ │ ├── ObservableObject+Cache.swift │ │ │ ├── ObservableObject+Filter.swift │ │ │ ├── ObservableObject+FilterAuthor.swift │ │ │ ├── ObservableObject+From.swift │ │ │ ├── ObservableObject+Map.swift │ │ │ ├── ObservableObject+New.swift │ │ │ ├── ObservableObject+NotFrom.swift │ │ │ ├── ObservableObject+Select.swift │ │ │ ├── ObservableObject+Unwrap.swift │ │ │ ├── ObservableObject+UnwrapWithDefault.swift │ │ │ └── ObservableObject+Weak.swift │ │ ├── Unwrapper.swift │ │ └── Weak.swift │ └── ObservationTransforms │ │ ├── ObservationTransformer+Transforms │ │ ├── ObservationTransformer+Filter.swift │ │ ├── ObservationTransformer+FilterAuthor.swift │ │ ├── ObservationTransformer+From.swift │ │ ├── ObservationTransformer+Map.swift │ │ ├── ObservationTransformer+New.swift │ │ ├── ObservationTransformer+NotFrom.swift │ │ ├── ObservationTransformer+Select.swift │ │ ├── ObservationTransformer+Unwrap.swift │ │ └── ObservationTransformer+UnwrapWithDefault.swift │ │ ├── ObservationTransformer.swift │ │ └── Observer+ObservationTransformer.swift │ └── Variable │ ├── Variable+Comparable.swift │ ├── Variable+ObservableCache.swift │ ├── Variable+PropertyWrapper.swift │ ├── Variable+ValueAssignment.swift │ └── Variable.swift ├── Documentation ├── Architecture │ ├── CombineObserver.png │ ├── ObservableObject.png │ ├── Observer.png │ ├── SwiftObserver.png │ ├── Transforms.png │ ├── Variable.png │ └── architecture.md ├── philosophy.md ├── specific-patterns.md ├── swift.png └── swift_original.jpg ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── Package.swift ├── README.md └── Tests ├── CombineObserverTests └── CombineObserverTests.swift └── SwiftObserverTests ├── BasicTests.swift ├── CustomObservableTests.swift ├── ObservableTransformTests.swift ├── ObservationTransformTests.swift └── VariableTests.swift /.github/ISSUE_TEMPLATE/i-found-a-bug-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: I Found a Bug! 3 | about: Help Fixing SwiftObserver 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/i-have-an-idea-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: I Have an Idea! 3 | about: Help Extending SwiftObserver 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .swiftpm/ 3 | Package.resolved -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [SwiftObserver] 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # SwiftObserver Changelog 2 | 3 | ## v7.0.3 4 | 5 | Avoids compiling CombineObserver code on Linux since it's unavailable there 6 | 7 | ## v7.0.2 8 | 9 | Fixes compile issues on iOS, tvOS and watchOS 10 | 11 | ## v7.0.1 12 | 13 | Fixes the unstable version of the dependence on SwiftyToolz 14 | 15 | ## v7 16 | 17 | This update lets go of some hard-earned features in favour of simplicity and shifts focus to native Swift features that have recently filled some gaps. 18 | 19 | 7.0.0 also expresses a renewed commitment to semantic versioning, in particular since SwiftObserver has moved to the [Codeface GitHub organization](https://github.com/codeface-io). 20 | 21 | ### Removed 22 | - `Promise` has been removed as any Promise/Future implementation is obsolete with Swift's latest native concurrency features. 23 | - "One-shot" observations have been removed, as their primary purpose was to enable `Promise`. 24 | - `FreeObserver` and "global" observations have been removed, since they undermined the memory management concept without much benefit 25 | - Any value type-specific extensions of `Var` have been removed, as the property wrapper now fulfills their purpose. 26 | - Cocoapods support has been dropped. 27 | 28 | ### Changed 29 | - Protocol `Observable` has been renamed to `ObservableObject` so the new property wrapper could be conveniently named `Observable` and because the protocol is actually a class protocol. 30 | - Author filters `from` and `notFrom` require **non**-optional `AnyAuthor`. 31 | 32 | ### Improved 33 | - Variable values don't need to be `Codable` anymore. `Var` remains `Codable` where `Value` is. 34 | - Many small refinements, more or less under the hood. 35 | 36 | ### Added 37 | - Property wrapper `Observable` allows to make any `var: Value` observable by storing it in a wrapped `Var`. 38 | - CombineObserver is a new library product of the SwiftObserver package, offering conversion of SwiftObserver's `ObservableObject`s into Combine `Publisher`s. 39 | 40 | ## v6.2 41 | 42 | New functions on `Promise` that return a mapped new `Promise`: 43 | * `map(...)` 44 | * `unwrap(default)` 45 | * `new()` 46 | 47 | ## v6.1 48 | 49 | All that's new is also compatible with message authors and ad-hoc transform chains: 50 | * Promises: 51 | * `Promise` is a `Messenger` with some conveniences for async returns 52 | * Promise composition functions `promise`, `then` and `and` 53 | * Free Observers: 54 | * Class for adhoc observers `FreeObserver` 55 | * Global function `observe(...)`, and `observed(...)` on observables, both use `FreeObserver.shared` 56 | * Global function `observeOnce(...)`, and `observedOnce(...)` on observables, both use `FreeObserver` 57 | * `BufferedObservable`: 58 | * `BufferedObservable` has been renamed to `ObservableCache`. 59 | * `ObservableCache` can be created via observable transform `cache()`, which makes `Message` optional only when necessary. 60 | * `whenCached` retrieves non-optional message from caches that have optional `Message`. 61 | * Observables can stop their observations via `stopBeingObserved()` and `stopBeingObserved(by: observer)`. 62 | * `Weak` is available as observable transform `weak()` and is generally a regular transform object. 63 | * All transforms have mutable property `origin` which is the underlying observable whose messages get transformed. 64 | * It's possible for an observer to do multiple simultaneous observations of the same observable. 65 | 66 | 67 | ## v6 68 | 69 | * Memory management is new: 70 | * Before 6.0, memory leaks were *technically* impossible, because SwiftObserver still had a handle on dead observers, and you could flush them out when you wanted "to be sure". Now, dead observations are actually impossible and you don't need to worry about them. 71 | * Observers now automatically clean up when they die, so a call of `stopObserving()` in deinit can now be omitted. Observers can still call `stopObserving(observable)` and `stopObserving()` if they want to manually end observations. 72 | * `Observer` now has one protocol requirement, which is typically implemented as `let connections = Connections()`. The `connections` object keeps the `Observer`s observations alive. 73 | * A few memory management functions were removed since they were overkill and are definitely unnecessary now. 74 | * The new design scales better and should be more performant with ginormous amounts of observers and observables. 75 | * The `Observable` protocol has become simpler. 76 | * The requirement `var latestMessage: Message {get}` is gone. 77 | * No more message duplication in messengers since the `latestMessage` requirement is limited to `BufferedObservable`s. And so, switching buffering on or off on messengers is also no more concern. 78 | * Message buffering now happens exactly whenever it is really possible, that is whenever the observable is backed by an actual value (like variables are) and there is no filter involved in the observable. Filters annihilate random access pulling. The weirdness of a mapping having to ignore its filter in its implementation of `latestMessage` is gone. 79 | * `Observable` just requires one `Messenger`. 80 | * All observables are now implemented the same way and are thereby on equal footing. You could now easily reimplement `Var` and benefit from the order maintaining message queue of `Messenger`. 81 | * Custom observables are simpler to implement: 82 | * The protocol is the familiar `Observable`. No more separate `CustomObservable`. 83 | * The `typealias Message = MyMessageType` can now be omitted. 84 | * The need to use optional message types to be able to implement `latestMessage` is gone. 85 | * Observers can optionally receive the author of a message via an alternative closure wherever they normally pass in a message handler, even in combined observations. And observables can optionally attach an author other than themselves to a message, if they want to. 86 | * This major addition breaks no existing code and the author argument is only present when declared in the observer's message handler or the observable's `send` function. 87 | * This is hugely beneficial when observing shared mutable states like the repository / store pattern, really any storage abstraction, classic messengers (notifiers) and more. 88 | * Most importantly, an observer can now ignore messages that he himself triggered, even when the trigger was indirect. This avoids redundant and unintended reactions. 89 | * The internals are better implemented and more readable. 90 | * No forced unwraps for the unwrap transforms 91 | * No weird function and filter compositions 92 | * No more unnecessary indirection and redundance in adhoc observation transforms 93 | * Former "Mappings" are now separated into the three simple composable transforms: map, filter and unwrap. 94 | * The number of lines has actually decreased from about 1250 to about 1050. 95 | * The `ObservableObject` base class is gone. 96 | * Other consistency improvements and features: 97 | * An observer can now check whether it is observing an observable via `observer.isObserving(observable)`. 98 | * Stand-alone and ad hoc transforms now also include an `unwrap()` transform that requires no default message. 99 | * Message order is maintained for all observables, not just for variables. All observables use a message queue now. 100 | * The source of transforms cannot be reset as it was the case for mappings. As a nice side effect, the question whether a mapping fires when its source is reset is no concern anymore. 101 | * `Change` is more appropriately named `Update` since its properties `old` and `new` can be equal. 102 | * `Update` is `Equatable` when its `Value` is `Equatable`, so messages of variables can be selected via `select(Update(specificOldValue, specificNewValue))` or any specific value update you define. 103 | * The issue that certain Apple classes (like NSTextView) cannot directly be `Observable` because they can't be referenced weakly is gone. SwiftObserver now only references an `Observable`'s `messenger` weakly. 104 | 105 | ## v5.0.1 Consistent Variable Operators, SPM, Gitter 106 | 107 | * Removed 108 | * Variable string assignment operator 109 | * Variable number assignment operators 110 | 111 | * Changed 112 | * Reworked Documentation 113 | 114 | * Added 115 | * SPM Support 116 | * Gitter chat 117 | 118 | ## v5 Performance, Consistency, Expressiveness, Safety 119 | 120 | * **Renamings:** 121 | * Some memory management functions have been renamed to be more consistent with the overall terminology. 122 | * The type `Observable.Update` has been renamed to `Observable.Message`. 123 | * **Non-optional generic types:** Variables and messengers do no longer add implicit optionals to their generic value and message types. This makes it possible to create variables with non-optional values and the code is more explicit and consistent. 124 | * You can still initialize variables and messengers without argument, when you choose to make their value or message type optional. 125 | * **Dedicated observer pools:** All *observables* now maintain their own dedicated pool of *observers*. This improves many aspects: 126 | * All *observables* get highest performance 127 | * The whole API is more consistent 128 | * Custom *observable* implementations are more expressive and customizable 129 | * Memory management is easier as all *observables*, when they die, stop their observations 130 | * **Meaningful custom observables:** Custom *observables* now adopt the `CustomObservable` protocol. And they provide a `Messenger` instead of the `latestUpdate`. 131 | * As long as Swift can't infer the type, you'll also have to specify the associated `Message` type. 132 | * **Consistent variables:** 133 | * The operators on string- and number variables now work on all combinations of optional and non-optional generic and main types. For instance, string concatenation via `+` works on all pairs of `String`, `String?`, `Var`, `Var`, `Var?` and `Var?`. 134 | * All variables with values of type `String`, `Int`, `Float` and `Double` also have a non-optional property that is named after the value type (`string`, `int` ...). 135 | 136 | ## v4.2 Messengers 137 | 138 | * Added class `Messenger` 139 | * Added class `ObservabeObject` as a mostly internally used abstract base class for *observables*. `Var`, `Mapping` and `Messenger` now derive from `ObservableObject`. 140 | 141 | ## v4.1 Consistent Mappings 142 | 143 | * Made *Mapping* API more consistent 144 | * Renamed `prefilter` to `filter` 145 | * Added `filterMap` to *mappings* as their (mostly internally used) designated transform function 146 | * Removed `prefilter` argument from `map` function 147 | 148 | ## v4 Ad Hoc Mapping 149 | 150 | * Added Ad Hoc Mapping of observations 151 | * Added filter mapping `select` for all *observables* 152 | * Removed `filter` argument from `observe` function 153 | * Removed `select` argument from `observe` function 154 | * Removed `Log` (back to SwiftyToolz) 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for improving SwiftObserver! 2 | 3 | The only prerequisite you need, is that you've read about [SwiftObserver's philosophy](https://github.com/codeface-io/SwiftObserver/blob/master/Documentation/philosophy.md) to get a feeling for the purpose of SwiftObserver. 4 | 5 | There's no red tape involved here: 6 | 7 | 1. Fork the project 8 | 2. Create a branch that's meaningfully named by the thing you want to accomplish 9 | 3. Implement your idea 10 | 4. Make a pull request 11 | -------------------------------------------------------------------------------- /Code/CombineObserver/SwiftObserver+Combine.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Combine) 2 | 3 | import Combine 4 | import SwiftObserver 5 | 6 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) 7 | public extension ObservableCache { 8 | 9 | /** 10 | Create a `Publisher` that publishes all messages sent by this `ObservableCache` 11 | */ 12 | func publisher() -> PublisherOnObservableCache { .init(self) } 13 | } 14 | 15 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) 16 | public class PublisherOnObservableCache: Publisher, Observer { 17 | 18 | init(_ observable: O) { 19 | self.observable = observable 20 | publisher = .init(observable.latestMessage) 21 | observe(observable, receive: publisher.send) 22 | } 23 | 24 | public let receiver = Receiver() 25 | private let observable: O 26 | 27 | public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { 28 | publisher.receive(subscriber: subscriber) 29 | } 30 | 31 | private let publisher: CurrentValueSubject 32 | 33 | public typealias Output = O.Message 34 | public typealias Failure = Never 35 | } 36 | 37 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) 38 | public extension SwiftObserver.ObservableObject { 39 | 40 | /** 41 | Create a `Publisher` that publishes all messages sent by this `ObservableObject` 42 | */ 43 | func publisher() -> PublisherOnObservable { .init(self) } 44 | } 45 | 46 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) 47 | public class PublisherOnObservable: Publisher, Observer { 48 | 49 | init(_ observable: O) { 50 | self.observable = observable 51 | observe(observable, receive: publisher.send) 52 | } 53 | 54 | public let receiver = Receiver() 55 | private let observable: O 56 | 57 | public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { 58 | publisher.receive(subscriber: subscriber) 59 | } 60 | 61 | private let publisher = PassthroughSubject() 62 | 63 | public typealias Output = O.Message 64 | public typealias Failure = Never 65 | } 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /Code/SwiftObserver/AnyAuthor.swift: -------------------------------------------------------------------------------- 1 | /** 2 | ``ObservableObject/Message`` authors must be objects. 3 | 4 | Typically, the message author is either an ``Observer`` that triggered the ``ObservableObject/Message`` or the sending ``ObservableObject`` itself. 5 | */ 6 | public typealias AnyAuthor = AnyObject 7 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Connection.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | // MARK: - Connection 4 | 5 | class Connection 6 | { 7 | init(messenger: MessengerInterface, receiver: ReceiverInterface) 8 | { 9 | self.receiver = receiver 10 | self.receiverKey = receiver.key 11 | self.messenger = messenger 12 | self.messengerKey = messenger.key 13 | } 14 | 15 | func releaseFromReceiver() 16 | { 17 | receiver?.releaseConnection(with: messengerKey) 18 | } 19 | 20 | func unregisterFromMessenger() 21 | { 22 | messenger?.unregisterConnection(with: receiverKey) 23 | } 24 | 25 | let receiverKey: ReceiverKey 26 | weak var receiver: ReceiverInterface? 27 | 28 | let messengerKey: MessengerKey 29 | weak var messenger: MessengerInterface? 30 | } 31 | 32 | // MARK: - Receiver Interface 33 | 34 | extension ReceiverInterface 35 | { 36 | var key: ReceiverKey { ReceiverKey(self) } 37 | } 38 | 39 | protocol ReceiverInterface: AnyObject 40 | { 41 | func releaseConnection(with messengerKey: MessengerKey) 42 | } 43 | 44 | // MARK: - Messenger Interface 45 | 46 | extension MessengerInterface 47 | { 48 | var key: MessengerKey { MessengerKey(self) } 49 | } 50 | 51 | protocol MessengerInterface: AnyObject 52 | { 53 | func unregisterConnection(with receiverKey: ReceiverKey) 54 | } 55 | 56 | // MARK: - Keys 57 | 58 | typealias ReceiverKey = ObjectIdentifier 59 | typealias MessengerKey = ObjectIdentifier 60 | -------------------------------------------------------------------------------- /Code/SwiftObserver/ObservableObject/Messenger.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | extension Messenger: MessengerInterface {} 4 | 5 | /** 6 | The simplest `ObservableObject` and the basis of every other `ObservableObject` 7 | 8 | `Messenger` doesn't send messages by itself, but anyone can send messages through it and use it for any type of message: 9 | 10 | ```swift 11 | let textMessenger = Messenger() 12 | 13 | observer.observe(textMessenger) { textMessage in 14 | // respond to textMessage 15 | } 16 | 17 | textMessenger.send("my message") 18 | ``` 19 | */ 20 | public class Messenger 21 | { 22 | // MARK: - Life Cycle 23 | 24 | public init() {} 25 | 26 | // MARK: - Send Messages to Receivers 27 | 28 | internal func _send(_ message: Message, from author: AnyAuthor) 29 | { 30 | messagesFromAuthors += (message, author) 31 | 32 | if messagesFromAuthors.count > 1 { return } 33 | 34 | while let (message, author) = messagesFromAuthors.first 35 | { 36 | storedLatestAuthor = author 37 | receivers.receive(message, from: author) 38 | messagesFromAuthors.removeFirst() 39 | } 40 | } 41 | 42 | private var messagesFromAuthors = [(Message, AnyAuthor)]() 43 | 44 | internal var _latestAuthor: AnyAuthor { storedLatestAuthor ?? self } 45 | private weak var storedLatestAuthor: AnyAuthor? 46 | 47 | // MARK: - Manage Receivers 48 | 49 | internal func isConnected(to receiver: ReceiverInterface) -> Bool 50 | { 51 | receivers.contains(receiver) 52 | } 53 | 54 | internal func connect(_ receiver: ReceiverInterface, 55 | receive: @escaping (Message) -> Void) -> Connection 56 | { 57 | connect(receiver) { message, _ in receive(message) } 58 | } 59 | 60 | internal func connect(_ receiver: ReceiverInterface, 61 | receive: @escaping (Message, AnyAuthor) -> Void) -> Connection 62 | { 63 | receivers.add(receiver, for: self, receive: receive) 64 | } 65 | 66 | internal func disconnectReceiver(with receiverKey: ReceiverKey) 67 | { 68 | receivers.releaseConnectionFromReceiver(with: receiverKey) 69 | receivers.removeReceiver(with: receiverKey) 70 | } 71 | 72 | internal func disconnectAllReceivers() 73 | { 74 | receivers.releaseAllConnectionsFromReceivers() 75 | receivers.removeAll() 76 | } 77 | 78 | // MARK: - MessengerInterface 79 | 80 | internal func unregisterConnection(with receiverKey: ReceiverKey) 81 | { 82 | receivers.removeReceiver(with: receiverKey) 83 | } 84 | 85 | // MARK: - Receivers 86 | 87 | private let receivers = ReceiverPool() 88 | } 89 | -------------------------------------------------------------------------------- /Code/SwiftObserver/ObservableObject/ObservableCache.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableCache 2 | { 3 | /** 4 | Send the ``latestMessage`` to all ``Observer``s 5 | */ 6 | func send() { send(latestMessage) } 7 | } 8 | 9 | /** 10 | An ``ObservableObject`` that can provide its last sent (or some analogous) ``ObservableObject/Message`` 11 | 12 | `ObservableCache` has a function ``send()`` that sends ``latestMessage``. 13 | 14 | A typical `ObservableCache` derives its `latestMessage` from some form of state. 15 | 16 | ``Variable`` is the most prominent `ObservableCache` in SwiftObserver. Its ``latestMessage`` is an ``Update`` in which ``Update/old`` and ``Update/new`` both hold the current ``Variable/value``. 17 | */ 18 | public protocol ObservableCache: ObservableObject 19 | { 20 | /** 21 | Typically the last sent ``ObservableObject/Message`` or one that indicates that "nothing has changed" 22 | */ 23 | var latestMessage: Message { get } 24 | } 25 | -------------------------------------------------------------------------------- /Code/SwiftObserver/ObservableObject/ObservableObject+StopObservation.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | /** 4 | Ends all observations by all ``Observer``s 5 | */ 6 | func stopBeingObserved() 7 | { 8 | messenger.disconnectAllReceivers() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Code/SwiftObserver/ObservableObject/ObservableObject.swift: -------------------------------------------------------------------------------- 1 | extension Messenger: ObservableObject 2 | { 3 | public var messenger: Messenger { self } 4 | } 5 | 6 | public extension ObservableObject 7 | { 8 | /** 9 | Send a ``Message`` to all ``Observer``s. Optionally identify a message author. 10 | */ 11 | func send(_ message: Message, from author: AnyAuthor? = nil) 12 | { 13 | messenger._send(message, from: author ?? self) 14 | } 15 | 16 | internal var latestAuthor: AnyAuthor { messenger._latestAuthor } 17 | } 18 | 19 | /** 20 | An object that can be observed by multiple ``Observer``s 21 | 22 | ``Observer``s are responsible for starting an observation. Technically, observation means the observable object sends ``ObservableObject/Message``s to its observers via its ``ObservableObject/messenger``. 23 | */ 24 | public protocol ObservableObject: AnyAuthor 25 | { 26 | /** 27 | The ``Messenger`` that the `ObservableObject` uses to send ``Message``s to its ``Observer``s 28 | */ 29 | var messenger: Messenger { get } 30 | 31 | /** 32 | The type of message that the observable object can send to its ``Observer``s 33 | */ 34 | associatedtype Message: Any 35 | } 36 | -------------------------------------------------------------------------------- /Code/SwiftObserver/ObservableObject/ReceiverPool.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | class ReceiverPool 4 | { 5 | deinit 6 | { 7 | receiverReferences.values.forEach { $0.connection?.releaseFromReceiver() } 8 | } 9 | 10 | func receive(_ message: Message, from author: AnyAuthor) 11 | { 12 | receiverReferences.values.forEach 13 | { 14 | $0.receive(message, from: author) 15 | } 16 | } 17 | 18 | func contains(_ receiver: ReceiverInterface) -> Bool 19 | { 20 | receiverReferences[receiver.key]?.connection?.receiver === receiver 21 | } 22 | 23 | func add(_ receiver: ReceiverInterface, 24 | for messenger: MessengerInterface, 25 | receive: @escaping (Message, AnyAuthor) -> Void) -> Connection 26 | { 27 | if let existingReceiverReference = receiverReferences[receiver.key] 28 | { 29 | guard let connection = existingReceiverReference.connection else 30 | { 31 | log(error: "Connection is dead, meaning its owning receiver is dead, which shouldn't happen since receivers, before they die, unregister their connections from the respective messengers.") 32 | 33 | existingReceiverReference.messageHandlers = [receive] 34 | let connection = Connection(messenger: messenger, receiver: receiver) 35 | existingReceiverReference.connection = connection 36 | return connection 37 | } 38 | 39 | existingReceiverReference.messageHandlers += receive 40 | return connection 41 | } 42 | else 43 | { 44 | let connection = Connection(messenger: messenger, receiver: receiver) 45 | let reference = ReceiverReference(connection: connection, receive: receive) 46 | receiverReferences[receiver.key] = reference 47 | return connection 48 | } 49 | } 50 | 51 | func releaseConnectionFromReceiver(with receiverKey: ReceiverKey) 52 | { 53 | receiverReferences[receiverKey]?.connection?.releaseFromReceiver() 54 | } 55 | 56 | func releaseAllConnectionsFromReceivers() 57 | { 58 | receiverReferences.values.forEach 59 | { 60 | $0.connection?.releaseFromReceiver() 61 | } 62 | } 63 | 64 | func removeReceiver(with receiverKey: ReceiverKey) 65 | { 66 | receiverReferences[receiverKey] = nil 67 | } 68 | 69 | func removeAll() 70 | { 71 | receiverReferences.removeAll() 72 | } 73 | 74 | private var receiverReferences = [ReceiverKey : ReceiverReference]() 75 | 76 | private class ReceiverReference 77 | { 78 | init(connection: Connection, receive: @escaping (Message, AnyAuthor) -> Void) 79 | { 80 | self.connection = connection 81 | self.messageHandlers = [receive] 82 | } 83 | 84 | func receive(_ message: Message, from author: AnyAuthor) 85 | { 86 | guard let connection = connection else 87 | { 88 | return log(error: "Tried to send message via dead connection.") 89 | } 90 | 91 | guard connection.receiver != nil else 92 | { 93 | return log(error: "Tried to send message to dead receiver.") 94 | } 95 | 96 | messageHandlers.forEach 97 | { 98 | receive in receive(message, author) 99 | } 100 | } 101 | 102 | weak var connection: Connection? 103 | var messageHandlers: [(Message, _ from: AnyAuthor) -> Void] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Code/SwiftObserver/ObservableObject/Update.swift: -------------------------------------------------------------------------------- 1 | extension Update: Equatable where Value : Equatable 2 | { 3 | /** 4 | If `Value` is `Equatable`, this indicates whether the `Update` represents a value change 5 | */ 6 | public var isChange: Bool { old != new } 7 | } 8 | 9 | /** 10 | Intended as a value update ``ObservableObject/Message`` and employed in that way by ``Variable`` 11 | */ 12 | public struct Update 13 | { 14 | public init() where Value == Wrapped? 15 | { 16 | self.init(nil, nil) 17 | } 18 | 19 | public init(_ old: Value, _ new: Value) 20 | { 21 | self.old = old 22 | self.new = new 23 | } 24 | 25 | public let old: Value 26 | public let new: Value 27 | } 28 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Observer/ObservableObject+Observer.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | /** 4 | End all observations of this object that were started by the given ``Observer`` 5 | */ 6 | func stopBeingObserved(by observer: Observer) 7 | { 8 | messenger.disconnectReceiver(with: ReceiverKey(observer.receiver)) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Observer/Observer.swift: -------------------------------------------------------------------------------- 1 | public extension Observer 2 | { 3 | /** 4 | Whether the `Observer` has at least one observation of the ``ObservableObject`` going 5 | */ 6 | func isObserving(_ observable: O) -> Bool 7 | { 8 | observable.messenger.isConnected(to: receiver) 9 | } 10 | 11 | /** 12 | Receive the ``ObservableObject``'s future ``ObservableObject/Message``s together with their authors 13 | */ 14 | func observe(_ observable: O, 15 | receive: @escaping (O.Message, AnyAuthor) -> Void) 16 | { 17 | let messenger = observable.messenger 18 | let connection = messenger.connect(receiver, receive: receive) 19 | receiver.retain(connection) 20 | } 21 | 22 | /** 23 | Receive the ``ObservableObject``'s future ``ObservableObject/Message``s 24 | */ 25 | func observe(_ observable: O, 26 | receive: @escaping (O.Message) -> Void) 27 | { 28 | let messenger = observable.messenger 29 | let connection = messenger.connect(receiver, receive: receive) 30 | receiver.retain(connection) 31 | } 32 | 33 | /** 34 | End all the ``Observer``'s observations of this particular observed ``ObservableObject`` 35 | 36 | Note that one ``Observer`` can have multiple distinct ongoing observations of the same ``ObservableObject`` 37 | */ 38 | func stopObserving(_ observable: O?) 39 | { 40 | observable.forSome 41 | { 42 | receiver.disconnectMessenger(with: $0.messenger.key) 43 | } 44 | } 45 | 46 | /** 47 | End all the ``Observer``'s observations of all its observed ``ObservableObject``s 48 | */ 49 | func stopObserving() 50 | { 51 | receiver.disconnectAllMessengers() 52 | } 53 | } 54 | 55 | /** 56 | An object that can observe multiple ``ObservableObject``s 57 | 58 | One `Observer` can have multiple distinct ongoing observations of the same ``ObservableObject`` 59 | */ 60 | public protocol Observer: AnyObject 61 | { 62 | /** 63 | Required for receiving ``ObservableObject/Message``s from observed ``ObservableObject``s 64 | 65 | The ``Observer`` just holds on to its `receiver` strongly, so the `receiver`'s lifetime is bound to the ``Observer``s lifetime. 66 | 67 | Be careful to keep the `Observer` alive as long as it's supposed to observe, because when it dies, all its observations end as well. 68 | 69 | And the other way around: Be careful to not leak an `Observer` into memory, because that would also leak all its ongoing observations, if the corresponding ``ObservableObject``s don't explicitly end them. 70 | */ 71 | var receiver: Receiver { get } 72 | } 73 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Observer/Receiver.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | extension Receiver: ReceiverInterface {} 4 | 5 | /** 6 | The receiver every ``Observer`` needs for receiving ``ObservableObject/Message``s from ``ObservableObject``s 7 | */ 8 | public final class Receiver 9 | { 10 | // MARK: - Life Cycle 11 | 12 | public init() {} 13 | 14 | deinit 15 | { 16 | connections.values.forEach { $0.unregisterFromMessenger() } 17 | } 18 | 19 | // MARK: - Manage Connections 20 | 21 | internal func disconnectMessenger(with messengerKey: MessengerKey) 22 | { 23 | connections[messengerKey]?.unregisterFromMessenger() 24 | connections[messengerKey] = nil 25 | } 26 | 27 | internal func disconnectAllMessengers() 28 | { 29 | connections.values.forEach { $0.unregisterFromMessenger() } 30 | connections.removeAll() 31 | } 32 | 33 | internal func retain(_ connection: Connection) 34 | { 35 | if connection.receiver !== self 36 | { 37 | log(error: "\(Self.self) will retain a connection that points to a different \(Self.self).") 38 | } 39 | 40 | connections[connection.messengerKey] = connection 41 | } 42 | 43 | // MARK: - ReceiverInterface 44 | 45 | internal func releaseConnection(with messengerKey: MessengerKey) 46 | { 47 | connections[messengerKey] = nil 48 | } 49 | 50 | // MARK: - Connections 51 | 52 | private var connections = [MessengerKey: Connection]() 53 | } 54 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/AuthorFilter.swift: -------------------------------------------------------------------------------- 1 | public final class AuthorFilter: Messenger, Observer 2 | { 3 | public init(_ origin: O, 4 | _ keep: @escaping (AnyAuthor) -> Bool) 5 | { 6 | self.origin = origin 7 | self.keep = keep 8 | super.init() 9 | observe(origin: origin) 10 | } 11 | 12 | public var origin: O 13 | { 14 | willSet 15 | { 16 | stopObserving(origin) 17 | observe(origin: newValue) 18 | } 19 | } 20 | 21 | private func observe(origin: O) 22 | { 23 | observe(origin) 24 | { 25 | [weak self] message, author in 26 | 27 | guard let self = self else { return } 28 | 29 | if self.keep(author) 30 | { 31 | self.send(message, from: author) 32 | } 33 | } 34 | } 35 | 36 | private let keep: (AnyAuthor) -> Bool 37 | 38 | public let receiver = Receiver() 39 | } 40 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/Cache.swift: -------------------------------------------------------------------------------- 1 | public final class Cache: 2 | Messenger, 3 | ObservableCache, 4 | Observer 5 | where O.Message == Unwrapped 6 | { 7 | public init(_ origin: O) 8 | { 9 | self.origin = origin 10 | super.init() 11 | observe(origin: origin) 12 | } 13 | 14 | override func _send(_ message: Unwrapped?, from author: AnyAuthor) 15 | { 16 | latestMessage = message 17 | super._send(message, from: author) 18 | } 19 | 20 | public var latestMessage: Unwrapped? 21 | 22 | public var origin: O 23 | { 24 | willSet 25 | { 26 | stopObserving(origin) 27 | observe(origin: newValue) 28 | } 29 | } 30 | 31 | private func observe(origin: O) 32 | { 33 | observe(origin) 34 | { 35 | [weak self] message, author in 36 | 37 | self?._send(message, from: author) 38 | } 39 | } 40 | 41 | public let receiver = Receiver() 42 | } 43 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/CacheOnOptionalMessage.swift: -------------------------------------------------------------------------------- 1 | public final class CacheOnOptionalMessage: 2 | Messenger, 3 | ObservableCache, 4 | Observer 5 | where O.Message == Unwrapped? 6 | { 7 | public init(_ origin: O) 8 | { 9 | self.origin = origin 10 | super.init() 11 | observe(origin: origin) 12 | } 13 | 14 | override func _send(_ message: Unwrapped?, from author: AnyAuthor) 15 | { 16 | latestMessage = message 17 | super._send(message, from: author) 18 | } 19 | 20 | public var latestMessage: Unwrapped? 21 | 22 | public var origin: O 23 | { 24 | willSet 25 | { 26 | stopObserving(origin) 27 | observe(origin: newValue) 28 | } 29 | } 30 | 31 | private func observe(origin: O) 32 | { 33 | observe(origin) 34 | { 35 | [weak self] message, author in 36 | 37 | self?._send(message, from: author) 38 | } 39 | } 40 | 41 | public let receiver = Receiver() 42 | } 43 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/Filter.swift: -------------------------------------------------------------------------------- 1 | public final class Filter: Messenger, Observer 2 | { 3 | public init(_ origin: O, 4 | _ keep: @escaping (O.Message) -> Bool) 5 | { 6 | self.origin = origin 7 | self.keep = keep 8 | super.init() 9 | observe(origin: origin) 10 | } 11 | 12 | public var origin: O 13 | { 14 | willSet 15 | { 16 | stopObserving(origin) 17 | observe(origin: newValue) 18 | } 19 | } 20 | 21 | private func observe(origin: O) 22 | { 23 | observe(origin) 24 | { 25 | [weak self] message, author in 26 | 27 | guard let self = self else { return } 28 | 29 | if self.keep(message) 30 | { 31 | self.send(message, from: author) 32 | } 33 | } 34 | } 35 | 36 | internal let keep: (O.Message) -> Bool 37 | 38 | public let receiver = Receiver() 39 | } 40 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/Mapper+Cache.swift: -------------------------------------------------------------------------------- 1 | extension Mapper: ObservableCache where O: ObservableCache 2 | { 3 | public var latestMessage: Mapped 4 | { 5 | map(origin.latestMessage) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/Mapper.swift: -------------------------------------------------------------------------------- 1 | public class Mapper: Messenger, Observer 2 | { 3 | public init(_ origin: O, 4 | _ map: @escaping (O.Message) -> Mapped) 5 | { 6 | self.origin = origin 7 | self.map = map 8 | super.init() 9 | observe(origin: origin) 10 | } 11 | 12 | public var origin: O 13 | { 14 | willSet 15 | { 16 | stopObserving(origin) 17 | observe(origin: newValue) 18 | } 19 | } 20 | 21 | private func observe(origin: O) 22 | { 23 | observe(origin) 24 | { 25 | [weak self] message, author in 26 | 27 | guard let self = self else { return } 28 | 29 | self.send(self.map(message), from: author) 30 | } 31 | } 32 | 33 | internal let map: (O.Message) -> Mapped 34 | 35 | public let receiver = Receiver() 36 | } 37 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+Cache.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | public extension ObservableCache 4 | { 5 | func cache() -> CacheOnOptionalMessage 6 | { 7 | log(warning: warningWhenApplyingCache(messageIsOptional: true)) 8 | return CacheOnOptionalMessage(self) 9 | } 10 | 11 | func cache() -> Cache 12 | { 13 | log(warning: warningWhenApplyingCache(messageIsOptional: false)) 14 | return Cache(self) 15 | } 16 | } 17 | 18 | internal extension ObservableCache 19 | { 20 | func warningWhenApplyingCache(messageIsOptional: Bool) -> String 21 | { 22 | var warning = "\(Self.self) is already a Cache. Creating another Cache with it is likely pointless." 23 | 24 | if !messageIsOptional 25 | { 26 | warning += " And making the \(Message.self) message optional is likely unnecessary." 27 | } 28 | 29 | return warning 30 | } 31 | } 32 | 33 | public extension ObservableObject 34 | { 35 | func cache() -> CacheOnOptionalMessage 36 | { 37 | CacheOnOptionalMessage(self) 38 | } 39 | 40 | func cache() -> Cache 41 | { 42 | Cache(self) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+Filter.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | func filter(_ keep: @escaping (Message) -> Bool) -> Filter 4 | { 5 | Filter(self, keep) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+FilterAuthor.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | func filterAuthor(_ keep: @escaping (AnyAuthor) -> Bool) -> AuthorFilter 4 | { 5 | AuthorFilter(self, keep) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+From.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | func from(_ selectedAuthor: AnyAuthor) -> AuthorFilter 4 | { 5 | filterAuthor { [weak selectedAuthor] in $0 === selectedAuthor } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+Map.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | func map(_ map: @escaping (Message) -> Mapped) -> Mapper 4 | { 5 | Mapper(self, map) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+New.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | func new() -> Mapper 4 | where Message == Update 5 | { 6 | Mapper(self) { $0.new } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+NotFrom.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | func notFrom(_ excludedAuthor: AnyAuthor) -> AuthorFilter 4 | { 5 | filterAuthor { [weak excludedAuthor] in $0 !== excludedAuthor } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+Select.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject where Message: Equatable 2 | { 3 | func select(_ message: Message) -> Mapper, Void> 4 | { 5 | Mapper(Filter(self, { $0 == message })) { _ in } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+Unwrap.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | func unwrap() -> Unwrapper 4 | where Message == Wrapped? 5 | { 6 | Unwrapper(self) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+UnwrapWithDefault.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | func unwrap(_ defaultMessage: Wrapped) -> Mapper 4 | where Message == Wrapped? 5 | { 6 | Mapper(self) { $0 ?? defaultMessage } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/ObservableObject+Transforms/ObservableObject+Weak.swift: -------------------------------------------------------------------------------- 1 | public extension ObservableObject 2 | { 3 | func weak() -> Weak 4 | { 5 | Weak(self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/Unwrapper.swift: -------------------------------------------------------------------------------- 1 | public class Unwrapper: Messenger, Observer 2 | where O.Message == Unwrapped? 3 | { 4 | public init(_ origin: O) 5 | { 6 | self.origin = origin 7 | super.init() 8 | observe(origin: origin) 9 | } 10 | 11 | public var origin: O 12 | { 13 | willSet 14 | { 15 | stopObserving(origin) 16 | observe(origin: newValue) 17 | } 18 | } 19 | 20 | private func observe(origin: O) 21 | { 22 | observe(origin) 23 | { 24 | [weak self] optionalMessage, author in 25 | 26 | guard let self = self else { return } 27 | 28 | if let unwrappedMessage = optionalMessage 29 | { 30 | self.send(unwrappedMessage, from: author) 31 | } 32 | } 33 | } 34 | 35 | public let receiver = Receiver() 36 | } 37 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservableTransforms/Weak.swift: -------------------------------------------------------------------------------- 1 | public class Weak: Messenger, Observer 2 | { 3 | public init(_ origin: O) 4 | { 5 | self.origin = origin 6 | super.init() 7 | observe(origin: origin) 8 | } 9 | 10 | public weak var origin: O? 11 | { 12 | willSet 13 | { 14 | stopObserving(origin) 15 | newValue.forSome(observe(origin:)) 16 | } 17 | } 18 | 19 | private func observe(origin: O) 20 | { 21 | observe(origin) 22 | { 23 | [weak self] message, author in 24 | 25 | self?.send(message, from: author) 26 | } 27 | } 28 | 29 | public let receiver = Receiver() 30 | } 31 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer+Transforms/ObservationTransformer+Filter.swift: -------------------------------------------------------------------------------- 1 | public extension ObservationTransformer 2 | { 3 | func filter(_ keep: @escaping (Transformed) -> Bool, 4 | receiveFiltered: @escaping (Transformed, AnyAuthor) -> Void) 5 | { 6 | startObservation 7 | { 8 | message, author in 9 | 10 | if keep(message) { receiveFiltered(message, author) } 11 | } 12 | } 13 | 14 | func filter(_ keep: @escaping (Transformed) -> Bool, 15 | receiveFiltered: @escaping (Transformed) -> Void) 16 | { 17 | startObservation 18 | { 19 | message, _ in 20 | 21 | if keep(message) { receiveFiltered(message) } 22 | } 23 | } 24 | 25 | func filter(_ keep: @escaping (Transformed) -> Bool) -> ObservationTransformer 26 | { 27 | ObservationTransformer 28 | { 29 | receiveFiltered in 30 | 31 | self.startObservation 32 | { 33 | message, author in 34 | 35 | if keep(message) { receiveFiltered(message, author) } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer+Transforms/ObservationTransformer+FilterAuthor.swift: -------------------------------------------------------------------------------- 1 | public extension ObservationTransformer 2 | { 3 | func filterAuthor(_ keep: @escaping (AnyAuthor) -> Bool, 4 | receiveFiltered: @escaping (Transformed, AnyAuthor) -> Void) 5 | { 6 | startObservation 7 | { 8 | message, author in 9 | 10 | if keep(author) { receiveFiltered(message, author) } 11 | } 12 | } 13 | 14 | func filterAuthor(_ keep: @escaping (AnyAuthor) -> Bool, 15 | receiveFiltered: @escaping (Transformed) -> Void) 16 | { 17 | startObservation 18 | { 19 | message, author in 20 | 21 | if keep(author) { receiveFiltered(message) } 22 | } 23 | } 24 | 25 | func filterAuthor(_ keep: @escaping (AnyAuthor) -> Bool) -> ObservationTransformer 26 | { 27 | ObservationTransformer 28 | { 29 | receiveFiltered in 30 | 31 | self.startObservation 32 | { 33 | message, author in 34 | 35 | if keep(author) { receiveFiltered(message, author) } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer+Transforms/ObservationTransformer+From.swift: -------------------------------------------------------------------------------- 1 | public extension ObservationTransformer 2 | { 3 | func from(_ selectedAuthor: AnyAuthor?, 4 | receiveSelected: @escaping (Transformed, AnyAuthor) -> Void) 5 | { 6 | filterAuthor({ [weak selectedAuthor] in $0 === selectedAuthor}, 7 | receiveFiltered: receiveSelected) 8 | } 9 | 10 | func from(_ selectedAuthor: AnyAuthor?, 11 | receiveSelected: @escaping (Transformed) -> Void) 12 | { 13 | filterAuthor({ [weak selectedAuthor] in $0 === selectedAuthor }, 14 | receiveFiltered: receiveSelected) 15 | } 16 | 17 | func from(_ selectedAuthor: AnyAuthor?) -> ObservationTransformer 18 | { 19 | filterAuthor { [weak selectedAuthor] in $0 === selectedAuthor } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer+Transforms/ObservationTransformer+Map.swift: -------------------------------------------------------------------------------- 1 | public extension ObservationTransformer 2 | { 3 | func map(_ map: @escaping (Transformed) -> Mapped, 4 | receiveMapped: @escaping (Mapped, AnyAuthor) -> Void) 5 | { 6 | startObservation 7 | { 8 | message, author in 9 | 10 | receiveMapped(map(message), author) 11 | } 12 | } 13 | 14 | func map(_ map: @escaping (Transformed) -> Mapped, 15 | receiveMapped: @escaping (Mapped) -> Void) 16 | { 17 | startObservation 18 | { 19 | message, _ in 20 | 21 | receiveMapped(map(message)) 22 | } 23 | } 24 | 25 | func map(_ map: @escaping (Transformed) -> Mapped) -> ObservationTransformer 26 | { 27 | ObservationTransformer 28 | { 29 | receiveMapped in 30 | 31 | self.startObservation 32 | { 33 | message, author in 34 | 35 | receiveMapped(map(message), author) 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer+Transforms/ObservationTransformer+New.swift: -------------------------------------------------------------------------------- 1 | public extension ObservationTransformer 2 | { 3 | func new(receiveNew: @escaping (Value, AnyAuthor) -> Void) 4 | where Transformed == Update 5 | { 6 | map({ $0.new }, receiveMapped: receiveNew) 7 | } 8 | 9 | func new(receiveNew: @escaping (Value) -> Void) 10 | where Transformed == Update 11 | { 12 | map({ $0.new }, receiveMapped: receiveNew) 13 | } 14 | 15 | func new() -> ObservationTransformer 16 | where Transformed == Update 17 | { 18 | map { $0.new } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer+Transforms/ObservationTransformer+NotFrom.swift: -------------------------------------------------------------------------------- 1 | public extension ObservationTransformer 2 | { 3 | func notFrom(_ excludedAuthor: AnyAuthor, 4 | receiveExcluded: @escaping (Transformed, AnyAuthor) -> Void) 5 | { 6 | filterAuthor({ [weak excludedAuthor] in $0 !== excludedAuthor }, 7 | receiveFiltered: receiveExcluded) 8 | } 9 | 10 | func notFrom(_ excludedAuthor: AnyAuthor, 11 | receiveExcluded: @escaping (Transformed) -> Void) 12 | { 13 | filterAuthor({ [weak excludedAuthor] in $0 !== excludedAuthor }, 14 | receiveFiltered: receiveExcluded) 15 | } 16 | 17 | func notFrom(_ excludedAuthor: AnyAuthor) -> ObservationTransformer 18 | { 19 | filterAuthor { [weak excludedAuthor] in $0 !== excludedAuthor } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer+Transforms/ObservationTransformer+Select.swift: -------------------------------------------------------------------------------- 1 | public extension ObservationTransformer where Transformed: Equatable 2 | { 3 | func select(_ message: Transformed, 4 | receiveSelected: @escaping (AnyAuthor) -> Void) 5 | { 6 | filter({ $0 == message }) { _, author in receiveSelected(author) } 7 | } 8 | 9 | func select(_ message: Transformed, 10 | receiveSelected: @escaping () -> Void) 11 | { 12 | filter({ $0 == message }).map({ _ in }, receiveMapped: receiveSelected) 13 | } 14 | 15 | func select(_ message: Transformed) -> ObservationTransformer 16 | { 17 | filter({ $0 == message }).map { _ in } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer+Transforms/ObservationTransformer+Unwrap.swift: -------------------------------------------------------------------------------- 1 | public extension ObservationTransformer 2 | { 3 | func unwrap(receiveUnwrapped: @escaping (Unwrapped, AnyAuthor) -> Void) 4 | where Transformed == Unwrapped? 5 | { 6 | startObservation 7 | { 8 | message, author in 9 | 10 | if let unwrapped = message { receiveUnwrapped(unwrapped, author) } 11 | } 12 | } 13 | 14 | func unwrap(receiveUnwrapped: @escaping (Unwrapped) -> Void) 15 | where Transformed == Unwrapped? 16 | { 17 | startObservation 18 | { 19 | message, _ in 20 | 21 | if let unwrapped = message { receiveUnwrapped(unwrapped) } 22 | } 23 | } 24 | 25 | func unwrap() -> ObservationTransformer 26 | where Transformed == Unwrapped? 27 | { 28 | ObservationTransformer 29 | { 30 | receiveUnwrapped in 31 | 32 | self.startObservation 33 | { 34 | message, author in 35 | 36 | if let unwrapped = message { receiveUnwrapped(unwrapped, author) } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer+Transforms/ObservationTransformer+UnwrapWithDefault.swift: -------------------------------------------------------------------------------- 1 | public extension ObservationTransformer 2 | { 3 | func unwrap(_ defaultMessage: Unwrapped, 4 | receiveUnwrapped: @escaping (Unwrapped, AnyAuthor) -> Void) 5 | where Transformed == Unwrapped? 6 | { 7 | map({ $0 ?? defaultMessage }, receiveMapped: receiveUnwrapped) 8 | } 9 | 10 | func unwrap(_ defaultMessage: Unwrapped, 11 | receiveUnwrapped: @escaping (Unwrapped) -> Void) 12 | where Transformed == Unwrapped? 13 | { 14 | map({ $0 ?? defaultMessage }, receiveMapped: receiveUnwrapped) 15 | } 16 | 17 | func unwrap(_ defaultMessage: Unwrapped) -> ObservationTransformer 18 | where Transformed == Unwrapped? 19 | { 20 | map { $0 ?? defaultMessage } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/ObservationTransformer.swift: -------------------------------------------------------------------------------- 1 | public struct ObservationTransformer 2 | { 3 | public func receive(_ receive: @escaping (Transformed, AnyAuthor) -> Void) 4 | { 5 | startObservation(receive) 6 | } 7 | 8 | public func receive(_ receive: @escaping (Transformed) -> Void) 9 | { 10 | startObservation 11 | { 12 | message, _ in receive(message) 13 | } 14 | } 15 | 16 | internal let startObservation: (@escaping (Transformed, AnyAuthor) -> Void) -> Void 17 | } 18 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Transforms/ObservationTransforms/Observer+ObservationTransformer.swift: -------------------------------------------------------------------------------- 1 | public extension Observer 2 | { 3 | func observe(_ observable: O) -> ObservationTransformer 4 | { 5 | ObservationTransformer 6 | { 7 | receive in self.observe(observable, receive: receive) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Variable/Variable+Comparable.swift: -------------------------------------------------------------------------------- 1 | extension Var: Comparable where Value: Comparable 2 | { 3 | public static func < (lhs: Variable, 4 | rhs: Variable) -> Bool 5 | { 6 | lhs.value < rhs.value 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Variable/Variable+ObservableCache.swift: -------------------------------------------------------------------------------- 1 | extension Variable: ObservableCache 2 | { 3 | /** 4 | An ``Update`` in which ``Update/old`` and ``Update/new`` both hold the ``Variable``'s current ``Variable/value`` 5 | */ 6 | public var latestMessage: Update 7 | { 8 | Update(value, value) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Variable/Variable+PropertyWrapper.swift: -------------------------------------------------------------------------------- 1 | /** 2 | Make an `Equatable` variable property observable by ``Observer``s 3 | 4 | The ``projectedValue`` provides the actual ``Variable`` so it can be observed: 5 | 6 | ```swift 7 | @ObservableVar var number = 7 8 | 9 | observer.observe($number) { numberUpdate in 10 | let numberChange = numberUpdate.new - numberUpdate.old 11 | } 12 | ``` 13 | */ 14 | @propertyWrapper 15 | public struct ObservableVar 16 | { 17 | public var projectedValue: Var { variable } 18 | 19 | public var wrappedValue: Value 20 | { 21 | get { variable.value } 22 | set { variable.value = newValue } 23 | } 24 | 25 | public init(wrappedValue: Value) 26 | { 27 | variable = Var(wrappedValue) 28 | } 29 | 30 | private let variable: Var 31 | } 32 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Variable/Variable+ValueAssignment.swift: -------------------------------------------------------------------------------- 1 | infix operator <-: AssignmentPrecedence 2 | 3 | public func <-(variable: Var?, value: Value) 4 | { 5 | variable?.value = value 6 | } 7 | -------------------------------------------------------------------------------- /Code/SwiftObserver/Variable/Variable.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | extension Var: Codable where Value: Codable {} 4 | 5 | public typealias Var = Variable 6 | 7 | /** 8 | An observable wrapper object that makes changes of its contained `Value` observable 9 | */ 10 | public final class Variable: Messenger>, Equatable 11 | { 12 | // MARK: - Initialization 13 | 14 | public convenience init() where Value == Wrapped? 15 | { 16 | self.init(nil) 17 | } 18 | 19 | public init(_ value: Value) 20 | { 21 | storedValue = value 22 | } 23 | 24 | // MARK: - Equatable 25 | 26 | /** 27 | Two `Variable`s count as equal when their ``value``s are equal 28 | */ 29 | public static func == (lhs: Variable, 30 | rhs: Variable) -> Bool 31 | { 32 | lhs.storedValue == rhs.storedValue 33 | } 34 | 35 | // MARK: - Value 36 | 37 | /** 38 | The actual stored `Value`. The `Variable` sends an ``Update`` when its `value` changes 39 | */ 40 | public var value: Value 41 | { 42 | get { storedValue } 43 | set { set(newValue, as: self) } 44 | } 45 | 46 | /** 47 | Set ``value`` itentifying the author of the potentially triggered ``Update`` message 48 | */ 49 | public func set(_ value: Value, as author: AnyAuthor) 50 | { 51 | let oldValue = storedValue 52 | 53 | if value != oldValue 54 | { 55 | storedValue = value 56 | send(Update(oldValue, value), from: author) 57 | } 58 | } 59 | 60 | private enum CodingKeys: String, CodingKey { case storedValue } 61 | 62 | private var storedValue: Value 63 | } 64 | -------------------------------------------------------------------------------- /Documentation/Architecture/CombineObserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftObserver/d171dcb3aefe848ca25591f81a8aba709026a839/Documentation/Architecture/CombineObserver.png -------------------------------------------------------------------------------- /Documentation/Architecture/ObservableObject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftObserver/d171dcb3aefe848ca25591f81a8aba709026a839/Documentation/Architecture/ObservableObject.png -------------------------------------------------------------------------------- /Documentation/Architecture/Observer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftObserver/d171dcb3aefe848ca25591f81a8aba709026a839/Documentation/Architecture/Observer.png -------------------------------------------------------------------------------- /Documentation/Architecture/SwiftObserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftObserver/d171dcb3aefe848ca25591f81a8aba709026a839/Documentation/Architecture/SwiftObserver.png -------------------------------------------------------------------------------- /Documentation/Architecture/Transforms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftObserver/d171dcb3aefe848ca25591f81a8aba709026a839/Documentation/Architecture/Transforms.png -------------------------------------------------------------------------------- /Documentation/Architecture/Variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftObserver/d171dcb3aefe848ca25591f81a8aba709026a839/Documentation/Architecture/Variable.png -------------------------------------------------------------------------------- /Documentation/Architecture/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | The following diagrams show the internal architecture (composition and dependencies) of all top-level source folders. They were generated with [Codeface](https://codeface.io): 4 | 5 | ## SwiftObserver 6 | 7 | ![](SwiftObserver.png) 8 | 9 | ## Transforms 10 | 11 | ![](Transforms.png) 12 | 13 | ## Variable 14 | 15 | ![](Variable.png) 16 | 17 | ## Observer 18 | 19 | ![](Observer.png) 20 | 21 | ## ObservableObject 22 | 23 | ![](ObservableObject.png) 24 | 25 | ## CombineObserver 26 | 27 | ![](CombineObserver.png) 28 | -------------------------------------------------------------------------------- /Documentation/philosophy.md: -------------------------------------------------------------------------------- 1 | # Philosophy and Features 2 | 3 | This is the opinionated side of SwiftObserver. I invite you put it on like a shoe. See if it fits, take it for what it's worth and evolve it via PR or email: . 4 | 5 | * [What You Might Like](#what-you-might-like) 6 | * [Meaningful Code](#meaningful-code) 7 | * [Non-intrusive Design](#non-intrusive-design) 8 | * [Simplicity and Flexibility](#simplicity-and-flexibility) 9 | * [Safety](#safety) 10 | * [Why Combined Observation is Overrated](#why-combined-observation-is-overrated) 11 | * [What You Might Not Like](#what-you-might-not-like) 12 | 13 | # What You Might Like 14 | 15 | ## Meaningful Code 16 | 17 | * Readable code down to the internals 18 | 19 | > I comb internal code with as much regularity and care as if it was public API, so you can peek under the hood to understand SwiftObserver perfectly. 20 | 21 | * Meaningful expressive metaphors 22 | 23 | * No arbitrary, contrived or technical metaphors like "disposable", "dispose bag", "signal", "emitter", "stream" or "sequence" 24 | 25 | > A note on "signals": In the tradition of Elm and the origins of reactive programming, many reactive libraries use "signal" as a metaphor, but how they apply the term is more confusing than helpful, in particular when they suggest that the "signal" is what's being observed. 26 | > 27 | > Our closest context of reference here is information theory, where a signal is what's being technically transmitted from a source to a receiver. By observing the source, the receiver receives a signal which conveys messages. Would we apply the metaphor to reactive programming, the signal would rather correspond to the actual data that *observables* send to *observers*. 28 | 29 | - Meaningful (semantically consistent) metaphor combinations 30 | 31 | > That is: no combination of incompatible metaphors that stem from completely different domains 32 | 33 | > A common and nonsensical mixture is "subscribing" to a "signal". Even Elm, which had signals and still has subscriptions, never mixed the two. 34 | > 35 | > "subscribing" to an "observable" doesn't make much sense either. Why isn't it a "subscribable" then? Why is it a radical idea to "observe" an "observable"? Is an "observable" a publication? 36 | 37 | - A meaningful level of abstraction that's focused on the essential *Observer Pattern* 38 | 39 | > SwiftObserver is pragmatic and doesn't overgeneralize the *Observer Pattern* in any arbitrary direction. It doesn't go overboard with models like "streams" or "sequences" but keeps things more simple, real-world oriented and meaningful to actual application domains. 40 | 41 | - Meaningful code at the point of use (no technical boilerplate) 42 | 43 | - No mediating property on *observables* for starting observations or creating mappings 44 | - No "tokens" and the like to pass around or store 45 | - No memory management boilerplate code at the point of observation 46 | - No tuple destructuring in combined observations 47 | 48 | - Meaningful syntax 49 | 50 | - The syntax reflects the intent and metaphor of the *Observer Pattern*: *Observers* are active subjects while *observables* are passive objects which are unconcerned about being observed: 51 | 52 | ```swift 53 | dog.observe(sky) 54 | observer.observe(observable) 55 | subject.actUpon(object) 56 | ``` 57 | 58 | > Note: Many definitions of the *Observer Pattern*, including [Wikipedia](https://en.wikipedia.org/wiki/Observer_pattern), have the subject / object roles reversed, which we consider not merely a misnomer but, above all, a secondary level of analysis. 59 | > 60 | > They look at observation from a technical rather than a conceptual point of view, focusing on *how* the problem is being *solved* rather than *what* the solution *means*. 61 | > 62 | > The illusion the *Observer Pattern* is supposed to create is that an *observer* observes an *observable*. Linguistically, that is: subject, predicate, object. The subject actively acts on the object, while the object is passively being acted upon. 63 | > 64 | > Of course, to achieve this under the hood, *observables* must actively trigger some data propagation. But we should look at the solution more pragmatically in terms of the real-world meaning that we set out to model in the first place. 65 | 66 | ## Non-intrusive Design 67 | 68 | - No delegate protocols to implement 69 | 70 | - Custom *observables* without having to inherit from any base class 71 | 72 | - You're in control of the ancestral tree of your classes. 73 | - All classes can easily be observed, even views and view controllers. 74 | - You can keep model and logic code independent of any observer frameworks and techniques. 75 | 76 | > If the model layer had to be stuffed with heavyweight constructs just to be observed, it would become a technical issue rather than an easy to change, meaningful, direct representation of domain-, business- and view logic. 77 | 78 | - No restrictions on how you organize, store or persist the state of your your app 79 | 80 | * You can freely model your domain-, business- and view logic with all your familiar design patterns and types. 81 | 82 | - No optional optionals 83 | 84 | - You have full control over value and *message* types. 85 | - You can make your *message* types optional. SwiftObserver will never spit them back at you wrapped in additional optionals, not even in combined observations. 86 | - You can easily unwrap optional *messages* via the *mapping* `unwrap`. 87 | 88 | - No under the hood side effects in terms of ownership and life cycles 89 | 90 | * You stay in control of when objects die and of which objects own which others. 91 | * Your code stays explicit. 92 | 93 | - No duplication 94 | 95 | - SwiftObserver never duplicates the *messages* that are being sent around, in particular in [combined observations](#combined-observations) and transforms. This is in stark contrast to other reactive libraries yet without compomising functional aspects. 96 | 97 | > Note: Not having to duplicate data where multiple things must be observed is one of the reasons to use combined observations in the first place. However, some reactive libraries choose to not make full use of object-orientation, so far that the combined observables could be value types. This forces these libraries to duplicate data by caching the messages sent from observables. 98 | > 99 | > SwiftObserver not only leverages object-orientation, for combined observations, it also offers a regular "pull model" in which observers can pull messages from observables, in addition to the typical reactive "push model" in which observables push their messages to observers. 100 | > 101 | > "Pulling" just reflects the natural way objects operate. Observers can act on observables without problem, since that is the actual technical direction of control and dependence. The problem that reactive techiques solve is propagating data **against** the direction of control. 102 | > 103 | > A "pull model" is also in line with functional programming: Instead of caching state, the combined observation calls and combines functions on observables. 104 | 105 | ## Simplicity and Flexibility 106 | 107 | - Very few simple but universal concepts and types 108 | 109 | - Pure Swift for clean modelling, not even any dependence on `Foundation` 110 | 111 | - Transforms can be instantiated as first-class *observables* that can be treated like any other *observable*. 112 | 113 | - One universal consistent syntax for transforming *messages* and chaining these transformations 114 | 115 | - Use a small but universal set of prebuilt transformations wherever you transform *messages*: 116 | 117 | - `map` 118 | - `new` 119 | - `unwrap` (with and without default) 120 | - `filter` 121 | - `select` 122 | - `filterAuthor` 123 | - `from` 124 | - `notFrom` 125 | 126 | - One universal (combined) observation 127 | 128 | - One function to observe 1-3 *observables* 129 | - Still, you get all the power of combined observation. 130 | - Combined observation has no special syntax and imposes no additional cognitive load. 131 | - [Here's more on the nature of combined observation](#combined-observation-is-overrated) 132 | 133 | - Create an *observable* plus a chain of *transforms* in one line. 134 | 135 | - Observe an *observable* with an ad-hoc chain of transformations in one line. 136 | 137 | - Use the `<-` operator to directly set variable values. 138 | 139 | - Use common operators directly on number- and string variables. 140 | 141 | - Variables are `Codable`, so model types are easy to encode and persist. 142 | 143 | - Pull the current *message* from any caching *observable* via `latestMessage`. 144 | 145 | - Receive the old **and** new value from variables 146 | 147 | - Seamless coverage of the *Messenger Pattern* (or *Notifier Pattern*) via the `Messenger` class 148 | 149 | - Reference any *observable* weakly by wrapping it in `Weak` 150 | 151 | - Hold `weak` references to *observables* in a data structure: 152 | 153 | ```swift 154 | let strongNumber = Var(12) 155 | var arrayOfWeakNumbers = [Weak>]() 156 | arrayOfWeakNumbers.append(Weak(strongNumber)) 157 | ``` 158 | 159 | - Create transforms that hold their sources weakly: 160 | 161 | ```swift 162 | let strongNumber = Var(12) 163 | let toString = Weak(strongNumber).new().map { "\($0)" } 164 | ``` 165 | 166 | ## Safety 167 | 168 | - When an observers or observables die, their observations stop automatically. 169 | - Memory leaks are impossible. 170 | - Stop observations in the same expressive way you start them: `observer.stopObserving(observable)` 171 | - Stop **all** observations of an *observer* with **one** call: `observer.stopObserving()` 172 | 173 | # Why Combined Observation is Overrated 174 | 175 | Other reactive libraries dump the combine functions `merge`, `zip` and `combineLatest` on your brain. And at one point, I was convinced combined observations are an essential part of reactive programming. Practice has changed my mind. SwiftObserver offers no combined observation anymore. This decision is the result of a long process, involving many practical applications, discovering what's really essential, and letting go of big fancy features, one by one. 176 | 177 | It has emerged as part of the philosophy (or insight if you will) on which SwiftObserver is built, that combined observation is a non-essential feature to the purpose of the observer pattern, dependency inversion, reactive code design and clean architecture. I would even argue that combined observation is an anti pattern. So SwiftObserver will not blow up its complexity or compromise its elegance, consistency or principles, just to support combined observation. 178 | 179 | Combined observation can always easily be replaced by single observations. Each single observation would just call a function that does the "combined" update and pulls the necessary data from wherever it needs to. 180 | 181 | `combineLatest` is by far the most used combine function in practice and covers practically all "needs" for combined observation. The above suggested update function can be equivalent to `combineLatest` when the involved observables conform to `ObservableCache`, so the update function can directly pull the latest message from them. `ObservableCache` generally reduces the need for explicit combine functions in the first place, since the interesting data can often be pulled directly from *observables*. 182 | 183 | As the founder of SwiftObserver, I even have to note, that I don't use combined observation anymore at all. It never offers me a benefit over single observation in practice. To the contrary: Managing observations proves to be harder when they're coupled. 184 | 185 | My hunch is that `merge`, `zip` and `combineLatest` in other reactive libraries originate less from practical need and more from a desire to gerneralize and to max out the metaphors of "data streams" or "sequences". The underlying conceptual mis-alignment here would be, that *observables* in an *observer-observable* relationship are really **supposed** to send **messages** rather than anonymous **data**. I'll explain why. 186 | 187 | All that is required for dependency inversion is that the *observer* gets informed about events that it might need to react to. The *observer* then decides whether to act at all, how to act and what data it requires, and it pulls exactly that data from wherever it needs to, including from *observables*. It is not the job of the *observable* to presume what data any *observer* might need. It's not supposed to depend on the *observer's* concerns, which is the whole reason why we invert that dependency through the *Observer Pattern*. The *observable's* job is just to tell what happened. 188 | 189 | So, in a clean decoupled design that adheres to the idea of the *ObserverPattern*, *observables* naturally send **messages** rather than **data**, and combining *messages* wouldn't be as meaningful or helpful. 190 | 191 | # What You Might Not Like 192 | 193 | - Not conform to Rx (the semi standard of reactive programming) 194 | - SwiftObserver is focused on the foundation of reactive programming. UI bindings are available as [UIObserver](https://github.com/flowtoolz/UIObserver), but that framework is still in its infancy. You're welcome to make PRs. 195 | - Observers and observables must be objects and cannot be of value types. However: 196 | 1. Variables can hold any type of values and observables can send any type of messages. 197 | 2. We found that entities active enough to observe or significant enough to be observed are typically not mere values that are being passed around. What's being passed around are the messages that observables send to observers, and those messages are prototypical value types. 198 | 3. For fine granular observing, the `Var` type is appropriate, further reducing the "need" (or shall we say "anti pattern"?) to observe value types. 199 | -------------------------------------------------------------------------------- /Documentation/specific-patterns.md: -------------------------------------------------------------------------------- 1 | # Specific Patterns 2 | 3 | This document describes a few patterns that emerged from usage. 4 | 5 | In general, SwiftObserver meets almost all needs for callbacks and continuous propagation of data up the control hierarchy (against the direction of control). Typical applications are the propagation of data from domain model to use cases, from use cases to view models, from view models to views, and from views to view controllers. 6 | 7 | ## The Messenger Pattern 8 | 9 | When *observer* and *observable* need to be more decoupled, it is common to use a mediating *observable* through which any object can anonymously send *messages*. An example of this mediator is [`NotificationCenter`](https://developer.apple.com/documentation/foundation/notificationcenter). 10 | 11 | This use of the *Observer Pattern* is sometimes called *Messenger*, *Notifier*, *Dispatcher*, *Event Emitter* or *Decoupler*. Its main differences to direct observation are: 12 | 13 | - The actual *observable*, which is the messenger, sends no *messages* by itself. 14 | - Every object can trigger *messages*, without adopting any protocol. 15 | - Multiple sending objects trigger the same type of *messages*. 16 | - An *observer* may indirectly observe multiple other objects through one observation. 17 | - *Observers* don't care as much who triggered a *message*. 18 | - *Observer* types don't need to depend on the types that trigger *messages*. 19 | 20 | SwiftObserver's class `Messenger` covers the messenger pattern directly. 21 | 22 | ## Stored Messenger 23 | 24 | A *Stored Messenger* is the bare bone pattern tht defines `Observable`. However, sometimes we have to implement observability more implicitly, without conforming to `Observable`. 25 | 26 | Instead of making a class `C` directly observable through `Observable`, you just give it a messenger as a property. `C` sends its updates via its messenger, and observers of `C` actually observe the messenger of `C`: 27 | 28 | ~~~swift 29 | class C { 30 | let messenger = Messenger() // C is indirectly observable via messenger 31 | } 32 | ~~~ 33 | 34 | And why would you want that? Avoiding explicit conformane to `Observable` helps in two scenarios ... 35 | 36 | ### 1. Require Specific Observability in an Interface 37 | 38 | We want to declare a variable or constant as conforming to an interface (let's say `Database`) specifying (among other functionality) observability with a specific message type (say `DatabaseUpdate`). 39 | 40 | #### Challenge 41 | 42 | We don't want to define an abstract base class because objects conforming to the interface should be able to derive from their own (and more meaningful) class (like `ICloudDatabase`). 43 | 44 | Now, we would want to define a protocol like this: 45 | 46 | ~~~swift 47 | protocol Database: Observable where Message == DatabaseUpdate { 48 | // declare other database functionality 49 | } 50 | ~~~ 51 | 52 | But this protocol could only be used as a generic constraint because it has an associated type requirement (Swift doesn't have generalized existentials yet). 53 | 54 | We can't declare a variable or constant of the protocol type `Database`, like we are used to with delegate protocols: 55 | 56 | ~~~swift 57 | weak var delegate: MyDelegateProtocol 58 | // ^^ perfectly fine 59 | 60 | var database: Database 61 | // ^^ compiler error: Protocol 'Database' can only be used as a generic constraint because it has Self or associated type requirements 62 | ~~~ 63 | 64 | #### Solution 65 | 66 | We use a `Database` protocol but without any conformance to `Observable`. Instead, we only require the `Database` to have a messenger: 67 | 68 | ~~~swift 69 | protocol Database { 70 | var messenger: Messenger { get } 71 | // declare other database functionality 72 | } 73 | ~~~ 74 | 75 | Now, in contrast to an `Observable`, we must manually route all observation of the database through its messenger, but at least it works. 76 | 77 | ### 2. Inherit and Extend Observability 78 | 79 | Consider this case: I have a generic class `Tree`. It is a `Observable`, so tree nodes can observe their branches. Then I have an `Item` which derives from `Tree`. `Item` cannot extend or override the `Tree.Message`. 80 | 81 | In order to further specify what items can send to their observers, the `Tree` must use its messenger without direct conformance to `Observable`. This tree messenger should (somewhat redundantly) be named after its class: `treeMessenger`, so that there's no confusion in inheriting classes about which ancestor owns the messenger. 82 | 83 | Generally speaking, to be able to extend observability in derived classes, observability should be more granular in the base class. Instead of making the base class as a whole observable, one could give it observable properties or dedicated messengers. -------------------------------------------------------------------------------- /Documentation/swift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftObserver/d171dcb3aefe848ca25591f81a8aba709026a839/Documentation/swift.png -------------------------------------------------------------------------------- /Documentation/swift_original.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftObserver/d171dcb3aefe848ca25591f81a8aba709026a839/Documentation/swift_original.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017-present, Sebastian Fichtner; sebastian@codeface.io; https://github.com/codeface-io/SwiftObserver 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Checklist: 2 | 3 | * If possible: include a test for this. 4 | * Link to github issue if one exists 5 | * Describe intention: What does this achieve? 6 | * Describe implementation: How does this work? 7 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftObserver", 7 | platforms: [ 8 | .iOS(.v12), .macOS(.v10_14), .tvOS(.v12), .watchOS(.v6) 9 | ], 10 | products: [ 11 | .library( 12 | name: "SwiftObserver", 13 | targets: ["SwiftObserver"] 14 | ), 15 | .library( 16 | name: "CombineObserver", 17 | targets: ["CombineObserver"] 18 | ), 19 | ], 20 | dependencies: [ 21 | .package( 22 | url: "https://github.com/flowtoolz/SwiftyToolz.git", 23 | exact: "0.5.1" 24 | ) 25 | ], 26 | targets: [ 27 | .target( 28 | name: "SwiftObserver", 29 | dependencies: ["SwiftyToolz"], 30 | path: "Code/SwiftObserver" 31 | ), 32 | .target( 33 | name: "CombineObserver", 34 | dependencies: ["SwiftObserver", "SwiftyToolz"], 35 | path: "Code/CombineObserver" 36 | ), 37 | .testTarget( 38 | name: "SwiftObserverTests", 39 | dependencies: ["SwiftObserver", "SwiftyToolz"] 40 | ), 41 | .testTarget( 42 | name: "CombineObserverTests", 43 | dependencies: ["CombineObserver", "SwiftObserver", "SwiftyToolz"] 44 | ) 45 | ] 46 | ) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![SwiftObserver](Documentation/swift.png) 2 | 3 | # SwiftObserver 4 | 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftObserver%2Fbadge%3Ftype%3Dswift-versions&style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftObserver)  [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftObserver%2Fbadge%3Ftype%3Dplatforms&style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftObserver)  [![](https://img.shields.io/badge/Documentation-DocC-blue.svg?style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftObserver/documentation)  [![](https://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat-square)](LICENSE) 6 | 7 | SwiftObserver is a lightweight package for reactive Swift. Its design goals make it easy to learn and a joy to use: 8 | 9 | 1. **Meaningful Code** 💡
SwiftObserver promotes meaningful metaphors, names and syntax, producing highly readable code. 10 | 2. **Non-intrusive Design** ✊🏻
SwiftObserver doesn't limit or modulate your design. It just makes it easy to do the right thing. 11 | 3. **Simplicity** 🕹
SwiftObserver employs few radically simple concepts and applies them consistently without exceptions. 12 | 4. **Flexibility** 🤸🏻‍♀️
SwiftObserver's types are simple but universal and composable, making them applicable in many situations. 13 | 5. **Safety** ⛑
SwiftObserver eliminates the memory leaks that such an easy to use observer-/reactive library might invite. 14 | 15 | SwiftObserver is only 1400 lines of production code, but it's well beyond 1000 hours of work. With precursor implementations going back to 2013, it has continuously been re-imagined, reworked and battle-tested, [letting go of many fancy features](https://github.com/codeface-io/SwiftObserver/releases) while refining documentation and [unit-tests](https://github.com/codeface-io/SwiftObserver/tree/master/Tests/SwiftObserverTests). 16 | 17 | ## Why the Hell Another Reactive Swift Framework? 18 | 19 | [*Reactive Programming*](https://en.wikipedia.org/wiki/Reactive_programming) adresses the central challenge of implementing effective architectures: controlling dependency direction, in particular making [specific concerns depend on abstract ones](https://en.wikipedia.org/wiki/Dependency_inversion_principle). SwiftObserver breaks reactive programming down to its essence, which is the [*Observer Pattern*](https://en.wikipedia.org/wiki/Observer_pattern). 20 | 21 | SwiftObserver diverges from convention as it doesn't inherit the metaphors, terms, types, or function- and operator arsenals of common reactive libraries. It's not as fancy as Rx and Combine and not as restrictive as Redux. Instead, it offers a powerful simplicity you might actually **love** to work with. 22 | 23 | ## Contents 24 | 25 | * [Introduction](#introduction) 26 | * [Get Involved](#get-involved) 27 | * [Install](#install) 28 | * [Get Started](#get-started) 29 | * [Messengers](#messengers) 30 | * [Understand Observable Objects](#understand-observable-objects) 31 | * [Variables](#variables) 32 | * [Observe Variables](#observe-variables) 33 | * [Access Variable Values](#access-variable-values) 34 | * [Encode and Decode Variables](#encode-and-decode-variables) 35 | * [Transforms](#transforms) 36 | * [Make Transforms Observable](#make-transforms-observable) 37 | * [Use Prebuilt Transforms](#use-prebuilt-transforms) 38 | * [Chain Transforms](#chain-transforms) 39 | * [Advanced](#advanced) 40 | * [Interoperate With Combine](#interoperate-with-combine) 41 | * [Pull Latest Messages](#pull-latest-messages) 42 | * [Identify Message Authors](#identify-message-authors) 43 | * [Observe Weak Objects](#observe-weak-objects) 44 | * [More](#more) 45 | 46 | # Introduction 47 | 48 | ## Get Involved 49 | 50 | * Found a **bug**? Create a [github issue](https://github.com/codeface-io/SwiftObserver/issues/new/choose). 51 | * Need a **feature**? Create a [github issue](https://github.com/codeface-io/SwiftObserver/issues/new/choose). 52 | * Want to **improve** stuff? Create a [pull request](https://github.com/codeface-io/SwiftObserver/pulls). 53 | * Need **support** and troubleshooting? Write at . 54 | * Want to **contact** us? Write at . 55 | 56 | ## Install 57 | 58 | With the [**Swift Package Manager**](https://github.com/apple/swift-package-manager/tree/master/Documentation#swift-package-manager), you add the SwiftObserver package [via Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) (11+). 59 | 60 | Or you manually adjust the [Package.swift](https://github.com/apple/swift-package-manager/blob/master/Documentation/Usage.md#create-a-package) file of your project: 61 | 62 | ~~~swift 63 | // swift-tools-version:5.6.0 64 | 65 | import PackageDescription 66 | 67 | let package = Package( 68 | name: "MyProject", 69 | platforms: [ 70 | .iOS(.v12), .macOS(.v10_14), .tvOS(.v12), .watchOS(.v6) 71 | ], 72 | products: [ 73 | .library( 74 | name: "MyProject", 75 | targets: ["MyProject"] 76 | ) 77 | ], 78 | dependencies: [ 79 | .package( 80 | url: "https://github.com/codeface-io/SwiftObserver.git", 81 | exact: "7.0.3" 82 | ) 83 | ], 84 | targets: [ 85 | .target(name: "MyProject", 86 | dependencies: ["SwiftObserver"]) 87 | ] 88 | ) 89 | ~~~ 90 | 91 | Then run `$ swift build` or `$ swift run`. 92 | 93 | Finally, in your **Swift** files: 94 | 95 | ```swift 96 | import SwiftObserver 97 | ``` 98 | 99 | ## Get Started 100 | 101 | No need to learn a bunch of arbitrary metaphors, terms or types. 102 | 103 | SwiftObserver is simple: **Objects *observe* other objects**. 104 | 105 | Or a tad more technically: ***Observable objects* send *messages* to their *observers***. 106 | 107 | That's it. Just readable code: 108 | 109 | ~~~swift 110 | dog.observe(Sky.shared) { color in 111 | // marvel at the sky changing its color 112 | } 113 | ~~~ 114 | 115 | ### Observers 116 | 117 | Any object can be an `Observer` if it has a `Receiver` for receiving messages: 118 | 119 | ```swift 120 | class Dog: Observer { 121 | let receiver = Receiver() 122 | } 123 | ``` 124 | 125 | The receiver keeps the observer's observations alive. The observer just holds on to it strongly. 126 | 127 | #### Notes on Observers 128 | 129 | * For a message receiving closure to be called, the `Observer`/`Receiver` must still be alive. There's no awareness after death in memory. 130 | * An `Observer` can do multiple simultaneous observations of the same `ObservableObject`, for example by calling `observe(...)` multiple times. 131 | * You can check wether an `observer` is observing an `observable` via `observer.isObserving(observable)`. 132 | 133 | ### Observable Objects 134 | 135 | Any object can be an `ObservableObject` if it has a `Messenger` for sending messages: 136 | 137 | ```swift 138 | class Sky: ObservableObject { 139 | let messenger = Messenger() // Message == Color 140 | } 141 | ``` 142 | 143 | #### Notes on Observable Objects 144 | 145 | * An `ObservableObject` sends messages via `send(_ message: Message)`. The object's clients, even its observers, are also free to call that function. 146 | * An `ObservableObject` delivers messages in exactly the order in which `send` is called, which helps when observers, from their message handling closures, somehow trigger further calls of `send`. 147 | * Just starting to observe an `ObservableObject` does **not** trigger it to send a message. This keeps everything simple, predictable and consistent. 148 | 149 | #### Ways to Create an Observable Object 150 | 151 | 1. Create a [`Messenger`](#messengers). It's a mediator through which other entities communicate. 152 | 2. Create an object of a [custom `ObservableObject`](#understand-observable-objects) class that utilizes `Messenger`. 153 | 3. Create a [`Variable`](#variables) (a.k.a. `Var`). It holds a value and sends value updates. 154 | 5. Create a [*transform*](#make-transforms-observable) object. It wraps and transforms another `ObservableObject`. 155 | 156 | ### Memory Management 157 | 158 | With SwiftObserver, you don't have to deal with "Cancellables", "Tokens", "DisposeBags" or any such weirdness for every new observation. And yet, you also don't have to worry about any specific memory management. When an `Observer` or `ObservableObject` dies, SwiftObserver cleans up all related observations automatically. 159 | 160 | Of course, observing- and observed objects are still free to stop particular or all their ongoing observations: 161 | 162 | ```swift 163 | dog.stopObserving(Sky.shared) // no more messages from the sky 164 | dog.stopObserving() // no more messages from anywhere 165 | Sky.shared.stopBeingObserved(by: dog) // no more messages to dog 166 | Sky.shared.stopBeingObserved() // no more messages to anywhere 167 | ``` 168 | 169 | # Messengers 170 | 171 | `Messenger` is the simplest `ObservableObject` and the basis of every other `ObservableObject`. It doesn't send messages by itself, but anyone can send messages through it and use it for any type of message: 172 | 173 | ```swift 174 | let textMessenger = Messenger() 175 | 176 | observer.observe(textMessenger) { textMessage in 177 | // respond to textMessage 178 | } 179 | 180 | textMessenger.send("my message") 181 | ``` 182 | 183 | `Messenger` embodies the common [messenger / notifier pattern](Documentation/specific-patterns.md#the-messenger-pattern) and can be used for that out of the box. 184 | 185 | ## Understand Observable Objects 186 | 187 | Having a `Messenger` is actually what defines an `ObservableObject`: 188 | 189 | ```swift 190 | public protocol ObservableObject: AnyObject { 191 | var messenger: Messenger { get } 192 | associatedtype Message: Any 193 | } 194 | ``` 195 | 196 | `Messenger` is itself an `ObservableObject` because it points to itself as the required `Messenger`: 197 | 198 | ```swift 199 | extension Messenger: ObservableObject { 200 | public var messenger: Messenger { self } 201 | } 202 | ``` 203 | 204 | Every other `ObservableObject` class is either a subclass of `Messenger` or a custom `ObservableObject` class that provides a `Messenger`. Custom observable objects often employ some `enum` as their message type: 205 | 206 | ```swift 207 | class Model: SuperModel, ObservableObject { 208 | func foo() { send(.willUpdate) } 209 | func bar() { send(.didUpdate) } 210 | deinit { send(.willDie) } 211 | let messenger = Messenger() // Message == Event 212 | enum Event { case willUpdate, didUpdate, willDie } 213 | } 214 | ``` 215 | 216 | # Variables 217 | 218 | `Var` is an `ObservableObject` that has a property `var value: Value`. 219 | 220 | ## Observe Variables 221 | 222 | Whenever its `value` changes, `Var` sends a message of type `Update`, informing about the `old` and `new` value: 223 | 224 | ~~~swift 225 | let number = Var(42) 226 | 227 | observer.observe(number) { update in 228 | let whatsTheBigDifference = update.new - update.old 229 | } 230 | 231 | number <- 123 // use convenience operator <- to set number.value 232 | ~~~ 233 | 234 | In addition, you can always manually call `variable.send()` (without argument) to send an update in which `old` and `new` both hold the current `value` (see [Pull Latest Messages](#pull-latest-messages)). 235 | 236 | ## Access Variable Values 237 | 238 | The property wrapper `ObservableVar` allows to access the actual `Value` directly. Let's apply it to the above example: 239 | 240 | ~~~swift 241 | @ObservableVar var number = 42 242 | 243 | observer.observe($number) { update in 244 | let whatsTheBigDifference = update.new - update.old 245 | } 246 | 247 | number = 123 248 | ~~~ 249 | 250 | The wrapper's projected value provides the underlying `Var`, which you access via the `$` sign like in the above example. This is analogous to how you access underlying publishers of `@Published` properties in Combine. 251 | 252 | ## Encode and Decode Variables 253 | 254 | A `Var` is automatically `Codable` if its `Value` is. So when one of your types has `Var` properties, you can make that type `Codable` by simply adopting the `Codable` protocol: 255 | 256 | ~~~swift 257 | class Model: Codable { 258 | private(set) var text = Var("String Variable") 259 | } 260 | ~~~ 261 | 262 | Note that `text` is a `var` instead of a `let`. It cannot be constant because Swift's implicit decoder must mutate it. However, clients of `Model` would be supposed to set only `text.value` and not `text` itself, so the setter is private. 263 | 264 | # Transforms 265 | 266 | Transforms make common steps of message processing more succinct and readable. They allow to map, filter and unwrap messages in many ways. You may freely chain these transforms together and also define new ones with them. 267 | 268 | This example transforms messages of type `Update` into ones of type `Int`: 269 | 270 | ```swift 271 | let title = Var() 272 | 273 | observer.observe(title).new().unwrap("Untitled").map({ $0.count }) { titleLength in 274 | // do something with the new title length 275 | } 276 | ``` 277 | 278 | ## Make Transforms Observable 279 | 280 | You may transform a particular observation directly on the fly, like in the above example. Such ad hoc transforms give the observer lots of flexibility. 281 | 282 | Or you may instantiate a new `ObservableObject` that has the transform chain baked into it. The above example could then look like this: 283 | 284 | ```swift 285 | let title = Var() 286 | let titleLength = title.new().unwrap("Untitled").map { $0.count } 287 | 288 | observer.observe(titleLength) { titleLength in 289 | // do something with the new title length 290 | } 291 | ``` 292 | 293 | Every transform object exposes its underlying `ObservableObject` as `origin`. You may even replace `origin`: 294 | 295 | ```swift 296 | let titleLength = Var("Dummy Title").new().map { $0.count } 297 | let title = Var("Real Title") 298 | titleLength.origin.origin = title 299 | ``` 300 | 301 | Such stand-alone transforms can offer the same preprocessing to multiple observers. But since these transforms are distinct `ObservableObject`s, you must hold them strongly somewhere. Holding transform chains as dedicated observable objects suits entities like view models that represent transformations of other data. 302 | 303 | ## Use Prebuilt Transforms 304 | 305 | Whether you apply transforms ad hoc or as stand-alone objects, they work the same way. The following list illustrates prebuilt transforms as observable objects. 306 | 307 | ### Map 308 | 309 | First, there is your regular familiar `map` function. It transforms messages and often also their type: 310 | 311 | ```swift 312 | let messenger = Messenger() // sends String 313 | let stringToInt = messenger.map { Int($0) } // sends Int? 314 | ``` 315 | 316 | ### New 317 | 318 | When an `ObservableObject` like a `Var` sends *messages* of type `Update`, we often only care about the `new` value, so we map the update with `new()`: 319 | 320 | ~~~swift 321 | let errorCode = Var() // sends Update 322 | let newErrorCode = errorCode.new() // sends Int 323 | ~~~ 324 | 325 | ### Filter 326 | 327 | When you want to receive only certain messages, use `filter`: 328 | 329 | ```swift 330 | let messenger = Messenger() // sends String 331 | let shortMessages = messenger.filter { $0.count < 10 } // sends String if length < 10 332 | ``` 333 | 334 | ### Select 335 | 336 | Use `select` to receive only one specific message. `select` works with all `Equatable` message types. `select` maps the message type onto `Void`, so a receiving closure after a selection takes no message argument: 337 | 338 | ```swift 339 | let messenger = Messenger() // sends String 340 | let myNotifier = messenger.select("my notification") // sends Void (no messages) 341 | 342 | observer.observe(myNotifier) { // no argument 343 | // someone sent "my notification" 344 | } 345 | ``` 346 | 347 | ### Unwrap 348 | 349 | Sometimes, we make message types optional, for example when there is no meaningful initial value for a `Var`. But we often don't want to deal with optionals down the line. So we can use `unwrap()`, suppressing `nil` messages entirely: 350 | 351 | ~~~swift 352 | let errorCodes = Messenger() // sends Int? 353 | let errorAlert = errorCodes.unwrap() // sends Int if the message is not nil 354 | ~~~ 355 | 356 | ### Unwrap with Default 357 | 358 | You may also unwrap optional messages by replacing `nil` values with a default: 359 | 360 | ~~~swift 361 | let points = Messenger() // sends Int? 362 | let pointsToShow = points.unwrap(0) // sends Int with 0 for nil 363 | ~~~ 364 | 365 | ## Chain Transforms 366 | 367 | You may chain transforms together: 368 | 369 | ```swift 370 | let numbers = Messenger() 371 | 372 | observer.observe(numbers).map { 373 | "\($0)" // Int -> String 374 | }.filter { 375 | $0.count > 1 // suppress single digit integers 376 | }.map { 377 | Int.init($0) // String -> Int? 378 | }.unwrap { // Int? -> Int 379 | print($0) // receive and process resulting Int 380 | } 381 | ``` 382 | 383 | Of course, ad hoc transforms like the above end on the actual message handling closure. Now, when the last transform in the chain also takes a closure argument for its processing, like `map` and `filter` do, we use `receive` to stick with the nice syntax of [trailing closures](https://docs.swift.org/swift-book/LanguageGuide/Closures.html#ID102): 384 | 385 | ~~~swift 386 | dog.observe(Sky.shared).map { 387 | $0 == .blue 388 | }.receive { 389 | print("Will we go outside? \($0 ? "Yes" : "No")!") 390 | } 391 | ~~~ 392 | 393 | # Advanced 394 | 395 | ## Interoperate With Combine 396 | 397 | **CombineObserver** is another library product of the SwiftObserver package. It depends on SwiftObserver and adds a simple way to transform any SwiftObserver-`ObservableObject` into a Combine-`Publisher`: 398 | 399 | ```swift 400 | import CombineObserver 401 | 402 | @ObservableVar var number = 7 // SwiftObserver 403 | let numberPublisher = $number.publisher() // Combine 404 | 405 | let cancellable = numberPublisher.dropFirst().sink { numberUpdate in 406 | print("\(numberUpdate.new)") 407 | } 408 | 409 | number = 42 // prints "42" 410 | ``` 411 | 412 | > This interoperation goes in only one direction. Here's some reasoning behind that: SwiftObserver is for pure Swift-/model code without external dependencies – not even on Combine. When combined with Combine (oops), SwiftObserver would be employed in the model core of an application, while Combine would be used more with I/O periphery like SwiftUI and other system-specific APIs that already rely on Combine. That means, the "Combine layer" might want to observe (react to-) the "SwiftObserver layer" – but hardly the other way around. 413 | 414 | ## Pull Latest Messages 415 | 416 | An `ObservableCache` is an `ObservableObject` that has a property `latestMessage: Message` which typically returns the last sent message or one that indicates that nothing has changed. `ObservableCache` has a function `send()` that takes no argument and sends `latestMessage`. 417 | 418 | ### Four Kinds of `ObservableCache` 419 | 420 | 1. Any `Var` is an `ObservableCache`. Its `latestMessage` is an `Update` in which `old` and `new` both hold the current `value`. 421 | 422 | 2. Custom observable objects can easily conform to `ObservableCache`. Even if their message type isn't based on some state, `latestMessage` can still return a meaningful default value - or even `nil` where `Message` is optional. 423 | 424 | 3. Calling `cache()` on an `ObservableObject` creates a [transform](#make-transforms-observable) that is an `ObservableCache`. That cache's `Message` will be optional but never an *optional optional*, even when the origin's `Message` is already optional. 425 | 426 | Of course, `cache()` wouldn't make sense as an adhoc transform of an observation, so it can only create a distinct observable object. 427 | 428 | 4. Any transform whose origin is an `ObservableCache` is itself implicitly an `ObservableCache` **if** it never suppresses (filters) messages. These compatible transforms are: `map`, `new` and `unwrap(default)`. 429 | 430 | Note that the `latestMessage` of a transform that is an implicit `ObservableCache` returns the transformed `latestMessage` of its underlying `ObservableCache` origin. Calling `send(transformedMessage)` on that transform itself will not "update" its `latestMessage`. 431 | 432 | ### State-Based Messages 433 | 434 | An `ObservableObject` like `Var`, that derives its messages from its state, can generate a "latest message" on demand and therefore act as an `ObservableCache`: 435 | 436 | ```swift 437 | class Model: Messenger, ObservableCache { // informs about the latest state 438 | var latestMessage: String { state } // ... either on demand 439 | 440 | var state = "initial state" { 441 | didSet { 442 | if state != oldValue { 443 | send(state) // ... or when the state changes 444 | } 445 | } 446 | } 447 | } 448 | ``` 449 | 450 | ## Identify Message Authors 451 | 452 | Every message has an author associated with it. This feature is only noticable in code if you use it. 453 | 454 | An observable object can send an author together with a message via `object.send(message, from: author)`. If noone specifies an author as in `object.send(message)`, the observable object itself becomes the author. 455 | 456 | ### Mutate Variables 457 | 458 | Variables have a special value setter that allows to identify change authors: 459 | 460 | ```swift 461 | let number = Var(0) 462 | number.set(42, as: controller) // controller becomes author of the update message 463 | ``` 464 | 465 | ### Receive Authors 466 | 467 | The observer can receive the author, by adding it as an argument to the message handling closure: 468 | 469 | ```swift 470 | observer.observe(observableObject) { message, author in 471 | // process message from author 472 | } 473 | ``` 474 | 475 | Through the author, observers can determine a message's origin. In the plain messenger pattern, the origin would simply be the message sender. 476 | 477 | ### Share Observable Objects 478 | 479 | Identifying message authors can become essential whenever multiple observers observe the same object while their actions can cause it so send messages. 480 | 481 | Mutable data is a common type of such shared observable objects. For example, when multiple entities observe and modify a storage abstraction or caching hierarchy, they often want to avoid reacting to their own actions. Such overreaction might lead to redundant work or inifnite response cycles. So they identify as change authors when modifying the data and ignore messages from `self` when observing it: 482 | 483 | ```swift 484 | class Collaborator: Observer { 485 | func observeText() { 486 | observe(sharedText).notFrom(self) { update, author in // see author filters below 487 | // someone else edited the text 488 | } 489 | } 490 | 491 | func editText() { 492 | sharedText.set("my new text", as: self) // identify as change author 493 | } 494 | 495 | let receiver = Receiver() 496 | } 497 | 498 | let sharedText = Var() 499 | ``` 500 | 501 | ### Filter by Author 502 | 503 | There are three transforms related to message authors. As with other transforms, we can apply them directly in observations or create them as standalone observable objects. 504 | 505 | #### Filter Author 506 | 507 | We filter authors just like messages: 508 | 509 | ```swift 510 | let messenger = Messenger() // sends String 511 | 512 | let friendMessages = messenger.filterAuthor { // sends String if message is from friend 513 | friends.contains($0) 514 | } 515 | ``` 516 | 517 | #### From 518 | 519 | If only one specific author is of interest, filter authors with `from`. It captures the selected author weakly: 520 | 521 | ```swift 522 | let messenger = Messenger() // sends String 523 | let joesMessages = messenger.from(joe) // sends String if message is from joe 524 | ``` 525 | 526 | #### Not From 527 | 528 | If **all but one** specific author are of interest, use `notFrom`. It also captures the excluded author weakly: 529 | 530 | ```swift 531 | let messenger = Messenger() // sends String 532 | let humanMessages = messenger.notFrom(hal9000) // sends String, but not from an evil AI 533 | ``` 534 | 535 | ## Observe Weak Objects 536 | 537 | When you want to put an `ObservableObject` into some data structure or as the *origin* into a *transform* object but hold it there as a `weak` reference, transform it via `observableObject.weak()`: 538 | 539 | ~~~swift 540 | let number = Var(12) 541 | let weakNumber = number.weak() 542 | 543 | observer.observe(weakNumber) { update in 544 | // process update of type Update 545 | } 546 | 547 | var weakNumbers = [Weak>]() 548 | weakNumbers.append(weakNumber) 549 | ~~~ 550 | 551 | Of course, `weak()` wouldn't make sense as an adhoc transform, so it can only create a distinct observable object. 552 | 553 | # More 554 | 555 | ## Architecture 556 | 557 | Here's the internal architecture (composition and [essential](https://en.wikipedia.org/wiki/Transitive_reduction) dependencies) of the "SwiftObserver" target: 558 | 559 | ![](Documentation/Architecture/SwiftObserver.png) 560 | 561 | More diagrams of top-level source folders [are over here](Documentation/Architecture/architecture.md). The images were generated with [Codeface](https://codeface.io). 562 | 563 | ## Further Reading 564 | 565 | * **DocC Documentation:** Check out the complete [reference documentation in DocC format](https://swiftpackageindex.com/codeface-io/SwiftObserver/documentation) 566 | * **Patterns *(incomplete)*:** Read more about some [patterns that emerged from using SwiftObserver](Documentation/specific-patterns.md#specific-patterns). 567 | * **Philosophy *(outdated)*:** Read more about the [philosophy and features of SwiftObserver](Documentation/philosophy.md#the-philosophy-of-swiftobserver). 568 | * **License:** SwiftObserver is released under the [MIT license](LICENSE). 569 | 570 | ## Open Tasks 571 | 572 | * Update and rework (or simply delete) texts about philosophy and patterns 573 | * Engage feedback and contribution -------------------------------------------------------------------------------- /Tests/CombineObserverTests/CombineObserverTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CombineObserver 3 | import SwiftObserver 4 | import SwiftyToolz 5 | 6 | @available(iOS 13.0, tvOS 13.0, *) 7 | class CombineObserverTests: XCTestCase 8 | { 9 | func testCreatingAndSubscribingToPublisher() 10 | { 11 | @ObservableVar var number = 7 12 | let numberPublisher = $number.publisher() 13 | 14 | var receivedNumbers = [Int]() 15 | 16 | let cancellable = numberPublisher.sink { receivedNumbers += $0.new } 17 | XCTAssertEqual(receivedNumbers, [7]) 18 | 19 | number = 42 20 | XCTAssertEqual(receivedNumbers, [7, 42]) 21 | 22 | cancellable.cancel() // just to avoid warning 23 | } 24 | 25 | func testUsingDropFirstOnPublisher() 26 | { 27 | @ObservableVar var number = 7 28 | let numberPublisher = $number.publisher() 29 | 30 | var receivedNumbers = [Int]() 31 | 32 | let cancellable = numberPublisher.dropFirst().sink { receivedNumbers += $0.new } 33 | XCTAssertEqual(receivedNumbers, []) 34 | 35 | number = 42 36 | XCTAssertEqual(receivedNumbers, [42]) 37 | 38 | cancellable.cancel() // just to avoid warning 39 | } 40 | 41 | func testCreatingPublisherOnUncachedObservable() 42 | { 43 | @ObservableVar var number = 200 44 | let numberFilter = $number.new().filter { $0 > 100 } 45 | let filterPublisher = numberFilter.publisher() 46 | 47 | var receivedNumbers = [Int]() 48 | 49 | let cancellable = filterPublisher.sink { receivedNumbers += $0 } 50 | XCTAssertEqual(receivedNumbers, []) 51 | 52 | number = 300 53 | XCTAssertEqual(receivedNumbers, [300]) 54 | 55 | cancellable.cancel() // just to avoid warning 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/SwiftObserverTests/BasicTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftObserver 3 | 4 | class BasicTests: XCTestCase 5 | { 6 | func testObserverving() 7 | { 8 | let messenger = Messenger() 9 | let observer = TestObserver() 10 | var receivedNumber: Int? 11 | 12 | observer.observe(messenger) { receivedNumber = $0 } 13 | 14 | XCTAssertEqual(receivedNumber, nil) 15 | 16 | messenger.send(42) 17 | 18 | XCTAssertEqual(receivedNumber, 42) 19 | } 20 | 21 | func testObservingAloneDoesNotSendAMessage() 22 | { 23 | let messenger = Messenger() 24 | 25 | var didTriggerUpdate = false 26 | 27 | let observer = TestObserver() 28 | 29 | observer.observe(messenger) 30 | { 31 | didTriggerUpdate = true 32 | } 33 | 34 | XCTAssertFalse(didTriggerUpdate) 35 | } 36 | 37 | func testMaintainingMessageOrder() 38 | { 39 | let messenger = Messenger() 40 | let observer1 = TestObserver() 41 | let observer2 = TestObserver() 42 | var receivedNumbers = [Int]() 43 | 44 | observer1.observe(messenger) 45 | { 46 | receivedNumbers.append($0) 47 | if $0 == 0 { messenger.send(1) } 48 | } 49 | 50 | observer2.observe(messenger) 51 | { 52 | receivedNumbers.append($0) 53 | if $0 == 0 { messenger.send(2) } 54 | } 55 | 56 | messenger.send(0) 57 | 58 | XCTAssertEqual(receivedNumbers.count, 6) 59 | XCTAssertEqual(receivedNumbers[0], 0) 60 | XCTAssertEqual(receivedNumbers[1], 0) 61 | XCTAssertEqual(receivedNumbers[2], receivedNumbers[3]) 62 | } 63 | 64 | func testObservingAndReceivingAuthor() 65 | { 66 | let messenger = Messenger() 67 | let observer = TestObserver() 68 | var receivedNumber: Int? 69 | var receivedAuthor: AnyAuthor? 70 | 71 | observer.observe(messenger) 72 | { 73 | number, author in 74 | receivedNumber = number 75 | receivedAuthor = author 76 | } 77 | 78 | messenger.send(42, from: observer) 79 | 80 | XCTAssertEqual(receivedNumber, 42) 81 | XCTAssert(receivedAuthor === observer) 82 | } 83 | 84 | func testObservingSameObservableWithMultipleMessageHandlers() 85 | { 86 | let messenger = Messenger() 87 | let observer = TestObserver() 88 | var sum = 0 89 | 90 | observer.observe(messenger) { sum += 1 } 91 | observer.observe(messenger) { sum += 2 } 92 | 93 | XCTAssertEqual(sum, 0) 94 | 95 | messenger.send(()) 96 | 97 | XCTAssertEqual(sum, 3) 98 | } 99 | 100 | class TestObserver: Observer 101 | { 102 | let receiver = Receiver() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/SwiftObserverTests/CustomObservableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftObserver 3 | 4 | class CustomObservableTests: XCTestCase 5 | { 6 | func testObservingACustomObservable() 7 | { 8 | let model = ObservableModel() 9 | 10 | var didUpdate = false 11 | 12 | let observer = TestObserver() 13 | 14 | observer.observe(model) 15 | { 16 | XCTAssertEqual($0, .didUpdate) 17 | didUpdate = true 18 | } 19 | 20 | model.send(.didUpdate) 21 | 22 | XCTAssert(didUpdate) 23 | } 24 | 25 | func testMapObservationOfCustomObservable() 26 | { 27 | let model = ObservableModel() 28 | 29 | var didFire = false 30 | 31 | let observer = TestObserver() 32 | 33 | observer.observe(model).map({ $0.rawValue }) 34 | { 35 | XCTAssertEqual($0, "didUpdate") 36 | didFire = true 37 | } 38 | 39 | model.send(.didUpdate) 40 | 41 | XCTAssert(didFire) 42 | } 43 | 44 | func testMapSelectObservationOfCustomObservable() 45 | { 46 | let model = ObservableModel() 47 | 48 | let mappedModel = model.select(.didUpdate) 49 | 50 | var didFire = false 51 | 52 | let observer = TestObserver() 53 | 54 | observer.observe(mappedModel) 55 | { 56 | didFire = true 57 | } 58 | 59 | model.send(.didUpdate) 60 | XCTAssert(didFire) 61 | 62 | didFire = false 63 | model.send(.didReset) 64 | XCTAssert(!didFire) 65 | } 66 | 67 | func testNewMappingOnCustomObservable() 68 | { 69 | let customObservable = ModelWithState() 70 | 71 | let newState = customObservable.new() 72 | 73 | customObservable.state = "state1" 74 | 75 | var didUpdate = false 76 | 77 | let observer = TestObserver() 78 | 79 | observer.observe(newState) 80 | { 81 | XCTAssert($0 == "state1" || $0 == "state2") 82 | 83 | didUpdate = $0 == "state2" 84 | } 85 | 86 | customObservable.state = "state2" 87 | 88 | XCTAssert(didUpdate) 89 | } 90 | 91 | class ObservableModel: ObservableCache 92 | { 93 | var latestMessage: Event { .didNothing } 94 | 95 | let messenger = Messenger() 96 | 97 | enum Event: String { case didNothing, didUpdate, didReset } 98 | } 99 | 100 | class ModelWithState: ObservableCache 101 | { 102 | var latestMessage: Update 103 | { 104 | Update(state, state) 105 | } 106 | 107 | var state = "initial state" 108 | { 109 | didSet 110 | { 111 | if oldValue != state 112 | { 113 | send(Update(oldValue, state)) 114 | } 115 | } 116 | } 117 | 118 | let messenger = Messenger>() 119 | } 120 | 121 | class TestObserver: Observer 122 | { 123 | let receiver = Receiver() 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /Tests/SwiftObserverTests/ObservableTransformTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftObserver 3 | import SwiftyToolz 4 | 5 | class ObservableTransformTests: XCTestCase 6 | { 7 | override func setUp() 8 | { 9 | super.setUp() 10 | Log.shared.minimumPrintLevel = .error 11 | 12 | Log.shared.add(observer: self) 13 | { 14 | self.latestLogEntry = $0 15 | } 16 | } 17 | 18 | func testCacheLatestMessageIsOptionalOnObservableWithOptionalMessage() 19 | { 20 | let cache = Messenger().cache() 21 | XCTAssert(type(of: cache.latestMessage) == Int?.self) 22 | } 23 | 24 | func testCacheLatestMessageIsOptionalOnObservableWithNonOptionalMessage() 25 | { 26 | let cache = Messenger().cache() 27 | XCTAssert(type(of: cache.latestMessage) == Int?.self) 28 | } 29 | 30 | func testLogWarningWhenApplyingCacheToCacheWithNonOptionalMessage() 31 | { 32 | let alreadyACache = Var() 33 | let expectedWarning = alreadyACache.warningWhenApplyingCache(messageIsOptional: false) 34 | _ = alreadyACache.cache() 35 | XCTAssert(latestLogEntry?.message.contains(expectedWarning) ?? false) 36 | } 37 | 38 | func testLogWarningWhenApplyingCacheToCacheWithOptionalMessage() 39 | { 40 | latestLogEntry = nil 41 | let alreadyACache = Messenger().cache() 42 | XCTAssertNil(latestLogEntry) 43 | let expectedWarning = alreadyACache.warningWhenApplyingCache(messageIsOptional: true) 44 | _ = alreadyACache.cache() 45 | XCTAssert(latestLogEntry?.message.contains(expectedWarning) ?? false) 46 | } 47 | 48 | func testReplacingOriginOfTransform() 49 | { 50 | let original = Var(1) 51 | 52 | let transform = original.new().unwrap().map { "\($0)" } 53 | 54 | let observer = TestObserver() 55 | 56 | var lastUpdateFromOriginal: Update? 57 | 58 | observer.observe(original) 59 | { 60 | lastUpdateFromOriginal = $0 61 | } 62 | 63 | var lastUpdateFromTransform: String? 64 | 65 | observer.observe(transform) 66 | { 67 | lastUpdateFromTransform = $0 68 | } 69 | 70 | XCTAssertNil(lastUpdateFromOriginal) 71 | XCTAssertNil(lastUpdateFromTransform) 72 | 73 | original.send() 74 | 75 | XCTAssertEqual(lastUpdateFromOriginal?.new, 1) 76 | XCTAssertEqual(lastUpdateFromTransform, "1") 77 | 78 | lastUpdateFromOriginal = nil 79 | lastUpdateFromTransform = nil 80 | 81 | let replacement = Var(42) 82 | transform.origin.origin.origin = replacement 83 | 84 | XCTAssertNil(lastUpdateFromOriginal) 85 | XCTAssertNil(lastUpdateFromTransform) 86 | 87 | original.send() 88 | 89 | XCTAssertEqual(lastUpdateFromOriginal?.new, 1) 90 | XCTAssertNil(lastUpdateFromTransform) 91 | 92 | replacement.send() 93 | 94 | XCTAssertEqual(lastUpdateFromTransform, "42") 95 | } 96 | 97 | func testThatMappersOfCachesAreCaches() 98 | { 99 | XCTAssertEqual(Var(1).new().latestMessage, 1) 100 | XCTAssertEqual(Var().new().unwrap(23).latestMessage, 23) 101 | XCTAssertEqual(Var(5).map({ $0.new == 5 }).latestMessage, true) 102 | } 103 | 104 | func testMappingSelect() 105 | { 106 | let text = Var() 107 | let textMapping = text.new().unwrap("").select("test") 108 | 109 | var didFire = false 110 | 111 | let observer = TestObserver() 112 | 113 | observer.observe(textMapping) 114 | { 115 | didFire = true 116 | } 117 | 118 | text <- "test" 119 | XCTAssert(didFire) 120 | 121 | didFire = false 122 | text <- "test2" 123 | XCTAssert(!didFire) 124 | } 125 | 126 | func testMappingsIncludingFilter() 127 | { 128 | let number = Var(99) 129 | let doubleDigits = number.new().unwrap(0).filter { $0 > 9 } 130 | 131 | var observedNumbers = [Int]() 132 | 133 | let observer = TestObserver() 134 | 135 | observer.observe(doubleDigits) 136 | { 137 | observedNumbers.append($0) 138 | } 139 | 140 | number <- 10 141 | number <- nil 142 | number <- 11 143 | number <- 1 144 | number <- 12 145 | number <- 2 146 | 147 | XCTAssertEqual(observedNumbers, [10, 11, 12]) 148 | } 149 | 150 | func testFilterSupressesMessage() 151 | { 152 | let messenger = Messenger() 153 | let transform = Filter(messenger) { $0 != nil } 154 | 155 | var observedNumber: Int? = nil 156 | 157 | let observer = TestObserver() 158 | 159 | observer.observe(transform) 160 | { 161 | observedNumber = $0 162 | XCTAssertNotNil($0) 163 | } 164 | 165 | messenger.send(3) 166 | XCTAssertEqual(observedNumber, 3) 167 | 168 | messenger.send(nil) 169 | XCTAssertEqual(observedNumber, 3) 170 | } 171 | 172 | func testObservableTransformObject() 173 | { 174 | let textMessenger = Var().new() 175 | var receivedMessage: String? 176 | let expectedMessage = "message" 177 | 178 | let observer = TestObserver() 179 | 180 | observer.observe(textMessenger) 181 | { 182 | receivedMessage = $0 183 | } 184 | 185 | textMessenger.send(expectedMessage) 186 | 187 | XCTAssertEqual(receivedMessage, expectedMessage) 188 | } 189 | 190 | func testObservableMapObject() 191 | { 192 | let text = Var() 193 | 194 | let nonOptionalText = text.map { $0.new ?? "" } 195 | 196 | var didUpdate = false 197 | 198 | let observer = TestObserver() 199 | 200 | observer.observe(nonOptionalText) 201 | { 202 | XCTAssertEqual($0, "") 203 | 204 | didUpdate = true 205 | } 206 | 207 | text.send() 208 | 209 | XCTAssert(didUpdate) 210 | } 211 | 212 | func testObservableNewAndUnwrapObject() 213 | { 214 | let text = Var() 215 | let unwrappedText = text.new().unwrap("") 216 | 217 | var didUpdate = false 218 | 219 | let observer = TestObserver() 220 | 221 | observer.observe(unwrappedText) 222 | { 223 | XCTAssertEqual($0, "") 224 | didUpdate = true 225 | } 226 | 227 | text.send() 228 | 229 | XCTAssert(didUpdate) 230 | } 231 | 232 | func testWeakObservableWrapper() 233 | { 234 | let weakNumber1 = Var(1).weak() 235 | XCTAssertNil(weakNumber1.origin) 236 | 237 | let strongNumber = Var(2) 238 | let weakNumber2 = strongNumber.weak() 239 | XCTAssertEqual(weakNumber2.origin?.value, 2) 240 | } 241 | 242 | func testWeakObservable() 243 | { 244 | var strongObservable: Var? = Var(10) 245 | 246 | let weakObservable = strongObservable!.weak() 247 | 248 | XCTAssert(strongObservable === weakObservable.origin) 249 | 250 | strongObservable = nil 251 | 252 | XCTAssertNil(weakObservable.origin) 253 | } 254 | 255 | func receive(_ entry: Log.Entry) { latestLogEntry = entry } 256 | private var latestLogEntry: Log.Entry? 257 | 258 | class TestObserver: Observer 259 | { 260 | let receiver = Receiver() 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Tests/SwiftObserverTests/ObservationTransformTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftObserver 3 | 4 | class ObservationTransformTests: XCTestCase 5 | { 6 | func testChainingObservationMappers() 7 | { 8 | var didFire = false 9 | 10 | let number = Var(42) 11 | 12 | controller.observe(number).new().map { 13 | "\($0)" // Int -> String 14 | }.filter { 15 | $0.count > 1 // filter out single digit integers 16 | }.map { 17 | Int.init($0) // String -> Int? 18 | }.filter { 19 | $0 != nil // filter out nil values 20 | }.unwrap(-1) { _ in // Int? -> Int 21 | didFire = true // process Int 22 | } 23 | 24 | number <- 10 25 | 26 | XCTAssert(didFire) 27 | } 28 | 29 | func testChainingObservationMappersWithReceive() 30 | { 31 | var didFire = false 32 | 33 | let number = Var(42) 34 | 35 | controller.observe(number).map { 36 | $0.new // Update -> Int 37 | }.receive { _ in 38 | didFire = true // process Int 39 | } 40 | 41 | number <- 10 42 | 43 | XCTAssert(didFire) 44 | } 45 | 46 | func testObservationMapping() 47 | { 48 | let testText = Var() 49 | 50 | var didFire = false 51 | var observedString: String? 52 | 53 | controller.observe(testText).map({ $0.new }) 54 | { 55 | observedString = $0 56 | didFire = true 57 | } 58 | 59 | testText <- "test" 60 | 61 | XCTAssert(didFire) 62 | XCTAssertEqual("test", observedString) 63 | } 64 | 65 | func testObservationMappingChained() 66 | { 67 | let testText = Var("non optional string") 68 | 69 | var didFire = false 70 | var observedString: String? 71 | 72 | controller.observe(testText).map { 73 | $0.new 74 | }.map { 75 | $0 ?? "untitled" 76 | }.receive { 77 | observedString = $0 78 | didFire = true 79 | } 80 | 81 | testText <- nil 82 | 83 | XCTAssert(didFire) 84 | XCTAssertEqual("untitled", observedString) 85 | } 86 | 87 | func testObservationMappingNew() 88 | { 89 | let testText = Var() 90 | 91 | var didFire = false 92 | var observedString: String? 93 | 94 | controller.observe(testText).new 95 | { 96 | observedString = $0 97 | didFire = true 98 | } 99 | 100 | testText <- "test" 101 | 102 | XCTAssert(didFire) 103 | XCTAssertEqual("test", observedString) 104 | } 105 | 106 | func testObservationMappingChainAfterNew() 107 | { 108 | let testText = Var() 109 | 110 | var didFire = false 111 | var observedString: String? 112 | 113 | controller.observe(testText).new().map({ $0 ?? ""}) 114 | { 115 | observedString = $0 116 | 117 | didFire = true 118 | } 119 | 120 | testText <- "test" 121 | 122 | XCTAssert(didFire) 123 | XCTAssertEqual("test", observedString) 124 | } 125 | 126 | func testObservationMappingUnwrap() 127 | { 128 | let text = Var("non optional string") 129 | 130 | var didFire = false 131 | var observedString: String? 132 | 133 | controller.observe(text).new().unwrap("untitled") 134 | { 135 | observedString = $0 136 | didFire = true 137 | } 138 | 139 | text <- nil 140 | 141 | XCTAssert(didFire) 142 | XCTAssertEqual("untitled", observedString) 143 | } 144 | 145 | func testObservationMappingChainAfterUnwrap() 146 | { 147 | let text = Var("non optional string") 148 | 149 | var didFire = false 150 | var observedCount: Int? 151 | 152 | controller.observe(text).new().unwrap("untitled").map({ $0.count }) 153 | { 154 | observedCount = $0 155 | didFire = true 156 | } 157 | 158 | text <- nil 159 | 160 | XCTAssert(didFire) 161 | XCTAssertEqual("untitled".count, observedCount) 162 | } 163 | 164 | func testObservationMappingFilter() 165 | { 166 | let testText = Var() 167 | 168 | var didFire = false 169 | var observedString: String? 170 | 171 | controller.observe(testText).filter({ $0.old != nil }) 172 | { 173 | observedString = $0.new 174 | didFire = true 175 | } 176 | 177 | testText <- "test" 178 | XCTAssert(!didFire) 179 | XCTAssertNil(observedString) 180 | 181 | testText <- "test2" 182 | XCTAssert(didFire) 183 | XCTAssertEqual("test2", observedString) 184 | } 185 | 186 | func testObservationMappingSelect() 187 | { 188 | let text = Var() 189 | let textMapping = text.new().unwrap("") 190 | 191 | var didFire = false 192 | 193 | controller.observe(textMapping).select("test") 194 | { 195 | didFire = true 196 | } 197 | 198 | text <- "test" 199 | XCTAssert(didFire) 200 | 201 | didFire = false 202 | text <- "test2" 203 | XCTAssert(!didFire) 204 | } 205 | 206 | func testSelect() 207 | { 208 | let textMessenger = Var().new() 209 | var didFire = false 210 | 211 | controller.observe(textMessenger).select("right message") 212 | { 213 | didFire = true 214 | } 215 | 216 | textMessenger.send("right message") 217 | XCTAssert(didFire) 218 | 219 | didFire = false 220 | textMessenger.send("wrong message") 221 | XCTAssert(!didFire) 222 | } 223 | 224 | func testSingleObservationFilter() 225 | { 226 | let number = Var(99) 227 | let latestUnwrappedNumber = number.new().unwrap(0) 228 | 229 | var observedNumbers = [Int]() 230 | 231 | controller.observe(latestUnwrappedNumber).filter({ $0 > 9 }) 232 | { 233 | observedNumbers.append($0) 234 | } 235 | 236 | number <- 10 237 | number <- nil 238 | number <- 11 239 | number <- 1 240 | number <- 12 241 | number <- 2 242 | 243 | XCTAssertEqual(observedNumbers, [10, 11, 12]) 244 | } 245 | 246 | let controller = TestObserver() 247 | 248 | class TestObserver: Observer 249 | { 250 | let receiver = Receiver() 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Tests/SwiftObserverTests/VariableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftObserver 3 | 4 | class VariableTests: XCTestCase 5 | { 6 | func testObservingVariableValueChange() 7 | { 8 | let text = Var("old text") 9 | 10 | var observedNewValue: String? 11 | var observedOldValue: String? 12 | 13 | let observer = TestObserver() 14 | 15 | observer.observe(text) 16 | { 17 | observedOldValue = $0.old 18 | observedNewValue = $0.new 19 | } 20 | 21 | text <- "new text" 22 | 23 | XCTAssertEqual(observedOldValue, "old text") 24 | XCTAssertEqual(observedNewValue, "new text") 25 | } 26 | 27 | func testOptionalValue() 28 | { 29 | let text = Var("initial value") 30 | 31 | text <- nil 32 | 33 | XCTAssertNil(text.value) 34 | 35 | var didUpdate = false 36 | 37 | let observer = TestObserver() 38 | 39 | observer.observe(text) 40 | { 41 | XCTAssertEqual($0.new, "text") 42 | 43 | didUpdate = true 44 | } 45 | 46 | text <- "text" 47 | 48 | XCTAssertEqual(text.value, "text") 49 | XCTAssert(didUpdate) 50 | } 51 | 52 | func testVariableIsCodable() 53 | { 54 | var didEncode = false 55 | var didDecode = false 56 | 57 | let variable = Var(123) 58 | 59 | if let variableData = try? JSONEncoder().encode(variable) 60 | { 61 | let actual = String(data: variableData, encoding: .utf8) ?? "fail" 62 | let expected = "{\"storedValue\":123}" 63 | XCTAssertEqual(actual, expected) 64 | 65 | didEncode = true 66 | 67 | if let decodedVariable = try? JSONDecoder().decode(Var.self, 68 | from: variableData) 69 | { 70 | XCTAssertEqual(decodedVariable.value, 123) 71 | didDecode = true 72 | } 73 | } 74 | 75 | XCTAssert(didEncode) 76 | XCTAssert(didDecode) 77 | } 78 | 79 | func testCustomObservableWithVariablePropertiesIsCodable() 80 | { 81 | class CodableModel: Codable 82 | { 83 | private(set) var text = Var() 84 | private(set) var number = Var() 85 | } 86 | 87 | let model = CodableModel() 88 | 89 | var didEncode = false 90 | var didDecode = false 91 | 92 | model.text <- "123" 93 | model.number <- 123 94 | 95 | if let modelJson = try? JSONEncoder().encode(model) 96 | { 97 | let actual = String(data: modelJson, encoding: .utf8) ?? "fail" 98 | let expected = "{\"number\":{\"storedValue\":123},\"text\":{\"storedValue\":\"123\"}}" 99 | XCTAssertEqual(actual, expected) 100 | 101 | didEncode = true 102 | 103 | if let decodedModel = try? JSONDecoder().decode(CodableModel.self, 104 | from: modelJson) 105 | { 106 | XCTAssertEqual(decodedModel.text.value, "123") 107 | XCTAssertEqual(decodedModel.number.value, 123) 108 | didDecode = true 109 | } 110 | } 111 | 112 | XCTAssert(didEncode) 113 | XCTAssert(didDecode) 114 | } 115 | 116 | func testPropertyWrapper() 117 | { 118 | @ObservableVar var text: String? = "old text" 119 | 120 | var observedNewValue: String? 121 | var observedOldValue: String? 122 | 123 | let observer = TestObserver() 124 | 125 | observer.observe($text) 126 | { 127 | observedOldValue = $0.old 128 | observedNewValue = $0.new 129 | } 130 | 131 | text = "new text" 132 | 133 | XCTAssertEqual(observedOldValue, "old text") 134 | XCTAssertEqual(observedNewValue, "new text") 135 | } 136 | 137 | func testSendOnVariable() 138 | { 139 | let initialText = "initial text" 140 | 141 | let text = Var(initialText) 142 | 143 | var observedText: String? 144 | 145 | let observer = TestObserver() 146 | 147 | observer.observe(text) { observedText = $0.new } 148 | 149 | text.send() 150 | 151 | XCTAssertEqual(observedText, initialText) 152 | } 153 | 154 | class TestObserver: Observer 155 | { 156 | let receiver = Receiver() 157 | } 158 | } 159 | --------------------------------------------------------------------------------