Presenting the UIAlertController with RxSwift
Showing an action sheet seems not to be a typical use case for the Observable
. My first implementation ended with multiple PublishSubject
inside UIViewController
and ViewModel
. Multiple subscriptions inside Observable’s chain is usually a code smell. Of course, I didn’t have multiple subscribe
method calls inside my code, but only a few bindings with bindTo
. However, bindTo
is just a wrapper over the subscribe
method. In the end, I missed the sign of the code smell.
In this article, I want to show you how you can separate presentation of UIAlertController
from the UIViewController
. Moreover, I want to show you how to do implement it using the RxSwift.
If I had had current knowledge these days I would definitely write unit tests for the action sheet stuff. TDD always helps in revealing code smells!
Example – the requirements
Everything is easier to understand if it is followed by the example. So, meet the requirements for our simple application.
I would like you to write an app which … changes the avatar 💯👊! The use case is simple: User can set the image from the library or choose the last image taken. We will use the action sheet to show available options to the user.
Drawing the RxDiagram
When you start thinking about RxDiagram you will end up with the conclusion what you really want is to map Observable<Void>
into Observable<UIImage>
:
buttonTap: ---X---------X-------->
image: ------I------------I-->
buttonTap
X - a button tap event
I - an image
What I want you to look closer at is that the UIImage
events are moved in time according to corresponding button taps. The user has to select a source of the image which is shown on an action sheet.
Do you remember the diagram which represents API request?
The question is, what’s the difference between doing an API request and choosing an image over an action sheet? None, I would say. It means you can try to use the same Observable.create
to wrap UIKit
API to present a UIActionController
.
Before writing any line of code I would like you to draw a more detailed diagram first 😉
buttonTap: ----X-----------X----->
actionSheetTap: -------T------------T->
photoAuthorization: ----------A---------A->
imageFromPHLibrary: ------------I-------I->
X - a button tap event
T - an action sheet option tap event
A - authorization status
I - UIImage event
In this article, I want to take care of actionSheetTap
and how to wrap action sheet within the Observable
.
Ready, steady, go!
First of all, we will start from writing an enum
which defines the source of the image. It will help us during displaying the action sheet:
enum ImageSource: CustomStringConvertible {
case lastPhotoTaken
case imagePicker
var description: String {
switch self {
case .lastPhotoTaken:
return "Last photo taken"
case .imagePicker:
return "Choose image from library"
}
}
}
Now when we know the image sources we can create the Observable<ImageSource>
. We have to wrap existing UIKit
within the Observable
. In 99% the Observable.create
is a right tool. The same is in our case:
var image: Observable<UIImage> {
return selectedOption
.flatMap { source in
switch(source):
case .lastPhotoTaken:
// return Observable with last photo taken
case .imagePicker:
// return Observable with the image from gallery
}
}
private var selectedOption: Observable<ImageSource> {
return Observable.create { [weak self] observer in
guard let `self` = self else {
observer.onCompleted()
return Disposables.create()
}
let actionSheet = self.prepareActionSheet(with: observer)
self.presenter?.present(actionSheet)
return Disposables.create {
actionSheet.dismiss(animated: true, completion: nil)
}
}
}
I hope the code is self-explanatory. You have to create the UIAlertController
and present it using a presenter
. The presenter
is just a UIViewController
on top of which you want to present the action sheet.
However, when you use the Observable.create
you have to return the Disposable
. Disposable
is used to clean internal resources used by the Observable
. It is a good place to dismiss the action sheet. If Observable is released/canceled, we will ensure the action sheet is dismissed.
Handling UIAlertAction behavior
Furthermore, you have to handle the actions displayed on top of UIAlertController
. When a user presses any button of presented action sheet we want to pass that information to the observer
. We do so, by sending a source
as next
event.
private func prepareActionSheet(with actionTapObserver: AnyObserver<ImageSource>) -> UIAlertController {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
prepareActionSheetActions(with: actionTapObserver)
.forEach { actionSheet.addAction($0) }
return actionSheet
}
private func prepareActionSheetActions(with tapObserver: AnyObserver<ImageSource>) -> [UIAlertAction] {
var actions = createSourcesActions(with: tapObserver)
let cancel = createCancelAction(with: tapObserver)
actions.append(cancel)
return actions
}
private func createSourcesActions(with tapObserver: AnyObserver<ImageSource>) -> [UIAlertAction] {
return sources.map { source in
return UIAlertAction(title: source.description, style: .default) { _ in
tapObserver.onNext(source)
tapObserver.onCompleted()
}
}
}
private func createCancelAction(with tapObserver: AnyObserver<ImageSource>) -> UIAlertAction {
return UIAlertAction(title: Strings.cancel, style: .cancel) { _ in
tapObserver.onCompleted()
}
}
If a user touches the cancel button, you have to send completed
event.
Sending
completed
event is very important. Don’t forget to send it whenever you know the "job" is finished. If an Observable receivescompleted
orerror
event it immediately disposes itself, which helps in avoiding retain cycles.
Make it more abstract 💪
The current implementation has one thing which I don’t like. The enum. Every time you use an enum or a switch think if you can make your code more abstract. I don’t want to say it is always a good thing to make things more abstract. Sometimes the gain may not be worth the cost.
However, in our example, it is worth to replace the enum ImageSource
with a protocol. Using a protocol is a key to obeying the open-close principle:
protocol ImageSource: CustomStringConvertible {
var image: Observable<UIImage> { get }
}
Now, you want to pass the array of ImageSource
into the ImageSourceChooser
during the init:
init(sources: [ImageSource]) {
self.sources = sources
}
At this moment you need to think … What do you want to send in next
event to the observer? It used to be the enum ImageSource
. You can still send the ImageSource
or … a better solution would be to send the image property of that protocol:
private func createSourcesActions(with tapObserver: AnyObserver<Observable<UIImage>>) -> [UIAlertAction] {
return sources.map { source in
return UIAlertAction(title: source.description, style: .default) { _ in
tapObserver.onNext(source.image)
tapObserver.onCompleted()
}
}
}
Such a change requires also changes of types in other functions:
private var selectedOption: Observable<Observable<UIImage>> { ... }
private func prepareActionSheet(with actionTapObserver: AnyObserver<Observable<UIImage>>) -> UIAlertController { ... }
private func prepareActionSheetActions(with tapObserver: yObserver<Observable<UIImage>>) -> [UIAlertAction] { ... }
private func createCancelAction(with tapObserver: AnyObserver<Observable<UIImage>>) -> UIAlertAction { ... }
In the end, we need to change to the body of var image
. Since selectedOption
sends an Observable<UIImage>
in every event, you can just use the Observable
inside the flatMap
:
var image: Observable<UIImage> {
return selectedOption
.flatMap { $0 }
}
The last word
I hope the above example helps you in writing more isolated features :). If you don’t know how to express something in Observable
chain draw the diagram. It really helps a lot.
Observable.create
is a powerful piece of code which allows you to easily create your custom observables by wrapping an existing API. It can be used to wrap a REST API request or to wrap UIKit API to display UIAlertController
🙂
You can find the code with working example on my github.
What do you think about RxSwift? Have wrapping the UIAlertController
helped you? Share your thoughts in comments below!