前言:
最近幾天在看《100個Swift必備tips》這本書,本篇爲讀書筆記,以作總結記錄用。
將protocol
的方法聲明爲mutating
:
在swift
裏,protocol
中定義的方法既可以被class
實現,也可以被struct
和enum
來實現。但若是被後兩者實現,默認情況下,實現的方法內部是無法更改struct
和enum
變量。若你將要定義的協議可能需要被struct
或enum
實現,請在定義協議方法時,加上關鍵字mutating
。
爲方便起見,若定義的協議需要被struct
或enum
實現,建議在協議中定義方法時均添加mutating
關鍵字。無論你定義的方法需不需要更改變量。
Sequence 協議:
Swift 中的 Sequence(一)
Swift 中的 Sequence(二)
元組Tuple:
讓我們想想,在C或OC中我們想讓函數返回多個值時,應該怎麼實現?你可以將多個返回值拼裝成一個Dictionary
或Model
再返回。你還可以使用“指針類型參數”來傳遞值。
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:
在編程語言中,都可以以下標來訪問Array
、Dictionary
等集合對象。
在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
表示任何類型,包括任何class
、struct
、enum
類型。
我們知道,在OC
中id
代表任何類型,並也有經常見於Cocoa
框架中,而Swift
用的框架仍爲Cocoa
。爲了能完美對接,Swift
不得不發明個與OC
中id
相對應的類型,這便是AnyObject
的來歷。(AnyObject
其實是個協議,Swift
中的所有class
均實現了該協議。)
但是很遺憾的是,Swift
中的所有的基本類型,包括Array
和Dictionary
這些在OC
中爲class
的東西,統統都是struct
。
所以,蘋果就又新增了更特殊,更強大,能代表任何類型的Any
。
多類型和容器:
按理說,數組中存入的元素都應該是類型相同的。但是在Swift
中有Any
和AnyObject
這兩個很特殊的,代表任何類型的類型。試想:若我們在定義數組時,將數組元素申明爲Any
或AnyObject
,是不是就可以存入不同類型的元素了。不管是Int
還是String
,它們也是Any
和AnyObject
類型啊。
let array: [Any] = [1, "string"]
這樣確實是可以的,但這樣是極不安全的,我們將其以Any
的方式存入數組,是一種類型提升,這樣會丟失很多原本具有的數據。若你從該數組中取出string
,它便不具備原本String
具有的能力了,包括屬性和方法等。此時,若你調用這些屬性或方法的話,程序就會出問題。
其實,數組中存入的元素,並不一定必須得是相同類型的,也可以是實現同一協議的不同類型。
像下面這樣,因爲Int
和String
本身都是實現了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
中的屬性有『存儲屬性』和『計算屬性』之分。前者會在內存中實際分配地址來存儲屬性,而後者並不是實際存於內存的,它只是通過提供get
和set
兩個方法來訪問,讀寫該屬性(它和普通的方法很類似)。
Swift
中的『計算型屬性』是無法使用willSet
/didSet
屬性觀察的,也就是說在定義屬性時,不能同時出現set
和willSet
/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
函數裏調用了UIKit
的UIApplicationMain
方法,這個方法根據第三個參數初始化一個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
函數的邏輯。
當然,這個標籤並不是必須的,若你需要指定子類化的UIApplication
和AppDelegate
,則可以自己給項目中添加個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
,在struct
和enum
中,它同樣是可用的。
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
}
給people
的money
屬性添加觀察,並重寫回調方法,在其中拿到改變後的新值。並且,一定要記得在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
。
如下面的例子,假如我們要對People
的money
屬性進行觀測,但是又不方便或不允許將money
屬性標爲dynamic
,所以我們新建繼承自People
的ChildPeople
類,在其中重寫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
裏是用===
來比較引用的內存地址是否相等。
下面創建的people1
和people2
內存地址是不同的,所以不會打印東西。
let people1 = People(mon: 10)
let people2 = People(mon: 10)
if people1===people2 {
print("===")
}
類簇:
字符串格式化:
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
,各個選項值爲static
的get
屬性。
要達到像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
外的struct
和enum
來實現的,而後兩者它們是值類型,它們不是通過引用計數規則來管理內存的,所以當然不能以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
來啓動。
然後就可以“交互式編程”了,在終端每輸入代碼,然後回車,就會實時編譯執行。
試試定義一個方法,並調用。可以看到它甚至還可以反饋給我們錯誤提示,提示我“調用myFunction(5)
方法時缺少參數標籤num
。”
打印對象,自定義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)
}