Created
December 21, 2022 10:23
-
-
Save HarshilShah/e997f321a747fb79a492e6cce5f4a6c5 to your computer and use it in GitHub Desktop.
A SwiftUI picker that tries to match the Years/Months/Days/All Photos palette in the Library tab of Photos for iOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
protocol TitleProvider { | |
var title: String { get } | |
} | |
extension ColorScheme { | |
var dual: ColorScheme { | |
switch self { | |
case .light: return .dark | |
case .dark: return .light | |
} | |
} | |
} | |
struct FrameKey: PreferenceKey { | |
static var defaultValue: CGRect { .zero } | |
static func reduce(value: inout CGRect, nextValue: () -> CGRect) { | |
value = nextValue() | |
} | |
} | |
extension View { | |
func onFrameChange( | |
in coordinateSpace: CoordinateSpace, | |
_ onChange: @escaping (CGRect) -> () | |
) -> some View { | |
overlay { | |
GeometryReader { proxy in | |
Color.clear.preference(key: FrameKey.self, value: proxy.frame(in: coordinateSpace)) | |
} | |
.onPreferenceChange(FrameKey.self, perform: onChange) | |
} | |
} | |
} | |
typealias PhotosStylePickerItem = TitleProvider & Hashable | |
struct PhotosStylePicker<Item: PhotosStylePickerItem>: View { | |
var label: LocalizedStringKey | |
@Binding var selectedItem: Item | |
var items: [Item] | |
private let coordinateSpaceName = "PhotosStylePicker" | |
@State private var frames: [Item: CGRect] = [:] | |
@Environment(\.colorScheme) private var colorScheme | |
init(label: LocalizedStringKey, selectedItem: Binding<Item>, items: [Item]) { | |
self.label = label | |
self._selectedItem = selectedItem | |
self.items = items | |
} | |
var body: some View { | |
HStack(spacing: 0) { | |
ForEach(items, id: \.self) { item in | |
Button(action: { selectedItem = item }) { | |
Text(item.title.capitalized) | |
.font(.callout.weight(.semibold)) | |
.lineLimit(1) | |
.foregroundStyle(foregroundStyle(for: item)) | |
.environment(\.colorScheme, colorScheme(for: item)) | |
.padding(.horizontal) | |
.padding(.vertical, 5) | |
.frame(maxWidth: .infinity) | |
} | |
.buttonStyle(.plain) | |
.onFrameChange(in: .named(coordinateSpaceName)) { | |
frames[item] = $0 | |
} | |
} | |
} | |
.background { | |
if let frame = frames[selectedItem] { | |
Capsule() | |
.fill(.thinMaterial.opacity(0.5)) | |
.environment(\.colorScheme, colorScheme.dual) | |
.frame(width: frame.size.width, height: frame.size.height) | |
.position(CGPoint(x: frame.midX, y: frame.midY)) | |
.animation(.interactiveSpring(), value: selectedItem) | |
} | |
} | |
.coordinateSpace(name: coordinateSpaceName) | |
.padding(5) | |
.environment(\.colorScheme, colorScheme) | |
.background(.thinMaterial, in: Capsule()) | |
.accessibilityRepresentation { | |
Picker( | |
label, | |
selection: $selectedItem | |
) { | |
ForEach(items, id: \.self) { item in | |
Text(item.title) | |
.tag(item) | |
} | |
} | |
} | |
} | |
func colorScheme(for item: Item) -> ColorScheme { | |
if item == selectedItem { | |
return .dark | |
} else { | |
return colorScheme | |
} | |
} | |
func foregroundStyle(for item: Item) -> Color { | |
if item == selectedItem { | |
return .primary | |
} else { | |
return .secondary | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
protocol TitleProvider { | |
var title: String { get } | |
} | |
extension ColorScheme { | |
var dual: ColorScheme { | |
switch self { | |
case .light: return .dark | |
case .dark: return .light | |
} | |
} | |
} | |
struct ItemFrame: Equatable { | |
var itemTitle: String | |
var anchorBounds: Anchor<CGRect>? = nil | |
} | |
struct ItemFramesPreferenceKey: PreferenceKey { | |
static var defaultValue: [ItemFrame] = [] | |
static func reduce(value: inout [ItemFrame], nextValue: () -> [ItemFrame]) { | |
value.append(contentsOf: nextValue()) | |
} | |
} | |
typealias PhotosStylePickerItem = TitleProvider & Hashable | |
struct PhotosStylePicker<Item: PhotosStylePickerItem>: View { | |
var label: LocalizedStringKey | |
@Binding var selectedItem: Item | |
var items: [Item] | |
@Environment(\.colorScheme) private var colorScheme | |
init(label: LocalizedStringKey, selectedItem: Binding<Item>, items: [Item]) { | |
self.label = label | |
self._selectedItem = selectedItem | |
self.items = items | |
} | |
var body: some View { | |
HStack(spacing: 0) { | |
ForEach(items, id: \.self) { item in | |
Button(action: { selectedItem = item }) { | |
Text(item.title.capitalized) | |
.font(.callout.weight(.semibold)) | |
.lineLimit(1) | |
.foregroundStyle(foregroundStyle(for: item)) | |
.environment(\.colorScheme, colorScheme(for: item)) | |
.padding(.horizontal) | |
.padding(.vertical, 5) | |
.frame(maxWidth: .infinity) | |
} | |
.buttonStyle(.plain) | |
.anchorPreference(key: ItemFramesPreferenceKey.self, value: .bounds) { | |
[ItemFrame(itemTitle: item.title, anchorBounds: $0)] | |
} | |
} | |
} | |
.backgroundPreferenceValue(ItemFramesPreferenceKey.self) { preferences in | |
GeometryReader { proxy in | |
if let anchorBounds = preferences.first(where: { $0.itemTitle == selectedItem.title })?.anchorBounds { | |
let bounds = proxy[anchorBounds] | |
Capsule() | |
.fill(.thinMaterial.opacity(0.5)) | |
.environment(\.colorScheme, colorScheme.dual) | |
.frame(width: bounds.width, height: bounds.height) | |
.offset(x: bounds.minX, y: bounds.minY) | |
.animation(.interactiveSpring(), value: selectedItem) | |
} | |
} | |
} | |
.padding(5) | |
.environment(\.colorScheme, colorScheme) | |
.background(.thinMaterial, in: Capsule()) | |
.accessibilityRepresentation { | |
Picker( | |
label, | |
selection: $selectedItem | |
) { | |
ForEach(items, id: \.self) { item in | |
Text(item.title) | |
.tag(item) | |
} | |
} | |
} | |
} | |
func colorScheme(for item: Item) -> ColorScheme { | |
if item == selectedItem { | |
return .dark | |
} else { | |
return colorScheme | |
} | |
} | |
func foregroundStyle(for item: Item) -> Color { | |
if item == selectedItem { | |
return .primary | |
} else { | |
return .secondary | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment