Cisco Spark適配iPhone X

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。

safeAreaLayoutGuide

這里面有些單詞不好理解,比如accommodates是什么意思。很容易把人搞混淆。不過如果我們對照這safeAreaInsets的定義來看,就能大概搞清楚safe area是什么來。

safeAreaInsets

簡單的總結一下什么是安全區:

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。


UILayoutGuideProtocol

然后擴展UIView,給它添加一個safeLayoutGuide的屬性,這個屬性是這樣實現的:


UIView extension

在使用的地方,需要調用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編程。

(第一次發技術文章,水平有限,如果大家看了有問題,請指出來。)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容