Individually modifying child views passed to a container using @ViewBuilder in SwiftUI
Asked Answered
F

3

6

In SwiftUI you are able to pass multiple views as one parameter to another view, thanks to @ViewBuilder. Each child view is treated individually, and so after being passed, can be contained in a VStack, HStack etc.

My question is: is it possible for this container view to individually modify each child view passed to it? For example, by adding a Spacer between each child view.

Here is the equivalent functionality I'm looking for:

struct SomeContainerView<Content: View> {
    
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content()
    }
    
    let content: Content
    
    var body: some View {
        HStack {
            ForEach(content.childViews, id: \.id) { child in
                // Give each child view a background
                // Add a Spacer in between each child view
                // Conditional formatting
                // Etc.
            }
        }
    }
    
}

I believe this to be possible as standard SwiftUI container views can influence the appearance and layout of child views passed to them. Unless this is achieved though internal features of SwiftUI?

A way around this may be to pass in multiple view parameters instead of one, though this is inflexible and messy if many views need to be passed. An array of views doesn't seem to make sense either, as I have not noticed this pattern anywhere in SwiftUI.

Formulate answered 22/2, 2021 at 10:46 Comment(1)
The idea is to pass array of viewHathaway
C
1

from my personal experience working with @ViewBuilder it is impossible, when you pass for example like a Button and a Text and maybe a HStack to SomeContainerView, it take all those as a Single View like a VStack or whatever u name it. and any modification happens to entire View, But the things get deferent when you pass a ForEach to your SomeContainerView, in this case more or less you have same level of control before sending it SomeContainerView, and you can modify every single row as you want but it is limited control, for example you do not have access to index or item anymore which you could have it in ForEach. Let my give you more light to see the things, even if you working in body I mean the higher level of observing and controlling a View, after a single line run code in body it is not editable anymore, For example you have a Text and you gave a backgroundColor to this Text, and after a while you want get rid of that Color, So SwiftUI has no control any more after giving that bachgroundColor, then it has to re-render/re-build the Text without backgroundColor to answer your order, And this is the big deference between SwiftUI and UIKit-Swift, a View is like a bullet after fire you cannot catch it or edit it, you have to fire a new bullet to the target you want, So coming back to your question again, after you send a or some View to your SomeContainerView, it take it almost as a single View.

But if you really wana badly to make the thing you want happen, then you should try other thing like an array of AnyView would solve your issue, but you have to keep eye on managing and controlling that array.

Conspectus answered 22/2, 2021 at 11:56 Comment(0)
H
1

As per my knowledge, You can not do something like this. But you can use the array View and iterate over the view and modify your child view.

Here is demo:

struct SomeModel<Content: View>: Identifiable {
    var body: Content
    var id = UUID()
}

public struct SomeContainerView<Content: View>: View {
    
    let items: [SomeModel<Content>]
    
    init(items: [SomeModel<Content>]) {
        self.items = items
    }
    
    public var body: some View {
        HStack {
            ForEach(items.indices) { index in
                items[index].body.background(index % 2 == 0 ? Color.red : Color.green)
            }
        }
    }
}

struct TestSomeView: View {
    
    var body: some View {
        SomeContainerView(items: [.init(body: Text("Child_view_1")),
                           .init(body: Text("Child_view_2")),
                           .init(body: Text("Child_view_3"))])
    }
}

enter image description here

Hathaway answered 22/2, 2021 at 12:7 Comment(0)
A
0

A better idea would be to let caller decide how to layout content in view, and model decide the data for each input.Check the code below-:

import SwiftUI

struct SomeContainerView<Content: View>:View {
    
    var model:[Model] = []
    
    init(model:[Model],@ViewBuilder content: @escaping (Model) -> Content) {
        self.content = content
        self.model = model
    }
    
    let content: (Model) -> Content
    
    var body: some View {
        
        VStack{
            ForEach(model,id:\.id, content: content)
            Spacer()
        }
    }
}

struct MainView:View {
    @ObservedObject var modelData:objects
    
    var body: some View{
        SomeContainerView(model: modelData.myObj){ data in
            Text(data.name)
                .background(data.color)
            
        }
    }
}

struct Model{
    
    var id = UUID().uuidString
    var name:String
    var color:Color
    
    init(name:String,color:Color) {
        self.name = name
        self.color = color
    }
    
}

class objects:ObservableObject,Identifiable{
    
    var id = UUID().uuidString
    @Published var myObj:[Model] = []
    
    init() {
        initModel()
    }
    
    func initModel(){
        let model = Model(name: "Jack", color: .green)
        let model1 = Model(name: "hey Jack", color: .red)
        
        myObj.append(model)
        myObj.append(model1)
    }
    
}

@main -:

import SwiftUI

    @main
    struct WaveViewApp: App {
        
        @StateObject var dataObj : objects
        
        init() {
            let obj = objects()
           _dataObj = StateObject(wrappedValue: obj)
            
        }
        var body: some Scene {
            WindowGroup {
                MainView(modelData: dataObj)
            }
        }
    }

Output-:

enter image description here

Anastase answered 22/2, 2021 at 12:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.