提问者:小点点

如何在SwiftUI中正确显示嵌套的JSON图像而不破坏旧的swift主题?


我想把这张图片展示在这本书的封面,即披露视图旁边。图像来自结构,该结构用于调用json嵌套数据,以与图像一起显示在公开组中。JSON文件保存在本地,调用这些文件将其显示在disclosuregroup中。

我所尝试的问题::

  1. 使用异步图像在“Hstack”中显示它,但编译器不允许我运行代码。
  2. 使用列表而不是 Foreach 来管理容器,但它使用十六进制代码破坏主题。

用于实现上述图像的代码:

import Foundation
import SwiftUI
import UIKit

struct ContentView: View {

  @EnvironmentObject var booksList: BooksList
  @State var books: [BookModel] = []
  @State var selection: BookModel?

  var body: some View {

    //        NavigationView {

    VStack(alignment: .trailing, spacing: 40) {

      ScrollView(.vertical, showsIndicators: false) {

          ForEach(booksList.books){ book in

          //Alternative way , each book on seperate view
          //NavigationLink(destination: lvl4(books: [book], selection: nil)){
          //                                               Text(book.bukTitle!)
          //

          if #available(iOS 15.0, *) {
              
              if #available(iOS 15, *){
              label: do {
                  AsyncImage(url: URL(string: "\(book.coverImage!)")) .scaledToFit()  .fixedSize(horizontal:true, vertical: true) .frame(minWidth: 10, maxWidth: 20)
              }
              }
              
              DisclosureGroup( "\(Text(book.bukTitle!) .fontWeight(.medium) .font(.system(size: 30)))"
            ) {
                 
              ForEach(book.bookContent ?? []) { bookContent in
                  
                DisclosureGroup(
                    "\(Text(bookContent.title).fontWeight(.medium) .font(.system(size: 25)))"
                ) {
                  OutlineGroup(bookContent.child, children: \.child) { item in
                      
                     

                    NavigationLink {

                      if #available(iOS 15, *) {
                        ScrollView {
                          Text(attributedString(from: item.title, font: Font.system(size: 25)))
                            .padding(30).lineSpacing(10).navigationTitle(Text(bookContent.title))
                            .navigationBarTitleDisplayMode(.inline)

                        }.frame(minWidth: 0, maxWidth: .infinity)
                          .background(Color(UIColor.hexStringToUIColor(hexStr: ("efe8d2"))))
                      }

                    } label: {
                      if #available(iOS 15, *) {
                        Text(
                          "\(Text(attributedString(from: item.title, font: Font.system(size: 23) )))"
                        )
                        .lineLimit(1).lineSpacing(20)
                      } else {
                        // Fallback on earlier versions
                      }
                    }
                    .disabled(item.child != nil).lineSpacing(20)
                  }.lineSpacing(20)
                }.lineSpacing(20)
              }
              }
          }
             }.lineSpacing(20)
      }.lineSpacing(20)
    }.padding(35).lineSpacing(20)

  }
import Foundation

class BooksList: ObservableObject {
    @Published var books:[BookModel]
    init() {
        let booksManager = VBBooksManager()
        books = booksManager.loadAllSavedBooks() ?? []
    }
}

以下代码用于BookModel。

import Foundation

enum BookParseError: Error {
    case bookParsingFailed
}

struct BookModelForJSONConversion: Codable {
    var id:Int
    var title: String?
    var content: [BookContent]?
    var bookCoverImage:String?
    
    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]?
    var coverImage:String?
    
    enum CodingKeys: String, CodingKey {
        case id = "id"
        case bukTitle = "title"
        case isLive = "is_live"
        case userCanCopy = "user_can_copy"
        case bookContent = "content"
        case coverImage = "bookCoverImage"
    }
}

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]?
}

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"
    }
}

@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  // <-- here
            return attaString
        } catch {
            print("\(error)")
        }
    }
    return AttributedString(str)
}


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
//                let bookModel = try loadBookFromDocumentDirectory(bookName: finalBookName)
//                print(bookModel?.coverImage)
            }
            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)
    }
    
    //https://stackoverflow.com/questions/39415249/best-practice-for-swift-methods-that-can-return-or-error
    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{ $0.pathExtension == "json" }
            let bookNames = books.map{ $0.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个答案

匿名用户

编辑包含的注释以更好地解释代码并显示图片

您可能以一种奇怪的方式嵌套For每一个NavigationLink和Stacks。

尝试此操作并沿着View添加您的特定代码,包括您的模型属性和AsyncImage。格式如您所愿。结果应该是您正在寻找的。

struct Example: View {
    @State private var bookList: [Book]
    
    init() {
        let book1 = Book(image: "book", content: ["Line 1", "Line 2"])
        let book2 = Book(image: "book.closed.fill", content: ["Line A", "Line B", "Line C"])
        bookList = [book1, book2]
    }
    
    var body: some View {
        
        // "NavigationView" is necessary to allow moving from the list to the details
        NavigationView {
            VStack {

                // Title before the list
                Text("Total books: \(bookList.count)")
                
                // "List" will provide the items one after the other
                List(bookList) { book in
                    
                    // Here is what you see for each single item in the list
                    HStack {
                        
                        // "NavigationLink: is how you see the details by tapping on the item
                        NavigationLink {
                            
                            // This is the detail view
                            ScrollView {
                                Text("First line")
                                Text("Second line")
                                Text("Third line")
                                Text("Fourth line")
                            }
                        } label: {
                            
                            // This is the item view (how you see each item in the list)
                            HStack {
                                // Showing an Image
                                Image(systemName: book.image)
                                
                                // On the side of the image, show another list (without navigation)
                                VStack {
                                    ForEach(book.content, id: \.self) { bookContent in
                                        Text(bookContent)
                                    }
                                }
                            }
                        }
                    }
                    .padding()
                    
                    // This is just for decoration
                    .overlay {
                    RoundedRectangle(cornerRadius: 10)
                        .stroke(lineWidth: 2)
                        .foregroundColor(.green)
                    }
                    .listRowSeparator(.hidden)
                }
                .listStyle(.plain)
            }
        }
    }
}

这是您看到的列表:

点击列表中的某一项时,您可以通过以下方式查看详细信息: