從程式利用 NSLayoutAnchor 設定 auto layout 條件的各種寫法

加入元件和設定 auto layout 的地方

  • view controller
    viewDidLoad
  • table view cell
    init(style: UITableViewCell.CellStyle, reuseIdentifier: String?)
  • 其它的 view,比方 collection view cell 或自訂的 view
    init(frame: CGRect)

設定元件樣式和 auto layout 的各種寫法

  • 宣告 property 時執行 closure 設定元件的樣式,在 viewDidLoad 或 init 設定 auto layout。
  • 宣告 property 時只產生元件,每個元件的樣式和條件設定另外寫成 function,然後在 viewDidLoad 或 init 呼叫設定元件的 function。

接下來我們將分別示範這些寫法的各種範例。

view controller

寫法 1: 宣告 property 時執行 closure 設定元件的樣式,在 viewDidLoad 設定 auto layout

class ViewController: UIViewController {

let diceImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.tintColor = .orange
return imageView
}()

let diceLabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 20)
label.textColor = .orange
label.text = "神奇骰子"
return label
}()

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(diceImageView)
view.addSubview(diceLabel)
configureConstraints()
diceImageView.image = UIImage(systemName: "die.face.\(Int.random(in: 1...6)).fill")

}

func configureConstraints() {
NSLayoutConstraint.activate([
diceImageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
diceImageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
diceImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
diceImageView.widthAnchor.constraint(equalTo: diceImageView.heightAnchor),

diceLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
diceLabel.topAnchor.constraint(equalTo: diceImageView.bottomAnchor, constant: 40)
])
}
}

寫法 2: 宣告 property 時只產生元件,每個元件的樣式和條件設定另外寫成 function,然後在 viewDidLoad 呼叫設定元件的 function

class ViewController: UIViewController {
let diceImageView = UIImageView()
let diceLabel = UILabel()

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(diceImageView)
view.addSubview(diceLabel)
configureDiceImageView()
configureDiceLabel()
}

func configureDiceImageView() {

diceImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
diceImageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
diceImageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
diceImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
diceImageView.widthAnchor.constraint(equalTo: diceImageView.heightAnchor)
])
diceImageView.contentMode = .scaleAspectFit
diceImageView.tintColor = .orange
diceImageView.image = UIImage(systemName: "die.face.\(Int.random(in: 1...6)).fill")
}

func configureDiceLabel() {
diceLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
diceLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
diceLabel.topAnchor.constraint(equalTo: diceImageView.bottomAnchor, constant: 40)
])
diceLabel.font = .systemFont(ofSize: 20)
diceLabel.textColor = .orange
diceLabel.text = "神奇骰子"
}
}

table view cell

寫法 1: 宣告 property 時執行 closure 設定元件的樣式,在 init(style: reuseIdentifier:) 設定 auto layout

table view cell 的定義,stack view 裡的元件可以不設定 translatesAutoresizingMaskIntoConstraints。

class SongTableViewCell: UITableViewCell {

let albumImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 6
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: 100),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor)
])
return imageView
}()

let nameLabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 24)
label.textColor = .systemTeal
return label
}()

let stackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = 10
stackView.distribution = .fill
stackView.alignment = .center
return stackView
}()

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
stackView.addArrangedSubview(albumImageView)
stackView.addArrangedSubview(nameLabel)
addSubview(stackView)

NSLayoutConstraint.activate([

stackView.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 1),
bottomAnchor.constraint(equalToSystemSpacingBelow: stackView.bottomAnchor, multiplier: 1),
stackView.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
trailingAnchor.constraint(equalToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1)
])
}

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

view controller 的定義。

class SongTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(SongTableViewCell.self, forCellReuseIdentifier: "\(SongTableViewCell.self)")
}

// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "\(SongTableViewCell.self)", for: indexPath) as! SongTableViewCell
cell.nameLabel.text = "琴有獨鍾"
cell.albumImageView.image = UIImage(named: "琴有獨鍾")
return cell
}
}

寫法 2: 宣告 property 時只產生元件,每個元件的樣式和條件設定另外寫成 function,然後在 init(style: reuseIdentifier:) 呼叫設定元件的 function

class SongTableViewCell: UITableViewCell {
let albumImageView = UIImageView()
let nameLabel = UILabel()
let stackView = UIStackView()

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
stackView.addArrangedSubview(albumImageView)
stackView.addArrangedSubview(nameLabel)
addSubview(stackView)

configureStackView()
configureAlbumImageView()
configureNameLabel()
}

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

func configureStackView() {
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = 10
stackView.distribution = .fill
stackView.alignment = .center
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 1),
bottomAnchor.constraint(equalToSystemSpacingBelow: stackView.bottomAnchor, multiplier: 1),
stackView.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
trailingAnchor.constraint(equalToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1)
])

}

