will and way

ただの自分用メモを人に伝える形式で書くことでわかりやすくまとめてるはずのブログ

アクションシートのクロージャをObservable化して処理を一本化する

アクションシートってよくつかわれるんですかね? 私が携わっているプロジェクトではそこそこ使われています。

f:id:matsuokah:20171012231640g:plain

コレです

アクションシートの基本的な使い方

let actionSheet = UIAlertController(title:"Title", message: "Message", preferredStyle: .actionSheet)

let action1 = UIAlertAction(title: "Action 1", style: .default) {
    (action: UIAlertAction!) in
    print("Selected Action!")
}

// キャンセル
let cancel = UIAlertAction(title: "Cancel", style: .cancel) {
    (action: UIAlertAction!) in
    print("Selected Cancel!")
}

actionSheet.addAction(action1)
actionSheet.addAction(cancel)

present(alert, animated: true, completion: nil)
  1. UIAlertControllerをつくる
  2. アクションを追加する
  3. 表示する

これが基本的な使い方です。

また、UIAlertActionには選択されたときのクロージャが用意されていて、都度、処理を定義する必要があります。

パッとみて感じるのが、繰り返しの記述ということです。 こういうのをループにしたくなるのがプログラマの性ですよね。

ここで私は2つ思い浮かびました

  • UIAlertActionの生成を共通化できないか?
  • 「どれが選択されたか」をストリームとして扱えないか?

ということで、Rxをつかってコレを実現したいと思います。

アクションシートの特徴

簡単に述べると下記の3点

  • アクションシート自体はタイトルとメッセージを持つ
  • アクション毎にタイトルと選択された時の処理を持つ
  • キャンセルは例外

選択をObservable化するにあたっての方針

  • アクションの生成元となる情報の配列を引数に取るメソッドを定義し、UIAlertActionの生成をまとめる。
  • どのアクションが選択されたかをenumで表現する
  • アクションシートの起動はUIViewControllerのextensionにまかせてします

実装

早速実装です。 なお、Githubにもソースを上げてます

ActionSheetAction.swift

まずは、アクションシートの各行を表現する構造体を定義します

// MARK: - Action
internal struct ActionSheetAction<Type: Equatable> {
    internal let title: String
    internal let actionType: Type
    internal let style: UIAlertActionStyle
}

ジェネリクス(Type)を用いることによって外からアクションのタイプ(enum)をインジェクトできるようにします。
この型は最終的にObservableに用います。

UIViewController+RxActionSheet.swift

次に、UIViewControllerにUIAlertControllerを表示するextensionを定義します

import UIKit
import RxSwift

internal extension UIViewController {
    internal func showActionSheet<Type>(title: String?, message: String? = nil, cancelMessage: String = "Cancel", actions: [ActionSheetAction<Type>]) -> Observable<Type> {
        let actionSheet = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)       
        return actionSheet.addAction(actions: actions, cancelMessage: cancelMessage, cancelAction: nil)
            .do(onSubscribed: { [weak self] in
                self?.present(actionSheet, animated: true, completion: nil)
            })
    }
}

サブスクライブされた時にアクションシートを表示しています

UIAlertController+RxActionSheet.swift

最後に、UIAlertControllerにアクション追加と同時にObservable化して返すextensionを実装します

import UIKit
import RxSwift

internal extension UIAlertController {
    internal func addAction<Type>(actions: [ActionSheetAction<Type>], cancelMessage: String, cancelAction: ((UIAlertAction) -> Void)? = nil) -> Observable<Type> {
        return Observable.create { [weak self] observer in
            actions.map { action in
                return UIAlertAction(title: action.title, style: action.style) { _ in
                    observer.onNext(action.actionType)
                    observer.onCompleted()
                }
                }.forEach { action in
                    self?.addAction(action)
            }
            
            self?.addAction(UIAlertAction(title: cancelMessage, style: .cancel) {
                cancelAction?($0)
                observer.onCompleted()
            })
            
            return Disposables.create {
                self?.dismiss(animated: true, completion: nil)
            }
        }
    }
}

こんな感じでObservableを作成し、各アクションのクロージャでactionTypeとともにonNextを発行してあげることで
アクションシートの選択をストリーム化することができました。

使い方

AnimalSelectAction.swift

まずアクションの一覧をenumで定義します

enum AnimalSelectAction: String {
    case dog, cat, rabbit, panda

    static var AnimalSelectActions: [AnimalSelectAction] {
        return [.dog, .cat, .rabbit, .panda]
    }
}

ViewController.swift

import UIKit
import RxSwift

final class ViewController: UIViewController {

    private let disposeBag = DisposeBag()
    
    @IBOutlet weak var showActionSheetButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        showActionSheetButton.rx
            .tap
            .asDriver()
            .drive(onNext: { [weak self] _ in
                self?.showActionSheet()
            }).disposed(by: disposeBag)
    }
}

private extension ViewController {
    func showActionSheet() {
        let actions = AnimalSelectAction.AnimalSelectActions
            .map {
                return ActionSheetAction(title: $0.rawValue, actionType: $0, style: .default)
        }

        // アクションシートを表示し、返ってくるObservableをSubscribeしておく。
        showActionSheet(title: "Which Do you like?", actions: actions)
            .subscribe { (event: Event<AnimalSelectAction>) in
                NSLog(event.debugDescription)
            }.disposed(by: disposeBag)
    }
}

動作

f:id:matsuokah:20171012225321g:plain

まとめ

ということで、ViewController側ではアクションシートの選択肢の列挙とそれに対して選択された時の記述をするだけで良くなりました。
UIAlertControllerもほぼ意識せずにできてるのはメリデメあるかもですが記述が簡潔になったかと思います。
extension最高\(^o^)/

サンプルリポジトリ

github.com