Skip to content
Merged
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 @@ -17,13 +17,15 @@ struct CardPreview: View {
Form {
AnimationScalePicker(selection: self.$model.animationScale)
Picker("Background Color", selection: self.$model.backgroundColor) {
Text("Clear").tag(Optional<UniversalColor>.none)
Text("Background").tag(UniversalColor.background)
Text("Secondary Background").tag(UniversalColor.secondaryBackground)
Text("Accent Background").tag(UniversalColor.accentBackground)
Text("Success Background").tag(UniversalColor.successBackground)
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)
Expand Down
18 changes: 17 additions & 1 deletion Sources/ComponentsKit/Components/Card/Models/CardVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}
57 changes: 48 additions & 9 deletions Sources/ComponentsKit/Components/Card/SUCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,9 @@ public struct SUCard<Content: View>: 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 }
Expand All @@ -71,9 +66,53 @@ public struct SUCard<Content: View>: 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<BackgroundShape: InsettableShape>(
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
}
}
}
}
51 changes: 42 additions & 9 deletions Sources/ComponentsKit/Components/Card/UKCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ open class UKCard<Content: UIView>: 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

Expand All @@ -29,6 +31,7 @@ open class UKCard<Content: UIView>: 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(
Expand Down Expand Up @@ -82,7 +85,8 @@ open class UKCard<Content: UIView>: 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
Expand All @@ -96,24 +100,29 @@ open class UKCard<Content: UIView>: 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
Expand Down Expand Up @@ -192,17 +201,41 @@ open class UKCard<Content: UIView>: 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
}
}
}
}
}