NSManaged Property causing DisclosureGroup animation problems in Swiftui

169 views Asked by At

I have an NSManagedObject with NSManaged properties that control the expanded/collapsed state of disclosure groups. Here's an example:

/// Views that provides UI for app settings.
struct SettingsView: View {
  @Binding var isExpanded: Bool
  
  var body: some View {
    let _ = Self._printChanges()
    DisclosureGroup(isExpanded: $isExpanded, content: {
      VStack {
        Text("Hello world!")
        Text("Hello world!")
        Text("Hello world!")
        Text("Hello world!")
        Text("Hello world!")
      }
    }, label: {
      HStack {
        Text("Settings")
      }
    })
    .padding([.leading,.trailing])
  }
}

The view's parent calls it like this:

    @EnvironmentObject var settings: SideBarSettings
    .
    .
    .  
    SettingsView(isExpanded: $settings.isExpanded)

The disclosure group animation is lost when using NSManaged property. Animation is preserved when using any other non-NSManaged property even if the property is declared inside NSManagedObject.

Why is DisclosureGroup animation lost when using the NSManaged property?

1

There are 1 answers

0
Phantom59 On

After spending a few days on this, I'm accepting that the underlying problem is the way NSManaged properties work with SwiftUI. So, a possible solution would be to not use the NSManaged property at all in the DisclosureGroup and use a value type instead.

And use modifiers to init it and track changes on the new State var; like this:

    struct SettingsView: View {
      @Binding var isExpanded: Bool
      @State private var isExpandedNonManaged = false // New property
    
      var body: some View {
        let _ = Self._printChanges()
        DisclosureGroup(isExpanded: $isExpandedNonManaged, content: {
          VStack {
            Text("Hello world!")
            Text("Hello world!")
            Text("Hello world!")
            Text("Hello world!")
            Text("Hello world!")
          }
        }, label: {
          HStack {
            Text("Settings")
          }
        })
        .onAppear {
          isExpandedNonManaged = isExpanded // Initialize the State property
        }
        .onChange(of: isExpandedNonManaged) { newValue in
          isExpanded = newValue // Update the managed property
        }
      }
   }

Not elegant, nor scalable ... but it works.

Open to better solutions!

Update: With a little help from this post, came up with a CustomDisclosureGroup that eliminates lots of code duplication.

public struct CustomDisclosureGroup<LabelContent: View, Content: View>: View {
  var label: LabelContent
  var content: Content
  @Binding var isExpanded: Bool
  
  public init(isExpanded: Binding<Bool>, @ViewBuilder label: () -> LabelContent, @ViewBuilder content: () -> Content) {
    self.label = label()
    self.content = content()
    self._isExpanded = isExpanded
  }
  
  public init(labelString: String, isExpanded: Binding<Bool>, @ViewBuilder content: () -> Content) where LabelContent == Text {
    self.init(isExpanded: isExpanded, label: { Text(labelString) }, content: content)
  }
  
  public var body: some View {
    DisclosureGroup(isExpanded: $isExpanded) {
      content
    } label: {
      label
    }
  }
}

@main
struct TestDisclosureApp: App {
  @StateObject var topModel = TopModel()
  
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

struct ContentView: View {
  @EnvironmentObject var topModel: TopModel
  @State var isExpanded1 = false
  @State var isExpanded2 = false

  var body: some View {

    CustomDisclosureGroup(isExpanded: $isExpanded1, label: { Text("Label") }) {
      HStack {
        Text("Content1")
      }
    }
    .padding()

    CustomDisclosureGroup(labelString: "Label", isExpanded: $isExpanded2) {
      HStack {
        Text("Content2")
      }
    }
    .padding()
  }
}