Skip to content

Instantly share code, notes, and snippets.

@alexanderedge
Created March 28, 2021 16:10
Show Gist options
  • Save alexanderedge/eaacad8f63dbde3e200a9e70ba625dd7 to your computer and use it in GitHub Desktop.
Save alexanderedge/eaacad8f63dbde3e200a9e70ba625dd7 to your computer and use it in GitHub Desktop.
Custom picker using style via environment
import SwiftUI
struct ContentView: View {
@State private var selection: Int = 0
var body: some View {
CustomPicker(selection: $selection) {
// ForEach(0..<10) {
// Text("\($0)")
// }
Text("First").tag(0)
Text("Second").tag(1)
VStack {
Image(systemName: "paperplane")
Text("Third")
}.tag(2)
Text("Fourth").tag(3)
}
}
}
protocol CustomPickerStyle {
func makeBody<SelectionValue: Hashable, Content: View>(selection: Binding<SelectionValue>, content: Content) -> AnyView
}
struct CustomPickerStyleEnvironmentKey: EnvironmentKey {
static var defaultValue: CustomPickerStyle = HorizontalCustomPickerStyle()
}
extension EnvironmentValues {
var customPickerStyle: CustomPickerStyle {
get { self[CustomPickerStyleEnvironmentKey.self] }
set { self[CustomPickerStyleEnvironmentKey.self] = newValue }
}
}
extension View {
func customPickerStyle<S: CustomPickerStyle>(_ style: S) -> some View {
environment(\.customPickerStyle, style)
}
}
extension View {
func findTag<T: Hashable>() -> T? {
if self is AnyView {
if let content = Mirror(reflecting: self).descendant("storage", "view") {
let tag = Mirror(reflecting: content).descendant("modifier", "value", "tagged")
if let tag = tag as? T {
return tag
} else {
return nil
}
} else {
return nil
}
} else {
return Mirror(reflecting: self).descendant("modifier", "value", "tagged") as? T
}
}
}
struct HorizontalCustomPickerStyle: CustomPickerStyle {
func makeBody<SelectionValue: Hashable, Content: View>(selection: Binding<SelectionValue>, content: Content) -> AnyView {
AnyView(LinearPicker(selection: selection, content: content))
}
}
struct LinearPicker<SelectionValue: Hashable, Content: View>: View {
@Namespace var ns
@Binding var selection: SelectionValue
let content: Content
@ViewBuilder
var body: some View {
let contentMirror = Mirror(reflecting: content)
if let contentDescendant = contentMirror.descendant("value") {
let blocksCount = Mirror(reflecting: contentDescendant).children.count
HStack {
ForEach(0..<blocksCount) { index in
if let value = contentMirror.descendant("value", ".\(index)"), let tupleBlock = AnyView(_fromValue: value), let tag: SelectionValue = tupleBlock.findTag() {
cell(for: tag, using: tupleBlock)
}
}
}
} else if let tag: SelectionValue = content.findTag() {
cell(for: tag, using: AnyView(content))
} else {
// TODO ForEach case
EmptyView()
}
}
private func cell(for tag: SelectionValue, using content: AnyView) -> some View {
Button {
selection = tag
} label: {
content
.padding(.vertical, 16)
}
.background(
Group {
if tag == selection {
Color.purple.frame(height: 2)
.matchedGeometryEffect(id: "selector", in: ns)
}
},
alignment: .bottom
)
.accentColor(tag == selection ? .purple : .black)
.animation(.easeInOut)
}
}
struct CustomPicker<SelectionValue, Content>: View where SelectionValue: Hashable, Content: View {
@Environment(\.customPickerStyle) private var style
@Binding var selection: SelectionValue
let content: Content
init(selection: Binding<SelectionValue>, @ViewBuilder content: () -> Content) {
self._selection = selection
self.content = content()
}
var body: some View {
style.makeBody(selection: $selection, content: content)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment