Skip to content

iOS 开发 - 高级 iOS

使用 Swift 进行 iOS 开发 - 高级 iOS 主题

Section titled “使用 Swift 进行 iOS 开发 - 高级 iOS 主题”

本章深入探讨了构建功能丰富的 iOS 应用程序的关键高级 iOS 功能。我们将涵盖创建多屏应用、导航、使用表格视图 (Table Views) 显示数据列表、数据持久化 (data persistence) 以及基本的网页内容显示 (web content display)。

请仔细阅读每个部分,因为这些概念构成了许多 iOS 应用程序的基础。

大多数应用程序由多个屏幕组成。每个屏幕通常由一个 UIViewController(视图控制器)管理。我们将学习如何添加多个视图控制器并在它们之间进行导航 (navigation)。

从一个新项目或现有项目开始。默认情况下,新项目(使用 Storyboard)在 Main.storyboard 中带有一个 UIViewController 场景及其对应的 ViewController.swift 文件。Storyboard 中指向此场景的箭头表示它是初始视图控制器 (initial view controller),即入口点。

  1. 打开 Main.storyboard。
  2. 打开对象库 (Object Library)(Shift-Command-L)。
  3. 搜索 ‘View Controller’ 并将一个新的 View Controller 对象拖拽到 Storyboard 画布上,放在现有视图控制器的旁边。
  4. 为此视图控制器创建一个新的 Swift 文件:在项目导航器中右键单击您的项目文件夹 > New File… > 选择 ‘Cocoa Touch Class’ > Next。命名文件(例如,SecondViewController),使其成为 UIViewController 的子类 (subclass),确保“Language”是 Swift,并取消勾选“Also create XIB file”。点击 Next,然后 Create。
  5. 将 Storyboard 场景链接到这个新类:在 Storyboard 中选择新的 View Controller 场景。转到身份检查器 (Identity Inspector)(检查器面板中从右数第三个图标)。在“Class”字段中,输入并选择 SecondViewController。

您现在有了第二个视图控制器场景及其 Swift 类,但还没有办法到达它。

从一个视图控制器转换到另一个视图控制器通常使用 Storyboard 中的“Segues”(转场)来实现,这些 Segues 通常由 UINavigationController 管理。

  1. 将第一个视图控制器嵌入到导航控制器中:在 Main.storyboard 中选择您的初始视图控制器场景。转到 Xcode 菜单:Editor > Embed In > Navigation Controller。这将在您的第一个视图控制器之前添加一个导航控制器场景,并为您的第一个视图控制器添加一个导航栏 (navigation bar)。
  2. 向第一个视图控制器添加一个按钮:将一个 UIButton 拖拽到您的第一个视图控制器的视图上。将其标题设为 ‘Go to Second View’。
  3. 创建一个 Segue:按住 Control 键从该按钮拖拽到 SecondViewController 场景。将出现一个弹窗。在“Action Segue”下,选择“Show”。这将创建一个 segue,当按钮被点击时,会将第二个视图控制器推送到导航堆栈 (navigation stack) 上。

运行应用程序。您将看到带导航栏的第一个视图控制器。点击按钮将转换到第二个视图控制器,并且导航栏会自动显示一个“Back”(返回)按钮以返回到第一个视图。

选择视图控制器的导航项 (Navigation Item)(它出现在 Storyboard 文档大纲中的导航栏内部,或者通过点击视图控制器场景内的导航栏区域来选择)。在属性检查器 (Attributes Inspector) 中,您可以设置:

  • Title:该视图控制器在导航栏中心显示的标题。
  • Prompt:标题上方较小的一行文本。
  • Back Button:自定义当该视图控制器被推出后,在 下一个 屏幕上出现的返回按钮的文本。

您还可以向导航栏添加 UIBarButtonItems(例如,用于“编辑”、“添加”、“完成”等操作)。从对象库中将一个“Bar Button Item”拖拽到 Storyboard 中的导航栏上。要使其触发 segue,按住 Control 键从该导航栏按钮项拖拽到另一个视图控制器,然后选择“Show”。

UITableView 用于以单列行列表的形式显示数据,这些行可以分组到节 (sections) 中。它对于显示大型数据集非常高效。

我们来创建一个简单的表格视图。创建一个新项目或使用现有的视图控制器。

  1. 从对象库中将一个“Table View”拖拽到您的视图控制器的视图上。使用 Auto Layout 使其填满视图。
  2. 原型单元格 (Prototype Cells):选择表格视图。在属性检查器中,将“Prototype Cells”设置为 1。这会在 Storyboard 中为表格视图添加一个模板单元格 (template cell)。
  3. 单元格标识符 (Cell Identifier):选择“Table View Cell”(可以直接选择,或通过文档大纲选择)。在其属性检查器中,将“Identifier”设置为一个唯一的字符串,例如 ‘MyCell’。此标识符用于在代码中复用单元格 (dequeue cells)。
  4. 样式 (Optional):您可以在属性检查器中更改单元格的“Style”(例如,Basic, Right Detail, Subtitle),以获得预配置的标签布局。

表格视图需要一个数据源 (data source)(提供数据)和一个委托 (delegate)(处理交互和外观)。

  1. 在 Storyboard 中:按住 Control 键从表格视图拖拽到其场景顶部的视图控制器图标(黄色圆圈)。选择“dataSource”。重复此操作并选择“delegate”。
  2. 在 ViewController.swift 中:遵循协议 (protocols): class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { ... }
  3. 为表格视图添加一个 outlet(插座变量):按住 Control 键从 Storyboard 中的表格视图拖拽到您的 ViewController.swift 文件,并创建一个名为 tableView 的 outlet。

实现必需的 UITableViewDataSource 方法:

// Sample data array
// 示例数据数组
let dataArray = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count // Number of rows
// 行数
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue a reusable cell
// 复用一个可重用的单元格
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath)
// Configure the cell
// 配置单元格
let item = dataArray[indexPath.row]
cell.textLabel?.text = item // Works if cell style is Basic, Left Detail, etc.
// 如果单元格样式是 Basic、Left Detail 等,则有效。
return cell
}

在 viewDidLoad() 中,如果未在 Storyboard 中设置,或者您更喜欢在代码中设置,请确保设置了委托和数据源: tableView.dataSource = self tableView.delegate = self 此外,如果您的数据在 viewDidLoad 之后发生变化,您可能需要添加 tableView.reloadData()。

运行应用程序。您应该会看到一个水果列表。对于更复杂的单元格,您需要创建一个自定义的 UITableViewCell 子类。

此应用程序演示了如何使用 Timer 创建一个倒计时 (countdown)。UI 将有一个标签来显示时间,以及用于开始、暂停、重置和调整时间的按钮。

  1. 导航栏:标题“Egg Timer”。左侧导航栏按钮项 (Left Bar Button Item)“Pause”(例如,系统暂停图标)。右侧导航栏按钮项 (Right Bar Button Item)“Start”(例如,系统播放图标)。
  2. 中心标签 (Center Label):大标签,显示剩余时间(例如,timeLeftLabel)。初始值为“210”。
  3. 工具栏 (底部 Toolbar):
    • 导航栏按钮项 (Bar Button Item)“-10s”。
    • 灵活间隔导航栏按钮项 (Flexible Space Bar Button Item)。
    • 导航栏按钮项“Reset”。
    • 灵活间隔导航栏按钮项。
    • 导航栏按钮项“+10s”。 将这些 UI 元素(标签作为 outlet,按钮作为 action)连接到您的 ViewController.swift。
