iOS 开发 - 高级 iOS
使用 Swift 进行 iOS 开发 - 高级 iOS 主题
Section titled “使用 Swift 进行 iOS 开发 - 高级 iOS 主题”本章深入探讨了构建功能丰富的 iOS 应用程序的关键高级 iOS 功能。我们将涵盖创建多屏应用、导航、使用表格视图 (Table Views) 显示数据列表、数据持久化 (data persistence) 以及基本的网页内容显示 (web content display)。
请仔细阅读每个部分,因为这些概念构成了许多 iOS 应用程序的基础。
多个视图控制器和导航
Section titled “多个视图控制器和导航”大多数应用程序由多个屏幕组成。每个屏幕通常由一个 UIViewController(视图控制器)管理。我们将学习如何添加多个视图控制器并在它们之间进行导航 (navigation)。
从一个新项目或现有项目开始。默认情况下,新项目(使用 Storyboard)在 Main.storyboard 中带有一个 UIViewController 场景及其对应的 ViewController.swift 文件。Storyboard 中指向此场景的箭头表示它是初始视图控制器 (initial view controller),即入口点。
添加第二个视图控制器
Section titled “添加第二个视图控制器”- 打开
Main.storyboard。 - 打开对象库 (Object Library)(Shift-Command-L)。
- 搜索 ‘View Controller’ 并将一个新的 View Controller 对象拖拽到 Storyboard 画布上,放在现有视图控制器的旁边。
- 为此视图控制器创建一个新的 Swift 文件:在项目导航器中右键单击您的项目文件夹 > New File… > 选择 ‘Cocoa Touch Class’ > Next。命名文件(例如,
SecondViewController),使其成为UIViewController的子类 (subclass),确保“Language”是 Swift,并取消勾选“Also create XIB file”。点击 Next,然后 Create。 - 将 Storyboard 场景链接到这个新类:在 Storyboard 中选择新的 View Controller 场景。转到身份检查器 (Identity Inspector)(检查器面板中从右数第三个图标)。在“Class”字段中,输入并选择
SecondViewController。
您现在有了第二个视图控制器场景及其 Swift 类,但还没有办法到达它。
为应用程序添加导航
Section titled “为应用程序添加导航”从一个视图控制器转换到另一个视图控制器通常使用 Storyboard 中的“Segues”(转场)来实现,这些 Segues 通常由 UINavigationController 管理。
使用导航控制器和 Segues
Section titled “使用导航控制器和 Segues”- 将第一个视图控制器嵌入到导航控制器中:在
Main.storyboard中选择您的初始视图控制器场景。转到 Xcode 菜单:Editor > Embed In > Navigation Controller。这将在您的第一个视图控制器之前添加一个导航控制器场景,并为您的第一个视图控制器添加一个导航栏 (navigation bar)。 - 向第一个视图控制器添加一个按钮:将一个
UIButton拖拽到您的第一个视图控制器的视图上。将其标题设为 ‘Go to Second View’。 - 创建一个 Segue:按住 Control 键从该按钮拖拽到
SecondViewController场景。将出现一个弹窗。在“Action Segue”下,选择“Show”。这将创建一个 segue,当按钮被点击时,会将第二个视图控制器推送到导航堆栈 (navigation stack) 上。
运行应用程序。您将看到带导航栏的第一个视图控制器。点击按钮将转换到第二个视图控制器,并且导航栏会自动显示一个“Back”(返回)按钮以返回到第一个视图。
自定义导航栏
Section titled “自定义导航栏”选择视图控制器的导航项 (Navigation Item)(它出现在 Storyboard 文档大纲中的导航栏内部,或者通过点击视图控制器场景内的导航栏区域来选择)。在属性检查器 (Attributes Inspector) 中,您可以设置:
- Title:该视图控制器在导航栏中心显示的标题。
- Prompt:标题上方较小的一行文本。
- Back Button:自定义当该视图控制器被推出后,在 下一个 屏幕上出现的返回按钮的文本。
您还可以向导航栏添加 UIBarButtonItems(例如,用于“编辑”、“添加”、“完成”等操作)。从对象库中将一个“Bar Button Item”拖拽到 Storyboard 中的导航栏上。要使其触发 segue,按住 Control 键从该导航栏按钮项拖拽到另一个视图控制器,然后选择“Show”。
表格视图 (UITableView)
Section titled “表格视图 (UITableView)”UITableView 用于以单列行列表的形式显示数据,这些行可以分组到节 (sections) 中。它对于显示大型数据集非常高效。
我们来创建一个简单的表格视图。创建一个新项目或使用现有的视图控制器。
在 Storyboard 中设置表格视图
Section titled “在 Storyboard 中设置表格视图”- 从对象库中将一个“Table View”拖拽到您的视图控制器的视图上。使用 Auto Layout 使其填满视图。
- 原型单元格 (Prototype Cells):选择表格视图。在属性检查器中,将“Prototype Cells”设置为 1。这会在 Storyboard 中为表格视图添加一个模板单元格 (template cell)。
- 单元格标识符 (Cell Identifier):选择“Table View Cell”(可以直接选择,或通过文档大纲选择)。在其属性检查器中,将“Identifier”设置为一个唯一的字符串,例如 ‘MyCell’。此标识符用于在代码中复用单元格 (dequeue cells)。
- 样式 (Optional):您可以在属性检查器中更改单元格的“Style”(例如,Basic, Right Detail, Subtitle),以获得预配置的标签布局。
连接数据源和委托
Section titled “连接数据源和委托”表格视图需要一个数据源 (data source)(提供数据)和一个委托 (delegate)(处理交互和外观)。
- 在 Storyboard 中:按住 Control 键从表格视图拖拽到其场景顶部的视图控制器图标(黄色圆圈)。选择“dataSource”。重复此操作并选择“delegate”。
- 在
ViewController.swift中:遵循协议 (protocols):class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { ... } - 为表格视图添加一个 outlet(插座变量):按住 Control 键从 Storyboard 中的表格视图拖拽到您的
ViewController.swift文件,并创建一个名为tableView的 outlet。
实现必需的 UITableViewDataSource 方法:
// Sample data array// 示例数据数组let dataArray = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
// MARK: - UITableViewDataSourcefunc 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)
Section titled “示例:计时器应用 (使用 Timer)”此应用程序演示了如何使用 Timer 创建一个倒计时 (countdown)。UI 将有一个标签来显示时间,以及用于开始、暂停、重置和调整时间的按钮。
UI 设置 (概念)
Section titled “UI 设置 (概念)”- 导航栏:标题“Egg Timer”。左侧导航栏按钮项 (Left Bar Button Item)“Pause”(例如,系统暂停图标)。右侧导航栏按钮项 (Right Bar Button Item)“Start”(例如,系统播放图标)。
- 中心标签 (Center Label):大标签,显示剩余时间(例如,
timeLeftLabel)。初始值为“210”。 - 工具栏 (底部 Toolbar):
- 导航栏按钮项 (Bar Button Item)“-10s”。
- 灵活间隔导航栏按钮项 (Flexible Space Bar Button Item)。
- 导航栏按钮项“Reset”。
- 灵活间隔导航栏按钮项。
- 导航栏按钮项“+10s”。
将这些 UI 元素(标签作为 outlet,按钮作为 action)连接到您的
ViewController.swift。
核心逻辑 (Swift 代码)
Section titled “核心逻辑 (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 方法。
将数据存储到本地存储
Section titled “将数据存储到本地存储”应用程序通常需要在设备本地保存数据。两种常见的方法是 UserDefaults(用于存储少量数据,如设置)和 Core Data(用于存储更大、结构化的数据集)。
UserDefaults
Section titled “UserDefaults”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// 如果未找到,默认为 0let 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
Section titled “Core Data”Core Data 是一个强大的对象图和持久化框架。它允许您对结构化数据进行建模、保存和检索。
- 项目设置 (Project Setup):创建新项目时,勾选“Use Core Data”复选框。这会在
AppDelegate.swift中添加一个.xcdatamodeld文件和用于 Core Data 堆栈 (NSPersistentContainer) 的样板代码 (boilerplate code)。 - 数据模型 (Data Model):打开
.xcdatamodeld文件。点击“Add Entity”创建一个表(例如,“User”)。选择实体并添加“Attributes”(列,例如类型为 String 的name,类型为 Integer 16/32/64 的age)。 - 访问上下文 (Accessing the Context):
AppDelegate中的NSPersistentContainer提供了viewContext,它是您处理托管对象 (managed objects) 的工作区 (scratchpad)。
import UIKitimport CoreData
// Assuming 'User' entity with 'name' (String) and 'creationDate' (Date) attributes// 假设存在 'User' 实体,包含 'name' (String) 和 'creationDate' (Date) 属性
// Get the AppDelegate and NSManagedObjectContext// 获取 AppDelegate 和 NSManagedObjectContextguard 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 块对于错误处理至关重要。
管理屏幕键盘对于良好的用户体验很重要。
点击文本字段外部时收起键盘
Section titled “点击文本字段外部时收起键盘”将此代码添加到您的 UIViewController 子类中:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { self.view.endEditing(true) // Dismisses keyboard for any active text field in the view // 收起视图中任意活动的文本字段的键盘}按下 Return 键时收起键盘
Section titled “按下 Return 键时收起键盘”- 让您的视图控制器遵循
UITextFieldDelegate协议:class ViewController: UIViewController, UITextFieldDelegate { ... } - 在
viewDidLoad()中,为您的一个或多个文本字段设置委托:myTextField.delegate = self(其中myTextField是连接到您的UITextField的 outlet) - 实现委托方法:
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)
Section titled “显示网页内容 (WKWebView)”要在您的应用程序中显示网页内容,请使用 WKWebView(来自 WebKit 框架)。UIWebView 已被弃用 (deprecated)。
- 将
WKWebView添加到您的视图中:在 Storyboard 中,在对象库中搜索 ‘Web Kit View’ 并将其拖拽到您的视图上。根据需要添加约束。为其创建一个 outlet(例如,webView)。 - 导入 WebKit:在您的
ViewController.swift中添加import WebKit。 - 加载 URL:
import UIKitimport 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 的文档并尝试使用这些功能。