Understanding iOS Layout

layoutSubviews()

This UIView method handles repositioning and resizing a view and all its subviews.

When is layoutSubviews called?

  • init 並不會觸發 layoutSubviews
  • addSubview會觸發 layoutSubviews
  • 修改 view 的 frame 會觸發 layoutSubviews
  • 滾動 UIScrollView會觸發 layoutSubviews
  • 旋轉 UISreen 時會觸發父 UIView 上的 layoutSubviews
  • 改變 UIView 大小的時候也會觸發父UIView上的 layoutSubviews

💡 Notice

  • 這個方法非常耗費資源,因為他需要不斷調用 View 和 subView 的對應方法
  • 如果想要強制更新 layout,不要直接調用這個方法(iOS官方文這樣建議)可以手動呼叫setNeedsLayoutlayoutIfNeeded

😎 Issue

If I want to create a cell with rounded corners?

🙅🏻‍♀️ 錯誤:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == 0 && indexPath.row == (tableView.numberOfRows(inSection: indexPath.section) - 1) {
cell.roundCorners(corners: .allCorners, radius: 16)
}else if indexPath.row == 0 {
cell.roundCorners(corners: [.topLeft,.topRight], radius: 16)
}else if indexPath.row == (tableView.numberOfRows(inSection: indexPath.section) - 1) {
cell.roundCorners(corners: [.bottomLeft, .bottomRight], radius: 16)
}
}

🔑 cell 裡的資料會增增減減,如果寫在 tableView(_:willDisplay:forRowAt:) 裡,cell 只會照著 dataSource 給的資料直進行操作,寫在這裡會造成判斷錯誤,哪個 indexPath.row 是需要圓角

🙅🏻‍♀️ 正確:

override func layoutSubviews() {
guard let cellIndexPath = cellIndexPath else {
return
}
if cellIndexPath.row == 0 && cellIndexPath.row == last {
self.roundCorners(corners: .allCorners, radius: 16)
}else if cellIndexPath.row == 0 {
self.roundCorners(corners: [.topLeft,.topRight], radius: 16)
}else if cellIndexPath.row == last {
self.roundCorners(corners: [.bottomLeft, .bottomRight], radius: 16)
}else {
self.layer.mask = nil
}
}

手動通知更新layout

setNeedsLayout()

  • 觸發 layoutSubViews 的消耗最小的方法
  • 不會立刻更新 View 而是等待下一個 Update recycle

layoutIfNeeded()

  • 不會等待下一個 Update recycle,而是直接調用 layoutSubviews 去立即更新 Layout

😎 Demo

驗證方式:跳出側邊欄動畫

🙅🏻‍♀️ 沒有調用 layoutIfNeeded

UIView.animate(withDuration: 0.3) {
self.infoView.snp.updateConstraints { make in
make.leading.equalToSuperview().offset(150)
}
}

Video: https://s3.us-west-2.amazonaws.com/secure.notion-static.com/15fab51e-8694-4d7f-827b-784e9ca78574/Simulator_Screen_Recording_-_iPhone_SE_%282nd_generation%29_-_2022-04-22_at_17.15.42.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220811%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220811T072552Z&X-Amz-Expires=86400&X-Amz-Signature=8edbef4f79724dbaffee8df40a56a473addb7d272059d470928195b99b6e214e&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Simulator%2520Screen%2520Recording%2520-%2520iPhone%2520SE%2520%282nd%2520generation%29%2520-%25202022-04-22%2520at%252017.15.42.mp4%22&x-id=GetObject

🙆🏻‍♀️ 有調用 layoutIfNeeded

UIView.animate(withDuration: 0.3) {
self.infoView.snp.updateConstraints { make in
make.leading.equalToSuperview().offset(150)
}
**self.tabBarController?.view.layoutIfNeeded()**
}

Video: https://s3.us-west-2.amazonaws.com/secure.notion-static.com/c6a640db-b7eb-4ba8-9f28-bcb22d0d9e83/Simulator_Screen_Recording_-_iPhone_SE_%282nd_generation%29_-_2022-04-22_at_17.14.23.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220811%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220811T072730Z&X-Amz-Expires=86400&X-Amz-Signature=5080b118e8260a1432d6a93cd53c55baa16d2d5a3b07a79690449bb1972ad11c&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Simulator%2520Screen%2520Recording%2520-%2520iPhone%2520SE%2520%282nd%2520generation%29%2520-%25202022-04-22%2520at%252017.14.23.mp4%22&x-id=GetObject

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Lillyy

Lillyy

Lillyy is a software engineer. She is a passionate iOS developer with a curious mind and positive attitude. Instagram: @ Jjialling