學習Swift的一些Tips

前言:

最近幾天在看《100個Swift必備tips》這本書,本篇爲讀書筆記,以作總結記錄用。


protocol的方法聲明爲mutating:

swift裏,protocol中定義的方法既可以被class實現,也可以被structenum來實現。但若是被後兩者實現,默認情況下,實現的方法內部是無法更改structenum變量。若你將要定義的協議可能需要被structenum實現,請在定義協議方法時,加上關鍵字mutating

爲方便起見,若定義的協議需要被structenum實現,建議在協議中定義方法時均添加mutating關鍵字。無論你定義的方法需不需要更改變量。


Sequence 協議:

Swift 中的 Sequence(一)
Swift 中的 Sequence(二)


元組Tuple:

讓我們想想,在C或OC中我們想讓函數返回多個值時,應該怎麼實現?你可以將多個返回值拼裝成一個DictionaryModel再返回。你還可以使用“指針類型參數”來傳遞值。

    CGRect small;
    CGRect large;
    CGRectDivide(self.view.bounds, &small, &large, 20, CGRectMinXEdge);

Swift中,就變得很簡單了,只需返回一個Tuple就可以了。對於元組裏的元素,既可以像數組那樣通過下標來訪問,也可以像字典那樣通過key來訪問。


可選鏈:

像下面的代碼,即便Toy類中的name屬性不是可選型的,但是最終獲取到的結果toyName仍然是可選的,因爲在整個調用鏈條中pet,toy都是可選的,都有可能爲nil,而提前中止調用鏈條,返回nil
既然返回了nil,就說明結果是可選的,是String?型的。因此,我們要用if let語句來解包。

if let toyName = xiaoming.pet?.toy?.name {
}

使參數可變 inout:

Swift中,方法參數默認是不可變的,不能在方法內部修改參數的值。
若你真的想直接修改參數的值,則需用inout關鍵字來修飾參數。 如此,則在方法內部就可以直接修改參數了,且inout讓參數具有了“傳址調用”的能力,因此在調用時要在參數名前加&

下面代碼定義了一個將變量重置爲0的方法reset

    func reset(x: inout Int) {
        x = 0;
    }
        var x = 5
        self.reset(x: &x)
        print("result:x=\(x)")
        // log:   result:x=0

自定義下標 subscript:

在編程語言中,都可以以下標來訪問ArrayDictionary等集合對象。
Swift中可就更厲害了,它支持給任何類、結構體和枚舉自定義下標,使它們都可以以下標的形式被訪問。
無論是項目中你自己定義的類,還是原先就有下標的集合類,你都可以自定義下標訪問它們!

自定義下標,用subscript關鍵字,非常類似用關鍵字init自定義構造器。

比如下面的代碼,我們拓展了Array的下標,使其下標可以爲數組,讀寫任意位置的元素。

extension Array {
    
    subscript(indexs: [Int]) -> [Element] {
        
        get {
            var resultArr = [Element]()
            for index in indexs {
                assert(index<self.count, "index out of range")
                resultArr.append(self[index])
            }
            return resultArr
        }
        
        set {
            
            for (index, i) in indexs.enumerated() {
                assert(i<self.count, "index out of range")
                self[i] = newValue[index]
            }
        }
    }
}

        var array = [1, 2, 3, 4, 5, 6]
        let resultArr = array[[0,1,2]] // 獲取下標[1,2,3]的值
        print(resultArr)
        // log ————> [1,2,3]
        
        array[[0,1,2]] = [0,0,0] // 修改下標[1,2,3]的值
        print(array)
        // log ————> [0,0,0,4,5,6]

Any 和 AnyObject:

簡單來說,AnyObject表示任何class類型;
Any表示任何類型,包括任何classstructenum類型。

我們知道,在OCid代表任何類型,並也有經常見於Cocoa框架中,而Swift用的框架仍爲Cocoa。爲了能完美對接,Swift不得不發明個與OCid相對應的類型,這便是AnyObject的來歷。(AnyObject其實是個協議,Swift中的所有class均實現了該協議。)

但是很遺憾的是,Swift中的所有的基本類型,包括ArrayDictionary這些在OC中爲class的東西,統統都是struct

