Skip to content

improve button #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
aedb812
SUButton new params
VislovIvan Apr 3, 2025
f3949d5
image fix
VislovIvan Apr 3, 2025
9e5d302
improve UKButton
VislovIvan Apr 3, 2025
68848a0
swiftlint fix
VislovIvan Apr 3, 2025
572135f
code style fix
VislovIvan Apr 6, 2025
6ac6171
shouldUpdateSize fix
VislovIvan Apr 6, 2025
ffd1d59
extension ImageSource and ImageLocation extracted
VislovIvan Apr 6, 2025
3171a79
rename extension buttonImage
VislovIvan Apr 6, 2025
f9bd154
params in alphabetical order
VislovIvan Apr 6, 2025
f316b55
fix some logic
VislovIvan Apr 6, 2025
8627341
fix uikit bug
VislovIvan Apr 6, 2025
31cc411
local image size bug fix
VislovIvan Apr 6, 2025
f74bf7b
added contentSpacing into preview
VislovIvan Apr 6, 2025
f48ce2c
hide title toggle
VislovIvan Apr 6, 2025
9923987
imageView contentMode
VislovIvan Apr 7, 2025
74e1bf1
swiftui image size bug fix
VislovIvan Apr 7, 2025
6a90b2b
preview "if" deleted
VislovIvan Apr 7, 2025
d97a971
deleted unused code
VislovIvan Apr 7, 2025
ef33dcd
fix constraints
VislovIvan Apr 7, 2025
29a2e9b
intrinsicContentSize fix
VislovIvan Apr 7, 2025
1af1227
uikit component code restructurization
VislovIvan Apr 7, 2025
9448544
image tint color extracted into model
VislovIvan Apr 7, 2025
19a71e6
func updateUIView fix
VislovIvan Apr 7, 2025
97622af
image extension fix
VislovIvan Apr 7, 2025
14d93ad
ImageLocation extracted
VislovIvan Apr 7, 2025
8971daa
fix spacing white titleLabel is hidden
VislovIvan Apr 7, 2025
5058c92
deleted if/else from swiftui component
VislovIvan Apr 7, 2025
0b523e4
fix tint color to local image
VislovIvan Apr 7, 2025
5cfdf75
fix preview sort
VislovIvan Apr 8, 2025
f3a2ee2
code fix
VislovIvan Apr 8, 2025
073f1ef
fix image size updating
VislovIvan Apr 8, 2025
9243dcd
some fix stackView
VislovIvan Apr 8, 2025
ae6e9c5
tint color fix
VislovIvan Apr 8, 2025
3ff5b27
fix imageView location bug
VislovIvan Apr 8, 2025
04ea424
improvements and bug fixes
mikhailChelbaev Apr 8, 2025
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why you included all the if conditions, but I think it's better to remove them. This way, people can understand how the parameters interact with each other.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also sort the params in alphabetical order

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add contentSpacing param and a toggle to show / hide the title (similar to the one in alert / modals)

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 @@ -18,14 +18,35 @@ struct ButtonPreview: View {
}
Form {
AnimationScalePicker(selection: self.$model.animationScale)
ButtonFontPicker(selection: self.$model.font)
ComponentOptionalColorPicker(selection: self.$model.color)
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom: 20px").tag(ComponentRadius.custom(20))
}
ButtonFontPicker(selection: self.$model.font)
Picker("Content Spacing", selection: self.$model.contentSpacing) {
Text("4").tag(CGFloat(4))
Text("8").tag(CGFloat(8))
Text("12").tag(CGFloat(12))
}
Toggle("Enabled", isOn: self.$model.isEnabled)
Toggle("Full Width", isOn: self.$model.isFullWidth)
Picker("Image Location", selection: self.$model.imageLocation) {
Text("Leading").tag(ButtonVM.ImageLocation.leading)
Text("Trailing").tag(ButtonVM.ImageLocation.trailing)
}
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)
}
Toggle("Loading", isOn: self.$model.isLoading)
SizePicker(selection: self.$model.size)
Toggle("Show Title", isOn: Binding<Bool>(
get: { !self.model.title.isEmpty },
set: { newValue in
self.model.title = newValue ? "Button" : ""
}
))
Picker("Style", selection: self.$model.style) {
Text("Filled").tag(ButtonStyle.filled)
Text("Plain").tag(ButtonStyle.plain)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

/// Specifies the position of the image relative to the button's title.
extension ButtonVM {
public enum ImageLocation {
/// The image is displayed before the title.
case leading
/// The image is displayed after the title.
case trailing
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation

/// Defines the image source options for a button.
extension ButtonVM {
public enum ImageSource: Hashable {
/// An image loaded from a system SF Symbol.
///
/// - Parameter name: The name of the SF Symbol.
case sfSymbol(String)

/// An image loaded from a local asset.
///
/// - Parameters:
/// - name: The name of the local image asset.
/// - bundle: The bundle containing the image resource. Defaults to `nil` to use the main bundle.
case local(String, bundle: Bundle? = nil)
}
}
81 changes: 74 additions & 7 deletions Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import SwiftUI
import UIKit

/// A model that defines the appearance properties for a button component.
public struct ButtonVM: ComponentVM {
/// The text displayed on the button.
public var title: String = ""

/// The scaling factor for the button's press animation, with a value between 0 and 1.
///
/// Defaults to `.medium`.
Expand All @@ -13,6 +11,11 @@ public struct ButtonVM: ComponentVM {
/// The color of the button.
public var color: ComponentColor?

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

/// The corner radius of the button.
///
/// Defaults to `.medium`.
Expand All @@ -23,6 +26,14 @@ public struct ButtonVM: ComponentVM {
/// If not provided, the font is automatically calculated based on the button's size.
public var font: UniversalFont?

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

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

/// A Boolean value indicating whether the button is enabled or disabled.
///
/// Defaults to `true`.
Expand All @@ -33,6 +44,16 @@ public struct ButtonVM: ComponentVM {
/// Defaults to `false`.
public var isFullWidth: Bool = false

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

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

/// The predefined size of the button.
///
/// Defaults to `.medium`.
Expand All @@ -43,21 +64,37 @@ public struct ButtonVM: ComponentVM {
/// Defaults to `.filled`.
public var style: ButtonStyle = .filled

/// The text displayed on the button.
public var title: String = ""

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

// MARK: Shared Helpers

extension ButtonVM {
var isInteractive: Bool {
self.isEnabled && !self.isLoading
}

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:
let color = self.color?.main ?? .content2
return color.enabled(self.isEnabled)
return color.enabled(self.isInteractive)
case .light:
let color = self.color?.background ?? .content1
return color.enabled(self.isEnabled)
return color.enabled(self.isInteractive)
case .plain, .bordered:
return nil
}
Expand All @@ -69,7 +106,7 @@ extension ButtonVM {
case .plain, .light, .bordered:
self.color?.main ?? .foreground
}
return color.enabled(self.isEnabled)
return color.enabled(self.isInteractive)
}
var borderWidth: CGFloat {
switch self.style {
Expand All @@ -85,7 +122,7 @@ extension ButtonVM {
return nil
case .bordered:
if let color {
return color.main.enabled(self.isEnabled)
return color.main.enabled(self.isInteractive)
} else {
return .divider
}
Expand All @@ -112,6 +149,13 @@ extension ButtonVM {
case .large: 52
}
}
var imageSide: CGFloat {
switch self.size {
case .small: 20
case .medium: 24
case .large: 28
}
}
var horizontalPadding: CGFloat {
return switch self.size {
case .small: 16
Expand All @@ -121,6 +165,18 @@ extension ButtonVM {
}
}

extension ButtonVM {
var image: UIImage? {
guard let imageSrc else { return nil }
switch imageSrc {
case .sfSymbol(let name):
return UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate)
case .local(let name, let bundle):
return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate)
}
}
}

// MARK: UIKit Helpers

extension ButtonVM {
Expand All @@ -145,6 +201,11 @@ extension ButtonVM {
return self.size != oldModel?.size
|| self.font != oldModel?.font
|| self.isFullWidth != oldModel?.isFullWidth
|| self.isLoading != oldModel?.isLoading
|| self.imageSrc != oldModel?.imageSrc
|| self.imageLocation != oldModel?.imageLocation
|| self.contentSpacing != oldModel?.contentSpacing
|| self.title != oldModel?.title
}
}

Expand All @@ -155,3 +216,9 @@ extension ButtonVM {
return self.isFullWidth ? 10_000 : nil
}
}

extension ButtonVM {
public var imageTintColor: UIColor? {
return self.foregroundColor.uiColor
}
}
85 changes: 69 additions & 16 deletions Sources/ComponentsKit/Components/Button/SUButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,78 @@ 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
}
}
.buttonStyle(CustomButtonStyle(model: self.model))
.simultaneousGesture(DragGesture(minimumDistance: 0.0)
.onChanged { _ in
self.isPressed = true
}
.onEnded { _ in
self.isPressed = false
}
)
.disabled(!self.model.isInteractive)
.scaleEffect(
self.isPressed ? self.model.animationScale.value : 1,
anchor: .center
)
}

@ViewBuilder
private var content: some View {
switch (self.model.isLoading, self.model.image, self.model.imageLocation) {
case (true, _, _) where self.model.title.isEmpty:
SULoading(model: self.model.preferredLoadingVM)
case (true, _, _):
SULoading(model: self.model.preferredLoadingVM)
Text(self.model.title)
case (false, let uiImage?, .leading) where self.model.title.isEmpty:
ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor)
.frame(width: self.model.imageSide, height: self.model.imageSide)
case (false, let uiImage?, .leading):
ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor)
.frame(width: self.model.imageSide, height: self.model.imageSide)
Text(self.model.title)
case (false, let uiImage?, .trailing) where self.model.title.isEmpty:
ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor)
.frame(width: self.model.imageSide, height: self.model.imageSide)
case (false, let uiImage?, .trailing):
Text(self.model.title)
ButtonImageView(image: uiImage, tintColor: self.model.imageTintColor)
.frame(width: self.model.imageSide, height: self.model.imageSide)
default:
Text(self.model.title)
}
}
}

// MARK: - Helpers
struct ButtonImageView: UIViewRepresentable {
class InternalImageView: UIImageView {
override var intrinsicContentSize: CGSize {
return .zero
}
}

let image: UIImage
let tintColor: UIColor?

func makeUIView(context: Context) -> UIImageView {
let imageView = InternalImageView()
imageView.image = self.image
imageView.contentMode = .scaleAspectFit
imageView.tintColor = self.tintColor
return imageView
}

func updateUIView(_ imageView: UIImageView, context: Context) {
imageView.image = self.image
imageView.tintColor = self.tintColor
}
}

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