[Swift] コードでUI作成してみた without storyboard

はじめに

元インフラエンジニアで現在はモバイルアプリ開発者として働いています。
それまでSwiftは独学でやっていたのですが、業務やっている中でコードでUIを作成する際にビギナー向けにまとまっているサイトが少ないなと思ったのでまとめます。

実行環境

この記事は以下の動作環境で動作確認しています。

  • Swift: 5.2
  • xcode: 11.6

目標

  • storyboardを使わない
  • AutoLayoutでレイアウト調整する
  • なるべくハードコードしない
  • なるべく共通化する
  • 以下のような簡単なログイン画面を作成する

初期設定

プロジェクトからstoryboardファイルを削除

[Target]→[General]→[Deployment Info]→[Main Interface]→”main”を削除して空欄にする

起動後に表示される初期画面(LoginViewController)を設定する

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

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

        window = UIWindow(frame: UIScreen.main.bounds)
        let loginVC = LoginViewController()
        window?.rootViewController = UINavigationController(rootViewController: loginVC)
        window?.makeKeyAndVisible()
        return true
    }

デフォルトであれば起動後MainStoryboardが起動するように設定されていますが、指定したい場合は設定を削除し、AppDelegateファイルでどのViewControllerに遷移するか処理を書く必要があります。※ iOS13の場合は、SceneDelegate側の設定もあります。

UIパーツを設定

class LoginViewController: UIViewController {

    private let mailLabel: UILabel = {
        let label = UILabel()
        label.setLabel(title: R.string.localizable.mailAddress())
        return label
    }()

    private let passwordLabel: UILabel = {
        let label = UILabel()
        label.setLabel(title: R.string.localizable.password())
        return label
    }()

    private lazy var mailInputField: UITextField = {
        let textField = UITextField()
        textField.delegate = self
        textField.setTextField(title: R.string.localizable.textInput())
        return textField
    }()

    private lazy var passwordInputField: UITextField = {
        let textField = UITextField()
        textField.delegate = self
        textField.isSecureTextEntry = true
        textField.setTextField(title: R.string.localizable.textInput())
        return textField
    }()

    private lazy var loginButton: UIButton = {
        let button = UIButton()
        button.setButton(title: R.string.localizable.login(), bgColor: .lightGray)
        button.addTarget(self, action: #selector(didLoginButtonTapped), for: .touchUpInside)
        return button
    }()

前提として、可読性や保守性を高めるためviewDidLoadの中の処理を少なくします。
そのため、UIPartsをviewDidLoad()に実装せずにプロパティでまとめています。
クラス外から操作することはないのでprivateで宣言しています。
R.swiftを使って文字列を管理しています。
selectorでボタンをタップした後の動作を指定しています。

UIパーツを画面に表示させる&AutoLayoutでレイアウトを指定する

override func viewDidLoad() {
        super.viewDidLoad()

        setupLayout()
}
private func setupLayout() {
        [mailLabel, passwordLabel, mailInputField, passwordInputField, loginButton]foreach { view.addSubview($0) }

        mailLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: ViewConst.mailLabelLeft).isActive = true
        mailLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConst.mailLabelTop).isActive = true
        mailLabel.widthAnchor.constraint(equalToConstant: ViewConst.mailLabelWidth).isActive = true
        mailLabel.heightAnchor.constraint(equalToConstant: ViewConst.mailLabelHeight).isActive = true

