小視訊
001--swift簡史小視訊
002--Playground體驗
003--常量&變數
一、swift簡史
1、介紹 swift是蘋果公司於2014年推出用於撰寫OS和iOS應用程式的語言。它由蘋果開發者工具部門總監“克里斯.拉特納”在2010年開始著手設計,歷時一年完成基本的架構。到後來蘋果公司大力投入swift語言的研發,於2014年釋出這一語言的第一版本。swift2.0之後的語法則趨於穩定,2017年釋出的swift4.0雖有改動,但也只是增添了一些新特性。這些新特性需要在Xcode9上執行才能顯示出效果。值得一提的是它支援unicode9,也就是說,可以用某些圖片圖示來充當變數。 例如:
"??".count // 人 + 膚色
"????".count // 有4個成員的家庭
"??\u{200D}??\u{200D}??\u{200D}??".count // 家庭 + 膚色
"???".count // 人 + 膚色 + 職業
複製程式碼
2、特點
- swift取消了預編譯指令,巨集也被包括在內。 某些開發者為了讓Objective-C和swift程式碼相容,會盡少在Objective-C中定義巨集。
- 取消了Objective-C中的指標等其他不安全訪問的使用
- 使用點語法來呼叫屬性或者函式
- 去除了NS字首
3、為什麼要學習swift
- swift作為面向協議語言,不僅能寫移動端,也可以做到搭建伺服器端。
- 縱觀國內外iOS開發界,已經有許多公司直接或間接採用swift開發,使用swift語言開發已成為未來iOS開發的趨勢。
- swift以簡潔、優雅等優點迅速俘獲廣大開發者的青睞。
二、用playground體驗swift開發
- 開啟Xcode,選擇建立一個playground專案
- 建立一個普通的UIView物件
正如上圖所示,playgound檔案的左邊是程式碼區,右邊則是顯示結果的區域。當點選用於眼睛時會實時顯示出介面效果。
-
swift與objective-C的重大區別
- 在swift中是沒有.h和.m檔案之分的。所有的程式碼全部都儲存在一個檔案裡面。
- 在swift中所有的程式碼都被封裝在{}裡面
- OC使用alloc init進行初始化,而swift使用()
- OC中使用[]來呼叫方法,而swift中採用點語法。比如
UIColor.red
- swift中不需要用分號分割語句
三、常量和變數
1、資料型別 在swift中也有各種資料型別來儲存不同的資訊。下表列舉的是常見的資料型別變數。
但其實,在swift中,是不存在基本的資料型別的,所謂的資料型別,其實都只是結構體。這也是swift中的一個特點。
2、變數和常量
- 宣告
swift中用
let
宣告常量,用var
宣告變數。
var x = 10;
let y = 20;
let z //錯誤示範,let z 在宣告的時候並沒有賦值常量是不可改變的,只能在宣告時賦值
複製程式碼
在開發中,通常會優先選擇使用let,因為不可變會更安全一點。所以建議在寫程式碼之時,先選擇let,等到需要變化的時候再改成var。
- 自動推導
建立一個UIView,不指定型別。可以看到控制檯上會列印出UIView的資訊。這個現象被稱為swift的自動推導。事實上,在程式碼左側定義的型別只是程式設計師希望的型別,而右側才是程式真實的型別。
let z = UIView()
print(z)
複製程式碼
也就是說,變數或常量的型別會根據右側程式碼執行的結果,推匯出對應的型別。
可以使用熱鍵option
點選檢視型別。
- swift對型別的嚴格要求
在swift中,任何不同型別的資料之間是不允許直接運算的。比如下面這段程式碼就會報錯。
//錯誤示範
let a = 10
let b = 12.5
print(x + y)
複製程式碼
如果非要讓不同型別資料之間能夠運算,可以將其中一個型別進行轉換。
let a = 10
let b = 12.5
print(a + Int(b))
複製程式碼
此時得到的結果就是22。在swift中,做型別轉換時是將資料括起來,相當於swift結構體中的建構函式。
當然也可以將前面的整數轉換成Double型。此時就能列印出小數來。
print(Double(a)+b)
複製程式碼
四、String型別和Bool型別
1、String型別
- 宣告
直接用雙引號將資料引起來
let str = "小仙女"
let str1:String = "hahh"
複製程式碼
- 拼接
字串的連線有兩種方法,一種是通過加號來連線,另一種則是通過反斜杆進行插入。
let str = "小仙女"
let mesg1 = "one"+str //用加號的方式
let mesg2 = "two,\(str)" //反斜槓的方式
print(mesg1,mesg2)
複製程式碼
在做字串拼接時要注意加號和反斜槓後面都不能出現空格,不然會報錯。
- 拼接字串時格式的變化
假設在某些特定的地方需要輸出特定位數的字元,比如或時間的輸出,就需要使用佔位符來調整字串的格式。使用String的建構函式,呼叫format方法,%0後面加上數字就表示需要佔多少位數。
let min = 2
let second = 10
String(format: "%02d:%02d", min,second)
複製程式碼
- 遍歷
呼叫字串的characters屬性,採用for...in...的方式來遍歷字串。
for c in str{
print(c) //swift4中的遍歷
}
print(str.count) //列印字串長度
for char in myString.characters {
print(char) // swift3的遍歷
}
print(str..characters.count) //swift3列印字串長度
複製程式碼
- 字串的擷取
最方便的方式就是將String型別轉換成OC的NSString型別,再來擷取。
let urlStr = "www.baidu.com"
let header = (urlStr as NSString).substring(to: 3) //擷取前三位
let middle = (urlStr as NSString).substring(with: NSMakeRange(4, 5))//去除前四個字元擷取,範圍之後五位字元
let footer = (urlStr as NSString).substring(from: 10) //從第十個字元開始擷取
複製程式碼
2、Bool型別
與其他語言一樣,Bool型別表示的就是真假,但是不同於Objective-C,swift中用true和false來表示真假。
五、可選型別
在Objective-C開發中,如果一個變數暫時不會使用到,可以將它賦值為0或者賦值為空,而在swift中,nil是一個特殊的型別,如果它和真實型別不匹配是不能進行賦值的。但是開發中將變數賦值為空是在所難免的事情,因此就推出了可選型別。 可選型別是swift的一大特色,在定義變數時,如果指定這個變數是可選的話,就是說這個變數可以有一個指定型別的值或者為nil。
1、定義一個optional的變數
let x:Optional = 10
print(x)
複製程式碼
點選進去檢視,可以發現Option其實是一個列舉型別。這個列舉有兩個值,一個是none,表示沒有值,而另一個是some,表示某一類值。
在輸出的時候,可以看見控制檯上的內容Optional(10)
,它的作用就是提示這是一個可選值。
而在實際開發中,一般不用上述方式建立可選值,而是指定一個型別,再在其後添一個問號。
let x:Optional = 10 //第一種寫法
let x:Int? = 20 //第二種寫法
print(x)
複製程式碼
上述程式碼問號的意思就是定義一個可選的Int型別,可能沒有值,也可能有一個整數。
2、 解包
試試將上面案例x和y相加,這個時候還能輸出結果麼?
此時可以看到編譯器已經報錯。在前面的教程中提到過,不同型別的值是不能直接運算的。而可選項有兩種值的產生,若它的值為nil則不能參加計算。
因此引入解包的概念,“!”代表強制解包。它的意思是從可選值中強行獲取對應的非空值。
print(x!+y!)
複製程式碼
3、解包常見錯誤
//錯誤示範1
let y : Int?
print(y)
複製程式碼
使用let定義的是常量,在初始化時必須要給出值。
//錯誤示範2:
let y : Int? = nil
print(y)
複製程式碼
強制解包是危險操作,如果可選值為nil,強制解包系統會奔潰。
4、let和var的可選項預設值
//預設值測試
let x: Int?
print(x)
var y :Int?
print(y)
複製程式碼
用let做測試時會直接報錯,說明let的可選值是沒有預設值的,而用var做測試時,報錯資訊就變成了警告,執行的結果為nil。可以由此推測出var的可選項預設值為nil。
swift中有規定,物件中的任何屬性在建立物件時,都必須有明確的初始化值。
5、可選繫結
用if let/var
表示。它將變數賦值給一個臨時變數,在這個操作中會做兩步操作:首先判斷變數是否有值,如果沒有值,則直接不執行大括號裡面的內容;如果有值,系統會自動將變數進行解包,並且將解包後的結果,賦值給臨時變數。
比如下面這個例子:
通過一個字串建立NSURL物件
let url: URL? = URL(string: "https://www.baidu.com")
複製程式碼
接著建立NSURLRequest物件。強制解包非常危險,當url有中文的時候可能會變成nil。所以要判斷url是否為空再對其進行解包。
if let url = url {
let request = URLRequest(url: url)
}
複製程式碼
六、swift中的分支
1、if語句
在swift中,if語句是不用帶小括號的,但是後面跟的語句必須有花括號,哪怕只有一行程式碼。許多公司的程式碼規範也是規定必須使用這一格式。
注意:在swift中沒有非0即真的說法,所以不能寫成if(num)
這樣的格式。
let x = 9
if x > 5 {
print("小仙女")
}else{
print("妖精哪裡跑")
}
複製程式碼
2、三目運算子
三目運算子的寫法是表示式後跟一個問號,用冒號來隔開條件是否成立的值。
let x = 10
x > 5 ? print("小仙女"):print("妖精")
複製程式碼
非常有意思的是,如果開發者只想處理條件成立的部分,此時可以在冒號後面用一個小括號來代替條件不成立的部分。
x > 5 ? print("你都寫了我兩次啦"):()
複製程式碼
3、 三目運算子的簡單模式
三目運算子的簡單模式通常是用於處理可選項的。“??”的意思是說,如果表示式有值,就使用那個值,如果沒有,就使用“??”後面的值來代替。
let x:Int? = nil
let y:Int? = 9
print((x ?? 0) + (y ?? 0))
複製程式碼
執行之後的結果為9。
之後再來說說運算子的優先順序。舉個簡單的栗子?!
let name:String? = "安琪拉"
print((name ?? "") + "火燒屁屁咯")
print(name ?? "" + "火燒屁屁咯")
複製程式碼
從執行的結果可以看到,“??”的優先順序是最低的。如果沒有小括號的約束,它會將後面的語句都當成是一個表示式。
4、 guard的用法
分支若是寫得過多,就會導致程式碼可讀性較差的問題。為了降低程式碼的層次,swift推出了guard。guard後面跟判斷表示式,else後面寫表示式不成立的程式碼。
需要注意的是guard必須寫在函式內部,在最末尾出必須要跟關鍵字return/continue/break/throw
中的一種。
import UIKit
let age = 20
func online(age : Int){
guard age >= 18 else {
print("還未成年呢")
return
}
print("一起來開黑吖")
}
複製程式碼
這樣或許看不到guard的特別之處,但若是像下面這樣的程式碼出現呢?
let age = 20
let money = true
let idcard = true
func online2(age : Int,money:Bool,idcard:Bool){
if age >= 18 {
if money {
if idcard {
print("一起來開黑吖")
}else{
print("回去帶身份證吧")
}
}else{
print("回去拿錢")
}
}else {
print("還未成年呢")
}
}
//呼叫
online2(age: age, money: money, idcard: idcard)
複製程式碼
如果用普通的分支方法,就會顯得可讀性太差。我們可以試著將它改成guard的寫法。
func online1(age : Int){
//判斷年齡
guard age >= 18 else {
print("還未成年呢")
return
}
//判斷是否有錢
guard money else {
print("回去拿錢")
return
}
//判斷是否帶了身份證
guard idcard else {
print("回去帶身份證吧")
return
}
print("一起來開黑吖")
}
複製程式碼
執行完所有的判斷語句之後才執行程式碼庫,閱讀性也比if……else分支強。
5、 switch
- 最基本的用法 switch後面的小括號可以省略。用case關鍵字來表示不同的情形,case語句結束後,break也可以省略。
let sex = 0
switch sex {
case 0:
print("男")
case 1:
print("女")
default:
print("其他")
}
複製程式碼
- 基礎語法的補充
如果系統某一個case中產生case穿透,可以在case結束後跟上fallthrough
case 0:
print("男")
fallthrough
複製程式碼
case後面可以判斷多個條件,這些條件以逗號分開
let sex = 0
switch sex {
case 0,1:
print("正常人")
default:
print("其他")
}
複製程式碼
switch可以判斷浮點型、字串型別和Bool型別
switch 3.14 {
case 0:
print("正常人")
default:
print("其他")
}
複製程式碼
let opration = "+"
switch opration {
case "+":
print("加法")
case "-":
print("減法")
default:
print("其他")
}
複製程式碼
七、swift的for迴圈和表示區間
1、變化
在swift3開始,就已經棄用了var i = 0; i < 10; i++
的這種寫法。並且++
這種寫法也被取消掉了,改為+=代替。
2、表示區間
swift常見區間有兩種,開區間用..<
表示,閉區間用...
表示。要注意的是數字和省略號之間是不能加空格的。
func demo1() {
for i in 0..<5 {
print(i)
}
print("^^^^^^^")
for i in 0...5 {
print(i)
}
}
demo1()
複製程式碼
3、逆序操作
如果想要做逆序操作,只要在in後面的表示式後新增reversed()
即可。
func demo1() {
for i in (0..<5).reversed() {
print(i)
}
}
demo1()
複製程式碼
八、swift中的陣列
Swift語言提供了Arrays、Sets和Dictionaries三種基本的集合型別用來儲存集合資料。陣列是有序資料的集,集合是無序無重複資料的集,而字典則是無序的鍵值對的集。
陣列使用有序列表儲存同一型別的多個值。相同的值可以多次出現在一個陣列的不同位置中。
1、定義陣列
用let定義出來的陣列就是不可變的
//定義不可變陣列
let array = ["愛麗絲","小紅帽","白雪公主"]
複製程式碼
使用var來定義可變陣列。正確的寫法是Array<Element>
這樣的形式。其中Element是這個陣列中唯一允許存在的資料型別。但是為了簡便,推薦使用[Element]()
的寫法。
//定義可變陣列
var arrayM = [String]()
var arrayM1:[String]
var arrayM2 = Array<String>()
複製程式碼
2、建立帶有預設值的陣列
swift中的array型別還提供一個可以建立特定大小並且所有資料都被預設的構造方法。開發者可以在裡面指定它的數量和型別。
var threeDouble = Array(repeating: 0.0, count: 3)
print(threeDouble[1])
複製程式碼
3、對可變陣列的基本操作
使用append給陣列新增元素
arrayM.append("1")
arrayM.append("2")
arrayM.append("3")
arrayM.append("4")
arrayM.append("5")
複製程式碼
使用insert方法將值新增到具體索引值之前
arrayM.insert("10", at: 2)
複製程式碼
使用remove系列方法可以對陣列做刪除操作
arrayM.remove(at: 0)
arrayM.removeSubrange(1..<3)
arrayM.removeAll()
arrayM.removeLast() //可以去除最後一項,避免捕獲陣列count屬性
複製程式碼
通過取下標的方式對陣列進行修改和查詢
arrayM[0] = "小紅帽"
print(arrayM[2])
複製程式碼
利用區間對具體範圍內的值替換
//替換第2項和第3項的值
arrayM[2...4] = ["22","33"]
print(arrayM[3])
複製程式碼
4、陣列的遍歷
//根據下標值進行遍歷
for i in 0..<arrayM.count {
print(arrayM[i])
}
//直接遍歷陣列中的元素
for i in arrayM {
print(i)
}
複製程式碼
若同時需要每個資料項的值和索引,可以使用陣列的emumerated()
方法來進行陣列遍歷。
for(index,value) in arrayM.enumerated(){
print(String(index+1)+":"+value)
}
複製程式碼
5、陣列的合併
只有相同型別的陣列才能進行合併。
let resultArray = arrayM + array
複製程式碼
九、swift中的集合
集合(Set)用來儲存相同型別並且沒有確定順序的值。當集合元素順序不重要時或者希望確保每個元素只出現一次時可以使用集合而不是陣列。
集合中的元素必須有確定的hashvalue,或者是實現了hashable協議。而swift提供的Int,String等型別其實都是實現了hashable協議的。hashable是equable的子協議,如果要判斷兩個元素是否相等,就要看他們的hashvalue是否相等。
1、定義集合
使用set<Element>
定義。
Element
表示集合中允許儲存的型別,和陣列不同的是,集合沒有等價的簡化形式。
//建立空集合
var letters = Set<Character>()
//使用字面量建立集合
var favorite:Set<String> = ["綺羅生","意琦行"]
複製程式碼
要注意的是一個Set型別是不能直接後面跟的字面量被單獨推斷出來的,因此這個Set
是必須要顯示宣告的。但是由於swift的自動推斷功能,可以不用寫出Set的具體型別。比如說上面那個例子,省去String,也能推斷出Set的正確型別。
var favorite:Set = ["綺羅生","意琦行"]
複製程式碼
2、訪問和修改集合
通過.count
屬性知道集合的長度,通過isEmpty
判斷集合是否為空。
3、新增元素
favorite.insert("寒煙翠")
print(favorite.count)
複製程式碼
4、刪除元素
通過remove
的方法刪除元素,若這個值真的存在就會刪除改值,並且返回被刪除的元素。若集合中不包含這個值,就會返回nil。
if let removeBack = favorite.remove("意琦行"){
print(removeBack)
}else{
print("沒有找到值")
}
複製程式碼
5、集合操作
swift提供了許多數學方法來操作集合。
print(oddD.union(evenD).sorted()) //並集
print(oddD.intersection(evenD).sorted())//交集
print(oddD.subtracting(siggleDPrime).sorted())//取差值
print(oddD.symmetricDifference(siggleDPrime).sorted())//去掉相同值
複製程式碼
6、遍歷集合
for item in favorite {
print(item)
}
//按照首字母的順序輸出
for item1 in favorite.sorted() {
print(item1)
}
複製程式碼
7、集合的成員關係
用 ==
來判斷兩個集合是否包含全部相同的值
用 isSubset(of:)
來判斷一個集合中的值是否也被包含在另外一個集合中
用 isSuperset(of:)
來判斷一個集合中包含另一個集合所有的值
用isStrictSubset(of:)
或者isStrictSuperset(of:)
方法來判斷一個集合是否是另外一個集合的子集合或父集合並且兩個集合不相等
十、字典
字典是一種儲存多個相同型別的值的容器。每個值value
都關聯這唯一的鍵key
。鍵就是這個字典的識別符號。而且字典中的資料項並沒有具體順序。鍵集合不能有重複元素,而值集合是可以重複的。
1、定義字典
使用let
定義不可變的字典,使用var
定義可變字典。用字面量賦值時,系統會自動判斷[]
中存放的是鍵值對還是要一個個的元素。
let dict = [1:"one",2:"two",3:"three"] //定義不可變字典
var dictM = Dictionary<String,NSObject>() //定義可變字典
var dictM1 = [String:NSObject]()
//AnyObject一般用於指定型別,NSObject一般用於建立物件
複製程式碼
2、對可變字典做基本操作
新增、刪除和獲取元素
dictM1["name"] = "小仙女" as NSObject
dictM["age"] = 17 as NSObject
dictM.removeValue(forKey:"name")
//獲取:swift中只保留了最簡單的寫法,OC中有objectforkey的方法在swift中也被刪除掉了。
dictM["name"]
複製程式碼
3、修改元素
若字典中已經有對應的key,操作的結果是直接修改原來的key中儲存的value。若字典中沒有對應的key,則會新增新的鍵值對。
dictM["name"] = "llx"
複製程式碼
4、遍歷字典
可以通過範圍for遍歷所有的key和value。也可以遍歷所有的鍵值對。
for (key,value) in dictM {
print(key)
print(value)
}
複製程式碼
5、合併字典
合併字典時通過遍歷的方式將第二個字典的內容新增到第一個字典中。絕對不能用相加的方式對字典進行合併。
var dict1 = ["name":"llx","age":"17"]
var dict2 = ["num":"007"]
for (key,value) in dict2 {
dict1[key] = value
}
dict
複製程式碼
十一、元組
元組是swift中特有的一種資料結構,用於定義一組資料,元組在數學中的應用十分廣泛。
1、定義元組
使用()
包含資訊,組成元組型別的資料可以被稱為“元素”。
//使用元組來描述個人資訊
let info1 = ("1001","張三",30)
複製程式碼
2、起別名
可以給元素加上名稱,之後可以通過元素名稱訪問元素
//給元素加上名稱,之後可以通過元素名稱訪問元素
let info2 = (id:"1001",name:"張三",age:30)
info2.name
複製程式碼
元組一般用於作為方法的返回值。元組中元素的別名,就是元組的名稱
let (name,age) = ("張三",18)
name
複製程式碼
十二、函式
函式相當於Objective-C中的方法,是一段完成特定任務的獨立程式碼片段。可以通過給函式命名來標誌某個函式的功能。而這個名字可以用來在需要的時候“呼叫”該函式完成其任務。格式如下:
func 函式名(引數列表)-> 返回值型別 {
程式碼塊
return 返回值
}
複製程式碼
func表示關鍵字,多個引數列表之間用逗號隔開,也可以沒有引數。使用->
指向返回值型別。如果沒有返回值,可以用Void
代替,也可以省略。
1、定義無參無返回的函式
func phone()->Void {
print("小米")
}
phone()
複製程式碼
2、定義有參無返回的函式
func phoneNum() -> String {
return "123456"
}
print(phoneNum())
複製程式碼
3、定義有參無返回的函式
func callPhone(phoneNum:String){
print("打電話給\(phoneNum)")
}
callPhone(phoneNum: "123456")
複製程式碼
4、定義有參有返回的函式
func sum(num1 : Int,num2 : Int) -> Int{
return num1 + num2
}
sum(num1: 30, num2: 30)
複製程式碼
在swift4之後,呼叫函式的時候,能直觀的看到引數。而在之前呼叫之時,只能看見第二個引數之後的名稱,表達起來並不直觀。如何解決這個問題呢?
可以採用給引數起別名的方式,在引數前面新增一個別名。
func sum(number1 num1: Int,number2 num2 : Int) -> Int{
return num1 + num2
}
sum(number1: 2, number2: 4)
複製程式碼
5、預設引數
在swift中可以給方法的引數設定預設值。比如說買甜筒的時候,商店預設會給顧客準備原味冰淇淋。但是使用者也可以選擇指定口味。
func makeIceCream(flavor:String = "原味") -> String {
return "製作一個\(flavor)冰淇淋"
}
makeIceCream()
makeIceCream(flavor: "抹茶")
複製程式碼
6、可變引數
有些時候,在建立方法的時候,並不確定引數的個數,於是swift推出了可變引數。引數的型別之後使用...
表示多個引數。
func sum(num:Int...) -> Int {
var result = 0
for i in num {
result += i
}
return result
}
sum(num: 18,29,3)
複製程式碼
7、引用傳遞
如果現在有這樣一個需求:要交換兩個數的值,不能使用系統提供的方法。要如何來完成呢?
如果按照上面的寫法就會報錯,可以按住option
鍵檢視,引數預設是不可變的。
而且就算可行,做到的也是值傳遞。為了解決這一問題,swift提供了關鍵字inout
來宣告資料地址傳遞,也被稱之為引用傳值。在swift3.0的時候,inout的位置發生了改變,被放置在標籤位置。但是作用與之前相同。
func swapNum1( m : inout Int, n : inout Int) {
let tempNum = m
m = n
n = tempNum
}
swapNum1(m: &m, n: &n)
print("m:\(m),n:\(n)")
複製程式碼
十三、類
swift用關鍵字class
來定義類。通常情況下,定義類時,讓它繼承自NSObject
,若沒有指定父類,那麼該類就是rootClass
。類的格式如下:
class 類名:SuperClass {
//定義屬性和方法
}
複製程式碼
1、定義儲存屬性和建立類物件
物件的屬性必須要賦值,用解包的方式賦值為nil。
class Person : NSObject {
//定義儲存屬性
var age : Int = 0
var name : String? //物件的屬性必須賦值,不賦值會報錯的哦
}
let p = Person()
複製程式碼
2、給類的屬性賦值
可以直接賦值,也可以通過KVC進行賦值
p.age = 10
p.name = "llx"
if let name = p.name {
print(name)
}
複製程式碼
3、定義方法
在swift中,如果使用當前某一物件的屬性或者方法,可以直接使用,不需要加self
// 定義方法,返回平均成績
func getAverage() -> Double {
return (mathScore + EnglishScore)*0.5
}
複製程式碼
let average = p.getAverage()
複製程式碼
4、定義計算屬性
通過別的方式計算到結果的屬性,稱之為計算屬性。
var averageS : Double {
return (mathScore + EnglishScore) * 0.5
}
複製程式碼
5、定義類屬性
類屬性是和整個類相關的屬性,用static修飾,作用域是整個類。通過類名進行訪問。
static var courseCount : Int = 0
複製程式碼
在類外通過類名訪問類屬性
Person.courseCount = 2
複製程式碼
6、類的建構函式
建構函式類似於OC中的init方法。預設情況下建立一個類時,必定會呼叫一個建構函式。如果一個類繼承自NSObjct,可以對父類的建構函式進行重寫。
在建構函式中,如果沒有明確super.init()
。那麼系統會預設呼叫super.init()
。
class Person : NSObject {
var name : String?
var age : Int = 0
override init() {
print("hello world")
}
}
let p = Person()
複製程式碼
7、自定義建構函式
自定義建構函式可以傳入引數,做賦值操作時採用self
呼叫屬性以示區分。
class Person : NSObject {
var name : String?
var age : Int = 0
// 自定義建構函式
init(name:String,age:Int){
self.name = name
self.age = age
}
}
// 呼叫自定義的建構函式
let p1 = Person(name: "kaka", age: 12)
print(p1.age)
複製程式碼
可以定義字典型別的建構函式。用KVC的方式將字典的值取出來,要呼叫系統的setValue
方法就必須先呼叫系統的建構函式建立出物件。為了防止取出的物件沒有屬性而導致程式奔潰,需要重寫系統的setValue方法。
如果用KVC的方式一定要先呼叫父類的建構函式。因為系統預設呼叫是放在方法最後面呼叫的。
class Person : NSObject {
@objc var name : String?
@objc var age : Int = 0
init(dict:[String : Any]) {
super.init()
// 要呼叫系統的`setValue`方法就必須先呼叫系統的建構函式建立出物件
setValuesForKeys(dict)
}
// 防止奔潰
override func setValue(_ value: Any?, forUndefinedKey key: String) {
}
}
let p2 = Person(dict:["name":"lala","age":18,"score":33])
p2.name
p2.age
複製程式碼
由於swift與objective-c的編譯方式不同,用KVC字典轉模型建構函式時,需要在屬性前面加上@objc
。
8、類的屬性監聽器
在object-c中,我們可以重寫set方法來監聽屬性的改變,而在swift中也可以通過屬性觀察者來監聽和響應屬性值的變化。通常用於監聽儲存屬性和類屬性的改變。對於計算屬性則不需要定義屬性觀察者,因為我們可以在計算屬性的setter
中直接觀察並響應這種值的變化。
可以通過設定以下觀察方法並響應這種值的變化。
willSet
:在屬性值被儲存之前設定,此時新屬性值作為一個常量引數被傳入。該引數名預設為newValue
,開發者可以自己定義該引數名。
didSet
:在新屬性值被儲存後立即呼叫,與willSet
不同的是,此時傳入的是屬性的舊值,預設引數名為oldValue
。
上面兩個方法都只有在屬性第一次被設定時才會呼叫,在初始化時,不會去呼叫這些監聽方法。
class Person : NSObject {
//屬性監聽器
var name:String? {
willSet {
print(name as Any)
//如果想要檢視接下來的新值,可以使用newValue
print(newValue as Any)
}
didSet {
print(name as Any)
}
}
}
let p = Person()
p.name = "llx"
複製程式碼
十四、閉包
閉包是swift中非常重要的一個知識點。類似於objective-c中的block,其實函式就相當於一個特殊的閉包。閉包需要提前寫好,在適當的時候再執行。
1、定義閉包
閉包的格式是(引數列表)->(返回值型別) in 實現程式碼
舉一個最簡單的栗子?
用常量記錄一個程式碼塊,按住option
鍵就能看到,b1
是一個閉包。再到適合的地方去呼叫它。
let b1 = {
print("幹掉他們")
}
b1()
複製程式碼
再來看一個帶引數的閉包。在閉包中,引數、返回值和實現程式碼都是寫在花括號裡面的。in
是用來定義分割和實現的。
let b2 = {
(x:String)->() in print(x)
}
b2("string")
複製程式碼
2、閉包案例
這個案例要模擬封裝一個網路請求的類。利用閉包將jsonData型別的資料傳遞給展示頁面。
- 建立一個新的專案,選擇swift語言
- 封裝一個網路請求的類
HttpTool.swift
繼承自NSObject
用非同步執行緒模擬網路資料請求,再回到主執行緒中回撥閉包
class HttpTool: NSObject {
//閉包型別:(引數列表)->(返回值型別)
func loadData(callback:@escaping(_ jsonData : String)->()) {
DispatchQueue.global().async {
print("發生網路請求:\(Thread.current)")
}
DispatchQueue.main.async {
()->Void in
print("獲取到資料,並且回撥:\(Thread.current)")
callback("jsonData資料")
}
}
}
複製程式碼
- 到需要接收資料的介面定義
Httptool
類的屬性,設定一個初始化值,將初始值賦值給變數
在swift中是不需要引入標頭檔案的,檔案之間可共享
import UIKit
class ViewController: UIViewController {
var tools : HttpTool = HttpTool()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//用閉包將json資料拿到
tools.loadData { (jsonData) ->() in
print("在viewcontroller中拿到資料\(jsonData)" )
}
}
}
複製程式碼
3、尾隨閉包
尾隨閉包用於需要將一個很長的閉包表示式作為最後一個引數傳遞給函式。也就是說如果按時的最後一個引數是閉包,那麼在呼叫它的時候就可以把這個閉包寫在括號外面,並緊跟括號,函式的其他引數則仍然寫在括號之中。
//這個函式接受一個String和一個閉包
//函式體內呼叫閉包,並且將String作為引數傳遞給閉包
func myFunc(strP:String,closeP:(String)->Void) {
closeP(strP)
}
//普通呼叫
myFunc(strP: "hello", closeP: {(string) in print(string)})
//尾隨閉包
myFunc(strP: "hello") {
(string) in print(string)
}
複製程式碼
4、逃逸閉包
當一個閉包作為引數傳到一個函式中,但是該閉包要在函式返回之後才被執行,於是就稱這樣的閉包為逃逸閉包。也就是說閉包逃離了函式的作用域。寫法是在這個閉包引數前加一個@escaping
用來指明這個閉包是允許逃逸出該函式的。
- 宣告一個方法,這個方法是一個逃逸閉包 該方法要做的事情,就是將閉包新增到陣列中去
//定義陣列,裡面的元素都是閉包型別的
var callBackArray : [()->Void] = []
//定義一個接收閉包的函式
func testEscapingClosure(callBack:@escaping ()-> Void) {
callBackArray.append(callBack)
}
複製程式碼
- 當改變陣列的時候,取第0個元素呼叫。此時就改變了變數x的值
class SomeClass {
var x = 10
func doSomething(){
testEscapingClosure {
self.x = 100
}
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
callBackArray.first?()
print(instance.x)
複製程式碼
因為逃逸閉包是函式執行之後才會執行,所以可以這樣理解:建立一個類的物件instance
;在物件中初始化一個x=10;利用物件執行了函式doSomething
;函式內部呼叫全域性函式testEscapingClosure
,期望修改instance物件的x值為100,但是此時並沒有執行這個包含了賦值語句的閉包。
查詢全域性陣列callBackArray
,找到裡面第一個元素,顯然找到的是在testEscapingClosure
函式中新增的閉包{self.x = 100}
,此時才通過全域性陣列的查詢找出閉包並執行,於是x此時才被賦值為100。這就是在函式執行完畢後才執行閉包。剛好符合逃逸閉包的定義。
結論: 逃逸閉包將在函式執行之後執行,於是這段程式碼最後輸出為100是因為閉包最後才被執行……
- 解決迴圈引用的三種方式 1、可以使用weak關鍵字將物件之間的聯絡變為弱引用
weak var weakself = self
複製程式碼
2、第一種方式的簡化
[weak self]
複製程式碼
3、使用unowned解決
[unowned self]
複製程式碼
但是該方法十分危險,要確保資料一定有值。否則會發生奔潰。
__weak 與__unretained有何區別? __weak修飾的弱引用,如果指向的物件被銷燬,那麼指標會立馬指向nil __unretained修飾的弱引用,如果指向的物件被銷燬,它的指標依然會指向之前的記憶體地址,很容易產生野指標(殭屍物件)
十五、tableView的用法
1、 懶載入
swift中也有懶載入的方式,並且在swift中有專門的關鍵字lazy
來實現某一個屬性實現懶載入。
格式:lazy var 變數:型別 = {建立變數程式碼}()
懶載入的本質在第一次使用的時候執行閉包,將閉包的返回值賦值給屬性,並且只會賦值一次。
//懶載入只能用於結構體或者類的成員變數中
class Person:NSObject {
lazy var array : [String] = {
()->[String] in
return ["llx","lll"]
}()
}
複製程式碼
2、tableView的使用
使用步驟如下:
- 建立tableView物件
使用懶載入的方式,到需要用到的時候再建立tableView。將tableView新增到控制器上的View。
class ViewController: UIViewController {
lazy var tableView:UITableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
}
}
複製程式碼
- 設定tableView的frame
tableView.frame = view.bounds
複製程式碼
- 設定資料來源和代理
實現UITableView的協議,併為tableView設定資料來源
class ViewController: UIViewController ,UITableViewDataSource,UITableViewDelegate{
lazy var tableView:UITableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.bounds
//設定資料來源
tableView.dataSource = self
tableView.delegate = self
}
}
複製程式碼
- 實現代理方法
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
複製程式碼
建立cell。因為cell是個可選型別,有可能有值,也可能為nil。所以要進行判斷。給cell設定資料的時候,選擇textLabel點選option
會發現textLabel也是可選型別。
在最後返回cell的時候,對cell進行強制解包。因為之前已經做過判斷,所以不會出現程式奔潰的問題。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let CellID = "CellID"
var cell = tableView.dequeueReusableCell(withIdentifier: CellID)
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: CellID)
}
cell?.textLabel?.text = "測試資料:\(indexPath.row)"
return cell!
}
複製程式碼
實現點選的代理方法
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("點選了:\(indexPath.row)")
}
複製程式碼
##十六、swift中的註釋
在swift中,類似於paramg --mark
的寫法是不可行的。
它是如下兩種形式
//MARK:- 要寫的內容
用於分組
class ViewController: UIViewController ,UITableViewDataSource,UITableViewDelegate{
// MARK:- 懶載入
lazy var tableView:UITableView = UITableView()
// MARK:- 系統回撥函式
override func viewDidLoad() {
super.viewDidLoad()
}
}
複製程式碼
這樣寫的話,就可以在選單欄看到分組的資訊
/// 提示資訊
用於提示
若在tableView系列的某個方法上面寫上///提示
,到其他地方呼叫該方法時,會出現前面寫的註釋資訊。
十七、列舉
1、定義
在swift中,列舉使用的是由enum
關鍵字來建立的列舉,列舉的所有成員都放在一對大括號裡面。它為一組相關的值定義一個共同的型別。使用case
關鍵字來定義一個新的列舉成員值。
enum SomeEnum {
// 在這裡定義列舉
case north
case south
case east
case west
}
複製程式碼
上面這個列舉定義的東南西北四個值就是這個列舉的成員值。與C語言和objective-c不同的是,swift的列舉成員值在建立的時候並不會被賦予一個預設的整形值。這些值的型別就是剛剛定義好的列舉的名字SomeEnum
。
如果希望多個成員值要寫在同一行中,可以使用逗號將他們分割開。
enum Plant {
case mercury,earth,mars
}
複製程式碼
每個列舉都定義了一個新的型別,就像swift中的其他型別一樣。此時可以把它賦值給一個變數,而且可以用點語法這種形式呼叫。
var directionT = SomeEnumeration.west
directionT = .east
複製程式碼
注意:在switch中使用列舉值的時候,一定要窮舉出所有的情況,如果忽略其中的一個,程式碼都無法編譯通過。因為它沒有考慮到列舉類的全部成員。如果說不需要匹配所有的列舉成員,可以提供一個default分支來涵蓋其他未明確處理的列舉成員。
class Person:NSObject{
var directionT = SomeEnum.west
func direc() {
switch directionT {
case .north:
print("north")
case .east:
print("east")
default:
print("沒有方向")
}
}
}
複製程式碼
2、關聯值
可以定義swift的列舉類儲存任意型別的關聯值,而且每個列舉成員的關聯值型別都可以不相同。比如說,來建立一個條形碼型別。類似於庫存,可以有不同型別的條形碼去識別商品,比如說通過數字,或者根據產品程式碼來識別。
enum BarCode {
case upc(Int,Int,Int,Int)
case qrCode(String)
}
複製程式碼
上面程式碼可以理解為定義一個名為BarCode
的列舉型別。它的一個成員值是一個具有(Int,Int,Int,Int)型別關聯值的upc
,另一個成員值是具有String型別的qrCode
之後可以使用任意的條形碼型別去建立新的條形碼
class Person:NSObject {
// 建立一個名為pBar變數,並將Barcode.upc賦值給它。
func function() {
var pBar = BarCode.upc(9, 0, 3, 3)
pBar = .qrCode("ABCD")
}
}
複製程式碼
這個時候原來的barcode.upc和其整數關聯值被新的Barcode.qrCode和其字串關聯值所替代了。
3、列舉的原始值
列舉的原始值就是列舉的預設值,這些原始值的型別必須相同。在定義列舉的時候必須給出型別。
enum ASCIICHAR : Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
複製程式碼
在使用原始值為整數或者字串型別的列舉時,不需要顯式的為每一個列舉成員設定原始值,swift將會自動未它們賦值。
enum Planet : Int {
case mercury = 1, venus,earth,mars
}
複製程式碼
上面這個例子,Planet.mercury
原始值是1,那麼後面的venus
就是2,之後以此類推。
可以通過rawValue
屬性來訪問列舉變數的原始值.
let earthsOrder = Planet.earth.rawValue
複製程式碼
4、列舉遞迴
列舉成員的關聯值為當前列舉型別時稱為遞迴列舉。那我們可以通過使用indirect
修飾列舉變數。indirect
修飾整個列舉時,所有成員均可遞迴(也可不遞迴?)。
indirect enum Ari {
case number(Int)
case addition(Ari,Ari)
case multi(Ari,Ari)
}
複製程式碼
上面定義的列舉型別可以儲存三種算術表示式:純數字、兩個表示式相加、兩個表示式相乘。
let five = Ari.number(5)
let four = Ari.number(4)
let sum = Ari.addition(five, four)
let product = Ari.multi(sum, Ari.number(2))
複製程式碼
通過列舉遞迴,就成功的建立了一個(5+4)*2的式子。
十八、結構體
結構體通過struct
去宣告。在swift中,用到了大量的結構體,比如說基本的資料型別都是結構體而不是類。這意味著它們被賦值給新的常量或者變數,或者被傳入函式或方法中時,值會被拷貝。
struct teacher {
var name : String = ""
var age : Int = 30
}
複製程式碼
十九、擴充套件
擴充套件 (Extension)可以做到無需修改原本的程式碼就直接把想要的功能實現。
extension 某個現有的class {
//新增新功能
}
複製程式碼
限制:
- 不能新增任何已存在的 法或是屬性
- 新增的屬性不能是儲存屬性,只能是計算屬性
1、擴充套件在方法中的應用
extension String {
func sayHello() {
print("Hello from extension")
}
}
複製程式碼
上面這段程式碼是對String
做了一個擴充套件。之後宣告一個變數呼叫擴充套件方法。
var hello = "hi"
hello.sayHello()
複製程式碼
此後,任何String
型別都可以呼叫該擴充套件方法。
2、用擴充套件進行計算
extension Int {
var squared : Int {
return (self * self)
}
}
複製程式碼
上面這段程式碼對Int
擴充套件了一個屬性,讓它計算一個數字的平方值。
var newInt = 30
newInt.squared
999.squared
複製程式碼
3、擴充套件類或結構體
- 建立一個普通類
class Lisa {
var lisa = "半邊天使"
}
複製程式碼
- 對類擴充套件,新增一個方法,使其能做自我介紹
extension Lisa {
func describe() -> String {
return "我可是會傲嬌的"
}
}
複製程式碼
- 建立物件呼叫方法
二十、泛型
泛型可以讓開發者寫出靈活可重複使用的方法跟結構。 先看一個栗子?!!
var stringArray = ["Hi", "Hello", "Bye"]
var intArray = [1,2,3]
var doubleArray = [1.1,2.2,3.3]
複製程式碼
上面建立了三個不同型別的陣列,若是要求列印所有陣列中的元素,通常會怎麼做呢?
func printStringFromArray(a: [String]) {
for s in a {
print(s) }
}
func printIntFromArray(a: [Int]){
for i in a {
print(i) }
}
func printdoubleFromArray(a:[Double]) {
for d in a {
print(d) }
}
printStringFromArray(a: stringArray)
printIntFromArray(a: intArray)
printdoubleFromArray(a: doubleArray)
複製程式碼
上面這段冗長的程式碼實在讓人不忍直視。而泛型的出現正好可以解決這一問題。
func printEelementFormArray<T>(a:[T]){
for element in a {
print(element)
}
}
複製程式碼
這段程式碼中的T
代表了任意的元素。無論上面型別的資料都能放入其中。之後只要呼叫者一個方法,傳入不同的陣列就能將不同型別的元素列印出來。
二十一、協議
1、對物件導向語言的吐槽
- 使用子類時,協議繼承父類的屬性和方法。其中某些方法或屬性並不是開發者所需要的。這會讓程式碼變得異常的臃腫。
- 若一個類擁有很多父類,會讓開發者很難找到每個類中的問題並進行修改。
- 物件引用到記憶體的同一地方,若是發生改變,可能會造成程式碼混亂的現象。
而swift是一種面向協議的語言。協議其實就像籃球教練,會告訴選手如何去訓練,但是教練本身並不會出現在球場。Swift中的protocol不僅能定義方法還能定義屬性,配合extension擴充套件的使用還能提供一些方法的預設實現,而且不僅類可以遵循協議,現在的列舉和結構體也能遵循協議了。
2、一個簡單的協議案例
- 建立一個簡單的協議,並讓一個結構體去遵循
遵循協議的方法與繼承類似。
protocol People {
}
struct Lisa: People {
}
複製程式碼
- 完善協議
給協議新增一些屬性和方法,用get
set
設定協議的狀態。遵循協議時要了解變數是否能讀取或賦值。
protocol People {
var name: String {get set}
var race: String {get set}
func sayHi()
}
複製程式碼
- 在結構體中實現協議的方法和變數
struct Lisa: People {
var name: String = "Lisa"
var race: String = "Asian"
func sayHi() {
print("Hi~, I'm \(name)")
}
}
複製程式碼
3、協議的繼承
- 建立一個協議,讓該協議繼承自之前建立的People協議
protocol superman {
var canFly: Bool {get set}
func punch()
}
protocol superman: People {
var canFly: Bool {get set}
func punch()
}
複製程式碼
- 呼叫
struct AngleLisa: superman {
var name: String = "Lisa"
var race: String = "Asian"
func sayHi() {
print("Hi, I'm \(name)")
}
var canFly: Bool = true
func punch() {
print("punch Vergil")
}
}
複製程式碼
由此可知,一旦協議進行了繼承,不但要實現本協議中所宣告的方法和屬性,連協議父類的方法和屬性也不能落下。
二十二、swift4新特性
以下內容來自 最全的 Swift 4 新特性解析
感謝大佬提供學習資源!!!
1、語法改進
- 在擴充套件extension中可以訪問private的屬性
舉一個簡單的栗子?!
struct Date: Equatable, Comparable {
private let secondsSinceReferenceDate: Double
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
複製程式碼
上面程式碼定義了一個 Date 結構體,並實現 Equatable 和 Comparable 協議。為了讓程式碼更清晰,可讀性更好,一般會把對協議的實現放在單獨的 extension 中,這也是一種非常符合 Swift 風格的寫法,如下:
struct Date {
private let secondsSinceReferenceDate: Double
}
extension Date: Equatable {
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
}
extension Date: Comparable {
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
複製程式碼
但是在 Swift 3 中,這樣寫會導致編譯報錯,extension 中無法獲取到 secondsSinceReferenceDate
屬性,因為它是 private 的。於是在 Swift 3 中,必須把 private
改為 fileprivate
。
struct Date {
fileprivate let secondsSinceReferenceDate: Double
}
...
複製程式碼
但是如果用 fileprivate
,屬性的作用域就會比我們需要的更大,可能會不小心造成屬性的濫用。
在 Swift 4 中,private 的屬性的作用域擴大到了 extension 中,並且被限定在了 struct 和 extension 內部,這樣struct的屬性就不需要再用 fileprivate
修飾了,這是最好的結果。
- 型別和協議的組合型別
protocol Shakeable {
func shake()
}
extension UIButton: Shakeable { func shake() {/* */ } }
extension UISlider: Shakeable { func shake() {/* */ } }
func shakeEm(controls: [???]) {
for control in controls where control.state.isEnabled {
}
control.shake()
}
複製程式碼
仔細思考上面的程式碼,如果是swift3中,func shakeEm(controls: [???])
中的???
應該寫上面型別呢?如果寫UIControl
,那麼呼叫control.shake()
時就會報錯。如果寫Shakeable
型別,那麼control.state.isEnabled
這條語句就會報錯。
swift4為了解決類似問題,實現了把型別和協議用&
組合在一起作為一個型別使用的寫法。把它宣告為UIControl & Shakeable
型別。
func shakeEm(controls: [UIControl & Shakeable]) {
for control in controls where control.isEnabled {
control.shake()
}
}
複製程式碼
- Associated Type 可以追加 Where 約束語句
在 Swift 4 中可以在 associatedtype
後面宣告的型別後追加 where 語句。
protocol Sequence {
associatedtype Element where Self.Element == Self.Iterator.Element
// ...
}
複製程式碼
它限定了 Sequence 中 Element 這個型別必須和 Iterator.Element 的型別一致。
通過 where 語句可以對型別新增更多的約束,使其更嚴謹,避免在使用這個型別時做多餘的型別判斷。
- 新的 Key Paths 語法
先來看看 Swift 3 中 Key Paths 的寫法:
@objcMembers class Kid: NSObject {
dynamic var nickname: String = ""
dynamic var age: Double = 0.0
dynamic var friends: [Kid] = []
}
var ben = Kid(nickname: "Benji", age: 5.5)
let kidsNameKeyPath = #keyPath(Kid.nickname)
let name = ben.valueForKeyPath(kidsNameKeyPath)
ben.setValue("Ben", forKeyPath: kidsNameKeyPath)
複製程式碼
Swift 4 中建立一個 KeyPath
用 \
作為開頭:
\Kid.nickname
複製程式碼
當編譯器可以推匯出型別時,可以省略基礎型別部分:
\.nickname
複製程式碼
上面的程式碼在 Swift 4 中就可以這樣寫:
struct Kid {
var nickname: String = ""
var age: Double = 0.0
var friends: [Kid] = []
}
var ben = Kid(nickname: "Benji", age: 8, friends: [])
let name = ben[keyPath: \Kid.nickname]
ben[keyPath: \Kid.nickname] = "BigBen"
複製程式碼
相比 Swift 3,Swift 4 的 Key Paths 具有以下優勢:
型別可以定義為 class、struct 定義型別時無需加上 @objcMembers、dynamic 等關鍵字 效能更好 型別安全和型別推斷,例如 ben.valueForKeyPath(kidsNameKeyPath) 返回的型別是 Any,ben[keyPath: \Kid.nickname] 直接返回 String 型別 可以在所有值型別上使用
- 下標支援泛型
Swift 支援通過下標來讀寫容器中的資料,但是如果容器類中的資料型別定義為泛型,以前的下標語法就只能返回 Any,在取出值後需要用 as? 來轉換型別。Swift 4 定義下標也可以使用泛型了。但是並不需要做轉型操作。
struct GenericDictionary<Key: Hashable, Value> {
private var data: [Key: Value]
init(data: [Key: Value]) {
self.data = data
}
subscript<T>(key: Key) -> T? {
return data[key] as? T
}
}
let dictionary = GenericDictionary(data: ["Name": "Xiaoming"])
let name: String? = dictionary["Name"] // 不需要再寫 as? String
複製程式碼
2、字串
- Unicode 字串在計算 count 時的正確性改善
在 Unicode 中,有些字元是由幾個其它字元組成的,比如 é 這個字元,它可以用 \u{E9} 來表示,也可以用 e 字元和上面一撇字元組合在一起表示 \u{65}\u{301}。
var family = "?"
family += "\u{200D}?"
family += "\u{200D}?"
family += "\u{200D}?"
print(family)
print(family.characters.count)
複製程式碼
這個 family 是一個由多個字元組合成的字元,列印出來的結果為 ????。上面的程式碼在 Swift 3 中列印的 count 數是 4,在 Swift 4 中列印出的 count 是 1。
- 更快的處理速度
Swift 4 的字串優化了底層實現,對於英語、法語、德語、西班牙語的處理速度提高了 3.5 倍。對於簡體中文、日語的處理速度提高了 2.5 倍。
-
去掉了characters
-
One-sided Slicing
Swift 4 新增了一個語法糖 ...
可以對字串進行單側邊界取子串。
Swift 3:
let values = "abcdefg"
let startSlicingIndex = values.index(values.startIndex, offsetBy: 3)
let subvalues = values[startSlicingIndex..<values.endIndex]
// defg
Swift 4:
let values = "abcdefg"
let startSlicingIndex = values.index(values.startIndex, offsetBy: 3)
let subvalues = values[startSlicingIndex...] // One-sided Slicing
// defg
複製程式碼
- String 當做 Collection 來用
Swift 4 中 String 可以當做 Collection 來用,並不是因為 String 實現了 Collection 協議,而是 String 本身增加了很多 Collection 協議中的方法,使得 String 在使用時看上去就是個 Collection。例如:
翻轉字串:
let abc: String = "abc"
print(String(abc.reversed()))
// cba
複製程式碼
遍歷字元:
let abc: String = "abc"
for c in abc {
print(c)
}
/*
a
b
c
*/
複製程式碼
Map、Filter、Reduce:
// map
let abc: String = "abc"
_ = abc.map {
print($0.description)
}
// filter
let filtered = abc.filter { $0 == "b" }
// reduce
let result = abc.reduce("1") { (result, c) -> String in
print(result)
print(c)
return result + String(c)
}
print(result)
複製程式碼
- Substring
在 Swift 中,String 的背後有個 Owner Object 來跟蹤和管理這個 String,String 物件在記憶體中的儲存由記憶體其實地址、字元數、指向 Owner Object 指標組成。Owner Object 指標指向 Owner Object 物件,Owner Object 物件持有 String Buffer。當對 String 做取子字串操作時,子字串的 Owner Object 指標會和原字串指向同一個物件,因此子字串的 Owner Object 會持有原 String 的 Buffer。當原字串銷燬時,由於原字串的 Buffer 被子字串的 Owner Object 持有了,原字串 Buffer 並不會釋放,造成極大的記憶體浪費。
在 Swift 4 中,做取子串操作的結果是一個 Substring 型別,它無法直接賦值給需要 String 型別的地方。必須用 String() 包一層,系統會通過複製建立出一個新的字串物件,這樣原字串在銷燬時,原字串的 Buffer 就可以完全釋放了。
let big = downloadHugeString()
let small = extractTinyString(from: big)
mainView.titleLabel.text = small // Swift 4 編譯報錯
mainView.titleLabel.text = String(small) // 編譯通過
複製程式碼
- 多行字串字面量
Swift 3 中寫很長的字串只能寫在一行。
func tellJoke(name: String, character: Character) {
let punchline = name.filter { $0 != character }
let n = name.count - punchline.count
let joke = "Q: Why does \(name) have \(n) \(character)'s in their name?\nA: I don't know, why does \(name) have \(n) \(character)'s in their name?\nQ: Because otherwise they'd be called \(punchline)."
print(joke)
}
tellJoke(name: "Edward Woodward", character: "d")
複製程式碼
字串中間有換行只能通過新增 \n 字元來代表換行。
Swift 4 可以把字串寫在一對 """ 中,這樣字串就可以寫成多行。
func tellJoke(name: String, character: Character) {
let punchline = name.filter { $0 != character }
let n = name.count - punchline.count
let joke = """
Q: Why does \(name) have \(n) \(character)'s in their name?
A: I don't know, why does \(name) have \(n) \(character)'s in their name?
Q: Because otherwise they'd be called \(punchline).
"""
print(joke)
}
tellJoke(name: "Edward Woodward", character: "d")
複製程式碼
3、Swift 標準庫
- Encoding and Decoding
當需要將一個物件持久化時,需要把這個物件序列化,往常的做法是實現 NSCoding 協議,寫過的人應該都知道實現 NSCoding 協議的程式碼寫起來很痛苦,尤其是當屬性非常多的時候。幾年前有一個工具能自動生成 Objective-C 的實現 NSCoding 協議程式碼,當時用著還不錯,但後來這個工具已經沒有人維護很久了,而且不支援 Swift。
Swift 4 中引入了 Codable
幫我們解決了這個問題。
struct Language: Codable {
var name: String
var version: Int
}
複製程式碼
我們想將這個 Language 物件的例項持久化,只需要讓 Language 符合 Codable 協議即可,Language 中不用寫別的程式碼。符合了 Codable 協議以後,可以選擇把物件 encode 成 JSON 或者 PropertyList。
Encode 操作如下:
let swift = Language(name: "Swift", version: 4)
if let encoded = try? JSONEncoder().encode(swift) {
// 把 encoded 儲存起來
}
複製程式碼
Decode 操作如下:
if let decoded = try? JSONDecoder().decode(Language.self, from: encoded) {
print(decoded.name)
}
複製程式碼
- Sequence 改進
Swift 3:
protocol Sequence {
associatedtype Iterator: IteratorProtocol
func makeIterator() -> Iterator
}
複製程式碼
Swift 4:
protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
func makeIterator() -> Iterator
}
複製程式碼
由於 Swift 4 中的 associatedtype
支援追加 where 語句,所以 Sequence
做了這樣的改進。
Swift 4 中獲取 Sequence
的元素型別可以不用 Iterator.Element
,而是直接取 Element
。
SubSequence 也做了修改:
protocol Sequence {
associatedtype SubSequence: Sequence
where SubSequence.SubSequence == SubSequence,
SubSequence.Element == Element
}
複製程式碼
通過 where 語句的限定,保證了型別正確,避免在使用 Sequence 時做一些不必要的型別判斷。
Collection 也有一些類似的修改。
- Protocol-oriented integers
整數型別符合的協議有修改,新增了 FixedWidthInteger 等協議,具體的協議繼承關係如下:
+-------------+ +-------------+
+------>+ Numeric | | Comparable |
| | (+,-,*) | | (==,<,>,...)|
| +------------++ +---+---------+
| ^ ^
+-------+------------+ | |
| SignedNumeric | +-+-------+-----------+
| (unary -) | | BinaryInteger |
+------+-------------+ |(words,%,bitwise,...)|
^ ++---+-----+----------+
| +-----------^ ^ ^---------------+
| | | |
+------+---------++ +---------+---------------+ +--+----------------+
| SignedInteger | | FixedWidthInteger | | UnsignedInteger |
| | |(endianness,overflow,...)| | |
+---------------+-+ +-+--------------------+--+ +-+-----------------+
^ ^ ^ ^
| | | |
| | | |
++--------+-+ +-+-------+-+
|Int family |-+ |UInt family|-+
+-----------+ | +-----------+ |
+-----------+ +-----------+
複製程式碼
- Dictionary and Set enhancements
這裡簡單列一下 Dictionary 和 Set 增強了哪些功能:
通過 Sequence 來初始化 可以包含重複的 Key Filter 的結果的型別和原型別一致 Dictionary 的 mapValues 方法 Dictionary 的預設值 Dictionary 可以分組 Dictionary 可以翻轉
- NSNumber bridging and Numeric types
let n = NSNumber(value: 999)
let v = n as? UInt8 // Swift 4: nil, Swift 3: 231
複製程式碼
在 Swift 4 中,把一個值為 999 的 NSNumber 轉換為 UInt8 後,能正確的返回 nil,而在 Swift 3 中會不可預料的返回 231。
- MutableCollection.swapAt(::)
MutableCollection 現在有了一個新方法 swapAt(::) 用來交換兩個位置的值,例如:
var mutableArray = [1, 2, 3, 4]
mutableArray.swapAt(1, 2)
print(mutableArray)
// 列印結果:[1, 3, 2, 4]
複製程式碼
4、構建過程改進
- New Build System
Xcode 9 引入了 New Build System,可在 Xcode 9 的 File -> Project Settings... 中選擇開啟。
- 預編譯 Bridging Headers 檔案
對於 Swift 和 Objective-C 混合的專案,Swift 呼叫 Objective-C 時,需要建立一個 Bridging Headers 檔案,然後把 Swift 要呼叫的 Objective-C 類的標頭檔案都寫在裡面,編譯器會讀取 Bridging Headers 中的標頭檔案,然後生成一個龐大的 Swift 檔案,檔案內容是這些標頭檔案內的 API 的 Swift 版本。然後編譯器會在編譯每一個 Swift 檔案時,都要編譯一遍這個龐大的 Swift 檔案的內容。
有了預編譯 Bridging Headers 以後,編譯器會在預編譯階段把 Bridging Headers 編譯一次,然後插入到每個 Swift 檔案中,這樣就大大提高了編譯速度。
蘋果宣稱 Xcode 9 和 Swift 4 對於 Swift 和 Objective-C 混合編譯的速度提高了 40%
- Indexing 可以在編譯的同時進行
用 Swift 開發專案時,近幾個版本的 Xcode 進行 Indexing 的速度慢的令人髮指。Xcode 9 和 Swift 4 在這方面做了優化,可以在編譯的同時進行 Indexing,一般編譯結束後 Indexing 也會同時完成。
- COW Existential Containers
Swift 中有個東西叫 Existential Containers,它用來儲存未知型別的值,它的內部是一個 Inline value buffer,如果 Inline value buffer 中的值佔用空間很大時,這個值會被分配在堆上,然而在堆上分配記憶體是一個效能比較慢的操作。
Swift 4 中為了優化效能引入了 COW Existential Containers,這裡的 COW 就代表 "Copy-On-Write",當存在多個相同的值時,他們會共用 buffer 上的空間,直到某個值被修改時,這個被修改的值才會被拷貝一份並分配記憶體空間
- 移除未呼叫的協議實現
struct Date {
private let secondsSinceReferenceDate: Double
}
extension Date: Equatable {
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
}
extension Date: Comparable {
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
複製程式碼
看上面例子,Date 實現了 Equatable 和 Comparable 協議。編譯時如果編譯器發現沒有任何地方呼叫了對 Date 進行大小比較的方法,編譯器會移除 Comparable 協議的實現,來達到減小包大小的目的。
- 減少隱式 @objc 自動推斷
在專案中想把 Swift 寫的 API 暴露給 Objective-C 呼叫,需要增加 @objc。在 Swift 3 中,編譯器會在很多地方為我們隱式的加上 @objc,例如當一個類繼承於 NSObject,那麼這個類的所有方法都會被隱式的加上 @objc。
class MyClass: NSObject {
func print() { ... } // 包含隱式的 @objc
func show() { ... } // 包含隱式的 @objc
}
複製程式碼
這樣很多並不需要暴露給 Objective-C 也被加上了 @objc。大量 @objc 會導致二進位制檔案大小的增加。
在 Swift 4 中,隱式 @objc 自動推斷只會發生在很少的當必須要使用 @objc 的情況,比如:
複寫父類的 Objective-C 方法 符合一個 Objective-C 的協議 其它大多數地方必須手工顯示的加上 @objc。
減少了隱式 @objc 自動推斷後,Apple Music app 的包大小減少了 5.7%。
5、 Exclusive Access to Memory
在遍歷一個 Collection 的時候可以去修改每一個元素的值,但是在遍歷時如果去新增或刪除一個元素就可能會引起 Crash。
例如為 MutableCollection 擴充套件一個 modifyEach 方法來修改每個元素的值,程式碼如下:
extension MutableCollection {
mutating func modifyEach(_ body: (inout Element) -> ()) {
for index in self.indices {
body(&self[index])
}
}
}
複製程式碼
假如在呼叫 modifyEach 時去刪除元素:
var numbers = [1, 2, 3]
numbers.modifyEach { element in
element *= 2
numbers.removeAll()
}
複製程式碼
就會在執行時 Crash。Swift 4 中引入了 Exclusive Access to Memory,使得這個錯誤可以在編譯時被檢查出來。