Skip to content

Commit e7470b4

Browse files
Merge pull request #32 from componentskit/SUCountdown
SUCountdown
2 parents 45998ef + 26b1ec1 commit e7470b4

File tree

11 files changed

+930
-1
lines changed

11 files changed

+930
-1
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import ComponentsKit
2+
import SwiftUI
3+
import UIKit
4+
5+
struct CountdownPreview: View {
6+
@State private var model = CountdownVM()
7+
8+
var body: some View {
9+
VStack {
10+
PreviewWrapper(title: "UIKit") {
11+
UKCountdown(model: self.model)
12+
.preview
13+
}
14+
PreviewWrapper(title: "SwiftUI") {
15+
SUCountdown(model: self.model)
16+
}
17+
Form {
18+
ComponentOptionalColorPicker(selection: self.$model.color)
19+
FontPicker(selection: self.$model.font)
20+
Picker("Locale", selection: self.$model.locale) {
21+
Text("Current").tag(Locale.current)
22+
Text("EN").tag(Locale(identifier: "en"))
23+
Text("ES").tag(Locale(identifier: "es"))
24+
Text("FR").tag(Locale(identifier: "fr"))
25+
Text("DE").tag(Locale(identifier: "de"))
26+
Text("ZH").tag(Locale(identifier: "zh"))
27+
Text("JA").tag(Locale(identifier: "ja"))
28+
Text("RU").tag(Locale(identifier: "ru"))
29+
Text("AR").tag(Locale(identifier: "ar"))
30+
Text("HI").tag(Locale(identifier: "hi"))
31+
Text("PT").tag(Locale(identifier: "pt"))
32+
}
33+
SizePicker(selection: self.$model.size)
34+
Picker("Style", selection: self.$model.style) {
35+
Text("Plain").tag(CountdownVM.Style.plain)
36+
Text("Light").tag(CountdownVM.Style.light)
37+
}
38+
Picker("Units Style", selection: self.$model.unitsStyle) {
39+
Text("None").tag(CountdownVM.UnitsStyle.hidden)
40+
Text("Bottom").tag(CountdownVM.UnitsStyle.bottom)
41+
Text("Trailing").tag(CountdownVM.UnitsStyle.trailing)
42+
}
43+
DatePicker("Until Date", selection: self.$model.until, in: Date()..., displayedComponents: [.date, .hourAndMinute])
44+
.datePickerStyle(.compact)
45+
}
46+
}
47+
}
48+
}
49+
50+
#Preview {
51+
CountdownPreview()
52+
}

