クラスの定義にジェネリックスを使って結果を定義し、クロージャで返すという手法は、結果を無視したいUIViewControllerにも影響を与える(し見た目複雑)ためにあまり良くないなと感じたので、シンプルに実装できる方法に変更しました。
元の内容は残しておきます。
##問題
iOSアプリの基本設計を考える:「UIViewController」画面遷移(1)では画面終了のクロージャについて描きました。ただ、このままでは少し困ったことがあります。
その画面、終了したことは分かったけど、結果はなんなの?と言うことです。
例えば下記のように「キャンセル」ボタンと「完了」ボタンがあった場合は、呼び出しもとは結果を知りたいですよね?
Read Only変数で返す
CoreViewControllerはそのままで変更しません。
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独自の定義を使用することができます。
import Foundation
/// 結果通知用定義
///
/// - success 成功の場合 オブジェクトを返す
/// - failure 失敗の場合 Error型を返す
enum Result<T, E:Error>
{
case success(T) // .successの場合はT型
case failure(E) // .failureの場合はError型
}
呼び出し先のViewControllerには下記のような結果を定義し、上記のResultに適用します。
// 成功時
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には、変数として
public private(set) var result: Result<vcResult, vcErr>?
を追加します。public private(set) varは、内部のみ設定可能、つまりRead Onlyを意味します。
ViewControllerのコードは下記のようになります。
それぞれOKボタンとキャンセルボタンが押下されたタイミングで単純にresult変数に結果を書き込んでいます。
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の型で自インスタンスを返してくるので
let vc: ViewController? = viewController as? ViewController
でキャストして使用しています。
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)」の完了クロージャに手を入れます。
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) が呼び出されました")
}
}
を
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) が呼び出されました")
}
}
と変えてみましょう。
enum Result<T, E:Error>
class CoreViewController<T, E:Error>: UIViewController
というような書き方をジェネリックスと呼びます。
ここではジェネリックスの詳細は述べませんが、これによってCoreViewControllerの子クラスは
自身の定義した結果を完了クロージャを通じて呼び出し元画面に戻すことができるようになります。
子クラスも修正してみます。
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)を返します。
補足
// localizedDescriptionの定義(変更)
extension vcErr: LocalizedError
{
public var errorDescription: String?
{
switch self
{
case .networkErr:
return "ネットワークエラーが発生しました"
case .unknownErr:
return "原因不明のエラーが発生しました"
}
}
}
ここは、エラーの内容をlocalizedDescriptionで取り出すためのextensionです。
LocalizedErrorプロトコルを具備しているところがポイントです。
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に上げております。
以上です。