Skip to content

Instantly share code, notes, and snippets.

@erenkabakci
Created September 27, 2024 17:10
Show Gist options
  • Save erenkabakci/0231d51bbce1cab9a66f5b0112d0befc to your computer and use it in GitHub Desktop.
Save erenkabakci/0231d51bbce1cab9a66f5b0112d0befc to your computer and use it in GitHub Desktop.
transition.swift
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