Created
March 28, 2021 16:10
-
-
Save alexanderedge/eaacad8f63dbde3e200a9e70ba625dd7 to your computer and use it in GitHub Desktop.
Custom picker using style via environment
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 | |
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