Storyboardを出来るだけ使わずに書き始める
昔は普通にこんな感じで既知な話なのですが、時代が進むにつれ各所で何で?
と聞かれる時のための忘備録として書捨てておきます。
Storyboard is 何
人が見てもよくわからないxmlのかたまり。
xibも似たようなものですので、同列に扱っていきます。
Storyboardの利点
- デフォルトで入ってくる
- ぱっと見わかりやすそう
これぐらいですかね。
Storyboardの問題点
- プロパティが分散する
各種プロパティがsotryboardに直接記述されてしまうので、後々の修正が大変になります。
例えば、ラベルの色やフォントを統一して変更したいとなると、コード上でしたら定義しておいた一箇所の変更などで済む話が、Storyboard上に散りばった全てのラベルをGUIでちくちく探り出し変更していくといったことになります。
重労働な上にミスる可能性が非常に高く、また変更差分を見たところで、どこのどのviewの値を変えた話なのかさっぱりわかりません。
androidのように、変数定義が出来れば少しは改善されるかもしれませんが。
まぁ、あの手この手を使えばなんとか出来ないこともないですが、そこまで我を通すのは no thank you です。
- viewが増えてくると何かわからなくなる
GUIなので、WYSIWYGなレベルだとまだよいのですが、状況別やエラー等などisHiddenをon/offしたりなどして貼り付けまくったviewcontrollerなど開いたところで何がなんだかわかないものになってきます。
- 変えていないのにどこか変わる
開いて閉じただけでgitの差分が。
何が変わったかわからない差分をとりあえずdiscardする気分ったら。
- バージョン毎で形式が変わる
Xcodeのバージョンが変わると形式の一部が変わってしまいます。
xibなど、1,2バージョン前ぐらいなら自動変換も効きますが、
それ以上となるともう手作業で移植するしかなくなります。
(もうあんなことはしたくない...)
- 使い方の説明が意外に難しい
GUIとしてviewを並べるまではよいとしても、コードと連結において、AssistantEditorを開き、右クリックで何かを選び、コード上のどこかにドラッグして繋げる、とか文章で書いても使い方を説明するのが難しいです。
VB6時代ぐらいクリックしてそこにクリックされた時のコード書け、なんて簡単だったらよかったのですが。
TouchUpInsideとかTouchUpOutsideとか普通に間違えますから。
アシスタント機能でコードが追加されても、じゃあ削除は?、とかなるとさらに説明が難しくなります。
- 重い/狭い
MacPro買えというわけでもなく、こんなことする割に重いのです。
プロジェクト作成からことはじめまで
Storyboard/xib使わないならどうすれば?、単にコードで書けばよいのです。
謎のxmlをGUIで書くか、まだ見た目でわかるコードで書くか程度の違いです。
最近のXcodeではプロジェクト作成からstoryboardが入ってきますので、
まずはこれを抜いた状態を作ります。
プロジェクト作成
Xcode9.xあたりの話となります。将来はまた変わるでしょう。
まずは、Xcode -> File -> New -> Project 、Single View App でプロジェクトを一つ作ります。
初期の構成はこんな感じでしょうか。
AppDelegate.swift
ViewController.swift
Main.storyboard
LaunchScreen.storyboard
Info.plist
Assets.xcaseets
この時点で、Main.storyboard と LaunchScreen.storyboard が作成されます。
いきなり日和りますが、LaunchScreen.storyboard はそのまま使います。
Launchの設定状況により対応するデバイス解像度の種別が変わり、
storyboardで無い場合、かなり多くのファイルを作らなければならなくなります。
憎いわけではなく楽したいだけなのです。
UIImageViewでも適当に画面の真ん中に追加して起動時の雰囲気を高めておきます。
どうしても憎ければ、Assets.xcassetsに New iOS Launch image を作成してTARGETに刺せばOKです。
Main.storyboard
置いといてもいいですが、決別の意として消します。
これで実行すると、本来 ViewController.swift にある class ViewController が表示されるところが、
1行目からSIGABRTで落ちてくれます。
気分が折れる前にViewControllerを表示するまでの少々のコードを加えます。
まず、TARGETにある、Deployment Info / Main Interface にある Main を空欄にします。
起動時にここあるstoryboardを前提として処理を開始するので、断ち切ります。
これで落ちることはなくなりますが、ViewController が起動しません。
起動時にこれを起動するコードを追加します。
AppDelegate.swift にて
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
return true
}
windowに新しいUIWindowを追加して、rootViewControllerに起動時のViewControllerを追加して、入力受け付けるようにして、というだけです。
これでViewControllerは起動していますが、見た目に透明&真っ黒でわからないので、ViewController.swiftにて背景色に白を追加してみます。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
}
はい、これで起動時においてstoryboardから決別できました。
view in viewcontroller
はい、次はUILabel貼って定番の Hello world ですよね。
storyboardだとGUIで直接ラベルを貼ってわーい、なのですが、コードでいきます。
やっていることは UILabelを作って、viewに貼るだけのことです。
class ViewController: UIViewController {
let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
label.text = "Hello World"
label.textAlignment = .center
view.addSubview(label)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
label.frame = view.bounds
}
}
こんなところですね。
色々なやり口はあるのですが今回は手短に、storyboardよろしくまずさっさとクラス作成時点で let label = UILabel() にてlabelを作ってしまいます。
そして、viewDidLoad() の時点で、labelの各プロパティ設定及びviewへの貼付けを行います。
frameについては、viewDidLayoutSubviews() で行ってしまいます。
勿論、viewDidLoadなどでframeを設定してもよいのですが、デバイスの縦横やsafeAreaのことを考えると、レイアウト更新のところで状況に従って更新した方が素直になります。
レイアウトの話はautolayout云々の話が絡んできますのでまた別途楽なやり方の話で。
extension UIColor {
static let normalText = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
}
extension UIFont {
static let normalText = UIFont.systemFont(ofSize: 14)
}
class ViewController: UIViewController {
let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
label.text = "Hello World"
label.textAlignment = .center
label.textColor = UIColor.normalText
label.font = UIFont.normalText
view.addSubview(label)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
label.frame = view.bounds
}
}
コードになってしまえば、こんな感じなりでフォントや色の統一も共通化したものを用意して使っていくなどやり方は自由です。
storyboardでラベルが何十個となってきたら、変更要求の度に全部GUIでちくちくと考えると辛いです。
ViewController遷移
はい次は、viewcontrollerからviewcontrollerへの遷移、segueとか面倒なので、さっさと行きます。
まずはよくある UINavigationCtonroller + UITableViewControllerの組み合わせ
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UINavigationController(rootViewController: UITableViewController(style: .plain))
window?.makeKeyAndVisible()
return true
}
これだけでナビゲーションバーとテーブルが表示されて雰囲気は出ます。
気分だけでも仕方ないので、テーブル&セルをタップしたら詳細画面ぐらいを出来るだけ短くいきます。
import UIKit
class ListViewController: UITableViewController {
override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
return 100
}
override func tableView(_: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.textLabel?.text = "row \(indexPath.row)"
return cell
}
override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = DetailViewController()
vc.label.text = "row \(indexPath.row)"
navigationController?.pushViewController(vc, animated: true)
}
}
class DetailViewController: UIViewController {
let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
label.textAlignment = .center
view.addSubview(label)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
label.frame = view.bounds
}
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UINavigationController(rootViewController: ListViewController(style: .plain))
window?.makeKeyAndVisible()
return true
}
}
詰め詰めの1ファイルに書いてこんなところですかね。
本来こんな書き方はするべきではないですが、ちょっとした動作確認ぐらいってなら数分なのでいいでしょう。
AppDelegate.swiftを上に差し替えるだけでOKです。
dequeueReusableCellでのcell使い回しも無しのイケイケですが、cell自体の作成が軽ければさしたる問題ではないでしょう。
ポイントは didSelectRowAt で DetailViewController を作成してさっさと pushViewController しているところですね。
segueのどこでどうコールされて何が起こってるかなど考える前に、見たまんまに処理しておしまいです。
ViewController間の遷移、基本手段としては
UINavigationControllerからなら
行き
func pushViewController(_ viewController: UIViewController, animated: Bool)
ex) navigationController?.pushViewController(vc, animated: true)
戻り
ユーザーがBackタップ
もしくは navigationController?.popViewController(animated: Bool)
Modalなら
行き
func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Swift.Void)? = nil)
ex) present(vc, animated: true, completion: nil)
戻り 子ViewControllerにてなんらかのアクションとして
func dismiss(animated flag: Bool, completion: (() -> Swift.Void)? = nil)
ex) dismiss(animated: true, completion: nil)
値の引き渡しは、これらをコールする前にViewControllerに渡せばOKです。
next
楽したいだけなんだけれども、
謎な方面などからあんまり嫌われなさそうならまた続きを書くかも知れません。
iOS13追記
iOS13/Xcode11以降でも基本的に同じように出来ます。
iOS13ではSceneDelegateで複数のUIインスタンスに対応できるようになったのですが、これを必要とするappだけ対応すればよいですし、実際そんなに必要なappはないだろうということで従来通り単一UIでの方法です。
Xcode10以前からのプロジェクトならそのままでよいです。
Xcode11新規プロジェクトでの場合でのみ追記します。
プロジェクト新規作成>UserInterfaceをStoryboardに(SwiftUIは変更が大きいので省略)
ここでSceneDelegate.swiftなりが追加されまして、UIWindowSceneDelegateの実装となります。
AppDelegate.swiftにデフォルトで入っている以下メソッドをコメントアウト(存在チェックされているのかも)
// func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
// }
//
// func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// }
あとは上記と同じように var window: UIWindow? をAppDelegateにもってくればOKです。
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
変化しているのは初期コードだけでなく、info.plistにもありまして、
Application Scene Manifest以下の云々があるとSceneDelegateでの処理をするんだなと判断されるようなので、これを削除します。
ほぼ動くようになればSceneDelegate.swiftは消してもOKです。