From aedb812d2ab9c07beaa7772ea1554c2c8edafd6f Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Thu, 3 Apr 2025 14:01:42 +0300 Subject: [PATCH 01/60] SUButton new params --- .../PreviewPages/ButtonPreview.swift | 24 +++++++- .../Components/Button/Models/ButtonVM.swift | 57 +++++++++++++++++++ .../Components/Button/SUButton.swift | 55 ++++++++++++------ 3 files changed, 118 insertions(+), 18 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index df58ad2e..c2e3b6c3 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -6,7 +6,7 @@ struct ButtonPreview: View { @State private var model = ButtonVM { $0.title = "Button" } - + var body: some View { VStack { PreviewWrapper(title: "UIKit") { @@ -25,6 +25,18 @@ struct ButtonPreview: View { ButtonFontPicker(selection: self.$model.font) Toggle("Enabled", isOn: self.$model.isEnabled) Toggle("Full Width", isOn: self.$model.isFullWidth) + Picker("Image Source", selection: self.$model.imageSrc) { + Text("SF Symbol").tag(ButtonVM.ImageSource.sfSymbol("star.fill")) + Text("Local").tag(ButtonVM.ImageSource.local("avatar_placeholder")) + Text("None").tag(Optional.none) + } + if self.model.imageSrc != nil { + Picker("Image Location", selection: self.$model.imageLocation) { + Text("Leading").tag(ButtonVM.ImageLocation.leading) + Text("Trailing").tag(ButtonVM.ImageLocation.trailing) + } + } + Toggle("Loading", isOn: self.$model.isLoading) SizePicker(selection: self.$model.size) Picker("Style", selection: self.$model.style) { Text("Filled").tag(ButtonStyle.filled) @@ -34,6 +46,16 @@ struct ButtonPreview: View { Text("Bordered with medium border").tag(ButtonStyle.bordered(.medium)) Text("Bordered with large border").tag(ButtonStyle.bordered(.large)) } + .onChange(of: self.model.imageLocation) { _ in + if self.model.isLoading { + self.model.isLoading = false + } + } + .onChange(of: self.model.imageSrc) { _ in + if self.model.isLoading { + self.model.isLoading = false + } + } } } } diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 8212febf..52e4e7a9 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -1,3 +1,4 @@ +import SwiftUI import UIKit /// A model that defines the appearance properties for a button component. @@ -43,6 +44,29 @@ public struct ButtonVM: ComponentVM { /// Defaults to `.filled`. public var style: ButtonStyle = .filled + /// The loading VM used for the loading indicator. + /// + /// If not provided, a default loading view model is used. + public var loadingVM: LoadingVM? + + /// A Boolean value indicating whether the button is currently in a loading state. + /// + /// Defaults to `false`. + public var isLoading: Bool = false + + /// The source of the image to be displayed. + public var imageSrc: ImageSource? + + /// The position of the image relative to the button's title. + /// + /// Defaults to `.leading`. + public var imageLocation: ImageLocation = .leading + + /// The spacing between the button's title and its image or loading indicator. + /// + /// Defaults to `8.0`. + public var contentSpacing: CGFloat = 8.0 + /// Initializes a new instance of `ButtonVM` with default values. public init() {} } @@ -50,6 +74,15 @@ public struct ButtonVM: ComponentVM { // MARK: Shared Helpers extension ButtonVM { + var preferredLoadingVM: LoadingVM { + return self.loadingVM ?? .init { + $0.color = .init( + main: foregroundColor, + contrast: self.color?.main ?? .background + ) + $0.size = .small + } + } var backgroundColor: UniversalColor? { switch self.style { case .filled: @@ -121,6 +154,18 @@ extension ButtonVM { } } +extension ButtonVM { + public enum ImageSource: Hashable { + case sfSymbol(String) + case local(String, bundle: Bundle? = nil) + } + + public enum ImageLocation { + case leading + case trailing + } +} + // MARK: UIKit Helpers extension ButtonVM { @@ -155,3 +200,15 @@ extension ButtonVM { return self.isFullWidth ? 10_000 : nil } } + +extension ButtonVM { + var buttonImage: Image? { + guard let imageSrc = self.imageSrc else { return nil } + switch imageSrc { + case .sfSymbol(let name): + return Image(systemName: name) + case .local(let name, let bundle): + return Image(name, bundle: bundle) + } + } +} diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index d415a8ae..ffb30f7c 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -29,25 +29,46 @@ public struct SUButton: View { // MARK: Body public var body: some View { - Button(self.model.title, action: self.action) - .buttonStyle(CustomButtonStyle(model: self.model)) - .simultaneousGesture(DragGesture(minimumDistance: 0.0) - .onChanged { _ in - self.isPressed = true - } - .onEnded { _ in - self.isPressed = false - } - ) - .disabled(!self.model.isEnabled) - .scaleEffect( - self.isPressed ? self.model.animationScale.value : 1, - anchor: .center - ) + Button(action: self.action) { + HStack(spacing: self.model.contentSpacing) { + self.content() + } + .frame(maxWidth: self.model.width) + .frame(height: self.model.height) + } + .buttonStyle(CustomButtonStyle(model: self.model)) + .simultaneousGesture(DragGesture(minimumDistance: 0.0) + .onChanged { _ in + self.isPressed = true + } + .onEnded { _ in + self.isPressed = false + } + ) + .disabled(!self.model.isEnabled || self.model.isLoading) + .scaleEffect( + self.isPressed ? self.model.animationScale.value : 1, + anchor: .center + ) } -} -// MARK: - Helpers + @ViewBuilder + private func content() -> some View { + switch (self.model.isLoading, self.model.buttonImage, self.model.imageLocation) { + case (true, _, _): + SULoading(model: self.model.preferredLoadingVM) + Text(self.model.title) + case (false, let image?, .leading): + image + Text(self.model.title) + case (false, let image?, .trailing): + Text(self.model.title) + image + default: + Text(self.model.title) + } + } +} private struct CustomButtonStyle: SwiftUI.ButtonStyle { let model: ButtonVM From f3949d52b199f4b10b0fdfcdc9001f54de3d67fc Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Thu, 3 Apr 2025 14:03:41 +0300 Subject: [PATCH 02/60] image fix --- .../DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index c2e3b6c3..58f83b49 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -26,7 +26,7 @@ struct ButtonPreview: View { Toggle("Enabled", isOn: self.$model.isEnabled) Toggle("Full Width", isOn: self.$model.isFullWidth) Picker("Image Source", selection: self.$model.imageSrc) { - Text("SF Symbol").tag(ButtonVM.ImageSource.sfSymbol("star.fill")) + Text("SF Symbol").tag(ButtonVM.ImageSource.sfSymbol("camera.fill")) Text("Local").tag(ButtonVM.ImageSource.local("avatar_placeholder")) Text("None").tag(Optional.none) } From 9e5d3028ec24eeaa6d0e201ace725e3f78a65c57 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Thu, 3 Apr 2025 18:56:49 +0300 Subject: [PATCH 03/60] improve UKButton --- .../PreviewPages/ButtonPreview.swift | 4 +- .../Components/Button/Models/ButtonVM.swift | 28 ++++++-- .../Components/Button/UKButton.swift | 70 +++++++++++++++++-- 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index 58f83b49..f74491a6 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -23,7 +23,9 @@ struct ButtonPreview: View { Text("Custom: 20px").tag(ComponentRadius.custom(20)) } ButtonFontPicker(selection: self.$model.font) - Toggle("Enabled", isOn: self.$model.isEnabled) + if !self.model.isLoading { + Toggle("Enabled", isOn: self.$model.isEnabled) + } Toggle("Full Width", isOn: self.$model.isFullWidth) Picker("Image Source", selection: self.$model.imageSrc) { Text("SF Symbol").tag(ButtonVM.ImageSource.sfSymbol("camera.fill")) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 52e4e7a9..e9693017 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -74,6 +74,10 @@ public struct ButtonVM: ComponentVM { // MARK: Shared Helpers extension ButtonVM { + private var isInteractive: Bool { + self.isEnabled && !self.isLoading + } + var preferredLoadingVM: LoadingVM { return self.loadingVM ?? .init { $0.color = .init( @@ -87,10 +91,10 @@ extension ButtonVM { switch self.style { case .filled: let color = self.color?.main ?? .content2 - return color.enabled(self.isEnabled) + return color.enabled(self.isInteractive) case .light: let color = self.color?.background ?? .content1 - return color.enabled(self.isEnabled) + return color.enabled(self.isInteractive) case .plain, .bordered: return nil } @@ -102,7 +106,7 @@ extension ButtonVM { case .plain, .light, .bordered: self.color?.main ?? .foreground } - return color.enabled(self.isEnabled) + return color.enabled(self.isInteractive) } var borderWidth: CGFloat { switch self.style { @@ -118,7 +122,7 @@ extension ButtonVM { return nil case .bordered: if let color { - return color.main.enabled(self.isEnabled) + return color.main.enabled(self.isInteractive) } else { return .divider } @@ -193,6 +197,18 @@ extension ButtonVM { } } +extension ButtonVM { + public var uiImage: UIImage? { + guard let imageSrc = self.imageSrc else { return nil } + switch imageSrc { + case .sfSymbol(let name): + return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) + case .local(let name, let bundle): + return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) + } + } +} + // MARK: SwiftUI Helpers extension ButtonVM { @@ -206,9 +222,9 @@ extension ButtonVM { guard let imageSrc = self.imageSrc else { return nil } switch imageSrc { case .sfSymbol(let name): - return Image(systemName: name) + return Image(systemName: name).renderingMode(.template) case .local(let name, let bundle): - return Image(name, bundle: bundle) + return Image(name, bundle: bundle).renderingMode(.template) } } } diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 058c1609..007ae8c7 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -32,6 +32,15 @@ open class UKButton: UIView, UKComponent { /// A label that displays the title from the model. public var titleLabel = UILabel() + /// A loader view, created with the preferred loading VM from the model. + public let loaderView: UKLoading + + /// A stack view that arranges the loader and title label. + private let stackView = UIStackView() + + /// A image view for displaying the image from the model. + public let imageView: UIImageView = UIImageView() + // MARK: UIView Properties open override var intrinsicContentSize: CGSize { @@ -50,6 +59,7 @@ open class UKButton: UIView, UKComponent { ) { self.model = model self.action = action + self.loaderView = UKLoading(model: model.preferredLoadingVM) super.init(frame: .zero) self.setup() @@ -64,7 +74,7 @@ open class UKButton: UIView, UKComponent { // MARK: Setup private func setup() { - self.addSubview(self.titleLabel) + self.addSubview(self.stackView) if #available(iOS 17.0, *) { self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in @@ -78,12 +88,23 @@ open class UKButton: UIView, UKComponent { private func style() { Self.Style.mainView(self, model: self.model) Self.Style.titleLabel(self.titleLabel, model: self.model) + Self.Style.configureStackView( + self.stackView, + model: self.model, + loaderView: self.loaderView, + titleLabel: self.titleLabel, + imageView: self.imageView + ) + + self.loaderView.model = self.model.preferredLoadingVM + + self.loaderView.isHidden = !self.model.isLoading } // MARK: Layout private func layout() { - self.titleLabel.center() + self.stackView.center() } open override func layoutSubviews() { @@ -99,7 +120,13 @@ open class UKButton: UIView, UKComponent { self.style() - if self.model.shouldUpdateSize(oldModel) { + self.imageView.image = self.model.uiImage + self.imageView.tintColor = self.model.foregroundColor.uiColor + + if self.model.shouldUpdateSize(oldModel) + || self.model.isLoading != oldModel.isLoading + || self.model.imageSrc != oldModel.imageSrc + || self.model.imageLocation != oldModel.imageLocation { self.invalidateIntrinsicContentSize() } } @@ -107,7 +134,7 @@ open class UKButton: UIView, UKComponent { // MARK: UIView methods open override func sizeThatFits(_ size: CGSize) -> CGSize { - let contentSize = self.titleLabel.sizeThatFits(size) + let contentSize = self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) let preferredSize = self.model.preferredSize( for: contentSize, parentWidth: self.superview?.bounds.width @@ -183,5 +210,40 @@ extension UKButton { label.font = model.preferredFont.uiFont label.textColor = model.foregroundColor.uiColor } + static func configureStackView( + _ stackView: UIStackView, + model: Model, + loaderView: UKLoading, + titleLabel: UILabel, + imageView: UIImageView + ) { + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = model.contentSpacing + + for subview in stackView.arrangedSubviews { + stackView.removeArrangedSubview(subview) + subview.removeFromSuperview() + } + + if model.isLoading { + stackView.addArrangedSubview(loaderView) + stackView.addArrangedSubview(titleLabel) + return + } + + if let _ = model.imageSrc { + switch model.imageLocation { + case .leading: + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(titleLabel) + case .trailing: + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(imageView) + } + } else { + stackView.addArrangedSubview(titleLabel) + } + } } } From 68848a0e394f586b3b16a6c3fe30b609211e252b Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Thu, 3 Apr 2025 19:06:46 +0300 Subject: [PATCH 04/60] swiftlint fix --- Sources/ComponentsKit/Components/Button/UKButton.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 007ae8c7..9d5f9efd 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -232,7 +232,7 @@ extension UKButton { return } - if let _ = model.imageSrc { + if model.imageSrc != nil { switch model.imageLocation { case .leading: stackView.addArrangedSubview(imageView) From 572135fddc3cc06a11e2e6c6602902d612a8f1ca Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sun, 6 Apr 2025 12:21:48 +0300 Subject: [PATCH 05/60] code style fix --- .../Components/Button/Models/ButtonVM.swift | 6 +++--- .../Components/Button/SUButton.swift | 8 +++---- .../Components/Button/UKButton.swift | 21 ++++++++++--------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index e9693017..8c4895ac 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -74,7 +74,7 @@ public struct ButtonVM: ComponentVM { // MARK: Shared Helpers extension ButtonVM { - private var isInteractive: Bool { + var isInteractive: Bool { self.isEnabled && !self.isLoading } @@ -199,7 +199,7 @@ extension ButtonVM { extension ButtonVM { public var uiImage: UIImage? { - guard let imageSrc = self.imageSrc else { return nil } + guard let imageSrc else { return nil } switch imageSrc { case .sfSymbol(let name): return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) @@ -219,7 +219,7 @@ extension ButtonVM { extension ButtonVM { var buttonImage: Image? { - guard let imageSrc = self.imageSrc else { return nil } + guard let imageSrc else { return nil } switch imageSrc { case .sfSymbol(let name): return Image(systemName: name).renderingMode(.template) diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index ffb30f7c..59cc1d93 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -31,10 +31,8 @@ public struct SUButton: View { public var body: some View { Button(action: self.action) { HStack(spacing: self.model.contentSpacing) { - self.content() + self.content } - .frame(maxWidth: self.model.width) - .frame(height: self.model.height) } .buttonStyle(CustomButtonStyle(model: self.model)) .simultaneousGesture(DragGesture(minimumDistance: 0.0) @@ -45,7 +43,7 @@ public struct SUButton: View { self.isPressed = false } ) - .disabled(!self.model.isEnabled || self.model.isLoading) + .disabled(!self.model.isInteractive) .scaleEffect( self.isPressed ? self.model.animationScale.value : 1, anchor: .center @@ -53,7 +51,7 @@ public struct SUButton: View { } @ViewBuilder - private func content() -> some View { + private var content: some View { switch (self.model.isLoading, self.model.buttonImage, self.model.imageLocation) { case (true, _, _): SULoading(model: self.model.preferredLoadingVM) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 9d5f9efd..77ec6ca6 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -32,14 +32,14 @@ open class UKButton: UIView, UKComponent { /// A label that displays the title from the model. public var titleLabel = UILabel() - /// A loader view, created with the preferred loading VM from the model. - public let loaderView: UKLoading + /// A loading indicator shown when the button is in a loading state. + public let loaderView = UKLoading() - /// A stack view that arranges the loader and title label. + /// A stack view that manages the layout of the button’s internal content. private let stackView = UIStackView() - /// A image view for displaying the image from the model. - public let imageView: UIImageView = UIImageView() + /// An optional image displayed alongside the title. + public let imageView = UIImageView() // MARK: UIView Properties @@ -59,7 +59,6 @@ open class UKButton: UIView, UKComponent { ) { self.model = model self.action = action - self.loaderView = UKLoading(model: model.preferredLoadingVM) super.init(frame: .zero) self.setup() @@ -95,9 +94,9 @@ open class UKButton: UIView, UKComponent { titleLabel: self.titleLabel, imageView: self.imageView ) + Self.Style.imageView(self.imageView, model: self.model) self.loaderView.model = self.model.preferredLoadingVM - self.loaderView.isHidden = !self.model.isLoading } @@ -120,9 +119,6 @@ open class UKButton: UIView, UKComponent { self.style() - self.imageView.image = self.model.uiImage - self.imageView.tintColor = self.model.foregroundColor.uiColor - if self.model.shouldUpdateSize(oldModel) || self.model.isLoading != oldModel.isLoading || self.model.imageSrc != oldModel.imageSrc @@ -245,5 +241,10 @@ extension UKButton { stackView.addArrangedSubview(titleLabel) } } + + static func imageView(_ imageView: UIImageView, model: Model) { + imageView.image = model.uiImage + imageView.tintColor = model.foregroundColor.uiColor + } } } From 6ac6171dfa67fc18124d5d1c0948e2a8899b4ba5 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sun, 6 Apr 2025 12:26:53 +0300 Subject: [PATCH 06/60] shouldUpdateSize fix --- .../ComponentsKit/Components/Button/Models/ButtonVM.swift | 3 +++ Sources/ComponentsKit/Components/Button/UKButton.swift | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 8c4895ac..e72998c0 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -194,6 +194,9 @@ extension ButtonVM { return self.size != oldModel?.size || self.font != oldModel?.font || self.isFullWidth != oldModel?.isFullWidth + || self.isLoading != oldModel?.isLoading + || self.imageSrc != oldModel?.imageSrc + || self.imageLocation != oldModel?.imageLocation } } diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 77ec6ca6..0e0346c0 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -119,10 +119,9 @@ open class UKButton: UIView, UKComponent { self.style() - if self.model.shouldUpdateSize(oldModel) - || self.model.isLoading != oldModel.isLoading - || self.model.imageSrc != oldModel.imageSrc - || self.model.imageLocation != oldModel.imageLocation { + if self.model.shouldUpdateSize(oldModel) { + Self.Style.imageView(self.imageView, model: self.model) + self.loaderView.model = self.model.preferredLoadingVM self.invalidateIntrinsicContentSize() } } From ffd1d5992d6283089c65b0dde004dc55ca8aeeea Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sun, 6 Apr 2025 19:51:12 +0300 Subject: [PATCH 07/60] extension ImageSource and ImageLocation extracted --- .../Button/Models/ButtonImageSource.swift | 26 +++++++++++++++++++ .../Components/Button/Models/ButtonVM.swift | 12 --------- 2 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 Sources/ComponentsKit/Components/Button/Models/ButtonImageSource.swift diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonImageSource.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonImageSource.swift new file mode 100644 index 00000000..034bebd7 --- /dev/null +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonImageSource.swift @@ -0,0 +1,26 @@ +import Foundation + +/// Defines the image source options for a button. +extension ButtonVM { + public enum ImageSource: Hashable { + /// An image loaded from a system SF Symbol. + /// + /// - Parameter name: The name of the SF Symbol. + case sfSymbol(String) + + /// An image loaded from a local asset. + /// + /// - Parameters: + /// - name: The name of the local image asset. + /// - bundle: The bundle containing the image resource. Defaults to `nil` to use the main bundle. + case local(String, bundle: Bundle? = nil) + } + + /// Specifies the position of the image relative to the button's title. + public enum ImageLocation { + /// The image is displayed before the title. + case leading + /// The image is displayed after the title. + case trailing + } +} diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index e72998c0..e9645e1e 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -158,18 +158,6 @@ extension ButtonVM { } } -extension ButtonVM { - public enum ImageSource: Hashable { - case sfSymbol(String) - case local(String, bundle: Bundle? = nil) - } - - public enum ImageLocation { - case leading - case trailing - } -} - // MARK: UIKit Helpers extension ButtonVM { From 3171a7914320ffc8f424a756fd48e5cf68ecaaf0 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sun, 6 Apr 2025 19:53:42 +0300 Subject: [PATCH 08/60] rename extension buttonImage - buttonImage renamed to image --- .../Components/Button/Models/ButtonVM.swift | 8 ++++---- .../ComponentsKit/Components/Button/SUButton.swift | 12 +++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index e9645e1e..16163adb 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -209,13 +209,13 @@ extension ButtonVM { } extension ButtonVM { - var buttonImage: Image? { - guard let imageSrc else { return nil } + public var image: UIImage? { + guard let imageSrc = self.imageSrc else { return nil } switch imageSrc { case .sfSymbol(let name): - return Image(systemName: name).renderingMode(.template) + return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) case .local(let name, let bundle): - return Image(name, bundle: bundle).renderingMode(.template) + return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) } } } diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index 59cc1d93..6db9a813 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -52,16 +52,18 @@ public struct SUButton: View { @ViewBuilder private var content: some View { - switch (self.model.isLoading, self.model.buttonImage, self.model.imageLocation) { + switch (self.model.isLoading, self.model.image, self.model.imageLocation) { + case (true, _, _) where self.model.title.isEmpty: + SULoading(model: self.model.preferredLoadingVM) case (true, _, _): SULoading(model: self.model.preferredLoadingVM) Text(self.model.title) - case (false, let image?, .leading): - image + case (false, let uiImage?, .leading): + Image(uiImage: uiImage) Text(self.model.title) - case (false, let image?, .trailing): + case (false, let uiImage?, .trailing): Text(self.model.title) - image + Image(uiImage: uiImage) default: Text(self.model.title) } From f9bd1548bb5253914e09c62bce46b9ed2685d861 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sun, 6 Apr 2025 19:54:31 +0300 Subject: [PATCH 09/60] params in alphabetical order --- .../Components/Button/Models/ButtonVM.swift | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 16163adb..943ca64e 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -3,9 +3,6 @@ import UIKit /// A model that defines the appearance properties for a button component. public struct ButtonVM: ComponentVM { - /// The text displayed on the button. - public var title: String = "" - /// The scaling factor for the button's press animation, with a value between 0 and 1. /// /// Defaults to `.medium`. @@ -14,6 +11,11 @@ public struct ButtonVM: ComponentVM { /// The color of the button. public var color: ComponentColor? + /// The spacing between the button's title and its image or loading indicator. + /// + /// Defaults to `8.0`. + public var contentSpacing: CGFloat = 8.0 + /// The corner radius of the button. /// /// Defaults to `.medium`. @@ -24,6 +26,14 @@ public struct ButtonVM: ComponentVM { /// If not provided, the font is automatically calculated based on the button's size. public var font: UniversalFont? + /// The position of the image relative to the button's title. + /// + /// Defaults to `.leading`. + public var imageLocation: ImageLocation = .leading + + /// The source of the image to be displayed. + public var imageSrc: ImageSource? + /// A Boolean value indicating whether the button is enabled or disabled. /// /// Defaults to `true`. @@ -34,38 +44,28 @@ public struct ButtonVM: ComponentVM { /// Defaults to `false`. public var isFullWidth: Bool = false - /// The predefined size of the button. - /// - /// Defaults to `.medium`. - public var size: ComponentSize = .medium - - /// The visual style of the button. + /// A Boolean value indicating whether the button is currently in a loading state. /// - /// Defaults to `.filled`. - public var style: ButtonStyle = .filled + /// Defaults to `false`. + public var isLoading: Bool = false /// The loading VM used for the loading indicator. /// /// If not provided, a default loading view model is used. public var loadingVM: LoadingVM? - /// A Boolean value indicating whether the button is currently in a loading state. + /// The predefined size of the button. /// - /// Defaults to `false`. - public var isLoading: Bool = false - - /// The source of the image to be displayed. - public var imageSrc: ImageSource? + /// Defaults to `.medium`. + public var size: ComponentSize = .medium - /// The position of the image relative to the button's title. + /// The visual style of the button. /// - /// Defaults to `.leading`. - public var imageLocation: ImageLocation = .leading + /// Defaults to `.filled`. + public var style: ButtonStyle = .filled - /// The spacing between the button's title and its image or loading indicator. - /// - /// Defaults to `8.0`. - public var contentSpacing: CGFloat = 8.0 + /// The text displayed on the button. + public var title: String = "" /// Initializes a new instance of `ButtonVM` with default values. public init() {} From f316b5575f8ad886955208d8bc96da2d20243096 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sun, 6 Apr 2025 21:00:07 +0300 Subject: [PATCH 10/60] fix some logic --- .../Components/Button/UKButton.swift | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 0e0346c0..d200ba6e 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -74,6 +74,13 @@ open class UKButton: UIView, UKComponent { private func setup() { self.addSubview(self.stackView) + self.stackView.axis = .horizontal + self.stackView.alignment = .center + self.stackView.spacing = self.model.contentSpacing + + self.stackView.addArrangedSubview(self.imageView) + self.stackView.addArrangedSubview(self.loaderView) + self.stackView.addArrangedSubview(self.titleLabel) if #available(iOS 17.0, *) { self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in @@ -212,32 +219,21 @@ extension UKButton { titleLabel: UILabel, imageView: UIImageView ) { - stackView.axis = .horizontal - stackView.alignment = .center stackView.spacing = model.contentSpacing - for subview in stackView.arrangedSubviews { - stackView.removeArrangedSubview(subview) - subview.removeFromSuperview() - } - - if model.isLoading { - stackView.addArrangedSubview(loaderView) - stackView.addArrangedSubview(titleLabel) - return - } - - if model.imageSrc != nil { - switch model.imageLocation { - case .leading: - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(titleLabel) - case .trailing: - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(imageView) - } - } else { - stackView.addArrangedSubview(titleLabel) + loaderView.isHidden = !model.isLoading + titleLabel.isHidden = false + imageView.isHidden = model.isLoading || (model.imageSrc == nil) + + switch (model.isLoading, model.imageSrc, model.imageLocation) { + case (false, .some(_), .leading): + stackView.removeArrangedSubview(imageView) + stackView.insertArrangedSubview(imageView, at: 0) + case (false, .some(_), .trailing): + stackView.removeArrangedSubview(imageView) + stackView.addArrangedSubview(imageView) + default: + break } } From 8627341080fb4698596239432c9552852124a8a4 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sun, 6 Apr 2025 21:09:47 +0300 Subject: [PATCH 11/60] fix uikit bug - inactive while loading --- Sources/ComponentsKit/Components/Button/UKButton.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index d200ba6e..0b53a23a 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -18,7 +18,7 @@ open class UKButton: UIView, UKComponent { /// A Boolean value indicating whether the button is pressed. public private(set) var isPressed: Bool = false { didSet { - self.transform = self.isPressed && self.model.isEnabled + self.transform = self.isPressed && self.model.isInteractive ? .init( scaleX: self.model.animationScale.value, y: self.model.animationScale.value @@ -164,7 +164,7 @@ open class UKButton: UIView, UKComponent { defer { self.isPressed = false } - if self.model.isEnabled, + if self.model.isInteractive, let location = touches.first?.location(in: self), self.bounds.contains(location) { self.action() From 31cc411a664698df1e8f7e018c34652bd6faada8 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sun, 6 Apr 2025 21:25:13 +0300 Subject: [PATCH 12/60] local image size bug fix --- .../Components/Button/Models/ButtonVM.swift | 4 ++-- .../Components/Button/SUButton.swift | 16 ++++++++++++++-- .../Components/Button/UKButton.swift | 5 +++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 943ca64e..45278c7f 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -195,7 +195,7 @@ extension ButtonVM { case .sfSymbol(let name): return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) case .local(let name, let bundle): - return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) + return UIImage(named: name, in: bundle, compatibleWith: nil) } } } @@ -215,7 +215,7 @@ extension ButtonVM { case .sfSymbol(let name): return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) case .local(let name, let bundle): - return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) + return UIImage(named: name, in: bundle, compatibleWith: nil) } } } diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index 6db9a813..8e77abd0 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -59,17 +59,29 @@ public struct SUButton: View { SULoading(model: self.model.preferredLoadingVM) Text(self.model.title) case (false, let uiImage?, .leading): - Image(uiImage: uiImage) + ButtonImageView(image: uiImage, size: self.model.height * 0.6) Text(self.model.title) case (false, let uiImage?, .trailing): Text(self.model.title) - Image(uiImage: uiImage) + ButtonImageView(image: uiImage, size: self.model.height * 0.6) default: Text(self.model.title) } } } +struct ButtonImageView: View { + let image: UIImage + let size: CGFloat + + var body: some View { + Image(uiImage: self.image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: self.size, height: self.size) + } +} + private struct CustomButtonStyle: SwiftUI.ButtonStyle { let model: ButtonVM diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 0b53a23a..8a5b5319 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -240,6 +240,11 @@ extension UKButton { static func imageView(_ imageView: UIImageView, model: Model) { imageView.image = model.uiImage imageView.tintColor = model.foregroundColor.uiColor + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + + let imageSize = model.height * 0.6 + _ = imageView.size(width: imageSize, height: imageSize) } } } From f74bf7b98c008b7212e8371846785938ae75edd4 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sun, 6 Apr 2025 22:52:38 +0300 Subject: [PATCH 13/60] added contentSpacing into preview --- .../ComponentsPreview/PreviewPages/ButtonPreview.swift | 5 +++++ .../ComponentsKit/Components/Button/Models/ButtonVM.swift | 1 + 2 files changed, 6 insertions(+) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index f74491a6..7a1c56d0 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -22,6 +22,11 @@ struct ButtonPreview: View { ComponentRadiusPicker(selection: self.$model.cornerRadius) { Text("Custom: 20px").tag(ComponentRadius.custom(20)) } + Picker("Content Spacing", selection: self.$model.contentSpacing) { + Text("4").tag(CGFloat(4)) + Text("8").tag(CGFloat(8)) + Text("12").tag(CGFloat(12)) + } ButtonFontPicker(selection: self.$model.font) if !self.model.isLoading { Toggle("Enabled", isOn: self.$model.isEnabled) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 45278c7f..a34ea0df 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -185,6 +185,7 @@ extension ButtonVM { || self.isLoading != oldModel?.isLoading || self.imageSrc != oldModel?.imageSrc || self.imageLocation != oldModel?.imageLocation + || self.contentSpacing != oldModel?.contentSpacing } } From f48ce2cffb933b4e9b8974a604f726717da87fe5 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 00:23:58 +0300 Subject: [PATCH 14/60] hide title toggle --- .../PreviewPages/ButtonPreview.swift | 8 +++++++- .../Components/Button/Models/ButtonVM.swift | 1 + .../Components/Button/SUButton.swift | 16 ++++++++++++---- .../Components/Button/UKButton.swift | 2 +- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index 7a1c56d0..b6b3381e 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -37,7 +37,7 @@ struct ButtonPreview: View { Text("Local").tag(ButtonVM.ImageSource.local("avatar_placeholder")) Text("None").tag(Optional.none) } - if self.model.imageSrc != nil { + if self.model.imageSrc != nil && !self.model.title.isEmpty { Picker("Image Location", selection: self.$model.imageLocation) { Text("Leading").tag(ButtonVM.ImageLocation.leading) Text("Trailing").tag(ButtonVM.ImageLocation.trailing) @@ -45,6 +45,12 @@ struct ButtonPreview: View { } Toggle("Loading", isOn: self.$model.isLoading) SizePicker(selection: self.$model.size) + Toggle("Show Title", isOn: Binding( + get: { !self.model.title.isEmpty }, + set: { newValue in + self.model.title = newValue ? "Button" : "" + } + )) Picker("Style", selection: self.$model.style) { Text("Filled").tag(ButtonStyle.filled) Text("Plain").tag(ButtonStyle.plain) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index a34ea0df..5d3efe5e 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -186,6 +186,7 @@ extension ButtonVM { || self.imageSrc != oldModel?.imageSrc || self.imageLocation != oldModel?.imageLocation || self.contentSpacing != oldModel?.contentSpacing + || self.title != oldModel?.title } } diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index 8e77abd0..6447cb49 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -59,11 +59,19 @@ public struct SUButton: View { SULoading(model: self.model.preferredLoadingVM) Text(self.model.title) case (false, let uiImage?, .leading): - ButtonImageView(image: uiImage, size: self.model.height * 0.6) - Text(self.model.title) + if self.model.title.isEmpty { + ButtonImageView(image: uiImage, size: self.model.height * 0.6) + } else { + ButtonImageView(image: uiImage, size: self.model.height * 0.6) + Text(self.model.title) + } case (false, let uiImage?, .trailing): - Text(self.model.title) - ButtonImageView(image: uiImage, size: self.model.height * 0.6) + if self.model.title.isEmpty { + ButtonImageView(image: uiImage, size: self.model.height * 0.6) + } else { + Text(self.model.title) + ButtonImageView(image: uiImage, size: self.model.height * 0.6) + } default: Text(self.model.title) } diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 8a5b5319..de43ba03 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -219,7 +219,7 @@ extension UKButton { titleLabel: UILabel, imageView: UIImageView ) { - stackView.spacing = model.contentSpacing + stackView.spacing = model.title.isEmpty ? 0 : model.contentSpacing loaderView.isHidden = !model.isLoading titleLabel.isHidden = false From 9923987f63c39d503493f1fc4a66e0f7a55d265f Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 13:38:31 +0300 Subject: [PATCH 15/60] imageView contentMode --- .../Components/Button/Models/ButtonVM.swift | 11 +++++++++-- .../Components/Button/SUButton.swift | 8 ++++---- .../Components/Button/UKButton.swift | 16 +++++++++++----- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 5d3efe5e..2c7a8353 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -149,6 +149,13 @@ extension ButtonVM { case .large: 52 } } + var imageSide: CGFloat { + switch self.size { + case .small: 20 + case .medium: 24 + case .large: 28 + } + } var horizontalPadding: CGFloat { return switch self.size { case .small: 16 @@ -195,7 +202,7 @@ extension ButtonVM { guard let imageSrc else { return nil } switch imageSrc { case .sfSymbol(let name): - return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) + return UIImage(systemName: name) case .local(let name, let bundle): return UIImage(named: name, in: bundle, compatibleWith: nil) } @@ -215,7 +222,7 @@ extension ButtonVM { guard let imageSrc = self.imageSrc else { return nil } switch imageSrc { case .sfSymbol(let name): - return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) + return UIImage(systemName: name) case .local(let name, let bundle): return UIImage(named: name, in: bundle, compatibleWith: nil) } diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index 6447cb49..edb82708 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -60,17 +60,17 @@ public struct SUButton: View { Text(self.model.title) case (false, let uiImage?, .leading): if self.model.title.isEmpty { - ButtonImageView(image: uiImage, size: self.model.height * 0.6) + ButtonImageView(image: uiImage, size: self.model.imageSide) } else { - ButtonImageView(image: uiImage, size: self.model.height * 0.6) + ButtonImageView(image: uiImage, size: self.model.imageSide) Text(self.model.title) } case (false, let uiImage?, .trailing): if self.model.title.isEmpty { - ButtonImageView(image: uiImage, size: self.model.height * 0.6) + ButtonImageView(image: uiImage, size: self.model.imageSide) } else { Text(self.model.title) - ButtonImageView(image: uiImage, size: self.model.height * 0.6) + ButtonImageView(image: uiImage, size: self.model.imageSide) } default: Text(self.model.title) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index de43ba03..08d56a25 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -111,6 +111,8 @@ open class UKButton: UIView, UKComponent { private func layout() { self.stackView.center() + + self.imageView.size(width: self.model.imageSide, height: self.model.imageSide) } open override func layoutSubviews() { @@ -130,6 +132,14 @@ open class UKButton: UIView, UKComponent { Self.Style.imageView(self.imageView, model: self.model) self.loaderView.model = self.model.preferredLoadingVM self.invalidateIntrinsicContentSize() + + for constraint in self.imageView.constraints { + if constraint.firstAttribute == .width || constraint.firstAttribute == .height { + constraint.constant = self.model.imageSide + } + } + self.setNeedsLayout() + self.layoutIfNeeded() } } @@ -240,11 +250,7 @@ extension UKButton { static func imageView(_ imageView: UIImageView, model: Model) { imageView.image = model.uiImage imageView.tintColor = model.foregroundColor.uiColor - imageView.contentMode = .scaleAspectFit - imageView.clipsToBounds = true - - let imageSize = model.height * 0.6 - _ = imageView.size(width: imageSize, height: imageSize) + imageView.contentMode = .scaleAspectFill } } } From 74e1bf1c37d6843575bc954430f02518875d8427 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 15:07:05 +0300 Subject: [PATCH 16/60] swiftui image size bug fix --- .../Components/Button/SUButton.swift | 40 ++++++++++++++----- .../Components/Button/UKButton.swift | 2 +- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index edb82708..d52ca001 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -60,17 +60,21 @@ public struct SUButton: View { Text(self.model.title) case (false, let uiImage?, .leading): if self.model.title.isEmpty { - ButtonImageView(image: uiImage, size: self.model.imageSide) + ButtonImageView(image: uiImage, tintColor: self.model.foregroundColor.uiColor) + .frame(width: self.model.imageSide, height: self.model.imageSide) } else { - ButtonImageView(image: uiImage, size: self.model.imageSide) + ButtonImageView(image: uiImage, tintColor: self.model.foregroundColor.uiColor) + .frame(width: self.model.imageSide, height: self.model.imageSide) Text(self.model.title) } case (false, let uiImage?, .trailing): if self.model.title.isEmpty { - ButtonImageView(image: uiImage, size: self.model.imageSide) + ButtonImageView(image: uiImage, tintColor: self.model.foregroundColor.uiColor) + .frame(width: self.model.imageSide, height: self.model.imageSide) } else { Text(self.model.title) - ButtonImageView(image: uiImage, size: self.model.imageSide) + ButtonImageView(image: uiImage, tintColor: self.model.foregroundColor.uiColor) + .frame(width: self.model.imageSide, height: self.model.imageSide) } default: Text(self.model.title) @@ -78,15 +82,29 @@ public struct SUButton: View { } } -struct ButtonImageView: View { +struct ButtonImageView: UIViewRepresentable { + class InternalImageView: UIImageView { + override var intrinsicContentSize: CGSize { + return self.bounds.size + } + } + let image: UIImage - let size: CGFloat + let tintColor: UIColor? + + func makeUIView(context: Context) -> UIImageView { + let imageView = InternalImageView() + imageView.image = self.image + imageView.contentMode = .scaleAspectFit + imageView.tintColor = self.tintColor + return imageView + } - var body: some View { - Image(uiImage: self.image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: self.size, height: self.size) + func updateUIView(_ uiView: UIImageView, context: Context) { + uiView.image = self.image + uiView.tintColor = self.tintColor + uiView.setNeedsLayout() + uiView.layoutIfNeeded() } } diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 08d56a25..0c582366 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -250,7 +250,7 @@ extension UKButton { static func imageView(_ imageView: UIImageView, model: Model) { imageView.image = model.uiImage imageView.tintColor = model.foregroundColor.uiColor - imageView.contentMode = .scaleAspectFill + imageView.contentMode = .scaleAspectFit } } } From 6a90b2ba370c09fc7fc42b8bf5a2ac97e05864d1 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 17:01:13 +0300 Subject: [PATCH 17/60] preview "if" deleted --- .../PreviewPages/ButtonPreview.swift | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index b6b3381e..4bdd75d3 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -18,6 +18,7 @@ struct ButtonPreview: View { } Form { AnimationScalePicker(selection: self.$model.animationScale) + ButtonFontPicker(selection: self.$model.font) ComponentOptionalColorPicker(selection: self.$model.color) ComponentRadiusPicker(selection: self.$model.cornerRadius) { Text("Custom: 20px").tag(ComponentRadius.custom(20)) @@ -27,22 +28,17 @@ struct ButtonPreview: View { Text("8").tag(CGFloat(8)) Text("12").tag(CGFloat(12)) } - ButtonFontPicker(selection: self.$model.font) - if !self.model.isLoading { - Toggle("Enabled", isOn: self.$model.isEnabled) - } + Toggle("Enabled", isOn: self.$model.isEnabled) Toggle("Full Width", isOn: self.$model.isFullWidth) + Picker("Image Location", selection: self.$model.imageLocation) { + Text("Leading").tag(ButtonVM.ImageLocation.leading) + Text("Trailing").tag(ButtonVM.ImageLocation.trailing) + } Picker("Image Source", selection: self.$model.imageSrc) { Text("SF Symbol").tag(ButtonVM.ImageSource.sfSymbol("camera.fill")) Text("Local").tag(ButtonVM.ImageSource.local("avatar_placeholder")) Text("None").tag(Optional.none) } - if self.model.imageSrc != nil && !self.model.title.isEmpty { - Picker("Image Location", selection: self.$model.imageLocation) { - Text("Leading").tag(ButtonVM.ImageLocation.leading) - Text("Trailing").tag(ButtonVM.ImageLocation.trailing) - } - } Toggle("Loading", isOn: self.$model.isLoading) SizePicker(selection: self.$model.size) Toggle("Show Title", isOn: Binding( @@ -59,16 +55,6 @@ struct ButtonPreview: View { Text("Bordered with medium border").tag(ButtonStyle.bordered(.medium)) Text("Bordered with large border").tag(ButtonStyle.bordered(.large)) } - .onChange(of: self.model.imageLocation) { _ in - if self.model.isLoading { - self.model.isLoading = false - } - } - .onChange(of: self.model.imageSrc) { _ in - if self.model.isLoading { - self.model.isLoading = false - } - } } } } From d97a971f60af77e5e6f18ae118bf3e1a239136fc Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 17:15:49 +0300 Subject: [PATCH 18/60] deleted unused code --- Sources/ComponentsKit/Components/Button/UKButton.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 0c582366..d060354e 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -129,8 +129,6 @@ open class UKButton: UIView, UKComponent { self.style() if self.model.shouldUpdateSize(oldModel) { - Self.Style.imageView(self.imageView, model: self.model) - self.loaderView.model = self.model.preferredLoadingVM self.invalidateIntrinsicContentSize() for constraint in self.imageView.constraints { @@ -138,8 +136,6 @@ open class UKButton: UIView, UKComponent { constraint.constant = self.model.imageSide } } - self.setNeedsLayout() - self.layoutIfNeeded() } } From ef33dcd9387addb0a1faf1851f43a532515194a4 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 17:21:25 +0300 Subject: [PATCH 19/60] fix constraints - using layout constraints --- .../ComponentsKit/Components/Button/UKButton.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index d060354e..0eb30de3 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -41,6 +41,10 @@ open class UKButton: UIView, UKComponent { /// An optional image displayed alongside the title. public let imageView = UIImageView() + // MARK: - Layout Constraints + + private var imageViewConstraints = LayoutConstraints() + // MARK: UIView Properties open override var intrinsicContentSize: CGSize { @@ -131,11 +135,8 @@ open class UKButton: UIView, UKComponent { if self.model.shouldUpdateSize(oldModel) { self.invalidateIntrinsicContentSize() - for constraint in self.imageView.constraints { - if constraint.firstAttribute == .width || constraint.firstAttribute == .height { - constraint.constant = self.model.imageSide - } - } + self.imageViewConstraints.width?.constant = self.model.imageSide + self.imageViewConstraints.height?.constant = self.model.imageSide } } From 29a2e9b46f840ea9add6a76015ef81aad7ce02d2 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 17:21:30 +0300 Subject: [PATCH 20/60] intrinsicContentSize fix --- Sources/ComponentsKit/Components/Button/SUButton.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index d52ca001..a8fdb9bb 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -85,7 +85,7 @@ public struct SUButton: View { struct ButtonImageView: UIViewRepresentable { class InternalImageView: UIImageView { override var intrinsicContentSize: CGSize { - return self.bounds.size + return .zero } } From 1af122741502a9dbe1ca5a03997b2c86c0c348f7 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 17:34:27 +0300 Subject: [PATCH 21/60] uikit component code restructurization --- .../Components/Button/UKButton.swift | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 0eb30de3..2dac2f28 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -132,6 +132,21 @@ open class UKButton: UIView, UKComponent { self.style() + self.loaderView.isHidden = !self.model.isLoading + self.titleLabel.isHidden = false + self.imageView.isHidden = self.model.isLoading || (self.model.imageSrc == nil) + + switch (self.model.isLoading, self.model.imageSrc, self.model.imageLocation) { + case (false, .some(_), .leading): + self.stackView.removeArrangedSubview(self.imageView) + self.stackView.insertArrangedSubview(self.imageView, at: 0) + case (false, .some(_), .trailing): + self.stackView.removeArrangedSubview(self.imageView) + self.stackView.addArrangedSubview(self.imageView) + default: + break + } + if self.model.shouldUpdateSize(oldModel) { self.invalidateIntrinsicContentSize() @@ -227,21 +242,6 @@ extension UKButton { imageView: UIImageView ) { stackView.spacing = model.title.isEmpty ? 0 : model.contentSpacing - - loaderView.isHidden = !model.isLoading - titleLabel.isHidden = false - imageView.isHidden = model.isLoading || (model.imageSrc == nil) - - switch (model.isLoading, model.imageSrc, model.imageLocation) { - case (false, .some(_), .leading): - stackView.removeArrangedSubview(imageView) - stackView.insertArrangedSubview(imageView, at: 0) - case (false, .some(_), .trailing): - stackView.removeArrangedSubview(imageView) - stackView.addArrangedSubview(imageView) - default: - break - } } static func imageView(_ imageView: UIImageView, model: Model) { From 9448544adec135db905e2fba5294e0896dfe587e Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 17:38:17 +0300 Subject: [PATCH 22/60] image tint color extracted into model --- .../ComponentsKit/Components/Button/Models/ButtonVM.swift | 6 ++++++ Sources/ComponentsKit/Components/Button/SUButton.swift | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 2c7a8353..5540200b 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -228,3 +228,9 @@ extension ButtonVM { } } } + +extension ButtonVM { + public var imageTintColor: UIColor? { + return self.foregroundColor.uiColor + } +} diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index a8fdb9bb..41442554 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -60,20 +60,20 @@ public struct SUButton: View { Text(self.model.title) case (false, let uiImage?, .leading): if self.model.title.isEmpty { - ButtonImageView(image: uiImage, tintColor: self.model.foregroundColor.uiColor) + ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) .frame(width: self.model.imageSide, height: self.model.imageSide) } else { - ButtonImageView(image: uiImage, tintColor: self.model.foregroundColor.uiColor) + ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) .frame(width: self.model.imageSide, height: self.model.imageSide) Text(self.model.title) } case (false, let uiImage?, .trailing): if self.model.title.isEmpty { - ButtonImageView(image: uiImage, tintColor: self.model.foregroundColor.uiColor) + ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) .frame(width: self.model.imageSide, height: self.model.imageSide) } else { Text(self.model.title) - ButtonImageView(image: uiImage, tintColor: self.model.foregroundColor.uiColor) + ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) .frame(width: self.model.imageSide, height: self.model.imageSide) } default: From 19a71e6f3292bf6644910a6f1590b324a7211bf3 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 17:39:21 +0300 Subject: [PATCH 23/60] func updateUIView fix --- Sources/ComponentsKit/Components/Button/SUButton.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index 41442554..e7c8f3ae 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -100,11 +100,9 @@ struct ButtonImageView: UIViewRepresentable { return imageView } - func updateUIView(_ uiView: UIImageView, context: Context) { - uiView.image = self.image - uiView.tintColor = self.tintColor - uiView.setNeedsLayout() - uiView.layoutIfNeeded() + func updateUIView(_ imageView: UIImageView, context: Context) { + imageView.image = self.image + imageView.tintColor = self.tintColor } } From 97622af7383c1fa1792c2f1164779fa9489ac98e Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 17:43:52 +0300 Subject: [PATCH 24/60] image extension fix - moved into shared helpers - duplicated code deleted --- .../Components/Button/Models/ButtonVM.swift | 36 +++++++------------ .../Components/Button/UKButton.swift | 2 +- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 5540200b..10995335 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -165,6 +165,18 @@ extension ButtonVM { } } +extension ButtonVM { + var image: UIImage? { + guard let imageSrc else { return nil } + switch imageSrc { + case .sfSymbol(let name): + return UIImage(systemName: name) + case .local(let name, let bundle): + return UIImage(named: name, in: bundle, compatibleWith: nil) + } + } +} + // MARK: UIKit Helpers extension ButtonVM { @@ -197,18 +209,6 @@ extension ButtonVM { } } -extension ButtonVM { - public var uiImage: UIImage? { - guard let imageSrc else { return nil } - switch imageSrc { - case .sfSymbol(let name): - return UIImage(systemName: name) - case .local(let name, let bundle): - return UIImage(named: name, in: bundle, compatibleWith: nil) - } - } -} - // MARK: SwiftUI Helpers extension ButtonVM { @@ -217,18 +217,6 @@ extension ButtonVM { } } -extension ButtonVM { - public var image: UIImage? { - guard let imageSrc = self.imageSrc else { return nil } - switch imageSrc { - case .sfSymbol(let name): - return UIImage(systemName: name) - case .local(let name, let bundle): - return UIImage(named: name, in: bundle, compatibleWith: nil) - } - } -} - extension ButtonVM { public var imageTintColor: UIColor? { return self.foregroundColor.uiColor diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 2dac2f28..bcfbbdd7 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -245,7 +245,7 @@ extension UKButton { } static func imageView(_ imageView: UIImageView, model: Model) { - imageView.image = model.uiImage + imageView.image = model.image imageView.tintColor = model.foregroundColor.uiColor imageView.contentMode = .scaleAspectFit } From 14d93ad03d8772d8790ad33517ce4a9ea1bc9e85 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 17:57:02 +0300 Subject: [PATCH 25/60] ImageLocation extracted --- .../Button/Models/ButtonImageLocation.swift | 11 +++++++++++ .../Components/Button/Models/ButtonImageSource.swift | 8 -------- 2 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 Sources/ComponentsKit/Components/Button/Models/ButtonImageLocation.swift diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonImageLocation.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonImageLocation.swift new file mode 100644 index 00000000..e22a109a --- /dev/null +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonImageLocation.swift @@ -0,0 +1,11 @@ +import Foundation + +/// Specifies the position of the image relative to the button's title. +extension ButtonVM { + public enum ImageLocation { + /// The image is displayed before the title. + case leading + /// The image is displayed after the title. + case trailing + } +} diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonImageSource.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonImageSource.swift index 034bebd7..c9598303 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonImageSource.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonImageSource.swift @@ -15,12 +15,4 @@ extension ButtonVM { /// - bundle: The bundle containing the image resource. Defaults to `nil` to use the main bundle. case local(String, bundle: Bundle? = nil) } - - /// Specifies the position of the image relative to the button's title. - public enum ImageLocation { - /// The image is displayed before the title. - case leading - /// The image is displayed after the title. - case trailing - } } From 8971daa32f73e9d17fb58bc71121d71d4be23960 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 18:22:08 +0300 Subject: [PATCH 26/60] fix spacing white titleLabel is hidden --- Sources/ComponentsKit/Components/Button/UKButton.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index bcfbbdd7..48ac38bc 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -133,8 +133,7 @@ open class UKButton: UIView, UKComponent { self.style() self.loaderView.isHidden = !self.model.isLoading - self.titleLabel.isHidden = false - self.imageView.isHidden = self.model.isLoading || (self.model.imageSrc == nil) + self.titleLabel.isHidden = self.model.title.isEmpty switch (self.model.isLoading, self.model.imageSrc, self.model.imageLocation) { case (false, .some(_), .leading): @@ -241,13 +240,14 @@ extension UKButton { titleLabel: UILabel, imageView: UIImageView ) { - stackView.spacing = model.title.isEmpty ? 0 : model.contentSpacing + stackView.spacing = model.contentSpacing } static func imageView(_ imageView: UIImageView, model: Model) { imageView.image = model.image imageView.tintColor = model.foregroundColor.uiColor imageView.contentMode = .scaleAspectFit + imageView.isHidden = model.isLoading || (model.imageSrc.isNil) } } } From 5058c92e7901bccba25dfc56a26e79a3ed23d054 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 18:38:29 +0300 Subject: [PATCH 27/60] deleted if/else from swiftui component --- .../Components/Button/SUButton.swift | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index e7c8f3ae..5f5099e1 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -58,24 +58,20 @@ public struct SUButton: View { case (true, _, _): SULoading(model: self.model.preferredLoadingVM) Text(self.model.title) + case (false, let uiImage?, .leading) where self.model.title.isEmpty: + ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) + .frame(width: self.model.imageSide, height: self.model.imageSide) case (false, let uiImage?, .leading): - if self.model.title.isEmpty { - ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) - .frame(width: self.model.imageSide, height: self.model.imageSide) - } else { - ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) - .frame(width: self.model.imageSide, height: self.model.imageSide) - Text(self.model.title) - } + ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) + .frame(width: self.model.imageSide, height: self.model.imageSide) + Text(self.model.title) + case (false, let uiImage?, .trailing) where self.model.title.isEmpty: + ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) + .frame(width: self.model.imageSide, height: self.model.imageSide) case (false, let uiImage?, .trailing): - if self.model.title.isEmpty { - ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) - .frame(width: self.model.imageSide, height: self.model.imageSide) - } else { - Text(self.model.title) - ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) - .frame(width: self.model.imageSide, height: self.model.imageSide) - } + Text(self.model.title) + ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) + .frame(width: self.model.imageSide, height: self.model.imageSide) default: Text(self.model.title) } From 0b523e4a9d7cc4738a160cea5e6651f4a1ee6819 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 7 Apr 2025 18:42:45 +0300 Subject: [PATCH 28/60] fix tint color to local image --- Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 10995335..0bdc2b88 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -170,9 +170,9 @@ extension ButtonVM { guard let imageSrc else { return nil } switch imageSrc { case .sfSymbol(let name): - return UIImage(systemName: name) + return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) case .local(let name, let bundle): - return UIImage(named: name, in: bundle, compatibleWith: nil) + return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) } } } From 5cfdf75b419b709dbbf158a2ccf6022552261e9b Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Tue, 8 Apr 2025 15:08:47 +0300 Subject: [PATCH 29/60] fix preview sort --- .../ComponentsPreview/PreviewPages/ButtonPreview.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index 4bdd75d3..7f7715e6 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -18,17 +18,17 @@ struct ButtonPreview: View { } Form { AnimationScalePicker(selection: self.$model.animationScale) - ButtonFontPicker(selection: self.$model.font) ComponentOptionalColorPicker(selection: self.$model.color) - ComponentRadiusPicker(selection: self.$model.cornerRadius) { - Text("Custom: 20px").tag(ComponentRadius.custom(20)) - } Picker("Content Spacing", selection: self.$model.contentSpacing) { Text("4").tag(CGFloat(4)) Text("8").tag(CGFloat(8)) Text("12").tag(CGFloat(12)) } + ComponentRadiusPicker(selection: self.$model.cornerRadius) { + Text("Custom: 20px").tag(ComponentRadius.custom(20)) + } Toggle("Enabled", isOn: self.$model.isEnabled) + ButtonFontPicker(selection: self.$model.font) Toggle("Full Width", isOn: self.$model.isFullWidth) Picker("Image Location", selection: self.$model.imageLocation) { Text("Leading").tag(ButtonVM.ImageLocation.leading) @@ -40,13 +40,13 @@ struct ButtonPreview: View { Text("None").tag(Optional.none) } Toggle("Loading", isOn: self.$model.isLoading) - SizePicker(selection: self.$model.size) Toggle("Show Title", isOn: Binding( get: { !self.model.title.isEmpty }, set: { newValue in self.model.title = newValue ? "Button" : "" } )) + SizePicker(selection: self.$model.size) Picker("Style", selection: self.$model.style) { Text("Filled").tag(ButtonStyle.filled) Text("Plain").tag(ButtonStyle.plain) From f3a2ee288cd209d31aa660417a79e6f9afea25b1 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Tue, 8 Apr 2025 15:28:42 +0300 Subject: [PATCH 30/60] code fix - delete unused code - code style fix - documentation fix - fix extension accessibility --- .../Components/Button/Models/ButtonVM.swift | 11 ++++++----- .../Components/Button/SUButton.swift | 17 ++++++++--------- .../Components/Button/UKButton.swift | 19 ++++++------------- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 0bdc2b88..cadb7ac1 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -77,7 +77,6 @@ extension ButtonVM { var isInteractive: Bool { self.isEnabled && !self.isLoading } - var preferredLoadingVM: LoadingVM { return self.loadingVM ?? .init { $0.color = .init( @@ -170,9 +169,12 @@ extension ButtonVM { guard let imageSrc else { return nil } switch imageSrc { case .sfSymbol(let name): - return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) + return UIImage(systemName: name)?.withTintColor( + self.foregroundColor.uiColor, + renderingMode: .alwaysTemplate + ) case .local(let name, let bundle): - return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) + return UIImage(named: name, in: bundle, compatibleWith: nil) } } } @@ -203,7 +205,6 @@ extension ButtonVM { || self.isFullWidth != oldModel?.isFullWidth || self.isLoading != oldModel?.isLoading || self.imageSrc != oldModel?.imageSrc - || self.imageLocation != oldModel?.imageLocation || self.contentSpacing != oldModel?.contentSpacing || self.title != oldModel?.title } @@ -218,7 +219,7 @@ extension ButtonVM { } extension ButtonVM { - public var imageTintColor: UIColor? { + var imageTintColor: UIColor? { return self.foregroundColor.uiColor } } diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index 5f5099e1..9dcf96d1 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -59,26 +59,28 @@ public struct SUButton: View { SULoading(model: self.model.preferredLoadingVM) Text(self.model.title) case (false, let uiImage?, .leading) where self.model.title.isEmpty: - ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) + ButtonImageView(image: uiImage) .frame(width: self.model.imageSide, height: self.model.imageSide) case (false, let uiImage?, .leading): - ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) + ButtonImageView(image: uiImage) .frame(width: self.model.imageSide, height: self.model.imageSide) Text(self.model.title) case (false, let uiImage?, .trailing) where self.model.title.isEmpty: - ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) + ButtonImageView(image: uiImage) .frame(width: self.model.imageSide, height: self.model.imageSide) case (false, let uiImage?, .trailing): Text(self.model.title) - ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor) + ButtonImageView(image: uiImage) .frame(width: self.model.imageSide, height: self.model.imageSide) - default: + case (false, _, _): Text(self.model.title) } } } -struct ButtonImageView: UIViewRepresentable { +// MARK: - Helpers + +private struct ButtonImageView: UIViewRepresentable { class InternalImageView: UIImageView { override var intrinsicContentSize: CGSize { return .zero @@ -86,19 +88,16 @@ struct ButtonImageView: UIViewRepresentable { } let image: UIImage - let tintColor: UIColor? func makeUIView(context: Context) -> UIImageView { let imageView = InternalImageView() imageView.image = self.image imageView.contentMode = .scaleAspectFit - imageView.tintColor = self.tintColor return imageView } func updateUIView(_ imageView: UIImageView, context: Context) { imageView.image = self.image - imageView.tintColor = self.tintColor } } diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 48ac38bc..d4de7d54 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -41,7 +41,7 @@ open class UKButton: UIView, UKComponent { /// An optional image displayed alongside the title. public let imageView = UIImageView() - // MARK: - Layout Constraints + // MARK: Private Properties private var imageViewConstraints = LayoutConstraints() @@ -100,15 +100,12 @@ open class UKButton: UIView, UKComponent { Self.Style.titleLabel(self.titleLabel, model: self.model) Self.Style.configureStackView( self.stackView, - model: self.model, - loaderView: self.loaderView, - titleLabel: self.titleLabel, - imageView: self.imageView + model: self.model ) Self.Style.imageView(self.imageView, model: self.model) self.loaderView.model = self.model.preferredLoadingVM - self.loaderView.isHidden = !self.model.isLoading + self.loaderView.isVisible = self.model.isLoading } // MARK: Layout @@ -132,7 +129,7 @@ open class UKButton: UIView, UKComponent { self.style() - self.loaderView.isHidden = !self.model.isLoading + self.loaderView.isVisible = self.model.isLoading self.titleLabel.isHidden = self.model.title.isEmpty switch (self.model.isLoading, self.model.imageSrc, self.model.imageLocation) { @@ -235,19 +232,15 @@ extension UKButton { } static func configureStackView( _ stackView: UIStackView, - model: Model, - loaderView: UKLoading, - titleLabel: UILabel, - imageView: UIImageView + model: Model ) { stackView.spacing = model.contentSpacing } static func imageView(_ imageView: UIImageView, model: Model) { imageView.image = model.image - imageView.tintColor = model.foregroundColor.uiColor imageView.contentMode = .scaleAspectFit - imageView.isHidden = model.isLoading || (model.imageSrc.isNil) + imageView.isHidden = model.isLoading || model.imageSrc.isNil } } } From 073f1ef3f5e9f35f788e93194a24b58414a01c73 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Tue, 8 Apr 2025 16:38:44 +0300 Subject: [PATCH 31/60] fix image size updating --- .../Components/Button/Models/ButtonVM.swift | 25 ++++++++++----- .../Components/Button/UKButton.swift | 32 ++++++++++++------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index cadb7ac1..4f243485 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -199,14 +199,23 @@ extension ButtonVM { return .init(width: width, height: self.height) } - func shouldUpdateSize(_ oldModel: Self?) -> Bool { - return self.size != oldModel?.size - || self.font != oldModel?.font - || self.isFullWidth != oldModel?.isFullWidth - || self.isLoading != oldModel?.isLoading - || self.imageSrc != oldModel?.imageSrc - || self.contentSpacing != oldModel?.contentSpacing - || self.title != oldModel?.title + func shouldUpdateImagePosition(_ oldModel: Self?) -> Bool { + guard let oldModel else { return true } + return self.imageLocation != oldModel.imageLocation + } + func shouldUpdateImageSize(_ oldModel: Self?) -> Bool { + guard let oldModel else { return true } + return self.imageSide != oldModel.imageSide + } + func shouldRecalculateSize(_ oldModel: Self?) -> Bool { + guard let oldModel else { return true } + return self.size != oldModel.size + || self.font != oldModel.font + || self.isFullWidth != oldModel.isFullWidth + || self.isLoading != oldModel.isLoading + || self.imageSrc != oldModel.imageSrc + || self.contentSpacing != oldModel.contentSpacing + || self.title != oldModel.title } } diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index d4de7d54..1568a758 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -113,7 +113,10 @@ open class UKButton: UIView, UKComponent { private func layout() { self.stackView.center() - self.imageView.size(width: self.model.imageSide, height: self.model.imageSide) + self.imageViewConstraints = self.imageView.size( + width: self.model.imageSide, + height: self.model.imageSide + ) } open override func layoutSubviews() { @@ -132,22 +135,27 @@ open class UKButton: UIView, UKComponent { self.loaderView.isVisible = self.model.isLoading self.titleLabel.isHidden = self.model.title.isEmpty - switch (self.model.isLoading, self.model.imageSrc, self.model.imageLocation) { - case (false, .some(_), .leading): - self.stackView.removeArrangedSubview(self.imageView) - self.stackView.insertArrangedSubview(self.imageView, at: 0) - case (false, .some(_), .trailing): + if self.model.shouldUpdateImagePosition(oldModel) { self.stackView.removeArrangedSubview(self.imageView) - self.stackView.addArrangedSubview(self.imageView) - default: - break + switch self.model.imageLocation { + case .leading: + self.stackView.insertArrangedSubview(self.imageView, at: 0) + case .trailing: + self.stackView.addArrangedSubview(self.imageView) + } } - if self.model.shouldUpdateSize(oldModel) { - self.invalidateIntrinsicContentSize() - + if self.model.shouldUpdateImageSize(oldModel) { self.imageViewConstraints.width?.constant = self.model.imageSide self.imageViewConstraints.height?.constant = self.model.imageSide + + UIView.performWithoutAnimation { + self.layoutIfNeeded() + } + } + + if self.model.shouldRecalculateSize(oldModel) { + self.invalidateIntrinsicContentSize() } } From 9243dcda45e7fd81ba451c77d7f8860eb8fdf634 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Tue, 8 Apr 2025 16:40:34 +0300 Subject: [PATCH 32/60] some fix stackView --- Sources/ComponentsKit/Components/Button/UKButton.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 1568a758..e8cbe3e3 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -78,9 +78,6 @@ open class UKButton: UIView, UKComponent { private func setup() { self.addSubview(self.stackView) - self.stackView.axis = .horizontal - self.stackView.alignment = .center - self.stackView.spacing = self.model.contentSpacing self.stackView.addArrangedSubview(self.imageView) self.stackView.addArrangedSubview(self.loaderView) @@ -243,6 +240,9 @@ extension UKButton { model: Model ) { stackView.spacing = model.contentSpacing + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = model.contentSpacing } static func imageView(_ imageView: UIImageView, model: Model) { From ae6e9c535ae1f9e74c891dce49fd60c30b52df47 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Tue, 8 Apr 2025 16:48:04 +0300 Subject: [PATCH 33/60] tint color fix --- 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 4f243485..78b38307 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -171,7 +171,7 @@ extension ButtonVM { case .sfSymbol(let name): return UIImage(systemName: name)?.withTintColor( self.foregroundColor.uiColor, - renderingMode: .alwaysTemplate + renderingMode: .alwaysOriginal ) case .local(let name, let bundle): return UIImage(named: name, in: bundle, compatibleWith: nil) From 3ff5b27c062d68750558593dbe0de655647d3243 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Tue, 8 Apr 2025 17:11:29 +0300 Subject: [PATCH 34/60] fix imageView location bug - the image view added based on the imagePosition value in the model --- Sources/ComponentsKit/Components/Button/UKButton.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index e8cbe3e3..49ce1b35 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -79,7 +79,13 @@ open class UKButton: UIView, UKComponent { private func setup() { self.addSubview(self.stackView) - self.stackView.addArrangedSubview(self.imageView) + switch self.model.imageLocation { + case .leading: + self.stackView.insertArrangedSubview(self.imageView, at: 0) + case .trailing: + self.stackView.addArrangedSubview(self.imageView) + } + self.stackView.addArrangedSubview(self.loaderView) self.stackView.addArrangedSubview(self.titleLabel) From 04ea424cb3b09af50e64e70f5a31f443bbdfa8c6 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 8 Apr 2025 16:42:08 +0200 Subject: [PATCH 35/60] improvements and bug fixes --- .../Components/Button/Models/ButtonVM.swift | 7 ------ .../Components/Button/UKButton.swift | 23 ++++++++----------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 78b38307..90c3bc45 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -1,4 +1,3 @@ -import SwiftUI import UIKit /// A model that defines the appearance properties for a button component. @@ -226,9 +225,3 @@ extension ButtonVM { return self.isFullWidth ? 10_000 : nil } } - -extension ButtonVM { - var imageTintColor: UIColor? { - return self.foregroundColor.uiColor - } -} diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 49ce1b35..113d2a63 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -79,6 +79,8 @@ open class UKButton: UIView, UKComponent { private func setup() { self.addSubview(self.stackView) + self.stackView.addArrangedSubview(self.loaderView) + self.stackView.addArrangedSubview(self.titleLabel) switch self.model.imageLocation { case .leading: self.stackView.insertArrangedSubview(self.imageView, at: 0) @@ -86,9 +88,6 @@ open class UKButton: UIView, UKComponent { self.stackView.addArrangedSubview(self.imageView) } - self.stackView.addArrangedSubview(self.loaderView) - self.stackView.addArrangedSubview(self.titleLabel) - if #available(iOS 17.0, *) { self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in view.handleTraitChanges() @@ -101,14 +100,9 @@ open class UKButton: UIView, UKComponent { private func style() { Self.Style.mainView(self, model: self.model) Self.Style.titleLabel(self.titleLabel, model: self.model) - Self.Style.configureStackView( - self.stackView, - model: self.model - ) + Self.Style.configureStackView(self.stackView, model: self.model) + Self.Style.loaderView(self.loaderView, model: self.model) Self.Style.imageView(self.imageView, model: self.model) - - self.loaderView.model = self.model.preferredLoadingVM - self.loaderView.isVisible = self.model.isLoading } // MARK: Layout @@ -135,9 +129,6 @@ open class UKButton: UIView, UKComponent { self.style() - self.loaderView.isVisible = self.model.isLoading - self.titleLabel.isHidden = self.model.title.isEmpty - if self.model.shouldUpdateImagePosition(oldModel) { self.stackView.removeArrangedSubview(self.imageView) switch self.model.imageLocation { @@ -240,6 +231,7 @@ extension UKButton { label.text = model.title label.font = model.preferredFont.uiFont label.textColor = model.foregroundColor.uiColor + label.isHidden = model.title.isEmpty } static func configureStackView( _ stackView: UIStackView, @@ -250,7 +242,10 @@ extension UKButton { stackView.alignment = .center stackView.spacing = model.contentSpacing } - + static func loaderView(_ view: UKLoading, model: Model) { + view.model = model.preferredLoadingVM + view.isVisible = model.isLoading + } static func imageView(_ imageView: UIImageView, model: Model) { imageView.image = model.image imageView.contentMode = .scaleAspectFit From bc5e1b3153cfd7e9ee155afff0afe823eab934bf Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 17 Apr 2025 12:11:47 +0200 Subject: [PATCH 36/60] change checkbox and checkmark sizes --- .../Checkbox/Models/CheckboxVM.swift | 31 ++++++++++++------- .../Components/Checkbox/SUCheckbox.swift | 27 +++++----------- .../Components/Checkbox/UKCheckbox.swift | 17 +--------- 3 files changed, 28 insertions(+), 47 deletions(-) diff --git a/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift b/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift index 56b824f4..29de9236 100644 --- a/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift +++ b/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift @@ -56,23 +56,16 @@ extension CheckboxVM { return self.title.isNil ? 0.0 : 8.0 } var checkmarkLineWidth: CGFloat { - switch self.size { - case .small: - return 1.5 - case .medium: - return 2.0 - case .large: - return 2.5 - } + return 1.5 } var checkboxSide: CGFloat { switch self.size { case .small: - return 16.0 - case .medium: return 20.0 - case .large: + case .medium: return 24.0 + case .large: + return 28.0 } } var checkboxCornerRadius: CGFloat { @@ -105,6 +98,22 @@ extension CheckboxVM { return .lgBody } } + var checkmarkPath: CGPath { + let path = UIBezierPath() + path.move(to: .init( + x: 7 / 24 * self.checkboxSide, + y: 12 / 24 * self.checkboxSide + )) + path.addLine(to: .init( + x: 11 / 24 * self.checkboxSide, + y: 16 / 24 * self.checkboxSide + )) + path.addLine(to: .init( + x: 17 / 24 * self.checkboxSide, + y: 8 / 24 * self.checkboxSide + )) + return path.cgPath + } } // MARK: UIKit Helpers diff --git a/Sources/ComponentsKit/Components/Checkbox/SUCheckbox.swift b/Sources/ComponentsKit/Components/Checkbox/SUCheckbox.swift index 0029acab..56db0274 100644 --- a/Sources/ComponentsKit/Components/Checkbox/SUCheckbox.swift +++ b/Sources/ComponentsKit/Components/Checkbox/SUCheckbox.swift @@ -45,27 +45,14 @@ public struct SUCheckbox: View { value: self.isSelected ) - Path { path in - path.move(to: .init( - x: self.model.checkboxSide / 4, - y: 11 / 24 * self.model.checkboxSide + Path(self.model.checkmarkPath) + .trim(from: 0, to: self.checkmarkStroke) + .stroke(style: StrokeStyle( + lineWidth: self.model.checkmarkLineWidth, + lineCap: .round, + lineJoin: .round )) - path.addLine(to: .init( - x: 11 / 24 * self.model.checkboxSide, - y: 17 / 24 * self.model.checkboxSide - )) - path.addLine(to: .init( - x: 3 / 4 * self.model.checkboxSide, - y: 7 / 24 * self.model.checkboxSide - )) - } - .trim(from: 0, to: self.checkmarkStroke) - .stroke(style: StrokeStyle( - lineWidth: self.model.checkmarkLineWidth, - lineCap: .round, - lineJoin: .round - )) - .foregroundStyle(self.model.foregroundColor.color) + .foregroundStyle(self.model.foregroundColor.color) } .overlay { RoundedRectangle(cornerRadius: self.model.checkboxCornerRadius) diff --git a/Sources/ComponentsKit/Components/Checkbox/UKCheckbox.swift b/Sources/ComponentsKit/Components/Checkbox/UKCheckbox.swift index 4a188c3f..164eeae0 100644 --- a/Sources/ComponentsKit/Components/Checkbox/UKCheckbox.swift +++ b/Sources/ComponentsKit/Components/Checkbox/UKCheckbox.swift @@ -96,22 +96,7 @@ open class UKCheckbox: UIView, UKComponent { self.checkmarkLayer.lineCap = .round self.checkmarkLayer.lineJoin = .round self.checkmarkLayer.strokeEnd = self.isSelected ? 1.0 : 0.0 - - let checkmarkPath = UIBezierPath() - checkmarkPath.move(to: .init( - x: self.model.checkboxSide / 4, - y: 11 / 24 * self.model.checkboxSide - )) - checkmarkPath.addLine(to: .init( - x: 11 / 24 * self.model.checkboxSide, - y: 17 / 24 * self.model.checkboxSide - )) - checkmarkPath.addLine(to: .init( - x: 3 / 4 * self.model.checkboxSide, - y: 7 / 24 * self.model.checkboxSide - )) - - self.checkmarkLayer.path = checkmarkPath.cgPath + self.checkmarkLayer.path = self.model.checkmarkPath } // MARK: Style From a799001c8ae6301382e0568102c1e7176eec9d56 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 17 Apr 2025 12:35:29 +0200 Subject: [PATCH 37/60] change checkmark line width --- .../Components/Checkbox/Models/CheckboxVM.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift b/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift index 29de9236..27f2f2a4 100644 --- a/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift +++ b/Sources/ComponentsKit/Components/Checkbox/Models/CheckboxVM.swift @@ -56,7 +56,14 @@ extension CheckboxVM { return self.title.isNil ? 0.0 : 8.0 } var checkmarkLineWidth: CGFloat { - return 1.5 + switch self.size { + case .small: + return 1.5 + case .medium: + return 1.75 + case .large: + return 2.0 + } } var checkboxSide: CGFloat { switch self.size { From 9d5e14d884f3e676f1addb2a47e49efe4bdeb31d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 17 Apr 2025 12:52:13 +0200 Subject: [PATCH 38/60] add `borderColor` param to `CardVM` --- .../PreviewPages/CardPreview.swift | 11 ++++++++++- .../Components/Card/Models/CardVM.swift | 13 ++++--------- Sources/ComponentsKit/Components/Card/SUCard.swift | 7 +++++-- Sources/ComponentsKit/Components/Card/UKCard.swift | 4 ++-- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift index 1543e844..1857d5a8 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift @@ -16,13 +16,22 @@ struct CardPreview: View { } Form { Picker("Background Color", selection: self.$model.backgroundColor) { - Text("Default").tag(Optional.none) + Text("Background").tag(UniversalColor.background) Text("Secondary Background").tag(UniversalColor.secondaryBackground) Text("Accent Background").tag(UniversalColor.accentBackground) Text("Success Background").tag(UniversalColor.successBackground) Text("Warning Background").tag(UniversalColor.warningBackground) Text("Danger Background").tag(UniversalColor.dangerBackground) } + Picker("Border Color", selection: self.$model.borderColor) { + Text("Divider").tag(UniversalColor.divider) + Text("Primary").tag(UniversalColor.primary) + Text("Accent").tag(UniversalColor.accent) + Text("Success").tag(UniversalColor.success) + Text("Warning").tag(UniversalColor.warning) + Text("Danger").tag(UniversalColor.danger) + Text("Custom").tag(UniversalColor.universal(.uiColor(.systemPurple))) + } BorderWidthPicker(selection: self.$model.borderWidth) Picker("Content Paddings", selection: self.$model.contentPaddings) { Text("12px").tag(Paddings(padding: 12)) diff --git a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift index a6baf586..fc9bbcdb 100644 --- a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift +++ b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift @@ -3,7 +3,10 @@ import Foundation /// A model that defines the appearance properties for a card component. public struct CardVM: ComponentVM { /// The background color of the card. - public var backgroundColor: UniversalColor? + public var backgroundColor: UniversalColor = .background + + /// The border color of the card. + public var borderColor: UniversalColor = .divider /// The border thickness of the card. /// @@ -28,11 +31,3 @@ public struct CardVM: ComponentVM { /// Initializes a new instance of `CardVM` with default values. public init() {} } - -// MARK: - Helpers - -extension CardVM { - var preferredBackgroundColor: UniversalColor { - return self.backgroundColor ?? .background - } -} diff --git a/Sources/ComponentsKit/Components/Card/SUCard.swift b/Sources/ComponentsKit/Components/Card/SUCard.swift index ad6be3dd..4e5646a9 100644 --- a/Sources/ComponentsKit/Components/Card/SUCard.swift +++ b/Sources/ComponentsKit/Components/Card/SUCard.swift @@ -39,11 +39,14 @@ public struct SUCard: View { public var body: some View { self.content() .padding(self.model.contentPaddings.edgeInsets) - .background(self.model.preferredBackgroundColor.color) + .background(self.model.backgroundColor.color) .cornerRadius(self.model.cornerRadius.value) .overlay( RoundedRectangle(cornerRadius: self.model.cornerRadius.value) - .stroke(UniversalColor.divider.color, lineWidth: self.model.borderWidth.value) + .stroke( + self.model.borderColor.color, + lineWidth: self.model.borderWidth.value + ) ) .shadow(self.model.shadow) } diff --git a/Sources/ComponentsKit/Components/Card/UKCard.swift b/Sources/ComponentsKit/Components/Card/UKCard.swift index bae38bde..2db3be2c 100644 --- a/Sources/ComponentsKit/Components/Card/UKCard.swift +++ b/Sources/ComponentsKit/Components/Card/UKCard.swift @@ -130,10 +130,10 @@ open class UKCard: UIView, UKComponent { extension UKCard { fileprivate enum Style { static func mainView(_ view: UIView, model: Model) { - view.backgroundColor = model.preferredBackgroundColor.uiColor + view.backgroundColor = model.backgroundColor.uiColor view.layer.cornerRadius = model.cornerRadius.value view.layer.borderWidth = model.borderWidth.value - view.layer.borderColor = UniversalColor.divider.cgColor + view.layer.borderColor = model.borderColor.cgColor view.shadow(model.shadow) } } From 459791324b54b12063a2f446936e91134a036896 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 17 Apr 2025 13:33:23 +0200 Subject: [PATCH 39/60] make card components tappable --- .../PreviewPages/CardPreview.swift | 2 + .../Components/Card/Models/CardVM.swift | 10 +++ .../Components/Card/SUCard.swift | 30 ++++++++- .../Components/Card/UKCard.swift | 64 ++++++++++++++++++- 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift index 1857d5a8..ce9e600f 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift @@ -15,6 +15,7 @@ struct CardPreview: View { SUCard(model: self.model, content: self.suCardContent) } Form { + AnimationScalePicker(selection: self.$model.animationScale) Picker("Background Color", selection: self.$model.backgroundColor) { Text("Background").tag(UniversalColor.background) Text("Secondary Background").tag(UniversalColor.secondaryBackground) @@ -48,6 +49,7 @@ struct CardPreview: View { Text("Large").tag(Shadow.large) Text("Custom").tag(Shadow.custom(20.0, .zero, UniversalColor.accentBackground)) } + Toggle("Tappable", isOn: self.$model.isTappable) } } } diff --git a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift index fc9bbcdb..2103b564 100644 --- a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift +++ b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift @@ -2,6 +2,11 @@ import Foundation /// A model that defines the appearance properties for a card component. public struct CardVM: ComponentVM { + /// The scaling factor for the card's tap animation, with a value between 0 and 1. + /// + /// Defaults to `.medium`. + public var animationScale: AnimationScale = .medium + /// The background color of the card. public var backgroundColor: UniversalColor = .background @@ -23,6 +28,11 @@ public struct CardVM: ComponentVM { /// Defaults to `.medium`. public var cornerRadius: ContainerRadius = .medium + /// A Boolean value indicating whether the card should allow to be tapped. + /// + /// Defaults to `true`. + public var isTappable: Bool = false + /// The shadow of the card. /// /// Defaults to `.medium`. diff --git a/Sources/ComponentsKit/Components/Card/SUCard.swift b/Sources/ComponentsKit/Components/Card/SUCard.swift index 4e5646a9..368db91a 100644 --- a/Sources/ComponentsKit/Components/Card/SUCard.swift +++ b/Sources/ComponentsKit/Components/Card/SUCard.swift @@ -16,8 +16,14 @@ public struct SUCard: View { /// A model that defines the appearance properties. public let model: CardVM + /// A closure that is triggered when the card is tapped. + public var onTap: () -> Void + + /// A Boolean value indicating whether the card is pressed. + @State public var isPressed: Bool = false @ViewBuilder private let content: () -> Content + @State private var contentSize: CGSize = .zero // MARK: - Initialization @@ -28,10 +34,12 @@ public struct SUCard: View { /// - content: The content that is displayed in the card. public init( model: CardVM = .init(), - content: @escaping () -> Content + content: @escaping () -> Content, + onTap: @escaping () -> Void = {} ) { self.model = model self.content = content + self.onTap = onTap } // MARK: - Body @@ -49,5 +57,25 @@ public struct SUCard: View { ) ) .shadow(self.model.shadow) + .observeSize { self.contentSize = $0 } + .simultaneousGesture(DragGesture(minimumDistance: 0.0) + .onChanged { _ in + guard self.model.isTappable else { return } + self.isPressed = true + } + .onEnded { value in + guard self.model.isTappable else { return } + + defer { self.isPressed = false } + + if CGRect(origin: .zero, size: self.contentSize).contains(value.location) { + self.onTap() + } + } + ) + .scaleEffect( + self.isPressed ? self.model.animationScale.value : 1, + anchor: .center + ) } } diff --git a/Sources/ComponentsKit/Components/Card/UKCard.swift b/Sources/ComponentsKit/Components/Card/UKCard.swift index 2db3be2c..aa634187 100644 --- a/Sources/ComponentsKit/Components/Card/UKCard.swift +++ b/Sources/ComponentsKit/Components/Card/UKCard.swift @@ -21,9 +21,22 @@ open class UKCard: UIView, UKComponent { /// The primary content of the card, provided as a custom view. public let content: Content - // MARK: - Properties + // MARK: - Public Properties - private var contentConstraints = LayoutConstraints() + /// A closure that is triggered when the card is tapped. + public var onTap: () -> Void + + /// A Boolean value indicating whether the button is pressed. + public private(set) var isPressed: Bool = false { + didSet { + self.transform = self.isPressed + ? .init( + scaleX: self.model.animationScale.value, + y: self.model.animationScale.value + ) + : .identity + } + } /// A model that defines the appearance properties. public var model: CardVM { @@ -32,6 +45,10 @@ open class UKCard: UIView, UKComponent { } } + // MARK: - Private Properties + + private var contentConstraints = LayoutConstraints() + // MARK: - Initialization /// Initializer. @@ -41,10 +58,12 @@ 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, + onTap: @escaping () -> Void = {} ) { self.model = model self.content = content() + self.onTap = onTap super.init(frame: .zero) @@ -95,6 +114,8 @@ open class UKCard: UIView, UKComponent { self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath } + // MARK: - Update + /// Updates appearance when the model changes. open func update(_ oldValue: CardVM) { guard self.model != oldValue else { return } @@ -113,6 +134,43 @@ open class UKCard: UIView, UKComponent { // MARK: - UIView Methods + open override func touchesBegan( + _ touches: Set, + with event: UIEvent? + ) { + super.touchesBegan(touches, with: event) + + guard self.model.isTappable else { return } + + self.isPressed = true + } + + open override func touchesEnded( + _ touches: Set, + with event: UIEvent? + ) { + super.touchesEnded(touches, with: event) + + guard self.model.isTappable else { return } + + defer { self.isPressed = false } + + if self.model.isTappable, + let location = touches.first?.location(in: self), + self.bounds.contains(location) { + self.onTap() + } + } + + open override func touchesCancelled( + _ touches: Set, + with event: UIEvent? + ) { + super.touchesCancelled(touches, with: event) + + self.isPressed = false + } + open override func traitCollectionDidChange( _ previousTraitCollection: UITraitCollection? ) { From 71adcf1b25cb082668155d9948a28a5215392603 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 18 Apr 2025 14:18:28 +0200 Subject: [PATCH 40/60] add no focus init to `SUInputField` --- .../Components/InputField/SUInputField.swift | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/Sources/ComponentsKit/Components/InputField/SUInputField.swift b/Sources/ComponentsKit/Components/InputField/SUInputField.swift index a217a258..aa735b3d 100644 --- a/Sources/ComponentsKit/Components/InputField/SUInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/SUInputField.swift @@ -14,7 +14,7 @@ public struct SUInputField: View { /// /// When the `localFocus` value matches `globalFocus`, this input field becomes focused. /// This enables centralized focus management for multiple text inputs and input fields within a single view. - @FocusState.Binding public var globalFocus: FocusValue + public let globalFocus: FocusState.Binding? /// The unique value for this field to match against the global focus state to determine whether this input field is focused. /// @@ -24,7 +24,7 @@ public struct SUInputField: View { /// /// - Warning: The `localFocus` value must be unique to each text input and input field, to ensure that different /// text inputs and input fields within the same view can be independently focused based on the shared `globalFocus`. - public var localFocus: FocusValue + public let localFocus: FocusValue // MARK: Initialization @@ -41,7 +41,7 @@ public struct SUInputField: View { model: InputFieldVM = .init() ) { self._text = text - self._globalFocus = globalFocus + self.globalFocus = globalFocus self.localFocus = localFocus self.model = model } @@ -71,7 +71,7 @@ public struct SUInputField: View { .tint(self.model.tintColor.color) .font(self.model.preferredFont.font) .foregroundStyle(self.model.foregroundColor.color) - .focused(self.$globalFocus, equals: self.localFocus) + .applyFocus(globalFocus: self.globalFocus, localFocus: self.localFocus) .disabled(!self.model.isEnabled) .keyboardType(self.model.keyboardType) .submitLabel(self.model.submitType.submitLabel) @@ -82,7 +82,7 @@ public struct SUInputField: View { .frame(height: self.model.height) .background(self.model.backgroundColor.color) .onTapGesture { - self.globalFocus = self.localFocus + self.globalFocus?.wrappedValue = self.localFocus } .clipShape( RoundedRectangle( @@ -92,6 +92,22 @@ public struct SUInputField: View { } } +// MARK: Helpers + +extension View { + @ViewBuilder + fileprivate func applyFocus( + globalFocus: FocusState.Binding?, + localFocus: FocusValue, + ) -> some View { + if let globalFocus { + self.focused(globalFocus, equals: localFocus) + } else { + self + } + } +} + // MARK: - Boolean Focus Value extension SUInputField where FocusValue == Bool { @@ -106,7 +122,25 @@ extension SUInputField where FocusValue == Bool { model: InputFieldVM = .init() ) { self._text = text - self._globalFocus = isFocused + self.globalFocus = isFocused + self.localFocus = true + self.model = model + } +} + +// MARK: - No Focus Value + +extension SUInputField where FocusValue == Bool { + /// Initializer. + /// - Parameters: + /// - text: A Binding value to control the inputted text. + /// - model: A model that defines the appearance properties. + public init( + text: Binding, + model: InputFieldVM = .init() + ) { + self._text = text + self.globalFocus = nil self.localFocus = true self.model = model } From a7b57eef99f5cf3ea9c1aa65a20573e1c9129f13 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 18 Apr 2025 14:24:39 +0200 Subject: [PATCH 41/60] add no focus init to `SUTextInput` --- .../Components/TextInput/SUTextInput.swift | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift index 47e68014..d91b66d9 100644 --- a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift @@ -14,7 +14,7 @@ public struct SUTextInput: View { /// /// When the `localFocus` value matches `globalFocus`, this text input becomes focused. /// This enables centralized focus management for multiple text inputs and input fields within a single view. - @FocusState.Binding public var globalFocus: FocusValue + public let globalFocus: FocusState.Binding? /// The unique value for this field to match against the global focus state to determine whether this text input is focused. /// @@ -24,7 +24,7 @@ public struct SUTextInput: View { /// /// - Warning: The `localFocus` value must be unique to each text input and input field, to ensure that different /// text inputs and input fields within the same view can be independently focused based on the shared `globalFocus`. - public var localFocus: FocusValue + public let localFocus: FocusValue @State private var textEditorPreferredHeight: CGFloat = 0 @@ -43,7 +43,7 @@ public struct SUTextInput: View { model: TextInputVM = .init() ) { self._text = text - self._globalFocus = globalFocus + self.globalFocus = globalFocus self.localFocus = localFocus self.model = model } @@ -67,7 +67,7 @@ public struct SUTextInput: View { .font(self.model.preferredFont.font) .foregroundStyle(self.model.foregroundColor.color) .tint(self.model.tintColor.color) - .focused(self.$globalFocus, equals: self.localFocus) + .applyFocus(globalFocus: self.globalFocus, localFocus: self.localFocus) .disabled(!self.model.isEnabled) .keyboardType(self.model.keyboardType) .submitLabel(self.model.submitType.submitLabel) @@ -154,6 +154,18 @@ extension View { UITextView.appearance().textContainer.lineFragmentPadding = 0 } } + + @ViewBuilder + fileprivate func applyFocus( + globalFocus: FocusState.Binding?, + localFocus: FocusValue, + ) -> some View { + if let globalFocus { + self.focused(globalFocus, equals: localFocus) + } else { + self + } + } } // MARK: - Boolean Focus Value @@ -170,7 +182,25 @@ extension SUTextInput where FocusValue == Bool { model: TextInputVM = .init() ) { self._text = text - self._globalFocus = isFocused + self.globalFocus = isFocused + self.localFocus = true + self.model = model + } +} + +// MARK: - No Focus Value + +extension SUTextInput where FocusValue == Bool { + /// Initializer. + /// - Parameters: + /// - text: A Binding value to control the inputted text. + /// - model: A model that defines the appearance properties. + public init( + text: Binding, + model: TextInputVM = .init() + ) { + self._text = text + self.globalFocus = nil self.localFocus = true self.model = model } From ca0ee89a6b440b897462852a243374e36c230ce0 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 18 Apr 2025 14:42:11 +0200 Subject: [PATCH 42/60] fix text container paddings in `UKTextInput` for ios 16 --- Sources/ComponentsKit/Components/TextInput/UKTextInput.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift b/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift index 8e4b8519..ab169844 100644 --- a/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift @@ -205,8 +205,11 @@ extension UKTextInput { } static func textView(_ textView: UITextView, padding: CGFloat) { - textView.textContainerInset = .init(inset: padding) textView.textContainer.lineFragmentPadding = 0 + textView.textContainerInset.top = padding + textView.textContainerInset.left = padding + textView.textContainerInset.right = padding + textView.textContainerInset.bottom = padding } static func placeholder( From 4317b07ea2853fb782d28b0e821ab4522a4393ff Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 18 Apr 2025 16:28:37 +0200 Subject: [PATCH 43/60] add `TitlePosition` param to `InputFieldVM` and implement in `SUInputField` --- .../Models/InputFieldTitlePosition.swift | 8 ++ .../InputField/Models/InputFieldVM.swift | 13 +++- .../Components/InputField/SUInputField.swift | 75 ++++++++++--------- 3 files changed, 61 insertions(+), 35 deletions(-) create mode 100644 Sources/ComponentsKit/Components/InputField/Models/InputFieldTitlePosition.swift diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldTitlePosition.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldTitlePosition.swift new file mode 100644 index 00000000..9f94afb9 --- /dev/null +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldTitlePosition.swift @@ -0,0 +1,8 @@ +import Foundation + +extension InputFieldVM { + public enum TitlePosition { + case inside + case outside + } +} diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift index 7ffd3b73..319bc777 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -67,6 +67,8 @@ public struct InputFieldVM: ComponentVM { /// The title displayed on the input field. public var title: String? + public var titlePosition: TitlePosition = .inside + /// Initializes a new instance of `InputFieldVM` with default values. public init() {} } @@ -104,7 +106,16 @@ extension InputFieldVM { } } var spacing: CGFloat { - return self.title.isNotNilAndEmpty ? 12 : 0 + guard self.title.isNotNilAndEmpty else { + return 0 + } + + switch self.titlePosition { + case .inside: + return 12 + case .outside: + return 8 + } } var backgroundColor: UniversalColor { return self.color?.background ?? .content1 diff --git a/Sources/ComponentsKit/Components/InputField/SUInputField.swift b/Sources/ComponentsKit/Components/InputField/SUInputField.swift index aa735b3d..fa51010f 100644 --- a/Sources/ComponentsKit/Components/InputField/SUInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/SUInputField.swift @@ -49,46 +49,53 @@ public struct SUInputField: View { // MARK: Body public var body: some View { - HStack(spacing: self.model.spacing) { - if let title = self.model.attributedTitle { + VStack(alignment: .leading, spacing: self.model.spacing) { + if let title = self.model.attributedTitle, + self.model.titlePosition == .outside { Text(title) - .font(self.model.preferredFont.font) } - Group { - if self.model.isSecureInput { - SecureField(text: self.$text, label: { - Text(self.model.placeholder ?? "") - .foregroundStyle(self.model.placeholderColor.color) - }) - } else { - TextField(text: self.$text, label: { - Text(self.model.placeholder ?? "") - .foregroundStyle(self.model.placeholderColor.color) - }) + HStack(spacing: self.model.spacing) { + if let title = self.model.attributedTitle, + self.model.titlePosition == .inside { + Text(title) } + + Group { + if self.model.isSecureInput { + SecureField(text: self.$text, label: { + Text(self.model.placeholder ?? "") + .foregroundStyle(self.model.placeholderColor.color) + }) + } else { + TextField(text: self.$text, label: { + Text(self.model.placeholder ?? "") + .foregroundStyle(self.model.placeholderColor.color) + }) + } + } + .tint(self.model.tintColor.color) + .font(self.model.preferredFont.font) + .foregroundStyle(self.model.foregroundColor.color) + .applyFocus(globalFocus: self.globalFocus, localFocus: self.localFocus) + .disabled(!self.model.isEnabled) + .keyboardType(self.model.keyboardType) + .submitLabel(self.model.submitType.submitLabel) + .autocorrectionDisabled(!self.model.isAutocorrectionEnabled) + .textInputAutocapitalization(self.model.autocapitalization.textInputAutocapitalization) } - .tint(self.model.tintColor.color) - .font(self.model.preferredFont.font) - .foregroundStyle(self.model.foregroundColor.color) - .applyFocus(globalFocus: self.globalFocus, localFocus: self.localFocus) - .disabled(!self.model.isEnabled) - .keyboardType(self.model.keyboardType) - .submitLabel(self.model.submitType.submitLabel) - .autocorrectionDisabled(!self.model.isAutocorrectionEnabled) - .textInputAutocapitalization(self.model.autocapitalization.textInputAutocapitalization) - } - .padding(.horizontal, self.model.horizontalPadding) - .frame(height: self.model.height) - .background(self.model.backgroundColor.color) - .onTapGesture { - self.globalFocus?.wrappedValue = self.localFocus - } - .clipShape( - RoundedRectangle( - cornerRadius: self.model.cornerRadius.value() + .padding(.horizontal, self.model.horizontalPadding) + .frame(height: self.model.height) + .background(self.model.backgroundColor.color) + .onTapGesture { + self.globalFocus?.wrappedValue = self.localFocus + } + .clipShape( + RoundedRectangle( + cornerRadius: self.model.cornerRadius.value() + ) ) - ) + } } } From 0a1554b549a9e33d4629d007f60b09c144379286 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 22 Apr 2025 16:18:43 +0300 Subject: [PATCH 44/60] add bordered style for input field --- .../PreviewPages/InputFieldPreview.swift | 8 ++++++ .../InputField/Models/InputFieldStyle.swift | 8 ++++++ .../InputField/Models/InputFieldVM.swift | 27 ++++++++++++++++++- .../Components/InputField/SUInputField.swift | 11 +++++++- .../Components/InputField/UKInputField.swift | 21 ++++++++++++++- 5 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift index 3851467d..042dd19c 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift @@ -52,6 +52,10 @@ struct InputFieldPreview: View { Toggle("Required", isOn: self.$model.isRequired) Toggle("Secure Input", isOn: self.$model.isSecureInput) SizePicker(selection: self.$model.size) + Picker("Style", selection: self.$model.style) { + Text("Light").tag(InputFieldVM.Style.light) + Text("Bordered").tag(InputFieldVM.Style.bordered) + } SubmitTypePicker(selection: self.$model.submitType) UniversalColorPicker( title: "Tint Color", @@ -65,6 +69,10 @@ struct InputFieldPreview: View { self.model.title = newValue ? "Title" : nil } )) + Picker("Title Position", selection: self.$model.titlePosition) { + Text("Inside").tag(InputFieldVM.TitlePosition.inside) + Text("Outside").tag(InputFieldVM.TitlePosition.outside) + } } } .toolbar { diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift new file mode 100644 index 00000000..449826c3 --- /dev/null +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift @@ -0,0 +1,8 @@ +import Foundation + +extension InputFieldVM { + public enum Style: Hashable { + case light + case bordered + } +} diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift index 319bc777..2eae5e62 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -54,6 +54,8 @@ public struct InputFieldVM: ComponentVM { /// Defaults to `.medium`. public var size: ComponentSize = .medium + public var style: Style = .light + /// The type of the submit button on the keyboard. /// /// Defaults to `.return`. @@ -118,7 +120,12 @@ extension InputFieldVM { } } var backgroundColor: UniversalColor { - return self.color?.background ?? .content1 + switch self.style { + case .light: + return self.color?.background ?? .content1 + case .bordered: + return .background + } } var foregroundColor: UniversalColor { let color = self.color?.main ?? .foreground @@ -131,6 +138,24 @@ extension InputFieldVM { return .secondaryForeground.enabled(self.isEnabled) } } + var borderWidth: CGFloat { + switch self.style { + case .light: + return 0 + case .bordered: + switch self.size { + case .small: + return BorderWidth.small.value + case .medium: + return BorderWidth.medium.value + case .large: + return BorderWidth.large.value + } + } + } + var borderColor: UniversalColor { + return (self.color?.main ?? .content3).enabled(self.isEnabled) + } } // MARK: - UIKit Helpers diff --git a/Sources/ComponentsKit/Components/InputField/SUInputField.swift b/Sources/ComponentsKit/Components/InputField/SUInputField.swift index fa51010f..f9e566af 100644 --- a/Sources/ComponentsKit/Components/InputField/SUInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/SUInputField.swift @@ -92,7 +92,16 @@ public struct SUInputField: View { } .clipShape( RoundedRectangle( - cornerRadius: self.model.cornerRadius.value() + cornerRadius: self.model.cornerRadius.value(), + ) + ) + .overlay( + RoundedRectangle( + cornerRadius: self.model.cornerRadius.value(), + ) + .stroke( + self.model.borderColor.color, + lineWidth: self.model.borderWidth ) ) } diff --git a/Sources/ComponentsKit/Components/InputField/UKInputField.swift b/Sources/ComponentsKit/Components/InputField/UKInputField.swift index 2300b4f7..4121d0ce 100644 --- a/Sources/ComponentsKit/Components/InputField/UKInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/UKInputField.swift @@ -83,6 +83,12 @@ open class UKInputField: UIView, UKComponent { self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap))) self.textField.addTarget(self, action: #selector(self.handleTextChange), for: .editingChanged) + + if #available(iOS 17.0, *) { + self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in + view.handleTraitChanges() + } + } } @objc private func handleTap() { @@ -168,8 +174,19 @@ open class UKInputField: UIView, UKComponent { height: min(size.height, self.model.height) ) } - + + open override func traitCollectionDidChange( + _ previousTraitCollection: UITraitCollection? + ) { + super.traitCollectionDidChange(previousTraitCollection) + self.handleTraitChanges() + } + // MARK: Helpers + + @objc private func handleTraitChanges() { + Self.Style.mainView(self, model: self.model) + } private func updateCornerRadius() { self.layer.cornerRadius = self.model.cornerRadius.value(for: self.bounds.height) @@ -186,6 +203,8 @@ extension UKInputField { ) { view.backgroundColor = model.backgroundColor.uiColor view.layer.cornerRadius = model.cornerRadius.value(for: view.bounds.height) + view.layer.borderWidth = model.borderWidth + view.layer.borderColor = model.borderColor.cgColor } static func titleLabel( _ label: UILabel, From 0a9ca09ae1596b1aa1dddf7560c3e452c20b7961 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 13:42:51 +0300 Subject: [PATCH 45/60] update `UKInputField` to support `titlePosition` --- .../PreviewPages/InputFieldPreview.swift | 9 +- .../InputField/Models/InputFieldVM.swift | 12 +- .../Components/InputField/UKInputField.swift | 114 ++++++++++++------ 3 files changed, 85 insertions(+), 50 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift index 042dd19c..0cd0b531 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift @@ -46,7 +46,7 @@ struct InputFieldPreview: View { return self.model.placeholder != nil }, set: { newValue in - self.model.placeholder = newValue ? "Placeholder" : nil + self.model.placeholder = newValue ? Self.placeholder : nil } )) Toggle("Required", isOn: self.$model.isRequired) @@ -66,7 +66,7 @@ struct InputFieldPreview: View { return self.model.title != nil }, set: { newValue in - self.model.title = newValue ? "Title" : nil + self.model.title = newValue ? Self.title : nil } )) Picker("Title Position", selection: self.$model.titlePosition) { @@ -87,9 +87,12 @@ struct InputFieldPreview: View { } } + private static let title = "Title" + private static let placeholder = "Placeholder" private static var initialModel: InputFieldVM { return .init { - $0.title = "Title" + $0.title = Self.title + $0.placeholder = Self.placeholder } } } diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift index 2eae5e62..9429a5f4 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -108,10 +108,6 @@ extension InputFieldVM { } } var spacing: CGFloat { - guard self.title.isNotNilAndEmpty else { - return 0 - } - switch self.titlePosition { case .inside: return 12 @@ -203,14 +199,16 @@ extension InputFieldVM { } return attributedString } + func shouldUpdateTitlePosition(_ oldModel: Self) -> Bool { + return self.titlePosition != oldModel.titlePosition + } func shouldUpdateLayout(_ oldModel: Self) -> Bool { return self.size != oldModel.size || self.horizontalPadding != oldModel.horizontalPadding || self.spacing != oldModel.spacing || self.cornerRadius != oldModel.cornerRadius - } - func shouldUpdateCornerRadius(_ oldModel: Self) -> Bool { - return self.cornerRadius != oldModel.cornerRadius + || self.titlePosition != oldModel.titlePosition + || self.title.isNilOrEmpty != oldModel.title.isNilOrEmpty } } diff --git a/Sources/ComponentsKit/Components/InputField/UKInputField.swift b/Sources/ComponentsKit/Components/InputField/UKInputField.swift index 4121d0ce..49129599 100644 --- a/Sources/ComponentsKit/Components/InputField/UKInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/UKInputField.swift @@ -3,7 +3,7 @@ import UIKit /// A UIKit component that displays a field to input a text. open class UKInputField: UIView, UKComponent { - // MARK: Properties + // MARK: Public Properties /// A closure that is triggered when the text changes. public var onValueChange: (String) -> Void @@ -28,15 +28,23 @@ open class UKInputField: UIView, UKComponent { } } - private var titleLabelConstraints: LayoutConstraints? - private var inputFieldConstraints: LayoutConstraints? - // MARK: Subviews /// A label that displays the title from the model. public var titleLabel = UILabel() /// An underlying text field from the standard library. public var textField = UITextField() + /// A view that contains `horizontalStackView` and `titleLabel` whet it is outside. + public var textFieldContainer = UIView() + /// A stack view that contains `textField` and `titleLabel` whet it is inside. + public var horizontalStackView = UIStackView() + /// A stack view that contains `textFieldContainer`. + public var verticalStackView = UIStackView() + + // MARK: Private Properties + + private var textFieldContainerConstraints = LayoutConstraints() + private var horizontalStackViewConstraints = LayoutConstraints() // MARK: UIView Properties @@ -78,10 +86,18 @@ open class UKInputField: UIView, UKComponent { // MARK: Setup private func setup() { - self.addSubview(self.titleLabel) - self.addSubview(self.textField) + self.addSubview(self.verticalStackView) + switch self.model.titlePosition { + case .inside: + self.horizontalStackView.addArrangedSubview(self.titleLabel) + case .outside: + self.verticalStackView.addArrangedSubview(self.titleLabel) + } + self.verticalStackView.addArrangedSubview(self.textFieldContainer) + self.horizontalStackView.addArrangedSubview(self.textField) + self.textFieldContainer.addSubview(self.horizontalStackView) - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap))) + self.textFieldContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap))) self.textField.addTarget(self, action: #selector(self.handleTextChange), for: .editingChanged) if #available(iOS 17.0, *) { @@ -102,7 +118,9 @@ open class UKInputField: UIView, UKComponent { // MARK: Style private func style() { - Self.Style.mainView(self, model: self.model) + Self.Style.textFieldContainer(self.textFieldContainer, model: self.model) + Self.Style.horizontalStackView(self.horizontalStackView, model: self.model) + Self.Style.verticalStackView(self.verticalStackView, model: self.model) Self.Style.textField(self.textField, model: self.model) Self.Style.titleLabel(self.titleLabel, model: self.model) } @@ -110,27 +128,18 @@ open class UKInputField: UIView, UKComponent { // MARK: Layout private func layout() { - self.titleLabelConstraints = self.titleLabel.leading(self.model.horizontalPadding) - self.titleLabel.centerVertically() + self.verticalStackView.allEdges() - self.textField.trailing(self.model.horizontalPadding) - self.textField.vertically() + self.textFieldContainerConstraints = self.textFieldContainer.height(self.model.height) + self.textFieldContainer.horizontally() - self.inputFieldConstraints = self.textField.after( - self.titleLabel, - padding: self.model.spacing - ) + self.horizontalStackView.vertically() + self.horizontalStackViewConstraints = self.horizontalStackView.horizontally(self.model.horizontalPadding) self.textField.setContentHuggingPriority(.defaultLow, for: .horizontal) self.titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) } - open override func layoutSubviews() { - super.layoutSubviews() - - self.updateCornerRadius() - } - // MARK: Update public func update(_ oldModel: InputFieldVM) { @@ -138,10 +147,19 @@ open class UKInputField: UIView, UKComponent { self.style() - self.inputFieldConstraints?.leading?.constant = self.model.spacing - self.titleLabelConstraints?.leading?.constant = self.model.horizontalPadding - if self.model.shouldUpdateCornerRadius(oldModel) { - self.updateCornerRadius() + self.horizontalStackViewConstraints.leading?.constant = self.model.horizontalPadding + self.horizontalStackViewConstraints.trailing?.constant = -self.model.horizontalPadding + self.textFieldContainerConstraints.height?.constant = self.model.height + + if self.model.shouldUpdateTitlePosition(oldModel) { + switch self.model.titlePosition { + case .inside: + self.verticalStackView.removeArrangedSubview(self.titleLabel) + self.horizontalStackView.insertArrangedSubview(self.titleLabel, at: 0) + case .outside: + self.horizontalStackView.removeArrangedSubview(self.titleLabel) + self.verticalStackView.insertArrangedSubview(self.titleLabel, at: 0) + } } if self.model.shouldUpdateLayout(oldModel) { self.setNeedsLayout() @@ -169,27 +187,26 @@ open class UKInputField: UIView, UKComponent { } else { width = 10_000 } + + let height = self.verticalStackView.sizeThatFits(UIView.layoutFittingCompressedSize).height + return .init( width: min(size.width, width), - height: min(size.height, self.model.height) + height: min(size.height, height) ) } - + open override func traitCollectionDidChange( _ previousTraitCollection: UITraitCollection? ) { super.traitCollectionDidChange(previousTraitCollection) self.handleTraitChanges() } - + // MARK: Helpers - - @objc private func handleTraitChanges() { - Self.Style.mainView(self, model: self.model) - } - private func updateCornerRadius() { - self.layer.cornerRadius = self.model.cornerRadius.value(for: self.bounds.height) + @objc private func handleTraitChanges() { + Self.Style.textFieldContainer(self.textFieldContainer, model: self.model) } } @@ -197,24 +214,25 @@ open class UKInputField: UIView, UKComponent { extension UKInputField { fileprivate enum Style { - static func mainView( + static func textFieldContainer( _ view: UIView, - model: InputFieldVM + model: Model ) { view.backgroundColor = model.backgroundColor.uiColor - view.layer.cornerRadius = model.cornerRadius.value(for: view.bounds.height) + view.layer.cornerRadius = model.cornerRadius.value(for: model.height) view.layer.borderWidth = model.borderWidth view.layer.borderColor = model.borderColor.cgColor } static func titleLabel( _ label: UILabel, - model: InputFieldVM + model: Model ) { label.attributedText = model.nsAttributedTitle + label.isVisible = model.title.isNotNilAndEmpty } static func textField( _ textField: UITextField, - model: InputFieldVM + model: Model ) { textField.font = model.preferredFont.uiFont textField.textColor = model.foregroundColor.uiColor @@ -227,5 +245,21 @@ extension UKInputField { textField.autocorrectionType = model.autocorrectionType textField.autocapitalizationType = model.autocapitalization.textAutocapitalizationType } + static func horizontalStackView( + _ stackView: UIStackView, + model: Model + ) { + stackView.axis = .horizontal + stackView.spacing = model.spacing + } + static func verticalStackView( + _ stackView: UIStackView, + model: Model + ) { + stackView.axis = .vertical + stackView.spacing = model.spacing + stackView.alignment = .leading + stackView.distribution = .fillProportionally + } } } From 8efeecaf6bb4892ca2fdf85cc67c733f1049ed49 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 14:15:26 +0300 Subject: [PATCH 46/60] add caption and titleFont for input fields --- .../PreviewPages/InputFieldPreview.swift | 16 ++++++- .../InputField/Models/InputFieldVM.swift | 44 +++++++++++++++++-- .../Components/InputField/SUInputField.swift | 6 +++ .../Components/InputField/UKInputField.swift | 13 ++++++ 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift index 0cd0b531..7b5ef151 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift @@ -34,6 +34,15 @@ struct InputFieldPreview: View { Form { AutocapitalizationPicker(selection: self.$model.autocapitalization) Toggle("Autocorrection Enabled", isOn: self.$model.isAutocorrectionEnabled) + Toggle("Caption", isOn: .init( + get: { + return self.model.caption != nil + }, + set: { newValue in + self.model.caption = newValue ? Self.caption : nil + } + )) + CaptionFontPicker(title: "Caption Font", selection: self.$model.captionFont) ComponentOptionalColorPicker(selection: self.$model.color) ComponentRadiusPicker(selection: self.$model.cornerRadius) { Text("Custom: 20px").tag(ComponentRadius.custom(20)) @@ -69,6 +78,7 @@ struct InputFieldPreview: View { self.model.title = newValue ? Self.title : nil } )) + BodyFontPicker(title: "Title Font", selection: self.$model.titleFont) Picker("Title Position", selection: self.$model.titlePosition) { Text("Inside").tag(InputFieldVM.TitlePosition.inside) Text("Outside").tag(InputFieldVM.TitlePosition.outside) @@ -87,12 +97,14 @@ struct InputFieldPreview: View { } } - private static let title = "Title" - private static let placeholder = "Placeholder" + private static let title = "Email" + private static let placeholder = "Enter your email" + private static let caption = "We'll send you a verification code" private static var initialModel: InputFieldVM { return .init { $0.title = Self.title $0.placeholder = Self.placeholder + $0.caption = Self.caption } } } diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift index 9429a5f4..680abf05 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -8,6 +8,10 @@ public struct InputFieldVM: ComponentVM { /// Defaults to `.sentences`, which capitalizes the first letter of each sentence. public var autocapitalization: TextAutocapitalization = .sentences + public var caption: String? + + public var captionFont: UniversalFont? + /// The color of the input field. public var color: ComponentColor? @@ -69,6 +73,8 @@ public struct InputFieldVM: ComponentVM { /// The title displayed on the input field. public var title: String? + public var titleFont: UniversalFont? + public var titlePosition: TitlePosition = .inside /// Initializes a new instance of `InputFieldVM` with default values. @@ -92,6 +98,34 @@ extension InputFieldVM { return .lgBody } } + var preferredTitleFont: UniversalFont { + if let titleFont { + return titleFont + } + + switch self.size { + case .small: + return .smBody + case .medium: + return .mdBody + case .large: + return .lgBody + } + } + var preferredCaptionFont: UniversalFont { + if let captionFont { + return captionFont + } + + switch self.size { + case .small: + return .smCaption + case .medium: + return .mdCaption + case .large: + return .lgCaption + } + } var height: CGFloat { return switch self.size { case .small: 40 @@ -124,8 +158,7 @@ extension InputFieldVM { } } var foregroundColor: UniversalColor { - let color = self.color?.main ?? .foreground - return color.enabled(self.isEnabled) + return (self.color?.main ?? .foreground).enabled(self.isEnabled) } var placeholderColor: UniversalColor { if let color { @@ -134,6 +167,9 @@ extension InputFieldVM { return .secondaryForeground.enabled(self.isEnabled) } } + var captionColor: UniversalColor { + return (self.color?.main ?? .secondaryForeground).enabled(self.isEnabled) + } var borderWidth: CGFloat { switch self.style { case .light: @@ -178,7 +214,7 @@ extension InputFieldVM { attributedString.append(NSAttributedString( string: title, attributes: [ - .font: self.preferredFont.uiFont, + .font: self.preferredTitleFont.uiFont, .foregroundColor: self.foregroundColor.uiColor ] )) @@ -192,7 +228,7 @@ extension InputFieldVM { attributedString.append(NSAttributedString( string: "*", attributes: [ - .font: self.preferredFont.uiFont, + .font: self.preferredTitleFont.uiFont, .foregroundColor: UniversalColor.danger.uiColor ] )) diff --git a/Sources/ComponentsKit/Components/InputField/SUInputField.swift b/Sources/ComponentsKit/Components/InputField/SUInputField.swift index f9e566af..022a64f8 100644 --- a/Sources/ComponentsKit/Components/InputField/SUInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/SUInputField.swift @@ -104,6 +104,12 @@ public struct SUInputField: View { lineWidth: self.model.borderWidth ) ) + + if let caption = self.model.caption, caption.isNotEmpty { + Text(caption) + .font(self.model.preferredCaptionFont.font) + .foregroundStyle(self.model.captionColor.color) + } } } } diff --git a/Sources/ComponentsKit/Components/InputField/UKInputField.swift b/Sources/ComponentsKit/Components/InputField/UKInputField.swift index 49129599..90d51299 100644 --- a/Sources/ComponentsKit/Components/InputField/UKInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/UKInputField.swift @@ -34,6 +34,8 @@ open class UKInputField: UIView, UKComponent { public var titleLabel = UILabel() /// An underlying text field from the standard library. public var textField = UITextField() + /// A label that displays the caption from the model. + public var captionLabel = UILabel() /// A view that contains `horizontalStackView` and `titleLabel` whet it is outside. public var textFieldContainer = UIView() /// A stack view that contains `textField` and `titleLabel` whet it is inside. @@ -94,6 +96,7 @@ open class UKInputField: UIView, UKComponent { self.verticalStackView.addArrangedSubview(self.titleLabel) } self.verticalStackView.addArrangedSubview(self.textFieldContainer) + self.verticalStackView.addArrangedSubview(self.captionLabel) self.horizontalStackView.addArrangedSubview(self.textField) self.textFieldContainer.addSubview(self.horizontalStackView) @@ -123,6 +126,7 @@ open class UKInputField: UIView, UKComponent { Self.Style.verticalStackView(self.verticalStackView, model: self.model) Self.Style.textField(self.textField, model: self.model) Self.Style.titleLabel(self.titleLabel, model: self.model) + Self.Style.captionLabel(self.captionLabel, model: self.model) } // MARK: Layout @@ -245,6 +249,15 @@ extension UKInputField { textField.autocorrectionType = model.autocorrectionType textField.autocapitalizationType = model.autocapitalization.textAutocapitalizationType } + static func captionLabel( + _ label: UILabel, + model: Model + ) { + label.text = model.caption + label.isVisible = model.caption.isNotNilAndEmpty + label.textColor = model.captionColor.uiColor + label.font = model.preferredCaptionFont.uiFont + } static func horizontalStackView( _ stackView: UIStackView, model: Model From 4dddef44437d723d1d3b473e5238cbd4556c6b29 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 14:22:44 +0300 Subject: [PATCH 47/60] add faded style for input fields --- .../ComponentsPreview/PreviewPages/InputFieldPreview.swift | 3 ++- .../Components/InputField/Models/InputFieldStyle.swift | 5 +++++ .../Components/InputField/Models/InputFieldVM.swift | 4 ++-- .../ComponentsKit/Components/InputField/UKInputField.swift | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift index 7b5ef151..f511e2ba 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift @@ -64,6 +64,7 @@ struct InputFieldPreview: View { Picker("Style", selection: self.$model.style) { Text("Light").tag(InputFieldVM.Style.light) Text("Bordered").tag(InputFieldVM.Style.bordered) + Text("Faded").tag(InputFieldVM.Style.faded) } SubmitTypePicker(selection: self.$model.submitType) UniversalColorPicker( @@ -99,7 +100,7 @@ struct InputFieldPreview: View { private static let title = "Email" private static let placeholder = "Enter your email" - private static let caption = "We'll send you a verification code" + private static let caption = "Your email address will be used to send a verification code" private static var initialModel: InputFieldVM { return .init { $0.title = Self.title diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift index 449826c3..52741b34 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift @@ -1,8 +1,13 @@ import Foundation extension InputFieldVM { + /// The input fields appearance style. public enum Style: Hashable { + /// An input field with a partially transparent background. case light + /// An input field with a transparent background and a border. case bordered + /// An input field with a partially transparent background and a border. + case faded } } diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift index 680abf05..55c39e02 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -151,7 +151,7 @@ extension InputFieldVM { } var backgroundColor: UniversalColor { switch self.style { - case .light: + case .light, .faded: return self.color?.background ?? .content1 case .bordered: return .background @@ -174,7 +174,7 @@ extension InputFieldVM { switch self.style { case .light: return 0 - case .bordered: + case .bordered, .faded: switch self.size { case .small: return BorderWidth.small.value diff --git a/Sources/ComponentsKit/Components/InputField/UKInputField.swift b/Sources/ComponentsKit/Components/InputField/UKInputField.swift index 90d51299..5e02341f 100644 --- a/Sources/ComponentsKit/Components/InputField/UKInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/UKInputField.swift @@ -257,6 +257,7 @@ extension UKInputField { label.isVisible = model.caption.isNotNilAndEmpty label.textColor = model.captionColor.uiColor label.font = model.preferredCaptionFont.uiFont + label.numberOfLines = 0 } static func horizontalStackView( _ stackView: UIStackView, From 07af638122ff6f47387aa72c80c0cc1885f1b96c Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 14:27:56 +0300 Subject: [PATCH 48/60] add missing docs --- .../InputField/Models/InputFieldTitlePosition.swift | 3 +++ .../Components/InputField/Models/InputFieldVM.swift | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldTitlePosition.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldTitlePosition.swift index 9f94afb9..5fc920a7 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldTitlePosition.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldTitlePosition.swift @@ -1,8 +1,11 @@ import Foundation extension InputFieldVM { + /// Specifies the position of the title relative to the input field. public enum TitlePosition { + /// The title is displayed inside the input field. case inside + /// The title is displayed above the input field. case outside } } diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift index 55c39e02..1b4d2c35 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -8,8 +8,12 @@ public struct InputFieldVM: ComponentVM { /// Defaults to `.sentences`, which capitalizes the first letter of each sentence. public var autocapitalization: TextAutocapitalization = .sentences + /// The caption displayed below the input field. public var caption: String? + /// The font used for the input field's caption. + /// + /// If not provided, the font is automatically calculated based on the input field's size. public var captionFont: UniversalFont? /// The color of the input field. @@ -58,6 +62,9 @@ public struct InputFieldVM: ComponentVM { /// Defaults to `.medium`. public var size: ComponentSize = .medium + /// The visual style of the input field. + /// + /// Defaults to `.light`. public var style: Style = .light /// The type of the submit button on the keyboard. @@ -73,8 +80,14 @@ public struct InputFieldVM: ComponentVM { /// The title displayed on the input field. public var title: String? + /// The font used for the input field's title. + /// + /// If not provided, the font is automatically calculated based on the input field's size. public var titleFont: UniversalFont? + /// The position of the title relative to the input field. + /// + /// Defaults to `.inside`. public var titlePosition: TitlePosition = .inside /// Initializes a new instance of `InputFieldVM` with default values. From 7c467a6fb4f3af6cb7ed390cb2004d0de62b6afd Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 14:39:39 +0300 Subject: [PATCH 49/60] improve docs in `UKInputField` --- .../ComponentsKit/Components/InputField/UKInputField.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ComponentsKit/Components/InputField/UKInputField.swift b/Sources/ComponentsKit/Components/InputField/UKInputField.swift index 5e02341f..068e5bb1 100644 --- a/Sources/ComponentsKit/Components/InputField/UKInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/UKInputField.swift @@ -36,11 +36,11 @@ open class UKInputField: UIView, UKComponent { public var textField = UITextField() /// A label that displays the caption from the model. public var captionLabel = UILabel() - /// A view that contains `horizontalStackView` and `titleLabel` whet it is outside. + /// A view that contains `horizontalStackView` to have paddings. public var textFieldContainer = UIView() - /// A stack view that contains `textField` and `titleLabel` whet it is inside. + /// A stack view that contains `textField` and `titleLabel` when it is inside. public var horizontalStackView = UIStackView() - /// A stack view that contains `textFieldContainer`. + /// A stack view that contains `textFieldContainer` and `titleLabel` when it is outside. public var verticalStackView = UIStackView() // MARK: Private Properties From b559dc8f203416794f2b74fd5d313ab918ee0cef Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 14:40:57 +0300 Subject: [PATCH 50/60] improve `UKInputField` docs --- Sources/ComponentsKit/Components/InputField/UKInputField.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/InputField/UKInputField.swift b/Sources/ComponentsKit/Components/InputField/UKInputField.swift index 068e5bb1..1a553155 100644 --- a/Sources/ComponentsKit/Components/InputField/UKInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/UKInputField.swift @@ -40,7 +40,7 @@ open class UKInputField: UIView, UKComponent { public var textFieldContainer = UIView() /// A stack view that contains `textField` and `titleLabel` when it is inside. public var horizontalStackView = UIStackView() - /// A stack view that contains `textFieldContainer` and `titleLabel` when it is outside. + /// A stack view that contains `textFieldContainer`, `captionLabel` and `titleLabel` when it is outside. public var verticalStackView = UIStackView() // MARK: Private Properties From baa06b2f586287b5740fd92fc1330d39703c30bc Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 17:31:35 +0300 Subject: [PATCH 51/60] remove commas from last params --- .../ComponentsKit/Components/InputField/SUInputField.swift | 6 +++--- .../ComponentsKit/Components/TextInput/SUTextInput.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ComponentsKit/Components/InputField/SUInputField.swift b/Sources/ComponentsKit/Components/InputField/SUInputField.swift index 022a64f8..a2a58b35 100644 --- a/Sources/ComponentsKit/Components/InputField/SUInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/SUInputField.swift @@ -92,12 +92,12 @@ public struct SUInputField: View { } .clipShape( RoundedRectangle( - cornerRadius: self.model.cornerRadius.value(), + cornerRadius: self.model.cornerRadius.value() ) ) .overlay( RoundedRectangle( - cornerRadius: self.model.cornerRadius.value(), + cornerRadius: self.model.cornerRadius.value() ) .stroke( self.model.borderColor.color, @@ -120,7 +120,7 @@ extension View { @ViewBuilder fileprivate func applyFocus( globalFocus: FocusState.Binding?, - localFocus: FocusValue, + localFocus: FocusValue ) -> some View { if let globalFocus { self.focused(globalFocus, equals: localFocus) diff --git a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift index d91b66d9..0ca46401 100644 --- a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift @@ -158,7 +158,7 @@ extension View { @ViewBuilder fileprivate func applyFocus( globalFocus: FocusState.Binding?, - localFocus: FocusValue, + localFocus: FocusValue ) -> some View { if let globalFocus { self.focused(globalFocus, equals: localFocus) From 5cd211e6df88f05f5bed631b4da4d05a23be07be Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 17:37:01 +0300 Subject: [PATCH 52/60] change a color of a star sign when the input field is disabled --- .../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 1b4d2c35..dd9c2f88 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -242,7 +242,7 @@ extension InputFieldVM { string: "*", attributes: [ .font: self.preferredTitleFont.uiFont, - .foregroundColor: UniversalColor.danger.uiColor + .foregroundColor: UniversalColor.danger.enabled(self.isEnabled).uiColor ] )) } From 8dbba1e28a65f5fc40e371d41254523abc99398c Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 18:04:09 +0300 Subject: [PATCH 53/60] add a shared `InputStyle` enum --- .../PreviewPages/InputFieldPreview.swift | 6 +++--- .../InputField/Models/InputFieldStyle.swift | 13 ------------- .../Components/InputField/Models/InputFieldVM.swift | 2 +- Sources/ComponentsKit/Shared/Types/InputStyle.swift | 11 +++++++++++ 4 files changed, 15 insertions(+), 17 deletions(-) delete mode 100644 Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift create mode 100644 Sources/ComponentsKit/Shared/Types/InputStyle.swift diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift index f511e2ba..e9e6a9b0 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift @@ -62,9 +62,9 @@ struct InputFieldPreview: View { Toggle("Secure Input", isOn: self.$model.isSecureInput) SizePicker(selection: self.$model.size) Picker("Style", selection: self.$model.style) { - Text("Light").tag(InputFieldVM.Style.light) - Text("Bordered").tag(InputFieldVM.Style.bordered) - Text("Faded").tag(InputFieldVM.Style.faded) + Text("Light").tag(InputStyle.light) + Text("Bordered").tag(InputStyle.bordered) + Text("Faded").tag(InputStyle.faded) } SubmitTypePicker(selection: self.$model.submitType) UniversalColorPicker( diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift deleted file mode 100644 index 52741b34..00000000 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldStyle.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -extension InputFieldVM { - /// The input fields appearance style. - public enum Style: Hashable { - /// An input field with a partially transparent background. - case light - /// An input field with a transparent background and a border. - case bordered - /// An input field with a partially transparent background and a border. - case faded - } -} diff --git a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift index dd9c2f88..fba374c2 100644 --- a/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift +++ b/Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift @@ -65,7 +65,7 @@ public struct InputFieldVM: ComponentVM { /// The visual style of the input field. /// /// Defaults to `.light`. - public var style: Style = .light + public var style: InputStyle = .light /// The type of the submit button on the keyboard. /// diff --git a/Sources/ComponentsKit/Shared/Types/InputStyle.swift b/Sources/ComponentsKit/Shared/Types/InputStyle.swift new file mode 100644 index 00000000..34da511c --- /dev/null +++ b/Sources/ComponentsKit/Shared/Types/InputStyle.swift @@ -0,0 +1,11 @@ +import Foundation + +/// The appearance style of inputs. +public enum InputStyle: Hashable { + /// An input with a partially transparent background. + case light + /// An input with a transparent background and a border. + case bordered + /// An input with a partially transparent background and a border. + case faded +} From 3d8260898b24bd9cac48878ff141cc9d3b3ba99d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 23 Apr 2025 18:20:32 +0300 Subject: [PATCH 54/60] add styles to text input --- .../Helpers/PreviewPickers.swift | 18 +++++++++-- .../PreviewPages/InputFieldPreview.swift | 6 +--- .../PreviewPages/TextInputPreview.swift | 1 + .../TextInput/Models/TextInputVM.swift | 32 ++++++++++++++++++- .../Components/TextInput/SUTextInput.swift | 9 ++++++ .../Components/TextInput/UKTextInput.swift | 21 +++++++++++- 6 files changed, 77 insertions(+), 10 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift index 4645fa94..e442e134 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift @@ -23,7 +23,7 @@ struct AutocapitalizationPicker: View { @Binding var selection: TextAutocapitalization var body: some View { - Picker("Autocapitalization", selection: $selection) { + Picker("Autocapitalization", selection: self.$selection) { Text("Never").tag(TextAutocapitalization.never) Text("Characters").tag(TextAutocapitalization.characters) Text("Words").tag(TextAutocapitalization.words) @@ -203,13 +203,25 @@ struct CaptionFontPicker: View { } } +struct InputStylePicker: View { + @Binding var selection: InputStyle + + var body: some View { + Picker("Style", selection: self.$selection) { + Text("Light").tag(InputStyle.light) + Text("Bordered").tag(InputStyle.bordered) + Text("Faded").tag(InputStyle.faded) + } + } +} + // MARK: - KeyboardTypePicker struct KeyboardTypePicker: View { @Binding var selection: UIKeyboardType var body: some View { - Picker("Keyboard Type", selection: $selection) { + Picker("Keyboard Type", selection: self.$selection) { Text("Default").tag(UIKeyboardType.default) Text("asciiCapable").tag(UIKeyboardType.asciiCapable) Text("numbersAndPunctuation").tag(UIKeyboardType.numbersAndPunctuation) @@ -260,7 +272,7 @@ struct SubmitTypePicker: View { @Binding var selection: SubmitType var body: some View { - Picker("Submit Type", selection: $selection) { + Picker("Submit Type", selection: self.$selection) { Text("done").tag(SubmitType.done) Text("go").tag(SubmitType.go) Text("join").tag(SubmitType.join) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift index e9e6a9b0..1b74b659 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift @@ -61,11 +61,7 @@ struct InputFieldPreview: View { Toggle("Required", isOn: self.$model.isRequired) Toggle("Secure Input", isOn: self.$model.isSecureInput) SizePicker(selection: self.$model.size) - Picker("Style", selection: self.$model.style) { - Text("Light").tag(InputStyle.light) - Text("Bordered").tag(InputStyle.bordered) - Text("Faded").tag(InputStyle.faded) - } + InputStylePicker(selection: self.$model.style) SubmitTypePicker(selection: self.$model.submitType) UniversalColorPicker( title: "Tint Color", diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/TextInputPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/TextInputPreview.swift index 978ee110..40ee622e 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/TextInputPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/TextInputPreview.swift @@ -60,6 +60,7 @@ struct TextInputPreviewPreview: View { } )) SizePicker(selection: self.$model.size) + InputStylePicker(selection: self.$model.style) SubmitTypePicker(selection: self.$model.submitType) UniversalColorPicker( title: "Tint Color", diff --git a/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift b/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift index 274cce4b..12ba475c 100644 --- a/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift +++ b/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift @@ -52,6 +52,11 @@ public struct TextInputVM: ComponentVM { /// Defaults to `.medium`. public var size: ComponentSize = .medium + /// The visual style of the text input. + /// + /// Defaults to `.light`. + public var style: InputStyle = .light + /// The type of the submit button on the keyboard. /// /// Defaults to `.return`. @@ -89,7 +94,12 @@ extension TextInputVM { } var backgroundColor: UniversalColor { - return self.color?.background ?? .content1 + switch self.style { + case .light, .faded: + return self.color?.background ?? .content1 + case .bordered: + return .background + } } var foregroundColor: UniversalColor { @@ -105,6 +115,26 @@ extension TextInputVM { } } + var borderWidth: CGFloat { + switch self.style { + case .light: + return 0 + case .bordered, .faded: + switch self.size { + case .small: + return BorderWidth.small.value + case .medium: + return BorderWidth.medium.value + case .large: + return BorderWidth.large.value + } + } + } + + var borderColor: UniversalColor { + return (self.color?.main ?? .content3).enabled(self.isEnabled) + } + var minTextInputHeight: CGFloat { let numberOfRows: Int if let maxRows { diff --git a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift index 0ca46401..efa9e4ac 100644 --- a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift @@ -124,6 +124,15 @@ public struct SUTextInput: View { cornerRadius: self.model.adaptedCornerRadius() ) ) + .overlay( + RoundedRectangle( + cornerRadius: self.model.cornerRadius.value() + ) + .stroke( + self.model.borderColor.color, + lineWidth: self.model.borderWidth + ) + ) } } diff --git a/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift b/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift index ab169844..7710feda 100644 --- a/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/UKTextInput.swift @@ -79,6 +79,12 @@ open class UKTextInput: UIView, UKComponent { self.addSubview(self.placeholderLabel) self.textView.delegate = self + + if #available(iOS 17.0, *) { + self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in + view.handleTraitChanges() + } + } } // MARK: - Style @@ -157,7 +163,18 @@ open class UKTextInput: UIView, UKComponent { return CGSize(width: width, height: height) } - // MARK: - Helpers + open override func traitCollectionDidChange( + _ previousTraitCollection: UITraitCollection? + ) { + super.traitCollectionDidChange(previousTraitCollection) + self.handleTraitChanges() + } + + // MARK: Helpers + + @objc private func handleTraitChanges() { + Self.Style.mainView(self, model: self.model) + } private func handleTextChanges() { self.onValueChange(self.text) @@ -187,6 +204,8 @@ extension UKTextInput { static func mainView(_ view: UIView, model: TextInputVM) { view.backgroundColor = model.backgroundColor.uiColor view.layer.cornerRadius = model.adaptedCornerRadius(for: view.bounds.height) + view.layer.borderColor = model.borderColor.cgColor + view.layer.borderWidth = model.borderWidth } static func textView( From 4653a34fe06bd6986a26f2970f07dc6a25db887f Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 24 Apr 2025 14:28:25 +0300 Subject: [PATCH 55/60] fix text input layout bug --- .../TextInput/Models/TextInputVM.swift | 2 +- .../Components/TextInput/SUTextInput.swift | 23 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift b/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift index 12ba475c..84dab608 100644 --- a/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift +++ b/Sources/ComponentsKit/Components/TextInput/Models/TextInputVM.swift @@ -147,7 +147,7 @@ extension TextInputVM { var maxTextInputHeight: CGFloat { if let maxRows { - return self.height(forRows: maxRows) + return self.height(forRows: max(maxRows, self.minRows)) } else { return 10_000 } diff --git a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift index efa9e4ac..bd90dcb3 100644 --- a/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift +++ b/Sources/ComponentsKit/Components/TextInput/SUTextInput.swift @@ -55,14 +55,23 @@ public struct SUTextInput: View { TextEditor(text: self.$text) .contentMargins(self.model.contentPadding) .transparentScrollBackground() - .frame(minHeight: self.model.minTextInputHeight) - .frame(height: max( - self.model.minTextInputHeight, - min( - self.model.maxTextInputHeight, - self.textEditorPreferredHeight + .frame( + minHeight: self.model.minTextInputHeight, + idealHeight: max( + self.model.minTextInputHeight, + min( + self.model.maxTextInputHeight, + self.textEditorPreferredHeight + ) + ), + maxHeight: max( + self.model.minTextInputHeight, + min( + self.model.maxTextInputHeight, + self.textEditorPreferredHeight + ) ) - )) + ) .lineSpacing(0) .font(self.model.preferredFont.font) .foregroundStyle(self.model.foregroundColor.color) From b68f23b5f6b35da5c59b55137f7460e9d0fb4f84 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 24 Apr 2025 18:08:49 +0300 Subject: [PATCH 56/60] add minimal button style --- .../PreviewPages/ButtonPreview.swift | 1 + .../Components/Button/Models/ButtonVM.swift | 38 ++++++++++++------- .../Shared/Types/ButtonStyle.swift | 2 + 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index 7f7715e6..045d258b 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -51,6 +51,7 @@ struct ButtonPreview: View { Text("Filled").tag(ButtonStyle.filled) Text("Plain").tag(ButtonStyle.plain) Text("Light").tag(ButtonStyle.light) + Text("Minimal").tag(ButtonStyle.minimal) Text("Bordered with small border").tag(ButtonStyle.bordered(.small)) Text("Bordered with medium border").tag(ButtonStyle.bordered(.medium)) Text("Bordered with large border").tag(ButtonStyle.bordered(.large)) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 90c3bc45..bd9242fb 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -93,7 +93,7 @@ extension ButtonVM { case .light: let color = self.color?.background ?? .content1 return color.enabled(self.isInteractive) - case .plain, .bordered: + case .plain, .bordered, .minimal: return nil } } @@ -101,14 +101,14 @@ extension ButtonVM { let color = switch self.style { case .filled: self.color?.contrast ?? .foreground - case .plain, .light, .bordered: + case .plain, .light, .bordered, .minimal: self.color?.main ?? .foreground } return color.enabled(self.isInteractive) } var borderWidth: CGFloat { switch self.style { - case .filled, .plain, .light: + case .filled, .plain, .light, .minimal: return 0.0 case .bordered(let borderWidth): return borderWidth.value @@ -116,7 +116,7 @@ extension ButtonVM { } var borderColor: UniversalColor? { switch self.style { - case .filled, .plain, .light: + case .filled, .plain, .light, .minimal: return nil case .bordered: if let color { @@ -140,11 +140,16 @@ extension ButtonVM { return .lgButton } } - var height: CGFloat { - return switch self.size { - case .small: 36 - case .medium: 44 - case .large: 52 + var height: CGFloat? { + switch self.style { + case .minimal: + return nil + case .light, .filled, .bordered, .plain: + return switch self.size { + case .small: 36 + case .medium: 44 + case .large: 52 + } } } var imageSide: CGFloat { @@ -155,10 +160,15 @@ extension ButtonVM { } } var horizontalPadding: CGFloat { - return switch self.size { - case .small: 16 - case .medium: 20 - case .large: 24 + switch self.style { + case .minimal: + return 0 + case .light, .filled, .bordered, .plain: + return switch self.size { + case .small: 16 + case .medium: 20 + case .large: 24 + } } } } @@ -196,7 +206,7 @@ extension ButtonVM { width = contentSize.width + 2 * self.horizontalPadding } - return .init(width: width, height: self.height) + return .init(width: width, height: self.height ?? contentSize.height) } func shouldUpdateImagePosition(_ oldModel: Self?) -> Bool { guard let oldModel else { return true } diff --git a/Sources/ComponentsKit/Shared/Types/ButtonStyle.swift b/Sources/ComponentsKit/Shared/Types/ButtonStyle.swift index bc598997..0dc44879 100644 --- a/Sources/ComponentsKit/Shared/Types/ButtonStyle.swift +++ b/Sources/ComponentsKit/Shared/Types/ButtonStyle.swift @@ -10,4 +10,6 @@ public enum ButtonStyle: Hashable { case light /// A button with a transparent background and a border. case bordered(BorderWidth) + /// A button with no background or padding, sized strictly to fit its content. + case minimal } From caf0a63369df67367a9a3051e85728505f1c95c7 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 27 Apr 2025 14:22:19 +0300 Subject: [PATCH 57/60] update `UKAlertController` to properly display buttons with `minimal` style --- .../ComponentsKit/Components/Alert/UKAlertController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/Alert/UKAlertController.swift b/Sources/ComponentsKit/Components/Alert/UKAlertController.swift index a1731720..2af9e366 100644 --- a/Sources/ComponentsKit/Components/Alert/UKAlertController.swift +++ b/Sources/ComponentsKit/Components/Alert/UKAlertController.swift @@ -143,11 +143,14 @@ public class UKAlertController: UKCenterModalController { self.buttonsStackView.removeArrangedSubview(self.secondaryButton) self.buttonsStackView.insertArrangedSubview(self.secondaryButton, at: 0) self.buttonsStackView.axis = .horizontal + self.buttonsStackView.distribution = .fillEqually case .vertical: self.buttonsStackView.axis = .vertical + self.buttonsStackView.distribution = .fillProportionally } } else { self.buttonsStackView.axis = .vertical + self.buttonsStackView.distribution = .fillProportionally } } } @@ -173,7 +176,6 @@ extension UKAlertController { } static func buttonsStackView(_ stackView: UIStackView) { - stackView.distribution = .fillEqually stackView.spacing = AlertVM.buttonsSpacing } } From 6641e46ea6e17d86338b262468ddc93a102f07f9 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 27 Apr 2025 14:22:41 +0300 Subject: [PATCH 58/60] add `ButtonStylePicker` helper --- .../Helpers/PreviewPickers.swift | 18 +++++++++++++++++- .../PreviewPages/AlertPreview.swift | 9 +-------- .../PreviewPages/ButtonPreview.swift | 15 ++++----------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift index e442e134..e97783d9 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift @@ -36,7 +36,7 @@ struct AutocapitalizationPicker: View { struct BorderWidthPicker: View { @Binding var selection: BorderWidth - + var body: some View { Picker("Border Width", selection: self.$selection) { Text("None").tag(BorderWidth.none) @@ -47,6 +47,22 @@ struct BorderWidthPicker: View { } } +struct ButtonStylePicker: View { + @Binding var selection: ComponentsKit.ButtonStyle + + var body: some View { + Picker("Style", selection: $selection) { + Text("Filled").tag(ButtonStyle.filled) + Text("Plain").tag(ButtonStyle.plain) + Text("Light").tag(ButtonStyle.light) + Text("Minimal").tag(ButtonStyle.minimal) + Text("Bordered with small border").tag(ButtonStyle.bordered(.small)) + Text("Bordered with medium border").tag(ButtonStyle.bordered(.medium)) + Text("Bordered with large border").tag(ButtonStyle.bordered(.large)) + } + } +} + // MARK: - ComponentColorPicker struct ComponentColorPicker: View { diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift index 93e3b098..658a73d0 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift @@ -115,14 +115,7 @@ struct AlertPreview: View { ComponentRadiusPicker(selection: buttonVM.cornerRadius) { Text("Custom: 20px").tag(ComponentRadius.custom(20)) } - Picker("Style", selection: buttonVM.style) { - Text("Filled").tag(ButtonStyle.filled) - Text("Plain").tag(ButtonStyle.plain) - Text("Light").tag(ButtonStyle.light) - Text("Bordered with small border").tag(ButtonStyle.bordered(.small)) - Text("Bordered with medium border").tag(ButtonStyle.bordered(.medium)) - Text("Bordered with large border").tag(ButtonStyle.bordered(.large)) - } + ButtonStylePicker(selection: buttonVM.style) } } diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index 045d258b..19dc015b 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -3,8 +3,9 @@ import SwiftUI import UIKit struct ButtonPreview: View { + private static let title = "Button" @State private var model = ButtonVM { - $0.title = "Button" + $0.title = Self.title } var body: some View { @@ -43,19 +44,11 @@ struct ButtonPreview: View { Toggle("Show Title", isOn: Binding( get: { !self.model.title.isEmpty }, set: { newValue in - self.model.title = newValue ? "Button" : "" + self.model.title = newValue ? Self.title : "" } )) SizePicker(selection: self.$model.size) - Picker("Style", selection: self.$model.style) { - Text("Filled").tag(ButtonStyle.filled) - Text("Plain").tag(ButtonStyle.plain) - Text("Light").tag(ButtonStyle.light) - Text("Minimal").tag(ButtonStyle.minimal) - Text("Bordered with small border").tag(ButtonStyle.bordered(.small)) - Text("Bordered with medium border").tag(ButtonStyle.bordered(.medium)) - Text("Bordered with large border").tag(ButtonStyle.bordered(.large)) - } + ButtonStylePicker(selection: self.$model.style) } } } From 505c430a6622137c44b0cffde24daef71f94335d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 30 Apr 2025 13:33:11 +0300 Subject: [PATCH 59/60] fixed button not animating when tapped on transparent background --- Sources/ComponentsKit/Components/Button/SUButton.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index 9dcf96d1..80cf0e89 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -108,6 +108,7 @@ private struct CustomButtonStyle: SwiftUI.ButtonStyle { configuration.label .font(self.model.preferredFont.font) .lineLimit(1) + .contentShape(.rect) .padding(.horizontal, self.model.horizontalPadding) .frame(maxWidth: self.model.width) .frame(height: self.model.height) From a27b1ee535973ff8f40276ad7c4937e88722f325 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 30 Apr 2025 14:55:25 +0300 Subject: [PATCH 60/60] update the size of full-width components when the width of their parents changes --- .../DemosApp.xcodeproj/project.pbxproj | 3 +- .../Components/Button/UKButton.swift | 2 +- .../Components/InputField/UKInputField.swift | 4 ++- .../ProgressBar/UKProgressBar.swift | 2 +- .../SegmentedControl/UKSegmentedControl.swift | 2 +- .../Components/Slider/UKSlider.swift | 2 +- .../Helpers/UIKit/FullWidthComponent.swift | 28 +++++++++++++++++++ 7 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 Sources/ComponentsKit/Helpers/UIKit/FullWidthComponent.swift diff --git a/Examples/DemosApp/DemosApp.xcodeproj/project.pbxproj b/Examples/DemosApp/DemosApp.xcodeproj/project.pbxproj index 00497518..81af7444 100644 --- a/Examples/DemosApp/DemosApp.xcodeproj/project.pbxproj +++ b/Examples/DemosApp/DemosApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -80,7 +80,6 @@ 740D221F2CD3BECA006731A5 /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1610; LastUpgradeCheck = 1610; TargetAttributes = { diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index 113d2a63..e7b63552 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -2,7 +2,7 @@ import AutoLayout import UIKit /// A UIKit component that performs an action when it is tapped by a user. -open class UKButton: UIView, UKComponent { +open class UKButton: FullWidthComponent, UKComponent { // MARK: Properties /// A closure that is triggered when the button is tapped. diff --git a/Sources/ComponentsKit/Components/InputField/UKInputField.swift b/Sources/ComponentsKit/Components/InputField/UKInputField.swift index 1a553155..1c1e4bf1 100644 --- a/Sources/ComponentsKit/Components/InputField/UKInputField.swift +++ b/Sources/ComponentsKit/Components/InputField/UKInputField.swift @@ -2,7 +2,7 @@ import AutoLayout import UIKit /// A UIKit component that displays a field to input a text. -open class UKInputField: UIView, UKComponent { +open class UKInputField: FullWidthComponent, UKComponent { // MARK: Public Properties /// A closure that is triggered when the text changes. @@ -140,6 +140,8 @@ open class UKInputField: UIView, UKComponent { self.horizontalStackView.vertically() self.horizontalStackViewConstraints = self.horizontalStackView.horizontally(self.model.horizontalPadding) + self.captionLabel.horizontally() + self.textField.setContentHuggingPriority(.defaultLow, for: .horizontal) self.titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) } diff --git a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift index 28943e6b..1583645b 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift @@ -2,7 +2,7 @@ import AutoLayout import UIKit /// A UIKit component that visually represents the progress of a task or process using a horizontal bar. -open class UKProgressBar: UIView, UKComponent { +open class UKProgressBar: FullWidthComponent, UKComponent { // MARK: - Public Properties /// A model that defines the appearance properties. diff --git a/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift b/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift index f759f3da..e642c6cb 100644 --- a/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift +++ b/Sources/ComponentsKit/Components/SegmentedControl/UKSegmentedControl.swift @@ -2,7 +2,7 @@ import AutoLayout import UIKit /// A UIKit component that allows users to choose between multiple segments or options. -open class UKSegmentedControl: UIView, UKComponent { +open class UKSegmentedControl: FullWidthComponent, UKComponent { // MARK: Properties /// A closure that is triggered when a selected segment changes. diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift index 1a715eb6..22615b63 100644 --- a/Sources/ComponentsKit/Components/Slider/UKSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -2,7 +2,7 @@ import AutoLayout import UIKit /// A UIKit component that lets users select a value from a range by dragging a thumb along a track. -open class UKSlider: UIView, UKComponent { +open class UKSlider: FullWidthComponent, UKComponent { // MARK: - Properties /// A closure that is triggered when the `currentValue` changes. diff --git a/Sources/ComponentsKit/Helpers/UIKit/FullWidthComponent.swift b/Sources/ComponentsKit/Helpers/UIKit/FullWidthComponent.swift new file mode 100644 index 00000000..85dc5c4f --- /dev/null +++ b/Sources/ComponentsKit/Helpers/UIKit/FullWidthComponent.swift @@ -0,0 +1,28 @@ +import UIKit + +/// A base-class for views whose intrinsic content size depends on the +/// width of their super-view (e.g. full width button, input field, etc.). +/// +/// By inheriting from `FullWidthComponent` the component gets automatic +/// `invalidateIntrinsicContentSize()` calls whenever the device rotates, the +/// window is resized (iPad multitasking, Stage Manager) or the view moves +/// into a different container with a new width. +open class FullWidthComponent: UIView { + private var lastKnownParentWidth: CGFloat = .nan + + open override func layoutSubviews() { + super.layoutSubviews() + + guard let parentWidth = self.superview?.bounds.width else { return } + + if parentWidth != self.lastKnownParentWidth { + self.lastKnownParentWidth = parentWidth + + // Defer to the next run-loop tick so the current layout pass + // finishes with the new parent size first. + DispatchQueue.main.async { + self.invalidateIntrinsicContentSize() + } + } + } +}