Как перебирать подвиды контента вьюбилдера в SwiftUI

Итак, я пытаюсь создать представление, которое принимает содержимое viewBuilder, перебирает представления содержимого и добавляет разделители между каждым представлением и другим.

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

так что там, где я написал "здесь", я хочу перебрать просмотры контента, если это имеет смысл. Я напишу код, который не работает, но объясняет, чего я пытаюсь достичь:

ForEach(content.subviews) { view  in
     view
     Divider()
}

Как это сделать?

3 ответа

Я только что ответил на другой похожий вопрос, ссылка здесь . Любые улучшения будут внесены в связанный ответ, поэтому сначала проверьте его.

Однако вот ответ с тем же расширением, но с другим кодом представления.

Применение:

      struct ContentView: View {
    
    var body: some View {
        BoxWithDividerView {
            Text("Something 1")
            Text("Something 2")
            Text("Something 3")
            Image(systemName: "circle")  // Different view types work!
        }
    }
}

Ваш 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) { index in
                if index != 0 {
                    Divider()
                }
                
                content[index]
            }
        }
//        .background(Color.black)
        .cornerRadius(14)
    }
}

И наконец, главное, TupleView расширение:

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

Результат:

Итак, я закончил этим

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

Затем я использовал свой конструктор функций вот так

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

Но проблема в том, что это работает только для двух представлений выражений. Я собираюсь отправить отдельный вопрос о том, как передать ему массив

Расширение ответа Эдди с использованием Swift 5.7 и buildPartialBlock из построителей результатов:

      @resultBuilder
struct MyViewBuilder {
    static func buildPartialBlock<C: View>(first: C) -> TupleView<(C)> {
        TupleView(first)
    }
    
    static func buildPartialBlock<C0, C1>(accumulated: C0, next: C1) -> TupleView<(C0, Divider, C1)> where C0: View, C1: View {
        TupleView((accumulated, Divider(), next))
    }
}

struct MyView<Content: View>: View {
    @MyViewBuilder var content: Content
    var body: some View {
        VStack {
            content
        }
    }
}

struct ContentView : View {
    var body: some View {
        MyView {
            Text("Text1")
            Text("Text2")
            Image(systemName: "chart.bar.fill", variableValue: 0.3)
            Text("Text3")
        }
    }
}

Однако у этого подхода есть одна загвоздка: он не позволяет нам рассматривать группу как «прозрачный контейнер», т. е. группа из двух элементов — это не то же самое, что два элемента, перечисленные без группы:

      var body: some View {
        MyView {
            Text("Text1")
            Text("Text2")
            Group {
                Image(systemName: "chart.bar.fill", variableValue: 0.3) // no Divider() between image and text
                Text("Text3")
            }
        }
    }

Если объединить 2 последних элемента в одну Группу, разделитель между ними исчезнет.

Единственный способ заставить его работать правильно с найденной мной группой описан здесь: https://movingparts.io/variadic-views-in-swiftui.

Другие вопросы по тегам