Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4a181e0
SUCountdown
VislovIvan Dec 8, 2024
77a2135
Localization
VislovIvan Dec 9, 2024
4af06c6
Some fix
VislovIvan Dec 9, 2024
cc435ef
Docs and Localization fix
VislovIvan Dec 9, 2024
4f18c59
pr fix
VislovIvan Dec 10, 2024
01592e5
Fix localization
VislovIvan Dec 11, 2024
1e3b5a2
refactor SUCountdown
VislovIvan Dec 11, 2024
8009cae
removed associated values
VislovIvan Dec 12, 2024
99038f8
localization fix
VislovIvan Dec 12, 2024
70caad0
removed unused code
VislovIvan Dec 12, 2024
532ed5f
minor fix and docs
VislovIvan Dec 12, 2024
be87586
CountdownHelpers
VislovIvan Dec 12, 2024
52f547b
layout refactor
VislovIvan Dec 12, 2024
29eecdf
calculateMaxWidth
VislovIvan Dec 15, 2024
df990f6
calculateWidth
VislovIvan Dec 16, 2024
a8b6a93
bug fix
VislovIvan Dec 16, 2024
a05fe2c
fix countdown foreground color and width + refactor
mikhailChelbaev Dec 17, 2024
7fb95c7
refactor: move a method for calculating time width to the model
mikhailChelbaev Dec 17, 2024
ece0bc5
Merge branch 'dev' into SUCountdown
mikhailChelbaev Dec 17, 2024
04c51b6
add spacing param to the model and rename `unitsPosition` to `unitsSt…
mikhailChelbaev Dec 17, 2024
399ad68
UKCountdown
VislovIvan Dec 19, 2024
c6fa757
swiftlint fix
VislovIvan Dec 19, 2024
5796de3
improve implementation of UKCountdown
mikhailChelbaev Dec 19, 2024
623615e
merge with origin UKCountdown
mikhailChelbaev Dec 19, 2024
440c477
pin stack view to superview edges
mikhailChelbaev Dec 19, 2024
885febe
docs and func shouldUpdateHeight
VislovIvan Dec 20, 2024
2690486
add docs for `UnitsLocalization`
mikhailChelbaev Dec 20, 2024
78baf60
update constraints in `UKCountdown`
mikhailChelbaev Dec 20, 2024
26b1ec1
Merge pull request #35 from componentskit/UKCountdown
mikhailChelbaev Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import ComponentsKit
import SwiftUI
import UIKit

struct CountdownPreview: View {
@State private var model = CountdownVM(
until: Date().addingTimeInterval(3600),
localization: defaultLocalizations
)
@State private var tempUntil = Date()
@State private var selectedLocale = Locale(identifier: "en")

var body: some View {
VStack {
PreviewWrapper(title: "SwiftUI") {
SUCountdown(model: self.model)
}
Form {
ComponentOptionalColorPicker(selection: self.$model.color)
FontPicker(selection: self.$model.font)
SizePicker(selection: self.$model.size)
Picker("Units Position", selection: $model.unitsPosition) {
Text("None").tag(UnitsPosition.none)
Text("Bottom").tag(UnitsPosition.bottom)
Text("Trailing").tag(UnitsPosition.trailing)
}
.onChange(of: model.unitsPosition) { newValue in
if newValue != .bottom {
model.style = .plain
}
}
Picker("Style", selection: $model.style) {
Text("Plain").tag(CountdownStyle.plain)
Text("Light").tag(CountdownStyle.light)
}
.onChange(of: model.style) { newValue in
if newValue == .light && model.unitsPosition != .bottom {
model.unitsPosition = .bottom
}
}
Picker("Localization", selection: $selectedLocale) {
ForEach(defaultLocalizations.keys.sorted(by: { $0.identifier < $1.identifier }), id: \.self) { locale in
Text(locale.localizedString(forIdentifier: locale.identifier) ?? locale.identifier)
.tag(locale)
}
}
.onChange(of: selectedLocale) { newLocale in
model.locale = newLocale
}
DatePicker("Select Date and Time", selection: $tempUntil, in: Date()..., displayedComponents: [.date, .hourAndMinute])
.datePickerStyle(.compact)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if tempUntil > Date() {
Button("Update Timer") {
model.until = tempUntil
}
}
}
}
}
}

#Preview {
CountdownPreview()
}
3 changes: 3 additions & 0 deletions Examples/DemosApp/DemosApp/Core/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ struct App: View {
NavigationLinkWithTitle("Checkbox") {
CheckboxPreview()
}
NavigationLinkWithTitle("Countdown") {
CountdownPreview()
}
NavigationLinkWithTitle("Divider") {
DividerPreview()
}
Expand Down
113 changes: 113 additions & 0 deletions Sources/ComponentsKit/Countdown/Localization/UnitsLocalization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import Foundation

// MARK: - UnitsLocalization

public struct UnitsLocalization: Equatable {
public struct UnitItemLocalization: Equatable {
public let short: String
public let long: String

public init(short: String, long: String) {
self.short = short
self.long = long
}
}

// MARK: - Properties

public let seconds: UnitItemLocalization
public let minutes: UnitItemLocalization
public let hours: UnitItemLocalization
public let days: UnitItemLocalization

// MARK: - Initializer

public init(seconds: UnitItemLocalization, minutes: UnitItemLocalization, hours: UnitItemLocalization, days: UnitItemLocalization) {
self.seconds = seconds
self.minutes = minutes
self.hours = hours
self.days = days
}
}

// MARK: - Localizations

public let defaultLocalizations: [Locale: UnitsLocalization] = [
// Spanish (es)
Locale(identifier: "en"): UnitsLocalization(
seconds: .init(short: "s", long: "Seconds"),
minutes: .init(short: "m", long: "Minutes"),
hours: .init(short: "h", long: "Hours"),
days: .init(short: "d", long: "Days")
),

// French (fr)
Locale(identifier: "es"): UnitsLocalization(
seconds: .init(short: "s", long: "Segundos"),
minutes: .init(short: "m", long: "Minutos"),
hours: .init(short: "h", long: "Horas"),
days: .init(short: "d", long: "Días")
),

// German (de)
Locale(identifier: "fr"): UnitsLocalization(
seconds: .init(short: "s", long: "Secondes"),
minutes: .init(short: "m", long: "Minutes"),
hours: .init(short: "h", long: "Heures"),
days: .init(short: "j", long: "Jours")
),

// Chinese (zh)
Locale(identifier: "de"): UnitsLocalization(
seconds: .init(short: "s", long: "Sekunden"),
minutes: .init(short: "m", long: "Minuten"),
hours: .init(short: "h", long: "Stunden"),
days: .init(short: "t", long: "Tage")
),

// Japanese (ja)
Locale(identifier: "zh"): UnitsLocalization(
seconds: .init(short: "秒", long: "秒"),
minutes: .init(short: "分", long: "分钟"),
hours: .init(short: "时", long: "小时"),
days: .init(short: "天", long: "天")
),

// Russian (ru)
Locale(identifier: "ja"): UnitsLocalization(
seconds: .init(short: "秒", long: "秒"),
minutes: .init(short: "分", long: "分"),
hours: .init(short: "時", long: "時間"),
days: .init(short: "日", long: "日")
),

// Arabic (ar)
Locale(identifier: "ru"): UnitsLocalization(
seconds: .init(short: "с", long: "Секунд"),
minutes: .init(short: "м", long: "Минут"),
hours: .init(short: "ч", long: "Часов"),
days: .init(short: "д", long: "Дней")
),

// Hindi (hi)
Locale(identifier: "ar"): UnitsLocalization(
seconds: .init(short: "ث", long: "ثوانٍ"),
minutes: .init(short: "د", long: "دقائق"),
hours: .init(short: "س", long: "ساعات"),
days: .init(short: "ي", long: "أيام")
),
Locale(identifier: "hi"): UnitsLocalization(
seconds: .init(short: "से", long: "सेकंड"),
minutes: .init(short: "मि", long: "मिनट"),
hours: .init(short: "घं", long: "घंटे"),
days: .init(short: "दि", long: "दिन")
),

// Portuguese (pt)
Locale(identifier: "pt"): UnitsLocalization(
seconds: .init(short: "s", long: "Segundos"),
minutes: .init(short: "m", long: "Minutos"),
hours: .init(short: "h", long: "Horas"),
days: .init(short: "d", long: "Dias")
)
]
50 changes: 50 additions & 0 deletions Sources/ComponentsKit/Countdown/Manager/CountdownManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import SwiftUI

class CountdownManager: ObservableObject {
// MARK: - Published Properties

@Published var days: Int = 0
@Published var hours: Int = 0
@Published var minutes: Int = 0
@Published var seconds: Int = 0

// MARK: - Properties

private var timer: Timer?
private var until: Date?

// MARK: - Methods

func start(until: Date) {
self.until = until
updateTime()
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.updateTime()
}
}

func stop() {
timer?.invalidate()
timer = nil
}

private func updateTime() {
guard let until = until else { return }

let now = Date()
let calendar = Calendar.current
let components = calendar.dateComponents(
[.day, .hour, .minute, .second],
from: now,
to: until
)
days = max(0, components.day ?? 0)
hours = max(0, components.hour ?? 0)
minutes = max(0, components.minute ?? 0)
seconds = max(0, components.second ?? 0)

if now >= until {
stop()
}
}
}
7 changes: 7 additions & 0 deletions Sources/ComponentsKit/Countdown/Models/CountdownStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

/// Defines the visual styles for the countdown component.
public enum CountdownStyle {
case plain
case light
}
117 changes: 117 additions & 0 deletions Sources/ComponentsKit/Countdown/Models/CountdownVM.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import SwiftUI

/// Defines the possible positions for units in the countdown.
public enum UnitsPosition {
case none
case bottom
case trailing
}

/// A model that defines the appearance properties for a countdown component.
public struct CountdownVM: ComponentVM {
/// The font used for displaying the countdown numbers and units.
public var font: UniversalFont?

/// The color of the countdown.
public var color: ComponentColor?

/// The predefined size of the countdown.
///
/// Defaults to `.medium`.
public var size: ComponentSize = .medium

/// The position of the units relative to the countdown numbers.
///
/// - Default value: `.bottom`
public var unitsPosition: UnitsPosition = .bottom

/// The visual style of the countdown component.
///
/// - Default value: `.plain`
public var style: CountdownStyle = .light

/// The target date until which the countdown runs.
///
public var until: Date = Date().addingTimeInterval(3600)

/// The locale used for formatting the countdown.
public var locale: Locale = .current

/// Localization.
public var localization: [Locale: UnitsLocalization] = [:]

/// Initializes a new instance of `CountdownVM` with default values.
public init() {}

/// Initializes a new instance of `CountdownVM` with specified parameters.
///
/// - Parameters:
/// - until: The target date until which the countdown runs.
/// - locale: The locale used for formatting the countdown. Defaults to `.current`.
/// - localization: Localization.
public init(until: Date, locale: Locale = .current, localization: [Locale: UnitsLocalization] = [:]) {
self.until = until
self.locale = locale
self.localization = localization
}
}

// MARK: - Shared Helpers

extension CountdownVM {
var preferredFont: UniversalFont {
if let font = self.font {
return font
}

switch self.size {
case .small:
return UniversalFont.Component.small
case .medium:
return UniversalFont.Component.medium
case .large:
return UniversalFont.Component.large
}
}
var unitFontSize: CGFloat {
switch self.size {
case .small:
return 6
case .medium:
return 8
case .large:
return 10
}
}
var backgroundColor: UniversalColor {
if let color {
return color.main.withOpacity(0.15)
} else {
return .init(
light: .rgba(r: 244, g: 244, b: 245, a: 1.0),
dark: .rgba(r: 39, g: 39, b: 42, a: 1.0)
)
}
}
var foregroundColor: UniversalColor {
let foregroundColor = self.color?.main ?? .init(
light: .rgba(r: 0, g: 0, b: 0, a: 1.0),
dark: .rgba(r: 255, g: 255, b: 255, a: 1.0)
)
return foregroundColor
}
var height: CGFloat {
return switch self.size {
case .small: 45
case .medium: 55
case .large: 60
}
}
var horizontalPadding: CGFloat {
return switch self.size {
case .small: 8
case .medium: 12
case .large: 16
}
}
}
Loading