以編程方式使用 Auto Layout 讓你直覺又簡單地設計 App UI!


本篇原文(標題:如何使用 Code Auto Layout )刊登於作者 Medium,由 yasuoyuhao 所著並授權轉載。

Auto Layout ㄧ直是 iOS 必學的技術之一,在 iOS 中你可以選擇使用 Storyboard 設置 Auto Layout,好處是非常直覺,而且多人使用時好懂,就算不大會 Swift / OC 都可以很容易做出想要的版面。最近公司面試需要出題,我也選擇了這個 Layout 題目。

為什麼喜歡用 Code Auto Layout?

對我而言,我比較喜歡 Code Auto Layout 表達,並且也可以常常運用 Code Auto Layout 技巧,產出比較複雜的畫面。這裡我不評斷哪個方式比較好,畢竟最合理還是應該依照專案屬性與工程師的熟悉程度來選擇。

今天要示範的畫面

今天我們來簡單做一個 App Store 的 Auto Layout,描述一下思路及實現方式。最終畫面應該如下:

code-auto-layout-final-product

開啟新專案

首先我們開啟一個新專案,選擇使用 Swift 語言,完成後使用 CMD + R 編譯執行,應該可以看到一個空白畫面。

客製化 TabBarController

TabBarController

接著我們要實現 TabBarController,思路如下:

  1. 建立檔案 BaseTabBarController
  2. 建立三個帶有 UINavigationControllerUIViewController
  3. 加入 TabBarController

建立檔案 BaseTabBarController

我們先建立一個名叫 BaseTabBarController 的檔案,並繼承 UITabBarController 實作:

