这是一个系列文章,内容是关于iOS开发相关的整理,内容包括Swift基础,SwiftUI,UIKit,SwiftData,RxSwift,Combine,Swift Package Manager等。 本文主要是针对iOS面试相关内容的一个整理
Swift基础 #
Swift优势 #
- 语法简洁文件结构清晰,易于阅读
- 类型安全,编译期检查,减少运行时错误
- 代码量更少
- 支持元祖,高级枚举等新特性
- 协议,比委托更强大,可以定义方法属性,下标,并且可以有默认实现
- 扩展,可以很方便的新增功能
- 官方支持和推荐
Swift与OC交互 #
- Swift调用OC,在bridging-Header.h文件里添加OC的头文件即可。
- OC调用Swift,系统会默认生成一个工程名称-Swift.h的文件,OC导入即可使用,需要注意的是,想要Swift的方法或者属性能被OC访问,需要在方法或属性前面加上 @objc注解, 如果需要该类所有属性和方法都可以被OC访问也可以在该类型上使用 @objcMembers注解
内存管理机制,自动引用计数器(ARC) #
- strong是默认行为,weak和unowned为了解决引用循环问题,通常在闭包中会使用
- weak和unowned本质上是一样的,不同的是,在对象释放后,weak会返回nil,程序不会崩溃,unowned会有一个无效的引用指向对象,既不是optional也不是nil,如果继续访问该对象,会导致程序崩溃,因此一般情况下,weak使用的相对较多,而unowned很少被使用(除非确定对象不可能被释放)
访问控制 #
- open,public,internal(默认), fileprivate, private
- open,puiblic 可以被模块外访问,但是pulic只能被访问,open可以被继承和复写
- internal 模块内部均可访问
- fileprivate 同一个文件内可访问
- private 类型内部可访问
- 如果使用final修饰的类,方法和属性,则不能被继承和复写
struct、class #
- struct 是值类型,赋值时会进行深拷贝,每个struct副本都是独立的,不会互相影响,没有引用计数器因此不会造成循环引用问题。
- struct不支持继承
- struct系统默认实现构造函数
- class是引用类型,存储的是引用地址,赋值时是前拷贝,多个变量可以引用同一个实例,修改其中一个会影响其他引用该实例的变量。
- class支持单一继承
- 需要手动实现构造函数,并给属性赋值
- struct分配在栈上,class分配在堆上,struct通常比class更快,struct没有引用计数器的开销,而class有引用计数器的开销,可能会因为循环引用造成内存泄露
数组和字典 #
if、guard、defer #
- if 条件为true时执行代码块,guard刚好相反,条件为false时执行代码块, 因此guard常用来提前退出函数,方法或循环,避免重复执行代码
- defer 语句用于在函数、方法或循环退出前执行代码,无论是否发生异常。常用来确保资源的正确释放,例如关闭文件、释放内存等
Any、AnyHashable、AnyObject和AnyClass #
- Any 可以表示任意类型,包括基本类型、结构体、枚举、类等,通常用于函数参数或返回值
- AnyHashable 可以表示任意可哈希类型,通常用于字典的键
- AnyObject类型是一个协议,任何对象都实现了这个协议。它主要用来表示任何类的实例。在Swift中,你可以使用AnyObject来存储任何对象的实例。值得注意的是,由于所有的类都隐式地实现了这个协议,因此只有类实例可以被赋给一个AnyObject类型的变量,而结构体和枚举的实例则不能。
- AnyClass类型是AnyObject.Type的别名,表示任意类的元类型。在Swift中,你可以使用AnyClass来存储类的类型信息。这意味着你可以将一个类的类型作为AnyClass类型的值来使用,这在泛型编程中非常有用。
协议和扩展 #
- 协议是一种定义方法、属性和下标等的方式,用于描述对象的行为。协议可以被类、结构体和枚举实现,从而实现多态。
- 扩展是一种为已有类型添加新功能的方式,通常用于为系统类型添加新功能,或者为自己的类型添加新功能。
- 协议也可以扩展,可以通过扩展为协议添加默认实现。
protocol Animal {
var name: String { get }
}
extension Animal {
func eat() {
print("\(name)吃东西")
}
}
class Person: Animal {
var name: String {
return "人"
}
}
let person = Person()
person.eat()
// 输出:人吃东西
枚举 #
原始值 #
枚举类型可以定义原始值,原始值可以是字符串、整数、布尔值、字符等,原始值可以作为枚举类型的成员,也可以作为枚举类型的初始化参数。枚举可以设置属性,和方法。
enum Direction: Int {
case north = 1
case south = 2
case east = 3
case west = 4
var description: String {
switch self {
case .north: return "北"
case .south: return "南"
case .east: return "东"
case .west: return "西"
}
}
func turnRight() -> Direction {
switch self {
case .north: return .east
case .south: return .west
case .east: return .south
case .west: return .north
}
}
}
let direction = Direction.east
print(direction.description)
// 输出:东
print(direction.turnRight().description)
// 输出:南
关联值 #
关联值可以表示枚举类型的不同情况,比如枚举类型Result,可以有成功和失败两种情况,成功情况可以有返回值,失败情况可以有错误信息。
enum Result<T> {
case success(T)
case failure(Error)
}
// 使用
let successResult = Result.success("Success")
let failureResult = Result.failure(NSError(domain: "Error", code: 0, userInfo: nil))
switch successResult {
case success(let value):
print(value)
case failure(let error):
print(error)
}
协议和扩展 #
枚举类型可以支持协议和扩展。
enum Direction: Int, CaseIterable {
case north = 1
case south = 2
case east = 3
case west = 4
}
extension Direction: CustomStringConvertible {
var description: String {
switch self {
case .north: return "北"
case .south: return "南"
case .east: return "东"
case .west: return "西"
}
}
}
递归枚举 #
枚举类型可以定义为递归枚举,递归枚举的成员可以引用自身,递归枚举的成员可以有参数,递归枚举的成员可以有返回值。 indirect关键字,这个关键字表示 Swift 以支持安全递归的方式处理枚举的内存,防止因循环引用可能引发的问题。
enum Tree<T> {
case leaf(T)
indirect case branch(Tree, Tree)
}
let tree = Tree.branch(Tree.leaf(1), Tree.leaf(2))
let tree2 = Tree.branch(tree, Tree.leaf(3))
print(tree2)
// 输出:branch(branch(leaf(1), leaf(2)), leaf(3))
func preorderTraversal<T>(_ tree: Tree<T>) -> [T] {
switch tree {
case .leaf(let value):
return [value]
case .branch(let left, let right):
return preorderTraversal(left) + preorderTraversal(right)
}
}
print(preorderTraversal(tree2))
// 输出:[1, 2, 3]
函数和闭包 #
函数 #
函数基本形式
// 无参数,无返回值
func add() {
print("add")
}
// 有参数,有返回值
func add(a: Int, b: Int) -> Int {
return a + b
}
// 函数参数可以设置默认值,系统会自动生成两个方法
// add(a: Int, b: Int = 0) -> Int
// add(a: Int) -> Int
func add(a: Int, b: Int = 0) -> Int {
return a + b
}
// 函数参数可以使用下划线来省略参数名称
func add(_ a: Int, b: Int = 0) -> Int {
return a + b
}
// 可以使用inout关键字来修改参数值
func add(a: inout Int, _ b: Int) {
a += b
}
// 调用
var num = 0
add(&num, 10)
print(num)
// 输出:10
// swift支持不定参函数,不定参函数可以接收任意数量的参数
// 不定参只能是同一个类型
func add(_ numbers: Int...) -> Int {
var sum = 0
for number in numbers {
sum += number
}
return sum
}
// 调用
add(1, 2, 3, 4, 5)
// 输出:15
闭包 #
闭包是 Swift 中一种非常强大的特性,它可以使代码更加灵活和通用。闭包可以作为参数传递给函数,也可以作为函数的返回值。
// 闭包表达式语法
// { (parameters) -> return type in
// statements
// }
// 定义一个闭包变量,定义时可以省略参数名称
var add2: (Int, Int) -> Int
// 给闭包赋值,等号右边即为闭包表达式
add2 = { (a: Int, b: Int) -> Int in
return a + b
}
// 调用
let result = add(1, 2)
print(result)
// 输出:3
闭包可以作为参数传递给函数,也可以作为函数的返回值。
let add = { (a: Int, b: Int) -> Int in
return a + b
}
// 1. 闭包作为参数
func fetchData(url: String, completion: (code: Int, message: String) -> Void) {
completion(code: 200, message: "success")
}
// 调用
// 挂尾闭包,当函数最后一个参数为闭包时,调用的时候可以省去参数名,将闭包参数的表达式直接写在函数后面
fetchData(url: "https://www.baidu.com") { (code, message) in
print(code) // 200
print(message) // success
}
// 2. 闭包作为返回值
func makeAdd(a: Int) -> (Int) -> Int {
return { (b: Int) -> Int in
return a + b
}
}
let add2 = makeAdd(a: 5)
print(add2(3))
// 输出:8
元祖、泛型、可选值 #
元祖 #
元祖是 Swift 中一种非常方便的数据结构,它可以将多个值组合在一起,形成一个新的类型。元组可以包含不同类型的值,例如整数、浮点数、字符串、数组、字典等。 一个简单元祖可以通过以下方式初始化,并通过下标来取值
let httpError = (404, "Not Found")
print(person.0)
// 输出:404
print(person.1)
// 输出:Not Found
元祖初始化时也可以设置元素名称,此时也可以通过元祖的元素名称来取值
let httpError = (code: 404, message: "Not Found")
print(httpError.code)
// 输出:404
print(httpError.message)
// 输出:Not Found
泛型 #
泛型是 Swift 中一种非常强大的特性,它可以使代码更加通用、灵活和类型安全。泛型可以用于定义函数、方法、结构体、枚举和类,使它们可以处理不同类型的数据,而不需要重复编写代码。
// 该方法定义了一个交换两个变量值的方法,使用了泛型,使方法可以处理不同类型的变量
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var a = 1
var b = 2
swap(&a, &b)
print(a) // 2
print(b) // 1
var c = true
var d = false
swap(&c, &d)
print(c) // false
print(d) // true
上面的交换方法可以使用元祖进行优化,代码如下:
func swap<T>(_ a: inout T, _ b: inout T) {
// 等号左侧创建了一个元祖,元祖的第一个元素是 a,第二个元素是 b
// 等号右侧的元祖使用了元祖的解包语法,将元祖的第一个元素赋值给左边的 a,将元祖的第二个元素赋值给左边的 b
(a, b) = (b, a)
}
范型约束
// 该方法定义了一个查找数组中第一个符合条件的元素的索引的方法,使用了泛型约束
// 因为使用了 == 运算符,而并不是所有类型都支持该运算符,此时无法成功编译
// 所以需要使用泛型约束,要求类型必须实现 == 运算符,因此加上给范型加上Equatable约束后,即可正常编译了
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
高阶函数和柯理化 #
高阶函数 #
高阶函数是指接受一个或多个函数作为参数,或者返回一个函数作为结果的函数。Swift 中的函数是一等公民,因此可以将函数作为参数传递,也可以将函数作为返回值。
// 定义一个高阶函数,该函数接受一个函数作为参数,返回一个函数
func makeAdd(a: Int) -> (Int) -> Int {
return { (b: Int) -> Int in
return a + b
}
}
柯理化 #
柯理化是指将一个多参数函数转换为多个单参数函数的技术。柯理化可以使函数的调用更加灵活,同时也可以使函数的实现更加简单。 以下是一个简单的例子:
// 加一函数
func addOne(_ num: Int) -> Int {
return num + 1
}
// 加二函数
func addTwo(_ num: Int) -> Int {
return num + 2
}
// 调用
print(addOne(1)) // 输出:2
print(addTwo(1)) // 输出:3
// 如果我们想要实现+3,+4,+5...,需要每一个都写一个函数,柯理化可以很好的解决这个问题
// 封装一个函数,传入一个数n,返回一个函数,该函数的参数是一个数,返回值是n加上该数
func addTo(_ n: Int) -> (Int)-> Int {
return {
return $0 + n // $0 表示第一个参数,是闭包参数的简写,多个参数则使用 $0, $1, $2...依此类推
}
}
// 上面的函不使用简写的完整写法如下:
func addTo2(_ n: Int) -> (Int)-> Int {
return { value in
return value + n
}
}
// 这样就可以创建出+1,+2...+n的函数
let addOne2 = addTo(1)
let addTwo2 = addTo(2)
// 调用
print(addOne2(1)) // 输出:2
print(addTwo2(1)) // 输出:3
// 也可以连续使用括号调用函数
print(addTo(2)(1)) // 输出:3
错误处理 #
- throws 用于表示一个函数或方法可能会抛出错误。当一个函数或方法被标记为throws时,它必须在函数体中使用try、try?或try!来调用可能会抛出错误的代码,或者在调用该函数或方法的代码中使用do-catch语句来处理可能抛出的错误。
enum SomeError: Error {
case offline
case outOfRange
}
func someFunction() throws {
// 使用throw抛出一个错误
throw SomeError.offline
}
// 处理错误
do {
try someFunction()
} catch SomeError.offline {
print("offline")
} catch SomeError.outOfRange {
print("outOfRange")
}
// 输出:offline
- rethrows 是Swift 中一个关键字,用于表示一个函数本身不直接抛出错误,但它会将其接收的闭包参数抛出的错误再次向上抛出。主要作用是使接收闭包的函数签名更准确,当闭包不抛出错误时,调用者就不需要进行不必要的错误处理,从而简化代码,使函数调用更加简洁。
func someFunction<T>(_ value: T, _ transform: (T) throws -> T) rethrows -> T {
return try transform(value)
}
并发和多线程 #
await、async
Swift开发Tips #
关键字和注解 #
@discardableResult #
@discardableResult
func someFunction() -> Int {
return 42
}
someFunction() // 函数返回值没有使用,并不会产生警告
@available、#available #
// @available 用于指定函数、方法或属性在哪个版本的系统中可用,以及在哪个版本中被弃用。
@available(iOS 14.0, *)
func someFunction() {
// 函数实现
}
// 使用#available判断系统
if #available(iOS 11.0, *) {
// 指定该方法仅在iOS11及以上的系统设置
} else {
// 其他情况
}
@autoclosure 、@escaping #
// 使用 @autoclosure 的前提是,该闭包只能是形似()-> 返回值 的无参表达式
// 1. 用于将表达式自动封装成闭包
func logIfTrue(@autoclosure predicate: () -> Bool) {
if predicate() {
print("Condition is true")
}
}
// 使用时可以直接传入一个表达式,这个表达式会被自动封装成一个闭包,无需写成 logIfTrue(predicate: { 10 > 5 })
logIfTrue(1 > 2)
// 2. 只有执行闭包的条件为true时,才会执行闭包,避免不必要的计算。
// 或运算的实现就是使用了 @autoclosure 延迟计算的特性
public static func ||(lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool {
return lhs ? lhs : try rhs()
}
// 3. @escaping 用于表示一个闭包参数会在函数执行结束后才被调用,而不是在函数执行时立即调用。
// 在 @escaping 标识的闭包中,会强制你显式的使用 self,提醒你此处代码可能会造成循环引用,需要你小心编写此处代码
// 通常异步执行的闭包前需要加上 @escaping 标识,提醒调用者此处代码可能会在异步执行,需要注意循环引用问题
func doSomethingAsync(completion: @escaping () -> Void) {
DispatchQueue.main.async {
// do something
completion()
}
}
// 4. @autoclosure 可以与 @escaping 结合使用
// 闭包可以被捕获到函数外部,在函数执行结束后仍可被调用
func executeAfter(delay: TimeInterval, closure: @escaping @autoclosure () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
closure()
}
}
@frozen #
用于指示编译器在构建库时对方法、属性或类进行“冷冻”,从而避免它们在运行时被修改,提高库的性能和稳定性。当使用 @frozen 属性时,表示这些被标记的成员将拥有固定的内存布局,不会在运行时发生改变。
typealias和associatedtype #
typealias 用于给类型起别名,方便在代码中使用。
// 1. 给CGPoint定义一个别名叫Location,方便理解,实际上Location仍然是一个CGPoint类型,使用起来也和CGPoint一样
typealias Location = CGPoint
let lo = Location(x: 10, y: 20)
print(lo) // (10.0, 20.0)
// 2. 给闭包定义名称,方便理解
typealias Success = (_ result: String) -> Void
typealias Failure = (_ error: String) -> Void
public func excuteNetworking(_ successBlock: Success, failBlock: Failure) {
// 模拟网络请求
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
successBlock("success")
}
// 模拟网络请求失败
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
failBlock("fail")
}
}
associatedtype 协议中使用范型不能像范型类或范型结构体那样使用,只能使用关联类型。
protocol Stackble { //定义一个栈的协议
associatedtype Element// 在协议中用来占位的类型是关联类型,声明一个关联类型Element
mutating func push(_ element:Element)
mutating func pop() -> Element
func top() -> Element
func size() -> Int
}
此处添加了锚点
其他关键字 #
@MainActor、nonisolated、@dynamicCallable、@Sendable、@propertyWrapper、lazy、Actor、@unchecked、Sendable、inout、subscrip、mutating
常用方法 #
typeof() #
用于获取变量的类型
let a = 1
typeof(a) // Int.Type
protocol Copyable {
func copy() -> Self
}
class MyClass: Copyable {
var num = 1
func copy() -> Self {
let result = type(of: self).init()
result.num = num
return result
}
required init() {
}
}
Tips: 内容在不断更新中…