所以,蘋果就又新增了更特殊,更強大,能代表任何類型的Any


多類型和容器:

按理說,數組中存入的元素都應該是類型相同的。但是在Swift中有AnyAnyObject這兩個很特殊的,代表任何類型的類型。試想:若我們在定義數組時,將數組元素申明爲AnyAnyObject,是不是就可以存入不同類型的元素了。不管是Int還是String,它們也是AnyAnyObject類型啊。

let array: [Any] = [1, "string"]

這樣確實是可以的,但這樣是極不安全的,我們將其以Any的方式存入數組,是一種類型提升,這樣會丟失很多原本具有的數據。若你從該數組中取出string,它便不具備原本String具有的能力了,包括屬性和方法等。此時,若你調用這些屬性或方法的話,程序就會出問題。

其實,數組中存入的元素,並不一定必須得是相同類型的,也可以是實現同一協議的不同類型。

像下面這樣,因爲IntString本身都是實現了CustomStringConvertible協議的。我們可以將數組申明爲『實現了CustomStringConvertible協議』的類型。

        let array: [CustomStringConvertible] = [1, "string"]
        
        for item in array {
            print(item.description);
        }

public protocol CustomStringConvertible {

    public var description: String { get }
}

還有另一種做法是使用enum可以帶有值的特點,將類型信息封裝到特定的enum中。下面定義了一個枚舉AnyType

enum AnyType {
        case IntType(Int)
        case StringType(String)
    }
let mixedArr = [AnyType.IntType(1), AnyType.StringType("string")]
        
        for item in mixedArr {
            switch item {
            case let .IntType(i):
                print(i)
            case let .StringType(str):
                print(str)
            }
        }
        

屬性觀察:

Swift讓屬性觀察這件事變得異常簡單。下面代碼觀察了People類的count屬性,若其值小於0,則打印提示信息。

class People: NSObject {

    var count: Int {
        willSet {
            print("count----willSet")
        }
        didSet {
            print("count----didSet")
            if(count<0){
                print("count不能小於0")
            }
        }
    }
    
    override init() {
        count = 0
    }
}

let people = People()
people.count = -10

需要注意的是,Swift中的屬性有『存儲屬性』和『計算屬性』之分。前者會在內存中實際分配地址來存儲屬性,而後者並不是實際存於內存的,它只是通過提供getset兩個方法來訪問,讀寫該屬性(它和普通的方法很類似)。

Swift中的『計算型屬性』是無法使用willSet/didSet屬性觀察的,也就是說在定義屬性時,不能同時出現setwillSet/didSet。因爲要監聽計算型屬性,我們完全可以在set裏面添加監聽屬性的處理代碼。

有時,我們往往需要觀察一個屬性,但是,這個屬性是別人定義的,我們不便直接在源代碼裏寫didSet用以監聽。此時,子類化該類,重寫其屬性,在重寫中對該屬性添加didSet監聽是個很好的解決方案。
更值得說的是: 子類化該類,重寫其屬性,然後添加didSet監聽,竟然可以用在父類是計算型屬性的情況下。也就是說,這也是解決Swift中計算型屬性無法寫didSet監聽的一種方案了,即把didSet寫到子類重寫的屬性中去。


單例的最正確寫法:

用兩個關鍵字static let修飾,可以限制sharedInstance是全局且不可變的,這就有了『單例』的意思了。但是還不夠,還要堵住其他構造途徑,防止調用其他構造器創建實例,所以在原本的init方法前加上了private關鍵字。

class UserInfoManager: NSObject {
    static let sharedInstance = UserInfoManager()
    private override init() {}
}
let userInfo = UserInfoManager.sharedInstance

@UIApplicationMain:

對於一個Objective-C的iOS項目,Xcode會自動幫我們生成一個main.m文件,其中有個main函數。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

main函數裏調用了UIKitUIApplicationMain方法,這個方法根據第三個參數初始化一個UIApplication或者子類的對象並開始接收事件(默認傳入的是nil,意味使用默認的UIApplication)。最後一個參數指定了AppDelegate類作爲委託,用來接收didFinishLaunchingWithOptions:applicationDidEnterBackground:等有關程序生命週期的方法。

後兩個參數都可以傳入自定義的子類來完成更加個性化的需求。

另外,main函數雖然標明返回一個int,但是它不會真正返回。它會一直存在於內存中,直到用戶或系統將其強制終止。

..... 現在,我們來看看在Swift中情況是怎樣的。

可以看到,在Swift的iOS項目中找不到main.m文件,但是卻在AppDelegate類開頭多了個@UIApplicationMain標籤。其實,這個標籤的作用就是上面所說的main函數的作用:
初始化一個默認爲UIApplication類的實例來接收事件,指定AppDelegate爲委託來接收程序生命週期方法。

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

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

所以說,Swift的iOS項目中,並不是不需要main函數,而是通過添加@UIApplicationMain標籤,Xcode幫我們自動處理了main函數的邏輯。

當然,這個標籤並不是必須的,若你需要指定子類化的UIApplicationAppDelegate,則可以自己給項目中添加個main.m文件,實現main函數,並指定這兩個子類化的實例爲參數。


如何動態地獲取一個實例的類型:

方法一,若類是繼承於NSObject類的,則我們可以利用OC的運行時:

public func object_getClass(_ obj: Any!) -> Swift.AnyClass!
        let people = People()
        let peoType: AnyClass! = object_getClass(people)
        print(peoType)
        // log ----> People

方法二:用一個全局的函數type(of:),如下:

        let people = People()
        let peoType = type(of: people)
        print(peoType)
        // log ----> People

// type(of:)方法是Swift3.0新增的,代替了前期版本中的dynamicType屬性(Swift3.0之前,使用該屬性可返回該對象的類型)。


自省:

自省:向一個對象發出詢問,以確定它是不是屬於某個類,這種操作就稱爲“自省”。

OC中我們是這樣判斷一個對象是不是屬於某個類的:

if([_type isKindOfClass:[NSString class]]){
            
}

Swift中,若該類是繼承自NSObject的,則也有上面相應的兩個方法:

class People: NSObject {
    
}
        let peo = People()
        if peo.isKind(of: People.self) {
            print("isKind")
        }
        if peo.isMember(of: People.self) {
           print("isMember")
        }

除此外,Swift提供了更強大簡潔的關鍵字is,同樣能達判斷類型的效果。
而且is的強大在於,它不僅適用於class,在structenum中,它同樣是可用的。

        if peo is People {
            print("isKind")
        }

KVO:

Swift中也可以使用KVO,但是僅限於在NSObject子類中。因爲KVO是基於KVC和動態派發技術的,而這些都是NSObject運行時的概念。另外,由於Swift爲了提高效率,默認禁止了動態派發,因此想用Swift使用KVO的話,還要將被觀測的對象標爲dynamic

簡單來說,Swift要使用KVO有兩個限制:
1.被觀測的對象屬於NSObject類;2.被觀測的對象屬性被標爲dynamic

下面的代碼,我們觀察People類裏的money屬性,它即爲被觀測的對象,所以要將其標爲dynamic

class People: NSObject {
    dynamic var money = 0 // 標爲dynamic
    
}

peoplemoney屬性添加觀察,並重寫回調方法,在其中拿到改變後的新值。並且,一定要記得在deinit方法中移除觀察。

private var myContext = 0

class ViewController: UIViewController {
    
    var people = People()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 添加觀察
        people.addObserver(self, forKeyPath: "money", options: .new, context: &myContext)
        people.money = 3
    }
    
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if let change = change, context == &myContext{
            let newValue = change[NSKeyValueChangeKey.newKey]
            if let newValue = newValue {
                print(newValue)
            }
        }
    }
    
    deinit {
        people.removeObserver(self, forKeyPath: "money")
    }
  
}

但是,在實際開發過程中,我們往往會遇到不符合上述兩點的情況,那在這種情況下,我們還能不能使用KVO呢?
若被觀測的對象不繼承自NSObject的話,那真的無法使用KVO,只能自己利用“屬性觀察”自己實現一套觀察機制(在didSet裏發送通知,通知外界屬性值已改變)。
若被觀測的對象我們無法直接標爲dynamic,比如系統類的對象。那我們可以子類化該類,重寫該屬性,重寫時將其標爲dynamic

