Created
July 27, 2020 15:55
-
-
Save john-mueller/6b8b963c26244964bf533403a88a0052 to your computer and use it in GitHub Desktop.
SwiftUI animation proof-of-concept
This file contains 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
// | |
// 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