Make NSTextField update its bound value when setting its string value programmatically - cocoa

I have an NSTextField bound to a key in the user defaults. When I press enter or leave the field the bound value is properly updated (I have an observer for it). However when I programmatically set the value of the text field the bound value is not updated. The text field however shows the new string I set with:
stockField1.stringValue = [sender representedObject];
(it's set from a menu item handler). Is it necessary to send an additional message to the text field or how else can I make this work?

Manually triggering key-value binding goes like this:
- (void)symbolSelected: (id)sender
{
NSTextField *field;
switch ([sender tag]) {
case 0:
field = stockField1;
break;
case 1:
field = stockField2;
break;
case 2:
field = stockField3;
break;
}
field.stringValue = [sender representedObject];
NSDictionary *bindingInfo = [field infoForBinding: NSValueBinding];
[[bindingInfo valueForKey: NSObservedObjectKey] setValue: field.stringValue
forKeyPath: [bindingInfo valueForKey: NSObservedKeyPathKey]];
}

Here's the Swift version of Mike's answer, for reference:
guard
let bindingInfo = self.infoForBinding(NSBindingName.value),
let observedObject = bindingInfo[NSBindingInfoKey.observedObject] as? NSObject,
let observedKeyPath = bindingInfo[NSBindingInfoKey.observedKeyPath] as? String else {
return
}
observedObject.setValue(self.stringValue, forKeyPath: observedKeyPath)

Related

Is there a way to force reloadData on a UICollectionView using RxSwift?

I have a UICollectionView bound to an array of entities using BehaviorSubject and all is fine, data is loaded from the network and displayed correctly.
The problem is, based on user action, I'd like to change the CellType used by the UICollectionView and force the collection to re-create all cells, how do I do that?
My bind code looks like:
self.dataSource.bind(to: self.collectionView!.rx.items) {
view, row, data in
let indexPath = IndexPath(row: row, section: 0)
var ret: UICollectionViewCell? = nil
if (self.currentReuseIdentifier == reuseIdentifierA) {
// Dequeue cell type A and bind it to model
ret = cell
} else {
// Dequeue cell type B and bind it to model
ret = cell
}
return ret!
}.disposed(by: disposeBag)
The general way to solve problems in Rx is to think of what you want the output effect to be and what input effects can affect it.
In your case, the output effect is the display of the table view. You have identified two input effects "data is loaded from the network" and "user action". In order to make your observable chain work properly, you will have to combine your two input effects in some way to get the behavior you want. I can't say how that combination should take place without more information, but here is an article explaining most of the combining operators available: https://medium.com/#danielt1263/recipes-for-combining-observables-in-rxswift-ec4f8157265f
As a workaround, you can emit an empty list then an actual data to force the collectionView to reload like so:
dataSource.onNext([])
dataSource.onNext([1,2,3])
I think you can use different data type to create cell
import Foundation
import RxDataSources
enum SettingsSection {
case setting(title: String, items: [SettingsSectionItem])
}
enum SettingsSectionItem {
case bannerItem(viewModel: SettingSwitchCellViewModel)
case nightModeItem(viewModel: SettingSwitchCellViewModel)
case themeItem(viewModel: SettingCellViewModel)
case languageItem(viewModel: SettingCellViewModel)
case contactsItem(viewModel: SettingCellViewModel)
case removeCacheItem(viewModel: SettingCellViewModel)
case acknowledgementsItem(viewModel: SettingCellViewModel)
case whatsNewItem(viewModel: SettingCellViewModel)
case logoutItem(viewModel: SettingCellViewModel)
}
extension SettingsSection: SectionModelType {
typealias Item = SettingsSectionItem
var title: String {
switch self {
case .setting(let title, _): return title
}
}
var items: [SettingsSectionItem] {
switch self {
case .setting(_, let items): return items.map {$0}
}
}
init(original: SettingsSection, items: [Item]) {
switch original {
case .setting(let title, let items): self = .setting(title: title, items: items)
}
}
}
let dataSource = RxTableViewSectionedReloadDataSource<SettingsSection>(configureCell: { dataSource, tableView, indexPath, item in
switch item {
case .bannerItem(let viewModel),
.nightModeItem(let viewModel):
let cell = (tableView.dequeueReusableCell(withIdentifier: switchReuseIdentifier, for: indexPath) as? SettingSwitchCell)!
cell.bind(to: viewModel)
return cell
case .themeItem(let viewModel),
.languageItem(let viewModel),
.contactsItem(let viewModel),
.removeCacheItem(let viewModel),
.acknowledgementsItem(let viewModel),
.whatsNewItem(let viewModel),
.logoutItem(let viewModel):
let cell = (tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as? SettingCell)!
cell.bind(to: viewModel)
return cell
}
}, titleForHeaderInSection: { dataSource, index in
let section = dataSource[index]
return section.title
})
output.items.asObservable()
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: rx.disposeBag)
RxDataSources
swiftHub

