Created
January 16, 2021 21:43
-
-
Save RobertMenke/6ab8cad5d116d23ce4654729e516559a to your computer and use it in GitHub Desktop.
SwiftUI Progress Stepper Demo
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
import SwiftUI | |
/// Diameter of the circle that represents an individual step in the stepper | |
private let ITEM_DIAMETER: CGFloat = 24 | |
/// Intended to be implemented by an enum that models each step in a progress stepper | |
protocol ProgressStepperDescriptor { | |
/// Get all steps for a given instance. This allows an enum to provide some subset of its cases to the stepper rather than | |
/// assuming that MyEnum.allCases is always representative of what the user sees. | |
func getSteps() -> [ProgressStepperDescriptor] | |
/// Used to determine if a tap is actionable. Skipping ahead is generally not the desired behavior. | |
func getStepIndex() -> UInt | |
/// Description underneath the stepper's circle | |
func getTitle() -> String | |
/// Determines the appearance of the circle | |
func hasBeenVisited(currentStep: ProgressStepperDescriptor) -> Bool | |
} | |
struct ProgressStepper: View { | |
let currentStep: ProgressStepperDescriptor | |
let items: [ProgressStepperDescriptor] | |
var backgroundColor: Color = .black | |
var onItemTapped: (ProgressStepperDescriptor) -> Void | |
var body: some View { | |
HStack(alignment: .center) { | |
ForEach(items.indices) { index in | |
VStack(alignment: .center, spacing: 4) { | |
if items[index].hasBeenVisited(currentStep: currentStep) { | |
ProgressStepperStepVisited() | |
.transition(.pivot) | |
} else { | |
ProgressStepperStepNotVisited() | |
.transition(.pivot) | |
} | |
Text(items[index].getTitle()) | |
.font(.system(size: 10.0)) | |
.foregroundColor(Color.white) | |
.fixedSize() | |
} | |
.onTapGesture { | |
withAnimation { | |
onItemTapped(items[index]) | |
} | |
} | |
if index != items.count - 1 { | |
Rectangle() | |
.fill(Color.purple) | |
.frameFillWidth(height: 4) | |
.cornerRadius(2) | |
.padding(.bottom, 12) | |
} | |
} | |
} | |
.frame( | |
minWidth: 0, | |
maxWidth: .infinity, | |
minHeight: 0, | |
maxHeight: 40, | |
alignment: .center | |
) | |
} | |
} | |
struct ProgressStepperStepVisited: View { | |
var body: some View { | |
Circle() | |
.foregroundColor(Color.purple) | |
.frame(width: ITEM_DIAMETER, height: ITEM_DIAMETER, alignment: .center) | |
} | |
} | |
struct ProgressStepperStepNotVisited: View { | |
var backgroundColor: Color = .black | |
var body: some View { | |
ZStack { | |
Circle() | |
.strokeBorder(Color.purple, lineWidth: 4) | |
.background(Circle().foregroundColor(backgroundColor)) | |
.frame(width: ITEM_DIAMETER, height: ITEM_DIAMETER, alignment: .center) | |
Rectangle() | |
.fill(backgroundColor) | |
.frame(width: ITEM_DIAMETER, height: 8) | |
.rotationEffect(Angle(degrees: 45.0)) | |
Rectangle() | |
.fill(backgroundColor) | |
.frame(width: ITEM_DIAMETER, height: 8) | |
.rotationEffect(Angle(degrees: -45.0)) | |
} | |
} | |
} | |
/// Example usage of a progress stepper | |
struct ProgressStepperPreview: View { | |
@State private var currentStep = Step.two | |
var body: some View { | |
VStack { | |
Spacer() | |
ProgressStepper(currentStep: currentStep, items: currentStep.getSteps()) { item in | |
if let step = item as? Step { | |
currentStep = step | |
} | |
} | |
.padding([.leading, .trailing], 14) | |
Spacer() | |
} | |
.frame( | |
minWidth: 0, | |
maxWidth: .infinity, | |
minHeight: 0, | |
maxHeight: .infinity, | |
alignment: .topLeading | |
) | |
} | |
} | |
/// Example of an enum conforming to ProgressStepperDescriptor | |
enum Step: UInt, Identifiable, CaseIterable, ProgressStepperDescriptor { | |
case one | |
case two | |
case three | |
case four | |
var id: UInt { | |
get { rawValue } | |
} | |
func getTitle() -> String { | |
switch self { | |
case .one: | |
return "One" | |
case .two: | |
return "Two" | |
case .three: | |
return "Three" | |
case .four: | |
return "Four" | |
} | |
} | |
func hasBeenVisited(currentStep: ProgressStepperDescriptor) -> Bool { | |
return self.rawValue <= currentStep.getStepIndex() | |
} | |
func getStepIndex() -> UInt { | |
rawValue | |
} | |
func getSteps() -> [ProgressStepperDescriptor] { | |
Step.allCases | |
} | |
} | |
/// Custom transition that pivots one circle out and another in to view | |
extension AnyTransition { | |
static var pivot: AnyTransition { | |
let insertion = AnyTransition | |
.modifier( | |
active: CornerRotateModifier(amount: -90, anchor: .center), | |
identity: CornerRotateModifier(amount: 0, anchor: .center) | |
) | |
.combined(with: .opacity) | |
let removal = AnyTransition | |
.modifier( | |
active: CornerRotateModifier(amount: 90, anchor: .center), | |
identity: CornerRotateModifier(amount: 0, anchor: .center) | |
) | |
.combined(with: .opacity) | |
return .asymmetric(insertion: insertion, removal: removal) | |
} | |
} | |
struct CornerRotateModifier: ViewModifier { | |
let amount: Double | |
let anchor: UnitPoint | |
func body(content: Content) -> some View { | |
content.rotationEffect(.degrees(amount), anchor: anchor).clipped() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment