Compare commits

..

No commits in common. "dcc5f7f71624617151f83bfdc0b7c510eac9f433" and "3eceffbb6b57de0ebedf2f399b3827e6f44b6295" have entirely different histories.

8 changed files with 46 additions and 219 deletions

View File

@ -152,21 +152,14 @@ extension GalleryViewController: GalleryItemViewControllerDelegate {
extension GalleryViewController: UIViewControllerTransitioningDelegate { extension GalleryViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
#if os(visionOS)
return nil
#else
if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: initialItemIndex) { if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: initialItemIndex) {
return GalleryPresentationAnimationController(sourceView: sourceView) return GalleryPresentationAnimationController(sourceView: sourceView)
} else { } else {
return nil return nil
} }
#endif
} }
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
#if os(visionOS)
return nil
#else
if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: currentItemViewController.itemIndex) { if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: currentItemViewController.itemIndex) {
let translation: CGPoint? let translation: CGPoint?
let velocity: CGPoint? let velocity: CGPoint?
@ -182,6 +175,5 @@ extension GalleryViewController: UIViewControllerTransitioningDelegate {
} else { } else {
return nil return nil
} }
#endif
} }
} }

View File

@ -204,19 +204,14 @@ class FastAccountSwitcherViewController: UIViewController {
@objc private func handleLongPress(_ recognizer: UIGestureRecognizer) { @objc private func handleLongPress(_ recognizer: UIGestureRecognizer) {
switch recognizer.state { switch recognizer.state {
case .began: case .began:
show()
#if !os(visionOS) #if !os(visionOS)
if #available(iOS 17.5, *) {
UIImpactFeedbackGenerator(style: .heavy, view: view).impactOccurred(at: CGPoint(x: view.bounds.midX, y: view.bounds.midY))
selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator(view: view)
} else {
UIImpactFeedbackGenerator(style: .heavy).impactOccurred() UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator() selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator()
}
selectionChangedFeedbackGenerator?.prepare() selectionChangedFeedbackGenerator?.prepare()
#endif #endif
show()
case .changed: case .changed:
let location = recognizer.location(in: view) let location = recognizer.location(in: view)
@ -265,11 +260,7 @@ class FastAccountSwitcherViewController: UIViewController {
#if !os(visionOS) #if !os(visionOS)
if hapticFeedback { if hapticFeedback {
if #available(iOS 17.5, *) {
selectionChangedFeedbackGenerator?.selectionChanged(at: location)
} else {
selectionChangedFeedbackGenerator?.selectionChanged() selectionChangedFeedbackGenerator?.selectionChanged()
}
selectionChangedFeedbackGenerator?.prepare() selectionChangedFeedbackGenerator?.prepare()
} }
#endif #endif

View File

@ -270,13 +270,8 @@ private class VideoScrubbingControl: UIControl {
} }
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
if touch.type == .pencil || touch.type == .indirectPointer {
touchStartLocation = .zero
scrubbingStartFraction = 0
} else {
touchStartLocation = touch.location(in: self) touchStartLocation = touch.location(in: self)
scrubbingStartFraction = fractionComplete scrubbingStartFraction = fractionComplete
}
animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear)
animator!.addAnimations { animator!.addAnimations {
@ -287,28 +282,17 @@ private class VideoScrubbingControl: UIControl {
sendActions(for: .editingDidBegin) sendActions(for: .editingDidBegin)
#if !os(visionOS) #if !os(visionOS)
if #available(iOS 17.5, *) {
feedbackGenerator = UIImpactFeedbackGenerator(style: .light, view: self)
} else {
feedbackGenerator = UIImpactFeedbackGenerator(style: .light) feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
}
feedbackGenerator!.prepare() feedbackGenerator!.prepare()
#endif #endif
updateScrubbing(for: touch)
return true return true
} }
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
updateScrubbing(for: touch)
return true
}
private func updateScrubbing(for touch: UITouch) {
guard let touchStartLocation, guard let touchStartLocation,
let scrubbingStartFraction else { let scrubbingStartFraction else {
return return false
} }
let location = touch.location(in: self) let location = touch.location(in: self)
let translation = CGPoint(x: location.x - touchStartLocation.x, y: location.y - touchStartLocation.y) let translation = CGPoint(x: location.x - touchStartLocation.x, y: location.y - touchStartLocation.y)
@ -317,12 +301,8 @@ private class VideoScrubbingControl: UIControl {
let newFractionComplete = max(0, min(1, unclampedFractionComplete)) let newFractionComplete = max(0, min(1, unclampedFractionComplete))
#if !os(visionOS) #if !os(visionOS)
if newFractionComplete != fractionComplete && (newFractionComplete == 0 || newFractionComplete == 1) { if newFractionComplete != fractionComplete && (newFractionComplete == 0 || newFractionComplete == 1) {
if #available(iOS 17.5, *) {
feedbackGenerator!.impactOccurred(intensity: 0.5, at: location)
} else {
feedbackGenerator!.impactOccurred(intensity: 0.5) feedbackGenerator!.impactOccurred(intensity: 0.5)
} }
}
#endif #endif
fractionComplete = newFractionComplete fractionComplete = newFractionComplete
sendActions(for: .editingChanged) sendActions(for: .editingChanged)
@ -338,6 +318,8 @@ private class VideoScrubbingControl: UIControl {
transform = CGAffineTransform(scaleX: 1 + stretchAmount / bounds.width, y: 1 + 0.5 * (1 - stretchFactor)) transform = CGAffineTransform(scaleX: 1 + stretchAmount / bounds.width, y: 1 + 0.5 * (1 - stretchFactor))
.translatedBy(x: sign(unclampedFractionComplete) * stretchAmount / 2, y: 0) .translatedBy(x: sign(unclampedFractionComplete) * stretchAmount / 2, y: 0)
} }
return true
} }
override func endTracking(_ touch: UITouch?, with event: UIEvent?) { override func endTracking(_ touch: UITouch?, with event: UIEvent?) {

View File

@ -150,16 +150,9 @@ class CustomAlertActionsView: UIControl {
private var separatorSizeConstraints: [NSLayoutConstraint] = [] private var separatorSizeConstraints: [NSLayoutConstraint] = []
#if !os(visionOS) #if !os(visionOS)
private lazy var generator: UISelectionFeedbackGenerator = { private let generator = UISelectionFeedbackGenerator()
if #available(iOS 17.5, *) {
UISelectionFeedbackGenerator(view: self)
} else {
UISelectionFeedbackGenerator()
}
}()
#endif #endif
private var currentSelectedActionIndex: Int? private var currentSelectedActionIndex: Int?
private var showPressedMenuWorkItem: DispatchWorkItem?
init(config: CustomAlertController.Configuration, dismiss: @escaping () -> Void) { init(config: CustomAlertController.Configuration, dismiss: @escaping () -> Void) {
self.dismiss = dismiss self.dismiss = dismiss
@ -320,42 +313,13 @@ class CustomAlertActionsView: UIControl {
actionButtons[currentSelectedActionIndex].backgroundColor = nil actionButtons[currentSelectedActionIndex].backgroundColor = nil
} }
#if !os(visionOS) #if !os(visionOS)
if #available(iOS 17.5, *) {
let view = selectedButton!.element
let location = convert(CGPoint(x: view.bounds.midX, y: view.bounds.midY), from: view)
generator.selectionChanged(at: location)
} else {
generator.selectionChanged() generator.selectionChanged()
}
#endif #endif
if let showPressedMenuWorkItem {
showPressedMenuWorkItem.cancel()
self.showPressedMenuWorkItem = nil
}
} }
currentSelectedActionIndex = selectedButton?.offset currentSelectedActionIndex = selectedButton?.offset
selectedButton?.element.backgroundColor = .secondarySystemFill selectedButton?.element.backgroundColor = .secondarySystemFill
if let currentSelectedActionIndex,
case .menu(_) = reorderedActions[currentSelectedActionIndex].style,
case let button = actionButtons[currentSelectedActionIndex],
let interaction = button.contextMenuInteraction,
showPressedMenuWorkItem == nil {
showPressedMenuWorkItem = DispatchWorkItem {
if #available(iOS 17.4, *) {
button.performPrimaryAction()
} else {
let selector = NSSelectorFromString(["Location:", "At", "Menu", "present", "_"].reversed().joined())
if interaction.responds(to: selector) {
interaction.perform(selector, with: recognizer.location(in: button))
}
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250), execute: showPressedMenuWorkItem!)
}
#if !os(visionOS) #if !os(visionOS)
generator.prepare() generator.prepare()
#endif #endif

