SwiftUI: How to disable the "smart quotes" in TextEditor - swift

I'm developing a Python-based graphic calculator for MacOS using SwiftUI.
https://github.com/snakajima/macplot
I am using SwiftUI's TextEditor as the editor for Python code, but I am not able to figure out how to disable the smart quote (UITextInputTraits, smartQuotesType: UITextSmartQuotesType).
VStack {
TextEditor(text: $pythonScript.script)
HStack {
Button(action: {
pythonScript.run(clear: settings.shouldClear)
}, label: {
Text("Plot")
})
Toggle("Clear", isOn: $settings.shouldClear)
}
if let errorMsg = pythonScript.errorMsg {
Text(errorMsg)
.foregroundColor(.pink)
}
}

After several trials, I came up with the following work-around. It relies on the fact that TextEditor is implemented on top of NSTextView, and changes its behavior across the entire application. It is ugly, but works.
// HACK to work-around the smart quote issue
extension NSTextView {
open override var frame: CGRect {
didSet {
self.isAutomaticQuoteSubstitutionEnabled = false
}
}
}

For those who are looking for an answer for UIKit (iOS, iPadOS) instead of AppKit (macOS), this works for me using a similar approach. Thanks Satoshi!
extension UITextView {
open override var frame: CGRect {
didSet {
self.smartQuotesType = UITextSmartQuotesType.no
}
}
}
This has the same drawbacks, which is that all text fields in your application will lose auto-smart-quotes, but at least you can control this if you need it.

Another solution would be to write an NSTextView wrapper:
struct TextView: NSViewRepresentable {
#Binding var text: String
private var customizations = [(NSTextView) -> Void]()
init(text: Binding<String>) {
_text = text
}
func makeNSView(context: Context) -> NSView {
NSTextView()
}
func updateNSView(_ nsView: NSView, context: Context) {
let textView = nsView as! NSTextView
textView.string = text
customizations.forEach { $0(textView) }
}
func automaticDashSubstitutionEnabled(_ enabled: Bool) -> Self {
customized { $0.isAutomaticDashSubstitutionEnabled = enabled }
}
func automaticQuoteSubstitutionEnabled(_ enabled: Bool) -> Self {
customized { $0.isAutomaticQuoteSubstitutionEnabled = enabled }
}
func automaticSpellingCorrectionEnabled(_ enabled: Bool) -> Self {
customized { $0.isAutomaticSpellingCorrectionEnabled = enabled }
}
}
private extension TextView {
func customized(_ customization: #escaping (NSTextView) -> Void) -> Self {
var copy = self
copy.customizations.append(customization)
return copy
}
}
, which can be used like this:
TextView(text: $myText)
.automaticDashSubstitutionEnabled(false)
.automaticQuoteSubstitutionEnabled(false)
.automaticSpellingCorrectionEnabled(false)

Related

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

NSTextField Changing Focus Order With Tab Key

I have a macOS SwiftUI app that displays a lot of text fields in a grid. Originally I was using SwiftUI TextField's, but they have the problem that you cannot set the focus order of them (the order they receive focus when you press the tab button). I need to change the focus order from horizontal row-by-row, to vertical column-by-column. There does not appear to be an easy way to do this in SwiftUI.
I found a solution for iOS here, that I tried to modify for mac. The problem is that the delegate functions are not getting called. My current code is below. How do I fix this?
Edit: I've updated my code in light of some of the comments, but the problem still remains the same: no delegate methods are being called.
import SwiftUI
struct OrderedTextField: NSViewRepresentable {
#Binding var text: String
#Binding var selectedField: Int
var tag: Int
func makeNSView(context: NSViewRepresentableContext<OrderedTextField>) -> NSTextField {
let textField = NSTextField()
textField.delegate = context.coordinator
textField.tag = tag
textField.placeholderString = ""
return textField
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
func updateNSView(_ nsView: NSTextField, context: NSViewRepresentableContext<OrderedTextField>) {
context.coordinator.newSelection = { newSelection in
DispatchQueue.main.async {
self.selectedField = newSelection
}
}
if nsView.tag == self.selectedField {
nsView.becomeFirstResponder()
}
}
}
extension OrderedTextField {
class Coordinator: NSObject, NSTextFieldDelegate {
#Binding var text: String
var newSelection: (Int) -> () = { _ in }
init(text: Binding<String>) {
print("Initializing!")
_text = text
}
func textShouldBeginEditing(_ textObject: NSText) -> Bool {
print("Should begin editing!")
return true
}
func textDidBeginEditing(_ notification: Notification) {
print("Began editing")
}
func textDidChange(_ notification: Notification) {
print("textDidChange")
}
func textShouldEndEditing(_ textObject: NSText) -> Bool {
print("should end editing")
return true
}
func textDidEndEditing(_ notification: Notification) {
print("did end editing")
}
}
}

How to make text typed in TextField undeletable?

I am Fairly new to programming, after looking around I thought that id take my chances with asking here. I am basically needing for text typed in a TextField to be undeletable, although additional text can be added/typed.
A different approach would be to create a custom keybaord without a delete key, although I couldn't find a good starting place as in research and etc for doing so in SwiftUI.
I have a basic TextField setup with an empty Binding<String>
Looking for pointers of what I should research and or learn.
Thank you.
The idea is the create UITextField class and use UIViewRepresentable to bind with SwiftUI view. By this, you can use all delegate methods and detect backspace. Also, using this you can prevent from cut and delete from tap action.
UndeletableTextField custom class
class UndeletableTextField: UITextField {
// This for prevent to cut and delete
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(UIResponderStandardEditActions.delete(_:)) ||
action == #selector(UIResponderStandardEditActions.cut(_:)) {
return false
}
return super.canPerformAction(action, withSender: sender)
}
}
UIViewRepresentable view
struct UndeletableTextFieldUI: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: Context) -> UndeletableTextField {
let textField = UndeletableTextField(frame: .zero)
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UndeletableTextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: UndeletableTextFieldUI
init(parent: UndeletableTextFieldUI) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Here we detect backspace and ignore it.
if let char = string.cString(using: String.Encoding.utf8) {
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
print("Backspace was pressed")
return false
}
}
return true
}
}
}
ContentView
struct ContentView: View {
#State private var text: String = ""
var body: some View {
UndeletableTextFieldUI(text: $text, placeholder: "Type here")
}
}
You will probably want a custom binding for that String. The following is a super basic example -- you'll probably want to cover more edge cases. Note that I've chosen to include the logic in an ObservableObject, but you could do the same in a View struct by changing _textStore to be a #State variable. You'd also want to include logic for initial text, etc.
class ViewModel : ObservableObject {
var _textStore = ""
var textBinding : Binding<String> {
Binding<String>(get: {
return _textStore
}, set: { newValue in
//do something here to compare newValue to what existed before
//note that this solution will allow text to be both prepended and appended to the existing text
if _textStore.contains(newValue) { _textStore = newValue }
})
}
}
...
#ObservedObject var vm = ViewModel()
TextField("", vm.textBinding)

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.