Skip to content

Instantly share code, notes, and snippets.

@ms-tii
Last active February 4, 2025 11:31
Show Gist options
  • Save ms-tii/7329dc57f8166dbbdc9ff926cfd8f156 to your computer and use it in GitHub Desktop.
Save ms-tii/7329dc57f8166dbbdc9ff926cfd8f156 to your computer and use it in GitHub Desktop.
FlipClock (SwiftUI)- To learn functionality like - Timer, AnyCancellable and Animation

FlipClock

FlipClock is a visually appealing and functional clock application implemented using SwiftUI. It features animated flip clock digits to display the current time in an engaging and modern way.

AppScreenshot

Table of Contents

Features

  • Smooth flipping animation for time changes.
  • Modular architecture for flexibility and scalability.
  • Fully implemented using SwiftUI for declarative UI development.
  • Customizable appearance for individual flip views.

Project Structure

The project consists of the following main components:

1. ClockView

  • Displays the complete clock interface with hours, minutes, and seconds.
  • Combines multiple FlipView components with separators (:).

2. FlipViewModel

  • Manages the time state and triggers updates for FlipView components.
  • Implements a timer to update time every second.

3. FlipView

  • A single digit display with animated top and bottom halves.
  • Uses SingleFlipView for each half.

4. SingleFlipView

  • Renders individual halves of a flip digit.
  • Provides alignment and padding configurations for top and bottom segments.

Usage

  1. Launch the app to view the clock interface.
  2. Observe the smooth flip animations as the time changes every second.

Customization

To customize the appearance or behavior:

  • Modify the FlipViewModel for animation timing or time formatting.
  • Adjust the SingleFlipView for padding, font size, or alignment.
  • Add/Modify the flip_background & text_color colors in assets,

Acknowledgments

This project was inspired by traditional flip clocks and aims to bring the nostalgic charm of flip digits into a modern app. Special thanks to the SwiftUI framework for simplifying declarative UI development.

struct ClockView: View {
let viewModel = ClockViewModel()
var body: some View {
VStack(spacing: 100){
Text("Flip Clock")
.font(.system(size: 40))
.fontWeight(.heavy)
.foregroundColor(.text)
HStack(spacing: 5) {
HStack(spacing: 5) {
FlipView(viewModel: viewModel.flipViewModels[0])
FlipView(viewModel: viewModel.flipViewModels[1])
}
Text(":")
.font(.system(size: 40))
.fontWeight(.heavy)
.foregroundColor(.text)
HStack(spacing: 5) {
FlipView(viewModel: viewModel.flipViewModels[2])
FlipView(viewModel: viewModel.flipViewModels[3])
}
Text(":")
.font(.system(size: 40))
.fontWeight(.heavy)
.foregroundColor(.text)
HStack(spacing: 5) {
FlipView(viewModel: viewModel.flipViewModels[4])
FlipView(viewModel: viewModel.flipViewModels[5])
}
}
Spacer()
}
}
}
class ClockViewModel {
init() {
setupTimer()
}
private(set) lazy var flipViewModels = { (0...5).map { _ in FlipViewModel() } }()
private func setupTimer() {
Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.map { [timeFormatter] in timeFormatter.string(from: $0)}
.removeDuplicates()
.sink(receiveValue: {[weak self] in self?.setTimeInViewModel(time: $0)})
.store(in: &cancellable)
}
private func setTimeInViewModel(time: String) {
zip(time, flipViewModels).forEach { number, viewModel in
viewModel.text = "\(number)"
}
}
private var cancellable = Set<AnyCancellable>()
private let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HHmmss"
return formatter
}()
}
struct FlipView: View {
init(viewModel: FlipViewModel) {
self.viewModel = viewModel
}
@ObservedObject var viewModel: FlipViewModel
var body: some View {
VStack(spacing: 0) {
ZStack {
SingleFlipView(text: viewModel.newValue ?? "", type: .top)
SingleFlipView(text: viewModel.oldValue ?? "", type: .top)
.rotation3DEffect(.init(degrees: self.viewModel.animateTop ? -90 : .zero),
axis: (1, 0, 0),
anchor: .bottom,
perspective: 0.5)
}
Color.seprator
.frame(height: 1)
ZStack {
SingleFlipView(text: viewModel.oldValue ?? "", type: .bottom)
SingleFlipView(text: viewModel.newValue ?? "", type: .bottom)
.rotation3DEffect(.init(degrees: self.viewModel.animateBottom ? .zero : 90),
axis: (1, 0, 0),
anchor: .top,
perspective: 0.5)
}
}
.fixedSize()
}
}
class FlipViewModel: ObservableObject, Identifiable {
var text: String = "" {
didSet {
updateText(old: oldValue, new: text)
}
}
@Published var newValue: String?
@Published var oldValue: String?
@Published var animateTop: Bool = false
@Published var animateBottom: Bool = false
func updateText(old: String?, new: String?) {
guard old != new else { return }
oldValue = old
animateTop = false
animateBottom = false
withAnimation(Animation.easeIn(duration: 0.2)) { [weak self] in
self?.newValue = new
self?.animateTop = true
}
withAnimation(Animation.easeOut(duration: 0.2).delay(0.2)) { [weak self] in
self?.animateBottom = true
}
}
}
struct SingleFlipView: View {
init(text: String, type: FlipType) {
self.text = text
self.type = type
}
var body: some View {
Text(text)
.font(.system(size: 40))
.fontWeight(.heavy)
.foregroundColor(.text)
.fixedSize()
.padding(type.padding, -20)
.frame(width: 15, height: 20, alignment: type.alignment)
.padding(type.paddingEdges, 10)
.clipped()
.background(Color.flipBackground)
.cornerRadius(4)
.padding(type.padding, -4.5)
.clipped()
}
enum FlipType {
case top
case bottom
var padding: Edge.Set {
switch self {
case .top:
return .bottom
case .bottom:
return .top
}
}
var paddingEdges: Edge.Set {
switch self {
case .top:
return [.top, .leading, .trailing]
case .bottom:
return [.bottom, .leading, .trailing]
}
}
var alignment: Alignment {
switch self {
case .top:
return .bottom
case .bottom:
return .top
}
}
}
// MARK: - Private
private let text: String
private let type: FlipType
}
struct SingleFlipView_Previews: PreviewProvider {
static var previews: some View {
SingleFlipView(text: "12", type: .top)
}
}

Comments are disabled for this gist.