Swift 知識小集

首發地址:
http://blog.ifelseboyxx.com/2017/12/07/swift-tips/
ps:簡書不知道怎么生成目錄,知道的望告知...

以下內容均是筆者學習過程中收集的知識點,順序比較跳躍,初衷是為了方便查閱,順便加深記憶。內容會不斷更新,如果有什么問題或者有好的 Swift 方面的語法糖或者知識點也可以提出來,我會挑選斟酌后收錄,歡迎大家關注~

環境:

Swift 4.0
Xcode 9.1

最近更新:2018.01.09

取消 asyncAfter 中延遲的事件
for in 和 forEach 的區別
訪問級別
幾種遍歷方式
weakSelf 和 strongSelf

更新:2017.12.13

優雅的定義通知名稱

更新:2017.12.11

作用域:do 語句塊
倒序 reversed()
標簽語句:指定跳出某個條件語句

最近更新:2017.12.08

Swift 中的 “@synchronized”
自定義日志輸出
Swift 中的 “readonly”

目錄:

Associated Object
Delegate 聲明為 weak
可選協議和協議擴展
單例
輸出格式化
Selector
將 protocol 的方法聲明為 mutating
數組遍歷 enumerate
輸入輸出參數 inout
Default 參數
延遲加載 lazy
編譯標記
換行符
字符串切割 split
KVC
Swift 中值類型和引用類型注意點
KVO
Swift UIButton 狀態的疊加
Swift 中的 “@synchronized”
自定義日志輸出
Swift 中的 “readonly”
作用域:do 語句塊
倒序 reversed()
標簽語句:指定跳出某個條件語句
優雅的定義通知名稱
weakSelf 和 strongSelf
幾種遍歷方式
訪問級別
for in 和 forEach 的區別
取消 asyncAfter 中延遲的事件

Associated Object

Objective-C 的 runtime 里的 Associated Object 允許我們在使用 Category 擴展現有的類的功能的時候,直接添加實例變量。在 Swift 中 extension 不能添加存儲屬性,我們可以利用 Associated Object 來實現,比如下面的 title 「實際上」是一個存儲屬性:

// MyClass.swift
class MyClass {}

// MyClassExtension.swift
private var key: Void?

