十二月 21, 2025
24 分钟阅读

初级 iOS 开发面试题:Swift、UIKit 与 SwiftUI

interview
career-advice
job-search
entry-level
初级 iOS 开发面试题:Swift、UIKit 与 SwiftUI
Milad Bonakdar

Milad Bonakdar

作者

练习初级 iOS 面试常见问题,涵盖 Swift、UIKit、SwiftUI、网络、数据持久化和异步代码,并提供清晰回答思路。


引言

初级 iOS 开发面试通常会考察你是否能解释 Swift 基础、用 UIKit 或 SwiftUI 构建简单界面、请求并解码数据、选择基本的本地存储方式,以及避免常见的内存和线程问题。好的回答应该简短、具体,并能说明你会如何构建一个小型 App。

使用本指南练习入门 iOS 岗位最常见的问题:概念是什么意思、什么时候使用,以及初级开发者需要注意什么风险。


Swift 基础 (6 个问题)

1. Swift 中 varlet 有什么区别?

答案:

  • var 声明一个可变变量。它的值在初始化后可以更改。
  • let 声明一个不可变常量。一旦设置,其值不能更改。
  • 最佳实践: 默认情况下使用 let 以确保安全性和清晰度。仅在您知道该值将更改时才使用 var
let name = "John"  // 无法更改
var age = 25       // 可以更改
age = 26           // 有效
// name = "Jane"   // 错误:无法赋值

稀有度: 非常常见 难度: 简单


2. 解释 Swift 中的可选类型(Optionals)。什么是可选绑定(Optional Binding)?

答案: 可选类型表示一个可能为 nil(缺少值)的值。

  • 声明: 在类型后使用 ?var name: String?
  • 解包方法:
    • 强制解包: name!(危险,如果为 nil 会崩溃)
    • 可选绑定: 使用 if letguard let 安全地解包
    • Nil 合并运算符: name ?? "Default"
    • 可选链: user?.address?.city
var email: String? = "[email protected]"

// 可选绑定
if let unwrappedEmail = email {
    print("Email: \(unwrappedEmail)")
} else {
    print("No email")
}

// Guard 语句
guard let email = email else { return }
print(email)

稀有度: 非常常见 难度: 简单


3. Swift 中 classstruct 有什么区别?

答案:

  • Class: 引用类型(存储在堆上)。支持继承。通过引用传递。
  • Struct: 值类型(存储在栈上)。没有继承。通过复制传递。
  • 何时使用:
    • Struct: 对于简单的数据模型,当您需要值语义(独立的副本)时
    • Class: 当您需要继承、引用语义或析构器时
struct Point {
    var x: Int
    var y: Int
}

class Person {
    var name: String
    init(name: String) { self.name = name }
}

var p1 = Point(x: 0, y: 0)
var p2 = p1  // 创建副本
p2.x = 10    // p1.x 仍然是 0

var person1 = Person(name: "John")
var person2 = person1  // 相同引用
person2.name = "Jane"  // person1.name 也是 "Jane"

稀有度: 非常常见 难度: 中等


4. 什么是 Swift 中的闭包(Closures)?

答案: 闭包是自包含的功能块,可以在您的代码中传递和使用。它们类似于其他语言中的 lambdas 或匿名函数。

  • 语法: { (parameters) -> ReturnType in statements }
  • 捕获值: 闭包可以捕获和存储对其周围上下文中变量的引用。
  • 常见用途: 完成处理程序、回调、数组操作。
// 简单闭包
let greet = { (name: String) -> String in
    return "Hello, \(name)!"
}
print(greet("Alice"))

// 尾随闭包语法
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { $0 * 2 }
print(doubled)  // [2, 4, 6, 8, 10]

稀有度: 非常常见 难度: 中等


5. 解释 weakunowned 引用的区别。