import UIKit
class EggTimerViewController: UIViewController {
@IBOutlet weak var timeLeftLabel: UILabel!
// Add outlets for buttons if their state needs to change (e.g., enable/disable)
// 如果按钮的状态需要改变(例如,启用/禁用),请添加 outlet
var eggTimer: Timer? // Use optional Timer
// 使用可选的 Timer
var timerActive = false
var timeLeft = 210 { // Initial time in seconds
// 初始时间(秒)
didSet {
timeLeftLabel.text = String(timeLeft)
}
}
override func viewDidLoad() {
super.viewDidLoad()
timeLeftLabel.text = String(timeLeft)
// Setup initial button states if needed
// 如果需要,设置按钮的初始状态
}
func processTimer() {
if timeLeft <= 0 {
stopTimer(reset: false) // Stop but don't reset automatically
// 停止但不自动重置
timeLeftLabel.text = "Done!"
// Optionally play a sound or show an alert
// 可选:播放声音或显示提示框
return
}
timeLeft -= 1
}
func startTimer() {
if !timerActive {
timerActive = true
// Invalidate existing timer before starting a new one
// 在启动新计时器之前使现有计时器失效
eggTimer?.invalidate()
eggTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.processTimer()
}
// Update button states (e.g., disable Start, enable Pause)
// 更新按钮状态(例如,禁用 Start,启用 Pause)
}
}
func stopTimer(reset: Bool) {
timerActive = false
eggTimer?.invalidate()
eggTimer = nil
if reset {
timeLeft = 210
}
// Update button states (e.g., enable Start, disable Pause)
// 更新按钮状态(例如,启用 Start,禁用 Pause)
}
// MARK: - IBActions
@IBAction func startTimerButtonTapped(_ sender: Any) {
startTimer()
}
@IBAction func pauseTimerButtonTapped(_ sender: Any) {
stopTimer(reset: false)
}
@IBAction func subtractTimeTapped(_ sender: Any) {
if timeLeft > 10 {
timeLeft -= 10
} else {
timeLeft = 0 // Prevent going below zero
// 防止时间小于零
}
if timeLeft == 0 && timerActive { // If timer was active and reached 0
// 如果计时器处于活动状态并且达到 0
processTimer() // To trigger "Done!" state
// 以触发“完成!”状态
}
}
@IBAction func resetTimerTapped(_ sender: Any) {
stopTimer(reset: true)
}
@IBAction func addTimeTapped(_ sender: Any) {
timeLeft += 10
}
}

现在 Timer.scheduledTimer 方法通常使用基于 block 的方式作为其操作 (action),它可以弱引用 self 以避免保留环 (retain cycles)。#selector 方法仍然有效,但需要 @objc 方法。

应用程序通常需要在设备本地保存数据。两种常见的方法是 UserDefaults(用于存储少量数据,如设置)和 Core Data(用于存储更大、结构化的数据集)。

UserDefaults 是一个简单的键值存储,非常适合存储用户偏好设置或简单的应用程序状态。

// Get the standard UserDefaults instance
// 获取标准的 UserDefaults 实例
let defaults = UserDefaults.standard
// Set values
// 设置值
defaults.set("John Doe", forKey: "userName")
defaults.set(25, forKey: "userAge")
defaults.set(true, forKey: "isUserLoggedIn")
// Get values (provide default if key doesn't exist or type mismatch)
// 获取值(如果键不存在或类型不匹配,则提供默认值)
let userName = defaults.string(forKey: "userName") ?? "Guest"
let userAge = defaults.integer(forKey: "userAge") // Defaults to 0 if not found
// 如果未找到,默认为 0
let isLoggedIn = defaults.bool(forKey: "isUserLoggedIn") // Defaults to false
// 默认为 false
// To save immediately (optional, system does it periodically)
// 立即保存(可选,系统会定期执行)
// defaults.synchronize() // Less needed in modern iOS, but can be used.
// 在现代 iOS 中不太需要,但仍可使用。

Core Data 是一个强大的对象图和持久化框架。它允许您对结构化数据进行建模、保存和检索。

  1. 项目设置 (Project Setup):创建新项目时,勾选“Use Core Data”复选框。这会在 AppDelegate.swift 中添加一个 .xcdatamodeld 文件和用于 Core Data 堆栈 (NSPersistentContainer) 的样板代码 (boilerplate code)。
  2. 数据模型 (Data Model):打开 .xcdatamodeld 文件。点击“Add Entity”创建一个表(例如,“User”)。选择实体并添加“Attributes”(列,例如类型为 String 的 name,类型为 Integer 16/32/64 的 age)。
  3. 访问上下文 (Accessing the Context):AppDelegate 中的 NSPersistentContainer 提供了 viewContext,它是您处理托管对象 (managed objects) 的工作区 (scratchpad)。
