Skip to content

Instantly share code, notes, and snippets.

@apptekstudios
Last active May 28, 2021 02:35
Show Gist options
  • Save apptekstudios/34d8abc8aa9f82512cf2de6013692b7f to your computer and use it in GitHub Desktop.
Save apptekstudios/34d8abc8aa9f82512cf2de6013692b7f to your computer and use it in GitHub Desktop.
A variation on the picker designed by Federico Zanetello in his blog post (https://fivestars.blog/swiftui/inspecting-views.html) that allows any type of content to be used
//
// CustomPicker.swift
//
// Created by T Brennan on 27/3/21.
//
import SwiftUI
struct ContentView: View {
@State private var selection: Int? = 0
var body: some View {
CustomPicker(selection: $selection) {
Text("First").tag(0)
Text("Second").tag(1)
VStack {
Image(systemName: "paperplane")
Text("Third")
}.tag(2)
Text("Fourth").tag(3)
}
}
}
struct CustomPicker<SelectionValue: Hashable>: View {
@Namespace var ns
@Binding var selection: SelectionValue?
@ViewArrayBuilder let content: [AnyView]
public var body: some View {
HStack {
ForEach(content.indexed(), id: \.index) { indexedItem in
let tag: SelectionValue? = indexedItem.element.findTag()
Button {
selection = tag
} label: {
indexedItem.element
.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)
}
}
}
}
// BUILD AN ARRAY OF VIEWS
@_functionBuilder
public struct ViewArrayBuilder
{
public enum Wrapper
{
case empty
case view(AnyView)
case group([Wrapper])
init<Content: View>(_ view: Content)
{
self = .view(AnyView(view))
}
func flattened() -> [AnyView]
{
switch self
{
case .empty:
return []
case let .view(theView):
return [theView]
case let .group(wrappers):
return wrappers.flatMap { $0.flattened() }
}
}
}
public static func buildExpression<Content: View>(_ view: Content?) -> Wrapper
{
view.map { Wrapper($0) } ?? .empty
}
public static func buildExpression<Content: View>(_ views: [Content]?) -> Wrapper
{
guard let views = views else { return .empty }
return Wrapper.group(views.map { Wrapper($0) })
}
public static func buildEither(first: Wrapper) -> Wrapper
{
first
}
public static func buildEither(second: Wrapper) -> Wrapper
{
second
}
public static func buildIf(_ item: Wrapper?) -> Wrapper
{
item ?? .empty
}
public static func buildBlock(_ item0: Wrapper) -> Wrapper
{
item0
}
public static func buildBlock(_ items: Wrapper...) -> Wrapper
{
.group(items)
}
public static func buildFinalResult(_ component: Wrapper) -> [AnyView] {
component.flattened()
}
}
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") as? T
{
return tag
} else { return nil }
} else {
return Mirror(reflecting: self).descendant("modifier", "value", "tagged") as? T
}
}
}
/// Required to use our array in a for each with ease
struct Indexed<Element, Index>
{
var index: Index
var offset: Int
var element: Element
}
extension RandomAccessCollection
{
func indexed() -> AnyRandomAccessCollection<Indexed<Element, Index>>
{
AnyRandomAccessCollection(
zip(zip(indices, 0...).lazy, self).lazy
.map { Indexed(index: $0.0.0, offset: $0.0.1, element: $0.1) }
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment