Displaying NSMenuItem in SwiftUI on it's own - swift

I have been trying to display a custom NSMenuItem (for a preview page of a menu manager) inside a SwiftUI view. But I can't achieve it. I have figured it needs to wrapped inside a menu first, and thought that there might be a way to pop the menu pragmatically but sadly, those efforts have failed and the app crashes.
So far, my code looks like this:
import Foundation
import SwiftUI
struct NSMenuItemView: NSViewRepresentable {
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeNSView(context: Context) -> NSView {
let view = NSView()
let menu = NSMenu()
let item = menu.addItem(withTitle: "Do Action", action: #selector(Coordinator.valueChanged(_:)), keyEquivalent: "")
item.target = context.coordinator
view.menu = menu
return view
}
func updateNSView(_ view: NSView, context: Context) {
// App crashes here :/
view.menu?.popUpMenuPositioningItem(
positioning: view.menu?.item(at: 0),
at: NSPoint(x: 0, y: 0),
in: view
)
}
}
extension NSMenuItemView {
final class Coordinator: NSObject {
var parent: NSMenuItemView
init(_ parent: NSMenuItemView) {
self.parent = parent
}
#objc
func valueChanged(_ sender: NSMenuItem) {
}
}
}
Am I missing something here? Is it even possible to just pragmatically display NSMenuItem?
The NSMenu comfors to NSViewRepresentable so I figured it might just workout, and have seen answers on StackOverflow (granted date a while back) showing similar code that should work.
Without the popUpMenuPositioningItem it works - in a way I guess - when I right click in the View, the MenuItem Appears. But I would like to be able to display the menu without the right click, just like that.

The problem is that the menu is shown while the view are still rendering so that the crash happens. To avoid this you should call popUp(positioning:at:in) after the your view appears on the screen. The way to achieve it, we have to use publisher to trigger an event to show menu inside onAppear modifier and listen it inside Coordinator. Here is the sample for that solution.
struct ContentView: View {
let menuPopUpTrigger = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
NSMenuItemView(menuPopUpTrigger)
Text("Hello, world!")
}
.padding()
.onAppear {
/// trigger an event when `onAppear` is executed
menuPopUpTrigger.send()
}
}
}
struct NSMenuItemView: NSViewRepresentable {
let base = NSView()
let menu = NSMenu()
var menuPopUpTrigger: PassthroughSubject<Void, Never>
init(_ menuPopUpTrigger: PassthroughSubject<Void, Never>) {
self.menuPopUpTrigger = menuPopUpTrigger
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeNSView(context: Context) -> NSView {
let item = menu.addItem(withTitle: "Do Action", action: #selector(Coordinator.valueChanged(_:)), keyEquivalent: "")
item.target = context.coordinator
base.menu = menu
context.coordinator.bindTrigger(menuPopUpTrigger)
return base
}
func updateNSView(_ view: NSView, context: Context) { }
}
extension NSMenuItemView {
final class Coordinator: NSObject {
var parent: NSMenuItemView
var cancellable: AnyCancellable?
init(_ parent: NSMenuItemView) {
self.parent = parent
}
#objc func valueChanged(_ sender: NSMenuItem) { }
/// bind trigger to listen an event
func bindTrigger(_ trigger: PassthroughSubject<Void, Never>) {
cancellable = trigger
.delay(for: .seconds(0.1), scheduler: RunLoop.main)
.sink { [weak self] in
self?.parent.menu.popUp(
positioning: self?.parent.menu.item(at: 0),
at: NSPoint(x: 0, y: 0),
in: self?.parent.base
)
}
}
}
}
I hope it will help you to get what you want.

Related

How to open a SwiftUI view by tapping an entity in RealityKit?

I am using SwiftUI with RealityKit. As displayed in the code below, I have a plane entity that when tapped simply prints the name of the entity. What approach should I take toward navigating to a new view when I tap the entity? It would be preferable to navigate as with a navigation link in a normal view, but if that is not possible then perhaps a fullScreenCover?
ARViewContainer.swift:
class Coordinator: NSObject {
weak var view: ARView?
#objc func handleTap(_ recognizer: UITapGestureRecognizer) {
guard let view = self.view else { return }
let tapLocation = recognizer.location(in: view)
if let entity = view.entity(at: tapLocation) as? ModelEntity {
print(entity.name)
}
}
}
struct ARViewContainer: UIViewRepresentable {
typealias UIViewType = ARView
func makeUIView(context: Context) -> ARView{
let arView = ARView(frame: .zero, cameraMode: .ar, automaticallyConfigureSession: true)
context.coordinator.view = arView
arView.addGestureRecognizer(UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap)))
arView.scene.anchors.removeAll()
let anchor = AnchorEntity()
let plane = MeshResource.generatePlane(width: 1, height: 1)
var material = UnlitMaterial()
material.color = .init(tint: .white,
texture: .init(try! .load(named: "instagram")))
let planeEntity = ModelEntity(mesh: plane, materials: [material])
planeEntity.generateCollisionShapes(recursive: true)
planeEntity.name = "Plane Entity"
planeEntity.position.z -= 1.0
planeEntity.setParent(anchor)
arView.scene.addAnchor(anchor)
return arView
}
func updateUIView(_ uiView: ARView, context: Context){
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
}
ContentView.swift
struct ContentView: View {
#State var open = false
var body: some View {
NavigationView{
ZStack {
ARViewContainer()
.ignoresSafeArea(.all)
}
}
}
}
View I want to navigate to:
struct TestView : View {
var body : some View {
VStack{
Text("Test View")
}
}
}
Manage the state of the view in an observable object and modify it from your AR view.
struct ContentView: View {
#ObservedObject var settings = Settings.shared
var body: some View {
NavigationView {
ZStack {
ARViewContainer()
.ignoresSafeArea(.all)
NavigationLink("", isActive: $settings.shouldOpenDetailsView) {
TestView()
}
}
}
}
}
class Settings: ObservableObject {
static let shared = Settings()
#Published var shouldOpenDetailsView = false
}
class Coordinator: NSObject {
weak var view: ARView?
#objc func handleTap(_ recognizer: UITapGestureRecognizer) {
guard let view = self.view else { return }
let tapLocation = recognizer.location(in: view)
if let entity = view.entity(at: tapLocation) as? ModelEntity {
Settings.shared.shouldOpenDetailsView = true
}
}
}

