diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 58a36d5e8..f302588cc 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -414,6 +414,7 @@ 45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */; }; 45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */; }; 4AC4EA13C8A444455DAB351F /* Pods_SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */; }; + 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; }; 4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */; }; 4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */; }; 4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; }; @@ -1080,6 +1081,7 @@ 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallManager.swift; sourceTree = ""; }; 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalCall.swift; sourceTree = ""; }; 45FDA43420A4D22700396358 /* OWSNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSNavigationBar.swift; sourceTree = ""; }; + 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HapticFeedback.swift; path = UserInterface/HapticFeedback.swift; sourceTree = ""; }; 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatusView.swift; sourceTree = ""; }; 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = ""; }; 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearchViewController.swift; sourceTree = ""; }; @@ -1855,6 +1857,7 @@ isa = PBXGroup; children = ( 450DF2071E0DD29E003D14BE /* Notifications */, + 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */, 34FD936E1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.h */, 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */, 34B3F8331E8DF1700035BE1A /* ViewControllers */, @@ -3254,6 +3257,7 @@ 34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */, 340FC8AD204DAC8D007AEB0F /* OWSLinkedDevicesTableViewController.m in Sources */, 340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */, + 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */, 3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */, 34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */, 34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */, diff --git a/Signal/src/UserInterface/HapticFeedback.swift b/Signal/src/UserInterface/HapticFeedback.swift new file mode 100644 index 000000000..beb47fd6a --- /dev/null +++ b/Signal/src/UserInterface/HapticFeedback.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +protocol HapticAdapter { + func selectionChanged() +} + +class LegacyHapticAdapter: NSObject, HapticAdapter { + + // MARK: HapticAdapter + + func selectionChanged() { + // do nothing + } +} + +@available(iOS 10, *) +class FeedbackGeneratorHapticAdapter: NSObject, HapticAdapter { + let selectionFeedbackGenerator: UISelectionFeedbackGenerator + + override init() { + selectionFeedbackGenerator = UISelectionFeedbackGenerator() + selectionFeedbackGenerator.prepare() + } + + // MARK: HapticAdapter + + func selectionChanged() { + selectionFeedbackGenerator.selectionChanged() + selectionFeedbackGenerator.prepare() + } +} + +class HapticFeedback: HapticAdapter { + let adapter: HapticAdapter + + init() { + if #available(iOS 10, *) { + adapter = FeedbackGeneratorHapticAdapter() + } else { + adapter = LegacyHapticAdapter() + } + } + + func selectionChanged() { + adapter.selectionChanged() + } +} diff --git a/Signal/src/ViewControllers/MenuActionsViewController.swift b/Signal/src/ViewControllers/MenuActionsViewController.swift index d18f28aa1..cf9f22813 100644 --- a/Signal/src/ViewControllers/MenuActionsViewController.swift +++ b/Signal/src/ViewControllers/MenuActionsViewController.swift @@ -71,10 +71,6 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapBackground)) self.view.addGestureRecognizer(tapGesture) - - let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didSwipeBackground)) - swipeGesture.direction = .down - self.view.addGestureRecognizer(swipeGesture) } override func viewDidAppear(_ animated: Bool) { @@ -249,11 +245,6 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate { animateDismiss(action: nil) } - @objc - func didSwipeBackground(gesture: UISwipeGestureRecognizer) { - animateDismiss(action: nil) - } - // MARK: MenuActionSheetDelegate func actionSheet(_ actionSheet: MenuActionSheetView, didSelectAction action: MenuAction) { @@ -269,6 +260,10 @@ class MenuActionSheetView: UIView, MenuActionViewDelegate { private let actionStackView: UIStackView private var actions: [MenuAction] + private var actionViews: [MenuActionView] + private var hapticFeedback: HapticFeedback + private var hasEverHighlightedAction = false + weak var delegate: MenuActionSheetDelegate? override var bounds: CGRect { @@ -288,29 +283,58 @@ class MenuActionSheetView: UIView, MenuActionViewDelegate { actionStackView.spacing = CGHairlineWidth() actions = [] + actionViews = [] + hapticFeedback = HapticFeedback() super.init(frame: frame) backgroundColor = UIColor.ows_light10 addSubview(actionStackView) - actionStackView.ows_autoPinToSuperviewEdges() + actionStackView.autoPinEdgesToSuperviewEdges() self.clipsToBounds = true - // Prevent panning from percolating to the superview, which would - // cause us to dismiss - let panGestureSink = UIPanGestureRecognizer(target: nil, action: nil) - self.addGestureRecognizer(panGestureSink) + let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(didTouch(gesture:))) + touchGesture.minimumPressDuration = 0.0 + touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude + self.addGestureRecognizer(touchGesture) } required init?(coder aDecoder: NSCoder) { fatalError("not implemented") } + @objc + public func didTouch(gesture: UIGestureRecognizer) { + switch gesture.state { + case .possible: + break + case .began: + let location = gesture.location(in: self) + highlightActionView(location: location, fromView: self) + case .changed: + let location = gesture.location(in: self) + highlightActionView(location: location, fromView: self) + case .ended: + Logger.debug("\(logTag) in \(#function) ended") + let location = gesture.location(in: self) + selectActionView(location: location, fromView: self) + case .cancelled: + Logger.debug("\(logTag) in \(#function) canceled") + unhighlightAllActionViews() + case .failed: + Logger.debug("\(logTag) in \(#function) failed") + unhighlightAllActionViews() + } + } + public func addAction(_ action: MenuAction) { + actions.append(action) + let actionView = MenuActionView(action: action) actionView.delegate = self - actions.append(action) + actionViews.append(actionView) + self.actionStackView.addArrangedSubview(actionView) } @@ -329,6 +353,47 @@ class MenuActionSheetView: UIView, MenuActionViewDelegate { mask.path = path.cgPath self.layer.mask = mask } + + private func unhighlightAllActionViews() { + for actionView in actionViews { + actionView.isHighlighted = false + } + } + + private func actionView(touchedBy touchPoint: CGPoint, fromView: UIView) -> MenuActionView? { + for actionView in actionViews { + let convertedPoint = actionView.convert(touchPoint, from: fromView) + if actionView.point(inside: convertedPoint, with: nil) { + return actionView + } + } + return nil + } + + private func highlightActionView(location: CGPoint, fromView: UIView) { + guard let touchedView = actionView(touchedBy: location, fromView: fromView) else { + unhighlightAllActionViews() + return + } + + if hasEverHighlightedAction, !touchedView.isHighlighted { + self.hapticFeedback.selectionChanged() + } + touchedView.isHighlighted = true + hasEverHighlightedAction = true + + self.actionViews.filter { $0 != touchedView }.forEach { $0.isHighlighted = false } + } + + private func selectActionView(location: CGPoint, fromView: UIView) { + guard let selectedView: MenuActionView = actionView(touchedBy: location, fromView: fromView) else { + unhighlightAllActionViews() + return + } + selectedView.isHighlighted = true + self.actionViews.filter { $0 != selectedView }.forEach { $0.isHighlighted = false } + delegate?.actionSheet(self, didSelectAction: selectedView.action) + } } protocol MenuActionViewDelegate: class { @@ -337,7 +402,7 @@ protocol MenuActionViewDelegate: class { class MenuActionView: UIButton { public weak var delegate: MenuActionViewDelegate? - private let action: MenuAction + public let action: MenuAction required init(action: MenuAction) { self.action = action @@ -378,10 +443,10 @@ class MenuActionView: UIButton { contentRow.isUserInteractionEnabled = false self.addSubview(contentRow) - contentRow.ows_autoPinToSuperviewMargins() + contentRow.autoPinEdgesToSuperviewMargins() contentRow.autoSetDimension(.height, toSize: 56, relation: .greaterThanOrEqual) - self.addTarget(self, action: #selector(didPress(sender:)), for: .touchUpInside) + self.isUserInteractionEnabled = false } override var isHighlighted: Bool {