Search code examples
swiftswiftuiswiftui-foreach

DisclosureGroup not opening when looped inside ForEach


I'm trying to add in a support section (this was a demo that turned into something more) and thought that I could fetch a json file and add it into DisclosureGroup to the end user.

I originally thought that the issue was a network issue, but adding the file locally still caused the same problem.

When I run it in the simulator, and try to open one of the DisclosureGroup items, it doesn't open. If I they to press more the RAM usage increases but can't see a reason why it should be after the initial Bundle load into the array.

This is the data I was testing:

SupportQuestions.json

{
 "sections": [
  {
   "title": "Section title 1",
   "description": null,
   "questions": [
    {
     "title": "Question title 1",
     "response": "Answer 1"
    },
    {
     "title": "Question title 3",
     "response": "Answer 3"
    }
   ]
  },
  {
   "title": "Section title 2",
   "description": "Section description",
   "questions": [
    {
     "title": "Question title 4",
     "response": "Answer 4"
    },
    {
     "title": "Question title 5",
     "response": "Answer 5"
    },
    {
     "title": "Question title 6",
     "response": "Answer 6"
    }
   ]
  },
  {
   "title": "Section title 3",
   "description": "Another section description",
   "questions": [
    {
     "title": "Question title 7",
     "response": "Answer 7"
    },
    {
     "title": "Question title 8",
     "response": "Answer 8"
    },
    {
     "title": "Question title 9",
     "response": "Answer 9"
    }
   ]
  }
 ]
}

Then the Swift I was using in the View:

struct SettingsHelpView: View {

  @State private
  var suppportItems: [SupportSections.SupportCategory] = []
  var body: some View {
    Form {
      ForEach(suppportItems) {
        item in
          Section {
            ForEach(item.questions) {
              question in
                DisclosureGroup {
                  Text(question.response)
                }
              label: {
                Text(question.title).bold()
              }
            }
          }
        header: {
          Text(item.title)
        }
        footer: {
          Text(item.decription ?? "")
        }
      }
    }
    .onAppear {
      fetchHelpSection()
    }
  }

  private func fetchHelpSection() {
    let questions = Bundle.main.decode(SupportSections.self, from: "SupportQuestions.json")
    suppportItems = questions.sections
  }
}

Model

struct SupportSections: Decodable {
 let sections: [SupportCategory]

 struct SupportCategory: Decodable, Identifiable {
  var id: String { UUID().uuidString }
  let title: String
  let decription: String?
  let questions: [SupportQuestion]

  struct SupportQuestion: Decodable, Identifiable {
   var id: String { UUID().uuidString }
   let title: String
   let response: String
  }
 }
}

Bundle+Ext

extension Bundle {
 func decode<T: Decodable>(_ type: T.Type, from file: String, dateDecodingStategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T {

  guard let url = self.url(forResource: file, withExtension: nil) else {
   fatalError("Error: Failed to locate \(file) in bundle.")
  }
  guard let data = try? Data(contentsOf: url) else {
   fatalError("Error: Failed to load \(file) from bundle.")
  }
  let decoder = JSONDecoder()
  decoder.dateDecodingStrategy = dateDecodingStategy
  decoder.keyDecodingStrategy = keyDecodingStrategy
  guard let loaded = try? decoder.decode(T.self, from: data) else {
   fatalError("Error: Failed to decode \(file) from bundle.")
  }
  return loaded
 }
}

Video of what is occurring (sorry don't know how to resize):

Imgur


Solution

  • The issue comes from your id properties in your models. Right now, you have id defined as a computed property:

    var id: String { UUID().uuidString }
    

    This means that every time SwiftUI asks for an id, it gets a different value, since a new UUID is generated each time. This confuses SwiftUI and it 'closes' the DisclosureGroup because it thinks it's a new View (because of the new ID).

    To fix this, declare your id properties as non-computed values and provide CodingKeys so that the system doesn't try to decode that property from the JSON file.

    struct SupportSections: Decodable {
        let sections: [SupportCategory]
        
        struct SupportCategory: Decodable, Identifiable {
            var id = UUID().uuidString //<-- Here
            let title: String
            let description: String? //note that you had a typo here in your original code
            let questions: [SupportQuestion]
            
            enum CodingKeys : String, CodingKey {
                case title, description, questions
            }
            
            struct SupportQuestion: Decodable, Identifiable {
                var id: String = UUID().uuidString //<-- Here
                let title: String
                let response: String
                
                enum CodingKeys : String, CodingKey {
                    case title, response
                }
            }
        }
    }