|
| 1 | +# Publishable |
| 2 | + |
| 3 | + |
| 4 | +[](https://codecov.io/github/NSFatalError/Publishable) |
| 5 | + |
| 6 | +Synchronous observation of `Observable` changes through `Combine` |
| 7 | + |
| 8 | +#### Contents |
| 9 | +- [What Problem Publishable Solves?](#what-problem-publishable-solves) |
| 10 | +- [How Publishable Works?](#how-publishable-works) |
| 11 | +- [Installation](#installation) |
| 12 | + |
| 13 | +## What Problem Publishable Solves? |
| 14 | + |
| 15 | +With the introduction of [SE-0475: Transactional Observation of Values](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0475-observed.md), |
| 16 | +Swift gains built-in support for observing changes to `Observable` types. This solution is great, but it only covers some of the use cases, as it |
| 17 | +publishes the updates via an `AsyncSequence`. |
| 18 | + |
| 19 | +In some scenarios, however, developers need to perform actions synchronously - immediately after a change occurs. |
| 20 | + |
| 21 | +This is where `Publishable` comes in. It allows `Observation` and `Combine` to coexist within a single type, letting you take advantage of the latest |
| 22 | +`Observable` features, while processing changes synchronously when needed. It even works with the `SwiftData.Model` macro! |
| 23 | + |
| 24 | +```swift |
| 25 | +import Publishable |
| 26 | + |
| 27 | +@Publishable @Observable |
| 28 | +final class Person { |
| 29 | + var name = "John" |
| 30 | + var surname = "Doe" |
| 31 | + |
| 32 | + var fullName: String { |
| 33 | + "\(name) \(surname)" |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +let person = Person() |
| 38 | +let nameCancellable = person.publisher.name.sink { name in |
| 39 | + print("Name -", name) |
| 40 | +} |
| 41 | +let fullNameCancellable = person.publisher.fullName.sink { fullName in |
| 42 | + print("Full name -", fullName) |
| 43 | +} |
| 44 | + |
| 45 | +// Initially prints (same as `Published` property wrapper): |
| 46 | +// Name - John |
| 47 | +// Full name - John Doe |
| 48 | + |
| 49 | +person.name = "Kamil" |
| 50 | +// Prints: |
| 51 | +// Name - Kamil |
| 52 | +// Full name - Kamil Doe |
| 53 | + |
| 54 | +person.surname = "Strzelecki" |
| 55 | +// Prints: |
| 56 | +// Full name - Kamil Strzelecki |
| 57 | +``` |
| 58 | + |
| 59 | +## How Publishable Works? |
| 60 | + |
| 61 | +The `@Publishable` macro relies on two key properties of Swift Macros and `Observation` module: |
| 62 | +- Macro expansions are compiled in the context of the module where they’re used. This allows references in the macro to be overloaded by locally available symbols. |
| 63 | +- Swift exposes `ObservationRegistrar` as a documented, public API, making it possible to use it safely and directly. |
| 64 | + |
| 65 | +`Publishable` leverages these facts to overload the default `ObservationRegistrar` with a custom one that: |
| 66 | +- Forwards changes to Swift’s native `ObservationRegistrar` |
| 67 | +- Simultaneously emits values through generated `Combine` publishers |
| 68 | + |
| 69 | +While I acknowledge that this usage might not have been intended by the authors, I would refrain from calling it a hack. |
| 70 | +It relies solely on well-understood behaviors of Swift and its public APIs. |
| 71 | + |
| 72 | +This approach has been carefully tested and verified to work with both `Observable` and `SwiftData.Model` macros. |
| 73 | + |
| 74 | +## Installation |
| 75 | + |
| 76 | +```swift |
| 77 | +.package( |
| 78 | + url: "https://github.com/NSFatalError/Publishable", |
| 79 | + from: "1.0.0" |
| 80 | +) |
| 81 | +``` |
0 commit comments