iPhone X已經發布有一段時間了,最近我負責把公司的產品Cisco Spark適配iPhone X。雖然還沒有采購到iPhone X,但是借助模擬器,勉強把代碼改好,讓Spark適配iPhone X。我們在產品中,除了啟動頁面,所有的UI都是用代碼來實現的。所以我下面提到的所有內容都是在代碼中如何適配iPhone X,不涉及Interface Builder。下面是一些總結,可能無法做到面面俱到,但是相信能解決大部分的iPhone X適配問題。
什么是Safe Area?
iOS11為了解決iPhone X上異形屏的問題,新引入了安全區(Safe Area)。在代碼中安全區是通過safeAreaLayoutGuide來表示的。來看看蘋果官方怎么定義safeAreaLayoutGuide。
這里面有些單詞不好理解,比如accommodates是什么意思。很容易把人搞混淆。不過如果我們對照這safeAreaInsets的定義來看,就能大概搞清楚safe area是什么來。
簡單的總結一下什么是安全區:
1. safeAreaLayoutGuide是UIView的一個屬性,類型是UILayoutGuide。UILayoutGuide有一個layoutFrame的屬性。說明它是一塊方形區域(CGRect)。該方形區域對應的就是Safe Area。那么該方形區域有多大呢?
2. Safe Area避開了導航欄,狀態欄,工具欄以及可能遮擋View顯示的父View。
????2.1. 對于一個控制器的root view,safe area等于該view的Frame減去各種bar以及additionalSafeAreaInsets剩余的部分。
????2.2. 對于視圖層級上除此之外的其他view,safe area對應沒有被其他內容(各種bar,additionalSafeAreaInsets)遮擋的部分。比如,如果一個view完全在superview的safe area中,這個view的safe area就是view本身。
3. 根據上面一條,我們在編碼的時候,只要針對safe area編程,就不會被各種bar遮擋。
4. 是不是所有view都需要針對safe area編程。其實也不是的,只要控制器View第一層級的subviews是針對safe area編程就可以了。后面層級的subview無需針對safe area編程。因為這種情況下,safe area的大小就是view frame的大小。
5. 如果view不顯示或者不在顯示層級中,該view的safe area的大小就等于view frame大小。
6. safeAreaLayoutGuide = view.frame - safeAreaInsets。 我們可以通過調整additionalSafeAreaInsets來控制safe area的大小。
如何適配iPhone X
現在已經搞清楚了safe area。那么我們在代碼中需要做哪些修改來適配iPhone X呢?我們所有的UI都是用代碼來實現的,并且基本上都是通過constraint來實現Auto layout。基本用到3種添加約束的方式。
1. 用的最多的就是VFL(visual format language), 這種方式一行代碼可以寫出多個約束,所有用的最多。
?customConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-8-[redView]-|", options: [], metrics: nil, views: views))
2. Layout Anchor。這種方式用的也不少。我們主要用來約束view相對其他view的X,Y中心位置.
customConstraints.append(bindToLyraSpaceButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)) ? ? ? ? customConstraints.append(bindRoomNameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor))
3. 這種又臭又長的創建NSLayoutConstraint方式,基本上用的比較少。
customConstraints.append(NSLayoutConstraint(item: redView, attribute: .top, relatedBy: .equal, toItem: view attribute: .top, multiplier: 1, constant: 0)
下面來逐個分析這幾種添加越蘇的方式需要怎么修改。
VFL的適配
比如,一個全屏的view(上下左右都不留margin的那種),我們一般用這種方式來寫:
let views: [String: AnyObject] = ["redView" : redView] ??
customConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|[redView]|", options: [], metrics: nil, views: views))
customConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|[redView]|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activate(customConstraints)
對于這種方式創建出來的view,很明顯,在iPhone X上是會被劉海遮擋住的。因為VFH添加的約束是針對View而不是上面提到的safe area。這種代碼改起來稍微有點痛苦,比如以上的代碼需要改成這樣,來針對safe area添加約束。? ? ? ? ? ? ?
let safeArea = view.safeAreaLayoutGuide? ??
redView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true ? ? ? ? redView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true ? ? ? ? redView.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true ? ? ? ? redView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor).isActive = true
還有一種情況,全屏的view上下左右留有一定的margin。我們一般這么寫:
let views: [String: AnyObject] = ["redView" : redView] ??
customConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-16-[redView]-16-|", options: [], metrics: nil, views: views))
customConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-16-[redView]-16-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activate(customConstraints)
這種情況跟上面的改法一樣,只需要指定一下constant值就行了
let safeArea = view.safeAreaLayoutGuide ?? ? ? ? ? ? ? ?
redView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 16).isActive = true? ? ?
redView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: 16).isActive = true ? ? ? ?
redView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 16).isActive = true ? ? ? ?
redView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: 16).isActive = true
如果恰好走狗屎運,你的產品設計的margin值等于系統的默認margin 8,你一般會這么寫。反正我們代碼里基本margin都是16。這種情況,你無需修改任何代碼就可以適配iPhone X。
let views: [String: AnyObject] = ["redView" : redView] ??
customConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-[redView]-|", options: [], metrics: nil, views: views))
customConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[redView]-|", options: [], metrics: nil, views: views))
NSLayoutConstraint.activate(customConstraints)
因為這種情況,你沒有修改系統默認的margins,系統會自動給你添加layoutMargins。而layoutMargins已經默認對safe area處理過了。以上代碼跟下面的代碼是一樣的:
let margin = view.layoutMarginsGuide ? ? ? ?
view.layoutMargins = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)? ? ? ? ? ? ? ?
redView.leadingAnchor.constraint(equalTo: margin.leadingAnchor).isActive = true ? ? ? ?
redView.trailingAnchor.constraint(equalTo: margin.trailingAnchor).isActive = true ? ? ? ?
redView.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true ? ? ? ?
redView.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
因為layoutMargins已經針對safe area處理過了,所以我們其實可以直接跳過safe area,針對layoutMarginGuide編程來適配iPhone X。比如,如果你的margins是(16,16,16,16),你只需要修改layoutMargins這個屬性,其余的代碼還跟原來一樣用VFL:
let views: [String: AnyObject] = ["redView" : redView] ?? ? ? ? ? ? ? ? customConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-[redView]-|", options: [], metrics: nil, views: views)) ? ? ? ?
customConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[redView]-|", options: [], metrics: nil, views: views)) ?? ? ? ? ? ? ? ?
view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) ? ? ? ? NSLayoutConstraint.activate(customConstraints)
所以對于VFL,有兩種辦法來修改你的代碼去適配iPhone X。一種是直接針對safe area編程,一種是直接跳過safe area針對layoutMarginsGuide編程。使用前一種,需要寫if/else來處理不同的iOS版本,這一點后面會提到。使用后一種,可能代碼看起來會有點雜亂。我們用的是前一種。
Layout Anchor的適配
layout Anchor的適配代碼比較簡單,只需要把view.xxxAnchor改成view.safeAreaLayoutGuide就可以了。
customConstraints.append(bindToLyraSpaceButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)) ? ? ? ? customConstraints.append(bindRoomNameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor))
// 改成這樣:view ------> view..safeAreaLayoutGuide
customConstraints.append(bindToLyraSpaceButton.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor)) ? ? ? ? customConstraints.append(bindRoomNameLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor))
NSLayoutConstraint方式的適配
這種方式的適配跟上一種一樣,也是把view改成safeAreaLayoutGuide就可以了
customConstraints.append(NSLayoutConstraint(item: redView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0)) ? ? ? ? customConstraints.append(NSLayoutConstraint(item: redView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0)) ? ? ? ? customConstraints.append(NSLayoutConstraint(item: redView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0)) ? ? ? ? customConstraints.append(NSLayoutConstraint(item: redView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0))
// 改成這樣:view ------> view..safeAreaLayoutGuide
customConstraints.append(NSLayoutConstraint(item: redView, attribute: .top, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .top, multiplier: 1, constant: 0)) ? ? ? ? customConstraints.append(NSLayoutConstraint(item: redView, attribute: .bottom, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .bottom, multiplier: 1, constant: 0)) ? ? ? ? customConstraints.append(NSLayoutConstraint(item: redView, attribute: .leading, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .leading, multiplier: 1, constant: 0)) ? ? ? ? customConstraints.append(NSLayoutConstraint(item: redView, attribute: .trailing, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .trailing, multiplier: 1, constant: 0))
兼容iOS 10 和 iOS 11
safeAreaLayoutGuide和safeAreaInsets這兩個屬性都是iOS 11新加入的。所以如果你的App需要兼容iOS 9和iOS 10,要寫很多這樣的判斷代碼
if #available(iOS 11.0, *) { ? ? ? ? ? ? return safeAreaLayoutGuide ? ? ? ? }
為了避免在代碼中到處寫這種判斷代碼,我們對對UIView進行了擴展。具體方式如下:
首先定義一個UILayoutGuideProtocol,讓UIView和UILayoutGuide都實現這個protocol。
然后擴展UIView,給它添加一個safeLayoutGuide的屬性,這個屬性是這樣實現的:
在使用的地方,需要調用view.safeAreaLayoutGuide的地方,改為調用view.safeLayoutGuide。這樣就不需要寫if/else了。
UITableView和UICollectionView
UITableView比較特殊,系統已經幫你出里過safe area。切記,在你的UITableViewCell中,所以的view都要添加到contentView。否則你的內容會被遮蓋。
override init(style: UITableViewCellStyle, reuseIdentifier: String?) { ? ? ? ?
????super.init(style: style, reuseIdentifier: reuseIdentifier) ?? ? ? ? ? ? ? ?
????contentView.addSubview(label)
????//addSubview(label)? ?//這種方式是有問題的
}
很不幸,UICollectionView不是自適應的,系統沒有幫你處理iPhone X。即使你所有的view都添加到contenView中,你的內容依然會被遮蓋。所有,對于CollectionView, 你必須像處理Label,button那樣,用前面提到的的方式針對safe area編程。
(第一次發技術文章,水平有限,如果大家看了有問題,請指出來。)