使用披露组/大纲组时,如何在新 SwiftUI 中正确显示来自多个 json(书籍)的最后嵌套 Child 数据?

How to correctly show the last nested Child data from multiple json(books) in new SwiftUI while using Disclosure group/ Outline group?

我正在使用的结构:

import SwiftUI

struct lvl4: View {
    //    @State var book: Book = Book()
    @State var books: [BookModel] = []  
    @State var selection: BookModel?

    
    
    //ios 14 must to get the syntex right..
    @available(iOS 14, *)
    var body: some View {
        
        
        NavigationView {
            
            List(books) { book in
                ForEach(book.bookContent ?? []) { bookContent in
                    Section(header: Text(bookContent.title).font(.largeTitle) .fontWeight(.heavy)) {
                        OutlineGroup(bookContent.child, children: \.child) { item in
                            if #available(iOS 15, *) {
                                
                                Text(attributedString(from: item.title, font: Font.system(size: 20)))
                                
                                    .navigationBarTitle(book.bukTitle!)
                                
                            }
                        }
                    }
                }
            }
            
            
        }
        
        .navigationBarTitleDisplayMode(.inline)
        .navigationViewStyle(StackNavigationViewStyle())
        //
        
        .listStyle(SidebarListStyle())
        //        .navigationViewStyle(.stack)
        .onAppear {
            //loadData()
        }
    }
    
    {

    
}

struct Buk: Identifiable, Codable {
    let id = UUID()
    var bukTitle: String = ""
    var isLive: Bool = false
    var userCanCopy: Bool = false
    var bookContent: [BookContent] = []

    enum CodingKeys: String, CodingKey {
        case bukTitle = "book_title"
        case isLive = "is_live"
        case userCanCopy = "user_can_copy"
        case bookContent = "book_content"
    }
}
struct BookContent: Identifiable, Codable {
    let id = UUID()
    var type,title:String
    var child: [Child]
    

}

struct Child: Identifiable, Codable {
    let id = UUID()
    var type,title:String
    var child: [Child]?
    

}

