admaDIC App Development & IT Solutions

SwiftUI Generic ControlBar

by Annett Schwarze | 2026-01-16

In the example a re-usable view `AdaptiveControlBar` is built using generics. The view provides a sort button with configurable sort options, option titles and a binding for the selected sort option. This is a nice approach to re-use custom views and decouple them from specific dependencies.

The concrete sort options are defined in the type `EarningSortOption`. This type is provided as a generic type parameter for the control bar using `AdaptiveControlBar < EarningsSortOption > `.

        
import SwiftUI

struct SampleControlBarView: View {
    struct Earning: Identifiable {
        let id: UUID = UUID()
        let text: String
        let date: Date
        let amount: Double
    }

    enum EarningsSortOption: String, CaseIterable, Identifiable {
        case text
        case date
        case amount

        var displayString: String {
            switch self {
            case .text: return "Name"
            case .date: return "Date"
            case .amount: return "Amount"
            }
        }

        public var id: String { rawValue }
    }

    @State private var selectedSortOption: EarningsSortOption? = nil
    @State private var allEarnings: [Earning] = Self.sampleEarnings()
    @State private var earnings: [Earning] = []

    static func sampleEarnings() -> [Earning] {
        let result: [Earning] = [
            Earning(text: "Work", date: Date.now, amount: 2500.00),
            Earning(text: "Lottery", date: Date.now, amount: 100.00),
            Earning(text: "Inheritance", date: Date.now, amount: 4000.00),
        ]
        return result
    }

    let formatter: NumberFormatter = {
        let result = NumberFormatter()
        result.numberStyle = .currency
        return result
    }()

    var body: some View {
        VStack(spacing: 16, content: {
            Text("Adaptive ControlBar")
                .font(Font.largeTitle)

            VStack {
                AdaptiveControlBar < EarningsSortOption > (
                    sortOptions: EarningsSortOption.allCases,
                    displayName: { o in
                        return o.displayString
                    },
                    selection: $selectedSortOption,
                    labelForUnsorted: "Unsorted"
                )
                .padding(.horizontal)
                List() {
                    ForEach(earnings) { earning in
                        VStack(alignment: .leading, spacing: 6) {
                            Text(earning.text)
                                .foregroundStyle(.primary)
                                .frame(maxWidth: .infinity, alignment: .leading)
                            Text(formatter.string(from: NSNumber(value: earning.amount)) ?? "-")
                                .foregroundStyle(.primary)
                                .monospacedDigit()
                                .frame(maxWidth: .infinity, alignment: .trailing)
                        }
                    }
                }
            }
        })
        .task {
            earnings = allEarnings
        }
        .onChange(of: selectedSortOption, perform: { option in
            if let option {
                switch option {
                case .text: earnings = allEarnings.sorted(by: { a, b in a.text < b.text })
                case .date: earnings = allEarnings.sorted(by: { a, b in a.date < b.date })
                case .amount: earnings = allEarnings.sorted(by: { a, b in a.amount < b.amount })
                }
            } else {
                earnings = allEarnings
            }
        })
    }

    public struct AdaptiveControlBar < SortOption: Identifiable & Hashable > : View {
        public let sortOptions: [SortOption]
        public let displayName: (SortOption) -> String
        @Binding public var selection: SortOption?
        public let labelForUnsorted: String

        public init(
            sortOptions: [SortOption],
            displayName: @escaping (SortOption) -> String,
            selection: Binding,
            labelForUnsorted: String
        ) {
            self.sortOptions = sortOptions
            self.displayName = displayName
            self._selection = selection
            self.labelForUnsorted = labelForUnsorted
        }

        public var body: some View {
            HStack {
                Spacer()
                Menu {
                    Button {
                        selection = nil
                    } label: {
                        Text(labelForUnsorted)
                    }

                    ForEach(sortOptions) { option in
                        Button {
                            selection = option
                        } label: {
                            Text(displayName(option))
                        }
                    }
                } label: {
                    Label(selection.map({ displayName($0) }) ?? labelForUnsorted,
                          systemImage: "arrow.up.arrow.down")
                        .font(.headline)
                        .padding(.horizontal, 8)
                        .padding(.vertical, 6)
                }
            }
        }
    }
}

#Preview {
    SampleControlBarView()
}
    
SwiftUI Generic ControlBar

 

www.admadic.de | webmaster@admadic.de | Legal Notice and Trademarks | Privacy
© 2005-2007 - admaDIC | All Rights Reserved
All other trademarks and/or registered trademarks are the property of their respective owners
Last Change: Fri Jan 16 09:41:33 2026 GMT