View File

@ -14,7 +14,6 @@ class PollOptionView: UIView {
private static let minHeight: CGFloat = 35 private static let minHeight: CGFloat = 35
private static let cornerRadius = 0.1 * minHeight private static let cornerRadius = 0.1 * minHeight
private static let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25) private static let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
private static let hoveredBackgroundColor = UIColor(white: 0.35, alpha: 0.25)
private(set) var label: EmojiLabel! private(set) var label: EmojiLabel!
@Lazy private var checkbox: PollOptionCheckboxView = PollOptionCheckboxView().configure { @Lazy private var checkbox: PollOptionCheckboxView = PollOptionCheckboxView().configure {
@ -34,12 +33,6 @@ class PollOptionView: UIView {
private var labelLeadingToSelfConstraint: NSLayoutConstraint! private var labelLeadingToSelfConstraint: NSLayoutConstraint!
private var fillViewWidthConstraint: NSLayoutConstraint? private var fillViewWidthConstraint: NSLayoutConstraint?
var hovered: Bool = false {
didSet {
backgroundColor = hovered ? PollOptionView.hoveredBackgroundColor : PollOptionView.unselectedBackgroundColor
}
}
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)

View File

@ -24,19 +24,12 @@ class PollOptionsView: UIControl {
private var poll: Poll! private var poll: Poll!
private var animator: UIViewPropertyAnimator! private var animator: UIViewPropertyAnimator!
private var currentSelectedOptionIndex: Int? private var currentSelectedOptionIndex: Int?
private var currentHoveredOptionIndex: Int?
static let animationDuration: TimeInterval = 0.1 private let animationDuration: TimeInterval = 0.1
static let scaledTransform = CGAffineTransform(scaleX: 0.95, y: 0.95) private let scaledTransform = CGAffineTransform(scaleX: 0.95, y: 0.95)
#if !os(visionOS) #if !os(visionOS)
private lazy var generator: UISelectionFeedbackGenerator = { private let generator = UISelectionFeedbackGenerator()
if #available(iOS 17.5, *) {
UISelectionFeedbackGenerator(view: self)
} else {
UISelectionFeedbackGenerator()
}
}()
#endif #endif
override var isEnabled: Bool { override var isEnabled: Bool {
@ -66,8 +59,6 @@ class PollOptionsView: UIControl {
stack.topAnchor.constraint(equalTo: topAnchor), stack.topAnchor.constraint(equalTo: topAnchor),
stack.bottomAnchor.constraint(equalTo: bottomAnchor), stack.bottomAnchor.constraint(equalTo: bottomAnchor),
]) ])
addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -130,20 +121,6 @@ class PollOptionsView: UIControl {
} }
} }
private func optionView(at point: CGPoint) -> (PollOptionView, Int)? {
for (index, view) in options.enumerated() {
// don't use view.frame because it changes when a transform is applied
var frame = CGRect(x: 0, y: view.center.y - view.bounds.height / 2, width: view.bounds.width, height: view.bounds.height)
if index != options.count - 1 {
frame = frame.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: -stack.spacing, right: 0))
}
if frame.contains(point) {
return (view, index)
}
}
return nil
}
// MARK: - UIControl // MARK: - UIControl
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
@ -155,21 +132,13 @@ class PollOptionsView: UIControl {
if view.point(inside: touch.location(in: view), with: event) { if view.point(inside: touch.location(in: view), with: event) {
currentSelectedOptionIndex = index currentSelectedOptionIndex = index
if animator?.isRunning == true { animator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeInOut) {
animator.stopAnimation(true) view.transform = self.scaledTransform
}
animator = UIViewPropertyAnimator(duration: Self.animationDuration, curve: .easeInOut) {
view.transform = Self.scaledTransform
view.hovered = true
} }
animator.startAnimation() animator.startAnimation()
#if !os(visionOS) #if !os(visionOS)
if #available(iOS 17.5, *) {
generator.selectionChanged(at: view.center)
} else {
generator.selectionChanged() generator.selectionChanged()
}
generator.prepare() generator.prepare()
#endif #endif
@ -182,31 +151,30 @@ class PollOptionsView: UIControl {
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self) let location = touch.location(in: self)
let newIndexAndOption = optionView(at: location) var newIndex: Int? = nil
let newIndex = newIndexAndOption?.1 for (index, view) in options.enumerated() {
let option = newIndexAndOption?.0 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 { if newIndex != currentSelectedOptionIndex {
currentSelectedOptionIndex = newIndex currentSelectedOptionIndex = newIndex
if animator.isRunning { UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseInOut) {
animator.stopAnimation(true)
}
animator = UIViewPropertyAnimator(duration: Self.animationDuration, curve: .easeInOut) {
for (index, view) in self.options.enumerated() { for (index, view) in self.options.enumerated() {
view.transform = index == newIndex ? Self.scaledTransform : .identity view.transform = index == newIndex ? self.scaledTransform : .identity
view.hovered = index == newIndex
} }
} }
animator.startAnimation()
#if !os(visionOS) #if !os(visionOS)
if let option { if newIndex != nil {
if #available(iOS 17.5, *) {
generator.selectionChanged(at: option.center)
} else {
generator.selectionChanged() generator.selectionChanged()
}
generator.prepare() generator.prepare()
} }
#endif #endif
@ -221,15 +189,14 @@ class PollOptionsView: UIControl {
func selectOption() { func selectOption() {
guard let index = currentSelectedOptionIndex else { return } guard let index = currentSelectedOptionIndex else { return }
let option = options[index] let option = options[index]
animator = UIViewPropertyAnimator(duration: Self.animationDuration, curve: .easeInOut) { animator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeInOut) {
option.transform = .identity option.transform = .identity
option.hovered = false
self.selectOption(option) self.selectOption(option)
} }
animator.startAnimation() animator.startAnimation()
} }
if animator?.isRunning == true { if animator.isRunning {
animator.addCompletion { (_) in animator.addCompletion { (_) in
selectOption() selectOption()
} }
@ -240,52 +207,12 @@ class PollOptionsView: UIControl {
override func cancelTracking(with event: UIEvent?) { override func cancelTracking(with event: UIEvent?) {
super.cancelTracking(with: event) super.cancelTracking(with: event)
if animator?.isRunning == true { UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseInOut) {
animator.stopAnimation(true)
}
animator = UIViewPropertyAnimator(duration: Self.animationDuration, curve: .easeInOut) {
for view in self.options { for view in self.options {
view.transform = .identity view.transform = .identity
view.hovered = false
} }
} }
animator.startAnimation()
}
@objc private func hoverRecognized(_ recognizer: UIHoverGestureRecognizer) {
guard let (option, index) = optionView(at: recognizer.location(in: self)) else {
return
}
switch recognizer.state {
case .began, .changed:
if index != currentHoveredOptionIndex {
let oldIndex = currentHoveredOptionIndex
currentHoveredOptionIndex = index
if animator?.isRunning == true {
animator.stopAnimation(true)
}
animator = UIViewPropertyAnimator(duration: Self.animationDuration, curve: .easeInOut) {
option.hovered = true
if let oldIndex {
self.options[oldIndex].hovered = false
}
}
animator.startAnimation()
}
case .ended, .cancelled:
if let currentHoveredOptionIndex {
self.currentHoveredOptionIndex = nil
if animator?.isRunning == true {
animator.stopAnimation(true)
}
animator = UIViewPropertyAnimator(duration: Self.animationDuration, curve: .easeInOut) {
self.options[currentHoveredOptionIndex].hovered = false
}
animator.startAnimation()
}
default:
break
}
} }
} }

View File

@ -65,7 +65,6 @@ class StatusPollView: UIView, StatusContentView {
addSubview(infoLabel) addSubview(infoLabel)
voteButton = UIButton(configuration: .plain()) voteButton = UIButton(configuration: .plain())
voteButton.isPointerInteractionEnabled = true
voteButton.translatesAutoresizingMaskIntoConstraints = false voteButton.translatesAutoresizingMaskIntoConstraints = false
voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside) voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside)
voteButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) voteButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)

View File

@ -24,13 +24,7 @@ class ScrollingSegmentedControl<Value: Hashable>: UIScrollView, UIGestureRecogni
private var changeSelectionPanRecognizer: UIGestureRecognizer! private var changeSelectionPanRecognizer: UIGestureRecognizer!
private var selectedOptionAtStartOfPan: Value? private var selectedOptionAtStartOfPan: Value?
#if !os(visionOS) #if !os(visionOS)
private lazy var selectionChangedFeedbackGenerator: UISelectionFeedbackGenerator = { private lazy var selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator()
if #available(iOS 17.5, *) {
UISelectionFeedbackGenerator(view: self)
} else {
UISelectionFeedbackGenerator()
}
}()
#endif #endif
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
@ -117,20 +111,14 @@ class ScrollingSegmentedControl<Value: Hashable>: UIScrollView, UIGestureRecogni
func setSelectedOption(_ value: Value, animated: Bool) { func setSelectedOption(_ value: Value, animated: Bool) {
guard selectedOption != value, guard selectedOption != value,
let index = options.firstIndex(where: { $0.value == value }) else { options.contains(where: { $0.value == value }) else {
return return
} }
#if !os(visionOS) #if !os(visionOS)
if selectedOption != nil { if selectedOption != nil {
if #available(iOS 17.5, *) {
let optionView = optionsStack.arrangedSubviews[index]
let location = convert(CGPoint(x: optionView.bounds.midX, y: optionView.bounds.midY), from: optionView)
selectionChangedFeedbackGenerator.selectionChanged(at: location)
} else {
selectionChangedFeedbackGenerator.selectionChanged() selectionChangedFeedbackGenerator.selectionChanged()
} }
}
#endif #endif
selectedOption = value selectedOption = value
@ -170,19 +158,15 @@ class ScrollingSegmentedControl<Value: Hashable>: UIScrollView, UIGestureRecogni
// MARK: Interaction // MARK: Interaction
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer === self.panGestureRecognizer else {
return true
}
let beganOnSelectedOption: Bool let beganOnSelectedOption: Bool
if let selectedIndex = options.firstIndex(where: { $0.value == selectedOption }), if let selectedIndex = options.firstIndex(where: { $0.value == selectedOption }),
optionsStack.arrangedSubviews[selectedIndex].frame.contains(gestureRecognizer.location(in: optionsStack)) { optionsStack.arrangedSubviews[selectedIndex].frame.contains(self.panGestureRecognizer.location(in: optionsStack)) {
beganOnSelectedOption = true beganOnSelectedOption = true
} else { } else {
beganOnSelectedOption = false beganOnSelectedOption = false
} }
// only begin changing selection if the gesture started on the currently selected item // only begin changing selection if the gesutre started on the currently selected item
// otherwise, let the scroll view handle things // otherwise, let the scroll view handle things
if gestureRecognizer == self.changeSelectionPanRecognizer { if gestureRecognizer == self.changeSelectionPanRecognizer {
return beganOnSelectedOption return beganOnSelectedOption
@ -239,12 +223,7 @@ class ScrollingSegmentedControl<Value: Hashable>: UIScrollView, UIGestureRecogni
} }
animator.startAnimation() animator.startAnimation()
#if !os(visionOS) #if !os(visionOS)
if #available(iOS 17.5, *) {
let locationInSelf = convert(location, from: optionsStack)
selectionChangedFeedbackGenerator.selectionChanged(at: locationInSelf)
} else {
selectionChangedFeedbackGenerator.selectionChanged() selectionChangedFeedbackGenerator.selectionChanged()
}
#endif #endif
return true return true
} else { } else {