swiftuidisclosuregroup

Custom DisclosureGroup


I need to have a custom DisclosureGroup. I have a version of it working like this:

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) {
    self.init(isExpanded: isExpanded, label: { Text(labelString) as! LabelContent }, 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<Text, View>(labelString: "Label", isExpanded: $isExpanded2) {
//      HStack {
//        Text("Content2")
//      }
//    }
//    .padding()
  }
}

I have two initializers. The first woks fine but the second one is giving me problems. Xcode somewhat forced me to add the type cast "as! LabelContent" which doesn't look nice. But, more importantly, I can't get it to work. Uncomment the second example to see the error message.

How can I declare an init to just take String instead of a LabelContent (i.e.: Label)?


Solution

  • If you use where to restrict the types you can define the type in the init

    public struct CustomDisclosureGroup<LabelContent, Content>: View where LabelContent : View, Content : View{
    

    Then in the init you can say that where LabelContent == Text

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

    Full code below

    public struct CustomDisclosureGroup<LabelContent, Content>: View where LabelContent : View, Content : 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
            }
        }
    }
    

    Your second usage would look like this, there is no need to add types.

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