Xcode
iOS
Swift

Storyboard 抜きで、コードオンリーで iOS アプリの UI を作る

はじめに

これは自分用のメモでもありますが、同時にもし「Storyboard 面倒くさい!ソースコードのみで UI 作りたい!」というような方がいらっしゃいましたら、ご参考になれればと思います。
また、ソースコードは GitHub に公開しております。

なぜコードで UI 作るか

ぶっちゃけ言いますと自分 Storyboard 使えないだけです。はい。そもそも以前 Interface Builder の時代からまともにそういったツール使ったことなかったし、要素配置とかページ遷移とかもどうやって作ればいいのかわからないし、今まではゲームを作ってきたから iOS 特有の画面遷移とかも特に使ったことなかったし、あとマウスよりもキーボードのほうが速いってのもありますね。まあ要するに自分にとってソースコードのほうが画面配置がわかりやすいです。

実際作ってみる

目標と手段

まあ何事もまずは目標を決めるところからですね。この記事を作るにあたってなるべく簡単なものにしたいと思いますので、とりあえず一番基本なところ:自前の UIView サブクラスを作って、そのサブクラスで定番の「Hello World!」を画面のド真ん中に表示したいと思います。ここで肝になっているのは、どんなデバイスであろうと、必ず「画面サイズ」から、必要な要素の配置すべき場所を計算して、画面に表示するところです。ですので今後どんな画面サイズの iOS 端末が出ようと、必ずその端末のド真ん中に表示するはずです。

プロジェクトを作る

目標と手段が決まったら、次は実際手を動かすところですね。はい、Xcode を立ち上げましょう。もちろん Swift ですので、最新の Xcode(記事作成時ではバージョン 6.1.1 です)で作りましょう。「ファイル」→「New」→「Project」で、iOS の Single View Application を選びます。
スクリーンショット

そして Language は Swift、Devices は Universal にします。
スクリーンショット

あとは場所とか決めて OK 押せばプロジェクトが作成されます。ここで必要に応じて git などのバージョン管理システムを導入することもおすすめします。

Storyboard をプロジェクトから削除する

初期プロジェクトが出来上がったら、次は Storyboard を実際プロジェクトから削除しましょう。だって使わないもん(笑)。削除するものは2箇所あります、まずはプロジェクトの Info から Main storyboard file base name という項目をまるごと削除します。
スクリーンショット

あとは Main.storyboard をプロジェクトから削除します。
スクリーンショット

自前の UIView サブクラスを作る

Xcode の左下から「+」ボタンを押して、「New File」を選びます。
スクリーンショット

そして Cocoa Touch Class を選びます。
スクリーンショット

その次に Class の項目に適当に好きな名前(ここでは MainView にしています)をつけて、Sublcass of の項目は「UIView」にしましょう。
スクリーンショット

あとは保存場所とか選べば(当たり前ですが普通はプロジェクトの本体の下に置きます)MainView という UIView のサブクラスが作られます。
スクリーンショット

自前の UIView サブクラスを編集

初期ファイルが出来上がったら次に実際このサブクラスを作りましょう。まずは「Hello World!」を表示するラベル mainLabel をクラスの直下に宣言しておきます

MainView.swift
    let mainLabel: UILabel

次に init メソッドを作ります。

MainView.swift
    override init(frame: CGRect) {

        // `mainLabel` を作ります。
        self.mainLabel = UILabel()

        // `mainLabel` の表示テキストを設定します。
        self.mainLabel.text = "Hello World!"

        // `mainLabel` の表示テキストのセンタリングを設定します。
        self.mainLabel.textAlignment = .center

        // `mainLabel` の設定が全て終わりましたので、親クラスを初期化します。
        super.init(frame: frame)

        // `MainView` クラスの背景色を白に設定します。
        self.backgroundColor = .white

        // `MainView` に `mainLabel` を表示します。
        self.addSubview(mainLabel)
    }

