diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift index ce9e600..ca976dd 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift @@ -17,6 +17,7 @@ struct CardPreview: View { Form { AnimationScalePicker(selection: self.$model.animationScale) Picker("Background Color", selection: self.$model.backgroundColor) { + Text("Clear").tag(Optional.none) Text("Background").tag(UniversalColor.background) Text("Secondary Background").tag(UniversalColor.secondaryBackground) Text("Accent Background").tag(UniversalColor.accentBackground) @@ -24,6 +25,7 @@ struct CardPreview: View { Text("Warning Background").tag(UniversalColor.warningBackground) Text("Danger Background").tag(UniversalColor.dangerBackground) } + BackgroundStylePicker(selection: self.$model.backgroundStyle) Picker("Border Color", selection: self.$model.borderColor) { Text("Divider").tag(UniversalColor.divider) Text("Primary").tag(UniversalColor.primary) diff --git a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift index 2103b56..ce37b9e 100644 --- a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift +++ b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift @@ -8,7 +8,10 @@ public struct CardVM: ComponentVM { public var animationScale: AnimationScale = .medium /// The background color of the card. - public var backgroundColor: UniversalColor = .background + public var backgroundColor: UniversalColor? = .background + + /// Defines how the card renders its background. + public var backgroundStyle: BackgroundStyle = .solid /// The border color of the card. public var borderColor: UniversalColor = .divider @@ -41,3 +44,16 @@ public struct CardVM: ComponentVM { /// Initializes a new instance of `CardVM` with default values. public init() {} } + +// MARK: - Helpers + +extension CardVM { + var isTapAnimationEnabled: Bool { + switch self.backgroundStyle { + case .solid, .blur: + return self.isTappable + case .liquidGlass: + return false + } + } +} diff --git a/Sources/ComponentsKit/Components/Card/SUCard.swift b/Sources/ComponentsKit/Components/Card/SUCard.swift index 63736e7..c6a1dc2 100644 --- a/Sources/ComponentsKit/Components/Card/SUCard.swift +++ b/Sources/ComponentsKit/Components/Card/SUCard.swift @@ -47,14 +47,9 @@ public struct SUCard: View { public var body: some View { self.content() .padding(self.model.contentPaddings.edgeInsets) - .background(self.model.backgroundColor.color) - .cornerRadius(self.model.cornerRadius.value) - .overlay( - RoundedRectangle(cornerRadius: self.model.cornerRadius.value) - .strokeBorder( - self.model.borderColor.color, - lineWidth: self.model.borderWidth.value - ) + .cardBackground( + shape: RoundedRectangle(cornerRadius: self.model.cornerRadius.value), + model: self.model ) .shadow(self.model.shadow) .observeSize { self.contentSize = $0 } @@ -71,9 +66,53 @@ public struct SUCard: View { .onEnded { _ in self.scale = 1.0 }, - isEnabled: self.model.isTappable + isEnabled: self.model.isTapAnimationEnabled ) .scaleEffect(self.scale, anchor: .center) .animation(.easeOut(duration: 0.05), value: self.scale) } } + +extension View { + @ViewBuilder + fileprivate func cardBackground( + shape: BackgroundShape, + model: CardVM + ) -> some View { + switch model.backgroundStyle { + case .solid: + self.background(model.backgroundColor?.color) + .clipShape(shape) + .overlay( + shape + .strokeBorder(model.borderColor.color, lineWidth: model.borderWidth.value) + ) + case .blur: + self + .background { + shape + .fill(.thinMaterial) + .overlay { + shape.strokeBorder(model.borderColor.color, lineWidth: model.borderWidth.value) + } + } + .background(model.backgroundColor?.color) + .clipShape(shape) + case .liquidGlass: + if #available(iOS 26.0, *) { + self + .overlay { + shape.strokeBorder(model.borderColor.color, lineWidth: model.borderWidth.value) + } + .glassEffect( + .regular + .tint(model.backgroundColor?.color) + .interactive(model.isTappable), + in: shape + ) + } else { + self + } + } + } +} diff --git a/Sources/ComponentsKit/Components/Card/UKCard.swift b/Sources/ComponentsKit/Components/Card/UKCard.swift index 8c620dd..190f05b 100644 --- a/Sources/ComponentsKit/Components/Card/UKCard.swift +++ b/Sources/ComponentsKit/Components/Card/UKCard.swift @@ -20,6 +20,8 @@ open class UKCard: UIView, UKComponent { /// The primary content of the card, provided as a custom view. public let content: Content + /// The visual effect container used to render blur and liquid glass card backgrounds. + public let backgroundEffectView = UIVisualEffectView() // MARK: - Public Properties @@ -29,6 +31,7 @@ open class UKCard: UIView, UKComponent { /// A Boolean value indicating whether the button is pressed. public private(set) var isPressed: Bool = false { didSet { + guard self.model.isTapAnimationEnabled else { return } UIView.animate(withDuration: 0.05, delay: 0, options: [.curveEaseOut]) { self.transform = self.isPressed ? .init( @@ -82,7 +85,8 @@ open class UKCard: UIView, UKComponent { /// Sets up the card's subviews. open func setup() { - self.addSubview(self.content) + self.addSubview(self.backgroundEffectView) + self.backgroundEffectView.contentView.addSubview(self.content) if #available(iOS 17.0, *) { self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in @@ -96,24 +100,29 @@ open class UKCard: UIView, UKComponent { /// Applies styling to the card's subviews. open func style() { Self.Style.mainView(self, model: self.model) + Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model) } // MARK: - Layout /// Configures the layout. open func layout() { + self.backgroundEffectView.allEdges() self.contentConstraints = LayoutConstraints.merged { - self.content.top(self.model.contentPaddings.top) - self.content.bottom(self.model.contentPaddings.bottom) - self.content.leading(self.model.contentPaddings.leading) - self.content.trailing(self.model.contentPaddings.trailing) + self.content.top(self.model.contentPaddings.top, to: self.backgroundEffectView.contentView) + self.content.bottom(self.model.contentPaddings.bottom, to: self.backgroundEffectView.contentView) + self.content.leading(self.model.contentPaddings.leading, to: self.backgroundEffectView.contentView) + self.content.trailing(self.model.contentPaddings.trailing, to: self.backgroundEffectView.contentView) } } open override func layoutSubviews() { super.layoutSubviews() - self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath + self.layer.shadowPath = UIBezierPath( + roundedRect: self.bounds, + cornerRadius: self.model.cornerRadius.value + ).cgPath } // MARK: - Update @@ -192,17 +201,41 @@ open class UKCard: UIView, UKComponent { @objc private func handleTraitChanges() { Self.Style.mainView(self, model: self.model) + Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model) } } extension UKCard { fileprivate enum Style { static func mainView(_ view: UIView, model: Model) { - view.backgroundColor = model.backgroundColor.uiColor view.layer.cornerRadius = model.cornerRadius.value - view.layer.borderWidth = model.borderWidth.value - view.layer.borderColor = model.borderColor.cgColor view.shadow(model.shadow) } + static func backgroundEffectView(_ view: UIVisualEffectView, model: Model) { + view.contentView.layer.cornerRadius = model.cornerRadius.value + view.layer.cornerRadius = model.cornerRadius.value + view.layer.borderColor = model.borderColor.cgColor + view.layer.borderWidth = model.borderWidth.value + view.clipsToBounds = true + + switch model.backgroundStyle { + case .solid: + view.effect = nil + view.backgroundColor = model.backgroundColor?.uiColor + case .blur: + view.effect = UIBlurEffect(style: .systemThinMaterial) + view.backgroundColor = model.backgroundColor?.uiColor + case .liquidGlass: + if #available(iOS 26.0, *) { + let effect = UIGlassEffect(style: .regular) + effect.tintColor = model.backgroundColor?.uiColor + effect.isInteractive = model.isTappable + view.effect = effect + view.backgroundColor = nil + } else { + view.effect = nil + } + } + } } }