How to append new values to an already existing value in a parse database?

I have a table in parse.com database called as test. I have a column called phone number in it. So i successfully inserted a value in the column (ex. 123456789) now i want to append another value in the same field. (Ex. 123456789,987654321) How do I do that ? I am using Parse and Xcode together. The language which I am using is swift.
I know it's Swift, but you can do it like this in Objective-c, (I'm sure it's close to the swift version):
PFQuery *query = [PFQuery queryWithClassName:#"className"];
[query getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (object) {
// let's update your phone number
NSString *numbers = #"123";
NSString *newStr = [NSString stringWithFormat:#"%# %# ",object[#"telNumber"], numbers]
object[#"telNumber"] = newStr;
[object saveInBackground];
}
}];
Do you want to add another column or do you want to change the current value in the column phone number?
To add another column you can create a new colmn in parse or you create it programatically:
var stream = PFObject(className: "test")
stream["phoneNumber"] = "123"
stream["anotherValue"] = "abc"
func saveInBackgroundWithBlock(stream: PFObject) {
stream.saveInBackgroundWithBlock {
(succeeded: Bool, error: NSError?) -> Void in
var withStatus = "succ"
if !succeeded {
withStatus = "not succ"
}
print("Upload: \(withStatus)")
}
}
If you want to edit the current value you have to edit it in the following way:
let phoneNumber = "345"
stream.setValue(phoneNumber, forKey: "phoneNumber")
user?.saveInBackgroundWithBlock {
(succeeded: Bool, error: NSError?) -> Void in
var withStatus = "succ Change"
if !succeeded {
withStatus = "not SuccChange"
}
print("Upload: \(withStatus)")
}
If I answered or understood your question wrong, please feel free to correct me. Regards, Alex

How do I use indexOf to get the index of a selected NSCollectionView?

I've seen some examples in Objective-C such as this one, but NSCollectionView.subview uses a property called .indexOfObject which I am assuming has been replaced with .indexOf, but I'm not sure how to use .indexOf.
Context in Objective-C
NSInteger index = [[myCollectionView subviews] indexOfObject:self];
Swift Version
var index = collectionView.subviews.indexOf(element: Self.Generator.Element)
Question:
How do I use indexOf to get the index of a selected NSCollectionView?
The usage of indexOf is straightforward:
let label = UILabel()
let tableView = UITableView()
let subviews: [UIView] = [label, tableView]
print(subviews.indexOf(label)) // Optional(0)
print(subviews.indexOf(tableView)) // Optional(1)
As you can see, it returns an optional which will be nil if the object is not found in the array. You can unwrap the actual index like this
if let index = collectionView.subviews.indexOf(self) {
// do your stuff
} else {
// view not found in subviews
}

How to get selected item of NSOutlineView without using NSTreeController?

How do I get the selected item of an NSOutlineView with using my own data source.
I see I can get selectedRow but it returns a row ID relative to the state of the outline. The only way to do it is to track the expanded collapsed state of the items, but that seems ridiculous.
I was hoping for something like:
array = [outlineViewOutlet selectedItems];
I looked at the other similar questions, they dont seem to answer the question.
NSOutlineView inherits from NSTableView, so you get nice methods such as selectedRow:
id selectedItem = [outlineView itemAtRow:[outlineView selectedRow]];
Swift 5
NSOutlineView has a delegate method outlineViewSelectionDidChange
func outlineViewSelectionDidChange(_ notification: Notification) {
// Get the outline view from notification object
guard let outlineView = notification.object as? NSOutlineView else {return}
// Here you can get your selected item using selectedRow
if let item = outlineView.item(atRow: outlineView.selectedRow) {
}
}
Bonus Tip: You can also get the parent item of the selected item like this:
func outlineViewSelectionDidChange(_ notification: Notification) {
// Get the outline view from notification object
guard let outlineView = notification.object as? NSOutlineView else {return}
// Here you can get your selected item using selectedRow
if let item = outlineView.item(atRow: outlineView.selectedRow) {
// Get the parent item
if let parentItem = outlineView.parent(forItem: item){
}
}
}
#Dave De Long: excellent answer, here is the translation to Swift 3.0
#objc private func onItemClicked() {
if let item = outlineView.item(atRow: outlineView.clickedRow) as? FileSystemItem {
print("selected item url: \(item.fileURL)")
}
}
Shown is a case where item is from class FileSystemItem with a property fileURL.

Execute an Action when the Enter-Key is pressed in a NSTextField?

I have a small problem right now. I want to execute a method when the Enter key is pressed in a NSTextField. The user should be able to enter his data and a calculation method should be executed as soon as he hits the enter key.
You can do this by setting the text field's action. In IB, wire the text field's selector to your controller or whatever object presents the IBAction you want to use.
To set it in code, send the NSTextField a setTarget: message and a setAction: message. For example, if you're setting this on your controller object in code, and your textField outlet is called myTextField:
- (void)someAction:(id)sender
{
// do something interesting when the user hits <enter> in the text field
}
// ...
[myTextField setTarget:self];
[myTextField setAction:#selector(someAction:)];
You have to do only this
For some keys (Enter, Delete, Backspace, etc)
self.textfield.delegate = self;
and then implement this method
- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
{
NSLog(#"Selector method is (%#)", NSStringFromSelector( commandSelector ) );
if (commandSelector == #selector(insertNewline:)) {
//Do something against ENTER key
} else if (commandSelector == #selector(deleteForward:)) {
//Do something against DELETE key
} else if (commandSelector == #selector(deleteBackward:)) {
//Do something against BACKSPACE key
} else if (commandSelector == #selector(insertTab:)) {
//Do something against TAB key
} else if (commandSelector == #selector(cancelOperation:)) {
//Do something against Escape key
}
// return YES if the action was handled; otherwise NO
}
The Swift 3 / 4 / 5 version of #M.ShuaibImran's solution:
First subclass your ViewController to: NSTextFieldDelegate
class MainViewController: NSViewController, NSTextFieldDelegate {
...
}
Assign the textField delegate to the ViewController in your viewDidLoad():
self.textField.delegate = self
Include the NSTextFieldDelegate method that handles keyboard responders:
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if (commandSelector == #selector(NSResponder.insertNewline(_:))) {
// Do something against ENTER key
print("enter")
return true
} else if (commandSelector == #selector(NSResponder.deleteForward(_:))) {
// Do something against DELETE key
return true
} else if (commandSelector == #selector(NSResponder.deleteBackward(_:))) {
// Do something against BACKSPACE key
return true
} else if (commandSelector == #selector(NSResponder.insertTab(_:))) {
// Do something against TAB key
return true
} else if (commandSelector == #selector(NSResponder.cancelOperation(_:))) {
// Do something against ESCAPE key
return true
}
// return true if the action was handled; otherwise false
return false
}
In your delegate (NSTextFieldDelegate), add the following:
-(void)controlTextDidEndEditing:(NSNotification *)notification
{
// See if it was due to a return
if ( [[[notification userInfo] objectForKey:#"NSTextMovement"] intValue] == NSReturnTextMovement )
{
NSLog(#"Return was pressed!");
}
}
It's very easy and you can do it directly from UI editor
Right click the Text Feild, drag Action reference to the button as shown below in the screenshot
Now it will give you some option as shown in screen shot below, you need to select perform click
Now it should look like this
Note: The event will be raised as soon as you press Tab or Enter key. In case you want the action should only be raised when user presses the Enter key then you have to do a setting. Go to the Attribute inspector and change the Action property to Send on Enter only as shown in screen shot below
NSTextFieldDelegate's – control:textView:doCommandBySelector: is your friend.
Look for the insertNewline: command.
In Interface Builder - click on your NSTextField, go to the connections editor, drag from selector to your controller object - you're actions should come up!
Best way to do that is to bind the NSTextField value with NSString property.
For Example,define a method:
(void)setTextFieldString:(NSString *)addressString {}
add bindings:
[textField bind:#"value" toObject:self withKeyPath:#"self.textFieldString" options:nil];
Enter any text and hit the return key, setTextFieldString will be called.

Resources