Examples/DemosApp/DemosApp/Core/App.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ struct App: View {
1212
NavigationLinkWithTitle("Checkbox") {
1313
CheckboxPreview()
1414
}
15+
NavigationLinkWithTitle("Countdown") {
16+
CountdownPreview()
17+
}
1518
NavigationLinkWithTitle("Divider") {
1619
DividerPreview()
1720
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
enum CountdownHelpers {
4+
enum Unit {
5+
case days
6+
case hours
7+
case minutes
8+
case seconds
9+
}
10+
11+
enum UnitLength {
12+
case short
13+
case long
14+
}
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import UIKit
2+
3+
struct CountdownWidthCalculator {
4+
private static let label = UILabel()
5+
6+
private init() {}
7+
8+
static func preferredWidth(
9+
for attributedText: NSAttributedString,
10+
model: CountdownVM
11+
) -> CGFloat {
12+
self.style(label, with: model)
13+
label.attributedText = attributedText
14+
15+
let targetSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: model.height)
16+
let estimatedSize = label.sizeThatFits(targetSize)
17+
18+
return estimatedSize.width
19+
}
20+
21+
private static func style(_ label: UILabel, with model: CountdownVM) {
22+
label.numberOfLines = 0
23+
}
24+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import Foundation
2+
3+
// MARK: - UnitsLocalization
4+
5+
/// A structure that provides localized representations of time units (seconds, minutes, hours, days).
6+
public struct UnitsLocalization: Equatable {
7+
/// A structure that represents the localized short and long forms of a single time unit.
8+
public struct UnitItemLocalization: Equatable {
9+
/// The short-form representation of the time unit (e.g., "s" for seconds).
10+
public let short: String
11+
/// The long-form representation of the time unit (e.g., "Seconds").
12+
public let long: String
13+
14+
/// Initializes a new `UnitItemLocalization` with specified short and long forms.
15+
///
16+
/// - Parameters:
17+
/// - short: The short-form representation of the time unit.
18+
/// - long: The long-form representation of the time unit.
19+
public init(short: String, long: String) {
20+
self.short = short
21+
self.long = long
22+
}
23+
}
24+
25+
// MARK: - Properties
26+
27+
/// The localized representation for seconds.
28+
public let seconds: UnitItemLocalization
29+
30+
/// The localized representation for minutes.
31+
public let minutes: UnitItemLocalization
32+
33+
/// The localized representation for hours.
34+
public let hours: UnitItemLocalization
35+
36+
/// The localized representation for days.
37+
public let days: UnitItemLocalization
38+
39+
// MARK: - Initialization
40+
41+
/// Initializes a new `UnitsLocalization` with localized representations for all time units.
42+
///
43+
/// - Parameters:
44+
/// - seconds: The localization for seconds.
45+
/// - minutes: The localization for minutes.
46+
/// - hours: The localization for hours.
47+
/// - days: The localization for days.
48+
public init(
49+
seconds: UnitItemLocalization,
50+
minutes: UnitItemLocalization,
51+
hours: UnitItemLocalization,
52+
days: UnitItemLocalization
53+
) {
54+
self.seconds = seconds
55+
self.minutes = minutes
56+
self.hours = hours
57+
self.days = days
58+
}
59+
}
60+
61+
// MARK: - Localizations
62+
63+
extension UnitsLocalization {
64+
static let defaultLocalizations: [Locale: UnitsLocalization] = [
65+
// English (en)
66+
Locale(identifier: "en"): UnitsLocalization(
67+
seconds: .init(short: "s", long: "Seconds"),
68+
minutes: .init(short: "m", long: "Minutes"),
69+
hours: .init(short: "h", long: "Hours"),
70+
days: .init(short: "d", long: "Days")
71+
),
72+
73+
// Spanish (es)
74+
Locale(identifier: "es"): UnitsLocalization(
75+
seconds: .init(short: "s", long: "Segundos"),
76+
minutes: .init(short: "m", long: "Minutos"),
77+
hours: .init(short: "h", long: "Horas"),
78+
days: .init(short: "d", long: "Días")
79+
),
80+
81+
// French (fr)
82+
Locale(identifier: "fr"): UnitsLocalization(
83+
seconds: .init(short: "s", long: "Secondes"),
84+
minutes: .init(short: "m", long: "Minutes"),
85+
hours: .init(short: "h", long: "Heures"),
86+
days: .init(short: "j", long: "Jours")
87+
),
88+
89+
// German (de)
90+
Locale(identifier: "de"): UnitsLocalization(
91+
seconds: .init(short: "s", long: "Sekunden"),
92+
minutes: .init(short: "m", long: "Minuten"),
93+
hours: .init(short: "h", long: "Stunden"),
94+
days: .init(short: "t", long: "Tage")
95+
),
96+
97+
// Chinese (zh)
98+
Locale(identifier: "zh"): UnitsLocalization(
99+
seconds: .init(short: "", long: ""),
100+
minutes: .init(short: "", long: "分钟"),
101+
hours: .init(short: "", long: "小时"),
102+
days: .init(short: "", long: "")
103+
),
104+
105+
// Japanese (ja)
106+
Locale(identifier: "ja"): UnitsLocalization(
107+
seconds: .init(short: "", long: ""),
108+
minutes: .init(short: "", long: ""),
109+
hours: .init(short: "", long: "時間"),
110+
days: .init(short: "", long: "")
111+
),
112+
113+
// Russian (ru)
114+
Locale(identifier: "ru"): UnitsLocalization(
115+
seconds: .init(short: "с", long: "Секунд"),
116+
minutes: .init(short: "м", long: "Минут"),
117+
hours: .init(short: "ч", long: "Часов"),
118+
days: .init(short: "д", long: "Дней")
119+
),
120+
121+
// Arabic (ar)
122+
Locale(identifier: "ar"): UnitsLocalization(
123+
seconds: .init(short: "ث", long: "ثوانٍ"),
124+
minutes: .init(short: "د", long: "دقائق"),
125+
hours: .init(short: "س", long: "ساعات"),
126+
days: .init(short: "ي", long: "أيام")
127+
),
128+
129+
// Hindi (hi)
130+
Locale(identifier: "hi"): UnitsLocalization(
131+
seconds: .init(short: "से", long: "सेकंड"),
132+
minutes: .init(short: "मि", long: "मिनट"),
133+
hours: .init(short: "घं", long: "घंटे"),
134+
days: .init(short: "दि", long: "दिन")
135+
),
136+
137+
// Portuguese (pt)
138+
Locale(identifier: "pt"): UnitsLocalization(
139+
seconds: .init(short: "s", long: "Segundos"),
140+
minutes: .init(short: "m", long: "Minutos"),
141+
hours: .init(short: "h", long: "Horas"),
142+
days: .init(short: "d", long: "Dias")
143+
)
144+
]
145+
146+
static var localizationFallback: UnitsLocalization {
147+
return UnitsLocalization(
148+
seconds: .init(short: "s", long: "Seconds"),
149+
minutes: .init(short: "m", long: "Minutes"),
150+
hours: .init(short: "h", long: "Hours"),
151+
days: .init(short: "d", long: "Days")
152+
)
153+
}
154+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import SwiftUI
2+
3+
class CountdownManager: ObservableObject {
4+
// MARK: - Published Properties
5+
6+
@Published var days: Int = 0
7+
@Published var hours: Int = 0
8+
@Published var minutes: Int = 0
9+
@Published var seconds: Int = 0
10+
11+
// MARK: - Properties
12+
13+
private var timer: Timer?
14+
private var until: Date?
15+
16+
// MARK: - Methods
17+
18+
func start(until: Date) {
19+
self.until = until
20+
self.updateUnitValues()
21+
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
22+
self?.updateUnitValues()
23+
}
24+
}
25+
26+
func stop() {
27+
self.timer?.invalidate()
28+
self.timer = nil
29+
}
30+
31+
private func updateUnitValues() {
32+
guard let until = self.until else { return }
33+
34+
let now = Date()
35+
let calendar = Calendar.current
36+
let components = calendar.dateComponents(
37+
[.day, .hour, .minute, .second],
38+
from: now,
39+
to: until
40+
)
41+
self.days = max(0, components.day ?? 0)
42+
self.hours = max(0, components.hour ?? 0)
43+
self.minutes = max(0, components.minute ?? 0)
44+
self.seconds = max(0, components.second ?? 0)
45+
46+
if now >= until {
47+
self.stop()
48+
}
49+
}
50+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Foundation
2+
3+
extension CountdownVM {
4+
/// Defines the visual styles for the countdown component.
5+
public enum Style: Equatable {
6+
case plain
7+
case light
8+
}
9+
10+
/// Defines the units style for the countdown component.
11+
public enum UnitsStyle: Equatable {
12+
case hidden
13+
case bottom
14+
case trailing
15+
}
16+
}

0 commit comments

Comments
 (0)