Skip to content

Instantly share code, notes, and snippets.

@end3r117
Last active May 28, 2020 23:59
Show Gist options
  • Save end3r117/2ab2ce89d680b8b091d35e92d344d94a to your computer and use it in GitHub Desktop.
Save end3r117/2ab2ce89d680b8b091d35e92d344d94a to your computer and use it in GitHub Desktop.
Playground: SwiftUI - SegmentedPicker custom control example
//SegmentedPickerExample.playground
//Created by (End3r117) on 5/28/20.
//
//Note: tiny indentation was intentional to paste into this gist. You might want to do a re-indent in XCode.
import SwiftUI
import PlaygroundSupport
fileprivate
struct SPPreferenceKey<Collection:RandomAccessCollection>: PreferenceKey {
typealias Value = [AnyHashable: (index: Collection.Index, anchor: Anchor<CGRect>)]
static var defaultValue: Value { [:] }
static func reduce(value: inout Value, nextValue: () -> Value) {
value.merge(nextValue(), uniquingKeysWith: { $1 })
}
}
struct SegmentedPicker<Values, Content>: View where
Values: RandomAccessCollection,
Values.Element: Hashable,
Values.Index: Hashable,
Content: View
{
@Binding var selectedValue: Values.Element
@GestureState private var dragAmount: CGFloat = 0
private let selections: Values
private let content: (Values.Element) -> Content
private let barHeight: CGFloat
private let barColor: Color
private let indicatorColor: Color
private let indicatorCoordinateSpace = "SegmentedPickerInner"
init(selectedValue: Binding<Values.Element>,
selections: Values,
barHeight: CGFloat = 40,
barColor: Color = Color(.lightGray),
indicatorColor: Color = Color(.systemGray),
@ViewBuilder content: @escaping (Values.Element) -> Content) {
self._selectedValue = selectedValue
self.selections = selections
self.barHeight = barHeight
self.barColor = barColor
self.indicatorColor = indicatorColor
self.content = content
}
var body: some View {
RoundedRectangle(cornerRadius: 8)
.fill(self.barColor)
.padding(.horizontal)
.frame(height: barHeight)
.allowsHitTesting(false)
.overlay(
HStack {
ForEach(self.selections.indices, id: \.self) { selectionIdx in
self.content(self.selections[selectionIdx])
.frame(maxWidth: .infinity)
.anchorPreference(key: SPPreferenceKey<Values>.self, value: .bounds, transform: {
[self.selections[selectionIdx]:(selectionIdx, $0)]
})
.padding()
.foregroundColor(self.selections[selectionIdx] == self.selectedValue ? .primary : .secondary)
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 20, coordinateSpace: .named(self.indicatorCoordinateSpace))
.updating(self.$dragAmount, body: { (value, state, transaction) in
guard self.selections[selectionIdx] == self.selectedValue else { return }
state = value.translation.width
})
.onChanged({ (val) in
guard self.selections[selectionIdx] == self.selectedValue else { return }
if val.translation.width > 0,
selectionIdx != self.selections.endIndex {
let next = self.selections.index(after: selectionIdx)
guard self.selections.indices.contains(next) else { return }
withAnimation(.easeOut(duration: 0.3)) {
self.selectedValue = self.selections[next]
}
}else if val.translation.width < 0,
selectionIdx != self.selections.startIndex {
let previous = self.selections.index(before: selectionIdx)
guard self.selections.indices.contains(previous) else { return }
withAnimation(.easeOut(duration: 0.3)) {
self.selectedValue = self.selections[previous]
}
}
})
)
.gesture(
TapGesture().onEnded({
withAnimation(.easeOut(duration: 0.3)) {
self.selectedValue = self.selections[selectionIdx]
}
})
)
}
}
.backgroundPreferenceValue(SPPreferenceKey<Values>.self, { (preferences) in
GeometryReader { innerGeo in
self.makeBackgroundIndicator(geometryProxy: innerGeo, preferences: preferences)
}
.coordinateSpace(name: self.indicatorCoordinateSpace)
})
)
}
fileprivate func makeBackgroundIndicator(geometryProxy geo: GeometryProxy, preferences: SPPreferenceKey<Values>.Value) -> some View {
var selectionRect: CGRect?
if let selectedAnchor = preferences[self.selectedValue]?.anchor {
let rect = geo[selectedAnchor]
selectionRect = rect
}
return Group {
if selectionRect != nil {
RoundedRectangle(cornerRadius: 8)
.fill(indicatorColor)
.position(CGPoint(x: selectionRect!.midX, y: selectionRect!.midY))
.frame(width: selectionRect!.size.width * 0.90, height: barHeight * 0.80)
.shadow(color: Color(UIColor.black.withAlphaComponent(0.3)), radius: 1, x: 0, y: 0)
}
}
}
}
struct ContentView: View {
enum Selection: Int, CaseIterable {
case blogs, evaluation
var stringValue: String {
switch self {
case .blogs: return "Blogs"
case .evaluation: return "Evaluation"
}
}
}
let strings = ["Oh", "Hi", "Mark"]
let selections = Selection.allCases
@State private var strSelection: Int = 0
@State private var selection = Selection.blogs
var body: some View {
ScrollView {
VStack {
SegmentedPicker(selectedValue: $selection, selections: selections, content: { selection in
Text(selection.stringValue)
})
Divider()
SegmentedPicker(selectedValue: $strSelection, selections: strings.indices, barColor: Color(.systemBlue), indicatorColor: .white, content: { selection in
Group {
if selection == self.strSelection {
Text(self.strings[selection])
.bold()
}else {
Text(self.strings[selection])
}
}
.foregroundColor(selection == self.strSelection ? .green : .secondary)
.animation(nil)
})
}
}
.frame(maxHeight: 250)
.colorScheme(.light)
}
}
PlaygroundPage.current.setLiveView(ContentView())
@end3r117
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment