SwiftUI Generic ControlBar
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()
}