LoginSignup
5
6

More than 5 years have passed since last update.

iOSアプリの基本設計を考える:「UIViewController」画面遷移(2)

Last updated at Posted at 2017-11-15

クラスの定義にジェネリックスを使って結果を定義し、クロージャで返すという手法は、結果を無視したいUIViewControllerにも影響を与える(し見た目複雑)ためにあまり良くないなと感じたので、シンプルに実装できる方法に変更しました。
元の内容は残しておきます。

問題

iOSアプリの基本設計を考える:「UIViewController」画面遷移(1)では画面終了のクロージャについて描きました。ただ、このままでは少し困ったことがあります。
その画面、終了したことは分かったけど、結果はなんなの?と言うことです。
 例えば下記のように「キャンセル」ボタンと「完了」ボタンがあった場合は、呼び出しもとは結果を知りたいですよね?

スクリーンショット 2017-11-15 23.23.34.png

Read Only変数で返す

CoreViewControllerはそのままで変更しません。

Swift
import UIKit

class CoreViewController: UIViewController {

    // 完了クロージャ
    var completion: ((_ viewController: UIViewController, _ animated: Bool) -> Void)? = nil

    // ボタンなどで画面を終了する場合
    func dismissCore(animated: Bool)
    {
        if completion != nil
        {
            completion?(self, animated)
            completion = nil
            print("\(String(describing: type(of: self)))\(#function) が呼び出されました")
        }
    }

    // ナビゲーションバーの戻るボタン "<" で画面を終了する場合
    deinit
    {
        dismissCore(animated: true)
    }
}

結果を通知するためのenum型を定義します。
ジェネリックスで記述されているために、Tを取り替えるだけでそのViewController独自の定義を使用することができます。

Swift
import Foundation

/// 結果通知用定義
///
/// - success       成功の場合 オブジェクトを返す
/// - failure       失敗の場合  Error型を返す
enum Result<T, E:Error>
{
    case success(T)     // .successの場合はT型
    case failure(E)     // .failureの場合はError型
}

呼び出し先のViewControllerには下記のような結果を定義し、上記のResultに適用します。

Swift
// 成功時
enum vcResult : Int
{
    case ok         // 正常終了
    case canceled   // キャンセルされた
}

// エラー時
enum vcErr : Error
{
    case networkErr
    case unknownErr
}

// localizedDescriptionの定義(変更)
extension vcErr: LocalizedError
{
    public var errorDescription: String?
    {
        switch self
        {
        case .networkErr:
            return "ネットワークエラーが発生しました"
        case .unknownErr:
            return "原因不明のエラーが発生しました"
        }
    }
}

ViewControllerには、変数として

Swift
public private(set) var  result: Result<vcResult, vcErr>?

を追加します。public private(set) varは、内部のみ設定可能、つまりRead Onlyを意味します。
ViewControllerのコードは下記のようになります。
それぞれOKボタンとキャンセルボタンが押下されたタイミングで単純にresult変数に結果を書き込んでいます。

Swift
class ViewController: CoreViewController {

    public private(set) var  result: Result<vcResult, vcErr>?

    // ストリーボードからインスタンを生成
    static func getInstance() -> ViewController?
    {
        let storyboardInstance = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboardInstance.instantiateViewController(withIdentifier: String(describing: self))
        return vc as? ViewController
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // ナビゲーションバーの戻るボタン "<" で画面を終了する場合のResultを指定する
        result = .success(.canceled) // 
    }

    // キャンセルボタンが押された
    @IBAction func pushedCancelButton(_ sender: Any) {
        self.result = .success(.canceled)
        dismissCore(animated: true)
    }

    // OKボタンが押された
    @IBAction func pushedOKButton(_ sender: Any) {
        self.result = .success(.ok)
        dismissCore(animated: true)
    }
}

呼び出し側の記述です。ちょっと長くなってしまいますが、結果の処理方法も記載しました。
switch文でenum Resultから必要なパラメータを抜き出しています。
この形式では、ViewControllerの終了クロージャがViewControllerの親クラスであるCoreViewControllerの型で自インスタンスを返してくるので

Swift
let vc: ViewController? = viewController as? ViewController

でキャストして使用しています。

Swift

import UIKit

class FirstViewController: UIViewController {

    // present(_:animated:completion:)を呼び出す
    @IBAction func pushedPresent(_ sender: Any) {

        if let vc = ViewController.getInstance()  // インスタンス生成
        {
            // 終了クロージャの設定
            vc.completion = { viewController, animated in

                let vc: ViewController? = viewController as? ViewController
                if let _vc = vc, let _result = _vc.result
                {
                    switch _result
                    {
                    case let .success(success):
                        switch success
                        {
                        case .ok:
                            print("*** ok ***")
                        case .canceled:
                            print("***  canceled ***")
                        }

                    case let .failure(error):
                        print("*** error \(error.localizedDescription) ***")
                        break
                    }
                }

                // 画面終了(インスタンスの消滅)
                viewController.dismiss(animated: animated, completion: nil)

                print("\(String(describing: type(of: self)))\(#function) が呼び出されました")

            }

            // 画面表示
            self.present(vc, animated: true)
        }
    }