Custom Mouse Up/Down View Modifier Prevents View Hierarchy from Updating

I'm trying to implement a custom view modifier to detect mouse up/down events within a SwiftUI view hierarchy. This is my solution so far:
extension View {
func onMouseDown(action: #escaping (Bool) -> Void) -> some View {
MouseDownView(action: action) { self }
}
}
struct MouseDownView<Content: View>: View {
let action: (Bool) -> Void
let content: () -> Content
init(action: #escaping (Bool) -> Void, #ViewBuilder content: #escaping () -> Content) {
self.action = action
self.content = content
}
var body: some View {
MouseDownRepresentable(action: action, content: content())
}
}
struct MouseDownRepresentable<Content: View>: NSViewRepresentable {
let action: (Bool) -> Void
let content: Content
func makeNSView(context: Context) -> NSHostingView<Content> {
MouseDownHostingView(action: action, rootView: content)
}
func updateNSView(_ view: NSHostingView<Content>, context: Context) {
}
}
class MouseDownHostingView<Content: View>: NSHostingView<Content> {
let action: (Bool) -> Void
init(action: #escaping (Bool) -> Void, rootView: Content) {
self.action = action
super.init(rootView: rootView)
}
required init(rootView: Content) {
fatalError()
}
required init?(coder: NSCoder) {
fatalError()
}
override func mouseDown(with event: NSEvent) {
action(true)
}
override func mouseUp(with event: NSEvent) {
action(false)
}
}
It works, in the sense that the closure passed to onMouseDown is getting called whenever a mouse up/down event occurs within the the view that it was applied to (even taking non-rectangular shapes into account). Unfortunately though, there is an issue that prevents the view from updating when any of its #State variables get modified inside the passed closure.
Example usage:
struct PlayerView<Content: View>: View {
private let content: () -> Content
#State var isPlaying = true
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
ZStack {
content()
ZStack {
Circle()
.foregroundStyle(.secondary)
Image(systemName: isPlaying ? "play.fill" : "pause.fill")
.foregroundColor(.white)
.font(.system(size: 40))
}
.onMouseDown { isDown in
if isDown {
// ...
} else {
isPlaying.toggle()
}
}
.frame(width: 80, height: 80)
}
}
}
isPlaying gets toggled everytime the circular view is clicked, but the image never updates. How can this be? Does this configuration of an NSViewRepresentable inside of an NSHostingView somehow mean, that the modified view is no longer formally part of the original view hierarchy, and therefore not allowed to update it?
I tried wrapping isPlaying as a published property inside of an ObservableObject that gets attached to the view as a #StateObject, but it showed the same behavior.
Does anyone know what's going on here and/or how to work around it?
Yes, SwiftUI does not see dependency through NSView bridge anymore, so instead the content should remain in SwiftUI world, but modifier/handler be placed above it, like
extension View {
func onMouseDown(action: #escaping (Bool) -> Void) -> some View {
self.overlay(
MouseDownView(action: action) {
Color.clear.contentShape(Rectangle())
})
}
}
Tested with Xcode 13.4 / macOS 12.5

