插播一些理论知识
显示属性和引用属性(Attributes and properties)
注:在我翻译的iOS课程中Attributes翻译为显示属性,properties翻译为引用属性。一般的计算机英语中这两个词都翻译作属性,它们之间的区别很微妙,在我们的教程中所有能够在Xcode界面上看到,并且可以用鼠标或者输入值的方式去调整的属性,称为Attributes;所有在代码中需要用点句法引用的属性称为properties。所以我将Attributes翻译为显示属性,properties翻译为引用属性以示区别。
大多数界面建造器上的指示器中的显示属性都可以直接对应到所选对象的引用属性。例如,一个UILabel有以下显示属性:
它们直接对应UILabel的以下引用属性:
如你所见,它们的名字也许并不完全一致,比如Lines对应numberOfLines,但是你可以轻易的猜到它们的对应关系。
你可以从Xcode的Help菜单中打开Documentation and API Reference,从中可以找到关于UILabel的所有引用属性的说明。只需要在搜索框中输入“uilabel”,就可以快速的找到它了:
UILable的文档中并没有列出全部显示属性对应的引用属性。例如,在属性检查器中有一个分节,叫做“View”。这个分节中的显示属性来自UIView,UIView是UILabel的基类。所以你无法在UILabel的文档中找到它,也许你可以在文档的“Inherits From(继承于)”栏目中找到它。
对象和类(Object and classes)
是时候讲点新东西了。一直到现在,我把几乎所有东西都称为“对象”。然而,为了更好的理解面向对象编程,我们不得不讨论一下对象和类。
当你像下面这样做时:
class ChecklistItem: NSObject {
... }
你确实的定义了一个名叫ChecklistItem的类,并不是一个对象。一个对象是当你实例化一个类的时候产生的:
let item = ChecklistItem()
这个item变量现在包含一个ChecklistItem类的对象。你也可以这样说:item变量现在包含一个ChecklistItem类的实例。属于对象和实例的意思是一样的。
换句话说:ChecklistItem类的实例是变量item的类型。
Swift语言和iOS框架已经有了许多内建的类型,但是你也可以通过制作新的类来添加你自定义的类型。
让我们用一个例子来说明类和实例(对象)间的区别。
你和我都饿了,所以我们决定吃点冰淇淋(我除了程序以外的最爱)。冰淇淋是一个食物的类,我们马上就要去吃它了。
冰淇淋类是这个样子的:
class IceCream: NSObject {
var flavor: String //要什么味道的
var scoops: Int //要几勺
func eatIt() {
// code goes in here
}
}
你和我走到冰淇淋柜台前,想要俩个冰淇淋球:
// 这个是你的
let iceCreamForYou = IceCream()
iceCreamForYou.flavor = "Strawberry"
iceCreamForYou.scoops = 2
// 这个是我的
let iceCreamForMe = IceCream()
iceCreamForMe.flavor = "Pistachio"
iceCreamForMe.scoops = 3
yeah,我比你多一勺。
现在这个app有了两个冰淇淋的实例,一个是为你创建的,一个是为我创建的。这里只有一个类来描述我们吃的是冰淇淋,但是这里有两个不同的实例。你的是草莓味的,我的是开心果味的。
IceCream是一个模版,它生成的实例有两个属性flavor(味道)和scoops(数量:多少勺),还有一个名为eatIt()的方法。
任何由这个模版新生成的实例都包含这两个实例变量和这个方法,但是它们存在在计算机内存中的不同位置,因此,每个实例中的属性都可以有不同的值。
如果你对食物不感兴趣,你可以把类想象为一个建筑的设计蓝图。它设计了一个建筑,但是它本身并不是建筑。根据一个蓝图,可以建造无数的建筑,它们的颜色或者窗口等都各有不同。
继承
我们这里要讲的并不是继承一大笔财产。我们讨论的是类的继承,面向对象编程的主要原则之一。
继承是非常强大的一个功能,它允许在一个类以另一个类为基础建立。新建立的类拥有基类的所有数据和功能,然后它还可以添加属于自己的专业功能。
我们还是用刚才的冰淇淋类为例子讲解。它是基于NSObject建立的,这是一个iOS框架内建的类。你可以通过代码中声明类的那一行知道IceCream类是继承了NSObject类的:
class IceCream: NSObject {
这就是说IceCream类拥有NSObject类的全部内容,并且有一些自己的小特色,比如名为flavor和scoops的引用属性,以及eatIt()方法。
NSObject类是iOS框架中大多数类的基类。大多数你遇到的从某个类中创建的对象也是直接或者间接从NSObject中继承来的,你无法避开NSObject。
你也见过有些类是这样声明的:
class ChecklistViewController: UITableViewController
ChecklistViewController其实就是一个拥有自己特色功能的UITableViewController类。它可以做UITableViewController能做的一切,并且还具备你所给予的额外的数据和功能。
继承是非常便利的一个功能,因为UITableViewController已经可以为你完成许多工作了,它有table view,可以处理标准cell和静态cell,还可以滚动大量列表,所有的这些都可以继承到你自定义的类中。
UITableViewController自身是基于UIViewController建立的,UIViewController是基于UIResponder建立的,并且它们最终都是基于NSObject建立的,这就叫做继承树:
位于上方的每一个类的功能都比它下面的要强大。
NSObject仅提供一些所有对象都需要的基础功能,例如,它包含一个alloc方法用于回收内存空间,和基础的init方法。
UIViewController是所有视图控制器的基类。如果你要创建一个你自己的视图控制器,你就可以扩展UIViewController类。扩展的意思就是你通过继承的方式创建一个类。
你不会想要从0开始为自己的界面和视图写代码。如果这样做的话,估计一辈子也没有什么成就。
感谢在苹果公司工作的大量聪明人,给我们带来了这么多的基类。使得你可以简单的通过继承就可以获得继承树中全部类的功能,你知需要添加一点点自己需要的数据和功能就可以了。
如果你的界面上主要部分是一个列表,那么你就创建一个类,继承UITableViewController就可以了。同时你也拥有了UIViewController的全部内容,因为它俩也是继承关系。只是UITableViewController在处理列表方面更加专业。
所以比起自己从0开始写代码,干嘛不利用现有的现成的东西呢?君子善假于物。类的继承可以使你以极小的代价来重用现有的功能,它所节省的时间是无法想象的。想象一下自己从头开始写table view的代码吧,如果是这样,现在你还在第一节课爬行。
当程序员谈到继承的时候,同时还会有两个伴随的术语,就是父类(superclass)和子类(subclass)。
在上面的例子中,UITableViewController是ChecklistViewController的父类,相应的ChecklistViewController是UITableViewController的子类。
在Swift中,一个父类可以有许多子类,但是一个子类只能有一个父类。当然父类也有自己的父类。有许多类都是继承了UIViewController的,例如:
因为几乎所有的类都是从NSObject继承来的,它们构成了一个庞大的继承树。所以理解类的层级是非常重要的,这样你才能够选择合适的类作为自己定义的类的父类。
在之后的课程中你会看到,在程序中还会有其他很多类型的层级。
注意一下,在OC当中,所有你自定义的类都至少要继承NSObject类。但是在Swift中则不需要,在OC中你不能像下面这样声明一个类:
class IceCream {
...
}
这时IceCream根本没有一个基类(父类)。但是在Swift中这样做是可以的,但是假如你试图结合iOS框架和IceCream类一起使用就会出问题,因为iOS框架是用OC写的。
例如,你不能将上面的那个IceCream和NSCoder和NSCoding一起使用,除非IceCream是从NSObject中继承来的。所以即使你是使用Swift编程,你最好也在声明每一个类的时候,让它继承NSObject。
重写方法
继承一个类意味着你的新类可以使用父类的属性和方法。
如果你创建一个新的类Snack(小吃):
class Snack {
var flavor: String
fun eatIt() {
....
}
}
然后创建IceCream类,继承这个类:
class IceCream: Snack {
var scoops: Int
}
然后你就可以在你的代码任意一处做这些事情:
let iceCreamForMe = IceCream()
iceCreamForMe.flavor = "Chocolate"
iceCreamForMe.scoops = 1
iceCreamForMe.eatIt()
即使在IceCream中你并没有声明eatIt()和flavor实例变量,代码依然可以工作。因为IceCream是从Snack中继承来的,所以它自动获得了Snack中的方法和实例变量。
如果你吃冰淇淋的吃法非常独特,那么你也可以在IceCram中声明自己的eatIt()方法:
class IceCream: Snack {
var scoops: Int
override func eatIt() {
// code goes in here
}
}
现在你调用iceCreamForMe.eatIt()时,被调用的就是新版的eatIt()了。注意一下,当你声明父类中已经存在的方法时,Swift要求你必须在前面以关键字override注明。
还可以像下面这样重写方法:
class IceCream: Snack {
var scoops: Int
var isMelted: Bool
override func eatIt() {
if isMelted {
throwAway()
} else {
super.eatIt()
}
}
}
如果冰淇淋已经变质了,那么你就把它丢进垃圾桶,如果没变质的话,你就调用父类Snack的eatIt()方法吃了它。
就像self是引用当前对象一样,super关键字的意思是引用父类的对象。这就是为什么你在代码的很多地方都能看到这个关键字,它的作用是调用父类的对象来完成你要的功能。
使用方法在类和子类间通信在iOS框架中非常常见,在某种通信下子类可以执行特殊的操作。比如viewDidLoad()和viewWillAppear()。
这些方法是由UIViewController定义并且执行的,但是你自己的视图控制器子类可以重写它们。
例如,当界面即将可视化的时候,UIViewController类会调用 viewWillAppear(true)。通常是由UIViewController自己调用viewWillAppear(),但是假如你在自己子类中重写了这个方法,那么就会调用子类中重写的这个方法。
通过重写viewWillAppear(),你就得到了一个机会可以抢在父类前执行操作:
class MyViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
// 在父类前执行自己的操作
// 别忘了调用父类的方法!
super.viewWillAppear(animated)
// 在父类的方法后执行自己的代码
}
}
这就是利用父类能力的方法。一个好的父类设计可以提供这种“挂钩”,使你可以对某些事件进行响应。
不要忘记调用父类的方法!如果你忽视了这条,那么父类就不会得到自己的通知,很奇怪的bug就会出现。
你也早就在table view的数据源方法中见过重写方法:
override func tableView(_ tableView: UITableView,didSelectRowAt indexPath: IndexPath){ ... }
父类UITableViewController早就执行了这些方法,所以假如你想要执行自己的自定义版本,那么你就需要重写这些方法。
⚠️:在这些table view的委托及数据源方法中,通常不需要调用父类的方法。是否需要调用父类,你可以从iOS API的文档中查看。
在创建子类时,init方法也需要特殊照顾。
如果你不需要改变父类的init方法或者新增init方法,那么很简单,你不要做任何事就可以了。子类会自动接管父类的init方法。
大多数时候都是如此,然而,假如你需要重写init方法或者新增自己的init方法,例如,把值放入子类中的新的实例变量。这种情况下,重写一个init方法是不够的,你需要重写它们全部。
在下一个课程中,你会创建一个类GradientView,继承UIView。这里会使用init(frame)来创建并且初始化GradientView对象。GradientView中会重写init方法,来设置background color的值:
class GradientView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.black
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
...
}
因为UIView还有一个init?(coder)方法,所以GradientView需要执行这个方法,即使它不做任何事情。
同时注意一下init(frame)的前面有override关键字,但是init?(coder)前面的关键字是required。required关键字用于强制每个子类总是执行某个特定的init方法。
Swift希望确保子类不会忘记将自己的东西添加到这些必需的init方法中,即使应用程序实际上并没有使用该init方法,就像GradientView中那样。Swift过度的关注了这一方面的内容。
继承init方法的规则确实有些复杂,官方的Swift编程指导中花了大量篇幅讲解这块内容,不幸中的大幸就是,即使你在这里出错了,Xcode会及时提醒你的。
私隐部分(Private parts)
那么,子类可以使用父类的全部方法吗?也不尽然。
UIViewController和其他的UIKit类都拥有许多方法是隐藏的。这些神秘的方法可以做很酷的事情,所以你可能会试图去使用它们。但是它们不属于官方API的一部分,所以我们这种凡人,还是不要去碰它们的好。
如果你曾经在昏暗的巷子听到其他开发者用低沉的声音说道“Private API”,它们说的就是这会事。
如果你知道这些方法的名字,那么理论上你是可以调用它们的,但是不推荐这样做。它可能会使你的app被拒绝发布,因为苹果公司会扫描app是否调用了这些Private API。
你不能使用这些API基于两个理由:
1、它们可能具有非常大的负面作用。
2、它们也许并不是在任何版本的iOS系统中都存在
使用它们的风险是非常大的。
有时,使用这些API是读取某些设备功能的唯一途径。如果你用了的话,你的好运也就到这里结束了。幸运的是,对于大多数app而言,官方公开的API已经非常够用了。
角色扮演(Casts)
顺便说一下,Casts的官翻是类型转换。
代码中经常会出现一个实例不是被它所属的类引用,而是它的父类引用。这听起来有点奇怪,我们来看一个例子。
你现在正在写的这个app中,有一个UITabBarController,它有三个子页,每一个都代表一个视图控制器。第一个子页的视图控制器就是CurrentLocationViewController。之后,你会新建另外两个,第二个是LocationsViewController,第三个是MapViewController。
iOS的设计师在创建UITabBarController时显然不知道这三个特定的视图控制器。tab bar controller唯一知道的事情就是,每个子页的视图控制器都是继承自UIViewController的。
所以对tab bar controller而言,它只能看到它们的父类UIViewController。
就tab bar controller而言,它只知道自己有三个UIViewController实例,它并不知道也不关心你所额外添加的东西。
对UINavigationController也是一样的,对导航控制器而言,任何新的被推到导航栈堆中的视图控制器,对它而言都是UIViewController的实例。
有时这会带来点麻烦。当你向导航控制器请求它栈堆中的一个视图控制器时,它会返回一个到UIViewController对象的引用,即使这并不是这个对象的完整类型。
如果你想像处理自己的子类控制器那样处理这个UIViewController对象,你就需要对这个UIViewController进行角色扮演(这也是为什么我把Casts翻译为角色扮演,而不是类型转换)。
在之前的课程中,你在prepare(for,sender)中执行过这样的代码:
let navigationController = segue.destination as! UINavigationController
let controller = navigationController.topViewController
as! ItemDetailViewController
controller.delegate = self
这段代码中,你想要从导航控制器的栈堆中获取最上面的视图控制器,这个视图控制器是一个ItemDetailViewController,并且设置它的delegate属性。
然而,导航控制器的topViewController属性不会给你一个类型为ItemDetailViewController的对象。它返回的是一个UIViewController类型,根本不会包含你所要的delegate属性。
如果这里不使用‘as! ItemDetailViewController’,而是像下面这样:
let controller = navigationController.topViewController
那么Xcode就会给出一个报错。因为Swift推断这个类型为UIViewController,而这个类型并没有delegate这个属性。这个属性在你创建的子类ItemDetailViewControllers中才有。
虽然你知道topViewController引用了一个ItemDetailViewController,但是Swift并不知道。
为了解决这个问题,你需要把这个对象进行角色扮演,让他看起来你要的对象。所以你使用了‘as! ItemDetailViewController’,来告诉编译器,我想要这个对象和ItemDetailViewController一样。
带上角色扮演,代码就成了,你所熟知的样子:
let controller = navigationController.topViewController
as! ItemDetailViewController
(在Xcode中,你可以把上面的代码放到一行。使用长度长的,可描述性强的名字是使代码可读性更好的绝佳方案,但是如果排版太乱了,也不好看)
但是编译器不会检查角色扮演的结果是否是你想要的那个对象,所以如果你写错了,app多半就会挂掉。
由于其他原因,角色扮演可能会失败。例如,你想要的扮演的对象的值为nil。如果可能出现这种情况的话,最好使用as?来使它成为可选型。此时你需要将它的值也存储为可选型,或者用if let去解包。
注意一下,角色扮演不是万能的。你不能对Int执行角色扮演,使它成为String型的。角色扮演只能在相互兼容的两个对象间执行。
总结一下,角色扮演的方式有三种:
1、as?:用于允许失败的角色扮演。如果一个对象为nil或者与你想要扮演的对象的类型不兼容。此时,角色扮演会失败,但是没有关系。这种角色扮演的返回值是可选型,你可以用if let解包。
2、as!:用于一个类和它的子类之间。与隐式解包可选型一样,你必须确认没有风险时,才可以使用as!。
3、as:用于绝对不会失败的情况。Swift有时会保证角色扮演永远有效,比如NSString和String之间。
到底使用那个,也会会使你陷入选择困难症。你可以每次都是用as,然后看看Xcode的建议,会有一个黄色的提醒,你只需要根据建议修改就可以了。