From d8629d9fb56e9575ef6f0402760e0c6b6ae6b741 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sat, 6 Sep 2025 15:06:14 -0700 Subject: [PATCH 01/12] Fix View Update Cycle --- Sources/MenuWithAView/AccessoryItem.swift | 22 ++++++---- .../ContextMenuIdentifierView.swift | 43 ++++++++++++------- .../UIContextMenuInteraction+Swizzle.swift | 5 ++- .../View/View+ContextMenuAccessories.swift | 23 +++++----- 4 files changed, 57 insertions(+), 36 deletions(-) diff --git a/Sources/MenuWithAView/AccessoryItem.swift b/Sources/MenuWithAView/AccessoryItem.swift index fde2ef7..3dc4ba6 100644 --- a/Sources/MenuWithAView/AccessoryItem.swift +++ b/Sources/MenuWithAView/AccessoryItem.swift @@ -10,20 +10,26 @@ import ContextMenuAccessoryStructs struct AccessoryItem: View { let configuration: Configuration - let content: () -> Content + let content: Content - init(configuration: ContextMenuAccessoryConfiguration, content: @escaping () -> Content) { + init( + configuration: ContextMenuAccessoryConfiguration, + @ViewBuilder content: () -> Content + ) { self.configuration = configuration - self.content = content + self.content = content() } - init(placement: Placement, content: @escaping () -> Content) { + init( + placement: Placement, + @ViewBuilder content: () -> Content + ) { self.configuration = Configuration(placement: placement) - self.content = content + self.content = content() } var body: some View { - content() + content } } @@ -78,9 +84,7 @@ public struct ContextMenuAccessoryTrackingAxis: OptionSet, Sendable { } /// 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..8b4e679 100644 --- a/Sources/MenuWithAView/ContextMenuIdentifierView.swift +++ b/Sources/MenuWithAView/ContextMenuIdentifierView.swift @@ -10,29 +10,42 @@ import SwiftUI import ContextMenuAccessoryStructs struct ContextMenuIdentifierView: UIViewRepresentable { - let accessoryView: () -> AccessoryItem + 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 + func makeUIView(context: Context) -> ContextMenuIdentifierUIView { + let uiView = ContextMenuIdentifierUIView( + accessoryView: accessoryView + ) + return uiView } - func updateUIView(_ uiView: UIViewType, context: Context) {} + func updateUIView(_ uiView: ContextMenuIdentifierUIView, context: Context) { + uiView.hostingView.rootView = accessoryView.content + } } -class ContextMenuIdentifierUIView: UIView { - let accessoryView: UIView - let configuration: ContextMenuAccessoryConfiguration - - init(accessoryView: UIView, configuration: ContextMenuAccessoryConfiguration) { +class AnyContextMenuIdentifierUIView: UIView { + var accessoryView: UIView? + var configuration: ContextMenuAccessoryConfiguration + + init(accessoryView: UIView? = nil, configuration: ContextMenuAccessoryConfiguration) { self.accessoryView = accessoryView self.configuration = configuration - super.init(frame: .zero) - + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class ContextMenuIdentifierUIView: AnyContextMenuIdentifierUIView { + let hostingView: _UIHostingView + + init(accessoryView: AccessoryItem) { + self.hostingView = _UIHostingView(rootView: accessoryView.content) + super.init(accessoryView: hostingView, configuration: accessoryView.configuration) + UIContextMenuInteraction.swizzle_delegate_getAccessoryViewsForConfigurationIfNeeded() } diff --git a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift index 04102b5..f2d2097 100644 --- a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift +++ b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift @@ -33,9 +33,10 @@ extension UIContextMenuInteraction { } @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 + { + 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..934a893 100644 --- a/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift +++ b/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift @@ -11,14 +11,18 @@ import ContextMenuAccessoryStructs private struct AccessoryWrapper: View { let configuration: ContextMenuAccessoryConfiguration - let accessory: () -> AccessoryView + let accessory: AccessoryView var body: some View { - ContextMenuIdentifierView(accessoryView: { - AccessoryItem(configuration: configuration) { - accessory() - } - }) + ContextMenuIdentifierView( + accessoryView: AccessoryItem( + configuration: configuration, + content: { + accessory + } + ) + ) + .accessibilityHidden(true) } } @@ -65,7 +69,7 @@ public extension View { location: ContextMenuAccessoryLocation? = nil, alignment: ContextMenuAccessoryAlignment? = nil, trackingAxis: ContextMenuAccessoryTrackingAxis? = nil, - @ViewBuilder accessory: @escaping () -> AccessoryView + @ViewBuilder accessory: () -> AccessoryView ) -> some View { var config = ContextMenuAccessoryConfiguration() if let placement = placement { config.placement = placement } @@ -74,10 +78,9 @@ public extension View { if let trackingAxis = trackingAxis { config.trackingAxis = trackingAxis } let wrapped = background { - AccessoryWrapper(configuration: config, accessory: accessory) - .accessibilityHidden(true) + AccessoryWrapper(configuration: config, accessory: accessory()) } - return wrapped.id(config.id) + return wrapped } } From ff2c18628a94ec3ad3263ff737edad739e181e3a Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sat, 6 Sep 2025 15:06:51 -0700 Subject: [PATCH 02/12] Fix target os --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 053faf2..a57ebc3 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. From 8eb0682b3e396e0c551511173ed4d5cddf12a4b3 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sun, 7 Sep 2025 10:54:42 -0700 Subject: [PATCH 03/12] Fix xcframework os --- .../Info.plist | 44 ------- .../ContextMenuAccessoryStructs | Bin 51808 -> 0 bytes .../Headers/ContextMenuAccessoryStructs.h | 24 ---- .../Info.plist | Bin 786 -> 0 bytes .../Modules/module.modulemap | 6 - .../_CodeSignature/CodeResources | 124 ------------------ .../ContextMenuAccessoryStructs | Bin 84240 -> 0 bytes .../Headers/ContextMenuAccessoryStructs.h | 24 ---- .../Info.plist | Bin 766 -> 0 bytes .../Modules/module.modulemap | 6 - .../_CodeSignature/CodeResources | 124 ------------------ Package.swift | 9 +- .../ContextMenuAccessoryStructs.m | 8 ++ .../include/ContextMenuAccessoryStructs.h | 7 + 14 files changed, 21 insertions(+), 355 deletions(-) delete mode 100644 Frameworks/ContextMenuAccessoryStructs.xcframework/Info.plist delete mode 100755 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs delete mode 100644 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Headers/ContextMenuAccessoryStructs.h delete mode 100644 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Info.plist delete mode 100644 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Modules/module.modulemap delete mode 100644 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources delete mode 100755 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs delete mode 100644 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Headers/ContextMenuAccessoryStructs.h delete mode 100644 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Info.plist delete mode 100644 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/Modules/module.modulemap delete mode 100644 Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources create mode 100644 Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m create mode 100644 Sources/ContextMenuAccessoryStructs/include/ContextMenuAccessoryStructs.h diff --git a/Frameworks/ContextMenuAccessoryStructs.xcframework/Info.plist b/Frameworks/ContextMenuAccessoryStructs.xcframework/Info.plist deleted file mode 100644 index c3e1b61..0000000 --- a/Frameworks/ContextMenuAccessoryStructs.xcframework/Info.plist +++ /dev/null @@ -1,44 +0,0 @@ - - - - - AvailableLibraries - - - BinaryPath - ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs - LibraryIdentifier - ios-arm64_x86_64-simulator - LibraryPath - ContextMenuAccessoryStructs.framework - SupportedArchitectures - - arm64 - x86_64 - - SupportedPlatform - ios - SupportedPlatformVariant - simulator - - - BinaryPath - ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs - LibraryIdentifier - ios-arm64 - LibraryPath - ContextMenuAccessoryStructs.framework - SupportedArchitectures - - arm64 - - SupportedPlatform - ios - - - CFBundlePackageType - XFWK - XCFrameworkFormatVersion - 1.0 - - diff --git a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs b/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs deleted file mode 100755 index c93f4072ef0d636a4749430349b08fb398ad0046..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51808 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 j03-kjKmw2eBmfCO0+0YC00}?>kN_kA2|xn>7X diff --git a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Headers/ContextMenuAccessoryStructs.h b/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Headers/ContextMenuAccessoryStructs.h deleted file mode 100644 index d27d8bb..0000000 --- a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Headers/ContextMenuAccessoryStructs.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// ContextMenuAccessoryStructs.h -// ContextMenuAccessoryStructs -// -// Created by Seb Vidal on 11/05/2025. -// - -#import - -//! Project version number for ContextMenuAccessoryStructs. -FOUNDATION_EXPORT double ContextMenuAccessoryStructsVersionNumber; - -//! Project version string for ContextMenuAccessoryStructs. -FOUNDATION_EXPORT const unsigned char ContextMenuAccessoryStructsVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - -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/ContextMenuAccessoryStructs.framework/Info.plist b/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Info.plist deleted file mode 100644 index 15fba5dd931a152307066b461dbde6186beb6645..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 786 zcmaJ;%Wl&^6!i?R@@m|MLf^D0&_cknlho>l1&tFa3ZWr((ttwE#GWLhjvx3DH4=yo zE07Q%R!A($nhig|53oaY%{L%^06Vdju9(fe=bm$}X3n&2N?ecUf3q_Ho;Z2x^qGmX z=gwcaIGJFlrjwUuE?=3Qo1a}+T)Milnp#`EcKycM`Yn}BZu^Al6{PiuiFf27QrWqD zaoaa_iY45~)Upk1dNtf7mZ`GSF;_Uin(v`D#m9yvJ&=+P!H&u<#mbV|u_U5-fgd@8 z26o(_YsKsXWCR%k=Pp zSHY%#Pt&mLTFxK{z}LLsksF$2a4v55iH<1uAE_*Iyi%;}mSutC50{&y+Oxv{ekiDH zDqMnWn_@2Lh6%YGrG10bC^Ea8pz@7OrjW^QHi;eDELRb>xLhknAy5P{%QpnEAmsQj z8#h_N!ozeOIfj^HCYVj8%ygOO%zNe=^PTy{`~j0-1*E}Ua34Gc`@jb;z-#asd<8$i xPjCeO!ey9(>o5)PKoM@iCs2n5bl^+)3ci7F;XC*Neq@FRf>C8|M>Q(&(?3s&^;G}> diff --git a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Modules/module.modulemap b/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/Modules/module.modulemap deleted file mode 100644 index ba0f298..0000000 --- a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/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/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources b/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources deleted file mode 100644 index 2ef3b14..0000000 --- a/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64/ContextMenuAccessoryStructs.framework/_CodeSignature/CodeResources +++ /dev/null @@ -1,124 +0,0 @@ - - - - - files - - Headers/ContextMenuAccessoryStructs.h - - b9Luviw1b76Lr6p1xgNrWNfPCVY= - - Info.plist - - oS7eZhHij+Sz+QXDnS0j7qeyoFY= - - 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/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs b/Frameworks/ContextMenuAccessoryStructs.xcframework/ios-arm64_x86_64-simulator/ContextMenuAccessoryStructs.framework/ContextMenuAccessoryStructs deleted file mode 100755 index 0a0b35e35ade51593cf2f4e96e6bc4792fa9ea99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84240 zcmeI*U2GKB6~OVkW*tLpObmfEAGBLxx#q)baIl;YBGxgEF$u=CE$a4x&8|I|1$&q5 z2ldue*{U^aN+hQUwOAFEDS|XjS~($kXd-GFHBg~635lvIsG#xyN<|2%)GFWz({pBK zV`J3CDt#&XU+K)fXXehGJHOqRS!wnjAA9|G#+W>hF$GdjGbSKa`>!X}os#BpuH?aezr}z0cJW!Qc~(O^?^FYRuki_Wm;N32r{Dou$ZsB-mjm zvY9EdN#aWqdMw)`5Z@X~ZmO({Mb%-g6;0K4bXbW*ES_#n##0^1gnwf^)MI@+7T=Pq zmp!CxmK&3?ry`rBlHc#jB33E-q z?pUbPieJ}9M=X=jdG*R>y3`BZTphK{m#~{9#tb*uyKZU|C||I!xn*5LOYPc+a(p>_ zKIyckzOF4-r;25_yL0(;C+MG@h7bCwn%5F+n+<#RL(3bJ^8|nH||{V*3LsE zSKcn1cV^$^&2zWz?mE>oXUE}Pd(yt=nqIVuQ@zjTuNfL{Y1*6_Id;$fic9a;9K5UU z@{SLR9^O{n-&gWk)te))4U~R7c%l7DWboYiuDY`|W9h!O!HVfM6-xsr{^%XCX8if{ z?{574!C4!Y%`eIX zy!6Xo36Oqg>_3YfJzc16eRifGfB*srAbML{f zaxY_+%ewdWN`e7nucmmvn=h4I??c^n4jIX{JtVbS6iPHpOe}g_Rg}bewt^HV5G;wXe z?pUbPieJ}9$19W2dG!XIH4GQJc}8+A^Cj$g*GjF>HzIJ|35+V@=3i&k-}_u2e4L&Ggin=>QF?%7{) z>HV66chz0q@j=nU+p7EfNCuGbgr9(?Ed&yVjtKQuQw(0caQPrvj^d+g}nHy-i+<;00CdF$HquWo;J z`*-?J9^Se7nI+HP?D0h=dG&|iKe_$E!^xND4edYroArAS94!tP&S@+8R(@G&m3#pD zoz!>y - -//! Project version number for ContextMenuAccessoryStructs. -FOUNDATION_EXPORT double ContextMenuAccessoryStructsVersionNumber; - -//! Project version string for ContextMenuAccessoryStructs. -FOUNDATION_EXPORT const unsigned char ContextMenuAccessoryStructsVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - -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 09eacdbb97d4cb950f657ed1e37c773e71009146..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 766 zcmaJ;&2G~`5Z+y&EhP}QDU`M}gp{B5V%bTEkT}sekwQruYLf!0Vma$gvSRJE{1Y`G zaYL#Q5)Z%$j$9BDN1mcDz$5SkNUT#^sW`Bgnfd1XMl;{`JVt|1QGQ8F5S=@JVP66NoR%(sy z$h8^K$cQlRIm8Y3$$)a#lor!o{fOWww0ex30h_j<(mo}=DXpi>rrYNl#i7t!{ngj`y-E&R$F5M%5KbTviwA@{>XsfRvw2WOj+=ZQA4+sq34+;;*~%BIRh z^infw-0>)jH_K{9N}XKXoz|z@EKGr^Ue|Ue>=&9L(wKOl8P74d$zc)``=l0isS`PS zdLr&VYYHbRCJoD>Y}_eT>ZNLVp9nkGYTOOUQP?7G^ax`T1l%8s3nLtgD>gBA z)V7%XAF5uJ6{FEQ*wIy4`MfSpr0pShi7xgKrf~nMYD&d;n&o+n$Re9~5y*^<92qmq z9muMwbV{XqseC`yd$B%9^@jRDu842?2aZugt(KLJTCJ-U<;%TI2}%gefih?U0$ziU z;2Zb}PQY)NfqA$EAHv753n>iX8+Z&q!LRT;`~gqk9|X}1nnxLwM+H - - - - 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 a57ebc3..0772021 100644 --- a/Package.swift +++ b/Package.swift @@ -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/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m b/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m new file mode 100644 index 0000000..9ff8cf9 --- /dev/null +++ b/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m @@ -0,0 +1,8 @@ +// +// NSObject+ContextMenuAccessoryStructs_m.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; From 2f1b734e1de31d43afd1a30effc5ba527382f761 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Mon, 8 Sep 2025 14:19:26 -0700 Subject: [PATCH 04/12] Add proxy to dismiss --- Sources/MenuWithAView/AccessoryItem.swift | 47 +++--------- .../ContextMenuIdentifierView.swift | 53 ++++++++++---- ...ction+AccessoryViewWithConfiguration.swift | 2 +- .../UIContextMenuInteraction+Swizzle.swift | 2 +- .../View/View+ContextMenuAccessories.swift | 71 ++++++++++++++++--- 5 files changed, 113 insertions(+), 62 deletions(-) diff --git a/Sources/MenuWithAView/AccessoryItem.swift b/Sources/MenuWithAView/AccessoryItem.swift index 3dc4ba6..6154b8e 100644 --- a/Sources/MenuWithAView/AccessoryItem.swift +++ b/Sources/MenuWithAView/AccessoryItem.swift @@ -8,43 +8,6 @@ import SwiftUI import ContextMenuAccessoryStructs -struct AccessoryItem: View { - let configuration: Configuration - let content: Content - - init( - configuration: ContextMenuAccessoryConfiguration, - @ViewBuilder content: () -> Content - ) { - self.configuration = configuration - self.content = content() - } - - init( - placement: Placement, - @ViewBuilder content: () -> 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 @@ -83,6 +46,16 @@ 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 { var location: ContextMenuAccessoryLocation = .preview diff --git a/Sources/MenuWithAView/ContextMenuIdentifierView.swift b/Sources/MenuWithAView/ContextMenuIdentifierView.swift index 8b4e679..b3f06c6 100644 --- a/Sources/MenuWithAView/ContextMenuIdentifierView.swift +++ b/Sources/MenuWithAView/ContextMenuIdentifierView.swift @@ -9,27 +9,34 @@ import UIKit import SwiftUI import ContextMenuAccessoryStructs -struct ContextMenuIdentifierView: UIViewRepresentable { - let accessoryView: AccessoryItem - - func makeUIView(context: Context) -> ContextMenuIdentifierUIView { +struct ContextMenuIdentifierView: UIViewRepresentable { + let configuration: ContextMenuAccessoryConfiguration + let accessory: (ContextMenuProxy) -> AccessoryView + + func makeUIView( + context: Context + ) -> ContextMenuIdentifierUIView { let uiView = ContextMenuIdentifierUIView( - accessoryView: accessoryView + configuration: configuration, + accessory: accessory ) return uiView } - func updateUIView(_ uiView: ContextMenuIdentifierUIView, context: Context) { - uiView.hostingView.rootView = accessoryView.content + func updateUIView( + _ uiView: ContextMenuIdentifierUIView, + context: Context + ) { + uiView.update(accessory) } } class AnyContextMenuIdentifierUIView: UIView { var accessoryView: UIView? var configuration: ContextMenuAccessoryConfiguration + weak var interaction: UIContextMenuInteraction? - init(accessoryView: UIView? = nil, configuration: ContextMenuAccessoryConfiguration) { - self.accessoryView = accessoryView + init(configuration: ContextMenuAccessoryConfiguration) { self.configuration = configuration super.init(frame: .zero) } @@ -39,12 +46,16 @@ class AnyContextMenuIdentifierUIView: UIView { } } -class ContextMenuIdentifierUIView: AnyContextMenuIdentifierUIView { - let hostingView: _UIHostingView +class ContextMenuIdentifierUIView: AnyContextMenuIdentifierUIView { + private var hostingView: _UIHostingView! - init(accessoryView: AccessoryItem) { - self.hostingView = _UIHostingView(rootView: accessoryView.content) - super.init(accessoryView: hostingView, configuration: accessoryView.configuration) + init( + configuration: ContextMenuAccessoryConfiguration, + accessory: (ContextMenuProxy) -> AccessoryView + ) { + super.init(configuration: configuration) + self.hostingView = _UIHostingView(rootView: accessory(makeProxy())) + accessoryView = hostingView UIContextMenuInteraction.swizzle_delegate_getAccessoryViewsForConfigurationIfNeeded() } @@ -52,6 +63,20 @@ class ContextMenuIdentifierUIView: AnyContextMenuIdentifierUIView 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/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift index bb074a0..47d4883 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 diff --git a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift index f2d2097..ff4a4f7 100644 --- a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift +++ b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift @@ -36,7 +36,7 @@ extension UIContextMenuInteraction { 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 934a893..ba9268f 100644 --- a/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift +++ b/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift @@ -11,16 +11,12 @@ import ContextMenuAccessoryStructs private struct AccessoryWrapper: View { let configuration: ContextMenuAccessoryConfiguration - let accessory: AccessoryView + let accessory: (ContextMenuProxy) -> AccessoryView var body: some View { ContextMenuIdentifierView( - accessoryView: AccessoryItem( - configuration: configuration, - content: { - accessory - } - ) + configuration: configuration, + accessory: accessory ) .accessibilityHidden(true) } @@ -76,9 +72,66 @@ public extension View { if let location = location { config.location = location } if let alignment = alignment { config.alignment = alignment } if let trackingAxis = trackingAxis { config.trackingAxis = trackingAxis } - + + let accessory = accessory() + let wrapped = background { + AccessoryWrapper(configuration: config, accessory: { _ in accessory }) + } + return wrapped + } + + /// 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: @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()) + AccessoryWrapper(configuration: config, accessory: accessory) } return wrapped } From fcc16906ec7ab2d872efa82069c030638d0e8537 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Thu, 11 Sep 2025 15:23:42 -0700 Subject: [PATCH 05/12] Cleanup --- .../ContextMenuIdentifierView.swift | 18 ++++++++++++- .../View/View+ContextMenuAccessories.swift | 25 +++++++------------ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Sources/MenuWithAView/ContextMenuIdentifierView.swift b/Sources/MenuWithAView/ContextMenuIdentifierView.swift index b3f06c6..1872761 100644 --- a/Sources/MenuWithAView/ContextMenuIdentifierView.swift +++ b/Sources/MenuWithAView/ContextMenuIdentifierView.swift @@ -9,7 +9,23 @@ import UIKit import SwiftUI import ContextMenuAccessoryStructs -struct ContextMenuIdentifierView: UIViewRepresentable { +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) + } +} + + +struct ContextMenuIdentifierViewBody: UIViewRepresentable { let configuration: ContextMenuAccessoryConfiguration let accessory: (ContextMenuProxy) -> AccessoryView diff --git a/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift b/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift index ba9268f..c45250a 100644 --- a/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift +++ b/Sources/MenuWithAView/Extensions/View/View+ContextMenuAccessories.swift @@ -9,19 +9,6 @@ import SwiftUI import UIKit import ContextMenuAccessoryStructs -private struct AccessoryWrapper: View { - let configuration: ContextMenuAccessoryConfiguration - let accessory: (ContextMenuProxy) -> AccessoryView - - var body: some View { - ContextMenuIdentifierView( - configuration: configuration, - accessory: accessory - ) - .accessibilityHidden(true) - } -} - public extension View { /// Adds an accessory view to instances of `.contextMenu`. /// @@ -75,7 +62,10 @@ public extension View { let accessory = accessory() let wrapped = background { - AccessoryWrapper(configuration: config, accessory: { _ in accessory }) + ContextMenuIdentifierView( + configuration: config, + accessory: { _ in accessory } + ) } return wrapped } @@ -89,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``. /// @@ -131,7 +121,10 @@ public extension View { if let trackingAxis = trackingAxis { config.trackingAxis = trackingAxis } let wrapped = background { - AccessoryWrapper(configuration: config, accessory: accessory) + ContextMenuIdentifierView( + configuration: config, + accessory: accessory + ) } return wrapped } From 8864d1a88e42bfc6809a1d088189d0fe29aea5f3 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Thu, 11 Sep 2025 15:24:35 -0700 Subject: [PATCH 06/12] Cleanup --- CONTRIBUTING.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a46a6e..1823faa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Please read and follow the [Code of Conduct](./CODE_OF_CONDUCT.md). We're commit Before opening a new issue, search existing issues to avoid duplicates. When filing a bug report, please include: - MenuWithAView version (e.g. `0.1.2`) and your Swift/Xcode versions -- Target platform (iOS 15.0+), device or simulator +- Target platform (iOS 16.0+), device or simulator - A concise description of the problem and steps to reproduce - Minimal code snippet or sample project demonstrating the issue - Any relevant console logs or screenshots diff --git a/README.md b/README.md index 92fc327..02a5b15 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

From 8e770000e5aa39a1df5427f33b7582d03c85315b Mon Sep 17 00:00:00 2001 From: Aether <64797587+Aeastr@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:02:23 -0400 Subject: [PATCH 07/12] Update xcodebuild to use sdk name instead of path --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 6199ee3..f63f732 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -53,4 +53,4 @@ jobs: - name: Build and Test (${{ matrix.config }}) run: | echo "Using simulator destination: ${{ steps.find_simulator.outputs.SIMULATOR_DESTINATION }}" - xcodebuild build -scheme MenuWithAView -sdk $(xcrun --sdk iphonesimulator --show-sdk-path) -destination "${{ steps.find_simulator.outputs.SIMULATOR_DESTINATION }}" SWIFT_VERSION=6.0 + xcodebuild build -scheme MenuWithAView -sdk iphonesimulator -destination "${{ steps.find_simulator.outputs.SIMULATOR_DESTINATION }}" SWIFT_VERSION=6.0 From 1e37b65e8127fd38f7116da4c8635879fba1fa91 Mon Sep 17 00:00:00 2001 From: Aether <64797587+Aeastr@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:03:38 -0400 Subject: [PATCH 08/12] Update README with contextMenuAccessory details Expanded documentation for the `contextMenuAccessory` SwiftUI modifier, clarifying its variants and parameters. Added examples for both basic usage and programmatic dismissal. Updated iOS version badge from 18+ to 16+. --- README.md | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 02a5b15..cdbd3e7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Swift Version - iOS + iOS License: MIT @@ -29,23 +29,25 @@ ## 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 receives a `ContextMenuProxy` and returns the accessory view. -### Example +### Examples + +#### Basic Usage ```swift Text("Turtle Rock") @@ -70,6 +72,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** From afb9b465eabb917b42e33ed5c5c0cf48fcb33bc5 Mon Sep 17 00:00:00 2001 From: Aether <64797587+Aeastr@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:07:42 -0400 Subject: [PATCH 09/12] Clarify accessory view builder documentation --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cdbd3e7..bb30de0 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,9 @@ *(Default: `.leading`)* - `trackingAxis`: The axis along which the accessory tracks user interaction. *(Default: `[.xAxis, .yAxis]`)* -- `accessory`: A view builder that receives a `ContextMenuProxy` and returns the accessory view. +- `accessory`: A view builder that returns the accessory view. Available in two variants: + - Simple: `@ViewBuilder accessory: () -> AccessoryView` + - With proxy: `@ViewBuilder accessory: (ContextMenuProxy) -> AccessoryView` ### Examples From c9f53948a7398f366958a318b766d663ad8cec46 Mon Sep 17 00:00:00 2001 From: Aether <64797587+Aeastr@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:13:53 -0400 Subject: [PATCH 10/12] refactor context menu accessory swizzling logic --- .../ContextMenuAccessoryStructs.m | 2 +- ...ction+AccessoryViewWithConfiguration.swift | 6 +++-- .../UIContextMenuInteraction+Swizzle.swift | 22 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m b/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m index 9ff8cf9..eb05a0c 100644 --- a/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m +++ b/Sources/ContextMenuAccessoryStructs/ContextMenuAccessoryStructs.m @@ -1,5 +1,5 @@ // -// NSObject+ContextMenuAccessoryStructs_m.m +// ContextMenuAccessoryStructs.m // MenuWithAView // // Created by Nathan Tannar on 2025-09-07. diff --git a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift index 47d4883..b622475 100644 --- a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift +++ b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+AccessoryViewWithConfiguration.swift @@ -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 ff4a4f7..aebc6ea 100644 --- a/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift +++ b/Sources/MenuWithAView/Extensions/UIContextMenuInteraction/UIContextMenuInteraction+Swizzle.swift @@ -9,27 +9,25 @@ 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] { From b69e8b52e3afb63539c6128d5c735c8b597d70e4 Mon Sep 17 00:00:00 2001 From: Aether <64797587+Aeastr@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:21:06 -0400 Subject: [PATCH 11/12] add advanced test views for context menu features --- Sources/MenuWithAView/AccessoryItem.swift | 2 +- Sources/MenuWithAView/Example.swift | 199 +++++++++++++++++++++- 2 files changed, 199 insertions(+), 2 deletions(-) diff --git a/Sources/MenuWithAView/AccessoryItem.swift b/Sources/MenuWithAView/AccessoryItem.swift index 6154b8e..9ee3257 100644 --- a/Sources/MenuWithAView/AccessoryItem.swift +++ b/Sources/MenuWithAView/AccessoryItem.swift @@ -30,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) { diff --git a/Sources/MenuWithAView/Example.swift b/Sources/MenuWithAView/Example.swift index 8b738c0..0ed3e15 100644 --- a/Sources/MenuWithAView/Example.swift +++ b/Sources/MenuWithAView/Example.swift @@ -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() +} From 0cb8598479e3de17b0106f86f10b22c9d7376042 Mon Sep 17 00:00:00 2001 From: Aether <64797587+Aeastr@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:23:08 -0400 Subject: [PATCH 12/12] change tracking axis from yAxis to xAxis in demo --- Sources/MenuWithAView/Example.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MenuWithAView/Example.swift b/Sources/MenuWithAView/Example.swift index 0ed3e15..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)