Implementing Reactive Patterns in Swift with RxSwift and RxCocoa
Alexander Murphy
Boulder IOS Meetup
Want to follow along?
https://github.com/thexande/denverswiftheads-march-rxswift
Alexander Murphy
-
Programming for 10 years, 5 professionally
-
Background in LAMP stack PHP and Node.js
-
Attended Galvanize summer 2016, Web Development
-
Began building IOS apps with Swift in 2015
-
Currently working as an IOS Engineer @ Ibotta
What is an Observable?
A sequence of data represented as events over time.
What is an Observer?
A subscription to a sequence of data represented as events over time.
ReactiveX
- "ReactiveX is a library for composing asynchronous and event-based programs by using observable sequences"
- Originally implemented with .NET framework
- Quickly gained popularity and was ported to most major programming languages
- Provides consistent reactive bindings, structures and patterns across many platforms
- Angular 2 change detection, event binding and property binding implemented with RxJS
What is a "Reactive Program"?
Reactive Programs
- Data changes are reflected in the UI immediately
- Provide a consistent and predictable User Experience
- "Reload" and "Refresh" user interfaces are not required
https://www.slideshare.net/scott.gardner/reactive-programming-with-rxswift
RxSwift and RxCocoa
- RxSwift is a framework for implementing reactive programming patterns in swift.
- RxSwift contains methods and classes for creating, transforming and combining observable data streams
- RxCocoa contains observable extensions for UIKit.
- RxCocoa extensions allow for the creation of observable bindings to UI elements.
Observable Lifecycle
1. onNext()
2. OnError()
3. onComplete()
4. Disposal
let arr1: [Int] = [1, 2, 3, 4, 5]
let arr2: [Int] = [6, 7]
let arr3: [Int] = [8, 9, 10]
// Map -> [2, 4, 6, 8, 10]
let mapped = arr1.map{ $0 * 2 }
// Flat Map -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let combo: [[Int]] = [arr1, arr2, arr3]
let flatMapped = combo.flatMap{ $0 }
// Filter -> [1, 3, 5]
let filtered = arr1.filter{ $0 % 2 != 0 }
// Reduce -> 15
let reduced = arr1.reduce(0, { $0 + $1 })
// Filter and Reduce -> 9
let oddsSum = arr1.filter{ $0 % 2 != 0 }.reduce(0, { $0 + $1 })
Transformations with Immutable Arrays
Immutable Arrays vs. Observables
- Both can be combined and transformed into new representations of their original type.
- Immutable arrays are static over time, while Observables emit immutable values over time.
- Immutable Arrays are statically defined at runtime while observables emit immutable values over time.
Observables are just immutable arrays over time!
import UIKit
import RxSwift
import RxCocoa
class EmailInputViewController: UIViewController {
let textField = UITextField()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// add subview and constraints
// configure observable
_ = self.textField.rx
.text
.subscribe(onNext: {
print("text here: \($0)")
}).addDisposableTo(self.disposeBag)
}
}
Observable UITextField
Observable UITextField Use Case:
Validating an Email address
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController { }
Class Properties
//Observable garbage collection
let disposeBag = DisposeBag()
// Email input text field
let textField: UITextField = {
let field = UITextField()
field.backgroundColor = UIColor.black
field.textColor = UIColor.white
field.layer.borderColor = UIColor.red.cgColor
field.layer.cornerRadius = 5
field.layer.borderWidth = 3
field.translatesAutoresizingMaskIntoConstraints = false
return field
}()
// View constraints
lazy var textFieldConstraints: [NSLayoutConstraint] = [
NSLayoutConstraint(item: self.textField, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 50),
NSLayoutConstraint(item: self.textField, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.textField, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self.textField, attribute: .left, relatedBy: .equal, toItem: self.view, attribute: .left, multiplier: 1, constant: 50),
NSLayoutConstraint(item: self.textField, attribute: .right, relatedBy: .equal, toItem: self.view, attribute: .right, multiplier: 1, constant: -50)
]
Observable Class Properties
// Email Validation
lazy var emailValidationObservable: Observable<Bool> = self.textField
.rx
.text
.debounce(0.3, scheduler: MainScheduler.instance)
.map{ text -> Bool in
guard let text = text else { return false }
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
return NSPredicate(format:"SELF MATCHES %@", emailRegEx).evaluate(with: text)
}
// Email Value
lazy var emailContentObserver: Observable<[String:String]> = self.textField
.rx
.text
.orEmpty
.debounce(0.3, scheduler: MainScheduler.instance)
.flatMap{ text -> Observable<[String:String]> in
return text.isEmpty ? .just([String: String]()) : .just(["email": text])
}
Observer Transform with Debounce
- Debounce prevents observation overflow
- Prevents complex logic or network calls from being executed too frequently
- Perfect for text input observables
Graphic sourced from http://rxmarbles.com/
View Configuration and Observers
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.textField)
NSLayoutConstraint.activate(self.textFieldConstraints)
configureObservers()
}
func configureObservers() {
_ = self.emailValidationObservable.subscribe(onNext: { hasValidated in
self.textField.layer.borderColor = hasValidated ? UIColor.green.cgColor : UIColor.red.cgColor
print(hasValidated ? "Email is valid." : "Email is invalid")
})
.addDisposableTo(self.disposeBag)
_ = self.emailContentObserver.subscribe(onNext: { email in
print("email input has changed: \(email)")
})
.addDisposableTo(self.disposeBag)
}
The Result
A encapsulated observable data structure to validate and capture value of a UITextFIeld in real time.
One Step Further...
What if we want to observe multiple text fields for validation and value?
Desired Functionality
-
A tableview form with a textfield for phone number, email, and password
-
Validation for each field with visual feedback
-
A submit button disabled until the form has validated
-
A data structure for validated form values
Form Field Cell SubClass
class InputCell: UITableViewCell {
let title: String
let validator: NSPredicate
let disposeBag = DisposeBag()
lazy var textField: UITextField
lazy var textFieldConstraints: [NSLayoutConstraint]
lazy var validationObservable: Observable<Bool>
lazy var contentObservable: Observable<[String:String]>
init(title: String, validator: NSPredicate) {
self.title = title
self.validator = validator
super.init(style: .default, reuseIdentifier: nil)
self.contentView.addSubview(self.textField)
NSLayoutConstraint.activate(textFieldConstraints)
self.contentView.backgroundColor = UIColor.black
}
}
Form Validators and ValidUser struct
struct Validators {
// Valid Email
static let email = NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}")
// Numeric Input
static let phone = NSPredicate(format: "SELF MATCHES %@", "^[0-9]+$")
// Longer than 4 chars, less than 8
static let password = NSPredicate(format: "SELF MATCHES %@", "^.{4,8}$")
}
struct ValidUser {
let email: String
let phone: String
let password: String
}
- Provide RegEx for each field type
- ValidUser struct to submit to API on form submission
UITextField Observables
// FieldCell class properties
lazy var validationObservable: Observable<Bool> = self.textField
.rx
.text
.debounce(0.3, scheduler: MainScheduler.instance)
.map{ text -> Bool in
guard let text = text else { return false }
let hasValidated = self.validator.evaluate(with: text)
self.textField.layer.borderColor = hasValidated ? UIColor.green.cgColor : UIColor.red.cgColor
return hasValidated
}
lazy var contentObservable: Observable<[String:String]> = self.textField
.rx
.text
.orEmpty
.debounce(0.3, scheduler: MainScheduler.instance)
.flatMap{ text -> Observable<[String:String]> in
return text.isEmpty ? .just([String: String]()) : .just([self.title: text])
}
- Validate cell based on specified Regular Expression
- Observe UITextField value by emitting a dictionary with a key of the field's title
View Controller containing UITableView
// FieldCell class properties
class ViewController: UIViewController {
let disposeBag = DisposeBag()
lazy var tableViewConstraints: [NSLayoutConstraint]
var formHasValidated: Bool = false
let cells: [UITableViewCell] = [
InputCell(title: "Email", validator: Validators.email),
InputCell(title: "Phone Number", validator: Validators.phone),
InputCell(title: "Password", validator: Validators.password)
]
lazy var table: UITableView = {
let table = UITableView()
table.delegate = self
table.dataSource = self
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.table)
NSLayoutConstraint.activate(self.tableViewConstraints)
configureValidationObserver()
configureContentObserver()
}
}
View Controller UITableView Delegates
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return self.cells[indexPath.row]
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.cells.count
}
}
Combination with combineLatest()
- combineLatest() allows observables to be grouped in a predictable manner
- Produces an array each time a single observable emits a value with the latest values from other grouped observables.
Validation Observer for all fields
func configureValidationObserver(){
var validationObservers = [Observable<Bool>]()
for cell in self.cells {
if let cell = cell as? InputCell {
validationObservers.append(cell.validationObservable)
}
}
_ = Observable.combineLatest(validationObservers) { registrationObservers -> Bool in
return registrationObservers.reduce(true, { $0 && $1 })
}
.subscribe(onNext:{ validationStatus in
self.formHasValidated = validationStatus
}).addDisposableTo(disposeBag)
}
- Reduce all form validators to a single boolean value
Content Observer for all fields
func configureContentObserver() {
var contentObservables = [Observable<[String:String]>]()
for cell in self.cells {
if let cell = cell as? InputCell {
contentObservables.append(cell.contentObservable)
}
}
_ = Observable.combineLatest(contentObservables)
{ contentObservables -> [String:String] in
return contentObservables
.flatMap{ $0 }
.reduce( [String:String]() ) { (dict, tuple) -> [String:String] in
var mutableDict = dict
mutableDict.updateValue(tuple.1, forKey: tuple.0)
return mutableDict
}
}
.subscribe(onNext:{ registrationFieldsDict in
print("Registration Fields: \(registrationFieldsDict)")
let user = ValidUser(
email: registrationFieldsDict["Email"]! as String,
phone: registrationFieldsDict["Phone"]! as String,
password: registrationFieldsDict["Password"]! as String
)
// Make API Call with valid user
}).addDisposableTo(disposeBag)
}
The Result
Where should this be used?
What about "mutable" Observables?
In other words, a Variable data point we can mutate programmatically.
Observable Variable
let disposeBag = DisposeBag()
let number = Variable(0)
number.asObservable().subscribe{ print("new value here: \($0)") }
for i in 0...4 {
number = i
}
// Prints "Next(0), Next(1), Next(2), Next(3)"
- Perfect for binding observables to your existing code
- Provides the functionality of a mutable variable while creating an immutable stream under the hood
NotificationCenter Observers
- RxBindings for NotificationCenter provide a consistent syntax for creating, observing and transforming events
- Can be combined with other observable data streams
// Create a NotificationCenter Observer
public func addObserverForName( name: String?,
object obj: AnyObject?,
queue: NSOperationQueue?,
usingBlock block: (NSNotification) -> Void) -> NSObjectProtocol
// Create a NotificationCenter Observer with RxSwift
NotificationCenter.default
.rx.notification(NSNotification.Name.UITextViewTextDidBeginEditing, object: myTextView)
.map { /*do something with data*/ }
UITableView data binding
- No delegates required
- Bind to a datasource that might be changing over time
- Handles the mess of creating, inserting, moving and removing tableview cells.
let data = Observable<[String]>.just(["first element", "second element", "third element"])
data.bindTo(tableView.rx.items(cellIdentifier: "Cell")) { index, model, cell in
cell.textLabel?.text = model
}
.disposed(by: disposeBag)
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Int>>()
Observable.just([SectionModel(model: "title", items: [1, 2, 3])])
.bindTo(tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
UITableView Example
Anti Patters to Avoid
- Observable composition soup
- onError without retry
- too many Variables()
var questions: [String]?
RxSwift Presentation
By thexande
RxSwift Presentation
- 163