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]?
Made with Slides.com