Skip to content

Commit

Permalink
Make image sizes configurable (#213)
Browse files Browse the repository at this point in the history
* Make image size of action item configurable

* Add missing reference images

* set action item image size in configuration example

* fix linter issues

* Add missing reference image

* Make size of button image configurable

* configure button image size in configuration example

* fix dynamic constraints creation of action item
  • Loading branch information
jjochen committed Sep 9, 2019
1 parent c1bbb6f commit 962706b
Show file tree
Hide file tree
Showing 18 changed files with 141 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ opt_in_rules:

line_length: 140
file_length: 1000
function_body_length: 60
function_body_length: 40
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ internal class ConfigurationExampleViewController: UIViewController {
actionButton.buttonImage = #imageLiteral(resourceName: "Dots")
actionButton.buttonColor = .red
actionButton.buttonImageColor = .white
actionButton.buttonImageSize = CGSize(width: 30, height: 30)

actionButton.buttonAnimationConfiguration = .transition(toImage: #imageLiteral(resourceName: "X"))
actionButton.itemAnimationConfiguration = .slideIn(withInterItemSpacing: 14)
Expand All @@ -76,6 +77,10 @@ internal class ConfigurationExampleViewController: UIViewController {
item.layer.shadowRadius = CGFloat(2)
}

addItems()
}

fileprivate func addItems() {
actionButton.addItem(title: "Balloon", image: #imageLiteral(resourceName: "Baloon")) { item in
Helper.showAlert(for: item)
}
Expand All @@ -98,6 +103,7 @@ internal class ConfigurationExampleViewController: UIViewController {
heartItem.imageView.image = #imageLiteral(resourceName: "Favourite")
heartItem.buttonColor = .clear
heartItem.buttonImageColor = .red
heartItem.imageSize = CGSize(width: 30, height: 30)
heartItem.action = { item in
Helper.showAlert(for: item)
}
Expand Down
10 changes: 10 additions & 0 deletions Example/Tests/JJActionItemSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ class JJActionItemSpec: QuickSpec {
actionItem.titleLabel.text = ""
expect(actionItem) == snapshot()
}

it("looks correct with smaller image size") {
actionItem.imageSize = CGSize(width: 10, height: 10)
expect(actionItem) == snapshot()
}

it("looks correct with bigger image size") {
actionItem.imageSize = CGSize(width: 30, height: 30)
expect(actionItem) == snapshot()
}
}

describe("JJActionItem loaded from xib") {
Expand Down
10 changes: 10 additions & 0 deletions Example/Tests/JJFloatingActionButtonSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,16 @@ class JJFloatingActionButtonSpec: QuickSpec {
expect(superview) == snapshot()
}

it("looks correct with smaller image size") {
actionButton.buttonImageSize = CGSize(width: 10, height: 10)
expect(superview) == snapshot()
}

it("looks correct with bigger image size") {
actionButton.buttonImageSize = CGSize(width: 40, height: 40)
expect(superview) == snapshot()
}

context("when multiple items are added") {
var action = "not done"
beforeEach {
Expand Down
Binary file added ...ages/JJActionItemSpec/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...ages/JJActionItemSpec/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...enceImages/JJActionItemSpec/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...ges/JJActionItemSpec/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ...ges/JJActionItemSpec/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 59 additions & 30 deletions Sources/JJActionItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ import UIKit
}

/// The position of the title label. Default is `-1`.
/// When titleSpacing is negative default spacing is used:
/// When `titleSpacing` is negative default spacing is used:
/// Default horizontal spacing is `12`.
/// Default vertical spaicng is `4`.
///
Expand All @@ -156,6 +156,17 @@ import UIKit
}
}

/// The size of the image view. Default is `(0, 0)`.
/// When imageSize is `.zero` the image is shrunken until it fits
/// compleately into the circle view. If it already does the actual
/// size of the image is used.
///
@objc public dynamic var imageSize: CGSize = .zero {
didSet {
setNeedsUpdateConstraints()
}
}

internal override init(frame: CGRect) {
super.init(frame: frame)
setup()
Expand Down Expand Up @@ -247,11 +258,8 @@ fileprivate extension JJActionItem {
var constraints: [NSLayoutConstraint] = []
var constraint: NSLayoutConstraint

let imageSizeMuliplier = CGFloat(1 / sqrt(2))
constraints.append(imageView.centerXAnchor.constraint(equalTo: circleView.centerXAnchor))
constraints.append(imageView.centerYAnchor.constraint(equalTo: circleView.centerYAnchor))
constraints.append(imageView.widthAnchor.constraint(lessThanOrEqualTo: circleView.widthAnchor, multiplier: imageSizeMuliplier))
constraints.append(imageView.heightAnchor.constraint(lessThanOrEqualTo: circleView.heightAnchor, multiplier: imageSizeMuliplier))

constraints.append(circleView.widthAnchor.constraint(equalTo: circleView.heightAnchor))

Expand Down Expand Up @@ -287,44 +295,65 @@ fileprivate extension JJActionItem {
}

func createDynamicConstraints() {
dynamicConstraints.append(contentsOf: titleSpacingConstraints)
dynamicConstraints.append(contentsOf: imageSizeConstraints)
}

var titleSpacingConstraints: [NSLayoutConstraint] {
var constraints: [NSLayoutConstraint] = []
let horizontalSpacing = titleSpacing(forAxis: .horizontal)
let verticalSpacing = titleSpacing(forAxis: .vertical)

switch titlePosition {
case .leading:
dynamicConstraints.append(circleView.trailingAnchor.constraint(equalTo: trailingAnchor))
dynamicConstraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
dynamicConstraints.append(titleLabel.trailingAnchor.constraint(equalTo: circleView.leadingAnchor, constant: -horizontalSpacing))
dynamicConstraints.append(titleLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor))
constraints.append(circleView.trailingAnchor.constraint(equalTo: trailingAnchor))
constraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
constraints.append(titleLabel.trailingAnchor.constraint(equalTo: circleView.leadingAnchor, constant: -horizontalSpacing))
constraints.append(titleLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor))
case .trailing:
dynamicConstraints.append(circleView.leadingAnchor.constraint(equalTo: leadingAnchor))
dynamicConstraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
dynamicConstraints.append(titleLabel.leadingAnchor.constraint(equalTo: circleView.trailingAnchor, constant: horizontalSpacing))
dynamicConstraints.append(titleLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor))
constraints.append(circleView.leadingAnchor.constraint(equalTo: leadingAnchor))
constraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
constraints.append(titleLabel.leadingAnchor.constraint(equalTo: circleView.trailingAnchor, constant: horizontalSpacing))
constraints.append(titleLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor))
case .left:
dynamicConstraints.append(circleView.rightAnchor.constraint(equalTo: rightAnchor))
dynamicConstraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
dynamicConstraints.append(titleLabel.rightAnchor.constraint(equalTo: circleView.leftAnchor, constant: -horizontalSpacing))
dynamicConstraints.append(titleLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor))
constraints.append(circleView.rightAnchor.constraint(equalTo: rightAnchor))
constraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
constraints.append(titleLabel.rightAnchor.constraint(equalTo: circleView.leftAnchor, constant: -horizontalSpacing))
constraints.append(titleLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor))
case .right:
dynamicConstraints.append(circleView.leftAnchor.constraint(equalTo: leftAnchor))
dynamicConstraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
dynamicConstraints.append(titleLabel.leftAnchor.constraint(equalTo: circleView.rightAnchor, constant: horizontalSpacing))
dynamicConstraints.append(titleLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor))
constraints.append(circleView.leftAnchor.constraint(equalTo: leftAnchor))
constraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
constraints.append(titleLabel.leftAnchor.constraint(equalTo: circleView.rightAnchor, constant: horizontalSpacing))
constraints.append(titleLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor))
case .top:
dynamicConstraints.append(circleView.bottomAnchor.constraint(equalTo: bottomAnchor))
dynamicConstraints.append(circleView.centerXAnchor.constraint(equalTo: centerXAnchor))
dynamicConstraints.append(titleLabel.bottomAnchor.constraint(equalTo: circleView.topAnchor, constant: -verticalSpacing))
dynamicConstraints.append(titleLabel.centerXAnchor.constraint(equalTo: circleView.centerXAnchor))
constraints.append(circleView.bottomAnchor.constraint(equalTo: bottomAnchor))
constraints.append(circleView.centerXAnchor.constraint(equalTo: centerXAnchor))
constraints.append(titleLabel.bottomAnchor.constraint(equalTo: circleView.topAnchor, constant: -verticalSpacing))
constraints.append(titleLabel.centerXAnchor.constraint(equalTo: circleView.centerXAnchor))
case .bottom:
dynamicConstraints.append(circleView.topAnchor.constraint(equalTo: topAnchor))
dynamicConstraints.append(circleView.centerXAnchor.constraint(equalTo: centerXAnchor))
dynamicConstraints.append(titleLabel.topAnchor.constraint(equalTo: circleView.bottomAnchor, constant: verticalSpacing))
dynamicConstraints.append(titleLabel.centerXAnchor.constraint(equalTo: circleView.centerXAnchor))
constraints.append(circleView.topAnchor.constraint(equalTo: topAnchor))
constraints.append(circleView.centerXAnchor.constraint(equalTo: centerXAnchor))
constraints.append(titleLabel.topAnchor.constraint(equalTo: circleView.bottomAnchor, constant: verticalSpacing))
constraints.append(titleLabel.centerXAnchor.constraint(equalTo: circleView.centerXAnchor))
case .hidden:
dynamicConstraints.append(circleView.centerXAnchor.constraint(equalTo: centerXAnchor))
dynamicConstraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
constraints.append(circleView.centerXAnchor.constraint(equalTo: centerXAnchor))
constraints.append(circleView.centerYAnchor.constraint(equalTo: centerYAnchor))
}

