SwiftUI Mac OSX SearchBar element - swift

I am currently developing a app in SwiftUI for MacOSX. I am having a custom List View with many rows. I want a SearchBar to filter my rows.
Coming from Objective C, I know there was a SearchBar and SearchBar Controller. I just would need a SearchBar with the default Mac OS X Search Bar design. However, I didn't found anything.
There was a answer on Stackoverflow, which dealted with the same problem but for iOS. But that is not convertible to Mac OS X.
Currently I am using a regular TextField and wrote my own filter script, which worked fine. However, I want to SearchField UI as Apple uses it. Is that possible?
Is there any chance to use AppKit to achieve that?
That's what I want to achieve:

Your best bet is to wrap an NSSearchField in an NSViewRepresentable.

Here is my solution that I am now using:
struct FirstResponderNSSearchFieldRepresentable: NSViewControllerRepresentable {
#Binding var text: String
func makeNSViewController(
context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
) -> FirstResponderNSSearchFieldController {
return FirstResponderNSSearchFieldController(text: $text)
}
func updateNSViewController(
_ nsViewController: FirstResponderNSSearchFieldController,
context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
) {
}
}
And here the NSViewController
class FirstResponderNSSearchFieldController: NSViewController {
#Binding var text: String
var isFirstResponder : Bool = true
init(text: Binding<String>, isFirstResponder : Bool = true) {
self._text = text
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let searchField = NSSearchField()
searchField.delegate = self
self.view = searchField
}
override func viewDidAppear() {
self.view.window?.makeFirstResponder(self.view)
}
}
extension FirstResponderNSSearchFieldController: NSSearchFieldDelegate {
func controlTextDidChange(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
self.text = textField.stringValue
}
}
}

Related

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

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

SwiftUI Representable TextField changes Font on disappear

I am using a Representable ViewController for a custom NSTextField in SwiftUI for MacOS. I am applying a customized, increased font size for that TextField. That works fine, I had a question regarding that topic in my last question here.
I am setting the Font of the TextField in my viewDidAppear() method now, which works fine. I can see a bigger font. The problem is now, when my view is not being in focus, the text size is shrinking back to normal. When I go back and activate the window again, I see the small font size. When I type again, it gets refreshed and I see the correct font size.
That is my code I am using:
class AddTextFieldController: NSViewController {
#Binding var text: String
let textField = NSTextField()
var isFirstResponder : Bool = true
init(text: Binding<String>, isFirstResponder : Bool = true) {
self._text = text
textField.font = NSFont.userFont(ofSize: 16.5)
super.init(nibName: nil, bundle: nil)
NSLog("init")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
textField.delegate = self
//textField.font = NSFont.userFont(ofSize: 16.5)
self.view = textField
}
override func viewDidAppear() {
self.view.window?.makeFirstResponder(self.view)
textField.font = NSFont.userFont(ofSize: 16.5)
}
}
extension AddTextFieldController: NSTextFieldDelegate {
func controlTextDidChange(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
self.text = textField.stringValue
}
textField.font = NSFont.userFont(ofSize: 16.5)
}
}
And this is the representable:
struct AddTextFieldRepresentable: NSViewControllerRepresentable {
#Binding var text: String
func makeNSViewController(
context: NSViewControllerRepresentableContext<AddTextFieldRepresentable>
) -> AddTextFieldController {
return AddTextFieldController(text: $text)
}
func updateNSViewController(
_ nsViewController: AddTextFieldController,
context: NSViewControllerRepresentableContext<AddTextFieldRepresentable>
) {
}
}
Here is a demo:
I thought about using Notification when the view is coming back, however not sure
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: NSApplication.willBecomeActiveNotification, object: nil)
Here is a solution. Tested with Xcode 11.4 / macOS 10.15.4
class AddTextFieldController: NSViewController {
#Binding var text: String
let textField = MyTextField() // << overridden field
and required custom classes (both!!) for cell & control (in all other places just remove user font usage as not needed any more):
class MyTextFieldCell: NSTextFieldCell {
override var font: NSFont? {
get {
return super.font
}
set {
// system tries to reset font to default several
// times (here!), so don't allow that
super.font = NSFont.userFont(ofSize: 16.5)
}
}
}
class MyTextField: NSTextField {
override class var cellClass: AnyClass? {
get { MyTextFieldCell.self }
set {}
}
override var font: NSFont? {
get {
return super.font
}
set {
// system tries to reset font to default several
// times (and here!!), so don't allow that
super.font = NSFont.userFont(ofSize: 16.5)
}
}
}