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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .spi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: 1
builder:
configs:
- documentation_targets:
- Publishable
14 changes: 14 additions & 0 deletions Sources/Publishable/Documentation.docc/Publishable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# ``Publishable-module``

Observe changes to `Observable` types synchronously with `Combine`.

## Topics

### Making Types Publishable

- ``Publishable()``
- ``Publishable-protocol``

### Getting Property Publishers

- ``AnyPropertyPublisher``
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,24 @@

import Combine

/// An object that exposes `Combine` publishers for ``willChange`` and ``didChange`` events.
///
/// Subclasses of this class are generated by the ``Publishable()`` macro,
/// and provide publishers for all mutable instance properties of the type the macro is applied to.
///
open class AnyPropertyPublisher<Object: AnyObject> {

private let _willChange = PassthroughSubject<Object, Never>()
private let _didChange = PassthroughSubject<Object, Never>()

/// Emits the `Object` **before** any of its stored properties are assigned a new value.
///
public var willChange: AnyPublisher<Object, Never> {
_willChange.eraseToAnyPublisher()
}

/// Emits the `Object` **after** any of its stored properties are assigned a new value.
///
public var didChange: AnyPublisher<Object, Never> {
_didChange.eraseToAnyPublisher()
}
Expand Down
31 changes: 30 additions & 1 deletion Sources/Publishable/PropertyPublisher/Publishable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@
// Copyright © 2025 Kamil Strzelecki. All rights reserved.
//

import Observation

/// A macro that adds ``Publishable-protocol`` conformance to `Observable` types.
///
/// - Note: This macro works only with `final` classes to which the `Observable` or `SwiftData.Model` macro has been applied directly.
///
/// The `Publishable` macro adds a new ``Publishable/publisher`` property to your type,
/// which exposes `Combine` publishers for all mutable instance properties - both stored and computed.
///
/// If a property’s type conforms to `Equatable`, its publisher automatically removes duplicate values.
/// Just like the `Published` property wrapper, subscribing to any of the exposed publishers immediately emits the current value.
///
/// - Important: Swift Macros do not have access to full type information of expressions used in the code they’re applied to.
/// Since working with `Combine` requires knowledge of concrete types, this macro attempts to infer the types of properties when they are not explicitly specified.
/// However, this inference may fail in non-trivial cases. If the generated code fails to compile, explicitly specifying the type of the affected property should resolve the issue.
///
@attached(
member,
names: named(_publisher),
Expand All @@ -22,9 +38,22 @@ public macro Publishable() = #externalMacro(
type: "PublishableMacro"
)

public protocol Publishable: AnyObject {
/// A type that can be observed using both the `Observation` and `Combine` frameworks.
///
/// You don't need to declare conformance to this protocol yourself.
/// It is generated automatically when you apply the ``Publishable()`` macro to your type.
///
public protocol Publishable: AnyObject, Observable {

/// A subclass of ``AnyPropertyPublisher`` generated by the ``Publishable()`` macro,
/// containing publishers for all mutable instance properties of the type.
///
associatedtype PropertyPublisher: AnyPropertyPublisher<Self>

/// An instance that exposes `Combine` publishers for all mutable instance properties of the type.
///
/// - Important: Don't store this instance in an external property. Accessing it after the original object has been deallocated
/// may result in a crash. Always access it directly through the object that exposes it.
///
var publisher: PropertyPublisher { get }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Observation

@_documentation(visibility: private)
public protocol PublishableObservationRegistrar {

associatedtype Object: Publishable, Observable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@

import Observation

@_documentation(visibility: private) // swiftformat:disable:next all
public typealias SwiftObservationRegistrar = ObservationRegistrar
Loading