forked from shadowfacts/Tusker
183 lines
5.5 KiB
Swift
183 lines
5.5 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 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
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|