在 SwiftUI 中解构(解析)JSON 文件通常涉及以下几个步骤:
1、准备 JSON 数据:可以是本地文件,也可以是网络请求获取的内容。
2、定义 Swift 数据模型:遵循 Codable 协议,用于与 JSON 数据结构匹配。
3、解码 JSON:使用 JSONDecoder 将 JSON 数据转换为 Swift 数据模型。
4、在 SwiftUI 中使用解构数据:通过 @State 或 @StateObject 等方式将解析结果绑定到界面。
解构示例
从本地 JSON 文件解析并展示数据
假设有一个名为 data.json 的文件,内容如下:
[
{
"id": 1,
"name": "Alice",
"age": 30
},
{
"id": 2,
"name": "Bob",
"age": 25
}
]
步骤 1:定义模型
struct User: Codable, Identifiable {
let id: Int
let name: String
let age: Int
}
定义的模型需要与JSON文件字段对应,字段格式对应(整数对应Int,字符串对应String),如果JSON文件字段较多,可考虑只解构需要的字段。
步骤 2:创建解码方法
将 JSON 文件加载为 Data 并解析为 User 数组:
import Foundation
func loadJSON() -> [User] {
guard let url = Bundle.main.url(forResource: "data", withExtension: "json"),
let data = try? Data(contentsOf: url) else {
return []
}
let decoder = JSONDecoder()
return (try? decoder.decode([User].self, from: data)) ?? []
}
解码代码中的Data(contentsOf:)用于读取指定URL的文件并初始化为一个Data对象。
JSONDecoder将Data数据解码为对应的类型格式,这里解码为[User]类型,表示User类型的数组。
如果解码类型为User.self,表示解码的对象是User类型。
解码成功,返回[User]类型的值,解码失败则报错。如解码失败但报错信息不能定位问题代码,可以通过DecodingError进一步了解报错信息。
步骤 3:绑定到 SwiftUI 界面
在 ContentView 中展示解析结果:
import SwiftUI
struct ContentView: View {
@State private var users: [User] = []
var body: some View {
List(users) { user in
VStack(alignment: .leading) {
Text(user.name).font(.headline)
Text("Age: \(user.age)").font(.subheadline)
}
}
.onAppear {
users = loadJSON()
}
}
}
从网络获取 JSON 数据并展示
步骤 1:定义模型
与本地文件示例相同。
步骤 2:创建网络请求方法
使用 URLSession 获取数据:
import Foundation
func fetchJSON(completion: @escaping ([User]) -> Void) {
guard let url = URL(string: "https://example.com/users.json") else {
completion([])
return
}
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
completion([])
return
}
let decoder = JSONDecoder()
let users = (try? decoder.decode([User].self, from: data)) ?? []
DispatchQueue.main.async {
completion(users)
}
}.resume()
}
使用URLSession获取JSON文件,然后通过JSONDecoder进行解码。
这里涉及URLSession、闭包、多线程任务框架(DispatchQueue.main.async)等知识,但总的流程是从互联网调取JSON文件,然后解码赋值,与本地JSON解码方式大体一致。
从互联网获取的文件之所以可以被JSONDecoder解码,是因为URL获取的JSON文件是Data类型。
补充知识:Data类型为“01101000 01100101…”用于网络传输、本地存储等功能,因此当获取到Data类型的JSON文件后,可以将其解码为对应的数据。更多了解详见《Swift为什么使用二进制(Data类型)?》
为什么从网页中下载的JSON文件是JSON格式,而这里的JSON却是Data类型?这是因为从网页上下载的JSON文件被浏览器/电脑自动识别并解码为可视化的JSON格式,无论是文件中的 JSON 数据,还是网络传输中的 JSON 数据,它们在计算机底层都以 Data 类型(二进制形式)存储和传输。
步骤 3:绑定到 SwiftUI 界面
import SwiftUI
struct ContentView: View {
@State private var users: [User] = []
var body: some View {
List(users) { user in
VStack(alignment: .leading) {
Text(user.name).font(.headline)
Text("Age: \(user.age)").font(.subheadline)
}
}
.onAppear {
fetchJSON { fetchedUsers in
users = fetchedUsers
}
}
}
}
关键点
1、错误处理:
使用 do-catch 处理解码或请求错误。
提供默认值避免解码失败导致崩溃。
2、性能优化:
对大数据集使用分页加载或后台线程解析。
在网络请求中添加缓存策略。
3、测试 JSON 文件:
保证 JSON 文件结构与模型匹配,避免解码失败。
实际应用
在学习Swift UI JSONDecoder解构JSON字符串时,发现存在JSONDecoder解码失败:
struct Astronaut: Codable, Identifiable {
let id: String
let name: String
let description: String
}
struct Astronauts: View {
var body: some View {
Button("获取Astronaut数据") {
if let fileURL = Bundle.main.url(forResource: "astronauts", withExtension: "json") {
if let data = try? Data(contentsOf: fileURL) {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(Astronaut.self, from: data) {
print("decoded:\(decoded)")
} else {
print("JSONDecoder解码失败")
}
} else {
print("Data转换失败")
}
} else {
print("获取astronauts.json文件失败")
}
}
}
}
问题出在下面这段解码的代码中:
if let decoded = try? decoder.decode(Astronaut.self, from: data) {
经排查发现,问题为我的JSON文件为包含多个宇航员对象的字典,不是单一的Astronaut对象,所以无法使用Astronaut.self解码。
{
"grissom": {
"id": "grissom",
"name": "Virgil I. \"Gus\" Grissom",
"description": "Virgil Ivan \"Gus\" Grissom (April 3, 1926 – January 27, 1967) was one of the seven original National Aeronautics and Space Administration's Project Mercury astronauts, and the first of the Mercury Seven to die. He was also a Project Gemini and an Apollo program astronaut. Grissom was the second American to fly in space, and the first member of the NASA Astronaut Corps to fly in space twice.\n\nIn addition, Grissom was a World War II and Korean War veteran, U.S. Air Force test pilot, and a mechanical engineer. He was a recipient of the Distinguished Flying Cross, and the Air Medal with an oak leaf cluster, a two-time recipient of the NASA Distinguished Service Medal, and, posthumously, the Congressional Space Medal of Honor."
},
"white": {
"id": "white",
"name": "Edward H. White II",
"description": "Edward Higgins White II (November 14, 1930 – January 27, 1967) (Lt Col, USAF) was an American aeronautical engineer, U.S. Air Force officer, test pilot, and NASA astronaut. On June 3, 1965, he became the first American to walk in space. White died along with astronauts Virgil \"Gus\" Grissom and Roger B. Chaffee during prelaunch testing for the first crewed Apollo mission at Cape Canaveral.\n\nHe was awarded the NASA Distinguished Service Medal for his flight in Gemini 4 and was then awarded the Congressional Space Medal of Honor posthumously."
}
...
}
所以,当我们想要使用Astronaut.self形式进行Decoder解码时,我们解码的对象只能是单一的Astronaut对象:
struct Astronaut: Codable, Identifiable {
let id: String
let name: String
let description: String
}
let name = """
{
"id": "grissom",
"name": "Virgil I. Grissom",
"description": "Virgil Ivan Grissom was one of the seven original NASA astronauts."
}
"""
还需要注意的是,当我们单一的Astronaut对象在Swift文件中时,对象内不要有”\”等特殊字符,只有在JOSN文件中可以有”\”字符,如果在Swift文件中就会报错。
"name": "Virgil I. \"Gus\" Grissom",
上面这个name在JSON文件中可以正常解码,但如果是在Swift文件中定义的变量内容存在“\”字符就会报错。
回到正文,因为我们本次解码的JSON文件是包含多个宇航员对象的字典,因此我们应该使用字典的形式进行解码:
if let astronautsInfo = try? decoder.decode([String: Astronaut].self, from: data) {
注意,我们将原来的Astronaut.self改为[String: Astronaut].self,原因为我们的JSON结构现在是一组键值对,所以我们需要用对应的键值对形式进行解码。
当我们使用键值对解码后,可以通过for-in的形式进行轮训并展示对应的数据:
if let astronautsInfo = try? decoder.decode([String: Astronaut].self, from: data) {
for (key, astronaut) in astronautsInfo {
print("astronaut.key:\(key)")
print("astronaut.id:\(astronaut.id)")
print("astronaut.name:\(astronaut.name)")
print("astronaut.description:\(astronaut.description)")
}
}
初次学习可能不明白这里为什么要用[String: Astronaut].self,以及为什么要用for-in轮训。
这里简单的说,原因为首先我们的JSON文件是键值对形式,因此我们必须使用对应的键值对格式进行解码,也因此我们应该使用[String: Astronaut]进行解码,通过.self表示解码的类型本身,因此,我们使用的就是:
[String: Astronaut].self
接着可以看到我们使用了:
for (key, astronaut) in astronautsInfo {
for-in循环语句中包含了key,这是因为我们的astronautsInfo返回的是元组的形式,即:
(key: "grissom", value: Astronaut)
所以,我们需要先解构元组,然后再访问Astronaut的形式,如果我们不解构元组,而是直接简单的for-in形式的话:
for astronaut in astronautsInfo {
print("astronaut.id:\(astronaut.id)")
print("astronaut.name:\(astronaut.name)")
print("astronaut.description:\(astronaut.description)")
}
就会报下方类似的错误;
Value of tuple type '(key: String, value: Astronaut)' has no member 'id'
原因就是因为我们无法通过astronaut.id直接访问元组,因为astronaut代表一个元组,而不是一个Astronaut对象。因此,我们只能先解构元组,再访问对应的Astronaut对象的属性。
for (key, astronaut) in astronautsInfo {
print("astronaut.id:\(astronaut.id)")
print("astronaut.name:\(astronaut.name)")
print("astronaut.description:\(astronaut.description)")
}
此外,如果不想要使用Key(键)的话,也可以使用 _ 忽略它:
for (_, astronaut) in astronautsInfo {
print("astronaut.id:\(astronaut.id)")
print("astronaut.name:\(astronaut.name)")
print("astronaut.description:\(astronaut.description)")
}
最后就可以完成整个JSON文件的输出:
astronaut.id:cernan
astronaut.name:Eugene A. Cernan
astronaut.description:Eugene Andrew Cernan (/ˈsɜːrnən/; March 14, 1934 – January 16, 2017) was an American astronaut, naval aviator, electrical engineer, aeronautical engineer, and fighter pilot. During the Apollo 17 mission, Cernan became the eleventh person to walk on the Moon. Since he re-entered the Apollo Lunar Module after Harrison Schmitt on their third and final lunar excursion, he was the last person to walk on the Moon.
完整文件代码
import SwiftUI
struct Astronaut: Codable, Identifiable {
let id: String
let name: String
let description: String
}
struct Astronauts: View {
// 简化的 JSON 数据,没有任何特殊字符
var body: some View {
Button("获取Astronaut数据") {
if let astronauts = Bundle.main.url(forResource: "astronauts", withExtension: "json") {
if let data = try? Data(contentsOf: astronauts) {
let decoder = JSONDecoder()
if let astronautsInfo = try? decoder.decode([String: Astronaut].self, from: data) {
for (_, astronaut) in astronautsInfo {
print("astronaut.id:\(astronaut.id)")
print("astronaut.name:\(astronaut.name)")
print("astronaut.description:\(astronaut.description)")
}
} else {
print("解码失败")
}
} else {
print("未转换为Data形式")
}
} else {
print("未找到该文件")
}
}
}
}
#Preview {
Astronauts()
}