如下面的例子,假如我們要對Peoplemoney屬性進行觀測,但是又不方便或不允許將money屬性標爲dynamic,所以我們新建繼承自PeopleChildPeople類,在其中重寫money時將其標爲dynamic

class People: NSObject {
    var money = 0
    
}

class ChildPeople: People {
    
    dynamic override var money: Int {
        get {
            return super.money
        }
        set {
            super.money = newValue
        }
    }
}

判等:

我們可以實現Equatable中的上面方法,來完成自定義對象判等邏輯。NSObject實現了Equatable協議,若類是繼承自NSObject的,則不需我們自己實現該協議。

像下面代碼,我們自定義了People對象的判等邏輯:只要倆人的錢一樣多,就相等。

class People {
    var money = 0
    
    init(mon: Int){
        money = mon
    }
}

extension People: Equatable {
    
    public static func ==(lhs: People, rhs: People) -> Bool {
        return lhs.money == lhs.money
    }
}

若類僅僅是繼承自NSObject,而不自己實現該協議方法,則==操作符默認是NSObject裏的實現,即對指針引用對象內存地址的比較。除非倆引用同一塊內存地址,否則不相等。

Swift裏是用===來比較引用的內存地址是否相等。

下面創建的people1people2內存地址是不同的,所以不會打印東西。

        let people1 = People(mon: 10)
        let people2 = People(mon: 10)
        if people1===people2 {
            print("===")
        }
屏幕快照 2017-07-03 下午3.08.01.png

類簇:

字符串格式化:

OC的字符串可以通過佔位符%@/%f/%d等,將多個元素拼接成新的字符串。

    NSString *name = @"wang66";
    NSString *str = [NSString stringWithFormat:@"%@%@", @"name =", name];
    NSLog(@"%@",str);

Swift的字符串中則不需要佔位符了,直接可以在字符串中插值。

        let name = "wang66"
        let str = "name = \(name)"
        print(str)
        // log ----> name = wang66

這樣確實非常簡潔了,但是若是出現將小數保留兩位小數的需求時該怎麼辦呢?,OC中是這樣的:

    CGFloat distance = 12.21424;
    NSString *str = [NSString stringWithFormat:@"%0.2f%@", distance,@"km"];
    NSLog(@"%@",str);

但是在Swift中就不能繼續簡潔了,而是要用``String(format:)

        let distance = 12.26578
        let distanceStr = String(format: "%0.2f", distance)
        let str = "\(distanceStr)km"
        print(str)
//         log ----> 12.27km


Options:

我們從UIView的一個動畫方法說起,下面這個執行動畫方法有個options參數,意爲“選項”,用以配置動畫。在OC中是這樣的:

    [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionCurveEaseOut animations:^{
        NSLog(@"動畫進行中...");
    } completion:^(BOOL finished) {
        NSLog(@"動畫完成...");
    }];

options參數是枚舉類型的,可以以|將多個枚舉值連接,組合使用。UIViewAnimationOptions的定義如下,因爲該參數允許組合枚舉各值,所以被定義成了支持掩碼位移的NS_OPTIONS:(它和NS_ENUM的區別主要是它自動支持掩碼位移)

typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    UIViewAnimationOptionLayoutSubviews            = 1 <<  0,
    UIViewAnimationOptionAllowUserInteraction      = 1 <<  1, // turn on user interaction while animating
    UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2, // start all views from current value, not initial value
    UIViewAnimationOptionRepeat                    = 1 <<  3, // repeat animation indefinitely
    UIViewAnimationOptionAutoreverse               = 1 <<  4, // if repeat, run animation back and forth
    UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5, // ignore nested duration
    UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6, // ignore nested curve
    UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7, // animate contents (applies to transitions only)
    UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8, // flip to/from hidden state instead of adding/removing
    UIViewAnimationOptionOverrideInheritedOptions  = 1 <<  9, // do not inherit any options or animation type

而在Swift中,options參數的情況卻大有不同。與上面OC對應的方法,在Swift中是這樣的:

        UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseIn, .curveEaseOut], animations: {
            print("動畫進行中...")
        }) { isFinshed in
            print("動畫結束...")
        }

