Skip to content
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

Add content view mechanism as alternative to text label #44

Merged
merged 7 commits into from
Oct 6, 2023
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
66 changes: 50 additions & 16 deletions TORoundedButton/TORoundedButton.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,42 @@ NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(RoundedButton)
IB_DESIGNABLE @interface TORoundedButton : UIControl

/// The text that is displayed in center of the button (Default is "Button").
@property (nonatomic, copy) IBInspectable NSString *text;

/// The attributed string used in the label of this button. See `UILabel.attributedText` documentation for full details (Default is nil).
@property (nonatomic, copy, nullable) NSAttributedString *attributedText;

/// The radius of the corners of this button (Default is 12.0f).
@property (nonatomic, assign) IBInspectable CGFloat cornerRadius;

/// The hosting container that manages all of the foreground views in this button.
/// You can either add your custom views to this view by default, or you can set
/// this property to your own custom UIView subclass in order to more efficiently manage sizing and layout.
@property (nonatomic, strong, null_resettable) UIView *contentView;

/// The amount of inset padding between the content view and the edges of the button.
/// (Default value is 15 points inset from each edge).
@property (nonatomic, assign) UIEdgeInsets contentInset;

/// The text that is displayed in center of the button (Default is nil).
@property (nonatomic, copy, nullable) IBInspectable NSString *text;

/// The attributed string used in the label of this button.
/// See `UILabel.attributedText` documentation for full details (Default is nil).
@property (nonatomic, copy, nullable) NSAttributedString *attributedText;

/// The color of the text in this button (Default is white).
@property (nonatomic, strong) IBInspectable UIColor *textColor;

/// When tapped, the level of transparency that the text label animates to. (Defaults to off with 1.0f).
@property (nonatomic, assign) IBInspectable CGFloat tappedTextAlpha;

/// The font of the text in the button (Default is size UIFontTextStyleBody with bold).
/// The font of the text in the button
/// (Default is size UIFontTextStyleBody with bold).
@property (nonatomic, strong) UIFont *textFont;

/// Because IB cannot handle fonts, this can alternatively be used to set the font size. (Default is off with 0.0).
/// Because IB cannot handle fonts, this can alternatively be used to set the font size.
/// (Default is off with 0.0).
@property (nonatomic, assign) IBInspectable CGFloat textPointSize;

/// Taking the default button background color apply a brightness offset for the tapped color (Default is -0.1f. Set 0.0 for off).
/// When tapped, the level of transparency that the text label animates to.
/// (Defaults to off with 1.0f).
@property (nonatomic, assign) IBInspectable CGFloat tappedTextAlpha;

/// Taking the default button background color apply a brightness offset for the tapped color
/// (Default is -0.1f. Set 0.0 for off).
@property (nonatomic, assign) IBInspectable CGFloat tappedTintColorBrightnessOffset;

/// If desired, explicity set the background color of the button when tapped (Default is nil).
Expand All @@ -60,15 +74,35 @@ IB_DESIGNABLE @interface TORoundedButton : UIControl
/// The duration of the tapping cross-fade animation (Default is 0.4f).
@property (nonatomic, assign) CGFloat tapAnimationDuration;

/// Given the current size of the text label, the smallest horizontal width in which this button can scale.
@property (nonatomic, readonly) CGFloat minimumWidth;

/// A callback handler triggered each time the button is tapped.
@property (nonatomic, copy) void (^tappedHandler)(void);

/// Create a new instance of a button with the provided text shown in the center. The size will be 288 points wide, and 50 tall.
/// Create a new instance of a button that can be further configured with either text or custom subviews.
/// The size will be 288 points wide, and 50 tall by default.
- (instancetype)init;

/// Create a new instance of a button that can be further configured with either text or custom subviews.
- (instancetype)initWithFrame:(CGRect)frame;

/// Create a new instance of a button with the provided text shown in the center.
/// The size will be 288 points wide, and 50 tall.
- (instancetype)initWithText:(NSString *)text;

/// Create a new instance of a button with the provided view set as the hosting content view.
- (instancetype)initWithContentView:(__kindof UIView *)contentView;

/// Resizes the button to fit the bounding size of all of the subviews in `contentView`, plus content insetting.
/// If subclassing this class, override this method for custom size control (Dont't forget to include content insetting).
/// If the content view only contains one subview (like the title label), or a custom content view is supplied, this will also be forwarded to it.
/// If the content vieww contains multiple subviews, their bounding size will be calculated and then applied to this button.
- (void)sizeToFit;

