Skip to content
Closed
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 @@ -6,7 +6,7 @@ struct ButtonPreview: View {
@State private var model = ButtonVM {
$0.title = "Button"
}

var body: some View {
VStack {
PreviewWrapper(title: "UIKit") {
Expand All @@ -25,6 +25,18 @@ struct ButtonPreview: View {
ButtonFontPicker(selection: self.$model.font)
Toggle("Enabled", isOn: self.$model.isEnabled)
Toggle("Full Width", isOn: self.$model.isFullWidth)
Picker("Image Source", selection: self.$model.imageSrc) {
Text("SF Symbol").tag(ButtonVM.ImageSource.sfSymbol("camera.fill"))
Text("Local").tag(ButtonVM.ImageSource.local("avatar_placeholder"))
Text("None").tag(Optional<ButtonVM.ImageSource>.none)
}
if self.model.imageSrc != nil {
Picker("Image Location", selection: self.$model.imageLocation) {
Text("Leading").tag(ButtonVM.ImageLocation.leading)
Text("Trailing").tag(ButtonVM.ImageLocation.trailing)
}
}
Toggle("Loading", isOn: self.$model.isLoading)
SizePicker(selection: self.$model.size)
Picker("Style", selection: self.$model.style) {
Text("Filled").tag(ButtonStyle.filled)
Expand All @@ -34,6 +46,16 @@ struct ButtonPreview: View {
Text("Bordered with medium border").tag(ButtonStyle.bordered(.medium))
Text("Bordered with large border").tag(ButtonStyle.bordered(.large))
}
.onChange(of: self.model.imageLocation) { _ in
if self.model.isLoading {
self.model.isLoading = false
}
}
.onChange(of: self.model.imageSrc) { _ in
if self.model.isLoading {
self.model.isLoading = false
}
}
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import SwiftUI
import UIKit

/// A model that defines the appearance properties for a button component.
Expand Down Expand Up @@ -43,13 +44,45 @@ public struct ButtonVM: ComponentVM {
/// Defaults to `.filled`.
public var style: ButtonStyle = .filled

/// The loading VM used for the loading indicator.
///
/// If not provided, a default loading view model is used.
public var loadingVM: LoadingVM?

/// A Boolean value indicating whether the button is currently in a loading state.
///
/// Defaults to `false`.
public var isLoading: Bool = false

/// The source of the image to be displayed.
public var imageSrc: ImageSource?

/// The position of the image relative to the button's title.
///
/// Defaults to `.leading`.
public var imageLocation: ImageLocation = .leading

/// The spacing between the button's title and its image or loading indicator.
///
/// Defaults to `8.0`.
public var contentSpacing: CGFloat = 8.0

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

// MARK: Shared Helpers

extension ButtonVM {
var preferredLoadingVM: LoadingVM {
return self.loadingVM ?? .init {
$0.color = .init(
main: foregroundColor,
contrast: self.color?.main ?? .background
)
$0.size = .small
}
}
var backgroundColor: UniversalColor? {
switch self.style {
case .filled:
Expand Down Expand Up @@ -121,6 +154,18 @@ extension ButtonVM {
}
}

extension ButtonVM {
public enum ImageSource: Hashable {
case sfSymbol(String)
case local(String, bundle: Bundle? = nil)
}

public enum ImageLocation {
case leading
case trailing
}
}

// MARK: UIKit Helpers

extension ButtonVM {
Expand Down Expand Up @@ -155,3 +200,15 @@ extension ButtonVM {
return self.isFullWidth ? 10_000 : nil
}
}

extension ButtonVM {
var buttonImage: Image? {
guard let imageSrc = self.imageSrc else { return nil }
switch imageSrc {
case .sfSymbol(let name):
return Image(systemName: name)
case .local(let name, let bundle):
return Image(name, bundle: bundle)
}
}
}
55 changes: 38 additions & 17 deletions Sources/ComponentsKit/Components/Button/SUButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,46 @@ public struct SUButton: View {
// MARK: Body

public var body: some View {
Button(self.model.title, action: self.action)
.buttonStyle(CustomButtonStyle(model: self.model))
.simultaneousGesture(DragGesture(minimumDistance: 0.0)
.onChanged { _ in
self.isPressed = true
}
.onEnded { _ in
self.isPressed = false
}
)
.disabled(!self.model.isEnabled)
.scaleEffect(
self.isPressed ? self.model.animationScale.value : 1,
anchor: .center
)
Button(action: self.action) {
HStack(spacing: self.model.contentSpacing) {
self.content()
}
.frame(maxWidth: self.model.width)
.frame(height: self.model.height)
}
.buttonStyle(CustomButtonStyle(model: self.model))
.simultaneousGesture(DragGesture(minimumDistance: 0.0)
.onChanged { _ in
self.isPressed = true
}
.onEnded { _ in
self.isPressed = false
}
)
.disabled(!self.model.isEnabled || self.model.isLoading)
.scaleEffect(
self.isPressed ? self.model.animationScale.value : 1,
anchor: .center
)
}
}

// MARK: - Helpers
@ViewBuilder
private func content() -> some View {
switch (self.model.isLoading, self.model.buttonImage, self.model.imageLocation) {
case (true, _, _):
SULoading(model: self.model.preferredLoadingVM)
Text(self.model.title)
case (false, let image?, .leading):
image
Text(self.model.title)
case (false, let image?, .trailing):
Text(self.model.title)
image
default:
Text(self.model.title)
}
}
}

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