Created
December 10, 2020 16:32
-
-
Save SergLam/b6b98e50a43a5c39663f8d2bf732cb24 to your computer and use it in GitHub Desktop.
Hashtags colored view sample implementation using UIStackViews
This file contains 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
import UIKit | |
protocol WordsFlexViewDelegate: class { | |
func didSelectWord(word: String, intId: Int, phraseView: WordsFlexView) | |
} | |
class WordsFlexView: UIView { | |
enum WordsFlexViewType { | |
case selectable | |
case fillable | |
} | |
weak var delegate: WordsFlexViewDelegate? | |
let type: WordsFlexViewType | |
private(set) var words = [String]() | |
private var stackView = UIStackView() | |
private var subStackViews = [UIStackView]() | |
private var buttons = [UIButton]() | |
private var minimumHeight: CGFloat | |
private var maxCountInRaw: Int | |
private var horizontalSpacing: CGFloat | |
private var verticalSpacing: CGFloat | |
private var minimumInsets: UIEdgeInsets | |
private var cornerRadius: CGFloat | |
private var showBorder: Bool | |
private let font = Theme.shared.fonts.settingsRecoveryPhraseWorld | |
private let textColor = Theme.shared.colors.settingsRecoveryPhraseWorldText! | |
private let textBackgroundColor = UIColor.clear | |
private let buttonBorderColor = Theme.shared.colors.settingsRecoveryPhraseWorldBorder! | |
private var heightConstraint: NSLayoutConstraint? | |
init(type: WordsFlexViewType, | |
words: [String], | |
width: CGFloat, | |
minimumHeight: CGFloat = 27, | |
maxCountInRaw: Int = 4, | |
horizontalSpacing: CGFloat = 12, | |
verticalSpacing: CGFloat = 14, | |
minimumInsets: UIEdgeInsets = UIEdgeInsets(top: 6.0, left: 12.0, bottom: 6.0, right: 12), | |
cornerRadius: CGFloat = 5.0, | |
showBorder: Bool = true) { | |
self.type = type | |
self.minimumHeight = minimumHeight | |
self.maxCountInRaw = maxCountInRaw | |
self.horizontalSpacing = horizontalSpacing | |
self.verticalSpacing = verticalSpacing | |
self.minimumInsets = minimumInsets | |
self.cornerRadius = cornerRadius | |
self.showBorder = showBorder | |
self.words = words | |
super.init(frame: .zero) | |
setup(words: words, width: width) | |
} | |
init(type: WordsFlexViewType, | |
minimumHeight: CGFloat, | |
maxCountInRaw: Int = 4, | |
horizontalSpacing: CGFloat = 12, | |
verticalSpacing: CGFloat = 14, | |
minimumInsets: UIEdgeInsets = UIEdgeInsets(top: 6.0, left: 12.0, bottom: 6.0, right: 12), | |
cornerRadius: CGFloat = 5.0, | |
showBorder: Bool = true) { | |
self.type = type | |
self.minimumHeight = minimumHeight | |
self.maxCountInRaw = maxCountInRaw | |
self.horizontalSpacing = horizontalSpacing | |
self.verticalSpacing = verticalSpacing | |
self.minimumInsets = minimumInsets | |
self.cornerRadius = cornerRadius | |
self.showBorder = showBorder | |
super.init(frame: .zero) | |
self.minimumHeight = minimumHeight | |
setupStackView(horizontalStackViews: [UIStackView]()) | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
func addWord(_ word: String, intId: Int) { | |
let newButton = createButton(word: word, intId: intId) | |
buttons.append(newButton) | |
words.append(word) | |
if let lastStackView = findLastFreeSubStackView(for: newButton) { | |
var existingButtons = lastStackView.subviews.filter({ $0 is UIButton }) | |
existingButtons.append(newButton) | |
stackView.removeArrangedSubview(lastStackView) | |
subStackViews.removeAll(where: { $0 == lastStackView }) | |
let newStack = horizontalStackView(with: existingButtons, intrinsicWidth: bounds.width) | |
subStackViews.append(newStack) | |
stackView.addArrangedSubview(newStack) | |
} else { | |
let newStackView = horizontalStackView(with: [newButton], intrinsicWidth: bounds.width) | |
subStackViews.append(newStackView) | |
stackView.addArrangedSubview(newStackView) | |
} | |
heightConstraint?.constant = heightForStackView() | |
} | |
func restore(word: String, intId: Int) { | |
if type != .selectable { return } | |
if let button = buttons.first(where: {$0.tag == intId}) { | |
if button.titleLabel?.text == word { | |
UIView.animate(withDuration: CATransaction.animationDuration()) { | |
button.alpha = 1.0 | |
} | |
} | |
} | |
} | |
@objc private func buttonAction(_ sender: UIButton) { | |
guard let word = sender.titleLabel?.text, let index = buttons.firstIndex(of: sender) else { return } | |
if type == .fillable { | |
words.remove(at: index) | |
buttons.remove(at: index) | |
} | |
delegate?.didSelectWord(word: word, intId: sender.tag, phraseView: self) | |
UIView.animate(withDuration: CATransaction.animationDuration(), animations: { | |
sender.alpha = 0.0 | |
}) { [weak self] (_) in | |
guard let self = self else { return } | |
switch self.type { | |
case .fillable: do { | |
self.stackView.removeFromSuperview() | |
self.stackView = UIStackView() | |
self.subStackViews = self.createSubStackViews(intrinsicWidth: self.bounds.width) | |
self.setupStackView(horizontalStackViews: self.subStackViews) | |
} | |
default: break | |
} | |
} | |
} | |
} | |
// MARK: Private Methods | |
extension WordsFlexView { | |
private func setup(words: [String], width: CGFloat) { | |
buttons = createButtons(words: words) | |
subStackViews = createSubStackViews(intrinsicWidth: width) | |
setupStackView(horizontalStackViews: subStackViews) | |
} | |
private func findLastFreeSubStackView(for button: UIButton) -> UIStackView? { | |
var buttonsCount = 0 | |
var width: CGFloat = 0 | |
subStackViews.last?.subviews.forEach({ | |
if let button = $0 as? UIButton { | |
buttonsCount += 1 | |
width += (button.titleLabel?.intrinsicContentSize.width ?? 0.0) + minimumInsets.left + minimumInsets.right | |
} | |
}) | |
width += CGFloat(buttonsCount - 1) * horizontalSpacing | |
let buttonWidth = (button.titleLabel?.intrinsicContentSize.width ?? 0.0) + minimumInsets.left + minimumInsets.right | |
if buttonsCount < maxCountInRaw && bounds.width > (width + buttonWidth + horizontalSpacing) { | |
return subStackViews.last | |
} else { | |
return nil | |
} | |
} | |
private func createButtons(words: [String]) -> [UIButton] { | |
var buttons = [UIButton]() | |
words.forEach({ | |
buttons.append(createButton(word: $0, intId: UUID().hashValue)) | |
}) | |
return buttons | |
} | |
private func createButton(word: String, intId: Int) -> UIButton { | |
let button = UIButton() | |
button.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside) | |
button.setTitle(word, for: .normal) | |
button.titleLabel?.font = font | |
button.tag = intId | |
button.backgroundColor = textBackgroundColor | |
button.setTitleColor(textColor, for: .normal) | |
button.setTitleColor(textColor.withAlphaComponent(0.5), for: .highlighted) | |
button.layer.cornerRadius = cornerRadius | |
button.layer.borderColor = showBorder ? buttonBorderColor.cgColor : UIColor.clear.cgColor | |
button.layer.borderWidth = 1.0 | |
button.layer.masksToBounds = true | |
let widthConstraint = button.widthAnchor.constraint(equalToConstant: ((button.titleLabel?.intrinsicContentSize.width ?? 0.0) + minimumInsets.left + minimumInsets.right)) | |
widthConstraint.isActive = true | |
widthConstraint.priority = .defaultHigh | |
return button | |
} | |
private func createSubStackViews(intrinsicWidth: CGFloat) -> [UIStackView] { | |
var width: CGFloat = 0.0 | |
var stackViews = [UIStackView]() | |
var currentStack = [UIView]() | |
buttons.forEach({ | |
let buttonWidth = ($0.titleLabel?.intrinsicContentSize.width ?? 0.0) + minimumInsets.left + minimumInsets.right | |
let newWidth = width + buttonWidth | |
if newWidth <= intrinsicWidth { | |
currentStack.append($0) | |
} | |
if currentStack.count == maxCountInRaw { | |
stackViews.append(horizontalStackView(with: currentStack, intrinsicWidth: intrinsicWidth)) | |
currentStack.removeAll() | |
width = 0.0 | |
return | |
} | |
if newWidth > intrinsicWidth { | |
stackViews.append(horizontalStackView(with: currentStack, intrinsicWidth: intrinsicWidth)) | |
currentStack.removeAll() | |
currentStack.append($0) | |
width = 0.0 | |
} | |
if $0 == buttons.last { | |
stackViews.append(horizontalStackView(with: currentStack, intrinsicWidth: intrinsicWidth)) | |
currentStack.removeAll() | |
width = 0.0 | |
} | |
width += (buttonWidth + horizontalSpacing) | |
}) | |
return stackViews | |
} | |
private func horizontalStackView(with views: [UIView], intrinsicWidth: CGFloat) -> UIStackView { | |
let stackView = UIStackView() | |
stackView.axis = .horizontal | |
stackView.distribution = .equalSpacing | |
stackView.spacing = horizontalSpacing | |
views.forEach { | |
stackView.addArrangedSubview($0) | |
} | |
return stackView | |
} | |
private func setupStackView(horizontalStackViews: [UIStackView]) { | |
stackView.axis = .vertical | |
stackView.distribution = .fillEqually | |
stackView.spacing = verticalSpacing | |
stackView.alignment = .leading | |
horizontalStackViews.forEach { | |
stackView.addArrangedSubview($0) | |
} | |
addSubview(stackView) | |
stackView.translatesAutoresizingMaskIntoConstraints = false | |
stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true | |
stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true | |
stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true | |
stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true | |
heightConstraint = stackView.heightAnchor.constraint(equalToConstant: heightForStackView()) | |
heightConstraint?.isActive = true | |
} | |
private func heightForStackView() -> CGFloat { | |
let labelHeight = (buttons.first?.titleLabel?.font.pointSize ?? 0.0) + minimumInsets.top + minimumInsets.bottom | |
let height: CGFloat = CGFloat(subStackViews.count) * labelHeight + CGFloat(subStackViews.count - 1) * stackView.spacing | |
return height < minimumHeight ? minimumHeight : height | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment