diff --git a/.swiftlint.yml b/.swiftlint.yml index e84370cf..68835cbc 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -166,7 +166,7 @@ only_rules: - is_disjoint # Tuples shouldn’t have too many members. Create a custom type instead - - large_tuple + # - large_tuple # Prefer using .last(where:) over .filter { }.last in collections - last_where diff --git a/Sources/ComponentsKit/Components/Card/SUCard.swift b/Sources/ComponentsKit/Components/Card/SUCard.swift index 77127a48..3c0f57e6 100644 --- a/Sources/ComponentsKit/Components/Card/SUCard.swift +++ b/Sources/ComponentsKit/Components/Card/SUCard.swift @@ -40,7 +40,6 @@ public struct SUCard: View { self.content() .padding(self.model.contentPaddings.edgeInsets) .background(self.model.preferredBackgroundColor.color) - .background(UniversalColor.background.color) .cornerRadius(self.model.cornerRadius.value) .overlay( RoundedRectangle(cornerRadius: self.model.cornerRadius.value) diff --git a/Sources/ComponentsKit/Components/Card/UKCard.swift b/Sources/ComponentsKit/Components/Card/UKCard.swift index d3ddea83..89fd1fcd 100644 --- a/Sources/ComponentsKit/Components/Card/UKCard.swift +++ b/Sources/ComponentsKit/Components/Card/UKCard.swift @@ -25,8 +25,6 @@ open class UKCard: UIView, UKComponent { /// The primary content of the card, provided as a custom view. public let content: UIView - /// The container view that holds the card's content. - public let contentView = UIView() // MARK: - Properties @@ -65,8 +63,7 @@ open class UKCard: UIView, UKComponent { /// Sets up the card's subviews. open func setup() { - self.addSubview(self.contentView) - self.contentView.addSubview(self.content) + self.addSubview(self.content) if #available(iOS 17.0, *) { self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in @@ -80,15 +77,12 @@ open class UKCard: UIView, UKComponent { /// Applies styling to the card's subviews. open func style() { Self.Style.mainView(self, model: self.model) - Self.Style.contentView(self.contentView, model: self.model) } // MARK: - Layout /// Configures the layout. open func layout() { - self.contentView.allEdges() - self.contentConstraints = LayoutConstraints.merged { self.content.top(self.model.contentPaddings.top) self.content.bottom(self.model.contentPaddings.bottom) @@ -138,7 +132,7 @@ open class UKCard: UIView, UKComponent { extension UKCard { fileprivate enum Style { static func mainView(_ view: UIView, model: Model) { - view.backgroundColor = UniversalColor.background.uiColor + view.backgroundColor = model.preferredBackgroundColor.uiColor view.layer.cornerRadius = model.cornerRadius.value view.layer.borderWidth = model.borderWidth.value view.layer.borderColor = UniversalColor.divider.cgColor @@ -147,10 +141,5 @@ extension UKCard { view.layer.shadowColor = model.shadow.color.cgColor view.layer.shadowOpacity = 1 } - - static func contentView(_ view: UIView, model: Model) { - view.backgroundColor = model.preferredBackgroundColor.uiColor - view.layer.cornerRadius = model.cornerRadius.value - } } } diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift index f3e66db3..df582a0b 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift @@ -59,7 +59,6 @@ struct ModalContent: View { } .frame(maxWidth: self.model.size.maxWidth, alignment: .leading) .background(self.model.preferredBackgroundColor.color) - .background(UniversalColor.background.color) .clipShape(RoundedRectangle(cornerRadius: self.model.cornerRadius.value)) .overlay( RoundedRectangle(cornerRadius: self.model.cornerRadius.value) diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalOverlay.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalOverlay.swift index 42b8f40e..87f549c5 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalOverlay.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalOverlay.swift @@ -21,8 +21,10 @@ struct ModalOverlay: View { case .blurred: Color.clear.background(.ultraThinMaterial) case .transparent: - // Note: It can't be completely transparent as it won't receive touch gestures. - Color.black.opacity(0.0001) + // Note: The tap gesture isn't recognized when a completely transparent + // color is clicked. This can be fixed by calling contentShape, which + // defines the interactive area of the underlying view. + Color.clear.contentShape(.rect) } } .ignoresSafeArea(.all) diff --git a/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift index 72dc998a..6a30d47f 100644 --- a/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift +++ b/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift @@ -66,7 +66,7 @@ public class UKBottomModalController: UKModalController { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - self.container.transform = .init(translationX: 0, y: self.view.screenBounds.height) + self.contentView.transform = .init(translationX: 0, y: self.view.screenBounds.height) self.overlay.alpha = 0 } @@ -74,7 +74,7 @@ public class UKBottomModalController: UKModalController { super.viewDidAppear(animated) UIView.animate(withDuration: self.model.transition.value) { - self.container.transform = .identity + self.contentView.transform = .identity self.overlay.alpha = 1 } } @@ -84,7 +84,7 @@ public class UKBottomModalController: UKModalController { public override func setup() { super.setup() - self.container.addGestureRecognizer(UIPanGestureRecognizer( + self.contentView.addGestureRecognizer(UIPanGestureRecognizer( target: self, action: #selector(self.handleDragGesture) )) @@ -95,7 +95,7 @@ public class UKBottomModalController: UKModalController { public override func layout() { super.layout() - self.container.bottom(self.model.outerPaddings.bottom, safeArea: true) + self.contentView.bottom(self.model.outerPaddings.bottom, safeArea: true) } // MARK: - UIViewController Methods @@ -105,7 +105,7 @@ public class UKBottomModalController: UKModalController { completion: (() -> Void)? = nil ) { UIView.animate(withDuration: self.model.transition.value) { - self.container.transform = .init(translationX: 0, y: self.view.screenBounds.height) + self.contentView.transform = .init(translationX: 0, y: self.view.screenBounds.height) self.overlay.alpha = 0 } completion: { _ in super.dismiss(animated: false) @@ -117,25 +117,25 @@ public class UKBottomModalController: UKModalController { extension UKBottomModalController { @objc private func handleDragGesture(_ gesture: UIPanGestureRecognizer) { - let translation = gesture.translation(in: self.container).y - let velocity = gesture.velocity(in: self.container).y + let translation = gesture.translation(in: self.contentView).y + let velocity = gesture.velocity(in: self.contentView).y let offset = ModalAnimation.bottomModalOffset(translation, model: self.model) switch gesture.state { case .changed: - self.container.transform = .init(translationX: 0, y: offset) + self.contentView.transform = .init(translationX: 0, y: offset) case .ended: - let viewHeight = self.container.frame.height + let viewHeight = self.contentView.frame.height if ModalAnimation.shouldHideBottomModal(offset: offset, height: viewHeight, velocity: velocity, model: self.model) { self.dismiss(animated: true) } else { UIView.animate(withDuration: 0.2) { - self.container.transform = .identity + self.contentView.transform = .identity } } case .failed, .cancelled: UIView.animate(withDuration: 0.2) { - self.container.transform = .identity + self.contentView.transform = .identity } default: break diff --git a/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift index 5ba485e6..64a5a34e 100644 --- a/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift +++ b/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift @@ -71,7 +71,7 @@ public class UKCenterModalController: UKModalController { super.viewWillAppear(animated) self.overlay.alpha = 0 - self.container.alpha = 0 + self.contentView.alpha = 0 } public override func viewDidAppear(_ animated: Bool) { @@ -79,7 +79,7 @@ public class UKCenterModalController: UKModalController { UIView.animate(withDuration: self.model.transition.value) { self.overlay.alpha = 1 - self.container.alpha = 1 + self.contentView.alpha = 1 } } @@ -88,11 +88,11 @@ public class UKCenterModalController: UKModalController { public override func layout() { super.layout() - self.container.bottomAnchor.constraint( + self.contentView.bottomAnchor.constraint( lessThanOrEqualTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -self.model.outerPaddings.bottom ).isActive = true - self.container.centerVertically() + self.contentView.centerVertically() } // MARK: - UIViewController Methods @@ -103,7 +103,7 @@ public class UKCenterModalController: UKModalController { ) { UIView.animate(withDuration: self.model.transition.value) { self.overlay.alpha = 0 - self.container.alpha = 0 + self.contentView.alpha = 0 } completion: { _ in super.dismiss(animated: false) } diff --git a/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift index 7d3b9c39..b0992489 100644 --- a/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift +++ b/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift @@ -14,7 +14,7 @@ open class UKModalController: UIViewController { /// A model that defines the appearance properties. public let model: VM - private var containerWidthConstraint: NSLayoutConstraint? + private var contentViewWidthConstraint: NSLayoutConstraint? // MARK: - Subviews @@ -24,10 +24,8 @@ open class UKModalController: UIViewController { public var body = UIView() /// The optional footer view of the modal. public var footer: UIView? - /// The container view that holds the modal's content. - public let container = UIView() - /// The content view inside the container, holding the header, body, and footer. - public let content = UIView() + /// The content view, holding the header, body, and footer. + public let contentView = UIView() /// A scrollable wrapper for the body content. public let bodyWrapper: UIScrollView = ContentSizedScrollView() /// The overlay view that appears behind the modal. @@ -70,14 +68,13 @@ open class UKModalController: UIViewController { /// Sets up the modal's subviews and gesture recognizers. open func setup() { self.view.addSubview(self.overlay) - self.view.addSubview(self.container) - self.container.addSubview(self.content) + self.view.addSubview(self.contentView) if let header { - self.content.addSubview(header) + self.contentView.addSubview(header) } - self.content.addSubview(self.bodyWrapper) + self.contentView.addSubview(self.bodyWrapper) if let footer { - self.content.addSubview(footer) + self.contentView.addSubview(footer) } self.bodyWrapper.addSubview(self.body) @@ -104,8 +101,7 @@ open class UKModalController: UIViewController { /// Applies styling to the modal's subviews. open func style() { Self.Style.overlay(self.overlay, model: self.model) - Self.Style.container(self.container, model: self.model) - Self.Style.content(self.content, model: self.model) + Self.Style.contentView(self.contentView, model: self.model) Self.Style.bodyWrapper(self.bodyWrapper) } @@ -114,7 +110,6 @@ open class UKModalController: UIViewController { /// Configures the layout of the modal's subviews. open func layout() { self.overlay.allEdges() - self.content.allEdges() if let header { header.top(self.model.contentPaddings.top) @@ -145,38 +140,38 @@ open class UKModalController: UIViewController { self.bodyWrapper.horizontally() self.bodyWrapper.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - self.body.leading(self.model.contentPaddings.leading, to: self.container) - self.body.trailing(self.model.contentPaddings.trailing, to: self.container) + self.body.leading(self.model.contentPaddings.leading, to: self.contentView) + self.body.trailing(self.model.contentPaddings.trailing, to: self.contentView) - self.container.topAnchor.constraint( + self.contentView.topAnchor.constraint( greaterThanOrEqualTo: self.view.safeAreaLayoutGuide.topAnchor, constant: self.model.outerPaddings.top ).isActive = true - self.container.leadingAnchor.constraint( + self.contentView.leadingAnchor.constraint( greaterThanOrEqualTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: self.model.outerPaddings.leading ).isActive = true - self.container.trailingAnchor.constraint( + self.contentView.trailingAnchor.constraint( lessThanOrEqualTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -self.model.outerPaddings.trailing ).isActive = true - self.container.heightAnchor.constraint( + self.contentView.heightAnchor.constraint( greaterThanOrEqualToConstant: 80 ).isActive = true - self.containerWidthConstraint = self.container.width(self.model.size.maxWidth).width - self.containerWidthConstraint?.priority = .defaultHigh + self.contentViewWidthConstraint = self.contentView.width(self.model.size.maxWidth).width + self.contentViewWidthConstraint?.priority = .defaultHigh - self.bodyWrapper.widthAnchor.constraint(equalTo: self.container.widthAnchor).isActive = true + self.bodyWrapper.widthAnchor.constraint(equalTo: self.contentView.widthAnchor).isActive = true - self.container.centerHorizontally() + self.contentView.centerHorizontally() } open override func viewWillTransition( to size: CGSize, with coordinator: any UIViewControllerTransitionCoordinator ) { - self.containerWidthConstraint?.isActive = false + self.contentViewWidthConstraint?.isActive = false super.viewWillTransition(to: size, with: coordinator) } @@ -188,11 +183,11 @@ open class UKModalController: UIViewController { + self.model.outerPaddings.leading + self.model.outerPaddings.trailing if availableWidth > requiredWidth { - self.containerWidthConstraint?.priority = .required + self.contentViewWidthConstraint?.priority = .required } else { - self.containerWidthConstraint?.priority = .defaultHigh + self.contentViewWidthConstraint?.priority = .defaultHigh } - self.containerWidthConstraint?.isActive = true + self.contentViewWidthConstraint?.isActive = true } // MARK: - UIViewController Methods @@ -207,7 +202,7 @@ open class UKModalController: UIViewController { // MARK: - Helpers @objc private func handleTraitChanges() { - Self.Style.content(self.content, model: self.model) + Self.Style.contentView(self.contentView, model: self.model) } } @@ -225,11 +220,7 @@ extension UKModalController { (view as? UIVisualEffectView)?.effect = UIBlurEffect(style: .systemUltraThinMaterial) } } - static func container(_ view: UIView, model: VM) { - view.backgroundColor = UniversalColor.background.uiColor - view.layer.cornerRadius = model.cornerRadius.value - } - static func content(_ view: UIView, model: VM) { + static func contentView(_ view: UIView, model: VM) { view.backgroundColor = model.preferredBackgroundColor.uiColor view.layer.cornerRadius = model.cornerRadius.value view.layer.borderColor = UniversalColor.divider.cgColor diff --git a/Sources/ComponentsKit/Shared/Colors/ComponentColor.swift b/Sources/ComponentsKit/Shared/Colors/ComponentColor.swift index 967c6a30..be4238a5 100644 --- a/Sources/ComponentsKit/Shared/Colors/ComponentColor.swift +++ b/Sources/ComponentsKit/Shared/Colors/ComponentColor.swift @@ -11,7 +11,11 @@ public struct ComponentColor: Hashable { public let contrast: UniversalColor /// The background color for the component. - public let background: UniversalColor + public var background: UniversalColor { + return self._background ?? self.main.withOpacity(0.15).blended(with: .background) + } + + private let _background: UniversalColor? // MARK: - Initialization @@ -28,6 +32,6 @@ public struct ComponentColor: Hashable { ) { self.main = main self.contrast = contrast - self.background = background ?? main.withOpacity(0.15) + self._background = background } } diff --git a/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift b/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift index 7cbed9e8..7792ba04 100644 --- a/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift +++ b/Sources/ComponentsKit/Shared/Colors/UniversalColor.swift @@ -86,6 +86,47 @@ public struct UniversalColor: Hashable { return UIColor(color) } } + + /// Returns a tuple containing the red, green, blue, and alpha components of the color. + private var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + switch self { + case let .rgba(r, g, b, a): + return (r, g, b, a) + case .uiColor, .color: + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + self.uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + return (red, green, blue, alpha) + } + } + + /// Returns a new `ColorRepresentable` by blending the current color with another color. + /// + /// The blending is performed using the alpha value of the current color, + /// where the second color is treated as fully opaque (alpha = `1.0 - self.alpha`). + /// + /// The resulting color's RGBA components are calculated as: + /// - `red = self.red * self.alpha + other.red * (1.0 - self.alpha)` + /// - `green = self.green * self.alpha + other.green * (1.0 - self.alpha)` + /// - `blue = self.blue * self.alpha + other.blue * (1.0 - self.alpha)` + /// - The resulting color's alpha will always be `1.0`. + /// + /// - Parameter other: The `ColorRepresentable` to blend with the current color. + /// - Returns: A new `ColorRepresentable` instance representing the blended color. + fileprivate func blended(with other: Self) -> Self { + let rgba = self.rgba + let otherRgba = other.rgba + + let red = rgba.r * rgba.a + otherRgba.r * (1.0 - rgba.a) + let green = rgba.g * rgba.a + otherRgba.g * (1.0 - rgba.a) + let blue = rgba.b * rgba.a + otherRgba.b * (1.0 - rgba.a) + + return .rgba(r: red, g: green, b: blue, a: 1.0) + } } // MARK: - Properties @@ -119,29 +160,6 @@ public struct UniversalColor: Hashable { return Self(light: universal, dark: universal) } - // MARK: - Methods - - /// Returns a new `UniversalColor` with the specified opacity. - /// - /// - Parameter alpha: The desired opacity (0.0–1.0). - /// - Returns: A new `UniversalColor` instance with the adjusted opacity. - public func withOpacity(_ alpha: CGFloat) -> Self { - return .init( - light: self.light.withOpacity(alpha), - dark: self.dark.withOpacity(alpha) - ) - } - - /// Returns a disabled version of the color based on a global opacity configuration. - /// - /// - Parameter isEnabled: A Boolean value indicating whether the color should be enabled. - /// - Returns: A new `UniversalColor` instance with reduced opacity if `isEnabled` is `false`. - public func enabled(_ isEnabled: Bool) -> Self { - return isEnabled - ? self - : self.withOpacity(ComponentsKitConfig.shared.layout.disabledOpacity) - } - // MARK: - Colors /// Returns the `UIColor` representation of the color. @@ -167,4 +185,47 @@ public struct UniversalColor: Hashable { public var cgColor: CGColor { return self.uiColor.cgColor } + + // MARK: - Methods + + /// Returns a new `UniversalColor` with the specified opacity. + /// + /// - Parameter alpha: The desired opacity (0.0–1.0). + /// - Returns: A new `UniversalColor` instance with the adjusted opacity. + public func withOpacity(_ alpha: CGFloat) -> Self { + return .init( + light: self.light.withOpacity(alpha), + dark: self.dark.withOpacity(alpha) + ) + } + + /// Returns a disabled version of the color based on a global opacity configuration. + /// + /// - Parameter isEnabled: A Boolean value indicating whether the color should be enabled. + /// - Returns: A new `UniversalColor` instance with reduced opacity if `isEnabled` is `false`. + public func enabled(_ isEnabled: Bool) -> Self { + return isEnabled + ? self + : self.withOpacity(ComponentsKitConfig.shared.layout.disabledOpacity) + } + + /// Returns a new `UniversalColor` by blending the current color with another color. + /// + /// The blending is performed using the alpha value of the current color, + /// where the second color is treated as fully opaque (alpha = `1.0 - self.alpha`). + /// + /// The resulting color's RGBA components are calculated as: + /// - `red = self.red * self.alpha + other.red * (1.0 - self.alpha)` + /// - `green = self.green * self.alpha + other.green * (1.0 - self.alpha)` + /// - `blue = self.blue * self.alpha + other.blue * (1.0 - self.alpha)` + /// - The resulting color's alpha will always be `1.0`. + /// + /// - Parameter other: The `UniversalColor` to blend with the current color. + /// - Returns: A new `UniversalColor` instance representing the blended color. + public func blended(with other: Self) -> Self { + return .init( + light: self.light.blended(with: other.light), + dark: self.dark.blended(with: other.dark) + ) + } }