SwiftUI Binding Edits Wrong TextField after Item Reordered

Xcode 13 beta 5, iOS 14, macOS 11.6
I have a parent SwiftUI view that lists some children. Each child is bound to an NSViewRepresentable. Everything works and I can edit the values as expected. But once I reorder the items in the list and edit a field, it edits the wrong field. It appears that the binding remains intact from the previous item order.
Here's what that looks like:
Here's the parent:
struct ParentView: View {
#StateObject var model = ThingModel.shared
var body: some View {
VStack{
ForEach($model.things){ $thing in
ChildView(thing: $thing)
//Reorder
.onDrag{
model.draggedThing = thing
return NSItemProvider(object: NSString())
}
}
Text("Value: \(model.value)").font(.title)
}
.frame(width:300, height: 200)
}
}
...and here's the child view:
struct ChildView: View {
#Binding var thing: Thing
#StateObject var model = ThingModel.shared
var body: some View{
HStack{
GrowingField(text: $thing.text, submit: {
model.value = thing.text
print(thing.text)
})
Text(" = ")
.opacity(0.4)
}
.padding(10)
.onDrop(of: [UTType.text], delegate: ThingReorderDelegate(hoveredThing: thing))
}
}
Last of all, here is the NSViewRepresentable which is called GrowingField. For simplicity, I have omitted the NSTextField subclass.
struct GrowingField: NSViewRepresentable{
#Binding var text: String
var submit:(() -> Void)? //Hit enter
func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField()
textField.delegate = context.coordinator
textField.stringValue = text
return textField
}
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = text
context.coordinator.textBinding = $text
}
//Delegates
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, NSTextFieldDelegate {
let parent: GrowingField
var textBinding : Binding<String>?
init(_ field: GrowingField) {
self.parent = field
}
func controlTextDidChange(_ obj: Notification) {
guard let textField = obj.object as? NSTextField else { return }
self.textBinding?.wrappedValue = textField.stringValue
}
//Listen for certain keyboard keys
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
switch commandSelector{
case #selector(NSStandardKeyBindingResponding.insertNewline(_:)):
//- Enter -
parent.submit?()
textView.window?.makeFirstResponder(nil) //Blur cursor
return true
default:
return false
}
}
}
}
Why does the binding to the NSViewRepresentable not follow the field after it is reordered?
Here is a sample project to download and try it out.
I believe the issue (bug?) is with the ForEach-generated binding.
If you forego the generated binding and create your own, everything seems to work as expected.
Added to the ThingModel:
func bindingForThing(id: String) -> Binding<Thing> {
.init {
self.things.first { $0.id == id }!
} set: { newThing in
self.things = self.things.map { $0.id == id ? newThing : $0 }
}
}
And the ParentView:
ForEach(model.things){ thing in
ChildView(thing: model.bindingForThing(id: thing.id))

Handle keyboard inputs in NSTextView embedded via NSViewRepresentable? (SwiftUI/MacOS)

I'm new to SwiftUI and am utterly confused.. I managed to embed a NSTextView into my SwiftUI View and bind its text with the below code.
What I don't understand; is there a way to handle keyboard inputs to the NSTextView and change its text accordingly (e.g. CMD + R sets the text color of the selected text to red)? Is there even any way to interact with UI-Elements in SwiftUI?
"RichTextField"
struct RichTextField: NSViewRepresentable {
typealias NSViewType = NSTextView
#Binding var attributedString: NSAttributedString
func makeNSView(context: Context) -> NSTextView {...
// [...]
}
View
struct EditWindow: View {
#ObservedObject var model: EditEntryViewModel
#Binding var isPresented: Bool
var body: some View {
RichTextField(attributedString: self.$model.answer1, isEditable: true)
// [...]
}
}
Furthermore, I've managed to set up a menu command in the AppDelegate, but how could I use this to change the text (at a certain position) in a NSTextView of an arbitrary View?
#IBAction func setTagImportant(_ sender: Any) {
print("setTagImportant")
}
Thanks a lot for shedding some light on this for me...
Ironically, immediately after finally posting this question, I found a solution; simply subclass the NSTextView and then override keyDown:
import SwiftUI
class RichTextFieldExtended: NSTextView {
override func keyDown(with event: NSEvent) {
if event.modifierFlags.contains(NSEvent.ModifierFlags.command) {
switch event.keyCode {
case 18: // 1
print("1 PRESSED")
default:
print("keyCode \(event.keyCode) wasn't handled")
super.keyDown(with: event)
}
} else {
super.keyDown(with: event)
}
}
}
Then include the subclassed NSTextView in the NSViewRepresentable, as follows
struct RichTextField: NSViewRepresentable {
typealias NSViewType = RichTextFieldExtended
#Binding var attributedString: NSAttributedString
var isEditable: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> RichTextFieldExtended {
let textView = RichTextFieldExtended(frame: .zero)
textView.textStorage?.setAttributedString(self.attributedString)
textView.isEditable = isEditable
textView.delegate = context.coordinator
textView.translatesAutoresizingMaskIntoConstraints = false
textView.autoresizingMask = [.width, .height]
return textView
}
func updateNSView(_ nsView: RichTextFieldExtended, context: Context) {
// nsView.textStorage!.setAttributedString(self.attributedString)
}
// Source: https://medium.com/fantageek/use-xib-de9d8a295757
class Coordinator: NSObject, NSTextViewDelegate {
let parent: RichTextField
init(_ RichTextField: RichTextField) {
self.parent = RichTextField
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? RichTextFieldExtended else { return }
self.parent.attributedString = textView.attributedString()
}
}
}
Cheers

SwiftUI using NSSharingServicePicker in MacOS

I am trying to use a Share function inside my MacOS app in SwiftUI. I am having a URL to a file, which I want to share. It can be images/ documents and much more.
I found NSSharingServicePicker for MacOS and would like to use it. However, I am struggeling to use it in SwiftUI.
Following the documentation, I am creating it like this:
let shareItems = [...]
let sharingPicker : NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems as [Any])
sharingPicker.show(relativeTo: NSZeroRect, of:shareView, preferredEdge: .minY)
My problem is in that show() method. I need to set a NSRect, where I can use NSZeroRect.. but I am struggeling with of: parameter. It requires a NSView. How can I convert my current view as NSView and use it that way. Or can I use my Button as NSView(). I am struggling with that approach.
Another option would be to use a NSViewRepresentable. But should I just create a NSView and use it for that method.
Here is minimal working demo example
struct SharingsPicker: NSViewRepresentable {
#Binding var isPresented: Bool
var sharingItems: [Any] = []
func makeNSView(context: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if isPresented {
let picker = NSSharingServicePicker(items: sharingItems)
picker.delegate = context.coordinator
// !! MUST BE CALLED IN ASYNC, otherwise blocks update
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
class Coordinator: NSObject, NSSharingServicePickerDelegate {
let owner: SharingsPicker
init(owner: SharingsPicker) {
self.owner = owner
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) {
// do here whatever more needed here with selected service
sharingServicePicker.delegate = nil // << cleanup
self.owner.isPresented = false // << dismiss
}
}
}
Demo of usage:
struct TestSharingService: View {
#State private var showPicker = false
var body: some View {
Button("Share") {
self.showPicker = true
}
.background(SharingsPicker(isPresented: $showPicker, sharingItems: ["Message"]))
}
}
Another option without using NSViewRepresentable is:
extension NSSharingService {
static func submenu(text: String) -> some View {
return Menu(
content: {
ForEach(items, id: \.title) { item in
Button(action: { item.perform(withItems: [text]) }) {
Image(nsImage: item.image)
Text(item.title)
}
}
},
label: {
Image(systemName: "square.and.arrow.up")
}
)
}
}
You lose things like the "more" menu item or recent recipients. But in my opinion it's more than enough, simple and pure SwiftUI.