答案: 两者都用于防止引用循环(内存泄漏)。

  • weak 可选引用,当引用的对象被释放时,会自动变为 nil。当引用可能比对象寿命更长时使用。
  • unowned 非可选引用,假设对象将始终存在。如果在释放后访问,则会崩溃。当您确定引用不会比对象寿命更长时使用。
class Person {
    var name: String
    weak var apartment: Apartment?  // weak 防止引用循环
    init(name: String) { self.name = name }
}

class Apartment {
    var unit: String
    unowned let tenant: Person  // unowned - 公寓始终有租户
    init(unit: String, tenant: Person) {
        self.unit = unit
        self.tenant = tenant
    }
}

稀有度: 常见 难度: 中等


6. 什么是 Swift 中的协议(Protocols)?

答案: 协议定义了适合特定任务或功能的的方法、属性和其他要求的蓝图。类、结构体和枚举可以采纳协议。

  • 类似于: 其他语言中的接口
  • 协议扩展: 可以提供默认实现
  • 面向协议编程: Swift 的首选范例
protocol Drawable {
    func draw()
}

struct Circle: Drawable {
    var radius: Double
    
    func draw() {
        print("Drawing circle with radius \(radius)")
    }
}

struct Rectangle: Drawable {
    var width: Double
    var height: Double
    
    func draw() {
        print("Drawing rectangle \(width)x\(height)")
    }
}

稀有度: 非常常见 难度: 简单


UIKit 基础 (5 个问题)

7. UIViewUIViewController 有什么区别?

答案:

  • UIView 表示屏幕上的一个矩形区域。处理绘图和事件处理。示例:UILabelUIButtonUIImageView
  • UIViewController 管理一个视图层级结构。处理视图生命周期、用户交互和导航。包含一个根视图并管理其子视图。
  • 关系: 视图控制器管理视图。一个视图控制器可以有多个视图。

稀有度: 非常常见 难度: 简单


8. 解释视图控制器生命周期方法。

答案: 视图控制器具有按顺序调用的特定生命周期方法:

  1. viewDidLoad() 视图加载到内存时调用一次。需要发生一次的设置。
  2. viewWillAppear(_:) 在视图出现在屏幕上之前调用。可以多次调用。
  3. viewDidAppear(_:) 在视图出现在屏幕上之后调用。在此处启动动画。
  4. viewWillDisappear(_:) 在视图消失之前调用。
  5. viewDidDisappear(_:) 在视图消失之后调用。清理资源。
override func viewDidLoad() {
    super.viewDidLoad()
    // 一次性设置:配置 UI,加载数据
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // 刷新数据,开始监听通知
}

稀有度: 非常常见 难度: 简单


9. 什么是 Auto Layout,你如何使用约束(constraints)?

答案: Auto Layout 是一种基于约束的布局系统,允许您创建可在不同屏幕尺寸和方向上工作的自适应 UI。

  • 约束: 定义视图之间的关系(宽度、高度、间距、对齐方式)
  • 方法:
    • Interface Builder: Xcode 中的可视化编辑器
    • 编程方式: NSLayoutConstraint 或 NSLayoutAnchor API
    • Visual Format Language: 基于字符串(现在不常用)
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)

NSLayoutConstraint.activate([
    button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    button.widthAnchor.constraint(equalToConstant: 200),
    button.heightAnchor.constraint(equalToConstant: 50)
])

稀有度: 非常常见 难度: 中等


10. framebounds 有什么区别?

答案:

  • frame 视图在其父视图坐标系中的位置和大小。用于定位视图。
  • bounds 视图在其自身坐标系中的位置和大小。原点通常是 (0, 0)。用于在视图内部绘制内容。
  • 主要区别: frame 相对于父视图,bounds 相对于自身。
let view = UIView(frame: CGRect(x: 50, y: 100, width: 200, height: 150))
print(view.frame)   // (50, 100, 200, 150) - 在父视图中的位置
print(view.bounds)  // (0, 0, 200, 150) - 自身坐标系

