TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: SwiftUI: slots and slot props?

Forums > SwiftUI

I'm starting with Swift (coming from front-end) with test projects and have a question about view and view builders

For reference: in a front-end framework Vue.js there is a concept of slots and slot props (vuejs org guide/components/slots.html#scoped-slots) Same goes for Svelte (svelte dev docs/special-elements#slot) and also same functionality can be achieved in React

For example I have some generic list that has some basic layout and makes some logic (filtration, sorting etc) inside and I want to use it to pass a list and display list items however I want, and if I won't provide a builder, there will be some default view that is specified inside that list

Is there any way to achieve such functionality in SwiftUI/UIKit?

What I've tried to do is something like the next code, I simplified it to show the general idea

import SwiftUI

struct ListTestView\<T: Hashable, Content: View\>: View {
    private let items: \[T\]
    private let listItemBuilder: (ListItemBuilderParams) -\> Content

    init(
        _ list: [T],
        @ViewBuilder listItem: @escaping (_ params: ListItemBuilderParams) -> Content
    ) {
        self.items = list
        self.listItemBuilder = listItem
    }

    var body: some View {
        VStack {
            ForEach(filteredItems, id: \.self) { item in
                listItemBuilder(.init(item1: item, item2: item, item3: item)) // ?? Text(String(item))
            }
        }
    }

    private var filteredItems: [T] {
        // ...some filtration
        return items
    }

    struct ListItemBuilderParams {
        let item1: T
        let item2: T
        let item3: T
        // ... and possible more
    }
}

struct ViewWithList: View {
    var body: some View {
        ListTestView(\[1, 2, 3, 4\]) { params in
            Text(String(params.item1))
            Button(String(params.item2)) {}
            Text(String(params.item3))
        }
    }
}

But it won't compile because in ViewWithList I get an error Generic parameter 'Content' could not be inferred

But it compiles if I pass all the params as plain arguments list, without a struct, but it may get really messy because there is no way to omit some specific arguments and I have to list them all in closure

import SwiftUI

struct ListTestView<T: Hashable, Content: View>: View {
    private let items: [T]
    private let listItemBuilder: (T, T, T) -> Content

    init(
        _ list: [T],
        @ViewBuilder listItem: @escaping (T, T, T) -> Content
    ) {
        self.items = list
        self.listItemBuilder = listItem
    }

    var body: some View {
        VStack {
            ForEach(filteredItems, id: \.self) { item in
                listItemBuilder(item, item, item) // ?? Text(String(item))
            }
        }
    }

    private var filteredItems: [T] {
        // ...some filtration
        return items
    }
}

struct ViewWithList: View {
    var body: some View {
        ListTestView([1, 2, 3, 4]) { item1, item2, item3 in
            Text(String(item1))
            Button(String(item2)) {}
            Text(String(item3))
        }
    }
}

2      

If someone is interested, I've managed to implement similar thing and it looks like this:)

import SwiftUI

typealias ItemBuilder<T: Hashable, TItem> = (ItemBuilderParams<T>) -> TItem

struct VeryAbstractListView<T: Hashable, TItem: View>: View {
    private let items: [T]
    private let itemBuilder: ItemBuilder<T, TItem>

    private var hasItemBuilder = true

    init(
        _ items: [T],
        @ViewBuilder item: @escaping ItemBuilder<T, TItem>
    ) {
        self.items = items
        self.itemBuilder = item
    }

    var body: some View {
        ForEach(items.indices, id: \.self) { i in
            let item = items[i]

            // if view used without itemBuilder, just render the default view
            if hasItemBuilder {
                itemBuilder(.init(item: item, index: i))
            } else {
                Text("Default view \(i)")
            }
        }
    }
}

// builder params on the top level to help swift infer generic types correctly
struct ItemBuilderParams<T: Hashable> {
    let item: T
    let index: Int
}

extension VeryAbstractListView {
    // itemBuilder is optional
    init(_ items: [T]) where TItem == EmptyView {
        self.init(items) { _ in EmptyView() }
        self.hasItemBuilder = false
    }
}

#Preview {
    Group {
        VeryAbstractListView([1, 2, 3, 4])
        VeryAbstractListView([1, 2, 3, 4]) { params in
            Text("Custom view \(params.item)")
        }
    }
}

And you can go even further and enchance the default view itself, but I don't know if there any downsides of such approach in terms of performance

import SwiftUI

typealias ItemBuilder<T: Hashable, TItem> = (ItemBuilderParams<T>) -> TItem

struct VeryAbstractListView<T: Hashable, TItem: View>: View {
    private let items: [T]
    private let itemBuilder: ItemBuilder<T, TItem>

    private var hasItemBuilder = true

    init(
        _ items: [T],
        @ViewBuilder item: @escaping ItemBuilder<T, TItem>
    ) {
        self.items = items
        self.itemBuilder = item
    }

    var body: some View {
        VStack {
            ForEach(items.indices, id: \.self) { i in
                let item = items[i]
                let view = VeryAbstractListItemView(i: i)

                // if view used without itemBuilder, just render the default view
                if hasItemBuilder {
                    itemBuilder(.init(item: item, index: i, view: view))
                } else {
                    view
                }
            }
        }
    }
}

// builder params on the top level to help swift infer generic types correctly
struct ItemBuilderParams<T: Hashable> {
    let item: T
    let index: Int
    let view: VeryAbstractListItemView
}

extension VeryAbstractListView {
    // itemBuilder is optional
    init(_ items: [T]) where TItem == EmptyView {
        self.init(items) { _ in EmptyView() }
        self.hasItemBuilder = false
    }
}

struct VeryAbstractListItemView: View {
    let i: Int

    var body: some View {
        Text("Default view \(i)")
    }
}

#Preview {
    Group {
        VeryAbstractListView([1, 2, 3, 4])
            .padding()
        VeryAbstractListView([1, 2, 3, 4]) { params in
            Text("Custom view \(params.item)")
        }
        .padding()
        VeryAbstractListView([1, 2, 3, 4]) { params in
            let isEven = params.index.isMultiple(of: 2)

            HStack {
                if isEven {
                    Text("Default view is flipped!")
                }

                params.view
                    .rotationEffect(isEven ? .degrees(180) : .zero)
            }

        }
    }
}

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.