From fd92229b28fb86523332f9951bde062b5724f68f Mon Sep 17 00:00:00 2001 From: Daniel DUMORTIER <43171132+daniel-dumortier@users.noreply.github.com> Date: Tue, 26 Jul 2022 17:30:11 +0200 Subject: [PATCH] feat(button): add icon alone UIKit VitaminButton type, and refactor icon management (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(button) : add icon alone button type, and refactor icon management * feat(button) : add icon alone button type, and refactor icon management - swiftlint fixes * feat(button) : add icon alone button type - fix whitespaces in README * feat(button): add icon alone UIKit VitaminButton type - PR reviews Co-authored-by: Florent LOTTHÉ * feat(button): add icon alone UIKit VitaminButton type - PR reviews Co-authored-by: Florent LOTTHÉ Co-authored-by: Florent LOTTHÉ --- .../Button/Cell/ButtonTableViewCell.swift | 41 ++++- .../Button/Cell/ButtonTableViewCell.xib | 34 +++- .../VitaminUIKit/Components/Button/README.md | 36 ++++ .../Button/VitaminButton+Deprecated.swift | 25 +++ .../Components/Button/VitaminButton.swift | 169 ++++++++++++++---- 5 files changed, 260 insertions(+), 45 deletions(-) create mode 100644 Sources/VitaminUIKit/Components/Button/VitaminButton+Deprecated.swift diff --git a/Showcase/Application/UIKit/Components/Button/Cell/ButtonTableViewCell.swift b/Showcase/Application/UIKit/Components/Button/Cell/ButtonTableViewCell.swift index 9cee8f4e..0ded0c53 100644 --- a/Showcase/Application/UIKit/Components/Button/Cell/ButtonTableViewCell.swift +++ b/Showcase/Application/UIKit/Components/Button/Cell/ButtonTableViewCell.swift @@ -19,21 +19,52 @@ final class ButtonTableViewCell: UITableViewCell { largeButton.size = .large } } + @IBOutlet weak var mediumIconAloneButton: VitaminButton! { + didSet { + mediumIconAloneButton.size = .medium + } + } + + @IBOutlet weak var largeIconAloneButton: VitaminButton! { + didSet { + largeIconAloneButton.size = .large + } + } func update(for style: VitaminButton.Style, isEnabled: Bool) { mediumButton.style = style largeButton.style = style + mediumIconAloneButton.style = style + largeIconAloneButton.style = style mediumButton.setTitle("\(style)", for: .normal) largeButton.setTitle("\(style)", for: .normal) - mediumButton.setLeadingImage(Vitamix.Line.Logos.apple.image, for: .normal, renderingMode: .alwaysTemplate) - largeButton.setTrailingImage( - Vitamix.Line.System.arrowRightS.image, - for: .normal, - renderingMode: .alwaysTemplate) + + mediumButton.setIconType( + .trailing( + image: Vitamix.Line.Logos.apple.image, + renderingMode: .alwaysTemplate), + for: .normal) + largeButton.setIconType( + .leading( + image: Vitamix.Line.System.arrowRightS.image, + renderingMode: .alwaysTemplate), + for: .normal) + mediumIconAloneButton.setIconType( + .alone( + image: Vitamix.Line.Logos.apple.image, + renderingMode: .alwaysTemplate), + for: .normal) + largeIconAloneButton.setIconType( + .alone( + image: Vitamix.Line.System.arrowRightS.image, + renderingMode: .alwaysTemplate), + for: .normal) mediumButton.isEnabled = isEnabled largeButton.isEnabled = isEnabled + mediumIconAloneButton.isEnabled = isEnabled + largeIconAloneButton.isEnabled = isEnabled contentView.backgroundColor = style.needsReversedBackground ? VitaminColor.Core.Background.brandPrimary : diff --git a/Showcase/Application/UIKit/Components/Button/Cell/ButtonTableViewCell.xib b/Showcase/Application/UIKit/Components/Button/Cell/ButtonTableViewCell.xib index 6e4d0e95..ed22e0c8 100644 --- a/Showcase/Application/UIKit/Components/Button/Cell/ButtonTableViewCell.xib +++ b/Showcase/Application/UIKit/Components/Button/Cell/ButtonTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -18,21 +18,41 @@ - + - + + + + + + + @@ -45,7 +65,9 @@ + + diff --git a/Sources/VitaminUIKit/Components/Button/README.md b/Sources/VitaminUIKit/Components/Button/README.md index f49d596b..00e67991 100644 --- a/Sources/VitaminUIKit/Components/Button/README.md +++ b/Sources/VitaminUIKit/Components/Button/README.md @@ -22,3 +22,39 @@ A good example of that would be to make your button stretched to fit its parent' Note: `VitaminButton` styles its title as `TextStyle.xxxbutton`, so make sure you setup the Roboto fonts properly. If you create your button through Storyboard or Xib, do not forget to set its type to `Custom` (instead of `System`, which is the default value). If not set to `Custom`, `VitaminButton` may act oddly in some circumstances. + +### Icon management +You can put an icon from Vitamin icons library in your button, or also have a square button with only one icon. + +To achieve this, `VitaminButton` provides you with a `setIconType(:for:)` method. +You can have different icons, or even different icon types per state. + +`IconType` has four cases: +- `.leading`: icon will be put before your button label +- `.trailing`: icon will be put after your button label +- `.alone`: button will be squared, an icon will be centered, and no button label will be displayed +- `.none`: no icon will be displayed in your button + +For `.leading`, `.trailing` and `.alone`, you must provide: +- an `UIImage` from Vitamin icons library (but will work with any `UIImage`) +- an optional `UIImage.RenderingMode`: if you provide one, image will be resized using this rendering mode, if not, image will be used as is (with unexpected behaviour if your image has a wrong size) + +If you do not specify specific icon type for a specific state, the one for `.normal` state will be chosen. +And by default, the icon type for `.normal` state is `.none`. + +Note: squareness of VitaminButton with `.alone` is handled through intrisicSize, it is not guaranteed if you apply constants that could impact width and height of button. + +```swift +import Vitamin + +// This button will have a white background with a dark border +// and an apple logo before text (in every state, since only .normal state has been set) +let button = VitaminButton(style: .secondary) +button.setIconType(.trailing(image: Vitamix.Line.Logos.apple.image, renderingMode: .alwaysTemplate), for: .normal) + +// This button will be square, have a blue background and a white icon centered +// icon will be an Apple logo in normal state, and an Android logo in .highlighted state +let button2 = VitaminButton(style: .primary) +button2.setIconType(.trailing(image: Vitamix.Line.Logos.apple.image, renderingMode: .alwaysTemplate), for: .normal) +button2.setIconType(.trailing(image: Vitamix.Line.Logos.android.image, renderingMode: .alwaysTemplate), for: .highlighted) +``` diff --git a/Sources/VitaminUIKit/Components/Button/VitaminButton+Deprecated.swift b/Sources/VitaminUIKit/Components/Button/VitaminButton+Deprecated.swift new file mode 100644 index 00000000..e160950d --- /dev/null +++ b/Sources/VitaminUIKit/Components/Button/VitaminButton+Deprecated.swift @@ -0,0 +1,25 @@ +// +// Vitamin iOS +// Apache License 2.0 +// + +import UIKit + +extension VitaminButton { + @available(*, deprecated, message: "Use setIconType(:for:) instead") + public func setLeadingImage(_ image: UIImage, for state: UIControl.State) { + setIconType(.leading(image: image, renderingMode: nil), for: state) + } + @available(*, deprecated, message: "Use setIconType(:for:) instead") + public func setTrailingImage(_ image: UIImage, for state: UIControl.State) { + setIconType(.trailing(image: image, renderingMode: nil), for: state) + } + @available(*, deprecated, message: "Use setIconType(:for:) instead") + public func setLeadingImage(_ image: UIImage, for state: UIControl.State, renderingMode: UIImage.RenderingMode) { + setIconType(.leading(image: image, renderingMode: renderingMode), for: state) + } + @available(*, deprecated, message: "Use setIconType(:for:) instead") + public func setTrailingImage(_ image: UIImage, for state: UIControl.State, renderingMode: UIImage.RenderingMode) { + setIconType(.trailing(image: image, renderingMode: renderingMode), for: state) + } +} diff --git a/Sources/VitaminUIKit/Components/Button/VitaminButton.swift b/Sources/VitaminUIKit/Components/Button/VitaminButton.swift index b2d132a9..07ed5836 100644 --- a/Sources/VitaminUIKit/Components/Button/VitaminButton.swift +++ b/Sources/VitaminUIKit/Components/Button/VitaminButton.swift @@ -34,15 +34,28 @@ public class VitaminButton: UIButton { } } + public enum IconType { + case trailing(image: UIImage, renderingMode: UIImage.RenderingMode?) + case leading(image: UIImage, renderingMode: UIImage.RenderingMode?) + case alone(image: UIImage, renderingMode: UIImage.RenderingMode?) + case none + } + + private var iconTypes: [UIControl.State: IconType] = [.normal: .none] + public override var isEnabled: Bool { didSet { updateOpacity() + updateSemantic() + updateImageInsets() } } public override var isHighlighted: Bool { didSet { updateBorder() + updateSemantic() + updateImageInsets() } } @@ -67,8 +80,8 @@ public class VitaminButton: UIButton { public override var intrinsicContentSize: CGSize { let baseSize = super.intrinsicContentSize return CGSize( - width: 2 * size.horizontalInset + baseSize.width, - height: 2 * size.verticalInset + baseSize.height + width: 2 * size.horizontalInset(iconType: getIconType(for: self.state)) + baseSize.width, + height: 2 * size.verticalInset(iconType: getIconType(for: self.state)) + baseSize.height ) } @@ -76,36 +89,6 @@ public class VitaminButton: UIButton { super.setTitle(title, for: state) applyNewTextStyle() } - - public func setLeadingImage(_ image: UIImage, for state: UIControl.State) { - self.setImage(image, for: state) - self.imageView?.tintColor = titleLabel?.textColor - self.tintColor = titleLabel?.textColor - self.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) - self.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - self.contentVerticalAlignment = .fill - self.contentMode = .center - self.imageView?.contentMode = .scaleAspectFit - } - - public func setTrailingImage(_ image: UIImage, for state: UIControl.State) { - self.setLeadingImage(image, for: state) - self.semanticContentAttribute = .forceRightToLeft - self.imageEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) - } - - public func setLeadingImage(_ image: UIImage, for state: UIControl.State, renderingMode: UIImage.RenderingMode) { - guard let resizedImage = image - .resizedImage(size: CGSize(width: 16, height: 16))? - .withRenderingMode(renderingMode) else { return } - self.setLeadingImage(resizedImage, for: state) - } - - public func setTrailingImage(_ image: UIImage, for state: UIControl.State, renderingMode: UIImage.RenderingMode) { - self.setLeadingImage(image, for: state, renderingMode: renderingMode) - self.semanticContentAttribute = .forceRightToLeft - self.imageEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) - } } // MARK: - Styling @@ -275,17 +258,135 @@ extension VitaminButton.Size { .button } - var horizontalInset: CGFloat { + func horizontalInset(iconType: VitaminButton.IconType) -> CGFloat { + if case .alone = iconType { + return 12 + } + switch self { case .medium: return 20 case .large: return 40 } } - var verticalInset: CGFloat { + func verticalInset(iconType: VitaminButton.IconType) -> CGFloat { + if case .alone = iconType { + return 12 + } + switch self { case .medium: return 16 case .large: return 20 } } + + func defaultIconSize(iconType: VitaminButton.IconType) -> CGFloat { + if case .alone = iconType { + switch self { + case .medium : return 24 + case .large: return 32 + } + } else { + switch self { + case .medium : return 20 + case .large: return 24 + } + } + } +} + +// - MARK: Icon managemant + +extension VitaminButton { + public func setIconType(_ iconType: IconType, for state: UIControl.State) { + iconTypes[state] = iconType + applyIcon(for: state) + } + + public func getIconType(for state: UIControl.State) -> IconType { + guard let iconType = iconTypes[state] else { + return iconTypes[.normal] ?? .none + } + return iconType + } + + private func applyIcon(for state: UIControl.State) { + self.invalidateIntrinsicContentSize() + let iconTypeForState = getIconType(for: state) + switch iconTypeForState { + case .none: + break + case let .trailing(image, renderingMode): + self.commonApplyIcon( + image: image, + iconType: iconTypeForState, + state: state, + renderingMode: renderingMode) + case let .leading(image, renderingMode): + self.commonApplyIcon( + image: image, + iconType: iconTypeForState, + state: state, + renderingMode: renderingMode) + case let .alone(image, renderingMode): + self.setTitle(nil, for: state) + self.commonApplyIcon( + image: image, + iconType: iconTypeForState, + state: state, + renderingMode: renderingMode) + } + } + + private func commonApplyIcon(image: UIImage, iconType: IconType, state: UIControl.State, renderingMode: UIImage.RenderingMode?) { + var imageUpdated = image + if let renderingMode = renderingMode { + guard let resizedImage = image + .resizedImage( + size: CGSize( + width: self.size.defaultIconSize(iconType: getIconType(for: state)), + height: self.size.defaultIconSize(iconType: getIconType(for: state))))? + .withRenderingMode(renderingMode) else { return } + imageUpdated = resizedImage + } + self.setImage(imageUpdated, for: state) + self.imageView?.tintColor = style.foregroundColor + self.tintColor = style.foregroundColor + self.imageEdgeInsets = iconType.imageEdgeInsets + self.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + self.contentVerticalAlignment = .fill + self.contentMode = .center + self.imageView?.contentMode = .scaleAspectFit + } + + private func updateSemantic() { + if case .trailing = getIconType(for: self.state) { + self.semanticContentAttribute = .forceRightToLeft + } else { + self.semanticContentAttribute = .forceLeftToRight + } + } + + private func updateImageInsets() { + self.imageEdgeInsets = self.getIconType(for: self.state).imageEdgeInsets + } +} + +extension VitaminButton.IconType { + var imageEdgeInsets: UIEdgeInsets { + switch self { + case .alone, .none: + return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + case .trailing: + return UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) + case .leading: + return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) + } + } +} + +extension UIControl.State: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.rawValue.hashValue) + } }