Swift的枚舉無法支持位移賦值。所以options並不是枚舉,而是一個實現OptionSet協議的struct,各個選項值爲staticget屬性。
要達到像OC中那樣多個選項值options|符號組合,在Swift中,我們以集合包裝每個選項值。 就像這樣:[.curveEaseIn, .curveEaseOut]

public struct UIViewAnimationOptions : OptionSet {

    public init(rawValue: UInt)

    public static var layoutSubviews: UIViewAnimationOptions { get }

    public static var allowUserInteraction: UIViewAnimationOptions { get } // turn on user interaction while animating

    public static var beginFromCurrentState: UIViewAnimationOptions { get } // start all views from current value, not initial value

    public static var `repeat`: UIViewAnimationOptions { get } // repeat animation indefinitely

    public static var autoreverse: UIViewAnimationOptions { get } // if repeat, run animation back and forth


weak delegate:

"委託代理"模式在iOS開發中可謂是最常用的模式,我們都知道,爲了避免委託和代理對象互相引用,無法釋放的問題,我們一般將委託的delegate屬性標爲weak弱引用,主動示弱,以打破互相引用的僵局。

但是我們在Swift中,我們不能直接在任何一個delegate屬性前面加weak關鍵字修飾。因爲Swift中的協議是可以被除了class外的structenum來實現的,而後兩者它們是值類型,它們不是通過引用計數規則來管理內存的,所以當然不能以weak,這個ARC裏的東西修飾。

所以,在Swift中使用“委託代理模式”,首先要在定義協議時將其申明爲只能被class實現。

protocol DemoDelegate: class {
    func demoFunction()
}

或者,還有方案二:將定義的協議用@objc申明爲Objective-C的。因爲在Objective-C中,協議只能被class實現。

@objc protocol DemoDelegate {
    func demoFunction()
}

Swift 命令行工具:

啓動REPL(Read-Eval-Print Loop)環境: 在終端輸入:xcrun swift來啓動。

屏幕快照 2017-07-05 下午2.00.31.png

然後就可以“交互式編程”了,在終端每輸入代碼,然後回車,就會實時編譯執行。

屏幕快照 2017-07-05 下午2.05.45.png

試試定義一個方法,並調用。可以看到它甚至還可以反饋給我們錯誤提示,提示我“調用myFunction(5)方法時缺少參數標籤num。”

屏幕快照 2017-07-05 下午2.07.14.png

打印對象,自定義description:

我們打印一下People的對象people

        let people = People(name: "wang66", mobile: "18693133051", address: "白石洲")
        print(people)
        // log ----> <LearnAlamofireDemo.People: 0x6080000d6960>

可以看到打印信息只有對象的類型People和內存地址。但是這樣的信息幾乎沒什麼用。

打印對象後展示什麼信息,我們是可以定製的。

只要這個類實現CustomStringConvertible協議,然後重寫description屬性,需要展示什麼就return什麼。

public protocol CustomStringConvertible {

    public var description: String { get }
}

比如,我們在拓展中實現了自定製的description屬性:

class People {
    var name: String?
    var mobile: String?
    var address: String?
    
    init(name: String?, mobile: String?, address: String?) {
        self.name = name
        self.mobile = mobile
        self.address = address
    }
}

extension People: CustomStringConvertible {
    var description: String {
        get {
            return "[\(type(of:self)): name=\(self.name ?? "") | mobile=\(self.mobile ?? "") | address=\(self.address ?? "") ]"
        }
    }
    
}

打印結果如下,非常完美。

        let people = People(name: "wang66", mobile: "18693133051", address: "白石洲")
        print(people)
        // log ----> [People: name=wang66 | mobile=18693133051 | address=白石洲 ]

斷言 assert:

有時我們寫的方法是有數據傳入限制的,但是當我寫的代碼別人調用,或者以後我自己調用時,可能都不熟悉這個方法具體需要傳入什麼條件的數據,這有可能讓大家浪費不必要的時間。最好,在調用傳入數據時有反饋提示。

斷言,可以很好地解決這個問題。當調用者傳入的數據不符合條件時,編譯不會通過,且會打印出提示信息。

printYourAge(age: -10)
    func printYourAge(age: Int) {
        assert(age >= 0, "年齡都是正數")
        print(age)
    }
屏幕快照 2017-07-05 下午3.18.13.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章