2021-04-28 23:00:17 +00:00
|
|
|
//
|
|
|
|
// PollOptionsView.swift
|
|
|
|
// Tusker
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 4/26/21.
|
|
|
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
import Pachyderm
|
|
|
|
|
|
|
|
class PollOptionsView: UIControl {
|
2023-02-19 23:21:20 +00:00
|
|
|
|
|
|
|
var mastodonController: MastodonController!
|
2021-04-28 23:00:17 +00:00
|
|
|
|
|
|
|
var checkedOptionIndices: [Int] {
|
2023-05-28 19:19:45 +00:00
|
|
|
options.enumerated().filter { $0.element.checkboxIfInitialized?.isChecked == true }.map(\.offset)
|
2021-04-28 23:00:17 +00:00
|
|
|
}
|
|
|
|
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)
|
|
|
|
|
2023-01-27 02:28:56 +00:00
|
|
|
private let generator = UISelectionFeedbackGenerator()
|
2021-05-05 21:46:41 +00:00
|
|
|
|
2021-04-28 23:00:17 +00:00
|
|
|
override var isEnabled: Bool {
|
|
|
|
didSet {
|
2023-05-28 19:19:45 +00:00
|
|
|
options.forEach { $0.checkboxIfInitialized?.readOnly = !isEnabled }
|
2021-04-28 23:00:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-28 19:19:45 +00:00
|
|
|
override var accessibilityElements: [Any]? {
|
|
|
|
get { options }
|
|
|
|
set {}
|
|
|
|
}
|
|
|
|
|
2021-04-28 23:00:17 +00:00
|
|
|
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
|
|
|
|
|
2023-05-28 19:19:45 +00:00
|
|
|
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()
|
2023-05-13 18:14:38 +00:00
|
|
|
}
|
2021-04-28 23:00:17 +00:00
|
|
|
}
|
2021-06-07 01:50:45 +00:00
|
|
|
|
2023-05-28 19:19:45 +00:00
|
|
|
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
|
|
|
|
}
|
2021-04-28 23:00:17 +00:00
|
|
|
}
|
|
|
|
|
2023-05-13 19:00:03 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-04-28 23:00:17 +00:00
|
|
|
private func selectOption(_ option: PollOptionView) {
|
|
|
|
if poll.multiple {
|
2023-05-28 19:19:45 +00:00
|
|
|
option.checkboxIfInitialized?.isChecked.toggle()
|
2021-04-28 23:00:17 +00:00
|
|
|
} else {
|
|
|
|
for opt in options {
|
|
|
|
if opt === option {
|
2023-05-28 19:19:45 +00:00
|
|
|
opt.checkboxIfInitialized?.isChecked = true
|
2021-04-28 23:00:17 +00:00
|
|
|
} else {
|
2023-05-28 19:19:45 +00:00
|
|
|
opt.checkboxIfInitialized?.isChecked = false
|
2021-04-28 23:00:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
checkedOptionsChanged?()
|
|
|
|
}
|
|
|
|
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
|
|
// don't let subviews receive touch events
|
2023-05-27 21:59:26 +00:00
|
|
|
if isEnabled {
|
|
|
|
return self
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
2021-04-28 23:00:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
|
2023-01-27 02:28:56 +00:00
|
|
|
generator.selectionChanged()
|
2021-05-05 21:46:41 +00:00
|
|
|
generator.prepare()
|
|
|
|
|
2021-04-28 23:00:17 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
2023-02-15 23:57:00 +00:00
|
|
|
let location = touch.location(in: self)
|
2021-04-28 23:00:17 +00:00
|
|
|
var newIndex: Int? = nil
|
|
|
|
for (index, view) in options.enumerated() {
|
2023-02-15 23:57:00 +00:00
|
|
|
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) {
|
2021-04-28 23:00:17 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2021-05-05 21:46:41 +00:00
|
|
|
|
|
|
|
if newIndex != nil {
|
2023-01-27 02:28:56 +00:00
|
|
|
generator.selectionChanged()
|
2021-05-05 21:46:41 +00:00
|
|
|
generator.prepare()
|
|
|
|
}
|
2021-04-28 23:00:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|