-
-typedef struct {
- unsigned long long attachment;
- unsigned long long alignment;
- double attachmentOffset;
- double alignmentOffset;
- long long gravity;
-} ContextMenuAccessoryAnchor;
diff --git a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Info.plist b/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Info.plist
deleted file mode 100644
index 09eacdb..0000000
Binary files a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Info.plist and /dev/null differ
diff --git a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Modules/module.modulemap b/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Modules/module.modulemap
deleted file mode 100644
index ba0f298..0000000
--- a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Modules/module.modulemap
+++ /dev/null
@@ -1,6 +0,0 @@
-framework module ContextMenuAccessoryStructs {
- umbrella header "ContextMenuAccessoryStructs.h"
- export *
-
- module * { export * }
-}
diff --git a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources b/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources
deleted file mode 100644
index 1ee20d8..0000000
--- a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
-
- files
-
- Headers/ContextMenuAccessoryStructs.h
-
- b9Luviw1b76Lr6p1xgNrWNfPCVY=
-
- Info.plist
-
- 5DvCNs2Br0B+tFd3LGX2GWX3YI4=
-
- Modules/module.modulemap
-
- ATR3fNklhb4n/v9ZT1/V52kwESk=
-
-
- files2
-
- Headers/ContextMenuAccessoryStructs.h
-
- hash2
-
- SwK0arudKlqsKFRArld22fmqwIqpiFxdeNrWJ36DmvQ=
-
-
- Modules/module.modulemap
-
- hash2
-
- bNjsWBrAeGn0pRiBgSwZN5WWpE7sPRHaQT1gVEZp5Eg=
-
-
-
- rules
-
- ^.*
-
- ^.*\.lproj/
-
- optional
-
- weight
- 1000
-
- ^.*\.lproj/locversion.plist$
-
- omit
-
- weight
- 1100
-
- ^Base\.lproj/
-
- weight
- 1010
-
- ^version.plist$
-
-
- rules2
-
- .*\.dSYM($|/)
-
- weight
- 11
-
- ^(.*/)?\.DS_Store$
-
- omit
-
- weight
- 2000
-
- ^.*
-
- ^.*\.lproj/
-
- optional
-
- weight
- 1000
-
- ^.*\.lproj/locversion.plist$
-
- omit
-
- weight
- 1100
-
- ^Base\.lproj/
-
- weight
- 1010
-
- ^Info\.plist$
-
- omit
-
- weight
- 20
-
- ^PkgInfo$
-
- omit
-
- weight
- 20
-
- ^embedded\.provisionprofile$
-
- weight
- 20
-
- ^version\.plist$
-
- weight
- 20
-
-
-
-
diff --git a/Package.swift b/Package.swift
index 053faf2..0772021 100644
--- a/Package.swift
+++ b/Package.swift
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "MenuWithAView",
platforms: [
- .iOS(.v18)
+ .iOS(.v16)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
@@ -14,6 +14,10 @@ let package = Package(
name: "MenuWithAView",
targets: ["MenuWithAView"]
),
+ .library(
+ name: "ContextMenuAccessoryStructs",
+ targets: ["ContextMenuAccessoryStructs"]
+ )
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
@@ -22,9 +26,8 @@ let package = Package(
name: "MenuWithAView",
dependencies: ["ContextMenuAccessoryStructs"]
),
- .binaryTarget(
- name: "ContextMenuAccessoryStructs",
- path: "Frameworks/ContextMenuAccessoryStructs.xcframework"
+ .target(
+ name: "ContextMenuAccessoryStructs"
)
]
)
diff --git a/README.md b/README.md
index 92fc327..bb30de0 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
MenuWithAView is a SwiftUI package that lets you add accessory views to your context menu interactions, with UIKit's private _UIContextMenuAccessoryView.
- Compatible with iOS 18 and later
+ Compatible with iOS 16 and later
@@ -14,7 +14,7 @@
-
+
@@ -29,23 +29,27 @@
## contextMenuAccessory
-`contextMenuAccessory` is a SwiftUI modifier that lets you attach an accessory view to a `.contextMenu`. You can control the accessory’s placement, location, alignment, and tracking axis.
+`contextMenuAccessory` is a SwiftUI modifier that lets you attach an accessory view to a `.contextMenu`. You can control the accessory's placement, location, alignment, and tracking axis. There are two variants: one for simple accessory views and another that provides a `ContextMenuProxy` for programmatic dismissal.
**DocC documentation is available for this modifier.**
### Parameters
-- `placement`: Where the accessory is attached relative to the context menu.
+- `placement`: Where the accessory is attached relative to the context menu.
*(Default: `.center`)*
-- `location`: The location where the accessory appears.
+- `location`: The location where the accessory appears.
*(Default: `.preview`)*
-- `alignment`: How the accessory aligns within its container.
+- `alignment`: How the accessory aligns within its container.
*(Default: `.leading`)*
-- `trackingAxis`: The axis along which the accessory tracks user interaction.
+- `trackingAxis`: The axis along which the accessory tracks user interaction.
*(Default: `[.xAxis, .yAxis]`)*
-- `accessory`: The view to display as the accessory.
+- `accessory`: A view builder that returns the accessory view. Available in two variants:
+ - Simple: `@ViewBuilder accessory: () -> AccessoryView`
+ - With proxy: `@ViewBuilder accessory: (ContextMenuProxy) -> AccessoryView`
-### Example
+### Examples
+
+#### Basic Usage
```swift
Text("Turtle Rock")
@@ -70,6 +74,32 @@ Text("Turtle Rock")
}
```
+#### With Programmatic Dismissal
+
+```swift
+Text("Turtle Rock")
+ .padding()
+ .contextMenu {
+ Button(action: {}) {
+ Label("Button", systemImage: "circle")
+ }
+ }
+ .contextMenuAccessory(placement: .center) { proxy in
+ VStack {
+ Text("Accessory View")
+ .font(.title2)
+
+ Button("Dismiss") {
+ proxy.dismiss()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ .padding()
+ .background(Color.blue.opacity(0.6))
+ .clipShape(RoundedRectangle(cornerRadius: 12))
+ }
+```
+
---
## **Acknowledgments**
diff --git a/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m b/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m
new file mode 100644
index 0000000..eb05a0c
--- /dev/null
+++ b/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m
@@ -0,0 +1,8 @@
+//
+// ContextMenuAccessoryStructs.m
+// MenuWithAView
+//
+// Created by Nathan Tannar on 2025-09-07.
+//
+
+#import "ContextMenuAccessoryStructs.h"
diff --git a/Sources/ContextMenuAccessoryStructs/include/ContextMenuAccessoryStructs.h b/Sources/ContextMenuAccessoryStructs/include/ContextMenuAccessoryStructs.h
new file mode 100644
index 0000000..c2fd16a
--- /dev/null
+++ b/Sources/ContextMenuAccessoryStructs/include/ContextMenuAccessoryStructs.h
@@ -0,0 +1,7 @@
+typedef struct {
+ unsigned long long attachment;
+ unsigned long long alignment;
+ double attachmentOffset;
+ double alignmentOffset;
+ long long gravity;
+} ContextMenuAccessoryAnchor;
diff --git a/Sources/MenuWithAView/AccessoryItem.swift b/Sources/MenuWithAView/AccessoryItem.swift
index fde2ef7..9ee3257 100644
--- a/Sources/MenuWithAView/AccessoryItem.swift
+++ b/Sources/MenuWithAView/AccessoryItem.swift
@@ -8,37 +8,6 @@
import SwiftUI
import ContextMenuAccessoryStructs
-struct AccessoryItem: View {
- let configuration: Configuration
- let content: () -> Content
-
- init(configuration: ContextMenuAccessoryConfiguration, content: @escaping () -> Content) {
- self.configuration = configuration
- self.content = content
- }
-
- init(placement: Placement, content: @escaping () -> Content) {
- self.configuration = Configuration(placement: placement)
- self.content = content
- }
-
- var body: some View {
- content()
- }
-}
-
-extension AccessoryItem {
- public typealias Location = ContextMenuAccessoryLocation
-
- public typealias Placement = ContextMenuAccessoryPlacement
-
- public typealias Alignment = ContextMenuAccessoryAlignment
-
- public typealias TrackingAxis = ContextMenuAccessoryTrackingAxis
-
- typealias Configuration = ContextMenuAccessoryConfiguration
-}
-
public enum ContextMenuAccessoryLocation: Int {
case background = 0
case preview = 1
@@ -61,7 +30,7 @@ public enum ContextMenuAccessoryAlignment: UInt64 {
case trailing = 8
}
-public struct ContextMenuAccessoryTrackingAxis: OptionSet, Sendable {
+public struct ContextMenuAccessoryTrackingAxis: OptionSet, Sendable, Hashable {
public let rawValue: Int
public init(rawValue: Int) {
@@ -77,10 +46,18 @@ public struct ContextMenuAccessoryTrackingAxis: OptionSet, Sendable {
}
}
+public struct ContextMenuProxy: @unchecked Sendable {
+
+ var dismissBlock: (() -> Void)?
+
+ @MainActor
+ public func dismiss() {
+ dismissBlock?()
+ }
+}
+
/// Configuration for context menu accessories, including placement, location, alignment, and tracking axis.
-struct ContextMenuAccessoryConfiguration: Identifiable {
- let id: UUID = UUID()
-
+struct ContextMenuAccessoryConfiguration {
var location: ContextMenuAccessoryLocation = .preview
// controls the attachment point
diff --git a/Sources/MenuWithAView/ContextMenuIdentifierView.swift b/Sources/MenuWithAView/ContextMenuIdentifierView.swift
index 10b8abd..1872761 100644
--- a/Sources/MenuWithAView/ContextMenuIdentifierView.swift
+++ b/Sources/MenuWithAView/ContextMenuIdentifierView.swift
@@ -9,36 +9,90 @@ import UIKit
import SwiftUI
import ContextMenuAccessoryStructs
-struct ContextMenuIdentifierView: UIViewRepresentable {
- let accessoryView: () -> AccessoryItem
-
- func makeUIView(context: Context) -> some UIView {
- let rootView = accessoryView()
- let hostingView = _UIHostingView(rootView: rootView)
- let identifierView = ContextMenuIdentifierUIView(accessoryView: hostingView, configuration: rootView.configuration)
-
- return identifierView
+struct ContextMenuIdentifierView: View {
+ let configuration: ContextMenuAccessoryConfiguration
+ let accessory: (ContextMenuProxy) -> AccessoryView
+
+ var body: some View {
+ ContextMenuIdentifierViewBody(
+ configuration: configuration,
+ accessory: accessory
+ )
+ // Disable accessibility so any accessibility modifiers used are
+ // not applied to `ContextMenuIdentifierViewBody`
+ .environment(\.accessibilityEnabled, false)
}
-
- func updateUIView(_ uiView: UIViewType, context: Context) {}
}
-class ContextMenuIdentifierUIView: UIView {
- let accessoryView: UIView
+
+struct ContextMenuIdentifierViewBody: UIViewRepresentable {
let configuration: ContextMenuAccessoryConfiguration
+ let accessory: (ContextMenuProxy) -> AccessoryView
+
+ func makeUIView(
+ context: Context
+ ) -> ContextMenuIdentifierUIView {
+ let uiView = ContextMenuIdentifierUIView(
+ configuration: configuration,
+ accessory: accessory
+ )
+ return uiView
+ }
- init(accessoryView: UIView, configuration: ContextMenuAccessoryConfiguration) {
- self.accessoryView = accessoryView
+ func updateUIView(
+ _ uiView: ContextMenuIdentifierUIView,
+ context: Context
+ ) {
+ uiView.update(accessory)
+ }
+}
+
+class AnyContextMenuIdentifierUIView: UIView {
+ var accessoryView: UIView?
+ var configuration: ContextMenuAccessoryConfiguration
+ weak var interaction: UIContextMenuInteraction?
+
+ init(configuration: ContextMenuAccessoryConfiguration) {
self.configuration = configuration
-
super.init(frame: .zero)
-
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
+
+class ContextMenuIdentifierUIView: AnyContextMenuIdentifierUIView {
+ private var hostingView: _UIHostingView!
+
+ init(
+ configuration: ContextMenuAccessoryConfiguration,
+ accessory: (ContextMenuProxy) -> AccessoryView
+ ) {
+ super.init(configuration: configuration)
+ self.hostingView = _UIHostingView(rootView: accessory(makeProxy()))
+ accessoryView = hostingView
+
UIContextMenuInteraction.swizzle_delegate_getAccessoryViewsForConfigurationIfNeeded()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
+
+ func update(_ accessory: (ContextMenuProxy) -> AccessoryView) {
+ hostingView.rootView = accessory(makeProxy())
+ }
+
+ private func makeProxy() -> ContextMenuProxy {
+ ContextMenuProxy { [weak self] in
+ self?.dismiss()
+ }
+ }
+
+ private func dismiss() {
+ interaction?.dismissMenu()
+ }
}
#Preview {
diff --git a/Sources/MenuWithAView/Example.swift b/Sources/MenuWithAView/Example.swift
index 8b738c0..e29504a 100644
--- a/Sources/MenuWithAView/Example.swift
+++ b/Sources/MenuWithAView/Example.swift
@@ -80,7 +80,7 @@ public struct MenuWithAView_Example: View {
placement: placement,
location: location,
alignment: alignment,
- trackingAxis: .yAxis
+ trackingAxis: .xAxis
) {
Text("Accessory View")
.font(.title2)
@@ -170,6 +170,203 @@ public struct MenuWithAView_Example: View {
}
}
-#Preview {
+// MARK: - Test Views for ContextMenuProxy and Advanced Features
+
+public struct ContextMenuProxyTestView: View {
+ @State private var dismissCount = 0
+ @State private var lastDismissTime = Date()
+
+ public init() {}
+
+ public var body: some View {
+ NavigationStack {
+ VStack(spacing: 30) {
+ Text("Test programmatic dismissal with ContextMenuProxy")
+ .font(.headline)
+ .multilineTextAlignment(.center)
+ .padding()
+
+ VStack(spacing: 20) {
+ // Basic dismissal test
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color.blue.gradient)
+ .frame(width: 150, height: 100)
+ .overlay {
+ VStack {
+ Text("Dismissal Test")
+ .font(.caption)
+ .foregroundColor(.white)
+ Text("Long press")
+ .font(.caption2)
+ .foregroundColor(.white.opacity(0.8))
+ }
+ }
+ .contextMenu {
+ Button("Menu Action") { }
+ }
+ .contextMenuAccessory(placement: .center) { proxy in
+ VStack(spacing: 8) {
+ Text("Dismissals: \(dismissCount)")
+ .font(.caption)
+
+ Button("Dismiss") {
+ dismissCount += 1
+ lastDismissTime = Date()
+ proxy.dismiss()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ .padding()
+ .background(Color.orange.opacity(0.9))
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ }
+
+ // Dismissal with action test
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color.green.gradient)
+ .frame(width: 150, height: 100)
+ .overlay {
+ VStack {
+ Text("Action + Dismiss")
+ .font(.caption)
+ .foregroundColor(.white)
+ Text("Long press")
+ .font(.caption2)
+ .foregroundColor(.white.opacity(0.8))
+ }
+ }
+ .contextMenu {
+ Button("Menu Action") { }
+ }
+ .contextMenuAccessory(placement: .bottom) { proxy in
+ VStack(spacing: 4) {
+ Button("Save & Close") {
+ // Simulate an action
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ proxy.dismiss()
+ }
+ }
+ .buttonStyle(.bordered)
+ .font(.caption)
+
+ Button("Cancel") {
+ proxy.dismiss()
+ }
+ .buttonStyle(.borderless)
+ .font(.caption2)
+ }
+ .padding(8)
+ .background(Color.white.opacity(0.95))
+ .clipShape(RoundedRectangle(cornerRadius: 6))
+ }
+ }
+
+ Text("Last dismiss: \(lastDismissTime.formatted(date: .omitted, time: .standard))")
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Spacer()
+ }
+ .padding()
+ .navigationTitle("Proxy Tests")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+ }
+}
+
+public struct ViewUpdatesTestView: View {
+ @State private var counter = 0
+ @State private var color = Color.red
+ @State private var isAnimating = false
+
+ public init() {}
+
+ public var body: some View {
+ NavigationStack {
+ VStack(spacing: 30) {
+ Text("Test real-time view updates in accessory")
+ .font(.headline)
+ .multilineTextAlignment(.center)
+ .padding()
+
+ VStack(spacing: 20) {
+ Button("Update Counter: \(counter)") {
+ counter += 1
+ color = [Color.red, Color.blue, Color.green, Color.orange, Color.purple].randomElement()!
+ }
+ .buttonStyle(.borderedProminent)
+
+ RoundedRectangle(cornerRadius: 12)
+ .fill(color.gradient)
+ .frame(width: 200, height: 120)
+ .scaleEffect(isAnimating ? 1.05 : 1.0)
+ .animation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true), value: isAnimating)
+ .overlay {
+ VStack {
+ Text("Live Updates")
+ .font(.caption)
+ .foregroundColor(.white)
+ Text("Long press")
+ .font(.caption2)
+ .foregroundColor(.white.opacity(0.8))
+ }
+ }
+ .contextMenu {
+ Button("Action") { counter += 1 }
+ }
+ .contextMenuAccessory(placement: .top) { proxy in
+ VStack(spacing: 8) {
+ Text("Counter: \(counter)")
+ .font(.title2)
+ .foregroundColor(.primary)
+
+ Circle()
+ .fill(color)
+ .frame(width: 20, height: 20)
+
+ HStack {
+ Button("+1") {
+ counter += 1
+ }
+ .buttonStyle(.bordered)
+ .font(.caption)
+
+ Button("Close") {
+ proxy.dismiss()
+ }
+ .buttonStyle(.borderless)
+ .font(.caption)
+ }
+ }
+ .padding()
+ .background(Color(.systemBackground))
+ .clipShape(RoundedRectangle(cornerRadius: 12))
+ .shadow(radius: 4)
+ }
+ }
+
+ Toggle("Animate", isOn: $isAnimating)
+ .padding(.horizontal, 50)
+
+ Spacer()
+ }
+ .padding()
+ .navigationTitle("Update Tests")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+ }
+}
+
+// MARK: - Preview Collection
+
+#Preview("Main Example") {
MenuWithAView_Example()
}
+
+#Preview("Proxy Tests") {
+ ContextMenuProxyTestView()
+}
+
+#Preview("Update Tests") {
+ ViewUpdatesTestView()
+}
diff --git a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift
index bb074a0..b622475 100644
--- a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift
+++ b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift
@@ -10,7 +10,7 @@ import SwiftUI
import ContextMenuAccessoryStructs
extension UIContextMenuInteraction {
- static func accessoryView(configuration: AccessoryItem.Configuration) -> UIView? {
+ static func accessoryView(configuration: ContextMenuAccessoryConfiguration) -> UIView? {
let accessoryViewClassString = ["View", "Accessory", "Menu", "Context", "UI", "_"].reversed().joined()
let accessoryViewClass = NSClassFromString(accessoryViewClassString) as? UIView.Type
@@ -37,9 +37,11 @@ extension UIContextMenuInteraction {
let anchorSelector = NSSelectorFromString(anchorString)
if accessoryView.responds(to: anchorSelector) {
- let method = class_getInstanceMethod(accessoryViewClass, anchorSelector)!
+ guard let method = class_getInstanceMethod(accessoryViewClass, anchorSelector) else {
+ return accessoryView
+ }
let implementation = method_getImplementation(method)
-
+
let type = (@convention(c) (AnyObject, Selector, ContextMenuAccessoryAnchor) -> Void).self
let setAnchor = unsafeBitCast(implementation, to: type)
setAnchor(accessoryView, anchorSelector, configuration.anchor)
diff --git a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift
index 04102b5..aebc6ea 100644
--- a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift
+++ b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift
@@ -9,33 +9,32 @@ import UIKit
import SwiftUI
extension UIContextMenuInteraction {
- private static var needsSwizzle_delegate_getAccessoryViewsForConfiguration: Bool = true
-
- static func swizzle_delegate_getAccessoryViewsForConfigurationIfNeeded() {
- guard needsSwizzle_delegate_getAccessoryViewsForConfiguration else { return }
-
+ private static let swizzleOnce: () = {
let originalString = [":", "Configuration", "For", "Views", "Accessory", "get", "_", "delegate", "_"].reversed().joined()
let swizzledString = [":", "Configuration", "For", "Views", "Accessory", "get", "_", "delegate", "_", "swizzled"].reversed().joined()
-
+
let originalSelector = NSSelectorFromString(originalString)
let swizzledSelector = NSSelectorFromString(swizzledString)
-
+
guard instancesRespond(to: originalSelector), instancesRespond(to: swizzledSelector) else { return }
-
+
let originalMethod = class_getInstanceMethod(UIContextMenuInteraction.self, originalSelector)
let swizzledMethod = class_getInstanceMethod(UIContextMenuInteraction.self, swizzledSelector)
-
+
guard let originalMethod, let swizzledMethod else { return }
-
+
method_exchangeImplementations(originalMethod, swizzledMethod)
-
- needsSwizzle_delegate_getAccessoryViewsForConfiguration = false
+ }()
+
+ static func swizzle_delegate_getAccessoryViewsForConfigurationIfNeeded() {
+ _ = swizzleOnce
}
@objc dynamic func swizzled_delegate_getAccessoryViewsForConfiguration(_ configuration: UIContextMenuConfiguration) -> [UIView] {
- if let identifierView = view?.firstSubview(ofType: ContextMenuIdentifierUIView.self) {
-
+ if let identifierView = view?.firstSubview(ofType: AnyContextMenuIdentifierUIView.self),
let contentView = identifierView.accessoryView
+ {
+ identifierView.interaction = view?.interactions.compactMap({ $0 as? UIContextMenuInteraction }).first
contentView.frame.size = contentView.intrinsicContentSize
let accessoryView = UIContextMenuInteraction.accessoryView(configuration: identifierView.configuration)
diff --git a/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift b/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift
index 61c039c..c45250a 100644
--- a/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift
+++ b/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift
@@ -9,20 +9,67 @@ import SwiftUI
import UIKit
import ContextMenuAccessoryStructs
-private struct AccessoryWrapper: View {
- let configuration: ContextMenuAccessoryConfiguration
- let accessory: () -> AccessoryView
+public extension View {
+ /// Adds an accessory view to instances of `.contextMenu`.
+ ///
+ /// > Note: This modifier should be used in combination with `.contextMenu`.
+ ///
+ /// - Parameters:
+ /// - placement: The placement of the accessory relative to the context menu. *(Optional, default: `.center`)*
+ /// - location: The location where the accessory should appear. *(Optional, default: `.preview`)*
+ /// - alignment: The alignment of the accessory within its container. *(Optional, default: `.leading`)*
+ /// - trackingAxis: The axis along which the accessory tracks user interaction. *(Optional, default: `[.xAxis, .yAxis]`)*
+ /// - accessory: A view builder that creates the accessory view.
+ ///
+ /// For more details on default values, see ``ContextMenuAccessoryConfiguration``.
+ ///
+ /// Example usage:
+ ///
+ /// ```swift
+ /// Text("Turtle Rock")
+ /// .padding()
+ /// .contextMenu {
+ /// Button(action: {}) {
+ /// Label("Button", systemImage: "circle")
+ /// }
+ /// }
+ /// .contextMenuAccessory(
+ /// placement: placement,
+ /// location: location,
+ /// alignment: alignment,
+ /// trackingAxis: .yAxis
+ /// ) {
+ /// Text("Accessory View")
+ /// .font(.title2)
+ /// .padding(8)
+ /// .background(Color.blue.opacity(0.6))
+ /// .clipShape(RoundedRectangle(cornerRadius: 12))
+ /// .padding(16)
+ /// }
+ /// ```
+ func contextMenuAccessory(
+ placement: ContextMenuAccessoryPlacement? = nil,
+ location: ContextMenuAccessoryLocation? = nil,
+ alignment: ContextMenuAccessoryAlignment? = nil,
+ trackingAxis: ContextMenuAccessoryTrackingAxis? = nil,
+ @ViewBuilder accessory: () -> AccessoryView
+ ) -> some View {
+ var config = ContextMenuAccessoryConfiguration()
+ if let placement = placement { config.placement = placement }
+ if let location = location { config.location = location }
+ if let alignment = alignment { config.alignment = alignment }
+ if let trackingAxis = trackingAxis { config.trackingAxis = trackingAxis }
- var body: some View {
- ContextMenuIdentifierView(accessoryView: {
- AccessoryItem(configuration: configuration) {
- accessory()
- }
- })
+ let accessory = accessory()
+ let wrapped = background {
+ ContextMenuIdentifierView(
+ configuration: config,
+ accessory: { _ in accessory }
+ )
+ }
+ return wrapped
}
-}
-public extension View {
/// Adds an accessory view to instances of `.contextMenu`.
///
/// > Note: This modifier should be used in combination with `.contextMenu`.
@@ -32,7 +79,7 @@ public extension View {
/// - location: The location where the accessory should appear. *(Optional, default: `.preview`)*
/// - alignment: The alignment of the accessory within its container. *(Optional, default: `.leading`)*
/// - trackingAxis: The axis along which the accessory tracks user interaction. *(Optional, default: `[.xAxis, .yAxis]`)*
- /// - accessory: A view builder that creates the accessory view.
+ /// - accessory: A view builder that creates the accessory view using a proxy to the `.contextMenu` interaction that allows dismissal of the view to be triggered.
///
/// For more details on default values, see ``ContextMenuAccessoryConfiguration``.
///
@@ -65,19 +112,21 @@ public extension View {
location: ContextMenuAccessoryLocation? = nil,
alignment: ContextMenuAccessoryAlignment? = nil,
trackingAxis: ContextMenuAccessoryTrackingAxis? = nil,
- @ViewBuilder accessory: @escaping () -> AccessoryView
+ @ViewBuilder accessory: @escaping (ContextMenuProxy) -> AccessoryView
) -> some View {
var config = ContextMenuAccessoryConfiguration()
if let placement = placement { config.placement = placement }
if let location = location { config.location = location }
if let alignment = alignment { config.alignment = alignment }
if let trackingAxis = trackingAxis { config.trackingAxis = trackingAxis }
-
+
let wrapped = background {
- AccessoryWrapper(configuration: config, accessory: accessory)
- .accessibilityHidden(true)
+ ContextMenuIdentifierView(
+ configuration: config,
+ accessory: accessory
+ )
}
- return wrapped.id(config.id)
+ return wrapped
}
}