RxModal
RxModal enforces the simple idea that a modal flow can be considered as a simple asynchroneous event:
- the view controller is presented on subscribe
- the user do what they want to do in the modal view
- the view controller is dismissed on dispose and eventually emit a value or an error
Usage
Here's an example | In Action |
---|---|
let mailComposer = RxModal.mailComposer {
$0.setToRecipients([
"[email protected]"
])
$0.setMessageBody(
"Hello World!",
isHTML: false
)
}
let messageComposer = RxModal.messageComposer {
$0.recipients = ["0639981337"]
$0.body = "Hello World!"
}
contactUsButton
.rx.tap
.flatMapFirst { [unowned contactUsButton] in
RxModal.actionSheet(
source: .bounds(contactUsButton),
actions: [
.default(
title: "Mail",
flatMapTo: mailComposer
),
.default(
title: "Message",
flatMapTo: messageComposer
),
.cancel(title: "Cancel")
])
}
.subscribe()
.disposed(by: disposeBag)
|
Supported Modals
// MFMailComposeViewController
RxModal.mailComposer() -> Single<MFMailComposeResult>
// MFMessageComposeViewController
RxModal.messageComposer() -> Single<MessageComposeResult>
// MPMediaPickerController
RxModal.mediaPicker() -> Single<MPMediaItemCollection>
// PHPickerViewController
RxModal.photoPicker() -> Single<[PHPickerResult]>
// ASWebAuthenticationSession
RxModal.webAuthenticationSession(url:callbackURLScheme:) -> Single<URL>
// UIAlertController
RxModal.alert<T>(title:message:textFields:actions:) -> Observable<T>
RxModal.actionSheet<T>(source:title:message:actions:) -> Observable<T>
Presenter
All these functions also include a presenter: Presenter
argument that allows you to choose where the modal will be presented.
Presenters are just lazy UIViewController
getters:
.viewController(_:) -> $0
.view(_:) -> $0.window?.rootViewController
.window(_:) -> $0.rootViewController
.scene(_:) -> $0.windows.first?.rootViewController
.keyWindow -> UIApplication.shared.keyWindow?.rootViewController
Default is .keyWindow
. On iPad or macCatalyst allowing multiple windows, we discourage you to use .keyWindow
or .scene(_:)
as it might select the wrong window.
Configuration
These functions also include a configuration closure : (ViewController) -> Void
that will let you configure the view controller before presentation.
If a modal requires some parameters at init
time, they will be part of the RxModal
function (ex: ASWebAuthenticationSession
, UIAlertController
).
Preconditions
Some RxModals perform precondition checks before presenting the modal and emit a RxModalError.unsupported
if they aren't fulfilled:
RxModal.mailComposer()
→MFMailComposeViewController.canSendMail()
RxModal.messageComposer()
→MFMessageComposeViewController.canSendText()
Some RxModals perform an authorization status check before presenting the modal and either request authorization, or emit a RxModalError.authorizationStatusDenied(Any)
if authorization is denied:
RxModal.mediaPicker()
→MPMediaLibrary.authorizationStatus()
Dialogs
RxModal.alert()
and RxModal.actionSheet()
allows you to define actions that are converted to a new Observable stream, a value, or an error:
DialogAction<T>.default(title:flatMapTo: Observable<T>)
DialogAction<T>.default(title:mapTo: T) // == flatMapTo: Observable.just(T)
DialogAction<T>.default(title:throw: Error) // == flatMapTo: Observable.error(Error)
DialogAction<T>.default(title:) // == flatMapTo: Observable.empty()
RxModal.alert()
also let you configure alert text fields:
RxModal.alert(
title: "Sign in",
message: "Please sign in using your credentials",
textFields: [
DialogTextField.email { $0.placeholder = "e-mail" },
DialogTextField.password { $0.placeholder = "password" }
],
actions: [
.cancel(title: "Cancel"),
.default(title: "Sign In") { textFields in
Credentials(
email: textFields[0].text ?? "",
password: textFields[1].text ?? ""
)
},
]
)
Extending RxModal
You can easily extend RxModal with your own controllers / modal flows.
If your controller is already returning its output using Rx, it's easy:
class MyModalViewController: UIViewController {
let myResult = PublishSubject<MyResult>()
// ...
}
extension RxModal {
func myModal(
presenter: Presenter = .keyWindow,
configuration: @escaping (MyModalViewController) -> Void
) -> Single<MyResult> {
RxModalCoordinator<MyModalViewController>.present(using: presenter) { _ in
let modal = MyModalViewController()
configuration(modal)
return modal
} sequence: {
$0.viewController.myResult.asSingle()
}
}
}
If your controller is rather using a traditional delegate
approach, you'll need to subclass RxModalCoordinator
:
protocol MyModalViewControllerDelegate: AnyObject {
func myModal(_ myModal: MyModalViewController, didFinishWith result: MyResult)
func myModal(_ myModal: MyModalViewController, didFinishWithError error: Error)
}
class MyModalViewController: UIViewController {
weak var delegate: MyModalViewControllerDelegate?
// ...
}
extension RxModal {
func myModal(
presenter: Presenter = .keyWindow,
configuration: @escaping (MyModalViewController) -> Void
) -> Single<MyResult> {
MyModalViewControllerCoordinator.present(using: presenter) { coordinator in
let modal = MyModalViewController()
modal.delegate = coordinator
configuration(modal)
return modal
} sequence: {
$0.myResult.asSingle()
}
}
}
final class MyModalViewControllerCoordinator: RxModalCoordinator<MyModalViewController>, MyModalViewControllerDelegate {
required init(){}
let myResult = PublishSubject<MyResult>()
func myModal(_ myModal: MyModalViewController, didFinishWith result: MyResult) {
myResult.onNext(result)
myResult.onCompleted()
}
func myModal(_ myModal: MyModalViewController, didFinishWithError error: Error) {
myResult.onError(error)
}
}
If your controller is embedded in a non UIViewController
object, you won't be able to leverage on RxModalCoordinator
and you'll need to handle all the present/dismiss boilerplate. See ASWebAuthenticationSession.swift as an example.
Author
License
RxModal is available under the MIT license. See the LICENSE file for more info.