Skip to content

Instantly share code, notes, and snippets.

@RobertMenke
Created January 16, 2021 21:43
Show Gist options
  • Save RobertMenke/6ab8cad5d116d23ce4654729e516559a to your computer and use it in GitHub Desktop.
Save RobertMenke/6ab8cad5d116d23ce4654729e516559a to your computer and use it in GitHub Desktop.
SwiftUI Progress Stepper Demo
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