Struct that i'm using:
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()
}
}
Current Situation:
There is a list of all the locally stored books(json files) that are being shown on same V-stack. Those books are fetched from local directory. Now, when the user presses on drop down of disclosure group/outline group. "Struct"( which is lvl4()) that was made to parse the json show the nested json data.
The current data looks like following by implementing Outline group/ disclosure group.
My Attempt ::
Showed books on same view where all the books are listed. but unable to show the last description child from json on another view. Following is my Code that i tried using Disclosure group. The outline group used in disclosure group uses children: .child which helps to make the nested data look on the view. The last nested data is shown until the coding keys of json's is "child": null until here:
small part of one of that json data(hundreds of similar nested data in same line):
[
{
"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;'> “હંમેશાં પોતાની મૂર્તિનું ચિંતન કરનારા, ભક્તજનોના હૃદય કમળમાં જણાતા અત્યંત શ્વેત મનોહર પ્રકાશવાળા, અક્ષર બ્રહ્મમાં જેમણે બતાવ્યું છે દિવ્યરૂપ એવા ભગવાન શ્રીહરિને હું નમસ્કાર કરું છું.”</p><p style='text-align: justify;'> અનંતકોટિ બ્રહ્માંડોના ઉત્પત્તિના કારણ તથા અનંત ઐશ્વર્ય યુક્ત એવા પૂર્ણપુરુષોત્તમ શ્રી સ્વામિનારાયણ મહાપ્રભુજી તથા આપણા (ઉદ્ધવ) સંપ્રદાયના આદ્ય સ્થાપક ઉદ્ધવાવતાર શ્રી રામાનંદસ્વામી તથા જેની શિષ્ય પરંપરાગતમાં મને શિષ્ય બનવાનો સુલભ અવસર પ્રાપ્ત થયો છે, જેઓને ખુદ સ્વામિનારાયણ ભગવાન ગુરુ તરીકે માનીને મર્યાદા રાખતા અને જેઓને સત્સંગની 'મા' તરીકેનું બિરુદ આપી શ્રીહરિજીએ બહુમાન કર્યું હતું, એવા સર્વગુણે સંપન્ન મારા આદિ ગુરુ સદ્ગુરુ શ્રી મુક્તાનંદ સ્વામી તથા મૂળ અક્ષરમૂર્તિ યોગીરાજ સદ્ગુરુ ગોપાળાનંદ સ્વામી તથા જેઓને ખુદ શ્રીજી મહારાજે પોતાને સ્થાને બેસાડી સંપ્રદાયની ધુરા સોંપી આચાર્યપદ અર્પણ કર્યું છે એવા, સંતોનો અપાર મહિમા સમજનારા અને ગૃહસ્થાશ્રમમાં હોવા છતાં નિષ્કામી વ્રતને ધારણ કરનાર એવા પ. પૂ. ધ. ધુ. ૧૦૦૮ આચાર્ય શ્રી રઘુવીરજી મહારાજ તથા ધ્યાનના અંગવાળા અને આત્મનિષ્ઠાને સાંગોપાંગ જીવનમાં ઉતારનારા પ.પૂ.ધ.ધુ.૧૦૦૮ આચાર્યશ્રી અયોધ્યાપ્રસાદજી મહારાજ તથા સર્વે મહાન સંતો અને મહાન ભક્તોના ચરણોમાં વંદના કરી 'ગ્રંથરાજ શ્રીમદ્ સત્સંગિજીવન' માંથી મારી અલ્પમતિ અનુસાર મંથન કરી સાર રૂપ ઘી શોધવા માટે જઇ રહ્યો છું.</p>",
"type": "content",
"child": null
}
]
}
]
}
]
}
]
}
]
The current data looks like following by implementing Disclosure group.
Following code used::
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 is inside struct lvl4 which is given above in first code.
BookModel code is following::
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)
}
//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
}
}
Issues::
Issue 1: If i try to use Navigation link (which is commented out) then it'll show Navigation Links in all the Childs of the Disclosure group instead of only the last child where the last child is obtained( "child": null).
CodePudding user response:
This took some time to parse through. With the next question, please pull everything out of your code that is not necessary to the question, so it is easier to understand.
With you model, the first mistake you made was making everything optional. That gives you a level of complexity that is unnecessary. Your big concern was dealing with the arrays of Child
, but you only have to deal with them if the arrays are not empty. If you make them optional, you are stuck having to unwrap them to then see if they are empty or not. That is unnecessary.
Also, as far as the data model goes, a BookContent == Child
. There is absolutely no reason to have both, so I dropped Child
.
Remodel the JSON so that every node has a value, even if it is simply an empty array or "" string. Since you control the JSON, keep it simple.
As you can see, I have rendered the views of each BookContent
recursively, since every BookContent
has an [BookContent]
. If the [BookContent]
is empty, the recursion ends.
Your Views:
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)
}
}
}
Your Models:
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"
}
}
Play with this code in a separate app, then work your way back through yours, integrating it as needed.