January 17, 2017

UIViewControllerをstoryboardから生成する時のデメリットを緩和する

Categories: 技術 | Tags: #iOS #Swift


最近Storyboardの肩身が狭い感じになってきましたが、流石にautolayoutを全部コードで書くなんて苦行はしたくない…全て切り替えるには少しつらい。
そんな時にUIViewControllerをstoryboardから生成する場合のデメリットとそれを少し緩和する方法を書いてみます。

ちなみに自分はStoryboardは使うが遷移はSegueを使わない派です。

Storyboardから生成する場合にパラメータを渡しづらい

Storyboardを捨てて、コードで全て生成するぜ!って場合、UIViewControllerのイニシャライザでパラメータを注入することが可能になります

class UserProfileViewController: UIViewController {
    private let userId: String
    init(userId: String) {
        ...
    }
}

class UserListViewController: UIViewController {
    func presentUserProfile(userId: String) {
        let vc = UserProfileViewController(userId: "xxxxx")
        self.present(vc, animated: true, completion: nil)
    }
}

更に、userId?や! を付けたり、外から代入するためにvarにしたり、アクセスコントロールをinternalにしなくて済んだりと恩恵が大きくなります。

それに対して、Storyboardから生成する場合、 StoryboardInstantiatable なるものを用意したとしても、生成時にイニシャライザでパラメータを注入することが不可能です。

// ちょっと汚いですが...ViewController名==Storyboard名を前提とした場合の、
// storyboardからUIViewControllerを生成するextension(仮)
public protocol StoryboardInstantiatable {
    static var storyboardName: String { get }
}

public extension StoryboardInstantiatable where Self: UIViewController {
    public static var storyboardName: String {
        return String(describing: self)
    }

    public static func instantiate() -> Self {
        let storyboard = UIStoryboard(name: storyboardName, bundle: bundle)
        return storyboard.instantiateInitialViewController() as! Self
    }
}
extension UserProfileViewController: StoryboardInstantiatable {}

let vc = UserProfileViewController.instantiate()
vc.userId = "xxxxx" // ☆

さて、この2行目の☆を達成するためには

var userId: String = "" //non-optionalなので初期値を入れる
var userId: String? // optionalにする。使う時にunwrapが必要
var userId: String! // IUOにする。unwrapは不要だがnilだとクラッシュ

のように、外に変数を公開しないといけないこと、外から値が変更可能になってしまいます。そしてvarなので内部でも書き換え可能に..
Optional を採用すると常にunwrapがついて回るというデメリットがでてきます。
IUOを使う場合には初期値がなければnilであるタイミングが存在するため、場合によってはクラッシュにつながります。
更に userId 渡し忘れちゃったとかそういった凡ミスも起きやすくなります。

Storyboardは使いたい。でもこのデメリットをなるべく回避したい

ということで、このデメリットをなるべく回避したいということで、Factoryクラス作ってみる方法を考えてみました。

struct UserProfileViewControllerFactory {
    private init() {}
    static func create(userId: String) -> UserProfileViewController {
        let vc = UserProfileViewController.instantiate()
        vc.userId = userId
        return vc
    }
}

class UserProfileViewController: UIViewController {

    fileprivate var userId: String!

    override func viewDidLoad() {
        super.viewDidLoad()
        print(userId)
    }
}

extension UserProfileViewController: StoryboardInstantiatable {}

どうしても、 IUO(!)を使わざるを得ない、もしくは初期値を入れてIUOをやめないといけないというデメリットと、
内部でもvarであるため値の再代入が可能になってしまうことが残りますが、
Factoryクラスで生成処理を包んであげることで、

  • 外に変数を公開しなくて済む(Factoryクラスだけ見れるようにすれば済む)
  • 生成処理でパラメータ渡し忘れた…を回避できる

と、デメリットを少し緩和することができます

呼び出す時は、コードベースだった場合とほぼ同等の呼び方ができるようになります。

class UserListViewController: UIViewController {
    func presentUserProfile(userId: String) {
        let vc = UserProfileViewControllerFactory.create(userId: "xxxxx")
        self.present(vc, animated: true, completion: nil)
    }
}

あくまでも一例にすぎませんし、都度Factoryクラスを作らないといけないですが、最初に挙げた例のまま素で使うよりは良くなった気もします。

ここまでまとめたGistはこちらになります

さいごに

人によって開発スタイルは様々なので自分に合ったものを選択した方が良いですが、プロジェクトに複数人いる場合はコードベースでやるのか、storyboardをガッツリ使うのか決めて置かないと現場が混沌としそうな…。
せめてカスタムなViewはxibでautolayoutまで書いて生成…という譲歩もありかもしれないですね。
あとデメリットと今回ちょっと強調して言っていますがちょっと劣ってしまう点くらいで捉えてもらえると..!

参考


written by sgr-ksmt