本文本人閱讀@onevcat 的 《Swifter - 100 個(gè)Swift 必備 tips》后所做筆記,有興趣可以去查看原書
創(chuàng)建check
func check(_ input: Any) {
print("type is \(type(of: input)), value is \(input)")
}
創(chuàng)建單例
class MyManager {
static let sharedInstance = MyManager()
private init() {}
}
struct MyManager {
static let sharedInstance = MyManager()
private init() {}
}
protocol 方法聲明為 mutating
如果需要修改對象中的屬性锭亏,需要增加mutating
protocol Vehicle
{
var numberOfWheels: Int {get}
var color: UIColor {get set}
mutating func changeColor()
}
struct MyCar: Vehicle {
let numberOfWheels = 4
var color = UIColor.blue
mutating func changeColor() {
color = UIColor.red
}
}
多元組 (tuple)
func swapMe<T>(a: inout T, b: inout T) {
(a,b) = (b,a)
}
var a = 1
var b = 2
swapMe(a: &a, b: &b)
??
獲取可選類型的值
func getOptional<T>(_ optional: T?,defaultValue: T) -> T
{
switch optional {
case .some(let value):
return value
case .none:
return defaultValue
}
}
var level : Int?
var startLevel = 1
var c = getOptional(level, defaultValue: 1)
Optional Chaining
struct Pet {
var toy: Toy?
}
struct Child {
var pet: Pet?
}
extension Toy {
func play() {}
}
//()? (Void?) 回值可能為 nil ,是可選的
let playClosure = {(child: Child) in child.pet?.toy?.play()}
let xiaoming = Child()
if let result = playClosure(xiaoming) {
print("好開心~")
} else {
print("沒有玩具可以玩 :(")
}
check(playClosure)
FUNC 的參數(shù)修飾
func makeIncrementor(addNumber: Int) -> ((inout Int) -> ()) {
func incrementor(variable: inout Int) -> () {
variable += addNumber;
}
return incrementor;
}
方法參數(shù)名稱省略 {#FUNC-NAMING}
實(shí)例方法
extension Car {
func moveToX(x: Int, y: Int) {
//...
}
}
car.moveToX(x: 10, y: 20)
類方法
struct Car {}
extension Car {
static func findACar(name: String) -> Car? {
var result: Car?
result = Car()
return result
}
}
let myPorsche = Car.findACar(name: "Porsche")
全局方法
// 注意剥纷,現(xiàn)在不在 Car 中贴妻,而是在一個(gè)全局作用域
func findACar(name: String, color: UIColor) -> Car? {
let result: Car? = Car()
//...
return result
}
let myFerrari = findACar(name: "Ferrari",color: UIColor.red)
SWIFT 命令行工具
直接將一個(gè) .swift 文件作為命令行工具的輸入,這樣里面的代碼也會被自動(dòng)地編譯和執(zhí)行。我們甚至還可以在 .swift 文件最上面加上命令行工具的路徑孕暇,然后將文件權(quán)限改為可執(zhí)行,之后就可以直接執(zhí)行這個(gè) .swift 文件了:
#!/usr/bin/env swift
print("hello")
// Terminal
> chmod 755 hello.swift
> ./hello.swift
// 輸出:
hello
swiftc 來進(jìn)行編譯
// MyClass.swift
class MyClass {
let name = "XiaoMing"
func hello() {
print("Hello \(name)")
}
}
// main.swift
let object = MyClass()
object.hello()
> swiftc MyClass.swift main.swift
字面量轉(zhuǎn)換
數(shù)字辐董,字符串或者是布爾值
let aNumber = 3
let aString = "Hello"
let aBool = true
Array 和 Dictionary
let anArray = [1,2,3]
let aDictionary = ["key1": "value1", "key2": "value2"]
這些初始化方法中去調(diào)用原來的 init(name value: String)甚亭,這種情況下我們需要在這些初始化方法前加上 convenience
class Person {
let name: String
init(name value: String) {
self.name = value
}
convenience init(value: String) {
self.init(name: value)
}
}
模式匹配
let contact = ("http://onevcat.com", "onev@onevcat.com")
let mailRegex: NSRegularExpression
let siteRegex: NSRegularExpression
mailRegex =
try ~/"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
siteRegex =
try ~/"^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$"
switch contact {
case (siteRegex, mailRegex): print("同時(shí)擁有有效的網(wǎng)站和郵箱")
case (_, mailRegex): print("只擁有有效的郵箱")
case (siteRegex, _): print("只擁有有效的網(wǎng)站")
default: print("嘛都沒有")
}
// 輸出
// 同時(shí)擁有網(wǎng)站和郵箱
方法嵌套
func makeIncrementor(addNumber: Int) -> ((inout Int) -> Void {
func incrementor(inout variable: Int) -> Void {
variable += addNumber;
}
return incrementor;
}
命名空間
在我們進(jìn)行 app 開發(fā)時(shí),默認(rèn)添加到 app 的主 target 的內(nèi)容都是處于同一個(gè)命名空間中的怔匣,我們可以通過創(chuàng)建 Cocoa (Touch) Framework 的 target 的方法來新建一個(gè) module握联,這樣我們就可以在兩個(gè)不同的 target 中添加同樣名字的類型了:
// MyFramework.swift
// 這個(gè)文件存在于 MyFramework.framework 中
public class MyClass {
public class func hello() {
print("hello from framework")
}
}
// MyApp.swift
// 這個(gè)文件存在于 app 的主 target 中
class MyClass {
class func hello() {
print("hello from app")
}
}
在使用時(shí),如果出現(xiàn)可能沖突的時(shí)候每瞒,我們需要在類型名稱前面加上 module 的名字 (也就是 target 的名字):
MyClass.hello()
// hello from app
MyFramework.MyClass.hello()
// hello from framework
使用類型嵌套的方法來指定訪問的范圍金闽。常見做法是將名字重復(fù)的類型定義到不同的 struct 中,以此避免沖突剿骨。這樣在不使用多個(gè) module 的情況下也能取得隔離同樣名字的類型的效果:
struct MyClassContainer1 {
class MyClass {
class func hello() {
print("hello from MyClassContainer1")
}
}
}
struct MyClassContainer2 {
class MyClass {
class func hello() {
print("hello from MyClassContainer2")
}
}
}
使用時(shí):
MyClassContainer1.MyClass.hello()
MyClassContainer2.MyClass.hello()
ANY 和 ANYOBJECT
protocol AnyObject {
}
使用 Any 和 AnyObject 并不是什么令人愉悅的事情代芜,正如開頭所說,這都是為妥協(xié)而存在的浓利。如果在我們自己的代碼里需要大量經(jīng)常地使用這兩者的話挤庇,往往意味著代碼可能在結(jié)構(gòu)和設(shè)計(jì)上存在問題钞速,應(yīng)該及時(shí)重新審視。簡單來說罚随,我們最好避免依賴和使用這兩者玉工,而去嘗試明確地指出確定的類型。
隨機(jī)數(shù)生成
func randomInRange(range: Range<Int>) -> Int {
let count = UInt32(range.endIndex - range.startIndex)
return Int(arc4random_uniform(count)) + range.startIndex
}
for _ in 0...100 {
print(randomInRange(1...6))
}
TYPEALIAS 和泛型接口
typealias 是用來為已經(jīng)存在的類型重新定義名字的淘菩,通過命名遵班,可以使代碼變得更加清晰。
import UIKit
typealias Location = CGPoint
typealias Distance = Double
func distanceBetweenPoint(location: Location,
toLocation: Location) -> Distance {
let dx = Distance(location.x - toLocation.x)
let dy = Distance(location.y - toLocation.y)
return sqrt(dx * dx + dy * dy)
}
let origin: Location = Location(x: 0, y: 0)
let point: Location = Location(x: 1, y: 1)
let distance: Distance = distanceBetweenPoint(origin, toLocation: point)
條件編譯
#if <condition>
#elseif <condition>
#else
#endif
編譯標(biāo)記
// MARK: 以外潮改,Xcode 還支持另外幾種標(biāo)記狭郑,它們分別是 // TODO: 和 // FIXME:。???
可變參數(shù)函數(shù)
func sum(input: Int...) -> Int {
return input.reduce(0, combine: +)
}
print(sum(1,2,3,4,5))
// 輸出:15
Swift 的可變參數(shù)十分靈活
func myFunc(numbers: Int..., string: String) {
numbers.forEach {
for i in 0..<$0 {
print("\(i + 1): \(string)")
}
}
}
myFunc(1, 2, 3, string: "hello")
// 輸出:
// 1: hello
// 1: hello
// 2: hello
// 1: hello
// 2: hello
// 3: hello
Swift 的 NSString 格式化的聲明就是這樣處理的:
extension NSString {
convenience init(format: NSString, _ args: CVarArgType...)
//...
}
@UIAPPLICATIONMAIN???
UIApplicationMain 幫助我們自動(dòng)生成了 Swift 的 app 也是需要 main 函數(shù)的
初始化方法順序
class Cat {
var name: String
init() {
name = "cat"
}
}
class Tiger: Cat {
let power: Int
override init() {
power = 10
super.init()
name = "tiger"
}
}
class Cat {
var name: String
init() {
name = "cat"
}
}
class Tiger: Cat {
let power: Int
override init() {
power = 10
// 如果我們不需要打改變 name 的話汇在,
// 雖然我們沒有顯式地對 super.init() 進(jìn)行調(diào)用
// 不過由于這是初始化的最后了翰萨,Swift 替我們自動(dòng)完成了
}
}
那些有用的tips2
DESIGNATED,CONVENIENCE 和 REQUIRED
對于某些我們希望子類中一定實(shí)現(xiàn)的 designated 初始化方法糕殉,我們可以通過添加 required 關(guān)鍵字進(jìn)行限制亩鬼,強(qiáng)制子類對這個(gè)方法重寫實(shí)現(xiàn)。這樣做的最大的好處是可以保證依賴于某個(gè) designated 初始化方法的 convenience 一直可以被使用阿蝶。
初始化返回 NIL
Apple 已經(jīng)為我們加上了初始化方法中返回 nil 的能力雳锋。我們可以在 init 聲明時(shí)在其后加上一個(gè) ? 或者 ! 來表示初始化失敗時(shí)可能返回 nil。比如為 Int 添加一個(gè)接收 String 作為參數(shù)的初始化方法羡洁。我們希望在方法中對中文和英文的數(shù)據(jù)進(jìn)行解析玷过,并輸出 Int 結(jié)果。對其解析并初始化的時(shí)候筑煮,就可能遇到初始化失敗的情況:
extension Int {
init?(fromString: String) {
self = 0
var digit = fromString.characters.count - 1
for c in fromString.characters {
var number = 0
if let n = Int(String(c)) {
number = n
} else {
switch c {
case "一": number = 1
case "二": number = 2
case "三": number = 3
case "四": number = 4
case "五": number = 5
case "六": number = 6
case "七": number = 7
case "八": number = 8
case "九": number = 9
case "零": number = 0
default: return nil
}
}
self = self + number * Int(pow(10, Double(digit)))
digit = digit - 1
}
}
}
let number1 = Int(fromString: "12")
// {Some 12}
let number2 = Int(fromString: "三二五")
// {Some 325}
let number3 = Int(fromString: "七9八")
// {Some 798}
let number4 = Int(fromString: "吃了么")
// nil
let number5 = Int(fromString: "1a4n")
// nil
而對應(yīng)地辛蚊,可能返回 nil 的 init 方法都加上了 ? 標(biāo)記:
convenience init?(string URLString: String)
PROTOCOL 組合
protocol A {
func bar() -> Int
}
protocol B {
func bar() -> String
}
兩個(gè)接口中 bar() 只有返回值的類型不同。我們?nèi)绻幸粋€(gè)類型 Class 同時(shí)實(shí)現(xiàn)了 A 和 B真仲,我們要怎么才能避免和解決調(diào)用沖突呢袋马?
class Class: A, B {
func bar() -> Int {
return 1
}
func bar() -> String {
return "Hi"
}
}
這樣一來,對于 bar()秸应,只要在調(diào)用前進(jìn)行類型轉(zhuǎn)換就可以了:
let instance = Class()
let num = (instance as A).bar() // 1
let str = (instance as B).bar() // "Hi"
STATIC 和 CLASS
protocol MyProtocol {
static func foo() -> String
}
struct MyStruct: MyProtocol {
static func foo() -> String {
return "MyStruct"
}
}
enum MyEnum: MyProtocol {
static func foo() -> String {
return "MyEnum"
}
}
class MyClass: MyProtocol {
// 在 class 中可以使用 class
class func foo() -> String {
return "MyClass.foo()"
}
// 也可以使用 static
static func bar() -> String {
return "MyClass.bar()"
}
}
現(xiàn)在只需要記住結(jié)論飞蛹,在任何時(shí)候使用 static 應(yīng)該都是沒有問題的。
可選接口和接口擴(kuò)展
protocol OptionalProtocol {
func optionalMethod() // 可選
func necessaryMethod() // 必須
func anotherOptionalMethod() // 可選
}
extension OptionalProtocol {
func optionalMethod() {
print("Implemented in extension")
}
func anotherOptionalMethod() {
print("Implemented in extension")
}
}
class MyClass: OptionalProtocol {
func necessaryMethod() {
print("Implemented in Class3")
}
func optionalMethod() {
print("Implemented in Class3")
}
}
let obj = MyClass()
obj.necessaryMethod() // Implemented in Class3
obj.optionalMethod() // Implemented in Class3
obj.anotherOptionalMethod() // Implemented in extension
多類型和容器
import Foundation
enum IntOrString {
case IntValue(Int)
case StringValue(String)
}
let mixed = [IntOrString.IntValue(1),
IntOrString.StringValue("two"),
IntOrString.IntValue(3)]
for value in mixed {
switch value {
case let .IntValue(i):
print(i * 2)
case let .StringValue(s):
print(s.capitalizedString)
}
}
// 輸出:
// 2
// Two
// 6
內(nèi)存管理灸眼,WEAK 和 UNOWNED
class Person {
let name: String
lazy var printName: ()->() = {
print("The name is \(self.name)")
}
init(personName: String) {
name = personName
}
deinit {
print("Person deinit \(self.name)")
}
}
var xiaoMing: Person? = Person(personName: "XiaoMing")
xiaoMing!.printName()
xiaoMing = nil
// 輸出:
// The name is XiaoMing,沒有被釋放
printName 是 self 的屬性墓懂,會被 self 持有焰宣,而它本身又在閉包內(nèi)持有 self,這導(dǎo)致了 xiaoMing 的 deinit 在自身超過作用域后還是沒有被調(diào)用捕仔,也就是沒有被釋放匕积。為了解決這種閉包內(nèi)的循環(huán)引用盈罐,我們需要在閉包開始的時(shí)候添加一個(gè)標(biāo)注,來表示這個(gè)閉包內(nèi)的某些要素應(yīng)該以何種特定的方式來使用闪唆≈逊啵可以將 printName 修改為這樣:
lazy var printName: ()->() = {
[weak self] in
if let strongSelf = self {
print("The name is \(strongSelf.name)")
}
}
@AUTORELEASEPOOL
func autoreleasepool(code: () -> ())
利用尾隨閉包的寫法,很容易就能在 Swift 中加入一個(gè)類似的自動(dòng)釋放池了:
func loadBigData() {
if let path = NSBundle.mainBundle()
.pathForResource("big", ofType: "jpg") {
for i in 1...10000 {
autoreleasepool {
let data = NSData.dataWithContentsOfFile(
path, options: nil, error: nil)
NSThread.sleepForTimeInterval(0.5)
}
}
}
}
DEFAULT 參數(shù)
func sayHello1(str1: String = "Hello", str2: String, str3: String) {
print(str1 + str2 + str3)
}
func sayHello2(str1: String, str2: String, str3: String = "World") {
print(str1 + str2 + str3)
}
sayHello1(str2: " ", str3: "World")
sayHello2("Hello", str2: " ")
//輸出都是 Hello World
正則表達(dá)式
一個(gè)最簡單的實(shí)現(xiàn)可能是下面這樣的:
struct RegexHelper {
let regex: NSRegularExpression
init(_ pattern: String) throws {
try regex = NSRegularExpression(pattern: pattern,
options: .CaseInsensitive)
}
func match(input: String) -> Bool {
let matches = regex.matchesInString(input,
options: [],
range: NSMakeRange(0, input.utf16.count))
return matches.count > 0
}
}
在使用的時(shí)候悄蕾,比如我們想要匹配一個(gè)郵箱地址票顾,我們可以這樣來使用:
let mailPattern =
"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
let matcher: RegexHelper
do {
matcher = try RegexHelper(mailPattern)
}
let maybeMailAddress = "onev@onevcat.com"
if matcher.match(maybeMailAddress) {
print("有效的郵箱地址")
}
// 輸出:
// 有效的郵箱地址
如果你想問 mailPattern 這一大串莫名其妙的匹配表達(dá)式是什么意思的話..>嘛..實(shí)在抱歉這里不是正則表達(dá)式的課堂,所以關(guān)于這個(gè)問題我>推薦看看這篇很棒的正則表達(dá)式 30 分鐘入門教程帆调,如果你連 30 分鐘都沒有的話奠骄,打開 8 個(gè)常用正則表達(dá)式 先開始抄吧..
上面那個(gè)式子就是我從這里抄來的
現(xiàn)在我們有了方便的封裝,接下來就讓我們實(shí)現(xiàn) =~ 吧番刊。這里只給出結(jié)果了含鳞,關(guān)于如何實(shí)現(xiàn)操作符和重載操作符的內(nèi)容,可以參考操作符一節(jié)的內(nèi)容芹务。
infix operator =~ {
associativity none
precedence 130
}
func =~(lhs: String, rhs: String) -> Bool {
do {
return try RegexHelper(rhs).match(lhs)
} catch _ {
return false
}
}
這下我們就可以使用類似于其他語言的正則匹配的方法了:
if "onev@onevcat.com" =~
"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$" {
print("有效的郵箱地址")
}
// 輸出:
// 有效的郵箱地址
模式匹配
首先我們要做的是重載 ~= 操作符蝉绷,讓它接受一個(gè) NSRegularExpression 作為模式,去匹配輸入的 String:
func ~=(pattern: NSRegularExpression, input: String) -> Bool {
return pattern.numberOfMatchesInString(input,
options: [],
range: NSRange(location: 0, length: input.characters.count)) > 0
}
然后為了簡便起見枣抱,我們再添加一個(gè)將字符串轉(zhuǎn)換為 NSRegularExpression 的操作符 (當(dāng)然也可以使用 StringLiteralConvertible熔吗,但是它不是這個(gè) tip 的主題,在此就先不使用它了):
prefix operator ~/ {}
prefix func ~/(pattern: String) -> NSRegularExpression {
return NSRegularExpression(pattern: pattern, options: nil, error: nil)
}
現(xiàn)在沃但,我們在 case 語句里使用正則表達(dá)式的話磁滚,就可以去匹配被 switch 的字符串了:
let contact = ("http://onevcat.com", "onev@onevcat.com")
let mailRegex: NSRegularExpression
let siteRegex: NSRegularExpression
mailRegex =
try ~/"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
siteRegex =
try ~/"^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$"
switch contact {
case (siteRegex, mailRegex): print("同時(shí)擁有有效的網(wǎng)站和郵箱")
case (_, mailRegex): print("只擁有有效的郵箱")
case (siteRegex, _): print("只擁有有效的網(wǎng)站")
default: print("嘛都沒有")
}
// 輸出
// 同時(shí)擁有網(wǎng)站和郵箱
那些有用的tips3
GCD 和延時(shí)調(diào)用
在下面我給出了一個(gè)日常里最通常會使用到的例子 (說這個(gè)例子能覆蓋到日常的 GCD 使用的 50% 以上也不為過),來展示一下 Swift 里的 GCD 調(diào)用會是什么樣子:
// 創(chuàng)建目標(biāo)隊(duì)列
let workingQueue = dispatch_queue_create("my_queue", nil)
// 派發(fā)到剛創(chuàng)建的隊(duì)列中宵晚,GCD 會負(fù)責(zé)進(jìn)行線程調(diào)度
dispatch_async(workingQueue) {
// 在 workingQueue 中異步進(jìn)行
print("努力工作")
NSThread.sleepForTimeInterval(2) // 模擬兩秒的執(zhí)行時(shí)間
dispatch_async(dispatch_get_main_queue()) {
// 返回到主線程更新 UI
print("結(jié)束工作垂攘,更新 UI")
}
}
GCD 里有一個(gè)很好用的延時(shí)調(diào)用我們可以加以利用寫出很漂亮的方法來,那就是 dispatch_after淤刃。最簡單的使用方法看起來是這樣的:
let time: NSTimeInterval = 2.0
let delay = dispatch_time(DISPATCH_TIME_NOW,
Int64(time * Double(NSEC_PER_SEC)))
dispatch_after(delay, dispatch_get_main_queue()) {
print("2 秒后輸出")
}
代碼非常簡單晒他,并沒什么值得詳細(xì)說明的。只是每次寫這么多的話也挺累的逸贾,在這里我們可以稍微將它封裝的好用一些陨仅,最好再加上取消的功能。在 iOS 8 中 GCD 得到了驚人的進(jìn)化铝侵,現(xiàn)在我們可以通過將一個(gè) dispatch_block_t 對象傳遞給 dispatch_block_cancel灼伤,來取消一個(gè)正在等待執(zhí)行的 block。取消一個(gè)任務(wù)這樣的特性咪鲜,這在以前是 NSOperation 的專利狐赡,但是現(xiàn)在我們使用 GCD 也能達(dá)到同樣的目的了。這里我們將類似地來嘗試實(shí)現(xiàn) delay call 的取消疟丙,整個(gè)封裝也許有點(diǎn)長颖侄,但我還是推薦一讀鸟雏。大家也可以把它當(dāng)作練習(xí)材料檢驗(yàn)一下自己的 Swift 基礎(chǔ)語法的掌握和理解的情況:
import Foundation
typealias Task = (cancel : Bool) -> Void
func delay(time:NSTimeInterval, task:()->()) -> Task? {
func dispatch_later(block:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(time * Double(NSEC_PER_SEC))),
dispatch_get_main_queue(),
block)
}
var closure: dispatch_block_t? = task
var result: Task?
let delayedClosure: Task = {
cancel in
if let internalClosure = closure {
if (cancel == false) {
dispatch_async(dispatch_get_main_queue(), internalClosure);
}
}
closure = nil
result = nil
}
result = delayedClosure
dispatch_later {
if let delayedClosure = result {
delayedClosure(cancel: false)
}
}
return result;
}
func cancel(task:Task?) {
task?(cancel: true)
}
使用的時(shí)候就很簡單了,我們想在 2 秒以后干點(diǎn)兒什么的話:
delay(2) { print("2 秒后輸出") }
想要取消的話览祖,我們可以先保留一個(gè)對 Task 的引用孝鹊,然后調(diào)用 cancel:
let task = delay(5) { print("撥打 110") }
// 仔細(xì)想一想..
// 還是取消為妙..
cancel(task)
... 和 ..<
0...3 就表示從 0 開始到 3 為止并包含 3 這個(gè)數(shù)字的范圍
0..<3 -- 都寫了小于號了,自然是不包含最后的 3 的意思咯
對于這樣得到的數(shù)字的范圍展蒂,我們可以對它進(jìn)行 for...in 的訪問:
for i in 0...3 {
print(i, terminator: "")
}
//輸出 0123
在 Swift 中又活,除了數(shù)字以外另一個(gè)實(shí)現(xiàn)了 Comparable 的基本類型就是 String。也就是說玄货,我們可以通過 ... 或者 ..< 來連接兩個(gè)字符串皇钞。一個(gè)常見的使用場景就是檢查某個(gè)字符是否是合法的字符。比如想確認(rèn)一個(gè)單詞里的全部字符都是小寫英文字母的話松捉,可以這么做:
let test = "helLo"
let interval = "a"..."z"
for c in test.characters {
if !interval.contains(String(c)) {
print("\(c) 不是小寫字母")
}
}
// 輸出
// L 不是小寫字母
在日常開發(fā)中夹界,我們可能會需要確定某個(gè)字符是不是有效的 ASCII 字符,和上面的例子很相似隘世,我們可以使用 \0...~ 這樣的 ClosedInterval 來進(jìn)行 (\0 和 ~ 分別是 ASCII 的第一個(gè)和最后一個(gè)字符)可柿。
獲取對象類型
let nibName = "\(type(of: self))"
//let name = string.dynamicType 已經(jīng)過時(shí)了
ANYCLASS,元類型和 .SELF
在 Cocoa API 中我們也常遇到需要一個(gè) AnyClass 的輸入丙者,這時(shí)候我們也應(yīng)該使用 .self 的方式來獲取所需要的元類型复斥,例如在注冊 tableView 的 cell 的類型的時(shí)候:
self.tableView.registerClass(
UITableViewCell.self, forCellReuseIdentifier: "myCell")
.Type 表示的是某個(gè)類型的元類型,而在 Swift 中械媒,除了 class目锭,struct 和 enum 這三個(gè)類型外,我們還可以定義 protocol纷捞。對于 protocol 來說痢虹,有時(shí)候我們也會想取得接口的元類型。這時(shí)我們可以在某個(gè) protocol 的名字后面使用 .Protocol 來獲取主儡,使用的方法和 .Type 是類似的奖唯。
接口和類方法中的 SELF
我們假設(shè)要實(shí)現(xiàn)一個(gè) Copyable 的接口,滿足這個(gè)接口的類型需要返回一個(gè)和接受方法調(diào)用的實(shí)例相同的拷貝糜值。我們可能考慮的接口是這樣的:
protocol Copyable {
func copy() -> Self
}
編譯器提示我們?nèi)绻胍獦?gòu)建一個(gè) Self 類型的對象的話丰捷,需要有 required 關(guān)鍵字修飾的初始化方法,這是因?yàn)?Swift 必須保證當(dāng)前類和其子類都能響應(yīng)這個(gè) init 方法寂汇。另一個(gè)解決的方案是在當(dāng)前類類的聲明前添加 final 關(guān)鍵字病往,告訴編譯器我們不再會有子類來繼承這個(gè)類型。在這個(gè)例子中骄瓣,我們選擇添加上 required 的 init 方法停巷。最后,MyClass 類型是這樣的:
class MyClass: Copyable {
var num = 1
func copy() -> Self {
let result = self.dynamicType.init()
result.num = num
return result
}
required init() {
}
}
我們可以通過測試來驗(yàn)證一下行為的正確性:
let object = MyClass()
object.num = 100
let newObject = object.copy()
object.num = 1
print(object.num) // 1
print(newObject.num) // 100
而對于 MyClass 的子類,copy() 方法也能正確地返回子類的經(jīng)過拷貝的對象了叠穆。
另一個(gè)可以使用 Self 的地方是在類方法中,使用起來也十分相似臼膏,核心就在于保證子類也能返回恰當(dāng)?shù)念愋汀?/p>
自省
首先它不僅可以用于 class 類型上硼被,也可以對 Swift 的其他像是 struct 或 enum 類型進(jìn)行判斷。使用起來是這個(gè)樣子的:
class ClassA { }
class ClassB: ClassA { }
let obj: AnyObject = ClassB()
if (obj is ClassA) {
print("屬于 ClassA")
}
if (obj is ClassB) {
print("屬于 ClassB")
}
另外渗磅,編譯器將對這種檢查進(jìn)行必要性的判斷:如果編譯器能夠唯一確定類型嚷硫,那么 is 的判斷就沒有必要,編譯器將會拋出一個(gè)警告始鱼,來提示你并沒有轉(zhuǎn)換的必要仔掸。
let string = "String"
if string is String {
// Do something
}
// 'is' test is always true
類型轉(zhuǎn)換 {#TYPE-CASTING}
Swift 中使用 as! 關(guān)鍵字做強(qiáng)制類型轉(zhuǎn)換
for object in self.view.subviews {
if object is UIView {
let view = object as! UIView
view.backgroundColor = UIColor.redColor()
}
}
這顯然還是太麻煩了,但是如果我們不加檢查就轉(zhuǎn)換的話医清,如果待轉(zhuǎn)換對象 (object) 并不是目標(biāo)類型 (UIView) 的話起暮,app 將崩潰,這是我們最不愿意看到的会烙。我們可以利用 Swift 的 Optional负懦,在保證安全的前提下讓代碼稍微簡單一些。在類型轉(zhuǎn)換的關(guān)鍵字 as 后面添加一個(gè)問號 ?柏腻,可以在類型不匹配及轉(zhuǎn)換失敗時(shí)返回 nil纸厉,這種做法顯然更有 Swift 范兒:
for object in self.view.subviews {
if let view = object as? UIView {
view.backgroundColor = UIColor.redColor()
}
}
不僅如此,我們還可以對整個(gè) [AnyObject] 的數(shù)組進(jìn)行轉(zhuǎn)換五嫂,先將其轉(zhuǎn)為 [UIView] 再直接使用:
if let subviews = self.view.subviews as? [UIView] {
for view in subviews {
view.backgroundColor = UIColor.redColor()
}
}
動(dòng)態(tài)類型和多方法
class Pet {}
class Cat: Pet {}
class Dog: Pet {}
func printPet(pet: Pet) {
print("Pet")
}
func printPet(cat: Cat) {
print("Meow")
}
func printPet(dog: Dog) {
print("Bark")
}
在對這些方法進(jìn)行調(diào)用時(shí)颗品,編譯器將幫助我們找到最精確的匹配:
printPet(Cat()) // Meow
printPet(Dog()) // Bark
printPet(Pet()) // Pet
對于 Cat 或者 Dog 的實(shí)例,總是會尋找最合適的方法沃缘,而不會去調(diào)用一個(gè)通用的父類 Pet 的方法躯枢。這一切的行為都是發(fā)生在編譯時(shí)的,如果我們寫了下面這樣的代碼
func printThem(pet: Pet, _ cat: Cat) {
printPet(pet)
printPet(cat)
}
printThem(Dog(), Cat())
// 輸出:
// Pet
// Meow
因?yàn)?Swift 默認(rèn)情況下是不采用動(dòng)態(tài)派發(fā)的孩灯,因此方法的調(diào)用只能在編譯時(shí)決定闺金。
要想繞過這個(gè)限制,我們可能需要進(jìn)行通過對輸入類型做判斷和轉(zhuǎn)換:
func printThem(pet: Pet, _ cat: Cat) {
if let aCat = pet as? Cat {
printPet(aCat)
} else if let aDog = pet as? Dog {
printPet(aDog)
}
printPet(cat)
}
// 輸出:
// Bark
// Meow
那些有用的tips4
屬性觀察
Swift 中為我們提供了兩個(gè)屬性觀察的方法峰档,它們分別是 willSet 和 didSet败匹。
class MyClass {
var date: NSDate {
willSet {
let d = date
print("即將將日期從 \(d) 設(shè)定至 \(newValue)")
}
didSet {
print("已經(jīng)將日期從 \(oldValue) 設(shè)定至 \(date)")
}
}
init() {
date = NSDate()
}
}
let foo = MyClass()
foo.date = foo.date.dateByAddingTimeInterval(10086)
// 輸出
// 即將將日期從 2014-08-23 12:47:36 +0000 設(shè)定至 2014-08-23 15:35:42 +0000
// 已經(jīng)將日期從 2014-08-23 12:47:36 +0000 設(shè)定至 2014-08-23 15:35:42 +0000
上面的例子中我們不希望 date 超過當(dāng)前時(shí)間的一年以上的話,我們可以將 didSet 修改一下:
class MyClass {
let oneYearInSecond: NSTimeInterval = 365 * 24 * 60 * 60
var date: NSDate {
//...
didSet {
if (date.timeIntervalSinceNow > oneYearInSecond) {
print("設(shè)定的時(shí)間太晚了讥巡!")
date = NSDate().dateByAddingTimeInterval(oneYearInSecond)
}
print("已經(jīng)將日期從 \(oldValue) 設(shè)定至 \(date)")
}
}
//...
}
更改一下調(diào)用掀亩,我們就能看到效果:
// 365 * 24 * 60 * 60 = 31_536_000
foo.date = foo.date.dateByAddingTimeInterval(100_000_000)
// 輸出
// 即將將日期從 2014-08-23 13:24:14 +0000 設(shè)定至 2017-10-23 23:10:54 +0000
// 設(shè)定的時(shí)間太晚了!
// 已經(jīng)將日期從 2014-08-23 13:24:14 +0000 設(shè)定至 2015-08-23 13:24:14 +0000
重寫的屬性并不知道父類屬性的具體實(shí)現(xiàn)情況欢顷,而只從父類屬性中繼承名字和類型槽棍,因此在子類的重載屬性中我們是可以對父類的屬性任意地添加屬性觀察的,而不用在意父類中到底是存儲屬性還是計(jì)算屬性:
class A {
var number :Int {
get {
print("get")
return 1
}
set {print("set")}
}
}
class B: A {
override var number: Int {
willSet {print("willSet")}
didSet {print("didSet")}
}
}
調(diào)用 number 的 set 方法可以看到工作的順序
let b = B()
b.number = 0
// 輸出
// get
// willSet
// set
// didSet
set 和對應(yīng)的屬性觀察的調(diào)用都在我們的預(yù)想之中。這里要注意的是 get 首先被調(diào)用了一次炼七。這是因?yàn)槲覀儗?shí)現(xiàn)了 didSet缆巧,didSet 中會用到 oldValue,而這個(gè)值需要在整個(gè) set 動(dòng)作之前進(jìn)行獲取并存儲待用豌拙,否則將無法確保正確性陕悬。如果我們不實(shí)現(xiàn) didSet 的話,這次 get 操作也將不存在按傅。
KVO
因?yàn)?KVO 是基于 KVC (Key-Value Coding) 以及動(dòng)態(tài)派發(fā)技術(shù)實(shí)現(xiàn)的捉超,而這些東西都是 Objective-C 運(yùn)行時(shí)的概念。另外由于 Swift 為了效率唯绍,默認(rèn)禁用了動(dòng)態(tài)派發(fā)
盡量不用
局部 SCOPE
使用匿名的閉包拼岳,寫代碼
titleLabel = {
let label = UILabel(frame: CGRectMake(150, 30, 20, 40))
label.textColor = UIColor.redColor()
label.text = "Title"
self.view.addSubview(label)
return label
}()
PRINT 和 DEBUGPRINT
對于一個(gè)普通的對象,我們在調(diào)用 print 對其進(jìn)行打印時(shí)只能打印出它的類型:
class MyClass {
var num: Int
init() {
num = 1
}
}
let obj = MyClass()
print(obj)
// MyClass
對于 struct 來說况芒,情況好一些惜纸。打印一個(gè) struct 實(shí)例的話,會列舉出它所有成員的名字和值:比如我們有一個(gè)日歷應(yīng)用存儲了一些會議預(yù)約牛柒,model 類型包括會議的地點(diǎn)堪簿,位置和參與者的名字:
struct Meeting {
var date: NSDate
var place: String
var attendeeName: String
}
let meeting = Meeting(date: NSDate(timeIntervalSinceNow: 86400),
place: "會議室B1",
attendeeName: "小明")
print(meeting)
// 輸出:
// Meeting(date: 2015-08-10 03:15:55 +0000,
// place: "會議室B1", attendeeName: "小明")
使用 CustomStringConvertible 接口,這個(gè)接口定義了將該類型實(shí)例輸出時(shí)所用的字符串皮壁。相對于直接在原來的類型定義中進(jìn)行更改椭更,我們更應(yīng)該傾向于使用一個(gè) extension,這樣不會使原來的核心部分的代碼變亂變臟蛾魄,是一種很好的代碼組織的形式:
extension Meeting: CustomStringConvertible {
var description: String {
return "于 \(self.date) 在 \(self.place) 與 \(self.attendeeName) 進(jìn)行會議"
}
}
這樣虑瀑,再當(dāng)我們使用 print 時(shí),就不再需要去做格式化滴须,而是簡單地將實(shí)例進(jìn)行打印就可以了:
print(meeting)
// 輸出:
// 于 2015-08-10 03:33:34 +0000 在 會議室B1 與 小明 進(jìn)行會議
CustomDebugStringConvertible 與 CustomStringConvertible 的作用很類似舌狗,但是僅發(fā)生在調(diào)試中使用 debugger 來進(jìn)行打印的時(shí)候的輸出。對于實(shí)現(xiàn)了 CustomDebugStringConvertible 接口的類型扔水,我們可以在給 meeting 賦值后設(shè)置斷點(diǎn)并在控制臺使用類似 po meeting 的命令進(jìn)行打印痛侍,控制臺輸出將為 CustomDebugStringConvertible 中定義的 debugDescription 返回的字符串。
判等
在 Swift 的字符串內(nèi)容判等魔市,我們簡單地使用 == 操作符來進(jìn)行:
let str1 = "快樂的字符串"
let str2 = "快樂的字符串"
let str3 = "開心的字符串"
str1 == str2 // true
str1 == str3 // false
在 Swift 中 === 只有一種重載:
func ===(lhs: AnyObject?, rhs: AnyObject?) -> Bool
它用來判斷兩個(gè) AnyObject 是否是同一個(gè)引用主届。
哈希
Int 的 hashValue 就是它本身:
let num = 19
print(num.hashValue) // 19
除非我們正在開發(fā)一個(gè)哈希散列的數(shù)據(jù)結(jié)構(gòu),否則我們不應(yīng)該直接依賴系統(tǒng)所實(shí)現(xiàn)的哈希值來做其他操作待德。首先哈希的定義是單向的君丁,對于相等的對象或值,我們可以期待它們擁有相同的哈希将宪,但是反過來并不一定成立绘闷。其次橡庞,某些對象的哈希值有可能隨著系統(tǒng)環(huán)境或者時(shí)間的變化而改變。因此你也不應(yīng)該依賴于哈希值來構(gòu)建一些需要確定對象唯一性的功能印蔗,在絕大部分情況下扒最,你將會得到錯(cuò)誤的結(jié)果。
錯(cuò)誤和異常處理
在 Swift 2.0 中华嘹,Apple 為這門語言引入了異常機(jī)制《筇龋現(xiàn)在,這類帶有 NSError 指針作為參數(shù)的 API 都被改為了可以拋出異常的形式除呵。比如上面的 writeToFile:options:error:,在 Swift 中變成了:
public func writeToFile(path: String,
options writeOptionsMask: NSDataWritingOptions) throws
我們在使用這個(gè) API 的時(shí)候爪喘,不再像之前那樣傳入一個(gè) error 指針去等待方法填充颜曾,而是變?yōu)槭褂?try catch 語句:
do {
try d.writeToFile("Hello", options: [])
} catch let error as NSError {
print ("Error: \(error.domain)")
}
關(guān)于 try 和 throws,想再多講兩個(gè)小點(diǎn)秉剑。首先泛豪,try 可以接 ! 表示強(qiáng)制執(zhí)行,這代表你確定知道這次調(diào)用不會拋出異常侦鹏。如果在調(diào)用中出現(xiàn)了異常的話诡曙,你的程序?qū)罎ⅲ@和我們在對 Optional 值用 ! 進(jìn)行強(qiáng)制解包時(shí)的行為是一致的略水。另外价卤,我們也可以在 try 后面加上 ? 來進(jìn)行嘗試性的運(yùn)行。try? 會返回一個(gè) Optional 值:如果運(yùn)行成功渊涝,沒有拋出錯(cuò)誤的話慎璧,它會包含這條語句的返回值,否則將為 nil跨释。和其他返回 Optional 的方法類似胸私,一個(gè)典型的 try? 的應(yīng)用場景是和 if let 這樣的語句搭配使用,不過如果你用了 try? 的話鳖谈,就意味著你無視了錯(cuò)誤的具體類型:
func methodThrowsWhenPassingNegative(number: Int) throws -> Int {
if number < 0 {
throw Error.Negative
}
return number
}
if let num = try? methodThrowsWhenPassingNegative(100) {
print(num.dynamicType)
} else {
print("failed")
}
// 輸出:
// Int
但是 rethrows 一般用在參數(shù)中含有可以 throws 的方法的高階函數(shù)中岁疼,也就是像是下面這樣的方法,我們可以在外層用 rethrows 進(jìn)行標(biāo)注:
如果你拿不準(zhǔn)要怎么使用的話缆娃,就先記住你在要 throws 另一個(gè) throws 時(shí)捷绒,應(yīng)該將前者改為 rethrows。
func methodThrows(num: Int) throws {
if num < 0 {
print("Throwing!")
throw Error.Negative
}
print("Executed!")
}
func methodRethrows(num: Int, f: Int throws -> ()) rethrows {
try f(num)
}
do {
try methodRethrows(1, f: methodThrows)
} catch _ {
}
斷言
斷言 (assertion) 在 Cocoa 開發(fā)里一般用來在檢查輸入?yún)?shù)是否滿足一定條件龄恋,并對其進(jìn)行“論斷”疙驾。
Swift 為我們提供了一系列的 assert 方法來使用斷言,其中最常用的一個(gè)是:
func assert(@autoclosure condition: () -> Bool,
@autoclosure _ message: () -> String = default,
file: StaticString = default,
line: UInt = default)
在使用時(shí)郭毕,最常見的情況是給定條件和一個(gè)簡單的說明它碎。舉一個(gè)在溫度轉(zhuǎn)換時(shí)候的例子, 我們想要把攝氏溫度轉(zhuǎn)為開爾文溫度的時(shí)候,因?yàn)榻^對零度永遠(yuǎn)不能達(dá)到扳肛,所以我們不可能接受一個(gè)小于 -273.15 攝氏度的溫度作為輸入:
func convertToKelvin(# celsius: Double) -> Double {
assert(celsius > absoluteZeroInCelsius, "輸入的攝氏溫度不能低于絕對零度")
return celsius - absoluteZeroInCelsius
}
let roomTemperature = convertToKelvin(celsius: 27)
// roomTemperature = 300.15
let tooCold = convertToKelvin(celsius: -300)
// 運(yùn)行時(shí)錯(cuò)誤:
// assertion failed:
// 輸入的攝氏溫度不能低于絕對零度 : file {YOUR_FILE_PATH}, line {LINE_NUMBER}
斷言的另一個(gè)優(yōu)點(diǎn)是它是一個(gè)開發(fā)時(shí)的特性傻挂,只有在 Debug 編譯的時(shí)候有效,而在運(yùn)行時(shí)是不被編譯執(zhí)行的挖息,因此斷言并不會消耗運(yùn)行時(shí)的性能金拒。這些特點(diǎn)使得斷言成為面向程序員的在調(diào)試開發(fā)階段非常合適的調(diào)試判斷,而在代碼發(fā)布的時(shí)候套腹,我們也不需要刻意去將這些斷言手動(dòng)清理掉绪抛,非常方便。
雖然默認(rèn)情況下只在 Release 的情況下斷言才會被禁用电禀,但是有時(shí)候我們可能出于某些目的希望斷言在調(diào)試開發(fā)時(shí)也暫時(shí)停止工作幢码,或者是在發(fā)布版本中也繼續(xù)有效。我們可以通過顯式地添加編譯標(biāo)記達(dá)到這個(gè)目的尖飞。在對應(yīng) target 的 Build Settings 中症副,我們在 Swift Compiler - Custom Flags 中的 Other Swift Flags 中添加 -assert-config Debug 來強(qiáng)制啟用斷言,或者 -assert-config Release 來強(qiáng)制禁用斷言政基。當(dāng)然贞铣,除非有充足的理由,否則并不建議做這樣的改動(dòng)沮明。如果我們需要在 Release 發(fā)布時(shí)在無法繼續(xù)時(shí)將程序強(qiáng)行終止的話辕坝,應(yīng)該選擇使用 fatalError。
PLAYGROUND 延時(shí)運(yùn)行
其中最基礎(chǔ)的一個(gè)就是異步代碼的執(zhí)行荐健,比如這樣的 NSTimer 在默認(rèn)的 Playground 中是不會執(zhí)行的:
class MyClass {
@objc func callMe() {
print("Hi")
}
}
let object = MyClass()
NSTimer.scheduledTimerWithTimeInterval(1, target: object,
selector: #selector(MyClass.callMe), userInfo: nil, repeats: true)
在執(zhí)行完 NSTimer 語句之后圣勒,整個(gè) Playground 將停止掉,Hi 永遠(yuǎn)不會被打印出來摧扇。放心圣贸,這種異步的操作沒有生效并不是因?yàn)槟銓戝e(cuò)了什么,而是 Playground 在執(zhí)行完了所有語句扛稽,然后正常退出了而已吁峻。
為了使 Playground 具有延時(shí)運(yùn)行的本領(lǐng),我們需要引入 Playground 的 “擴(kuò)展包” XCPlayground 框架≡谡牛現(xiàn)在這個(gè)框架中包含了幾個(gè)與 Playground 的行為交互以及控制 Playground 特性的 API用含,其中就包括使 Playground 能延時(shí)執(zhí)行的黑魔法,XCPlaygroundPage 和 needsIndefiniteExecution帮匾。
我們只需要在剛才的代碼上面加上:
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
就可以看到 Hi 以每秒一次的頻率被打印出來了啄骇。
在實(shí)際使用和開發(fā)中,我們最經(jīng)常面臨的異步需求可能就是網(wǎng)絡(luò)請求了瘟斜,如果我們想要在 Playground 里驗(yàn)證某個(gè) API 是否正確工作的話缸夹,使用 XCPlayground 的這個(gè)方法開啟延時(shí)執(zhí)行也是必要的:
let url = NSURL(string: "http://httpbin.org/get")!
let getTask = NSURLSession.sharedSession().dataTaskWithURL(url) {
(data, response, error) -> Void in
let dictionary = try! NSJSONSerialization.JSONObjectWithData(data!, options: [])
print(dictionary)
}
getTask.resume()
如果你想改變這個(gè)時(shí)間的話痪寻,可以通過 Alt + Cmd + 回車 來打開輔助編輯器。在這里你會看到控制臺輸出和時(shí)間軸虽惭,將右下角的 30 改成你想要的數(shù)字橡类,就可以對延時(shí)運(yùn)行的最長時(shí)間進(jìn)行設(shè)定了。
PLAYGROUND 可視化
在 Playground 中事情就變得簡單多了:我們可以使用 XCPlayground 框架的 XCPCaptureValue 方法來將一組數(shù)據(jù)輕而易舉地繪制到時(shí)間軸上芽唇,從而讓我們能看到每一步的結(jié)果顾画。這不僅對我們直觀且及時(shí)地了解算法內(nèi)部的變化很有幫助,也會是教學(xué)或者演示時(shí)候的神兵利器匆笤。
XCPCaptureValue 的使用方法很簡單研侣,在 import XCPlayground 導(dǎo)入框架后,可以找到該方法的定義:
func XCPCaptureValue<T>(identifier: String, value: T)
下面的代碼實(shí)現(xiàn)了簡單的冒泡排序炮捧,我們在每一輪排序完成后使用 plot 方法將當(dāng)前的數(shù)組狀態(tài)用 XCPCaptureValue 的方式進(jìn)行了輸出义辕。通過在時(shí)間軸 (通過 “Alt+Cmd+回車” 打開 Assistant Editor) 的輸出圖,我們就可以非常清楚地了解到整個(gè)算法的執(zhí)行過程了寓盗。
import XCPlayground
var arr = [14, 11, 20, 1, 3, 9, 4, 15, 6, 19,
2, 8, 7, 17, 12, 5, 10, 13, 18, 16]
func plot<T>(title: String, array: [T]) {
for value in array {
XCPCaptureValue(title, value: value)
}
}
plot("起始", array: arr)
func swap(inout x: Int, inout y: Int) {
(x, y) = (y, x)
}
func bubbleSort<T: Comparable>(inout input: [T]) {
for i in 0 ..< input.count - 1 {
let i = input.count - i
var didSwap = false
for j in 0 ..< i - 1 {
if input[j] > input[j + 1] {
didSwap = true
swap(&input[j], &input[j + 1])
}
}
if !didSwap {
break
}
plot("第 \(input.count - (i - 1)) 次迭代", array: input)
}
plot("結(jié)果", array: input)
}
bubbleSort(&arr)
因?yàn)?XCPCaptureValue 的數(shù)據(jù)輸入是任意類型的,所以不論是傳什么進(jìn)去都是可以表示的璧函。它們將以 QuickLook 預(yù)覽的方式被表現(xiàn)出來傀蚌,一些像 UIImage,UIColor 或者 UIBezierPath 這樣的類型已經(jīng)實(shí)現(xiàn)了 QuickLook蘸吓。當(dāng)然對于那些沒有實(shí)現(xiàn)快速預(yù)覽的 NSObject 子類善炫,也可以通過重寫
func debugQuickLookObject() -> AnyObject?
來提供一個(gè)預(yù)覽輸出。在上面的冒泡排序方法中库继,我們可以接收任意滿足 Comparable 的數(shù)組箩艺,而繪圖方法也可以接受任意類型的輸入。作為練習(xí)宪萄,可以試試看把 arr 的全部數(shù)字都換成一些隨機(jī)的字符串看看時(shí)間軸的輸出是什么樣子吧艺谆。
SWIZZLE
Swizzle 是 Objective-C 運(yùn)行時(shí)的黑魔法之一。我們可以通過 Swizzle 的手段拜英,在運(yùn)行時(shí)對某些方法的實(shí)現(xiàn)進(jìn)行替換静汤,這是 Objective-C 甚至說 Cocoa 開發(fā)中最為華麗,同時(shí)也是最為危險(xiǎn)的技巧之一居凶。
一般來說可能不太用得到這樣的技術(shù)虫给,但是在某些情況下會非常有用,特別是當(dāng)我們需要觸及到一些系統(tǒng)框架的東西的時(shí)候侠碧。比如我們已經(jīng)有一個(gè)龐大的項(xiàng)目抹估,并使用了很多 UIButton 來讓用戶交互。某一天弄兜,產(chǎn)品汪突然說我們需要統(tǒng)計(jì)一下整個(gè) app 中用戶點(diǎn)擊所有按鈕的次數(shù)十气。對于完全不懂技術(shù)的選手來說猜年,在他們眼中這似乎不應(yīng)該是什么難事 -- 只要弄個(gè)計(jì)數(shù)器然后在每次點(diǎn)按鈕的時(shí)候加一就可以了嘛月培。但是對于每一個(gè)以代碼為生的人來說,面臨的一個(gè)嚴(yán)峻的問題是蒿往,這要怎么辦。
這種時(shí)候就該輪到 Swizzle 大顯身手了湿弦。我們在全局范圍內(nèi)將所有的 UIButton 的發(fā)送事件的方法換掉瓤漏,就可以一勞永逸地解決這個(gè)問題 -- 沒有一段段代碼的替換查找,不會遺漏任何按鈕颊埃,之后開發(fā)中也不需要對這個(gè)計(jì)數(shù)的功能特別地注意什么蔬充。
在 Swift 中,我們也可以利用 Objective-C 運(yùn)行時(shí)來進(jìn)行 Swizzle班利。比如上面的例子饥漫,我們就可以使用這樣的擴(kuò)展來完成:
extension UIButton {
class func xxx_swizzleSendAction() {
struct xxx_swizzleToken {
static var onceToken : dispatch_once_t = 0
}
dispatch_once(&xxx_swizzleToken.onceToken) {
let cls: AnyClass! = UIButton.self
let originalSelector = #selector(sendAction(_:to:forEvent:))
let swizzledSelector = #selector(xxx_sendAction(_:to:forEvent:))
let originalMethod =
class_getInstanceMethod(cls, originalSelector)
let swizzledMethod =
class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
public func xxx_sendAction(action: Selector,
to: AnyObject!,
forEvent: UIEvent!)
{
struct xxx_buttonTapCounter {
static var count: Int = 0
}
xxx_buttonTapCounter.count += 1
print(xxx_buttonTapCounter.count)
xxx_sendAction(action, to: to, forEvent: forEvent)
}
}
最后我們需要在 app 啟動(dòng)時(shí)調(diào)用這個(gè) xxx_swizzleSendAction 方法。在 Objective-C 中我們一般在 category 的 +load 中完成罗标,但是 Swift 的 extension 和 Objective-C 的 category 略有不同庸队,extension 并不是運(yùn)行時(shí)加載的,因此也沒有加載時(shí)候就會被調(diào)用的類似 load 的方法闯割。另外彻消,extension 中也不應(yīng)該做方法重寫去覆蓋 load (其實(shí)重寫也是無效的)。事實(shí)上宙拉,Swift 實(shí)現(xiàn)的 load 并不是在 app 運(yùn)行開始就被調(diào)用的宾尚。基于這些理由谢澈,我們使用另一個(gè)類初始化時(shí)會被調(diào)用的方法來進(jìn)行交換:
extension UIButton {
override public class func initialize() {
if self != UIButton.self {
return
}
UIButton.xxx_swizzleSendAction()
}
}
和 +load 不同的是煌贴,+initialize 會在當(dāng)前類以及它的子類被初始化時(shí)調(diào)用。在這里我們對當(dāng)前類的類型進(jìn)行了判斷锥忿,來保證安全性牛郑。另外,在 xxx_swizzleSendAction 中敬鬓,也使用一個(gè) once_token 來保證交換代碼僅會被執(zhí)行一次井濒。
這種方式的 Swizzle 使用了 Objective-C 的動(dòng)態(tài)派發(fā),對于 NSObject 的子類是可以直接使用的列林,但是對于 Swift 的類瑞你,因?yàn)槟J(rèn)并沒有使用 Objective-C 運(yùn)行時(shí),因此也沒有動(dòng)態(tài)派發(fā)的方法列表希痴,所以如果要 Swizzle 的是 Swift 類型的方法的話者甲,我們需要將原方法和替換方法都加上 dynamic 標(biāo)記,以指明它們需要使用動(dòng)態(tài)派發(fā)機(jī)制砌创。
那些有用的tips5
LAZY 修飾符和 LAZY 方法
在 Swift 中我們使用在變量屬性前加 lazy 關(guān)鍵字的方式來簡單地指定延時(shí)加載虏缸。
class ClassA {
lazy var str: String = {
let str = "Hello"
print("只在首次訪問輸出")
return str
}()
}
為了簡化鲫懒,我們?nèi)绻恍枰鍪裁搭~外工作的話,也可以對這個(gè) lazy 的屬性直接寫賦值語句:
lazy var str: String = "Hello"
如果我們先進(jìn)行一次 lazy 操作的話刽辙,我們就能得到延時(shí)運(yùn)行版本的容器:
let data = 1...3
let result = data.lazy.map {
(i: Int) -> Int in
print("正在處理 \(i)")
return i * 2
}
print("準(zhǔn)備訪問結(jié)果")
for i in result {
print("操作后結(jié)果為 \(i)")
}
print("操作完畢")
此時(shí)的運(yùn)行結(jié)果:
// 準(zhǔn)備訪問結(jié)果
// 正在處理 1
// 操作后結(jié)果為 2
// 正在處理 2
// 操作后結(jié)果為 4
// 正在處理 3
// 操作后結(jié)果為 6
// 操作完畢
數(shù)學(xué)和數(shù)字
我們可以使用 Int.max 和 Int.min 來取得對應(yīng)平臺的 Int 的最大和最小值窥岩。另外在 Double 中,我們還有兩個(gè)很特殊的值宰缤,infinity 和 NaN颂翼。
1.797693134862315e+308 < Double.infinity // true
1.797693134862316e+308 < Double.infinity // false
另一個(gè)有趣的東西是 NaN,它是 “Not a Number” 的簡寫慨灭,可以用來表示某些未被定義的或者出現(xiàn)了錯(cuò)誤的運(yùn)算朦乏,比如下面的操作都會產(chǎn)生 NaN:
let a = 0.0 / 0.0
let b = sqrt(-1.0)
let c = 0.0 * Double.infinity
let num = Double.NaN
if num == num {
print("Num is \(num)")
} else {
print("NaN")
}
// 輸出:
// NaN
let num = Double.NaN
if num.isNaN {
print("NaN")
}
if isnan(num) {
print("NaN")
}
// 輸出:
// NaN
// NaN
JSON
SwiftyJSON 這樣的項(xiàng)目,它就使用了重載下標(biāo)訪問的方式簡化了 JSON 操作氧骤。使用這個(gè)工具呻疹,上面的訪問可以簡化為下面的類型安全的樣子:
// 使用 SwiftJSON
if let value = JSON(json)["menu"]["popup"]["menuitem"][0]["value"].string {
print(value)
}
NSNull
// 假設(shè) jsonValue 是從一個(gè) JSON 中取出的 NSNull
let jsonValue: AnyObject = NSNull()
if let string = jsonValue as? String {
print(string.hasPrefix("a"))
} else {
print("不能解析")
}
// 輸出:
// 不能解析
調(diào)用 C 動(dòng)態(tài)庫
Swift 是可以通過 {product-module-name}-Bridging-Header.h 來調(diào)用 Objective-C 代碼的,于是 C 作為 Objective-C 的子集筹陵,自然也一并被解決了刽锤。比如對于上面提到的 MD5 的例子,我們就可以通過頭文件導(dǎo)入以及添加 extension 來解決:
// TargetName-Bridging-Header.h
#import <CommonCrypto/CommonCrypto.h>
// StringMD5.swift
extension String {
var MD5: String {
let cString = self.cStringUsingEncoding(NSUTF8StringEncoding)
let length = CUnsignedInt(
self.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
)
let result = UnsafeMutablePointer<CUnsignedChar>.alloc(
Int(CC_MD5_DIGEST_LENGTH)
)
CC_MD5(cString!, length, result)
return String(format:
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15])
}
}
// 測試
print("swifter.tips".MD5)
// 輸出
// dff88de99ff03d109de22fed4f71a273
REFLECTION 和 MIRROR
使用 Mirror 類型來做類似反射 (Reflection)的事情:
struct Person {
let name: String
let age: Int
}
let xiaoMing = Person(name: "XiaoMing", age: 16)
let r = Mirror(reflecting: xiaoMing) // r 是 MirrorType
print("xiaoMing 是 \(r.displayStyle!)")
print("屬性個(gè)數(shù):\(r.children.count)")
for i in r.children.startIndex..<r.children.endIndex {
print("屬性名:\(r.children[i].0!)朦佩,值:\(r.children[i].1)")
}
// 輸出:
// xiaoMing 是 Struct
// 屬性個(gè)數(shù):2
// 屬性名:name并思,值:XiaoMing
// 屬性名:age,值:16
使用 dump 方法來通過獲取一個(gè)對象的鏡像并進(jìn)行標(biāo)準(zhǔn)輸出的方式將其輸出出來吕粗。比如對上面的對象 xiaoMing:
dump(xiaoMing)
// 輸出:
// ? Person
// - name: XiaoMing
// - age: 16
類似對 Swift 類型的對象做像 Objective-C 中 KVC 那樣的 valueForKey: 的取值。通過比較取到的屬性的名字和我們想要取得的 key 值就行了旭愧,非常簡單:
func valueFrom(object: Any, key: String) -> Any? {
let mirror = Mirror(reflecting: object)
for i in mirror.children.startIndex..<mirror.children.endIndex {
let (targetKey, targetMirror) = mirror.children[i]
if key == targetKey {
return targetMirror
}
}
return nil
}
// 接上面的 xiaoMing
if let name = valueFrom(xiaoMing, key: "name") as? String {
print("通過 key 得到值: \(name)")
}
// 輸出:
// 通過 key 得到值: XiaoMing
輸出格式化
在 Swift 里颅筋,我們在輸出時(shí)一般使用的 print 中是支持字符串插值的,而字符串插值時(shí)將直接使用類型的 Streamable输枯,Printable 或者 DebugPrintable 接口 (按照先后次序议泵,前面的沒有實(shí)現(xiàn)的話則使用后面的) 中的方法返回的字符串并進(jìn)行打印。
let a = 3;
let b = 1.234567 // 我們在這里不去區(qū)分 float 和 Double 了
let c = "Hello"
print("int:\(a) double:\(b) string:\(c)")
// 輸出:
// int:3 double:1.234567 string:Hello
我們打算只輸出上面的 b 中的小數(shù)點(diǎn)后兩位的話桃熄,在 Objective-C 中使用 NSLog 時(shí)可以寫成下面這樣:
NSLog(@"float:%.2f",b);
// 輸出:
// float:1.23
swift
let format = String(format:"%.2f",b)
print("double:\(format)")
// 輸出:
// double:1.23
當(dāng)然先口,每次這么寫的話也很麻煩。如果我們需要大量使用類似的字符串格式化功能的話瞳收,我們最好為 Double 寫一個(gè)擴(kuò)展:
extension Double {
func format(f: String) -> String {
return String(format: "%\(f)f", self)
}
}
這樣碉京,在使用字符串插值和 print 的時(shí)候就能方便一些了:
let f = ".2"
print("double:\(b.format(f))")
文檔注釋
注釋快捷鍵:command + alt + /
OPTIONS
我們可以使用 | 或者 & 這樣的按位邏輯符對這些選項(xiàng)進(jìn)行操作,這是因?yàn)橐话銇碚f在 Objective-C 中的 Options 的定義都是類似這樣的按位錯(cuò)開的:
typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
UIViewAnimationOptionLayoutSubviews = 1 << 0,
UIViewAnimationOptionAllowUserInteraction = 1 << 1,
UIViewAnimationOptionBeginFromCurrentState = 1 << 2,
//...
UIViewAnimationOptionTransitionFlipFromBottom = 7 << 20,
}
要實(shí)現(xiàn)一個(gè) Options 的 struct 的話螟深,可以參照已有的寫法建立類并實(shí)現(xiàn) OptionSetType谐宙。因?yàn)榛旧纤械?Options 都是很相似的,所以最好是準(zhǔn)備一個(gè) snippet 以快速重用:
struct YourOption: OptionSetType {
let rawValue: UInt
static let None = YourOption(rawValue: 0)
static let Option1 = YourOption(rawValue: 1)
static let Option2 = YourOption(rawValue: 1 << 1)
//...
}
數(shù)組 ENUMERATE
var result = 0
for (idx, num) in [1,2,3,4,5].enumerate() {
result += num
if idx == 2 {
break
}
}
print(result)
基本上來說界弧,是時(shí)候可以和 enumerateObjectsUsingBlock: 說再見了凡蜻。
類型編碼 @ENCODE
在 Objective-C 中 @encode 使用起來很簡單搭综,通過傳入一個(gè)類型,我們就可以獲取代表這個(gè)類型的編碼 C 字符串:
char *typeChar1 = @encode(int32_t);
char *typeChar2 = @encode(NSArray);
// typeChar1 = "i", typeChar2 = "{NSArray=#}"
我們可以對任意的類型進(jìn)行這樣的操作划栓。這個(gè)關(guān)鍵字最常用的地方是在 Objective-C 運(yùn)行時(shí)的消息發(fā)送機(jī)制中兑巾,在傳遞參數(shù)時(shí),由于類型信息的缺失忠荞,需要類型編碼進(jìn)行輔助以保證類型信息也能夠被傳遞蒋歌。在實(shí)際的應(yīng)用開發(fā)中,其實(shí)使用案例比較少:某些 API 中 Apple 建議使用 NSValue 的 valueWithBytes:objCType: 來獲取值 (比如 CIAffineClamp 的文檔里) 钻洒,這時(shí)的 objCType 就需要類型的編碼值奋姿;另外就是在類型信息丟失時(shí)我們可能需要用到這個(gè)特性,我們稍后會舉一個(gè)這方面的例子素标。
let int: Int = 0
let float: Float = 0.0
let double: Double = 0.0
let intNumber: NSNumber = int
let floatNumber: NSNumber = float
let doubleNumber: NSNumber = double
String.fromCString(intNumber.objCType)
String.fromCString(floatNumber.objCType)
String.fromCString(doubleNumber.objCType)
// 結(jié)果分別為:
// {Some "q"}
// {Some "f"}
// {Some "d"}
// 注意称诗,fromCString 返回的是 `String?`
let p = NSValue(CGPoint: CGPointMake(3, 3))
String.fromCString(p.objCType)
// {Some "{CGPoint=dd}"}
let t = NSValue(CGAffineTransform: CGAffineTransformIdentity)
String.fromCString(t.objCType)
// {Some "{CGAffineTransform=dddddd}"}
LOG 輸出
在 Swift 中,編譯器為我們準(zhǔn)備了幾個(gè)很有用的編譯符號头遭,用來處理類似這樣的需求寓免,它們分別是:
符號 | 類型 | 描述 |
---|---|---|
#file | String | 包含這個(gè)符號的文件的路徑 |
#line | Int | 符號出現(xiàn)處的行號 |
#column | Int | 符號出現(xiàn)處的列 |
#function | String | 包含這個(gè)符號的方法名字 |
因此,我們可以通過使用這些符號來寫一個(gè)好一些的 Log 輸出方法:
func printLog<T>(message: T,
file: String = #file,
method: String = #function,
line: Int = #line)
{
#if DEBUG
print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
#endif
}
// Test.swift
func method() {
//...
printLog("這是一條輸出")
//...
}
// 輸出:
// Test.swift[62], method(): 這是一條輸出
那些有用的tips6
C 代碼調(diào)用和 @ASMNAME
對于第三方的 C 代碼计维,Swift 也提供了協(xié)同使用的方法袜香。我們知道,Swift 中調(diào)用 Objective-C 代碼非常簡單鲫惶,只需要將合適的頭文件暴露在 {product-module-name}-Bridging-Header.h 文件中就行了蜈首。而如果我們想要調(diào)用非標(biāo)準(zhǔn)庫的 C 代碼的話,可以遵循同樣的方式欠母,將 C 代碼的頭文件在橋接的頭文件中進(jìn)行導(dǎo)入:
//test.h
int test(int a);
//test.c
int test(int a) {
return a + 1;
}
//Module-Bridging-Header.h
#import "test.h"
//File.swift
func testSwift(input: Int32) {
let result = test(input)
print(result)
}
testSwift(1)
// 輸出:2
asmname 可以通過方法名字將某個(gè) C 函數(shù)直接映射為 Swift 中的函數(shù)欢策。比如上面的例子,我們可以將 test.h 和 Module-Bridging-Header.h 都刪掉赏淌,然后將 swift 文件中改為下面這樣踩寇,也是可以正常進(jìn)行使用的:
//File.swift
//將 C 的 test 方法映射為 Swift 的 c_test 方法
asmname("test") func c_test(a: Int32) -> Int32
func testSwift(input: Int32) {
let result = c_test(input)
print(result)
}
testSwift(1)
// 輸出:2
這種導(dǎo)入在第三方 C 方法與系統(tǒng)庫重名導(dǎo)致調(diào)用發(fā)生命名沖突時(shí),可以用來為其中之一的函數(shù)重新命名以解決問題六水。當(dāng)然我們也可以利用 Module 名字 + 方法名字的方式來解決這個(gè)問題俺孙。
我們不能簡單地用 sizeofValue 來獲取長度,而需要進(jìn)行一些計(jì)算掷贾。上面的生成 NSData 的方法在 Swift 中書寫的話睛榄,等效的代碼應(yīng)該是下面這樣的:
SIZEOF 和 SIZEOFVALUE
而在 Swift 中,我們?nèi)绻苯訉?bytes 做 sizeofValue 操作的話想帅,將返回 8懈费,這其實(shí)是在 64 位系統(tǒng)上一個(gè)引用的長度:
// C
char bytes[] = {1, 2, 3};
sizeof(bytes);
// 3
// Swift
var bytes: [CChar] = [1,2,3]
sizeofValue(bytes)
// 8
所以,我們不能簡單地用 sizeofValue 來獲取長度博脑,而需要進(jìn)行一些計(jì)算憎乙。上面的生成 NSData 的方法在 Swift 中書寫的話票罐,等效的代碼應(yīng)該是下面這樣的:
var bytes: [CChar] = [1,2,3]
let data = NSData(bytes: &bytes, length:sizeof(CChar) * bytes.count)
OPTIONAL MAP
let arr = [1,2,3]
let doubled = arr.map{
$0 * 2
}
print(doubled)
// 輸出:
// [2,4,6]
let num: Int? = 3
let result = num.map {
$0 * 2
}
// result 為 {Some 6}
在 Swift 中,我們可以使用以下這五個(gè)帶有 & 的操作符泞边,這樣 Swift 就會忽略掉溢出的錯(cuò)誤:
溢出加法 (&+)
溢出減法 (&-)
溢出乘法 (&*)
溢出除法 (&/)
溢出求模 (&%)
var max = Int.max
max = max &+ 1
// 64 位系統(tǒng)下
// max = -9,223,372,036,854,775,808
屬性訪問控制
對于那些我們只希望在當(dāng)前文件中使用的屬性來說该押,當(dāng)然我們可以在聲明前面加上 private 使其變?yōu)樗接校?/p>
class MyClass {
private var name: String?
}
但是在開發(fā)中所面臨的更多的情況是我們希望在類型之外也能夠讀取到這個(gè)類型,同時(shí)為了保證類型的封裝和安全阵谚,只能在類型內(nèi)部對其進(jìn)行改變和設(shè)置蚕礼。這時(shí),我們可以通過下面的寫法將讀取和設(shè)置的控制權(quán)限分開:
class MyClass {
private(set) var name: String?
}
因?yàn)?set 被限制為了 private梢什,我們就可以保證 name 只會在當(dāng)前文件被更改奠蹬。
如果我們希望在別的 module 中也能訪問這個(gè)屬性,同時(shí)又保持只在當(dāng)前文件可以設(shè)置的話嗡午,我們需要將 get 的訪問權(quán)限提高為 public囤躁。
public class MyClass {
public private(set) var name: String?
}
這時(shí)我們就可以在 module 之外也訪問到 MyClass 的 name 了。
我們在 MyClass 前面也添加的 public荔睹,這是編譯器所要求的狸演。因?yàn)槿绻粸?name 的 get 添加 public 而不管 MyClass 的話,module 外就連 MyClass 都訪問不到了僻他,屬性的訪問控制級別也就沒有任何意義了宵距。
SWIFT 中的測試
在 Swift 2.0 中, Apple 為 app 的測試開了“后門”《洲郑現(xiàn)在我們可以通過在測試代碼中導(dǎo)入 app 的 target 時(shí)满哪,在之前追加 @testable,就可以訪問到 app target 中 internal 的內(nèi)容了劝篷。
// 位于 app target 的業(yè)務(wù)代碼
func methodToTest() {
}
// 測試
@testable import MyApp
//...
func testMethodToTest() {
// 配置測試
someObj.methodToTest()
// 斷言結(jié)果
}
CORE DATA
Core Data 是 Cocoa 的一個(gè)重要組成部分哨鸭,也是非常依賴 @dynamic 特性的部分。Apple 在 Swift 中專門為 Core Data 加入了一個(gè)特殊的標(biāo)注來處理動(dòng)態(tài)代碼携龟,那就是 @NSManaged兔跌。我們只需要在 NSManagedObject 的子類的成員的字段上加上 @NSManaged 就可以了:
class MyModel: NSManagedObject {
@NSManaged var title: String
}
閉包歧義
3.times { (i: Int)->() in
print(i)
}
3.times { (i: Void)->() in
print(i)
}
3.times { (i: (Int,Int))->() in
print(i)
}
我們想在擴(kuò)展中實(shí)現(xiàn)一個(gè) random 方法來隨機(jī)地取出 Array 中的一個(gè)元素:
extension Array {
var random: Element? {
return self.count != 0 ?
self[Int(arc4random_uniform(UInt32(self.count)))] :
nil
}
}
let languages = ["Swift","ObjC","C++","Java"]
languages.random!
// 隨機(jī)輸出是這四個(gè)字符串中的某個(gè)
let ranks = [1,2,3,4]
ranks.random!
// 隨機(jī)輸出是這四個(gè)數(shù)字中的某個(gè)
在擴(kuò)展中是不能添加整個(gè)類型可用的新泛型符號的勘高,但是對于某個(gè)特定的方法來說峡蟋,我們可以添加 T 以外的其他泛型符號。比如在剛才的擴(kuò)展中加上:
func appendRandomDescription
<U: CustomStringConvertible>(input: U) -> String {
if let element = self.random {
return "\(element) " + input.description
} else {
return "empty array"
}
}
我們限定了只接受實(shí)現(xiàn)了 CustomStringConvertible 的參數(shù)作為參數(shù)华望,然后將這個(gè)內(nèi)容附加到自身的某個(gè)隨機(jī)元素的描述上蕊蝗。因?yàn)閰?shù) input 實(shí)現(xiàn)了 CustomStringConvertible,所以在方法中我們可以使用 description 來獲取描述字符串赖舟。
let languages = ["Swift","ObjC","C++","Java"]
languages.random!
let ranks = [1,2,3,4]
ranks.random!
languages.appendRandomDescription(ranks.random!)
// 隨機(jī)組合 languages 和 ranks 中的各一個(gè)元素蓬戚,然后輸出
那些有用的tips7
列舉 ENUM 類型
撲克牌花色和牌面大小分別由下面兩個(gè) enum 來定義:
enum Suit: String {
case Spades = "黑桃"
case Hearts = "紅桃"
case Clubs = "草花"
case Diamonds = "方片"
}
enum Rank: Int, Printable {
case Ace = 1
case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
case Jack, Queen, King
var description: String {
switch self {
case .Ace:
return "A"
case .Jack:
return "J"
case .Queen:
return "Q"
case .King:
return "K"
default:
return String(self.rawValue)
}
}
}
因?yàn)樵谖覀冞@個(gè)特定的情況中并沒有帶有參數(shù)的枚舉類型,所以我們可以利用 static 的屬性來獲取一個(gè)可以進(jìn)行循環(huán)的數(shù)據(jù)結(jié)構(gòu):
protocol EnumeratableEnumType {
static var allValues: [Self] {get}
}
extension Suit: EnumeratableEnumType {
static var allValues: [Suit] {
return [.Spades, .Hearts, .Clubs, .Diamonds]
}
}
extension Rank: EnumeratableEnumType {
static var allValues: [Rank] {
return [.Ace, .Two, .Three,
.Four, .Five, .Six,
.Seven, .Eight, .Nine,
.Ten, .Jack, .Queen, .King]
}
}
在這里我們使用了一個(gè)接口來更好地定義適用的接口宾抓。關(guān)于其中的 class 和 static 的使用情景子漩,可以參看這一篇總結(jié)豫喧。在實(shí)現(xiàn)了 allValues 后,我們就可以按照上面的思路寫出:
for suit in Suit.allValues {
for rank in Rank.allValues {
print("\(suit.rawValue)\(rank)")
}
}
// 輸出:
// 黑桃A
// 黑桃2
// 黑桃3
// ...
// 方片K
尾遞歸
一般對于遞歸幢泼,解決棧溢出的一個(gè)好方法是采用尾遞歸的寫法紧显。顧名思義,尾遞歸就是讓函數(shù)里的最后一個(gè)動(dòng)作是一個(gè)函數(shù)調(diào)用的形式缕棵,這個(gè)調(diào)用的返回值將直接被當(dāng)前函數(shù)返回孵班,從而避免在棧上保存狀態(tài)。這樣一來程序就可以更新最后的棧幀招驴,而不是新建一個(gè)篙程,來避免棧溢出的發(fā)生。在 Swift 2.0 中别厘,編譯器現(xiàn)在支持嵌套方法的遞歸調(diào)用了虱饿,因此 sum 函數(shù)的尾遞歸版本可以寫為:
func tailSum(n: UInt) -> UInt {
func sumInternal(n: UInt, current: UInt) -> UInt {
if n == 0 {
return current
} else {
return sumInternal(n - 1, current: current + n)
}
}
return sumInternal(n, current: 0)
}
tailSum(1000000)
但是如果你在項(xiàng)目中直接嘗試運(yùn)行這段代碼的話還是會報(bào)錯(cuò),因?yàn)樵?Debug 模式下 Swift 編譯器并不會對尾遞歸進(jìn)行優(yōu)化丹允。我們可以在 scheme 設(shè)置中將 Run 的配置從 Debug 改為 Release郭厌,這段代碼就能正確運(yùn)行了。
ASSOCIATED OBJECT
兩個(gè)對應(yīng)的運(yùn)行時(shí)的 get 和 set Associated Object 的 API 是這樣的:
func objc_getAssociatedObject(object: AnyObject!,
key: UnsafePointer<Void>
) -> AnyObject!
func objc_setAssociatedObject(object: AnyObject!,
key: UnsafePointer<Void>,
value: AnyObject!,
policy: objc_AssociationPolicy)
這兩個(gè) API 所接受的參數(shù)也都 Swift 化了雕蔽,并且因?yàn)?Swift 的安全性折柠,在類型檢查上嚴(yán)格了不少,因此我們有必要也進(jìn)行一些調(diào)整批狐。在 Swift 中向某個(gè) extension 里使用 Associated Object 的方式將對象進(jìn)行關(guān)聯(lián)的寫法是:
// MyClass.swift
class MyClass {
}
// MyClassExtension.swift
private var key: Void?
extension MyClass {
var title: String? {
get {
return objc_getAssociatedObject(self, &key) as? String
}
set {
objc_setAssociatedObject(self,
&key, newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
// 測試
func printTitle(input: MyClass) {
if let title = input.title {
print("Title: \(title)")
} else {
print("沒有設(shè)置")
}
}
let a = MyClass()
printTitle(a)
a.title = "Swifter.tips"
printTitle(a)
// 輸出:
// 沒有設(shè)置
// Title: Swifter.tips
key 的類型在這里聲明為了 Void?扇售,并且通過 & 操作符取地址并作為 UnsafePointer<Void> 類型被傳入。這在 Swift 與 C 協(xié)作和指針操作時(shí)是一種很常見的用法嚣艇。
INDIRECT 和嵌套 ENUM
我們用 enum 來重新定義鏈表結(jié)構(gòu)的話承冰,會是下面這個(gè)樣子:
indirect enum LinkedList<Element: Comparable> {
case Empty
case Node(Element, LinkedList<Element>)
}
let linkedList = LinkedList.Node(1, .Node(2, .Node(3, .Node(4, .Empty))))
// 單項(xiàng)鏈表:1 -> 2 -> 3 -> 4
實(shí)現(xiàn)鏈表節(jié)點(diǎn)的刪除方法,在 enum 中添加:
func linkedListByRemovingElement(element: Element)
-> LinkedList<Element> {
guard case let .Node(value, next) = self else {
return .Empty
}
return value == element ?
next : LinkedList.Node(value, next.linkedListByRemovingElement(element))
}
let result = linkedList.linkedListByRemovingElement(2)
print(result)
// 1 -> 3 -> 4
PROTOCOL EXTENSION
我們定義了這樣的一個(gè)接口和它的一個(gè)擴(kuò)展:
protocol A1 {
func method1() -> String
}
struct B1: A1 {
func method1() -> String {
return "hello"
}
}
在使用的時(shí)候食零,無論我們將實(shí)例的類型為 A1 還是 B1困乒,因?yàn)閷?shí)現(xiàn)只有一個(gè),所以沒有任何疑問贰谣,調(diào)用方法時(shí)的輸出都是 “hello”:
let b1 = B1() // b1 is B1
b1.method1()
// hello
let a1: A1 = B1()
// a1 is A1
a1.method1()
// hello
但是如果在接口里只定義了一個(gè)方法娜搂,而在接口擴(kuò)展中實(shí)現(xiàn)了額外的方法的話,事情就變得有趣起來了吱抚“儆睿考慮下面這組接口和它的擴(kuò)展:
protocol A2 {
func method1() -> String
}
extension A2 {
func method1() -> String {
return "hi"
}
func method2() -> String {
return "hi"
}
}
struct B2: A2 {
func method1() -> String {
return "hello"
}
func method2() -> String {
return "hello"
}
}
let b2 = B2()
b2.method1() // hello
b2.method2() // hello
但是如果我們稍作改變,在上面的代碼后面繼續(xù)添加:
let a2 = b2 as A2
a2.method1() // hello
a2.method2() // hi
我們可以看到秘豹,對 a2 調(diào)用 method2 實(shí)際上是接口擴(kuò)展中的方法被調(diào)用了携御,而不是 a2 實(shí)例中的方法被調(diào)用。我們不妨這樣來理解:對于 method1,因?yàn)樗?protocol 中被定義了啄刹,因此對于一個(gè)被聲明為遵守接口的類型的實(shí)例 (也就是對于 a2) 來說涮坐,可以確定實(shí)例必然實(shí)現(xiàn)了 method1,我們可以放心大膽地用動(dòng)態(tài)派發(fā)的方式使用最終的實(shí)現(xiàn) (不論它是在類型中的具體實(shí)現(xiàn)誓军,還是在接口擴(kuò)展中的默認(rèn)實(shí)現(xiàn))膊升;但是對于 method2 來說,我們只是在接口擴(kuò)展中進(jìn)行了定義谭企,沒有任何規(guī)定說它必須在最終的類型中被實(shí)現(xiàn)廓译。在使用時(shí),因?yàn)?a2 只是一個(gè)符合 A2 接口的實(shí)例债查,編譯器對 method2 唯一能確定的只是在接口擴(kuò)展中有一個(gè)默認(rèn)實(shí)現(xiàn)非区,因此在調(diào)用時(shí),無法確定安全盹廷,也就不會去進(jìn)行動(dòng)態(tài)派發(fā)征绸,而是轉(zhuǎn)而編譯期間就確定的默認(rèn)實(shí)現(xiàn)。
整理一下相關(guān)的規(guī)則的話:
如果類型推斷得到的是實(shí)際的類型
那么類型中的實(shí)現(xiàn)將被調(diào)用俄占;如果類型中沒有實(shí)現(xiàn)的話管怠,那么接口擴(kuò)展中的默認(rèn)實(shí)現(xiàn)將被使用
如果類型推斷得到的是接口,而不是實(shí)際類型
并且方法在接口中進(jìn)行了定義缸榄,那么類型中的實(shí)現(xiàn)將被調(diào)用渤弛;如果類型中沒有實(shí)現(xiàn),那么接口擴(kuò)展中的默認(rèn)實(shí)現(xiàn)被使用
否則 (也就是方法沒有在接口中定義)甚带,擴(kuò)展中的默認(rèn)實(shí)現(xiàn)將被調(diào)用
代碼組織和 FRAMEWORK
http://swifter.tips/code-framework/
DELEGATE
protocol MyClassDelegate {
func method()
}
class MyClass {
weak var delegate: MyClassDelegate?
}
class ViewController: UIViewController, MyClassDelegate {
// ...
var someInstance: MyClass!
override func viewDidLoad() {
super.viewDidLoad()
someInstance = MyClass()
someInstance.delegate = self
}
func method() {
print("Do something")
}
//...
}
// weak var delegate: MyClassDelegate? 編譯錯(cuò)誤
// 'weak' cannot be applied to non-class type 'MyClassDelegate'
在 protocol 聲明的名字后面加上 class她肯,這可以為編譯器顯式地指明這個(gè) protocol 只能由 class 來實(shí)現(xiàn)。
protocol MyClassDelegate: class {
func method()
}
LOCK
如果我們喜歡以前的那種形式鹰贵,甚至可以寫一個(gè)全局的方法晴氨,并接受一個(gè)閉包,來將 objc_sync_enter 和 objc_sync_exit 封裝起來:
func synchronized(lock: AnyObject, closure: () -> ()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}
再結(jié)合 Swift 的尾隨閉包的語言特性碉输,這樣籽前,使用起來的時(shí)候就和 Objective-C 中很像了:
func myMethodLocked(anObj: AnyObject!) {
synchronized(anObj) {
// 在括號內(nèi) anObj 不會被其他線程改變
}
}
PLAYGROUND 與項(xiàng)目協(xié)作
Playground 其實(shí)是可以用在項(xiàng)目里的,通過配置敷钾,我們是可以做到讓 Playground 使用項(xiàng)目中已有的代碼的枝哄。直接說結(jié)論的話,我們需要滿足以下的一些條件:
Playground 必須加入到項(xiàng)目之中闰非,單獨(dú)的 Playground 是不能使用項(xiàng)目中的已有代碼的膘格;
最簡單的方式是在項(xiàng)目中使用 File -> New -> File... 然后在里面選擇 Playground峭范。注意不要直接選擇 File -> New -> Playground...财松,否則的話你還需要將新建的 Playground 拖到項(xiàng)目中來。
想要使用的代碼必須是通過 Cocoa (Touch) Framework 以一個(gè)單獨(dú)的 target 的方式進(jìn)行組織的;
編譯結(jié)果的位置需要保持默認(rèn)位置辆毡,即在 Xcode 設(shè)置中的 Locations 里 Derived Data 保持默認(rèn)值菜秦;
如果是 iOS 應(yīng)用,這個(gè)框架必須已經(jīng)針對 iPhone 5s Simulator 這樣的** 64 位的模擬器**作為目標(biāo)進(jìn)行過編譯舶掖;
iOS 的 Playground 其實(shí)是運(yùn)行在 64 位模擬器上的球昨,因此為了能找到對應(yīng)的符號和執(zhí)行文件,框架代碼的位置和編譯架構(gòu)都是有所要求的眨攘。
在滿足這些條件后主慰,你就可以在 Playground 中通過 import 你的框架 module 名字來導(dǎo)入代碼,然后進(jìn)行使用了鲫售。
安全的資源組織方式
假設(shè)我們有下面的代碼:
enum ImageName: String {
case MyImage = "my_image"
}
enum SegueName: String {
case MySegue = "my_segue"
}
extension UIImage {
convenience init!(imageName: ImageName) {
self.init(named: imageName.rawValue)
}
}
extension UIViewController {
func performSegueWithSegueName(segueName: SegueName, sender: AnyObject?) {
performSegueWithIdentifier(segueName.rawValue, sender: sender)
}
}
在使用時(shí)共螺,就可以直接用 extension 中的類型安全的版本了:
let image = UIImage(imageName: .MyImage)
performSegueWithSegueName(.MySegue, sender: self)
但僅僅這樣其實(shí)還是沒有徹底解決名稱變更帶來的問題。不過在 Swift 中情竹,根據(jù)項(xiàng)目內(nèi)容來自動(dòng)化生成像是 ImageName 和 SegueName 這樣的類型并不是一件難事藐不。Swift 社區(qū)中現(xiàn)在也有一些比較成熟的自動(dòng)化工具了,R.swift 和 SwiftGen 就是其中的佼佼者秦效。它們通過掃描我們的項(xiàng)目文件雏蛮,來提取出對應(yīng)的字符串,然后自動(dòng)生成對應(yīng)的 enum 或者 struct 文件阱州。當(dāng)我們之后添加挑秉,刪除或者改變資源名稱的時(shí)候,這些工具可以為我們重新生成對應(yīng)的代表資源名字的類型苔货,從而讓我們可以利用編譯器的檢查來確保代碼中所有對該資源的引用都保持正確衷模。這在需要協(xié)作的項(xiàng)目中會是非常可靠和值得提倡的做法蒲赂。
TOLL-FREE BRIDGING 和 UNMANAGED
細(xì)心的讀者可能會發(fā)現(xiàn)在 Objective-C 中類型的名字是 CFURLRef阱冶,而到了 Swift 里成了 CFURL。CFURLRef 在 Swift 中是被 typealias 到 CFURL 上的滥嘴,其實(shí)不僅是 URL木蹬,其他的各類 CF 類型都進(jìn)行了類似的處理。這主要是為了減少 API 的迷惑:現(xiàn)在這些 CF 類型的行為更接近于 ARC 管理下的對象若皱,因此去掉 Ref 更能表現(xiàn)出這一特性镊叁。
另外在 Objective-C 時(shí)代 ARC 不能處理的一個(gè)問題是 CF 類型的創(chuàng)建和釋放。雖然不能自動(dòng)化走触,但是遵循命名規(guī)則來處理的話還是比較簡單的:對于 CF 系的 API晦譬,如果 API 的名字中含有 Create,Copy 或者 Retain 的話互广,在使用完成后敛腌,我們需要調(diào)用 CFRelease 來進(jìn)行釋放卧土。
不過 Swift 中這條規(guī)則已成明日黃花。既然我們有了明確的規(guī)則像樊,那為什么還要一次一次不厭其煩地手動(dòng)去寫 Release 呢尤莺?基于這種想法,Swift 中我們不再需要顯式地去釋放帶有這些關(guān)鍵字的內(nèi)容了 (事實(shí)上生棍,含有 CFRelease 的代碼甚至無法通過編譯)颤霎。也就是說,CF 現(xiàn)在也在 ARC 的管轄范圍之內(nèi)了涂滴。其實(shí)背后的機(jī)理一點(diǎn)都不復(fù)雜友酱,只不過在合適的地方加上了像 CF_RETURNS_RETAINED 和 CF_RETURNS_NOT_RETAINED 這樣的標(biāo)注。
但是有一點(diǎn)例外柔纵,那就是對于非系統(tǒng)的 CF API (比如你自己寫的或者是第三方的)粹污,因?yàn)椴]有強(qiáng)制機(jī)制要求它們一定遵照 Cocoa 的命名規(guī)范,所以貿(mào)然進(jìn)行自動(dòng)內(nèi)存管理是不可行的首量。如果你沒有明確地使用上面的標(biāo)注來指明內(nèi)存管理的方式的話壮吩,將這些返回 CF 對象的 API 導(dǎo)入 Swift 時(shí),它們的類型會被對對應(yīng)為 Unmanaged<T>加缘。