稀有度: 常见 难度: 中等


11. 如何处理来自 UITextField 的用户输入?

答案: 存在多种方法:

  • Target-Action:.editingChanged 事件添加 target
  • Delegate: 遵循 UITextFieldDelegate 协议
  • Combine: 使用 publishers(现代方法)
class ViewController: UIViewController, UITextFieldDelegate {
    let textField = UITextField()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Delegate 方法
        textField.delegate = self
        
        // Target-Action 方法
        textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
    }
    
    @objc func textChanged(_ textField: UITextField) {
        print("Text: \(textField.text ?? "")")
    }
    
    // Delegate 方法
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}

稀有度: 非常常见 难度: 简单


SwiftUI 基础 (4 个问题)

12. 什么是 SwiftUI,它与 UIKit 有何不同?

答案: SwiftUI 是 Apple 的声明式 UI 框架。你根据状态描述界面,状态变化时 SwiftUI 会更新渲染后的视图。UIKit 是命令式的,你会直接创建和修改 UIViewUILabelUIViewController 等对象。

在初级面试中,不要把它说成 SwiftUI 已经完全替代 UIKit。很多生产 App 会同时使用两者。更好的回答是:SwiftUI 适合新页面和状态驱动的 UI,代码更简洁;UIKit 在旧代码库、自定义控件以及 SwiftUI 尚未完整覆盖的 API 中仍然重要。

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

稀有度: 非常常见 难度: 简单


13. 解释 SwiftUI 中的 @State@Binding@ObservedObject

答案: 这些属性包装器说明状态存放在哪里,以及谁可以修改它。

  • @State 由某个 View 拥有的私有状态,通常用于简单的本地 UI 状态。
  • @Binding 指向父 View 所拥有状态的双向连接。子 View 可以读写它,但不拥有它。
  • @ObservedObject 指向外部可观察模型数据的引用。当数据由其他对象拥有,而 View 需要随数据变化刷新时使用。较新的 SwiftUI 代码中也会看到 Observation framework 和 @Observable,但很多面试仍会问这些包装器。
class UserData: ObservableObject {
    @Published var name = ""
}

struct ParentView: View {
    @State private var isOn = false
    @ObservedObject var userData = UserData()

    var body: some View {
        VStack {
            ChildView(isOn: $isOn)
            Text(userData.name)
        }
    }
}

struct ChildView: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("Switch", isOn: $isOn)
    }
}

稀有度: 常见 难度: 中等


14. 如何在 SwiftUI 中创建一个列表?

答案: 使用 List 视图来显示可滚动的数据集合。

struct ContentView: View {
    let fruits = ["Apple", "Banana", "Orange", "Grape"]
    
    var body: some View {
        List(fruits, id: \.self) { fruit in
            Text(fruit)
        }
    }
}

// 使用自定义模型
struct Task: Identifiable {
    let id = UUID()
    let title: String
}

struct TaskListView: View {
    let tasks = [
        Task(title: "Buy groceries"),
        Task(title: "Walk the dog")
    ]
    
    var body: some View {
        List(tasks) { task in
            Text(task.title)
        }
    }
}

稀有度: 常见 难度: 简单


15. 什么是 SwiftUI 中的视图修饰符(View Modifiers)?

答案: 视图修饰符是创建具有修改属性的新视图的方法。它们是可链接的。

  • 常用修饰符: .padding().background().foregroundColor().font().frame()
  • 顺序很重要: 修饰符按顺序应用
Text("Hello, World!")
    .font(.title)
    .foregroundColor(.blue)
    .padding()
    .background(Color.yellow)
    .cornerRadius(10)
    .shadow(radius: 5)

// 自定义修饰符
struct PrimaryButtonStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
    }
}

extension View {
    func primaryButton() -> some View {
        modifier(PrimaryButtonStyle())
    }
}

稀有度: 常见 难度: 简单


