Tusker/Tusker/Views/Poll/PollOptionsView.swift

219 lines
6.8 KiB
Swift

//
// 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
}
}
}
}