UIView 有一個(gè)名叫 layer ,類型為 CALayer 的對(duì)象屬性,它們的行為很相似,主要區(qū)別在于:CALayer 繼承自 NSObject ,不能夠響應(yīng)事件。
這是因?yàn)?UIView 除了負(fù)責(zé)響應(yīng)事件 ( 繼承自 UIReponder ) 外,它還是一個(gè)對(duì) CALayer 的底層封裝。可以說,它們的相似行為都依賴于 CALayer 的實(shí)現(xiàn),UIView 只不過是封裝了它的高級(jí)接口而已。
那 CALayer 是什么呢?
CALayer(圖層)
<u>文檔對(duì)它定義是:管理基于圖像內(nèi)容的對(duì)象,允許您對(duì)該內(nèi)容執(zhí)行動(dòng)畫。</u>
概念
<u>圖層通常用于為 view 提供后備存儲(chǔ),但也可以在沒有 view 的情況下使用以顯示內(nèi)容。圖層的主要工作是管理您提供的可視內(nèi)容,但圖層本身可以設(shè)置可視屬性(例如背景顏色、邊框和陰影)。除了管理可視內(nèi)容外,該圖層還維護(hù)有關(guān)內(nèi)容幾何的信息(例如位置、大小和變換),用于在屏幕上顯示該內(nèi)容。</u>
和 UIView 之間的關(guān)系
示例1 - CALayer 影響 UIVIew 的變化:
let view = UIView(frame: CGRect(x: 44, y: 44, width: UIScreen.width - 88, height: 300))
view.backgroundColor = .red
view.layer.backgroundColor = UIColor.orange.cgColor
print("view: \(view.backgroundColor!)")
print("layer: \(view.layer.backgroundColor!)")
// Prints "view: 1 0.5 0 1"
// Prints "layer: 1 0.5 0 1"
view.layer.frame.origin.y = 100
print("view: \(view.frame.origin.y)")
print("layer: \(view.layer.frame.origin.y)")
// Prints "view: 100"
// Prints "layer: 100"
可以看到,無論是修改了 layer 的可視內(nèi)容或是幾何信息,view 都會(huì)跟著變化,反之也是如此。這就證明:UIView 依賴于 CALayer 得以顯示。
既然他們的行為如此相似,為什么不直接用一個(gè) UIView 或 CALayer 處理所有事件呢?主要是基于兩點(diǎn)考慮:
職責(zé)不同
UIVIew的主要職責(zé)是負(fù)責(zé)接收并響應(yīng)事件;而CALayer的主要職責(zé)是負(fù)責(zé)顯示 UI。需要復(fù)用
在 macOS 和 App 系統(tǒng)上,NSView和UIView雖然行為相似,在實(shí)現(xiàn)上卻有著顯著的區(qū)別,卻又都依賴于CALayer。在這種情況下,只能封裝一個(gè)CALayer出來。
CALayerDelegate
你可以使用 delegate (CALayerDelegate) 對(duì)象來提供圖層的內(nèi)容,處理任何子圖層的布局,并提供自定義操作以響應(yīng)與圖層相關(guān)的更改。如果圖層是由 UIView 創(chuàng)建的,則該 UIView 對(duì)象通常會(huì)自動(dòng)指定為圖層的委托。
注意:
- 在 iOS 中,如果圖層與
UIView對(duì)象關(guān)聯(lián),則必須將此屬性設(shè)置為擁有該圖層的UIView對(duì)象。delegate只是另一種為圖層提供處理內(nèi)容的方式,并不是唯一的。UIView的顯示跟它圖層委托沒有太大關(guān)系。
-
func display(_ layer: CALayer)
當(dāng)圖層標(biāo)記其內(nèi)容為需要更新 (
setNeedsDisplay()) 時(shí),調(diào)用此方法。例如,為圖層設(shè)置contents屬性:let delegate = LayerDelegate() lazy var sublayer: CALayer = { let layer = CALayer() layer.delegate = self.delegate return layer }() // 調(diào)用 `sublayer.setNeedsDisplay()` 時(shí),會(huì)調(diào)用 `sublayer.display(_:)`。 class LayerDelegate: NSObject, CALayerDelegate { func display(_ layer: CALayer) { layer.contents = UIImage(named: "rabbit.png")?.cgImage } }那什么是
contents呢?contents被定義為是一個(gè)Any類型,但實(shí)際上它只作用于CGImage。造成這種奇怪的原因是,在 macOS 系統(tǒng)上,它能接受CGImage和NSImage兩種類型的對(duì)象。你可以把它想象中
UIImageView中的image屬性,實(shí)際上是,UIImageView在內(nèi)部通過轉(zhuǎn)換,將image.cgImage賦值給了contents。注意:
如果是 view 的圖層,應(yīng)避免直接設(shè)置此屬性的內(nèi)容。視圖和圖層之間的相互作用通常會(huì)導(dǎo)致視圖在后續(xù)更新期間替換此屬性的內(nèi)容。
-
func draw(_ layer: CALayer, in ctx: CGContext)
和
display(_:)一樣,但是可以使用圖層的CGContext來實(shí)現(xiàn)顯示的過程(官方示例):// sublayer.setNeedsDisplay() class LayerDelegate: NSObject, CALayerDelegate { func draw(_ layer: CALayer, in ctx: CGContext) { ctx.addEllipse(in: ctx.boundingBoxOfClipPath) ctx.strokePath() } }-
和 view 中
draw(_ rect: CGRect)的關(guān)系文檔對(duì)其的解釋大概是:
<u>此方法默認(rèn)不執(zhí)行任何操作。使用 Core Graphics 和 UIKit 等技術(shù)繪制視圖內(nèi)容的子類應(yīng)重寫此方法,并在其中實(shí)現(xiàn)其繪圖代碼。 如果視圖以其他方式設(shè)置其內(nèi)容,則無需覆蓋此方法。 例如,如果視圖僅顯示背景顏色,或是使用基礎(chǔ)圖層對(duì)象直接設(shè)置其內(nèi)容等。</u>
<u>調(diào)用此方法時(shí),在調(diào)用此方法的時(shí)候,UIKit 已經(jīng)配置好了繪圖環(huán)境。具體來說,UIKit 創(chuàng)建并配置用于繪制的圖形上下文,并調(diào)整該上下文的變換,使其原點(diǎn)與視圖邊界矩形的原點(diǎn)匹配??梢允褂?
UIGraphicsGetCurrentContext()函數(shù)獲取對(duì)圖形上下文的引用(非強(qiáng)引用)。</u>那它是如何創(chuàng)建并配置繪圖環(huán)境的?我在調(diào)查它們的關(guān)系時(shí)發(fā)現(xiàn):
/// 注:此方法默認(rèn)不執(zhí)行任何操作,調(diào)用 super.draw(_:) 與否并無影響。 override func draw(_ rect: CGRect) { print(#function) } override func draw(_ layer: CALayer, in ctx: CGContext) { print(#function) } // Prints "draw(_:in:)"這種情況下,只輸出圖層的委托方法,而屏幕上沒有任何 view 的畫面顯示。而如果調(diào)用圖層的
super.draw(_:in:)方法:/// 注:此方法默認(rèn)不執(zhí)行任何操作,調(diào)用 super.draw(_:) 與否并無影響。 override func draw(_ rect: CGRect) { print(#function) } override func draw(_ layer: CALayer, in ctx: CGContext) { print(#function) super.draw(layer, in: ctx) } // Prints "draw(_:in:)" // Prints "draw"屏幕上有 view 的畫面顯示,為什么呢?首先我們要知道,在調(diào)用 view 的
draw(_:in:)時(shí),它需要一個(gè)載體/畫板/圖形上下文 (UIGraphicsGetCurrentContext) 來進(jìn)行繪制操作。所以我猜測是,這個(gè)UIGraphicsGetCurrentContext是在圖層的super.draw(_:in:)方法里面創(chuàng)建和配置的。具體的調(diào)用順序是:
- 首先調(diào)用圖層的
draw(_:in:)方法; - 隨后在
super.draw(_:in:)方法里面創(chuàng)建并配置好繪圖環(huán)境; - 通過圖層的
super.draw(_:in:)調(diào)用 view 的draw(_:)方法。
此外,還有另一種情況是:
override func draw(_ layer: CALayer, in ctx: CGContext) { print(#function) }只實(shí)現(xiàn)一個(gè)圖層的
draw(_:in:)方法,并且沒有繼續(xù)調(diào)用它的super.draw(_:in:)來創(chuàng)建繪圖環(huán)境。那在沒有繪圖環(huán)境的時(shí)候,view 能顯示嗎?答案是可以的!這是因?yàn)椋簐iew 的顯示不依賴于UIGraphicsGetCurrentContext,只有在繪制的時(shí)候才需要。 - 首先調(diào)用圖層的
-
和
contents之間的關(guān)系經(jīng)過測試發(fā)現(xiàn),調(diào)用 view 中
draw(_ rect: CGRect)方法的所有繪制操作,都被保存在其圖層的contents屬性中:// ------ LayerView.swift ------ override func draw(_ rect: CGRect) { UIColor.brown.setFill() // 填充 UIRectFill(rect) UIColor.white.setStroke() // 描邊 let frame = CGRect(x: 20, y: 20, width: 80, height: 80) UIRectFrame(frame) } // ------ ViewController.swift ------ DispatchQueue.main.asyncAfter(deadline: .now() + 2) { print("contents: \(self.layerView.layer.contents)") } // Prints "Optional(<CABackingStore 0x7faf91f06e20 (buffer [480 256] BGRX8888)>)"這也是為什么要
CALayer提供繪圖環(huán)境、以及在上面介紹contents這個(gè)屬性時(shí)需要注意的地方。重要:
如果委托實(shí)現(xiàn)了
display(_ :),將不會(huì)調(diào)用此方法。 -
和
display(_ layer: CALayer)之間的關(guān)系前面說過,view 的
draw(_:)方法是由它圖層的draw(_:in:)方法調(diào)用的。但是如果我們實(shí)現(xiàn)的是display(_:)而不是draw(_:in:)呢?這意味著draw(_:in:)失去了它的作用,在沒有上下文的支持下,屏幕上將不會(huì)有任何關(guān)于 view 的畫面顯示,而display(_:)也不會(huì)自動(dòng)調(diào)用 view 的draw(_:),view 的draw(_:)方法也失去了意義,那display(_ layer: CALayer)的作用是什么?例如:override func draw(_ rect: CGRect) { print(#function) } override func display(_ layer: CALayer) { print(#function) } // Prints "display"這里
draw(_:)沒有被調(diào)用,屏幕上也沒有相關(guān) view 的顯示。也就是說,此時(shí)除了在display(_:)上進(jìn)行操作外,已經(jīng)沒有任何相關(guān)的地方可以設(shè)置圖層的可視內(nèi)容了(參考 "1. func display(_ layer: CALayer)",這里不再贅述,當(dāng)然也可以設(shè)置背景顏色等)。當(dāng)然,你可能永遠(yuǎn)都不會(huì)這么做,除非你創(chuàng)建了一個(gè)單獨(dú)的圖層。至于為什么不在
display(_ layer: CALayer)方法里面調(diào)用它的父類實(shí)現(xiàn),這是因?yàn)槿绻{(diào)用了會(huì)崩潰:// unrecognized selector sent to instance 0x7fbcdad03ba0至于為什么?根據(jù)我的參考資料,他們都沒有在此繼續(xù)調(diào)用
super(UIView) 的方法。我隨意猜測一下是這樣的:首先錯(cuò)誤提示的意思翻譯過來就是:<u>無法識(shí)別的選擇器(方法)發(fā)送到實(shí)例</u>。那我們來分析一下,是哪一個(gè)實(shí)例中?是什么方法?
- 是
super實(shí)例; - 是
display(_ layer: CALayer)。
也就是說,在調(diào)用
super.display(_ layer: CALayer)方法的時(shí)候,super中找不到該方法。為什么呢?請(qǐng)注意UIView默認(rèn)已經(jīng)遵循了CALayerDelegate協(xié)議(右鍵點(diǎn)擊UIView查看頭文件),但是應(yīng)該沒有實(shí)現(xiàn)它的display(_:)方法,而是選擇交給了子類去實(shí)現(xiàn)。類似的實(shí)現(xiàn)應(yīng)該是:// 示意 `CALayerDelegate` @objc protocol LayerDelegate: NSObjectProtocol { @objc optional func display() @objc optional func draw() } // 示意 `CALayer` class Layer: NSObject { var delegate: LayerDelegate? } // 示意 `UIView` class BaseView: NSObject, LayerDelegate { let layer = Layer() override init() { super.init() layer.delegate = self } } // 注意:并沒有實(shí)現(xiàn)委托的 `display()` 方法。 extension BaseView: LayerDelegate { func draw() {} } // 示意 `UIView` 的子類 class LayerView: BaseView { func display() { // 同樣的代碼在OC上實(shí)現(xiàn)沒有問題。 // 由于Swift是靜態(tài)編譯的關(guān)系,它會(huì)檢測在 `BaseView` 類中有沒有這個(gè)方法, // 如果沒有就會(huì)提示編譯錯(cuò)誤。 super.display() } } // ------ ViewController.swift ------ let layerView = LayerView() // 如果在方法里面調(diào)用了 `super.display()` 將引發(fā)崩潰。 layerView.display() // 正常執(zhí)行 layerView.darw() - 是
注意:
只有當(dāng)系統(tǒng)在檢測到 view 的
draw(_:)方法被實(shí)現(xiàn)時(shí),才會(huì)自動(dòng)調(diào)用圖層的display(_:)或draw(_ rect: CGRect)方法。否則就必須通過手動(dòng)調(diào)用圖層的setNeedsDisplay()方法來調(diào)用。 -
-
func layerWillDraw(_ layer: CALayer)
在
draw(_ layer: CALayer, in ctx: CGContext)調(diào)用之前調(diào)用,可以使用此方法配置影響內(nèi)容的任何圖層狀態(tài)(例如contentsFormat和isOpaque)。 -
func layoutSublayers(of layer: CALayer)
和
UIView的layoutSubviews()類似。當(dāng)發(fā)現(xiàn)邊界發(fā)生變化并且其sublayers可能需要重新排列時(shí)(例如通過frame改變大?。瑢⒄{(diào)用此方法。 -
func action(for layer: CALayer, forKey event: String) -> CAAction?
CALayer之所以能夠執(zhí)行動(dòng)畫,是因?yàn)樗欢x在 Core Animation 框架中,是 Core Animation 執(zhí)行操作的核心。也就是說,CALayer除了負(fù)責(zé)顯示內(nèi)容外,還能執(zhí)行動(dòng)畫(其實(shí)是 Core Animation 與硬件之間的操作在執(zhí)行,CALayer負(fù)責(zé)存儲(chǔ)操作需要的數(shù)據(jù),相當(dāng)于 Model)。因此,使用CALayer的大部分屬性都附帶動(dòng)畫效果。但是在UIView中,默認(rèn)將這個(gè)效果給關(guān)掉了,可以通過它圖層的委托方法重新開啟 ( 在 view animation block 中也會(huì)自動(dòng)開啟 ),返回決定它動(dòng)畫特效的對(duì)象,如果返回的是nil,將使用默認(rèn)隱含的動(dòng)畫特效。示例 - 使用圖層的委托方法返回一個(gè)從左到右移動(dòng)對(duì)象的基本動(dòng)畫:
final class CustomView: UIView { override func action(for layer: CALayer, forKey event: String) -> CAAction? { guard event == "moveRight" else { return super.action(for: layer, forKey: event) } let animation = CABasicAnimation() animation.valueFunction = CAValueFunction(name: .translateX) animation.fromValue = 1 animation.toValue = 300 animation.duration = 2 return animation } } let view = CustomView(frame: CGRect(x: 44, y: 44, width: UIScreen.width - 88, height: 300)) view.backgroundColor = .orange self.view.addSubview(view) let action = view.layer.action(forKey: "moveRight") action?.run(forKey: "transform", object: view.layer, arguments: nil)那怎么知道它的哪些屬性是可以附帶動(dòng)畫的呢?核心動(dòng)畫編程指南列出了你可能需要考慮設(shè)置動(dòng)畫的
CALayer屬性:
CALayer 坐標(biāo)系
CALayer 具有除了 frame 、bounds 之外區(qū)別于 UIView 的其他位置屬性。UIView 使用的所謂 frame 、bounds 、center 等屬性,其實(shí)都是從 CALayer 中返回的,而 frame 只是 CALayer 中的一個(gè)計(jì)算型屬性而已。
這里主要說一下 CALayer 中的 anchorPoint 和 position 這兩個(gè)屬性,也是 CALayer 坐標(biāo)系中的主要依賴:
-
var anchorPoint: CGPoint ( 錨點(diǎn) )
圖層錨點(diǎn)示意圖 看 iOS 部分即可??梢钥闯?,錨點(diǎn)是基于圖層的內(nèi)部坐標(biāo),它取值范圍是 (0-1, 0-1) ,你可以把它想象成是
bounds的縮放因子。中間的 (0.5, 0.5) 是每個(gè)圖層的anchorPoint默認(rèn)值;而左上角的 (0.0, 0.0) 被視為是anchorPoint的起始點(diǎn)。<u>任何基于圖層的幾何操作都發(fā)生在指定點(diǎn)附近</u>。例如,將旋轉(zhuǎn)變換應(yīng)用于具有默認(rèn)錨點(diǎn)的圖層會(huì)導(dǎo)致圍繞其中心旋轉(zhuǎn),錨點(diǎn)更改為其他位置將導(dǎo)致圖層圍繞該新點(diǎn)旋轉(zhuǎn)。
錨點(diǎn)影響圖層變換示意圖 -
var position: CGPoint ( 錨點(diǎn)所處的位置 )
錨點(diǎn)影響圖層的位置示意圖 看 iOS 部分即可。圖1中的
position被標(biāo)記為了(100, 100),怎么來的?對(duì)于錨點(diǎn)來說,它在父圖層中有著更詳細(xì)的坐標(biāo)。對(duì)
position通俗來解釋一下,就是錨點(diǎn)在父圖層中的位置。一個(gè)圖層它的默認(rèn)錨點(diǎn)是
(0.5, 0.5),既然如此,那就先看下錨點(diǎn)x在父圖層中的位置,可以看到,從父圖層x到錨點(diǎn)x的位置是 100,那么此時(shí)的position.x就是 100;而y也是類似的,從父圖層y到錨點(diǎn)y的位置也是 100;則可以得出,此時(shí)錨點(diǎn)在父圖層中的坐標(biāo)是(100, 100),也就是此時(shí)圖層中position的值。對(duì)圖2也是如此,此時(shí)的錨點(diǎn)處于起始點(diǎn)位置
(0.0, 0.0),從父圖層x到錨點(diǎn)x的位置是 40;而從父圖層y到錨點(diǎn)y的位置是60,由此得出,此時(shí)圖層中position的值是(40, 60)。這里其實(shí)計(jì)算
position是有公式的,根據(jù)圖1可以套用如下公式:-
position.x = frame.origin.x + 0.5 * bounds.size.width; -
position.y = frame.origin.y + 0.5 * bounds.size.height。
因?yàn)槔锩娴?0.5 是
anchorPoint的默認(rèn)值,更通用的公式應(yīng)該是:-
position.x = frame.origin.x + anchorPoint.x * bounds.size.width; -
position.y = frame.origin.y + anchorPoint.y * bounds.size.height。
注意:
實(shí)際上,
position就是UIView中的center。如果我們修改了圖層的position,那么 view 的center會(huì)隨之改變,反之也是如此。 -
anchorPoint 和 position 之間的關(guān)系
前面說過,position 處于錨點(diǎn)中的位置(相對(duì)于父圖層)。這里就有一個(gè)問題,那就是,既然 position 相對(duì)于 anchorPoint ,那如果修改了 anchorPoint 會(huì)不會(huì)導(dǎo)致 position 的變化?結(jié)論是不會(huì):
let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
print(self.redView.layer.position) // Prints "(100.0, 100.0)"
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(self.redView.layer.position) // Prints "(100.0, 100.0)"
那修改了 position 會(huì)導(dǎo)致 anchorPoint 的變化嗎?結(jié)論是也不會(huì):
let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
print(redView.layer.anchorPoint) // Prints "(0.5, 0.5)"
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(redView.layer.anchorPoint) // Prints "(0.5, 0.5)"
經(jīng)過測試,無論修改了誰另一方都不會(huì)受到影響,受到影響的只會(huì)是 frame.origin 。至于為什么兩者互不影響,我暫時(shí)還沒想到。我隨意猜測一下是這樣的:
其實(shí) anchorPoint 就是 anchorPoint ;position 就是 position 。他們本身其實(shí)是沒有關(guān)聯(lián)的,因?yàn)樗鼈兡J(rèn)處在的位置正好重疊了,所以就給我們?cè)斐闪艘环N誤區(qū),認(rèn)為 position 就一定是 anchorPoint 所在的那個(gè)點(diǎn)。
和 frame 之間的關(guān)系
<u>CALayer 的 frame 在文檔中被描述為是一個(gè)計(jì)算型屬性,它是從 bounds 、anchorPoint 和 position 的值中派生出來的。為此屬性指定新值時(shí),圖層會(huì)更改其 position 和 bounds 屬性以匹配您指定的矩形。</u>
那它們是如何決定 frame 的?根據(jù)圖片可以套用如下公式:
-
frame.x = position.x - anchorPoint.x * bounds.size.width; -
frame.y = position.y - anchorPoint.y * bounds.size.height。
這就解釋了為什么修改 position 和 anchorPoint 會(huì)導(dǎo)致 frame 發(fā)生變化,我們可以測試一下,假設(shè)把錨點(diǎn)改為處在左下角 (0.0, 1.0) :
let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(redView.frame.origin) // Prints "(100.0, 20.0)"
用公式來計(jì)算就是:frame.x (100) = 100 - 0 * 120 、frame.y (20) = 100 - 1 * 80 ;正好和打印的結(jié)果相符。反之,修改 position 屬性也會(huì)導(dǎo)致 frame.origin 發(fā)生如公式般的變化,這里就不再贅述了。
注意:
如果修改了
frame的值是會(huì)導(dǎo)致position發(fā)生變化的,因?yàn)?position是基于父圖層定義的;frame的改變意味著它自身的位置在父圖層中有所改變,position也會(huì)因此改變。但是修改了
frame并不會(huì)導(dǎo)致anchorPoint發(fā)生變化,因?yàn)?anchorPoint是基于自身圖層定義的,無論外部怎么變,anchorPoint都不會(huì)跟著變化。
修改 anchorPoint 所帶來的困惑
對(duì)于修改 position 來說其實(shí)就是修改它的 "center" ,這里很容易理解。但是對(duì)于修改 anchorPoint ,相信很多人都有過同樣的困惑,為什么修改了 anchorPoint 所帶來的變化往往和自己想象中的不太一樣呢?來看一個(gè)修改錨點(diǎn) x 的例子 ( 0.5 → 0.2 ):

