Swift 知识小集

前端之家收集整理的这篇文章主要介绍了Swift 知识小集前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

以下内容均是笔者学习过程中收集的知识点,顺序比较跳跃,初衷是为了方便查阅,顺便加深记忆。内容会不断更新,如果有什么问题或者有好的 Swift 方面的语法糖或者知识点也可以提出来,我会挑选斟酌后收录,欢迎大家关注~

环境:

Swift 4.0
Xcode 9.1

Associated Object

Objective-C 的 runtime 里的 Associated Object 允许我们在使用 Category 扩展现有的类的功能的时候,直接添加实例变量。在 Swift 中 extension 不能添加存储属性,我们可以利用 Associated Object 来实现,比如下面的 title 「实际上」是一个存储属性

// MyClass.swift
class MyClass {}

// MyClassExtension.swift
private var key: Void?

extension MyClass {
    var title: String? {
        get {
            return swift_getAssociatedObject(self,&key) as? String
        }

        set {
            swift_setAssociatedObject(self,&key,newValue,.swift_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}
// 测试
func printTitle(_ input: MyClass) {
    if let title = input.title {
        print("Title: \(title)")
    } else {
        print("没有设置")
    }
}

let a = MyClass()
printTitle(a)
a.title = "Swifter.tips"
printTitle(a)

// 输出:
// 没有设置
// Title: Swifter.tips”

Delegate 声明为 weak

Swift 中 Delegate 需要被声明成 weak,来避免访问到已被回收的内存而导致崩溃,如果我们像下面这样,是编译不过的:

protocol MyClassDelegate {
    func method()
}

class MyClass {
    weak var delegate: MyClassDelegate?
}

class ViewController: UIViewController,MyClassDelegate {
    // ...
    var someInstance: MyClass!

    override func viewDidLoad() {
        super.viewDidLoad()

        someInstance = MyClass()
        someInstance.delegate = self
    }

    func method() {
        print("Do something")
    }

    //...
}

// 编译失败
// 'weak' may only be applied to class and class-bound protocol types,not 'MyClassDelegate'

这是因为 Swift 的 protocol 是可以被除了 class 以外的其他类型遵守的,而对于像 struct 或是 enum 这样的类型,本身就不通过引用计数来管理内存,所以也不可能用 weak 这样的 ARC 的概念来进行修饰。

想要在 Swift 中使用 weak delegate,我们就需要将 protocol 限制在 class 内:

  • 一种做法是将 protocol 声明为 Objective-C 的,这可以通过在 protocol 前面加上 @objc 关键字来达到,Objective-C 的 protocol 都只有类能实现,因此使用 weak 来修饰就合理了:
@objc protocol MyClassDelegate {
    func method()
}
  • 另一种可能更好的办法是在 protocol 声明的名字后面加上 class,这可以为编译器显式地指明这个 protocol 只能由 class 来实现,避免了过多的不必要的 Objective-C 兼容:
protocol MyClassDelegate: class {
    func method()
}

可选协议和协议扩展

Objective-C 中的 protocol 里存在 @optional 关键字,被这个关键字修饰的方法并非必须要被实现,原生的 Swift protocol 里没有可选项,所有定义的方法都是必须实现的,如果不是实现是无法编译的:

class ViewController: UIViewController,MyProtocol { }

// 编译失败
// Type 'ViewController' does not conform to protocol 'MyProtocol'

如果我们想要像 Objective-C 里那样定义可选的协议方法,就需要将协议本身和可选方法都定义为 Objective-C 的,也即在 protocol 定义之前加上 @objc方法之前加上 @objc optional

@objc protocol MyProtocol {
    @objc optional func myMethod()
}

另外,对于所有的声明,它们的前缀修饰是完全分开的,也就是说你不能像是在 Objective-C 里那样用一个 @optional 指定接下来的若干个方法都是可选的了,必须对每一个可选方法添加前缀,对于没有前缀的方法来说,它们是默认必须实现的:

@objc protocol MyProtocol {
    @objc optional func optionalMethod()        // 可选
    func necessaryMethod()                      // 必须
    @objc optional func anotherOptionalMethod() // 可选
}

一个不可避免的限制是,使用 @objc 修饰的 protocol 就只能被 class 实现了,也就是说,对于 structenum 类型,我们是无法令它们所实现的协议中含有可选方法或者属性的。另外,实现它的 class 中的方法还必须也被标注为 @objc,或者整个类就是继承自 NSObject。对于这种问题,在 Swift 2.0 中,我们有了另一种选择,那就是使用 protocol extension。我们可以在声明一个 protocol 之后再用 extension 的方式给出部分方法默认的实现,这样这些方法在实际的类中就是可选实现的了:

protocol MyProtocol {
    func optionalMethod()        // 可选
    func necessaryMethod()       // 必须
    func anotherOptionalMethod() // 可选
}

extension MyProtocol {
    
    //默认的可选实现
    func optionalMethod() {
        print("optionalMethod")
    }
    
    //默认的可选实现
    func anotherOptionalMethod() {
        print("anotherOptionalMethod")
    }   
}
class ViewController: UIViewController,MyProtocol {
    
    // 必须的实现
    func necessaryMethod() {
        print("necessaryMethod")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.optionalMethod();
        self.necessaryMethod();
        self.anotherOptionalMethod();
    }
}

// 输出:
// optionalMethod
// necessaryMethod
// necessaryMethod

单例

Swift 中的单例非常简单,Swift 1.2 以及之后:

class Singleton  {
    static let sharedInstance = Singleton()
    private init() {}
}

这种写法不但是线程安全的,也是懒加载的,let 定义的属性本身就是线程安全的,同时 static 定义的是一个 class constant,拥有全局作用域和懒加载特性。

另外,这个类型中加入了一个私有的初始化方法,来覆盖默认的公开初始化方法,这让项目中的其他地方不能够通过 init 来生成自己的 Singleton 实例,也保证了类型单例的唯一性。如果你需要的是类似 default 的形式的单例 (也就是说这个类的使用者可以创建自己的实例) 的话,可以去掉这个私有的 init 方法

输出格式化

在 Objective-C 中的 %@ 这样的格式在指定的位置设定占位符,然后通过参数的方式将实际要输出内容补充完整。例如 Objective-C 中常用的向控制台输出NSLog 方法就使用了这种格式化方法

float a = 1.234567;
NSString *b = @"Helllo";
NSLog(@"float:%.2f  str:%p",a,b);

// 输出:
// float:1.23  str:0x1024a1078

对应 Swift 中我们可以这样:

let a = 1.234567
let b = "Helllo"
let c = String(format:"float:%.2f str:%p",b)
print(c)

// 输出:
// float:1.23 str:0x604000249e10

Selector

@selector 是 Objective-C 时代的一个关键字,它可以将一个方法转换并赋值给一个 SEL 类型,它的表现很类似一个动态的函数指针。在 Swift 中没有 @selector 了,取而代之,从 Swift 2.2 开始我们使用 #selector 来从暴露给 Objective-C 的代码获取一个 selector,并且因为 selector 是 Objective-C runtime 的概念,在 Swift 4 中,默认情况下所有的 Swift 方法在 Objective-C 中都是不可见的,所以你需要在这类方法前面加上 @objc 关键字,将这个方法暴露给 Objective-C,才能进行使用:

let btn = UIButton.init(type: .system)
btn.backgroundColor = UIColor.red
btn.frame = CGRect(x: 100,y: 100,width: 150,height: 40)
btn.setTitle("Button",for: .normal)
//无参数
btn.addTarget(self,action: #selector(btnClick),for: .touchUpInside)
view.addSubview(btn)

@objc func btnClick()  {
    print("button click !")
}
...
//有参数
btn.addTarget(self,action: #selector(btnClick(_ :)),for: .touchUpInside)
...

@objc func btnClick(_ button: UIButton)  {
    print("button click !")
}

将 protocol 的方法声明为 mutating

Swift 的 protocol 不仅可以被 class 类型实现,也适用于 structenum,因为这个原因,我们在写给别人用的协议时需要多考虑是否使用 mutating 来修饰方法。Swift 的 mutating 关键字修饰方法是为了能在该方法修改 struct 或是 enum 的变量,所以如果你没在协议方法里写 mutating 的话,别人如果用 struct 或者 enum 来实现这个协议的话,就不能在方法里改变自己的变量了,比如下面的代码是编译不过的:

protocol Vehicle {
    
    func changeColor()
    
}

struct MyCar: Vehicle {
    
    var color = "blue"
    
    func changeColor() {
        color = "red"
    }
}

// 编译失败
// Cannot assign to property: 'self' is immutable

我们应该加上 mutating 关键字:

protocol Vehicle {
    
    mutating func changeColor()
    
}

struct MyCar: Vehicle {
    
    var color = "blue"
    
    mutating func changeColor() {
        color = "red"
    }
}

override func viewDidLoad() {
    super.viewDidLoad()

    var car = MyCar()
    print(car.color)
    car.changeColor()
    print(car.color)    
}

// 输出:
// blue
// 输出:
// red

数组遍历 enumerate

使用 NSArray 时一个很常遇见的的需求是在枚举数组内元素的同时也想使用对应的下标索引,在 Objective-C 中最方便的方式是使用 NSArray 的 enumerateObjectsUsingBlock: ,在 Swift 中存在一个效率,安全性和可读性都很好的替代,那就是快速枚举某个数组的EnumerateGenerator,它的元素是同时包含了元素下标索引以及元素本身的多元组:

let arr = ["a","b","c","d","e"]
for (idx,str) in arr.enumerated() {
    print("idx: \(idx) str: \(str)")
}

// 输出:
idx: 0 str: a
idx: 1 str: b
idx: 2 str: c
idx: 3 str: d
idx: 4 str: e

输入输出参数 inout

函数参数默认是常量,如果试图在函数体中更改参数值将会导致编译错误,比如下面的例子中尝试着交换值:

func swapTwoInts(_ a: Int,_ b: Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

// 编译失败
// Cannot assign to value: 'a' is a 'let' constant
// Cannot assign to value: 'b' is a 'let' constant

如果想要一个函数可以修改参数的值,并且想要在这些修改函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters):

func swapTwoInts(_ a: inout Int,_ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Default 参数

Swift 的方法支持默认参数的,也就是说在声明方法时,可以给某个参数指定一个默认使用的值。在调用方法时要是传入了这个参数,则使用传入的值,如果缺少这个输入参数,那么直接使用设定的默认值进行调用。和其他很多语言的默认参数相比较,Swift 中的默认参数限制更少,并没有所谓 "默认参数之后不能再出现无默认值的参数"这样的规则,举个例子,下面两种方法的声明在 Swift 里都是合法可用的:

func sayHello1(str1: String = "Hello",str2: String,str3: String) {
    print(str1 + str2 + str3)
}

func sayHello2(str1: String,str3: String = "World") {
    print(str1 + str2 + str3)
}
sayHello1(str2: " ",str3: "World")
sayHello2(str1: "Hello",str2: " ")

//输出都是 Hello World

延迟加载 lazy

延时加载或者说延时初始化是很常用的优化方法,在构建和生成新的对象的时候,内存分配会在运行时耗费不少时间,如果有一些对象的属性内容非常复杂的话,这个时间更是不可忽略。另外,有些情况下我们并不会立即用到一个对象的所有属性,而默认情况下初始化时,那些在特定环境下不被使用的存储属性,也一样要被初始化和赋值,也是一种浪费。在 Objective-C 中,一个延迟加载一般是这样的:

// ClassA.h
@property (nonatomic,copy) NSString *testString;

// ClassA.m
- (NSString *)testString {
     if (!_testString) {
         _testString = @"Hello";
        NSLog(@"只在首次访问输出");
     }
     return _testString;
}

对应在 Swift 中,使用 lazy 作为属性修饰符时,只能声明属性变量,且我们需要显式地指定属性类型,否则会编译错误

class ClassA {
    lazy let str: String = {
        let str = "Hello"
        print("只在首次访问输出")
        return str
    }()
}

// 编译失败
// 'lazy' cannot be used on a let

class ClassA {
    lazy var str = {
        let str = "Hello"
        print("只在首次访问输出")
        return str
    }()
}

// 编译失败
// Unable to infer complex closure return type

我们应该声明为 var 并指定好类型:

class ClassA {
    lazy var str: String = {
        let str = "Hello"
        print("只在首次访问输出")
        return str
    }()
}

override func viewDidLoad() {
    super.viewDidLoad()

    let ca = ClassA()
    print(ca.str)
    print(ca.str)
    print(ca.str)   
}

// 输出:
// 只在首次访问输出
// Hello
// Hello
// Hello

如果不需要做什么额外工作的话,也可以对这个 lazy属性直接写赋值语句:

lazy var str: String = "Hello"

我们还可以利用 lazy 配合像 map 或是 filter 这类接受闭包并进行运行的方法一起,让整个行为变成延时进行的。在某些情况下这么做也对性能会有不小的帮助。例如,直接使用 map 时:

let data = 1...3
let result = data.map {
    (i: Int) -> Int in
    print("正在处理 \(i)")
    return i * 2
}

print("准备访问结果")
for i in result {
    print("操作后结果为 \(i)")
}

print("操作完毕")

// 输出:
// 正在处理 1
// 正在处理 2
// 正在处理 3
// 准备访问结果
// 操作后结果为 2
// 操作后结果为 4
// 操作后结果为 6
// 操作完毕

而如果我们先进行一次 lazy 操作的话,我们就能得到延时运行版本的容器:

let data = 1...3
let result = data.lazy.map {
    (i: Int) -> Int in
    print("正在处理 \(i)")
    return i * 2
}

print("准备访问结果")
for i in result {
    print("操作后结果为 \(i)")
}

print("操作完毕")

// 准备访问结果
// 正在处理 1
// 操作后结果为 2
// 正在处理 2
// 操作后结果为 4
// 正在处理 3
// 操作后结果为 6
// 操作完毕

对于那些不需要完全运行,可能提前退出的情况,使用 lazy 来进行性能优化效果会非常有效。

编译标记

在 Objective-C 中,我们经常在代码中插入 #param 符号来标记代码的区间,这样在 Xcode 的导航栏中我们就可以看到组织分块后的方法列表。在 Swift 中我们可以用 MARK: 来代替:

在 Objective-C 中还有一个很常用的编译标记,那就是 #warning,一个 #warning 标记可以在 Xcode 的代码编辑器中显示为明显的黄色警告条,非常适合用来提示代码的维护者和使用者需要对某些东西加以关注。在 Swift 中我们可以用 FIXME:TODO: 配合 shell 来代替:

脚本:

TAGS="TODO:|FIXME:"
echo "searching ${SRCROOT} for ${TAGS}"
find "${SRCROOT}" \( -name "*.swift" \) -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/"

效果


换行符

在 Swift 3 中,需要换行时是需要 \n

let str = "xxxx\nxxx"

// 输出:
// xxxx
// xxx

在 swift 4 中,我们可以使用 """

let jsonStr = """
        {
            "id": 123455,"nickname": "xxxx","isMale": true,"birthday": "2000年3月24日","personalURL": "https://xxxxxx.github.io"
        }
        """
          
// 输出:
{
    "id": 123455,"personalURL": "https://xxxxxx.github.io"
}

字符串切割 split

我们需要切割某个字符串时可以用 split 方法,需要注意的是,返回的结果是个数组

let str = "Hello,world !"
print(str.split(separator: ","))

// 输出:
// ["Hello","world !"]

KVC

class MyClass {
    var name = "ifelseboyxx"
}

Swift 4 中 Apple 引入了新的 KeyPath 的表达方式,现在,对于类型 MyClass 中的变量 name,对应的 KeyPath 可以写为 \MyClass.name,利用 KVC 修改 name 值的话,我们可以这么操作:

let object = MyClass()
print("name: \(object.name)")
// set
object[keyPath: \MyClass.name] = "ifelseboy"
// get
print("name: \(object[keyPath: \MyClass.name])")

// 输出:
// name: ifelseboyxx
// name: ifelseboy

另外 Swift 4 中 struct 同样支持 KVC :

struct MyStruct {
    var age: Int
}

var obj = MyStruct(age: 18)
print("我今年 \(obj.age) 岁了")
obj[keyPath: \MyStruct.age] = 8
print("我今年 \(obj[keyPath: \MyStruct.age]) 岁了")

// 输出:
// 我今年 18 岁了
// 我今年 8 岁了

Swift 中值类型和引用类型注意点

KVC 一节中代码里有个注意点:

var obj = MyStruct(age: 18)
//替换为
let obj = MyStruct(age: 18)

是编译不过的,会报错:

Cannot assign to immutable expression of type 'Int'

笔者初次也犯了这样的错误,想当然的认为 MyClasslet 声明的是没有问题的,struct 也一样:

let object = MyClass()

其实原因很简单,swift 中 Class 是引用类型的,而 struct 是值类型的:值类型在被赋给一个变量,或被传给函数时,实际是做了一次拷贝。引用类型在被赋给一个变量,或被传给函数时,传递的是引用。

KVO

很遗憾,依然只有 NSObject 才能支持 KVO,另外由于 Swift 为了效率,默认禁用了动态派发,因此想用 Swift 来实现 KVO,我们还需要做额外的工作,那就是将想要观测的对象标记dynamic@objc,下面的

猜你在找的Swift相关文章