Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<ComponentSize>.none)
Text("Small").tag(ComponentSize.small)
Text("Medium").tag(ComponentSize.medium)
Text("Large").tag(ComponentSize.large)
}
}
}

// MARK: - SubmitTypePicker

struct SubmitTypePicker: View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CGFloat>.none)
Text("2").tag(Optional<CGFloat>.some(2))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CGFloat>.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)
}
}
}
Expand Down

This file was deleted.

This file was deleted.

64 changes: 33 additions & 31 deletions Sources/ComponentsKit/Components/Loading/Models/LoadingVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ 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.
public var lineWidth: CGFloat?

/// 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() {}
Expand All @@ -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)
}
}

Expand All @@ -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
)
}
}
32 changes: 17 additions & 15 deletions Sources/ComponentsKit/Components/Loading/SULoading.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
)
}
}
32 changes: 15 additions & 17 deletions Sources/ComponentsKit/Components/Loading/UKLoading.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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()
}
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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() {
Expand Down
36 changes: 36 additions & 0 deletions Sources/ComponentsKit/Shared/Types/LineCap.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}