return constraints
}

var imageSizeConstraints: [NSLayoutConstraint] {
var constraints: [NSLayoutConstraint] = []
if imageSize == .zero {
let muliplier = CGFloat(1 / sqrt(2))
constraints.append(imageView.widthAnchor.constraint(lessThanOrEqualTo: circleView.widthAnchor, multiplier: muliplier))
constraints.append(imageView.heightAnchor.constraint(lessThanOrEqualTo: circleView.heightAnchor, multiplier: muliplier))
} else {
constraints.append(imageView.widthAnchor.constraint(equalToConstant: imageSize.width))
constraints.append(imageView.heightAnchor.constraint(equalToConstant: imageSize.height))
}
return constraints
}

func titleSpacing(forAxis axis: NSLayoutConstraint.Axis) -> CGFloat {
Expand Down
60 changes: 55 additions & 5 deletions Sources/JJFloatingActionButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ import UIKit
}
}

/// The size of the image view.
/// Default is `CGSize.zero`.
/// If set to `.zero` the actual size of the image is used.
///
/// - SeeAlso: `imageView`
///
@objc public dynamic var buttonImageSize: CGSize = .zero {
didSet {
setNeedsUpdateConstraints()
}
}

/// The tint color of the image view.
/// Default is `UIColor.white`.
///
Expand Down Expand Up @@ -269,6 +281,8 @@ import UIKit