@available(iOS 15, *)
func attributedString(from str: String, font: Font) -> AttributedString {
    if let theData = str.data(using: .utf16) {
        do {
            let theString = try NSAttributedString(data: theData, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
            var attaString = AttributedString(theString)
            attaString.font = font  
            return attaString
        } catch {
            print("\(error)")
        }
    }
    return AttributedString(str)
}


struct lvl4_Previews: PreviewProvider {
    static var previews: some View {
        lvl4()
    }
}

现状:

在同一个 V-stack 上显示了所有本地存储的书籍(json 文件)的列表。这些书是从本地目录中获取的。现在,当用户按下公开 group/outline 组的下拉菜单时。用于解析 json 的“结构”(即 lvl4())显示嵌套的 json 数据。

通过实施大纲组/披露组,当前数据如下所示。

我的尝试 ::

在列出所有书籍的同一视图中显示书籍。但无法在另一个视图中显示来自 json 的最后描述 child。 以下是我尝试使用披露组的代码。 disclosure 组中使用的大纲组使用 children: .child 这有助于使嵌套数据在视图上看起来。显示最后一个嵌套数据,直到 json 的编码键为“child”:null 直到这里:

json 数据之一的一小部分(同一行中数百个类似的嵌套数据):

[
  {
    "book_title": "સત્સંગિજીવન સાગર મંથન",
    "is_live": false,
    "user_can_copy": true,
    "book_content": [
      {
        "title": "સત્સંગિજીવન માહાત્મ્ય",
        "type": "title",
        "child": [
          {
            "title": "૦૧. પૂર્વભૂમિકા",
            "type": "title",
            "child": [
              {
                "title": "૦૧. મંગલાચરણ",
                "type": "title",
                "child": [
                  {
                    "title": "<p>સતતં નિજમૂર્તિ ચિન્તકાનામ્, અધિક શ્વેત મનોહર પ્રકાશે ।<br />હૃદિ દર્શિત રમ્ય દિવ્યરૂપં, ભગવન્તં તમહં હરિં નમામિ ।।</p><h3 style='text-align: center;'><strong>( </strong><strong>અર્થ</strong><strong> )</strong></h3><p style='text-align: justify;'>&nbsp; &nbsp; &nbsp; “હંમેશાં પોતાની મૂર્તિનું ચિંતન કરનારા, ભક્તજનોના હૃદય કમળમાં જણાતા અત્યંત શ્વેત મનોહર પ્રકાશવાળા, અક્ષર બ્રહ્મમાં જેમણે બતાવ્યું છે દિવ્યરૂપ એવા ભગવાન શ્રીહરિને હું નમસ્કાર કરું છું.”</p><p style='text-align: justify;'>&nbsp; &nbsp; &nbsp; અનંતકોટિ બ્રહ્માંડોના ઉત્પત્તિના કારણ તથા અનંત ઐશ્વર્ય યુક્ત એવા પૂર્ણપુરુષોત્તમ શ્રી સ્વામિનારાયણ મહાપ્રભુજી તથા આપણા (ઉદ્ધવ) સંપ્રદાયના આદ્ય સ્થાપક ઉદ્ધવાવતાર શ્રી રામાનંદસ્વામી તથા જેની શિષ્ય પરંપરાગતમાં મને શિષ્ય બનવાનો સુલભ અવસર પ્રાપ્ત થયો છે, જેઓને ખુદ સ્વામિનારાયણ ભગવાન ગુરુ તરીકે માનીને મર્યાદા રાખતા અને જેઓને સત્સંગની 'મા' તરીકેનું બિરુદ આપી શ્રીહરિજીએ બહુમાન કર્યું હતું, એવા સર્વગુણે સંપન્ન મારા આદિ ગુરુ સદ્ગુરુ શ્રી મુક્તાનંદ સ્વામી તથા મૂળ અક્ષરમૂર્તિ યોગીરાજ સદ્ગુરુ ગોપાળાનંદ સ્વામી તથા જેઓને ખુદ શ્રીજી મહારાજે પોતાને સ્થાને બેસાડી સંપ્રદાયની ધુરા સોંપી આચાર્યપદ અર્પણ કર્યું છે એવા, સંતોનો અપાર મહિમા સમજનારા અને ગૃહસ્થાશ્રમમાં હોવા છતાં નિષ્કામી વ્રતને ધારણ કરનાર એવા પ. પૂ. ધ. ધુ. ૧૦૦૮ આચાર્ય શ્રી રઘુવીરજી મહારાજ તથા ધ્યાનના અંગવાળા અને આત્મનિષ્ઠાને સાંગોપાંગ જીવનમાં ઉતારનારા પ.પૂ.ધ.ધુ.૧૦૦૮ આચાર્યશ્રી અયોધ્યાપ્રસાદજી મહારાજ તથા સર્વે મહાન સંતો અને મહાન ભક્તોના ચરણોમાં વંદના કરી 'ગ્રંથરાજ શ્રીમદ્ સત્સંગિજીવન' માંથી મારી અલ્પમતિ અનુસાર મંથન કરી સાર રૂપ ઘી શોધવા માટે જઇ રહ્યો છું.</p>",
                    "type": "content",
                    "child": null
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  }
]

通过实施披露组,当前数据如下所示。

使用了以下代码::

import SwiftUI
import Foundation


struct ContentView: View {
    
    @EnvironmentObject var booksList:BooksList
    @State var books: [BookModel] = []
    @State var selection: BookModel?
    
    
    var body: some View {
        
//        NavigationView {
            
            
            VStack{
                List(booksList.books) { book in
            
//                        NavigationLink(destination: lvl4(books: [book], selection: nil)){
//                                               Text(book.bukTitle!)
//
            
                    if #available(iOS 15.0, *) {
                        DisclosureGroup ("\(Text(book.bukTitle!) .fontWeight(.medium) .font(.system(size: 27)))"){
                            ForEach(book.bookContent ?? []) { bookContent in
                                DisclosureGroup("\(Text(bookContent.title).fontWeight(.light) .font(.system(size: 25)))")
                                {
                                    OutlineGroup(bookContent.child  , children: \.child) { item in
                                         if #available(iOS 15, *) {
                                                
                                         
                                             Text(attributedString(from: item.title, font: Font.system(size: 23) ))
                                                 .navigationTitle(Text(bookContent.title))
                                                
                                            
//                                             if (([Child].self as? NSNull) == nil)  {
                                                   
                                                 
//                                                     NavigationLink(destination: ScrollView {Text(attributedString(from: item.title, font: Font.system(size: 25) )).padding(30) .lineSpacing(10) .navigationTitle(Text(bookContent.title)) .navigationBarTitleDisplayMode(.inline)
//
//                                                        })
//                                                        {
//
//    //                                                        EmptyView()
//        //                                                    .navigationTitle(Text(bookContent.title))
//                                                        }
                                                
                                                 
                                                 
//                                                    }
                                         
                                             
                                         }
                                        
                                       }
                                     }
                            }
                            
                        }
                    } else {
                        // Fallback on earlier versions
                    }
                    
//                }
                
            }
            
        }
        
    }
                  
//
//                    DisclosureGroup("\(Text(book.bukTitle!).fontWeight(.light) .font(.system(size: 23)))"){
//
//                        ForEach(book.bookContent ?? []) { bookContent in
//
//                            DisclosureGroup("\(Text(bookContent.title))" ){
//
//                                OutlineGroup(bookContent.child, children: \.child) { chld in
//
//
//                                    List(bookContent.child, children: \.child)
//                                    {
//                                      OutlineGroup(bookContent.child, children: \.child) { item in
//                                            if #available(iOS 15, *) {
//
//                                                NavigationLink(destination: ScrollView{Text(attributedString(from: item.title, font: Font.system(size: 22) )).padding(30) .lineSpacing(10) .navigationTitle(Text(bookContent.title)) .navigationBarTitleDisplayMode(.inline)}){
//                                                    EmptyView()
//
//                                                }
//                                            }
//                                        }
//                                    }
//                            }
//                        }
//                    }
//                }
//            }
//        }
//    }
//}

@available(iOS 13.0.0, *)
struct ContentView_Previews: PreviewProvider {
    @available(iOS 13.0.0, *)
    static var previews: some View {
        ContentView()
    }
}
}

struct Child 位于上面第一个代码中给出的 struct lvl4 内部。

BookModel 代码如下::

import Foundation

enum BookParseError: Error {
    case bookParsingFailed
}

struct BookModelForJSONConversion: Codable {
    var id:Int
    var title: String?
    var content: [BookContent]?
    
    
    func convertToJsonString()->String?{
        let jsonEncoder = JSONEncoder()
        jsonEncoder.outputFormatting = .prettyPrinted
        var encodedString:String?
        do {
            let encodePerson = try jsonEncoder.encode(self)
            let endcodeStringPerson = String(data: encodePerson, encoding: .utf8)!
            //print(endcodeStringPerson)
            encodedString = endcodeStringPerson
        } catch {
            print(error.localizedDescription)
            return nil
        }
        return encodedString
    }
}

struct BookModel: Identifiable, Codable {
    var id:Int
    var bukTitle: String?
    var isLive: Bool?
    var userCanCopy: Bool?
    var bookContent: [BookContent]?
    
    enum CodingKeys: String, CodingKey {
        case id = "id"
        case bukTitle = "title"
        case isLive = "is_live"
        case userCanCopy = "user_can_copy"
        case bookContent = "content"
    }
}

//struct BookContent: Identifiable, Codable {
//    let id = UUID()
//    var title, type: String
//    var child: [Child]
//}
//
//struct Child: Identifiable, Codable {
//    let id = UUID()
//    var title, type: String
//    var child: [Child]?
//}


enum BooksDirectory {
    /// Default, system Documents directory, for persisting media files for upload.
    case downloads

    /// Returns the directory URL for the directory type.
    ///
    fileprivate var url: URL {
        let fileManager = FileManager.default
        // Get a parent directory, based on the type.
        let parentDirectory: URL
        switch self {
        case .downloads:
            parentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
        }
        return parentDirectory.appendingPathComponent(VBBooksManager.booksDirectoryName, isDirectory: true)
    }
}

class VBBooksManager:NSObject {
    fileprivate static let booksDirectoryName = "books"

    let directory: BooksDirectory
    
    @objc (defaultManager)
    static let `default`: VBBooksManager = {
        return VBBooksManager()
    }()
    
    // MARK: - Init
    /// Init with default directory of .uploads.
    ///
    /// - Note: This is particularly because the original Media directory was in the NSFileManager's documents directory.
    ///   We shouldn't change this default directory lightly as older versions of the app may rely on Media files being in
    ///   the documents directory for upload.
    ///
    init(directory: BooksDirectory = .downloads) {
        self.directory = directory
    }
    
    // MARK: - Instance methods
    /// Returns filesystem URL for the local Media directory.
    ///
    @objc func directoryURL() throws -> URL {
        let fileManager = FileManager.default
        let mediaDirectory = directory.url
        // Check whether or not the file path exists for the Media directory.
        // If the filepath does not exist, or if the filepath does exist but it is not a directory, try creating the directory.
        // Note: This way, if unexpectedly a file exists but it is not a dir, an error will throw when trying to create the dir.
        var isDirectory: ObjCBool = false
        if fileManager.fileExists(atPath: mediaDirectory.path, isDirectory: &isDirectory) == false || isDirectory.boolValue == false {
            try fileManager.createDirectory(at: mediaDirectory, withIntermediateDirectories: true, attributes: nil)
        }
        return mediaDirectory
    }
    
    func saveBook(bookName:String,bookData:String)->Error?{
        //TODO: Save book into Document directory
        
        do {
            var finalBookName = bookName
            if !finalBookName.contains(".json"){
                finalBookName = "\(bookName).json"
            }
            
            let bookPath = try? self.directoryURL().appendingPathComponent(finalBookName)
            print(bookPath?.relativePath)
           
            
            do {
                let fileManager = FileManager.default
                if fileManager.fileExists(atPath: bookPath!.relativePath){
                    try fileManager.removeItem(at: bookPath!)
                }
                let data = Data(bookData.utf8)
                try? data.write(to: bookPath!, options: .atomic)
                //Just for Testing purpose call load book
                //lodBook(bookName: finalBookName)
            }
            catch let error as NSError {
                print(error)
                return error
            }
            
        }
        catch let error as NSError{
            print(error)
            return error
        }
       
        
        return nil
        
        //fileManager.wri wr(bookPath.relativePath, contents: Data(bookData), attributes: nil)
    }
    
    //
    func loadBookFromDocumentDirectory(bookName:String) throws -> BookModel? {
        let fileManager = FileManager.default
        do {
            var finalBookName = bookName
            if !finalBookName.contains(".json"){
                finalBookName = "\(bookName).json"
            }
            let bookPath = try? self.directoryURL().appendingPathComponent(finalBookName)
            print(bookPath?.relativePath)
           
            
            do {
                if fileManager.fileExists(atPath: bookPath!.relativePath){
                    let jsonBookString = fileManager.contents(atPath: bookPath!.relativePath)
                    do {
                        let data = try Data(jsonBookString!)
                        guard let parsedBookObject:BookModel? = try JSONDecoder().decode(BookModel.self, from: data) else {
                            throw BookParseError.bookParsingFailed
                        }
                        return parsedBookObject ?? nil
                        //print(parsedBookObject)
                    }
                    catch let error as NSError{
                        print("error: \(error)")
                        throw error
                    }
                    
                }else{
                
                }
            }
            catch let error as NSError {
                print(error)
                throw error
            }
            
        }
        catch let error as NSError{
            print(error)
            throw error
        }
        return nil
    }
    
    func loadAllSavedBooks()->[BookModel]?{
        var allBooks:[BookModel] = []
        let fileManager = FileManager.default
        guard let booksPath = try? self.directoryURL() else {
            return []
        }
        print(booksPath)
        
        do {
            // Get the directory contents urls (including subfolders urls)
            let directoryContents = try fileManager.contentsOfDirectory(at: booksPath, includingPropertiesForKeys: nil)
            print(directoryContents)

            // if you want to filter the directory contents you can do like this:
            let books = directoryContents.filter{ [=15=].pathExtension == "json" }
            let bookNames = books.map{ [=15=].deletingPathExtension().lastPathComponent }
            print("bookNames list:", bookNames)
            //TODO: Load all the books and send array back

            for bookName in bookNames {
                do {
                    let book = try loadBookFromDocumentDirectory(bookName:bookName)
                    allBooks.append(book!)
                } catch BookParseError.bookParsingFailed {
                    continue
                }
                
                
            }
            return allBooks
            
            

        } catch let error as NSError {
            print(error)
        }
        
    
        return allBooks
    }
    
    
    
}

问题::

问题 1: 如果我尝试使用 Navigation link(已注释掉),那么它将在 Disclosure 组的所有 Child 中显示导航链接,而不是仅在最后一个 child 中显示导航链接得到child( "child": null).

这需要一些时间来解析。下一个问题,请把你的代码中所有与问题无关的东西都去掉,这样更容易理解。

对于您的模型,您犯的第一个错误是将所有内容都设为可选。这给你带来了不必要的复杂程度。您最关心的是处理 Child 的数组,但只有当数组不为空时才需要处理它们。如果您将它们设置为可选,您将不得不打开它们,然后查看它们是否为空。那是不必要的。

此外,就数据模型而言,BookContent == Child。绝对没有理由同时拥有两者,所以我放弃了 Child.

改造 JSON 以便每个节点都有一个值,即使它只是一个空数组或“”字符串。由于您控制 JSON,因此请保持简单。

如您所见,我递归地渲染了每个 BookContent 的视图,因为每个 BookContent 都有一个 [BookContent]。如果 [BookContent] 为空,则递归结束。

您的观点:

struct ContentView: View {

    @State var booksList: [BookModel] = [
        BookModel(id: 1, bukTitle: "Book Title", isLive: false, userCanCopy: false, bookContent: [
            BookContent(title: "Content Title", type: "", children: [
                BookContent(title: "2nd Level Book Content", type: "", children: [
                    BookContent(title: "3rd Level Book Content", type: "", children: [
                        BookContent(title: "4th Level Book Content", type: "", children: [])
                    ])
                ])
            ])
        ])]
    
    var body: some View {
        NavigationView {
            VStack{
                List(booksList) { book in
                    Text(book.bukTitle)
                    ForEach(book.bookContent) { bookContent in
                        BookContentView(bookContent: bookContent)
                    }
                }
            }
        }
    }
}

struct BookContentView: View {
    
    let bookContent: BookContent
    
    var body: some View {
        Text(bookContent.title)
        ForEach(bookContent.children) { bookContent in
            BookContentView(bookContent: bookContent)
        }
    }
}

您的模特:

struct BookModel: Identifiable, Codable {
    var id:Int
    var bukTitle: String
    var isLive: Bool
    var userCanCopy: Bool
    var bookContent: [BookContent]
    
    enum CodingKeys: String, CodingKey {
        case id = "id"
        case bukTitle = "title"
        case isLive = "is_live"
        case userCanCopy = "user_can_copy"
        case bookContent = "content"
    }
}

struct BookContent: Identifiable, Codable {
    let id = UUID()
    var title, type: String
    var children: [BookContent]
    
    // Since your id is a let constant, adding CodingKeys without id
    // silences the Codable warning that id won't be coded.
    enum CodingKeys: String, CodingKey {
        case title = "title"
        case type = "type"
        case children = "child"
    }
}

在单独的应用程序中使用此代码,然后按照您的方式返回,根据需要进行集成。