3

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?

Andrei Herford
  • 17,570
  • 19
  • 91
  • 225

1 Answers1

-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)
malhal
  • 26,330
  • 7
  • 115
  • 133
  • 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. – Andrei Herford Sep 02 '22 at 06:03
  • No it used for layout, e.g. LabelStyle .titleAndIcon – malhal Sep 02 '22 at 08:31
  • And the style system is designed to be customisable, e.g. by implementing the LabelStyle protocol here is an example https://useyourloaf.com/blog/adapting-swiftui-label-style/ – malhal Sep 02 '22 at 08:42
  • 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. – Andrei Herford Sep 02 '22 at 09:27
  • A style can be supplied a view builder, invoked in its init and used in its makeBody – malhal Sep 02 '22 at 09:46
  • 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. – Andrei Herford Sep 02 '22 at 12:44