From d9410bf54a5e260c573d8908149aa849ca6e565d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 5 Feb 2026 17:26:18 +0800 Subject: [PATCH 1/5] remove loading style --- .../PreviewPages/LoadingPreview.swift | 3 --- .../Loading/Models/LoadingStyle.swift | 8 ------- .../Components/Loading/Models/LoadingVM.swift | 22 ++++++------------- 3 files changed, 7 insertions(+), 26 deletions(-) delete mode 100644 Sources/ComponentsKit/Components/Loading/Models/LoadingStyle.swift diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift index 3ee062f6..783f2972 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift @@ -21,9 +21,6 @@ struct LoadingPreview: View { Text("Custom: 6px").tag(CGFloat(6.0)) } SizePicker(selection: self.$model.size) - Picker("Style", selection: self.$model.style) { - Text("Spinner").tag(LoadingVM.Style.spinner) - } } } } diff --git a/Sources/ComponentsKit/Components/Loading/Models/LoadingStyle.swift b/Sources/ComponentsKit/Components/Loading/Models/LoadingStyle.swift deleted file mode 100644 index 71031529..00000000 --- a/Sources/ComponentsKit/Components/Loading/Models/LoadingStyle.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -extension LoadingVM { - /// The loading appearance style. - public enum Style { - case spinner - } -} diff --git a/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift index c9f7f30b..2d370009 100644 --- a/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift +++ b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift @@ -17,11 +17,6 @@ public struct LoadingVM: ComponentVM { /// Defaults to `.medium`. public var size: ComponentSize = .medium - /// The style of the loading indicator (e.g., spinner, bar). - /// - /// Defaults to `.spinner`. - public var style: Style = .spinner - /// Initializes a new instance of `LoadingVM` with default values. public init() {} } @@ -33,16 +28,13 @@ extension LoadingVM { return self.lineWidth ?? max(self.preferredSize.width / 8, 2) } var preferredSize: CGSize { - switch self.style { - case .spinner: - switch self.size { - case .small: - return .init(width: 24, height: 24) - case .medium: - return .init(width: 36, height: 36) - case .large: - return .init(width: 48, height: 48) - } + switch self.size { + case .small: + return .init(width: 24, height: 24) + case .medium: + return .init(width: 36, height: 36) + case .large: + return .init(width: 48, height: 48) } } var radius: CGFloat { From acea575e60162fec58687bd6b890db2829c6ca15 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 5 Feb 2026 19:11:32 +0800 Subject: [PATCH 2/5] make the size in loader optional --- .../Helpers/PreviewPickers.swift | 13 +++++++ .../PreviewPages/LoadingPreview.swift | 2 +- .../Components/Loading/Models/LoadingVM.swift | 37 ++++++++++--------- .../Components/Loading/SULoading.swift | 33 ++++++++++------- .../Components/Loading/UKLoading.swift | 25 ++++++------- 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift index e97783d9..e2b2da01 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift @@ -282,6 +282,19 @@ struct SizePicker: View { } } +struct OptionalSizePicker: View { + @Binding var selection: ComponentSize? + + var body: some View { + Picker("Size", selection: self.$selection) { + Text("Nil").tag(Optional.none) + Text("Small").tag(ComponentSize.small) + Text("Medium").tag(ComponentSize.medium) + Text("Large").tag(ComponentSize.large) + } + } +} + // MARK: - SubmitTypePicker struct SubmitTypePicker: View { diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift index 783f2972..905f74d8 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift @@ -20,7 +20,7 @@ struct LoadingPreview: View { Text("Default").tag(Optional.none) Text("Custom: 6px").tag(CGFloat(6.0)) } - SizePicker(selection: self.$model.size) + OptionalSizePicker(selection: self.$model.size) } } } diff --git a/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift index 2d370009..7ea60e41 100644 --- a/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift +++ b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift @@ -14,8 +14,13 @@ public struct LoadingVM: ComponentVM { /// The predefined size of the loading indicator. /// + /// If nil, the loader is intended to expand to the available space provided by + /// the surrounding layout: + /// - In SwiftUI, constrain it with .frame(...). + /// - In UIKit, constrain it with Auto Layout. + /// /// Defaults to `.medium`. - public var size: ComponentSize = .medium + public var size: ComponentSize? = .medium /// Initializes a new instance of `LoadingVM` with default values. public init() {} @@ -25,10 +30,20 @@ public struct LoadingVM: ComponentVM { extension LoadingVM { var loadingLineWidth: CGFloat { - return self.lineWidth ?? max(self.preferredSize.width / 8, 2) + if let lineWidth { + return lineWidth + } else if let width = self.preferredSize?.width { + return max(width / 8, 2) + } else { + return 3 + } } - var preferredSize: CGSize { - switch self.size { + var preferredSize: CGSize? { + guard let size else { + return nil + } + + switch size { case .small: return .init(width: 24, height: 24) case .medium: @@ -37,9 +52,6 @@ extension LoadingVM { return .init(width: 48, height: 48) } } - var radius: CGFloat { - return self.preferredSize.height / 2 - self.loadingLineWidth / 2 - } } // MARK: UIKit Helpers @@ -49,14 +61,3 @@ extension LoadingVM { return self.size != oldModel.size || self.lineWidth != oldModel.lineWidth } } - -// MARK: SwiftUI Helpers - -extension LoadingVM { - var center: CGPoint { - return .init( - x: self.preferredSize.width / 2, - y: self.preferredSize.height / 2 - ) - } -} diff --git a/Sources/ComponentsKit/Components/Loading/SULoading.swift b/Sources/ComponentsKit/Components/Loading/SULoading.swift index 553e7644..d0b96d47 100644 --- a/Sources/ComponentsKit/Components/Loading/SULoading.swift +++ b/Sources/ComponentsKit/Components/Loading/SULoading.swift @@ -22,15 +22,19 @@ public struct SULoading: View { // MARK: Body public var body: some View { - Path { path in - path.addArc( - center: self.model.center, - radius: self.model.radius, - startAngle: .radians(0), - endAngle: .radians(2 * .pi), - clockwise: true - ) - } + GeometryReader { geometry in + Path { path in + path.addArc( + center: .init( + x: geometry.size.width / 2, + y: geometry.size.height / 2 + ), + radius: min(geometry.size.width, geometry.size.height) / 2 - self.model.loadingLineWidth, + startAngle: .radians(0), + endAngle: .radians(2 * .pi), + clockwise: true + ) + } .trim(from: 0, to: 0.75) .stroke( self.model.color.main.color, @@ -47,15 +51,16 @@ public struct SULoading: View { .repeatForever(autoreverses: false), value: self.rotationAngle ) - .frame( - width: self.model.preferredSize.width, - height: self.model.preferredSize.height, - alignment: .center - ) .onAppear { DispatchQueue.main.async { self.rotationAngle = 2 * .pi } } + } + .frame( + width: self.model.preferredSize?.width, + height: self.model.preferredSize?.height, + alignment: .center + ) } } diff --git a/Sources/ComponentsKit/Components/Loading/UKLoading.swift b/Sources/ComponentsKit/Components/Loading/UKLoading.swift index bd4adecd..502c3ce6 100644 --- a/Sources/ComponentsKit/Components/Loading/UKLoading.swift +++ b/Sources/ComponentsKit/Components/Loading/UKLoading.swift @@ -64,12 +64,6 @@ open class UKLoading: UIView, UKComponent { self.addSpinnerAnimation() - NotificationCenter.default.addObserver( - self, - selector: #selector(self.handleAppWillMoveToBackground), - name: UIApplication.willResignActiveNotification, - object: nil - ) NotificationCenter.default.addObserver( self, selector: #selector(self.handleAppMovedFromBackground), @@ -92,9 +86,6 @@ open class UKLoading: UIView, UKComponent { self.shapeLayer.strokeEnd = 0.75 } - @objc private func handleAppWillMoveToBackground() { - self.shapeLayer.removeAllAnimations() - } @objc private func handleAppMovedFromBackground() { self.addSpinnerAnimation() } @@ -116,8 +107,8 @@ open class UKLoading: UIView, UKComponent { } private func updateShapePath() { - let radius = self.model.preferredSize.height / 2 - self.shapeLayer.lineWidth / 2 let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY) + let radius = min(self.bounds.height, self.bounds.width) / 2 - self.model.loadingLineWidth self.shapeLayer.path = UIBezierPath( arcCenter: center, radius: radius, @@ -132,9 +123,11 @@ open class UKLoading: UIView, UKComponent { open override func layoutSubviews() { super.layoutSubviews() - // Adjust the layer's frame to fit within the view's bounds + CATransaction.begin() + CATransaction.setDisableActions(true) self.shapeLayer.frame = self.bounds self.updateShapePath() + CATransaction.commit() if self.isVisible { self.addSpinnerAnimation() @@ -144,7 +137,7 @@ open class UKLoading: UIView, UKComponent { // MARK: UIView methods open override func sizeThatFits(_ size: CGSize) -> CGSize { - let preferredSize = self.model.preferredSize + let preferredSize = self.model.preferredSize ?? size return .init( width: min(preferredSize.width, size.width), height: min(preferredSize.height, size.height) @@ -161,13 +154,19 @@ open class UKLoading: UIView, UKComponent { // MARK: Helpers private func addSpinnerAnimation() { + let animationKey = "rotationAnimation" + + guard self.shapeLayer.animation(forKey: animationKey).isNil else { + return + } + let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z") rotationAnimation.fromValue = 0 rotationAnimation.toValue = CGFloat.pi * 2 rotationAnimation.duration = 1.0 rotationAnimation.repeatCount = .infinity rotationAnimation.timingFunction = CAMediaTimingFunction(name: .linear) - self.shapeLayer.add(rotationAnimation, forKey: "rotationAnimation") + self.shapeLayer.add(rotationAnimation, forKey: animationKey) } private func handleTraitChanges() { From f47059fd992ccd7c05448cbee11ff5c30a280a15 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 5 Feb 2026 19:18:18 +0800 Subject: [PATCH 3/5] add line cap param to loading --- .../Helpers/PreviewPickers.swift | 13 +++++++ .../CircularProgressPreview.swift | 5 +-- .../PreviewPages/LoadingPreview.swift | 1 + .../Models/CircularProgressLineCap.swift | 38 ------------------- .../Components/Loading/Models/LoadingVM.swift | 3 ++ .../Components/Loading/SULoading.swift | 2 +- .../Components/Loading/UKLoading.swift | 3 +- .../ComponentsKit/Shared/Types/LineCap.swift | 36 ++++++++++++++++++ 8 files changed, 57 insertions(+), 44 deletions(-) delete mode 100644 Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift create mode 100644 Sources/ComponentsKit/Shared/Types/LineCap.swift diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift index e2b2da01..bf46eec7 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift @@ -254,6 +254,19 @@ struct KeyboardTypePicker: View { } } +// MARK: - LineCap + +struct LineCapPicker: View { + @Binding var selection: LineCap + + var body: some View { + Picker("Line Cap", selection: self.$selection) { + Text("Rounded").tag(LineCap.rounded) + Text("Square").tag(LineCap.square) + } + } +} + // MARK: - OverlayStylePicker struct OverlayStylePicker: View { diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift index ef98cd9d..defdae71 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CircularProgressPreview.swift @@ -29,10 +29,7 @@ struct CircularProgressPreview: View { Form { ComponentColorPicker(selection: self.$model.color) CaptionFontPicker(selection: self.$model.font) - Picker("Line Cap", selection: self.$model.lineCap) { - Text("Rounded").tag(CircularProgressVM.LineCap.rounded) - Text("Square").tag(CircularProgressVM.LineCap.square) - } + LineCapPicker(selection: self.$model.lineCap) Picker("Line Width", selection: self.$model.lineWidth) { Text("Default").tag(Optional.none) Text("2").tag(Optional.some(2)) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift index 905f74d8..c8003bb3 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift @@ -16,6 +16,7 @@ struct LoadingPreview: View { } Form { ComponentColorPicker(selection: self.$model.color) + LineCapPicker(selection: self.$model.lineCap) Picker("Line Width", selection: self.$model.lineWidth) { Text("Default").tag(Optional.none) Text("Custom: 6px").tag(CGFloat(6.0)) diff --git a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift b/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift deleted file mode 100644 index 72a86b8c..00000000 --- a/Sources/ComponentsKit/Components/CircularProgress/Models/CircularProgressLineCap.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SwiftUI -import UIKit - -extension CircularProgressVM { - /// Defines the style of line endings. - public enum LineCap { - /// The line ends with a semicircular arc that extends beyond the endpoint, creating a rounded appearance. - case rounded - /// The line ends exactly at the endpoint with a flat edge. - case square - } -} - -// MARK: - UIKit Helpers - -extension CircularProgressVM.LineCap { - var shapeLayerLineCap: CAShapeLayerLineCap { - switch self { - case .rounded: - return .round - case .square: - return .butt - } - } -} - -// MARK: - SwiftUI Helpers - -extension CircularProgressVM.LineCap { - var cgLineCap: CGLineCap { - switch self { - case .rounded: - return .round - case .square: - return .butt - } - } -} diff --git a/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift index 7ea60e41..fc3d11a3 100644 --- a/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift +++ b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift @@ -7,6 +7,9 @@ public struct LoadingVM: ComponentVM { /// Defaults to `.accent`. public var color: ComponentColor = .accent + /// The style of line endings. + public var lineCap: LineCap = .rounded + /// The width of the lines used in the loading indicator. /// /// If not provided, the line width is automatically adjusted based on the size. diff --git a/Sources/ComponentsKit/Components/Loading/SULoading.swift b/Sources/ComponentsKit/Components/Loading/SULoading.swift index d0b96d47..206d03aa 100644 --- a/Sources/ComponentsKit/Components/Loading/SULoading.swift +++ b/Sources/ComponentsKit/Components/Loading/SULoading.swift @@ -40,7 +40,7 @@ public struct SULoading: View { self.model.color.main.color, style: StrokeStyle( lineWidth: self.model.loadingLineWidth, - lineCap: .round, + lineCap: self.model.lineCap.cgLineCap, lineJoin: .round, miterLimit: 0 ) diff --git a/Sources/ComponentsKit/Components/Loading/UKLoading.swift b/Sources/ComponentsKit/Components/Loading/UKLoading.swift index 502c3ce6..040a1d0f 100644 --- a/Sources/ComponentsKit/Components/Loading/UKLoading.swift +++ b/Sources/ComponentsKit/Components/Loading/UKLoading.swift @@ -82,7 +82,7 @@ open class UKLoading: UIView, UKComponent { self.shapeLayer.lineWidth = self.model.loadingLineWidth self.shapeLayer.strokeColor = self.model.color.main.uiColor.cgColor self.shapeLayer.fillColor = UIColor.clear.cgColor - self.shapeLayer.lineCap = .round + self.shapeLayer.lineCap = self.model.lineCap.shapeLayerLineCap self.shapeLayer.strokeEnd = 0.75 } @@ -97,6 +97,7 @@ open class UKLoading: UIView, UKComponent { self.shapeLayer.lineWidth = self.model.loadingLineWidth self.shapeLayer.strokeColor = self.model.color.main.uiColor.cgColor + self.shapeLayer.lineCap = self.model.lineCap.shapeLayerLineCap if self.model.shouldUpdateShapePath(oldModel) { self.updateShapePath() diff --git a/Sources/ComponentsKit/Shared/Types/LineCap.swift b/Sources/ComponentsKit/Shared/Types/LineCap.swift new file mode 100644 index 00000000..833aa40c --- /dev/null +++ b/Sources/ComponentsKit/Shared/Types/LineCap.swift @@ -0,0 +1,36 @@ +import SwiftUI +import UIKit + +/// Defines the style of line endings. +public enum LineCap { + /// The line ends with a semicircular arc that extends beyond the endpoint, creating a rounded appearance. + case rounded + /// The line ends exactly at the endpoint with a flat edge. + case square +} + +// MARK: - UIKit Helpers + +extension LineCap { + var shapeLayerLineCap: CAShapeLayerLineCap { + switch self { + case .rounded: + return .round + case .square: + return .butt + } + } +} + +// MARK: - SwiftUI Helpers + +extension LineCap { + var cgLineCap: CGLineCap { + switch self { + case .rounded: + return .round + case .square: + return .butt + } + } +} From 0f04cf49b33fccd168b3e18b904e1af11bf0b8ab Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 6 Feb 2026 13:13:33 +0800 Subject: [PATCH 4/5] add center and radius helpers to loading model --- .../Components/Loading/Models/LoadingVM.swift | 6 ++++++ Sources/ComponentsKit/Components/Loading/SULoading.swift | 7 ++----- Sources/ComponentsKit/Components/Loading/UKLoading.swift | 6 ++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift index fc3d11a3..d92606de 100644 --- a/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift +++ b/Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift @@ -55,6 +55,12 @@ extension LoadingVM { return .init(width: 48, height: 48) } } + func radius(size: CGSize) -> CGFloat { + return min(size.width, size.height) / 2 - self.loadingLineWidth / 2 + } + func center(size: CGSize) -> CGPoint { + return .init(x: size.width / 2, y: size.height / 2) + } } // MARK: UIKit Helpers diff --git a/Sources/ComponentsKit/Components/Loading/SULoading.swift b/Sources/ComponentsKit/Components/Loading/SULoading.swift index 206d03aa..2133d22e 100644 --- a/Sources/ComponentsKit/Components/Loading/SULoading.swift +++ b/Sources/ComponentsKit/Components/Loading/SULoading.swift @@ -25,11 +25,8 @@ public struct SULoading: View { GeometryReader { geometry in Path { path in path.addArc( - center: .init( - x: geometry.size.width / 2, - y: geometry.size.height / 2 - ), - radius: min(geometry.size.width, geometry.size.height) / 2 - self.model.loadingLineWidth, + center: self.model.center(size: geometry.size), + radius: self.model.radius(size: geometry.size), startAngle: .radians(0), endAngle: .radians(2 * .pi), clockwise: true diff --git a/Sources/ComponentsKit/Components/Loading/UKLoading.swift b/Sources/ComponentsKit/Components/Loading/UKLoading.swift index 040a1d0f..9a9f0e27 100644 --- a/Sources/ComponentsKit/Components/Loading/UKLoading.swift +++ b/Sources/ComponentsKit/Components/Loading/UKLoading.swift @@ -108,11 +108,9 @@ open class UKLoading: UIView, UKComponent { } private func updateShapePath() { - let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY) - let radius = min(self.bounds.height, self.bounds.width) / 2 - self.model.loadingLineWidth self.shapeLayer.path = UIBezierPath( - arcCenter: center, - radius: radius, + arcCenter: self.model.center(size: self.bounds.size), + radius: self.model.radius(size: self.bounds.size), startAngle: 0, endAngle: 2 * .pi, clockwise: true From 84c5fe158f82c6e678dee442621bd1c72acc86fe Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 6 Feb 2026 13:18:34 +0800 Subject: [PATCH 5/5] fix typo --- .../PreviewPages/SegmentedControlPreview.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SegmentedControlPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SegmentedControlPreview.swift index 6566bdeb..86e66339 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SegmentedControlPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SegmentedControlPreview.swift @@ -18,7 +18,7 @@ struct SegmentedControlPreview: View { $0.title = "iPad" }, .init(id: .mac) { - $0.title = "Mackbook" + $0.title = "Macbook" } ] }