(This is a revised question - including answer - following on from macOS: Take emoji from characterPalette which describes the problems encountered in more detail)
Background/use case
I have an app where, instead of creating and maintaining an icon library, I let users type an emoji as a placeholder graphic. This works beautifully within the context of my app, but I am not happy with the input mechanism I use.
Problem
I would like to simplify this so I open the characterPalette, select an emoji, and display it either as the button's StringValue or in a Label (=non-editable NSTextField).
This does not seem possible. Unlike NSColorPanel or NSFontPanel, the characterPanel is not exposed to the Cocoa framework, so I cannot take its selectedValue, set its action, or catch a notification. The documentation for orderFrontCharacterPalette simply says Opens the character palette which ... is not helpful.
Attempted solutions and problems encountered
I tried to work with making my receiver the firstResponder, but unlike NSTextView, NSTextField cannot process emoji. I found a workaround using an NSTextView with an NSBox in front, making it the firstResponder, and using NSApp.orderFrontCharacterPalette(sender)but found that under various circumstances which all seem to involve an extra drawing call – setting the button's title, showing a label in SystemFont Mini size (regular size worked fine) the CharacterPalette will open (=the system menu now offers 'Hide Emoji & Symbols') without being displayed. (This persists until the application closes, even if you try to open the CharacterPalette through the regular menu/shortcut)
For the partial solution involving NSTextInputClient (the no-show seems to be a persistent bug), see answer below.
The emoji picker needs a minimal implementation of NSTextInputClient. For example a button:
class MyButton: NSButton, NSTextInputClient {
override var acceptsFirstResponder: Bool {
get {
return true
}
}
override func becomeFirstResponder() -> Bool {
return true
}
override func resignFirstResponder() -> Bool {
return true
}
func insertText(_ string: Any, replacementRange: NSRange) {
// this method is called when the user selects an emoji
if let string = string as? String {
self.title = string
}
}
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
}
func unmarkText() {
}
func selectedRange() -> NSRange {
return NSMakeRange(0, 0)
}
func markedRange() -> NSRange {
return NSMakeRange(NSNotFound, 0)
}
func hasMarkedText() -> Bool {
return false
}
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
return nil
}
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
return []
}
func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
// the emoji picker uses the returned rect to position itself
var rect = self.bounds
rect.origin.x = NSMidX(rect)
rect.size.width = 0
return self.window!.convertToScreen(self.convert(rect, to:nil))
}
func characterIndex(for point: NSPoint) -> Int {
return 0
}
}
NSTextInputClient needs a NSTextInputContext. NSView returns a context from inputContext if the class conforms to NSTextInputClient unless isEditable is implemented and returns false. A label doesn't return a NSTextInputContext, the solution is to override inputContext:
class MyTextField: NSTextField, NSTextInputClient {
var myInputContext : NSTextInputContext?
override var inputContext: NSTextInputContext? {
get {
if myInputContext == nil {
myInputContext = NSTextInputContext(client:self)
}
return myInputContext
}
}
// and the same methods as the button, set self.stringValue instead of self.title in insertText(_:replacementRange:)
}
Willeke pointed me at NSTextInputClient which has provided the best solution so far. Apple's only example is in ObjectiveC, convoluted, and overly complex for what I was trying to do, so I am reproducing my code here.
Caveat: this is not a full implementation of NSTextInputClient, just enough to capture emoji input
I have created an NSButton subclass:
class TextReceiverButton: NSButton, NSTextInputClient {
//specific methods
func setButtonTitle(_ string: String?){
self.title = string ?? 🦊
}
//NSTextInputClient methods
func insertText(_ string: Any, replacementRange: NSRange) {
let receivedText = string as? String
setButtonTitle(receivedText)
}
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
return [.font, .paragraphStyle, .writingDirection]
}
//Omitted: For anything else that wants a value, I return NSMakeRange(0, 0)/NSRect.zero or 0 as well as false for marked text and nil for attributed substring
}
(If you add the protocol to your class, it will offer stubs for the other methods)
The full set for NSAttributedString.Key is
[.font, .foregroundColor, .glyphInfo, .kern, .ligature, .link, .markedClauseSegment, .obliqueness, .paragraphStyle, .shadow, .spellingState, .strikethroughColor, .strikethroughStyle, .strokeColor, .strokeWidth, .superscript, .textAlternatives, .textEffect, .toolTip, .underlineColor, .underlineStyle, .verticalGlyphForm, .writingDirection]
(I have tested the short form with simple and composite emoji and nothing else seems necessary.)
The button's action is
#IBAction func displayEmojiInButton(_ sender: Any) {
NSApp.orderFrontCharacterPalette(self)
view.window?.makeFirstResponder(textReceiverButton)
}
Problems/Bugs
The NSTextInputClient document says 'you can subclass NSView' and Apple's code turns an NSView into a fully functional (receiving and drawing) text view class (I can't built it, but I assume it worked). So theoretically, you should be able to use the same code for NSTextField, which also ultimately inherits from NSView.
However, it turns out that NSTextField displays the 'CharacterPalette allegedly opens but never displays' bug I talked about earlier; though it does work with NSView. (I have not tested this further).
Furthermore, NSTextInputClient is not a complete replacement for NSTextView: it does not receive input from the keyboard viewer. (See Willecke's answer/comment for explanation/solution to these).
Verdict
NSApp.orderFrontCharacterPalette(self) fails 95% of the time when called from a view in the vincinity of a tab view (in splitView next to TabViewController, embedded in TabViewController), so while this code may be correct, it's also useless a lot of the time, at least under 10.13.
Currently, I am converting Obj-C code to Swift.
Setup:
Protocol A
func methodA()
Protocol B
func methodB
Class C
func passing() {
if deleagteA?.methodA?() {}
else if delegateB?.methodB?() {}
}
Originally in Objective-C, it were handled by if delegate is conforms A pass methodA, else if conforms B protocol then pass methodB. Since Swift no have or not need responseToSelector method, and came up with above implementation. Is there better way to write on Swift instead having empty block?
From what you've posted, it's not clear if your protocol methods are optional or not. This will work either way.
(delegateA?.methodA ?? delegateB?.methodB)?()
And if you've got a lot of them, you can put them into an array.
[delegateA?.methodA, delegateB?.methodB, delegateA?.methodA, delegateB?.methodB]
.compactMap { $0 }
.first?()
If you want to have a solution more similar to the old one with only one delegate property you can do like this
class C {
var delegate: Any?
func passing() {
if let delegate = self.delegate as? A {
delegate.methodA()
} else if let delegate = self.delegate as? B {
delegate.methodB()
}
}
}
It's a little bit more code but you don't need 2 properties and avoid the risk of assigning 2 delegates at the same time
It turns out ?? Nil-Coalescing Operator is the fix for this case.
delegateA?.methodA() ?? delegateB?.methodB()
I've just migrated my code to Swift 4 and it added in lots of #objc code infront of several functions.
I think I get why after reading lots of posts on here, but my question is this - is there an alternative way of writing this code, that doesn't need to expose the Objective C runtime?
For example:
#objc func textFieldDidChange(textField: UITextField) {
checkCode(textField.text ?? "")
}
Or:
#objc func hideKeyboard() {
commentsBox.resignFirstResponder()
}
Or:
#objc func setDateChanged(_ sender:UIDatePicker) {
setDate.text = dateFormatter.string(from: sender.date)
}
Is there a different way to write this code, that doesn't involve the #objc bit at the start?
Why? I like shorter, cleaner code (which is why I love Swift).
Many thanks in advance!
How can I write this so that it updates the variable when the user finishes using the field (for Cocoa?). The aim is to allow the user to specify a custom IP address for the TV's location on the network.
import Cocoa
import Alamofire
class ViewController: NSViewController, NSTextFieldDelegate {
#IBAction func MenuButton(_ sender: NSButtonCell) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAABgAw==")
}
#IBAction func ReturnButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAgAAAJcAAAAjAw==")
}
…
#IBOutlet var IPField: NSTextField! // [A] Set by the user
…
func triggerRemoteControl(irccc: String) {
Alamofire.request(IPField, // [B] Goes here when it's updated.
method: .post,
parameters: ["parameter" : "value"],
encoding: SOAPEncoding(service: "urn:schemas-sony-com:service:IRCC:1",
action: "X_SendIRCC", IRCCC: irccc)).responseString { response in
print(response)
}
}
}
— UPDATE
I tried declaring a variable:
var IPString: String
and then (I set the textField's delegate to ViewController, and placed this function inside):
override func controlTextDidEndEditing(_ obj: Notification){
let IPString = IPField.stringValue
}
Even using the "-> String" and return notation still has it complaining about unused variables. I obviously don't know my Syntax well enough.
Complier also complains about not the ViewController not being initialised.
What you need is to override the func controlTextDidEndEditing(_ obj: Notification) function
You should take a look at:
object (property of obj) - sometimes you would like to know which object sent you the end editing action.
userInfo (property of obj) - contains a "NSTextMovement" key, which allows you to define how the user did end the editing.
override func controlTextDidEndEditing(_ obj: Notification){
let IPString = IPField.stringValue
}
Here, you're creating new constant. What you want is to set this value into your class variable, so you should make IPString = IPField.stringValue
But it's not quite correct, because func controlTextDidEndEditing(_ obj: Notification) could be called from other objects, so first you should check if obj notification contain object which send it with guard, for example.
guard let object = obj.object else {
return
}
Then check if object is your IPField with identity operators
guard object === IPField else {
return
}
And finally you can assign your field value to your IPString var
IPString = object.stringValue
Hope it will help you. Ohh and one advice from my side, you should use lower camel case naming convention for you variables.
I've tried looking at other StackOverflow inquires about this, I can't seem to find the solution to this. I'm not sure if this is due to me looking at threads which are non-Swift 2.0.
I will need NSNotification to pass any kind of value to my Main View Controller. But I keep getting:
Optional(foobar)
Here's a function on View Controller B:
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
let notificationName = "CoreLocationNSN"
let notification = NSNotification(name: notificationName, object: self, userInfo: ["doge":"foobar"])
NSNotificationCenter.defaultCenter().postNotification(notification)
}
Here's my main View Controller's initialization function:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "testNSNWithObject:", name: CoreLocationNSN, object: nil)
}
func testNSNWithObject(notification: NSNotification) {
print("Printing...")
print(String(notification.userInfo!["doge"]))
print(String(notification.userInfo?["doge"]))
print(notification.userInfo!["doge"])
print(notification.userInfo?["doge"])
}
But, here's my output:
Printing...
Optional(foobar)
Optional(foobar)
Optional(foobar)
Optional(foobar)
You just need to unwrap the String
func testNSNWithObject(notification: NSNotification) {
if let dodge = notification.userInfo?["dodge"] as? String {
print(dodge)
}
}
I suggest you to avoid the force unwrap operator ! since it's dangerous and there are several safer solutions available in Swift.
I've solved it, looks like it can be unwrapped by adding ! at the end, for some reason I thought my compiler was yelling at me for doing so:
func testNSNWithObject(notification: NSNotification) {
print("Printing...")
print(String(notification.userInfo!["doge"]))
print(String(notification.userInfo?["doge"]))
print(notification.userInfo!["doge"]!) // <<-- THIS ONE WORKED!
print(notification.userInfo?["doge"]!)
}
Just going to keep this here since this issue has been troublesome for me in the past and the syntax seems very strange.
Does anyone know what else I could have done instead that would've removed the "Optional()" from the string?