// // PollOptionsView.swift // Tusker // // Created by Shadowfacts on 4/26/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class PollOptionsView: UIControl { var checkedOptionIndices: [Int] { options.enumerated().filter { $0.element.checkbox.isChecked }.map(\.offset) } var checkedOptionsChanged: (() -> Void)? private let stack: UIStackView private var options: [PollOptionView] = [] private var poll: Poll! private var animator: UIViewPropertyAnimator! private var currentSelectedOptionIndex: Int? private let animationDuration: TimeInterval = 0.1 private let scaledTransform = CGAffineTransform(scaleX: 0.95, y: 0.95) private let generator = UISelectionFeedbackGenerator() override var isEnabled: Bool { didSet { options.forEach { $0.checkbox.readOnly = !isEnabled } } } override init(frame: CGRect) { stack = UIStackView() super.init(frame: frame) stack.translatesAutoresizingMaskIntoConstraints = false stack.axis = .vertical stack.spacing = 4 addSubview(stack) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: leadingAnchor), stack.trailingAnchor.constraint(equalTo: trailingAnchor), stack.topAnchor.constraint(equalTo: topAnchor), stack.bottomAnchor.constraint(equalTo: bottomAnchor), ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func updateUI(poll: Poll) { self.poll = poll options.forEach { $0.removeFromSuperview() } options = poll.options.enumerated().map { (index, opt) in let optionView = PollOptionView(poll: poll, option: opt) optionView.checkbox.readOnly = !isEnabled optionView.checkbox.isChecked = poll.ownVotes?.contains(index) ?? false optionView.checkbox.voted = poll.voted ?? false stack.addArrangedSubview(optionView) return optionView } accessibilityElements = options } private func selectOption(_ option: PollOptionView) { if poll.multiple { option.checkbox.isChecked.toggle() } else { for opt in options { if opt === option { opt.checkbox.isChecked = true } else { opt.checkbox.isChecked = false } } } checkedOptionsChanged?() } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { // don't let subviews receive touch events return self } // MARK: - UIControl override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { guard isEnabled else { return false } for (index, view) in options.enumerated() { if view.point(inside: touch.location(in: view), with: event) { currentSelectedOptionIndex = index animator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeInOut) { view.transform = self.scaledTransform } animator.startAnimation() generator.selectionChanged() generator.prepare() return true } } return false } override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { var newIndex: Int? = nil for (index, view) in options.enumerated() { if view.point(inside: touch.location(in: view), with: event) { newIndex = index break } } if newIndex != currentSelectedOptionIndex { currentSelectedOptionIndex = newIndex UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseInOut) { for (index, view) in self.options.enumerated() { view.transform = index == newIndex ? self.scaledTransform : .identity } } if newIndex != nil { generator.selectionChanged() generator.prepare() } } return true } override func endTracking(_ touch: UITouch?, with event: UIEvent?) { super.endTracking(touch, with: event) func selectOption() { guard let index = currentSelectedOptionIndex else { return } let option = options[index] animator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeInOut) { option.transform = .identity self.selectOption(option) } animator.startAnimation() } if animator.isRunning { animator.addCompletion { (_) in selectOption() } } else { selectOption() } } override func cancelTracking(with event: UIEvent?) { super.cancelTracking(with: event) UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseInOut) { for view in self.options { view.transform = .identity } } } }