/// Calculates and returns the appropriate minimum size this button needs to be to fit into the provided size.
/// If subclassing this class, override this method for custom size control (Dont't forget to include content insetting).
/// If the content view only contains one subview (like the title label), or a custom content view is supplied, this will also be forwarded to it.
/// If the content vieww contains multiple subviews, their bounding size will be calculated and then applied to this button.
- (CGSize)sizeThatFits:(CGSize)size;

@end

NS_ASSUME_NONNULL_END
Expand Down
137 changes: 104 additions & 33 deletions TORoundedButton/TORoundedButton.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ @implementation TORoundedButton {
or not because the state can change before blocks complete. */
BOOL _isTapped;

/** A container view that holds all of the content view and performs the clipping. */
/** A hosting container holding all of the view content that tap animations are applied to. */
UIView *_containerView;

/** The title label displaying the text in the center of the button. */
/** If `text` is set, the internally managed title label to show it. */
UILabel *_titleLabel;

/** A background view that displays the rounded box behind the button text. */
Expand All @@ -53,18 +53,14 @@ @implementation TORoundedButton {

#pragma mark - View Creation -

- (instancetype)initWithText:(NSString *)text {
if (self = [super initWithFrame:(CGRect){0,0, 288.0f, 50.0f}]) {
[self _roundedButtonCommonInit];
_titleLabel.text = text;
[_titleLabel sizeToFit];
}

- (instancetype)init {
if (self = [self initWithFrame:(CGRect){0,0, 288.0f, 50.0f}]) { }
return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_contentView = [UIView new];
[self _roundedButtonCommonInit];
}

Expand All @@ -73,7 +69,27 @@ - (instancetype)initWithFrame:(CGRect)frame {

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
_contentView = [UIView new];
[self _roundedButtonCommonInit];
}

return self;
}

- (instancetype)initWithContentView:(__kindof UIView *)contentView {
if (self = [super initWithFrame:contentView.bounds]) {
_contentView = contentView;
[self _roundedButtonCommonInit];
}
return self;
}

- (instancetype)initWithText:(NSString *)text {
if (self = [super initWithFrame:(CGRect){0,0, 288.0f, 50.0f}]) {
[self _roundedButtonCommonInit];
[self _makeTitleLabelIfNeeded];
_titleLabel.text = text;
[_titleLabel sizeToFit];
}

return self;
Expand All @@ -86,11 +102,12 @@ - (void)_roundedButtonCommonInit TOROUNDEDBUTTON_OBJC_DIRECT {
_tapAnimationDuration = (_tapAnimationDuration > FLT_EPSILON) ?: 0.4f;
_tappedButtonScale = (_tappedButtonScale > FLT_EPSILON) ?: 0.97f;
_tappedTintColorBrightnessOffset = !TO_ROUNDED_BUTTON_FLOAT_IS_ZERO(_tappedTintColorBrightnessOffset) ?: -0.15f;
_contentInset = (UIEdgeInsets){15.0, 15.0, 15.0, 15.0};

// Set the tapped tint color if we've set to dynamically calculate it
[self _updateTappedTintColorForTintColor];

// Create the container view that manages the image view and text
// Create the container view that holds all of the views for animations.
_containerView = [[UIView alloc] initWithFrame:self.bounds];
_containerView.backgroundColor = [UIColor clearColor];
_containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
Expand All @@ -108,14 +125,24 @@ - (void)_roundedButtonCommonInit TOROUNDEDBUTTON_OBJC_DIRECT {
#endif
[_containerView addSubview:_backgroundView];

// Create the title label that will display the button text
UIFont *buttonFont = [UIFont systemFontOfSize:17.0f weight:UIFontWeightBold];
if (@available(iOS 11.0, *)) {
// Apply resizable button metrics to font
UIFontMetrics *metrics = [[UIFontMetrics alloc] initForTextStyle:UIFontTextStyleBody];
buttonFont = [metrics scaledFontForFont:buttonFont];
}

// The foreground content view
[_containerView addSubview:_contentView];

// Create action events for all possible interactions with this control
[self addTarget:self action:@selector(_didTouchDownInside) forControlEvents:UIControlEventTouchDown|UIControlEventTouchDownRepeat];
[self addTarget:self action:@selector(_didTouchUpInside) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:@selector(_didDragOutside) forControlEvents:UIControlEventTouchDragExit|UIControlEventTouchCancel];
[self addTarget:self action:@selector(_didDragInside) forControlEvents:UIControlEventTouchDragEnter];
}

- (void)_makeTitleLabelIfNeeded TOROUNDEDBUTTON_OBJC_DIRECT {
if (_titleLabel) { return; }

// Make the font bold, and opt it into Dynamic Type sizing
UIFontMetrics *const metrics = [[UIFontMetrics alloc] initForTextStyle:UIFontTextStyleBody];
UIFont *const buttonFont = [metrics scaledFontForFont:[UIFont systemFontOfSize:17.0f weight:UIFontWeightBold]];

// Configure the title label
_titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_titleLabel.textAlignment = NSTextAlignmentCenter;
_titleLabel.textColor = [UIColor whiteColor];
Expand All @@ -124,24 +151,60 @@ - (void)_roundedButtonCommonInit TOROUNDEDBUTTON_OBJC_DIRECT {
_titleLabel.backgroundColor = [self _labelBackgroundColor];
_titleLabel.text = @"Button";
_titleLabel.numberOfLines = 0;
[_containerView addSubview:_titleLabel];

// Create action events for all possible interactions with this control
[self addTarget:self action:@selector(_didTouchDownInside) forControlEvents:UIControlEventTouchDown|UIControlEventTouchDownRepeat];
[self addTarget:self action:@selector(_didTouchUpInside) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:@selector(_didDragOutside) forControlEvents:UIControlEventTouchDragExit|UIControlEventTouchCancel];
[self addTarget:self action:@selector(_didDragInside) forControlEvents:UIControlEventTouchDragEnter];
[_contentView addSubview:_titleLabel];
}

#pragma mark - View Displaying -
#pragma mark - View Layout -

- (void)layoutSubviews {
[super layoutSubviews];

const CGSize boundsSize = self.bounds.size;
_contentView.frame = (CGRect){
.origin.x = _contentInset.left,
.origin.y = _contentInset.top,
.size.width = boundsSize.width - (_contentInset.left + _contentInset.right),
.size.height = boundsSize.height - (_contentInset.top + _contentInset.bottom),
};

// Configure the button text
[_titleLabel sizeToFit];
_titleLabel.center = _containerView.center;
_titleLabel.frame = CGRectIntegral(_titleLabel.frame);
if (_titleLabel) {
[_titleLabel sizeToFit];
_titleLabel.center = (CGPoint){
.x = CGRectGetMidX(_contentView.bounds),
.y = CGRectGetMidY(_contentView.bounds)
};
_titleLabel.frame = CGRectIntegral(_titleLabel.frame);
}
}

- (void)sizeToFit { [super sizeToFit]; }

- (CGSize)sizeThatFits:(CGSize)size {
const CGFloat horizontalPadding = (_contentInset.left + _contentInset.right);
const CGFloat verticalPadding = (_contentInset.top + _contentInset.bottom);
const CGSize contentSize = CGSizeMake(size.width - horizontalPadding, size.height - verticalPadding);
CGSize newSize = CGSizeZero;

// Check to see if the content view was overridden with custom class that implements its own sizing method.
const BOOL isMethodOverridden = [_contentView methodForSelector:@selector(sizeThatFits:)] !=
[UIView instanceMethodForSelector:@selector(sizeThatFits:)];
if (isMethodOverridden) {
newSize = [_contentView sizeThatFits:size];
} else if (_contentView.subviews.count == 1) {
// When there is 1 view, we can reliably scale the whole view around it.
newSize = [_contentView.subviews.firstObject sizeThatFits:contentSize];
} else if (_contentView.subviews.count > 1) {
// For multiple subviews, work out the bounds of all of the views and scale the button to fit
for (UIView *view in _contentView.subviews) {
newSize.width = MAX(CGRectGetMaxX(view.frame), newSize.width);
newSize.height = MAX(CGRectGetMaxY(view.frame), newSize.height);
}
}

newSize.width += horizontalPadding;
newSize.height += verticalPadding;
return newSize;
}

- (void)tintColorDidChange {
Expand Down Expand Up @@ -323,7 +386,18 @@ - (void)_setButtonScaledTappedAnimated:(BOOL)animated TOROUNDEDBUTTON_OBJC_DIREC

#pragma mark - Public Accessors -

- (void)setContentView:(UIView *)contentView {
if (_contentView == contentView) { return; }

_titleLabel = nil;
[_contentView removeFromSuperview];
_contentView = contentView ?: [UIView new];
[self addSubview:_contentView];
[self setNeedsLayout];
}

- (void)setAttributedText:(NSAttributedString *)attributedText {
[self _makeTitleLabelIfNeeded];
_titleLabel.attributedText = attributedText;
[_titleLabel sizeToFit];
[self setNeedsLayout];
Expand All @@ -332,6 +406,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText {
- (NSAttributedString *)attributedText { return _titleLabel.attributedText; }

- (void)setText:(NSString *)text {
[self _makeTitleLabelIfNeeded];
_titleLabel.text = text;
[_titleLabel sizeToFit];
[self setNeedsLayout];
Expand Down Expand Up @@ -397,10 +472,6 @@ - (void)setEnabled:(BOOL)enabled {
_containerView.alpha = enabled ? 1 : 0.4;
}

- (CGFloat)minimumWidth {
return _titleLabel.frame.size.width;
}

#pragma mark - Graphics Handling -

- (UIColor *)_brightnessAdjustedColorWithColor:(UIColor *)color amount:(CGFloat)amount TOROUNDEDBUTTON_OBJC_DIRECT {
Expand Down
17 changes: 6 additions & 11 deletions TORoundedButtonExample/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="GV4-PL-MlK">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="GV4-PL-MlK">
<device id="retina5_5" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
Expand All @@ -17,19 +17,16 @@
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3Rx-9D-Udo" userLabel="RoundedButton" customClass="TORoundedButton">
<rect key="frame" x="67" y="343" width="280" height="50"/>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="3Rx-9D-Udo" userLabel="RoundedButton" customClass="TORoundedButton">
<rect key="frame" x="67" y="353" width="280" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="280" id="OXU-dJ-PiL"/>
<constraint firstAttribute="height" constant="50" id="s1F-Eq-SaU"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="text" value="Continue"/>
</userDefinedRuntimeAttributes>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Tapped!" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YuK-c8-gCA">
<rect key="frame" x="159.66666666666666" y="166.66666666666666" width="94.666666666666657" height="35"/>
<rect key="frame" x="159.66666666666666" y="171.66666666666666" width="94.666666666666657" height="35"/>
<constraints>
<constraint firstAttribute="height" constant="35" id="A2b-ZQ-Mpt"/>
</constraints>
Expand All @@ -41,9 +38,7 @@
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="YuK-c8-gCA" firstAttribute="centerY" secondItem="yn0-Wx-glq" secondAttribute="centerY" multiplier="0.5" id="1aK-Gn-u8l"/>
<constraint firstItem="3Rx-9D-Udo" firstAttribute="centerX" secondItem="yn0-Wx-glq" secondAttribute="centerX" id="4Pd-sN-oZH"/>
<constraint firstItem="YuK-c8-gCA" firstAttribute="centerX" secondItem="yn0-Wx-glq" secondAttribute="centerX" id="KUL-o1-7MR"/>
<constraint firstItem="3Rx-9D-Udo" firstAttribute="centerY" secondItem="yn0-Wx-glq" secondAttribute="centerY" id="rFU-Gn-CY5"/>
</constraints>
</view>
<connections>
Expand Down
16 changes: 12 additions & 4 deletions TORoundedButtonExample/ViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,24 @@ - (void)viewDidLoad {
// Hide the tapped label
self.tappedLabel.alpha = 0.0f;

__weak typeof(self) weakSelf = self;
self.button.tappedHandler = ^{
[weakSelf playFadeAnimationOnView:weakSelf.tappedLabel];
};

// Uncomment this line for an attributed string example
// self.button.attributedText = [[self class] makeExampleAttributedString];

// Uncomment to apply an alpha value to the button
// self.button.tintColor = [self.view.tintColor colorWithAlphaComponent:0.4];

__weak typeof(self) weakSelf = self;
self.button.tappedHandler = ^{
[weakSelf playFadeAnimationOnView:weakSelf.tappedLabel];
};
// Uncomment to have the button shrink to wrap the text
// [self.button sizeToFit];
}

- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.button.center = self.view.center;
}

- (void)playFadeAnimationOnView:(UIView *)view
Expand Down
Loading