仔細(xì)觀察一下 "圖2" 就會(huì)發(fā)現(xiàn),不管是新錨點(diǎn)還是舊錨點(diǎn),它們?cè)谧陨韴D層中的位置中都沒有變化。既然錨點(diǎn)本身不會(huì)變化,那變化的就只能是 x 了。x 是如何變化的?從圖片中可以很清楚地看到,是把新錨點(diǎn)移動(dòng)到舊錨點(diǎn)的所在位置。這也是大部分人的誤區(qū),以為修改 0.5 -> 0.2 就是把舊的錨點(diǎn)移動(dòng)到新錨點(diǎn)的所在位置,結(jié)果恰恰相反,這就是造成修改 anchorPoint 往往和自己想象中不太一樣的原因。
還有一種比較好理解的方式就是,想象一下,假設(shè) "圖1" 中的底部紅色圖層是一張紙,而中間的白點(diǎn)相當(dāng)于一枚大頭釘固定在它中間,移動(dòng)的時(shí)候,你就按住中間的大頭釘讓其保持不動(dòng)。這時(shí)候假設(shè)你要開始移動(dòng)到任意點(diǎn)了,那你會(huì)怎么做呢?唯一的一種方式就是,<u>移動(dòng)整個(gè)圖層</u>,讓新的錨點(diǎn)順著舊錨點(diǎn)中的位置靠攏,最終完全重合,就算移動(dòng)完成了。
參考
徹底理解position與anchorPoint
核心動(dòng)畫編程指南
iOS 核心動(dòng)畫:高級(jí)技巧
蘋果 UIView 文檔
蘋果 CALayer 文檔