        mailInputField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: ViewConst.textFieldLeft).isActive = true
        mailInputField.topAnchor.constraint(equalTo: mailLabel.topAnchor, constant: ViewConst.textFieldTop).isActive = true
        mailInputField.widthAnchor.constraint(equalToConstant: ViewConst.textFieldWidth).isActive = true
        mailInputField.heightAnchor.constraint(equalToConstant: ViewConst.textFieldHeight).isActive = true

        passwordLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: ViewConst.passLabelLeft).isActive = true
        passwordLabel.topAnchor.constraint(equalTo: mailInputField.topAnchor, constant: ViewConst.passLabelTop).isActive = true
        passwordLabel.widthAnchor.constraint(equalToConstant: ViewConst.passLabelWidth).isActive = true
        passwordLabel.heightAnchor.constraint(equalToConstant: ViewConst.passLabelHeight).isActive = true

        passwordInputField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: ViewConst.textFieldLeft).isActive = true
        passwordInputField.topAnchor.constraint(equalTo: passwordLabel.topAnchor, constant: ViewConst.textFieldTop).isActive = true
        passwordInputField.widthAnchor.constraint(equalToConstant: ViewConst.textFieldWidth).isActive = true
        passwordInputField.heightAnchor.constraint(equalToConstant: ViewConst.textFieldHeight).isActive = true

        loginButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: ViewConst.buttonLeft).isActive = true
        loginButton.topAnchor.constraint(equalTo: passwordInputField.topAnchor, constant: ViewConst.buttonTop).isActive = true
        loginButton.widthAnchor.constraint(equalToConstant: ViewConst.buttonWidth).isActive = true
        loginButton.heightAnchor.constraint(equalToConstant: ViewConst.buttonHeight).isActive = true
}

こちらもviewDidLoad内の処理が長くなって見にくくなるので
関数(setupLayout)として抽出しています。
マジックナンバーはハードコードせずに変数で管理しています。(詳細は以下)
オートレイアウトの設定は直に書くと大変なのでPureLayoutなどのライブラリなどを使用してもよいかと思います。

UIパーツを拡張する

import UIKit

extension UILabel {
    func setLabel(title: String) {
        self.font = UIFont.boldSystemFont(ofSize: 15)
        self.text = title
        self.translatesAutoresizingMaskIntoConstraints = false
    }
}

extension UITextField {
    func setTextField(title: String) {
        self.layer.borderWidth = 1
        self.layer.borderColor = UIColor.black.cgColor
        self.translatesAutoresizingMaskIntoConstraints = false
        self.textAlignment = .center
        self.placeholder = title
    }
}

extension UIButton {
    func setButton(title: String, bgColor: UIColor) {
        self.backgroundColor = bgColor
        self.setTitle(title, for: .normal)
        self.setTitleColor(.black, for: .normal)
        self.translatesAutoresizingMaskIntoConstraints = false
    }
}

UIParts+Extension.swiftなどのように別ファイルを用意します。
色や大きさなどが共通する箇所はこのようにして拡張関数を使ってまとめます。

マジックナンバーは変数で管理する

private struct ViewConst {
        static let mailLabelTop: CGFloat = 150
        static let mailLabelLeft: CGFloat = 40
        static let mailLabelWidth: CGFloat = 150
        static let mailLabelHeight: CGFloat = 50

        static let passLabelTop: CGFloat = 100
        static let passLabelLeft: CGFloat = 40
        static let passLabelWidth: CGFloat = 150
        static let passLabelHeight: CGFloat = 50

        static let textFieldTop: CGFloat = 35
        static let textFieldLeft: CGFloat = 40
        static let textFieldWidth: CGFloat = 300
        static let textFieldHeight: CGFloat = 50

        static let buttonTop: CGFloat = 100
        static let buttonLeft: CGFloat = 40
        static let buttonWidth: CGFloat = 300
        static let buttonHeight: CGFloat = 50
    }

LoginViewController内で変数として管理しています。
宣言する場所は、class LoginViewController: UIViewController {の下あたり。
ファイルを新規に作成してそちらで管理しても良いです。
値が動的に変化する訳ではないのでstaticで宣言しています。

R.swiftを使う

"login" = "Login";
"loadCell" = "LoadCell";
"photoButton" = "PhotoButton";

R.swift(ライブラリ)を導入することでフォントや色などが1ファイルで管理できます。
例えば、ラベルテキストで"login"とハードコードしていた箇所をR.string.localizable.login()のように書けます。
【Swift】R.swiftが優秀すぎるので紹介してみる

ボタンをタップした際の処理

@objc private func didLoginButtonTapped(sender: UIButton) {
  //ボタンをタップした際の処理
}

セレクターなので@objc、またクラス外から操作しないのでprivateで宣言
UI作成が目的なので処理については割愛します。

おわりに

以上、簡単ではありますがまとめてみました。