extension MyClass {
    var title: String? {
        get {
            return swift_getAssociatedObject(self, &key) as? String
        }

        set {
            swift_setAssociatedObject(self,
                &key, newValue,
                .swift_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}
// 測試
func printTitle(_ input: MyClass) {
    if let title = input.title {
        print("Title: \(title)")
    } else {
        print("沒有設置")
    }
}

let a = MyClass()
printTitle(a)
a.title = "Swifter.tips"
printTitle(a)

// 輸出:
// 沒有設置
// Title: Swifter.tips”

Delegate 聲明為 weak

Swift 中 Delegate 需要被聲明成 weak,來避免訪問到已被回收的內存而導致崩潰,如果我們像下面這樣,是編譯不過的:

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' may only be applied to class and class-bound protocol types, not 'MyClassDelegate'

這是因為 Swift 的 protocol 是可以被除了 class 以外的其他類型遵守的,而對于像 struct 或是 enum 這樣的類型,本身就不通過引用計數來管理內存,所以也不可能用 weak 這樣的 ARC 的概念來進行修飾。

想要在 Swift 中使用 weak delegate,我們就需要將 protocol 限制在 class 內:

  • 一種做法是將 protocol 聲明為 Objective-C 的,這可以通過在 protocol 前面加上 @swift 關鍵字來達到,Objective-C 的 protocol 都只有類能實現,因此使用 weak 來修飾就合理了:
@swift protocol MyClassDelegate {
    func method()
}
  • 另一種可能更好的辦法是在 protocol 聲明的名字后面加上 class,這可以為編譯器顯式地指明這個 protocol 只能由 class 來實現,避免了過多的不必要的 Objective-C 兼容:
protocol MyClassDelegate: class {
    func method()
}

可選協議和協議擴展

Objective-C 中的 protocol 里存在 @optional 關鍵字,被這個關鍵字修飾的方法并非必須要被實現,原生的 Swift protocol 里沒有可選項,所有定義的方法都是必須實現的,如果不是實現是無法編譯的:

class ViewController: UIViewController,MyProtocol { }

// 編譯失敗
// Type 'ViewController' does not conform to protocol 'MyProtocol'

如果我們想要像 Objective-C 里那樣定義可選的協議方法,就需要將協議本身和可選方法都定義為 Objective-C 的,也即在 protocol 定義之前加上 @swift,方法之前加上 @swift optional

@swift protocol MyProtocol {
    @swift optional func myMethod()
}

另外,對于所有的聲明,它們的前綴修飾是完全分開的,也就是說你不能像是在 Objective-C 里那樣用一個 @optional 指定接下來的若干個方法都是可選的了,必須對每一個可選方法添加前綴,對于沒有前綴的方法來說,它們是默認必須實現的:

@swift protocol MyProtocol {
    @swift optional func optionalMethod()        // 可選
    func necessaryMethod()                      // 必須
    @swift optional func anotherOptionalMethod() // 可選
}

一個不可避免的限制是,使用 @swift 修飾的 protocol 就只能被 class 實現了,也就是說,對于 structenum 類型,我們是無法令它們所實現的協議中含有可選方法或者屬性的。另外,實現它的 class 中的方法還必須也被標注為 @swift,或者整個類就是繼承自 NSObject。對于這種問題,在 Swift 2.0 中,我們有了另一種選擇,那就是使用 protocol extension。我們可以在聲明一個 protocol 之后再用 extension 的方式給出部分方法默認的實現,這樣這些方法在實際的類中就是可選實現的了:

protocol MyProtocol {
    func optionalMethod()        // 可選
    func necessaryMethod()       // 必須
    func anotherOptionalMethod() // 可選
}

extension MyProtocol {
    
    //默認的可選實現
    func optionalMethod() {
        print("optionalMethod")
    }
    
    //默認的可選實現
    func anotherOptionalMethod() {
        print("anotherOptionalMethod")
    }   
}
class ViewController: UIViewController,MyProtocol {
    
    // 必須的實現
    func necessaryMethod() {
        print("necessaryMethod")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.optionalMethod();
        self.necessaryMethod();
        self.anotherOptionalMethod();
    }
}

// 輸出:
// optionalMethod
// necessaryMethod
// necessaryMethod

單例

Swift 中的單例非常簡單,Swift 1.2 以及之后:

class Singleton  {
    static let sharedInstance = Singleton()
    private init() {}
}

這種寫法不但是線程安全的,也是懶加載的,let 定義的屬性本身就是線程安全的,同時 static 定義的是一個 class constant,擁有全局作用域和懶加載特性。

另外,這個類型中加入了一個私有的初始化方法,來覆蓋默認的公開初始化方法,這讓項目中的其他地方不能夠通過 init 來生成自己的 Singleton 實例,也保證了類型單例的唯一性。如果你需要的是類似 default 的形式的單例 (也就是說這個類的使用者可以創建自己的實例) 的話,可以去掉這個私有的 init 方法。

輸出格式化

在 Objective-C 中的 %@ 這樣的格式在指定的位置設定占位符,然后通過參數的方式將實際要輸出的內容補充完整。例如 Objective-C 中常用的向控制臺輸出的 NSLog 方法就使用了這種格式化方法:

float a = 1.234567;
NSString *b = @"Helllo";
NSLog(@"float:%.2f  str:%p",a,b);

// 輸出:
// float:1.23  str:0x1024a1078

對應 Swift 中我們可以這樣:

let a = 1.234567
let b = "Helllo"
let c = String(format:"float:%.2f str:%p",a,b)
print(c)

// 輸出:
// float:1.23 str:0x604000249e10

Selector

@selector 是 Objective-C 時代的一個關鍵字,它可以將一個方法轉換并賦值給一個 SEL 類型,它的表現很類似一個動態的函數指針。在 Swift 中沒有 @selector 了,取而代之,從 Swift 2.2 開始我們使用 #selector 來從暴露給 Objective-C 的代碼中獲取一個 selector,并且因為 selector 是 Objective-C runtime 的概念,在 Swift 4 中,默認情況下所有的 Swift 方法在 Objective-C 中都是不可見的,所以你需要在這類方法前面加上 @swift 關鍵字,將這個方法暴露給 Objective-C,才能進行使用:

let btn = UIButton.init(type: .system)
btn.backgroundColor = UIColor.red
btn.frame = CGRect(x: 100, y: 100, width: 150, height: 40)
btn.setTitle("Button", for: .normal)
//無參數
btn.addTarget(self, action: #selector(btnClick), for: .touchUpInside)
view.addSubview(btn)

@swift func btnClick()  {
    print("button click !")
}
...
//有參數
btn.addTarget(self, action: #selector(btnClick(_ :)), for: .touchUpInside)
...

@swift func btnClick(_ button: UIButton)  {
    print("button click !")
}

將 protocol 的方法聲明為 mutating

Swift 的 protocol 不僅可以被 class 類型實現,也適用于 structenum,因為這個原因,我們在寫給別人用的協議時需要多考慮是否使用 mutating 來修飾方法。Swift 的 mutating 關鍵字修飾方法是為了能在該方法中修改 struct 或是 enum 的變量,所以如果你沒在協議方法里寫 mutating 的話,別人如果用 struct 或者 enum 來實現這個協議的話,就不能在方法里改變自己的變量了,比如下面的代碼是編譯不過的:

protocol Vehicle {
    
    func changeColor()
    
}

struct MyCar: Vehicle {
    
    var color = "blue"
    
    func changeColor() {
        color = "red"
    }
}

// 編譯失敗
// Cannot assign to property: 'self' is immutable

我們應該加上 mutating 關鍵字:


protocol Vehicle {
    
    mutating func changeColor()
    
}

struct MyCar: Vehicle {
    
    var color = "blue"
    
    mutating func changeColor() {
        color = "red"
    }
}

override func viewDidLoad() {
    super.viewDidLoad()

    var car = MyCar()
    print(car.color)
    car.changeColor()
    print(car.color)    
}

// 輸出:
// blue
// 輸出:
// red

數組遍歷 enumerate

使用 NSArray 時一個很常遇見的的需求是在枚舉數組內元素的同時也想使用對應的下標索引,在 Objective-C 中最方便的方式是使用 NSArray 的 enumerateObjectsUsingBlock: ,在 Swift 中存在一個效率,安全性和可讀性都很好的替代,那就是快速枚舉某個數組的EnumerateGenerator,它的元素是同時包含了元素下標索引以及元素本身的多元組:

let arr = ["a","b","c","d","e"]
for (idx, str) in arr.enumerated() {
    print("idx: \(idx) str: \(str)")
}

// 輸出:
idx: 0 str: a
idx: 1 str: b
idx: 2 str: c
idx: 3 str: d
idx: 4 str: e

輸入輸出參數 inout

函數參數默認是常量,如果試圖在函數體中更改參數值將會導致編譯錯誤,比如下面的例子中嘗試著交換值:

func swapTwoInts(_ a: Int, _ b: Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

// 編譯失敗
// Cannot assign to value: 'a' is a 'let' constant
// Cannot assign to value: 'b' is a 'let' constant

如果想要一個函數可以修改參數的值,并且想要在這些修改在函數調用結束后仍然存在,那么就應該把這個參數定義為輸入輸出參數(In-Out Parameters):

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Default 參數

Swift 的方法是支持默認參數的,也就是說在聲明方法時,可以給某個參數指定一個默認使用的值。在調用該方法時要是傳入了這個參數,則使用傳入的值,如果缺少這個輸入參數,那么直接使用設定的默認值進行調用。和其他很多語言的默認參數相比較,Swift 中的默認參數限制更少,并沒有所謂 "默認參數之后不能再出現無默認值的參數"這樣的規則,舉個例子,下面兩種方法的聲明在 Swift 里都是合法可用的:

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(str1: "Hello", str2: " ")

//輸出都是 Hello World

延遲加載 lazy

延時加載或者說延時初始化是很常用的優化方法,在構建和生成新的對象的時候,內存分配會在運行時耗費不少時間,如果有一些對象的屬性和內容非常復雜的話,這個時間更是不可忽略。另外,有些情況下我們并不會立即用到一個對象的所有屬性,而默認情況下初始化時,那些在特定環境下不被使用的存儲屬性,也一樣要被初始化和賦值,也是一種浪費。在 Objective-C 中,一個延遲加載一般是這樣的:

// ClassA.h
@property (nonatomic, copy) NSString *testString;

// ClassA.m
- (NSString *)testString {
     if (!_testString) {
         _testString = @"Hello";
        NSLog(@"只在首次訪問輸出");
     }
     return _testString;
}

對應在 Swift 中,使用 lazy 作為屬性修飾符時,只能聲明屬性是變量,且我們需要顯式地指定屬性類型,否則會編譯錯誤:

class ClassA {
    lazy let str: String = {
        let str = "Hello"
        print("只在首次訪問輸出")
        return str
    }()
}

// 編譯失敗
// 'lazy' cannot be used on a let

class ClassA {
    lazy var str = {
        let str = "Hello"
        print("只在首次訪問輸出")
        return str
    }()
}

// 編譯失敗
// Unable to infer complex closure return type

我們應該聲明為 var 并指定好類型:

class ClassA {
    lazy var str: String = {
        let str = "Hello"
        print("只在首次訪問輸出")
        return str
    }()
}

override func viewDidLoad() {
    super.viewDidLoad()

    let ca = ClassA()
    print(ca.str)
    print(ca.str)
    print(ca.str)   
}

// 輸出:
// 只在首次訪問輸出
// Hello
// Hello
// Hello

如果不需要做什么額外工作的話,也可以對這個 lazy 的屬性直接寫賦值語句:

lazy var str: String = "Hello"

我們還可以利用 lazy 配合像 map 或是 filter 這類接受閉包并進行運行的方法一起,讓整個行為變成延時進行的。在某些情況下這么做也對性能會有不小的幫助。例如,直接使用 map 時:

let data = 1...3
let result = data.map {
    (i: Int) -> Int in
    print("正在處理 \(i)")
    return i * 2
}

print("準備訪問結果")
for i in result {
    print("操作后結果為 \(i)")
}

print("操作完畢")

// 輸出:
// 正在處理 1
// 正在處理 2
// 正在處理 3
// 準備訪問結果
// 操作后結果為 2
// 操作后結果為 4
// 操作后結果為 6
// 操作完畢

而如果我們先進行一次 lazy 操作的話,我們就能得到延時運行版本的容器:

let data = 1...3
let result = data.lazy.map {
    (i: Int) -> Int in
    print("正在處理 \(i)")
    return i * 2
}

print("準備訪問結果")
for i in result {
    print("操作后結果為 \(i)")
}

print("操作完畢")

// 準備訪問結果
// 正在處理 1
// 操作后結果為 2
// 正在處理 2
// 操作后結果為 4
// 正在處理 3
// 操作后結果為 6
// 操作完畢

對于那些不需要完全運行,可能提前退出的情況,使用 lazy 來進行性能優化效果會非常有效。

編譯標記

在 Objective-C 中,我們經常在代碼中插入 #param 符號來標記代碼的區間,這樣在 Xcode 的導航欄中我們就可以看到組織分塊后的方法列表。在 Swift 中我們可以用 MARK: 來代替:

image

在 Objective-C 中還有一個很常用的編譯標記,那就是 #warning,一個 #warning 標記可以在 Xcode 的代碼編輯器中顯示為明顯的黃色警告條,非常適合用來提示代碼的維護者和使用者需要對某些東西加以關注。在 Swift 中我們可以用 FIXME:TODO: 配合 shell 來代替:

image

腳本:

TAGS="TODO:|FIXME:"
echo "searching ${SRCROOT} for ${TAGS}"
find "${SRCROOT}" \( -name "*.swift" \) -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/"

效果:

image

image

換行符

在 Swift 3 中,需要換行時是需要 \n

let str = "xxxx\nxxx"

// 輸出:
// xxxx
// xxx

在 swift 4 中,我們可以使用 """

let jsonStr = """
        {
            "id": 123455,
            "nickname": "xxxx",
            "isMale": true,
            "birthday": "2000年3月24日",
            "personalURL": "https://xxxxxx.github.io"
        }
        """
          
// 輸出:
{
    "id": 123455,
    "nickname": "xxxx",
    "isMale": true,
    "birthday": "2000年3月24日",
    "personalURL": "https://xxxxxx.github.io"
}

字符串切割 split

我們需要切割某個字符串時可以用 split 方法,需要注意的是,返回的結果是個數組

let str = "Hello,world !"
print(str.split(separator: ","))

// 輸出:
// ["Hello", "world !"]

KVC

class MyClass {
    var name = "ifelseboyxx"
}

Swift 4 中 Apple 引入了新的 KeyPath 的表達方式,現在,對于類型 MyClass 中的變量 name,對應的 KeyPath 可以寫為 \MyClass.name,利用 KVC 修改 name 值的話,我們可以這么操作:

let object = MyClass()
print("name: \(object.name)")
// set
object[keyPath: \MyClass.name] = "ifelseboy"
// get
print("name: \(object[keyPath: \MyClass.name])")

// 輸出:
// name: ifelseboyxx
// name: ifelseboy

另外 Swift 4 中 struct 同樣支持 KVC :

struct MyStruct {
    var age: Int
}

var obj = MyStruct(age: 18)
print("我今年 \(obj.age) 歲了")
obj[keyPath: \MyStruct.age] = 8
print("我今年 \(obj[keyPath: \MyStruct.age]) 歲了")

// 輸出:
// 我今年 18 歲了
// 我今年 8 歲了

Swift 中值類型和引用類型注意點

KVC 一節中代碼里有個注意點:

var obj = MyStruct(age: 18)
//替換為
let obj = MyStruct(age: 18)

是編譯不過的,會報錯:

Cannot assign to immutable expression of type 'Int'

筆者初次也犯了這樣的錯誤,想當然的認為 MyClasslet 聲明的是沒有問題的,struct 也一樣:

let object = MyClass()

其實原因很簡單,swift 中 Class 是引用類型的,而 struct 是值類型的:值類型在被賦給一個變量,或被傳給函數時,實際是做了一次拷貝。引用類型在被賦給一個變量,或被傳給函數時,傳遞的是引用。

KVO

很遺憾,依然只有 NSObject 才能支持 KVO,另外由于 Swift 為了效率,默認禁用了動態派發,因此想用 Swift 來實現 KVO,我們還需要做額外的工作,那就是將想要觀測的對象標記為 dynamic@objc,下面的 ?? 是 ViewController 監聽 MyClassdate 屬性:

class MyClass: NSObject {
    @objc dynamic var date = Date()
}

class ViewController: UIViewController {
    
    var myObject: MyClass!
    var observation: NSKeyValueObservation?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        myObject = MyClass()
        print("當前日期:\(myObject.date)")
    
        observation = myObject.observe(\MyClass.date, options: [.old,.new], changeHandler: { (_, change) in
            if let newDate = change.newValue , let oldDate = change.oldValue {
                print("日期發生變化 old:\(oldDate) new:\(newDate) ")
            }
        })
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
            self.myObject.date = Date()
        }
    }
}

// 輸出:
// 當前日期:2017-12-07 06:31:26 +0000
// 日期發生變化 old:2017-12-07 06:31:26 +0000 new:2017-12-07 06:31:27 +0000 

在 Objective-C 中我們幾乎可以沒有限制地對所有滿足 KVC 的屬性進行監聽,而現在我們需要屬性有 dynamic@objc 進行修飾。大多數情況下,我們想要觀察的類包含這兩個修飾 (除非這個類的開發者有意為之,否則一般也不會有人愿意多花功夫在屬性前加上它們,因為這畢竟要損失一部分性能),并且有時候我們很可能也無法修改想要觀察的類的源碼。遇到這樣的情況的話,一個可能可行的方案是繼承這個類并且將需要觀察的屬性使用 dynamic@objc 進行重寫。比如剛才我們的 MyClass 中如果 date 沒有相應標注的話,我們可能就需要一個新的 MyChildClass了:

class MyClass: NSObject {
    var date = Date()
}

class MyChildClass: MyClass {
    @objc dynamic override var date: Date {
        get { return super.date }
        set { super.date = newValue }
    }
}

class ViewController: UIViewController {
    
    var myObject: MyChildClass!
    var observation: NSKeyValueObservation?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        myObject = MyChildClass()
        print("當前日期:\(myObject.date)")
    
        observation = myObject.observe(\MyChildClass.date, options: [.old,.new], changeHandler: { (_, change) in
            if let newDate = change.newValue , let oldDate = change.oldValue {
                print("日期發生變化 old:\(oldDate) new:\(newDate) ")
            }
        })
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
            self.myObject.date = Date()
        }
    }
}

// 輸出:
// 當前日期:2017-12-07 06:36:50 +0000
// 日期發生變化 old:2017-12-07 06:36:50 +0000 new:2017-12-07 06:36:51 +0000 

Swift UIButton 狀態的疊加

在 Objective-C 中,如果我們想疊加按鈕的某個狀態,可以這么寫:

UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setTitle:@"Test" forState:UIControlStateNormal | UIControlStateSelected];

對應的 Swift 我們可以這么寫:

let btn = UIButton.init(type: .custom)
btn.setTitle("hehe", for: [.normal ,.selected])

把需要疊加的狀態用個數組裝起來就行了。

Swift 中的 “@synchronized”

在 Objective-C 中,我們可以用 @synchronized 這個關鍵字可以用來修飾一個變量,并為其自動加上和解除互斥鎖。這樣,可以保證變量在作用范圍內不會被其他線程改變:

- (void)myMethod:(id)anObj {
    @synchronized(anObj) {
        // 在括號內持有 anObj 鎖
    }
}

雖然這個方法很簡單好用,但是很不幸的是在 Swift 中它已經 (或者是暫時) 不存在了。其實 @synchronized 在幕后做的事情是調用了 objc_sync 中的 objc_sync_enterobjc_sync_exit 方法,并且加入了一些異常判斷。因此,在 Swift 中,如果我們忽略掉那些異常的話,我們想要 lock 一個變量的話,可以這樣寫:

func myMethod(anObj: AnyObject!) {
    objc_sync_enter(anObj)

    // 在 enter 和 exit 之間持有 anObj 鎖

    objc_sync_exit(anObj)
}

更進一步,如果我們喜歡以前的那種形式,甚至可以寫一個全局的方法,并接受一個閉包,來將 objc_sync_enterobjc_sync_exit 封裝起來:

func synchronized(_ lock: AnyObject, closure: () -> ()) {
    objc_sync_enter(lock)
    closure()
    objc_sync_exit(lock)
}

// 使用:
synchronized(self) {
            
}

這樣使用起來就和 Objective-C 中 @synchronized 很像了。

再舉個 ?? ,如果我們想為某個類實現一個線程安全的 setter,可以這樣:

class Obj {
    var _str = "123"
    var str: String {
        get {
            return _str
        }
        set {
            synchronized(self) {
                _str = newValue
            }
        }
    }
}

自定義日志輸出

在 Objective-C 中,我們通常會自定義日志輸出來完善信息以及避免 release 下的輸出,比如下面這種,可以額外提供行數、方法名等信息:

#ifdef DEBUG
#define XXLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#else
#define XXLog(...)
#endif
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    XXLog(@"ifelseboyxx");

    return YES;
}

// 輸出:
// 2017-12-08 13:32:02.211306+0800 Demo[17902:88775537] -[AppDelegate application:didFinishLaunchingWithOptions:] [Line 28] ifelseboyxx

在 Swift 中,我們可以這樣自定義:

func xxprint<T>(_ message: T, filePath: String = #file, line: Int = #line, function: String = #function) {
    #if DEBUG
        let fileName = (filePath as NSString).lastPathComponent.replacingOccurrences(of: ".Swift", with: "")
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale.current
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        print("[" + dateFormatter.string(from: Date()) + " " + fileName + " " + function + " \(line)" + "]:" + "\(message)")
    #endif
}
class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        xxprint("ifelseboyxx")
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
            xxprint("ifelseboyxx")
        }
    }
}

// 輸出:
// [2017-12-08 13:49:38 ViewController.swift viewDidLoad() 27]:ifelseboyxx
// [2017-12-08 13:49:39 ViewController.swift viewDidLoad() 29]:ifelseboyxx

Swift 中的 “readonly”

在 Objective-C 中,我們通常把屬性聲明為 readonly 來提醒別人:“不要修改!!”,通常這么寫:

@interface Person : NSObject

@property (nonatomic, readonly, copy) NSString *name;

@end

如果外部嘗試修改的話,會編譯錯誤:

- (void)viewDidLoad {
    [super viewDidLoad];
   
    Person *p = [Person new];
    p.name = @"ifelseboyxx";
    
}

// 編譯錯誤:
// Assignment to readonly property

有些情況下,我們希望內部可以點語法訪問 name 屬性,也就是 self.name,但是因為是 readonly 的,會編譯錯誤:

@implementation Person

- (instancetype)init {
    self = [super init];
    if (self) {
        self.name = @"ifelseboyxx";
    }
    return self;
}

@end

// 編譯錯誤:
// Assignment to readonly property

這時候我們就會在內部的 extension 重新聲明一個 readwrite的同樣的屬性,也就是“外部只讀,內部可寫”

@interface Person ()

@property (nonatomic, readwrite, copy) NSString *name;

@end

在 Swift 中,我們可能有同樣的場景。這里就不得不提到 privatefileprivate 關鍵字了。
private表示聲明為私有的實體只能在其聲明的范圍內被訪問。比如我在 MyClass 中聲明了一個私有的 name 屬性,外部訪問的話會編譯錯誤:

class MyClass {
    private var name: String = "Test"
}

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        let only = MyClass()
        print(only.name) 
        only.name = "ifelseboyxxv587"       
    }
}

// 編譯異常:
// 'name' is inaccessible due to 'private' protection level

fileprivate,看命名我們大概能猜到,就是將對實體的訪問權限于它聲明的源文件。通俗點講,比如我上面的代碼都是在 ViewController.swift 這個文件里的,我把 private 修改為 fileprivate,就不會編譯錯誤了:

class MyClass {
    fileprivate var name: String = "Test"
}

那么如果非 ViewController.swift 文件,也想訪問 MyClassname 屬性該怎么辦呢?我們可以把 name 屬性聲明為 fileprivate(set),就要就達到類似 Objective-C 中的 readonly 效果了 :

// ViewController.swift 文件

class MyClass {
    fileprivate(set) var name: String = "Test"
}

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        let only = MyClass()
        print(only.name)
        only.name = "ifelseboyxxv587"
        print(only.name)
    }
}

// 編譯正常,ViewController.swift 文件內可讀可寫
// 輸出:
// Test
// ifelseboyxxv587
// AppDelegate.swift 文件

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

    let only = MyClass()
    print(only.name) //只能讀
    only.name = "ifelseboyxxv587" //這里報錯,不能寫
        
    return true
}

// 編譯異常:
// Cannot assign to property: 'name' setter is inaccessible

作用域:do 語句塊

在 Objective-C 中,我們可以利用 {} 來開辟新的作用域,來避免對象名稱重復的問題:

NSString *ha = @"測試一";
    
{
    NSString *ha = @"測試二";
    NSLog(@"%@",ha);
}
    
NSLog(@"%@",ha);

// 輸出:
// 2017-12-11 16:55:20.303132+0800 Demo[48418:93027416] 測試二
// 2017-12-11 16:55:20.303316+0800 Demo[48418:93027416] 測試一

在 Swift 中,取代 {} 的是 do {}

let ha = "測試一"
        
do {
    let ha = "測試二"
    print(ha)
}
        
print(ha)

// 輸出:
// 測試二
// 測試一

倒序 reversed()

在 Objective-C 中,我們如果想倒序數組一般這樣:

NSArray *array = @[@"1",@"2",@"3"];
    
NSArray *reversedArray = [[array reverseObjectEnumerator] allObjects];

// 輸出:
// 2017-12-11 17:39:57.127466+0800 Demo[49004:93210504] (
    3,
    2,
    1
)

在 Swift 中,相對簡單點:

let arr:[String] = ["1","2","3"]
let reversedArr:[String] = arr.reversed()

// 輸出:
// ["3", "2", "1"]

標簽語句:指定跳出某個條件語句

在 Objective-C 中,如果遇到多層嵌套的條件語句,我們如果想要指定跳出某個條件語句是很不方便的。比如有兩個循環,一旦找到它們相同的,就立刻停止循環,我們可能會這么做:

NSArray *arr1 = @[@"1",@"2",@"3",@"4",@"5"];
NSArray *arr2 = @[@"4",@"6",@"8",@"9",@"2"];

BOOL finded = NO;
for (NSString *x in arr1) {
    if (finded) {
        break;
    }
    NSLog(@"x:%@",x);
    for (NSString *y in arr2) {
        NSLog(@"y:%@",y);
        if ([x isEqualToString:y]) {
            NSLog(@"找到相等的了:%@",x);
            finded = YES;
            break;
        }
    }
}

我們需要借助 finded 這個 BOOL,來方便我們跳出循環。在 Swift 中,我們就可以利用標簽語句,來指定具體跳出哪個循環,語法是這樣的:

標簽名: 條件語句 {

}

上面的 ?? 我們可以這么寫:

let arr1 = ["1","2","3","4","5"]
let arr2 = ["4","6","8","9","2"]
        
label: for x in arr1 {
    print("x: \(x)")
    for y in arr2 {
        print("y: \(y)")
        if x == y {
            print("找到相等的了:\(y)")
            break label
        }
    }
} 

上面代碼,我們把第一層循環定義了標簽:label。在第二層循環中,一旦條件成立,立刻跳出第一層循環 label。這個特性,可以說十分方便了!

優雅的定義通知名稱

在 Objective-C 中,我們自定義通知時,對于名稱的定義一般都有規范:

// xxx.h 
UIKIT_EXTERN NSString * const XXXXNotification;

// xxx.m 
NSString * const XXXXNotification = @"XXXXNotification";

在 Swift 中,我們可以參考 Alamofire 的方式,創建個專門存放通知名的文件,擴展 Notification.Name 并以結構體 struct 方式聲明:

// XXNotification.swift 文件

import Foundation

extension Notification.Name {
    public struct Task {
        public static let 通知名 = Notification.Name(rawValue: "org.target名稱.notification.name.task.通知名")
    }
}

然后我們就可以愉快的使用了:

// add
NotificationCenter.default.addObserver(self, selector: #selector(myNotification(_ :)), name: NSNotification.Name.Task.通知名, object: self)

// post
NotificationCenter.default.post(name: NSNotification.Name.Task.通知名, object: self)

// remove
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.Task.通知名, object: self)

weakSelf 和 strongSelf

在 Objective-C 中,為了防止 block 循環引用,我們通用利用 __weak__strong 搭配使用:

__weak typeof(self) weakSelf = self;
 
__strong typeof(weakSelf) strongSelf = weakSelf;

對應的 Swift 中我們可以這樣:

resource.request().onComplete { [weak self] response in
  guard let strongSelf = self else {
    return
  }
  let model = strongSelf.updateModel(response)
  strongSelf.updateUI(model)
}

幾種遍歷方式

for _ in 0..<3 {
  print("Hello three times")
}

// 帶索引
for (index, person) in attendeeList.enumerated() {
  print("\(person) is at position #\(index)")
}

// 每隔兩個打印
for index in stride(from: 0, to: 5, by: 2) {
  print(index)
}
// 輸出: 0 2 4

// 倒敘
for index in (0...3).reversed() {
  print(index)
}
// 輸出:3 2 1 0

訪問級別

Swift 中的訪問級別有以下五種

  • open:公開權限, 最高的權限,可以被其他模塊訪問,繼承及復寫。
  • public:公有訪問權限,類或者類的公有屬性或者公有方法可以從文件或者模塊的任何地方進行訪問。那么什么樣才能成為一個模塊呢?一個 App 就是一個模塊,一個第三方 API,第三等方框架等都是一個完整的模塊,這些模塊如果要對外留有訪問的屬性或者方法,就應該使用 public 的訪問權限。public 的權限在 Swift 3.0 后無法在其他模塊被復寫方法/屬性或被繼承。
  • fileprivate:文件私有訪問權限,被 fileprivate 修飾的類或者類的屬性或方法可以在同一個物理文件中訪問。如果超出該物理文件,那么有著 fileprivate 訪問權限的類,屬性和方法就不能被訪問。
  • private:私有訪問權限,被 private 修飾的類或者類的屬性或方法可以在同一個物理文件中的同一個類型(包含 extension)訪問。如果超出該物理文件或不屬于同一類型,那么有著 private 訪問權限的屬性和方法就不能被訪問。
  • internal: 顧名思義,internal 是內部的意思,即有著 internal 訪問權限的屬性和方法說明在模塊內部可以訪問,超出模塊內部就不可被訪問了。在 Swift 中默認就是 internal 的訪問權限。

for in 和 forEach 的區別

for in 能使用 returnbreakcontinue 關鍵字,forEach 不能使用 breakcontinue 關鍵字。它們的主要區別在于 return 關鍵字結果的不同:

for in 中,return 會導致循環終止:

let array = ["1", "2", "3", "4", "5"]
for element in array {
    if element == "3" {
        return
    }
    print(element)
}

// 輸出:
// 1
// 2

forEach 會跳過當前循環,完成剩余的循環:

let array = ["1", "2", "3", "4", "5"]
array.forEach { (element) in
    if element == "3" {
        return
    }
    print(element)
}

// 輸出:
// 1
// 2
// 4
// 5

這樣看來,forEach 中的 return 關鍵字倒是有點類似 for in 中的 continue 了!

取消 asyncAfter 中延遲的事件

class ViewController: UIViewController {

    private let workItem = DispatchWorkItem {
        print("test")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: workItem)
    }
    
    // 這里點擊取消之前延遲的事情
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        print(workItem.isCancelled ? "取消了" : "未取消")
        
        workItem.cancel()

        print(workItem.isCancelled ? "取消了" : "未取消")
    }
}

// 輸出:
// 未取消
// 取消了
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容