Documentation Index
Fetch the complete documentation index at: https://mintlify.com/pointfreeco/swift-composable-architecture/llms.txt
Use this file to discover all available pages before exploring further.
While TCA was designed with SwiftUI in mind, it provides comprehensive tools for UIKit applications through the UIKitNavigation framework.
Basic UIKit Integration
Using @UIBindable
The @UIBindable property wrapper enables observation and binding creation in UIKit view controllers:
import ComposableArchitecture
import UIKit
class FeatureViewController: UIViewController {
@UIBindable var store: StoreOf<Feature>
init(store: StoreOf<Feature>) {
self.store = store
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
}
Using @ViewAction
The @ViewAction macro simplifies sending actions from view controllers:
@ViewAction(for: Feature.self)
class FeatureViewController: UIViewController {
@UIBindable var store: StoreOf<Feature>
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(
type: .system,
primaryAction: UIAction { [weak self] _ in
self?.send(.buttonTapped)
}
)
}
}
Source: LoginViewController.swift:6-12
Observing State Changes
Use the observe method to react to state changes:
override func viewDidLoad() {
super.viewDidLoad()
let titleLabel = UILabel()
let activityIndicator = UIActivityIndicatorView()
observe { [weak self, weak titleLabel, weak activityIndicator] in
guard let self else { return }
titleLabel?.text = store.title
titleLabel?.isHidden = store.isTitleHidden
if store.isLoading {
activityIndicator?.startAnimating()
} else {
activityIndicator?.stopAnimating()
}
}
}
Source: LoginViewController.swift:86-92
Always use [weak self] and [weak view] captures in observe closures to prevent retain cycles.
Two-Way Bindings
Create UIKit bindings using the $store syntax:
let emailTextField = UITextField(text: $store.email)
emailTextField.placeholder = "email@address.com"
emailTextField.borderStyle = .roundedRect
let passwordTextField = UITextField(text: $store.password)
passwordTextField.placeholder = "Password"
passwordTextField.isSecureTextEntry = true
Source: LoginViewController.swift:42-50
Navigation
Navigation Destinations
Use navigationDestination(item:) to handle push navigation:
override func viewDidLoad() {
super.viewDidLoad()
navigationDestination(
item: $store.scope(state: \.destination, action: \.destination)
) { store in
DestinationViewController(store: store)
}
}
Source: LoginViewController.swift:98-100
Stack-Based Navigation
For more complex navigation, use NavigationStackController:
import UIKitNavigation
let navigationController = NavigationStackController(
path: $store.scope(state: \.path, action: \.path),
root: { RootViewController(store: store) },
destination: { store in
switch store.case {
case .detail:
DetailViewController(store: store)
case .settings:
SettingsViewController(store: store)
}
}
)
Source: NavigationStackControllerUIKit.swift:4-78
Programmatic Navigation
Push to the stack programmatically using UIPushAction:
@available(iOS 17, *)
@MainActor
func pushToDetail() {
let pushAction = UIPushAction()
pushAction(state: DetailFeature.State())
}
Source: NavigationStackControllerUIKit.swift:80-110
Alerts and Dialogs
Presenting Alerts
Use UIAlertController with store scoping:
override func viewDidLoad() {
super.viewDidLoad()
present(item: $store.scope(state: \.alert, action: \.alert)) { store in
UIAlertController(store: store)
}
}
Source: LoginViewController.swift:94-96
Alert State Integration
Define alerts in your feature’s state:
@Reducer
struct Feature {
@ObservableState
struct State {
@Presents var alert: AlertState<Action.Alert>?
}
enum Action {
case alert(PresentationAction<Alert>)
enum Alert {
case confirmDelete
case cancel
}
}
}
The UIAlertController initializer automatically handles the alert presentation:
public convenience init<Action>(
store: Store<AlertState<Action>, Action>
) {
// Automatically presents alert from store state
}
Source: AlertStateUIKit.swift:7-30
Confirmation Dialogs
Similar to alerts, present confirmation dialogs:
present(item: $store.scope(state: \.dialog, action: \.dialog)) { store in
UIAlertController(store: store)
}
Source: AlertStateUIKit.swift:32-54
Modal Presentation
Present view controllers modally using the present(item:) method:
override func viewDidLoad() {
super.viewDidLoad()
present(
item: $store.scope(state: \.sheet, action: \.sheet),
modalPresentationStyle: .formSheet
) { store in
SheetViewController(store: store)
}
}
Custom Bindings
Create custom bindings for UIKit controls:
let slider = UISlider()
observe { [weak self, weak slider] in
guard let self else { return }
slider?.value = Float(store.volume)
}
slider.addAction(
UIAction { [weak self] action in
if let slider = action.sender as? UISlider {
self?.send(.volumeChanged(Double(slider.value)))
}
},
for: .valueChanged
)
Best Practices
Memory Management
Always use weak references in closures:
// Good: Prevents retain cycles
observe { [weak self, weak label] in
guard let self else { return }
label?.text = store.text
}
// Bad: Creates retain cycle
observe {
self.label.text = self.store.text
}
View Lifecycle
Set up observations in viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupObservations() // Observe state here
}
private func setupObservations() {
observe { [weak self] in
// Update UI from store
}
}
Combine @UIBindable with @ViewAction
Use both macros together for the best experience:
@ViewAction(for: Feature.self)
class FeatureViewController: UIViewController {
@UIBindable var store: StoreOf<Feature>
// @ViewAction provides the send() method
// @UIBindable provides the $store binding
}
Some UIKit integration features require specific iOS versions:
#if canImport(UIKit) && !os(watchOS)
// UIKit code here
#endif
@available(iOS 17, macOS 14, tvOS 17, *)
func useModernFeature() {
// iOS 17+ features
}
Source: NavigationStackControllerUIKit.swift:1-3
Complete Example
Here’s a complete UIKit view controller example:
import ComposableArchitecture
import UIKit
@ViewAction(for: Login.self)
class LoginViewController: UIViewController {
@UIBindable var store: StoreOf<Login>
init(store: StoreOf<Login>) {
self.store = store
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Login"
view.backgroundColor = .systemBackground
let emailTextField = UITextField(text: $store.email)
emailTextField.placeholder = "email@address.com"
emailTextField.borderStyle = .roundedRect
let passwordTextField = UITextField(text: $store.password)
passwordTextField.placeholder = "Password"
passwordTextField.isSecureTextEntry = true
let loginButton = UIButton(
type: .system,
primaryAction: UIAction { [weak self] _ in
self?.send(.loginButtonTapped)
}
)
loginButton.setTitle("Login", for: .normal)
let activityIndicator = UIActivityIndicatorView()
// Layout code...
observe { [weak self, weak activityIndicator, weak loginButton] in
guard let self else { return }
loginButton?.isEnabled = !store.isLoading
if store.isLoading {
activityIndicator?.startAnimating()
} else {
activityIndicator?.stopAnimating()
}
}
present(item: $store.scope(state: \.alert, action: \.alert)) { store in
UIAlertController(store: store)
}
navigationDestination(
item: $store.scope(state: \.destination, action: \.destination)
) { store in
DestinationViewController(store: store)
}
}
}
Migration from ViewStore
If you’re migrating from the legacy ViewStore pattern:
// Old pattern (deprecated)
let viewStore = ViewStore(store, observe: { $0 })
viewStore.send(.action)
let value = viewStore.state.value
// New pattern
@UIBindable var store: StoreOf<Feature>
store.send(.action) // Or send(.action) with @ViewAction
let value = store.value