跳过正文
iOS面试整理(Swift)
  1. 文章/

iOS面试整理(Swift)

IOS Swift
iOS开发 - 系列文章
§ 1: 本文

这是一个系列文章,内容是关于iOS开发相关的整理,内容包括Swift基础,SwiftUI,UIKit,SwiftData,RxSwift,Combine,Swift Package Manager等。 本文主要是针对iOS面试相关内容的一个整理

Swift基础
#

Swift优势
#

  1. 语法简洁文件结构清晰,易于阅读
  2. 类型安全,编译期检查,减少运行时错误
  3. 代码量更少
  4. 支持元祖,高级枚举等新特性
  5. 协议,比委托更强大,可以定义方法属性,下标,并且可以有默认实现
  6. 扩展,可以很方便的新增功能
  7. 官方支持和推荐

Swift与OC交互
#

  1. Swift调用OC,在bridging-Header.h文件里添加OC的头文件即可。
  2. OC调用Swift,系统会默认生成一个工程名称-Swift.h的文件,OC导入即可使用,需要注意的是,想要Swift的方法或者属性能被OC访问,需要在方法或属性前面加上 @objc注解, 如果需要该类所有属性和方法都可以被OC访问也可以在该类型上使用 @objcMembers注解

内存管理机制,自动引用计数器(ARC)
#

  1. strong是默认行为,weak和unowned为了解决引用循环问题,通常在闭包中会使用
  2. weak和unowned本质上是一样的,不同的是,在对象释放后,weak会返回nil,程序不会崩溃,unowned会有一个无效的引用指向对象,既不是optional也不是nil,如果继续访问该对象,会导致程序崩溃,因此一般情况下,weak使用的相对较多,而unowned很少被使用(除非确定对象不可能被释放)

访问控制
#

  1. open,public,internal(默认), fileprivate, private
  2. open,puiblic 可以被模块外访问,但是pulic只能被访问,open可以被继承和复写
  3. internal 模块内部均可访问
  4. fileprivate 同一个文件内可访问
  5. private 类型内部可访问
  6. 如果使用final修饰的类,方法和属性,则不能被继承和复写

struct、class
#

  1. struct 是值类型,赋值时会进行深拷贝,每个struct副本都是独立的,不会互相影响,没有引用计数器因此不会造成循环引用问题。
  2. struct不支持继承
  3. struct系统默认实现构造函数
  4. class是引用类型,存储的是引用地址,赋值时是前拷贝,多个变量可以引用同一个实例,修改其中一个会影响其他引用该实例的变量。
  5. class支持单一继承
  6. 需要手动实现构造函数,并给属性赋值
  7. struct分配在栈上,class分配在堆上,struct通常比class更快,struct没有引用计数器的开销,而class有引用计数器的开销,可能会因为循环引用造成内存泄露

数组和字典
#

if、guard、defer
#

  1. if 条件为true时执行代码块,guard刚好相反,条件为false时执行代码块, 因此guard常用来提前退出函数,方法或循环,避免重复执行代码
  2. defer 语句用于在函数、方法或循环退出前执行代码,无论是否发生异常。常用来确保资源的正确释放,例如关闭文件、释放内存等

Any、AnyHashable、AnyObject和AnyClass
#

  1. Any 可以表示任意类型,包括基本类型、结构体、枚举、类等,通常用于函数参数或返回值
  2. AnyHashable 可以表示任意可哈希类型,通常用于字典的键
  3. AnyObject类型是一个协议,任何对象都实现了这个协议。它主要用来表示任何类的实例。在Swift中,你可以使用AnyObject来存储任何对象的实例。值得注意的是,由于所有的类都隐式地实现了这个协议,因此只有类实例可以被赋给一个AnyObject类型的变量,而结构体和枚举的实例则不能。
  4. AnyClass类型是AnyObject.Type的别名,表示任意类的元类型。在Swift中,你可以使用AnyClass来存储类的类型信息。这意味着你可以将一个类的类型作为AnyClass类型的值来使用,这在泛型编程中非常有用。

协议和扩展
#

  1. 协议是一种定义方法、属性和下标等的方式,用于描述对象的行为。协议可以被类、结构体和枚举实现,从而实现多态。
  2. 扩展是一种为已有类型添加新功能的方式,通常用于为系统类型添加新功能,或者为自己的类型添加新功能。
  3. 协议也可以扩展,可以通过扩展为协议添加默认实现。
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

错误处理
#

  1. 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
  1. 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: 内容在不断更新中…

iOS开发 - 系列文章
§ 1: 本文