So I’m trying to create a view that takes viewBuilder content, loops over the views of the content and add dividers between each view and the other
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
// here
}
.background(Color.black)
.cornerRadius(14)
}
}
so where I wrote “here” I want to loop over the views of the content, if that makes sense. I’ll write a code that doesn’t work but that explains what I’m trying to achieve:
ForEach(content.subviews) { view in
view
Divider()
}
How to do that?
TupleView is a concrete View type uses to store multiple View values.
ForEach in SwiftUI is a view struct in its own right, which means you can return it directly from your view body if you want. You provide it an array of items, and you may also need to tell SwiftUI how it can identify each of your items uniquely so it knows how to update them when values change.
A property wrapper that lets you build views declaratively. ViewBuilder is used extensively in SwiftUI to let you create new on-screen views by just listing them out in a trailing closure. It's a property wrapper applied to function parameter. Usually, it's just working behind the scenes, so you don't have to worry about it.
Thankfully, there is an alternative, more immediate and quick to implement, which is to implement parts of a view in methods. That is feasible thanks to a specific attribute in SwiftUI, called ViewBuilder, which according to official documentation, it is: A custom parameter attribute that constructs views from closures.
The swift compiler will try to find the static buildBlock method declared in @ViewBuilder struct that has two views as parameters. Let’s take a look at @ViewBuilder struct declaration to find that method. As you can see, @ViewBuilder has a static buildBlock method that accepts two views, combine them and return TupleView.
This example prints out the value of i five times and is valid Swift and can be run in a Swift Playground. Unfortunately, this type of For loop will not work in SwiftUI because ViewBuilder cannot handle the control flow statements.
I just answered on another similar question, link here. Any improvements to this will be made for the linked answer, so check there first.
GitHub link of this (but more advanced) in a Swift Package here
However, here is the answer with the same TupleView
extension, but different view code.
Usage:
struct ContentView: View {
var body: some View {
BoxWithDividerView {
Text("Something 1")
Text("Something 2")
Text("Something 3")
Image(systemName: "circle") // Different view types work!
}
}
}
Your BoxWithDividerView
:
struct BoxWithDividerView: View {
let content: [AnyView]
init<Views>(@ViewBuilder content: @escaping () -> TupleView<Views>) {
self.content = content().getViews
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
ForEach(content.indices, id: \.self) { index in
if index != 0 {
Divider()
}
content[index]
}
}
// .background(Color.black)
.cornerRadius(14)
}
}
And finally the main thing, the TupleView
extension:
extension TupleView {
var getViews: [AnyView] {
makeArray(from: value)
}
private struct GenericView {
let body: Any
var anyView: AnyView? {
AnyView(_fromValue: body)
}
}
private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] {
func convert(child: Mirror.Child) -> AnyView? {
withUnsafeBytes(of: child.value) { ptr -> AnyView? in
let binded = ptr.bindMemory(to: GenericView.self)
return binded.first?.anyView
}
}
let tupleMirror = Mirror(reflecting: tuple)
return tupleMirror.children.compactMap(convert)
}
}
Result:
So I ended up doing this
@_functionBuilder
struct UIViewFunctionBuilder {
static func buildBlock<V: View>(_ view: V) -> some View {
return view
}
static func buildBlock<A: View, B: View>(
_ viewA: A,
_ viewB: B
) -> some View {
return TupleView((viewA, Divider(), viewB))
}
}
Then I used my function builder like this
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(@UIViewFunctionBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(spacing: 0.0) {
content()
}
.background(Color(UIColor.AdUp.carbonGrey))
.cornerRadius(14)
}
}
But the problem is this only works for up to 2 expression views. I’m gonna post a separate question for how to be able to pass it an array
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With