SwiftUI using NSSharingServicePicker in MacOS - swift

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.

Related

Displaying NSMenuItem in SwiftUI on it's own

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.

Is there a way to know if a view has already been loaded in SwiftUI

I'm trying to focus the isUsernameFocused textField as soon as it loads on the screen, I tried doing it directly in the onAppear method but it looks like it needs a delay in order for it to focus. My concern is that for some reason the focus only occurs with a delay greater than 0.6 fractions of a second. Setting it at 0.7 fractions of a second seems to work fine but I'm afraid that eventually, this will stop working if the view gets bigger since it will need more time to load.
Is there a way to know when the VStack is fully loaded so I can trigger the isUsernameFocused? Something like, viewDidLoad in UIKit.
struct ContentView: View {
#FocusState private var isUsernameFocused: Bool
#State private var username = ""
var body: some View {
VStack {
TextField("Username", text: $username)
.focused($isUsernameFocused)
}
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7){
self.isUsernameFocused = true
}
}
}
}
If you are on macOS or tvOS you can use prefersDefaultFocus for that. It should come to iOS in June.
In the meantime, I just created this example that works around the issue. If your form appears multiple times you might want to check other values before setting the focus.
import SwiftUI
import UIKit
struct FocusTestView : View {
#State var presented = false
var body: some View {
Button("Click Me") {
presented = true
}
.sheet(isPresented: $presented) {
LoginForm()
}
}
}
struct LoginForm : View {
enum Field: Hashable {
case usernameField
case passwordField
}
#State private var username = ""
#State private var password = ""
#FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .usernameField)
SecureField("Password", text: $password)
.focused($focusedField, equals: .passwordField)
Button("Sign In") {
if username.isEmpty {
focusedField = .usernameField
} else if password.isEmpty {
focusedField = .passwordField
} else {
// handleLogin(username, password)
}
}
}
.uiKitOnAppear {
focusedField = .usernameField
}
}
}
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.action = action
return vc
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {
}
}
class UIAppearViewController: UIViewController {
var action: () -> Void = {}
override func viewDidLoad() {
view.addSubview(UILabel())
}
override func viewDidAppear(_ animated: Bool) {
DispatchQueue.main.asyncAfter(deadline:.now()) { [weak self] in
self?.action()
}
}
}
public extension View {
func uiKitOnAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}
UIKitAppear was taken from dev forum post and I added the dispatch async to call the action. LoginForm is from the docs on FocusState.

Is there a way to show the system keyboard and take inputs from it without a SwiftUI TextField?

