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 @@ -19,6 +19,7 @@ struct ButtonPreview: View {
}
Form {
AnimationScalePicker(selection: self.$model.animationScale)
BackgroundStylePicker(selection: self.$model.backgroundStyle)
ComponentOptionalColorPicker(selection: self.$model.color)
Picker("Content Spacing", selection: self.$model.contentSpacing) {
Text("4").tag(CGFloat(4))
Expand Down
14 changes: 14 additions & 0 deletions Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public struct ButtonVM: ComponentVM {
/// The color of the button.
public var color: ComponentColor?

/// Defines how the button renders its background.
///
/// Defaults to `.solid`.
public var backgroundStyle: BackgroundStyle = .solid

/// The spacing between the button's title and its image or loading indicator.
///
/// Defaults to `8.0`.
Expand Down Expand Up @@ -84,6 +89,14 @@ extension ButtonVM {
var isInteractive: Bool {
self.isEnabled && !self.isLoading
}
var isCustomTapAnimationEnabled: Bool {
switch self.backgroundStyle {
case .solid, .blur:
return true
case .liquidGlass:
return false
}
}
var preferredLoadingVM: LoadingVM {
return self.loadingVM ?? .init {
$0.color = .init(
Expand Down Expand Up @@ -246,6 +259,7 @@ extension ButtonVM {
|| self.imageWithLegacyFallback != oldModel.imageWithLegacyFallback
|| self.contentSpacing != oldModel.contentSpacing
|| self.title != oldModel.title
|| self.style != oldModel.style
}
}

Expand Down
104 changes: 73 additions & 31 deletions Sources/ComponentsKit/Components/Button/SUButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,28 @@ public struct SUButton: View {
HStack(spacing: self.model.contentSpacing) {
self.content
}
.font(self.model.preferredFont.font)
.lineLimit(1)
.padding(.horizontal, self.model.horizontalPadding)
.frame(maxWidth: self.model.width)
.frame(height: self.model.height)
.contentShape(.rect)
.foregroundStyle(self.model.foregroundColor.color)
.buttonBackground(
shape: RoundedRectangle(cornerRadius: self.model.cornerRadius.value()),
model: self.model
)
}
.buttonStyle(CustomButtonStyle(model: self.model))
.simultaneousGesture(DragGesture(minimumDistance: 0.0)
.onChanged { _ in
self.scale = self.model.animationScale.value
}
.onEnded { _ in
self.scale = 1.0
}
.buttonStyle(CustomButtonStyle())
.simultaneousGesture(
DragGesture(minimumDistance: 0.0)
.onChanged { _ in
self.scale = self.model.animationScale.value
}
.onEnded { _ in
self.scale = 1.0
},
isEnabled: self.model.isCustomTapAnimationEnabled
)
.disabled(!self.model.isInteractive)
.scaleEffect(self.scale, anchor: .center)
Expand Down Expand Up @@ -109,31 +122,60 @@ private struct ButtonImage: View {
}

private struct CustomButtonStyle: SwiftUI.ButtonStyle {
let model: ButtonVM

func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(self.model.preferredFont.font)
.lineLimit(1)
.padding(.horizontal, self.model.horizontalPadding)
.frame(maxWidth: self.model.width)
.frame(height: self.model.height)
.contentShape(.rect)
.foregroundStyle(self.model.foregroundColor.color)
.background(self.model.backgroundColor?.color ?? .clear)
.clipShape(
RoundedRectangle(
cornerRadius: self.model.cornerRadius.value()
)
)
.overlay {
RoundedRectangle(
cornerRadius: self.model.cornerRadius.value()
)
.strokeBorder(
self.model.borderColor?.color ?? .clear,
lineWidth: self.model.borderWidth
)
}
}

extension View {
@ViewBuilder
fileprivate func buttonBackground<BackgroundShape: InsettableShape>(
shape: BackgroundShape,
model: ButtonVM
) -> some View {
switch model.backgroundStyle {
case .solid:
self
.background(model.backgroundColor?.color ?? .clear)
.clipShape(shape)
.overlay {
shape.strokeBorder(
model.borderColor?.color ?? .clear,
lineWidth: model.borderWidth
)
}
case .blur:
self
.background {
shape
.fill(.thinMaterial)
.overlay {
shape.strokeBorder(
model.borderColor?.color ?? .clear,
lineWidth: model.borderWidth
)
}
}
.background(model.backgroundColor?.color)
.clipShape(shape)
case .liquidGlass:
if #available(iOS 26.0, *) {
self
.overlay {
shape.strokeBorder(
model.borderColor?.color ?? .clear,
lineWidth: model.borderWidth
)
}
.glassEffect(
.regular
.tint(model.backgroundColor?.color)
.interactive(model.isInteractive),
in: shape
)
} else {
self
}
}
}
}
49 changes: 43 additions & 6 deletions Sources/ComponentsKit/Components/Button/UKButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ open class UKButton: FullWidthComponent, UKComponent {
/// A Boolean value indicating whether the button is pressed.
public private(set) var isPressed: Bool = false {
didSet {
guard self.model.isCustomTapAnimationEnabled else { return }
UIView.animate(withDuration: 0.05, delay: 0, options: [.curveEaseOut]) {
self.transform = self.isPressed && self.model.isInteractive
? .init(
Expand All @@ -43,6 +44,9 @@ open class UKButton: FullWidthComponent, UKComponent {
/// An optional image displayed alongside the title.
public let imageView = UIImageView()

/// The visual effect container used to render blur and liquid glass button backgrounds.
public let backgroundEffectView = UIVisualEffectView()

// MARK: Private Properties

private var imageViewConstraints = LayoutConstraints()
Expand Down Expand Up @@ -79,7 +83,8 @@ open class UKButton: FullWidthComponent, UKComponent {
// MARK: Setup

private func setup() {
self.addSubview(self.stackView)
self.addSubview(self.backgroundEffectView)
self.backgroundEffectView.contentView.addSubview(self.stackView)

self.stackView.addArrangedSubview(self.loaderView)
self.stackView.addArrangedSubview(self.titleLabel)
Expand All @@ -101,6 +106,7 @@ open class UKButton: FullWidthComponent, UKComponent {

private func style() {
Self.Style.mainView(self, model: self.model)
Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model)
Self.Style.titleLabel(self.titleLabel, model: self.model)
Self.Style.configureStackView(self.stackView, model: self.model)
Self.Style.loaderView(self.loaderView, model: self.model)
Expand All @@ -110,6 +116,7 @@ open class UKButton: FullWidthComponent, UKComponent {
// MARK: Layout

private func layout() {
self.backgroundEffectView.allEdges()
self.stackView.center()

self.imageViewConstraints = self.imageView.size(
Expand All @@ -121,7 +128,10 @@ open class UKButton: FullWidthComponent, UKComponent {
open override func layoutSubviews() {
super.layoutSubviews()

self.layer.cornerRadius = self.model.cornerRadius.value(for: self.bounds.height)
let cornerRadius = self.model.cornerRadius.value(for: self.bounds.height)
self.layer.cornerRadius = cornerRadius
self.backgroundEffectView.layer.cornerRadius = cornerRadius
self.backgroundEffectView.contentView.layer.cornerRadius = cornerRadius
}

// MARK: Update
Expand Down Expand Up @@ -218,7 +228,8 @@ open class UKButton: FullWidthComponent, UKComponent {
// MARK: Helpers

@objc private func handleTraitChanges() {
self.layer.borderColor = self.model.borderColor?.uiColor.cgColor
Self.Style.mainView(self, model: self.model)
Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model)
}
}

Expand All @@ -227,13 +238,39 @@ open class UKButton: FullWidthComponent, UKComponent {
extension UKButton {
fileprivate enum Style {
static func mainView(_ view: UIView, model: Model) {
view.layer.borderWidth = model.borderWidth
view.layer.borderColor = model.borderColor?.uiColor.cgColor
view.backgroundColor = model.backgroundColor?.uiColor
view.backgroundColor = nil
view.layer.cornerRadius = model.cornerRadius.value(
for: view.bounds.height
)
}
static func backgroundEffectView(_ view: UIVisualEffectView, model: Model) {
let cornerRadius = model.cornerRadius.value(for: view.bounds.height)
view.contentView.layer.cornerRadius = cornerRadius
view.layer.cornerRadius = cornerRadius
view.layer.borderColor = model.borderColor?.uiColor.cgColor
view.layer.borderWidth = model.borderWidth
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.isInteractive
view.effect = effect
view.backgroundColor = nil
} else {
view.effect = nil
view.backgroundColor = model.backgroundColor?.uiColor
}
}
}
static func titleLabel(_ label: UILabel, model: Model) {
label.textAlignment = .center
label.text = model.title
Expand Down