Created
September 27, 2024 17:10
-
-
Save erenkabakci/0231d51bbce1cab9a66f5b0112d0befc to your computer and use it in GitHub Desktop.
transition.swift
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 | |
@main | |
struct magic_switcherApp: App { | |
var body: some Scene { | |
WindowGroup { | |
ContentView() | |
.environmentObject(ThemeObservable()) | |
} | |
} | |
} | |
class ThemeObservable: ObservableObject { | |
@Published var theme: Theme = .curious | |
func toggle() { | |
theme.toggle() | |
} | |
} | |
enum Theme: String, CaseIterable { | |
case curious | |
case fun | |
var primaryColor: Color { | |
Color("\(self.rawValue)Primary") | |
} | |
var secondaryColor: Color { | |
Color("\(self.rawValue)Secondary") | |
} | |
var icon: String { | |
switch self { | |
case .curious: return "text.book.closed" | |
case .fun: return "balloon" | |
} | |
} | |
var title: String { | |
switch self { | |
case .curious: return "curious" | |
case .fun: return "fun" | |
} | |
} | |
var transitionAnimation: Animation { | |
switch self { | |
case .curious: return .bouncy(duration: 0.3, extraBounce: 0.35) | |
case .fun: return .linear(duration: 0.2) | |
} | |
} | |
func switherTransitionAnimationBlock(animationBlock: @escaping () -> Void) -> Void { | |
switch self { | |
case .curious: return { withAnimation(self.transitionAnimation) { | |
animationBlock() | |
}}() | |
case .fun: return { withAnimation(self.transitionAnimation) { | |
animationBlock() | |
}}() | |
} | |
} | |
mutating func toggle() { | |
switch self { | |
case .curious: self = .fun | |
case .fun: self = .curious | |
} | |
} | |
} | |
import SwiftUI | |
struct ContentView: View { | |
@EnvironmentObject var themeObserver: ThemeObservable | |
var body: some View { | |
VStack { | |
HeaderView() | |
.background(Color.gray.opacity(0.2).blur(radius: 12)) | |
Spacer() | |
} | |
.background(Color("bgColor")) | |
} | |
} | |
struct HeaderView: View { | |
@EnvironmentObject var themeObserver: ThemeObservable | |
@State private var opacity: Double = 1.0 | |
var body: some View { | |
ZStack { | |
HStack { | |
if themeObserver.theme == .fun { | |
HStack { | |
Text(Theme.fun.title) | |
Text("mode").opacity(opacity) | |
Spacer() | |
} | |
.font(.headline) | |
.transition(.move(edge: .leading).combined(with: .opacity)) | |
} else { | |
HStack { | |
Text(Theme.curious.title) | |
Text("mode").opacity(opacity) | |
Spacer() | |
} | |
.font(.headline) | |
.transition(.move(edge: .leading).combined(with: .opacity)) | |
} | |
Spacer() | |
if themeObserver.theme == .curious { | |
TimerBadge() | |
.transition(.move(edge: .trailing).combined(with: .opacity)) | |
} | |
} | |
.foregroundColor(Color("textColor")) | |
.animation(.linear(duration: 0.3), value: themeObserver.theme) | |
.onChange(of: themeObserver.theme, { _, theme in | |
opacity = 1 | |
withAnimation(.easeInOut(duration: 1.5)) { | |
opacity = 0 | |
} | |
}) | |
ThemeSwitch() | |
.frame(width: ThemeSwitch.Constants.switchWidth, height: ThemeSwitch.Constants.switchHeight) | |
.gradientGlow( | |
gradient: LinearGradient( | |
gradient: Gradient( | |
colors: [.blue, .blue, .black, .purple, .purple]), | |
startPoint: .topLeading, | |
endPoint: .bottomTrailing | |
), | |
radius: 12, | |
size: CGSize(width: ThemeSwitch.Constants.switchWidth, height: ThemeSwitch.Constants.switchHeight * 0.75), | |
applicationCount: 3 | |
) | |
} | |
.padding(.horizontal, 8) | |
.background(themeObserver.theme.primaryColor) | |
} | |
} | |
struct ThemeSwitch: View { | |
@EnvironmentObject var themeObserver: ThemeObservable | |
@State private var isPulsing = false | |
@State private var selectedTheme: Theme = .curious | |
enum Constants { | |
static let horizontalOffset: CGFloat = 27 | |
static let switchWidth: CGFloat = 100 | |
static let switchHeight: CGFloat = 44 | |
} | |
var body: some View { | |
ZStack { | |
RoundedRectangle(cornerRadius: 18) | |
.fill(selectedTheme.primaryColor) | |
.opacity(0.7) | |
ZStack { | |
RoundedRectangle(cornerRadius: 18) | |
.fill(selectedTheme.secondaryColor) | |
.frame(width: Constants.switchWidth/2) | |
.offset(x: selectedTheme == .curious ? -Constants.horizontalOffset : Constants.horizontalOffset) | |
.scaleEffect(isPulsing ? 0.5 : 1.0) | |
HStack { | |
themeImage(for: .curious) | |
themeImage(for: .fun) | |
} | |
} | |
.padding(4) | |
.background(Color.black.opacity(0.1)) | |
} | |
.cornerRadius(20) | |
.onTapGesture { | |
themeObserver.toggle() | |
selectedTheme.switherTransitionAnimationBlock { | |
selectedTheme = themeObserver.theme | |
} | |
if selectedTheme == .curious { | |
withAnimation( | |
.easeInOut(duration: 0.1) | |
.delay(0.0) | |
) { | |
isPulsing.toggle() | |
} | |
withAnimation( | |
.easeInOut(duration: 0.1) | |
.delay(0.1) | |
) { | |
isPulsing.toggle() | |
} | |
} | |
} | |
} | |
private func themeImage(for theme: Theme) -> some View { | |
Image(systemName: theme.icon) | |
.font(.headline) | |
.padding(.vertical, 8) | |
.padding(.horizontal, 16) | |
.foregroundColor(selectedTheme == theme ? selectedTheme.primaryColor : selectedTheme.secondaryColor) | |
} | |
} | |
extension Date { | |
var isToday: Bool { | |
Calendar.current.startOfDay(for: Date()) == Calendar.current.startOfDay(for: self) | |
} | |
} | |
struct TimerBadge: View { | |
@State private var isExpanded = false | |
@State private var isExpired = false | |
@State private var timerLabel: String = "" | |
@State private var timer: Timer? | |
@State private var sessionEndsAt: Date = Date().addingTimeInterval(60 * 60) | |
@Environment(\.colorScheme) var colorScheme | |
var color: Color { | |
switch colorScheme { | |
case .dark: return .white | |
case .light: fallthrough | |
@unknown default: return .white | |
} | |
} | |
var body: some View { | |
Button(action: { | |
withAnimation(.easeInOut(duration: 0.3)) { | |
isExpanded.toggle() | |
} | |
}) { | |
HStack(spacing: 4) { | |
Image(systemName: "hourglass") | |
if isExpanded { | |
Text(timerLabel) | |
} | |
} | |
.padding(.horizontal, 12) | |
.padding(.vertical, 6) | |
.background(Capsule().stroke(lineWidth: 1)) | |
} | |
.foregroundColor(color) | |
.onAppear(perform: startTimer) | |
.onDisappear(perform: stopTimer) | |
} | |
private func startTimer() { | |
updateRemainingTime() | |
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in | |
updateRemainingTime() | |
} | |
} | |
private func stopTimer() { | |
timer?.invalidate() | |
timer = nil | |
} | |
private func updateRemainingTime() { | |
if .now < sessionEndsAt { | |
let remainingInterval = sessionEndsAt.timeIntervalSince(.now) | |
let minutes = (remainingInterval / 60) | |
let seconds = Int(remainingInterval) % 60 | |
if minutes < 2, minutes > 1 { | |
timerLabel = String(format: "%2im %is left", Int(minutes), Int(seconds)) | |
} else if minutes < 1 { | |
timerLabel = String(format: "%2is left", Int(seconds)) | |
} else { | |
timerLabel = String(format: "%i min left", Int(minutes)) | |
} | |
} else { | |
timerLabel = "time expired" | |
isExpired = true | |
} | |
} | |
} | |
extension View { | |
func gradientGlow<G: ShapeStyle & View>( | |
gradient: G, | |
radius: CGFloat, | |
size: CGSize, | |
applicationCount: Int = 1 | |
) -> some View { | |
ZStack { | |
ForEach(0..<applicationCount) { i in | |
Rectangle() | |
.fill(gradient) | |
.frame(width: size.width * 2, height: size.height * 2) | |
.mask( | |
Rectangle() | |
.frame(width: size.width, height: size.height) | |
.blur(radius: 12) | |
) | |
} | |
self | |
} | |
} | |
} | |
#Preview { | |
ContentView() | |
.environmentObject(ThemeObservable()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment