如何使用 StateObject 并结合 JSON 加载器?

How to use StateObject and Combine with JSON loader?

我目前正在尝试在 Swift 中加载 JSON,以便在我的 UI 中使用它。我想我已经设法让 JSON 正确加载,但由于代码中出现多个错误,我无法对其进行测试。

JSONReader.swift:

import Foundation

struct DatabaseObject: Decodable {
    let name: String
    let books: Books
    let memoryVerses: MemoryVerses
    
    struct Books: Codable {
        let Romans: Book
        let James: Book
        
        struct Book: Codable {
            let abbreviation: String
            let chapters: [Chapter]
            
            struct Chapter: Codable {
                let sections: [Section]
                let footnotes: Footnotes
                
                struct Section: Codable {
                    let title: String
                    let verses: [String]
                }
                
                struct Footnotes: Codable {
                    let a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z: String
                }
                
            }
            
        }
        
    }
    struct MemoryVerses: Codable {
        let singles: [String]
        let multiples: [String]
    }
}

public class JSONReaderSuperclass {
    @Published var contentData: (status: String, result: DatabaseObject?)
    init() {
        contentData = (status: "loading", result: nil)
    }
}

public class JSONReader: JSONReaderSuperclass, ObservableObject {
    
    private func parse(jsonData: Data) -> (status: String, result: DatabaseObject?) {
        do {
            let decodedData = try JSONDecoder().decode(DatabaseObject.self, from: jsonData)
            print(decodedData)
            return (status: "success", result: decodedData)
        } catch {
            return (status: "fail", result: nil)
        }
    }
    private func loadJson(fromURLString urlString: String,
                          completion: @escaping (Result<Data, Error>) -> Void) {
        if let url = URL(string: urlString) {
            let urlSession = URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
                if let error = error {
                    completion(.failure(error))
                }
                
                if let data = data {
                    completion(.success(data))
                }
            }
            urlSession.resume()
        }
    }
    override init() {
        super.init()
        DispatchQueue.main.async {
            self.loadJson(fromURLString: "redacted for anonymity") { result in
                switch result {
                    case .success(let data):
                        self.contentData = self.parse(jsonData: data)
                    case .failure:
                        self.contentData = (status: "fail", result: nil)
                }
            }
        }
    }
}

ContentView.swift:

import SwiftUI

struct ContentView: View {
    @StateObject var databaseObject = JSONReader()
    var body: some View {
        switch ($databaseObject.status) {
            case "loading":
                Text("Loading...")
            case "success":
                VersePicker(databaseObject: $databaseObject.result)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .navigationTitle("Content Judge")
                    .navigationBarTitleDisplayMode(.inline)
            case "fail":
                Text("An unknown error occured. Check your internet connection and try again.")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

VersePicker.swift:

import SwiftUI

enum Book: String, CaseIterable, Identifiable {
    case romans
    case james

    var id: String { self.rawValue }
}

struct VersePicker: View {
    var databaseObject: DatabaseObject
    @State private var selectedBook = Book.romans
    @State private var selectedChapter: Int = 1
    @State private var selectedVerse: Int = 1
    
    var body: some View {
        VStack {
            Picker("Book", selection: $selectedBook) {
                ForEach(Book.allCases) { book in
                    Text(book.rawValue.capitalized)
                        .tag(book)
                }
            }
            HStack {
                Picker("Chapter", selection: $selectedChapter) {
                    ForEach(1...100, id: \.self) { number in
                        Text("\(number)")
                    }
                }
                Picker("Verse", selection: $selectedVerse) {
                    ForEach(1...100, id: \.self) { number in
                        Text("\(number)")
                    }
                }
            }
            .frame(maxHeight: .infinity)
            Spacer()
            NavigationLink(destination: VerseDisplay()) {
                Label("Go", systemImage: "arrow.right.circle")
            }
        }
        .padding()
    }
}

struct VersePicker_Previews: PreviewProvider {
    static var previews: some View {
        VersePicker(databaseObject: JSONReader().result)
    }
}

我收到以下错误:

知道我做错了什么吗?我对 Swift 完全陌生,所以我很茫然。

首先删除元组并改用两个单独的属性

public class JSONReaderSuperclass {
    @Published var status: String = ""
    @Published var result: DatabaseObject? = nil
}

这里我建议你把status改为enum

然后我们需要更改 init 中的 switch 因为我们删除了元组

switch result {
case .success(let data):
    let decoded = self.parse(jsonData: data)
    self.status = decoded.status
    self.result = decoded.result
case .failure:
    self.status = "fail"
    self.result = nil
}

作为未来的更改,我建议您不要在 init 中进行下载和解码,而是将其放在您从 .onAppear.task 调用的 public 函数中在你看来。

VersePicker 中,您需要将 databaseObject 属性 设为可选(或删除它,因为您似乎不使用它)

var databaseObject: DatabaseObject?

最后,在 ContentView 中,您还需要进行一些更改,以针对新属性进行调整,而且当您只读取 @State、@StateObject 或类似的 属性 时,您应该 not 在 属性 前加上 $ 符号,只有在将 属性 传递给将更改它的函数或视图时才这样做。

这里是ContentView

body
var body: some View {
    switch (databaseObject.status) {
        case "loading":
            Text("Loading...")
        case "success":
            VersePicker(databaseObject: databaseObject.result)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .navigationTitle("Content Judge")
                .navigationBarTitleDisplayMode(.inline)
        case "fail":
            Text("An unknown error occured. Check your internet connection and try again.")
    default:
        fatalError() //Avoid this by changing status from string to an enum
    }
}