GeometryReader 是 SwiftUI 里专门用来读取父视图尺寸和位置信息的容器。很多人第一次用它都会被坑,因为它看起来像个布局工具,实际上它的主要职责是: 获取几何信息(Geometry),顺便参与布局。
GeometryReader { geometry in
Text("Hello")
}
// 其中
geometry.size.width
geometry.size.height
// 表示 GeometryReader 实际获得的空间的大小例如
struct ContentView: View {
var body: some View {
GeometryReader { geo in
VStack {
Text("宽度: \(Int(geo.size.width))")
Text("高度: \(Int(geo.size.height))")
}
}
}
}运行后会显示当前可用区域大小
struct ContentView: View {
var body: some View {
GeometryReader { geo in
Color.red
}
}
}红色会铺满整个 GeometryReader 获得的空间。
获取百分比布局,例如做响应式 UI
GeometryReader { geo in
Rectangle()
.fill(.blue)
.frame(width: geo.size.width * 0.8)
}获取安全区域
GeometryReader { geo in
Text("Top Safe Area")
Text("\(geo.safeAreaInsets.top)")
}获取坐标,GeometryReader 最强大的地方
local 相对于自身 geo.frame(in: .local) 返回
(0,0,width,height) 因为坐标原点就是自己。
global 相对于整个屏幕 geo.frame(in: .global),
可能会输出 (x:20,y:120,w:300,h:50) ``` GeometryReader { geo
in
Text(“Hello”) .onAppear { print( geo.frame(in: .global) ) } } ```
named 自定义坐标系
.coordinateSpace(name: "container") 读取
geo.frame(in: .named("container"))
大型滚动页面经常使用。
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { geo in
VStack(spacing: 20) {
Text("宽: \(Int(geo.size.width))")
Text("高: \(Int(geo.size.height))")
Rectangle()
.fill(.blue)
.frame(
width: geo.size.width * 0.8,
height: 100
)
Circle()
.fill(.green)
.frame(
width: min(
geo.size.width,
geo.size.height
) * 0.3
)
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
}
}
}
#Preview {
ContentView()
}Shape 是一个继承自 View 的协议,换句话说 所有的 Shape 也都是 View,SwiftUI 已经 内置了许多 Shape,例如:RoundedRectangle 圆角矩形、Circle 圆形、Capsule 胶囊形 等等。
如果想创建属于自己的 Shape(自定义形状)怎么办?Shape 协议(通过扩展)已经帮你实现了 View 的 body 属性。 但是,它新增了一个你必须实现的方法:
func path(in rect: CGRect) -> Path {
return a Path
}在这个方法里,你需要创建并返回一个 Path,它可以绘制任何你想要的图形。Path 提供了大量用于绘图的函数(可以查看官方文档)。
它能够组合:直线 lines、圆弧 arcs、贝塞尔曲线 Bezier curves 等等,从而构成一个完整的 Shape 形状。
import SwiftUI
struct Star: SwiftUI.Shape{
var points: Int = 5
var sharpness: CGFloat = 0.5 // 内外半径比
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
let outer = min(rect.width, rect.height) / 2
let inner = outer * sharpness
let step = Double.pi / Double(points)
for i in 0..<(points * 2) {
let radius = i % 2 == 0 ? outer : inner
let angle = Double(i) * step - .pi / 2
let p = CGPoint(
x: center.x + CGFloat(cos(angle)) * radius,
y: center.y + CGFloat(sin(angle)) * radius
)
if i == 0 { path.move(to: p) }
else { path.addLine(to: p) }
}
path.closeSubpath()
return path
}
}
struct ContentView: View {
var body: some View {
Star()
}
}
#Preview {
ContentView()
}import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { geo in
Path { path in
path.move(
to: CGPoint(
x: geo.size.width / 2,
y: 0
)
)
path.addLine(
to: CGPoint(
x: geo.size.width,
y: geo.size.height
)
)
path.addLine(
to: CGPoint(
x: 0,
y: geo.size.height
)
)
path.closeSubpath()
}
.fill(.red)
}
}
}
#Preview {
ContentView()
}.contentShape() 是 SwiftUI 中一个非常重要但容易被忽略的
Modifier,它的作用是:
定义 View 哪一部分参与命中测试(Hit Testing),也就是响应点击、手势、拖拽、Hover 等交互的区域。
例如
Text("Hello")
.padding()
.background(.blue)
.onTapGesture {
print("Tap")
}看起来整个蓝色区域都应该可以点击。实际上:默认只有 Text 本身可以点击。padding 出来的空白可能并不会参与 Hit Test。
Text("Tap Me")
.padding(50)
.background(.blue)
.foregroundStyle(.white)
.onTapGesture {
print("点击")
}整个 Rectangle 都变成点击区域。加入 contentShape。
| Modifier | 作用 | 是否影响显示 | 是否影响点击 |
|---|---|---|---|
.frame() |
改变布局大小 | ✅ | ❌(不直接决定命中区域) |
.background() |
添加背景 | ✅ | ❌ |
.clipShape() |
裁剪显示 | ✅ | ❌(通常不改变命中区域) |
.contentShape() |
定义命中测试区域 | ❌ | ✅ |
获取用户输入,SwiftUI 拥有强大的原语,用于识别由用户手指做出的“手势”(“gestures”)。这被称为多点触控(因为可以同时涉及多根手指)。
当多点触控手势发生时,SwiftUI 几乎完全负责了“识别”工作。你唯一需要做的就是去处理这些手势。换句话说,决定当用户进行拖拽(drags)、捏合(pinches)或轻点(taps)时该做什么。
让你的视图 View 识别手势,为了让你的视图开始识别某种特定的手势,你需要使用 .gesture 视图修饰符(View modifier)。
myView.gesture(theGesture) // theGesture 必须实现 Gesture 协议 (protocol)创建一个手势,通常,theGesture 将由你创建的某个函数(func)或计算属性(computed var)来创建。 或者也可能是你视图(View)的 body 属性内部的一个局部变量(local var)。
var theGesture: some Gesture {
return TapGesture(count: 2)
}这恰好是一个“双击”手势(因为 count: 2)。SwiftUI 现在会识别这个 TapGesture,但它不会对此做任何事情……
如何对一个被识别出的手势做点什么呢,这取决于该手势是离散的还是非离散(连续)的。
TapGesture(轻点手势)是一个离散手势。它属于“一次性发生”(all at once),并且在被识别时只做一件事。
LongPressGesture(长按手势)也可以被当作离散手势来处理。为了在识别出离散手势时“做点什么”,我们使用
.onEnded { }。
var theGesture: some Gesture {
return TapGesture(count: 2)
.onEnded { /* 做点什么 do something */ }
}离散手势还拥有你已经熟知的“便捷视图修饰符”(convenience view modifiers)。
myView.onTapGesture(count: Int) { /* 做点什么 do something */ }
myView.onLongPressGesture(...) { /* 做点什么 do something */ }import SwiftUI
struct ContentView: View {
@State private var message = "等待手势..."
@State private var color: Color = .blue
@State private var tapCount = 0
@State private var longPressCount = 0
var body: some View {
VStack(spacing: 30) {
Text("离散手势 Demo")
.font(.largeTitle.bold())
//--------------------------------------
// 使用 Gesture + onEnded
//--------------------------------------
RoundedRectangle(cornerRadius: 20)
.fill(color)
.frame(width: 250, height: 120)
.overlay(
Text("双击我")
.font(.title2)
.foregroundStyle(.white)
)
.gesture(doubleTapGesture)
//--------------------------------------
// 使用便捷 Modifier
//--------------------------------------
RoundedRectangle(cornerRadius: 20)
.fill(.green)
.frame(width: 250, height: 120)
.overlay(
Text("长按我")
.font(.title2)
.foregroundStyle(.white)
)
.onLongPressGesture(minimumDuration: 1.0) {
longPressCount += 1
message = "长按成功!"
color = .orange
}
//--------------------------------------
// 使用 onTapGesture
//--------------------------------------
RoundedRectangle(cornerRadius: 20)
.fill(.purple)
.frame(width: 250, height: 120)
.overlay(
Text("单击我")
.font(.title2)
.foregroundStyle(.white)
)
.onTapGesture {
message = "点击了紫色区域"
}
Divider()
VStack(alignment: .leading, spacing: 10) {
Text("状态信息")
.font(.headline)
Text("消息:\(message)")
Text("双击次数:\(tapCount)")
Text("长按次数:\(longPressCount)")
}
Spacer()
}
.padding()
.animation(.smooth, value: color)
}
// MARK: - TapGesture + onEnded
var doubleTapGesture: some Gesture {
TapGesture(count: 2)
.onEnded {
tapCount += 1
message = "检测到双击!"
color = color == .blue ? .red : .blue
print("Double Tap")
}
}
}
#Preview {
ContentView()
}其他手势是非离散的(例如:DragGesture 拖拽、MagnifyGesture 缩放、RotationGesture 旋转)。
对于这些手势,你可以在手势的 value(值)发生变化时同步更新你的状态。
var theGesture: some Gesture {
DragGesture(...)
.onChanged { value in /* 根据 value 改变某些视图或模型的状态 change some View or Model state based on value */ }
.onEnded { value in /* 在手势结束时做点什么 do something at the end of the gesture */ }
}这个 .onChanged 会导致你传给它的闭包在手指移动时被调用。该 value 的具体内容因手势而异。
import SwiftUI
struct ContentView: View {
// 当前偏移
@State private var offset: CGSize = .zero
// 当前缩放
@State private var scale: CGFloat = 1.0
// 当前旋转角度
@State private var rotation: Angle = .zero
var body: some View {
VStack(spacing: 30) {
Text("非离散手势 Demo")
.font(.largeTitle.bold())
Text("""
拖拽:移动方块
双指缩放:改变大小
双指旋转:旋转方块
""")
.multilineTextAlignment(.center)
RoundedRectangle(cornerRadius: 20)
.fill(.blue.gradient)
.frame(width: 180, height: 180)
// 当前状态
.scaleEffect(scale)
.rotationEffect(rotation)
.offset(offset)
// 同时响应多个手势
.gesture(dragGesture)
.simultaneousGesture(magnifyGesture)
.simultaneousGesture(rotationGesture)
.animation(.spring(), value: offset)
.animation(.spring(), value: scale)
.animation(.spring(), value: rotation)
Divider()
VStack(alignment: .leading, spacing: 8) {
Text("当前状态")
.font(.headline)
Text("Offset : x = \(Int(offset.width)), y = \(Int(offset.height))")
Text(String(format: "Scale : %.2f", scale))
Text(String(format: "Rotation : %.1f°", rotation.degrees))
}
Spacer()
}
.padding()
}
// MARK: - DragGesture
var dragGesture: some Gesture {
DragGesture()
.onChanged { value in
// value.translation 表示拖动距离
offset = value.translation
}
.onEnded { value in
print("拖拽结束")
print("起点:\(value.startLocation)")
print("终点:\(value.location)")
print("拖动距离:\(value.translation)")
}
}
// MARK: - MagnifyGesture
var magnifyGesture: some Gesture {
MagnifyGesture()
.onChanged { value in
// value.magnification 是当前缩放比例
scale = value.magnification
}
.onEnded { value in
print("缩放结束:\(value.magnification)")
}
}
// MARK: - RotationGesture
var rotationGesture: some Gesture {
RotateGesture()
.onChanged { value in
rotation = value.rotation
}
.onEnded { value in
print("旋转结束:\(value.rotation.degrees)°")
}
}
}
#Preview {
ContentView()
}Storing Data Permanently 永久存储数据,除了 SwiftData 之外,在 iOS 中还有很多种数据 持久化 的 方法
这个示例展示了一个宏观的持久化概念对比,使用 @AppStorage
底层是 UserDefaults 存储轻量级偏好设置,并提示了其他持久化方案的存在
import SwiftUI
struct ContentView: View {
// 使用 @AppStorage 演示轻量级数据持久化 (底层为 UserDefaults)
@AppStorage("appLaunchCount") private var launchCount: Int = 0
@State private var showAdvancedDB = false
var body: some View {
VStack(spacing: 20) {
Image(systemName: "internaldrive")
.font(.system(size: 60))
.foregroundColor(.blue)
Text("Persistence 持久化")
.font(.largeTitle.bold())
Text("本应用已启动次数: \(launchCount)")
.font(.title2)
Text("这种轻量级数据适合使用 UserDefaults。")
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
.padding()
Button("模拟一次启动") {
launchCount += 1
}
.buttonStyle(.borderedProminent)
Divider()
Button("查看其他持久化方案") {
showAdvancedDB = true
}
}
.padding()
.onAppear {
// 每次视图出现,计数器+1
launchCount += 1
}
.confirmationDialog("其他持久化方案", isPresented: $showAdvancedDB, titleVisibility: .visible) {
Button("SwiftData") { }
Button("File System (FileManager)") { }
Button("CloudKit") { }
Button("Firebase (第三方)") { }
Button("取消", role: .cancel) { }
} message: {
Text("除了 UserDefaults,还可以使用上述方案进行重量级数据持久化。")
}
}
}
#Preview {
ContentView()
}你的应用程序看到的 iOS 文件系统就像一个普通的 Unix 文件系统。
它始于根目录 /,当然 这里也存在文件保护机制,就像普通的
Unix 一样,所以你无法看到所有的内容。 事实上,你只能在你的应用程序“沙盒
sandbox” 中进行读取和(有时可以进行)写入操作。
为什么需要沙盒?
沙盒里都有什么?
访问文件系统:
当我们访问文件系统时,我们总是从一个指向特定目录的 URL 开始。这些目录可以通过 URL 的静态属性来获取……
URL.documentsDirectory(文档目录 URL)URL.applicationSupportDirectory(应用支持目录
URL)URL.cachesDirectory(缓存目录 URL)etc.(等等)这个示例演示了如何获取并展示沙盒中不同的系统目录 URL
import SwiftUI
struct ContentView: View {
@State private var directories: [(String, URL)] = []
var body: some View {
NavigationStack {
List(directories, id: \.0) { item in
VStack(alignment: .leading, spacing: 4) {
Text(item.0)
.font(.headline)
Text(item.1.path)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
.navigationTitle("File System 沙盒")
.toolbar {
Button("获取目录") {
fetchDirectories()
}
}
}
}
private func fetchDirectories() {
directories = []
// 获取系统提供的沙盒目录 URL
directories.append(("Documents 目录", URL.documentsDirectory))
directories.append(("Application Support 目录", URL.applicationSupportDirectory))
directories.append(("Caches 目录", URL.cachesDirectory))
// 获取 App Bundle 目录
if let bundleURL = Bundle.main.bundleURL.path(percentEncoded: false).split(separator: "/").dropLast().joined(separator: "/").asURL {
directories.append(("Bundle 目录", bundleURL))
}
}
}
// 简单的 String 转 URL 扩展,仅为演示
extension String {
var asURL: URL? {
URL(string: "file:///" + self)
}
}
#Preview {
ContentView()
}在这些系统路径之上构建
URL 方法:
func appendingPathComponent(String) -> URL
拼接路径组件,返回新的 URL)func appendingPathExtension(String) -> URL // e.g. "jpg"
(拼接文件扩展名,返回新的 URL // 例如 “jpg”)示例, 在文档目录下拼接一个名为 “filename.doc” 的文件路径
let url = URL.documentsDirectory.appendingPathComponent("filename.doc")了解 URL 的另一端有些什么
var isFileURL: Bool // is this a file URL (whether file exists or not) or something else?这是一个文件类型的 URL 吗(无论该文件实际是否存在),还是其他类型的 URL?
func resourceValues(forKeys: [URLResourceKey]) throws -> [URLResourceKey: Any]?获取指定键的资源属性值,该方法可能会抛出异常
示例键名:.creationDateKey(创建日期键)、.isDirectoryKey(是否为目录键)、.fileSizeKey(文件大小键)
从 URL 读取二进制数据,
init(contentsOf: URL, options: Data.ReadingOptions) throws例如
let data = try Data(contentsOf: url)这里让 options 保持默认值 [] ,这个函数会 throw
抛出错误。
如果这个 URL 是网络 URL,这个方法并不能正确用于网络请求,而且它会阻塞线程(blocks),因此绝不要用它来读取网络资源。 它只应该用于读取本地文件(file URL)。
异步读取 URL 中的数据
let (data, urlresponse) =
try await URLSession.shared.data(from: url)这个方法既可能 throw 抛出错误,又是 async 异步的,因此需要使用 await。
urlresponse 中包含了:
很多时候我们会忽略这些内容,只使用 data。
由于这里需要 await,因此它必须运行在 异步执行环境(asynchronous execution context)中。
例如:
将二进制数据写入文件 URL:
func write(
to url: URL,
options: Data.WritingOptions
) throws -> Bool写入选项可以是,
try data.write(
to: fileURL,
options: [.atomic, .withoutOverwriting]
)这个示例演示了如何在系统目录上拼接路径组件和扩展名,并检查 URL 属性
import SwiftUI
struct ContentView: View {
@State private var fileURL: URL?
@State private var isFileURL = false
@State private var fileExists = false
var body: some View {
VStack(spacing: 20) {
Image(systemName: "link")
.font(.system(size: 50))
Text("URL 路径拼接与检查")
.font(.title2.bold())
if let url = fileURL {
VStack(spacing: 10) {
Text("完整路径:")
.font(.headline)
Text(url.path)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
Text("isFileURL: \(isFileURL ? "是" : "否")")
Text("文件是否存在: \(fileExists ? "是" : "否")")
}
.padding()
} else {
Text("点击下方按钮构建 URL")
.foregroundColor(.secondary)
}
Button("构建并检查 URL") {
buildAndCheckURL()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
private func buildAndCheckURL() {
// 1. 拼接路径组件
let docURL = URL.documentsDirectory
// 2. 拼接文件名和扩展名
let finalURL = docURL.appendingPathComponent("myProfile").appendingPathExtension("json")
fileURL = finalURL
isFileURL = finalURL.isFileURL
// 检查文件是否存在
fileExists = FileManager.default.fileExists(atPath: finalURL.path)
}
}
#Preview {
ContentView()
}提供各种文件操作功能,例如:
fileExists(atPath: String) -> Bool用于判断指定路径上的文件是否存在:还可以完成:
线程安全 Thread Safety,只要同一个 FileManager 实例始终只在一个线程中使用,它就是线程安全的。
Delegate(代理),FileManager 还提供了 Delegate(代理)。当某些事件发生时,会调用代理对象中的对应方法,你可以借此介入文件操作过程。
这个示例演示了使用 FileManager 创建目录、检查存在性以及删除文件。
import SwiftUI
struct ContentView: View {
@State private var logText = ""
private let fm = FileManager.default
var body: some View {
VStack(spacing: 20) {
Image(systemName: "folder.badge.gearshape")
.font(.system(size: 50))
Text("FileManager 演示")
.font(.title2.bold())
ScrollView {
Text(logText)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(height: 150)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
HStack {
Button("创建目录") { createDirectory() }
Button("检查存在") { checkExistence() }
Button("删除目录") { removeDirectory() }
}
.buttonStyle(.bordered)
}
.padding()
.onAppear { logText = "准备就绪...\n" }
}
private var customDirURL: URL {
URL.documentsDirectory.appendingPathComponent("MyCustomFolder")
}
private func createDirectory() {
do {
try fm.createDirectory(at: customDirURL, withIntermediateDirectories: true)
logText += "✅ 目录已创建: \(customDirURL.lastPathComponent)\n"
} catch {
logText += "❌ 创建失败: \(error.localizedDescription)\n"
}
}
private func checkExistence() {
let exists = fm.fileExists(atPath: customDirURL.path)
logText += exists ? "🔍 目录存在\n" : "🔍 目录不存在\n"
}
private func removeDirectory() {
do {
try fm.removeItem(at: customDirURL)
logText += "🗑️ 目录已删除\n"
} catch {
logText += "❌ 删除失败: \(error.localizedDescription)\n"
}
}
}
#Preview {
ContentView()
}用于访问随应用一起打包发布的资源文件(支持本地化资源)。例如
图片、JSON、音频、字体、txt、plist、等各种资源,可以根据 文件名、文件扩展名 搜索并获取资源文件。
这个示例演示了如何从 App Bundle 中读取内置的资源文件(这里以读取项目自带的 Info.plist 为例)
import SwiftUI
struct ContentView: View {
@State private var bundleFiles: [String] = []
@State private var plistContent = ""
var body: some View {
VStack(spacing: 15) {
Image(systemName: "shippingbox")
.font(.system(size: 50))
Text("Bundle 资源包读取")
.font(.title2.bold())
Text("读取 App 内置的 Info.plist:")
.font(.headline)
ScrollView {
Text(plistContent)
.font(.system(.caption, design: .monospaced))
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxHeight: 200)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
.padding()
.onAppear {
readInfoPlist()
}
}
private func readInfoPlist() {
// 1. 在 Bundle 中查找指定名称和扩展名的资源
if let url = Bundle.main.url(forResource: "Info", withExtension: "plist") {
plistContent += "找到资源路径:\n\(url.path)\n\n"
// 2. 读取内容
do {
let data = try Data(contentsOf: url)
if let dict = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] {
plistContent += "内容解析成功:\n"
// 打印前几个 Key
for (key, value) in dict.prefix(3) {
plistContent += "\(key): \(value)\n"
}
}
} catch {
plistContent += "读取失败: \(error)"
}
} else {
plistContent = "未找到 Info.plist"
}
}
}
#Preview {
ContentView()
}本质上,它是一种将对象中的所有变量收集到一个可持久化的数据块中(例如 JSON 数据)的方式。
它是让 struct(结构体) 或 class(类) 能够持久化(保存到文件系统或其他地方)的一个非常好的方法。为了能够工作,你想要持久化的类型必须实现 Codable 协议。
对于只包含其他 Codable 类型的结构体(struct),Swift 会自动为你合成(synthesize)Codable 实现。
对于 enum(枚举) 也是如此,但前提是它没有关联值(associated data),或者关联值本身也是 Codable。
如果你的所有变量并不是 Codable(或者你使用的是 class),那么你就必须自己编写代码来实现 Codable。
已经实现 Codable 的标准类型,
String、Bool、Int、Double、Float、URL、Optional、Array、Dictionary、 Set、Data、Date、DateComponents、DateInterval、Calendar、CGFloat、 AffineTransform、CGPoint、CGSize、CGRect、CGVector
这个示例演示了 Swift 自动合成的 Codable,将一个结构体数组编码为 JSON 并解码回来。
import SwiftUI
struct Student: Codable {
var name: String
var age: Int
var gpa: Double
}
struct ContentView: View {
@State private var jsonString = ""
var body: some View {
VStack(spacing: 15) {
Image(systemName: "tray.and.arrow.down")
.font(.system(size: 50))
Text("Codable 自动合成")
.font(.title2.bold())
ScrollView {
Text(jsonString)
.font(.system(.body, design: .monospaced))
.padding()
}
.frame(maxHeight: .infinity)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
}
.padding()
.onAppear {
encodeDecodeDemo()
}
}
private func encodeDecodeDemo() {
let students = [
Student(name: "张三", age: 20, gpa: 3.8),
Student(name: "李四", age: 21, gpa: 3.5)
]
do {
// 编码为 JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(students)
// 转换为字符串展示
jsonString = String(data: jsonData, encoding: .utf8) ?? "转换失败"
// 解码回对象
let decodedStudents = try JSONDecoder().decode([Student].self, from: jsonData)
jsonString += "\n\n✅ 成功解码回来,第一个学生是: \(decodedStudents.first?.name ?? "未知")"
} catch {
jsonString = "Error: \(error)"
}
}
}
#Preview {
ContentView()
}Codable Implementation,如果你无法获得编译器自动合成(automatic synthesis)的 Codable 实现,那么你就需要自己实现它。
添加一个解码初始化方法 (Initializer)为你的 struct 或 class 添加一个初始化方法(也可以写在 extension 中):
init(from decoder: Decoder) throws {
...
}实现编码方法,还需要实现一个编码函数
func encode(to encoder: Encoder) throws {
...
}在这两个函数内部,你需要使用:Decoder(解码器)、Encoder(编码器)来保存(store)和恢复(restore)你的各个成员变量。
编码器/解码器(encoder/decoder)有一个名为 container(容器)的变量,你可以用它来存储/恢复变量……
init(from decoder: Decoder) throws {
self.init() // 如果是一个类(class)
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name) // name 是我的变量之一
...
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
}此外,你必须在上方定义一个名为 CodingKeys 的枚举(enum),并为你的每一个变量(vars)提供一个 case……
enum CodingKeys: String, CodingKey { // 必须遵循 CodingKey 协议
case name
case fubar = "foo"
. . .
}这个示例演示了当变量名与JSON键名不同时,如何手动实现 Codable 和 CodingKeys。
import SwiftUI
// 自定义实现 Codable
struct ServerConfig: Codable {
var appName: String
var apiKey: String
// 定义 CodingKeys 映射代码名与 JSON 键名
enum CodingKeys: String, CodingKey {
case appName = "application_name" // JSON 中叫 application_name
case apiKey = "api_key" // JSON 中叫 api_key
}
init(appName: String, apiKey: String) {
self.appName = appName
self.apiKey = apiKey
}
// 自定义解码
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
appName = try container.decode(String.self, forKey: .appName)
apiKey = try container.decode(String.self, forKey: .apiKey)
}
// 自定义编码
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(appName, forKey: .appName)
try container.encode(apiKey, forKey: .apiKey)
}
}
struct ContentView: View {
@State private var resultText = ""
var body: some View {
VStack(spacing: 15) {
Image(systemName: "hammer")
.font(.system(size: 50))
Text("Codable 手动实现")
.font(.title2.bold())
ScrollView {
Text(resultText)
.font(.system(.body, design: .monospaced))
.padding()
}
.frame(maxHeight: .infinity)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
}
.padding()
.onAppear {
manualCodableDemo()
}
}
private func manualCodableDemo() {
let originalConfig = ServerConfig(appName: "MySwiftApp", apiKey: "12345-ABCDE")
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
// 编码
let jsonData = try encoder.encode(originalConfig)
let jsonString = String(data: jsonData, encoding: .utf8)!
resultText = "编码后的 JSON:\n\(jsonString)\n\n"
// 模拟从服务端获取到的 JSON 进行解码
let mockServerJSON = """
{
"application_name" : "DownLoadedApp",
"api_key" : "99999-ZZZZZ"
}
"""
let data = mockServerJSON.data(using: .utf8)!
let decodedConfig = try JSONDecoder().decode(ServerConfig.self, from: data)
resultText += "解码后的对象:\nappName: \(decodedConfig.appName)\napiKey: \(decodedConfig.apiKey)"
} catch {
resultText = "Error: \(error)"
}
}
}
#Preview {
ContentView()
}一旦你的结构体 (struct)、类 (class) 或枚举 (enum) 实现了 Codable 协议,你就可以将它转换为 JSON(一种标准格式)。
struct MyType: Codable {
var someDate: Date
var someString: String
var other: SomeOtherType // SomeOtherType 也必须是 Codable 的!
}这个结构体可以被转换为 JSON,看起来会像下面这样
{
"someDate" : "2017-11-05T16:30:00Z",
"someString" : "Hello",
"other" : <无论 SomeOtherType 在 JSON 中长什么样>
}如何将编码 Codable 转换为 JSON 格式
let object: MyType = ... // MyType 必须遵循 Codable 协议
let jsonData: Data = try JSONEncoder().encode(object)JSON 是一种文本格式,你可以像这样用你的 jsonData 生成一个 JSON 字符串
let jsonString = String(data: jsonData, encoding: .utf8) // JSON 总是使用 UTF-8 编码也可以直接将 jsonData 写入到通过 FileManager 获取的指定 URL 文件中
try jsonData.write(to: url) // 注意:这同样会抛出异常如何将 jsonData 转换回结构体 类
if let myObject: MyType = try? JSONDecoder().decode(MyType.self, from: jsonData) {
// 使用刚从 jsonData 解码出来的 myObject 执行某些操作
}这个示例专注于将获取到的 JSON 数据写入本地文件,然后再从文件读取并解码。
import SwiftUI
struct MyBook: Codable, Identifiable {
var id: Int
var title: String
var author: String
}
struct ContentView: View {
@State private var books: [MyBook] = []
@State private var statusMessage = ""
var body: some View {
VStack {
Image(systemName: "books.vertical")
.font(.system(size: 50))
.padding(.top)
Text("JSON 文件读写")
.font(.title2.bold())
Text(statusMessage)
.foregroundColor(.blue)
List(books) { book in
VStack(alignment: .leading) {
Text(book.title).bold()
Text("作者: \(book.author)").font(.caption).foregroundColor(.gray)
}
}
HStack {
Button("保存 JSON 到文件") {
saveJSONToFile()
}
.buttonStyle(.borderedProminent)
Button("从文件加载 JSON") {
loadJSONFromFile()
}
.buttonStyle(.bordered)
}
.padding()
}
}
private var fileURL: URL {
URL.documentsDirectory.appendingPathComponent("books.json")
}
private func saveJSONToFile() {
let sampleBooks = [
MyBook(id: 1, title: "SwiftUI 实战", author: "Apple"),
MyBook(id: 2, title: "iOS 编程", author: "Big Nerd Ranch")
]
do {
// 1. 编码为 JSON Data
let jsonData = try JSONEncoder().encode(sampleBooks)
// 2. 写入文件系统
try jsonData.write(to: fileURL, options: [.atomic])
statusMessage = "✅ 保存成功! (\(fileURL.lastPathComponent))"
books = sampleBooks
} catch {
statusMessage = "❌ 保存失败: \(error.localizedDescription)"
}
}
private func loadJSONFromFile() {
do {
// 1. 从文件读取 Data
let data = try Data(contentsOf: fileURL)
print(fileURL) // macos 访达 + shift command + g 前往路径 可以看到 book.json
// 2. 解码为对象数组
let decodedBooks = try JSONDecoder().decode([MyBook].self, from: data)
books = decodedBooks
statusMessage = "✅ 读取成功,共 \(decodedBooks.count) 本书。"
} catch {
statusMessage = "❌ 读取失败: \(error.localizedDescription) (请先点击保存)"
books = []
}
}
}
#Preview {
ContentView()
}UserDefaults 是一个非常简单、持久化的、类似于字典(dictionary-like)的东西。
要使用它,你需要一个 UserDefaults 的实例。最常用的是这一个:
let defaults = UserDefaults.standard存储数据 Storing Data
要存储一些东西
defaults.set(object, forKey: "SomeKey") // object 必须是一个属性列表(Property List)属性列表(Property List)是一个概念(不是一个协议或具体的类型)。
String、Int、Double、[Property List](数组)、Dictionary<String, List Property>(字典)都是属性列表。
并且 Data 也是一个属性列表,因此任何实现了 Codable 协议的对象都可以被转换为 JSON 数据,并放入 UserDefaults 中。
为了方便起见,还有许多诸如此类的函数
defaults.setDouble(37.5, forKey: "MyDouble")用于唯一标识的键(forKey)字符串在你的整个应用程序中是共享的,因此请选择良好且不冲突的名称。
检索数据 (Retrieving Data),要检索某些内容
let i: Int? = defaults.integer(forKey: "MyInteger")
let b: Data? = defaults.data(forKey: "MyData")
let u: URL? = defaults.url(forKey: "MyURL")
let strings: [String]? = defaults.stringArray(forKey: "MyStrings")
// 等等...进阶:获取字典或包含任意内容的数组
检索一个字典或一个包含“任何内容”的数组,但字符串(String)除外,情况会更复杂一些……
let a = array(forKey: "MyArray")
// 将会返回一个 Array<Any>?(可选的任意类型数组)。在此时,你必须使用 Swift 中的 as 操作符来对数组元素进行“类型转换”(type cast)。
这个示例演示了如何使用 UserDefaults 存储和读取各种基本类型,以及通过 Data 存储遵循 Codable 的对象。
import SwiftUI
struct UserSettings: Codable {
var username: String
var notificationsEnabled: Bool
}
struct ContentView: View {
let defaults = UserDefaults.standard
@State private var username: String = ""
@State private var notificationsEnabled: Bool = false
@State private var favoriteNumber: Int = 0
var body: some View {
NavigationStack {
Form {
Section("基本类型") {
TextField("用户名", text: $username)
.onChange(of: username) { _, newValue in
defaults.set(newValue, forKey: "userNameKey")
}
Toggle("开启通知", isOn: $notificationsEnabled)
.onChange(of: notificationsEnabled) { _, newValue in
defaults.set(newValue, forKey: "notificationsKey")
}
Stepper("最喜欢的数字: \(favoriteNumber)", value: $favoriteNumber, in: 0...100)
.onChange(of: favoriteNumber) { _, newValue in
defaults.set(newValue, forKey: "favNumberKey")
}
}
Section("操作") {
Button("保存 Codable 对象") {
saveCodableObject()
}
Button("读取所有数据并刷新界面") {
loadAllData()
}
Button("清除所有数据", role: .destructive) {
defaults.removeObject(forKey: "userNameKey")
defaults.removeObject(forKey: "notificationsKey")
defaults.removeObject(forKey: "favNumberKey")
defaults.removeObject(forKey: "userSettingsKey")
loadAllData()
}
}
}
.navigationTitle("UserDefaults 演示")
}
.onAppear {
loadAllData()
}
}
private func saveCodableObject() {
let settings = UserSettings(username: username.isEmpty ? "Guest" : username, notificationsEnabled: notificationsEnabled)
// 将 Codable 对象转为 JSON Data 存入 UserDefaults
if let data = try? JSONEncoder().encode(settings) {
defaults.set(data, forKey: "userSettingsKey")
print("Codable 对象已保存到 UserDefaults")
}
}
private func loadAllData() {
// 读取基本类型
username = defaults.string(forKey: "userNameKey") ?? ""
notificationsEnabled = defaults.bool(forKey: "notificationsKey")
favoriteNumber = defaults.integer(forKey: "favNumberKey")
// 读取 Codable 对象
if let data = defaults.data(forKey: "userSettingsKey") {
if let settings = try? JSONDecoder().decode(UserSettings.self, from: data) {
print("读取到的 Codable 对象: \(settings)")
// Update UI with loaded settings
username = settings.username
notificationsEnabled = settings.notificationsEnabled
}
}
}
}
#Preview {
ContentView()
}Info.plist Values
Support Document Browser 更改为 YES。
iPhone 可以在文件 我的iPhone 中查看文件。