SKPublished Property Wrapper: Reactive State Management in SectionKit
SKPublished is a Swift property wrapper that transforms stored properties into Combine-compatible observable publishers, enabling reactive data binding without the boilerplate of manual subject management.
The SKPublished property wrapper is a core reactive primitive in the linhay/sectionkit open-source library. Unlike SwiftUI's built-in @Published, this implementation provides granular control over publishing semantics and includes a composable transform system for customizing data flow. As implemented in Sources/SectionKit/Common/SKPublished.swift, it bridges imperative property assignment with declarative Combine pipelines, making it ideal for UIKit-based sectioned interfaces that require precise state synchronization.
Core Architecture and Components
Under the hood, SKPublished composes three distinct types defined in Sources/SectionKit/Common/SKPublished.swift:
SKPublishedValue acts as a custom Publisher that maintains the current state and forwards changes through Combine pipelines. It wraps either a PassthroughSubject or CurrentValueSubject depending on the configuration.
SKPublishedTransform provides a type-safe, composable description of publisher transformations. These transforms inject behaviors—such as deduplication or thread hopping—without requiring manual Combine chain construction at every call site.
SKPublished (the wrapper itself) exposes the stored value through wrappedValue and the observable interface through projectedValue (accessed via $property). This dual interface allows both imperative assignment and reactive subscription.
Publishing Behaviors with SKPublishedKind
The SKPublishedKind enum defines two distinct broadcasting strategies that determine which Combine subject powers the observable:
public enum SKPublishedKind {
case passThrough // Emits every assignment regardless of previous value
case currentValue // Starts with the initial value and emits subsequent changes
}
Pass-through behavior uses PassthroughSubject, emitting events only when the value changes after initialization. Current-value behavior uses CurrentValueSubject, immediately emitting the initial value to new subscribers and caching the latest value for future subscribers.
Composable Transform Pipelines
Transforms are stored in SKPublishedValue.transforms and applied sequentially when the publisher property is accessed. The SKPublishedTransform type provides several built-in helpers:
.print(prefix:)– Logs value changes to the console with a custom prefix for debugging..receiveOnMainQueue()– Ensures delivery on the main dispatch queue for UI updates..removeDuplicates()/.removeDuplicates(by:)– Filters out consecutive equal values to prevent redundant processing..dropFirst(count:)– Skips the specified number of initial emissions..filter(_:)– Applies a predicate to conditionally emit values.
These transforms are declared at the property definition site and execute automatically upon subscription.
Practical Implementation Examples
Observing a Basic Property
Any stored property can become observable by applying the wrapper. Accessing $count returns the SKPublishedValue publisher:
class Counter {
@SKPublished var count: Int = 0
}
let counter = Counter()
// Subscribe to changes
let cancellable = counter.$count.sink { newValue in
print("Count changed to:", newValue)
}
// Update the value – subscriber prints the change
counter.count = 1 // → "Count changed to: 1"
counter.count = 2 // → "Count changed to: 2"
Chaining Transforms for Logging and Deduplication
Transforms are passed as an array to the wrapper initializer. The following configuration logs all changes while suppressing duplicate consecutive values:
class Settings {
@SKPublished(
transform: [
.print(prefix: "🔧"),
.removeDuplicates()
]
)
var mode: String = "light"
}
let settings = Settings()
let sub = settings.$mode.sink { _ in } // subscription needed to activate pipeline
settings.mode = "dark" // prints: [SKPublished] 🔧 light => dark
settings.mode = "dark" // no output – duplicate filtered
Fire-on-Every-Assignment with passThrough
When you need to emit events even if the value remains identical, specify kind: .passThrough:
class Logger {
@SKPublished(kind: .passThrough) var message: String = ""
}
let logger = Logger()
let cancellable = logger.$message.sink { print("Log:", $0) }
logger.message = "Start" // prints "Log: Start"
logger.message = "Start" // prints again because `.passThrough` ignores previous value
Binding to UI Controls
The wrapper integrates with UIKit through the bind(_:) method, which handles thread safety and memory management:
class ViewModel {
@SKPublished var title: String = "Hello"
}
let vm = ViewModel()
let label = UILabel()
// Bind label text to the published value
let bindCancellable = vm.$title.bind { newTitle in
label.text = newTitle
}
// Updating the model updates the label automatically
vm.title = "World" // label.text becomes "World"
Broadcasting Void Events
For stateless triggers like refresh controls, SKPublished supports Void types with a convenience send() method:
class RefreshTrigger {
@SKPublished var refresh = ()
}
let trigger = RefreshTrigger()
let cancellable = trigger.$refresh.sink { _ in
print("Refresh requested")
}
trigger.refresh = () // prints "Refresh requested"
trigger.refresh() // convenience `send()` for Void publishes again
Thread Safety and Main Queue Delivery
As implemented in Sources/SectionKit/Common/SKPublished.swift, the bind(_:) helper ensures thread safety by optionally delivering the initial value on the main queue. This prevents UI updates from occurring on background threads when binding model properties to view layers. The receiveOnMainQueue() transform provides similar guarantees for custom subscriber implementations.
Summary
- SKPublished is a Combine-compatible property wrapper defined in
Sources/SectionKit/Common/SKPublished.swiftthat converts stored properties into reactive publishers. - It supports two publishing strategies via SKPublishedKind:
currentValue(caching) andpassThrough(event-only). - SKPublishedTransform enables declarative composition of behaviors like deduplication, logging, and thread dispatch without manual Combine chaining.
- The
projectedValue(accessed with$) exposesSKPublishedValue, which conforms toPublisherand supports binding helpers for UIKit integration. - Thread safety is handled through built-in main-queue delivery options, making it suitable for UI-driven architectures.
Frequently Asked Questions
How does SKPublished differ from SwiftUI's @Published?
While both wrappers enable Combine observation, SKPublished provides explicit control over publishing semantics through SKPublishedKind and supports composable transforms. SwiftUI's @Published always behaves like currentValue semantics and lacks the built-in transform pipeline system found in SectionKit's implementation.
Can SKPublished be used with optional values?
Yes. The wrapper includes dedicated initializers for optional types, simplifying usage with nullable model fields. You can declare @SKPublished var name: String? = nil and subscribe to the optional stream directly through the projected value.
What Combine subject does SKPublished use internally?
The implementation selects between CurrentValueSubject for .currentValue kind and PassthroughSubject for .passThrough kind. This choice is made at initialization and determines whether new subscribers receive the current cached value immediately or only receive subsequent updates.
Is SKPublished thread-safe for UI updates?
Yes. When using the bind(_:) method or the .receiveOnMainQueue() transform, values are delivered on the main thread. The wrapper is designed to prevent common threading issues in UIKit applications by ensuring that mutable state observations occur on the appropriate dispatch queue.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →