admin管理员组

文章数量:1317909

I'm building a custom tabs component in SwiftUI, that I want to re-use. In order to do this, I have some UI that handles the state of the tabs that is custom and in order to show the content that belongs to the tab I want to use the native TabView. In order to make it re-usable, I want to be able to pass an array of a specific struct (lets call it TabInput) that includes an id, a title but also its content, which can be in theory any view, like a VStack, a List, etc. What I have now:

public struct TabInput: Identifiable {
    public let id: UUID = UUID()
    let title: Text
    // content property here..
}

public struct Tabs: View {
    @Binding var activeIndex: Int
    
    public var tabs: [TabInput]
    
    public var body: some View {
        TabBarView(activeIndex: $activeIndex, tabs: tabs.map { $0.title })
        
        TabView(selection: $activeIndex) {
            ForEach (tabs.indices, id: \.self) { tabId in
                // tabs content property here...
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    }
}

I've tried adding a ViewBuilder property to the TabInput struct with a generic, but that gives me the problem that I have to define the generic upfront when typing the tabs var: Reference to generic type 'TabInput' requires arguments in <...>. Whatever I try to use as argument there doesn't seem to work. My code so far:

public struct TabInput<Content: View>: Identifiable {
    public let id: UUID = UUID()
    let title: Text
    
    @ViewBuilder let content: () -> Content
    
    public init(title: Text, content: @escaping () -> Content) {
        self.title = title
        self.content = content
    }
}

public struct Tabs: View {
    @Binding var activeIndex: Int
    
    public var tabs: [TabInput]
    
    public var body: some View {
        TabBarView(activeIndex: $activeIndex, tabs: tabs.map { $0.title })
        
        TabView(selection: $activeIndex) {
            ForEach (tabs.indices, id: \.self) { tabId in
                tabs[tabId].content()
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    }
}


#Preview {
    Tabs(
        activeIndex: .constant(1),
        tabs: [
            TabInput(title: Text("Discover")) {
                HStack {
                    Text("Discover")
                }
            },
            TabInput(title: Text("For you")) {
                VStack {
                    Text("For you")
                }
            },
        ]
    )
    .loadCustomFontsForPreviews()
}

Anyone knows how I can make this better and achieve my goal?

I'm building a custom tabs component in SwiftUI, that I want to re-use. In order to do this, I have some UI that handles the state of the tabs that is custom and in order to show the content that belongs to the tab I want to use the native TabView. In order to make it re-usable, I want to be able to pass an array of a specific struct (lets call it TabInput) that includes an id, a title but also its content, which can be in theory any view, like a VStack, a List, etc. What I have now:

public struct TabInput: Identifiable {
    public let id: UUID = UUID()
    let title: Text
    // content property here..
}

public struct Tabs: View {
    @Binding var activeIndex: Int
    
    public var tabs: [TabInput]
    
    public var body: some View {
        TabBarView(activeIndex: $activeIndex, tabs: tabs.map { $0.title })
        
        TabView(selection: $activeIndex) {
            ForEach (tabs.indices, id: \.self) { tabId in
                // tabs content property here...
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    }
}

I've tried adding a ViewBuilder property to the TabInput struct with a generic, but that gives me the problem that I have to define the generic upfront when typing the tabs var: Reference to generic type 'TabInput' requires arguments in <...>. Whatever I try to use as argument there doesn't seem to work. My code so far:

public struct TabInput<Content: View>: Identifiable {
    public let id: UUID = UUID()
    let title: Text
    
    @ViewBuilder let content: () -> Content
    
    public init(title: Text, content: @escaping () -> Content) {
        self.title = title
        self.content = content
    }
}

public struct Tabs: View {
    @Binding var activeIndex: Int
    
    public var tabs: [TabInput]
    
    public var body: some View {
        TabBarView(activeIndex: $activeIndex, tabs: tabs.map { $0.title })
        
        TabView(selection: $activeIndex) {
            ForEach (tabs.indices, id: \.self) { tabId in
                tabs[tabId].content()
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    }
}


#Preview {
    Tabs(
        activeIndex: .constant(1),
        tabs: [
            TabInput(title: Text("Discover")) {
                HStack {
                    Text("Discover")
                }
            },
            TabInput(title: Text("For you")) {
                VStack {
                    Text("For you")
                }
            },
        ]
    )
    .loadCustomFontsForPreviews()
}

Anyone knows how I can make this better and achieve my goal?

Share Improve this question edited Jan 23 at 16:22 Joakim Danielson 52.1k5 gold badges33 silver badges71 bronze badges asked Jan 23 at 14:45 GiesburtsGiesburts 7,66816 gold badges52 silver badges92 bronze badges 4
  • I think you need variadic generics here – Cy-4AH Commented Jan 23 at 15:28
  • Have you considered taking a @ViewBuilder closure instead of an array of structs? – Sweeper Commented Jan 23 at 16:30
  • developer.apple/documentation/SwiftUI/… – malhal Commented Jan 23 at 16:49
  • Can you update your code to include the TabBarView which is missing, so it's reproducible? – Andrei G. Commented Jan 23 at 19:43
Add a comment  | 

2 Answers 2

Reset to default 1

You basically described the solution in your question:

but also its content, which can be in theory any view, like a VStack, a List, etc.

Since you need to pass multiple views as content to TabInput you need a @ViewBuilder, but you don't want to be restricted as to the type of views passed, which requires type erasure using AnyView.

So let the content be of type AnyView, pass a @ViewBuilder to the initializer, and treat the content as AnyView:

struct TabInput: Identifiable {
    
    //Parameters
    let id: UUID
    let title: Text
    let content: AnyView
    
    //Initializer
    init<Content: View>(id: UUID = UUID(), title: Text, @ViewBuilder content: () -> Content) {
        self.id = id
        self.title = title
        self.content = AnyView(content())
    }
}

Since your code wasn't reproducible due to the missing TabBarView, I put one together to get it working. If you provide your actual TabBarView, I will update the code below to include it:

import SwiftUI

struct TabInput: Identifiable {
    
    //Parameters
    let id: UUID
    let title: Text
    let content: AnyView
    
    //Initializer
    init<Content: View>(id: UUID = UUID(), title: Text, @ViewBuilder content: () -> Content) {
        self.id = id
        self.title = title
        self.content = AnyView(content())
    }
}

struct Tabs: View {
    
    //Parameters
    @Binding var activeIndex: Int
    let tabs: [TabInput]
    
    //Body
    var body: some View {
        ZStack(alignment: .bottom) {
            
            TabView(selection: $activeIndex) {
                ForEach(tabs.indices, id: \.self) { tabId in
                    tabs[tabId].content
                        .font(.largeTitle)
                }
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            
            TabBarView(activeIndex: $activeIndex, tabs: tabs)
                .animation(.easeInOut, value: activeIndex)
        }
    }
}

struct TabBarView: View {
    
    //Parameters
    @Binding var activeIndex: Int
    let tabs: [TabInput]
    
    //Body
    var body: some View {
        
        HStack {
            ForEach(tabs.indices, id: \.self) { tabIndex in
                Button {
                    withAnimation {
                        activeIndex = tabIndex
                    }
                } label: {
                    tabs[tabIndex].title
                        .frame(maxWidth: .infinity, alignment: .center)
                        .padding()
                        .foregroundStyle(activeIndex == tabIndex ? Color.white : Color.primary)
                }
                .background(
                    activeIndex == tabIndex ? Color.blue : Color.clear,
                    in: Capsule()
                )
            }
            .padding(.horizontal)
        }
    }
}

//Preview
#Preview("Tabs") {
    @Previewable @State var activeIndex: Int = 0 // <- use @Previewable so you don't have to use a constant value binding
    
    Tabs(
        activeIndex: $activeIndex,
        tabs: [
            TabInput(title: Text("Discover")) {
                HStack {
                    Text("Discover")
                }
            },
            TabInput(title: Text("For you")) {
                VStack {
                    Text("For you")
                }
            },
        ]
    )
}

Possibly this sample can be useful for you:

public struct SampleTabView<T: CaseIterable & Equatable>: View where T: Hashable {
    private let tab: Binding<T>
    private let views: [AnyView]
    
    public init<A: View, B: View> (tab: Binding<T>, @ViewBuilder content: () -> TupleView<(A, B)>) {
        self.tab = tab
        let views = content().value
        self.views = [AnyView(views.0), AnyView(views.1)]
    }
    
    public init<A: View, B: View, C: View>(tab: Binding<T>, @ViewBuilder content: () -> TupleView<(A, B, C)>) {
        self.tab = tab
        let views = content().value
        self.views = [AnyView(views.0), AnyView(views.1), AnyView(views.2)]
    }
    
    public var body: some View {
        let countMin = min(T.allCases.count, views.count)
        let selectedIdx = T.firstIdxOf(t: tab.wrappedValue)
        
        VStack {
            views[ min(selectedIdx, countMin) ]
        }
    }
}

本文标签: swiftHow to have any view as a property in a struct that can be pass to a view in an arrayStack Overflow