I want to be able to display the system keyboard and my app take inputs from the keyboard without using a TextField or the like. My simple example app is as follows:
struct TypingGameView: View {
let text = “Some example text”
#State var displayedText: String = ""
var body: some View {
Text(displayedText)
}
}
I'm making a memorization app, so when I user types an input on the keyboard, it should take the next word from text and add it to displayedText to display onscreen. The keyboard should automatically pop up when the view is displayed.
If there is, a native SwiftUI solution would be great, something maybe as follows:
struct TypingGameView: View {
let text = “Some example text”
#State var displayedText: String = ""
var body: some View {
Text(displayedText)
.onAppear {
showKeyboard()
}
.onKeyboardInput { keyPress in
displayedText += keyPress
}
}
}
A TextField could work if there is some way to 1. Make it so that whatever is typed does not display in the TextField, 2. Disable tapping the text (e.g. moving the cursor or selecting), 3. Disable deleting text.
Here's a possible solution using UIViewRepresentable:
Create a subclass of UIView that implements UIKeyInput but doesn't draw anything
Wrap it inside a struct implementing UIViewRepresentable, use a Coordinator as a delegate to your custom UIView to carry the edited text "upstream"
Wrap it again in a ViewModifier that shows the content, pass a binding to the wrapper and triggers the first responder of your custom UIView when tapped
I'm sure there's a more synthetic solution to find, three classes for such a simple problem seems a bit much.
protocol InvisibleTextViewDelegate {
func valueChanged(text: String?)
}
class InvisibleTextView: UIView, UIKeyInput {
var text: String?
var delegate: InvisibleTextViewDelegate?
override var canBecomeFirstResponder: Bool { true }
// MARK: UIKeyInput
var keyboardType: UIKeyboardType = .decimalPad
var hasText: Bool { text != nil }
func insertText(_ text: String) {
self.text = (self.text ?? "") + text
setNeedsDisplay()
delegate?.valueChanged(text: self.text)
}
func deleteBackward() {
if var text = text {
_ = text.popLast()
self.text = text
}
setNeedsDisplay()
delegate?.valueChanged(text: self.text)
}
}
struct InvisibleTextViewWrapper: UIViewRepresentable {
typealias UIViewType = InvisibleTextView
#Binding var text: String?
#Binding var isFirstResponder: Bool
class Coordinator: InvisibleTextViewDelegate {
var parent: InvisibleTextViewWrapper
init(_ parent: InvisibleTextViewWrapper) {
self.parent = parent
}
func valueChanged(text: String?) {
parent.text = text
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> InvisibleTextView {
let view = InvisibleTextView()
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: InvisibleTextView, context: Context) {
if isFirstResponder {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
}
}
struct EditableText: ViewModifier {
#Binding var text: String?
#State var editing: Bool = false
func body(content: Content) -> some View {
content
.background(InvisibleTextViewWrapper(text: $text, isFirstResponder: $editing))
.onTapGesture {
editing.toggle()
}
.background(editing ? Color.gray : Color.clear)
}
}
extension View {
func editableText(_ text: Binding<String?>) -> some View {
modifier(EditableText(text: text))
}
}
struct CustomTextField_Previews: PreviewProvider {
struct Container: View {
#State private var value: String? = nil
var body: some View {
HStack {
if let value = value {
Text(value)
Text("meters")
.font(.subheadline)
} else {
Text("Enter a value...")
}
}
.editableText($value)
}
}
static var previews: some View {
Group {
Container()
}
}
}
you can have the textfield on screen but set opacity to 0 if you don't want it shown. Though this would not solve for preventing of deleting the text
You can then programmatically force it to become first responder using something like this https://stackoverflow.com/a/56508132/3919220

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))

Passing data from simple NSView to SwiftUI View

I'm making a macOS app with SwiftUI. I have a struct conforming to NSViewRepresentable, whose purpose is to detect the key code of whichever key was pressed. I want to pass the event.keyCode back to SwiftUI and save it into an environment object so I can use the key code elsewhere in my app.
I know I am supposed to use SwiftUI coordinators, but all tutorials and Stack Overflow questions I can find use ready-made classes such as UIPageControl or UISearchBar that have pre-configured delegates. I'm not sure what to do when using a simple custom NSView. Can somebody explain how to pass the data from the NSViewRepresentable struct into my #EnvironmentObject when using a custom NSView?
struct KeyboardEvent: NSViewRepresentable {
class KeyView: NSView {
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
print("\(event.keyCode)")
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
DispatchQueue.main.async {
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
struct ContentView: View {
#EnvironmentObject var input: KeyboardInput // save the keyCode here
var body: some View {
VStack {
Text(input.keyCode)
KeyboardEvent()
}
}
}
Right now it prints the key code normally to the Xcode console, so the detection works fine.
Here is a solution (with some replicated parts). Tested with Xcode 11.4 / macOS 10.15.4
class KeyboardInput: ObservableObject {
#Published var keyCode: UInt16 = 0
}
struct KeyboardEvent: NSViewRepresentable {
#Binding var keyStorage: UInt16 // << here !!
init(into storage: Binding<UInt16>) {
_keyStorage = storage
}
class KeyView: NSView {
var owner: KeyboardEvent? // << view holder
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
print("\(event.keyCode)")
owner?.keyStorage = event.keyCode
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
view.owner = self // << inject
DispatchQueue.main.async {
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
struct ContentView: View {
#EnvironmentObject var input: KeyboardInput // save the keyCode here
var body: some View {
VStack {
Text("Code: \(input.keyCode)")
KeyboardEvent(into: $input.keyCode) // << binding !!!
}
}
}