How to use SwiftUI @ViewBuilder to create views which require parameters?
Asked Answered
D

1

4

In the answers to my question on How to add a generic SwiftUI view to another view, I learned that this can be done using @ViewBuilder.

While this works fine for most of my use cases, I now came across a new problem:

  • The @ViewBuilder solution basically creates the ContentView view outside the GenericView<Content: View>
  • The ContentView is than passed to the GenericView<Content: View> which shows it

BUT: What if the ContentView has to be created inside GenericView<Content: View> because it requires some parameters which are only available there?


Example:

  • UserView is created by providing a user ID
  • UserViews view model fetches the user name using the ID. So the which creates the UserView does only know the ID but not the name. The name is only available within UserView
  • UserView is used at different places within an app or even in different apps. The different places require to show the username in different layouts/styles/etc. To not hard code all layouts into UserView, the view is generic and is given a Content view type which is used to show the username

Code

protocol NameView: View {
    init(name: String)
}

struct NameOnlyView: NameView {
    private let name: String
    
    init(name: String) {
        self.name = name
    }
    
    var body: some View {
        Text(name)
    }
}

struct NameGreetingView: NameView {
    private let name: String
    
    init(name: String) {
        self.name = name
    }
    
    var body: some View {
        Text("Hello \(name)")
    }
}

struct UserView<Content: NameView>: View {
    private let name: String
    private let nameView: Content
    
    init(userId: Int, @ViewBuilder nameViewBuilder: (String) -> Content) {
        name = LoadUserName(usingId: userId)
        nameView = nameViewBuilder(name)
    }
    
    var body: some View {
        nameView
    }
}


struct SomeView: View {
    var body: some View {
        // What I would like to do
        UserView(userId: 123, NameOnlyView)
        UserView(userId: 987, NameGreetingView)

        // What is required instead
        UserView(userId: 123) {
            NameOnlyView("Name which is not known here")
        }
    }
} 

Of course I could move the logic to load the username from the given ID and make it available in SomeView. However, this is just an example for any value which is only available in UserView but not in SomeView. Especially when using the UserView in different apps I do not want to implement the same logic to load the username (or whatever) in all possible parent views of UserView.

Can this be solved using the @ViewBuilder solution?

Can this be solved in SwiftUI at all?

Or am I completely on the wrong track?

Dhu answered 1/9, 2022 at 15:27 Comment(1)
Just a heads up, your link doesn't work. You might try reformattingChairwoman
P
-1

Take a look at SwiftUI's styling system, e.g. ButtonStyle, and the blog post Encapsulating SwiftUI view styles (Swift by Sundell) has some great info. Hopefully you could end up with something like:

List {
    UserView(userId: 987)
    UserView(userId: 123)
}
.userViewStyle(.nameOnly)
Pelmas answered 1/9, 2022 at 22:1 Comment(6)
Thank you. This looks interesting but does not address the problem I tried to describe. As understood this solution, it pre-defines sytles (e.g. font, color, padding) which can than be applied. However, may question is more about layout (which views/controls should be used) than about styling. Additionally I would like to re-use UserView across different project without knowing how these project would like to present their usernames. Each project should be able implement and use its own NameView instead of just selecting a pre-defined hard coded style.Dhu
No it used for layout, e.g. LabelStyle .titleAndIconPelmas
And the style system is designed to be customisable, e.g. by implementing the LabelStyle protocol here is an example useyourloaf.com/blog/adapting-swiftui-label-stylePelmas
Yes, but it is done by creating a view and providing it to the button so that it can use the view e.g. as label. To be able to do this, everything I need to create the layout needs to be known outside the button. However, in my case some information/values/what ever is only available inside the button. So the button would need to handle the creation. The button should only be provided with the type of its content view and not the content view itself. It could than handle the creation itself in order to apply its (private) values.Dhu
A style can be supplied a view builder, invoked in its init and used in its makeBodyPelmas
I tried to figure out how this could work. Unfortunately without any success. Problem is, that I do not understand how one could pass the "internal" values to the ViewBuilder. The linked articles does not provide an example for this, do they? sorry if i'm on the fence here.Dhu

© 2022 - 2024 — McMap. All rights reserved.