iPhone X已經發(fā)布有一段時間了姆打,最近我負責把公司的產品Cisco Spark適配iPhone X良姆。雖然還沒有采購到iPhone X,但是借助模擬器幔戏,勉強把代碼改好玛追,讓Spark適配iPhone X。我們在產品中闲延,除了啟動頁面痊剖,所有的UI都是用代碼來實現的。所以我下面提到的所有內容都是在代碼中如何適配iPhone X垒玲,不涉及Interface Builder陆馁。下面是一些總結,可能無法做到面面俱到合愈,但是相信能解決大部分的iPhone X適配問題叮贩。
什么是Safe Area?
iOS11為了解決iPhone X上異形屏的問題佛析,新引入了安全區(qū)(Safe Area)益老。在代碼中安全區(qū)是通過safeAreaLayoutGuide來表示的。來看看蘋果官方怎么定義safeAreaLayoutGuide寸莫。
這里面有些單詞不好理解捺萌,比如accommodates是什么意思。很容易把人搞混淆膘茎。不過如果我們對照這safeAreaInsets的定義來看桃纯,就能大概搞清楚safe area是什么來。
簡單的總結一下什么是安全區(qū):
1. safeAreaLayoutGuide是UIView的一個屬性披坏,類型是UILayoutGuide慈参。UILayoutGuide有一個layoutFrame的屬性。說明它是一塊方形區(qū)域(CGRect)刮萌。該方形區(qū)域對應的就是Safe Area驮配。那么該方形區(qū)域有多大呢?
2. Safe Area避開了導航欄,狀態(tài)欄壮锻,工具欄以及可能遮擋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. 這種又臭又長的創(chuàng)建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)
對于這種方式創(chuàng)建出來的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編程滨彻。
(第一次發(fā)技術文章藕届,水平有限,如果大家看了有問題亭饵,請指出來休偶。)