大牛說:這是不可錯(cuò)過的iOS開發(fā)技巧(三)
Swift2出來了,還是得與時(shí)俱進(jìn)啊,不然就成老古董了。再者它開源了,又有事情要做了。當(dāng)個(gè)程序猿真是累啊,一直在追,可從來沒追上,剛有那么點(diǎn)念想了,人家又踩了腳油門。
這一期主要有三個(gè)內(nèi)容:
Tint Color
Build Configurations in Swift
鍵盤事件
Tint Color
在iOS 7后,UIView新增加了一個(gè)tintColor屬性,這個(gè)屬性定義了一個(gè)非默認(rèn)的著色顏色值,其值的設(shè)置會(huì)影響到以視圖為根視圖的整個(gè)視圖層次結(jié)構(gòu)。它主要是應(yīng)用到諸如app圖標(biāo)、導(dǎo)航欄、按鈕等一些控件上,以獲取一些有意思的視覺效果。
tintColor屬性的聲明如下:
- var tintColor: UIColor!
默認(rèn)情況下,一個(gè)視圖的tintColor是為nil的,這意味著視圖將使用父視圖的tint color值。當(dāng)我們指定了一個(gè)視圖的tintColor后,這個(gè)色值會(huì)自動(dòng)傳播到視圖層次結(jié)構(gòu)(以當(dāng)前視圖為根視圖)中所有的子視圖上。如果系統(tǒng)在視圖層次結(jié)構(gòu)中沒有找到一個(gè)非默認(rèn)的tintColor值,則會(huì)使用系統(tǒng)定義的顏色值(藍(lán)色,RGB值為[0,0.478431,1],我們可以在IB中看到這個(gè)顏色)。因此,這個(gè)值總是會(huì)返回一個(gè)顏色值,即我們沒有指定它。
與tintColor屬性相關(guān)的還有個(gè)tintAdjustmentMode屬性,它是一個(gè)枚舉值,定義了tint color的調(diào)整模式。其聲明如下:
- var tintAdjustmentMode: UIViewTintAdjustmentMode
枚舉UIViewTintAdjustmentMode的定義如下:
- enum UIViewTintAdjustmentMode : Int {
- case Automatic // 視圖的著色調(diào)整模式與父視圖一致
- case Normal // 視圖的tintColor屬性返回完全未修改的視圖著色顏色
- case Dimmed // 視圖的tintColor屬性返回一個(gè)去飽和度的、變暗的視圖著色顏色
- }
因此,當(dāng)tintAdjustmentMode屬性設(shè)置為Dimmed時(shí),tintColor的顏色值會(huì)自動(dòng)變暗。而如果我們?cè)谝晥D層次結(jié)構(gòu)中沒有找到默認(rèn)值,則該值默認(rèn)是Normal。
與tintColor相關(guān)的還有一個(gè)tintColorDidChange方法,其聲明如下:
- func tintColorDidChange()
這個(gè)方法會(huì)在視圖的tintColor或tintAdjustmentMode屬性改變時(shí)自動(dòng)調(diào)用。另外,如果當(dāng)前視圖的父視圖的tintColor或tintAdjustmentMode屬性改變時(shí),也會(huì)調(diào)用這個(gè)方法。我們可以在這個(gè)方法中根據(jù)需要去刷新我們的視圖。
示例
接下來我們通過示例來看看tintColor的強(qiáng)大功能(示例盜用了Sam Davies寫的一個(gè)例子,具體可以查看iOS7 Day-by-Day :: Day 6 :: Tint Color,我就負(fù)責(zé)搬磚,用swift實(shí)現(xiàn)了一下,代碼可以在這里下載)。
先來看看最終效果吧(以下都是盜圖,請(qǐng)見諒,太懶了):
這個(gè)界面包含的元素主要有UIButton, UISlider, UIProgressView, UIStepper, UIImageView, ToolBar和一個(gè)自定義的子視圖CustomView。接下來我們便來看看修改視圖的tintColor會(huì)對(duì)這些控件產(chǎn)生什么樣的影響。
在ViewController的viewDidLoad方法中,我們做了如下設(shè)置:
- override func viewDidLoad() {
- super.viewDidLoad()
- println("\(self.view.tintAdjustmentMode.rawValue)") // 輸出:1
- println("\(self.view.tintColor)") // 輸出:UIDeviceRGBColorSpace 0 0.478431 1 1
- self.view.tintAdjustmentMode = .Normal
- self.dimTintSwitch?.on = false
- // 加載圖片
- var shinobiHead = UIImage(named: "shinobihead")
- // 設(shè)置渲染模式
- shinobiHead = shinobiHead?.imageWithRenderingMode(.AlwaysTemplate)
- self.tintedImageView?.image = shinobiHead
- self.tintedImageView?.contentMode = .ScaleAspectFit
- }
首先,我們嘗試打印默認(rèn)的tintColor和tintAdjustmentMode,分別輸出了[UIDeviceRGBColorSpace 0 0.478431 1 1]和1,這是在我們沒有對(duì)整個(gè)視圖層次結(jié)構(gòu)設(shè)置任何tint color相關(guān)的值的情況下的輸出。可以看到,雖然我們沒有設(shè)置tintColor,但它仍然返回了系統(tǒng)的默認(rèn)值;而tintAdjustmentMode則默認(rèn)返回Normal的原始值。
接下來,我們顯式設(shè)置tintAdjustmentMode的值為Normal,同時(shí)設(shè)置UIImageView的圖片及渲染模式。
當(dāng)我們點(diǎn)擊”Change Color”按鈕時(shí),會(huì)執(zhí)行以下的事件處理方法:
- @IBAction func changeColorHandler(sender: AnyObject) {
- let hue = CGFloat(arc4random() % 256) / 256.0
- let saturation = CGFloat(arc4random() % 128) / 256.0 + 0.5
- let brightness = CGFloat(arc4random() % 128) / 256.0 + 0.5
- let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
- self.view.tintColor = color
- updateViewConstraints()
- }
- private func updateProgressViewTint() {
- self.progressView?.progressTintColor = self.view.tintColor
- }
這段代碼主要是隨機(jī)生成一個(gè)顏色值,并賦值給self.view的tintColor屬性,同時(shí)去更新進(jìn)度條的tintColor值。
注:有些控件的特定組成部件的tint color由特定的屬性控制,例如進(jìn)度就有2個(gè)tint color:一個(gè)用于進(jìn)度條本身,另一個(gè)用于背景。
點(diǎn)擊”Change Color”按鈕,可得到以下效果:
可以看到,我們?cè)谑纠胁⒂袥]手動(dòng)去設(shè)置UIButton, UISlider, UIStepper, UIImageView, ToolBar等子視圖的顏色值,但隨著self.view的tintColor屬性顏色值的變化,這些控件的外觀也同時(shí)跟著改變。也就是說self.view的tintColor屬性顏色值的變化,影響到了以self.view為根視圖的整個(gè)視圖層次結(jié)果中所有子視圖的外觀。
看來tintColor還是很強(qiáng)大的嘛。
在界面中還有個(gè)UISwitch,這個(gè)是用來開啟關(guān)閉dim tint的功能,其對(duì)應(yīng)處理方法如下:
- @IBAction func dimTimtHandler(sender: AnyObject) {
- if let isOn = self.dimTintSwitch?.on {
- self.view.tintAdjustmentMode = isOn ? .Dimmed : .Normal
- }
- updateViewConstraints()
- }
當(dāng)tintAdjustmentMode設(shè)置Dimmed時(shí),其實(shí)際的效果是整個(gè)色值都變暗(此處無圖可盜)。
另外,我們?cè)谧右晥DCustomView中重寫了tintColorDidChange方法,以監(jiān)聽tintColor的變化,以更新我們的自定義視圖,其實(shí)現(xiàn)如下:
- override func tintColorDidChange() {
- tintColorLabel.textColor = self.tintColor
- tintColorBlock.backgroundColor = self.tintColor
- }
所以方框和”Tint color label”顏色是跟著子視圖的tintColor來變化的,而子視圖的tintColor又是繼承自父視圖的。
在這個(gè)示例中,比較有意思的是還是對(duì)圖片的處理。對(duì)圖像的處理比較簡(jiǎn)單粗暴,對(duì)一個(gè)像素而言,如果它的alpha值為1的話,就將它的顏色設(shè)置為tint color;如果不為1的話,則設(shè)置為透明的。示例中的忍者頭像就是這么處理的。不過我們需要設(shè)置圖片的imageWithRenderingMode屬性為AlwaysTemplate,這樣渲染圖片時(shí)會(huì)將其渲染為一個(gè)模板而忽略它的顏色信息,如代碼所示:
var shinobiHead = UIImage(named: "shinobihead")
// 設(shè)置渲染模式
shinobiHead = shinobiHead?.imageWithRenderingMode(.AlwaysTemplate)
題外話
插個(gè)題外話,跟主題關(guān)系不大。
在色彩理論(color theory)中,一個(gè)tint color是一種顏色與白色的混合。與之類似的是shade color和tone color。shade color是將顏色與黑色混合,tone color是將顏色與灰色混合。它們都是基于Hues色調(diào)的。這幾個(gè)色值的效果如下圖所示:
一些基礎(chǔ)的理論知識(shí)可以參考Hues, Tints, Tones and Shades: What’s the Difference?或更專業(yè)的一些文章。
小結(jié)
如果我們想指定整個(gè)App的tint color,則可以通過設(shè)置window的tint color。這樣同一個(gè)window下的所有子視圖都會(huì)繼承此tint color。
當(dāng)彈出一個(gè)alert或者action sheet時(shí),iOS7會(huì)自動(dòng)將后面視圖的tint color變暗。此時(shí),我們可以在自定義視圖中重寫tintColorDidChange方法來執(zhí)行我們想要的操作。
有些復(fù)雜控件,可以有多個(gè)tint color,不同的tint color控件不同的部分。如上面提到的UIProgressView,又如navigation bars, tab bars, toolbars, search bars, scope bars等,這些控件的背景著色顏色可以使用barTintColor屬性來處理。
#p#
Build Configurations in Swift
在Objective-C中,我們經(jīng)常使用預(yù)處理指令來幫助我們根據(jù)不同的平臺(tái)執(zhí)行不同的代碼,以讓我們的代碼支持不同的平臺(tái),如:
- #if TARGET_OS_IPHONE
- #define MAS_VIEW UIView
- #elif TARGET_OS_MAC
- #define MAS_VIEW NSView
- #endif
在swift中,由于對(duì)C語言支持沒有Objective-C來得那么友好(暫時(shí)不知swift 2到C的支持如何),所以我們無法像在Objective-C中那樣自如而舒坦地使用預(yù)處理指令。
不過,swift也提供了自己的方式來支持條件編譯,即使用build configurations(構(gòu)建配置)。Build configurations已經(jīng)包含了字面量true和false,以及兩個(gè)平臺(tái)測(cè)試函數(shù)os()和arch()。
其中os()用于測(cè)試系統(tǒng)類型,可傳入的參數(shù)包含OSX, iOS, watchOS,所以上面的代碼在swift可改成:
- #if os(iOS)
- typealias MAS_VIEW = UIView
- #elseif os(OSX)
- typealias MAS_VIEW = NSView
- #endif
注:在WWDC 2014的“Sharing code between iOS and OS X”一節(jié)(session 233)中,Elizabeth Reid將這種方式稱為Shimming
遺憾的是,os()只能檢測(cè)系統(tǒng)類型,而無法檢測(cè)系統(tǒng)的版本,所以這些工作只能放在運(yùn)行時(shí)去處理。關(guān)于如何檢測(cè)系統(tǒng)的版本,Mattt Thompson老大在它的Swift System Version Checking一文中給了我們答案。
我們?cè)賮砜纯碼rch()。arch()用于測(cè)試CPU的架構(gòu),可傳入的值包括x86_64, arm, arm64, i386。需要注意的是arch(arm)對(duì)于ARM 64的設(shè)備來說,不會(huì)返回true。而arch(i386)在32位的iOS模擬器上編譯時(shí)會(huì)返回true。
如果我們想自定義一些在調(diào)試期間使用的編譯配置選項(xiàng),則可以使用-D標(biāo)識(shí)來告訴編譯器,具體操作是在”Build Setting”–>“Swift Compiler-Custom Flags”–>“Other Swift Flags”–>“Debug”中添加所需要的配置選項(xiàng)。如我們想添加常用的DEGUB選項(xiàng),則可以在此加上”-D DEBUG”。這樣我們就可以在代碼中來執(zhí)行一些debug與release時(shí)不同的操作,如
- #if DEBUG
- let totalSeconds = totalMinutes
- #else
- let totalSeconds = totalMinutes * 60
- #endif
- 一個(gè)簡(jiǎn)單的條件編譯聲明如下所示:
- #if build configuration
- statements
- #else
- statements
- #endif
當(dāng)然,statements中可以包含0個(gè)或多個(gè)有效的swift的statements,其中可以包括表達(dá)式、語句、和控制流語句。另外,我們也可以使用&&和||操作符來組合多個(gè)build configuration,同時(shí),可以使用!操作符來對(duì)build configuration取反,如下所示:
- #if build configuration && !build configuration
- statements
- #elseif build configuration
- statements
- #else
- statements
- #endif
需要注意的是,在swift中,條件編譯語句必須在語法上是有效的,因?yàn)榧词惯@些代碼不會(huì)被編譯,swift也會(huì)對(duì)其進(jìn)行語法檢查。
#p#
鍵盤事件
在涉及到表單輸入的界面中,我們通常需要監(jiān)聽一些鍵盤事件,并根據(jù)實(shí)際需要來執(zhí)行相應(yīng)的操作。如,鍵盤彈起時(shí),要讓我們的UIScrollView自動(dòng)收縮,以能看到整個(gè)UIScrollView的內(nèi)容。為此,在UIWindow.h中定義了如下6個(gè)通知常量,來配合鍵盤在不同時(shí)間點(diǎn)的事件處理:
- UIKeyboardWillShowNotification // 鍵盤顯示之前
- UIKeyboardDidShowNotification // 鍵盤顯示完成后
- UIKeyboardWillHideNotification // 鍵盤隱藏之前
- UIKeyboardDidHideNotification // 鍵盤消息之后
- UIKeyboardWillChangeFrameNotification // 鍵盤大小改變之前
- UIKeyboardDidChangeFrameNotification // 鍵盤大小改變之后
- 這幾個(gè)通知的object對(duì)象都是nil。而userInfo字典都包含了一些鍵盤的信息,主要是鍵盤的位置大小信息,我們可以通過使用以下的key來獲取字典中對(duì)應(yīng)的值:
- // 鍵盤在動(dòng)畫開始前的frame
- let UIKeyboardFrameBeginUserInfoKey: String
- // 鍵盤在動(dòng)畫線束后的frame
- let UIKeyboardFrameEndUserInfoKey: String
- // 鍵盤的動(dòng)畫曲線
- let UIKeyboardAnimationCurveUserInfoKey: String
- // 鍵盤的動(dòng)畫時(shí)間
- let UIKeyboardAnimationDurationUserInfoKey: String
在此,我感興趣的是鍵盤事件的調(diào)用順序和如何獲取鍵盤的大小,以適當(dāng)?shù)恼{(diào)整視圖的大小。
從定義的鍵盤通知的類型可以看到,實(shí)際上我們關(guān)注的是三個(gè)階段的鍵盤的事件:顯示、隱藏、大小改變。在此我們?cè)O(shè)定兩個(gè)UITextField,它們的鍵盤類型不同:一個(gè)是普通鍵盤,一個(gè)是數(shù)字鍵盤。我們監(jiān)聽所有的鍵盤事件,并打印相關(guān)日志(在此就不貼代碼了),直接看結(jié)果。
1) 當(dāng)我們讓textField1獲取輸入焦點(diǎn)時(shí),打印的日志如下:
- keyboard will change
- keyboard will show
- keyboard did change
- keyboard did show
2) 在不隱藏鍵盤的情況下,讓textField2獲取焦點(diǎn),打印的日志如下:
- keyboard will change
- keyboard will show
- keyboard did change
- keyboard did show
3) 再收起鍵盤,打印的日志如下:
- keyboard will change
- keyboard will hide
- keyboard did change
- keyboard did hide
從上面的日志可以看出,不管是鍵盤的顯示還是隱藏,都會(huì)發(fā)送大小改變的通知,而且是在show和hide的對(duì)應(yīng)事件之前。而在大小不同的鍵盤之間切換時(shí),除了發(fā)送change事件外,還會(huì)發(fā)送show事件(不發(fā)送hide事件)。
另外還有兩點(diǎn)需要注意的是:
如果是在兩個(gè)大小相同的鍵盤之間切換,則不會(huì)發(fā)送任何消息
如果是普通鍵盤中類似于中英文鍵盤的切換,只要大小改變了,都會(huì)發(fā)送一組或多組與上面2)相同流程的消息
了解了事件的調(diào)用順序,我們就可以根據(jù)自己的需要來決定在哪個(gè)消息處理方法中來執(zhí)行操作。為此,我們需要獲取一些有用的信息。這些信息是封裝在通知的userInfo中,通過上面常量key來獲取相關(guān)的值。通常我們關(guān)心的是UIKeyboardFrameEndUserInfoKey,來獲取動(dòng)畫完成后,鍵盤的frame,以此來計(jì)算我們的scroll view的高度。另外,我們可能希望scroll view高度的變化也是通過動(dòng)畫來過渡的,此時(shí)UIKeyboardAnimationCurveUserInfoKey和UIKeyboardAnimationDurationUserInfoKey就有用了。
我們可以通過以下方式來獲取這些值:
- if let dict = notification.userInfo {
- var animationDuration: NSTimeInterval = 0
- var animationCurve: UIViewAnimationCurve = .EaseInOut
- var keyboardEndFrame: CGRect = CGRectZero
- dict[UIKeyboardAnimationCurveUserInfoKey]?.getValue(&animationCurve)
- dict[UIKeyboardAnimationDurationUserInfoKey]?.getValue(&animationDuration)
- dict[UIKeyboardFrameEndUserInfoKey]?.getValue(&keyboardEndFrame)
- ......
- }
實(shí)際上,userInfo中還有另外三個(gè)值,只不過這幾個(gè)值從iOS 3.2開始就已經(jīng)廢棄不用了。所以我們不用太關(guān)注。
***說下表單。一個(gè)表單界面看著比較簡(jiǎn)單,但交互和UI總是能想出各種方法來讓它變得復(fù)雜,而且其實(shí)里面設(shè)計(jì)到的細(xì)節(jié)還是很多的。像我們金融類的App,通常都會(huì)涉及到大量的表單輸入,所以如何做好,還是需要花一番心思的。空閑時(shí),打算總結(jié)一下,寫一篇文章。
#p#
零碎
自定義UIPickerView的行
UIPickerView的主要內(nèi)容實(shí)際上并不多,主要是一個(gè)UIPickerView類和對(duì)應(yīng)的UIPickerViewDelegate,UIPickerViewDataSource協(xié)議,分別表示代理和數(shù)據(jù)源。在此不細(xì)說這些,只是解答我們遇到的一個(gè)小需求。
通常,UIPickerView是可以定義多列內(nèi)容的,比如年、月、日三列,這些列之間相互不干擾,可以自已滾自己的,不礙別人的事。不過,我們有這么一個(gè)需求,也是有三列,但這三列需要一起滾。嗯,這個(gè)就需要另行處理了。
- 在UIPickerViewDelegate中,聲明了下面這樣一個(gè)代理方法:
- - (UIView *)pickerView:(UIPickerView *)pickerView
- viewForRow:(NSInteger)row
- forComponent:(NSInteger)component
- reusingView:(UIView *)view
我們通過這個(gè)方法就可以來自定義行的視圖。時(shí)間不早,廢話就不多說了,直接上代碼吧:
- - (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
- PickerViewCell *pickerCell = (PickerViewCell *)view;
- if (!pickerCell) {
- NSInteger column = 3;
- pickerCell = [[PickerViewCell alloc] initWithFrame:(CGRect){CGPointZero, [UIScreen mainScreen].bounds.size.width, 45.0f} column:column];
- }
- [pickerCell setLabelTexts:@[...]];
- return pickerCell;
- }
我們定義了一個(gè)PickerViewCell視圖,里面根據(jù)我們的傳入的column參數(shù)來等分放置column個(gè)UILabel,并通過setLabelTexts來設(shè)置每個(gè)UILabel的文本。當(dāng)然,我們也可以在PickerViewCell去定義UILabel的外觀顯示。就是這么簡(jiǎn)單。
不過,還有個(gè)需要注意的就是,雖然看上去是顯示了3列,但實(shí)際上是按1列來處理的,所以下面的實(shí)現(xiàn)應(yīng)該是返回1:
- - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
- return 1;
- }
Constructing an object of class type ‘**’ with a metatype value must use a ‘required’ initializer.
Swift中”[AnyObject]? does not have a member named generator” 問題的處理
有個(gè)小需求,需要遍歷當(dāng)前導(dǎo)航控制器棧的所有ViewController。UINavigationController類自身的viewControllers屬性返回的是一個(gè)[AnyObject]!數(shù)組,不過由于我的導(dǎo)航控制器本身有可能是nil,所以我獲取到的ViewController數(shù)組如下:
- var myViewControllers: [AnyObject]? = navigationController?.viewControllers
- 獲取到的myViewControllers是一個(gè)[AnyObject]?可選類型,這時(shí)如果我直接去遍歷myViewControllers,如下代碼所示
- for controller in myViewControllers {
- ...
- }
編譯器會(huì)報(bào)錯(cuò),提示如下:
- [AnyObject]? does not have a member named "Generator"
實(shí)際上,不管是[AnyObject]?還是其它的諸如[String]?類型,都會(huì)報(bào)這個(gè)錯(cuò)。其原因是可選類型只是個(gè)容器,它與其所包裝的值是不同的類型,也就是說[AnyObject]是一個(gè)數(shù)組類型,但[AnyObject]?并不是數(shù)組類型。我們可以迭代一個(gè)數(shù)組,但不是迭代一個(gè)非集合類型。
在stackoverflow上有這樣一個(gè)有趣的比方,我犯懶就直接貼出來了:
To understand the difference, let me make a real life example: you buy a new TV on ebay, the package is shipped to you, the first thing you do is to check if the package (the optional) is empty (nil). Once you verify that the TV is inside, you have to unwrap it, and put the box aside. You cannot use the TV while it's in the package. Similarly, an optional is a container: it is not the value it contains, and it doesn't have the same type. It can be empty, or it can contain a valid value.
所有,這里的處理應(yīng)該是:
- if let controllers = myViewControllers {
- for controller in controllers {
- ......
- }
- }


























