如何使用 StateObject 并与 JSON loader 结合?

Posted

技术标签:

【中文标题】如何使用 StateObject 并与 JSON loader 结合?【英文标题】:How to use StateObject and Combine with JSON loader? 【发布时间】:2022-01-18 01:57:12 【问题描述】:

我目前正在尝试在 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)
    

我收到以下错误:

ContentView.swift:13 - “'ObservedObject.Wrapper' 类型的值没有动态成员 'status' 使用来自根类型 'JSONReader' 的密钥路径" ContentView.swift:17 - “无法将 'Binding' 类型的值转换为预期的参数类型 'DatabaseObject'” ContentView.swift:17 - “'ObservedObject.Wrapper' 类型的值没有动态成员 'result' 使用来自根类型 'JSONReader' 的密钥路径" VersePicker.swift:55 - “'JSONReader' 类型的值没有成员 'result'”

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

【问题讨论】:

您试图以错误的方式访问您的元组属性,但我建议您删除该属性并用两个单独的属性替换它而不是修复它 @JoakimDanielson 我似乎也无法让它工作。也许您可以使用示例代码添加答案? 【参考方案1】:

首先删除元组并使用两个单独的属性来代替

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 调用的公共函数中。

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

var databaseObject: DatabaseObject?

最后在ContentView你也需要做一些改变,以适应新的属性,但当你只读取@State、@StateObject 或类似的属性时,你应该前缀$ 符号的属性,只有在将属性传递给将更改它的函数或视图时才这样做。

这是ContentViewbody

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
    

【讨论】:

以上是关于如何使用 StateObject 并与 JSON loader 结合?的主要内容,如果未能解决你的问题,请参考以下文章

如何防止 SwiftUI 像使用 @StateObject 一样重新初始化我的包装属性?

SwiftUI:如何在父视图中初始化新的 StateObject?

如何以 stateobject 作为参数初始化视图?

从子视图设置 StateObject 值会导致 NavigationView 弹出所有视图

在支持 iOS 13.0 的同时在 iOS 14.0 中使用 @StateObject

StateObject 作为 init() 中另一个对象的参数