tinder/combineui
Overview
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
$buttonprojected 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).
Bindings Benefits
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))Swift Extensions
Many types are also enhanced with Swift extensions providing Combine publishers that may be used as an alternative to the property wrappers when needed.
Supported UI Frameworks
UIKit is currently supported. CombineUI may support additional UI frameworks in the future.
Installation
Swift Package Collection
<details><summary>Expand / Collapse</summary><br>
https://swiftpackageindex.com/Tinder/collection.json</details>
Swift Package Manager
<details><summary>Expand / Collapse</summary><br>
Package Dependency
Replace
<version>with the desired minimum version.
.package(url: "https://github.com/Tinder/CombineUI.git", from: "<version>")Target Dependency
"CombineUI"</details>
Xcode
<details><summary>Expand / Collapse</summary><br>
Package Dependency
https://github.com/Tinder/CombineUI.gitTarget Dependency
CombineUI</details>
Code Examples
All examples require this setup:
import Combine
import CombineUI
import UIKit
var cancellables = Set<AnyCancellable>()Property Wrapper Example: UIButton
@Button var button = UIButton()
button.setImage(UIImage(systemName: "heart"), for: .normal)
$button
.sink { print("Tapped") }
.store(in: &cancellables)Binding Example: UILabel
let label = UILabel()
let subject = PassthroughSubject<String, Never>()
subject
.bind(to: label.bindable.text)
.store(in: &cancellables)
subject.send("Text")Extension Methods Example: UIViewController
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)Comprehensive Examples
Additional examples are available within an Xcode project included in this repository. See the example project README containing setup instructions for guidance.
<img src="Example/Examples.png" width="234" />
Cheat Sheets
### UIKit
Property Wrappers
| | Property Wrapper | Projected Type |
| :-- | :-- | :-- |
| `UIButton` | `@Button` | `AnyPublisher<Void, Never>` <tr></tr> |
| `UIControl` | `@Control` | `ControlInterface` <tr></tr> |
| `UIDatePicker` | `@DatePicker` | `DatePickerInterface` <tr></tr> |
| `UIGestureRecognizer` | `@GestureRecognizer` | `GestureRecognizerInterface` <tr></tr> |
| `UIPageControl` | `@PageControl` | `AnyPublisher<Int, Never>` <tr></tr> |
| `UIRefreshControl` | `@RefreshControl` | `AnyPublisher<Void, Never>` <tr></tr> |
| `UIScrollView` | `@ScrollView` | `ScrollViewInterface` <tr></tr> |
| `UISearchBar` | `@SearchBar` | `SearchBarInterface` <tr></tr> |
| `UISegmentedControl` | `@SegmentedControl` | `AnyPublisher<Int, Never>` <tr></tr> |
| `UISlider` | `@Slider` | `AnyPublisher<Float, Never>` <tr></tr> |
| `UIStepper` | `@Stepper` | `AnyPublisher<Double, Never>` <tr></tr> |
| `UISwitch` | `@Switch` | `AnyPublisher<Bool, Never>` <tr></tr> |
| `UITextField` | `@TextField` | `TextFieldInterface` <tr></tr> |
| `UITextView` | `@TextView` | `TextViewInterface` <tr></tr> |
| `UIViewController` | `@ViewController` | `ViewControllerInterface` <tr></tr> |
Bindings
<table>
<thead>
<tr>
<th align="left"></th>
<th align="left">Binding</th>
<th align="left">Type</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="7"><code>UIActivityIndicatorView</code></td>
<td><code>style</code></td>
<td><code>UIActivityIndicatorView.Style</code></td>
</tr>
<tr></tr>
<tr>
<td><code>color</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>hidesWhenStopped</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isAnimating</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="11"><code>UIButton</code></td>
<td><code>titleColor(for: UIControl.State)</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>titleShadowColor(for: UIControl.State)</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>title(for: UIControl.State)</code></td>
<td><code>String</code></td>
</tr>
<tr></tr>
<tr>
<td><code>attributedTitle(for: UIControl.State)</code></td>
<td><code>AttributedString</code></td>
</tr>
<tr></tr>
<tr>
<td><code>image(for: UIControl.State)</code></td>
<td><code>UIImage?</code></td>
</tr>
<tr></tr>
<tr>
<td><code>backgroundImage(for: UIControl.State)</code></td>
<td><code>UIImage?</code></td>
</tr>
<tr></tr>
<tr>
<td><code>UIControl</code></td>
<td><code>isEnabled</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="5"><code>UIDatePicker</code></td>
<td><code>countDownDuration</code></td>
<td><code>TimeInterval</code></td>
</tr>
<tr></tr>
<tr>
<td><code>date</code></td>
<td><code>Date</code></td>
</tr>
<tr></tr>
<tr>
<td><code>date(animated: Bool)</code></td>
<td><code>Date</code></td>
</tr>
<tr></tr>
<tr>
<td><code>UIGestureRecognizer</code></td>
<td><code>isEnabled</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="5"><code>UIImageView</code></td>
<td><code>image</code></td>
<td><code>UIImage?</code></td>
</tr>
<tr></tr>
<tr>
<td><code>highlightedImage</code></td>
<td><code>UIImage?</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isHighlighted</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="9"><code>UILabel</code></td>
<td><code>isEnabled</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>font</code></td>
<td><code>UIFont</code></td>
</tr>
<tr></tr>
<tr>
<td><code>textColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>text</code></td>
<td><code>String</code></td>
</tr>
<tr></tr>
<tr>
<td><code>attributedText</code></td>
<td><code>AttributedString</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="9"><code>UIPageControl</code></td>
<td><code>pageIndicatorTintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>currentPageIndicatorTintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>currentPage</code></td>
<td><code>Int</code></td>
</tr>
<tr></tr>
<tr>
<td><code>numberOfPages</code></td>
<td><code>Int</code></td>
</tr>
<tr></tr>
<tr>
<td><code>hidesForSinglePage</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="7"><code>UIProgressView</code></td>
<td><code>trackTintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>progressTintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>progress</code></td>
<td><code>Float</code></td>
</tr>
<tr></tr>
<tr>
<td><code>progress(animated: Bool)</code></td>
<td><code>Float</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="5"><code>UIRefreshControl</code></td>
<td><code>tintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>attributedTitle</code></td>
<td><code>AttributedString</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isRefreshing</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="11"><code>UISegmentedControl</code></td>
<td><code>isMomentary</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>selectedSegmentIndex</code></td>
<td><code>Int</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isEnabledForSegment(at: Int)</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>widthForSegment(at: Int)</code></td>
<td><code>CGFloat</code></td>
</tr>
<tr></tr>
<tr>
<td><code>titleForSegment(at: Int)</code></td>
<td><code>String</code></td>
</tr>
<tr></tr>
<tr>
<td><code>imageForSegment(at: Int)</code></td>
<td><code>UIImage?</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="15"><code>UISlider</code></td>
<td><code>isContinuous</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>minimumValue</code></td>
<td><code>Float</code></td>
</tr>
<tr></tr>
<tr>
<td><code>maximumValue</code></td>
<td><code>Float</code></td>
</tr>
<tr></tr>
<tr>
<td><code>minimumTrackTintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>maximumTrackTintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>thumbTintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>value</code></td>
<td><code>Float</code></td>
</tr>
<tr></tr>
<tr>
<td><code>value(animated: Bool)</code></td>
<td><code>Float</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="13"><code>UIStepper</code></td>
<td><code>isContinuous</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>autorepeat</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>wraps</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>minimumValue</code></td>
<td><code>Double</code></td>
</tr>
<tr></tr>
<tr>
<td><code>maximumValue</code></td>
<td><code>Double</code></td>
</tr>
<tr></tr>
<tr>
<td><code>stepValue</code></td>
<td><code>Double</code></td>
</tr>
<tr></tr>
<tr>
<td><code>value</code></td>
<td><code>Double</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="7"><code>UISwitch</code></td>
<td><code>onTintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>thumbTintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isOn</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isOn(animated: Bool)</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="13"><code>UITextField</code></td>
<td><code>font</code></td>
<td><code>UIFont</code></td>
</tr>
<tr></tr>
<tr>
<td><code>textColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>textAlignment</code></td>
<td><code>NSTextAlignment</code></td>
</tr>
<tr></tr>
<tr>
<td><code>placeholder</code></td>
<td><code>String</code></td>
</tr>
<tr></tr>
<tr>
<td><code>attributedPlaceholder</code></td>
<td><code>AttributedString</code></td>
</tr>
<tr></tr>
<tr>
<td><code>text</code></td>
<td><code>String</code></td>
</tr>
<tr></tr>
<tr>
<td><code>attributedText</code></td>
<td><code>AttributedString</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="11"><code>UITextView</code></td>
<td><code>isEditable</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>font</code></td>
<td><code>UIFont</code></td>
</tr>
<tr></tr>
<tr>
<td><code>textColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>textAlignment</code></td>
<td><code>NSTextAlignment</code></td>
</tr>
<tr></tr>
<tr>
<td><code>text</code></td>
<td><code>String</code></td>
</tr>
<tr></tr>
<tr>
<td><code>attributedText</code></td>
<td><code>AttributedString</code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="21"><code>UIView</code></td>
<td><code>isUserInteractionEnabled</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isMultipleTouchEnabled</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isExclusiveTouch</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>clipsToBounds</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>tintColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>backgroundColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>borderColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>shadowColor</code></td>
<td><code>UIColor</code></td>
</tr>
<tr></tr>
<tr>
<td><code>alpha</code></td>
<td><code>CGFloat</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isOpaque</code></td>
<td><code>Bool</code></td>
</tr>
<tr></tr>
<tr>
<td><code>isHidden</code></td>
<td><code>Bool</code></td>
</tr>
</tbody>
</table>
Extension Methods
<table>
<thead>
<tr>
<th align="left"></th>
<th align="left">Method</th>
<th align="left">Type</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>UIButton</code></td>
<td><code>tapPublisher()</code></td>
<td><code>AnyPublisher<Void, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>UIControl</code></td>
<td><code>publisher(for: UIControl.Event)</code></td>
<td><code>AnyPublisher<UIControl.Event, Never></code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="3"><code>UIDatePicker</code></td>
<td><code>countDownDurationPublisher()</code></td>
<td><code>AnyPublisher<TimeInterval, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>datePublisher()</code></td>
<td><code>AnyPublisher<Date, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>UIGestureRecognizer</code></td>
<td><code>publisher(attachingTo: UIView)</code></td>
<td><code>AnyPublisher<UIGestureRecognizer, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>UIPageControl</code></td>
<td><code>currentPagePublisher()</code></td>
<td><code>AnyPublisher<Int, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>UIRefreshControl</code></td>
<td><code>refreshPublisher()</code></td>
<td><code>AnyPublisher<Void, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>UISegmentedControl</code></td>
<td><code>selectedSegmentIndexPublisher()</code></td>
<td><code>AnyPublisher<Int, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>UISlider</code></td>
<td><code>valuePublisher()</code></td>
<td><code>AnyPublisher<Float, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>UIStepper</code></td>
<td><code>valuePublisher()</code></td>
<td><code>AnyPublisher<Double, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>UISwitch</code></td>
<td><code>isOnPublisher()</code></td>
<td><code>AnyPublisher<Bool, Never></code></td>
</tr>
<tr></tr>
<tr>
<td rowspan="3"><code>UITextField</code></td>
<td><code>textPublisher()</code></td>
<td><code>AnyPublisher<String, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>attributedTextPublisher()</code></td>
<td><code>AnyPublisher<AttributedString, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>UIViewController</code></td>
<td><code>lifecyclePublisher()</code></td>
<td><code>AnyPublisher<ViewControllerLifecycleEvent, Never></code></td>
</tr>
<tr></tr>
<tr>
<td><code>Publisher where Output == ViewControllerLifecycleEvent</code></td>
<td><code>isVisiblePublisher()</code></td>
<td><code>AnyPublisher<Bool, Never></code></td>
</tr>
</tbody>
</table>Documentation
- UIKit
- UIActivityIndicatorView - UIButton - UIControl - UIDatePicker - UIGestureRecognizer - UIImageView - UILabel - UIPageControl - UIProgressView - UIRefreshControl - UIScrollView - UISearchBar - UISegmentedControl - UISlider - UIStepper - UISwitch - UITextField - UITextView - UIView - UIViewController
`UIActivityIndicatorView`
Bindings
var style: Binding<UIActivityIndicatorView.Style>
var color: Binding<UIColor>
var hidesWhenStopped: Binding<Bool>
var isAnimating: Binding<Bool>Code Example
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)`UIControl`
Property Wrapper
@Control // Projected Value: ControlInterfaceControlInterface
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>Binding
var isEnabled: Binding<Bool>Extension Method
func publisher(for controlEvents: UIControl.Event) -> AnyPublisher<UIControl.Event, Never>Use
contains()-5usmy) on the OptionSet that is received to determine which event occurred.
Supported Types
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
Code Example
// 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)Notes
- Use the
@Controlproperty wrapper only when publishers for theUIControl.Eventtypes are needed, otherwise the type specific property wrappers should be preferred.
`UIDatePicker`
| Property | Event | | :-- | :-- | | .countDownDuration | .valueChanged <tr></tr> | | .date | .valueChanged <tr></tr> |
Property Wrapper
@DatePicker(mode: UIDatePicker.Mode = .dateAndTime) // Projected Value: DatePickerInterfaceDatePickerInterface
var countDownDuration: AnyPublisher<TimeInterval, Never>
var date: AnyPublisher<Date, Never>Bindings
var countDownDuration: Binding<TimeInterval>
var date: Binding<Date>
func date(animated: Bool) -> Binding<Date>Extension Methods
func datePublisher() -> AnyPublisher<Date, Never>
func countDownDurationPublisher() -> AnyPublisher<TimeInterval, Never>Code Example
// 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)`UIGestureRecognizer`
Property Wrapper
@GestureRecognizer // Projected Value: GestureRecognizerInterfaceGestureRecognizerInterface
func attaching(to view: UIView) -> AnyPublisher<UIGestureRecognizer, Never>Binding
var isEnabled: Binding<Bool>Extension Method
func publisher(attachingTo view: UIView) -> AnyPublisher<UIGestureRecognizer, Never>Code Example
// 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)Notes
- The gesture recognizer will be added to the provided view automatically.
`UIImageView`
Bindings
var image: Binding<UIImage?>
var highlightedImage: Binding<UIImage?>
var isHighlighted: Binding<Bool>Code Example
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)`UILabel`
Bindings
var isEnabled: Binding<Bool>
var font: Binding<UIFont>
var textColor: Binding<UIColor>
var text: Binding<String>
var attributedText: Binding<AttributedString>Code Example
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)`UIPageControl`
| Property | Event | | :-- | :-- | | .currentPage | .valueChanged <tr></tr> |
Property Wrapper
@PageControl // Projected Value: AnyPublisher<Int, Never>Bindings
var pageIndicatorTintColor: Binding<UIColor>
var currentPageIndicatorTintColor: Binding<UIColor>
var currentPage: Binding<Int>
var numberOfPages: Binding<Int>
var hidesForSinglePage: Binding<Bool>Extension Method
func currentPagePublisher() -> AnyPublisher<Int, Never>Code Example
// 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)`UIProgressView`
Bindings
var trackTintColor: Binding<UIColor>
var progressTintColor: Binding<UIColor>
var progress: Binding<Float>
func progress(animated: Bool) -> Binding<Float>Code Example
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)`UIRefreshControl`
| Property | Event | Value | | :-- | :-- | :-- | | .isRefreshing | .valueChanged | true <tr></tr> |
Property Wrapper
@RefreshControl // Projected Value: AnyPublisher<Void, Never>Bindings
var tintColor: Binding<UIColor>
var attributedTitle: Binding<AttributedString>
var isRefreshing: Binding<Bool>Extension Method
func refreshPublisher() -> AnyPublisher<Void, Never>Code Example
// 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)`UIScrollView`
| Protocol | | :-- | | UIScrollViewDelegate <tr></tr> |
Property Wrapper
@ScrollView // Projected Value: ScrollViewInterfaceScrollViewInterface
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>Code Example
@ScrollView var scrollView = UIScrollView()
$scrollView
.didScroll
.sink { print("Scrolling") }
.store(in: &cancellables)`UISearchBar`
| Protocol | | :-- | | UISearchBarDelegate <tr></tr> |
Property Wrapper
@SearchBar // Projected Value: SearchBarInterfaceSearchBarInterface
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>Code Example
@SearchBar var searchBar = UISearchBar()
$searchBar
.textDidChange
.sink { text in }
.store(in: &cancellables)`UISegmentedControl`
| Property | Event | | :-- | :-- | | .selectedSegmentIndex | .valueChanged <tr></tr> |
Property Wrapper
@SegmentedControl // Projected Value: AnyPublisher<Int, Never>Bindings
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?>Extension Method
func selectedSegmentIndexPublisher() -> AnyPublisher<Int, Never>Code Example
// 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)`UISlider`
| Property | Event | | :-- | :-- | | .value | .valueChanged <tr></tr> |
Property Wrapper
@Slider // Projected Value: AnyPublisher<Float, Never>Bindings
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>Extension Method
func valuePublisher() -> AnyPublisher<Float, Never>Code Example
// 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)`UIStepper`
| Property | Event | | :-- | :-- | | .value | .valueChanged <tr></tr> |
Property Wrapper
@Stepper // Projected Value: AnyPublisher<Double, Never>Bindings
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>Extension Method
func valuePublisher() -> AnyPublisher<Double, Never>Code Example
// 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)`UISwitch`
| Property | Event | | :-- | :-- | | .isOn | .valueChanged <tr></tr> |
Property Wrapper
@Switch // Projected Value: AnyPublisher<Bool, Never>Bindings
var onTintColor: Binding<UIColor>
var thumbTintColor: Binding<UIColor>
var isOn: Binding<Bool>
func isOn(animated: Bool) -> Binding<Bool>Extension Method
func isOnPublisher() -> AnyPublisher<Bool, Never>Code Example
// 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)`UITextField`
| Property | Events | | :-- | :-- | | .text | .allEditingEvents <tr></tr> | | .attributedText | .allEditingEvents <tr></tr> |
| Protocol | | :-- | | UITextFieldDelegate <tr></tr> |
Property Wrapper
@TextField // Projected Value: TextFieldInterfaceTextFieldInterface
// 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>Bindings
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>Extension Methods
func textPublisher() -> AnyPublisher<String, Never>
func attributedTextPublisher() -> AnyPublisher<AttributedString, Never>Code Example
// 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)`UITextView`
| Protocol | | :-- | | UITextViewDelegate <tr></tr> |
Property Wrapper
@TextView // Projected Value: TextViewInterfaceTextViewInterface
// 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>Bindings
var isEditable: Binding<Bool>
var font: Binding<UIFont>
var textColor: Binding<UIColor>
var textAlignment: Binding<NSTextAlignment>
var text: Binding<String>
var attributedText: Binding<AttributedString>Code Example
// 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)`UIView`
Bindings
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>Supported Types
Just as every CombineUI binding may be used with subclasses of its supported type, the UIView bindings are compatible with all UIView subclasses.
Code Example
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)`UIViewController`
Property Wrapper
@ViewController // Projected Value: ViewControllerInterfaceViewControllerInterface
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>Extension Method
func lifecyclePublisher() -> AnyPublisher<ViewControllerLifecycleEvent, Never>Publisher Extension
func isVisiblePublisher() -> AnyPublisher<Bool, Failure> where Output == ViewControllerLifecycleEventCode Example
// 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)Notes
- 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
@ViewControllerproperty 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)
}Caveats
Delegation
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.
Customization
Adding Bindings to Views and Controls
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)
}
}Adopting CombineUI in Custom Views and Controls
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.
Contributing
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.
License
Licensed under the Match Group Modified 3-Clause BSD License.
Package Metadata
Repository: tinder/combineui
Default branch: main
README: README.md