This is the code that I currently have. Basically I want to have an optional ViewBuilder to add a conditional view, lets say an accessory view. In a kind of .overlay
syntax, where you can either have or don't have an overlay.
But at the time of inserting the view that I'm getting this error
Cannot convert value of type '' to closure result type 'Content'
public struct MyView<Content: View>: View {
@State var isHidden: Bool = false
// stuff here
let content: (Int) -> Content
var additionalView: (() -> Content)?
public init(@ViewBuilder content: @escaping (Int) -> Content, additionalView: (() -> Content)? = nil) {
self.content = content
self.additionalView = additionalView
}
public var body: some View {
VStack(spacing: 0) {
ZStack {
content(selection)
}
// more stuff
if !isHidden {
if let additionalView {
additionalView()
}
// Another view
}
}
}
public func accessoryView(@ViewBuilder content: @escaping () -> Content) -> some View {
var view = self
view.additionalView = content
return view
}
}
Finally I want to call it this way.
struct MyMainView<Content: View>: View {
@StateObject var appInfo: AppInfo = AppInfo()
@ViewBuilder let content: (Int) -> Content
var body: some View {
VStack(spacing: 0) {
MyView(content: content)
.accessoryView {
AnyKindOfView()
// This is where it fails with "Cannot convert value of type 'AnyKindOfView' to closure result type 'Content'"
}
.onAppear {
//code
}
}
.environmentObject(appInfo)
}
}
What am I missing here? I have tried to create an additional, <AdditionalContent: View> for it but it doesn't seem to work.
You should indeed use an extra type parameter to identify the type of the accessory view. The accessory view will not necessarily be the same as Content
after all. This is how some of the built-in SwiftUI views are designed, like how Label
has 2 type parameters representing the title and the icon.
public struct MyView<Content: View, Accessory: View>: View {
// ...
var additionalView: (() -> Accessory)?
// ...
public init(@ViewBuilder content: @escaping (Int) -> Content, additionalView: (() -> Accessory)?) {
// ...
}
}
extension MyView where Accessory == Never {
public init(@ViewBuilder content: @escaping (Int) -> Content) {
self.init(content: content, additionalView: nil)
}
}
Note that the optional parameter makes Swift have a hard time inferring the Accessory
type, so I removed it, and added a one-parameter init
in an extension where Accessory == Never
. Again, this is similar to how SwiftUI's built-in views with optional view builders work. For example, the Button
initialisers that does not take a label
view builder are declared in an extension where Label == Text
.
For the accessoryView
modifier, it needs to be generic too. It should return a new MyView
, with a different Accessory
type parameter.
public func accessoryView<NewAccessory: View>(@ViewBuilder content: @escaping () -> NewAccessory) -> some View {
MyView<Content, NewAccessory>(content: self.content, additionalView: content)
}
Though I don't think this is necessary. Why not just pass in the view directly into the initialiser's additionalView
argument?
Another way is to use AnyView
as the type of the accessory view. This involves making the init
s generic as well.
public struct MyView<Content: View>: View {
// ...
var additionalView: (() -> AnyView)?
public init<Accessory: View>(@ViewBuilder content: @escaping (Int) -> Content, additionalView: (() -> Accessory)?) {
self.content = content
if let additionalView {
self.additionalView = { AnyView(additionalView()) }
}
}
// ...
public func accessoryView<Accessory: View>(@ViewBuilder content: @escaping () -> Accessory) -> some View {
var view = self
view.additionalView = { AnyView(content()) }
return view
}
}
extension MyView {
public init(@ViewBuilder content: @escaping (Int) -> Content) {
self.init(content: content, additionalView: nil as (() -> Never)?)
}
}