init メソッドを独自に作ったり、オーバーライドしたりしたら、必ずエラーで init(coder:) が足りないと言われるので、そのまま Fix 押せば大丈夫です
スクリーンショット 2018-03-04 12.42.29.png

押したら勝手に下記のようなイニシャライザーが挿入されます:

MainView.swift
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

中身がただの fatalError なので、もし Storyboard や XIB などから MainView を使う予定がなければそのままで大丈夫です。

最後に、ラベルがどんな画面であっても必ず真ん中に来てほしいので、レイアウトを調整します。iOS のレイアウトサイクルでは UIView#layoutSubviews() のメソッド内で具体的なレイアウト処理を行いますので、このメソッドをオーバーライドします:

MainView.swift
    override func layoutSubviews() {

        // 必ず `super` の `layoutSubviews()` を呼び出します
        super.layoutSubviews()

        // `mainLabel` がほしいサイズを自分のサイズから `sizeThatFits(_:)` を通して取り出します
        let labelSize = self.mainLabel.sizeThatFits(self.bounds.size)

        // `mainLabel` を真ん中に置くように、原点座標を先ほど取得したサイズと自分のサイズから割り出します
        let x = (self.bounds.width - labelSize.width) / 2
        let y = (self.bounds.height - labelSize.height) / 2
        let labelOrigin = CGPoint(x: x, y: y)

        // `mainLabel` のレイアウトは、`frame` に原点座標とサイズで代入して決めます
        self.mainLabel.frame = CGRect(origin: labelOrigin, size: labelSize)

    }

ViewControllerMainView を入れる

MainView クラスの設定が終わったら、次にいよいよ ViewControllerMainView を入れますが、やり方はとても簡単です。viewDidLoad() メソッドの super.viewDidLoad() の命令の下に2命令を入れるだけです。

ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        // 画面サイズを初期値として `MainView` クラスを `mainView` としてインスタンス化します。
        let mainView = MainView(frame: self.view.bounds)

        // `MainView` に自動サイズ調整用に `autoresizingMask` を設定
        mainView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        // `mainView` オブジェクトを表示します。
        self.view.addSubview(mainView)

    }

ViewController を Root View Controller として設定

さていよいよラストです。勝利がもう目の前に見えます。まずは AppDelegate クラスの直下に window 宣言の直後に ViewController クラスをインスタンス化します。

AppDelegate.swift
    // `ViewController` を遅延生成で宣言します
    private(set) lazy var viewController = ViewController()

最後は application(_:, didFinishLaunchingWithOptions:) メソッドに ViewController を利用するように設定します

AppDelegate.swift
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        // アプリウィンドウを設定します。
        self.window = UIWindow(frame: UIScreen.main.bounds)

        // ウィンドウをヴィジブルにします。
        self.window?.makeKeyAndVisible()

        // ウィンドウの rootViewController を viewController に設定します。
        self.window?.rootViewController = viewController

        return true
    }

プレビュー

ここまで出来たらもう勝利です。あとは実際動かしてみてちゃんと「Hello World!」が画面のど真ん中に表示されていることを確認するだけです。ここでは一応シミュレーターの iPhone 5 と iPad 2 だけ貼り付けますが、まあ理論上どんな解像度でもちゃんとド真ん中に表示してくれます。
iPhone 5
iPad 2

後記

以上を持ちまして、自前でクラスを作って、Storyboard というやらを使わずにソースコードで画面配置することができました。あとは適当に MainView クラスを弄れば思うどおりの動作ができてしまいます。もちろんちょっとソースコードが面倒になりますが、ボタンを4つ作って一列に綺麗に並べるなんてことも余裕で出来てしまいます。更にもう少し複雑になってしまいますがもちろん UITabBarController のサブクラスを自作して iOS ならではの画面遷移を作ることだってできます。なにせソースコード化できなかったらそもそもアプリが作れないから Storyboard ができてソースコードでできないことなんて普通ないはずです。やりたいことによって面倒になるかもしれないだけです。