Swift Property Wrappers, Bindings and Combine Publishers for UI Gestures, Controls and Views
CombineUI provides Swift property wrappers for select Apple types such as gestures, controls and views. These property wrappers project Combine publishers for gesture recognition, control events, property changes and delegate callbacks.
Example:
@Button var button = UIButton()
The
$button
projected value is a publisher that will send when the button is tapped.
And bindings are provided for updating properties of select Apple views with values from Combine publishers.
- The property wrappers are for publishing (i.e. to publish a value when a UI element changes).
- The bindings are for subscribing (i.e. to update a UI element with a published value).
As compared to subscribing via Combine's' assign(to:on:)
method, subscribing via CombineUI's bindings is preferable since they use weak
references and can accept method arguments.
Example:
publisher.bind(to: button.bindable.title(for: .normal))
Many types are also enhanced with Swift extensions providing Combine publishers that may be used as an alternative to the property wrappers when needed.
UIKit is currently supported. CombineUI may support additional UI frameworks in the future.
- iOS
15.0
- Swift
5.8
Package Dependency
Replace
<version>
with the desired minimum version.
.package(url: "https://github.com/Tinder/CombineUI.git", from: "<version>")
Target Dependency
"CombineUI"
https://swiftpackageindex.com/Tinder/collection.json
All examples require this setup:
import Combine
import CombineUI
import UIKit
var cancellables = Set<AnyCancellable>()
@Button var button = UIButton()
button.setImage(UIImage(systemName: "heart"), for: .normal)
$button
.sink { print("Tapped") }
.store(in: &cancellables)
let label = UILabel()
let subject = PassthroughSubject<String, Never>()
subject
.bind(to: label.bindable.text)
.store(in: &cancellables)
subject.send("Text")
let viewController = UIViewController()
let lifecycle = viewController
.lifecyclePublisher()
.share()
lifecycle
.sink { print($0) } // .viewWillAppear | .viewDidAppear | .viewWillDisappear | .viewDidDisappear
.store(in: &cancellables)
lifecycle
.isVisiblePublisher()
.sink { print($0) } // true | false
.store(in: &cancellables)
Additional examples are available within an Xcode project included in this repository. See the example project README containing setup instructions for guidance.
Property Wrappers
Property Wrapper | Projected Type | |
---|---|---|
UIButton |
@Button |
AnyPublisher<Void, Never> |
UIControl |
@Control |
ControlInterface |
UIDatePicker |
@DatePicker |
DatePickerInterface |
UIGestureRecognizer |
@GestureRecognizer |
GestureRecognizerInterface |
UIPageControl |
@PageControl |
AnyPublisher<Int, Never> |
UIRefreshControl |
@RefreshControl |
AnyPublisher<Void, Never> |
UIScrollView |
@ScrollView |
ScrollViewInterface |
UISearchBar |
@SearchBar |
SearchBarInterface |
UISegmentedControl |
@SegmentedControl |
AnyPublisher<Int, Never> |
UISlider |
@Slider |
AnyPublisher<Float, Never> |
UIStepper |
@Stepper |
AnyPublisher<Double, Never> |
UISwitch |
@Switch |
AnyPublisher<Bool, Never> |
UITextField |
@TextField |
TextFieldInterface |
UITextView |
@TextView |
TextViewInterface |
UIViewController |
@ViewController |
ViewControllerInterface |
Bindings
Binding | Type | |
---|---|---|
UIActivityIndicatorView |
style |
UIActivityIndicatorView.Style |
color |
UIColor |
|
hidesWhenStopped |
Bool |
|
isAnimating |
Bool |
|
UIButton |
titleColor(for: UIControl.State) |
UIColor |
titleShadowColor(for: UIControl.State) |
UIColor |
|
title(for: UIControl.State) |
String |
|
attributedTitle(for: UIControl.State) |
AttributedString |
|
image(for: UIControl.State) |
UIImage? |
|
backgroundImage(for: UIControl.State) |
UIImage? |
|
UIControl |
isEnabled |
Bool |
UIDatePicker |
countDownDuration |
TimeInterval |
date |
Date |
|
date(animated: Bool) |
Date |
|
UIGestureRecognizer |
isEnabled |
Bool |
UIImageView |
image |
UIImage? |
highlightedImage |
UIImage? |
|
isHighlighted |
Bool |
|
UILabel |
isEnabled |
Bool |
font |
UIFont |
|
textColor |
UIColor |
|
text |
String |
|
attributedText |
AttributedString |
|
UIPageControl |
pageIndicatorTintColor |
UIColor |
currentPageIndicatorTintColor |
UIColor |
|
currentPage |
Int |
|
numberOfPages |
Int |
|
hidesForSinglePage |
Bool |
|
UIProgressView |
trackTintColor |
UIColor |
progressTintColor |
UIColor |
|
progress |
Float |
|
progress(animated: Bool) |
Float |
|
UIRefreshControl |
tintColor |
UIColor |
attributedTitle |
AttributedString |
|
isRefreshing |
Bool |
|
UISegmentedControl |
isMomentary |
Bool |
selectedSegmentIndex |
Int |
|
isEnabledForSegment(at: Int) |
Bool |
|
widthForSegment(at: Int) |
CGFloat |
|
titleForSegment(at: Int) |
String |
|
imageForSegment(at: Int) |
UIImage? |
|
UISlider |
isContinuous |
Bool |
minimumValue |
Float |
|
maximumValue |
Float |
|
minimumTrackTintColor |
UIColor |
|
maximumTrackTintColor |
UIColor |
|
thumbTintColor |
UIColor |
|
value |
Float |
|
value(animated: Bool) |
Float |
|
UIStepper |
isContinuous |
Bool |
autorepeat |
Bool |
|
wraps |
Bool |
|
minimumValue |
Double |
|
maximumValue |
Double |
|
stepValue |
Double |
|
value |
Double |
|
UISwitch |
onTintColor |
UIColor |
thumbTintColor |
UIColor |
|
isOn |
Bool |
|
isOn(animated: Bool) |
Bool |
|
UITextField |
font |
UIFont |
textColor |
UIColor |
|
textAlignment |
NSTextAlignment |
|
placeholder |
String |
|
attributedPlaceholder |
AttributedString |
|
text |
String |
|
attributedText |
AttributedString |
|
UITextView |
isEditable |
Bool |
font |
UIFont |
|
textColor |
UIColor |
|
textAlignment |
NSTextAlignment |
|
text |
String |
|
attributedText |
AttributedString |
|
UIView |
isUserInteractionEnabled |
Bool |
isMultipleTouchEnabled |
Bool |
|
isExclusiveTouch |
Bool |
|
clipsToBounds |
Bool |
|
tintColor |
UIColor |
|
backgroundColor |
UIColor |
|
borderColor |
UIColor |
|
shadowColor |
UIColor |
|
alpha |
CGFloat |
|
isOpaque |
Bool |
|
isHidden |
Bool |
Extension Methods
Method | Type | |
---|---|---|
UIButton |
tapPublisher() |
AnyPublisher<Void, Never> |
UIControl |
publisher(for: UIControl.Event) |
AnyPublisher<UIControl.Event, Never> |
UIDatePicker |
countDownDurationPublisher() |
AnyPublisher<TimeInterval, Never> |
datePublisher() |
AnyPublisher<Date, Never> |
|
UIGestureRecognizer |
publisher(attachingTo: UIView) |
AnyPublisher<UIGestureRecognizer, Never> |
UIPageControl |
currentPagePublisher() |
AnyPublisher<Int, Never> |
UIRefreshControl |
refreshPublisher() |
AnyPublisher<Void, Never> |
UISegmentedControl |
selectedSegmentIndexPublisher() |
AnyPublisher<Int, Never> |
UISlider |
valuePublisher() |
AnyPublisher<Float, Never> |
UIStepper |
valuePublisher() |
AnyPublisher<Double, Never> |
UISwitch |
isOnPublisher() |
AnyPublisher<Bool, Never> |
UITextField |
textPublisher() |
AnyPublisher<String, Never> |
attributedTextPublisher() |
AnyPublisher<AttributedString, Never> |
|
UIViewController |
lifecyclePublisher() |
AnyPublisher<ViewControllerLifecycleEvent, Never> |
Publisher where Output == ViewControllerLifecycleEvent |
isVisiblePublisher() |
AnyPublisher<Bool, Never> |
- UIKit
- Caveats
- Customization
var style: Binding<UIActivityIndicatorView.Style>
var color: Binding<UIColor>
var hidesWhenStopped: Binding<Bool>
var isAnimating: Binding<Bool>
let activityIndicatorView = UIActivityIndicatorView()
Just(.medium)
.bind(to: activityIndicatorView.bindable.style)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: activityIndicatorView.bindable.color)
.store(in: &cancellables)
Just(true)
.bind(to: activityIndicatorView.bindable.hidesWhenStopped)
.store(in: &cancellables)
Just(true)
.bind(to: activityIndicatorView.bindable.isAnimating)
.store(in: &cancellables)
Event |
---|
.primaryActionTriggered |
@Button // Projected Value: AnyPublisher<Void, Never>
func titleColor(for state: UIControl.State) -> Binding<UIColor>
func titleShadowColor(for state: UIControl.State) -> Binding<UIColor>
func title(for state: UIControl.State) -> Binding<String>
func attributedTitle(for state: UIControl.State) -> Binding<AttributedString>
func image(for state: UIControl.State) -> Binding<UIImage?>
func backgroundImage(for state: UIControl.State) -> Binding<UIImage?>
func tapPublisher() -> AnyPublisher<Void, Never>
// Property Wrapper
@Button var button = UIButton()
button.setImage(UIImage(systemName: "heart"), for: .normal)
$button
.sink { print("Tapped") }
.store(in: &cancellables)
// Bindings
Just(.systemPink)
.bind(to: button.bindable.titleColor(for: .normal))
.store(in: &cancellables)
Just(.systemPink)
.bind(to: button.bindable.titleShadowColor(for: .normal))
.store(in: &cancellables)
Just("Title")
.bind(to: button.bindable.title(for: .normal))
.store(in: &cancellables)
Just(AttributedString("Title"))
.bind(to: button.bindable.attributedTitle(for: .normal))
.store(in: &cancellables)
Just(.checkmark)
.bind(to: button.bindable.image(for: .normal))
.store(in: &cancellables)
Just(.checkmark)
.bind(to: button.bindable.backgroundImage(for: .normal))
.store(in: &cancellables)
// Extension Method
button
.tapPublisher()
.sink { print("Tapped") }
.store(in: &cancellables)
@Control // Projected Value: ControlInterface
ControlInterface
var touchDown: AnyPublisher<Void, Never>
var touchDownRepeat: AnyPublisher<Void, Never>
var touchDragInside: AnyPublisher<Void, Never>
var touchDragOutside: AnyPublisher<Void, Never>
var touchDragEnter: AnyPublisher<Void, Never>
var touchDragExit: AnyPublisher<Void, Never>
var touchUpInside: AnyPublisher<Void, Never>
var touchUpOutside: AnyPublisher<Void, Never>
var touchCancel: AnyPublisher<Void, Never>
var valueChanged: AnyPublisher<Void, Never>
var menuActionTriggered: AnyPublisher<Void, Never>
var primaryActionTriggered: AnyPublisher<Void, Never>
var editingDidBegin: AnyPublisher<Void, Never>
var editingChanged: AnyPublisher<Void, Never>
var editingDidEnd: AnyPublisher<Void, Never>
var editingDidEndOnExit: AnyPublisher<Void, Never>
var isEnabled: Binding<Bool>
func publisher(for controlEvents: UIControl.Event) -> AnyPublisher<UIControl.Event, Never>
Use
contains()
on the OptionSet that is received to determine which event occurred.
Just as every CombineUI property wrapper and binding may be used with subclasses of their supported type, the UIControl
property wrapper and binding are compatible with UIControl
subclasses, including (but not limited to) the following:
UIButton
UIDatePicker
UIPageControl
UIRefreshControl
UISegmentedControl
UISlider
UIStepper
UISwitch
UITextField
// Property Wrapper
@Control var control = UIButton()
control.setImage(UIImage(systemName: "heart"), for: .normal)
$control
.primaryActionTriggered
.sink { print("Triggered") }
.store(in: &cancellables)
// Binding
Just(true)
.bind(to: control.bindable.isEnabled)
.store(in: &cancellables)
// Extension Method
control
.publisher(for: .primaryActionTriggered)
.sink { controlEvents in }
.store(in: &cancellables)
- Use the
@Control
property wrapper only when publishers for theUIControl.Event
types are needed, otherwise the type specific property wrappers should be preferred.
Property | Event |
---|---|
.countDownDuration |
.valueChanged |
.date |
.valueChanged |
@DatePicker(mode: UIDatePicker.Mode = .dateAndTime) // Projected Value: DatePickerInterface
DatePickerInterface
var countDownDuration: AnyPublisher<TimeInterval, Never>
var date: AnyPublisher<Date, Never>
var countDownDuration: Binding<TimeInterval>
var date: Binding<Date>
func date(animated: Bool) -> Binding<Date>
func datePublisher() -> AnyPublisher<Date, Never>
func countDownDurationPublisher() -> AnyPublisher<TimeInterval, Never>
// Property Wrapper
@DatePicker(mode: .countDownTimer)
var datePicker1 = UIDatePicker()
@DatePicker var datePicker2 = UIDatePicker()
$datePicker1
.countDownDuration
.sink { countDownDuration in }
.store(in: &cancellables)
$datePicker2
.date
.sink { date in }
.store(in: &cancellables)
// Bindings
Just(60)
.bind(to: datePicker1.bindable.countDownDuration)
.store(in: &cancellables)
Just(Date())
.bind(to: datePicker2.bindable.date)
.store(in: &cancellables)
Just(Date())
.bind(to: datePicker2.bindable.date(animated: true))
.store(in: &cancellables)
// Extension Methods
datePicker1
.countDownDurationPublisher()
.sink { countDownDuration in }
.store(in: &cancellables)
datePicker2
.datePublisher()
.sink { date in }
.store(in: &cancellables)
@GestureRecognizer // Projected Value: GestureRecognizerInterface
GestureRecognizerInterface
func attaching(to view: UIView) -> AnyPublisher<UIGestureRecognizer, Never>
var isEnabled: Binding<Bool>
func publisher(attachingTo view: UIView) -> AnyPublisher<UIGestureRecognizer, Never>
// Property Wrapper
@GestureRecognizer var swipe = UISwipeGestureRecognizer()
$swipe
.attaching(to: view)
.sink { swipe in }
.store(in: &cancellables)
// Binding
Just(true)
.bind(to: swipe.bindable.isEnabled)
.store(in: &cancellables)
// Extension Method
swipe
.publisher(attachingTo: view)
.sink { swipe in }
.store(in: &cancellables)
- The gesture recognizer will be added to the provided view automatically.
var image: Binding<UIImage?>
var highlightedImage: Binding<UIImage?>
var isHighlighted: Binding<Bool>
let imageView = UIImageView()
Just(.checkmark)
.bind(to: imageView.bindable.image)
.store(in: &cancellables)
Just(.checkmark)
.bind(to: imageView.bindable.highlightedImage)
.store(in: &cancellables)
Just(true)
.bind(to: imageView.bindable.isHighlighted)
.store(in: &cancellables)
var isEnabled: Binding<Bool>
var font: Binding<UIFont>
var textColor: Binding<UIColor>
var text: Binding<String>
var attributedText: Binding<AttributedString>
let label = UILabel()
Just(true)
.bind(to: label.bindable.isEnabled)
.store(in: &cancellables)
Just(.preferredFont(forTextStyle: .body))
.bind(to: label.bindable.font)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: label.bindable.textColor)
.store(in: &cancellables)
Just("Text")
.bind(to: label.bindable.text)
.store(in: &cancellables)
Just(AttributedString("Text"))
.bind(to: label.bindable.attributedText)
.store(in: &cancellables)
Property | Event |
---|---|
.currentPage |
.valueChanged |
@PageControl // Projected Value: AnyPublisher<Int, Never>
var pageIndicatorTintColor: Binding<UIColor>
var currentPageIndicatorTintColor: Binding<UIColor>
var currentPage: Binding<Int>
var numberOfPages: Binding<Int>
var hidesForSinglePage: Binding<Bool>
func currentPagePublisher() -> AnyPublisher<Int, Never>
// Property Wrapper
@PageControl var pageControl = UIPageControl()
$pageControl
.sink { currentPage in }
.store(in: &cancellables)
// Bindings
Just(.systemPink)
.bind(to: pageControl.bindable.pageIndicatorTintColor)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: pageControl.bindable.currentPageIndicatorTintColor)
.store(in: &cancellables)
Just(1)
.bind(to: pageControl.bindable.currentPage)
.store(in: &cancellables)
Just(1)
.bind(to: pageControl.bindable.numberOfPages)
.store(in: &cancellables)
Just(true)
.bind(to: pageControl.bindable.hidesForSinglePage)
.store(in: &cancellables)
// Extension Method
pageControl
.currentPagePublisher()
.sink { currentPage in }
.store(in: &cancellables)
var trackTintColor: Binding<UIColor>
var progressTintColor: Binding<UIColor>
var progress: Binding<Float>
func progress(animated: Bool) -> Binding<Float>
let progressView = UIProgressView()
Just(.systemPink)
.bind(to: progressView.bindable.trackTintColor)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: progressView.bindable.progressTintColor)
.store(in: &cancellables)
Just(1)
.bind(to: progressView.bindable.progress)
.store(in: &cancellables)
Just(1)
.bind(to: progressView.bindable.progress(animated: true))
.store(in: &cancellables)
Property | Event | Value |
---|---|---|
.isRefreshing |
.valueChanged |
true |
@RefreshControl // Projected Value: AnyPublisher<Void, Never>
var tintColor: Binding<UIColor>
var attributedTitle: Binding<AttributedString>
var isRefreshing: Binding<Bool>
func refreshPublisher() -> AnyPublisher<Void, Never>
// Property Wrapper
@RefreshControl var refreshControl = UIRefreshControl()
$refreshControl
.sink { print("Refreshing") }
.store(in: &cancellables)
// Bindings
Just(.systemPink)
.bind(to: refreshControl.bindable.tintColor)
.store(in: &cancellables)
Just(AttributedString("Title"))
.bind(to: refreshControl.bindable.attributedTitle)
.store(in: &cancellables)
Just(true)
.bind(to: refreshControl.bindable.isRefreshing)
.store(in: &cancellables)
// Extension Method
refreshControl
.refreshPublisher()
.sink { print("Refreshing") }
.store(in: &cancellables)
Protocol |
---|
UIScrollViewDelegate |
@ScrollView // Projected Value: ScrollViewInterface
ScrollViewInterface
var didScroll: AnyPublisher<Void, Never>
var didZoom: AnyPublisher<Void, Never>
var willBeginDragging: AnyPublisher<Void, Never>
var willEndDragging: AnyPublisher<ScrollViewWillEndDragging, Never>
var didEndDragging: AnyPublisher<Bool, Never>
var willBeginDecelerating: AnyPublisher<Void, Never>
var didEndDecelerating: AnyPublisher<Void, Never>
var didEndScrollingAnimation: AnyPublisher<Void, Never>
var willBeginZooming: AnyPublisher<UIView?, Never>
var didEndZooming: AnyPublisher<ScrollViewDidEndZooming, Never>
var didScrollToTop: AnyPublisher<Void, Never>
var didChangeAdjustedContentInset: AnyPublisher<Void, Never>
@ScrollView var scrollView = UIScrollView()
$scrollView
.didScroll
.sink { print("Scrolling") }
.store(in: &cancellables)
Protocol |
---|
UISearchBarDelegate |
@SearchBar // Projected Value: SearchBarInterface
SearchBarInterface
var textDidBeginEditing: AnyPublisher<Void, Never>
var textDidEndEditing: AnyPublisher<Void, Never>
var textDidChange: AnyPublisher<String, Never>
var searchButtonClicked: AnyPublisher<Void, Never>
var bookmarkButtonClicked: AnyPublisher<Void, Never>
var cancelButtonClicked: AnyPublisher<Void, Never>
var resultsListButtonClicked: AnyPublisher<Void, Never>
var selectedScopeButtonIndexDidChange: AnyPublisher<Int, Never>
@SearchBar var searchBar = UISearchBar()
$searchBar
.textDidChange
.sink { text in }
.store(in: &cancellables)
Property | Event |
---|---|
.selectedSegmentIndex |
.valueChanged |
@SegmentedControl // Projected Value: AnyPublisher<Int, Never>
var isMomentary: Binding<Bool>
var selectedSegmentIndex: Binding<Int>
func isEnabledForSegment(at index: Int) -> Binding<Bool>
func widthForSegment(at index: Int) -> Binding<CGFloat>
func titleForSegment(at index: Int) -> Binding<String>
func imageForSegment(at index: Int) -> Binding<UIImage?>
func selectedSegmentIndexPublisher() -> AnyPublisher<Int, Never>
// Property Wrapper
@SegmentedControl var segmentedControl = UISegmentedControl(items: items)
$segmentedControl
.sink { selectedSegmentIndex in }
.store(in: &cancellables)
// Bindings
Just(true)
.bind(to: segmentedControl.bindable.isMomentary)
.store(in: &cancellables)
Just(1)
.bind(to: segmentedControl.bindable.selectedSegmentIndex)
.store(in: &cancellables)
Just(true)
.bind(to: segmentedControl.bindable.isEnabledForSegment(at: 1))
.store(in: &cancellables)
Just(100)
.bind(to: segmentedControl.bindable.widthForSegment(at: 1))
.store(in: &cancellables)
Just("Title")
.bind(to: segmentedControl.bindable.titleForSegment(at: 1))
.store(in: &cancellables)
Just(.checkmark)
.bind(to: segmentedControl.bindable.imageForSegment(at: 1))
.store(in: &cancellables)
// Extension Method
segmentedControl
.selectedSegmentIndexPublisher()
.sink { selectedSegmentIndex in }
.store(in: &cancellables)
Property | Event |
---|---|
.value |
.valueChanged |
@Slider // Projected Value: AnyPublisher<Float, Never>
var isContinuous: Binding<Bool>
var minimumValue: Binding<Float>
var maximumValue: Binding<Float>
var minimumTrackTintColor: Binding<UIColor>
var maximumTrackTintColor: Binding<UIColor>
var thumbTintColor: Binding<UIColor>
var value: Binding<Float>
func value(animated: Bool) -> Binding<Float>
func valuePublisher() -> AnyPublisher<Float, Never>
// Property Wrapper
@Slider var slider = UISlider()
$slider
.sink { value in }
.store(in: &cancellables)
// Bindings
Just(true)
.bind(to: slider.bindable.isContinuous)
.store(in: &cancellables)
Just(1)
.bind(to: slider.bindable.minimumValue)
.store(in: &cancellables)
Just(100)
.bind(to: slider.bindable.maximumValue)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: slider.bindable.minimumTrackTintColor)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: slider.bindable.maximumTrackTintColor)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: slider.bindable.thumbTintColor)
.store(in: &cancellables)
Just(1)
.bind(to: slider.bindable.value)
.store(in: &cancellables)
Just(1)
.bind(to: slider.bindable.value(animated: true))
.store(in: &cancellables)
// Extension Method
slider
.valuePublisher()
.sink { value in }
.store(in: &cancellables)
Property | Event |
---|---|
.value |
.valueChanged |
@Stepper // Projected Value: AnyPublisher<Double, Never>
var isContinuous: Binding<Bool>
var autorepeat: Binding<Bool>
var wraps: Binding<Bool>
var minimumValue: Binding<Double>
var maximumValue: Binding<Double>
var stepValue: Binding<Double>
var value: Binding<Double>
func valuePublisher() -> AnyPublisher<Double, Never>
// Property Wrapper
@Stepper var stepper = UIStepper()
$stepper
.sink { value in }
.store(in: &cancellables)
// Bindings
Just(true)
.bind(to: stepper.bindable.isContinuous)
.store(in: &cancellables)
Just(true)
.bind(to: stepper.bindable.autorepeat)
.store(in: &cancellables)
Just(true)
.bind(to: stepper.bindable.wraps)
.store(in: &cancellables)
Just(1)
.bind(to: stepper.bindable.minimumValue)
.store(in: &cancellables)
Just(100)
.bind(to: stepper.bindable.maximumValue)
.store(in: &cancellables)
Just(10)
.bind(to: stepper.bindable.stepValue)
.store(in: &cancellables)
Just(100)
.bind(to: stepper.bindable.value)
.store(in: &cancellables)
// Extension Method
stepper
.valuePublisher()
.sink { value in }
.store(in: &cancellables)
Property | Event |
---|---|
.isOn |
.valueChanged |
@Switch // Projected Value: AnyPublisher<Bool, Never>
var onTintColor: Binding<UIColor>
var thumbTintColor: Binding<UIColor>
var isOn: Binding<Bool>
func isOn(animated: Bool) -> Binding<Bool>
func isOnPublisher() -> AnyPublisher<Bool, Never>
// Property Wrapper
@Switch var `switch` = UISwitch()
$switch
.sink { isOn in }
.store(in: &cancellables)
// Bindings
Just(.systemPink)
.bind(to: `switch`.bindable.onTintColor)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: `switch`.bindable.thumbTintColor)
.store(in: &cancellables)
Just(true)
.bind(to: `switch`.bindable.isOn)
.store(in: &cancellables)
Just(true)
.bind(to: `switch`.bindable.isOn(animated: true))
.store(in: &cancellables)
// Extension Method
`switch`
.isOnPublisher()
.sink { isOn in }
.store(in: &cancellables)
Property | Events |
---|---|
.text |
.allEditingEvents |
.attributedText |
.allEditingEvents |
Protocol |
---|
UITextFieldDelegate |
@TextField // Projected Value: TextFieldInterface
TextFieldInterface
// Properties
var text: AnyPublisher<String, Never>
var attributedText: AnyPublisher<AttributedString, Never>
// UITextFieldDelegate
var didBeginEditing: AnyPublisher<Void, Never>
var didEndEditing: AnyPublisher<Void, Never>
var didChangeSelection: AnyPublisher<Void, Never>
var font: Binding<UIFont>
var textColor: Binding<UIColor>
var textAlignment: Binding<NSTextAlignment>
var placeholder: Binding<String>
var attributedPlaceholder: Binding<AttributedString>
var text: Binding<String>
var attributedText: Binding<AttributedString>
func textPublisher() -> AnyPublisher<String, Never>
func attributedTextPublisher() -> AnyPublisher<AttributedString, Never>
// Property Wrapper
@TextField var textField = UITextField()
$textField
.text
.sink { text in }
.store(in: &cancellables)
$textField
.attributedText
.sink { attributedText in }
.store(in: &cancellables)
// Bindings
Just(.preferredFont(forTextStyle: .body))
.bind(to: textField.bindable.font)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: textField.bindable.textColor)
.store(in: &cancellables)
Just(.natural)
.bind(to: textField.bindable.textAlignment)
.store(in: &cancellables)
Just("Placeholder")
.bind(to: textField.bindable.placeholder)
.store(in: &cancellables)
Just(AttributedString("Placeholder"))
.bind(to: textField.bindable.attributedPlaceholder)
.store(in: &cancellables)
Just("Text")
.bind(to: textField.bindable.text)
.store(in: &cancellables)
Just(AttributedString("Text"))
.bind(to: textField.bindable.attributedText)
.store(in: &cancellables)
// Extension Methods
textField
.textPublisher()
.sink { text in }
.store(in: &cancellables)
textField
.attributedTextPublisher()
.sink { attributedText in }
.store(in: &cancellables)
Protocol |
---|
UITextViewDelegate |
@TextView // Projected Value: TextViewInterface
TextViewInterface
// Properties
var text: AnyPublisher<String, Never>
var attributedText: AnyPublisher<AttributedString, Never>
// UITextViewDelegate
var didChange: AnyPublisher<Void, Never>
var didBeginEditing: AnyPublisher<Void, Never>
var didEndEditing: AnyPublisher<Void, Never>
var didChangeSelection: AnyPublisher<Void, Never>
var isEditable: Binding<Bool>
var font: Binding<UIFont>
var textColor: Binding<UIColor>
var textAlignment: Binding<NSTextAlignment>
var text: Binding<String>
var attributedText: Binding<AttributedString>
// Property Wrapper
@TextView var textView = UITextView()
$textView
.text
.sink { text in }
.store(in: &cancellables)
$textView
.attributedText
.sink { attributedText in }
.store(in: &cancellables)
// Bindings
Just(true)
.bind(to: textView.bindable.isEditable)
.store(in: &cancellables)
Just(.preferredFont(forTextStyle: .body))
.bind(to: textView.bindable.font)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: textView.bindable.textColor)
.store(in: &cancellables)
Just(.natural)
.bind(to: textView.bindable.textAlignment)
.store(in: &cancellables)
Just("Text")
.bind(to: textView.bindable.text)
.store(in: &cancellables)
Just(AttributedString("Text"))
.bind(to: textView.bindable.attributedText)
.store(in: &cancellables)
var isUserInteractionEnabled: Binding<Bool>
var isMultipleTouchEnabled: Binding<Bool>
var isExclusiveTouch: Binding<Bool>
var clipsToBounds: Binding<Bool>
var tintColor: Binding<UIColor>
var backgroundColor: Binding<UIColor>
var borderColor: Binding<UIColor>
var shadowColor: Binding<UIColor>
var alpha: Binding<CGFloat>
var isOpaque: Binding<Bool>
var isHidden: Binding<Bool>
Just as every CombineUI binding may be used with subclasses of its supported type, the UIView
bindings are compatible with all UIView
subclasses.
let view = UIView()
Just(true)
.bind(to: view.bindable.isUserInteractionEnabled)
.store(in: &cancellables)
Just(true)
.bind(to: view.bindable.isMultipleTouchEnabled)
.store(in: &cancellables)
Just(true)
.bind(to: view.bindable.isExclusiveTouch)
.store(in: &cancellables)
Just(true)
.bind(to: view.bindable.clipsToBounds)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: view.bindable.tintColor)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: view.bindable.backgroundColor)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: view.bindable.borderColor)
.store(in: &cancellables)
Just(.systemPink)
.bind(to: view.bindable.shadowColor)
.store(in: &cancellables)
Just(0.5)
.bind(to: view.bindable.alpha)
.store(in: &cancellables)
Just(true)
.bind(to: view.bindable.isOpaque)
.store(in: &cancellables)
Just(true)
.bind(to: view.bindable.isHidden)
.store(in: &cancellables)
@ViewController // Projected Value: ViewControllerInterface
ViewControllerInterface
var isVisible: AnyPublisher<Bool, Never>
var viewWillAppear: AnyPublisher<Void, Never>
var viewDidAppear: AnyPublisher<Void, Never>
var viewWillDisappear: AnyPublisher<Void, Never>
var viewDidDisappear: AnyPublisher<Void, Never>
func lifecyclePublisher() -> AnyPublisher<ViewControllerLifecycleEvent, Never>
func isVisiblePublisher() -> AnyPublisher<Bool, Failure> where Output == ViewControllerLifecycleEvent
// Property Wrapper
@ViewController var viewController = UIViewController()
$viewController
.isVisible
.sink { isVisible in }
.store(in: &cancellables)
$viewController
.viewWillAppear
.sink { print("Appearing") }
.store(in: &cancellables)
$viewController
.viewDidAppear
.sink { print("Appeared") }
.store(in: &cancellables)
$viewController
.viewWillDisappear
.sink { print("Disappearing") }
.store(in: &cancellables)
$viewController
.viewDidDisappear
.sink { print("Disappeared") }
.store(in: &cancellables)
// Extension Methods
let lifecycle = viewController
.lifecyclePublisher()
.share()
lifecycle
.sink { event in }
.store(in: &cancellables)
lifecycle
.isVisiblePublisher()
.sink { isVisible in }
.store(in: &cancellables)
-
Every subscription adds a helper view (of zero size) to the view hierarchy using view controller containment, therefore consider sharing the subscription when there are multiple subscribers (as demonstrated in the example above).
-
In stark contrast to the other property wrappers, the
@ViewController
property wrapper is considered to be less useful than the extension methods. This is because the extension methods may be used within a view controller instance to subscribe to its own lifecycle events, for example:
override func viewDidLoad() {
super.viewDidLoad()
lifecyclePublisher()
.isVisiblePublisher()
.sink { isVisible in }
.store(in: &cancellables)
}
CombineUI provides publishers for common delegate protocol methods, however due to the nature of publishers, delegate methods that have return values are not available as publishers. Furthermore, the delegate method publishers provided by CombineUI are available as a convenience only. For complex setups, or even when more than just a few delegate methods are required, it is recommended to use an actual delegate class instance.
Note too that setting a delegate property will disable the delegate publisher(s). This means it is not possible to use an actual delegate class instance along with the delegate publishers. Therefore select one pattern or the other for each specific use case.
Additional bindings are easily added to existing views and controls. This is useful for properties that CombineUI does not yet support natively.
Example:
extension Bindable where Target: UIView {
var tag: Binding<Int> {
Binding(self, for: \.tag)
}
}
The same type of APIs that CombineUI provides can also be adopted by custom views and controls.
Example:
@propertyWrapper
struct Example<T: ExampleControl> {
var wrappedValue: T
var projectedValue: AnyPublisher<ExampleValue, Never>
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
self.projectedValue = wrappedValue
.valuePublisher()
.share()
.eraseToAnyPublisher()
}
}
extension Bindable where Target: ExampleControl {
var value: Binding<ExampleValue> {
Binding(self, for: \.value)
}
}
extension ExampleControl {
func valuePublisher() -> AnyPublisher<ExampleValue, Never> {
publisher(for: .valueChanged)
.compactMap { [weak self] _ in self?.value }
.prepend(value)
.eraseToAnyPublisher()
}
}
The CombineUI source code may be used as reference for additional examples.
While interest in contributing to this project is appreciated, it has been open sourced solely for the purpose of sharing with the community. This means we are unable to accept outside contributions at this time and pull requests will not be reviewed or merged. To report a security concern or vulnerability, please submit a GitHub issue.
Licensed under the Match Group Modified 3-Clause BSD License.