Last active
May 28, 2020 23:59
-
-
Save end3r117/2ab2ce89d680b8b091d35e92d344d94a to your computer and use it in GitHub Desktop.
Playground: SwiftUI - SegmentedPicker custom control example
This file contains hidden or 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
//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()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Preview Image