Instantly share code, notes, and snippets.
Created
December 18, 2017 14:15
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save blwinters/1c4df9ff38a59bb92bd0453c2db7e4f2 to your computer and use it in GitHub Desktop.
A protocol that creates a grid of multi-selectable buttons with contents specified by the delegate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ButtonGridDisplayable.swift | |
// Summit_iOS | |
// | |
// Created by Ben Winters on 9/26/17. | |
// Copyright © 2017 Goals LLC. All rights reserved. | |
// | |
import UIKit | |
@objc protocol ButtonGridDisplayable { | |
var hSeparatorContainerStack: UIStackView! { get } | |
var vSeparatorContainerStack: UIStackView! { get } | |
var buttonContainerStack: UIStackView! { get } | |
var columnCount: Int { get } | |
var separatorThickness: CGFloat { get } | |
var selectedColor: UIColor { get } | |
var separatorColor: UIColor { get } | |
var buttonValues: [Int] { get } | |
var selectedValues: Set<Int> { get } | |
func titleForButtonValue(_ value: Int) -> String? | |
@objc func didTapButton(sender: UIButton) | |
} | |
extension ButtonGridDisplayable { | |
var rowCount: Int { | |
var result = Int(buttonValues.count / columnCount) | |
if buttonValues.count % columnCount != 0 { | |
result += 1 | |
} | |
return result | |
} | |
var buttonRowStacks: [UIStackView] { | |
return buttonContainerStack.arrangedSubviews.flatMap({$0 as? UIStackView}) | |
} | |
var hSeparatorRowStacks: [UIStackView] { | |
return hSeparatorContainerStack.arrangedSubviews.flatMap({$0 as? UIStackView}) | |
} | |
var vSeparatorRowStacks: [UIStackView] { | |
return vSeparatorContainerStack.arrangedSubviews.flatMap({$0 as? UIStackView}) | |
} | |
private func gridIndexPaths() -> [IndexPath] { | |
var paths: [IndexPath] = [] | |
for section in 0..<rowCount { | |
for item in 0..<columnCount { | |
paths.append(IndexPath(item: item, section: section)) | |
} | |
} | |
return paths | |
} | |
private func buttonValue(for indexPath: IndexPath) -> Int? { | |
let sectionOffset = indexPath.section * columnCount | |
let valueIndex = sectionOffset + indexPath.item //e.g. IndexPath(item: 1, section: 2) with 4 columns = 9 | |
guard valueIndex < buttonValues.count else { return nil } | |
return buttonValues[valueIndex] | |
} | |
private func hSeparatorIndexPaths(for buttonPath: IndexPath) -> [IndexPath] { | |
return [IndexPath(item: buttonPath.item, section: buttonPath.section), | |
IndexPath(item: buttonPath.item, section: buttonPath.section + 1), | |
] | |
} | |
private func vSeparatorIndexPaths(for buttonPath: IndexPath) -> [IndexPath] { | |
return [IndexPath(item: buttonPath.item, section: buttonPath.section), | |
IndexPath(item: buttonPath.item + 1, section: buttonPath.section), | |
] | |
} | |
private var monthButtonsBySection: [[UIButton]] { | |
return buttonRowStacks.map({ stack in | |
return stack.arrangedSubviews.flatMap({$0 as? UIButton}) | |
}) | |
} | |
func didSetSelectedValues() { | |
for row in buttonRowStacks { | |
for button in row.arrangedSubviews.flatMap({$0 as? UIButton}) { | |
button.isSelected = selectedValues.contains(button.tag) | |
} | |
} | |
updateSeparators(for: selectedValues) | |
} | |
///Each horizontal stack corresponds to a section and the index within that stack matches the IndexPath.item | |
func indexPath(forValue value: Int) -> IndexPath { | |
let section = Int(value / columnCount) //truncates decimals | |
let column = Int(value % columnCount) - 1 | |
return IndexPath(item: column, section: section) | |
} | |
func setupStackViews() { | |
buttonContainerStack.axis = .vertical | |
buttonContainerStack.distribution = .fillEqually | |
buttonContainerStack.alignment = .fill | |
buttonContainerStack.spacing = 0 | |
buttonContainerStack.translatesAutoresizingMaskIntoConstraints = false | |
vSeparatorContainerStack.axis = .vertical | |
vSeparatorContainerStack.distribution = .fillEqually //child horizontal stacks fill the vertical space | |
vSeparatorContainerStack.alignment = .fill //left and right margins of child stacks | |
vSeparatorContainerStack.spacing = 0 | |
vSeparatorContainerStack.translatesAutoresizingMaskIntoConstraints = false | |
hSeparatorContainerStack.axis = .vertical | |
hSeparatorContainerStack.distribution = .equalSpacing //child horizontal stacks are 0.5pts tall and spaced equally vertically | |
hSeparatorContainerStack.alignment = .fill //left and right margins of child stacks | |
hSeparatorContainerStack.translatesAutoresizingMaskIntoConstraints = false | |
for _ in 0..<rowCount { | |
let buttonStack = UIStackView() | |
buttonStack.axis = .horizontal | |
buttonStack.distribution = .fillEqually | |
buttonStack.alignment = .fill | |
buttonStack.spacing = 0 | |
buttonStack.translatesAutoresizingMaskIntoConstraints = false | |
buttonContainerStack.addArrangedSubview(buttonStack) | |
let vSeparatorStack = UIStackView() | |
vSeparatorStack.axis = .horizontal | |
vSeparatorStack.distribution = .equalSpacing | |
vSeparatorStack.alignment = .fill | |
vSeparatorStack.translatesAutoresizingMaskIntoConstraints = false | |
vSeparatorContainerStack.addArrangedSubview(vSeparatorStack) | |
} | |
for _ in 0...rowCount { | |
let hSeparatorStack = UIStackView() | |
hSeparatorStack.axis = .horizontal | |
hSeparatorStack.distribution = .fillEqually | |
hSeparatorStack.alignment = .fill | |
hSeparatorStack.translatesAutoresizingMaskIntoConstraints = false | |
hSeparatorStack.heightAnchor.constraint(equalToConstant: separatorThickness).isActive = true | |
hSeparatorContainerStack.addArrangedSubview(hSeparatorStack) | |
} | |
} | |
func setupButtons() { | |
for stack in buttonRowStacks { | |
stack.removeAllSubviews() | |
} | |
let selectedBackgroundImage = UIImage.image(withColor: selectedColor) | |
for path in gridIndexPaths() { | |
if let bValue = buttonValue(for: path) { | |
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) //arbitrary frame | |
if let title = titleForButtonValue(bValue) { | |
let normalTitle = NSMutableAttributedString().regular(title, size: 17, color: Style.cellDarkText) | |
let selectedTitle = NSMutableAttributedString().medium(title, size: 17, color: Style.textOnPrimaryFill) | |
button.setAttributedTitle(normalTitle, for: .normal) | |
button.setAttributedTitle(selectedTitle, for: .selected) | |
} | |
button.tag = bValue | |
button.setBackgroundImage(selectedBackgroundImage, for: .selected) | |
button.addTarget(self, action: #selector(self.didTapButton(sender:)), for: .touchUpInside) | |
button.translatesAutoresizingMaskIntoConstraints = false | |
let stack = buttonRowStacks[path.section] | |
stack.addArrangedSubview(button) | |
} else { | |
//values run out before last row is completed, need to insert invisible buttons for spacing | |
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) //arbitrary frame | |
button.backgroundColor = UIColor.clear | |
button.isUserInteractionEnabled = false | |
button.translatesAutoresizingMaskIntoConstraints = false | |
let stack = buttonRowStacks[path.section] | |
stack.addArrangedSubview(button) | |
} | |
} | |
} | |
func addSeparators() { | |
//vertical separators | |
for vSeparatorStack in vSeparatorRowStacks { | |
for _ in 0...columnCount { | |
let separator = UIView() | |
separator.backgroundColor = separatorColor | |
separator.translatesAutoresizingMaskIntoConstraints = false | |
separator.widthAnchor.constraint(equalToConstant: separatorThickness).isActive = true | |
vSeparatorStack.addArrangedSubview(separator) | |
} | |
} | |
//horizontal separators | |
for hSeparatorStack in hSeparatorRowStacks { | |
for _ in 0..<columnCount { | |
let separator = UIView() | |
separator.backgroundColor = separatorColor | |
separator.translatesAutoresizingMaskIntoConstraints = false | |
//The stack itself has a height constraint | |
hSeparatorStack.addArrangedSubview(separator) | |
} | |
} | |
} | |
func updateSeparators(for selectedValues: Set<Int>) { | |
var selectedButtonPaths: Set<IndexPath> = [] | |
selectedValues.forEach({ | |
selectedButtonPaths.insert(indexPath(forValue: $0)) | |
}) | |
//Get index paths for horizontal separators that touch the selected buttons | |
var selectedHSeparatorPaths: Set<IndexPath> = [] | |
selectedButtonPaths.forEach({ buttonPath in | |
hSeparatorIndexPaths(for: buttonPath).forEach({ hSepPath in | |
selectedHSeparatorPaths.insert(hSepPath) | |
}) | |
}) | |
//Get index paths for vertical separators that touch the selected buttons | |
var selectedVSeparatorPaths: Set<IndexPath> = [] | |
selectedButtonPaths.forEach({ buttonPath in | |
vSeparatorIndexPaths(for: buttonPath).forEach({ vSepPath in | |
selectedVSeparatorPaths.insert(vSepPath) | |
}) | |
}) | |
var hiddenVSeparatorPaths: Set<IndexPath> = [] | |
gridIndexPaths().forEach({ buttonPath in | |
if buttonValue(for: buttonPath) == nil { //buttonValues.count < rowCount * columnCount | |
//Only hide the trailing separator | |
let hiddenSepPath = IndexPath(item: buttonPath.item + 1, section: buttonPath.section) | |
hiddenVSeparatorPaths.insert(hiddenSepPath) | |
} | |
}) | |
//Apply the selected horizontal separator paths to update the separator background color | |
for (stackIndex, stack) in hSeparatorRowStacks.enumerated() { | |
for (separatorIndex, separator) in stack.arrangedSubviews.enumerated() { | |
let separatorPath = IndexPath(item: separatorIndex, section: stackIndex) | |
let isTopOrBottom = (stackIndex == 0 || stackIndex == rowCount) //hide separators that can overlap with cell separators to cause double thick lines | |
if isTopOrBottom || selectedHSeparatorPaths.contains(separatorPath) { | |
separator.backgroundColor = UIColor.clear | |
} else { | |
separator.backgroundColor = separatorColor | |
} | |
} | |
} | |
//Apply the selected vertical separator paths to update the separator background color | |
for (stackIndex, stack) in vSeparatorRowStacks.enumerated() { | |
for (separatorIndex, separator) in stack.arrangedSubviews.enumerated() { | |
let separatorPath = IndexPath(item: separatorIndex, section: stackIndex) | |
let shouldHidePath = hiddenVSeparatorPaths.contains(separatorPath) || selectedVSeparatorPaths.contains(separatorPath) | |
separator.backgroundColor = shouldHidePath ? UIColor.clear : separatorColor | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment