学习Swift的一些Tips
前言:
最近几天在看《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("===")
}
屏幕快照 2017-07-03 下午3.08.01.png
类簇:
字符串格式化:
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
来启动。
然后就可以“交互式编程”了,在终端每输入代码,然后回车,就会实时编译执行。
屏幕快照 2017-07-05 下午2.05.45.png试试定义一个方法,并调用。可以看到它甚至还可以反馈给我们错误提示,提示我“调用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)
}
屏幕快照 2017-07-05 下午3.18.13.png