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)
}
}
}
Related
This question already has answers here:
iOS Custom keyboard increase UIInputViewController height
(1 answer)
Unable to change UIInputView height
(4 answers)
Closed 8 months ago.
I have made a custom keyboard using a .xib and .swift file. I set it for a textfield by doing the following:
let customNumberPad = CustomNumberPad()
length.inputView = customNumberPad.inputView
However, the size of the keyboard area is large like it is using the old keyboard.
I have tried setting the height in the .xib to 200 via constraint.
I have tried:
length.inputView!.autoresizingMask = []
let heightAnch = length.inputView!.heightAnchor.constraint(equalToConstant: 200)
heightAnch.isActive = true
length.reloadInputViews()
Full CustomNumberPad code:
import UIKit
import AudioToolbox
class CustomNumberPadSmall: UIInputViewController {
#IBOutlet var numberPad: UIView!
#IBAction func insertText(_ sender: UIButton) {
if let text = sender.currentTitle {
AudioServicesPlaySystemSound (1104)
self.textDocumentProxy.insertText(text)
}
}
#IBAction func backSpace(_ sender: UIButton) {
self.textDocumentProxy.deleteBackward()
}
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = USERINFO.darkModeValue
Bundle.main.loadNibNamed("CustomNumberPadSmall", owner: self)
numberPad.translatesAutoresizingMaskIntoConstraints = false
let inputView = self.inputView!
inputView.translatesAutoresizingMaskIntoConstraints = false
inputView.addSubview(numberPad)
NSLayoutConstraint.activate([
numberPad.topAnchor.constraint(equalTo: inputView.topAnchor),
numberPad.bottomAnchor.constraint(equalTo: inputView.bottomAnchor),
numberPad.leadingAnchor.constraint(equalTo: inputView.leadingAnchor),
numberPad.trailingAnchor.constraint(equalTo: inputView.trailingAnchor),
numberPad.heightAnchor.constraint(equalToConstant: 200)
])
}
}
If you only need a number pad it's better to use keyboardType property of UITextField.
yourTextField.keyboardType = .numberPad
In another case you need a custom keyboard itself, override intrinsicContentSize property of UIInputView
class CustomNumberPad: UIInputView {
// your calculated input view height
var intrinsicHeight: CGFloat = 200 {
didSet {
self.invalidateIntrinsicContentSize()
}
}
init() {
super.init(frame: CGRect(), inputViewStyle: .keyboard)
self.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.translatesAutoresizingMaskIntoConstraints = false
}
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: self.intrinsicHeight)
}
}
How to use :
let customNumberPad = CustomNumberPad()
yourTextField.inputView = customNumberPad
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
}
}
}
I have an NSPopover which contains an NSViewController with a containing NSScrollView.
The Popover height has to be either the height of the NSScrollView content or the current window. Once it hits the bounds of the window it should scroll.
Using Snapkit
I have added the NSScrollView to the controller:
view.addSubview(scrollView)
scrollView.snp.makeConstraints { (make) in
make.edges.equalTo(view)
make.height.equalTo(mainView.content.snp.height)
}
This works fine until the content is greater than the window, then what happens is the NSScrollView will not scroll to the top of the content because the view has pushed itself upwards out of bounds.
I have gone down the route of removing the height constraint and in the viewDidLayout try to update the height but it doesn't work.
If more code examples are needed let me know.
Finally got to the bottom of the issue and found a sensible solution.
The app I am developing has a few popovers that are required at various stages, to ensure that they closed as required I created a service that manages every popover, here is an example:
class PopoverService: NSObject {
enum PopoverType {
case subscription, edit
}
//================================================================================
// MARK: - Properties
//================================================================================
private var dismissingPopover = false
private lazy var currentPopover: NSPopover = {
let popover = NSPopover()
popover.delegate = self
return popover
}()
private var nextPopoverType: PopoverType?
private var currentView: NSView!
public static var delegate: PopoverServiceDelegate?
//================================================================================
// MARK: - Singleton
//================================================================================
static let shared = PopoverService()
//================================================================================
// MARK: - Helpers
//================================================================================
public static func increaseHeight(_ height: CGFloat) {
shared.currentPopover.contentSize.height = height
}
public static func isDisplayingType(_ type: PopoverType) -> Bool {
switch type {
case .edit:
return shared.currentPopover.contentViewController is EditEntryController
case .language:
return shared.currentPopover.contentViewController is CodeTypeController
default:
return false
}
}
public static func displayPopover(type: PopoverType, fromView view: NSView) {
shared.nextPopoverType = type
shared.currentView = view
switch type {
case .subscription:
displaySubscriptionPopoverFrom(view)
// Create functions to display your popovers
}
}
static func dismissPopover(clearUpcoming: Bool = true) {
if clearUpcoming {
shared.nextPopoverType = nil
}
shared.currentPopover.performClose(nil)
if shared.currentPopover.contentViewController == nil {
shared.dismissingPopover = false; return
}
}
}
extension PopoverService: NSPopoverDelegate {
func popoverDidClose(_ notification: Notification) {
currentPopover.contentViewController = nil
dismissingPopover = false
guard let nextPopoverType = nextPopoverType else { return }
PopoverService.displayPopover(
type: nextPopoverType,
fromView: currentView,
entry: currentEntry
)
}
}
To update the current popover, there is a function increaseHeight which takes and CGFloat and will update the current popovers height.
In the NSViewController override the viewDidLayout():
override func viewDidLayout() {
super.viewDidLayout()
let windowFrameHeight = view.window?.frame.size.height ?? 0
let contentHeight = scrollView.content.frame.height
let adjustment = contentHeight > windowFrameHeight ? windowFrameHeight : contentHeight
PopoverService.increaseHeight(adjustment)
if contentHeight > 0 && firstLayout {
if let documentView = scrollView.documentView {
documentView.scroll(NSPoint(x: 0, y: documentView.bounds.size.height))
}
}
}
The scrollView will need to be forced to the top so there is a variable firstLayout which you can set to true in the viewDidAppear
I try to implement search behavior like in Xcode: if you enter something in search field, icon changes color.
I delegate both searchFieldDidStartSearching and searchFieldDidEndSearching to controller and change the image.
The problem is icon's image changes only when window lose it's focus.
class ViewController: NSViewController {
#IBOutlet weak var searchField: NSSearchField!
func searchFieldDidStartSearching(_ sender: NSSearchField) {
print("\(#function)")
(searchField.cell as! NSSearchFieldCell).searchButtonCell?.image = NSImage.init(named: "NSActionTemplate")
}
func searchFieldDidEndSearching(_ sender: NSSearchField) {
print("\(#function)")
(searchField.cell as! NSSearchFieldCell).searchButtonCell?.image = NSImage.init(named: "NSHomeTemplate")
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
Thanks in advance for any ideas/suggestions.
Although I don't know the reason, it works:
NSApp.mainWindow?.resignMain()
NSApp.mainWindow?.becomeMain()
Here is the whole code:
class MyViewController: NSViewController {
private lazy var searchField: NSSearchField = {
let searchField = NSSearchField(string: "")
if let searchButtonCell = searchField.searchButtonCell {
searchButtonCell.setButtonType(.toggle)
let filterImage = #imageLiteral(resourceName: "filter")
searchButtonCell.image = filterImage.tinted(with: .systemGray)
searchButtonCell.alternateImage = filterImage.tinted(with: .systemBlue)
}
searchField.focusRingType = .none
searchField.bezelStyle = .roundedBezel
searchField.delegate = self
return searchField
}()
...
}
extension MyViewController: NSSearchFieldDelegate {
func searchFieldDidStartSearching(_ sender: NSSearchField) {
sender.searchable = true
}
func searchFieldDidEndSearching(_ sender: NSSearchField) {
sender.searchable = false
}
}
extension NSSearchField {
var searchButtonCell: NSButtonCell? {
(self.cell as? NSSearchFieldCell)?.searchButtonCell
}
var searchable: Bool {
get {
self.searchButtonCell?.state == .on
}
set {
self.searchButtonCell?.state = newValue ? .on : .off
self.refreshSearchIcon()
}
}
private func refreshSearchIcon() {
NSApp.mainWindow?.resignMain()
NSApp.mainWindow?.becomeMain()
}
}
extension NSImage {
func tinted(with color: NSColor) -> NSImage? {
guard let image = self.copy() as? NSImage else { return nil }
image.lockFocus()
color.set()
NSRect(origin: NSZeroPoint, size: self.size).fill(using: .sourceAtop)
image.unlockFocus()
image.isTemplate = false
return image
}
}
I was having the same issue. A simple override fixed this issue for me
extension NSSearchField{
open override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
}
As you can see when you click inside the view it's still focussed on the search text field(as you can still type in it after you clicked underneath it). Since the change image is on when it loses focus, you should check if you clicked outside of the text field.
Solve problem by subclassing NSSearchFieldCell and assign this class to field's cell.
You don't even need to subclass NSSearchFieldCell.
When you create your NSSearchField from code, you can do something like this:
if let searchFieldCell = searchField.cell as? NSSearchFieldCell {
let image = NSImage(named: "YourImageName")
searchFieldCell.searchButtonCell?.image = image
searchFieldCell.searchButtonCell?.alternateImage = image // Optionally
}
If you're using storyboards, you can do the same in didSet of your #IBOutlet.
Currently I am having multiple textfields in a view. If the user taps at one of them there should be a function responding to the event. Is there a way on how to do react (if a textfield got the focus)? I tried it with the NSTextFieldDelegate method but there is no appropriate function for this event.
This is how my code looks at the moment:
class ViewController: NSViewController, NSTextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let textField = NSTextField(frame: CGRectMake(10, 10, 37, 17))
textField.stringValue = "Label"
textField.bordered = false
textField.backgroundColor = NSColor.controlColor()
view.addSubview(textField)
textField.delegate = self
let textField2 = NSTextField(frame: CGRectMake(30, 30, 37, 17))
textField2.stringValue = "Label"
textField2.bordered = false
textField2.backgroundColor = NSColor.controlColor()
view.addSubview(textField2)
textField2.delegate = self
}
func control(control: NSControl, textShouldBeginEditing fieldEditor: NSText) -> Bool {
print("working") // this only works if the user enters a charakter
return true
}
}
The textShouldBeginEditing function only handles the event if the user tries to enter a character but this isn't what I want. It has to handle the event if he clicks on the textfield.
Any ideas, thanks a lot?
Edit
func myAction(sender: NSView)
{
print("aktuell: \(sender)")
currentObject = sender
}
This is the function I want to call.
1) Create a subclass of NSTextField.
import Cocoa
class MyTextField: NSTextField {
override func mouseDown(theEvent:NSEvent) {
let viewController:ViewController = ViewController()
viewController.textFieldClicked()
}
}
2) With Interface building, select the text field you want to have a focus on. Navigate to Custom Class on the right pane. Then set the class of the text field to the one you have just created.
3) The following is an example for ViewController.
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func textFieldClicked() -> Void {
print("You've clicked on me!")
}
}
4) Adding text fields programmatically...
import Cocoa
class ViewController: NSViewController {
let myField:MyTextField = MyTextField()
override func viewDidLoad() {
super.viewDidLoad()
//let myField:MyTextField = MyTextField()
myField.setFrameOrigin(NSMakePoint(20,70))
myField.setFrameSize(NSMakeSize(120,22))
let textField:NSTextField = NSTextField()
textField.setFrameOrigin(NSMakePoint(20,40))
textField.setFrameSize(NSMakeSize(120,22))
self.view.addSubview(myField)
self.view.addSubview(textField)
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func textFieldClicked() -> Void {
print("You've clicked on me!")
}
}
I know it’s been answered some while ago but I did eventually find this solution for macOS in Swift 3 (it doesn’t work for Swift 4 unfortunately) which notifies when a textfield is clicked inside (and for each key stroke).
Add this delegate to your class:-
NSTextFieldDelegate
In viewDidLoad() add these:-
imputTextField.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: Notification.Name.NSTextViewDidChangeSelection, object: nil)
Then add this function:-
func textDidChange(_ notification: Notification) {
print("Its come here textDidChange")
guard (notification.object as? NSTextView) != nil else { return }
let numberOfCharatersInTextfield: Int = textFieldCell.accessibilityNumberOfCharacters()
print("numberOfCharatersInTextfield = \(numberOfCharatersInTextfield)")
}
Hope this helps others.