WWDC 2014 Session笔记 - 可视化开发,IB 的新时代
本文是我的?WWDC 2014 筆記?中的一篇,涉及的 Session 有
- What's New in Xcode 6
- What's New in Interface Builder
如果說在 WWDC 14 之前 Interface Builder (IB) 還是可選項(xiàng)的話,我相信在此之后 IB 已經(jīng)是毫無疑問的 iOS 開發(fā)標(biāo)配了,純代碼界面可以說已經(jīng)漸行漸遠(yuǎn),可以逐漸離開我們的視線了。
一言蔽之,就是 Apple 在催促大家使用 IB,特別是 Storyboard 做為界面開發(fā)的唯一選擇這件事上,下定了決心,也做出了實(shí)際的行動。
如果是純代碼 UI 在此之前還能有所掙扎的話,那么壓死這個(gè)方案的最后一根稻草就是 Size Classes。我已經(jīng)在之前的筆記中對這方面內(nèi)容做了些簡單的探索,但是還遠(yuǎn)遠(yuǎn)不夠,也許在將來某一天我還會重新整理下 Size Classes 這個(gè)主題的內(nèi)容,以及使用 IB 適配不同屏幕的一些實(shí)踐,但是不是這次。這篇文章里想要介紹的是 Xcode 6 中為 IB 錦上添花的一個(gè)特性,那就是實(shí)時(shí)地預(yù)覽自定義 view,這個(gè)特性讓 IB 開發(fā)的流程更加直觀可視,也可以減少很多無聊的參數(shù)配置和 UI 設(shè)置的時(shí)間。
以前 IB 的不足
作為可視化開發(fā)的工具,IB 和 Storyboard 在組織和構(gòu)建 ViewController 及其導(dǎo)航關(guān)系時(shí)已經(jīng)做得很好的。對于 ViewController 的 view 畫布上的諸如?UILabel?或者?UIImageView?這樣的基礎(chǔ)的類,IB 是能夠很好地支持并實(shí)時(shí)在設(shè)計(jì)的時(shí)候進(jìn)行顯示的。但是對于那些自定義的類,之前的 IB 就束手無策了。我們能做的僅僅是在 IB 中拖放一個(gè)?UIView,然后通過將?Custom Class?屬性設(shè)置為我們自定義的?UIView?的子類來在 “暗示” IB 在運(yùn)行時(shí)初始化一個(gè)對應(yīng)的子類。這樣的問題是在開發(fā)自定義的 view 時(shí),我們不得不一遍遍地修改代碼并運(yùn)行,再根據(jù)運(yùn)行結(jié)果進(jìn)行調(diào)整和修正。而實(shí)際上,單一對某個(gè) view 的調(diào)試這種問題只涉及到設(shè)計(jì)層面,而非運(yùn)行層面,如果我們能夠在設(shè)計(jì)時(shí)就有一個(gè)實(shí)時(shí)地對自定義 view 的預(yù)覽該多好。
沒錯(cuò),Apple 也是這么想的,并且在 Xcode 6 中,我們就已經(jīng)可以創(chuàng)建這樣的?UIView?子類了:利用新加入的?@IBDesignable?和?@IBInspectable,我們可以非常方便地完成在 IB 中實(shí)時(shí)顯示自定義視圖,甚至和其他一些內(nèi)置?UIView?子類一樣,直接在 IB 的 Inspector 改變某些屬性,甚至我們還能通過設(shè)置斷點(diǎn)來在 IB 中顯示視圖時(shí)進(jìn)行調(diào)試。新的這些特性非常強(qiáng)大,使用起來卻出乎意料的簡單。下面我將通過一個(gè)實(shí)際的小例子加以說明。最終的完整例子已經(jīng)放在?GitHub?上了,現(xiàn)在我們從開始一步步開始吧。這些代碼基于 Xcode 6.1 和 Swift 1.1。
時(shí)鐘 view 的例子
單純的自定義 view
假設(shè)我們有一個(gè)自定義的 view,用來描畫一個(gè)時(shí)鐘,如果有在讀?objc.io?或者?objc 中國?的讀者,可能會發(fā)現(xiàn)這段代碼是動畫一章一篇文章里代碼的改造過的 Swift 版本。
在這里我們有一個(gè)自定義的?UIView?的子類:ClockFaceView,其中嵌套了一個(gè)?ClockFaceLayer作為 layer。如果我們不需要?jiǎng)赢?#xff0c;我們也可以簡單地使用?-drawRect:?來完成繪制。但是在這里我們還是選擇使用添加?CALayer?的方式,這會使之后做動畫簡單好幾個(gè)數(shù)量級 -- 因?yàn)槲覀兛梢院唵蔚赝ㄟ^ CA 動畫而不是每幀去計(jì)算繪制來完成動畫 (在這篇帖子里不會涉及這些內(nèi)容)。
// ClockFaceView.swift import UIKitclass ClockFaceView : UIView {class ClockFaceLayer : CAShapeLayer {private let hourHand: CAShapeLayerprivate let minuteHand: CAShapeLayeroverride init() {hourHand = CAShapeLayer()minuteHand = CAShapeLayer()super.init()frame = CGRect(x: 0, y: 0, width: 200, height: 200)path = UIBezierPath(ovalInRect: CGRectInset(frame, 5, 5)).CGPathfillColor = UIColor.whiteColor().CGColorstrokeColor = UIColor.blackColor().CGColorlineWidth = 4hourHand.path = UIBezierPath(rect: CGRect(x: -2, y: -70, width: 4, height: 70)).CGPathhourHand.fillColor = UIColor.blackColor().CGColorhourHand.position = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)addSublayer(hourHand)minuteHand.path = UIBezierPath(rect: CGRect(x: -1, y: -90, width: 2, height: 90)).CGPathminuteHand.fillColor = UIColor.blackColor().CGColorminuteHand.position = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)addSublayer(minuteHand) }required init(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")}func refreshToHour(hour: Int, minute: Int) {hourHand.setAffineTransform(CGAffineTransformMakeRotation(CGFloat(Double(hour) / 12.0 * 2.0 * M_PI)))minuteHand.setAffineTransform(CGAffineTransformMakeRotation(CGFloat(Double(minute) / 60.0 * 2.0 * M_PI)))}}private let clockFace: ClockFaceLayervar time: NSDate? {didSet {refreshTime()}}private func refreshTime() {if let realTime = time {if let calendar = NSCalendar(calendarIdentifier: NSGregorianCalendar) {let components = calendar.components(NSCalendarUnit.CalendarUnitHour |NSCalendarUnit.CalendarUnitMinute, fromDate: realTime)clockFace.refreshToHour(components.hour, minute: components.minute)}}}override init(frame: CGRect) {clockFace = ClockFaceLayer()super.init(frame: frame)layer.addSublayer(clockFace)}required init(coder aDecoder: NSCoder) {clockFace = ClockFaceLayer()super.init(coder: aDecoder)layer.addSublayer(clockFace)} }如果你沒有耐心看完的話也沒有關(guān)系,簡單來說就是?ClockFaceView?在被初始化時(shí)會向自己添加一個(gè)?ClockFaceLayer,用來顯示分針和時(shí)針。通過設(shè)置?time?屬性我們可以更新時(shí)鐘的位置。因?yàn)樘峁┝?span id="ozvdkddzhkzd" class="Apple-converted-space">?initWithCoder:,因此我們是可以直接從 IB 里加載這個(gè) view 的。方法就是最普通的類型指定,并讓 app 在加載時(shí)初始化對應(yīng)的類型:在新建的 Single View Application 的 Storyboard 中添加一個(gè)?UIView?控件,然后設(shè)置好約束,并且將 Class 設(shè)置為?ClockFaceView:
運(yùn)行應(yīng)用,可以看到?ClockFaceView?被正確地初始化了,指針指向默認(rèn)的 12 點(diǎn)整。通過為這個(gè) view 建立 outlet 或者用其他 (比如 tag 的方式,雖然我不太喜歡這么做,但是我見過不少人這么弄) 方法找到這個(gè)?ClockFaceView?并設(shè)置時(shí)間的話,我們可以正確地改變其時(shí)針和分針的指向:
// ViewController.swift class ViewController: UIViewController {@IBOutlet weak var clockFaceView: ClockFaceView!override func viewDidLoad() {super.viewDidLoad()// Do any additional setup after loading the view, typically from a nib.clockFaceView.time = NSDate()} }IBDesignable,IB 中自定義 view 的渲染
把大象裝進(jìn)冰箱有三個(gè)步驟,而讓 IB 顯示自定義 view 居然只有一個(gè)步驟!
只要我們在?class ClockFaceView : UIView?這個(gè)類型定義上面加上一個(gè)?@IBDesignable?的標(biāo)記,就完成了!
在進(jìn)行更改并等待編譯和 IB 自動識別后,我們就可以在 IB 中原來一塊白色的地方看到初始化后的時(shí)鐘了:
如你所想,這個(gè)標(biāo)記的作用是告訴 IB 如果遇到對應(yīng)的?UIView?子類的話,可以對其進(jìn)行渲染。深入一些來說,IB 將尋找你的子類中的?-initWithFrame:?方法,并給入當(dāng)前自定義 view 的 frame 對其進(jìn)行調(diào)用。需要注意的是,在使用 IB 初始化 view 時(shí),被調(diào)用的是?-initWithCoder:?而非 frame 版本,所以說在想要實(shí)現(xiàn)自定義 view 在 IB 中的預(yù)覽的話,我們至少必須實(shí)現(xiàn)這兩個(gè)版本的初始化方法。不過好消息是,如果我們只添加了?@IBDesignable,而忘了實(shí)現(xiàn)?-initWithFrame:?的話,在 IB 渲染 view 時(shí)會給我們拋出大大的錯(cuò)誤,所以因?yàn)檫z漏而花大量時(shí)間在查找哪里出了問題這種事情應(yīng)該不太可能發(fā)生。
僅設(shè)計(jì)時(shí)的配置
現(xiàn)在在 IB 中我們顯示的時(shí)鐘只能默認(rèn)地指向 0 點(diǎn) 0 分,這是因?yàn)樵谠O(shè)計(jì)的時(shí)候,我們并沒有機(jī)會去設(shè)定這個(gè) view 的?time?屬性,所以時(shí)針和分針都停留在了初始的位置上。在 Xcode 6 中可以在@IBDesignable?標(biāo)記的?UIView?子類中添加一個(gè)?prepareForInterfaceBuilder?方法。每次在 IB 即將把這個(gè)自定義的 view 渲染到畫布之前會調(diào)用這個(gè)方法進(jìn)行最后的配置。比如我們想在 IB 中這個(gè)時(shí)鐘的 view 上顯示當(dāng)前時(shí)間的話,可以在?ClockFaceView?中加入這個(gè)方法:
class ClockFaceView : UIView {//...override func prepareForInterfaceBuilder() {time = NSDate()}//... }保存并切換到 IB,靜待自動編譯和執(zhí)行,可以看到類似下面的結(jié)果:
挺好的...現(xiàn)在我們的 IB 不僅被用來設(shè)計(jì)界面了,還兼?zhèn)淞丝磿r(shí)間的功能 - 雖然這個(gè)時(shí)鐘并不是實(shí)時(shí)的,只有在切換編輯器界面到 IB 或者是修改了相關(guān)文件時(shí)才會進(jìn)行刷新。
另外雖然這篇文章沒有涉及,但是需要一提的是,如果你想要在?prepareForInterfaceBuilder?里加載圖片的話,需要弄清楚 bundle 的概念。IB 使用的 bundle 和 app 運(yùn)行時(shí)的?mainBundle?不是一個(gè)概念,我們需要在設(shè)計(jì)時(shí)的 IB 的 bundle 可以通過在自定義的 view 自身的 bundle 中進(jìn)行查找并使用。比如想要加載一張名為?image.png?的圖片的話:
let bundle = NSBundle(forClass: self.dynamicType) if let fileName = bundle.pathForResource("image", ofType: "png") {if let image = UIImage(contentsOfFile: fileName) {// 在此處可以使用 image} }在使用 IB 中的方法讀取資源時(shí)一定要注意運(yùn)行環(huán)境不同這一點(diǎn)。
用 IBInspectable 在 IB 中調(diào)整屬性
IBDesignable 的 view 的另一個(gè)很方便的地方是我們可以向 Inspector 中添加自定義的內(nèi)容了。通過這樣做,就可以直接在 IB 中對 view 進(jìn)行一些編輯和配置。以前對于自定義 view,我們通常只能通過用類似?IBOutlet?的方式在代碼中進(jìn)行設(shè)置,或者是配置 Runtime Attribute 來進(jìn)行,而現(xiàn)在我們有能力直接通過像給一個(gè)?UILabel?設(shè)定字符串或者給?UIImageView?設(shè)定圖片這樣的方式來設(shè)置自定義 view 的部分屬性了,這也使得在 IB 中的自定義 view 的易用性和完整性得到了極大增強(qiáng)。
使用方法也非常簡單,只需要在某個(gè)屬性前加上?@IBInspectable?標(biāo)記即可。比如我們可以在ClockFaceView?中加入以下代碼:
class ClockFaceView : UIView {//...@IBInspectablevar color: UIColor? {didSet {refreshColor()}}private func refreshColor() {if let realColor = color {clockFace.refreshColor(realColor)}}//... }然后在?ClockFaceLayer?中加入對應(yīng)的?refreshColor?方法:
class ClockFaceLayer : CAShapeLayer {//...func refreshColor(color: UIColor) {hourHand.fillColor = color.CGColorminuteHand.fillColor = color.CGColorstrokeColor = color.CGColor}//... }我們對?ClockFaceView?中的?color?屬性添加了?@IBInspectable,在保存和編譯后,這會在 IB 中對應(yīng)的 view 的 Attribute Inspector 中添加一個(gè)顏色選取的屬性:
當(dāng)我們在 IB 中設(shè)置這個(gè)屬性的時(shí)候,對應(yīng)的?didSet?將會被執(zhí)行,通過?refreshColor?方法就可以直接改變 IB 中這個(gè) view 的時(shí)針和分針的顏色了。
注意這個(gè)改變并不像?prepareForInterfaceBuilder?那樣僅發(fā)生在設(shè)計(jì)時(shí),我們直接運(yùn)行代碼,會看到運(yùn)行時(shí)的顏色也是發(fā)生了改變的。其實(shí)?@IBInspectable?并沒有做什么太神奇的事情,我們?nèi)绻榭?IB 中這個(gè) view 的 Identity Inspector 的話會看到剛才所設(shè)定的顏色值被作為了 Runtime Attribute 被使用了。其實(shí)手動直接在 Runtime Attributes 中設(shè)定顏色也有同樣的效果,因此@IBInspectable?唯一做的事情就是在 IB 面板上為我們提供了一個(gè)很方便地修改屬性的入口,別沒有其他太多神奇之處。
這個(gè)原理同時(shí)也決定了?@IBInspectable?是有一定限制的,即只有能夠在 Runtime Attributes 中指定的類型才能夠被標(biāo)記后顯示在 IB 中,這些類型包括Boolean,Number,String,Ponit,Size,Rect,Range,Color?和?Image。像是如果想要把類似?time?這樣的屬性標(biāo)記為?@IBInspectable?的話,在 IB 中還是無法顯示的,因?yàn)?Xcode 并沒有準(zhǔn)備?NSDate?類型。不過其實(shí)通過 KVC 進(jìn)行動態(tài)設(shè)定這種事情在原理上是沒有問題的,界面的支持應(yīng)該也可以通過?Xcode 插件進(jìn)行擴(kuò)展,感覺上并不是一件特別困難的事情,有興趣的同學(xué)不妨嘗試,應(yīng)該挺有意思 (當(dāng)然也有可能會是個(gè)坑)。
自定義渲染 view 的調(diào)試
對于簡單的自定義 view 來說,實(shí)時(shí)顯示和屬性設(shè)定什么的并不是一件很難的事情。但是對于那些比較復(fù)雜的 view,如果我們遇到某些渲染上的問題的話,如果只能靠猜的話,就未免太可憐了。幸好,Apple 為 view 在 IB 中的渲染的調(diào)試也提供了相應(yīng)的方法。在 view 的源代碼中設(shè)置好斷點(diǎn),然后切到 IB,點(diǎn)選中我們的自定義的 view 后,我們就可以使用菜單里的 Editor -> Debug Selected Views 來讓 IB 對這個(gè)自定義 view 進(jìn)行渲染。如果觸發(fā)了代碼中的斷點(diǎn),那我們的代碼就會被暫停在斷點(diǎn)處,lldb 也會就位聽我們調(diào)遣。一切都感覺良好,不是么?
總結(jié)
Xcode 6 中的很多 key feature 都是基于或者重度依賴 Interface Builder 的。比如 Size Classes,比如 xib 的啟動畫面,再比如本篇文章中說到的自定義 view 渲染等等。在 iOS 或者 Mac 開發(fā)中,IB 現(xiàn)在處于一個(gè)比以往任何時(shí)候都重要的時(shí)期,使用 IB 和這些方便的特性進(jìn)行開發(fā)已經(jīng)從可選項(xiàng)變?yōu)榱吮仨氻?xiàng)。很難想象沒有 IB 的話要怎么才能使用這些工具,更進(jìn)一步地說,很難想象沒有 IB 的話開發(fā)者需要浪費(fèi)多少時(shí)間在本應(yīng)該迅速完成的工作中。
如果你還在使用代碼來構(gòu)建 UI 的話,現(xiàn)在也許是你最后的放下代碼,拿起 IB 武裝自己的機(jī)會了。一開始可能會有迷惑,會不習(xí)慣,會覺著被拽出了舒適區(qū)渾身無力。但是一旦適應(yīng)以后,你不僅能夠收獲最新的技能和工具,也有機(jī)會站在一個(gè)全新的高度,來審視 app 中界面開發(fā)的種種,并從中找到樂趣。
P.S. 如果你不知道要從哪里入手,推薦可以從 raywenderlich 家的這篇?AutoLayout 教程開始你的 IB 之旅。
總結(jié)
以上是生活随笔為你收集整理的WWDC 2014 Session笔记 - 可视化开发,IB 的新时代的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ChatGPT 热潮席卷,AI 监管能否
- 下一篇: Activiti学习(二)数据表结构