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 @@ -82,12 +82,8 @@ struct ModalPreviewHelpers {
Text("16px").tag(Paddings(padding: 16))
Text("20px").tag(Paddings(padding: 20))
}
Picker("Corner Radius", selection: self.$model.cornerRadius) {
Text("None").tag(ModalRadius.none)
Text("Small").tag(ModalRadius.small)
Text("Medium").tag(ModalRadius.medium)
Text("Large").tag(ModalRadius.large)
Text("Custom 30px").tag(ModalRadius.custom(30))
ContainerRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom 30px").tag(ContainerRadius.custom(30))
}
Picker("Overlay Style", selection: self.$model.overlayStyle) {
Text("Blurred").tag(ModalOverlayStyle.blurred)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ struct ComponentOptionalColorPicker: View {

// MARK: - CornerRadiusPicker

struct CornerRadiusPicker<Custom: View>: View {
struct ComponentRadiusPicker<Custom: View>: View {
@Binding var selection: ComponentRadius
@ViewBuilder var custom: () -> Custom

Expand All @@ -91,6 +91,21 @@ struct CornerRadiusPicker<Custom: View>: View {
}
}

struct ContainerRadiusPicker<Custom: View>: View {
@Binding var selection: ContainerRadius
@ViewBuilder var custom: () -> Custom

var body: some View {
Picker("Corner Radius", selection: self.$selection) {
Text("None").tag(ContainerRadius.none)
Text("Small").tag(ContainerRadius.small)
Text("Medium").tag(ContainerRadius.medium)
Text("Large").tag(ContainerRadius.large)
self.custom()
}
}
}

// MARK: - FontPickers

struct BodyFontPicker: View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct ButtonPreview: View {
Form {
AnimationScalePicker(selection: self.$model.animationScale)
ComponentOptionalColorPicker(selection: self.$model.color)
CornerRadiusPicker(selection: self.$model.cornerRadius) {
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom: 20px").tag(ComponentRadius.custom(20))
}
ButtonFontPicker(selection: self.$model.font)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import ComponentsKit
import SwiftUI
import UIKit

struct CardPreview: View {
@State private var model = CardVM()

var body: some View {
VStack {
PreviewWrapper(title: "UIKit") {
UKCard(model: self.model, content: cardContent)
.preview
}
Form {
Picker("Background Color", selection: self.$model.backgroundColor) {
Text("Default").tag(Optional<UniversalColor>.none)
Text("Secondary Background").tag(UniversalColor.secondaryBackground)
Text("Accent Background").tag(ComponentColor.accent.background)
Text("Success Background").tag(ComponentColor.success.background)
Text("Warning Background").tag(ComponentColor.warning.background)
Text("Danger Background").tag(ComponentColor.danger.background)
}
Picker("Content Paddings", selection: self.$model.contentPaddings) {
Text("12px").tag(Paddings(padding: 12))
Text("16px").tag(Paddings(padding: 16))
Text("20px").tag(Paddings(padding: 20))
}
ContainerRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom 4px").tag(ContainerRadius.custom(4))
}
Picker("Shadow", selection: self.$model.shadow) {
Text("None").tag(Shadow.none)
Text("Small").tag(Shadow.small)
Text("Medium").tag(Shadow.medium)
Text("Large").tag(Shadow.large)
Text("Custom").tag(Shadow.custom(20.0, .zero, ComponentColor.accent.background))
}
}
}
}
}

#Preview {
CardPreview()
}

// MARK: - Helpers

private func cardContent() -> UIView {
let titleLabel = UILabel()
titleLabel.text = "Card"
titleLabel.font = UniversalFont.mdHeadline.uiFont
titleLabel.textColor = UniversalColor.foreground.uiColor
titleLabel.numberOfLines = 0

let subtitleLabel = UILabel()
subtitleLabel.text = "Card is a container for text, images, and other content."
subtitleLabel.font = UniversalFont.mdBody.uiFont
subtitleLabel.textColor = UniversalColor.secondaryForeground.uiColor
subtitleLabel.numberOfLines = 0

let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
stackView.axis = .vertical
stackView.spacing = 8

return stackView
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct CheckboxPreview: View {
Text("Long").tag("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
}
ComponentColorPicker(selection: self.$model.color)
CornerRadiusPicker(selection: self.$model.cornerRadius) {
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom: 2px").tag(ComponentRadius.custom(2))
}
BodyFontPicker(selection: self.$model.font)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct InputFieldPreview: View {
AutocapitalizationPicker(selection: self.$model.autocapitalization)
Toggle("Autocorrection Enabled", isOn: self.$model.isAutocorrectionEnabled)
ComponentOptionalColorPicker(selection: self.$model.color)
CornerRadiusPicker(selection: self.$model.cornerRadius) {
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom: 20px").tag(ComponentRadius.custom(20))
}
Toggle("Enabled", isOn: self.$model.isEnabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ struct SegmentedControlPreview: View {
}
Form {
ComponentOptionalColorPicker(selection: self.$model.color)
CornerRadiusPicker(selection: self.$model.cornerRadius) {
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom: 4px").tag(ComponentRadius.custom(4))
}
Toggle("Enabled", isOn: self.$model.isEnabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct TextInputPreviewPreview: View {
AutocapitalizationPicker(selection: self.$model.autocapitalization)
Toggle("Autocorrection Enabled", isOn: self.$model.isAutocorrectionEnabled)
ComponentOptionalColorPicker(selection: self.$model.color)
CornerRadiusPicker(selection: self.$model.cornerRadius) {
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom: 20px").tag(ComponentRadius.custom(20))
}
Toggle("Enabled", isOn: self.$model.isEnabled)
Expand Down
3 changes: 3 additions & 0 deletions Examples/DemosApp/DemosApp/Core/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ struct App: View {
NavigationLinkWithTitle("Button") {
ButtonPreview()
}
NavigationLinkWithTitle("Card") {
CardPreview()
}
NavigationLinkWithTitle("Checkbox") {
CheckboxPreview()
}
Expand Down
33 changes: 33 additions & 0 deletions Sources/ComponentsKit/Components/Card/CardVM.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

/// A model that defines the appearance properties for a card component.
public struct CardVM: ComponentVM {
/// The background color of the card.
public var backgroundColor: UniversalColor?

/// The padding applied to the card's content area.
///
/// Defaults to a padding value of `16` for all sides.
public var contentPaddings: Paddings = .init(padding: 16)

/// The corner radius of the card.
///
/// Defaults to `.medium`.
public var cornerRadius: ContainerRadius = .medium

/// The shadow of the card.
///
/// Defaults to `.medium`.
public var shadow: Shadow = .medium

/// Initializes a new instance of `CardVM` with default values.
public init() {}
}

// MARK: - Helpers

extension CardVM {
var preferredBackgroundColor: UniversalColor {
return self.backgroundColor ?? .background
}
}
152 changes: 152 additions & 0 deletions Sources/ComponentsKit/Components/Card/UKCard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import AutoLayout
import UIKit

/// A UIKit component that serves as a container for provided content.
///
/// - Example:
/// ```swift
/// let banner = UKCard(
/// model: .init(),
/// content: { _ in
/// let label = UILabel()
/// label.text = "This is the content of the card."
/// label.numberOfLines = 0
/// return label
/// }
/// )
open class UKCard: UIView, UKComponent {
// MARK: - Typealiases

/// A closure that returns the content view to be displayed inside the card.
public typealias Content = () -> UIView

// MARK: - Subviews

/// 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

private var contentConstraints = LayoutConstraints()

/// A model that defines the appearance properties.
public var model: CardVM {
didSet {
self.update(oldValue)
}
}

// MARK: - Initialization

/// Initializer.
///
/// - Parameters:
/// - model: A model that defines the appearance properties.
/// - content: The content that is displayed in the card.
public init(model: CardVM, content: @escaping Content) {
self.model = model
self.content = content()

super.init(frame: .zero)

self.setup()
self.style()
self.layout()
}

public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Setup

/// Sets up the card's subviews.
open func setup() {
self.addSubview(self.contentView)
self.contentView.addSubview(self.content)

if #available(iOS 17.0, *) {
self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in
view.handleTraitChanges()
}
}
}

// MARK: - Style

/// 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)
self.content.leading(self.model.contentPaddings.leading)
self.content.trailing(self.model.contentPaddings.trailing)
}
}

open override func layoutSubviews() {
super.layoutSubviews()

self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
}

/// Updates appearance when the model changes.
open func update(_ oldValue: CardVM) {
guard self.model != oldValue else { return }

self.style()

if self.model.contentPaddings != oldValue.contentPaddings {
self.contentConstraints.top?.constant = self.model.contentPaddings.top
self.contentConstraints.bottom?.constant = -self.model.contentPaddings.bottom
self.contentConstraints.leading?.constant = self.model.contentPaddings.leading
self.contentConstraints.trailing?.constant = -self.model.contentPaddings.trailing

self.layoutIfNeeded()
}
}

// MARK: - UIView Methods

open override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
self.handleTraitChanges()
}

// MARK: - Helpers

@objc private func handleTraitChanges() {
Self.Style.mainView(self, model: self.model)
}
}

extension UKCard {
fileprivate enum Style {
static func mainView(_ view: UIView, model: Model) {
view.backgroundColor = UniversalColor.background.uiColor
view.layer.cornerRadius = model.cornerRadius.value
view.layer.borderWidth = 1
view.layer.borderColor = UniversalColor.divider.cgColor
view.shadow(model.shadow)
}

static func contentView(_ view: UIView, model: Model) {
view.backgroundColor = model.preferredBackgroundColor.uiColor
view.layer.cornerRadius = model.cornerRadius.value
}
}
}
Loading
Loading