How Reactive Binding Works in SectionKit: A Complete Guide to @SKPublished and Combine
SectionKit implements reactive binding through pure-Swift property wrappers @SKPublished and @SKBinding built on Combine, enabling automatic UI updates when state changes without manual reloads.
SectionKit is an open-source Swift framework that simplifies collection view management through a reactive data flow architecture. Understanding how reactive binding works in SectionKit allows developers to build declarative, fine-grained UI updates that sync automatically with underlying state changes using standard Combine publishers.
Core Components of the Reactive System
SectionKit's reactive architecture centers on four interconnected components that work together to provide type-safe, memory-efficient data binding.
@SKPublished Property Wrapper
The @SKPublished property wrapper, defined in Sources/SectionKit/Common/SKPublished.swift, serves as the primary state holder for reactive values. When you annotate a property with @SKPublished, the wrapper creates an underlying SKPublishedValue instance that acts as a Combine publisher.
SKPublishedValue can wrap either a CurrentValueSubject (the default) or a PassthroughSubject depending on the SKPublishedKind configuration. The publisher executes attached SKPublishedTransform callbacks—such as removeDuplicates or receiveOnMainQueue—before emitting values to subscribers.
@SKPublished var counter = 0
@SKPublished var isSelected = false
@SKBinding for Two-Way Connections
Defined in Sources/SectionKit/Common/SKBinding.swift, the @SKBinding property wrapper exposes a getter and setter pair alongside a changedPublisher. Unlike @SKPublished, which owns the state, SKBinding can reference arbitrary storage via key-paths, CurrentValueSubject instances, or other SKBinding objects, enabling composition of reactive chains.
let binding = SKBinding(on: model, keyPath: \.isSelected)
binding.wrappedValue = true // Equivalent to model.isSelected = true
The bind(_:) Extension Method
SKPublishedValue provides a bind(_:) convenience method that immediately delivers the current value on the main thread before subscribing to future updates. This method returns an AnyCancellable for memory management, ensuring subscribers can store the token in a Set<AnyCancellable> or similar collection.
viewModel.$counter.bind { [weak self] newValue in
self?.counterLabel.text = "Count: \(newValue)"
}.store(in: &cancellables)
SKCollectionViewController Integration
The UI layer SKCollectionViewController (located in Sources/SectionUI/CollectionView/SectionCollectionView/SKCollectionViewController.swift) hosts a manager that coordinates sections and cells. Cells configured with SKConfigurableView receive models containing @SKPublished properties, establishing per-cell reactive subscriptions during the config(_:) lifecycle.
The Reactive Data Flow in Practice
SectionKit implements a five-step data flow that connects state changes to UI updates without requiring manual diff calculations or view reloads.
1. Declare reactive state with @SKPublished
Properties annotated with @SKPublished automatically generate a projected value (accessed via $) that returns the underlying SKPublishedValue publisher.
class CounterVM {
@SKPublished var count = 0
}
2. Subscribe to changes using bind
Subscribers receive the current value immediately on the main thread, followed by all subsequent changes. This pattern eliminates the need for initial manual UI configuration.
vm.$count.bind { [weak self] v in
self?.label.text = "Count: \(v)"
}.store(in: &bag)
3. Update values through the wrapper
Assignments to the wrapped value trigger the publisher, automatically notifying all bound subscribers.
vm.count += 1 // Triggers UI update
4. Leverage SKBinding for complex getters and setters
When binding UI controls directly to model properties, SKBinding provides a lightweight abstraction over key-paths or subjects.
let binding = SKBinding(on: model, keyPath: \.title)
binding.changedPublisher
.sink { print("Title changed to \($0)") }
.store(in: &bag)
5. Establish cell-level subscriptions during configuration
Each cell maintains its own Set<AnyCancellable>, clearing previous subscriptions in config(_:) to prevent memory leaks while ensuring reactive updates persist across reuse cycles.
Implementing Reactive Binding in Collection Views
The ReactiveDataViewController.swift example in the repository demonstrates end-to-end reactive patterns at both the view-controller and cell levels.
View-Controller Level Binding
class CounterVC: SKCollectionViewController {
let vm = CounterVM()
private var bag = Set<AnyCancellable>()
private let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
vm.$count.bind { [weak self] v in
self?.label.text = "Count: \(v)"
}.store(in: &bag)
}
@objc func increment() {
vm.count += 1
}
}
Cell-Level Reactive Configuration
Cells implementing SKConfigurableView establish bindings during configuration, enabling fine-grained animations and state changes without reloading the entire collection view.
class ColorCellModel {
@SKPublished var isSelected = false
var color: UIColor?
}
class ColorCell: UICollectionViewCell, SKConfigurableView {
private var bag = Set<AnyCancellable>()
func config(_ model: ColorCellModel) {
bag.removeAll()
contentView.backgroundColor = model.color ?? .gray
model.$isSelected.bind { [weak self] selected in
UIView.animate(withDuration: 0.3) {
self?.layer.borderWidth = selected ? 4 : 0
self?.transform = selected
? CGAffineTransform(scaleX: 0.9, y: 0.9)
: .identity
}
}.store(in: &bag)
}
}
Advanced Usage with SKBinding
SKBinding supports composition through initialization with CurrentValueSubject or other SKBinding instances, enabling derived state and complex reactive chains.
// Binding to a subject
let subject = CurrentValueSubject<Bool, Never>(false)
let binding = SKBinding(subject: subject)
// Binding to another binding
let derivedBinding = SKBinding(binding: originalBinding)
Summary
- @SKPublished creates
SKPublishedValuepublishers that emit changes through CombineCurrentValueSubjectorPassthroughSubjectinstances, located inSources/SectionKit/Common/SKPublished.swift. - SKPublishedValue supports transform chains like
removeDuplicatesand guarantees main-thread delivery through thebind(_:)method. - @SKBinding provides a property wrapper interface for arbitrary getter/setter pairs, enabling two-way data flow without owning the underlying storage.
- Cell-level integration occurs through
SKConfigurableView, where cells storeAnyCancellabletokens in per-instance bags to manage subscription lifecycles. - Thread safety is handled internally by
bind(_:), which ensures initial values and updates arrive on the main queue suitable for UIKit operations.
Frequently Asked Questions
What is the difference between @SKPublished and @SKBinding in SectionKit?
@SKPublished owns the state and acts as the source of truth, creating a publisher that emits when the wrapped value changes. @SKBinding does not own state; it provides read-write access to existing storage via key-paths or subjects and exposes a changedPublisher for observation. Use @SKPublished for view models and models, and @SKBinding when connecting UI controls to existing properties.
How does SectionKit ensure thread safety when updating UI?
The bind(_:) extension on SKPublishedValue automatically delivers the current value and all subsequent updates on the main thread. This main-queue guarantee ensures that UI updates triggered by reactive bindings occur safely within UIKit's requirements, regardless of which thread initiated the state change.
Can I use SectionKit's reactive binding without SKCollectionViewController?
Yes. The reactive components @SKPublished, SKPublishedValue, and SKBinding are defined in the core SectionKit module and have no dependency on SKCollectionViewController or UIKit. You can use them in any Swift context requiring Combine-based reactivity, including SwiftUI integration or standalone view controllers.
What Combine publishers does SKPublishedValue use internally?
According to the source code in SKPublished.swift, SKPublishedValue uses CurrentValueSubject by default (maintaining the current value for new subscribers) or PassthroughSubject when configured with SKPublishedKind.passthrough. Both publishers conform to Combine's Subject protocol and support standard operators like sink and assign.
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 →