An open API service indexing awesome lists of open source software.

https://github.com/tinder/combineui

Swift Property Wrappers, Bindings and Combine Publishers for UI Gestures, Controls and Views
https://github.com/tinder/combineui

Last synced: 4 months ago
JSON representation

Swift Property Wrappers, Bindings and Combine Publishers for UI Gestures, Controls and Views

Awesome Lists containing this project

README

          

[![Swift](https://github.com/Tinder/CombineUI/actions/workflows/swift.yml/badge.svg?event=push)](https://github.com/Tinder/CombineUI/actions/workflows/swift.yml)
 
[![DocC](https://github.com/Tinder/CombineUI/actions/workflows/docc.yml/badge.svg?event=push)](https://github.com/Tinder/CombineUI/actions/workflows/docc.yml)

# CombineUI

Swift Property Wrappers, Bindings and Combine Publishers for UI Gestures, Controls and Views

## Overview

CombineUI provides [Swift property wrappers](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Property-Wrappers) for select Apple types such as gestures, controls and views. These property wrappers [project](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Projecting-a-Value-From-a-Property-Wrapper) Combine publishers for gesture recognition, control events, property changes and delegate callbacks.

Example:

```swift
@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).

### Bindings Benefits

As compared to subscribing via Combine's' [`assign(to:on:)`](https://developer.apple.com/documentation/combine/publisher/assign(to:on:)) method, subscribing via CombineUI's bindings is preferable since they use `weak` references and can accept method arguments.

Example:

```swift
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

Expand / Collapse

```
https://swiftpackageindex.com/Tinder/collection.json
```

### Swift Package Manager

Expand / Collapse

**Package Dependency**

> Replace `` with the desired minimum version.

```swift
.package(url: "https://github.com/Tinder/CombineUI.git", from: "")
```

**Target Dependency**

```swift
"CombineUI"
```

### Xcode

Expand / Collapse

**Package Dependency**

```
https://github.com/Tinder/CombineUI.git
```

**Target Dependency**

```
CombineUI
```

## Code Examples

All examples require this setup:

```swift
import Combine
import CombineUI
import UIKit

var cancellables = Set()
```

### Property Wrapper Example: `UIButton`

```swift
@Button var button = UIButton()

button.setImage(UIImage(systemName: "heart"), for: .normal)

$button
.sink { print("Tapped") }
.store(in: &cancellables)
```

### Binding Example: `UILabel`

```swift
let label = UILabel()
let subject = PassthroughSubject()

subject
.bind(to: label.bindable.text)
.store(in: &cancellables)

subject.send("Text")
```

### Extension Methods Example: `UIViewController`

```swift
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](Example/). See the [example project README containing setup instructions](Example/) for guidance.

## Cheat Sheets

### UIKit

Property Wrappers

| | Property Wrapper | Projected Type |
| :-- | :-- | :-- |
| `UIButton` | `@Button` | `AnyPublisher` |
| `UIControl` | `@Control` | `ControlInterface` |
| `UIDatePicker` | `@DatePicker` | `DatePickerInterface` |
| `UIGestureRecognizer` | `@GestureRecognizer` | `GestureRecognizerInterface` |
| `UIPageControl` | `@PageControl` | `AnyPublisher` |
| `UIRefreshControl` | `@RefreshControl` | `AnyPublisher` |
| `UIScrollView` | `@ScrollView` | `ScrollViewInterface` |
| `UISearchBar` | `@SearchBar` | `SearchBarInterface` |
| `UISegmentedControl` | `@SegmentedControl` | `AnyPublisher` |
| `UISlider` | `@Slider` | `AnyPublisher` |
| `UIStepper` | `@Stepper` | `AnyPublisher` |
| `UISwitch` | `@Switch` | `AnyPublisher` |
| `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>

## Documentation

- UIKit
- [UIActivityIndicatorView](#uiactivityindicatorview)
- [UIButton](#uibutton)
- [UIControl](#uicontrol)
- [UIDatePicker](#uidatepicker)
- [UIGestureRecognizer](#uigesturerecognizer)
- [UIImageView](#uiimageview)
- [UILabel](#uilabel)
- [UIPageControl](#uipagecontrol)
- [UIProgressView](#uiprogressview)
- [UIRefreshControl](#uirefreshcontrol)
- [UIScrollView](#uiscrollview)
- [UISearchBar](#uisearchbar)
- [UISegmentedControl](#uisegmentedcontrol)
- [UISlider](#uislider)
- [UIStepper](#uistepper)
- [UISwitch](#uiswitch)
- [UITextField](#uitextfield)
- [UITextView](#uitextview)
- [UIView](#uiview)
- [UIViewController](#uiviewcontroller)
- [Caveats](#caveats)
- [Customization](#customization)

## `UIActivityIndicatorView`

### Bindings

```swift
var style: Binding
var color: Binding
var hidesWhenStopped: Binding
var isAnimating: Binding
```

### Code Example

```swift
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)
```

## `UIButton`

| Event |
| :-- |
| `.primaryActionTriggered` |

### Property Wrapper

```swift
@Button // Projected Value: AnyPublisher
```

### Bindings

```swift
func titleColor(for state: UIControl.State) -> Binding
func titleShadowColor(for state: UIControl.State) -> Binding
func title(for state: UIControl.State) -> Binding
func attributedTitle(for state: UIControl.State) -> Binding
func image(for state: UIControl.State) -> Binding
func backgroundImage(for state: UIControl.State) -> Binding
```

### Extension Method

```swift
func tapPublisher() -> AnyPublisher
```

### Code Example

```swift
// 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)
```

## `UIControl`

### Property Wrapper

```swift
@Control // Projected Value: ControlInterface
```

`ControlInterface`

```swift
var touchDown: AnyPublisher
var touchDownRepeat: AnyPublisher
var touchDragInside: AnyPublisher
var touchDragOutside: AnyPublisher
var touchDragEnter: AnyPublisher
var touchDragExit: AnyPublisher
var touchUpInside: AnyPublisher
var touchUpOutside: AnyPublisher
var touchCancel: AnyPublisher
var valueChanged: AnyPublisher
var menuActionTriggered: AnyPublisher
var primaryActionTriggered: AnyPublisher
var editingDidBegin: AnyPublisher
var editingChanged: AnyPublisher
var editingDidEnd: AnyPublisher
var editingDidEndOnExit: AnyPublisher
```

### Binding

```swift
var isEnabled: Binding
```

### Extension Method

```swift
func publisher(for controlEvents: UIControl.Event) -> AnyPublisher
```

> Use [`contains()`](https://developer.apple.com/documentation/swift/setalgebra/contains(_:)-5usmy) on the [OptionSet](https://developer.apple.com/documentation/swift/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

```swift
// 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 `@Control` property wrapper only when publishers for the `UIControl.Event` types are needed, otherwise the type specific property wrappers should be preferred.

## `UIDatePicker`

| Property | Event |
| :-- | :-- |
| `.countDownDuration` | `.valueChanged` |
| `.date` | `.valueChanged` |

### Property Wrapper

```swift
@DatePicker(mode: UIDatePicker.Mode = .dateAndTime) // Projected Value: DatePickerInterface
```

`DatePickerInterface`

```swift
var countDownDuration: AnyPublisher
var date: AnyPublisher
```

### Bindings

```swift
var countDownDuration: Binding
var date: Binding

func date(animated: Bool) -> Binding
```

### Extension Methods

```swift
func datePublisher() -> AnyPublisher
func countDownDurationPublisher() -> AnyPublisher
```

### Code Example

```swift
// 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

```swift
@GestureRecognizer // Projected Value: GestureRecognizerInterface
```

`GestureRecognizerInterface`

```swift
func attaching(to view: UIView) -> AnyPublisher
```

### Binding

```swift
var isEnabled: Binding
```

### Extension Method

```swift
func publisher(attachingTo view: UIView) -> AnyPublisher
```

### Code Example

```swift
// 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

```swift
var image: Binding
var highlightedImage: Binding
var isHighlighted: Binding
```

### Code Example

```swift
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

```swift
var isEnabled: Binding
var font: Binding
var textColor: Binding
var text: Binding
var attributedText: Binding
```

### Code Example

```swift
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` |

### Property Wrapper

```swift
@PageControl // Projected Value: AnyPublisher
```

### Bindings

```swift
var pageIndicatorTintColor: Binding
var currentPageIndicatorTintColor: Binding
var currentPage: Binding
var numberOfPages: Binding
var hidesForSinglePage: Binding
```

### Extension Method

```swift
func currentPagePublisher() -> AnyPublisher
```

### Code Example

```swift
// 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

```swift
var trackTintColor: Binding
var progressTintColor: Binding
var progress: Binding

func progress(animated: Bool) -> Binding
```

### Code Example

```swift
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` |

### Property Wrapper

```swift
@RefreshControl // Projected Value: AnyPublisher
```

### Bindings

```swift
var tintColor: Binding
var attributedTitle: Binding
var isRefreshing: Binding
```

### Extension Method

```swift
func refreshPublisher() -> AnyPublisher
```

### Code Example

```swift
// 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` |

### Property Wrapper

```swift
@ScrollView // Projected Value: ScrollViewInterface
```

`ScrollViewInterface`

```swift
var didScroll: AnyPublisher
var didZoom: AnyPublisher
var willBeginDragging: AnyPublisher
var willEndDragging: AnyPublisher
var didEndDragging: AnyPublisher
var willBeginDecelerating: AnyPublisher
var didEndDecelerating: AnyPublisher
var didEndScrollingAnimation: AnyPublisher
var willBeginZooming: AnyPublisher
var didEndZooming: AnyPublisher
var didScrollToTop: AnyPublisher
var didChangeAdjustedContentInset: AnyPublisher
```

### Code Example

```swift
@ScrollView var scrollView = UIScrollView()

$scrollView
.didScroll
.sink { print("Scrolling") }
.store(in: &cancellables)
```

## `UISearchBar`

| Protocol |
| :-- |
| `UISearchBarDelegate` |

### Property Wrapper

```swift
@SearchBar // Projected Value: SearchBarInterface
```

`SearchBarInterface`

```swift
var textDidBeginEditing: AnyPublisher
var textDidEndEditing: AnyPublisher
var textDidChange: AnyPublisher
var searchButtonClicked: AnyPublisher
var bookmarkButtonClicked: AnyPublisher
var cancelButtonClicked: AnyPublisher
var resultsListButtonClicked: AnyPublisher
var selectedScopeButtonIndexDidChange: AnyPublisher
```

### Code Example

```swift
@SearchBar var searchBar = UISearchBar()

$searchBar
.textDidChange
.sink { text in }
.store(in: &cancellables)
```

## `UISegmentedControl`

| Property | Event |
| :-- | :-- |
| `.selectedSegmentIndex` | `.valueChanged` |

### Property Wrapper

```swift
@SegmentedControl // Projected Value: AnyPublisher
```

### Bindings

```swift
var isMomentary: Binding
var selectedSegmentIndex: Binding

func isEnabledForSegment(at index: Int) -> Binding
func widthForSegment(at index: Int) -> Binding
func titleForSegment(at index: Int) -> Binding
func imageForSegment(at index: Int) -> Binding
```

### Extension Method

```swift
func selectedSegmentIndexPublisher() -> AnyPublisher
```

### Code Example

```swift
// 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` |

### Property Wrapper

```swift
@Slider // Projected Value: AnyPublisher
```

### Bindings

```swift
var isContinuous: Binding
var minimumValue: Binding
var maximumValue: Binding
var minimumTrackTintColor: Binding
var maximumTrackTintColor: Binding
var thumbTintColor: Binding
var value: Binding

func value(animated: Bool) -> Binding
```

### Extension Method

```swift
func valuePublisher() -> AnyPublisher
```

### Code Example

```swift
// 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` |

### Property Wrapper

```swift
@Stepper // Projected Value: AnyPublisher
```

### Bindings

```swift
var isContinuous: Binding
var autorepeat: Binding
var wraps: Binding
var minimumValue: Binding
var maximumValue: Binding
var stepValue: Binding
var value: Binding
```

### Extension Method

```swift
func valuePublisher() -> AnyPublisher
```

### Code Example

```swift
// 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` |

### Property Wrapper

```swift
@Switch // Projected Value: AnyPublisher
```

### Bindings

```swift
var onTintColor: Binding
var thumbTintColor: Binding
var isOn: Binding

func isOn(animated: Bool) -> Binding
```

### Extension Method

```swift
func isOnPublisher() -> AnyPublisher
```

### Code Example

```swift
// 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` |
| `.attributedText` | `.allEditingEvents` |

| Protocol |
| :-- |
| `UITextFieldDelegate` |

### Property Wrapper

```swift
@TextField // Projected Value: TextFieldInterface
```

`TextFieldInterface`

```swift
// Properties

var text: AnyPublisher
var attributedText: AnyPublisher

// UITextFieldDelegate

var didBeginEditing: AnyPublisher
var didEndEditing: AnyPublisher
var didChangeSelection: AnyPublisher
```

### Bindings

```swift
var font: Binding
var textColor: Binding
var textAlignment: Binding
var placeholder: Binding
var attributedPlaceholder: Binding
var text: Binding
var attributedText: Binding
```

### Extension Methods

```swift
func textPublisher() -> AnyPublisher
func attributedTextPublisher() -> AnyPublisher
```

### Code Example

```swift
// 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` |

### Property Wrapper

```swift
@TextView // Projected Value: TextViewInterface
```

`TextViewInterface`

```swift
// Properties

var text: AnyPublisher
var attributedText: AnyPublisher

// UITextViewDelegate

var didChange: AnyPublisher
var didBeginEditing: AnyPublisher
var didEndEditing: AnyPublisher
var didChangeSelection: AnyPublisher
```

### Bindings

```swift
var isEditable: Binding
var font: Binding
var textColor: Binding
var textAlignment: Binding
var text: Binding
var attributedText: Binding
```

### Code Example

```swift
// 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

```swift
var isUserInteractionEnabled: Binding
var isMultipleTouchEnabled: Binding
var isExclusiveTouch: Binding
var clipsToBounds: Binding
var tintColor: Binding
var backgroundColor: Binding
var borderColor: Binding
var shadowColor: Binding
var alpha: Binding
var isOpaque: Binding
var isHidden: Binding
```

### 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

```swift
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

```swift
@ViewController // Projected Value: ViewControllerInterface
```

`ViewControllerInterface`

```swift
var isVisible: AnyPublisher
var viewWillAppear: AnyPublisher
var viewDidAppear: AnyPublisher
var viewWillDisappear: AnyPublisher
var viewDidDisappear: AnyPublisher
```

### Extension Method

```swift
func lifecyclePublisher() -> AnyPublisher
```

### Publisher Extension

```swift
func isVisiblePublisher() -> AnyPublisher where Output == ViewControllerLifecycleEvent
```

### Code Example

```swift
// 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](https://developer.apple.com/documentation/combine/publisher/share()) 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:

```swift
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:

```swift
extension Bindable where Target: UIView {

var tag: Binding {
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:

```swift
@propertyWrapper
struct Example {

var wrappedValue: T
var projectedValue: AnyPublisher

init(wrappedValue: T) {
self.wrappedValue = wrappedValue
self.projectedValue = wrappedValue
.valuePublisher()
.share()
.eraseToAnyPublisher()
}
}

extension Bindable where Target: ExampleControl {

var value: Binding {
Binding(self, for: \.value)
}
}

extension ExampleControl {

func valuePublisher() -> AnyPublisher {
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](
https://github.com/Tinder/CombineUI/blob/main/LICENSE
).