class BaseTabBarController: UITabBarController {

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

現在你的 BaseTabBarController 應該像這樣。

建立三個帶有 UINavigationController 的 UIViewController

我們先試著建立一個:

override func viewDidLoad() {
        super.viewDidLoad()

        // 建立一個 ViewController
        let vc = UIViewController()

        // 建立一個 UINavigationController,並帶著上面的 UIViewController
        let navController = UINavigationController(rootViewController: vc)

        // 設置大標題
        navController.navigationBar.prefersLargeTitles = true

        // 設置標題
        vc.navigationItem.title = "搜索"

        // 設置 ViewController 背景白色
        vc.view.backgroundColor = .white

        // 設置 tabBarItem title
        navController.tabBarItem.title = "搜索"

        // 設置 tabBarItem image
        navController.tabBarItem.image = #imageLiteral(resourceName: "search"))

        // 與 TabBarController 連結
        viewControllers = [
            navController
        ]
    }

現在編譯且運行,你的畫面應該如下:

UIViewController

接下來,我們要建立三個這樣的東西。你可以選擇寫三次:

viewControllers = [
    navController1
    navController2
    navController3
]

//....

或者寫出一個方法來產生:

fileprivate func createNavController(viewController: UIViewController, title: String, image: UIImage) -> UIViewController {
        let navController = UINavigationController(rootViewController: viewController)
        navController.navigationBar.prefersLargeTitles = true
        viewController.navigationItem.title = title
        viewController.view.backgroundColor = .white
        navController.tabBarItem.title = title
        navController.tabBarItem.image = image
        return navController

    }

並在 viewDidLoad 產生並綁定:

override func viewDidLoad() {
        super.viewDidLoad()

        viewControllers = [
            createNavController(viewController: UIViewController(), title: "搜索", image: #imageLiteral(resourceName: "search")),
            createNavController(viewController: UIViewController(), title: "今日", image: #imageLiteral(resourceName: "today_icon")),
            createNavController(viewController: UIViewController(), title: "應用", image: #imageLiteral(resourceName: "apps")),
        ]
    }

最後的 BaseTabBarController 應該如下:

import UIKit

class BaseTabBarController: UITabBarController {

    override func viewDidLoad() {
        super.viewDidLoad()

        viewControllers = [
            createNavController(viewController: UIViewController(), title: "搜索", image: #imageLiteral(resourceName: "search")),
            createNavController(viewController: UIViewController(), title: "今日", image: #imageLiteral(resourceName: "today_icon")),
            createNavController(viewController: UIViewController(), title: "應用", image: #imageLiteral(resourceName: "apps")),
        ]
    }

    fileprivate func createNavController(viewController: UIViewController, title: String, image: UIImage) -> UIViewController {
        let navController = UINavigationController(rootViewController: viewController)
        navController.navigationBar.prefersLargeTitles = true
        viewController.navigationItem.title = title
        viewController.view.backgroundColor = .white
        navController.tabBarItem.title = title
        navController.tabBarItem.image = image
        return navController

    }
}

現在,再次運行吧!你應該可以看到以下畫面:

BaseTabBarController

實作 AppsSearchController

創建檔案 AppsSearchController,並且繼承 UICollectionViewController, UICollectionViewDelegateFlowLayout

接著,我們做一些初始化:

class AppsSearchController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

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

    init() {
        super.init(collectionViewLayout: UICollectionViewFlowLayout())
    }

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

}

CollectionView 產生 Cell View:

import UIKit

class AppsSearchController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    override func viewDidLoad() {
        super.viewDidLoad()

        // 設置 collectionView 白色背景
        collectionView.backgroundColor = .white

        // 註冊 collectionView cell
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cellId")
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        // 設置 cell 長寬,寬=整個螢幕寬,高=350
        return .init(width: view.frame.width, height: 350)
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // 產生 5 個 cell
        return 5
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath)
        // 對每個 cell 設置不同背景,讓我們分辨得出來
        cell.backgroundColor = .init(red: 155/255, green: 233/255, blue: 29/255, alpha: CGFloat(0.2 * Double(indexPath.item)))
        return cell
    }

    init() {
        super.init(collectionViewLayout: UICollectionViewFlowLayout())
    }

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

}

別忘了回到 BaseTabBarController 填入我們自定義的 AppsSearchController

override func viewDidLoad() {
        super.viewDidLoad()

        viewControllers = [
            // 將 UIViewController() -> 置換成 AppsSearchController()
            createNavController(viewController: AppsSearchController(), title: "搜索", image: #imageLiteral(resourceName: "search")),
            createNavController(viewController: UIViewController(), title: "今日", image: #imageLiteral(resourceName: "today_icon")),
            createNavController(viewController: UIViewController(), title: "應用", image: #imageLiteral(resourceName: "apps")),
        ]
    }

現在,再次運行:

AppSearchController

幹得好,出現了 5 個不同背景的 cell!

實作 SearchResultCellView 自定義 cell use code auto layout

創建檔案 SearchResultCellView,並繼承 UICollectionViewCell

class SearchResultCellView: UICollectionViewCell {

}

加入自定義的 identifier:

class SearchResultCellView: UICollectionViewCell {
    static let identifier = "SearchResultCellId"

    override var reuseIdentifier: String? {
        return SearchResultCellView.identifier
    }
}

覆寫 init

override init(frame: CGRect) {
        super.init(frame: frame)
}

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

別忘了回到 AppsSearchController 註冊我們的 Cell

override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.backgroundColor = .white

        collectionView.register(SearchResultCellView.self, forCellWithReuseIdentifier: SearchResultCellView.identifier)
    }

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SearchResultCellView.identifier, for: indexPath)
        return cell
    }

建立元件:

    // 用於 appicon
    let appIconImageView: UIImageView = {
        let iv = UIImageView()
        iv.backgroundColor = .red

        // 約束寬度=64
        iv.widthAnchor.constraint(equalToConstant: 64).isActive = true
        // 約束高度=64
        iv.heightAnchor.constraint(equalToConstant: 64).isActive = true

        // 約束圓角=12
        iv.layer.cornerRadius = 12
        return iv
    }()

    // 用於應用名稱
    let nameLabel: UILabel = {
        let label = UILabel()
        label.text = "應用名稱"
        return label
    }()

    // 用於應用種類
    let categoryLabel: UILabel = {
        let label = UILabel()
        label.text = "生產力工具"
        return label
    }()

    // 用於應用大小
    let ratingsLabel: UILabel = {
        let label = UILabel()
        label.text = "54.87M"
        return label
    }()

    // 用於應用取得按鈕
    let getButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("取得", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.titleLabel?.font = .boldSystemFont(ofSize: 14)
        button.backgroundColor = UIColor(white: 0.95, alpha: 1)

        // 約束寬度=80
        button.widthAnchor.constraint(equalToConstant: 80).isActive = true
        // 約束高度=32
        button.heightAnchor.constraint(equalToConstant: 32).isActive = true
        // 圓角=16
        button.layer.cornerRadius = 16
        return button
    }()

接著,我們在 init layout 這些元件。先建立垂直的標籤:

vStackView

let vStackView = UIStackView(arrangedSubviews: [
            nameLabel, categoryLabel, ratingsLabel
    ])

vStackView.axis = .vertical

再建立水平方向的 View

infoTopStackView

let infoTopStackView = UIStackView(arrangedSubviews: [
            appIconImageView,
            vStackView,
            getButton
            ])
        infoTopStackView.spacing = 12
        infoTopStackView.alignment = .center

使用 Auto Layout

// 加入 Subview
addSubview(infoTopStackView)

// 設置 layout
        infoTopStackView.translatesAutoresizingMaskIntoConstraints = false

// infoTopStackView topAnchor,對齊 cell topAnchor,並啟動約束
        infoTopStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true

// infoTopStackView leadingAnchor,對齊 cell leadingAnchor,並啟動約束
        infoTopStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true

// infoTopStackView bottomAnchor,對齊 cell bottomAnchor,並啟動約束
        infoTopStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true

// infoTopStackView trailingAnchor,對齊 cell trailingAnchor,並啟動約束
        infoTopStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true

現在你應該可以看到以下畫面:

code-auto-layout-1

設置截圖區域

快速製作三個 ImageView

lazy var screenshot1ImageView = self.createScreenshotImageView()
lazy var screenshot2ImageView = self.createScreenshotImageView()
lazy var screenshot3ImageView = self.createScreenshotImageView()

func createScreenshotImageView() -> UIImageView {
    let imageView = UIImageView()
    imageView.backgroundColor = .blue
    return imageView
}

建立截圖 StackView

let screenshotsStackView = UIStackView(arrangedSubviews: [screenshot1ImageView, screenshot2ImageView, screenshot3ImageView])
    screenshotsStackView.spacing = 12
    screenshotsStackView.distribution = .fillEqually

疊加 infoTopStackView:

let overallStackView = UIStackView(arrangedSubviews: [
            infoTopStackView, screenshotsStackView])

overallStackView.axis = .vertical
overallStackView.spacing = 16

移除 infoTopStackView 的 layout,加入 overallStackView 的 layout:

addSubview(overallStackView)
        overallStackView.translatesAutoresizingMaskIntoConstraints = false
        overallStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        overallStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        overallStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        overallStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true

現在運行!

code-auto-layout-2

嗯!很不錯,但我們還是要設置一下邊界,修改一下 overallStackView 的 layout:

addSubview(overallStackView)
        overallStackView.translatesAutoresizingMaskIntoConstraints = false
        overallStackView.topAnchor.constraint(equalTo: topAnchor, constant: 16).isActive = true
        overallStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16).isActive = true
        overallStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 16).isActive = true
        overallStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16).isActive = true

constant: 16 代表我們修正了邊界,注意最後的 trailingAnchor-16

code-auto-layout-3

總結

至此,我們完成了 Code Auto Layout,當然, 這還只是入門,更多 Layout 的趣味等著你去發掘!

如果你需要範例輔助您,請點此專案

感謝您的閱讀,祝你有個美好的 Coding。

本篇原文(標題:如何使用 Code Auto Layout )刊登於作者 Medium,由 yasuoyuhao 所著並授權轉載。
yasuoyuhao,自認為終身學習者,對多領域都有濃厚興趣,喜歡探討各種事物。目前專職軟體開發,系統架構設計,企業解決方案。最喜歡 iOS with Swift。
作者的話:yasuoyuhao 2019/04/29
如果喜歡我的文章,可以按下喜歡或追隨讓我知道呦,更歡迎許多大神指點討論。感謝您的閱讀。
部落格:yasuoyuhao’s Area

此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。

blog comments powered by Disqus
Shares
Share This