以編程方式使用 Auto Layout 讓你直覺又簡單地設計 App UI!
Auto Layout ㄧ直是 iOS 必學的技術之一,在 iOS 中你可以選擇使用 Storyboard 設置 Auto Layout,好處是非常直覺,而且多人使用時好懂,就算不大會 Swift / OC 都可以很容易做出想要的版面。最近公司面試需要出題,我也選擇了這個 Layout 題目。
為什麼喜歡用 Code Auto Layout?
對我而言,我比較喜歡 Code Auto Layout 表達,並且也可以常常運用 Code Auto Layout 技巧,產出比較複雜的畫面。這裡我不評斷哪個方式比較好,畢竟最合理還是應該依照專案屬性與工程師的熟悉程度來選擇。
今天要示範的畫面
今天我們來簡單做一個 App Store 的 Auto Layout,描述一下思路及實現方式。最終畫面應該如下:
開啟新專案
首先我們開啟一個新專案,選擇使用 Swift 語言,完成後使用 CMD + R 編譯執行,應該可以看到一個空白畫面。
客製化 TabBarController
接著我們要實現 TabBarController,思路如下:
- 建立檔案
BaseTabBarController
- 建立三個帶有
UINavigationController
的UIViewController
- 加入
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 ] }
現在編譯且運行,你的畫面應該如下:
接下來,我們要建立三個這樣的東西。你可以選擇寫三次:
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 } }
現在,再次運行吧!你應該可以看到以下畫面:
實作 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")), ] }
現在,再次運行:
幹得好,出現了 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 這些元件。先建立垂直的標籤:
let vStackView = UIStackView(arrangedSubviews: [ nameLabel, categoryLabel, ratingsLabel ]) vStackView.axis = .vertical
再建立水平方向的 View
:
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
現在你應該可以看到以下畫面:
設置截圖區域
快速製作三個 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
現在運行!
嗯!很不錯,但我們還是要設置一下邊界,修改一下 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,當然, 這還只是入門,更多 Layout 的趣味等著你去發掘!
如果你需要範例輔助您,請點此專案。
感謝您的閱讀,祝你有個美好的 Coding。