iOS 面試策略之系統框架-UIKit

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本章節主要從視圖、網絡、設計模式幾個方面考察開發者的開發水準,這是任何一個合格的 iOS 開發者都應該具備的基本素養。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOS 開發中最重要的 API 就是 UIKit。它是蘋果官方提供的管理界面和交互的最基本的 API。UIKit 被用在所有的 iPhone 和 iPad 開發中,它涵蓋的內容包括觸摸和交互處理、視圖佈局、圖形繪製中。可以說 UIKit 相關知識點的考察是所有面試中最基本、最必不可少、最重要的一環。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本節將從用戶界面聊起,回答開發中常見的佈局和交互問題;之後將重點集中在動畫渲染上,最後的問答題將集中在 iPad 的多屏開發上。對於 iOS 11 中最新的 drag and drop 和安全區域亦有涉及。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"UI 控件和基本佈局","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.要在 UIView 上定義一個 Label有 哪幾種方式?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞:#storyboard #xib #Frame #Auto Layout","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這道題本身問法十分模糊。定義一個 Label,指的是創建一個,還是說給它做相應的佈局,亦或是設置它的屬性值?這都是要和麪試官進行進一步溝通確定的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假如我們要從零創建一個 label,配置它在頁面上的佈局,並設置屬性值,有以下幾種方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用 storyboard 或 xib 完成。直接在庫面板中拖拽一個 label 完成創建,然後設置相應的 constraint 進行佈局,最後在屬性檢查器面板對相應屬性進行設置。這是蘋果推薦的做法。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用純代碼的方式來做。在 ViewController 中新建一個 label,然後用 frame 或是 auto layout(可以用 anchor 或 NSLayoutConstraint )來佈局,最後再一個個屬性進行手動設置。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.storyboard/xib,和純代碼構建 UI 相比,有什麼優缺點?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞:#可視化 #多人協作 #性能","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"storyboard/xib 的開發方式優點和缺點都十分明顯。優點是:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**簡單直接。**直接拖拽和點選即可配置 UI,界面所見即所得。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**跳轉關係清楚。**Storyboards 中可以清楚的區分 View Controller 界面之間的跳轉關係。而且在代碼中,通過實現 prepare(for segue: UIStoryboardSegue, sender: Any?),我們可以統一管理界面跳轉和數據管理。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺點是:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**協作衝突。**多人編輯時很容易產生衝突,且衝突很難解決。因爲自帶 Xcode 和系統的版本號,協作時 storyboard/xib 會在相同位置做同樣修改,這樣代碼衝突幾乎是不可避免的。解決方法是細分 storyboard 以及對應工程師的職責,但是這樣同樣帶來了維護成本。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**很難做到界面繼承和重用。**代碼中實現要容易和明確得多,然而 storyboard/xib 卻很難做到。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**不便於進行模塊化管理。**storyboard/xib 中搜索起來很不方便,且統一修改多個 UI 控件的屬性值不可能,必須一個一個改。在代碼中一個工廠模式就可以搞定。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**性能影響。**storyboard/xib 在界面渲染上有時會成爲性能殺手。例如首頁 UI 構造時,代碼書寫和優化就會比 storyboard 多圖層的渲染要好很多。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.Auto Layout 和 Frame 在 UI 佈局和渲染上有什麼區別?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞: #性能","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**Auto Layout 是針對多尺寸屏幕的設計。**其本質是通過線性不等式對 UI 控件的相對位置進行設定,從而適配多種 iPhone/iPad 屏幕的尺寸。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**Frame 是基於 xy 座標軸系統的佈局機制。**它從數學上限定了 UI 控件的具體位置,是 iOS 開發中最底層、最基本的界面佈局機制。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**Auto Layout 的性能比 Frame 差很多。**Auto Layout 的佈局過程首先求解線性不等式,然後再轉化爲 Frame 去進行佈局。其中求解的計算量非常大,通常 Auto Layout 的性能損耗是 Frame 佈局的 10 倍左右。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"加分回答:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決方法是儘量壓縮視圖層級減少計算量;同時 Layout 的計算也可以通過後臺線程來處理,這樣就可以不阻塞主線程操作。計算結果亦可以緩存起來,加速之後界面佈局渲染。成熟的解決方案有 Facebook 的 ComponentKit,Pinterest 的 Texture(前身是 ASDK ),以及 LinkdedIn 的 LayoutKit。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.UIView 和 CALayer 有什麼區別?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞: #性能 #交互","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**UIView 和 CALayer 都是 UI 操作的對象。**兩者都是 NSObject 的子類,發生在 UIView 上的操作本質上也發生在對應的 CALayer 上。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**UIView 是 CALayer 用於交互的抽象。**UIView 是 UIResponder 的子類( UIResponder 是 NSObject 的子類),提供了很多 CALayer 所沒有的交互上的接口,主要負責處理用戶觸發的種種操作。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**CALayer 在圖像和動畫渲染上性能更好。**這是因爲 UIView 有冗餘的交互接口,而且相比 CALayer 還有層級之分。CALayer 在無需處理交互時進行渲染可以節省大量時間。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.請說明並比較以下關鍵詞:Frame, Bounds, Center","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞: #座標 #父視圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Frame 是指當前視圖(View)相對於父視圖的平面座標系統中的位置和大小。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Bounds 是指當前視圖相對於自己的平面座標系統中的位置和大小。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Center 是一個 CGPoint,指當前視圖在父視圖的平面座標系統中最中間位置點 。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"加分回答:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"介紹完上述概念後,下面用畫圖的方式來講解這三個詞的區別。如下圖:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/df/df034896337c38525fd357d4e16d362c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中 View B 左上角的點的frame 值是(200, 100),bounds 值是(0, 0),center 所對應點的值是(275, 200)。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"6.請說明並比較以下方法:layoutIfNeeded, layoutSubviews, setNeedsLayout","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞: #佈局 #週期","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"layoutIfNeeded 方法一旦調用,主線程會立即強制重新佈局,它從當前視圖開始,一直到完成所有子視圖的佈局。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"layoutSubviews 是用來自定義視圖尺寸調整的。它是系統自動調用的,開發者不能手動調用。我們能做的就是重寫該方法,讓系統在尺寸調整時能按照希望的效果去進行佈局。這個方法主要在屏幕旋轉、滑動或觸摸界面、子視圖修改時被觸發。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"setNeedsLayout 與 layoutIfNeeded 相似,唯一不同的就是它不會立刻強制視圖重新佈局,而是在下一個佈局週期纔會觸發更新。它主要用在多個 view 佈局先後更新的場景下。例如我們要在兩個佈局不停變化的點之間連一條線,這個線的佈局就可以調用 setNeedsLayout 方法。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"7.請說明並比較以下關鍵詞:Safe Area, SafeAreaLayoutGuide, SafeAreaInsets","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞: #安全區域","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於 iPhone X 全新的劉海設計,iOS 11 中引入了安全區域(Safe Area)這套概念。如下圖:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/99/9930d2009be1add9878a8df88d5f734b.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**Safe Area 是指應用合理顯示內容的區域。**它不包括 status bar, navigation bar, tab bar , tool bar 等。iPhone X 中一般是指扣除了頂部的 status bar(高度爲20)、navigation bar(高度爲44)和底部的 home indicator 區域(高度爲34),這樣應用的內容不會被劉海擋住或是影響底部手勢操作。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**SafeAreaLayoutGuide 是指 SafeArea 的區域範圍和限制 。**在佈局設置中,我們可以分別取得它的上下左右 4 個邊界的位置進行相應佈局處理。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**SafeAreaInsets 限定了 SafeArea 區域與整個屏幕之間的佈局關係。**一般我們用上下左右 4 個值來獲取 SafeArea 與屏幕邊緣之間的距離。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"動畫","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"8.iOS 中實現動畫的方式有幾種?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞: #UIViewPropertyAnimator #UIView Animation #CALayer Animation","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最主要的實現動畫方式有 3 種,UIView Animation、CALayer Animation、UIViewPropertyAnimator。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**UIView Animation 可以實現基於 UIView 的簡單動畫。**它是 CALayer Animation 的封裝,主要可以實現移動、旋轉、縮放、變色等基本操作。其基本函數爲","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"+ animateWithDuration:animations:","attrs":{}}],"attrs":{}},{"type":"text","text":",其中持續時間(duration)爲基本參數,block 中對 UIView 屬性的調整就是動畫結束後的最終效果。除此之外他還有關鍵幀動畫和兩個 view 轉化等接口。它實現的動畫無法回撤、暫停、與手勢交互。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**CALayer Animation 是更在底層 CALayer 上的動畫接口。**除了 UIView Animation 可以實現的效果。它可以修改更多的屬性以實現各種複雜的動畫效果。其實現的動畫可以回撤、暫停、與手勢交互。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**UIViewPropertyAnimator 是 iOS 10 引進的處理交互式動畫的接口。**它也是基於 UIView 實現,可以實現所有的 UIView Animation 效果。它最大的優點在於 timing function 以及與手勢配合的交互式動畫設置相比 CALayer Animation 十分簡便,可以說是爲交互而生的動畫接口。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"9.代碼實現:控制屏幕上的圓形小球,使其水平向右滑動 200 個 point。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞: #UIViewPropertyAnimator #交互式動畫","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這道題很明顯是要求實現動畫。然而,題目中對於動畫的各種參數(持續時間,延時,速度控制等)都沒有要求。我們在做這道題目的時候一定要就相關細節向面試官詢問清楚,切忌上來就寫——實際開發中最怕用戶需求都不明白就寫代碼,最終也只會是南轅北轍。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設圓形小球已經在屏幕上,面試官沒有參數要求,只是要實現水平移動的效果。那麼實現代碼如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// UIViewPropertyAnimator實現\nlet animator = UIViewPropertyAnimator(duration: 2, curve: .linear) {\n circle.frame = circle.frame.offsetBy(dx: 200, dy: 0)\n}\nanimator.startAnimation()\n\n// UIView Animation實現\nUIView.animate(withDuration: 2) {\n circle.frame = circle.frame.offsetBy(dx: 200, dy: 0)\n}\n\n// CALayer實現\nlet animation = CABasicAnimation.init(keyPath: \"position.x\")\nanimation.fromValue = circle.center.x\nanimation.toValue = circle.center.x + 200\nanimation.duration = 2\nself.circle.layer.add(animation, forKey: nil)\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"追問:假如需要根據手勢來控制小球的水平移動,該怎麼操作?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這次考察的是交互式動畫,那麼交互式動畫用 UIViewPropertyAnimator 來做最爲方便。關於手勢具體如何控制球的移動,請向面試官詢問。我們假設面試官給出如下要求:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一開始小球靜止,除非用戶觸摸屏幕,否則小球不動","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按住屏幕並左右滑動,此時小球隨手勢線性左右滑動","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鬆開手,小球從當前位置滑動到水平初始距離向右 200 points 處,整個移動過程是先快後慢的效果","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當再次觸摸屏幕時,如果小球未滑動到終點,則小球將暫停滑動,再次隨手勢線性滑動","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當到達終點後,無論用戶如何觸摸屏幕,小球在終點靜止不動","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上述要求中我們知道:timing function 是 ease out,開始時暫停動畫。隨着手勢的移動,我們記錄動畫的完成度 fractionComplete。當手勢釋放時,我們繼續動畫,讓其自動完成。注意手勢操控動畫進行交互的時候,Animator 會自動將 timing function 從 ease out 轉爲 linear。代碼如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"var progress: CGFloat = 0\nvar animator: UIViewPropertyAnimator!\n\noverride func viewDidLoad() {\n let gesture = UIPanGestureRecognizer(target: self, action: Selector.handlePan)\n view.addGestureRecognizer(gesture)\n\n animator = UIViewPropertyAnimator.init(duration: 2, curve: .easeOut, animations: {\n self.circle.frame = self.circle.frame.offsetBy(dx: 200, dy: 0)\n })\n}\n\nfunc handlePan(recognizer:UIPanGestureRecognizer) {\n switch recognizer.state {\n case .began:\n animator.pauseAnimation()\n progress = animator.fractionComplete\n case .changed:\n let translation = recognizer.translation(in: self.circle)\n animator.fractionComplete = translation.x / 200 + progress\n case .ended:\n animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)\n default:\n break\n }\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"多任務開發","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"10.iOS 開發中,如何保證應用的 UI 在 iPhone、iPad 以及 iPad 分屏情況下依然適用?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞:#Adaptive UI #Size Class #Auto Layout","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了針對各種機型,蘋果在 iOS 8 中引入了 Adaptive UI 的概念。所以要保證應用的 UI 在各種情況下依然良好,主要注意以下幾個點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**採用 Auto Layout。**與 frame 設置絕對位置不同,所有的 UI 控件將保持相對位置。例如將 label 設置成對應屏幕 center X, center Y,此時無論是 iPhone 還是 iPad,此 label 都將相對於屏幕居中。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**採用 Size Class。**很多時候 UI 控件可能在 iPhone 上大小剛好,但在 iPad 上可能偏小,位置也有可能有偏移。此時用 Size Class,可以分別在不同的機型上進行安裝/卸載對應的 constraint,並且可以方便的進行預覽。蘋果將自家設備按照橫縱兩個尺寸進行區別,不同的情況對應的 Regular 和Compact 組合。比如豎屏的 iPhone 寬度是 Compact,高度是 Regular。具體分類請看下圖:","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d2/d218706dcf3dba396f13367d1124dc5a.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**關注多屏情況。**iPad 上引進的多屏情況主要分三種:Slide Over,Split View,Picture in Picture。蘋果明確指出應用應該支持 Slide Over 和 Split View。這時候作爲工程師,應該多多與設計師交流針對這兩種情況的 UI 設計,並配合 Size Class 進行分類適配。下圖詳盡說明了 iPad 上多任務的尺寸分類:","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1c/1c96cb7db4a0d6cd626bd69f0e3ac1a7.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"11.代碼實現:將 UIImageView 上的圖片直接拖拽到另一個 UIImageView 上。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"關鍵詞:#Drag and Drop","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這道題考察的是 iOS 11 最新引入的 Drag and Drop 功能。跟很多面試題一樣,它沒有說明起始和終止的 UIImageView 是否在一個應用之內。如果在同一個應用之內,那麼無論是 iPhone 還是 iPad 都能實現這樣的功能;如果是把圖片從一個應用拖拽到另一個應用之上,那麼只能是 iPad 實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們假設面試官考察的是在同一個應用中,將一張圖片從一個 UIImageView 中拖拽到另一個 UIImageView 。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Drag and Drop 一般實現起來分3步:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1. 對相應的 UIImageView 分別添加 drag 和 drop delegate","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"dragImageView.addInteraction(UIDragInteraction(delegate: self))\ndropImageView.addInteraction(UIDropInteraction(delegate: self))\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意,dragImageView 和 dropImageView 的 userInteractionEnabled 必須是 true。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2.實現 drag delegate 規定的方法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"extension ViewController: UIDragInteractionDelegate {\n func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {\n if interaction.view == dragImageView {\n let dragImage = dragImageView.image\n let itemProvider = NSItemProvider(object: dragImage!)\n let dragItem = UIDragItem(itemProvider: itemProvider)\n return [dragItem]\n } else {\n return []\n }\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個方法的功能就是告訴系統,我們要拖動的對象。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"方法裏面的 NSItemProvider 簡單來說就是用來在 Drag and Drop,或者 Extension app 和 Host app 之間傳輸數據的類。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"UIDragItem 則是像對 NSItemProvider 的進一步封裝,除了包含傳輸數據外,還可以自定義一些數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現完該方法後,圖片就可以從 dragImageView 裏拖動出來了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3.實現 drop delegate 對應的方法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般來講,需要實現 3 個方法:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// 詢問是否可以處理 drag 的數據,默認是 true,所以並不一定要實現\nfunc dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool\n\n// 詢問系統當 drop 之時,以何種方式處理 drag 的數據\n// UIDropProposal對應的操作是 cancel, forbidden, copy, move\nfunc dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal\n\n// drop 已經發生,取出 drag 中的數據並進行處理\nfunc dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession)\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體的代碼如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {\n let dropLocation = session.location(in: view)\n let operation: UIDropOperation\n\n if dropImageView.frame.contains(dropLocation) {\n operation = .copy\n } else {\n operation = .cancel\n }\n return UIDropProposal(operation: operation)\n}\n\nfunc dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {\n session.loadObjects(ofClass: UIImage.self) { [weak self] (imageItems) in\n self?.dragImageView.image = nil\n self?.dropImageView.image = imageItems.first as? UIImage\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上是最簡單直接的實現方法。Drag and Drop 中還有很多可以定製的方法和屬性,例如支持多點觸摸的 preview 方法。工作中,你可能只需要實現 drag 功能,也可能只需要支持 drop 功能。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"訪問我的","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/iOS-Mayday/iOSAdvanced-Roadmap","title":"","type":null},"content":[{"type":"text","text":"Github","attrs":{}}]},{"type":"text","text":"倉庫查看更多精彩分享","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章