Swift4.2光速入门٩(๑>◡<๑)۶

9月25日,Xcode10发布了!支持swift3和swift4,而如今swift也日趋成熟,使用swift开发的项目也越来越多。
ʕ•̀ω•́ʔ✧是时候学swift了,一起来光速入门吧~

版本兼容性

swift采用了现代编程模式,以避免大量常见的低级编程错误:

  • 变量永远会在被使用前完成初始化
  • 对数组的索引操作会自动检查是否出现越界错误
  • 整型数值会自动检查是否溢出
  • 可选值确保nil值被正确处理
  • 内存被自动管理
  • 错误处理允许从异常故障控制恢复

当swift4.2编译器编译swift3的代码时,swift 4 大部分新功能是可用的,只有如下功能swift 4 独享:

  • 子字符串的操作返回的实例是Substring类型而不是String
  • 在较少的地方会隐性增加@objc属性
  • 同一文件中类型的拓展可以访问该类型中的私有成员

概述

首先,从hello world开始
用swift实现可太tm简单了
用xcode创建一个swift blank项目,在playground中输入一句话

1
print("hello world!")

全局作用域中的代码会自动作为程序的入口,因此;不需要main()函数,同样的,你也不需要写;了。

简单值

lei声明常量
var声明变量

1
2
3
var num = 1
num = 2
let number = 3

swift不需要在声明时声明类型,编译器会根据你创建的变量或者常量的初始值进行类型推断。上面的例子中,num是个整数,因为它的初始值是一个整数。

如果没有初始值,你又想声明类型,你只要在变量后声明类型,用:分割。

1
let num3:Double = 1

swift有一种更简单的方式让值转为字符串:把值写在()内,在括号之前再加一个\

1
2
3
4
5
6
7
let str = "girlfriend"
let sum1 = 1
let sum2 = 0

print("I used to have \(sum1) \(str)")
print("and now I have \(sum1*sum2) \(str)")
print("pretty girl check your wechat number and send it to me pls:)")

