從程式利用 NSLayoutAnchor 設定 auto layout 條件的各種寫法
Published in
26 min readAug 23, 2021
加入元件和設定 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
}()