Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit e07d032

Browse files
authored
Fix password menu edit actions in iOS 18 (#3953)
Task/Issue URL: https://app.asana.com/0/1202926619870900/1209132360844274 **Description**: The menu actions on the username / password / website URL form entries are no longer appearing as of iOS 18. This fixes that by using the `UIEditMenuInteraction` to replace the deprecated `UIMenuController` **Steps to test this PR**: 1. Go to the password management screen 2. Ensure you have at least one password saved with all fields populated (username, password, website URL, Notes) 3. Press each of the fields You should see Copy (Username|Password|Website URL|Notes) and, for the Password field, also Show Password **Definition of Done (Internal Only)**: * [x] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad **OS Testing**: * [x] iOS 15 * [x] iOS 16 * [ ] iOS 17 **Theme Testing**: * [ ] Light theme * [ ] Dark theme --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943)
1 parent 4191990 commit e07d032

File tree

2 files changed

+84
-18
lines changed

2 files changed

+84
-18
lines changed

DuckDuckGo/AutofillLoginDetailsView.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,11 @@ private struct Copyable: ViewModifier {
569569

570570
public func body(content: Content) -> some View {
571571
ZStack {
572+
content
573+
.allowsHitTesting(false)
574+
.contentShape(Rectangle())
575+
.frame(maxWidth: .infinity)
576+
.frame(minHeight: Constants.minRowHeight)
572577
Rectangle()
573578
.foregroundColor(.clear)
574579
.menuController(menuTitle,
@@ -578,12 +583,6 @@ private struct Copyable: ViewModifier {
578583
onOpen: menuOpenedAction,
579584
onClose: menuClosedAction)
580585

581-
content
582-
.allowsHitTesting(false)
583-
.contentShape(Rectangle())
584-
.frame(maxWidth: .infinity)
585-
.frame(minHeight: Constants.minRowHeight)
586-
587586
}
588587
}
589588
}

DuckDuckGo/MenuControllerView.swift

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ struct MenuControllerView<Content: View>: UIViewControllerRepresentable {
5656

5757
let tap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tap))
5858
hostingController.view.addGestureRecognizer(tap)
59-
59+
60+
if #available(iOS 16.0, *) {
61+
let menu = UIEditMenuInteraction(delegate: coordinator)
62+
hostingController.view.addInteraction(menu)
63+
context.coordinator.menu = menu
64+
}
65+
6066
return hostingController
6167
}
6268

@@ -68,8 +74,8 @@ struct MenuControllerView<Content: View>: UIViewControllerRepresentable {
6874
context.coordinator.onOpen = onOpen
6975
context.coordinator.onClose = onClose
7076
}
71-
72-
class Coordinator<CoordinatorContent: View>: NSObject {
77+
78+
class Coordinator<CoordinatorContent: View>: NSObject, UIEditMenuInteractionDelegate {
7379
var responder: UIResponder?
7480
var observer: Any?
7581
var title: String
@@ -78,23 +84,38 @@ struct MenuControllerView<Content: View>: UIViewControllerRepresentable {
7884
var secondaryAction: (() -> Void)?
7985
var onOpen: (() -> Void)?
8086
var onClose: (() -> Void)?
81-
87+
88+
// Define me as UIEditMenuInteraction when iOS 15 is dropped
89+
var menu: NSObject?
90+
8291
init(title: String, secondaryTitle: String?, action: @escaping () -> Void, secondaryAction: (() -> Void)?, onOpen: (() -> Void)?, onClose: (() -> Void)?) {
8392
self.title = title
8493
self.secondaryTitle = secondaryTitle
8594
self.action = action
8695
self.secondaryAction = secondaryAction
8796
self.onOpen = onOpen
8897
self.onClose = onClose
98+
super.init()
8999
}
90100

91101
@objc func tap(_ gestureRecognizer: UIGestureRecognizer) {
102+
if #available(iOS 16.0, *) {
103+
handleiOS16Tap(gestureRecognizer)
104+
} else {
105+
handleiOS15Tap(gestureRecognizer)
106+
}
107+
onOpen?()
108+
}
109+
110+
// MARK: Private
111+
112+
private func handleiOS15Tap(_ gestureRecognizer: UIGestureRecognizer) {
92113
let menu = UIMenuController.shared
93114

94115
guard gestureRecognizer.state == .ended, let view = gestureRecognizer.view, !menu.isMenuVisible else {
95116
return
96117
}
97-
118+
98119
responder?.becomeFirstResponder()
99120

100121
menu.menuItems = [
@@ -111,15 +132,59 @@ struct MenuControllerView<Content: View>: UIViewControllerRepresentable {
111132
observer = NotificationCenter.default.addObserver(forName: UIMenuController.willHideMenuNotification,
112133
object: nil,
113134
queue: nil) { [weak self] _ in
114-
if let observer = self?.observer {
115-
NotificationCenter.default.removeObserver(observer)
116-
}
117-
self?.responder?.resignFirstResponder()
118-
self?.onClose?()
135+
self?.handleClose()
119136
}
120-
onOpen?()
121137
}
122138

139+
private func handleClose() {
140+
if let observer {
141+
NotificationCenter.default.removeObserver(observer)
142+
}
143+
responder?.resignFirstResponder()
144+
onClose?()
145+
}
146+
147+
@available(iOS 16.0, *)
148+
private func handleiOS16Tap(_ gestureRecognizer: UIGestureRecognizer) {
149+
guard let menuInteraction = menu as? UIEditMenuInteraction else {
150+
return
151+
}
152+
153+
guard gestureRecognizer.state == .ended, let view = gestureRecognizer.view else {
154+
return
155+
}
156+
157+
let menuConfig = UIEditMenuConfiguration.init(identifier: nil, sourcePoint: view.center)
158+
159+
menuInteraction.presentEditMenu(with: menuConfig)
160+
}
161+
162+
// MARK: UIEditMenuInteractionDelegate
163+
164+
@available(iOS 16.0, *)
165+
func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? {
166+
var actions: [UIAction] = [.init(title: title) { [weak self] _ in
167+
self?.action()
168+
}]
169+
170+
if let secondaryTitle, let secondaryAction {
171+
actions.append(.init(title: secondaryTitle) { _ in
172+
secondaryAction()
173+
})
174+
}
175+
176+
let uiMenu: UIMenu = .init(title: "menu",
177+
children: actions)
178+
179+
return uiMenu
180+
}
181+
182+
@available(iOS 16.0, *)
183+
func editMenuInteraction(_ interaction: UIEditMenuInteraction, willDismissMenuFor configuration: UIEditMenuConfiguration, animator: any UIEditMenuInteractionAnimating) {
184+
handleClose()
185+
}
186+
187+
// Delete me when iOS 15 is dropped
123188
deinit {
124189
if let observer = observer {
125190
NotificationCenter.default.removeObserver(observer)
@@ -146,11 +211,13 @@ struct MenuControllerView<Content: View>: UIViewControllerRepresentable {
146211
override var canBecomeFirstResponder: Bool {
147212
true
148213
}
149-
214+
215+
// Delete me when iOS 15 is dropped
150216
@objc func menuItemAction(_ sender: Any) {
151217
self.action?()
152218
}
153219

220+
// Delete me when iOS 15 is dropped
154221
@objc func menuItemSecondaryAction(_ sender: Any) {
155222
self.secondaryAction?()
156223
}

0 commit comments

Comments
 (0)