Commit 52dd9554 by Daniel Dahan Committed by GitHub

Merge pull request #1004 from OrkhanAlikhanov/development

RadioButton/CheckButton and RadioButtonGroup/CheckButtonGroup
parents 6cb79199 68c47fb9
...@@ -174,6 +174,12 @@ ...@@ -174,6 +174,12 @@
96E3C39A1D3A1CC20086A024 /* ErrorTextField.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96E3C39A1D3A1CC20086A024 /* ErrorTextField.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E3C39C1D3A1CC20086A024 /* Offset.swift in Headers */ = {isa = PBXBuildFile; fileRef = 968C99461D377849000074FF /* Offset.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96E3C39C1D3A1CC20086A024 /* Offset.swift in Headers */ = {isa = PBXBuildFile; fileRef = 968C99461D377849000074FF /* Offset.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96F1A5531F24F17A001D8CAF /* TabsController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E09DC71F2287E50000B121 /* TabsController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96F1A5531F24F17A001D8CAF /* TabsController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E09DC71F2287E50000B121 /* TabsController.swift */; settings = {ATTRIBUTES = (Public, ); }; };
9DE84D721FF0252600586C8B /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */; };
9DE84D731FF0252600586C8B /* BaseButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */; };
9DE84D741FF0252600586C8B /* CheckButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */; };
9DF352421FED20C900B2A11B /* BaseIconLayerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */; };
9DF352441FED20ED00B2A11B /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF352431FED20ED00B2A11B /* RadioButton.swift */; };
9DF352461FED210000B2A11B /* CheckButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF352451FED210000B2A11B /* CheckButton.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
...@@ -285,6 +291,12 @@ ...@@ -285,6 +291,12 @@
96E09DC71F2287E50000B121 /* TabsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsController.swift; sourceTree = "<group>"; }; 96E09DC71F2287E50000B121 /* TabsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsController.swift; sourceTree = "<group>"; };
96E3C3931D397AE90086A024 /* Material+UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIView.swift"; sourceTree = "<group>"; }; 96E3C3931D397AE90086A024 /* Material+UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIView.swift"; sourceTree = "<group>"; };
96F1DC871D654FDF0025F925 /* Material+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CALayer.swift"; sourceTree = "<group>"; }; 96F1DC871D654FDF0025F925 /* Material+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CALayer.swift"; sourceTree = "<group>"; };
9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = "<group>"; };
9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseButtonGroup.swift; sourceTree = "<group>"; };
9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckButtonGroup.swift; sourceTree = "<group>"; };
9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseIconLayerButton.swift; sourceTree = "<group>"; };
9DF352431FED20ED00B2A11B /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
9DF352451FED210000B2A11B /* CheckButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckButton.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
...@@ -520,6 +532,7 @@ ...@@ -520,6 +532,7 @@
96264BE41D833C8400576F37 /* Bar */, 96264BE41D833C8400576F37 /* Bar */,
962DDD081D6FBBD0001C307C /* BottomTabBar */, 962DDD081D6FBBD0001C307C /* BottomTabBar */,
96BCB8031CB40F4B00C806FE /* Button */, 96BCB8031CB40F4B00C806FE /* Button */,
9DE84D6E1FF0250E00586C8B /* ButtonGroup */,
96BCB8021CB40F3B00C806FE /* Card */, 96BCB8021CB40F3B00C806FE /* Card */,
961154CA1F32999000A78D74 /* Chip */, 961154CA1F32999000A78D74 /* Chip */,
96BCB8051CB40F9C00C806FE /* Collection */, 96BCB8051CB40F9C00C806FE /* Collection */,
...@@ -609,6 +622,9 @@ ...@@ -609,6 +622,9 @@
96BCB7601CB40DC500C806FE /* FlatButton.swift */, 96BCB7601CB40DC500C806FE /* FlatButton.swift */,
96BCB7931CB40DC500C806FE /* RaisedButton.swift */, 96BCB7931CB40DC500C806FE /* RaisedButton.swift */,
9658F2161CD6FA4700B902C1 /* IconButton.swift */, 9658F2161CD6FA4700B902C1 /* IconButton.swift */,
9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */,
9DF352431FED20ED00B2A11B /* RadioButton.swift */,
9DF352451FED210000B2A11B /* CheckButton.swift */,
); );
name = Button; name = Button;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -732,6 +748,16 @@ ...@@ -732,6 +748,16 @@
name = Animation; name = Animation;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9DE84D6E1FF0250E00586C8B /* ButtonGroup */ = {
isa = PBXGroup;
children = (
9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */,
9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */,
9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */,
);
name = ButtonGroup;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */ /* Begin PBXHeadersBuildPhase section */
...@@ -919,6 +945,7 @@ ...@@ -919,6 +945,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
961E6BE21DDA2AF3004E6C93 /* Screen.swift in Sources */, 961E6BE21DDA2AF3004E6C93 /* Screen.swift in Sources */,
9DF352441FED20ED00B2A11B /* RadioButton.swift in Sources */,
965E81261DD4D7C800D61E4B /* Material+NSMutableAttributedString.swift in Sources */, 965E81261DD4D7C800D61E4B /* Material+NSMutableAttributedString.swift in Sources */,
965E80FF1DD4D5C800D61E4B /* BottomNavigationController.swift in Sources */, 965E80FF1DD4D5C800D61E4B /* BottomNavigationController.swift in Sources */,
965E81031DD4D5C800D61E4B /* CollectionView.swift in Sources */, 965E81031DD4D5C800D61E4B /* CollectionView.swift in Sources */,
...@@ -926,6 +953,7 @@ ...@@ -926,6 +953,7 @@
965E81071DD4D5C800D61E4B /* CollectionViewLayout.swift in Sources */, 965E81071DD4D5C800D61E4B /* CollectionViewLayout.swift in Sources */,
965E81081DD4D5C800D61E4B /* CollectionReusableView.swift in Sources */, 965E81081DD4D5C800D61E4B /* CollectionReusableView.swift in Sources */,
965E81091DD4D5C800D61E4B /* DataSourceItem.swift in Sources */, 965E81091DD4D5C800D61E4B /* DataSourceItem.swift in Sources */,
9DF352461FED210000B2A11B /* CheckButton.swift in Sources */,
965E810A1DD4D5C800D61E4B /* Font.swift in Sources */, 965E810A1DD4D5C800D61E4B /* Font.swift in Sources */,
965E810B1DD4D5C800D61E4B /* RobotoFont.swift in Sources */, 965E810B1DD4D5C800D61E4B /* RobotoFont.swift in Sources */,
965E810C1DD4D5C800D61E4B /* DynamicFontType.swift in Sources */, 965E810C1DD4D5C800D61E4B /* DynamicFontType.swift in Sources */,
...@@ -940,6 +968,7 @@ ...@@ -940,6 +968,7 @@
965E81181DD4D5C800D61E4B /* Snackbar.swift in Sources */, 965E81181DD4D5C800D61E4B /* Snackbar.swift in Sources */,
965E81191DD4D5C800D61E4B /* SnackbarController.swift in Sources */, 965E81191DD4D5C800D61E4B /* SnackbarController.swift in Sources */,
9618006D1F4D384200CD77A1 /* Material+UIViewController.swift in Sources */, 9618006D1F4D384200CD77A1 /* Material+UIViewController.swift in Sources */,
9DE84D741FF0252600586C8B /* CheckButtonGroup.swift in Sources */,
965E811A1DD4D5C800D61E4B /* StatusBarController.swift in Sources */, 965E811A1DD4D5C800D61E4B /* StatusBarController.swift in Sources */,
965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */, 965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */,
965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */, 965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */,
...@@ -951,6 +980,7 @@ ...@@ -951,6 +980,7 @@
965E80E71DD4C55200D61E4B /* Material+UIView.swift in Sources */, 965E80E71DD4C55200D61E4B /* Material+UIView.swift in Sources */,
965E80E81DD4C55200D61E4B /* Material+CALayer.swift in Sources */, 965E80E81DD4C55200D61E4B /* Material+CALayer.swift in Sources */,
965E80E91DD4C55200D61E4B /* Material+String.swift in Sources */, 965E80E91DD4C55200D61E4B /* Material+String.swift in Sources */,
9DE84D731FF0252600586C8B /* BaseButtonGroup.swift in Sources */,
965E80F71DD4D59500D61E4B /* Card.swift in Sources */, 965E80F71DD4D59500D61E4B /* Card.swift in Sources */,
965E80EA1DD4C55200D61E4B /* Material+UIFont.swift in Sources */, 965E80EA1DD4C55200D61E4B /* Material+UIFont.swift in Sources */,
965E80EB1DD4C55200D61E4B /* Material+UIImage.swift in Sources */, 965E80EB1DD4C55200D61E4B /* Material+UIImage.swift in Sources */,
...@@ -958,8 +988,10 @@ ...@@ -958,8 +988,10 @@
965E80ED1DD4C55200D61E4B /* Material+UIWindow.swift in Sources */, 965E80ED1DD4C55200D61E4B /* Material+UIWindow.swift in Sources */,
961527B91F3A509900E8B2AC /* ChipBarController.swift in Sources */, 961527B91F3A509900E8B2AC /* ChipBarController.swift in Sources */,
965E80E41DD4C53300D61E4B /* PulseView.swift in Sources */, 965E80E41DD4C53300D61E4B /* PulseView.swift in Sources */,
9DF352421FED20C900B2A11B /* BaseIconLayerButton.swift in Sources */,
966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */, 966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */,
965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */, 965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */,
9DE84D721FF0252600586C8B /* RadioButtonGroup.swift in Sources */,
965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */, 965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */,
96328B971E05C0BB009A4C90 /* TableView.swift in Sources */, 96328B971E05C0BB009A4C90 /* TableView.swift in Sources */,
965E80F81DD4D59500D61E4B /* ImageCard.swift in Sources */, 965E80F81DD4D59500D61E4B /* ImageCard.swift in Sources */,
......
//
// BaseButtonGroup.swift
// Material
//
// Created by Orkhan Alikhanov on 12/24/17.
// Copyright © 2017 CosmicMind, Inc. All rights reserved.
//
import UIKit
open class BaseButtonGroup<T: Button>: View {
/// Holds reference to buttons within the group.
open var buttons: [T] = [] {
didSet {
oldValue.forEach {
$0.removeFromSuperview()
$0.removeTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
prepareButtons()
grid.views = buttons
grid.axis.rows = buttons.count
}
}
/// Initializes group with the provided buttons.
///
/// - Parameter buttons: Array of buttons.
public convenience init(buttons: [T]) {
self.init(frame: .zero)
defer { self.buttons = buttons } // defer allows didSet to be called
}
open override func prepare() {
super.prepare()
grid.axis.direction = .vertical
grid.axis.columns = 1
}
open override var intrinsicContentSize: CGSize { return sizeThatFits(bounds.size) }
open override func sizeThatFits(_ size: CGSize) -> CGSize {
let size = CGSize(width: size.width == 0 ? .greatestFiniteMagnitude : size.width, height: size.height == 0 ? .greatestFiniteMagnitude : size.height)
let availableW = size.width - grid.contentEdgeInsets.left - grid.contentEdgeInsets.right - grid.layoutEdgeInsets.left - grid.layoutEdgeInsets.right
let maxW = buttons.reduce(0) { max($0, $1.sizeThatFits(.init(width: availableW, height: .greatestFiniteMagnitude)).width) }
let h = buttons.reduce(0) { $0 + $1.sizeThatFits(.init(width: maxW, height: .greatestFiniteMagnitude)).height }
+ grid.contentEdgeInsets.top + grid.contentEdgeInsets.bottom
+ grid.layoutEdgeInsets.top + grid.layoutEdgeInsets.bottom
+ CGFloat(buttons.count - 1) * grid.interimSpace
return CGSize(width: maxW + grid.contentEdgeInsets.left + grid.contentEdgeInsets.right + grid.layoutEdgeInsets.left + grid.layoutEdgeInsets.right, height: min(h, size.height))
}
open override func layoutSubviews() {
super.layoutSubviews()
grid.reload()
}
open func didTap(button: T, at index: Int) { }
@objc
private func didTap(_ sender: Button) {
guard let sender = sender as? T,
let index = buttons.index(of: sender)
else { return }
didTap(button: sender, at: index)
}
}
private extension BaseButtonGroup {
func prepareButtons() {
buttons.forEach {
addSubview($0)
$0.removeTarget(nil, action: nil, for: .allEvents)
$0.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
}
}
//
// CheckButton.swift
// Material
//
// Created by Orkhan Alikhanov on 12/22/18.
// Copyright © 2017 CosmicMind. All rights reserved.
//
import UIKit
open class CheckButton: BaseIconLayerButton {
class override var iconLayer: BaseIconLayer { return CheckBoxLayer() }
/// Color of the checkmark (✓)
open var checkmarkColor: UIColor {
get {
return (iconLayer as! CheckBoxLayer).checkmarkColor
}
set {
(iconLayer as! CheckBoxLayer).checkmarkColor = newValue
}
}
open override func prepare() {
super.prepare()
addTarget(self, action: #selector(didTap), for: .touchUpInside)
}
@objc
private func didTap() {
guard !isAnimating else { return }
setSelected(!isSelected, animated: true)
}
}
internal class CheckBoxLayer: BaseIconLayer {
var checkmarkColor: UIColor = .white {
didSet {
checkMarkLeftLayer.strokeColor = checkmarkColor.cgColor
checkMarkRightLayer.strokeColor = checkmarkColor.cgColor
}
}
let borderLayer = CALayer()
let checkMarkLeftLayer = CAShapeLayer()
let checkMarkRightLayer = CAShapeLayer()
let checkMarkLayer = CALayer()
override var selectedColor: UIColor {
didSet {
guard isSelected, isEnabled else { return }
borderLayer.borderColor = selectedColor.cgColor
borderLayer.backgroundColor = selectedColor.cgColor
}
}
override var normalColor: UIColor {
didSet {
guard !isSelected, isEnabled else { return }
borderLayer.borderColor = normalColor.cgColor
}
}
override var disabledColor: UIColor {
didSet {
guard !isEnabled else { return }
borderLayer.borderColor = disabledColor.cgColor
if isSelected { borderLayer.backgroundColor = disabledColor.cgColor }
}
}
open override func prepare() {
super.prepare()
addSublayer(borderLayer)
addSublayer(checkMarkLayer)
checkMarkLayer.addSublayer(checkMarkLeftLayer)
checkMarkLayer.addSublayer(checkMarkRightLayer)
checkMarkLeftLayer.lineCap = kCALineCapSquare
checkMarkRightLayer.lineCap = kCALineCapSquare
checkMarkLeftLayer.strokeEnd = 0
checkMarkRightLayer.strokeEnd = 0
checkmarkColor = { checkmarkColor }() // calling didSet
}
override func prepareForFirstAnimation() {
borderLayer.borderColor = (isEnabled ? (isSelected ? selectedColor : normalColor) : disabledColor).cgColor
if isSelected {
borderLayer.borderWidth = borderLayerNormalBorderWidth
} else {
borderLayer.backgroundColor = (isEnabled ? normalColor : disabledColor).cgColor
checkMarkLeftLayer.strokeEnd = 1
checkMarkRightLayer.strokeEnd = 1
}
checkMarkLayer.transform = .identity
}
override func firstAnimation() {
borderLayer.transform = borderLayerScaleToShrink
checkMarkLayer.transform = borderLayerScaleToShrink
if isSelected {
borderLayer.animate(#keyPath(CALayer.borderWidth), to: borderLayerFullBorderWidth)
} else {
checkMarkLeftLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 0)
checkMarkRightLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 0)
checkMarkLayer.transform = CATransform3DMakeTranslation(sideLength / 2 - checkMarkStartPoint.x, -(checkMarkStartPoint.y - sideLength / 2), 0)
}
}
override func prepareForSecondAnimation() {
borderLayer.backgroundColor = (isSelected ? (isEnabled ? selectedColor : disabledColor) : .clear).cgColor
if isSelected {
borderLayer.borderWidth = borderLayerNormalBorderWidth
checkMarkLeftLayer.strokeEnd = 0.0001
checkMarkRightLayer.strokeEnd = 0.0001
checkMarkLayer.opacity = 0
checkMarkLayer.animate(#keyPath(CALayer.opacity), to: 1, dur: Constants.totalDuration * 0.1)
} else {
borderLayer.borderWidth = borderLayerCenterDotBorderWidth
}
}
override func secondAnimation() {
borderLayer.transform = .identity
checkMarkLayer.transform = .identity
if isSelected {
checkMarkLeftLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 1)
checkMarkRightLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 1)
} else {
borderLayer.animate(#keyPath(CALayer.borderWidth), to: borderLayerNormalBorderWidth)
}
}
override func layoutSublayers() {
super.layoutSublayers()
guard !isAnimating else { return }
let s = CGSize(width: sideLength, height: sideLength)
borderLayer.frame.size = s
checkMarkLayer.frame.size = s
checkMarkLeftLayer.frame.size = s
checkMarkRightLayer.frame.size = s
checkMarkLeftLayer.path = checkMarkPathLeft.cgPath
checkMarkRightLayer.path = checkMarkPathRigth.cgPath
checkMarkLeftLayer.lineWidth = lineWidth
checkMarkRightLayer.lineWidth = lineWidth
borderLayer.borderWidth = borderLayerNormalBorderWidth
borderLayer.cornerRadius = borderLayerCornerRadius
}
}
private extension CheckBoxLayer {
var borderLayerFullBorderWidth: CGFloat { return sideLength / 2 * 1.1 } //without multipling 1.1 a weird plus sign (+) appears sometimes.
var borderLayerCenterDotBorderWidth: CGFloat { return sideLength / 2 * 0.87 }
var borderLayerNormalBorderWidth: CGFloat { return sideLength * 0.1 }
var borderLayerCornerRadius: CGFloat { return sideLength * 0.1 }
var borderLayerScalePercentageToShrink: CGFloat { return 0.9 }
var borderLayerScaleToShrink: CATransform3D {
return CATransform3DMakeScale(borderLayerScalePercentageToShrink, borderLayerScalePercentageToShrink, 1)
}
var checkMarkStartPoint: CGPoint {
return CGPoint(x: sideLength * 14 / 36, y: sideLength * 25 / 36)
}
var checkMarkRightEndPoint: CGPoint {
return CGPoint(x: sideLength - (sideLength * 6 / 36), y: sideLength * 9 / 36)
}
var checkMarkLeftEndPoint: CGPoint {
return CGPoint(x: sideLength * 6 / 36, y: sideLength * 18 / 36)
}
var checkMarkPathRigth: UIBezierPath {
let path = UIBezierPath()
path.move(to: checkMarkStartPoint)
path.addLine(to: checkMarkRightEndPoint)
return path
}
var checkMarkPathLeft: UIBezierPath {
let path = UIBezierPath()
path.move(to: checkMarkStartPoint)
path.addLine(to: checkMarkLeftEndPoint)
return path
}
var lineWidth: CGFloat { return sideLength * 0.1 }
}
//
// CheckButtonGroup.swift
// Material
//
// Created by Orkhan Alikhanov on 12/24/17.
// Copyright © 2017 CosmicMind, Inc. All rights reserved.
//
/// Lays out provided check buttons within itself.
///
/// Unlike RadioButtonGroup, checking one check button that belongs to a check group *does not* unchecks any previously checked
/// check button within the same group. Intially, all of the check buttons are unchecked.
///
/// The buttons are laid out by `Grid` system, so that changing properites of grid instance
/// (e.g interimSpace) are reflected.
open class CheckButtonGroup: BaseButtonGroup<CheckButton> {
/// Initializes CheckButtonGroup with an array of check buttons each having
/// title equal to corresponding string in the `titles` parameter.
///
/// - Parameter titles: An array of title strings
public convenience init(titles: [String]) {
let buttons = titles.map { CheckButton(title: $0) }
self.init(buttons: buttons)
}
/// Returns all selected check buttons within the group
/// or empty array if none is seleceted.
open var selecetedButtons: [CheckButton] {
return buttons.filter { $0.isSelected }
}
/// Returns indexes of all selected check buttons within the group
/// or empty array if none is seleceted.
open var selectedIndices: [Int] {
return selecetedButtons.map { buttons.index(of: $0)! }
}
open override func didTap(button: CheckButton, at index: Int) {
button.setSelected(!button.isSelected, animated: true)
}
}
//
// RadioButton.swift
// Material
//
// Created by Orkhan Alikhanov on 12/22/18.
// Copyright © 2017 CosmicMind. All rights reserved.
//
import UIKit
open class RadioButton: BaseIconLayerButton {
class override var iconLayer: BaseIconLayer { return RadioBoxLayer() }
open override func prepare() {
super.prepare()
addTarget(self, action: #selector(didTap), for: .touchUpInside)
}
@objc
private func didTap() {
setSelected(true, animated: true)
}
}
internal class RadioBoxLayer: BaseIconLayer {
private let centerDot = CALayer()
private let outerCircle = CALayer()
override var selectedColor: UIColor {
didSet {
guard isSelected, isEnabled else { return }
outerCircle.borderColor = selectedColor.cgColor
centerDot.backgroundColor = selectedColor.cgColor
}
}
override var normalColor: UIColor {
didSet {
guard !isSelected, isEnabled else { return }
outerCircle.borderColor = normalColor.cgColor
}
}
override var disabledColor: UIColor {
didSet {
guard !isEnabled else { return }
outerCircle.borderColor = disabledColor.cgColor
if isSelected { centerDot.backgroundColor = disabledColor.cgColor }
}
}
override func prepare() {
super.prepare()
addSublayer(centerDot)
addSublayer(outerCircle)
}
override func prepareForFirstAnimation() {
outerCircle.borderColor = (isEnabled ? (isSelected ? selectedColor : normalColor) : disabledColor).cgColor
if !isSelected {
centerDot.backgroundColor = (isEnabled ? normalColor : disabledColor).cgColor
}
outerCircle.borderWidth = outerCircleBorderWidth
}
override func firstAnimation() {
outerCircle.transform = outerCircleScaleToShrink
let to = isSelected ? sideLength / 2.0 : outerCircleBorderWidth * percentageOfOuterCircleWidthToStart
outerCircle.animate(#keyPath(CALayer.borderWidth), to: to)
if !isSelected {
centerDot.transform = centerDotScaleForMeeting
}
}
override func prepareForSecondAnimation() {
centerDot.transform = isSelected ? centerDotScaleForMeeting : .identity
centerDot.backgroundColor = (isSelected ? (isEnabled ? selectedColor : disabledColor) : .clear).cgColor
outerCircle.borderWidth = isSelected ? outerCircleBorderWidth * percentageOfOuterCircleWidthToStart : outerCircleFullBorderWidth
}
override func secondAnimation() {
outerCircle.transform = .identity
outerCircle.animate(#keyPath(CALayer.borderWidth), to: outerCircleBorderWidth)
if isSelected {
centerDot.transform = .identity
}
}
override func layoutSublayers() {
super.layoutSublayers()
guard !isAnimating else { return }
centerDot.frame = CGRect(x: centerDotDiameter / 2.0, y: centerDotDiameter / 2.0, width: centerDotDiameter, height: centerDotDiameter)
outerCircle.frame.size = CGSize(width: sideLength, height: sideLength)
centerDot.cornerRadius = centerDot.bounds.width / 2
outerCircle.cornerRadius = sideLength / 2
outerCircle.borderWidth = outerCircleBorderWidth
}
}
private extension RadioBoxLayer {
var percentageOfOuterCircleSizeToShrinkTo: CGFloat { return 0.9 }
var percentageOfOuterCircleWidthToStart: CGFloat { return 1 }
var outerCircleScaleToShrink: CATransform3D {
let s = percentageOfOuterCircleSizeToShrinkTo
return CATransform3DMakeScale(s, s, 1)
}
var centerDotScaleForMeeting: CATransform3D {
let s = ((sideLength - 2 * percentageOfOuterCircleWidthToStart * outerCircleBorderWidth) * percentageOfOuterCircleSizeToShrinkTo) / centerDotDiameter
return CATransform3DMakeScale(s, s, 1)
}
var outerCircleFullBorderWidth: CGFloat {
return (self.sideLength / 2.0) * 1.1 //without multipling 1.1 a weird plus sign (+) appears sometimes.
}
var centerDotDiameter: CGFloat { return sideLength / 2.0 }
var outerCircleBorderWidth: CGFloat { return sideLength * 0.11 }
}
//
// RadioButtonGroup.swift
// Material
//
// Created by Orkhan Alikhanov on 12/24/17.
// Copyright © 2017 CosmicMind, Inc. All rights reserved.
//
/// Lays out provided radio buttons within itself.
///
/// Checking one radio button that belongs to a radio group unchecks any previously checked
/// radio button within the same group. Intially, all of the radio buttons are unchecked.
///
/// The buttons are laid out by `Grid` system, so that changing properites of grid instance
/// (e.g interimSpace) are reflected.
open class RadioButtonGroup: BaseButtonGroup<RadioButton> {
/// Initializes RadioButtonGroup with an array of radio buttons each having
/// title equal to corresponding string in the `titles` parameter.
///
/// - Parameter titles: An array of title strings.
public convenience init(titles: [String]) {
let buttons = titles.map { RadioButton(title: $0) }
self.init(buttons: buttons)
}
/// Returns selected radio button within the group.
/// If none is selected (e.g in initial state), nil is returned.
open var selectedButton: RadioButton? {
return buttons.first { $0.isSelected }
}
/// Returns index of selected radio button within the group.
/// If none is selected (e.g in initial state), -1 is returned.
open var selectedIndex: Int {
guard let b = selectedButton else { return -1 }
return buttons.index(of: b)!
}
open override func didTap(button: RadioButton, at index: Int) {
let isAnimating = buttons.reduce(false) { $0 || $1.isAnimating }
guard !isAnimating else { return }
buttons.forEach { $0.setSelected($0 == button, animated: true) }
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment