NSTextField Changing Focus Order With Tab Key - swift

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

Related

Disable button if PKCanvasView is empty

I'm building a format using SwiftUI on which a user has to draw a signature, and must not be allowed to move forward if the PKCanvasView is empty. I'm trying to disable the next button by using: .disabled(pkCanvasView.drawing.bounds.isEmpty)but this condition always return true so the button is always disable. But if I try the following, it seems the condition is working.
Button(text: "SAVE") {
if pkCanvasView.drawing.bounds.isEmpty {
showAlert = true //CONDITION WORKS
} else {
//Do something else
}
}
So this way an alert shows if the canvas is empty, but in this case I cannot use an alert, the button has to be disable. Any ideas on how I can achive this by using the .disabled()property?
EDIT:
This is how I wrapped PKCanvasView in a UIViewRepresentable
struct SignatureCanvas: UIViewRepresentable {
#Binding var canvas: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvas.drawingPolicy = .anyInput
canvas.tool = PKInkingTool(.pen, color: .black, width: 10)
return canvas
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
func checkIfEmpty() -> Bool {
if canvas.drawing.strokes.isEmpty {
return true
} else {
return false
}
}
}
You need to implement the coordinator pattern with a PKCanvasView delegate.
struct SignatureCanvas: UIViewRepresentable {
#Binding var canvas: PKCanvasView
#Binding var flag: Bool
//Create the coordinator and assign it to the context
func makeCoordinator() -> Coordinator {
Coordinator(flag: $flag)
}
func makeUIView(context: Context) -> PKCanvasView {
canvas.drawingPolicy = .anyInput
canvas.tool = PKInkingTool(.pen, color: .black, width: 10)
//assign coordinator as delegate
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) {}
func checkIfEmpty() -> Bool {
if canvas.drawing.strokes.isEmpty {
return true
} else {
return false
}
}
}
// the coordinator storing the binding and implementing the delegate
class Coordinator: NSObject {
#Binding var flag: Bool
init(flag: Binding<Bool>) {
self._flag = flag
}
}
extension Coordinator: PKCanvasViewDelegate {
//gets called when the drawing changes
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
//update the binding
flag = canvasView.drawing.strokes.isEmpty
}
}
In your ContentView
VStack {
SignatureCanvas(canvas: $canvas, flag: $flag)
Button(text: "SAVE") {
}
.disabled(flag)
}
And of course you need the appropriate state variable in Contentview
#State private var flag: Bool = true

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

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

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)

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