From 509ec8a766aea9ade106203ff492622b0d2d7386 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 11 Feb 2025 17:15:11 +0100 Subject: [PATCH 01/48] fix: use superview's width in fullwidth components only when it's greater than 0 --- .../Components/ProgressBar/UKProgressBar.swift | 8 +++++++- Sources/ComponentsKit/Components/Slider/UKSlider.swift | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift index 3a14cc31..7fcbf095 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift @@ -182,7 +182,13 @@ open class UKProgressBar: UIView, UKComponent { // MARK: - UIView methods open override func sizeThatFits(_ size: CGSize) -> CGSize { - let width = self.superview?.bounds.width ?? size.width + let width: CGFloat + if let parentWidth = self.superview?.bounds.width, + parentWidth > 0 { + width = parentWidth + } else { + width = 10_000 + } return CGSize( width: min(size.width, width), height: min(size.height, self.model.backgroundHeight) diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift index 07ce9b96..81980197 100644 --- a/Sources/ComponentsKit/Components/Slider/UKSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -193,7 +193,13 @@ open class UKSlider: UIView, UKComponent { // MARK: - UIView Methods open override func sizeThatFits(_ size: CGSize) -> CGSize { - let width = self.superview?.bounds.width ?? size.width + let width: CGFloat + if let parentWidth = self.superview?.bounds.width, + parentWidth > 0 { + width = parentWidth + } else { + width = 10_000 + } return CGSize( width: min(size.width, width), height: min(size.height, self.model.handleSize.height) From 57ddce5afe728e6c0bb704920ef9bcf3ad140713 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 17 Feb 2025 11:24:09 +0100 Subject: [PATCH 02/48] change default backaground color of modals to `secondaryBackground` --- Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift b/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift index 55af7ea3..42797599 100644 --- a/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift +++ b/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift @@ -37,9 +37,6 @@ public protocol ModalVM: ComponentVM { extension ModalVM { var preferredBackgroundColor: UniversalColor { - return self.backgroundColor ?? .themed( - light: UniversalColor.background.light, - dark: UniversalColor.secondaryBackground.dark - ) + return self.backgroundColor ?? .secondaryBackground } } From 20791260b85390a582ed77ac44cee919ca623528 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 17 Feb 2025 11:24:36 +0100 Subject: [PATCH 03/48] change default color of the selected segment to `background` --- .../SegmentedControl/Models/SegmentedControlVM.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift index e95308a7..1e4c4c6c 100644 --- a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift @@ -55,10 +55,7 @@ extension SegmentedControlVM { return .content1 } var selectedSegmentColor: UniversalColor { - let color = self.color?.main ?? .themed( - light: UniversalColor.white.light, - dark: UniversalColor.content2.dark - ) + let color = self.color?.main ?? .background return color.enabled(self.isEnabled) } func item(for id: ID) -> SegmentedControlItemVM? { From 2bddd9d083d17bb738f2585385f1793f0261675a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 17 Feb 2025 11:38:26 +0100 Subject: [PATCH 04/48] add `accentBackground`, `successBackground`, `warningBackground` and `dangerBackground` computed properties --- .../Helpers/ModalPreview+Helpers.swift | 8 ++++---- .../PreviewPages/AlertPreview.swift | 10 +++++----- .../PreviewPages/CardPreview.swift | 10 +++++----- .../ComponentsKit/Configuration/Palette.swift | 16 ++++++++++++++++ 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift index bc195431..2b27dd5c 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift @@ -60,10 +60,10 @@ struct ModalPreviewHelpers { Section("Properties") { Picker("Background Color", selection: self.$model.backgroundColor) { Text("Default").tag(Optional.none) - Text("Accent Background").tag(ComponentColor.accent.background) - Text("Success Background").tag(ComponentColor.success.background) - Text("Warning Background").tag(ComponentColor.warning.background) - Text("Danger Background").tag(ComponentColor.danger.background) + Text("Accent Background").tag(UniversalColor.accentBackground) + Text("Success Background").tag(UniversalColor.successBackground) + Text("Warning Background").tag(UniversalColor.warningBackground) + Text("Danger Background").tag(UniversalColor.dangerBackground) } BorderWidthPicker(selection: self.$model.borderWidth) Toggle("Closes On Overlay Tap", isOn: self.$model.closesOnOverlayTap) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift index e791b250..93e3b098 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift @@ -84,10 +84,10 @@ struct AlertPreview: View { Section("Main Properties") { Picker("Background Color", selection: self.$model.backgroundColor) { Text("Default").tag(Optional.none) - Text("Accent Background").tag(ComponentColor.accent.background) - Text("Success Background").tag(ComponentColor.success.background) - Text("Warning Background").tag(ComponentColor.warning.background) - Text("Danger Background").tag(ComponentColor.danger.background) + Text("Accent Background").tag(UniversalColor.accentBackground) + Text("Success Background").tag(UniversalColor.successBackground) + Text("Warning Background").tag(UniversalColor.warningBackground) + Text("Danger Background").tag(UniversalColor.dangerBackground) } BorderWidthPicker(selection: self.$model.borderWidth) Toggle("Closes On Overlay Tap", isOn: self.$model.closesOnOverlayTap) @@ -154,7 +154,7 @@ Enim habitant laoreet inceptos scelerisque senectus, tellus molestie ut. Eros ri } static let initialSecondaryButton = AlertButtonVM { $0.title = SecondaryButtonText.short.rawValue - $0.style = .light + $0.style = .plain } var primaryButtonVMOrDefault: Binding { diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift index c64ae340..1543e844 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift @@ -18,10 +18,10 @@ struct CardPreview: View { Picker("Background Color", selection: self.$model.backgroundColor) { Text("Default").tag(Optional.none) Text("Secondary Background").tag(UniversalColor.secondaryBackground) - Text("Accent Background").tag(ComponentColor.accent.background) - Text("Success Background").tag(ComponentColor.success.background) - Text("Warning Background").tag(ComponentColor.warning.background) - Text("Danger Background").tag(ComponentColor.danger.background) + Text("Accent Background").tag(UniversalColor.accentBackground) + Text("Success Background").tag(UniversalColor.successBackground) + Text("Warning Background").tag(UniversalColor.warningBackground) + Text("Danger Background").tag(UniversalColor.dangerBackground) } BorderWidthPicker(selection: self.$model.borderWidth) Picker("Content Paddings", selection: self.$model.contentPaddings) { @@ -37,7 +37,7 @@ struct CardPreview: View { Text("Small").tag(Shadow.small) Text("Medium").tag(Shadow.medium) Text("Large").tag(Shadow.large) - Text("Custom").tag(Shadow.custom(20.0, .zero, ComponentColor.accent.background)) + Text("Custom").tag(Shadow.custom(20.0, .zero, UniversalColor.accentBackground)) } } } diff --git a/Sources/ComponentsKit/Configuration/Palette.swift b/Sources/ComponentsKit/Configuration/Palette.swift index 0d0bb5e1..ab7f7024 100644 --- a/Sources/ComponentsKit/Configuration/Palette.swift +++ b/Sources/ComponentsKit/Configuration/Palette.swift @@ -203,16 +203,32 @@ extension UniversalColor { public static var accent: Self { return ComponentsKitConfig.shared.colors.accent.main } + /// The accent background color. + public static var accentBackground: Self { + return ComponentsKitConfig.shared.colors.accent.background + } /// The success state color, used for indicating positive actions or statuses. public static var success: Self { return ComponentsKitConfig.shared.colors.success.main } + /// The success background color. + public static var successBackground: Self { + return ComponentsKitConfig.shared.colors.success.background + } /// The warning state color, used for indicating caution or non-critical alerts. public static var warning: Self { return ComponentsKitConfig.shared.colors.warning.main } + /// The warning background color. + public static var warningBackground: Self { + return ComponentsKitConfig.shared.colors.warning.background + } /// The danger state color, used for indicating errors, destructive actions, or critical alerts. public static var danger: Self { return ComponentsKitConfig.shared.colors.danger.main } + /// The danger background color. + public static var dangerBackground: Self { + return ComponentsKitConfig.shared.colors.danger.background + } } From 969d6388c072c772f5a67f5bbdfc877e096e00ac Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 17 Feb 2025 11:47:50 +0100 Subject: [PATCH 05/48] add `isEnabled` param to `Badge` --- .../PreviewPages/BadgePreview.swift | 18 +++++------ .../Components/Badge/Models/BadgeVM.swift | 31 ++++++++++++------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/BadgePreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/BadgePreview.swift index be023fbc..55b06b51 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/BadgePreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/BadgePreview.swift @@ -17,21 +17,17 @@ struct BadgePreview: View { SUBadge(model: self.model) } Form { + ComponentOptionalColorPicker(selection: self.$model.color) + ComponentRadiusPicker(selection: self.$model.cornerRadius) { + Text("Custom: 4px").tag(ComponentRadius.custom(4)) + } + Toggle("Enabled", isOn: self.$model.isEnabled) Picker("Font", selection: self.$model.font) { - Text("Default").tag(Optional.none) Text("Small").tag(UniversalFont.smButton) Text("Medium").tag(UniversalFont.mdButton) Text("Large").tag(UniversalFont.lgButton) Text("Custom: system bold of size 16").tag(UniversalFont.system(size: 16, weight: .bold)) } - ComponentOptionalColorPicker(selection: self.$model.color) - ComponentRadiusPicker(selection: self.$model.cornerRadius) { - Text("Custom: 4px").tag(ComponentRadius.custom(4)) - } - Picker("Style", selection: self.$model.style) { - Text("Filled").tag(BadgeVM.Style.filled) - Text("Light").tag(BadgeVM.Style.light) - } Picker("Paddings", selection: self.$model.paddings) { Text("8px; 6px") .tag(Paddings(top: 6, leading: 8, bottom: 6, trailing: 8)) @@ -40,6 +36,10 @@ struct BadgePreview: View { Text("12px; 10px") .tag(Paddings(top: 10, leading: 12, bottom: 10, trailing: 12)) } + Picker("Style", selection: self.$model.style) { + Text("Filled").tag(BadgeVM.Style.filled) + Text("Light").tag(BadgeVM.Style.light) + } } } } diff --git a/Sources/ComponentsKit/Components/Badge/Models/BadgeVM.swift b/Sources/ComponentsKit/Components/Badge/Models/BadgeVM.swift index 4232dff6..8e61d7d9 100644 --- a/Sources/ComponentsKit/Components/Badge/Models/BadgeVM.swift +++ b/Sources/ComponentsKit/Components/Badge/Models/BadgeVM.swift @@ -8,24 +8,29 @@ public struct BadgeVM: ComponentVM { /// The color of the badge. public var color: ComponentColor? - /// The visual style of the badge. + /// The corner radius of the badge. /// - /// Defaults to `.filled`. - public var style: Style = .filled + /// Defaults to `.medium`. + public var cornerRadius: ComponentRadius = .medium /// The font used for the badge's text. /// /// Defaults to `.smButton`. public var font: UniversalFont = .smButton - /// The corner radius of the badge. + /// A Boolean value indicating whether the button is enabled or disabled. /// - /// Defaults to `.medium`. - public var cornerRadius: ComponentRadius = .medium + /// Defaults to `true`. + public var isEnabled: Bool = true /// Paddings for the badge. public var paddings: Paddings = .init(horizontal: 10, vertical: 8) + /// The visual style of the badge. + /// + /// Defaults to `.filled`. + public var style: Style = .filled + /// Initializes a new instance of `BadgeVM` with default values. public init() {} } @@ -35,22 +40,24 @@ public struct BadgeVM: ComponentVM { extension BadgeVM { /// Returns the background color of the badge based on its style. var backgroundColor: UniversalColor { - switch self.style { + let color = switch self.style { case .filled: - return self.color?.main ?? .content2 + self.color?.main ?? .content2 case .light: - return self.color?.background ?? .content1 + self.color?.background ?? .content1 } + return color.enabled(self.isEnabled) } /// Returns the foreground color of the badge based on its style. var foregroundColor: UniversalColor { - switch self.style { + let color = switch self.style { case .filled: - return self.color?.contrast ?? .foreground + self.color?.contrast ?? .foreground case .light: - return self.color?.main ?? .foreground + self.color?.main ?? .foreground } + return color.enabled(self.isEnabled) } } From 3ca377515222e26581a81e40af42586a04debb4b Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Feb 2025 16:01:59 +0100 Subject: [PATCH 06/48] improve code for `SUButton` and `UKButton` --- Sources/ComponentsKit/Components/Button/SUButton.swift | 3 +-- Sources/ComponentsKit/Components/Button/UKButton.swift | 9 --------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index f9e4ed73..d415a8ae 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -56,8 +56,7 @@ private struct CustomButtonStyle: SwiftUI.ButtonStyle { configuration.label .font(self.model.preferredFont.font) .lineLimit(1) - .padding(.leading, self.model.horizontalPadding) - .padding(.trailing, self.model.horizontalPadding) + .padding(.horizontal, self.model.horizontalPadding) .frame(maxWidth: self.model.width) .frame(height: self.model.height) .foregroundStyle(self.model.foregroundColor.color) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index c7f573ce..058c1609 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -27,8 +27,6 @@ open class UKButton: UIView, UKComponent { } } - private var titleLabelConstraints: LayoutConstraints = .init() - // MARK: Subviews /// A label that displays the title from the model. @@ -85,11 +83,7 @@ open class UKButton: UIView, UKComponent { // MARK: Layout private func layout() { - self.titleLabelConstraints = self.titleLabel.horizontally(self.model.horizontalPadding) self.titleLabel.center() - - self.titleLabelConstraints.leading?.priority = .defaultHigh - self.titleLabelConstraints.trailing?.priority = .defaultHigh } open override func layoutSubviews() { @@ -106,10 +100,7 @@ open class UKButton: UIView, UKComponent { self.style() if self.model.shouldUpdateSize(oldModel) { - self.titleLabelConstraints.leading?.constant = self.model.horizontalPadding - self.titleLabelConstraints.trailing?.constant = -self.model.horizontalPadding self.invalidateIntrinsicContentSize() - self.setNeedsLayout() } } From 38f62e26f5fc0033b1d734b6caf01884a99dede4 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Feb 2025 16:19:36 +0100 Subject: [PATCH 07/48] change large headline font size value from 28 to 24 --- Sources/ComponentsKit/Configuration/Layout.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Configuration/Layout.swift b/Sources/ComponentsKit/Configuration/Layout.swift index 24c83a28..8163e6b7 100644 --- a/Sources/ComponentsKit/Configuration/Layout.swift +++ b/Sources/ComponentsKit/Configuration/Layout.swift @@ -260,7 +260,7 @@ extension ComponentsKitConfig { headline: .init( small: .system(size: 14, weight: .semibold), medium: .system(size: 20, weight: .semibold), - large: .system(size: 28, weight: .semibold) + large: .system(size: 24, weight: .semibold) ), body: .init( small: .system(size: 14, weight: .regular), From dc78c94a52ea5e4ae4b433b8e4dbca66cc519288 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Feb 2025 16:23:37 +0100 Subject: [PATCH 08/48] conform `ComponentsKitConfig` to `Equatable` protocol --- Sources/ComponentsKit/Configuration/Config.swift | 2 +- Sources/ComponentsKit/Configuration/Layout.swift | 16 ++++++++-------- .../ComponentsKit/Configuration/Palette.swift | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ComponentsKit/Configuration/Config.swift b/Sources/ComponentsKit/Configuration/Config.swift index c31c807a..82324ab9 100644 --- a/Sources/ComponentsKit/Configuration/Config.swift +++ b/Sources/ComponentsKit/Configuration/Config.swift @@ -1,7 +1,7 @@ import Foundation /// A configuration structure for customizing colors and layout attributes of the components. -public struct ComponentsKitConfig: Initializable, Updatable { +public struct ComponentsKitConfig: Initializable, Updatable, Equatable { // MARK: - Properties /// The palette of colors. diff --git a/Sources/ComponentsKit/Configuration/Layout.swift b/Sources/ComponentsKit/Configuration/Layout.swift index 8163e6b7..84013c48 100644 --- a/Sources/ComponentsKit/Configuration/Layout.swift +++ b/Sources/ComponentsKit/Configuration/Layout.swift @@ -2,11 +2,11 @@ import Foundation extension ComponentsKitConfig { /// A structure that defines the layout-related configurations for components in the framework. - public struct Layout: Initializable, Updatable { + public struct Layout: Initializable, Updatable, Equatable { // MARK: - Radius /// A structure representing radius values for components. - public struct Radius { + public struct Radius: Equatable { /// The small radius size. public var small: CGFloat /// The medium radius size. @@ -30,7 +30,7 @@ extension ComponentsKitConfig { // MARK: - BorderWidth /// A structure representing border width values for components. - public struct BorderWidth { + public struct BorderWidth: Equatable { /// The small border width. public var small: CGFloat /// The medium border width. @@ -56,7 +56,7 @@ extension ComponentsKitConfig { /// A structure representing animation scale values for components. /// /// The values must be between `0.0` and `1.0`. - public struct AnimationScale { + public struct AnimationScale: Equatable { /// The small animation scale. public var small: CGFloat /// The medium animation scale. @@ -88,7 +88,7 @@ extension ComponentsKitConfig { // MARK: - Shadow /// A structure that defines the parameters for a shadow effect. - public struct ShadowParams { + public struct ShadowParams: Equatable { /// The blur radius of the shadow. /// /// A larger radius results in a more diffuse shadow. @@ -119,7 +119,7 @@ extension ComponentsKitConfig { } /// A structure that defines shadow presets for small, medium, and large shadows. - public struct Shadow { + public struct Shadow: Equatable { /// The shadow parameters for a small shadow. public var small: ShadowParams @@ -147,7 +147,7 @@ extension ComponentsKitConfig { // MARK: - Typography /// A structure representing a set of fonts for different component sizes. - public struct FontSet { + public struct FontSet: Equatable { /// The small font. public var small: UniversalFont /// The medium font. @@ -169,7 +169,7 @@ extension ComponentsKitConfig { } /// A structure representing typography settings for various components. - public struct Typography { + public struct Typography: Equatable { /// The font set for headlines. public var headline: FontSet /// The font set for body text. diff --git a/Sources/ComponentsKit/Configuration/Palette.swift b/Sources/ComponentsKit/Configuration/Palette.swift index ab7f7024..ff177f51 100644 --- a/Sources/ComponentsKit/Configuration/Palette.swift +++ b/Sources/ComponentsKit/Configuration/Palette.swift @@ -2,7 +2,7 @@ import Foundation extension ComponentsKitConfig { /// Defines a set of colors that are used for styling components and interfaces. - public struct Palette: Initializable, Updatable { + public struct Palette: Initializable, Updatable, Equatable { /// The color for the main background of the interface. public var background: UniversalColor = .themed( light: .hex("#FFFFFF"), From ccf8296027f8082b2e3c487b266c89936c7dbaf2 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 18 Feb 2025 16:45:32 +0100 Subject: [PATCH 09/48] rename `ComponentsKitConfig` to `Theme` --- README.md | 25 +++++----- .../ComponentsKit/Configuration/Config.swift | 24 ---------- .../Shared/Colors/UniversalColor.swift | 2 +- .../Shared/Fonts/UniversalFont.swift | 24 +++++----- .../Shared/Types/AnimationScale.swift | 8 ++-- .../Shared/Types/BorderWidth.swift | 6 +-- .../Shared/Types/ComponentRadius.swift | 6 +-- .../Shared/Types/ContainerRadius.swift | 6 +-- .../ComponentsKit/Shared/Types/Shadow.swift | 18 +++---- .../{Configuration => Theme}/Layout.swift | 2 +- .../{Configuration => Theme}/Palette.swift | 48 +++++++++---------- Sources/ComponentsKit/Theme/Theme.swift | 25 ++++++++++ 12 files changed, 98 insertions(+), 96 deletions(-) delete mode 100644 Sources/ComponentsKit/Configuration/Config.swift rename Sources/ComponentsKit/{Configuration => Theme}/Layout.swift (99%) rename Sources/ComponentsKit/{Configuration => Theme}/Palette.swift (82%) create mode 100644 Sources/ComponentsKit/Theme/Theme.swift diff --git a/README.md b/README.md index 6fa036ec..5dcd4c8d 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,12 @@ inputField.resignFirstResponder() ### Styling -**Config** +**Theme** -The library comes with predefined fonts, sizes and colors, but you can change these values to customize the appearance of your app. To do this, alter the config: +The library comes with predefined fonts, sizes and colors, but you can change these values to customize the appearance of your app. To do this, alter the current theme: ```swift -ComponentsKitConfig.shared.update { +Theme.current.update { // Update colors $0.colors.primary = ... @@ -103,12 +103,12 @@ ComponentsKitConfig.shared.update { ``` > [!Note] -> The best place to set up the initial config is in the `func application(_:, didFinishLaunchingWithOptions:) -> Bool` method in your `AppDelegate` or a similar method in `SceneDelegate`. +> The best place to set up the initial theme is in the `func application(_:, didFinishLaunchingWithOptions:) -> Bool` method in your `AppDelegate` or a similar method in `SceneDelegate`. -By altering the config, you can also create *custom themes* for your app. To do this, first create a new instance of a config: +By altering the theme, you can also create *custom themes* for your app. To do this, first create a new instance of a `Theme`: ```swift -let halloweenTheme = ComponentsKitConfig { +let halloweenTheme = Theme { $0.colors.background = .themed( light: .hex("#e38f36"), dark: .hex("#ba5421") @@ -117,25 +117,26 @@ let halloweenTheme = ComponentsKitConfig { } ``` -When the user switches the theme, apply it by assigning it to the `shared` instance: +When the user switches the theme, apply it by assigning it to the `current` instance: ```swift -ComponentsKitConfig.shared = halloweenTheme +Theme.current = halloweenTheme ``` **Extend Colors** -All colors from the config can be used within the app. For example: +All colors from the theme can be used within the app. For example: ```swift // in UIKit view.backgroundColor = UniversalColor.background.uiColor // in SwiftUI -UniversalColor.background.color +SomeView() + .background(UniversalColor.background.color) ``` -If you want to use additional colors that are not included in the config, you can extend `UniversalColor`: +If you want to use additional colors that are not included in the theme, you can extend `UniversalColor`: ```swift extension UniversalColor { @@ -155,7 +156,7 @@ view.backgroundColor = UniversalColor.special.uiColor **Extend Fonts** -If you want to use additional fonts that are not included in the config, you can extend `UniversalFont`: +If you want to use additional fonts that are not included in the theme, you can extend `UniversalFont`: ```swift extension UniversalFont { diff --git a/Sources/ComponentsKit/Configuration/Config.swift b/Sources/ComponentsKit/Configuration/Config.swift deleted file mode 100644 index 82324ab9..00000000 --- a/Sources/ComponentsKit/Configuration/Config.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -/// A configuration structure for customizing colors and layout attributes of the components. -public struct ComponentsKitConfig: Initializable, Updatable, Equatable { - // MARK: - Properties - - /// The palette of colors. - public var colors: Palette = .init() - - /// The layout configuration. - public var layout: Layout = .init() - - // MARK: - Initialization - - /// Initializes a new `ComponentsKitConfig` instance with default values. - public init() {} -} - -// MARK: - ComponentsKitConfig + Shared - -extension ComponentsKitConfig { - /// A shared instance of `ComponentsKitConfig` for global use. - public static var shared: Self = .init() -} diff --git a/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift b/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift index 24be0319..28c240a5 100644 --- a/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift +++ b/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift @@ -206,7 +206,7 @@ public struct UniversalColor: Hashable { public func enabled(_ isEnabled: Bool) -> Self { return isEnabled ? self - : self.withOpacity(ComponentsKitConfig.shared.layout.disabledOpacity) + : self.withOpacity(Theme.current.layout.disabledOpacity) } /// Returns a new `UniversalColor` by blending the current color with another color. diff --git a/Sources/ComponentsKit/Shared/Fonts/UniversalFont.swift b/Sources/ComponentsKit/Shared/Fonts/UniversalFont.swift index cd1ec1f0..ccc6cd29 100644 --- a/Sources/ComponentsKit/Shared/Fonts/UniversalFont.swift +++ b/Sources/ComponentsKit/Shared/Fonts/UniversalFont.swift @@ -158,53 +158,53 @@ extension UniversalFont.Weight { extension UniversalFont { /// Small headline font. public static var smHeadline: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.headline.small + return Theme.current.layout.typography.headline.small } /// Medium headline font. public static var mdHeadline: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.headline.medium + return Theme.current.layout.typography.headline.medium } /// Large headline font. public static var lgHeadline: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.headline.large + return Theme.current.layout.typography.headline.large } /// Small body font. public static var smBody: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.body.small + return Theme.current.layout.typography.body.small } /// Medium body font. public static var mdBody: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.body.medium + return Theme.current.layout.typography.body.medium } /// Large body font. public static var lgBody: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.body.large + return Theme.current.layout.typography.body.large } /// Small button font. public static var smButton: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.button.small + return Theme.current.layout.typography.button.small } /// Medium button font. public static var mdButton: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.button.medium + return Theme.current.layout.typography.button.medium } /// Large button font. public static var lgButton: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.button.large + return Theme.current.layout.typography.button.large } /// Small caption font. public static var smCaption: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.caption.small + return Theme.current.layout.typography.caption.small } /// Medium caption font. public static var mdCaption: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.caption.medium + return Theme.current.layout.typography.caption.medium } /// Large caption font. public static var lgCaption: UniversalFont { - return ComponentsKitConfig.shared.layout.typography.caption.large + return Theme.current.layout.typography.caption.large } } diff --git a/Sources/ComponentsKit/Shared/Types/AnimationScale.swift b/Sources/ComponentsKit/Shared/Types/AnimationScale.swift index 9a6f0695..7d423ee5 100644 --- a/Sources/ComponentsKit/Shared/Types/AnimationScale.swift +++ b/Sources/ComponentsKit/Shared/Types/AnimationScale.swift @@ -21,7 +21,7 @@ extension AnimationScale { /// /// - Returns: /// - `1.0` for `.none` (no scaling). - /// - Predefined values from `ComponentsKitConfig` for `.small`, `.medium`, and `.large`. + /// - Predefined values from `Theme` for `.small`, `.medium`, and `.large`. /// - The custom value provided for `.custom`, constrained between `0.0` and `1.0`. /// - Note: If the custom value is outside the range `0.0–1.0`, an assertion failure occurs, /// and a default value of `1.0` is returned. @@ -30,11 +30,11 @@ extension AnimationScale { case .none: return 1.0 case .small: - return ComponentsKitConfig.shared.layout.animationScale.small + return Theme.current.layout.animationScale.small case .medium: - return ComponentsKitConfig.shared.layout.animationScale.medium + return Theme.current.layout.animationScale.medium case .large: - return ComponentsKitConfig.shared.layout.animationScale.large + return Theme.current.layout.animationScale.large case .custom(let value): guard value >= 0 && value <= 1.0 else { assertionFailure("Animation scale value should be between 0 and 1") diff --git a/Sources/ComponentsKit/Shared/Types/BorderWidth.swift b/Sources/ComponentsKit/Shared/Types/BorderWidth.swift index 2ebb7082..6110b233 100644 --- a/Sources/ComponentsKit/Shared/Types/BorderWidth.swift +++ b/Sources/ComponentsKit/Shared/Types/BorderWidth.swift @@ -19,11 +19,11 @@ extension BorderWidth { case .none: return 0.0 case .small: - return ComponentsKitConfig.shared.layout.borderWidth.small + return Theme.current.layout.borderWidth.small case .medium: - return ComponentsKitConfig.shared.layout.borderWidth.medium + return Theme.current.layout.borderWidth.medium case .large: - return ComponentsKitConfig.shared.layout.borderWidth.large + return Theme.current.layout.borderWidth.large } } } diff --git a/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift b/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift index f8af07c3..229d8395 100644 --- a/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift +++ b/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift @@ -28,9 +28,9 @@ extension ComponentRadius { let maxValue = height / 2 let value = switch self { case .none: CGFloat(0) - case .small: ComponentsKitConfig.shared.layout.componentRadius.small - case .medium: ComponentsKitConfig.shared.layout.componentRadius.medium - case .large: ComponentsKitConfig.shared.layout.componentRadius.large + case .small: Theme.current.layout.componentRadius.small + case .medium: Theme.current.layout.componentRadius.medium + case .large: Theme.current.layout.componentRadius.large case .full: height / 2 case .custom(let value): value } diff --git a/Sources/ComponentsKit/Shared/Types/ContainerRadius.swift b/Sources/ComponentsKit/Shared/Types/ContainerRadius.swift index 419b988a..b5f7b863 100644 --- a/Sources/ComponentsKit/Shared/Types/ContainerRadius.swift +++ b/Sources/ComponentsKit/Shared/Types/ContainerRadius.swift @@ -20,9 +20,9 @@ extension ContainerRadius { var value: CGFloat { return switch self { case .none: CGFloat(0) - case .small: ComponentsKitConfig.shared.layout.containerRadius.small - case .medium: ComponentsKitConfig.shared.layout.containerRadius.medium - case .large: ComponentsKitConfig.shared.layout.containerRadius.large + case .small: Theme.current.layout.containerRadius.small + case .medium: Theme.current.layout.containerRadius.medium + case .large: Theme.current.layout.containerRadius.large case .custom(let value): value } } diff --git a/Sources/ComponentsKit/Shared/Types/Shadow.swift b/Sources/ComponentsKit/Shared/Types/Shadow.swift index 2be6dd37..ea90e10b 100644 --- a/Sources/ComponentsKit/Shared/Types/Shadow.swift +++ b/Sources/ComponentsKit/Shared/Types/Shadow.swift @@ -24,9 +24,9 @@ extension Shadow { var radius: CGFloat { return switch self { case .none: CGFloat(0) - case .small: ComponentsKitConfig.shared.layout.shadow.small.radius - case .medium: ComponentsKitConfig.shared.layout.shadow.medium.radius - case .large: ComponentsKitConfig.shared.layout.shadow.large.radius + case .small: Theme.current.layout.shadow.small.radius + case .medium: Theme.current.layout.shadow.medium.radius + case .large: Theme.current.layout.shadow.large.radius case .custom(let radius, _, _): radius } } @@ -34,9 +34,9 @@ extension Shadow { var offset: CGSize { return switch self { case .none: .zero - case .small: ComponentsKitConfig.shared.layout.shadow.small.offset - case .medium: ComponentsKitConfig.shared.layout.shadow.medium.offset - case .large: ComponentsKitConfig.shared.layout.shadow.large.offset + case .small: Theme.current.layout.shadow.small.offset + case .medium: Theme.current.layout.shadow.medium.offset + case .large: Theme.current.layout.shadow.large.offset case .custom(_, let offset, _): offset } } @@ -44,9 +44,9 @@ extension Shadow { var color: UniversalColor { return switch self { case .none: .clear - case .small: ComponentsKitConfig.shared.layout.shadow.small.color - case .medium: ComponentsKitConfig.shared.layout.shadow.medium.color - case .large: ComponentsKitConfig.shared.layout.shadow.large.color + case .small: Theme.current.layout.shadow.small.color + case .medium: Theme.current.layout.shadow.medium.color + case .large: Theme.current.layout.shadow.large.color case .custom(_, _, let color): color } } diff --git a/Sources/ComponentsKit/Configuration/Layout.swift b/Sources/ComponentsKit/Theme/Layout.swift similarity index 99% rename from Sources/ComponentsKit/Configuration/Layout.swift rename to Sources/ComponentsKit/Theme/Layout.swift index 84013c48..c79f51a2 100644 --- a/Sources/ComponentsKit/Configuration/Layout.swift +++ b/Sources/ComponentsKit/Theme/Layout.swift @@ -1,6 +1,6 @@ import Foundation -extension ComponentsKitConfig { +extension Theme { /// A structure that defines the layout-related configurations for components in the framework. public struct Layout: Initializable, Updatable, Equatable { // MARK: - Radius diff --git a/Sources/ComponentsKit/Configuration/Palette.swift b/Sources/ComponentsKit/Theme/Palette.swift similarity index 82% rename from Sources/ComponentsKit/Configuration/Palette.swift rename to Sources/ComponentsKit/Theme/Palette.swift index ff177f51..5def595c 100644 --- a/Sources/ComponentsKit/Configuration/Palette.swift +++ b/Sources/ComponentsKit/Theme/Palette.swift @@ -1,6 +1,6 @@ import Foundation -extension ComponentsKitConfig { +extension Theme { /// Defines a set of colors that are used for styling components and interfaces. public struct Palette: Initializable, Updatable, Equatable { /// The color for the main background of the interface. @@ -122,23 +122,23 @@ extension ComponentsKitConfig { extension ComponentColor { /// The primary color. public static var primary: Self { - return ComponentsKitConfig.shared.colors.primary + return Theme.current.colors.primary } /// The accent color. public static var accent: Self { - return ComponentsKitConfig.shared.colors.accent + return Theme.current.colors.accent } /// The success state color, used for indicating positive actions or statuses. public static var success: Self { - return ComponentsKitConfig.shared.colors.success + return Theme.current.colors.success } /// The warning state color, used for indicating caution or non-critical alerts. public static var warning: Self { - return ComponentsKitConfig.shared.colors.warning + return Theme.current.colors.warning } /// The danger state color, used for indicating errors, destructive actions, or critical alerts. public static var danger: Self { - return ComponentsKitConfig.shared.colors.danger + return Theme.current.colors.danger } } @@ -161,74 +161,74 @@ extension UniversalColor { extension UniversalColor { /// The color for the main background of the interface. public static var background: Self { - return ComponentsKitConfig.shared.colors.background + return Theme.current.colors.background } /// The color for the secondary background of the interface. public static var secondaryBackground: Self { - return ComponentsKitConfig.shared.colors.secondaryBackground + return Theme.current.colors.secondaryBackground } /// The color for text labels that contain primary content. public static var foreground: Self { - return ComponentsKitConfig.shared.colors.foreground + return Theme.current.colors.foreground } /// The color for text labels that contain secondary content. public static var secondaryForeground: Self { - return ComponentsKitConfig.shared.colors.secondaryForeground + return Theme.current.colors.secondaryForeground } /// The color for thin borders or divider lines. public static var divider: Self { - return ComponentsKitConfig.shared.colors.divider + return Theme.current.colors.divider } /// The first content color. public static var content1: Self { - return ComponentsKitConfig.shared.colors.content1 + return Theme.current.colors.content1 } /// The second content color. public static var content2: Self { - return ComponentsKitConfig.shared.colors.content2 + return Theme.current.colors.content2 } /// The third content color. public static var content3: Self { - return ComponentsKitConfig.shared.colors.content3 + return Theme.current.colors.content3 } /// The forth content color. public static var content4: Self { - return ComponentsKitConfig.shared.colors.content4 + return Theme.current.colors.content4 } /// The primary color. public static var primary: Self { - return ComponentsKitConfig.shared.colors.primary.main + return Theme.current.colors.primary.main } /// The accent color. public static var accent: Self { - return ComponentsKitConfig.shared.colors.accent.main + return Theme.current.colors.accent.main } /// The accent background color. public static var accentBackground: Self { - return ComponentsKitConfig.shared.colors.accent.background + return Theme.current.colors.accent.background } /// The success state color, used for indicating positive actions or statuses. public static var success: Self { - return ComponentsKitConfig.shared.colors.success.main + return Theme.current.colors.success.main } /// The success background color. public static var successBackground: Self { - return ComponentsKitConfig.shared.colors.success.background + return Theme.current.colors.success.background } /// The warning state color, used for indicating caution or non-critical alerts. public static var warning: Self { - return ComponentsKitConfig.shared.colors.warning.main + return Theme.current.colors.warning.main } /// The warning background color. public static var warningBackground: Self { - return ComponentsKitConfig.shared.colors.warning.background + return Theme.current.colors.warning.background } /// The danger state color, used for indicating errors, destructive actions, or critical alerts. public static var danger: Self { - return ComponentsKitConfig.shared.colors.danger.main + return Theme.current.colors.danger.main } /// The danger background color. public static var dangerBackground: Self { - return ComponentsKitConfig.shared.colors.danger.background + return Theme.current.colors.danger.background } } diff --git a/Sources/ComponentsKit/Theme/Theme.swift b/Sources/ComponentsKit/Theme/Theme.swift new file mode 100644 index 00000000..d65e0665 --- /dev/null +++ b/Sources/ComponentsKit/Theme/Theme.swift @@ -0,0 +1,25 @@ +import Foundation + +/// A predefined set of colors and layout attributes that ensure visual consistency across the +/// application. +public struct Theme: Initializable, Updatable, Equatable { + // MARK: - Properties + + /// The palette of colors. + public var colors: Palette = .init() + + /// The layout configuration. + public var layout: Layout = .init() + + // MARK: - Initialization + + /// Initializes a new `Theme` instance with default values. + public init() {} +} + +// MARK: - Theme + Shared + +extension Theme { + /// A current instance of `Theme` for global use. + public static var current: Self = .init() +} From 1cb7d20a271e5d264227c4dd8ef773b87f72882a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 19 Feb 2025 12:26:34 +0100 Subject: [PATCH 10/48] change access level of type helpers to `public` --- .../Shared/Types/ComponentRadius.swift | 2 +- .../Shared/Types/ContainerRadius.swift | 2 +- .../ComponentsKit/Shared/Types/Paddings.swift | 16 +++++++++++++++- Sources/ComponentsKit/Shared/Types/Shadow.swift | 10 +++++----- .../ComponentsKit/Shared/Types/SubmitType.swift | 4 ++-- .../Shared/Types/TextAutocapitalization.swift | 4 ++-- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift b/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift index 229d8395..f62dad6a 100644 --- a/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift +++ b/Sources/ComponentsKit/Shared/Types/ComponentRadius.swift @@ -24,7 +24,7 @@ extension ComponentRadius { /// /// - Parameter height: The height of the component. Defaults to a large number (10,000) for unrestricted calculations. /// - Returns: The calculated corner radius as a `CGFloat`, capped at half of the height for `full` rounding or custom values. - func value(for height: CGFloat = 10_000) -> CGFloat { + public func value(for height: CGFloat = 10_000) -> CGFloat { let maxValue = height / 2 let value = switch self { case .none: CGFloat(0) diff --git a/Sources/ComponentsKit/Shared/Types/ContainerRadius.swift b/Sources/ComponentsKit/Shared/Types/ContainerRadius.swift index b5f7b863..ca62838b 100644 --- a/Sources/ComponentsKit/Shared/Types/ContainerRadius.swift +++ b/Sources/ComponentsKit/Shared/Types/ContainerRadius.swift @@ -17,7 +17,7 @@ public enum ContainerRadius: Hashable { } extension ContainerRadius { - var value: CGFloat { + public var value: CGFloat { return switch self { case .none: CGFloat(0) case .small: Theme.current.layout.containerRadius.small diff --git a/Sources/ComponentsKit/Shared/Types/Paddings.swift b/Sources/ComponentsKit/Shared/Types/Paddings.swift index f6674901..641fbf3c 100644 --- a/Sources/ComponentsKit/Shared/Types/Paddings.swift +++ b/Sources/ComponentsKit/Shared/Types/Paddings.swift @@ -1,4 +1,5 @@ import SwiftUI +import UIKit /// Defines padding values for each edge. public struct Paddings: Hashable { @@ -54,7 +55,7 @@ public struct Paddings: Hashable { // MARK: - SwiftUI Helpers extension Paddings { - var edgeInsets: EdgeInsets { + public var edgeInsets: EdgeInsets { return EdgeInsets( top: self.top, leading: self.leading, @@ -63,3 +64,16 @@ extension Paddings { ) } } + +// MARK: - UIKit Helpers + +extension Paddings { + public var uiEdgeInsets: UIEdgeInsets { + return UIEdgeInsets( + top: self.top, + left: self.leading, + bottom: self.bottom, + right: self.trailing + ) + } +} diff --git a/Sources/ComponentsKit/Shared/Types/Shadow.swift b/Sources/ComponentsKit/Shared/Types/Shadow.swift index ea90e10b..ed873a18 100644 --- a/Sources/ComponentsKit/Shared/Types/Shadow.swift +++ b/Sources/ComponentsKit/Shared/Types/Shadow.swift @@ -21,7 +21,7 @@ public enum Shadow: Hashable { } extension Shadow { - var radius: CGFloat { + public var radius: CGFloat { return switch self { case .none: CGFloat(0) case .small: Theme.current.layout.shadow.small.radius @@ -31,7 +31,7 @@ extension Shadow { } } - var offset: CGSize { + public var offset: CGSize { return switch self { case .none: .zero case .small: Theme.current.layout.shadow.small.offset @@ -41,7 +41,7 @@ extension Shadow { } } - var color: UniversalColor { + public var color: UniversalColor { return switch self { case .none: .clear case .small: Theme.current.layout.shadow.small.color @@ -55,7 +55,7 @@ extension Shadow { // MARK: - UIKit + Shadow extension UIView { - func shadow(_ shadow: Shadow) { + public func shadow(_ shadow: Shadow) { self.layer.shadowRadius = shadow.radius self.layer.shadowOffset = shadow.offset self.layer.shadowColor = shadow.color.cgColor @@ -66,7 +66,7 @@ extension UIView { // MARK: - SwiftUI + Shadow extension View { - func shadow(_ shadow: Shadow) -> some View { + public func shadow(_ shadow: Shadow) -> some View { self.shadow( color: shadow.color.color, radius: shadow.radius, diff --git a/Sources/ComponentsKit/Shared/Types/SubmitType.swift b/Sources/ComponentsKit/Shared/Types/SubmitType.swift index 5169f4bb..aab22ce7 100644 --- a/Sources/ComponentsKit/Shared/Types/SubmitType.swift +++ b/Sources/ComponentsKit/Shared/Types/SubmitType.swift @@ -26,7 +26,7 @@ public enum SubmitType { // MARK: - UIKit Helpers extension SubmitType { - var returnKeyType: UIReturnKeyType { + public var returnKeyType: UIReturnKeyType { switch self { case .done: return .done @@ -53,7 +53,7 @@ extension SubmitType { // MARK: - SwiftUI Helpers extension SubmitType { - var submitLabel: SubmitLabel { + public var submitLabel: SubmitLabel { switch self { case .done: return .done diff --git a/Sources/ComponentsKit/Shared/Types/TextAutocapitalization.swift b/Sources/ComponentsKit/Shared/Types/TextAutocapitalization.swift index c8159e67..cedc6c77 100644 --- a/Sources/ComponentsKit/Shared/Types/TextAutocapitalization.swift +++ b/Sources/ComponentsKit/Shared/Types/TextAutocapitalization.swift @@ -14,7 +14,7 @@ public enum TextAutocapitalization { } extension TextAutocapitalization { - var textAutocapitalizationType: UITextAutocapitalizationType { + public var textAutocapitalizationType: UITextAutocapitalizationType { switch self { case .never: return .none @@ -29,7 +29,7 @@ extension TextAutocapitalization { } extension TextAutocapitalization { - var textInputAutocapitalization: TextInputAutocapitalization { + public var textInputAutocapitalization: TextInputAutocapitalization { switch self { case .never: return .never From cd9c1e78c83e634847a9a2f675dca6db3929edc3 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 19 Feb 2025 14:41:29 +0100 Subject: [PATCH 11/48] revert change of selected segmented color and modal's background color --- Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift | 5 ++++- .../SegmentedControl/Models/SegmentedControlVM.swift | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift b/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift index 42797599..55af7ea3 100644 --- a/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift +++ b/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift @@ -37,6 +37,9 @@ public protocol ModalVM: ComponentVM { extension ModalVM { var preferredBackgroundColor: UniversalColor { - return self.backgroundColor ?? .secondaryBackground + return self.backgroundColor ?? .themed( + light: UniversalColor.background.light, + dark: UniversalColor.secondaryBackground.dark + ) } } diff --git a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift index 1e4c4c6c..e95308a7 100644 --- a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift @@ -55,7 +55,10 @@ extension SegmentedControlVM { return .content1 } var selectedSegmentColor: UniversalColor { - let color = self.color?.main ?? .background + let color = self.color?.main ?? .themed( + light: UniversalColor.white.light, + dark: UniversalColor.content2.dark + ) return color.enabled(self.isEnabled) } func item(for id: ID) -> SegmentedControlItemVM? { From d7719c7a226df15db9571ae0a96e3831bedda787 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 21 Feb 2025 14:03:50 +0100 Subject: [PATCH 12/48] make UKCard generic over a content --- Sources/ComponentsKit/Components/Card/UKCard.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Sources/ComponentsKit/Components/Card/UKCard.swift b/Sources/ComponentsKit/Components/Card/UKCard.swift index 42a54629..bae38bde 100644 --- a/Sources/ComponentsKit/Components/Card/UKCard.swift +++ b/Sources/ComponentsKit/Components/Card/UKCard.swift @@ -15,16 +15,11 @@ import UIKit /// } /// ) /// ``` -open class UKCard: UIView, UKComponent { - // MARK: - Typealiases - - /// A closure that returns the content view to be displayed inside the card. - public typealias Content = () -> UIView - +open class UKCard: UIView, UKComponent { // MARK: - Subviews /// The primary content of the card, provided as a custom view. - public let content: UIView + public let content: Content // MARK: - Properties @@ -46,7 +41,7 @@ open class UKCard: UIView, UKComponent { /// - content: The content that is displayed in the card. public init( model: CardVM = .init(), - content: @escaping Content + content: @escaping () -> Content ) { self.model = model self.content = content() From c04f44042d1344d92d354fde99872250e7670329 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 10 Mar 2025 15:38:34 +0100 Subject: [PATCH 13/48] fix: modal not being presented on app launch --- .../Modal/SwiftUI/Helpers/ModalPresentationModifier.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift index 3eb02ad4..04be9a05 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift @@ -23,6 +23,11 @@ struct ModalPresentationModifier: ViewModifier { func body(content: Content) -> some View { content + .onAppear { + if self.isContentVisible { + self.isPresented = true + } + } .onChange(of: self.isContentVisible) { newValue in if newValue { self.isPresented = true From 18f53ba6b6f9f172794ee998cb3e0d2ac943b2b5 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 10 Mar 2025 17:46:48 +0100 Subject: [PATCH 14/48] change api of modals: calculate model with item --- .../Helpers/ModalPresentationModifier.swift | 4 ++-- .../ModalPresentationWithItemModifier.swift | 18 ++++++++++++------ .../Modal/SwiftUI/SUBottomModal.swift | 12 ++++++------ .../Modal/SwiftUI/SUCenterModal.swift | 12 ++++++------ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift index 04be9a05..9b0eebc7 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationModifier.swift @@ -28,8 +28,8 @@ struct ModalPresentationModifier: ViewModifier { self.isPresented = true } } - .onChange(of: self.isContentVisible) { newValue in - if newValue { + .onChange(of: self.isContentVisible) { isVisible in + if isVisible { self.isPresented = true } else { DispatchQueue.main.asyncAfter(deadline: .now() + self.transitionDuration) { diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationWithItemModifier.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationWithItemModifier.swift index 31e7cdc8..0ad9a283 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationWithItemModifier.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/Helpers/ModalPresentationWithItemModifier.swift @@ -6,12 +6,12 @@ struct ModalPresentationWithItemModifier: ViewM @ViewBuilder var content: (Item) -> Modal - let transitionDuration: TimeInterval + let transitionDuration: (Item) -> TimeInterval let onDismiss: (() -> Void)? init( item: Binding, - transitionDuration: TimeInterval, + transitionDuration: @escaping (Item) -> TimeInterval, onDismiss: (() -> Void)?, @ViewBuilder content: @escaping (Item) -> Modal ) { @@ -23,11 +23,17 @@ struct ModalPresentationWithItemModifier: ViewM func body(content: Content) -> some View { content - .onChange(of: self.visibleItem.isNotNil) { newValue in - if newValue { + .onAppear { + self.presentedItem = self.visibleItem + } + .onChange(of: self.visibleItem.isNotNil) { isVisible in + if isVisible { self.presentedItem = self.visibleItem } else { - DispatchQueue.main.asyncAfter(deadline: .now() + self.transitionDuration) { + let duration = self.presentedItem.map { item in + self.transitionDuration(item) + } ?? 0.3 + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { self.presentedItem = self.visibleItem } } @@ -49,7 +55,7 @@ struct ModalPresentationWithItemModifier: ViewM extension View { func modal( item: Binding, - transitionDuration: TimeInterval, + transitionDuration: @escaping (Item) -> TimeInterval, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Item) -> Modal ) -> some View { diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUBottomModal.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUBottomModal.swift index 000a1466..8323bc39 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUBottomModal.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUBottomModal.swift @@ -195,7 +195,7 @@ extension View { /// } /// .bottomModal( /// item: $selectedItem, - /// model: BottomModalVM(), + /// model: { _ in BottomModalVM() }, /// onDismiss: { /// print("Modal dismissed") /// }, @@ -218,7 +218,7 @@ extension View { /// ``` public func bottomModal( item: Binding, - model: BottomModalVM = .init(), + model: @escaping (Item) -> BottomModalVM = { _ in .init() }, onDismiss: (() -> Void)? = nil, @ViewBuilder header: @escaping (Item) -> Header, @ViewBuilder body: @escaping (Item) -> Body, @@ -226,7 +226,7 @@ extension View { ) -> some View { return self.modal( item: item, - transitionDuration: model.transition.value, + transitionDuration: { model($0).transition.value }, onDismiss: onDismiss, content: { unwrappedItem in SUBottomModal( @@ -242,7 +242,7 @@ extension View { } } ), - model: model, + model: model(unwrappedItem), header: { header(unwrappedItem) }, body: { body(unwrappedItem) }, footer: { footer(unwrappedItem) } @@ -289,7 +289,7 @@ extension View { /// } /// .bottomModal( /// item: $selectedItem, - /// model: BottomModalVM(), + /// model: { _ in BottomModalVM() }, /// onDismiss: { /// print("Modal dismissed") /// }, @@ -302,7 +302,7 @@ extension View { /// ``` public func bottomModal( item: Binding, - model: BottomModalVM = .init(), + model: @escaping (Item) -> BottomModalVM = { _ in .init() }, onDismiss: (() -> Void)? = nil, @ViewBuilder body: @escaping (Item) -> Body ) -> some View { diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift index dd7c65d2..610a4933 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/SUCenterModal.swift @@ -157,7 +157,7 @@ extension View { /// } /// .centerModal( /// item: $selectedItem, - /// model: CenterModalVM(), + /// model: { _ in CenterModalVM() }, /// onDismiss: { /// print("Modal dismissed") /// }, @@ -180,7 +180,7 @@ extension View { /// ``` public func centerModal( item: Binding, - model: CenterModalVM = .init(), + model: @escaping (Item) -> CenterModalVM = { _ in .init() }, onDismiss: (() -> Void)? = nil, @ViewBuilder header: @escaping (Item) -> Header, @ViewBuilder body: @escaping (Item) -> Body, @@ -188,7 +188,7 @@ extension View { ) -> some View { return self.modal( item: item, - transitionDuration: model.transition.value, + transitionDuration: { model($0).transition.value }, onDismiss: onDismiss, content: { unwrappedItem in SUCenterModal( @@ -204,7 +204,7 @@ extension View { } } ), - model: model, + model: model(unwrappedItem), header: { header(unwrappedItem) }, body: { body(unwrappedItem) }, footer: { footer(unwrappedItem) } @@ -251,7 +251,7 @@ extension View { /// } /// .centerModal( /// item: $selectedItem, - /// model: CenterModalVM(), + /// model: { _ in CenterModalVM() }, /// onDismiss: { /// print("Modal dismissed") /// }, @@ -264,7 +264,7 @@ extension View { /// ``` public func centerModal( item: Binding, - model: CenterModalVM = .init(), + model: @escaping (Item) -> CenterModalVM = { _ in .init() }, onDismiss: (() -> Void)? = nil, @ViewBuilder body: @escaping (Item) -> Body ) -> some View { From dc8e9b333e938db1c7c87a1a68b11f9e77d28922 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 10 Mar 2025 18:22:08 +0100 Subject: [PATCH 15/48] added new api to present `suAlert` with an item --- .../Components/Alert/SUAlert.swift | 296 +++++++++++++----- 1 file changed, 212 insertions(+), 84 deletions(-) diff --git a/Sources/ComponentsKit/Components/Alert/SUAlert.swift b/Sources/ComponentsKit/Components/Alert/SUAlert.swift index 517612c9..645510b5 100644 --- a/Sources/ComponentsKit/Components/Alert/SUAlert.swift +++ b/Sources/ComponentsKit/Components/Alert/SUAlert.swift @@ -1,5 +1,133 @@ import SwiftUI +struct AlertContent: View { + @Binding var isPresented: Bool + let model: AlertVM + let primaryAction: (() -> Void)? + let secondaryAction: (() -> Void)? + + var body: some View { + SUCenterModal( + isVisible: self.$isPresented, + model: self.model.modalVM, + header: { + if self.model.message.isNotNil, + let text = self.model.title { + self.title(text) + } + }, + body: { + if let text = self.model.message { + self.message(text) + } else if let text = self.model.title { + self.title(text) + } + }, + footer: { + switch AlertButtonsOrientationCalculator.preferredOrientation(model: model) { + case .horizontal: + HStack(spacing: AlertVM.buttonsSpacing) { + self.button( + model: self.model.secondaryButtonVM, + action: self.secondaryAction + ) + self.button( + model: self.model.primaryButtonVM, + action: self.primaryAction + ) + } + case .vertical: + VStack(spacing: AlertVM.buttonsSpacing) { + self.button( + model: self.model.primaryButtonVM, + action: self.primaryAction + ) + self.button( + model: self.model.secondaryButtonVM, + action: self.secondaryAction + ) + } + } + } + ) + } + + // MARK: - Helpers + + func title(_ text: String) -> some View { + Text(text) + .font(UniversalFont.mdHeadline.font) + .foregroundStyle(UniversalColor.foreground.color) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } + + func message(_ text: String) -> some View { + Text(text) + .font(UniversalFont.mdBody.font) + .foregroundStyle(UniversalColor.secondaryForeground.color) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } + + func button( + model: ButtonVM?, + action: (() -> Void)? + ) -> some View { + Group { + if let model { + SUButton(model: model) { + action?() + self.isPresented = false + } + } + } + } +} + +// MARK: - Helpers + +private struct AlertTitle: View { + let text: String + + var body: some View { + Text(self.text) + .font(UniversalFont.mdHeadline.font) + .foregroundStyle(UniversalColor.foreground.color) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } +} + +private struct AlertMessage: View { + let text: String + + var body: some View { + Text(self.text) + .font(UniversalFont.mdBody.font) + .foregroundStyle(UniversalColor.secondaryForeground.color) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } +} + +private struct AlertButton: View { + @Binding var isAlertPresented: Bool + let model: ButtonVM? + let action: (() -> Void)? + + var body: some View { + if let model { + SUButton(model: model) { + self.action?() + self.isAlertPresented = false + } + } + } +} + +// MARK: - Presentation Helpers + extension View { /// A SwiftUI view modifier that presents an alert with a title, message, and up to two action buttons. /// @@ -53,95 +181,95 @@ extension View { transitionDuration: model.transition.value, onDismiss: onDismiss, content: { - SUCenterModal( - isVisible: isPresented, - model: model.modalVM, - header: { - if model.message.isNotNil, - let title = model.title { - AlertTitle(text: title) - } - }, - body: { - if let message = model.message { - AlertMessage(text: message) - } else if let title = model.title { - AlertTitle(text: title) - } - }, - footer: { - switch AlertButtonsOrientationCalculator.preferredOrientation(model: model) { - case .horizontal: - HStack(spacing: AlertVM.buttonsSpacing) { - AlertButton( - isAlertPresented: isPresented, - model: model.secondaryButtonVM, - action: secondaryAction - ) - AlertButton( - isAlertPresented: isPresented, - model: model.primaryButtonVM, - action: primaryAction - ) - } - case .vertical: - VStack(spacing: AlertVM.buttonsSpacing) { - AlertButton( - isAlertPresented: isPresented, - model: model.primaryButtonVM, - action: primaryAction - ) - AlertButton( - isAlertPresented: isPresented, - model: model.secondaryButtonVM, - action: secondaryAction - ) - } - } - } + AlertContent( + isPresented: isPresented, + model: model, + primaryAction: primaryAction, + secondaryAction: secondaryAction ) } ) } -} - -// MARK: - Helpers - -private struct AlertTitle: View { - let text: String - - var body: some View { - Text(self.text) - .font(UniversalFont.mdHeadline.font) - .foregroundStyle(UniversalColor.foreground.color) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - } -} - -private struct AlertMessage: View { - let text: String - - var body: some View { - Text(self.text) - .font(UniversalFont.mdBody.font) - .foregroundStyle(UniversalColor.secondaryForeground.color) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - } -} - -private struct AlertButton: View { - @Binding var isAlertPresented: Bool - let model: ButtonVM? - let action: (() -> Void)? - var body: some View { - if let model { - SUButton(model: model) { - self.action?() - self.isAlertPresented = false + /// A SwiftUI view modifier that presents an alert with a title, message, and up to two action buttons. + /// + /// All actions in an alert dismiss the alert after the action runs. If no actions are present, a standard “OK” action is included. + /// + /// - Parameters: + /// - isPresented: A binding that determines whether the alert is presented. + /// - item: A binding to an optional `Item` that determines whether the alert is presented. + /// When `item` is `nil`, the alert is hidden. + /// - primaryAction: An optional closure executed when the primary button is tapped. + /// - secondaryAction: An optional closure executed when the secondary button is tapped. + /// - onDismiss: An optional closure executed when the alert is dismissed. + /// + /// - Example: + /// ```swift + /// struct ContentView: View { + /// struct AlertData: Identifiable { + /// var id: String { + /// return text + /// } + /// let text: String + /// } + /// + /// @State private var selectedItem: AlertData? + /// private let items: [AlertData] = [ + /// AlertData(text: "data 1"), + /// AlertData(text: "data 2") + /// ] + /// + /// var body: some View { + /// List(items) { item in + /// Button("Show Alert") { + /// selectedItem = item + /// } + /// } + /// .suAlert( + /// item: $selectedItem, + /// model: { data in + /// return AlertVM { + /// $0.title = "Data Preview" + /// $0.message = data.text + /// } + /// }, + /// onDismiss: { + /// print("Alert dismissed") + /// } + /// ) + /// } + /// } + /// ``` + public func suAlert( + item: Binding, + model: @escaping (Item) -> AlertVM, + primaryAction: ((Item) -> Void)? = nil, + secondaryAction: ((Item) -> Void)? = nil, + onDismiss: (() -> Void)? = nil + ) -> some View { + return self.modal( + item: item, + transitionDuration: { model($0).transition.value }, + onDismiss: onDismiss, + content: { unwrappedItem in + AlertContent( + isPresented: .init( + get: { + return item.wrappedValue.isNotNil + }, + set: { isPresented in + if isPresented { + item.wrappedValue = unwrappedItem + } else { + item.wrappedValue = nil + } + } + ), + model: model(unwrappedItem), + primaryAction: { primaryAction?(unwrappedItem) }, + secondaryAction: { secondaryAction?(unwrappedItem) } + ) } - } + ) } } From e43d0641afac60c2d0e773ce715e67ad87bfaa29 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 11 Mar 2025 12:23:11 +0100 Subject: [PATCH 16/48] remove unused helpers --- .../Components/Alert/SUAlert.swift | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/Sources/ComponentsKit/Components/Alert/SUAlert.swift b/Sources/ComponentsKit/Components/Alert/SUAlert.swift index 645510b5..741a8ba6 100644 --- a/Sources/ComponentsKit/Components/Alert/SUAlert.swift +++ b/Sources/ComponentsKit/Components/Alert/SUAlert.swift @@ -85,47 +85,6 @@ struct AlertContent: View { } } -// MARK: - Helpers - -private struct AlertTitle: View { - let text: String - - var body: some View { - Text(self.text) - .font(UniversalFont.mdHeadline.font) - .foregroundStyle(UniversalColor.foreground.color) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - } -} - -private struct AlertMessage: View { - let text: String - - var body: some View { - Text(self.text) - .font(UniversalFont.mdBody.font) - .foregroundStyle(UniversalColor.secondaryForeground.color) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - } -} - -private struct AlertButton: View { - @Binding var isAlertPresented: Bool - let model: ButtonVM? - let action: (() -> Void)? - - var body: some View { - if let model { - SUButton(model: model) { - self.action?() - self.isAlertPresented = false - } - } - } -} - // MARK: - Presentation Helpers extension View { From 303cbf02a9ce580d71162538518ca7c8bc2ca1df Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 13 Mar 2025 14:11:18 +0100 Subject: [PATCH 17/48] send notification when the current theme changes --- Sources/ComponentsKit/Theme/Theme.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Sources/ComponentsKit/Theme/Theme.swift b/Sources/ComponentsKit/Theme/Theme.swift index d65e0665..01aac7e4 100644 --- a/Sources/ComponentsKit/Theme/Theme.swift +++ b/Sources/ComponentsKit/Theme/Theme.swift @@ -17,9 +17,21 @@ public struct Theme: Initializable, Updatable, Equatable { public init() {} } -// MARK: - Theme + Shared +// MARK: - Theme + Current extension Theme { + /// A notification that is triggered when a theme changes. + public static let didChangeThemeNotification = Notification.Name("didChangeThemeNotification") + /// A current instance of `Theme` for global use. - public static var current: Self = .init() + /// + /// Triggers `Theme.didChangeThemeNotification` notification when the value changes. + public static var current = Self() { + didSet { + NotificationCenter.default.post( + name: Self.didChangeThemeNotification, + object: nil + ) + } + } } From a12d03f28cb3697b073c1dac1ebbe039ffbea222 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 13 Mar 2025 14:11:41 +0100 Subject: [PATCH 18/48] add helpers to observe theme changes --- .../Helpers/SwiftUI/ThemeChangeObserver.swift | 48 +++++++++++ .../UIKit/NSObject+ObserveThemeChange.swift | 80 +++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 Sources/ComponentsKit/Helpers/SwiftUI/ThemeChangeObserver.swift create mode 100644 Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift diff --git a/Sources/ComponentsKit/Helpers/SwiftUI/ThemeChangeObserver.swift b/Sources/ComponentsKit/Helpers/SwiftUI/ThemeChangeObserver.swift new file mode 100644 index 00000000..df3bd5ae --- /dev/null +++ b/Sources/ComponentsKit/Helpers/SwiftUI/ThemeChangeObserver.swift @@ -0,0 +1,48 @@ +import SwiftUI + +/// A SwiftUI wrapper that listens for theme changes and automatically refreshes its content. +/// +/// `ThemeChangeObserver` ensures that its child views **rebuild** whenever the theme changes, +/// helping to apply updated theme styles dynamically. +/// +/// ## Usage +/// +/// Wrap your view inside `ThemeChangeObserver` to make it responsive to theme updates: +/// +/// ```swift +/// @main +/// struct Root: App { +/// var body: some Scene { +/// WindowGroup { +/// ThemeChangeObserver { +/// Content() +/// } +/// } +/// } +/// } +/// ``` +/// +/// ## Performance Considerations +/// +/// - This approach forces a **full re-evaluation** of the wrapped content, which ensures all theme-dependent +/// properties are updated. +/// - Use it **at a high level** in your SwiftUI hierarchy (e.g., wrapping entire screens) rather than for small components. +public struct ThemeChangeObserver: View { + @State private var themeId = UUID() + @ViewBuilder var content: () -> Content + + public init(content: @escaping () -> Content) { + self.content = content + } + + public var body: some View { + self.content() + .onReceive(NotificationCenter.default.publisher( + for: Theme.didChangeThemeNotification, + object: nil + )) { _ in + self.themeId = UUID() + } + .id(self.themeId) + } +} diff --git a/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift b/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift new file mode 100644 index 00000000..5691d49f --- /dev/null +++ b/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift @@ -0,0 +1,80 @@ +import Foundation +import Combine + +extension NSObject { + /// Observes changes to the `.current` theme and updates dependent views. + /// + /// This method allows you to respond to theme changes by updating view properties that depend on the theme. + /// + /// You can invoke the ``observeThemeChange(_:)`` method a single time in the `viewDidLoad` + /// and update all the view elements: + /// + /// ```swift + /// override func viewDidLoad() { + /// super.viewDidLoad() + /// + /// style() + /// + /// observeThemeChanges { [weak self] in + /// guard let self else { return } + /// + /// self.style() + /// } + /// } + /// + /// func style() { + /// view.backgroundColor = UniversalColor.background.uiColor + /// // ... + /// } + /// ``` + /// + /// > Note: There is no need to update components from the library as they observe the changes internally. + /// + /// ## Cancellation + /// + /// The method returns an ``AnyCancellable`` that can be used to cancel observation. For + /// example, if you only want to observe while a view controller is visible, you can start + /// observation in the `viewWillAppear` and then cancel observation in the `viewWillDisappear`: + /// + /// ```swift + /// var cancellable: AnyCancellable? + /// + /// func viewWillAppear() { + /// super.viewWillAppear() + /// cancellable = observeThemeChange { [weak self] in + /// // ... + /// } + /// } + /// func viewWillDisappear() { + /// super.viewWillDisappear() + /// cancellable?.cancel() + /// } + /// ``` + /// + /// - Parameter apply: A closure that will be called whenever the `.current` theme changes. + /// This should contain logic to update theme-dependent views. + /// - Returns: An `AnyCancellable` instance that can be used to stop observing the theme changes when needed. + @discardableResult + public func observeThemeChange(_ apply: @escaping () -> Void) -> AnyCancellable { + let cancellable = NotificationCenter.default.publisher( + for: Theme.didChangeThemeNotification + ) + .receive(on: DispatchQueue.main) + .sink { _ in + apply() + } + self.cancellables.append(cancellable) + return cancellable + } + + fileprivate var cancellables: [Any] { + get { + objc_getAssociatedObject(self, Self.cancellablesKey) as? [Any] ?? [] + } + set { + objc_setAssociatedObject(self, Self.cancellablesKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + private static let cancellablesKey = "themeChangeObserverCancellables" +} From b31f71d0b7fc36dba21b0626c34b16bd4da7e915 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 13 Mar 2025 14:28:48 +0100 Subject: [PATCH 19/48] Update README.md --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/README.md b/README.md index 5dcd4c8d..39014212 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,74 @@ When the user switches the theme, apply it by assigning it to the `current` inst Theme.current = halloweenTheme ``` +**Handling Theme Changes** + +When changing themes dynamically, you may need to **update the UI** to reflect the new theme. Below are approaches for handling this in different environments. + +**SwiftUI** + +For SwiftUI apps, you can use `ThemeChangeObserver` to automatically refresh views when the theme updates. + +```swift +@main +struct Root: App { + var body: some Scene { + WindowGroup { + ThemeChangeObserver { + Content() + } + } + } +} +``` + +We recommend using this helper in the root of your app to redraw everything at once. + +**UIKit** + +For UIKit apps, use the `observeThemeChange(_:)` method to update elements that depend on the properties from the library. + +```swift +override func viewDidLoad() { + super.viewDidLoad() + + style() + + observeThemeChange { [weak self] in + guard let self else { return } + self.style() + } +} + +func style() { + view.backgroundColor = UniversalColor.background.uiColor +} +``` + +**Manually Handling Theme Changes** + +If you are not using the built-in helpers, you can listen for theme change notifications and manually update the UI: + +```swift +NotificationCenter.default.addObserver( + self, + selector: #selector(handleThemeChange), + name: Theme.didChangeThemeNotification, + object: nil +) + +@objc private func handleThemeChange() { + view.backgroundColor = UniversalColor.background.uiColor +} +``` + +Don't forget to remove the observer when the view is deallocated: +```swift +deinit { + NotificationCenter.default.removeObserver(self, name: Theme.didChangeThemeNotification, object: nil) +} +``` + **Extend Colors** All colors from the theme can be used within the app. For example: From 8e5160aa7537d159fbd969a2bf71b294ee519a01 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 13 Mar 2025 15:29:10 +0100 Subject: [PATCH 20/48] update docs --- README.md | 4 ++++ .../Helpers/UIKit/NSObject+ObserveThemeChange.swift | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 39014212..27c1b1b9 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,10 @@ override func viewDidLoad() { func style() { view.backgroundColor = UniversalColor.background.uiColor + button.model = ButtonVM { + $0.title = "Tap me" + $0.color = .accent + } } ``` diff --git a/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift b/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift index 5691d49f..a0876878 100644 --- a/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift +++ b/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift @@ -24,12 +24,14 @@ extension NSObject { /// /// func style() { /// view.backgroundColor = UniversalColor.background.uiColor + /// button.model = ButtonVM { + /// $0.title = "Tap me" + /// $0.color = .accent + /// } /// // ... /// } /// ``` /// - /// > Note: There is no need to update components from the library as they observe the changes internally. - /// /// ## Cancellation /// /// The method returns an ``AnyCancellable`` that can be used to cancel observation. For From 8eeb592243e88e2876f66ea2da741c51c3d5a299 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 13 Mar 2025 16:57:17 +0100 Subject: [PATCH 21/48] run swiftlint --- .../ComponentsKit/Helpers/SwiftUI/ThemeChangeObserver.swift | 4 ++-- .../Helpers/UIKit/NSObject+ObserveThemeChange.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ComponentsKit/Helpers/SwiftUI/ThemeChangeObserver.swift b/Sources/ComponentsKit/Helpers/SwiftUI/ThemeChangeObserver.swift index df3bd5ae..bc590c8e 100644 --- a/Sources/ComponentsKit/Helpers/SwiftUI/ThemeChangeObserver.swift +++ b/Sources/ComponentsKit/Helpers/SwiftUI/ThemeChangeObserver.swift @@ -30,11 +30,11 @@ import SwiftUI public struct ThemeChangeObserver: View { @State private var themeId = UUID() @ViewBuilder var content: () -> Content - + public init(content: @escaping () -> Content) { self.content = content } - + public var body: some View { self.content() .onReceive(NotificationCenter.default.publisher( diff --git a/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift b/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift index a0876878..88eb1f2a 100644 --- a/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift +++ b/Sources/ComponentsKit/Helpers/UIKit/NSObject+ObserveThemeChange.swift @@ -1,5 +1,5 @@ -import Foundation import Combine +import Foundation extension NSObject { /// Observes changes to the `.current` theme and updates dependent views. @@ -68,7 +68,7 @@ extension NSObject { self.cancellables.append(cancellable) return cancellable } - + fileprivate var cancellables: [Any] { get { objc_getAssociatedObject(self, Self.cancellablesKey) as? [Any] ?? [] @@ -77,6 +77,6 @@ extension NSObject { objc_setAssociatedObject(self, Self.cancellablesKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } - + private static let cancellablesKey = "themeChangeObserverCancellables" } From 889678fa4a2e57776739b5d2bb7c251cacc0fd31 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 24 Mar 2025 11:35:59 +0100 Subject: [PATCH 22/48] make the light and dark color properties publicly accessible in `UniversalColor` --- Sources/ComponentsKit/Shared/Colors/UniversalColor.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift b/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift index 28c240a5..03baf7b0 100644 --- a/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift +++ b/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift @@ -132,10 +132,10 @@ public struct UniversalColor: Hashable { // MARK: - Properties /// The color used in light mode. - let light: ColorRepresentable + public let light: ColorRepresentable /// The color used in dark mode. - let dark: ColorRepresentable + public let dark: ColorRepresentable // MARK: - Initialization From 287d861e1f23f301a3395d7fe6ae2d6a131a6e02 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 25 Mar 2025 17:18:02 +0100 Subject: [PATCH 23/48] change default color for the selected segmented control --- .../Components/SegmentedControl/Models/SegmentedControlVM.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift index e95308a7..876e41aa 100644 --- a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift @@ -56,7 +56,7 @@ extension SegmentedControlVM { } var selectedSegmentColor: UniversalColor { let color = self.color?.main ?? .themed( - light: UniversalColor.white.light, + light: UniversalColor.background.light, dark: UniversalColor.content2.dark ) return color.enabled(self.isEnabled) From e42e06e6da9ce3757f9abdea667666ed8edfe078 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 27 Mar 2025 14:23:47 +0100 Subject: [PATCH 24/48] improve method for calculating adapted corner radius for the text input --- .../TextInput/Models/TextInputVM.swift | 32 +++++++------------ .../Components/TextInput/SUTextInput.swift | 2 +- .../Components/TextInput/UKTextInput.swift | 7 ++-- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift b/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift index db000784..274cce4b 100644 --- a/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift +++ b/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift @@ -69,23 +69,6 @@ public struct TextInputVM: ComponentVM { // MARK: - Shared Helpers extension TextInputVM { - var adaptedCornerRadius: ComponentRadius { - switch self.cornerRadius { - case .none: - return .none - case .small: - return .small - case .medium: - return .medium - case .large: - return .large - case .full: - return .custom(self.height(forRows: 1) / 2) - case .custom(let value): - return .custom(value) - } - } - var preferredFont: UniversalFont { if let font { return font @@ -140,6 +123,17 @@ extension TextInputVM { } } + func adaptedCornerRadius(for height: CGFloat = 10_000) -> CGFloat { + switch self.cornerRadius { + case .none, .small, .medium, .large, .full: + let value = self.cornerRadius.value(for: height) + let maxValue = ComponentRadius.custom(self.height(forRows: 1) / 2).value(for: height) + return min(value, maxValue) + case .custom(let value): + return ComponentRadius.custom(value).value(for: height) + } + } + private func height(forRows rows: Int) -> CGFloat { if rows < 1 { assertionFailure("Number of rows in TextInput must be greater than or equal to 1") @@ -162,8 +156,4 @@ extension TextInputVM { var autocorrectionType: UITextAutocorrectionType { return self.isAutocorrectionEnabled ? .yes : .no } - - func shouldUpdateCornerRadius(_ oldModel: Self) -> Bool { - return self.adaptedCornerRadius != oldModel.adaptedCornerRadius - } } diff --git a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift index 3616297c..ca95da09 100644 --- a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift @@ -116,7 +116,7 @@ public struct SUTextInput: View { ) .clipShape( RoundedRectangle( - cornerRadius: self.model.adaptedCornerRadius.value() + cornerRadius: self.model.adaptedCornerRadius() ) ) } diff --git a/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift b/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift index 1e539bc0..8e4b8519 100644 --- a/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift @@ -118,9 +118,6 @@ open class UKTextInput: UIView, UKComponent { self.style() - if self.model.shouldUpdateCornerRadius(oldModel) { - self.updateCornerRadius() - } if self.model.shouldUpdateLayout(oldModel) { self.invalidateIntrinsicContentSize() self.setNeedsLayout() @@ -171,7 +168,7 @@ open class UKTextInput: UIView, UKComponent { } private func updateCornerRadius() { - self.layer.cornerRadius = self.model.adaptedCornerRadius.value(for: self.bounds.height) + self.layer.cornerRadius = self.model.adaptedCornerRadius(for: self.bounds.height) } } @@ -189,7 +186,7 @@ extension UKTextInput { fileprivate enum Style { static func mainView(_ view: UIView, model: TextInputVM) { view.backgroundColor = model.backgroundColor.uiColor - view.layer.cornerRadius = model.adaptedCornerRadius.value(for: view.bounds.height) + view.layer.cornerRadius = model.adaptedCornerRadius(for: view.bounds.height) } static func textView( From 4b98a2a39465426fdd70371aa76d2e92860f1521 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 27 Mar 2025 14:36:35 +0100 Subject: [PATCH 25/48] recalculate text input height when device rotates --- .../ComponentsKit/Components/TextInput/SUTextInput.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift index ca95da09..44019dc0 100644 --- a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift @@ -112,6 +112,13 @@ public struct SUTextInput: View { ) } } + .onChange(of: geometry.size.width) { newValue in + self.textEditorPreferredHeight = TextInputHeightCalculator.preferredHeight( + for: self.text, + model: self.model, + width: newValue + ) + } } ) .clipShape( From e539c85424a2f387ddc88f15ec1700443e27168b Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 27 Mar 2025 14:38:19 +0100 Subject: [PATCH 26/48] make text input resistant to compression --- .../Components/TextInput/SUTextInput.swift | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift index 44019dc0..47e68014 100644 --- a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift @@ -55,16 +55,14 @@ public struct SUTextInput: View { TextEditor(text: self.$text) .contentMargins(self.model.contentPadding) .transparentScrollBackground() - .frame( - minHeight: self.model.minTextInputHeight, - maxHeight: max( - self.model.minTextInputHeight, - min( - self.model.maxTextInputHeight, - self.textEditorPreferredHeight - ) + .frame(minHeight: self.model.minTextInputHeight) + .frame(height: max( + self.model.minTextInputHeight, + min( + self.model.maxTextInputHeight, + self.textEditorPreferredHeight ) - ) + )) .lineSpacing(0) .font(self.model.preferredFont.font) .foregroundStyle(self.model.foregroundColor.color) From 86f64206260a36c1211529cf67d6cc38e105fe08 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 28 Mar 2025 11:15:11 +0100 Subject: [PATCH 27/48] force modal's content take all available width --- .../Helpers/ModalPreview+Helpers.swift | 20 +++++++++++-------- .../Modal/SwiftUI/ModalContent.swift | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift index 2b27dd5c..eb8fccea 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift @@ -203,16 +203,20 @@ Enim habitant laoreet inceptos scelerisque senectus, tellus molestie ut. Eros ri } static func suBody(body: ContentBody) -> some View { - Group { - switch body { - case .shortText: - Text(self.bodyShortText) - case .longText: - Text(self.bodyLongText) + HStack { + Group { + switch body { + case .shortText: + Text(self.bodyShortText) + case .longText: + Text(self.bodyLongText) + } } + .font(self.bodyFont.font) + .multilineTextAlignment(.leading) + + Spacer() } - .font(self.bodyFont.font) - .multilineTextAlignment(.leading) } static func suFooter( diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift index 1f379c1e..50b7fa55 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift @@ -43,7 +43,7 @@ struct ModalContent: View { .padding(.top, self.bodyTopPadding) .padding(.bottom, self.bodyBottomPadding) } - .frame(maxHeight: self.scrollViewMaxHeight) + .frame(maxWidth: .infinity, maxHeight: self.scrollViewMaxHeight) .disableScrollWhenContentFits() self.contentFooter() From d4bf81459df52273005f32e6734b2af4b47eee6d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 28 Mar 2025 13:15:41 +0100 Subject: [PATCH 28/48] remove `Binding` from `currentValue` in `SUProgressBar` --- .../PreviewPages/ProgressBarPreview.swift | 2 +- .../Components/ProgressBar/SUProgressBar.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ProgressBarPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ProgressBarPreview.swift index 19589326..c7b5133e 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ProgressBarPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ProgressBarPreview.swift @@ -26,7 +26,7 @@ struct ProgressBarPreview: View { } } PreviewWrapper(title: "SwiftUI") { - SUProgressBar(currentValue: self.$currentValue, model: self.model) + SUProgressBar(currentValue: self.currentValue, model: self.model) } Form { ComponentColorPicker(selection: self.$model.color) diff --git a/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift b/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift index 756d49a4..8da78bd7 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift @@ -6,8 +6,8 @@ public struct SUProgressBar: View { /// A model that defines the appearance properties. public var model: ProgressBarVM - /// A binding to control the current value. - @Binding public var currentValue: CGFloat + /// The current progress value. + public var currentValue: CGFloat private var progress: CGFloat { self.model.progress(for: self.currentValue) @@ -17,13 +17,13 @@ public struct SUProgressBar: View { /// Initializer. /// - Parameters: - /// - currentValue: A binding to the current value. + /// - currentValue: The current progress value. /// - model: A model that defines the appearance properties. public init( - currentValue: Binding, + currentValue: CGFloat, model: ProgressBarVM = .init() ) { - self._currentValue = currentValue + self.currentValue = currentValue self.model = model } From d62c05a9641d4ccca9ee82be2c5590fa21199468 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 28 Mar 2025 14:10:53 +0100 Subject: [PATCH 29/48] fix: do not update selected id if the item is disabled in `UKSegmentedControl` --- .../Components/SegmentedControl/UKSegmentedControl.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift b/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift index eced335a..0b47d10e 100644 --- a/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift @@ -261,6 +261,7 @@ open class UKSegmentedControl: UIView, UKComponent { let segment = self.segments.first(where: { segment in segment.bounds.contains(touch.location(in: segment)) }), + self.model.item(for: segment.id)?.isEnabled == true, self.selectedId != segment.id, let currentlySelectedSegment = self.segment(for: self.selectedId) else { From 87d7175965520a0db04bc7bc7aaf2e5456ceb161 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 28 Mar 2025 14:46:43 +0100 Subject: [PATCH 30/48] change previews order --- Examples/DemosApp/DemosApp/Core/App.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/DemosApp/DemosApp/Core/App.swift b/Examples/DemosApp/DemosApp/Core/App.swift index 42d78710..52966511 100644 --- a/Examples/DemosApp/DemosApp/Core/App.swift +++ b/Examples/DemosApp/DemosApp/Core/App.swift @@ -41,15 +41,15 @@ struct App: View { NavigationLinkWithTitle("Loading") { LoadingPreview() } - NavigationLinkWithTitle("Progress Bar") { - ProgressBarPreview() - } NavigationLinkWithTitle("Modal (Bottom)") { BottomModalPreview() } NavigationLinkWithTitle("Modal (Center)") { CenterModalPreview() } + NavigationLinkWithTitle("Progress Bar") { + ProgressBarPreview() + } NavigationLinkWithTitle("Radio Group") { RadioGroupPreview() } From 0bec5b1dc6ed59d35d78ef606abd42d54566ce0d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 31 Mar 2025 16:22:07 +0200 Subject: [PATCH 31/48] add cotrast colors as `UniversalColor` extensions --- Sources/ComponentsKit/Theme/Palette.swift | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Sources/ComponentsKit/Theme/Palette.swift b/Sources/ComponentsKit/Theme/Palette.swift index 5def595c..fb435dfc 100644 --- a/Sources/ComponentsKit/Theme/Palette.swift +++ b/Sources/ComponentsKit/Theme/Palette.swift @@ -199,6 +199,14 @@ extension UniversalColor { public static var primary: Self { return Theme.current.colors.primary.main } + /// The primary background color. + public static var primaryBackground: Self { + return Theme.current.colors.primary.background + } + /// The primary contrast color. + public static var primaryContrast: Self { + return Theme.current.colors.primary.contrast + } /// The accent color. public static var accent: Self { return Theme.current.colors.accent.main @@ -207,6 +215,10 @@ extension UniversalColor { public static var accentBackground: Self { return Theme.current.colors.accent.background } + /// The accent contrast color. + public static var accentContrast: Self { + return Theme.current.colors.accent.contrast + } /// The success state color, used for indicating positive actions or statuses. public static var success: Self { return Theme.current.colors.success.main @@ -215,6 +227,10 @@ extension UniversalColor { public static var successBackground: Self { return Theme.current.colors.success.background } + /// The success contrast color. + public static var successContrast: Self { + return Theme.current.colors.success.contrast + } /// The warning state color, used for indicating caution or non-critical alerts. public static var warning: Self { return Theme.current.colors.warning.main @@ -223,6 +239,10 @@ extension UniversalColor { public static var warningBackground: Self { return Theme.current.colors.warning.background } + /// The warning contrast color. + public static var warningContrast: Self { + return Theme.current.colors.warning.contrast + } /// The danger state color, used for indicating errors, destructive actions, or critical alerts. public static var danger: Self { return Theme.current.colors.danger.main @@ -231,4 +251,8 @@ extension UniversalColor { public static var dangerBackground: Self { return Theme.current.colors.danger.background } + /// The danger contrast color. + public static var dangerContrast: Self { + return Theme.current.colors.danger.contrast + } } From 7652036ff98366993cd60a66c9e69b215b454527 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 31 Mar 2025 16:31:32 +0200 Subject: [PATCH 32/48] fix button's preferred font in the large size --- Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 1b7ea7a6..8212febf 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -102,7 +102,7 @@ extension ButtonVM { case .medium: return .mdButton case .large: - return .mdButton + return .lgButton } } var height: CGFloat { From 71281630ebab71bd793140d69d48603e4fd8c119 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 1 Apr 2025 17:55:31 +0200 Subject: [PATCH 33/48] improve description for badge --- Sources/ComponentsKit/Components/Badge/SUBadge.swift | 2 +- Sources/ComponentsKit/Components/Badge/UKBadge.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ComponentsKit/Components/Badge/SUBadge.swift b/Sources/ComponentsKit/Components/Badge/SUBadge.swift index 216af4b5..b87c95f3 100644 --- a/Sources/ComponentsKit/Components/Badge/SUBadge.swift +++ b/Sources/ComponentsKit/Components/Badge/SUBadge.swift @@ -1,6 +1,6 @@ import SwiftUI -/// A SwiftUI component that displays a badge. +/// A SwiftUI component that is used to display status, notification counts, or labels. public struct SUBadge: View { // MARK: Properties diff --git a/Sources/ComponentsKit/Components/Badge/UKBadge.swift b/Sources/ComponentsKit/Components/Badge/UKBadge.swift index d6e100db..2dc63e3a 100644 --- a/Sources/ComponentsKit/Components/Badge/UKBadge.swift +++ b/Sources/ComponentsKit/Components/Badge/UKBadge.swift @@ -1,7 +1,7 @@ import AutoLayout import UIKit -/// A UIKit component that displays a badge. +/// A UIKit component that is used to display status, notification counts, or labels. open class UKBadge: UIView, UKComponent { // MARK: - Properties From 5ca7b7dbe7c5aad26281771b25d3a16922c4d5d2 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 12:33:45 +0200 Subject: [PATCH 34/48] remove stroked style; add arc shape in the circular progress --- .../CircularProgressPreview.swift | 9 +- .../Models/CircularProgressShape.swift | 9 ++ .../Models/CircularProgressStyle.swift | 9 -- .../Models/CircularProgressVM.swift | 126 +++++------------- .../CircularProgress/SUCircularProgress.swift | 85 +++--------- .../CircularProgress/UKCircularProgress.swift | 70 ++-------- 6 files changed, 79 insertions(+), 229 deletions(-) create mode 100644 Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressShape.swift delete mode 100644 Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressStyle.swift diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift index 40c9edee..39734ec7 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift @@ -7,6 +7,7 @@ struct CircularProgressPreview: View { @State private var currentValue: CGFloat = Self.initialValue private let circularProgress = UKCircularProgress( + initialValue: Self.initialValue, model: Self.initialModel ) @@ -43,9 +44,9 @@ struct CircularProgressPreview: View { Text("8").tag(Optional.some(8)) } SizePicker(selection: self.$model.size) - Picker("Style", selection: self.$model.style) { - Text("Light").tag(CircularProgressVM.Style.light) - Text("Striped").tag(CircularProgressVM.Style.striped) + Picker("Shape", selection: self.$model.shape) { + Text("Circle").tag(CircularProgressVM.Shape.circle) + Text("Arc").tag(CircularProgressVM.Shape.arc) } } .onReceive(self.timer) { _ in @@ -71,7 +72,7 @@ struct CircularProgressPreview: View { private static var initialModel = CircularProgressVM { $0.label = "0%" - $0.style = .light + $0.shape = .arc } } diff --git a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressShape.swift b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressShape.swift new file mode 100644 index 00000000..586e7872 --- /dev/null +++ b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressShape.swift @@ -0,0 +1,9 @@ +import Foundation + +extension CircularProgressVM { + /// Defines the shapes for the circular progress component. + public enum Shape { + case circle + case arc + } +} diff --git a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressStyle.swift b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressStyle.swift deleted file mode 100644 index 3c0588d7..00000000 --- a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressStyle.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -extension CircularProgressVM { - public enum Style { - /// Defines the visual styles for the circular progress component. - case light - case striped - } -} diff --git a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift index a032d056..e145e5e5 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift @@ -7,34 +7,34 @@ public struct CircularProgressVM: ComponentVM { /// Defaults to `.accent`. public var color: ComponentColor = .accent - /// The style of the circular progress indicator. - /// - /// Defaults to `.light`. - public var style: Style = .light + /// The font used for the circular progress label text. + public var font: UniversalFont? - /// The size of the circular progress. - /// - /// Defaults to `.medium`. - public var size: ComponentSize = .medium + /// An optional label to display inside the circular progress. + public var label: String? - /// The minimum value of the circular progress. - /// - /// Defaults to `0`. - public var minValue: CGFloat = 0 + /// The width of the circular progress stroke. + public var lineWidth: CGFloat? /// The maximum value of the circular progress. /// /// Defaults to `100`. public var maxValue: CGFloat = 100 - /// The width of the circular progress stroke. - public var lineWidth: CGFloat? + /// The minimum value of the circular progress. + /// + /// Defaults to `0`. + public var minValue: CGFloat = 0 - /// An optional label to display inside the circular progress. - public var label: String? + /// The shape of the circular progress indicator. + /// + /// Defaults to `.circle`. + public var shape: Shape = .circle - /// The font used for the circular progress label text. - public var font: UniversalFont? + /// The size of the circular progress. + /// + /// Defaults to `.medium`. + public var size: ComponentSize = .medium /// Initializes a new instance of `CircularProgressVM` with default values. public init() {} @@ -68,6 +68,22 @@ extension CircularProgressVM { y: self.preferredSize.height / 2 ) } + var startAngle: CGFloat { + switch self.shape { + case .circle: + return -0.5 * .pi + case .arc: + return 0.75 * .pi + } + } + var endAngle: CGFloat { + switch self.shape { + case .circle: + return 1.5 * .pi + case .arc: + return 2.25 * .pi + } + } var titleFont: UniversalFont { if let font { return font @@ -81,44 +97,6 @@ extension CircularProgressVM { return .lgCaption } } - var stripeWidth: CGFloat { - return 0.5 - } - private func stripesCGPath(in rect: CGRect) -> CGMutablePath { - let stripeSpacing: CGFloat = 3 - let stripeAngle: Angle = .degrees(135) - - let path = CGMutablePath() - let step = stripeWidth + stripeSpacing - let radians = stripeAngle.radians - - let dx: CGFloat = rect.height * tan(radians) - for x in stride(from: 0, through: rect.width + rect.height, by: step) { - let topLeft = CGPoint(x: x, y: 0) - let bottomRight = CGPoint(x: x + dx, y: rect.height) - - path.move(to: topLeft) - path.addLine(to: bottomRight) - path.closeSubpath() - } - return path - } -} - -extension CircularProgressVM { - func gap(for normalized: CGFloat) -> CGFloat { - return normalized > 0 ? 0.05 : 0 - } - - func stripedArcStart(for normalized: CGFloat) -> CGFloat { - let gapValue = self.gap(for: normalized) - return max(0, min(1, normalized + gapValue)) - } - - func stripedArcEnd(for normalized: CGFloat) -> CGFloat { - let gapValue = self.gap(for: normalized) - return 1 - gapValue - } } extension CircularProgressVM { @@ -133,33 +111,6 @@ extension CircularProgressVM { // MARK: - UIKit Helpers extension CircularProgressVM { - var isStripesLayerHidden: Bool { - switch self.style { - case .light: - return true - case .striped: - return false - } - } - var isBackgroundLayerHidden: Bool { - switch self.style { - case .light: - return false - case .striped: - return true - } - } - func stripesBezierPath(in rect: CGRect) -> UIBezierPath { - let center = CGPoint(x: rect.midX, y: rect.midY) - let path = UIBezierPath(cgPath: self.stripesCGPath(in: rect)) - var transform = CGAffineTransform.identity - transform = transform - .translatedBy(x: center.x, y: center.y) - .rotated(by: -CGFloat.pi / 2) - .translatedBy(x: -center.x, y: -center.y) - path.apply(transform) - return path - } func shouldInvalidateIntrinsicContentSize(_ oldModel: Self) -> Bool { return self.preferredSize != oldModel.preferredSize } @@ -170,12 +121,7 @@ extension CircularProgressVM { return self.minValue != oldModel.minValue || self.maxValue != oldModel.maxValue } -} - -// MARK: - SwiftUI Helpers - -extension CircularProgressVM { - func stripesPath(in rect: CGRect) -> Path { - Path(self.stripesCGPath(in: rect)) + func shouldUpdateShape(_ oldModel: Self) -> Bool { + return self.shape != oldModel.shape } } diff --git a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift index 0ba970ac..02597c4f 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift @@ -33,14 +33,22 @@ public struct SUCircularProgress: View { public var body: some View { ZStack { // Background part - Group { - switch self.model.style { - case .light: - self.lightBackground - case .striped: - self.stripedBackground - } + Path { path in + path.addArc( + center: self.model.center, + radius: self.model.radius, + startAngle: .radians(self.model.startAngle), + endAngle: .radians(self.model.endAngle), + clockwise: false + ) } + .stroke( + self.model.color.background.color, + style: StrokeStyle( + lineWidth: self.model.circularLineWidth, + lineCap: .round + ) + ) .frame( width: self.model.preferredSize.width, height: self.model.preferredSize.height @@ -51,8 +59,8 @@ public struct SUCircularProgress: View { path.addArc( center: self.model.center, radius: self.model.radius, - startAngle: .radians(0), - endAngle: .radians(2 * .pi), + startAngle: .radians(self.model.startAngle), + endAngle: .radians(self.model.endAngle), clockwise: false ) } @@ -64,7 +72,6 @@ public struct SUCircularProgress: View { lineCap: .round ) ) - .rotationEffect(.degrees(-90)) .frame( width: self.model.preferredSize.width, height: self.model.preferredSize.height @@ -82,62 +89,4 @@ public struct SUCircularProgress: View { value: self.progress ) } - - // MARK: - Subviews - - var lightBackground: some View { - Path { path in - path.addArc( - center: self.model.center, - radius: self.model.radius, - startAngle: .radians(0), - endAngle: .radians(2 * .pi), - clockwise: false - ) - } - .stroke( - self.model.color.background.color, - lineWidth: self.model.circularLineWidth - ) - } - - var stripedBackground: some View { - StripesShapeCircularProgress(model: self.model) - .stroke( - self.model.color.main.color, - style: StrokeStyle(lineWidth: self.model.stripeWidth) - ) - .mask { - Path { maskPath in - maskPath.addArc( - center: self.model.center, - radius: self.model.radius, - startAngle: .radians(0), - endAngle: .radians(2 * .pi), - clockwise: false - ) - } - .trim( - from: self.model.stripedArcStart(for: self.progress), - to: self.model.stripedArcEnd(for: self.progress) - ) - .stroke( - style: StrokeStyle( - lineWidth: self.model.circularLineWidth, - lineCap: .round - ) - ) - } - .rotationEffect(.degrees(-90)) - } -} - -// MARK: - Helpers - -struct StripesShapeCircularProgress: Shape, @unchecked Sendable { - var model: CircularProgressVM - - func path(in rect: CGRect) -> Path { - self.model.stripesPath(in: rect) - } } diff --git a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift index e910e623..3edec8c0 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift @@ -21,18 +21,12 @@ open class UKCircularProgress: UIView, UKComponent { // MARK: - Subviews - /// The shape layer responsible for rendering the background of the circular progress indicator in a light style. + /// The shape layer responsible for rendering the background of the circular progress indicator. public let backgroundLayer = CAShapeLayer() /// The shape layer responsible for rendering the progress arc of the circular progress indicator. public let progressLayer = CAShapeLayer() - /// The shape layer responsible for rendering the striped effect in the circular progress indicator. - public let stripesLayer = CAShapeLayer() - - /// The shape layer that acts as a mask for `stripesLayer`, ensuring it has the intended shape. - public let stripesMaskLayer = CAShapeLayer() - /// The label used to display text inside the circular progress indicator. public let label = UILabel() @@ -69,24 +63,16 @@ open class UKCircularProgress: UIView, UKComponent { private func setup() { self.layer.addSublayer(self.backgroundLayer) - self.layer.addSublayer(self.stripesLayer) self.layer.addSublayer(self.progressLayer) self.addSubview(self.label) - self.stripesLayer.mask = self.stripesMaskLayer - if #available(iOS 17.0, *) { self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in view.handleTraitChanges() } } - let progress = self.model.progress(for: self.currentValue) - self.progressLayer.strokeEnd = progress - if !self.model.isStripesLayerHidden { - self.stripesMaskLayer.strokeStart = self.model.stripedArcStart(for: progress) - self.stripesMaskLayer.strokeEnd = self.model.stripedArcEnd(for: progress) - } + self.progressLayer.strokeEnd = self.model.progress(for: self.currentValue) self.label.text = self.model.label } @@ -96,8 +82,6 @@ open class UKCircularProgress: UIView, UKComponent { Self.Style.backgroundLayer(self.backgroundLayer, model: self.model) Self.Style.progressLayer(self.progressLayer, model: self.model) Self.Style.label(self.label, model: self.model) - Self.Style.stripesLayer(self.stripesLayer, model: self.model) - Self.Style.stripesMaskLayer(self.stripesMaskLayer, model: self.model) } // MARK: - Update @@ -105,7 +89,6 @@ open class UKCircularProgress: UIView, UKComponent { public func update(_ oldModel: CircularProgressVM) { guard self.model != oldModel else { return } self.style() - self.updateShapePaths() if self.model.shouldUpdateText(oldModel) { UIView.transition( @@ -121,6 +104,9 @@ open class UKCircularProgress: UIView, UKComponent { if self.model.shouldRecalculateProgress(oldModel) { self.updateProgress() } + if self.model.shouldUpdateShape(oldModel) { + self.updateShapePaths() + } if self.model.shouldInvalidateIntrinsicContentSize(oldModel) { self.invalidateIntrinsicContentSize() } @@ -128,31 +114,25 @@ open class UKCircularProgress: UIView, UKComponent { private func updateShapePaths() { let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY) + let minSide = min(self.bounds.width, self.bounds.height) + let radius = (minSide - self.model.circularLineWidth) / 2 let circlePath = UIBezierPath( arcCenter: center, - radius: self.model.radius, - startAngle: -CGFloat.pi / 2, - endAngle: -CGFloat.pi / 2 + 2 * .pi, + radius: radius, + startAngle: self.model.startAngle, + endAngle: self.model.endAngle, clockwise: true ) self.backgroundLayer.path = circlePath.cgPath self.progressLayer.path = circlePath.cgPath - self.stripesMaskLayer.path = circlePath.cgPath - self.stripesLayer.path = self.model.stripesBezierPath(in: self.bounds).cgPath } private func updateProgress() { - let progress = self.model.progress(for: self.currentValue) - CATransaction.begin() CATransaction.setAnimationDuration(self.model.animationDuration) CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .linear)) - self.progressLayer.strokeEnd = progress - if !self.model.isStripesLayerHidden { - self.stripesMaskLayer.strokeStart = self.model.stripedArcStart(for: progress) - self.stripesMaskLayer.strokeEnd = self.model.stripedArcEnd(for: progress) - } + self.progressLayer.strokeEnd = self.model.progress(for: self.currentValue) CATransaction.commit() } @@ -167,8 +147,6 @@ open class UKCircularProgress: UIView, UKComponent { self.backgroundLayer.frame = self.bounds self.progressLayer.frame = self.bounds - self.stripesLayer.frame = self.bounds - self.stripesMaskLayer.frame = self.bounds self.updateShapePaths() } @@ -191,8 +169,6 @@ open class UKCircularProgress: UIView, UKComponent { private func handleTraitChanges() { Self.Style.backgroundLayer(self.backgroundLayer, model: self.model) Self.Style.progressLayer(self.progressLayer, model: self.model) - Self.Style.stripesLayer(self.stripesLayer, model: self.model) - Self.Style.stripesMaskLayer(self.stripesMaskLayer, model: self.model) } } @@ -205,10 +181,9 @@ extension UKCircularProgress { model: CircularProgressVM ) { layer.fillColor = UIColor.clear.cgColor - layer.strokeColor = model.color.background.uiColor.cgColor + layer.strokeColor = model.color.background.cgColor layer.lineCap = .round layer.lineWidth = model.circularLineWidth - layer.isHidden = model.isBackgroundLayerHidden } static func progressLayer( @@ -226,29 +201,8 @@ extension UKCircularProgress { model: CircularProgressVM ) { label.textAlignment = .center - label.adjustsFontSizeToFitWidth = true - label.minimumScaleFactor = 0.5 label.font = model.titleFont.uiFont label.textColor = model.color.main.uiColor } - - static func stripesLayer( - _ layer: CAShapeLayer, - model: CircularProgressVM - ) { - layer.isHidden = model.isStripesLayerHidden - layer.strokeColor = model.color.main.uiColor.cgColor - layer.lineWidth = model.stripeWidth - } - - static func stripesMaskLayer( - _ layer: CAShapeLayer, - model: CircularProgressVM - ) { - layer.fillColor = UIColor.clear.cgColor - layer.strokeColor = model.color.background.uiColor.cgColor - layer.lineCap = .round - layer.lineWidth = model.circularLineWidth - } } } From f1f5144f4b1746b1bab3cd32de5edcd06c09087a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 12:45:55 +0200 Subject: [PATCH 35/48] add `lineCap` param to circular progress --- .../CircularProgressPreview.swift | 7 ++- .../Models/CircularProgressLineCap.swift | 44 +++++++++++++++++++ .../Models/CircularProgressShape.swift | 2 + .../Models/CircularProgressVM.swift | 3 ++ .../CircularProgress/SUCircularProgress.swift | 4 +- .../CircularProgress/UKCircularProgress.swift | 4 +- 6 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift index 39734ec7..8acbcf61 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift @@ -37,17 +37,22 @@ struct CircularProgressPreview: View { Form { ComponentColorPicker(selection: self.$model.color) CaptionFontPicker(selection: self.$model.font) + Picker("Line Cap", selection: self.$model.lineCap) { + Text("Butt").tag(CircularProgressVM.LineCap.butt) + Text("Rounded").tag(CircularProgressVM.LineCap.rounded) + Text("Square").tag(CircularProgressVM.LineCap.square) + } Picker("Line Width", selection: self.$model.lineWidth) { Text("Default").tag(Optional.none) Text("2").tag(Optional.some(2)) Text("4").tag(Optional.some(4)) Text("8").tag(Optional.some(8)) } - SizePicker(selection: self.$model.size) Picker("Shape", selection: self.$model.shape) { Text("Circle").tag(CircularProgressVM.Shape.circle) Text("Arc").tag(CircularProgressVM.Shape.arc) } + SizePicker(selection: self.$model.size) } .onReceive(self.timer) { _ in if self.currentValue < self.model.maxValue { diff --git a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift new file mode 100644 index 00000000..078c49a7 --- /dev/null +++ b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift @@ -0,0 +1,44 @@ +import SwiftUI +import UIKit + +extension CircularProgressVM { + /// Defines the style of line endings. + public enum LineCap { + /// The line ends exactly at the endpoint with a flat edge. + case butt + /// The line ends with a semicircular arc that extends beyond the endpoint, creating a rounded appearance. + case rounded + /// The line ends with a square cap that extends beyond the endpoint by half the line width, similar to `butt` but with a protruding end. + case square + } +} + +// MARK: - UIKit Helpers + +extension CircularProgressVM.LineCap { + var shapeLayerLineCap: CAShapeLayerLineCap { + switch self { + case .butt: + return .butt + case .rounded: + return .round + case .square: + return .square + } + } +} + +// MARK: - SwiftUI Helpers + +extension CircularProgressVM.LineCap { + var cgLineCap: CGLineCap { + switch self { + case .butt: + return .butt + case .rounded: + return .round + case .square: + return .square + } + } +} diff --git a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressShape.swift b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressShape.swift index 586e7872..7c4e4e1c 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressShape.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressShape.swift @@ -3,7 +3,9 @@ import Foundation extension CircularProgressVM { /// Defines the shapes for the circular progress component. public enum Shape { + /// Renders a complete circle to represent the progress. case circle + /// Renders only a portion of the circle (an arc) to represent progress. case arc } } diff --git a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift index e145e5e5..768c487e 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift @@ -13,6 +13,9 @@ public struct CircularProgressVM: ComponentVM { /// An optional label to display inside the circular progress. public var label: String? + /// The style of line endings. + public var lineCap: LineCap = .rounded + /// The width of the circular progress stroke. public var lineWidth: CGFloat? diff --git a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift index 02597c4f..f4f258ec 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift @@ -46,7 +46,7 @@ public struct SUCircularProgress: View { self.model.color.background.color, style: StrokeStyle( lineWidth: self.model.circularLineWidth, - lineCap: .round + lineCap: self.model.lineCap.cgLineCap ) ) .frame( @@ -69,7 +69,7 @@ public struct SUCircularProgress: View { self.model.color.main.color, style: StrokeStyle( lineWidth: self.model.circularLineWidth, - lineCap: .round + lineCap: self.model.lineCap.cgLineCap ) ) .frame( diff --git a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift index 3edec8c0..8e82a701 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift @@ -182,7 +182,7 @@ extension UKCircularProgress { ) { layer.fillColor = UIColor.clear.cgColor layer.strokeColor = model.color.background.cgColor - layer.lineCap = .round + layer.lineCap = model.lineCap.shapeLayerLineCap layer.lineWidth = model.circularLineWidth } @@ -192,7 +192,7 @@ extension UKCircularProgress { ) { layer.fillColor = UIColor.clear.cgColor layer.strokeColor = model.color.main.uiColor.cgColor - layer.lineCap = .round + layer.lineCap = model.lineCap.shapeLayerLineCap layer.lineWidth = model.circularLineWidth } From 8d4a46df68919cec299c6111e5a33c6feae9873c Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 12:48:02 +0200 Subject: [PATCH 36/48] update initial model for circular progress in the demos --- .../ComponentsPreview/PreviewPages/CircularProgressPreview.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift index 8acbcf61..059273f1 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift @@ -77,7 +77,6 @@ struct CircularProgressPreview: View { private static var initialModel = CircularProgressVM { $0.label = "0%" - $0.shape = .arc } } From 57bb363aa0475c9135145a8bc89f2514679c80a8 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 12:57:49 +0200 Subject: [PATCH 37/48] improve docs --- .../Components/CircularProgress/SUCircularProgress.swift | 2 +- .../Components/CircularProgress/UKCircularProgress.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift index f4f258ec..993c4dee 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift @@ -1,6 +1,6 @@ import SwiftUI -/// A SwiftUI component that displays a circular progress. +/// A SwiftUI component that displays the progress of a task or operation in a circular form. public struct SUCircularProgress: View { // MARK: - Properties diff --git a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift index 8e82a701..0aeb8858 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift @@ -1,7 +1,7 @@ import AutoLayout import UIKit -/// A UIKit component that displays a circular progress indicator. +/// A UIKit component that displays the progress of a task or operation in a circular form. open class UKCircularProgress: UIView, UKComponent { // MARK: - Properties From 9f550313ea8c32aa6824fdaf2df564a0d4ad5d0a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 13:14:54 +0200 Subject: [PATCH 38/48] improve docs for `UKCircularProgress` --- .../Components/CircularProgress/UKCircularProgress.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift index 0aeb8858..e68f5001 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift @@ -21,13 +21,13 @@ open class UKCircularProgress: UIView, UKComponent { // MARK: - Subviews - /// The shape layer responsible for rendering the background of the circular progress indicator. + /// The shape layer responsible for rendering the background. public let backgroundLayer = CAShapeLayer() - /// The shape layer responsible for rendering the progress arc of the circular progress indicator. + /// The shape layer responsible for rendering the progress arc. public let progressLayer = CAShapeLayer() - /// The label used to display text inside the circular progress indicator. + /// The label used to display text. public let label = UILabel() // MARK: - UIView Properties From a776584be8438dd98d681084462ad459a7e2a653 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 13:32:07 +0200 Subject: [PATCH 39/48] improve countdown docs --- .../Countdown/Models/CountdownVM.swift | 38 +++++++++---------- .../Components/Countdown/SUCountdown.swift | 2 +- .../Components/Countdown/UKCountdown.swift | 4 +- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Sources/ComponentsKit/Components/Countdown/Models/CountdownVM.swift b/Sources/ComponentsKit/Components/Countdown/Models/CountdownVM.swift index ea7db580..4c780a01 100644 --- a/Sources/ComponentsKit/Components/Countdown/Models/CountdownVM.swift +++ b/Sources/ComponentsKit/Components/Countdown/Models/CountdownVM.swift @@ -5,6 +5,25 @@ public struct CountdownVM: ComponentVM { /// The color of the countdown. public var color: ComponentColor? + /// The locale used for localizing the countdown. + public var locale: Locale = .current + + /// A dictionary containing localized representations of time units (days, hours, minutes, seconds) for various locales. + /// + /// This property can be used to override the default localizations for supported languages or to add + /// localizations for unsupported languages. By default, the library provides strings for the following locales: + /// - English ("en") + /// - Spanish ("es") + /// - French ("fr") + /// - German ("de") + /// - Chinese ("zh") + /// - Japanese ("ja") + /// - Russian ("ru") + /// - Arabic ("ar") + /// - Hindi ("hi") + /// - Portuguese ("pt") + public var localization: [Locale: UnitsLocalization] = [:] + /// The font used for displaying the countdown numbers and trailing units. public var mainFont: UniversalFont? @@ -29,25 +48,6 @@ public struct CountdownVM: ComponentVM { /// The target date until which the countdown runs. public var until: Date = Date().addingTimeInterval(3600 * 85) - /// The locale used for localizing the countdown. - public var locale: Locale = .current - - /// A dictionary containing localized representations of time units (days, hours, minutes, seconds) for various locales. - /// - /// This property can be used to override the default localizations for supported languages or to add - /// localizations for unsupported languages. By default, the library provides strings for the following locales: - /// - English ("en") - /// - Spanish ("es") - /// - French ("fr") - /// - German ("de") - /// - Chinese ("zh") - /// - Japanese ("ja") - /// - Russian ("ru") - /// - Arabic ("ar") - /// - Hindi ("hi") - /// - Portuguese ("pt") - public var localization: [Locale: UnitsLocalization] = [:] - /// Initializes a new instance of `CountdownVM` with default values. public init() {} } diff --git a/Sources/ComponentsKit/Components/Countdown/SUCountdown.swift b/Sources/ComponentsKit/Components/Countdown/SUCountdown.swift index 2830f296..5076f54e 100644 --- a/Sources/ComponentsKit/Components/Countdown/SUCountdown.swift +++ b/Sources/ComponentsKit/Components/Countdown/SUCountdown.swift @@ -1,6 +1,6 @@ import SwiftUI -/// A SwiftUI component that displays a countdown. +/// A SwiftUI timer component that counts down from a specified duration to zero. public struct SUCountdown: View { // MARK: - Properties diff --git a/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift b/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift index ad60f58b..e45df5c5 100644 --- a/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift +++ b/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift @@ -2,7 +2,7 @@ import AutoLayout import Combine import UIKit -/// A UIKit component that displays a countdown. +/// A UIKit timer component that counts down from a specified duration to zero. public class UKCountdown: UIView, UKComponent { // MARK: - Public Properties @@ -13,6 +13,8 @@ public class UKCountdown: UIView, UKComponent { } } + // MARK: - Subviews + /// The main container stack view containing all time labels and colon labels. public let stackView = UIStackView() From 01e29b3e9f0d9604eb89b31efa1e6590240b51e0 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 14:40:07 +0200 Subject: [PATCH 40/48] remove `butt` line cap --- .../PreviewPages/CircularProgressPreview.swift | 1 - .../Models/CircularProgressLineCap.swift | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift index 059273f1..436dcb68 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift @@ -38,7 +38,6 @@ struct CircularProgressPreview: View { ComponentColorPicker(selection: self.$model.color) CaptionFontPicker(selection: self.$model.font) Picker("Line Cap", selection: self.$model.lineCap) { - Text("Butt").tag(CircularProgressVM.LineCap.butt) Text("Rounded").tag(CircularProgressVM.LineCap.rounded) Text("Square").tag(CircularProgressVM.LineCap.square) } diff --git a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift index 078c49a7..72a86b8c 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift @@ -4,11 +4,9 @@ import UIKit extension CircularProgressVM { /// Defines the style of line endings. public enum LineCap { - /// The line ends exactly at the endpoint with a flat edge. - case butt /// The line ends with a semicircular arc that extends beyond the endpoint, creating a rounded appearance. case rounded - /// The line ends with a square cap that extends beyond the endpoint by half the line width, similar to `butt` but with a protruding end. + /// The line ends exactly at the endpoint with a flat edge. case square } } @@ -18,12 +16,10 @@ extension CircularProgressVM { extension CircularProgressVM.LineCap { var shapeLayerLineCap: CAShapeLayerLineCap { switch self { - case .butt: - return .butt case .rounded: return .round case .square: - return .square + return .butt } } } @@ -33,12 +29,10 @@ extension CircularProgressVM.LineCap { extension CircularProgressVM.LineCap { var cgLineCap: CGLineCap { switch self { - case .butt: - return .butt case .rounded: return .round case .square: - return .square + return .butt } } } From 080603c8821d52e4c863dee73ac04d55b5952f84 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 15:41:39 +0200 Subject: [PATCH 41/48] fix typo in the input field docs --- .../Components/InputField/Models/InputFieldVM.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift index dd339bb1..7ffd3b73 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -18,7 +18,7 @@ public struct InputFieldVM: ComponentVM { /// The font used for the input field's text. /// - /// If not provided, the font is automatically calculated based on the checkbox's size. + /// If not provided, the font is automatically calculated based on the input field's size. public var font: UniversalFont? /// A Boolean value indicating whether autocorrection is enabled for the input field. From 4a318073ff9047eb78dcdd7002412634135062ab Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 16:14:04 +0200 Subject: [PATCH 42/48] add `currentValue` to `ProgressBarVM` --- .../PreviewPages/ProgressBarPreview.swift | 23 +++++-------- .../ProgressBar/Models/ProgressBarVM.swift | 32 ++++++++++++------- .../ProgressBar/SUProgressBar.swift | 16 ++++++++-- .../ProgressBar/UKProgressBar.swift | 25 +++++++++++---- 4 files changed, 61 insertions(+), 35 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ProgressBarPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ProgressBarPreview.swift index c7b5133e..b01b6726 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ProgressBarPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ProgressBarPreview.swift @@ -4,10 +4,9 @@ import UIKit struct ProgressBarPreview: View { @State private var model = Self.initialModel - @State private var currentValue: CGFloat = Self.initialValue - - private let progressBar = UKProgressBar(initialValue: Self.initialValue, model: Self.initialModel) - + + private let progressBar = UKProgressBar(model: Self.initialModel) + private let timer = Timer .publish(every: 0.5, on: .main, in: .common) .autoconnect() @@ -18,7 +17,6 @@ struct ProgressBarPreview: View { self.progressBar .preview .onAppear { - self.progressBar.currentValue = self.currentValue self.progressBar.model = Self.initialModel } .onChange(of: self.model) { newValue in @@ -26,7 +24,7 @@ struct ProgressBarPreview: View { } } PreviewWrapper(title: "SwiftUI") { - SUProgressBar(currentValue: self.currentValue, model: self.model) + SUProgressBar(model: self.model) } Form { ComponentColorPicker(selection: self.$model.color) @@ -42,25 +40,20 @@ struct ProgressBarPreview: View { } } .onReceive(self.timer) { _ in - if self.currentValue < self.model.maxValue { + if self.model.currentValue < self.model.maxValue { let step = (self.model.maxValue - self.model.minValue) / 100 - self.currentValue = min( + self.model.currentValue = min( self.model.maxValue, - self.currentValue + CGFloat(Int.random(in: 1...20)) * step + self.model.currentValue + CGFloat(Int.random(in: 1...20)) * step ) } else { - self.currentValue = self.model.minValue + self.model.currentValue = self.model.minValue } - - self.progressBar.currentValue = self.currentValue } } // MARK: - Helpers - private static var initialValue: Double { - return 0.0 - } private static var initialModel: ProgressBarVM { return .init() } diff --git a/Sources/ComponentsKit/Components/ProgressBar/Models/ProgressBarVM.swift b/Sources/ComponentsKit/Components/ProgressBar/Models/ProgressBarVM.swift index 902293e9..f5bb2e47 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/Models/ProgressBarVM.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/Models/ProgressBarVM.swift @@ -7,26 +7,29 @@ public struct ProgressBarVM: ComponentVM { /// Defaults to `.accent`. public var color: ComponentColor = .accent - /// The visual style of the progress bar component. - /// - /// Defaults to `.striped`. - public var style: Style = .striped - - /// The size of the progress bar. + /// The corner radius of the progress bar. /// /// Defaults to `.medium`. - public var size: ComponentSize = .medium + public var cornerRadius: ComponentRadius = .medium - /// The minimum value of the progress bar. - public var minValue: CGFloat = 0 + /// The current value of the progress bar. + public var currentValue: CGFloat = 0 /// The maximum value of the progress bar. public var maxValue: CGFloat = 100 - /// The corner radius of the progress bar. + /// The minimum value of the progress bar. + public var minValue: CGFloat = 0 + + /// The size of the progress bar. /// /// Defaults to `.medium`. - public var cornerRadius: ComponentRadius = .medium + public var size: ComponentSize = .medium + + /// The visual style of the progress bar component. + /// + /// Defaults to `.striped`. + public var style: Style = .striped /// Initializes a new instance of `ProgressBarVM` with default values. public init() {} @@ -139,6 +142,13 @@ extension ProgressBarVM { } extension ProgressBarVM { + var progress: CGFloat { + let range = self.maxValue - self.minValue + guard range > 0 else { return 0 } + let normalized = (self.currentValue - self.minValue) / range + return max(0, min(1, normalized)) + } + func progress(for currentValue: CGFloat) -> CGFloat { let range = self.maxValue - self.minValue guard range > 0 else { return 0 } diff --git a/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift b/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift index 8da78bd7..01652176 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift @@ -1,16 +1,16 @@ import SwiftUI -/// A SwiftUI component that displays a progress bar. +/// A SwiftUI component that visually represents the progress of a task or process using a horizontal bar. public struct SUProgressBar: View { // MARK: - Properties /// A model that defines the appearance properties. public var model: ProgressBarVM /// The current progress value. - public var currentValue: CGFloat + public var currentValue: CGFloat? private var progress: CGFloat { - self.model.progress(for: self.currentValue) + self.currentValue.map { self.model.progress(for: $0) } ?? self.model.progress } // MARK: - Initializer @@ -19,6 +19,7 @@ public struct SUProgressBar: View { /// - Parameters: /// - currentValue: The current progress value. /// - model: A model that defines the appearance properties. + @available(*, deprecated, message: "Set `currentValue` in the model instead.") public init( currentValue: CGFloat, model: ProgressBarVM = .init() @@ -27,6 +28,15 @@ public struct SUProgressBar: View { self.model = model } + /// Initializer. + /// - Parameters: + /// - model: A model that defines the appearance properties. + public init( + model: ProgressBarVM = .init() + ) { + self.model = model + } + // MARK: - Body public var body: some View { diff --git a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift index 7fcbf095..2c956482 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift @@ -1,9 +1,9 @@ import AutoLayout import UIKit -/// A UIKit component that displays a progress bar. +/// A UIKit component that visually represents the progress of a task or process using a horizontal bar. open class UKProgressBar: UIView, UKComponent { - // MARK: - Properties + // MARK: - Public Properties /// A model that defines the appearance properties. public var model: ProgressBarVM { @@ -13,7 +13,7 @@ open class UKProgressBar: UIView, UKComponent { } /// The current progress value for the progress bar. - public var currentValue: CGFloat { + public var currentValue: CGFloat? { didSet { self.updateProgressWidthAndAppearance() } @@ -39,7 +39,7 @@ open class UKProgressBar: UIView, UKComponent { // MARK: - Private Properties private var progress: CGFloat { - self.model.progress(for: self.currentValue) + self.currentValue.map { self.model.progress(for: $0) } ?? self.model.progress } // MARK: - UIView Properties @@ -54,6 +54,7 @@ open class UKProgressBar: UIView, UKComponent { /// - Parameters: /// - initialValue: The initial progress value. Defaults to `0`. /// - model: A model that defines the appearance properties. + @available(*, deprecated, message: "Set `currentValue` in the model instead.") public init( initialValue: CGFloat = 0, model: ProgressBarVM = .init() @@ -67,6 +68,18 @@ open class UKProgressBar: UIView, UKComponent { self.layout() } + /// Initializer. + /// - Parameters: + /// - model: A model that defines the appearance properties. + public init(model: ProgressBarVM = .init()) { + self.model = model + super.init(frame: .zero) + + self.setup() + self.style() + self.layout() + } + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -138,9 +151,9 @@ open class UKProgressBar: UIView, UKComponent { self.setNeedsLayout() } - UIView.performWithoutAnimation { +// UIView.performWithoutAnimation { self.updateProgressWidthAndAppearance() - } +// } } private func updateProgressWidthAndAppearance() { From c7bcd738771f4a8d8981c11beef4cd6a850addc9 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 16:22:07 +0200 Subject: [PATCH 43/48] add `currentValue` to `CircularProgressVM` --- .../CircularProgressPreview.swift | 27 ++++++------------- .../Models/CircularProgressVM.swift | 13 +++++++++ .../CircularProgress/SUCircularProgress.swift | 12 +++++++-- .../CircularProgress/UKCircularProgress.swift | 23 +++++++++++++--- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift index 436dcb68..ef98cd9d 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift @@ -4,12 +4,8 @@ import UIKit struct CircularProgressPreview: View { @State private var model = Self.initialModel - @State private var currentValue: CGFloat = Self.initialValue - private let circularProgress = UKCircularProgress( - initialValue: Self.initialValue, - model: Self.initialModel - ) + private let circularProgress = UKCircularProgress(model: Self.initialModel) private let timer = Timer .publish(every: 0.5, on: .main, in: .common) @@ -21,18 +17,14 @@ struct CircularProgressPreview: View { self.circularProgress .preview .onAppear { - self.circularProgress.currentValue = Self.initialValue self.circularProgress.model = Self.initialModel } .onChange(of: model) { newModel in self.circularProgress.model = newModel } - .onChange(of: self.currentValue) { newValue in - self.circularProgress.currentValue = newValue - } } PreviewWrapper(title: "SwiftUI") { - SUCircularProgress(currentValue: self.currentValue, model: self.model) + SUCircularProgress(model: self.model) } Form { ComponentColorPicker(selection: self.$model.color) @@ -54,28 +46,25 @@ struct CircularProgressPreview: View { SizePicker(selection: self.$model.size) } .onReceive(self.timer) { _ in - if self.currentValue < self.model.maxValue { + if self.model.currentValue < self.model.maxValue { let step = (self.model.maxValue - self.model.minValue) / 100 - self.currentValue = min( + self.model.currentValue = min( self.model.maxValue, - self.currentValue + CGFloat(Int.random(in: 1...20)) * step + self.model.currentValue + CGFloat(Int.random(in: 1...20)) * step ) } else { - self.currentValue = self.model.minValue + self.model.currentValue = self.model.minValue } - self.model.label = "\(Int(self.currentValue))%" + self.model.label = "\(Int(self.model.currentValue))%" } } } // MARK: - Helpers - private static var initialValue: Double { - return 0.0 - } - private static var initialModel = CircularProgressVM { $0.label = "0%" + $0.currentValue = 0.0 } } diff --git a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift index 768c487e..3422d75b 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressVM.swift @@ -7,6 +7,11 @@ public struct CircularProgressVM: ComponentVM { /// Defaults to `.accent`. public var color: ComponentColor = .accent + /// The current value of the circular progress. + /// + /// Defaults to `0`. + public var currentValue: CGFloat = 0 + /// The font used for the circular progress label text. public var font: UniversalFont? @@ -103,6 +108,13 @@ extension CircularProgressVM { } extension CircularProgressVM { + var progress: CGFloat { + let range = self.maxValue - self.minValue + guard range > 0 else { return 0 } + let normalized = (self.currentValue - self.minValue) / range + return max(0, min(1, normalized)) + } + func progress(for currentValue: CGFloat) -> CGFloat { let range = self.maxValue - self.minValue guard range > 0 else { return 0 } @@ -123,6 +135,7 @@ extension CircularProgressVM { func shouldRecalculateProgress(_ oldModel: Self) -> Bool { return self.minValue != oldModel.minValue || self.maxValue != oldModel.maxValue + || self.currentValue != oldModel.currentValue } func shouldUpdateShape(_ oldModel: Self) -> Bool { return self.shape != oldModel.shape diff --git a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift index 993c4dee..f8a3f325 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift @@ -8,10 +8,10 @@ public struct SUCircularProgress: View { public var model: CircularProgressVM /// The current progress value. - public var currentValue: CGFloat + public var currentValue: CGFloat? private var progress: CGFloat { - self.model.progress(for: self.currentValue) + self.currentValue.map { self.model.progress(for: $0) } ?? self.model.progress } // MARK: - Initializer @@ -20,6 +20,7 @@ public struct SUCircularProgress: View { /// - Parameters: /// - currentValue: Current progress. /// - model: A model that defines the appearance properties. + @available(*, deprecated, message: "Set `currentValue` in the model instead.") public init( currentValue: CGFloat = 0, model: CircularProgressVM = .init() @@ -28,6 +29,13 @@ public struct SUCircularProgress: View { self.model = model } + /// Initializer. + /// - Parameters: + /// - model: A model that defines the appearance properties. + public init(model: CircularProgressVM = .init()) { + self.model = model + } + // MARK: - Body public var body: some View { diff --git a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift index e68f5001..fc993e60 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift @@ -13,12 +13,16 @@ open class UKCircularProgress: UIView, UKComponent { } /// The current progress value. - public var currentValue: CGFloat { + public var currentValue: CGFloat? { didSet { self.updateProgress() } } + private var progress: CGFloat { + self.currentValue.map { self.model.progress(for: $0) } ?? self.model.progress + } + // MARK: - Subviews /// The shape layer responsible for rendering the background. @@ -42,6 +46,7 @@ open class UKCircularProgress: UIView, UKComponent { /// - Parameters: /// - initialValue: The initial progress value. Defaults to `0`. /// - model: The model that defines the appearance properties. + @available(*, deprecated, message: "Set `currentValue` in the model instead.") public init( initialValue: CGFloat = 0, model: CircularProgressVM = .init() @@ -55,6 +60,18 @@ open class UKCircularProgress: UIView, UKComponent { self.layout() } + /// Initializer. + /// - Parameters: + /// - model: The model that defines the appearance properties. + public init(model: CircularProgressVM = .init()) { + self.model = model + super.init(frame: .zero) + + self.setup() + self.style() + self.layout() + } + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -72,7 +89,7 @@ open class UKCircularProgress: UIView, UKComponent { } } - self.progressLayer.strokeEnd = self.model.progress(for: self.currentValue) + self.progressLayer.strokeEnd = self.progress self.label.text = self.model.label } @@ -132,7 +149,7 @@ open class UKCircularProgress: UIView, UKComponent { CATransaction.begin() CATransaction.setAnimationDuration(self.model.animationDuration) CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .linear)) - self.progressLayer.strokeEnd = self.model.progress(for: self.currentValue) + self.progressLayer.strokeEnd = self.progress CATransaction.commit() } From c76c41469aba2e6c1dfffc1c1f0fd0b58c931f33 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 2 Apr 2025 16:37:05 +0200 Subject: [PATCH 44/48] remove default values from progress components' inits --- .../Components/CircularProgress/SUCircularProgress.swift | 2 +- .../Components/CircularProgress/UKCircularProgress.swift | 2 +- .../ComponentsKit/Components/ProgressBar/SUProgressBar.swift | 4 +--- .../ComponentsKit/Components/ProgressBar/UKProgressBar.swift | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift index f8a3f325..392e4438 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift @@ -32,7 +32,7 @@ public struct SUCircularProgress: View { /// Initializer. /// - Parameters: /// - model: A model that defines the appearance properties. - public init(model: CircularProgressVM = .init()) { + public init(model: CircularProgressVM) { self.model = model } diff --git a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift index fc993e60..193d2a11 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift @@ -63,7 +63,7 @@ open class UKCircularProgress: UIView, UKComponent { /// Initializer. /// - Parameters: /// - model: The model that defines the appearance properties. - public init(model: CircularProgressVM = .init()) { + public init(model: CircularProgressVM) { self.model = model super.init(frame: .zero) diff --git a/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift b/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift index 01652176..96a1e0cc 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift @@ -31,9 +31,7 @@ public struct SUProgressBar: View { /// Initializer. /// - Parameters: /// - model: A model that defines the appearance properties. - public init( - model: ProgressBarVM = .init() - ) { + public init(model: ProgressBarVM) { self.model = model } diff --git a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift index 2c956482..28943e6b 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift @@ -71,7 +71,7 @@ open class UKProgressBar: UIView, UKComponent { /// Initializer. /// - Parameters: /// - model: A model that defines the appearance properties. - public init(model: ProgressBarVM = .init()) { + public init(model: ProgressBarVM) { self.model = model super.init(frame: .zero) From 774b97629b3ba5eecfddc8aea2d5314c0e041350 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 3 Apr 2025 12:23:50 +0200 Subject: [PATCH 45/48] improve radio group docs --- .../Components/RadioGroup/Models/RadioGroupVM.swift | 2 +- .../Components/RadioGroup/Models/RadioItemVM.swift | 2 +- .../Components/RadioGroup/UIKit/UKRadioGroup.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift b/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift index bc985340..a437f058 100644 --- a/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift +++ b/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift @@ -1,7 +1,7 @@ import Foundation import UIKit -/// A model that defines the appearance of a radio group component. +/// A model that defines the appearance properties for a radio group component. public struct RadioGroupVM: ComponentVM { /// The scaling factor for the button's press animation, with a value between 0 and 1. /// diff --git a/Sources/ComponentsKit/Components/RadioGroup/Models/RadioItemVM.swift b/Sources/ComponentsKit/Components/RadioGroup/Models/RadioItemVM.swift index 17efd00d..ebbe1ba4 100644 --- a/Sources/ComponentsKit/Components/RadioGroup/Models/RadioItemVM.swift +++ b/Sources/ComponentsKit/Components/RadioGroup/Models/RadioItemVM.swift @@ -1,6 +1,6 @@ import Foundation -/// A model that defines the appearance properties for an item in a radio group. +/// A model that defines the data and appearance properties for an item in a radio group. public struct RadioItemVM { /// The unique identifier for the radio item. public var id: ID diff --git a/Sources/ComponentsKit/Components/RadioGroup/UIKit/UKRadioGroup.swift b/Sources/ComponentsKit/Components/RadioGroup/UIKit/UKRadioGroup.swift index 3ee1a78e..229f4cba 100644 --- a/Sources/ComponentsKit/Components/RadioGroup/UIKit/UKRadioGroup.swift +++ b/Sources/ComponentsKit/Components/RadioGroup/UIKit/UKRadioGroup.swift @@ -15,7 +15,7 @@ open class UKRadioGroup: UIView, UKComponent, UIGestureRecognizerD } } - /// An identifier of the selected segment. + /// An identifier of the selected item. public var selectedId: ID? { didSet { guard self.selectedId != oldValue else { return } From b4fbd90c49e37c5ecf940b656657c70ffd7f14bc Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 3 Apr 2025 12:38:14 +0200 Subject: [PATCH 46/48] improve segmented control docs --- .../Components/RadioGroup/Models/RadioGroupVM.swift | 2 +- .../SegmentedControl/Models/SegmentedControlItemVM.swift | 2 +- .../SegmentedControl/Models/SegmentedControlVM.swift | 4 ++-- .../Components/SegmentedControl/SUSegmentedControl.swift | 2 +- .../Components/SegmentedControl/UKSegmentedControl.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift b/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift index a437f058..5021a771 100644 --- a/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift +++ b/Sources/ComponentsKit/Components/RadioGroup/Models/RadioGroupVM.swift @@ -1,7 +1,7 @@ import Foundation import UIKit -/// A model that defines the appearance properties for a radio group component. +/// A model that defines the data and appearance properties for a radio group component. public struct RadioGroupVM: ComponentVM { /// The scaling factor for the button's press animation, with a value between 0 and 1. /// diff --git a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlItemVM.swift b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlItemVM.swift index cae5b40c..b51f9f4b 100644 --- a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlItemVM.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlItemVM.swift @@ -1,6 +1,6 @@ import Foundation -/// A model that defines the appearance properties for an item in a segmented control. +/// A model that defines the data and appearance properties for an item in a segmented control. public struct SegmentedControlItemVM: Updatable { /// The unique identifier for the segmented control item. public var id: ID diff --git a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift index 876e41aa..8f0f8617 100644 --- a/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/Models/SegmentedControlVM.swift @@ -1,7 +1,7 @@ import SwiftUI import UIKit -/// A model that defines the appearance properties for a segmented control component. +/// A model that defines the data and appearance properties for a segmented control component. public struct SegmentedControlVM: ComponentVM { /// The color of the segmented control. public var color: ComponentColor? @@ -19,7 +19,7 @@ public struct SegmentedControlVM: ComponentVM { /// Defaults to `true`. public var isEnabled: Bool = true - /// A Boolean value indicating whether the segmented control should take the full width of its superview. + /// A Boolean value indicating whether the segmented control should take the full width of its parent view. /// /// Defaults to `false`. public var isFullWidth: Bool = false diff --git a/Sources/ComponentsKit/Components/SegmentedControl/SUSegmentedControl.swift b/Sources/ComponentsKit/Components/SegmentedControl/SUSegmentedControl.swift index 12e068e2..b001cae5 100644 --- a/Sources/ComponentsKit/Components/SegmentedControl/SUSegmentedControl.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/SUSegmentedControl.swift @@ -1,6 +1,6 @@ import SwiftUI -/// A SwiftUI component with multiple segments that allows users to select them. +/// A SwiftUI component that allows users to choose between multiple segments or options. public struct SUSegmentedControl: View { // MARK: Properties diff --git a/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift b/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift index 0b47d10e..f759f3da 100644 --- a/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift @@ -1,7 +1,7 @@ import AutoLayout import UIKit -/// A UIKit component with multiple segments that allows users to select them. +/// A UIKit component that allows users to choose between multiple segments or options. open class UKSegmentedControl: UIView, UKComponent { // MARK: Properties From e1add4f4a9cbfe07c34170d8fb4667b369b80f42 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 3 Apr 2025 12:52:38 +0200 Subject: [PATCH 47/48] improve slider docs --- .../Components/Slider/Models/SliderVM.swift | 26 +++++++++---------- .../Components/Slider/SUSlider.swift | 2 +- .../Components/Slider/UKSlider.swift | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift index beb271b9..4384de5e 100644 --- a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -7,32 +7,32 @@ public struct SliderVM: ComponentVM { /// Defaults to `.accent`. public var color: ComponentColor = .accent - /// The visual style of the slider component. + /// The corner radius of the slider track and handle. /// - /// Defaults to `.light`. - public var style: Style = .light + /// Defaults to `.full`. + public var cornerRadius: ComponentRadius = .full - /// The size of the slider. - /// - /// Defaults to `.medium`. - public var size: ComponentSize = .medium + /// The maximum value of the slider. + public var maxValue: CGFloat = 100 /// The minimum value of the slider. public var minValue: CGFloat = 0 - /// The maximum value of the slider. - public var maxValue: CGFloat = 100 - - /// The corner radius of the slider track and handle. + /// The size of the slider. /// - /// Defaults to `.full`. - public var cornerRadius: ComponentRadius = .full + /// Defaults to `.medium`. + public var size: ComponentSize = .medium /// The step value for the slider. /// /// Defaults to `1`. public var step: CGFloat = 1 + /// The visual style of the slider component. + /// + /// Defaults to `.light`. + public var style: Style = .light + /// Initializes a new instance of `SliderVM` with default values. public init() {} } diff --git a/Sources/ComponentsKit/Components/Slider/SUSlider.swift b/Sources/ComponentsKit/Components/Slider/SUSlider.swift index 257a1c35..fbf43b16 100644 --- a/Sources/ComponentsKit/Components/Slider/SUSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/SUSlider.swift @@ -1,6 +1,6 @@ import SwiftUI -/// A SwiftUI component that displays a slider. +/// A SwiftUI component that lets users select a value from a range by dragging a thumb along a track. public struct SUSlider: View { // MARK: - Properties diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift index 81980197..1a715eb6 100644 --- a/Sources/ComponentsKit/Components/Slider/UKSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -1,7 +1,7 @@ import AutoLayout import UIKit -/// A UIKit component that displays a slider. +/// A UIKit component that lets users select a value from a range by dragging a thumb along a track. open class UKSlider: UIView, UKComponent { // MARK: - Properties From 30e9c69420a8d31def132949f9754e28f7773f7c Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 3 Apr 2025 13:46:35 +0200 Subject: [PATCH 48/48] fix typo in param name and docs --- .../ComponentsPreview/PreviewPages/BottomModalPreview.swift | 2 +- .../Components/Modal/Models/BottomModalVM.swift | 5 +++-- .../Components/Modal/Models/CenterModalVM.swift | 2 +- .../Components/Modal/SharedHelpers/ModalAnimation.swift | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/BottomModalPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/BottomModalPreview.swift index 88931092..fe441eca 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/BottomModalPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/BottomModalPreview.swift @@ -62,7 +62,7 @@ struct BottomModalPreview: View { footer: self.$contentFooter, additionalPickers: { Toggle("Draggable", isOn: self.$model.isDraggable) - Toggle("Hides On Swap", isOn: self.$model.hidesOnSwap) + Toggle("Hides On Swipe", isOn: self.$model.hidesOnSwipe) } ) } diff --git a/Sources/ComponentsKit/Components/Modal/Models/BottomModalVM.swift b/Sources/ComponentsKit/Components/Modal/Models/BottomModalVM.swift index b2fed090..e16a4f8a 100644 --- a/Sources/ComponentsKit/Components/Modal/Models/BottomModalVM.swift +++ b/Sources/ComponentsKit/Components/Modal/Models/BottomModalVM.swift @@ -31,11 +31,12 @@ public struct BottomModalVM: ModalVM { /// A Boolean value indicating whether the modal should hide when it is swiped down. /// /// Defaults to `true`. - public var hidesOnSwap: Bool = true + public var hidesOnSwipe: Bool = true /// A Boolean value indicating whether the modal is draggable. /// - /// If `true`, the modal can be dragged vertically. Defaults to `true`. + /// If `true`, the modal can be dragged vertically allowing the user to pull the modal up or down + /// to interact or dismiss it. Defaults to `true`. public var isDraggable: Bool = true /// The style of the overlay displayed behind the modal. diff --git a/Sources/ComponentsKit/Components/Modal/Models/CenterModalVM.swift b/Sources/ComponentsKit/Components/Modal/Models/CenterModalVM.swift index 17af179b..ca0e0b47 100644 --- a/Sources/ComponentsKit/Components/Modal/Models/CenterModalVM.swift +++ b/Sources/ComponentsKit/Components/Modal/Models/CenterModalVM.swift @@ -48,6 +48,6 @@ public struct CenterModalVM: ModalVM { /// Defaults to `.fast`. public var transition: ModalTransition = .fast - /// Initializes a new instance of `BottomModalVM` with default values. + /// Initializes a new instance of `CenterModalVM` with default values. public init() {} } diff --git a/Sources/ComponentsKit/Components/Modal/SharedHelpers/ModalAnimation.swift b/Sources/ComponentsKit/Components/Modal/SharedHelpers/ModalAnimation.swift index 190aba8d..cb279d26 100644 --- a/Sources/ComponentsKit/Components/Modal/SharedHelpers/ModalAnimation.swift +++ b/Sources/ComponentsKit/Components/Modal/SharedHelpers/ModalAnimation.swift @@ -10,7 +10,7 @@ enum ModalAnimation { static func bottomModalOffset(_ translation: CGFloat, model: BottomModalVM) -> CGFloat { if translation > 0 { - return model.hidesOnSwap + return model.hidesOnSwipe ? translation : (model.isDraggable ? Self.rubberBandClamp(translation) : 0) } else { @@ -26,7 +26,7 @@ enum ModalAnimation { velocity: CGFloat, model: BottomModalVM ) -> Bool { - guard model.hidesOnSwap else { + guard model.hidesOnSwipe else { return false }