// // PollOptionsView.swift // Tusker // // Created by Shadowfacts on 4/26/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import UIKit import Pachyderm class PollOptionsView: UIControl { var mastodonController: MastodonController! var checkedOptionIndices: [Int] { options.enumerated().filter { $0.element.checkboxIfInitialized?.isChecked == true }.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) #if !os(visionOS) private let generator = UISelectionFeedbackGenerator() #endif override var isEnabled: Bool { didSet { options.forEach { $0.checkboxIfInitialized?.readOnly = !isEnabled } } } override var accessibilityElements: [Any]? { get { options } set {} } 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 if poll.options.count > options.count { for _ in 0..<(poll.options.count - options.count) { let optView = PollOptionView() options.append(optView) stack.addArrangedSubview(optView) } } else if poll.options.count < options.count { for _ in 0..<(options.count - poll.options.count) { options.removeLast().removeFromSuperview() } } for (index, (view, opt)) in zip(options, poll.options).enumerated() { view.updateUI(poll: poll, option: opt, ownVoted: poll.ownVotes?.contains(index) ?? false, mastodonController: mastodonController) view.checkboxIfInitialized?.readOnly = !isEnabled } } func estimateHeight(effectiveWidth: CGFloat) -> CGFloat { var height: CGFloat = 0 height += CGFloat(options.count - 1) * stack.spacing for option in options { // this isn't the actual width, but it's close enough for the estimate height += option.label.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height } return height } private func selectOption(_ option: PollOptionView) { if poll.multiple { option.checkboxIfInitialized?.isChecked.toggle() } else { for opt in options { if opt === option { opt.checkboxIfInitialized?.isChecked = true } else { opt.checkboxIfInitialized?.isChecked = false } } } checkedOptionsChanged?() } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { // don't let subviews receive touch events if isEnabled { return self } else { return nil } } // 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() #if !os(visionOS) generator.selectionChanged() generator.prepare() #endif return true } } return false } override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { let location = touch.location(in: self) var newIndex: Int? = nil for (index, view) in options.enumerated() { var frame = view.frame if index != options.count - 1 { frame = frame.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: -stack.spacing, right: 0)) } if frame.contains(location) { 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 !os(visionOS) if newIndex != nil { generator.selectionChanged() generator.prepare() } #endif } 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 } } } }