App Transport Security(ATS)
Swift2出來了,還是得與時俱進啊,不然就成老古董了。再者它開源了,又有事情要做了。當個程式猿真是累啊,一直在追,可從來沒追上,剛有那麼點念想了,人家又踩了腳油門。
一個月又要過去了,說好的一月兩到三篇的,看來希望也是有點渺茫了。本來想好好整理下殭屍物件的內容,看看時間也不多了,也只好放到後面了。這一期沒啥好內容,質量也不高,大家湊合著看吧,有疏漏還請大家指出,我一定好好改正。
這一期主要有三個內容:
- Tint Color
- Build Configurations in Swift
- 鍵盤事件
Tint Color
在iOS 7後,UIView新增加了一個tintColor屬性,這個屬性定義了一個非預設的著色顏色值,其值的設定會影響到以檢視為根檢視的整個檢視層次結構。它主要是應用到諸如app圖示、導航欄、按鈕等一些控制元件上,以獲取一些有意思的視覺效果。
tintColor屬性的宣告如下:
var tintColor: UIColor!
預設情況下,一個檢視的tintColor是為nil的,這意味著檢視將使用父檢視的tint color值。當我們指定了一個檢視的tintColor後,這個色值會自動傳播到檢視層次結構(以當前檢視為根檢視)中所有的子檢視上。如果系統在檢視層次結構中沒有找到一個非預設的tintColor值,則會使用系統定義的顏色值(藍色,RGB值為[0,0.478431,1],我們可以在IB中看到這個顏色)。因此,這個值總是會返回一個顏色值,即我們沒有指定它。
與tintColor屬性相關的還有個tintAdjustmentMode屬性,它是一個列舉值,定義了tint color的調整模式。其宣告如下:
var tintAdjustmentMode: UIViewTintAdjustmentMode
列舉UIViewTintAdjustmentMode的定義如下:
enum UIViewTintAdjustmentMode : Int {
case Automatic // 檢視的著色調整模式與父檢視一致
case Normal // 檢視的tintColor屬性返回完全未修改的檢視著色顏色
case Dimmed // 檢視的tintColor屬性返回一個去飽和度的、變暗的檢視著色顏色
}
因此,當tintAdjustmentMode屬性設定為Dimmed時,tintColor的顏色值會自動變暗。而如果我們在檢視層次結構中沒有找到預設值,則該值預設是Normal。
與tintColor相關的還有一個tintColorDidChange方法,其宣告如下:
func tintColorDidChange()
這個方法會在檢視的tintColor或tintAdjustmentMode屬性改變時自動呼叫。另外,如果當前檢視的父檢視的tintColor或tintAdjustmentMode屬性改變時,也會呼叫這個方法。我們可以在這個方法中根據需要去重新整理我們的檢視。
示例
接下來我們通過示例來看看tintColor的強大功能(示例盜用了Sam Davies寫的一個例子,具體可以檢視iOS7 Day-by-Day :: Day 6 :: Tint Color,我就負責搬磚,用swift實現了一下,程式碼可以在這裡下載)。
先來看看最終效果吧(以下都是盜圖,請見諒,太懶了):
這個介面包含的元素主要有UIButton, UISlider, UIProgressView, UIStepper, UIImageView, ToolBar和一個自定義的子檢視CustomView。接下來我們便來看看修改檢視的tintColor會對這些控制元件產生什麼樣的影響。
在ViewController的viewDidLoad方法中,我們做了如下設定:
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")
// 設定渲染模式
shinobiHead = shinobiHead?.imageWithRenderingMode(.AlwaysTemplate)
self.tintedImageView?.image = shinobiHead
self.tintedImageView?.contentMode = .ScaleAspectFit
}
首先,我們嘗試列印預設的tintColor和tintAdjustmentMode,分別輸出了[UIDeviceRGBColorSpace 0 0.478431 1 1]和1,這是在我們沒有對整個檢視層次結構設定任何tint color相關的值的情況下的輸出。可以看到,雖然我們沒有設定tintColor,但它仍然返回了系統的預設值;而tintAdjustmentMode則預設返回Normal的原始值。
接下來,我們顯式設定tintAdjustmentMode的值為Normal,同時設定UIImageView的圖片及渲染模式。
當我們點選”Change Color”按鈕時,會執行以下的事件處理方法:
@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
}
這段程式碼主要是隨機生成一個顏色值,並賦值給self.view的tintColor屬性,同時去更新進度條的tintColor值。
注:有些控制元件的特定組成部件的tint color由特定的屬性控制,例如進度就有2個tint color:一個用於進度條本身,另一個用於背景。
點選”Change Color”按鈕,可得到以下效果:
可以看到,我們在示例中並有沒手動去設定UIButton, UISlider, UIStepper, UIImageView, ToolBar等子檢視的顏色值,但隨著self.view的tintColor屬性顏色值的變化,這些控制元件的外觀也同時跟著改變。也就是說self.view的tintColor屬性顏色值的變化,影響到了以self.view為根檢視的整個檢視層次結果中所有子檢視的外觀。
看來tintColor還是很強大的嘛。
在介面中還有個UISwitch,這個是用來開啟關閉dim tint的功能,其對應處理方法如下:
@IBAction func dimTimtHandler(sender: AnyObject) {
if let isOn = self.dimTintSwitch?.on {
self.view.tintAdjustmentMode = isOn ? .Dimmed : .Normal
}
updateViewConstraints()
}
當tintAdjustmentMode設定Dimmed時,其實際的效果是整個色值都變暗(此處無圖可盜)。
另外,我們在子檢視CustomView中重寫了tintColorDidChange方法,以監聽tintColor的變化,以更新我們的自定義檢視,其實現如下:
override func tintColorDidChange() {
tintColorLabel.textColor = self.tintColor
tintColorBlock.backgroundColor = self.tintColor
}
所以方框和”Tint color label”顏色是跟著子檢視的tintColor來變化的,而子檢視的tintColor又是繼承自父檢視的。
在這個示例中,比較有意思的是還是對圖片的處理。對影象的處理比較簡單粗暴,對一個畫素而言,如果它的alpha值為1的話,就將它的顏色設定為tint color;如果不為1的話,則設定為透明的。示例中的忍者頭像就是這麼處理的。不過我們需要設定圖片的imageWithRenderingMode屬性為AlwaysTemplate,這樣渲染圖片時會將其渲染為一個模板而忽略它的顏色資訊,如程式碼所示:
var shinobiHead = UIImage(named: "shinobihead")
// 設定渲染模式
shinobiHead = shinobiHead?.imageWithRenderingMode(.AlwaysTemplate)
題外話
插個題外話,跟主題關係不大。
在色彩理論(color theory)中,一個tint color是一種顏色與白色的混合。與之類似的是shade color和tone color。shade color是將顏色與黑色混合,tone color是將顏色與灰色混合。它們都是基於Hues色調的。這幾個色值的效果如下圖所示:
小結
如果我們想指定整個App的tint color,則可以通過設定window的tint color。這樣同一個window下的所有子檢視都會繼承此tint color。
當彈出一個alert或者action sheet時,iOS7會自動將後面檢視的tint color變暗。此時,我們可以在自定義檢視中重寫tintColorDidChange方法來執行我們想要的操作。
有些複雜控制元件,可以有多個tint color,不同的tint color控制元件不同的部分。如上面提到的UIProgressView,又如navigation bars, tab bars, toolbars, search bars, scope bars等,這些控制元件的背景著色顏色可以使用barTintColor屬性來處理。
參考
Build Configurations in Swift
在Objective-C中,我們經常使用預處理指令來幫助我們根據不同的平臺執行不同的程式碼,以讓我們的程式碼支援不同的平臺,如:
#if TARGET_OS_IPHONE
#define MAS_VIEW UIView
#elif TARGET_OS_MAC
#define MAS_VIEW NSView
#endif
在swift中,由於對C語言支援沒有Objective-C來得那麼友好(暫時不知swift 2到C的支援如何),所以我們無法像在Objective-C中那樣自如而舒坦地使用預處理指令。
不過,swift也提供了自己的方式來支援條件編譯,即使用build configurations(構建配置)。Build configurations已經包含了字面量true和false,以及兩個平臺測試函式os()和arch()。
其中os()用於測試系統型別,可傳入的引數包含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”一節(session 233)中,Elizabeth Reid將這種方式稱為Shimming
遺憾的是,os()只能檢測系統型別,而無法檢測系統的版本,所以這些工作只能放在執行時去處理。關於如何檢測系統的版本,Mattt Thompson老大在它的Swift System Version Checking一文中給了我們答案。
我們再來看看arch()。arch()用於測試CPU的架構,可傳入的值包括x86_64, arm, arm64, i386。需要注意的是arch(arm)對於ARM 64的裝置來說,不會返回true。而arch(i386)在32位的iOS模擬器上編譯時會返回true。
如果我們想自定義一些在除錯期間使用的編譯配置選項,則可以使用-D標識來告訴編譯器,具體操作是在”Build Setting”–>“Swift Compiler-Custom Flags”–>“Other Swift Flags”–>“Debug”中新增所需要的配置選項。如我們想新增常用的DEGUB選項,則可以在此加上”-D DEBUG”。這樣我們就可以在程式碼中來執行一些debug與release時不同的操作,如
#if DEBUG
let totalSeconds = totalMinutes
#else
let totalSeconds = totalMinutes * 60
#endif
一個簡單的條件編譯宣告如下所示:
#if build configuration
statements
#else
statements
#endif
當然,statements中可以包含0個或多個有效的swift的statements,其中可以包括表示式、語句、和控制流語句。另外,我們也可以使用&&和||操作符來組合多個build configuration,同時,可以使用!操作符來對build configuration取反,如下所示:
#if build configuration && !build configuration
statements
#elseif build configuration
statements
#else
statements
#endif
需要注意的是,在swift中,條件編譯語句必須在語法上是有效的,因為即使這些程式碼不會被編譯,swift也會對其進行語法檢查。
參考
鍵盤事件
在涉及到表單輸入的介面中,我們通常需要監聽一些鍵盤事件,並根據實際需要來執行相應的操作。如,鍵盤彈起時,要讓我們的UIScrollView自動收縮,以能看到整個UIScrollView的內容。為此,在UIWindow.h中定義瞭如下6個通知常量,來配合鍵盤在不同時間點的事件處理:
UIKeyboardWillShowNotification // 鍵盤顯示之前
UIKeyboardDidShowNotification // 鍵盤顯示完成後
UIKeyboardWillHideNotification // 鍵盤隱藏之前
UIKeyboardDidHideNotification // 鍵盤訊息之後
UIKeyboardWillChangeFrameNotification // 鍵盤大小改變之前
UIKeyboardDidChangeFrameNotification // 鍵盤大小改變之後
這幾個通知的object物件都是nil。而userInfo字典都包含了一些鍵盤的資訊,主要是鍵盤的位置大小資訊,我們可以通過使用以下的key來獲取字典中對應的值:
// 鍵盤在動畫開始前的frame
let UIKeyboardFrameBeginUserInfoKey: String
// 鍵盤在動畫線束後的frame
let UIKeyboardFrameEndUserInfoKey: String
// 鍵盤的動畫曲線
let UIKeyboardAnimationCurveUserInfoKey: String
// 鍵盤的動畫時間
let UIKeyboardAnimationDurationUserInfoKey: String
在此,我感興趣的是鍵盤事件的呼叫順序和如何獲取鍵盤的大小,以適當的調整檢視的大小。
從定義的鍵盤通知的型別可以看到,實際上我們關注的是三個階段的鍵盤的事件:顯示、隱藏、大小改變。在此我們設定兩個UITextField,它們的鍵盤型別不同:一個是普通鍵盤,一個是數字鍵盤。我們監聽所有的鍵盤事件,並列印相關日誌(在此就不貼程式碼了),直接看結果。
1) 當我們讓textField1獲取輸入焦點時,列印的日誌如下:
keyboard will change
keyboard will show
keyboard did change
keyboard did show
2) 在不隱藏鍵盤的情況下,讓textField2獲取焦點,列印的日誌如下:
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
從上面的日誌可以看出,不管是鍵盤的顯示還是隱藏,都會發送大小改變的通知,而且是在show和hide的對應事件之前。而在大小不同的鍵盤之間切換時,除了傳送change事件外,還會發送show事件(不傳送hide事件)。
另外還有兩點需要注意的是:
- 如果是在兩個大小相同的鍵盤之間切換,則不會發送任何訊息
- 如果是普通鍵盤中類似於中英文鍵盤的切換,只要大小改變了,都會發送一組或多組與上面2)相同流程的訊息
瞭解了事件的呼叫順序,我們就可以根據自己的需要來決定在哪個訊息處理方法中來執行操作。為此,我們需要獲取一些有用的資訊。這些資訊是封裝在通知的userInfo中,通過上面常量key來獲取相關的值。通常我們關心的是UIKeyboardFrameEndUserInfoKey,來獲取動畫完成後,鍵盤的frame,以此來計算我們的scroll view的高度。另外,我們可能希望scroll view高度的變化也是通過動畫來過渡的,此時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)
......
}
實際上,userInfo中還有另外三個值,只不過這幾個值從iOS 3.2開始就已經廢棄不用了。所以我們不用太關注。
最後說下表單。一個表單介面看著比較簡單,但互動和UI總是能想出各種方法來讓它變得複雜,而且其實裡面設計到的細節還是很多的。像我們金融類的App,通常都會涉及到大量的表單輸入,所以如何做好,還是需要花一番心思的。空閒時,打算總結一下,寫一篇文章。
參考
零碎
自定義UIPickerView的行
UIPickerView的主要內容實際上並不多,主要是一個UIPickerView類和對應的UIPickerViewDelegate,UIPickerViewDataSource協議,分別表示代理和資料來源。在此不細說這些,只是解答我們遇到的一個小需求。
通常,UIPickerView是可以定義多列內容的,比如年、月、日三列,這些列之間相互不干擾,可以自已滾自己的,不礙別人的事。不過,我們有這麼一個需求,也是有三列,但這三列需要一起滾。嗯,這個就需要另行處理了。
在UIPickerViewDelegate中,聲明瞭下面這樣一個代理方法:
- (UIView *)pickerView:(UIPickerView *)pickerView
viewForRow:(NSInteger)row
forComponent:(NSInteger)component
reusingView:(UIView *)view
我們通過這個方法就可以來自定義行的檢視。時間不早,廢話就不多說了,直接上程式碼吧:
- (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;
}
我們定義了一個PickerViewCell檢視,裡面根據我們的傳入的column引數來等分放置column個UILabel,並通過setLabelTexts來設定每個UILabel的文字。當然,我們也可以在PickerViewCell去定義UILabel的外觀顯示。就是這麼簡單。
不過,還有個需要注意的就是,雖然看上去是顯示了3列,但實際上是按1列來處理的,所以下面的實現應該是返回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” 問題的處理
有個小需求,需要遍歷當前導航控制器棧的所有ViewController。UINavigationController類自身的viewControllers屬性返回的是一個[AnyObject]!陣列,不過由於我的導航控制器本身有可能是nil,所以我獲取到的ViewController陣列如下:
var myViewControllers: [AnyObject]? = navigationController?.viewControllers
獲取到的myViewControllers是一個[AnyObject]?可選型別,這時如果我直接去遍歷myViewControllers,如下程式碼所示
for controller in myViewControllers {
...
}
編譯器會報錯,提示如下:
[AnyObject]? does not have a member named "Generator"
實際上,不管是[AnyObject]?還是其它的諸如[String]?型別,都會報這個錯。其原因是可選型別只是個容器,它與其所包裝的值是不同的型別,也就是說[AnyObject]是一個數組型別,但[AnyObject]?並不是陣列型別。我們可以迭代一個數組,但不是迭代一個非集合型別。
在stackoverflow上有這樣一個有趣的比方,我犯懶就直接貼出來了:
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.
所有,這裡的處理應該是:
if let controllers = myViewControllers {
for controller in controllers {
......
}
}