对于占用多行的字符串可以使用三个引号"""每行的来头缩进要和右引号的缩进相同

1
2
3
4
5
let string = """
hello~
this my wechat number
"""
print(string)

使用[]来创建数组和字典,并且使用下标或者键来访问它们的元素。其中最后一个元素后面允许有逗号

1
2
3
4
5
6
7
8
var arr = ["my","name","is","hades"]
arr[3] = "whz"

var dic = [
"key1":"value1",
"key2":"valeu2"
]
dic["key2"] = "value change"

使用初始化语法来创建一个空数组或者字典

1
2
let emptyArr = [String]()
let emptyDic = [String:String]()

如果类型能被推断,则可以更简单,就像你给一个变量赋值一样(但注意,不能推断类型的时候不能这么使用,即不能用这种方式声明一个空数组或者字典以供后面使用)

1
2
arr = []
dic = [:]

控制流

使用ifswitch来创建条件语句,使用for-in,while,以及repeat-while来创建循环语句。包裹条件或者循环变量的括号是可选的。但语句体的大括号是必不可缺的。

1
2
3
4
5
6
7
8
9
10
11
12
let numArr = [123,13,4,13,43,65]
var totalNum = 0
for tempNum in numArr {
if tempNum < 50 {
totalNum -= 1
}
else{
totalNum += 1
}
print(totalNum)
}
print("end of totalnum = \(totalNum)")

if语句中,条件语句必须是布尔表达式,可以使用iflet来处理值缺失的情况。这些值由可选值来代表。可选值要么包含一个值,要么为nil表示值缺失。在值得类型后面跟随一个?则表示这个值是可选的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var exampleStr:String? = "hello"
print(exampleStr == nil)

var name:String? = "hades"
var greeting = "hello~"
//name = "xiye"
//name = nil
if let nameTemp = name { //如果类型转换成功,则将值赋值给nameTemp直接使用
greeting = "hello~ \(nameTemp)"
print("nameTemp = \(nameTemp) name = \(name!)")
print("( let nameTemp = name ) = true");
}
else{
print("name = nil ") //强制解析name 会报错 nameTemp 在这里是没法使用的 因为逻辑上只有是nil时才会走这部分代码,在swift中是什么值都没有
print("( let nameTemp = name ) = false");
}

如果可选值为nil,条件语句就为false,则大括号中的代码会被跳过,否则可选值将被解包,并赋值给let后的常量,这样代码中就可以使用这个值。

处理可选值得另一种方法是使用??操作符来提供默认值。如果缺少可选值,则使用默认值(如果有赋值,也不会使用提供的默认值,如果设置了可选类型,但后面被设置成nil,就会使用默认值)。

1
2
3
4
5
6
7
var nickName: String? = nil //"whz"
let fullName: String = "hades"
//nickName = "whz"
//nickName = nil
let informalGreeting = "hi~ \(nickName ?? fullName)"

print(informalGreeting)

oc中的nil和swift中的nil
Objective-C中的nil:表示缺少一个合法的对象,是指向不存在对象的指针,对结构体、枚举等类型不起作用(会返回NSNotFound)
Swift中的nil:表示任意类型的值缺失,是一个确定的值,要么是该类型的一个值要么什么都没有(即为nil)

‘Switch’语句支持任何类型的数据以及各种各样的比较操作——不仅仅局限于整数和测试相等。

1
2
3
4
5
6
7
8
9
10
11
var people = "ex"
switch people {
case "girl":
print("hey~ could you give me your wechat number?")
case "man","boy":
print("oh...next pls")
case let x where x.hasSuffix("x")://hasSuffix以指定后缀结束,hasPrefix以指定前缀开始
print("if time can come back...")
default:
print("hello stranger.")
}

在swift中,使用switch语句,在执行完case后不需要显示的添加break,程序会自动执行完一个case跳出。
你可以为字典中的键值对起一组名字,并用for in语句来遍历字典。由于字典是无序的,所以它的遍历也是无序的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let someNumbers = [
"key1":[2,4,1,5,31],
"key2":[2,43,2,5,1,53],
"key3":[2345,13,1,3,13,65]
]
var maxNum = 0;
var maxName:String = ""

for (keyInSomeNumbers,valueInSomeNumbers) in someNumbers {
for values in valueInSomeNumbers {
if values > maxNum {
maxNum = values
maxName = "\(keyInSomeNumbers)"
}
}
}
print("\(maxName) \(maxNum)")

使用while来循环执行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
var n = 0
while n < 100 {
n = n*n + 1
}
print(n)

var m = 0
repeat{
m = m + 1
print("m = \(m)")
}while m < 10

print(m)

可以使用..<来限定索引范围,并在循环中遍历该索引范围

1
2
3
4
5
var total = 0
for i in 1..<4 {
total += i
}
print(total)

函数和闭包

使用func来声明一个函数。使用函数名和参数名来调用函数。使用->来指定函数返回值类型。

1
2
3
4
func greet (person:String,day:String)-> String{
return "Hello \(person),today is \(day)"
}
print(greet(person: "whz", day: "thuesday"))

默认情况下,函数会使用它们的参数名称作为参数标签,在参数名称前可以自定义参数标签,或使用_来表示来不使用参数标签。

1
2
3
4
func greet(_ preson:String,on day:String)-> String{
return "hello \(preson),today is \(day)"
}
print(greet("hades", on: "friday"))

使用元组来生成复合值,例如使用元组来让一个函数返回多个值。该元组的元素可以通过名称或者数字来获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func function(scores:[Int]) -> (min:Int,max:Int,sum:Int){
var min = scores[0]
var max = scores[0]
var sum = 0

for score in scores {
if score > max {
max = score
}
else{
min = score
}
sum = sum + score
}
return (min,max,sum)
}
let result = function(scores: [1,31,413,356,1,376,463])
print(result.max)
print(result)
print(result.2)

函数间可相互嵌套。被嵌套的函数可以访问外部函数中声明的变量,你可以使用嵌套函数来重构一个过于冗长或者复杂的函数。

1
2
3
4
5
6
7
8
9
func returnFifteen()-> Int {
var x = 10
func add(){
x += 5
}
add()
return x
}
print(returnFifteen())

函数是一个类型。意味着函数可以作为其他函数的返回值。

1
2
3
4
5
6
7
8
func returnOneFunction() -> ((Int) -> Int){
func returnOneInt(number:Int) -> Int{
return number + 1
}
return returnOneInt
}
var anotherFunction = returnOneFunction()
print(anotherFunction(3))

一个函数也可以作为参数传入另一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func hasAnyMatches(list:[Int],condition:(Int)->Bool) -> Bool {
for item in list {
if condition(item) {
print(item)
return true
}
}
return false
}
func lessThanTen(number: Int) -> Bool {
return number < 10
}
var numbers = [20,19,7,12]
hasAnyMatches(list: numbers, condition: lessThanTen)

函数是一种特殊的闭包:它是可以在之后被调用的一段代码。在闭包里的代码可以访问到闭包作用域范围内的变量和函数,即使闭包是在不同的作用域被执行
你可以使用{}来创建一个匿名闭包。使用in将参数和返回值类型与闭包函数体分离。

1
2
3
4
5
6
7
8
var numbers = [20,19,7,12]
hasAnyMatches(list: numbers, condition: lessThanTen)

numbers.map({ (number: Int) -> Int in
let result = 3 * number
print(111)
return result
})

写出更简洁的闭包有很多种方法。当我们已知一个闭包的类型,比如作为一个代理的回调,你可以忽略参数、返回值,甚至两个都忽略。单个语句闭包会把它语句的值当做结果返回。

1
2
3
4
5
var temp = [3,41,5,1,51]
let temp1 = temp.map{
num in
num + 1
}

swift自动为闭包提供参数名缩写功能,可以直接通过$0$1等来表示闭包中的第一个第二个参数,并且对应的参数类型会根据函数类型来进行判断,再次简写

1
2
3
4
print(temp1)
let temp2 = temp.map{
$0 + 1
}

对象和类

通过在类名前加class关键字的方法来创建一个类。类中的属性声明和变量的属性声明相同,唯一不同的是,类的属性声明上下文是类。类似的,方法和函数也是同样方式来声明。

1
2
3
4
5
6
class Shape {
var numberOfSides = 0
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides"
}
}

通过在类名称后面插入括号来创建类的实例。使用.语法的方式来访问实例中的属性和方法。

1
2
3
4
var shape = Shape()
shape.numberOfSides = 9
var shapeDescription = shape.simpleDescription()
print(shapeDescription)

当一个类的属性没有初始值,你就需要使用init来创建一个构造器。

1
2
3
4
5
6
7
8
9
10
11
class NamedShape {
var numberOfSides:Int = 0
var name: String

init(name: String) {
self.name = name
}
func description() -> String {
return "A shape with \(numberOfSides) sides"
}
}

这里的self被用来区分name属性和构造器的name参数。当你创建类实例时,会像传入参数一样,给类传入构造器的参数。每个属性都要指定一个值— 无论在声明中还是在构造器里。
如果你需要在对象被释放前执行一些清理的行为,可以使用deinit来创建一个折构器。

子类会在其类名后面加上父类的名字,并用冒号分割。创建类的时候,并不需要一个标准根类,因此你可以根据自己的需求,添加或省略父类的声明。

子类如果需要重写父类的方法,则需要使用override来标记—不使用override关键字来标记会导致编译器报错。编译器同样也会检查override标记的方法是否存在父类当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Square: NamedShape {
var sideLength: Double
init(sideLength: Double,name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 5
}

func area() -> Double {
return sideLength * sideLength
}

override func description() -> String {
return "A square with sides of length \(sideLength)"
}
}
let test = Square(sideLength: 4.1, name: "test square")

print(test.area())
print(test.description())

除了存储简单的属性,属性还可以拥有getter和setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class EquilaterTriangle: NamedShape {
var sideLength: Double = 0.0

init(sideLength: Double,name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 3
}

var perimeter: Double{
get{
return 3.0 * sideLength
}
set{
sideLength = newValue / 3.0
}
}

override func description() -> String {
return "this is a triangle with sides of length \(sideLength)"
}
}

var triangle = EquilaterTriangle(sideLength: 4.3, name: "a triangle")
print(triangle.perimeter)
triangle.perimeter = 9
print(triangle.sideLength)

perimeter的setter中,新值被隐式的命名为newValue。你可以在set的括号后面,显式的提供一个名字。

注意EquilateralTriangle类的初始化有三个不同的步骤:
- 1 设定子类的声明的属性值
- 2 调用父类的构造器
- 3 改变父类定义的属性值。其他的工作如调用方法,getter或者setter都可以在这个时候完成。


如果你不需要计算属性,但是仍需要在设置一个新值之前或之后来执行代码,则可以使用willSetdidSet。代码会在属性值发生改变时被执行,在构造器中属性值发生改变的情况除外。例如,下面的类确保三角形的边长始终和正方形的边长相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TriangleAndSquare {
var triangle: EquilaterTriangle {
willSet {
square.sideLength = newValue.sideLength
}
}
var square:Square {
willSet {
triangle.sideLength = newValue.sideLength
}
}
init(size:Double,name:String) {
square = Square(sideLength: size, name: name)
triangle = EquilaterTriangle(sideLength: size, name: name)
}
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "test")
print(triangleAndSquare.square.sideLength)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 40, name: "larger square")
print(triangleAndSquare.triangle.sideLength)

在处理可选值时,你可以在如方法、属性和下标脚本等操作之前使用?。如果?前的值是nil,则?后面的所有内容都会被忽略,且整个表达式为nil。否则,可选项的值将被展开,然后?后面的代码会根据展开的值来执行。在这两种情况下,整个表达式的值是一个可选值。

1
2
let optionalSquare: Square? = Square(sideLength: 2.3, name: "optional square")
let sideLength = optionalSquare?.sideLength


枚举和结构体

使用enum来创建枚举。像类和其他所有命名类型一样,枚举也包含方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Rank: Int {
case ace = 1
case two,three,four,five,six,seven,eight,nine,ten
case jack,queen,king
func simpleDescription() -> String {
switch self {
case .ace:
return "ace"
case .jack:
return "jack"
case .queen:
return "queen"
case .king:
return "king"
default:
return String(self.rawValue)
}
}
}
let ace = Rank.ace
let aceRawValue = ace.rawValue

默认情况下,swift 从 0 开始给原始值赋值,而后依次递增 1,你也可以通过指定一个特定值来改变这一行行为。在上边的例子中,Ace的原始值被显示赋值为1,其余的原始值会按照顺序来赋值。同样的,你也可以使用字符串或者浮点数来作为枚举的原始值。使用rawValue属性来访问一个枚举成员的原始值。
使用init?(rawValue:)初始化构造器来创建一个拥有原始值得枚举实例。如果在Rank中有与该原始值相匹配的枚举实例则返回该实例,没有则返回nil

1
2
3
if let convertedRank = Rank(rawValue: 3) {
let threeDescription = convertedRank.simpleDescription()
}

枚举成员的值是实际值,而不是原始值的另外一种写法。事实上,如果没有一个有意义的原始值,你也没必要再提供一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Suit {
case spades,hearts,diamonds,clubs
func simpleDescription() -> String {
switch self {
case .spades:
return "spades"
case .hearts:
return "hearts"
case .diamonds:
return "diamonds"
case .clubs:
return "clubs"
}
}
}
let hearts = Suit.hearts
let haartsDescriotion = hearts.simpleDescription()

注意在上面例子中用了两种方法来调用hearts成员:给hearts指定一个常量时,枚举成员Suit.hearts需要全名调用,因为常量没有显示指定类型。在switch语句中,枚举成员可以通过缩写的方式.hearts被调用,因为self的值已经确定是Suit类型。在值得类型已经被明确的情况下可以使用缩写。


如果一个枚举成员拥有原始值,那么这些值在声明时就会被确定,也就是说,每一个不同枚举实例的枚举成员总有一个相同的原始值。另外一种选择是为枚举成员设定关联值—这些值会在实例被创建时确定,这样它们在每一个实例中的原始值就不一样了。你可以将关联值想象成与枚举实例存储属性一样。例如,考虑在服务器上请求日出和日落的情况。服务器要么返回请求信息,要么返回错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
enum ServerResponse {
case result(String,String)
case failure(String)
}
let success = ServerResponse.result("6:00 am","8:00 pm")
let failure = ServerResponse.failure("fail")

switch success {
case let .result(sunrise,sunset):
print(" sunrise is at \(sunrise) and sunset is at\(sunset)")
case let .failure(message):
print("fail - \(message)")
}

注意日出日落时间是如何从ServerResponse值中进行提取,并与 switch cases 相匹配的。

使用struct来创建一个结构体。结构体提供了很多和类相似的行为,包括方法和构造器。类和结构体最重要的区别就是结构体在传递的时候会拷贝自身,而类则会传递引用。

1
2
3
4
5
6
7
8
9
struct Card {
var rank: Rank
var suit: Suit
func simpleDescription() -> String {
return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
}
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()


协议和扩展

使用protocol来声明一个协议。
mutating关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量

1
2
3
4
protocol ExampleProtocol {
var simpleDescription: String{ get }
mutating func adjust()
}

类、枚举和结构都可以遵循协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SimpleClass: ExampleProtocol {
var simpleDescription: String = "a simple class"
var anotherPriperty: Int = 69105
func adjust() {
simpleDescription += "now 100% adjusted"
}
}

var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription

struct simpleStructure: ExampleProtocol {
var simpleDescription: String = "a simple sturcture"
mutating func adjust() {
simpleDescription += "(adjusted)"
}
}

var b = simpleStructure()
b.adjust()
let bDescription = b.simpleDescription

注意声明SimpleStructure时使用了关键字mutating来标记一个可以修改结构体的方法。而声明SimpleClass时,则不需要标记任何方法,因为一个类中的方法总是可以修改类属性的。

使用extension可以为现有的类型添加功能,例如新方法和计算属性。你可以使用拓展将协议一致性添加到其他地方声明的类型,甚至是你从其他库或框架导入的类型。

1
2
3
4
5
6
7
8
9
extension Int: ExampleProtocol {
var simpleDescription: String {
return "the number \(self)"
}
mutating func adjust() {
self += 42
}
}
print(7.simpleDescription)

你可以像使用其他命名类型一样来使用协议—例如,创建一个具有不同类型但是都遵守某一个协议对的对象集合。当你处理的类型为协议的值时,协议外定义的方法是不可用的。

1
2
3
4
let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
//下面这句会报错
print(protocolValue.anotherProperty)

尽管变量protocolValue在运行时类型为SimpleClass,但编译器依旧会把它的类型当做ExampleProtocol。这也就意味着,你不能随意访问在协议外的方法或属性。


错误处理

你可以使用任何遵循Error协议的类型来表示错误。

1
2
3
4
5
enum PrintError: Error {
case outOfPaper
case noToner
case onFire
}

使用throw跑出异常并且用throws来标记一个可以抛出异常的函数。如果你在一个函数中抛出异常,这个函数会立即返回并且调用处理函数错误的代码。

1
2
3
4
5
6
func send(job: Int, toPrinter printerName: String) throws -> String {
if printerName == "Never Has Toner" {
throw PrintError.noToner
}
return "Job sent"
}

这里有几种方法可以处理异常。一种是使用do-catch。在do代码块里,你可以是用try在抛出的异常的函数前标记。在catch代码块里边,如果你不给定其他名字的话,错误会自动赋予名字为error

1
2
3
4
5
6
do {
let printerResponse = try send(job: 1040, toPrinter: "Never Has Toner")
print(printerResponse)
} catch{
print(error)
}

你可以提供多个catch代码块来处理特定的错误。你可以在catch后面一个一个模式,就像switch语句里面的case一样。

1
2
3
4
5
6
7
8
9
10
 do {
let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
print(printerResponse)
} catch PrintError.onFire {
print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrintError {
print("Printer error: \(printerError)")
} catch {
print(error)
}

另外一种处理错误的方法是用try?去转换结果为可选项。如果这个函数抛出了异常,那么这个错误会被忽略并且结果为nil。否则,结果是一个包含了函数返回值的和选项。

1
2
let printerSuccess = try? send(job: 1883, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

使用defer来写在函数返回后也会被执行的代码块。无论这个函数是否抛出异常,这个代码都会被执行。即使他们需要在不同的时间段执行,你仍可以使用defer来简化代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fridgeIsOpen = false
let fridgeContent = ["milk","eggs","leftovers"]

func fridgeContains(_ food: String) -> Bool {
fridgeIsOpen = true
defer {
fridgeIsOpen = false
}
let result = fridgeContent.contains(food)
return result
}

fridgeContains("milk")
print(fridgeIsOpen)

defer常用于数据库操作中的打开关闭或者我们要执行某些必要操作流程时候
defer会在该当前声明的作用域结束的时候执行
优先级: 局部优先、同级自下而上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func firstProcesses(_ isOpen: Bool) {

//作用域1 整个函数作用域
defer{
print("推迟操作🐢")
}

print("😳")

if isOpen == true {
//作用域2 if的作用域
defer{
print("推迟操作🐌")
}
print("😁")
}
print(111)
}
firstProcesses(true)


泛型

把名字写在尖括号里来创建一个泛型方法或者类型。

1
2
3
4
5
6
7
8
func makeArray<Item>(repeating item: Item,numberOfTimes: Int) -> [Item] {
var result = [Item]()
for _ in 0..<numberOfTimes {
result.append(item)
}
return result
}
makeArray(repeating: "knock", numberOfTimes: 4)

你可以从函数的方法中,同时还有类,枚举以及结构体中创建泛型。

1
2
3
4
5
6
enum OptionalValue<Wrapped> {
case none
case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)

在类型名称后紧接where来明确一系列需求—例如,需求类型实现一个协议,要求两个类型必须相同,或者要求类必须继承来自特定的父亲。

1
2
3
4
5
6
7
8
9
10
11
func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T,_ rhs: U) -> Bool where T.Iterator.Element: Equatable, T.Iterator.Element == U.Iterator.Element {
for lhsItem in lhs {
for rhsItem in rhs {
if lhsItem == rhsItem {
return true
}
}
}
return false
}
anyCommonElements([1,2,3], [3])

以上就是Swift4.2的概述,看完这些写一个简单的app已经不是难事了~
但简单的app可不是我们的目标,还有很多语法上的细节需要深入研究学习,在后面的文章我会逐步更新语法上的细节ʕु•̫͡•ʔु ✧