Skip to content

Instantly share code, notes, and snippets.

@john-mueller
Created July 27, 2020 15:55
Show Gist options
  • Save john-mueller/6b8b963c26244964bf533403a88a0052 to your computer and use it in GitHub Desktop.
Save john-mueller/6b8b963c26244964bf533403a88a0052 to your computer and use it in GitHub Desktop.
SwiftUI animation proof-of-concept
//
// WeekdayPicker.swift
//
// Created by John Mueller on 7/27/20.
import SwiftUI
enum Weekday: Int, CaseIterable {
case sunday
case monday
case tuesday
case wednesday
case thursday
case friday
case saturday
var letter: String {
String(String(describing: self).capitalized.first!)
}
var abbreviation: String {
String(String(describing: self).capitalized.prefix(3))
}
}
// Makes setting size of circles easier
extension View {
func frame(size: CGFloat) -> some View {
self.frame(width: size, height: size)
}
}
// Makes comparing integers in a switch statement easier
extension Int {
enum ComparisonResult {
case equal
case less
case greater
}
func compare(to other: Int) -> ComparisonResult {
if self < other { return .less }
if self > other { return .greater }
return .equal
}
}
class WeekdayPickerModel: ObservableObject {
// Tracks which label is expanded (from "S" to "Sun")
@Published var weekday: Weekday
// Tracks which circle is enlarged
@Published var number: Int
// Used to keep animation smooth just in case input data jumps around during animation
private var currentlyChanging = false
// The circle which
private var target: Int
// How long it takes to animate from one circle to the next
let incrementDuration = 0.05
init(selection: Weekday) {
self.weekday = selection
self.number = selection.rawValue
self.target = selection.rawValue
}
func animate(to day: Weekday) {
// Make sure another animation isn't already in progress
guard !currentlyChanging else { return }
currentlyChanging = true
// Set new target day
target = day.rawValue
// Begin animating
self.increment()
}
private func increment() {
// Delay the animation between circles by incrementDuration
DispatchQueue.main.asyncAfter(deadline: .now() + incrementDuration) { [weak self] in
// Make sure model still exists
guard let self = self else { return }
// Move one step closer to target
switch self.number.compare(to: self.target) {
case .less:
self.number += 1
case .greater:
self.number -= 1
case .equal:
// If we reached target, update the label and stop the increment() recursion
self.weekday = Weekday(rawValue: self.target)!
self.currentlyChanging = false
return
}
// Increment again, as we have not reached target
self.increment()
}
}
}
struct WeekdayPicker: View {
// An external binding, passed in so state is kept outside of picker
@Binding var selection: Weekday
// The internal model to handle animation state
@StateObject var model: WeekdayPickerModel
init(selection: Binding<Weekday>) {
self._selection = selection
// This syntax is required to inject the initial value of the binding into the model
self._model = StateObject(wrappedValue: WeekdayPickerModel(selection: selection.wrappedValue))
}
var body: some View {
HStack {
ForEach(Weekday.allCases, id: \.self) { day in
Button {
// When the button is pressed, update the binding
selection = day
} label: {
VStack {
Circle()
.strokeBorder(Color.gray, lineWidth: model.number == day.rawValue ? 2 : 1)
.frame(size: model.number == day.rawValue ? 25 : 15)
.frame(size: 25)
.animation(.easeInOut(duration: model.incrementDuration))
Text(model.weekday == day ? day.abbreviation : day.letter)
.fontWeight(model.weekday == day ? .bold : .regular)
.frame(maxWidth: .infinity)
.animation(.none)
}
}
.buttonStyle(PlainButtonStyle())
}
}
// When the binding changes, animate to the new selection
.onChange(of: selection) { newValue in
model.animate(to: newValue)
}
}
}
struct ContentView: View {
@State private var selection: Weekday = .wednesday
var body: some View {
VStack {
Picker("Weekday", selection: $selection) {
ForEach(Weekday.allCases, id: \.self) { day in
Text(day.abbreviation)
}
}
.pickerStyle(SegmentedPickerStyle())
WeekdayPicker(selection: $selection)
}
.animation(.default)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment