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 4 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 @@ -23,8 +23,22 @@ struct ButtonPreview: View {
Text("Custom: 20px").tag(ComponentRadius.custom(20))
}
ButtonFontPicker(selection: self.$model.font)
Toggle("Enabled", isOn: self.$model.isEnabled)
if !self.model.isLoading {
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 +48,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
81 changes: 77 additions & 4 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,21 +44,57 @@ 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 {
private 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 Down Expand Up @@ -121,6 +158,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 @@ -148,10 +197,34 @@ extension ButtonVM {
}
}

extension ButtonVM {
public var uiImage: UIImage? {
guard let imageSrc = self.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: SwiftUI Helpers

extension ButtonVM {
var width: CGFloat? {
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).renderingMode(.template)
case .local(let name, let bundle):
return Image(name, bundle: bundle).renderingMode(.template)
}
}
}
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
70 changes: 66 additions & 4 deletions Sources/ComponentsKit/Components/Button/UKButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ open class UKButton: UIView, UKComponent {
/// A label that displays the title from the model.
public var titleLabel = UILabel()

/// A loader view, created with the preferred loading VM from the model.
public let loaderView: UKLoading

/// A stack view that arranges the loader and title label.
private let stackView = UIStackView()

/// A image view for displaying the image from the model.
public let imageView: UIImageView = UIImageView()

// MARK: UIView Properties

open override var intrinsicContentSize: CGSize {
Expand All @@ -50,6 +59,7 @@ open class UKButton: UIView, UKComponent {
) {
self.model = model
self.action = action
self.loaderView = UKLoading(model: model.preferredLoadingVM)
super.init(frame: .zero)

self.setup()
Expand All @@ -64,7 +74,7 @@ open class UKButton: UIView, UKComponent {
// MARK: Setup

private func setup() {
self.addSubview(self.titleLabel)
self.addSubview(self.stackView)

if #available(iOS 17.0, *) {
self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in
Expand All @@ -78,12 +88,23 @@ open class UKButton: UIView, UKComponent {
private func style() {
Self.Style.mainView(self, model: self.model)
Self.Style.titleLabel(self.titleLabel, model: self.model)
Self.Style.configureStackView(
self.stackView,
model: self.model,
loaderView: self.loaderView,
titleLabel: self.titleLabel,
imageView: self.imageView
)

self.loaderView.model = self.model.preferredLoadingVM

self.loaderView.isHidden = !self.model.isLoading
}

// MARK: Layout

private func layout() {
self.titleLabel.center()
self.stackView.center()
}

open override func layoutSubviews() {
Expand All @@ -99,15 +120,21 @@ open class UKButton: UIView, UKComponent {

self.style()

if self.model.shouldUpdateSize(oldModel) {
self.imageView.image = self.model.uiImage
self.imageView.tintColor = self.model.foregroundColor.uiColor

if self.model.shouldUpdateSize(oldModel)
|| self.model.isLoading != oldModel.isLoading
|| self.model.imageSrc != oldModel.imageSrc
|| self.model.imageLocation != oldModel.imageLocation {
self.invalidateIntrinsicContentSize()
}
}

// MARK: UIView methods

open override func sizeThatFits(_ size: CGSize) -> CGSize {
let contentSize = self.titleLabel.sizeThatFits(size)
let contentSize = self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
let preferredSize = self.model.preferredSize(
for: contentSize,
parentWidth: self.superview?.bounds.width
Expand Down Expand Up @@ -183,5 +210,40 @@ extension UKButton {
label.font = model.preferredFont.uiFont
label.textColor = model.foregroundColor.uiColor
}
static func configureStackView(
_ stackView: UIStackView,
model: Model,
loaderView: UKLoading,
titleLabel: UILabel,
imageView: UIImageView
) {
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = model.contentSpacing

for subview in stackView.arrangedSubviews {
stackView.removeArrangedSubview(subview)
subview.removeFromSuperview()
}

if model.isLoading {
stackView.addArrangedSubview(loaderView)
stackView.addArrangedSubview(titleLabel)
return
}

if model.imageSrc != nil {
switch model.imageLocation {
case .leading:
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(titleLabel)
case .trailing:
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(imageView)
}
} else {
stackView.addArrangedSubview(titleLabel)
}
}
}
}