func configureAlbumImageView() {
albumImageView.contentMode = .scaleAspectFill
albumImageView.clipsToBounds = true
albumImageView.layer.cornerRadius = 6
NSLayoutConstraint.activate([
albumImageView.widthAnchor.constraint(equalToConstant: 100),
albumImageView.heightAnchor.constraint(equalTo: albumImageView.widthAnchor)
])
}

func configureNameLabel() {
nameLabel.font = UIFont.systemFont(ofSize: 24)
nameLabel.textColor = .systemTeal
}
}

其它 custom view 的設定,比方 view & collection view cell

寫法 1: 宣告 property 時執行 closure 設定元件的樣式,在 init(frame:) 設定 auto layout

collection view cell 的定義。

class MovieCollectionViewCell: UICollectionViewCell {

let coverImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 6
NSLayoutConstraint.activate([
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.4)
])
return imageView
}()

let nameLabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 20)
label.textColor = .systemTeal
label.textAlignment = .center
return label
}()

let stackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 5
stackView.distribution = .fill
stackView.alignment = .fill
return stackView
}()

override init(frame: CGRect) {
super.init(frame: frame)
stackView.addArrangedSubview(coverImageView)
stackView.addArrangedSubview(nameLabel)
addSubview(stackView)

NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor)
])
}

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

view controller 的定義。

class MovieCollectionViewController: UICollectionViewController {

override func viewDidLoad() {
super.viewDidLoad()

collectionView.register(MovieCollectionViewCell.self, forCellWithReuseIdentifier: "\(MovieCollectionViewCell.self)")
configureCellSize()
}

func configureCellSize() {
let space: Double = 10
let columnCount: Double = 2
let ratio = 1.4
let textHeight: Double = 30
let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout
let width = floor((collectionView.bounds.width - space * (columnCount - 1 + 2)) / columnCount)
flowLayout?.itemSize = CGSize(width: width, height: width * ratio + textHeight)
flowLayout?.sectionInset = UIEdgeInsets(top: 0, left: space, bottom: 0, right: space)
flowLayout?.estimatedItemSize = .zero
flowLayout?.minimumInteritemSpacing = space
flowLayout?.minimumLineSpacing = space
}

// MARK: UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(MovieCollectionViewCell.self)", for: indexPath) as! MovieCollectionViewCell

cell.coverImageView.image = UIImage(named: "尚氣與十環傳奇")
cell.nameLabel.text = "尚氣與十環傳奇"
return cell
}

}

寫法 2: 宣告 property 時只產生元件,每個元件的樣式和條件設定另外寫成 function,然後在 init(frame:) 呼叫設定元件的 function

class MovieCollectionViewCell: UICollectionViewCell {

let coverImageView = UIImageView()
let nameLabel = UILabel()
let stackView = UIStackView()

override init(frame: CGRect) {
super.init(frame: frame)
stackView.addArrangedSubview(coverImageView)
stackView.addArrangedSubview(nameLabel)
addSubview(stackView)

configureStackView()
configurecoverImageView()
configureNameLabel()
}

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

func configureStackView() {
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 5
stackView.distribution = .fill
stackView.alignment = .fill

NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor)
])
}

func configurecoverImageView() {
coverImageView.contentMode = .scaleAspectFill
coverImageView.clipsToBounds = true
coverImageView.layer.cornerRadius = 6
NSLayoutConstraint.activate([
coverImageView.heightAnchor.constraint(equalTo: coverImageView.widthAnchor, multiplier: 1.4)
])
}

func configureNameLabel() {
nameLabel.font = UIFont.systemFont(ofSize: 20)
nameLabel.textColor = .systemTeal
nameLabel.textAlignment = .center
}
}

定義一次加入多個元件的 addSubviews

設定 auto layout 前我們通常要先呼叫 addSubview 將 view 加到畫面上,因此可以考慮定義一個加入多個 view 的 function addSubviews。

extension UIView {
func addSubviews(_ views: UIView...) {
views.forEach(addSubview(_:))
}
}

之後呼叫 addSubviews 即可傳入多個要加到畫面的元件。

override func viewDidLoad() {
super.viewDidLoad()

view.addSubviews(diceImageView, diceLabel)
configureConstraints()
}

宣告 property 時加上 lazy

宣告 property 執行 closure 設定元件樣式時,我們也可以加上 lazy。以下方程式的 diceImageView 為例,此做法可讓 closure 等到第一次讀取 diceImageView 時才執行,讓 App 的效能更好。

class ViewController: UIViewController {

lazy var diceImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.tintColor = .orange
return imageView
}()

宣告 property 時加上 private

只會在 class 的 { } 裡使用 property,不會在其它地方使用時,也可以在宣告 property 時加上 private。

比方 diceImageView 只會在 ViewController 的 { } 裡使用,因此加上 private。

class ViewController: UIViewController {

private lazy var diceImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.tintColor = .orange
return imageView
}()

參考連結

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com