写在前面
这是我学习Swift3.0的学习笔记系列的第二篇文章,本篇同第一篇一样,将主要介绍Swift的一些基础语法,包括控制流、闭包等内容。由于本人水平有限,接触Swift语言的时间也只有一个月左右,所以文中有些描述的不准确,甚至是错误的观点,还望各路大神能够不吝赐教,帮我指出。我原来一直使用Objective-C语言进行iOS的开发,出于对新知识的好奇与渴望,才开始自学Swift,但刚开始的时候,遇到了很多问题,网上检索资料,发现目前Swift3.0的学习资料比较少,大部分都是之前版本的,由于3.0较之前版本在某些部分的改动还较大,感觉缺少资料的学习还是比较吃力的,如果你也像我一样,是刚从OC转向Swift的,并且一开始就接触的是Swift3.0,那么我的学习笔记也许会对你有些许帮助。另外,我有个和大家一起讨论Swift3.0学习的技术交流群185826825,欢迎大家来与我们共同学习!我的本系列其它文章:
Swift3.0学习笔记(一)
-
控制流
-
if else
众所周知,Swift没有了麻烦的;
作为句尾结束符,那么对于简单的条件判断我们干脆也就不用()
了,现在,你可以像这样写一个if else
:
let a = 3, b = 2
if a > b {
print("a>b的条件为真")
} else {
print("a>b的条件为假")
}
如果是比较复杂的条件,你仍需要加上()
,来说明条件都包括什么:
let a = 3, b = 2, c = 4, d = 1
let condition_1 = a > b
let condition_2 = c < d
if (condition_1 || condition_2) {
print("条件1为真或者条件2为真")
}
我们在开发中,经常会判断一个对象是否为nil,在Swift中我们可以这样做:
let image: UIImage? = UIImage.init(named: "abc")
if let image = image {
print("我们拿到image的内容了")
} else {
print("我们拿到的image为nil")
}
此时如果运行后,在MainBundle中没有abc.png这个文件的话,控制台则会输出我们拿到的image为nil
。在条件判断时,我们使用了Swift中的if let
语法,它的含义是,我们重新定义一个常量,使用一个可选类型的值给这个常量进行赋值,如果赋值后,这个可选类型的值是nil的话,也就相当于我们在使用if image == nil
去进行判断,需要注意的是,新定义的这个image
只能在if
的{}
内访问。
-
guard else
同刚才那个案例一样,我们经常会做一些对象是否为nil的判断,事实上,我们只会在对象不为nil的时候才会去做一些逻辑,而当对象真的是nil的时候,我们也许什么也不做,如果能够对我们不期望的结果早做检查,会使我们的代码更加贴近我们想要实现的目的,并且让它看起来更容易理解。guard else
就是来做这件事的,从字面的意思来看,保证,除此之外
确实很容易理解,那么就让我们看看这个语法该怎么使用吧,比如我有一个用于接收消息的函数,需求是只对消息内容中含有'abc'的内容作处理,代码如下:
func didReceivedMessage(msg: String) {
guard msg.contains("abc") else { return }
print("消息中包含字符串abc,我需要做处理了")
}
这里需要注意,在else中一定需要加上return
,如果是在循环内或switch中可以加break
,用以结束这个函数,不加的话编译不能通过。
同if let
语法类似的,guard else
也有guard let else
的用法。
-
switch
和OC的switch相比,swift的switch语句强大了很多,用法也更灵活。首先,switch不再只能检查整型类型的值,它还能检查浮点型、布尔类型、字符串、元组等,同时,每个case
默认都会有一个break
,因此你不再额外需要写break
关键字了;每个case
下的代码块也不需要{}
来进行整理,只需要将代码写在两个case
之间就好;每个switch语句必须涵盖所有的条件,不能够像OC那样只写一两个你关注的条件,剩余你不想关注的条件可以使用default: break
来补充;default
关键字不能出现在case的前面。
switch中还增加了一个有意思的用法,值绑定。所谓值绑定就是你可以在case分⽀里将匹配的值绑定到⼀个临时的常量或变量,这个常量或变量在该case分⽀⾥就可以被引⽤了:
switch "abc123" {
case let x where x.hasSuffix("123"):
print("x = \(x)")
default: break
}
控制台输出abc123
,代码中的let x
就是将匹配到的值赋值给常量x,where
就是对被匹配的对象进行筛选,使得满足条件的被匹配对象才能进入case。
switch用来匹配元组,就像这样:
let point = (2, 0)
switch point {
case (let x, 0):
print("点在x轴上,横坐标为\(x)")
case (0, let y):
print("点在y轴上,纵坐标为 \(y)")
case let (x, y): //匹配所有
print("点不在坐标轴上,该点坐标是(\(x), \(y))")
}
-
循环
循环语句和OC的循环类似,大概有for
循环和while
循环几种,对于for
循环来说,swift取消了C语言风格的for (int i = 0; i < 10; i++)
语法,取而代之的是这样for i in 0..<10
,如果你不需要i作为循环次数的索引,你可以这样写for _ in 0..<10
,使用一个下划线_
来省略。你也可以使用for in
对某个数组进行遍历,像这样:
let arr = Array.init(repeating: "abc", count: 3)
for str in arr {
print(str)
}
控制台输出:
abc
abc
abc
-
数组
在Swift中,取消了NS
的前缀,我们可以直接定义一个Array类型的变量或常量,但前提是我必须指定存放在数组中元素的类型,对于未知的类型我们可以使用Any来表示,像这样:
let array: Array = [("" as Any)]
我们声明了一个不可变数组,其中有一个元素,是一个空字符串,由于类型安全,我们需要将这个字符串类型的元素转换为Any
类型。
如果改为var
类型,则该数组为可变数组,可以使用添加、删除等函数对该数组进行修改:
var array: Array = [""]
array.append("abc")
array.remove(at: 0)
-
字典
同数组一样,声明时需要指定字典的key-value
的数据类型,比如:
var dict = Dictionary()
dict["abc"] = 123
-
类
在Swift中,没有了.h
和.m
文件,取而代之的是一个.swift
文件,在.swift
文件中使用public
和private
关键字来区别函数和属性的公开和私有,对于没有添加这个关键字的函数和属性,在swift中默认是公开的。对于继承,swift仍延续了使用:
来表示继承关系,而之后,我们可以方便地使用","
对要继承的父类进行分割,在后面添加上我们要遵守的协议,这看起来就像这样:
-
懒加载
我们在swift中可以在声明属性的时候,用懒加载的方式进行声明,这样类似OC,可以在只有访问这个属性的时候才在内存开辟空间。比如这样:
lazy private var placeHolderLabel: UILabel = {
let tempLabel = UILabel()
return tempLabel
}()
-
属性监听
我们可以使用Swift提供给我们的属性观察器,获取到属性被赋值前和被赋值后的时机,像这样:
private var x: Int = 0 {
willSet {
print("当前值 = \(x)")
print("将被赋予的新值 = \(newValue)")
}
didSet {
print("当前值 = \(x)")
print("被赋新值之前的旧值 = \(oldValue)")
}
}
其中在willSet
方法中提供了一个值newValue
,用来访问这个属性要被赋的新值,在didSet
方法中提供了一个oldValue
用来访问被赋值之前的旧值,这这两个方法中我们都可以使用属性本身来访问当前的值。现在我们让x = 1
,可以看到控制台输出:
当前值 = 0
将被赋予的新值 = 1
当前值 = 1
被赋新值之前的旧值 = 0
-
函数
swift中,函数使用关键字func
作为函数的开始,紧接着后面是函数名,不同于OC的风格,swift不再将函数名和参数混在一起写,而是将所有的参数都使用()
括起来,之后使用一个->
符号来表示此函数的返回类型,在这个符号的后面写上返回类型,就像这样:
func login(userName: String, password: String) -> Bool {
if (userName == "abc" && password == "123") {
return true
}
return false
}
我们都知道在OC中有类方法和实例方法的区别,在Swift中,如果在func
关键字前增加class
关键字,则该方法为类方法,否则默认都是实例方法。可以看到,刚才我们写的那个login
的函数就是一个实例方法,我们调用这个函数的方式同OC一样,首先要有这个函数所在类的实例,然后使用实例对象去调用这个方法,在Swift中,不再像OC一样使用方括号+空格
的方式去向对象发消息,而是直接使用点语法
,假设这个函数在某个类中,我们这样调用该函数:
let isSuccess = self.login(userName: "abc", password: "123")
在这个函数中,userName
和password
是函数的形参
,是函数的内部参数名
,那么对应的,什么是函数的实参
和外部参数名
呢?我们在调用这个函数时,在输入参数的地方,我们输入的参数就是这个函数的实参
,对于外部参数名
即是指我们在函数调用时所看到的参数名,其写法是在内部参数名
的前面使用一个空格
来分割,并在前面写上参数名,比如func inputUserName(用户名 userName: String) -> Void
,这样我们在调用这个函数时就是这个样子的:
self.inputUserName(用户名: "abc")
如果我们没有刻意去写外部参数名
,那么在调用函数时会默认该函数的外部参数名
同内部参数名
一致,那么如果我们不希望在调用时看到这个函数的外部参数名
该怎么办呢?像刚才那个登录的函数,我们可以使用这样的方式对外部参数名进行省略:
//注意:函数的返回值如果是空的话,可以写(),或者Void
//或者直接省略这些: "-> ()",即参数后面直接接函数体
func login(_ userName: String, _ password: String) -> () {
if (userName == "abc" && password == "123") {
return true
}
return false
}
//函数调用
self.login("abc", "123")
另外,我们还可以给函数的参数一个默认值,比如:
func showMsg(msg: String? = "hello") {
print(msg)
}
self.showMessage()
控制台输出Optional("hello")
-
闭包
闭包其实类似OC中的Block,是一段代码块,可以像函数一样实现某些功能,也可以作为参数进行传递,可以看成是匿名函数。
闭包的语法:
{(参数名) -> 返回类型 in
代码块
}
//对比OC的写法
返回类型(^bolck名)(参数类型及参数名) {
代码块
}
在网上关于Swift的闭包有比较多的介绍,在此向大家推荐一个网址来学习:Swift教程-闭包
在此举一个简单的例子,简单封装一下网络请求方式:
class func get(url: String, parameters: Dictionary? = nil,
completionHandler: @escaping (_ response: DataResponse) -> ()) {
Alamofire.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: nil)
.responseData { (response) in
completionHandler(response)
}
}
细心的你会发现在completionHandler
这个闭包中有个@escaping
关键字,字面意思是逃逸,那么什么是逃逸呢?又是否有与之对应的类似不可逃逸的关键字呢?
-
逃逸闭包
当我们在进行网络请求时,由于是异步操作,所以在函数返回时,闭包中的内容可能还没有被触发,所以要求闭包的生命周期能够长于函数本身,也就是让闭包能够逃出函数存在,这就是逃逸闭包,在函数声明时,在闭包前使用@escaping
关键字。
-
非逃逸闭包
对于某些函数,例如Swift数组中的map函数,其作用是将给定数组复制出来一份副本,并根据你写的map规则将此副本映射成一个新的数组,对于这样执行同步操作的函数,在函数执行完成之后,就不再需要此闭包继续存在了,可以使用非逃逸闭包
,其关键字是@noescape
。
let arr = ["a", "b"]
let newArr = arr.map { (originalValue) -> String in
return "字母" + originalValue
}
print(newArr)
控制台输出["字母a", "字母b"]
。
-
参数名缩写
Swift为闭包提供了参数名的缩写功能,我们可以直接使用$0, $1, $2
来顺序访问闭包内的参数,上面那个例子我们就可以简写成这样:
let arr = ["a", "b"]
let newArr = arr.map({ "字母" + $0 })
-
构造器
所谓构造器,就是我们可以通过这个构造器来构造一个类的实例。在OC中,我们使用init
函数来对类进行实例化操作,在Swift中,我们有两种构造方式,指定构造器和遍历构造器。关于构造器的介绍,我引用了Draveness的文章Swift 类构造器的使用,文章介绍的很详细。
-
指定构造器designated initialize
指定构造器是类的主要构造器,要在指定构造器中初始化所有的属性,并且要在调用父类合适的指定构造器。每个类应该只有少量的指定构造器,大多数类只有一个指定构造器,我们使用 Swift 做 iOS 开发时就会用到很多UIKit框架类的指定构造器, 比如说:
init()
init(frame: CGRect)
init(style: UITableViewCellStyle, reuseIdentifier: String?)
当定义一个指定构造器的时候, 必须调用父类的某一个指定构造器:
init(imageName: String, prompt: String = "") {
super.init(style: .Default, reuseIdentifier: nil)
}
-
便利构造器convenience initialize
便利构造器是类的次要构造器, 你需要让便利构造器调用同一个类中的指定构造器, 并将这个指定构造器中的参数填上你想要的默认参数。
如果你的类不需要便利构造器的话,那么你就不必定义便利构造器, 便利构造器前面必须加上convenience
关键字。
在这里我们就不举例了, 但是我们要提一下便利构造器的语法:
convenience init(name: String?, user_id: String?) {
self.init()
self.name = name
self.user_id = user_id
}
其中调用self.init()
时,相当于调用类的designated initialize方法,即调用ClassName()
,该类中所有的变量都会被使用默认值进行初始化。
-
init规则
定义 init 方法必须遵循三条规则:
-
指定构造器必须调用它直接父类的指定构造器方法。
-
便利构造器必须调用同一个类中定义的其它初始化方法。
-
便利构造器在最后必须调用一个指定构造器。
只有指定构造器才可以调用父类的指定构造器,而便利构造器是不可以的,这也遵循了这的三条规则。
-
init 的继承和重载
跟 OC 不同,Swift 中的子类默认不会继承来自父类的所有构造器。这样可以防止错误的继承并使用父类的构造器生成错误的实例(可能导致子类中的属性没有被赋值而正确初始化)。与方法不同的一点是, 在重载构造器的时候, 你不需要添加
override
关键字。
虽然子类不会默认继承来自父类的构造器,但是我们也可以通过别的方法来自动继承来自父类的构造器,构造器的继承就遵循以下的规则:
-
如果子类没有定义任何的指定构造器,那么会默认继承所有来自父类的指定构造器。
-
如果子类提供了所有父类指定构造器的实现,不管是通过上面的规则继承过来的,还是通过自定义实现的,它将自动继承所有父类的便利构造器。
-
构造器总结
Swift 中构造器需要遵循的规则还是很多的,总结一下,有以下规则:
- 调用相关
- 指定构造器必须调用它直接父类的指定构造器方法。
- 便利构造器必须调用同一个类中定义的其它初始化方法。
- 便利构造器在最后必须调用一个指定构造器。
- 属性相关
- 指定构造器必须要确保所有被类中提到的属性在代理向上调用父类的指定构造器前被初始化,之后才能将其它构造任务代理给父类中的构造器。
- 指定构造器必须先向上代理调用父类中的构造器,然后才能为任意属性赋值。
- 便利构造器必须先代理调用同一个类中的其他构造器,然后再为属性赋值。
- 构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,self 不能被引用。
- 继承相关
- 如果子类没有定义任何的指定构造器,那么会默认继承所有来自父类的指定构造器。
- 如果子类提供了所有父类指定构造器的实现,不管是通过上一条规则继承过来的,还是通过自定义实现的,它将自动继承所有父类的便利构造器。
说了这么多,上个例子给大家感受下,比如我们自己写了一个基类MyViewController
继承自UIViewController
,然后在使用时,又写了一个类FirstViewController
继承自刚才写的MyViewController
,在MyViewController
中有一个函数,后面的子类在实例化时都会调用这个基类的函数,并且这个函数由MyViewController
的一个指定带参的构造器调用,最后我们希望在创建FirstViewController
的实例时使用FirstViewController()
这样的方式,写的代码就像这样:
//首先,基类代码
import UIKit
class MyViewController: UIViewController {
//声明一个属性,默认是公开的
var color: UIColor
//写一个指定初始化器,需要调用父类指定构造器
init(color: UIColor) {
//需要在调用父类的指定构造器前完成本类所有属性的初始化工作
self.color = color
//调用父类的其中一个指定初始化器
super.init(nibName: nil, bundle: nil)
self.initUI()
}
//要写本类的指定初始化器就要实现父类要求的指定构造器
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func initUI() {
let view = UIView.init(frame: CGRect.init(x: 100, y: 100, width: 100, height: 100))
self.view.addSubview(view)
view.backgroundColor = self.color
}
}
//基类的子类代码
import UIKit
class FirstViewController: MyViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
//覆载父类的指定构造器
override init(color: UIColor) {
super.init(color: color)
}
//新写一个便利构造器,需要调用本类的指定构造器
convenience init() {
self.init(color: .cyan)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
这样即完成了这两个类的初始化设置,比如这时候我们在其它的控制器中调用FirstViewController
时,只需要写FirstViewController()
即会调用MyViewController
中的initUI()
函数了。