5. W H A T I S R X ?
5
•RxSwift - Swift implementation of ReactiveX
•Follows the “Observer pattern”
•Declarative way of defining the data flow in your app
•Avoid “callback hell”
•Data flow is handled via manageable streams
6. W H A T I S R X ?
6
STREAMS
Observable<WaterMolecule>
Observable<Bool>
Observable<MeetUp>
of things.
One thing at a time.
12. 12
01User actions (button taps, text field delegates)
02Async operations (Network calls, processing)
03Bindings (VC!!<-> VM !!<-> Model)
L I S T
WHEN TO
USE RX
04Prevent code 🍝
13. B U T T O N A C T I O N
13
WITHOUT RX
@IBAction func logoTapped(_ sender: UIButton) {
dismissUntilHome()
}
navBar.logoButton !=> dismissUntilHome !!>>> rx_disposeBag
WITH RX
Drag and drop to create IBAction function. A bit more complicated if it is nested in a custom
view.
We are using Fira Code font: https://github.com/tonsky/FiraCode
14. D A T E P I C K E R
14
WITH RX
WITHOUT RX
Drag and drop to create IBAction function. A bit more complicated if it is nested in a custom
view, or number of date pickers are not constant.
datePicker.rx.date !=> viewModel.endDate !!>>> rx_disposeBag
@IBAction func datePicked(_ sender: UIDatePicker) {
viewModel.endDate = sender.date
}
15. T E X T F I E L D
15
WITH RX
titleField.textView.rx.text.orEmpty !!<-> viewModel.title !!>>> rx_disposeBag
Create binding in view controller.
WITHOUT RX
Set up delegate for the text field to listen for edit events to update view model, and manually
trigger UI update when view model’s property has changed.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
replacementString string: String)
var title: String = "" {
didSet {
updateTextFields()
}
}
17. S C R O L L V I E W
17
WITH RX
tableView.rx_scrolledUnderTop !=> viewModel.showTopGradient !!>>> rx_disposeBag
tableView.rx_scrolledUnderBottom !=> viewModel.showBottomGradient !!>>> rx_disposeBag
Create binding in view controller.
WITHOUT RX
Set up delegate extensions and do the calculation within the method, at multiple places for
multiple classes:
func scrollViewDidScroll(_ scrollView: UIScrollView)
18. P A G I N A T I O N
18
SET UP DATA CONTROLLER
func getPaginatedData<T: RealmSwift.Object>(resource: Resource, loadNextPageTrigger: Observable<Void>, dataParser: @escaping (Data) !-> ([T], Int)) !->
Observable<[T]> {
let existingObjects: [T] = Realm.ip_objects(type: T.self)!?.toArray() !?? []
return recursiveGetPaginatedData(resource: resource, lastModified: lastModifiedDate, dataParser: dataParser, loadedSoFar: [], page: 1, loadNextPageTrigger:
loadNextPageTrigger).startWith(existingObjects)
}
func recursiveGetPaginatedData<T: RealmSwift.Object>(resource: Resource, dataParser: @escaping (Data) !-> ([T], Int), loadedSoFar: [T], page: Int,
loadNextPageTrigger: Observable<Void>) !-> Observable<[T]> {
guard let urlRequest = URLRequest(builder: URLRequestBuilder(resource: resource, paginationPage: page, authenticationToken = authenticationToken) else {
return Observable.just(loadedSoFar)
}
return networkOperationQueue.add(dataRequest: urlRequest).observeOn(MainScheduler.instance)
.flatMap { data !-> Observable<[T]> in
var justLoaded = loadedSoFar
let (models, paginationTotalItems) = dataParser(data)
justLoaded.append(contentsOf: models)
if justLoaded.count !== paginationTotalItems {
Realm.ip_add(justLoaded, update: true, configuration: self.realmConfiguration)
return Observable.just(justLoaded)
}
return Observable.concat([
Observable.just(justLoaded),
Observable.never().takeUntil(loadNextPageTrigger),
Observable.deferred { self.recursiveGetPaginatedData(resource: resource, dataParser: dataParser, loadedSoFar: justLoaded, page: page + 1,
loadNextPageTrigger: loadNextPageTrigger) }
])
}
}
Functions of the network call in data controller:
19. P A G I N A T I O N
19
SET UP VIEW MODEL
func opportunities(loadNextPageTrigger: Observable<Void>) !-> Observable<[OpportunityModel]> {
return getPaginatedData(resource: Resource.opportunities, loadNextPageTrigger: loadNextPageTrigger) { (data) !-> ([OpportunityRealmModel], Int) in
let opportunitiesModel = try! OpportunitiesModel(node: data)
return (opportunitiesModel.opportunities, opportunitiesModel.total)
}
.map { $0 as [OpportunityModel] }
}
Function of the API call in data controller:
Where we make the API call in view model:
dataController.opportunities(loadNextPageTrigger: nextPageTrigger.asObservable())
.map { $0.map { OpportunityCellViewModel(opportunity: $0) } }
.subscribe(
onNext: {
self.opportunityCellViewModels = $0
self.hasMoreOpportunities = true
},
onError: {
Logger.error($0)
NotificationCenter.postMessage(type: .requestFailure)
self.hasMoreOpportunities = false
},
onCompleted: {
self.opportunityCellViewModels.append(EndOfListViewModel())
self.hasMoreOpportunities = false
}) !!>>> rx_disposeBag
20. P A G I N A T I O N
20
GET NEXT PAGE IN VIEW MODEL
func nextPage() {
nextPageTrigger.fire()
}
How we get the next page in the view model:
21. N E T W O R K C A L L S
21
CHAINED NETWORK CALLS
guard let s3Object = requestS3Object(for: .opportunity) else { return nil }
return s3Object.observeOn(MainScheduler.instance).flatMap { s3Object !-> Observable<Bool> in
opportunity.imageURL = URL(string: s3Object.publicURL)
opportunity.imageKey = s3Object.key
guard let presignedURL = URL(string: s3Object.presignedURL) else {
return Observable.error(RxURLSessionError.requestCreationError)
}
return self.uploadImage(data: imageData, to: presignedURL)
}.observeOn(MainScheduler.instance).flatMap { imageUploadSuccess !-> Observable<Data> in
requestBuilder.data = opportunity.toJson()
guard let urlRequest = URLRequest(builder: requestBuilder) else {
return Observable.error(RxURLSessionError.requestCreationError)
}
return self.networkOperationQueue.add(dataRequest: urlRequest)
}
22. R E A C H A B I L I T Y
22
CREATE REACHABILITY SERVICE
class DefaultReachabilityService: ReachabilityService {
private let _reachabilitySubject: BehaviorSubject<ReachabilityStatus>
var reachability: Observable<ReachabilityStatus> {
return _reachabilitySubject.asObservable()
}
let _reachability: Reachability
init() throws {
guard let reachabilityRef = Reachability() else { throw ReachabilityServiceError.failedToCreate }
let reachabilitySubject = BehaviorSubject<ReachabilityStatus>(value: .unreachable)
let backgroundQueue = DispatchQueue(label: "reachability.wificheck")
reachabilityRef.whenReachable = { reachability in
backgroundQueue.async {
reachabilitySubject.on(.next(.reachable(viaWiFi: reachabilityRef.isReachableViaWiFi)))
}
}
reachabilityRef.whenUnreachable = { reachability in
backgroundQueue.async {
reachabilitySubject.on(.next(.unreachable))
}
}
try reachabilityRef.startNotifier()
_reachability = reachabilityRef
_reachabilitySubject = reachabilitySubject
}
}
How we create observable for reachability of network (by Krunoslav Zaher):
23. R E A C H A B I L I T Y
23
DISPLAY REACHABILITY MESSAGE
reachabilityService.reachability
.skip(1)
.throttle(10, scheduler: MainScheduler.instance)
.observeOn(MainScheduler.instance)
.subscribe(onNext: {
$0.reachable ? self.hideMessage() : self.showMessage(.lostConnection)
}) !!>>> disposeBag
How we subscribe to reachability observable:
24. B L U E T O O T H
24
SUBSCRIBING TO A BLUETOOTH STREAM
class AwesomeViewController: UIViewController {
let viewModel = DeviceStatusViewModel()
@IBOutlet weak var batteryImageView: UIImageView!
func viewDidLoad() {
bindToViewModel()
}
override func bindToViewModel() {
super.viewDidLoad()
viewModel.devicesManager.batteryStatus
.subscribeOn(MainScheduler.instance)
.subscribe(next: { batteryStatus in
self.batteryImageView.image = self.batteryImageForStatus(batteryStatus)
}) !!>>> rx_diposeBag
}
}
25. L O O K S G R E A T B U T …
25
STACKTRACE
HELL
27. B E S T P R A C T I C E S
27
infix operator !=> : Binding
infix operator !!>>> : Binding
public func !=> <T, P: ObserverType>(left: Variable<T>, right: P) !-> Disposable where P.E !== T {
return left.asObservable().bindTo(right)
}
public func !=> (left: UIButton, right: @escaping () !-> Void) !-> Disposable {
return left.rx.tap.subscribe(onNext: { right() })
}
CREATE OPERATORS FOR COMMON TASKS
Syntax sugar that greatly reduces boilerplate code:
28. B E S T P R A C T I C E S
28
public func !!<-> <T>(property: ControlProperty<T>, variable: Variable<T>) !->
Disposable {
let bindToUIDisposable = variable
.asObservable()
.bindTo(property)
let bindToVariable = property
.subscribe(
onNext: { n in
variable.value = n
},
onCompleted: {
bindToUIDisposable.dispose()
}
)
return Disposables.create(bindToUIDisposable, bindToVariable)
}
TWO-WAY BINDING
29. S C R O L L V I E W
29
SCROLL VIEW EXTENSIONS (AS PROMISED)
extension UIScrollView {
public var rx_scrolledUnderTop: Observable<Bool> {
return self.rx.contentOffset
.map { $0.y > 0 }
.distinctUntilChanged()
}
public var rx_scrolledUnderBottom: Observable<Bool> {
return self.rx.contentOffset
.map { $0.y < self.contentSize.height - self.frame.size.height - 1 }
.distinctUntilChanged()
}
}
Create extension for scroll view.
30. B E S T P R A C T I C E S
30
cell.viewOpportunityOverlayView.rx_tapGesture !=> {
self.showOpportunityDetail(opportunityVM.opportunity)
} !!>>> cell.cellDisposeBag
WATCH OUT FOR CELL REUSE
Be sure to reset bindings on cell reuse! In view controller:
override func prepareForReuse() {
super.prepareForReuse()
cellDisposeBag = DisposeBag()
}
In table view cell:
31. B E S T P R A C T I C E S
31
func bindToViewModel() {
Observable.combineLatest(vm.passwordValid, vm.passwordIsMinLength) {
$0 !&& $1
} !=> passwordReqsLabel.rx_hidden !!>>> rx_disposeBag
vm.emailAddress !<- emailAddressField.rx_text !!>>> rx_disposeBag
vm.password !<- passwordField.rx_text !!>>> rx_disposeBag
vm.passwordConfirmation !<- confirmPasswordField.rx_text !!>>> rx_disposeBag
}
@IBOutlet weak var settingsButton: UIButton! {
didSet {
settingsButton !=> showSettingsVC !!>>> rx_disposeBag
}
}
DESIGNATED METHOD FOR BINDING
32. B E S T P R A C T I C E S
32
class DeviceManager {
private var batteryStatus = Variable<BatteryLevel>(.low)
public var batteryStatusObs = batteryStatus.asObservable()
}
PUBLIC VS. PRIVATE
33. B E S T P R A C T I C E S
33
extension ObservableType {
public func ip_repeatingTimeouts(
interval dueTime: RxTimeInterval,
element: E,
scheduler: SchedulerType = MainScheduler.instance
) !-> Observable<E> {
return
Observable.of(
self.asObservable(),
debounce(dueTime, scheduler: scheduler).map { _ in element }
)
.merge()
}
}
REPEATING TIMEOUTS
35. 35
• What are you reacting to?
• Are you using a struct or a class?
• Observable vs. Variable?
• Does the subscription need to update things on the screen?
• Will the view update while it’s being displayed?
ASK YOURSELF…
C O N C L U S I O N S
37. 37
• RxMarbles.com
• ReactiveX.io
• https://github.com/IntrepidPursuits/swift-wisdom
• https://github.com/ReactiveX/RxSwift
• rxswift.slack.com
USEFUL LINKS
C O N C L U S I O N S