├── .swift-version ├── Contents ├── down.gif ├── example1.gif ├── example2.gif ├── example3.gif ├── example4.gif ├── example5.gif ├── example6.gif ├── left.gif ├── main.gif ├── right.gif └── up.gif ├── ExpandableButton.podspec ├── ExpandableButton ├── ActionButton.swift ├── ArrowButton.swift ├── ExpandableButtonItem.swift └── ExpandableButtonView.swift ├── ExpandableButtonExample.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── ExpandableButtonExample ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── delete.imageset │ │ ├── Contents.json │ │ └── Image.png │ ├── edit.imageset │ │ ├── Contents.json │ │ └── Image.png │ ├── like.imageset │ │ ├── Contents.json │ │ └── Image.png │ ├── photo.imageset │ │ ├── 1496710261194222281.jpg │ │ └── Contents.json │ └── share.imageset │ │ ├── Contents.json │ │ └── Image.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── ExpandableButtonExampleTests ├── ExpandableButtonExampleTests.swift └── Info.plist ├── ExpandableButtonExampleUITests ├── ExpandableButtonExampleUITests.swift └── Info.plist ├── LICENSE └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /Contents/down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/down.gif -------------------------------------------------------------------------------- /Contents/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/example1.gif -------------------------------------------------------------------------------- /Contents/example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/example2.gif -------------------------------------------------------------------------------- /Contents/example3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/example3.gif -------------------------------------------------------------------------------- /Contents/example4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/example4.gif -------------------------------------------------------------------------------- /Contents/example5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/example5.gif -------------------------------------------------------------------------------- /Contents/example6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/example6.gif -------------------------------------------------------------------------------- /Contents/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/left.gif -------------------------------------------------------------------------------- /Contents/main.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/main.gif -------------------------------------------------------------------------------- /Contents/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/right.gif -------------------------------------------------------------------------------- /Contents/up.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/Contents/up.gif -------------------------------------------------------------------------------- /ExpandableButton.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'ExpandableButton' 3 | s.version = '1.1.0' 4 | s.summary = 'ExpandableButton' 5 | 6 | s.description = <<-DESC 7 | Customizable and easy to use expandable button in Swift. 8 | DESC 9 | 10 | s.homepage = 'https://github.com/DimaMishchenko/ExpandableButton' 11 | s.license = { :type => 'MIT', :file => 'LICENSE' } 12 | s.author = { 'Dima Mishchenko' => 'narmdv5@gmail.com' } 13 | s.source = { :git => 'https://github.com/DimaMishchenko/ExpandableButton.git', :tag => s.version.to_s } 14 | 15 | s.ios.deployment_target = '9.0' 16 | s.source_files = 'ExpandableButton/*' 17 | s.exclude_files = "ExpandableButton/*.plist" 18 | s.swift_version = '4.2' 19 | 20 | end -------------------------------------------------------------------------------- /ExpandableButton/ActionButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MIT License 3 | // 4 | // Copyright (c) 2018 DimaMishchenko 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public class ActionButton: UIButton { 28 | 29 | public typealias ButtonActionBlock = () -> Void 30 | 31 | // MARK: - Public properties 32 | 33 | public var actionBlock: ButtonActionBlock? 34 | 35 | // MARK: - Init 36 | 37 | override public init(frame: CGRect) { 38 | 39 | super.init(frame: frame) 40 | addTarget(self, action: #selector(buttonSelected), for: .touchUpInside) 41 | } 42 | 43 | required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 44 | 45 | // MARK: - Actions 46 | 47 | @objc private func buttonSelected() { actionBlock?() } 48 | } 49 | -------------------------------------------------------------------------------- /ExpandableButton/ArrowButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MIT License 3 | // 4 | // Copyright (c) 2018 DimaMishchenko 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public class ArrowButton: ActionButton { 28 | 29 | private typealias TopBottomPaths = (top: CGPath, bottom: CGPath) 30 | 31 | // MARK: - Public properties 32 | 33 | public var animationDuration: TimeInterval = 0.2 34 | public var arrowInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) 35 | 36 | public var arrowWidth: CGFloat = 1 { 37 | didSet { 38 | topLineLayer.lineWidth = arrowWidth 39 | bottomLineLayer.lineWidth = arrowWidth 40 | } 41 | } 42 | 43 | public var arrowColor: UIColor = .black { 44 | didSet { 45 | topLineLayer.strokeColor = arrowColor.cgColor 46 | bottomLineLayer.strokeColor = arrowColor.cgColor 47 | } 48 | } 49 | 50 | public var isArrowsHidden = false { 51 | didSet { 52 | topLineLayer.isHidden = isArrowsHidden 53 | bottomLineLayer.isHidden = isArrowsHidden 54 | } 55 | } 56 | 57 | // MARK: - Private properties 58 | 59 | private lazy var topLineLayer: CAShapeLayer = layer() 60 | private lazy var bottomLineLayer: CAShapeLayer = layer() 61 | 62 | // MARK: - Public 63 | 64 | public func showUpArrow() { animateArrow(with: upArrowPaths()) } 65 | public func showDownArrow() { animateArrow(with: downArrowPaths()) } 66 | public func showLeftArrow() { animateArrow(with: leftArrowPaths()) } 67 | public func showRightArrow() { animateArrow(with: rightArrowPaths()) } 68 | 69 | // MARK: - Private 70 | 71 | private func animateArrow(with paths: TopBottomPaths) { 72 | 73 | let keyPath = "path" 74 | 75 | let topLineAnimation = aimation( 76 | keyPath: keyPath, 77 | duration: animationDuration, 78 | fromValue: topLineLayer.path, 79 | toValue: paths.top 80 | ) 81 | 82 | let bottomLineAnimation = aimation( 83 | keyPath: keyPath, 84 | duration: animationDuration, 85 | fromValue: bottomLineLayer.path, 86 | toValue: paths.bottom 87 | ) 88 | topLineLayer.path = paths.top 89 | topLineLayer.add(topLineAnimation, forKey: keyPath) 90 | 91 | bottomLineLayer.path = paths.bottom 92 | bottomLineLayer.add(bottomLineAnimation, forKey: keyPath) 93 | } 94 | 95 | private func aimation(keyPath: String, duration: TimeInterval, fromValue: Any?, toValue: Any?) -> CAAnimation { 96 | 97 | let animation = CABasicAnimation(keyPath: keyPath) 98 | animation.duration = duration 99 | animation.fromValue = fromValue 100 | animation.toValue = toValue 101 | 102 | return animation 103 | } 104 | 105 | private func upArrowPaths() -> TopBottomPaths { 106 | 107 | let verticalInset = bounds.size.height / 3 108 | let horizontalInset = bounds.size.width / 2.5 109 | 110 | let firstPoint = CGPoint( 111 | x: center.x - horizontalInset + arrowInsets.left, 112 | y: center.y + verticalInset - arrowInsets.bottom 113 | ) 114 | let secondPoint = CGPoint( 115 | x: center.x + horizontalInset - arrowInsets.right, 116 | y: center.y + verticalInset - arrowInsets.bottom 117 | ) 118 | let centerPoint = CGPoint( 119 | x: center.x, 120 | y: center.y - verticalInset + arrowInsets.top 121 | ) 122 | 123 | return arrowPaths(firstPoint: firstPoint, secondPoint: secondPoint, centerPoint: centerPoint) 124 | } 125 | 126 | private func downArrowPaths() -> TopBottomPaths { 127 | 128 | let verticalInset = bounds.size.height / 3 129 | let horizontalInset = bounds.size.width / 2.5 130 | 131 | let firstPoint = CGPoint( 132 | x: center.x - horizontalInset + arrowInsets.left, 133 | y: center.y - verticalInset + arrowInsets.top 134 | ) 135 | let secondPoint = CGPoint( 136 | x: center.x + horizontalInset - arrowInsets.right, 137 | y: center.y - verticalInset + arrowInsets.top 138 | ) 139 | let centerPoint = CGPoint( 140 | x: center.x, 141 | y: center.y + verticalInset - arrowInsets.bottom 142 | ) 143 | 144 | return arrowPaths(firstPoint: firstPoint, secondPoint: secondPoint, centerPoint: centerPoint) 145 | } 146 | 147 | private func leftArrowPaths() -> TopBottomPaths { 148 | 149 | let verticalInset = bounds.size.height / 2.5 150 | let horizontalInset = bounds.size.width / 3 151 | 152 | let firstPoint = CGPoint( 153 | x: center.x + horizontalInset - arrowInsets.right, 154 | y: center.y - verticalInset + arrowInsets.top 155 | ) 156 | let secondPoint = CGPoint( 157 | x: center.x + horizontalInset - arrowInsets.right, 158 | y: center.y + verticalInset - arrowInsets.bottom 159 | ) 160 | let centerPoint = CGPoint( 161 | x: center.x - horizontalInset + arrowInsets.left, 162 | y: center.y 163 | ) 164 | 165 | return arrowPaths(firstPoint: firstPoint, secondPoint: secondPoint, centerPoint: centerPoint) 166 | } 167 | 168 | private func rightArrowPaths() -> TopBottomPaths { 169 | 170 | let verticalInset = bounds.size.height / 2.5 171 | let horizontalInset = bounds.size.width / 3 172 | 173 | let firstPoint = CGPoint( 174 | x: center.x - horizontalInset + arrowInsets.left, 175 | y: center.y - verticalInset + arrowInsets.top 176 | ) 177 | let secondPoint = CGPoint( 178 | x: center.x - horizontalInset + arrowInsets.left, 179 | y: center.y + verticalInset - arrowInsets.bottom 180 | ) 181 | let centerPoint = CGPoint( 182 | x: center.x + horizontalInset - arrowInsets.right, 183 | y: center.y 184 | ) 185 | 186 | return arrowPaths(firstPoint: firstPoint, secondPoint: secondPoint, centerPoint: centerPoint) 187 | } 188 | 189 | private func arrowPaths(firstPoint top: CGPoint, secondPoint: CGPoint, centerPoint: CGPoint) -> TopBottomPaths { 190 | 191 | let gravityCenter = CGPoint(x: (top.x + secondPoint.x + centerPoint.x) / 3, 192 | y: (top.y + secondPoint.y + centerPoint.y) / 3) 193 | let offsetFromCenter = CGPoint(x: center.x - gravityCenter.x, y: center.y - gravityCenter.y) 194 | 195 | let topLinePath = line(from: top, to: centerPoint, offset: offsetFromCenter) 196 | let bottomLinePath = line(from: secondPoint, to: centerPoint, offset: offsetFromCenter) 197 | 198 | return (topLinePath, bottomLinePath) 199 | } 200 | 201 | private func line(from startPoint: CGPoint, to endPoint: CGPoint, offset: CGPoint) -> CGPath { 202 | 203 | let path = CGMutablePath() 204 | 205 | path.move(to: CGPoint(x: offset.x + startPoint.x, y: offset.y + startPoint.y)) 206 | path.addLine(to: CGPoint(x: offset.x + endPoint.x, y: offset.y + endPoint.y)) 207 | 208 | return path 209 | } 210 | 211 | private func layer() -> CAShapeLayer { 212 | 213 | let layer = CAShapeLayer() 214 | layer.strokeColor = arrowColor.cgColor 215 | layer.lineWidth = arrowWidth 216 | layer.lineJoin = CAShapeLayerLineJoin.round 217 | layer.lineCap = CAShapeLayerLineCap.round 218 | self.layer.addSublayer(layer) 219 | return layer 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /ExpandableButton/ExpandableButtonItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MIT License 3 | // 4 | // Copyright (c) 2018 DimaMishchenko 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public class ExpandableButtonItem { 28 | 29 | public typealias ActionBlock = (ExpandableButtonItem) -> Void 30 | 31 | // MARK: - Properties 32 | 33 | // image 34 | public var image: UIImage? 35 | public var highlightedImage: UIImage? 36 | 37 | // title 38 | public var attributedTitle: NSAttributedString? 39 | public var highlightedAttributedTitle: NSAttributedString? 40 | 41 | // insets 42 | public var contentEdgeInsets: UIEdgeInsets = .zero 43 | public var titleEdgeInsets: UIEdgeInsets = .zero 44 | public var imageEdgeInsets: UIEdgeInsets = .zero 45 | 46 | // width 47 | public var size: CGSize? 48 | 49 | // alignment 50 | public var titleAlignment: NSTextAlignment = .center 51 | 52 | // content mode 53 | public var imageContentMode: UIView.ContentMode = .scaleAspectFit 54 | 55 | // action 56 | public var action: ActionBlock = {_ in} 57 | 58 | // identifier 59 | public var identifier: String = "" 60 | 61 | // MARK: - Init 62 | 63 | public init(image: UIImage? = nil, 64 | highlightedImage: UIImage? = nil, 65 | attributedTitle: NSAttributedString? = nil, 66 | highlightedAttributedTitle: NSAttributedString? = nil, 67 | contentEdgeInsets: UIEdgeInsets = .zero, 68 | titleEdgeInsets: UIEdgeInsets = .zero, 69 | imageEdgeInsets: UIEdgeInsets = .zero, 70 | size: CGSize? = nil, 71 | titleAlignment: NSTextAlignment = .center, 72 | imageContentMode: UIView.ContentMode = .scaleAspectFit, 73 | identifier: String = "", 74 | action: @escaping ActionBlock = {_ in}) { 75 | 76 | self.image = image 77 | self.highlightedImage = highlightedImage 78 | self.attributedTitle = attributedTitle 79 | self.highlightedAttributedTitle = highlightedAttributedTitle 80 | self.contentEdgeInsets = contentEdgeInsets 81 | self.titleEdgeInsets = titleEdgeInsets 82 | self.imageEdgeInsets = imageEdgeInsets 83 | self.size = size 84 | self.titleAlignment = titleAlignment 85 | self.imageContentMode = imageContentMode 86 | self.identifier = identifier 87 | self.action = action 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ExpandableButton/ExpandableButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MIT License 3 | // 4 | // Copyright (c) 2018 DimaMishchenko 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public class ExpandableButtonView: UIView { 28 | 29 | public enum Direction { 30 | case up 31 | case down 32 | case left 33 | case right 34 | } 35 | 36 | public enum State { 37 | case opened 38 | case closed 39 | case animating 40 | } 41 | 42 | // MARK: - UI properties 43 | 44 | private var arrowButton: ArrowButton! 45 | private var separatorView: UIView! 46 | private var itemsButtons: [ActionButton] = [] 47 | 48 | // MARK: - Public properties 49 | 50 | public private(set) var direction: Direction 51 | 52 | public private(set) var state: State = .closed 53 | 54 | public var animationDuration: TimeInterval = 0.2 { 55 | didSet { arrowButton.animationDuration = animationDuration } 56 | } 57 | 58 | public var closeOnAction: Bool = false 59 | public var isHapticFeedback = true 60 | 61 | // arrow 62 | 63 | public var arrowInsets: UIEdgeInsets { 64 | get { return arrowButton.arrowInsets } 65 | set { arrowButton.arrowInsets = newValue } 66 | } 67 | 68 | public var arrowWidth: CGFloat { 69 | get { return arrowButton.arrowWidth } 70 | set { arrowButton.arrowWidth = newValue } 71 | } 72 | 73 | public var arrowColor: UIColor { 74 | get { return arrowButton.arrowColor } 75 | set { arrowButton.arrowColor = newValue } 76 | } 77 | 78 | public var closeOpenImagesInsets: UIEdgeInsets { 79 | get { return arrowButton.imageEdgeInsets } 80 | set { arrowButton.imageEdgeInsets = newValue } 81 | } 82 | public var closeImage: UIImage? 83 | public var openImage: UIImage? 84 | 85 | // separator 86 | 87 | public var isSeparatorHidden: Bool = false { didSet { separatorView.isHidden = isSeparatorHidden } } 88 | public var separatorColor: UIColor = .black { didSet { separatorView.backgroundColor = separatorColor } } 89 | public var separatorInset: CGFloat = 8 { didSet { reloadSeparatorFrame() } } 90 | public var separatorWidth: CGFloat = 1 { didSet { reloadSeparatorFrame() } } 91 | 92 | // MARK: - Private properties 93 | 94 | private var firstLayout = true 95 | 96 | // MARK: - Init 97 | 98 | public init(frame: CGRect = .zero, direction: Direction = .right, items: [ExpandableButtonItem]) { 99 | 100 | self.direction = direction 101 | super.init(frame: frame) 102 | setupUI() 103 | setupButtons(with: items) 104 | } 105 | 106 | required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 107 | 108 | // MARK: - Overrides 109 | 110 | public override var frame: CGRect { didSet { setupFrames() } } 111 | public override var backgroundColor: UIColor? { didSet { arrowButton.backgroundColor = backgroundColor } } 112 | 113 | override public func layoutSubviews() { 114 | 115 | super.layoutSubviews() 116 | 117 | if firstLayout { 118 | 119 | setupFrames() 120 | showCloseArrow() 121 | 122 | firstLayout = false 123 | } 124 | } 125 | 126 | // MARK: - Public 127 | 128 | public func open() { 129 | 130 | guard state == .closed else { return } 131 | 132 | state = .animating 133 | showOpenArrow() 134 | 135 | itemsButtons.forEach { $0.isHidden = false } 136 | 137 | UIView.animate(withDuration: animationDuration, animations: { 138 | self.itemsButtons.forEach { $0.alpha = 0; $0.alpha = 1 } 139 | self.open(with: self.direction) 140 | }) { 141 | if $0 { 142 | self.state = .opened 143 | self.impactHapticFeedback() 144 | } 145 | } 146 | } 147 | 148 | public func close() { 149 | 150 | guard state == .opened else { return } 151 | 152 | state = .animating 153 | 154 | // because of CABasicAnimation in ArrowButton. 155 | if direction == .up || direction == .left { self.close(with: self.direction) } 156 | 157 | showCloseArrow() 158 | 159 | // because of CABasicAnimation in ArrowButton. 160 | if direction == .up || direction == .left { self.open(with: self.direction) } 161 | 162 | UIView.animate(withDuration: animationDuration, animations: { 163 | self.itemsButtons.forEach { $0.alpha = 1; $0.alpha = 0 } 164 | self.close(with: self.direction) 165 | }) { 166 | if $0 { 167 | self.itemsButtons.forEach { $0.isHidden = true } 168 | self.state = .closed 169 | self.impactHapticFeedback() 170 | } 171 | } 172 | 173 | } 174 | 175 | // MARK: - Private 176 | 177 | private func setupUI() { 178 | 179 | clipsToBounds = true 180 | 181 | // arrow button 182 | 183 | arrowButton = ArrowButton() 184 | arrowButton.actionBlock = { [weak self] in 185 | 186 | guard let state = self?.state else { return } 187 | 188 | switch state { 189 | case .opened: self?.close() 190 | case .closed: self?.open() 191 | case .animating: break 192 | } 193 | } 194 | arrowButton.backgroundColor = backgroundColor 195 | addSubview(arrowButton) 196 | 197 | // separator 198 | 199 | separatorView = UIView() 200 | separatorView.backgroundColor = separatorColor 201 | insertSubview(separatorView, belowSubview: arrowButton) 202 | } 203 | 204 | private func setupButtons(with items: [ExpandableButtonItem]) { 205 | 206 | items.forEach { item in 207 | 208 | let button = ActionButton() 209 | insertSubview(button, belowSubview: arrowButton) 210 | 211 | button.setImage(item.image, for: .normal) 212 | button.setImage(item.highlightedImage, for: .highlighted) 213 | 214 | button.setAttributedTitle(item.attributedTitle, for: .normal) 215 | button.setAttributedTitle(item.highlightedAttributedTitle, for: .highlighted) 216 | 217 | button.contentEdgeInsets = item.contentEdgeInsets 218 | button.titleEdgeInsets = item.titleEdgeInsets 219 | button.imageEdgeInsets = item.imageEdgeInsets 220 | 221 | button.titleLabel?.textAlignment = item.titleAlignment 222 | button.imageView?.contentMode = item.imageContentMode 223 | 224 | if let size = item.size { button.frame = CGRect(origin: .zero, size: size) } 225 | 226 | button.actionBlock = { [weak self] in 227 | item.action(item) 228 | if let closeOnAction = self?.closeOnAction, closeOnAction { self?.close() } 229 | } 230 | 231 | itemsButtons.append(button) 232 | } 233 | } 234 | 235 | // MARK: - Layout 236 | 237 | private func setupFrames() { 238 | 239 | guard arrowButton != nil, separatorView != nil else { return } 240 | 241 | // arrow button 242 | 243 | arrowButton.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height) 244 | 245 | // separator 246 | 247 | reloadSeparatorFrame() 248 | 249 | // items buttons 250 | 251 | setupItemButtonsFrames() 252 | itemsButtons.forEach { $0.isHidden = true } 253 | 254 | // self 255 | 256 | switch state { 257 | case .closed: 258 | showCloseArrow() 259 | case .opened: 260 | open(with: direction) 261 | showOpenArrow() 262 | default: break 263 | } 264 | } 265 | 266 | private func reloadSeparatorFrame() { 267 | 268 | switch direction { 269 | case .up: 270 | let y = itemsButtons.reduce(0, { $0 + $1.frame.height }) 271 | let width = frame.width - separatorInset * 2 272 | separatorView.frame = CGRect(x: separatorInset, y: y, width: width, height: separatorWidth) 273 | case .down: 274 | let width = frame.width - separatorInset * 2 275 | separatorView.frame = CGRect(x: separatorInset, y: frame.height, width: width, height: separatorWidth) 276 | case .left: 277 | let x = itemsButtons.reduce(0, { $0 + $1.frame.width }) 278 | let height = frame.height - separatorInset * 2 279 | separatorView.frame = CGRect(x: x, y: separatorInset, width: separatorWidth, height: height) 280 | case .right: 281 | let height = frame.height - separatorInset * 2 282 | separatorView.frame = CGRect(x: frame.width, y: separatorInset, width: separatorWidth, height: height) 283 | } 284 | } 285 | 286 | private func setupItemButtonsFrames() { 287 | 288 | var previousButton: UIButton? 289 | 290 | itemsButtons.forEach { 291 | 292 | let width = $0.frame.width == 0 ? arrowButton.frame.width : $0.frame.width 293 | let height = $0.frame.height == 0 ? arrowButton.frame.height : $0.frame.height 294 | 295 | var x: CGFloat = 0 296 | var y: CGFloat = 0 297 | 298 | switch direction { 299 | case .up: 300 | y = previousButton != nil ? 301 | previousButton!.frame.origin.y + previousButton!.frame.height : 302 | arrowButton.frame.origin.y 303 | case .down: 304 | y = previousButton != nil ? 305 | previousButton!.frame.origin.y + previousButton!.frame.height : 306 | arrowButton.frame.origin.y + arrowButton.frame.height 307 | case .left: 308 | x = previousButton != nil ? 309 | previousButton!.frame.origin.x + previousButton!.frame.width : 310 | arrowButton.frame.origin.x 311 | case .right: 312 | x = previousButton != nil ? 313 | previousButton!.frame.origin.x + previousButton!.frame.width : 314 | arrowButton.frame.origin.x + arrowButton.frame.width 315 | } 316 | $0.frame = CGRect(x: x, y: y, width: width, height: height) 317 | 318 | previousButton = $0 319 | } 320 | } 321 | 322 | // MARK: - Arrows 323 | 324 | private func showOpenArrow() { 325 | 326 | arrowButton.setImage(openImage, for: .normal) 327 | arrowButton.isArrowsHidden = openImage != nil 328 | 329 | if openImage == nil { 330 | 331 | if closeImage == nil { 332 | 333 | switch direction { 334 | case .up: arrowButton.showDownArrow() 335 | case .down: arrowButton.showUpArrow() 336 | case .left: arrowButton.showRightArrow() 337 | case .right: arrowButton.showLeftArrow() 338 | } 339 | } 340 | } 341 | } 342 | 343 | private func showCloseArrow() { 344 | 345 | arrowButton.setImage(closeImage, for: .normal) 346 | arrowButton.isArrowsHidden = closeImage != nil 347 | 348 | if closeImage == nil { 349 | 350 | switch direction { 351 | case .up: arrowButton.showUpArrow() 352 | case .down: arrowButton.showDownArrow() 353 | case .left: arrowButton.showLeftArrow() 354 | case .right: arrowButton.showRightArrow() 355 | } 356 | } 357 | } 358 | 359 | // MARK: - Haptic Feedback 360 | 361 | private func impactHapticFeedback() { 362 | 363 | if #available(iOS 10.0, *), isHapticFeedback { 364 | 365 | let generator = UISelectionFeedbackGenerator() 366 | generator.selectionChanged() 367 | } 368 | } 369 | 370 | // MARK: - Open close 371 | 372 | private func open(with direction: Direction) { 373 | 374 | switch direction { 375 | case .up: 376 | let itemsHeight = itemsButtons.reduce(0, { $0 + $1.frame.height }) 377 | let y = frame.origin.y - itemsHeight 378 | let height = frame.size.height + itemsHeight 379 | 380 | super.frame = CGRect(x: frame.origin.x, y: y, width: frame.size.width, height: height) 381 | 382 | let arrY = super.frame.height - arrowButton.frame.height 383 | arrowButton.frame = CGRect(x: 0, y: arrY, width: arrowButton.frame.width, height: arrowButton.frame.height) 384 | case .down: 385 | let height = frame.size.height + itemsButtons.reduce(0, { $0 + $1.frame.height }) 386 | super.frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.size.width, height: height) 387 | case .left: 388 | let itemsWidth = itemsButtons.reduce(0, { $0 + $1.frame.width }) 389 | let x = frame.origin.x - itemsWidth 390 | let width = frame.size.width + itemsWidth 391 | super.frame = CGRect(x: x, y: frame.origin.y, width: width, height: frame.size.height) 392 | 393 | let arrX = super.frame.width - arrowButton.frame.width 394 | arrowButton.frame = CGRect(x: arrX, y: 0, width: arrowButton.frame.width, height: arrowButton.frame.height) 395 | case .right: 396 | let width = frame.size.width + itemsButtons.reduce(0, { $0 + $1.frame.width }) 397 | super.frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: width, height: frame.size.height) 398 | } 399 | } 400 | 401 | private func close(with direction: Direction) { 402 | 403 | switch direction { 404 | case .up: 405 | let itemsHeight = itemsButtons.reduce(0, { $0 + $1.frame.height }) 406 | let y = frame.origin.y + itemsHeight 407 | let height = frame.size.height - itemsHeight 408 | super.frame = CGRect(x: frame.origin.x, y: y, width: frame.size.width, height: height) 409 | 410 | arrowButton.frame = CGRect(x: 0, y: 0, width: arrowButton.frame.width, height: arrowButton.frame.height) 411 | case .down: 412 | let height = frame.size.height - itemsButtons.reduce(0, { $0 + $1.frame.height }) 413 | super.frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.size.width, height: height) 414 | case .left: 415 | let itemsWidth = itemsButtons.reduce(0, { $0 + $1.frame.width }) 416 | let x = frame.origin.x + itemsWidth 417 | let width = frame.size.width - itemsWidth 418 | super.frame = CGRect(x: x, y: frame.origin.y, width: width, height: frame.size.height) 419 | 420 | arrowButton.frame = CGRect(x: 0, y: 0, width: arrowButton.frame.width, height: arrowButton.frame.height) 421 | case .right: 422 | let width = frame.size.width - itemsButtons.reduce(0, { $0 + $1.frame.width }) 423 | super.frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: width, height: frame.size.height) 424 | } 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /ExpandableButtonExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 963F695321021DA9007C5ACA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963F695221021DA9007C5ACA /* AppDelegate.swift */; }; 11 | 963F695521021DA9007C5ACA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963F695421021DA9007C5ACA /* ViewController.swift */; }; 12 | 963F695821021DAA007C5ACA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 963F695621021DAA007C5ACA /* Main.storyboard */; }; 13 | 963F695A21021DB6007C5ACA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 963F695921021DB6007C5ACA /* Assets.xcassets */; }; 14 | 963F695D21021DB6007C5ACA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 963F695B21021DB6007C5ACA /* LaunchScreen.storyboard */; }; 15 | 963F696821021DB6007C5ACA /* ExpandableButtonExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963F696721021DB6007C5ACA /* ExpandableButtonExampleTests.swift */; }; 16 | 963F697421021E3D007C5ACA /* ExpandableButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963F697321021E3D007C5ACA /* ExpandableButtonView.swift */; }; 17 | 963F6976210224CB007C5ACA /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963F6975210224CB007C5ACA /* ActionButton.swift */; }; 18 | 965C34E121109E400081C003 /* ExpandableButtonExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965C34E021109E400081C003 /* ExpandableButtonExampleUITests.swift */; }; 19 | 96BA3FFF21062BCC004F4EB0 /* ArrowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BA3FFE21062BCC004F4EB0 /* ArrowButton.swift */; }; 20 | 96FF4BC8210238EA00938FE3 /* ExpandableButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96FF4BC7210238EA00938FE3 /* ExpandableButtonItem.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 963F696421021DB6007C5ACA /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 963F694721021DA9007C5ACA /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 963F694E21021DA9007C5ACA; 29 | remoteInfo = ExpandableButtonExample; 30 | }; 31 | 965C34E321109E400081C003 /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = 963F694721021DA9007C5ACA /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = 963F694E21021DA9007C5ACA; 36 | remoteInfo = ExpandableButtonExample; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 963F694F21021DA9007C5ACA /* ExpandableButtonExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExpandableButtonExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 963F695221021DA9007C5ACA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | 963F695421021DA9007C5ACA /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 44 | 963F695721021DAA007C5ACA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 45 | 963F695921021DB6007C5ACA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | 963F695C21021DB6007C5ACA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 47 | 963F695E21021DB6007C5ACA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 963F696321021DB6007C5ACA /* ExpandableButtonExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExpandableButtonExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 963F696721021DB6007C5ACA /* ExpandableButtonExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableButtonExampleTests.swift; sourceTree = ""; }; 50 | 963F696921021DB6007C5ACA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 963F697321021E3D007C5ACA /* ExpandableButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableButtonView.swift; sourceTree = ""; }; 52 | 963F6975210224CB007C5ACA /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; 53 | 965C34DE21109E400081C003 /* ExpandableButtonExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExpandableButtonExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 965C34E021109E400081C003 /* ExpandableButtonExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableButtonExampleUITests.swift; sourceTree = ""; }; 55 | 965C34E221109E400081C003 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 96BA3FFE21062BCC004F4EB0 /* ArrowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowButton.swift; sourceTree = ""; }; 57 | 96FF4BC7210238EA00938FE3 /* ExpandableButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableButtonItem.swift; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | 963F694C21021DA9007C5ACA /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | 963F696021021DB6007C5ACA /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | 965C34DB21109E400081C003 /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | /* End PBXFrameworksBuildPhase section */ 83 | 84 | /* Begin PBXGroup section */ 85 | 963F694621021DA9007C5ACA = { 86 | isa = PBXGroup; 87 | children = ( 88 | 963F697221021E08007C5ACA /* ExpandableButton */, 89 | 963F695121021DA9007C5ACA /* ExpandableButtonExample */, 90 | 963F696621021DB6007C5ACA /* ExpandableButtonExampleTests */, 91 | 965C34DF21109E400081C003 /* ExpandableButtonExampleUITests */, 92 | 963F695021021DA9007C5ACA /* Products */, 93 | ); 94 | sourceTree = ""; 95 | }; 96 | 963F695021021DA9007C5ACA /* Products */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 963F694F21021DA9007C5ACA /* ExpandableButtonExample.app */, 100 | 963F696321021DB6007C5ACA /* ExpandableButtonExampleTests.xctest */, 101 | 965C34DE21109E400081C003 /* ExpandableButtonExampleUITests.xctest */, 102 | ); 103 | name = Products; 104 | sourceTree = ""; 105 | }; 106 | 963F695121021DA9007C5ACA /* ExpandableButtonExample */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 963F695221021DA9007C5ACA /* AppDelegate.swift */, 110 | 963F695421021DA9007C5ACA /* ViewController.swift */, 111 | 963F695621021DAA007C5ACA /* Main.storyboard */, 112 | 963F695921021DB6007C5ACA /* Assets.xcassets */, 113 | 963F695B21021DB6007C5ACA /* LaunchScreen.storyboard */, 114 | 963F695E21021DB6007C5ACA /* Info.plist */, 115 | ); 116 | path = ExpandableButtonExample; 117 | sourceTree = ""; 118 | }; 119 | 963F696621021DB6007C5ACA /* ExpandableButtonExampleTests */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 963F696721021DB6007C5ACA /* ExpandableButtonExampleTests.swift */, 123 | 963F696921021DB6007C5ACA /* Info.plist */, 124 | ); 125 | path = ExpandableButtonExampleTests; 126 | sourceTree = ""; 127 | }; 128 | 963F697221021E08007C5ACA /* ExpandableButton */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 963F697321021E3D007C5ACA /* ExpandableButtonView.swift */, 132 | 96FF4BC7210238EA00938FE3 /* ExpandableButtonItem.swift */, 133 | 96BA3FFE21062BCC004F4EB0 /* ArrowButton.swift */, 134 | 963F6975210224CB007C5ACA /* ActionButton.swift */, 135 | ); 136 | path = ExpandableButton; 137 | sourceTree = ""; 138 | }; 139 | 965C34DF21109E400081C003 /* ExpandableButtonExampleUITests */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 965C34E021109E400081C003 /* ExpandableButtonExampleUITests.swift */, 143 | 965C34E221109E400081C003 /* Info.plist */, 144 | ); 145 | path = ExpandableButtonExampleUITests; 146 | sourceTree = ""; 147 | }; 148 | /* End PBXGroup section */ 149 | 150 | /* Begin PBXNativeTarget section */ 151 | 963F694E21021DA9007C5ACA /* ExpandableButtonExample */ = { 152 | isa = PBXNativeTarget; 153 | buildConfigurationList = 963F696C21021DB6007C5ACA /* Build configuration list for PBXNativeTarget "ExpandableButtonExample" */; 154 | buildPhases = ( 155 | 963F694B21021DA9007C5ACA /* Sources */, 156 | 963F694C21021DA9007C5ACA /* Frameworks */, 157 | 963F694D21021DA9007C5ACA /* Resources */, 158 | ); 159 | buildRules = ( 160 | ); 161 | dependencies = ( 162 | ); 163 | name = ExpandableButtonExample; 164 | productName = ExpandableButtonExample; 165 | productReference = 963F694F21021DA9007C5ACA /* ExpandableButtonExample.app */; 166 | productType = "com.apple.product-type.application"; 167 | }; 168 | 963F696221021DB6007C5ACA /* ExpandableButtonExampleTests */ = { 169 | isa = PBXNativeTarget; 170 | buildConfigurationList = 963F696F21021DB6007C5ACA /* Build configuration list for PBXNativeTarget "ExpandableButtonExampleTests" */; 171 | buildPhases = ( 172 | 963F695F21021DB6007C5ACA /* Sources */, 173 | 963F696021021DB6007C5ACA /* Frameworks */, 174 | 963F696121021DB6007C5ACA /* Resources */, 175 | ); 176 | buildRules = ( 177 | ); 178 | dependencies = ( 179 | 963F696521021DB6007C5ACA /* PBXTargetDependency */, 180 | ); 181 | name = ExpandableButtonExampleTests; 182 | productName = ExpandableButtonExampleTests; 183 | productReference = 963F696321021DB6007C5ACA /* ExpandableButtonExampleTests.xctest */; 184 | productType = "com.apple.product-type.bundle.unit-test"; 185 | }; 186 | 965C34DD21109E400081C003 /* ExpandableButtonExampleUITests */ = { 187 | isa = PBXNativeTarget; 188 | buildConfigurationList = 965C34E721109E400081C003 /* Build configuration list for PBXNativeTarget "ExpandableButtonExampleUITests" */; 189 | buildPhases = ( 190 | 965C34DA21109E400081C003 /* Sources */, 191 | 965C34DB21109E400081C003 /* Frameworks */, 192 | 965C34DC21109E400081C003 /* Resources */, 193 | ); 194 | buildRules = ( 195 | ); 196 | dependencies = ( 197 | 965C34E421109E400081C003 /* PBXTargetDependency */, 198 | ); 199 | name = ExpandableButtonExampleUITests; 200 | productName = ExpandableButtonExampleUITests; 201 | productReference = 965C34DE21109E400081C003 /* ExpandableButtonExampleUITests.xctest */; 202 | productType = "com.apple.product-type.bundle.ui-testing"; 203 | }; 204 | /* End PBXNativeTarget section */ 205 | 206 | /* Begin PBXProject section */ 207 | 963F694721021DA9007C5ACA /* Project object */ = { 208 | isa = PBXProject; 209 | attributes = { 210 | LastSwiftUpdateCheck = 0940; 211 | LastUpgradeCheck = 0940; 212 | ORGANIZATIONNAME = Dima; 213 | TargetAttributes = { 214 | 963F694E21021DA9007C5ACA = { 215 | CreatedOnToolsVersion = 9.4.1; 216 | }; 217 | 963F696221021DB6007C5ACA = { 218 | CreatedOnToolsVersion = 9.4.1; 219 | TestTargetID = 963F694E21021DA9007C5ACA; 220 | }; 221 | 965C34DD21109E400081C003 = { 222 | CreatedOnToolsVersion = 9.4.1; 223 | TestTargetID = 963F694E21021DA9007C5ACA; 224 | }; 225 | }; 226 | }; 227 | buildConfigurationList = 963F694A21021DA9007C5ACA /* Build configuration list for PBXProject "ExpandableButtonExample" */; 228 | compatibilityVersion = "Xcode 9.3"; 229 | developmentRegion = en; 230 | hasScannedForEncodings = 0; 231 | knownRegions = ( 232 | en, 233 | Base, 234 | ); 235 | mainGroup = 963F694621021DA9007C5ACA; 236 | productRefGroup = 963F695021021DA9007C5ACA /* Products */; 237 | projectDirPath = ""; 238 | projectRoot = ""; 239 | targets = ( 240 | 963F694E21021DA9007C5ACA /* ExpandableButtonExample */, 241 | 963F696221021DB6007C5ACA /* ExpandableButtonExampleTests */, 242 | 965C34DD21109E400081C003 /* ExpandableButtonExampleUITests */, 243 | ); 244 | }; 245 | /* End PBXProject section */ 246 | 247 | /* Begin PBXResourcesBuildPhase section */ 248 | 963F694D21021DA9007C5ACA /* Resources */ = { 249 | isa = PBXResourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | 963F695D21021DB6007C5ACA /* LaunchScreen.storyboard in Resources */, 253 | 963F695A21021DB6007C5ACA /* Assets.xcassets in Resources */, 254 | 963F695821021DAA007C5ACA /* Main.storyboard in Resources */, 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | 963F696121021DB6007C5ACA /* Resources */ = { 259 | isa = PBXResourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | }; 265 | 965C34DC21109E400081C003 /* Resources */ = { 266 | isa = PBXResourcesBuildPhase; 267 | buildActionMask = 2147483647; 268 | files = ( 269 | ); 270 | runOnlyForDeploymentPostprocessing = 0; 271 | }; 272 | /* End PBXResourcesBuildPhase section */ 273 | 274 | /* Begin PBXSourcesBuildPhase section */ 275 | 963F694B21021DA9007C5ACA /* Sources */ = { 276 | isa = PBXSourcesBuildPhase; 277 | buildActionMask = 2147483647; 278 | files = ( 279 | 963F697421021E3D007C5ACA /* ExpandableButtonView.swift in Sources */, 280 | 96BA3FFF21062BCC004F4EB0 /* ArrowButton.swift in Sources */, 281 | 963F695521021DA9007C5ACA /* ViewController.swift in Sources */, 282 | 963F695321021DA9007C5ACA /* AppDelegate.swift in Sources */, 283 | 963F6976210224CB007C5ACA /* ActionButton.swift in Sources */, 284 | 96FF4BC8210238EA00938FE3 /* ExpandableButtonItem.swift in Sources */, 285 | ); 286 | runOnlyForDeploymentPostprocessing = 0; 287 | }; 288 | 963F695F21021DB6007C5ACA /* Sources */ = { 289 | isa = PBXSourcesBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | 963F696821021DB6007C5ACA /* ExpandableButtonExampleTests.swift in Sources */, 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | 965C34DA21109E400081C003 /* Sources */ = { 297 | isa = PBXSourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | 965C34E121109E400081C003 /* ExpandableButtonExampleUITests.swift in Sources */, 301 | ); 302 | runOnlyForDeploymentPostprocessing = 0; 303 | }; 304 | /* End PBXSourcesBuildPhase section */ 305 | 306 | /* Begin PBXTargetDependency section */ 307 | 963F696521021DB6007C5ACA /* PBXTargetDependency */ = { 308 | isa = PBXTargetDependency; 309 | target = 963F694E21021DA9007C5ACA /* ExpandableButtonExample */; 310 | targetProxy = 963F696421021DB6007C5ACA /* PBXContainerItemProxy */; 311 | }; 312 | 965C34E421109E400081C003 /* PBXTargetDependency */ = { 313 | isa = PBXTargetDependency; 314 | target = 963F694E21021DA9007C5ACA /* ExpandableButtonExample */; 315 | targetProxy = 965C34E321109E400081C003 /* PBXContainerItemProxy */; 316 | }; 317 | /* End PBXTargetDependency section */ 318 | 319 | /* Begin PBXVariantGroup section */ 320 | 963F695621021DAA007C5ACA /* Main.storyboard */ = { 321 | isa = PBXVariantGroup; 322 | children = ( 323 | 963F695721021DAA007C5ACA /* Base */, 324 | ); 325 | name = Main.storyboard; 326 | sourceTree = ""; 327 | }; 328 | 963F695B21021DB6007C5ACA /* LaunchScreen.storyboard */ = { 329 | isa = PBXVariantGroup; 330 | children = ( 331 | 963F695C21021DB6007C5ACA /* Base */, 332 | ); 333 | name = LaunchScreen.storyboard; 334 | sourceTree = ""; 335 | }; 336 | /* End PBXVariantGroup section */ 337 | 338 | /* Begin XCBuildConfiguration section */ 339 | 963F696A21021DB6007C5ACA /* Debug */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ALWAYS_SEARCH_USER_PATHS = NO; 343 | CLANG_ANALYZER_NONNULL = YES; 344 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 345 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 346 | CLANG_CXX_LIBRARY = "libc++"; 347 | CLANG_ENABLE_MODULES = YES; 348 | CLANG_ENABLE_OBJC_ARC = YES; 349 | CLANG_ENABLE_OBJC_WEAK = YES; 350 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 351 | CLANG_WARN_BOOL_CONVERSION = YES; 352 | CLANG_WARN_COMMA = YES; 353 | CLANG_WARN_CONSTANT_CONVERSION = YES; 354 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 355 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 356 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 357 | CLANG_WARN_EMPTY_BODY = YES; 358 | CLANG_WARN_ENUM_CONVERSION = YES; 359 | CLANG_WARN_INFINITE_RECURSION = YES; 360 | CLANG_WARN_INT_CONVERSION = YES; 361 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 363 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 364 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 365 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 366 | CLANG_WARN_STRICT_PROTOTYPES = YES; 367 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 368 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 369 | CLANG_WARN_UNREACHABLE_CODE = YES; 370 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 371 | CODE_SIGN_IDENTITY = "iPhone Developer"; 372 | COPY_PHASE_STRIP = NO; 373 | DEBUG_INFORMATION_FORMAT = dwarf; 374 | ENABLE_STRICT_OBJC_MSGSEND = YES; 375 | ENABLE_TESTABILITY = YES; 376 | GCC_C_LANGUAGE_STANDARD = gnu11; 377 | GCC_DYNAMIC_NO_PIC = NO; 378 | GCC_NO_COMMON_BLOCKS = YES; 379 | GCC_OPTIMIZATION_LEVEL = 0; 380 | GCC_PREPROCESSOR_DEFINITIONS = ( 381 | "DEBUG=1", 382 | "$(inherited)", 383 | ); 384 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 385 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 386 | GCC_WARN_UNDECLARED_SELECTOR = YES; 387 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 388 | GCC_WARN_UNUSED_FUNCTION = YES; 389 | GCC_WARN_UNUSED_VARIABLE = YES; 390 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 391 | MTL_ENABLE_DEBUG_INFO = YES; 392 | ONLY_ACTIVE_ARCH = YES; 393 | SDKROOT = iphoneos; 394 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 395 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 396 | SWIFT_VERSION = 4.2; 397 | }; 398 | name = Debug; 399 | }; 400 | 963F696B21021DB6007C5ACA /* Release */ = { 401 | isa = XCBuildConfiguration; 402 | buildSettings = { 403 | ALWAYS_SEARCH_USER_PATHS = NO; 404 | CLANG_ANALYZER_NONNULL = YES; 405 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 406 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 407 | CLANG_CXX_LIBRARY = "libc++"; 408 | CLANG_ENABLE_MODULES = YES; 409 | CLANG_ENABLE_OBJC_ARC = YES; 410 | CLANG_ENABLE_OBJC_WEAK = YES; 411 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 412 | CLANG_WARN_BOOL_CONVERSION = YES; 413 | CLANG_WARN_COMMA = YES; 414 | CLANG_WARN_CONSTANT_CONVERSION = YES; 415 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 416 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 417 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 418 | CLANG_WARN_EMPTY_BODY = YES; 419 | CLANG_WARN_ENUM_CONVERSION = YES; 420 | CLANG_WARN_INFINITE_RECURSION = YES; 421 | CLANG_WARN_INT_CONVERSION = YES; 422 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 423 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 424 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 425 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 426 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 427 | CLANG_WARN_STRICT_PROTOTYPES = YES; 428 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 429 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 430 | CLANG_WARN_UNREACHABLE_CODE = YES; 431 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 432 | CODE_SIGN_IDENTITY = "iPhone Developer"; 433 | COPY_PHASE_STRIP = NO; 434 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 435 | ENABLE_NS_ASSERTIONS = NO; 436 | ENABLE_STRICT_OBJC_MSGSEND = YES; 437 | GCC_C_LANGUAGE_STANDARD = gnu11; 438 | GCC_NO_COMMON_BLOCKS = YES; 439 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 440 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 441 | GCC_WARN_UNDECLARED_SELECTOR = YES; 442 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 443 | GCC_WARN_UNUSED_FUNCTION = YES; 444 | GCC_WARN_UNUSED_VARIABLE = YES; 445 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 446 | MTL_ENABLE_DEBUG_INFO = NO; 447 | SDKROOT = iphoneos; 448 | SWIFT_COMPILATION_MODE = wholemodule; 449 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 450 | SWIFT_VERSION = 4.2; 451 | VALIDATE_PRODUCT = YES; 452 | }; 453 | name = Release; 454 | }; 455 | 963F696D21021DB6007C5ACA /* Debug */ = { 456 | isa = XCBuildConfiguration; 457 | buildSettings = { 458 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 459 | CODE_SIGN_STYLE = Automatic; 460 | DEVELOPMENT_TEAM = 77X75EH6F4; 461 | INFOPLIST_FILE = ExpandableButtonExample/Info.plist; 462 | LD_RUNPATH_SEARCH_PATHS = ( 463 | "$(inherited)", 464 | "@executable_path/Frameworks", 465 | ); 466 | PRODUCT_BUNDLE_IDENTIFIER = com.dima.mishchenko.ExpandableButtonExample; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SWIFT_VERSION = 4.2; 469 | TARGETED_DEVICE_FAMILY = "1,2"; 470 | }; 471 | name = Debug; 472 | }; 473 | 963F696E21021DB6007C5ACA /* Release */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 477 | CODE_SIGN_STYLE = Automatic; 478 | DEVELOPMENT_TEAM = 77X75EH6F4; 479 | INFOPLIST_FILE = ExpandableButtonExample/Info.plist; 480 | LD_RUNPATH_SEARCH_PATHS = ( 481 | "$(inherited)", 482 | "@executable_path/Frameworks", 483 | ); 484 | PRODUCT_BUNDLE_IDENTIFIER = com.dima.mishchenko.ExpandableButtonExample; 485 | PRODUCT_NAME = "$(TARGET_NAME)"; 486 | SWIFT_VERSION = 4.2; 487 | TARGETED_DEVICE_FAMILY = "1,2"; 488 | }; 489 | name = Release; 490 | }; 491 | 963F697021021DB6007C5ACA /* Debug */ = { 492 | isa = XCBuildConfiguration; 493 | buildSettings = { 494 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 495 | BUNDLE_LOADER = "$(TEST_HOST)"; 496 | CODE_SIGN_STYLE = Automatic; 497 | DEVELOPMENT_TEAM = 77X75EH6F4; 498 | INFOPLIST_FILE = ExpandableButtonExampleTests/Info.plist; 499 | LD_RUNPATH_SEARCH_PATHS = ( 500 | "$(inherited)", 501 | "@executable_path/Frameworks", 502 | "@loader_path/Frameworks", 503 | ); 504 | PRODUCT_BUNDLE_IDENTIFIER = com.dima.mishchenko.ExpandableButtonExampleTests; 505 | PRODUCT_NAME = "$(TARGET_NAME)"; 506 | SWIFT_VERSION = 4.2; 507 | TARGETED_DEVICE_FAMILY = "1,2"; 508 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ExpandableButtonExample.app/ExpandableButtonExample"; 509 | }; 510 | name = Debug; 511 | }; 512 | 963F697121021DB6007C5ACA /* Release */ = { 513 | isa = XCBuildConfiguration; 514 | buildSettings = { 515 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 516 | BUNDLE_LOADER = "$(TEST_HOST)"; 517 | CODE_SIGN_STYLE = Automatic; 518 | DEVELOPMENT_TEAM = 77X75EH6F4; 519 | INFOPLIST_FILE = ExpandableButtonExampleTests/Info.plist; 520 | LD_RUNPATH_SEARCH_PATHS = ( 521 | "$(inherited)", 522 | "@executable_path/Frameworks", 523 | "@loader_path/Frameworks", 524 | ); 525 | PRODUCT_BUNDLE_IDENTIFIER = com.dima.mishchenko.ExpandableButtonExampleTests; 526 | PRODUCT_NAME = "$(TARGET_NAME)"; 527 | SWIFT_VERSION = 4.2; 528 | TARGETED_DEVICE_FAMILY = "1,2"; 529 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ExpandableButtonExample.app/ExpandableButtonExample"; 530 | }; 531 | name = Release; 532 | }; 533 | 965C34E521109E400081C003 /* Debug */ = { 534 | isa = XCBuildConfiguration; 535 | buildSettings = { 536 | CODE_SIGN_STYLE = Automatic; 537 | DEVELOPMENT_TEAM = 77X75EH6F4; 538 | INFOPLIST_FILE = ExpandableButtonExampleUITests/Info.plist; 539 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 540 | LD_RUNPATH_SEARCH_PATHS = ( 541 | "$(inherited)", 542 | "@executable_path/Frameworks", 543 | "@loader_path/Frameworks", 544 | ); 545 | PRODUCT_BUNDLE_IDENTIFIER = com.dima.mishchenko.ExpandableButtonExampleUITests; 546 | PRODUCT_NAME = "$(TARGET_NAME)"; 547 | SWIFT_VERSION = 4.2; 548 | TARGETED_DEVICE_FAMILY = "1,2"; 549 | TEST_TARGET_NAME = ExpandableButtonExample; 550 | }; 551 | name = Debug; 552 | }; 553 | 965C34E621109E400081C003 /* Release */ = { 554 | isa = XCBuildConfiguration; 555 | buildSettings = { 556 | CODE_SIGN_STYLE = Automatic; 557 | DEVELOPMENT_TEAM = 77X75EH6F4; 558 | INFOPLIST_FILE = ExpandableButtonExampleUITests/Info.plist; 559 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 560 | LD_RUNPATH_SEARCH_PATHS = ( 561 | "$(inherited)", 562 | "@executable_path/Frameworks", 563 | "@loader_path/Frameworks", 564 | ); 565 | PRODUCT_BUNDLE_IDENTIFIER = com.dima.mishchenko.ExpandableButtonExampleUITests; 566 | PRODUCT_NAME = "$(TARGET_NAME)"; 567 | SWIFT_VERSION = 4.2; 568 | TARGETED_DEVICE_FAMILY = "1,2"; 569 | TEST_TARGET_NAME = ExpandableButtonExample; 570 | }; 571 | name = Release; 572 | }; 573 | /* End XCBuildConfiguration section */ 574 | 575 | /* Begin XCConfigurationList section */ 576 | 963F694A21021DA9007C5ACA /* Build configuration list for PBXProject "ExpandableButtonExample" */ = { 577 | isa = XCConfigurationList; 578 | buildConfigurations = ( 579 | 963F696A21021DB6007C5ACA /* Debug */, 580 | 963F696B21021DB6007C5ACA /* Release */, 581 | ); 582 | defaultConfigurationIsVisible = 0; 583 | defaultConfigurationName = Release; 584 | }; 585 | 963F696C21021DB6007C5ACA /* Build configuration list for PBXNativeTarget "ExpandableButtonExample" */ = { 586 | isa = XCConfigurationList; 587 | buildConfigurations = ( 588 | 963F696D21021DB6007C5ACA /* Debug */, 589 | 963F696E21021DB6007C5ACA /* Release */, 590 | ); 591 | defaultConfigurationIsVisible = 0; 592 | defaultConfigurationName = Release; 593 | }; 594 | 963F696F21021DB6007C5ACA /* Build configuration list for PBXNativeTarget "ExpandableButtonExampleTests" */ = { 595 | isa = XCConfigurationList; 596 | buildConfigurations = ( 597 | 963F697021021DB6007C5ACA /* Debug */, 598 | 963F697121021DB6007C5ACA /* Release */, 599 | ); 600 | defaultConfigurationIsVisible = 0; 601 | defaultConfigurationName = Release; 602 | }; 603 | 965C34E721109E400081C003 /* Build configuration list for PBXNativeTarget "ExpandableButtonExampleUITests" */ = { 604 | isa = XCConfigurationList; 605 | buildConfigurations = ( 606 | 965C34E521109E400081C003 /* Debug */, 607 | 965C34E621109E400081C003 /* Release */, 608 | ); 609 | defaultConfigurationIsVisible = 0; 610 | defaultConfigurationName = Release; 611 | }; 612 | /* End XCConfigurationList section */ 613 | }; 614 | rootObject = 963F694721021DA9007C5ACA /* Project object */; 615 | } 616 | -------------------------------------------------------------------------------- /ExpandableButtonExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ExpandableButtonExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ExpandableButtonExample 4 | // 5 | // Created by Dima Mishchenko on 20.07.2018. 6 | // Copyright © 2018 Dima. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/delete.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Image.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/delete.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/ExpandableButtonExample/Assets.xcassets/delete.imageset/Image.png -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/edit.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Image.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/edit.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/ExpandableButtonExample/Assets.xcassets/edit.imageset/Image.png -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/like.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Image.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/like.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/ExpandableButtonExample/Assets.xcassets/like.imageset/Image.png -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/photo.imageset/1496710261194222281.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/ExpandableButtonExample/Assets.xcassets/photo.imageset/1496710261194222281.jpg -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/photo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "1496710261194222281.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/share.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Image.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ExpandableButtonExample/Assets.xcassets/share.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimaMishchenko/ExpandableButton/81144b8269f8b31fb86d05a18a85c475a565a28e/ExpandableButtonExample/Assets.xcassets/share.imageset/Image.png -------------------------------------------------------------------------------- /ExpandableButtonExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ExpandableButtonExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /ExpandableButtonExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ExpandableButtonExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ExpandableButtonExample 4 | // 5 | // Created by Dima Mishchenko on 20.07.2018. 6 | // Copyright © 2018 Dima. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | var buttonView: ExpandableButtonView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | view.backgroundColor = .black 18 | 19 | let insets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) 20 | 21 | let items = [ 22 | ExpandableButtonItem( 23 | image: #imageLiteral(resourceName: "delete"), 24 | highlightedImage: #imageLiteral(resourceName: "delete").alpha(0.5), 25 | imageEdgeInsets: insets, 26 | identifier: "delete", 27 | action: {_ in} 28 | ), 29 | ExpandableButtonItem( 30 | image: #imageLiteral(resourceName: "edit"), 31 | highlightedImage: #imageLiteral(resourceName: "edit").alpha(0.5), 32 | imageEdgeInsets: insets, 33 | identifier: "edit", 34 | action: {_ in} 35 | ), 36 | ExpandableButtonItem( 37 | image: #imageLiteral(resourceName: "share"), 38 | highlightedImage: #imageLiteral(resourceName: "share").alpha(0.5), 39 | imageEdgeInsets: insets, 40 | identifier: "share", 41 | action: { _ in} 42 | ), 43 | ExpandableButtonItem( 44 | image: #imageLiteral(resourceName: "like"), 45 | highlightedImage: #imageLiteral(resourceName: "like").alpha(0.5), 46 | imageEdgeInsets: insets, 47 | identifier: "like", 48 | action: { _ in} 49 | ) 50 | ] 51 | 52 | buttonView = ExpandableButtonView(direction: .right, items: items) 53 | buttonView.backgroundColor = .white 54 | buttonView.arrowWidth = 2 55 | buttonView.separatorWidth = 2 56 | buttonView.separatorInset = 12 57 | buttonView.layer.cornerRadius = 30 58 | buttonView.accessibilityIdentifier = "expandableButton" 59 | view.addSubview(buttonView) 60 | setupFrame() 61 | } 62 | 63 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 64 | coordinator.animate(alongsideTransition: { (_) in 65 | self.setupFrame() 66 | }, completion: nil) 67 | } 68 | 69 | private func setupFrame() { 70 | buttonView.frame = CGRect(x: 24, y: UIScreen.main.bounds.size.height - 24 - 80, width: 60, height: 60) 71 | } 72 | } 73 | 74 | extension UIImage { 75 | 76 | func alpha(_ value: CGFloat) -> UIImage { 77 | 78 | UIGraphicsBeginImageContextWithOptions(size, false, scale) 79 | draw(at: CGPoint.zero, blendMode: .normal, alpha: value) 80 | let newImage = UIGraphicsGetImageFromCurrentImageContext() 81 | UIGraphicsEndImageContext() 82 | return newImage! 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /ExpandableButtonExampleTests/ExpandableButtonExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandableButtonExampleTests.swift 3 | // ExpandableButtonExampleTests 4 | // 5 | // Created by Dima Mishchenko on 20.07.2018. 6 | // Copyright © 2018 Dima. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ExpandableButtonExample 11 | 12 | class ExpandableButtonExampleTests: XCTestCase { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /ExpandableButtonExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ExpandableButtonExampleUITests/ExpandableButtonExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandableButtonExampleUITests.swift 3 | // ExpandableButtonExampleUITests 4 | // 5 | // Created by Dima Mishchenko on 31.07.2018. 6 | // Copyright © 2018 Dima. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class ExpandableButtonExampleUITests: XCTestCase { 12 | 13 | var app: XCUIApplication! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | 18 | continueAfterFailure = false 19 | app = XCUIApplication() 20 | } 21 | 22 | func testOpenOnTap() { 23 | 24 | app.launch() 25 | 26 | let button = app.otherElements["expandableButton"].children(matching: .button).element(boundBy: 0) 27 | 28 | XCTAssert(button.exists) 29 | XCTAssert(app.buttons.count == 1) 30 | 31 | button.tap() 32 | 33 | XCTAssert(app.buttons.count == 5) 34 | } 35 | 36 | func testCloseOnTap() { 37 | 38 | app.launch() 39 | 40 | let button = app.otherElements["expandableButton"].children(matching: .button).element(boundBy: 0) 41 | 42 | XCTAssert(app.buttons.count == 1) 43 | 44 | button.tap() 45 | XCTAssert(app.buttons.count != 1) 46 | 47 | 48 | button.tap() 49 | XCTAssert(app.buttons.count == 1) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /ExpandableButtonExampleUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 DimaMishchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExpandableButton 2 | [![Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg?style=flat)](https://developer.apple.com/swift/) 3 | [![CocoaPods compatible](https://img.shields.io/cocoapods/v/ExpandableButton.svg)](https://cocoapods.org/pods/ExpandableButton) 4 | [![Packagist](https://img.shields.io/packagist/l/doctrine/orm.svg)](LICENSE) 5 | 6 | ![](Contents/main.gif) 7 | 8 | ## Requirements 9 | - iOS 9.0+ 10 | 11 | ## Installation 12 | ### [CocoaPods](http://www.cocoapods.org): 13 | - Add the following line to your [`Podfile`](http://guides.cocoapods.org/using/the-podfile.html): 14 | ``` ruby 15 | pod 'ExpandableButton' 16 | 17 | #for swift less than 4.2 use: 18 | pod 'ExpandableButton', '~> 1.0.0' 19 | ``` 20 | - Add `use_frameworks!` to your [`Podfile`](http://guides.cocoapods.org/using/the-podfile.html). 21 | - Run `pod install`. 22 | - Add to files: 23 | ``` swift 24 | import ExpandableButton 25 | ``` 26 | 27 | ## Usage 28 | You can init **ExpandableButton** with `frame` (default is `.zero`), `direction` (default is `.right`) and items (each item will be button). `direction` is opening direction. `items` is `[ExpandableButtonItem]` whiches contain information about future buttons. 29 | Diretions example: 30 | ``` swift 31 | let rightButton = ExpandableButtonView(frame: frame, direction: .right, items: items) 32 | let leftButton = ExpandableButtonView(frame: frame, direction: .left, items: items) 33 | let upButton = ExpandableButtonView(frame: frame, direction: .up, items: items) 34 | let downButton = ExpandableButtonView(frame: frame, direction: .down, items: items) 35 | ``` 36 | ![](Contents/right.gif) 37 | ![](Contents/left.gif) 38 | ![](Contents/up.gif) 39 | ![](Contents/down.gif) 40 | 41 | Items with `image` and `action`: 42 | ``` swift 43 | // create items with images and actions 44 | let items = [ 45 | ExpandableButtonItem( 46 | image: UIImage(named: "delete"), 47 | action: {_ in 48 | print("delete") 49 | } 50 | ), 51 | ExpandableButtonItem( 52 | image: UIImage(named: "edit"), 53 | action: {_ in 54 | print("edit") 55 | } 56 | ), 57 | ExpandableButtonItem( 58 | image: UIImage(named: "share"), 59 | action: { _ in 60 | print("share") 61 | } 62 | ) 63 | ] 64 | 65 | // create ExpandableButton 66 | let buttonView = ExpandableButtonView(items: items) 67 | buttonView.frame = CGRect(x: 0, y: 0, width: 60, height: 60) 68 | buttonView.backgroundColor = .white 69 | view.addSubview(buttonView) 70 | ``` 71 | ![](Contents/example1.gif) 72 | 73 | With `image`, `highlightedImage`, `imageEdgeInsets`: 74 | ``` swift 75 | let insets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) 76 | 77 | // create items with image, highlighted image, image insets. 78 | let items = [ 79 | ExpandableButtonItem( 80 | image: UIImage(named: "delete"), 81 | highlightedImage: UIImage(named: "highlightedDelete"), 82 | imageEdgeInsets: insets, 83 | action: {_ in 84 | print("delete") 85 | } 86 | ) 87 | ... 88 | ] 89 | ``` 90 | ![](Contents/example2.gif) 91 | 92 | `arrowWidth`, `separatorWidth` and `cornerRadius`: 93 | ``` swift 94 | buttonView.arrowWidth = 2 95 | buttonView.separatorWidth = 2 96 | buttonView.layer.cornerRadius = 30 97 | ``` 98 | ![](Contents/example3.gif) 99 | 100 | Custom icons for `open` and `close` actions, `closeOpenImagesInsets`: 101 | ``` swift 102 | // custom open and close images 103 | buttonView.openImage = UIImage(named: "open") 104 | buttonView.closeImage = UIImage(named: "close") 105 | buttonView.closeOpenImagesInsets = insets 106 | ``` 107 | ![](Contents/example4.gif) 108 | 109 | With `attributedTitle`, `highlightedAttributedTitle` and custom item `size`: 110 | ``` swift 111 | // with attributed string, highlighted attributed string, custom size. 112 | let items = [ 113 | ExpandableButtonItem( 114 | attributedTitle: NSAttributedString( 115 | string: "Attributed Text", 116 | attributes: [.foregroundColor: UIColor.red] 117 | ), 118 | highlightedAttributedTitle: NSAttributedString( 119 | string: "Attributed Text", 120 | attributes: [.foregroundColor: UIColor.green] 121 | ), 122 | size: CGSize(width: 160, height: 60) 123 | ) 124 | ] 125 | ``` 126 | ![](Contents/example5.gif) 127 | 128 | With `attributedTitle` under `image` (using `contentEdgeInsets`, `titleEdgeInsets`, `imageEdgeInsets`, `titleAlignment`, `imageContentMode`, `size`): 129 | ``` swift 130 | let attributedTitle = NSAttributedString( 131 | string: "Share", 132 | attributes: [.foregroundColor: UIColor.black, 133 | .font: UIFont.systemFont(ofSize: 12)] 134 | ) 135 | 136 | let highlightedAttributedTitle = NSAttributedString( 137 | string: "Share", 138 | attributes: [.foregroundColor: UIColor.lightGray, 139 | .font: UIFont.systemFont(ofSize: 12)] 140 | ) 141 | 142 | let items = [ 143 | ExpandableButtonItem( 144 | image: UIImage(named: "share"), 145 | highlightedImage: UIImage(named: "haglightedShare"), 146 | attributedTitle: attributedTitle, 147 | highlightedAttributedTitle: highlightedAttributedTitle, 148 | contentEdgeInsets: UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6), 149 | titleEdgeInsets: UIEdgeInsets(top: 24, left: -200, bottom: 0, right: 0), 150 | imageEdgeInsets: UIEdgeInsets(top: 6, left: 0, bottom: 24, right: 0), 151 | size: CGSize(width: 80, height: 60), 152 | titleAlignment: .center, 153 | imageContentMode: .scaleAspectFit 154 | ) 155 | ] 156 | ``` 157 | ![](Contents/example6.gif) 158 | 159 | You can also `open()` and `close()`: 160 | ``` swift 161 | let buttonView = ExpandableButtonView(items: items) 162 | 163 | buttonView.open() 164 | buttonView.close() 165 | ``` 166 | 167 | ## All options 168 | ### [`ExpandableButtonView`](ExpandableButton/ExpandableButtonView.swift) 169 | | Name | Type | Default value | Description | 170 | | :--- | :--- | :--- | :--- | 171 | | `direction` | `Direction` | `.right` | Opening direction. Could be `.left`, `.right`, `.up`, `.down`. Set only on `init(frame:direction:items:)`. 172 | | `state` | `State` | `.closed` | Current state. Could be `.opened`, `.closed` or `.animating`. | 173 | | `animationDuration` | `TimeInterval` | `0.2` | Opening, closing and arrow animation duration. | 174 | | `closeOnAction` | `Bool` | `false` | If `true` call `close()` after any item action. | 175 | | `isHapticFeedback` | `Bool` | `true` | Turn on haptic feedback (Taptic engine) | 176 | | `arrowInsets` | `UIEdgeInsets` | `top: 12 left: 12 bottom: 12 right: 12` | Arrow insets. | 177 | | `arrowWidth` | `CGFloat` | `1` | Arrow line width. | 178 | | `arrowColor` | `UIColor` | `UIColor.black` | Arrow color. | 179 | | `closeOpenImagesInsets` | `UIEdgeInsets` | `.zero` | Insets for custom close and open images. | 180 | | `closeImage` | `UIImage?` | `nil` | Custom close image. | 181 | | `openImage` | `UIImage?` | `nil` | Custom open image. | 182 | | `isSeparatorHidden` | `Bool` | `false` | If `true` hide separator view. | 183 | | `separatorColor` | `UIColor` | `UIColor.black` | Separator color. | 184 | | `separatorInset` | `CGFloat` | `8` | Separator inset from top, bottom for `.left`, `.right` directions and from left, right for `up`, `down`. | 185 | | `separatorWidth` | `CGFloat` | `1` | Separator view width. | 186 | 187 | ### [`ExpandableButtonItem`](ExpandableButton/ExpandableButtonItem.swift) 188 | | Name | Type | Default value | Description | 189 | | :--- | :--- | :--- | :--- | 190 | | `image` | `UIImage?` | `nil` | Image for `.normal` state. | 191 | | `highlightedImage` | `UIImage?` | `nil` | Image for `.highlighted` state. | 192 | | `attributedTitle` | `NSAttributedString?` | `nil` | Attributed string for `.normal` state. | 193 | | `highlightedAttributedTitle` | `NSAttributedString?` | `nil` | Attributed string for `.highlighted` state. | 194 | | `contentEdgeInsets` | `UIEdgeInsets` | `.zero` | `contentEdgeInsets` for `UIButton` | 195 | | `titleEdgeInsets` | `UIEdgeInsets` | `.zero` | `titleEdgeInsets` for `UIButton`. | 196 | | `imageEdgeInsets` | `UIEdgeInsets` | `.zero` | `imageEdgeInsets` for `UIButton`. | 197 | | `size` | `CGSize?` | `nil` | `UIButton` size for current item. If `nil` will be equal to arrow button size. | 198 | | `titleAlignment` | `NSTextAlignment` | `.center` | `titleAlignment` for `titleLabel` in `UIButton`. | 199 | | `imageContentMode` | `UIViewContentMode` | `.scaleAspectFit` | `imageContentMode` for `imageView` in `UIButton`.| 200 | | `action` | `(ExpandableButtonItem) -> Void` | `{_ in}` | Action closure. Calls on `.touchUpInside` | 201 | | `identifier` | `String` | `""` | Identifier for `ExpandableButtonItem`. | 202 | 203 | 204 | You can also use [`ArrowButton`](ExpandableButton/ArrowButton.swift) (button which can drow left, right, up and down arrows using core graphics, just call `showLeftArrow()`, `showRightArrow()`, `showUpArrow()` or `showDownArrow()`) and [`ActionButton`](ExpandableButton/ActionButton.swift) (simple `UIButton` but with `actionBlock` propertie which calls on `.touchUpInside`) in your projects. 205 | 206 | ## License 207 | **ExpandableButton** is under MIT license. See the [LICENSE](LICENSE) file for more info. 208 | --------------------------------------------------------------------------------