fileprivate var defaultItemConfiguration: ((JJActionItem) -> Void)?
fileprivate var itemsWithSetup: Set<JJActionItem> = []

fileprivate var dynamicConstraints: [NSLayoutConstraint] = []
}

// MARK: - Public Methods
Expand Down Expand Up @@ -369,6 +383,13 @@ extension JJFloatingActionButton {
open override var intrinsicContentSize: CGSize {
return CGSize(width: buttonDiameter, height: buttonDiameter)
}

/// Updates constraints for the view.
///
open override func updateConstraints() {
updateDynamicConstraints()
super.updateConstraints()
}
}

// MARK: - Setup
Expand All @@ -389,6 +410,15 @@ fileprivate extension JJFloatingActionButton {
circleView.addSubview(imageView)

circleView.translatesAutoresizingMaskIntoConstraints = false
imageView.translatesAutoresizingMaskIntoConstraints = false

createStaticConstraints()
createDynamicConstraints()

configureButtonImage()
}

func createStaticConstraints() {
circleView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
circleView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
circleView.widthAnchor.constraint(equalTo: circleView.heightAnchor).isActive = true
Expand All @@ -401,19 +431,14 @@ fileprivate extension JJFloatingActionButton {
heightConstraint.priority = .defaultHigh
heightConstraint.isActive = true

let imageSizeMuliplier = CGFloat(1 / sqrt(2))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.centerXAnchor.constraint(equalTo: circleView.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: circleView.centerYAnchor).isActive = true
imageView.widthAnchor.constraint(lessThanOrEqualTo: circleView.widthAnchor, multiplier: imageSizeMuliplier).isActive = true
imageView.heightAnchor.constraint(lessThanOrEqualTo: circleView.heightAnchor, multiplier: imageSizeMuliplier).isActive = true

imageView.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
imageView.setContentCompressionResistancePriority(.fittingSizeLevel, for: .vertical)
circleView.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal)
circleView.setContentHuggingPriority(.fittingSizeLevel, for: .vertical)

configureButtonImage()
}

func configureButtonImage() {
Expand All @@ -437,6 +462,31 @@ fileprivate extension JJFloatingActionButton {

defaultItemConfiguration?(item)
}

func updateDynamicConstraints() {
NSLayoutConstraint.deactivate(dynamicConstraints)
dynamicConstraints.removeAll()
createDynamicConstraints()
NSLayoutConstraint.activate(dynamicConstraints)
setNeedsLayout()
}

func createDynamicConstraints() {
dynamicConstraints.append(contentsOf: imageSizeConstraints)
}

var imageSizeConstraints: [NSLayoutConstraint] {
var constraints: [NSLayoutConstraint] = []
if buttonImageSize == .zero {
let muliplier = CGFloat(1 / sqrt(2))
constraints.append(imageView.widthAnchor.constraint(lessThanOrEqualTo: circleView.widthAnchor, multiplier: muliplier))
constraints.append(imageView.heightAnchor.constraint(lessThanOrEqualTo: circleView.heightAnchor, multiplier: muliplier))
} else {
constraints.append(imageView.widthAnchor.constraint(equalToConstant: buttonImageSize.width))
constraints.append(imageView.heightAnchor.constraint(equalToConstant: buttonImageSize.height))
}
return constraints
}
}

// MARK: - Helper
Expand Down

0 comments on commit 962706b

Please sign in to comment.