import UIKit
import CoreData
// Assuming 'User' entity with 'name' (String) and 'creationDate' (Date) attributes
// 假设存在 'User' 实体,包含 'name' (String) 和 'creationDate' (Date) 属性
// Get the AppDelegate and NSManagedObjectContext
// 获取 AppDelegate 和 NSManagedObjectContext
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
// Handle error appropriately
// 进行适当的错误处理
return
}
let context = appDelegate.persistentContainer.viewContext
// Saving Data
// 保存数据
func saveNewUser(name: String) {
// Create a new User managed object
// 创建一个新的 User 托管对象
// If you've generated NSManagedObject subclasses (Editor > Create NSManagedObject Subclass...):
// 如果您已经生成了 NSManagedObject 子类(菜单:Editor > Create NSManagedObject Subclass...):
// let newUser = User(context: context)
// newUser.name = name
// newUser.creationDate = Date()
// Or using NSEntityDescription if no subclass generated:
// 或者,如果没有生成子类,则使用 NSEntityDescription:
guard let userEntity = NSEntityDescription.entity(forEntityName: "User", in: context) else {
print("Failed to find User entity")
// 未找到 User 实体
return
}
let newUser = NSManagedObject(entity: userEntity, insertInto: context)
newUser.setValue(name, forKey: "name")
newUser.setValue(Date(), forKey: "creationDate")
do {
try context.save() // Save changes to persistent store
// 将更改保存到持久化存储
print("User '\(name)' saved.")
// 用户 '\(name)' 已保存。
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
// 无法保存。
}
}
// Fetching Data
// 获取数据
func fetchUsers() -> [NSManagedObject] { // Or [User] if using subclasses
// 如果使用子类,则为 [User]
// Create a fetch request
// 创建一个获取请求
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "User")
// For subclasses: let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
// 对于子类:let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
// Optional: Add sort descriptors or predicates (filters)
// 可选:添加排序描述符或谓词(过滤器)
// let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
// fetchRequest.sortDescriptors = [sortDescriptor]
// let predicate = NSPredicate(format: "name CONTAINS[c] %@", "John")
// fetchRequest.predicate = predicate
do {
let users = try context.fetch(fetchRequest)
for user in users {
if let userName = user.value(forKey: "name") as? String {
print("Fetched user: \(userName)")
// 获取到的用户:\(userName)
}
}
return users
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
// 无法获取。
return []
}
}
// Example Usage (e.g., in viewDidLoad or an IBAction)
// 示例用法(例如,在 viewDidLoad 或某个 IBAction 中)
// saveNewUser(name: "Alice")
// let allUsers = fetchUsers()

Core Data 涉及更多的设置,但对于复杂的数据模型和大型数据集来说非常强大。do-catch 块对于错误处理至关重要。

管理屏幕键盘对于良好的用户体验很重要。

将此代码添加到您的 UIViewController 子类中:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true) // Dismisses keyboard for any active text field in the view
// 收起视图中任意活动的文本字段的键盘
}
  1. 让您的视图控制器遵循 UITextFieldDelegate 协议: class ViewController: UIViewController, UITextFieldDelegate { ... }
  2. 在 viewDidLoad() 中,为您的一个或多个文本字段设置委托: myTextField.delegate = self(其中 myTextField 是连接到您的 UITextField 的 outlet)
  3. 实现委托方法:
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder() // Dismisses the keyboard for this specific text field
// 收起此特定文本字段的键盘
return true // Indicates the return key press was handled
// 表示 Return 键按下事件已被处理
}

要在您的应用程序中显示网页内容,请使用 WKWebView(来自 WebKit 框架)。UIWebView 已被弃用 (deprecated)。

  1. 将 WKWebView 添加到您的视图中:在 Storyboard 中,在对象库中搜索 ‘Web Kit View’ 并将其拖拽到您的视图上。根据需要添加约束。为其创建一个 outlet(例如,webView)。
  2. 导入 WebKit:在您的 ViewController.swift 中添加 import WebKit。
  3. 加载 URL:
import UIKit
import WebKit
class WebViewController: UIViewController, WKNavigationDelegate {
@IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView.navigationDelegate = self // Optional, for handling navigation events
// 可选,用于处理导航事件
guard let url = URL(string: "https://www.apple.com") else { return }
let request = URLRequest(url: url)
webView.load(request)
}
// Example WKNavigationDelegate methods (optional)
// WKNavigationDelegate 方法示例(可选)
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print("Started to load")
// 开始加载
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Finished loading")
// 加载完成
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("Failed to load: \(error.localizedDescription)")
// 加载失败:\(error.localizedDescription)
}
}

请记住 App Transport Security (ATS):如果加载非 HTTPS 的 URL,您需要在 Info.plist 中配置例外(例如,NSAllowsArbitraryLoads 或特定域名的例外),但强烈建议使用 HTTPS。

这些高级主题为构建复杂和交互式的 iOS 应用程序奠定了坚实的基础。继续探索 Apple 的文档并尝试使用这些功能。