数据与网络 (5 个问题)

16. 如何在 iOS 中发出网络请求?

答案: 使用 URLSession。在现代 Swift 中,初级回答可以展示 async/await,并说明旧代码库中仍可能使用 completion handler。更接近实际开发的回答还会验证 HTTP 响应、处理错误、解码为 Codable 模型,并在 main actor 上更新 UI。

struct User: Codable {
    let id: Int
    let name: String
}

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
        throw URLError(.badServerResponse)
    }

    return try JSONDecoder().decode(User.self, from: data)
}

稀有度: 非常常见 难度: 中等


17. Swift 中的 Codable 是什么?

答案: CodableEncodable & Decodable 协议的类型别名。它允许在 Swift 类型和外部表示(如 JSON)之间轻松转换。

  • 自动合成: 如果所有属性都是 Codable,Swift 会自动生成编码/解码代码。
  • 自定义键: 使用 CodingKeys 枚举来映射不同的属性名称。
struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

// 解码 JSON
let json = """
{
    "id": 1,
    "name": "John Doe",
    "email": "[email protected]"
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: json)

// 编码为 JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(user)

稀有度: 非常常见 难度: 简单


18. 如何在 iOS 中本地持久化数据?

答案: 本地数据持久化有多种选择:

  • UserDefaults: 用于少量数据的简单键值存储(设置、首选项)
  • 文件系统: 将文件保存到 Documents 或 Cache 目录
  • Core Data: Apple 的对象图管理和持久化框架
  • SQLite: 关系数据库
  • Keychain: 用于敏感数据的安全存储(密码、令牌)
// UserDefaults
UserDefaults.standard.set("John", forKey: "username")
let username = UserDefaults.standard.string(forKey: "username")

// 文件系统
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let filePath = documentsPath.appendingPathComponent("data.json")

// 写入
try data.write(to: filePath)

// 读取
let loadedData = try Data(contentsOf: filePath)

稀有度: 非常常见 难度: 简单


19. 同步和异步操作有什么区别?

答案: 同步操作会阻塞当前线程,直到操作完成。如果在主线程上执行,App 可能会失去响应。异步操作会启动任务并让当前线程继续执行,结果稍后通过 async/await、completion handler、delegate 或 publisher 返回。

在 iOS 面试中,要把这个概念和 UI 响应性联系起来:网络请求、文件操作和重计算不应该阻塞 main thread,UI 更新应在 main actor 上执行。

func loadProfile() async {
    do {
        let user = try await fetchUser(id: 42)
        await MainActor.run {
            self.nameLabel.text = user.name
        }
    } catch {
        await MainActor.run {
            self.errorLabel.text = "Could not load profile"
        }
    }
}

稀有度: 常见 难度: 简单


20. 什么是 Grand Central Dispatch (GCD)?

答案: GCD 是 Apple 用于把任务调度到 dispatch queue 的底层 API。main queue 用于 UI 工作,global queue 可以按不同 QoS 优先级执行后台任务。

在较新的 Swift 代码中,通常会优先使用 Swift concurrency(async/awaitTask 和 actor),因为它让异步代码更易读。但 GCD 仍然重要,因为许多 UIKit 项目、旧代码库以及基于 callback 的 API 仍会使用 DispatchQueue.main.async 或后台队列。

DispatchQueue.global(qos: .userInitiated).async {
    let image = renderThumbnail()

    DispatchQueue.main.async {
        self.imageView.image = image
    }
}

稀有度: 常见 难度: 中等


Newsletter subscription

真正有效的每周职业建议

将最新见解直接发送到您的收件箱

停止申请,开始被录用

使用全球求职者信赖的AI驱动优化,将您的简历转变为面试磁铁。

免费开始

分享这篇文章

让面试回访翻倍

根据职位描述定制简历的候选人获得的面试机会是其他人的2.5倍。使用我们的AI为每一份申请即时自动定制您的简历。