    // pushViewController(_:animated:)を呼び出す
    @IBAction func pushedPushViewController(_ sender: Any) {

        if let vc = ViewController.getInstance()  // インスタンス生成
        {
            // 終了クロージャの設定
            vc.completion = { viewController, animated in

                let vc: ViewController? = viewController as? ViewController
                if let _vc = vc, let _result = _vc.result
                {
                    switch _result
                    {
                    case let .success(success):
                        switch success
                        {
                        case .ok:
                            print("*** ok ***")
                        case .canceled:
                            print("***  canceled ***")
                        }

                    case let .failure(error):
                        print("*** error \(error.localizedDescription) ***")
                        break
                    }
                }

                // 画面終了(インスタンスの消滅)
                viewController.navigationController?.popViewController(animated: true)

                print("\(String(describing: type(of: self)))\(#function) が呼び出されました")

            }

            // 画面表示
            self.navigationController?.pushViewController(vc, animated: true)
        }
    }
}

ソースコード

なおソースコードをGitHubに上げております。

ソースコードはここです

ここから過去記事(あまり良くないかも知れません)

ジェネリックス

そこで、「iOSアプリの基本設計を考える:「UIViewController」画面遷移(1)」の完了クロージャに手を入れます。

Swift
import UIKit

class CoreViewController: UIViewController {

    // 完了クロージャ
    var completion: ((_ viewController: UIViewController, _ animated: Bool) -> Void)? = nil

    // ボタンなどで画面を終了する場合
    func dismissCore(animated: Bool)
    {
        completion?(self, animated)
        completion = nil
        print("\(String(describing: type(of: self)))\(#function) が呼び出されました")
    }

    // ナビゲーションバーの戻るボタン "<" で画面を終了する場合
    deinit
    {
        completion?(self, true)
        completion = nil
        print("\(String(describing: type(of: self)))\(#function) が呼び出されました")
    }
}

Swift
import UIKit

/// 結果通知用定義
///
/// - success       成功の場合 オブジェクトを返す
/// - failure       失敗の場合  Error型を返す
enum Result<T, E:Error>
{
    case success(T)     // .successの場合はT型
    case failure(E)     // .failureの場合はError型
}

class CoreViewController<T, E:Error>: UIViewController {

    // 完了クロージャ
    var completion: ((_ viewController: UIViewController, _ result: Result<T, E>, _ animated: Bool) -> Void)? = nil
    // ナビゲーションバーの戻るボタン "<" で画面を終了する場合のResultを指定する
    var result: Result<T, E>? = nil

    // ボタンなどで画面を終了する場合
    func dismissCore(result: Result<T, E>, animated: Bool)
    {
        completion?(self, result, animated)
        completion = nil
        print("\(String(describing: type(of: self)))\(#function) が呼び出されました")
    }

    // ナビゲーションバーの戻るボタン "<" で画面を終了する場合
    deinit
    {
        if let _result = result
        {
            completion?(self, _result, true)
            completion = nil
        }

        print("\(String(describing: type(of: self)))\(#function) が呼び出されました")
    }
}

と変えてみましょう。

Swift
 enum Result<T, E:Error>
Swift
 class CoreViewController<T, E:Error>: UIViewController

というような書き方をジェネリックスと呼びます。
ここではジェネリックスの詳細は述べませんが、これによってCoreViewControllerの子クラスは
自身の定義した結果を完了クロージャを通じて呼び出し元画面に戻すことができるようになります。

子クラスも修正してみます。

Swift

import UIKit

// 成功時
enum vcResult : Int
{
    case ok
    case canceled
}

// エラー時
enum vcErr : Error
{
    case networkErr
    case unknownErr
}

// localizedDescriptionの定義(変更)
extension vcErr: LocalizedError
{
    public var errorDescription: String?
    {
        switch self
        {
        case .networkErr:
            return "ネットワークエラーが発生しました"
        case .unknownErr:
            return "原因不明のエラーが発生しました"
        }
    }
}

class ViewController: CoreViewController<vcResult, vcErr> {

    // ストリーボードからインスタンを生成
    static func getInstance() -> ViewController?
    {
        let storyboardInstance = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboardInstance.instantiateViewController(withIdentifier: String(describing: self))
        return vc as? ViewController
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // ナビゲーションバーの戻るボタン "<" で画面を終了する場合のResultを指定する
        result = .success(.canceled) // 
    }

    // キャンセルボタンが押された
    @IBAction func pushedCancelButton(_ sender: Any) {
        dismissCore(result: .success(.canceled), animated: true)
    }

    // OKボタンが押された
    @IBAction func pushedOKButton(_ sender: Any) {
        dismissCore(result: .success(.ok), animated: true)
    }
}

これで、
キャンセルボタンが押された場合は、.success(.canceled)を返します。
OKボタンが押された場合は、.success(.ok)を返します。
ナビゲーションバーの戻るボタン "<" で画面を終了する場合は、.success(.canceled)を返します。

補足

Swift

// localizedDescriptionの定義(変更)
extension vcErr: LocalizedError
{
    public var errorDescription: String?
    {
        switch self
        {
        case .networkErr:
            return "ネットワークエラーが発生しました"
        case .unknownErr:
            return "原因不明のエラーが発生しました"
        }
    }
}

ここは、エラーの内容をlocalizedDescriptionで取り出すためのextensionです。
LocalizedErrorプロトコルを具備しているところがポイントです。

Swift
switch result
                {
                case let .success(result):
                    switch result
                    {
                    case .ok:
                        // 処理
                    case .canceled:
                        // 処理
                    }

                case let .failure(error):
                    self.alert(error.localizedDescription, ok: nil, cancel: nil) // 適当・・・
                }

というような感じで使います。

ソースコード

なおソースコードをGitHubに上げております。

ソースコードはここです

以上です。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6