diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift index e97783d9..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 { @@ -282,6 +295,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/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 3ee062f6..c8003bb3 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/LoadingPreview.swift @@ -16,14 +16,12 @@ 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)) } - SizePicker(selection: self.$model.size) - Picker("Style", selection: self.$model.style) { - Text("Spinner").tag(LoadingVM.Style.spinner) - } + OptionalSizePicker(selection: self.$model.size) } } } 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/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..d92606de 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. @@ -14,13 +17,13 @@ public struct LoadingVM: ComponentVM { /// The predefined size of the loading indicator. /// - /// Defaults to `.medium`. - public var size: ComponentSize = .medium - - /// The style of the loading indicator (e.g., spinner, bar). + /// 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 `.spinner`. - public var style: Style = .spinner + /// Defaults to `.medium`. + public var size: ComponentSize? = .medium /// Initializes a new instance of `LoadingVM` with default values. public init() {} @@ -30,23 +33,33 @@ 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.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) - } + var preferredSize: CGSize? { + guard let size else { + return nil + } + + switch 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 { - return self.preferredSize.height / 2 - self.loadingLineWidth / 2 + 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) } } @@ -57,14 +70,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..2133d22e 100644 --- a/Sources/ComponentsKit/Components/Loading/SULoading.swift +++ b/Sources/ComponentsKit/Components/Loading/SULoading.swift @@ -22,21 +22,22 @@ 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: self.model.center(size: geometry.size), + radius: self.model.radius(size: geometry.size), + startAngle: .radians(0), + endAngle: .radians(2 * .pi), + clockwise: true + ) + } .trim(from: 0, to: 0.75) .stroke( self.model.color.main.color, style: StrokeStyle( lineWidth: self.model.loadingLineWidth, - lineCap: .round, + lineCap: self.model.lineCap.cgLineCap, lineJoin: .round, miterLimit: 0 ) @@ -47,15 +48,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..9a9f0e27 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), @@ -88,13 +82,10 @@ 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 } - @objc private func handleAppWillMoveToBackground() { - self.shapeLayer.removeAllAnimations() - } @objc private func handleAppMovedFromBackground() { self.addSpinnerAnimation() } @@ -106,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() @@ -116,11 +108,9 @@ 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) 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 @@ -132,9 +122,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 +136,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 +153,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() { 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 + } + } +}