NSTextFields using bindings not updating after initialization - xcode

I am importing data from JSON to be used as the models for some items in a CollectionView, and it seems that they are being initialized, and with the correct number of elements. But for some reason the representedObject (aliased as morpheme below) is returning nil initially. Hence the placeholder if nil values being the ones showing up.
If you click the items, I have it set up to show up in the log the name of the item clicked, and it works fine, and doesn't return the debugging defaults. So I'm guessing there is a concurrency issue going on.
For more details, this are items being manually prototyped because XCode 7 still hasn't fixed the segue bug with collection item prototypes.
Here is a screenshot I hopefully managed to get it all the important info in:
Here is the cell's controller/delagating class code in detail:
///Acts as view controller for the items of the morpheme collection
public class MorphemeCell: NSCollectionViewItem, NSTextViewDelegate{
var backgroundColor = NSColor.clearColor()
var morphemeLabel: String{
get{
return morpheme?.morphemeDisplayName ?? "Morpheme"
}
}
var allomorphsLabel: String{
get{
return (morpheme?.allomorphsAsString ?? "Allomorphs")
}
}
///The morpheme data contained in the cell
public var morpheme : Morpheme?{
get{
return representedObject as? Morpheme
}
set{
representedObject = newValue
}
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
}
///Detects clicks on each item
public override func mouseUp(theEvent: NSEvent) {
Swift.print("Clicked on " + morphemeLabel)
backgroundColor = NSColor.blueColor()
}
}
Not sure if this is needed, but just in case here is the main window's ViewController doing some setup/loading functionality.
The loading code itself:
///Loads morpheme data into memory and then into the collection view
func loadMorphemeData(){
//Open morphemes.json and begin parsing
let morphemeDataPath = "morphemes"
if let file = NSBundle.mainBundle().URLForResource(morphemeDataPath, withExtension: "json")
{
let data = NSData(contentsOfURL: file)
let json = JSON(data:data!)
//Create Morpheme objects passing in JSON elements
for morphemeElement in json{
let toAdd = Morpheme(JSONElement: morphemeElement)
fullMorphemesList.append(toAdd)
}
///TODO Use full range or filters in final product
let morphemesToLoad = fullMorphemesList[0...100]
collectionView.content.appendContentsOf(Array(morphemesToLoad) as [AnyObject])
}
else
{
print("Resource Failure")
}
So, recap: It seems that I either need to delay the collectionView's setup, or find out how to update the Labels once the data is in.
Thanks very much for any help! I'm very new to the Cocoa framework so it's been a doozy.

Take different route: bind your label to self.morpheme.morphemeDisplayName. Then set "Null Placeholder" text to be "Morpheme" (in that right panel see the list of text edits below binding settings). Finally make property morphemeDisplayName dynamic:
dynamic var morphemeDisplayName: String?
Obviously, you dont need morphemeLabel property inside cell anymore.
morpheme property of cell must be dynamic as well, or if there is setter-based property, you can call:
set {
willChangeValueForKey("morpheme")
<whatever variable> = newValue
didChangeValueForKey("morpheme")
}
Edit by original poster:
Also, in order to avoid binding synchrony issues, it turns out using viewWillAppear() instead of viewDidLoad() was causing issues with data loading and "freezing the labels".

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

Passing NSOpenPanel options to NSDocument

I know how to add an AccessoryView to an NSOpenPanel (and that works correctly).
Now I would like to make the options that the user selects in the AccessoryView available to the document that is opened.
Any suggestions how that can be doen (if at all?)
I have not found a standard solution, so I created my own:
Introduced a dictionary in the NSDocumentController that associates file URLs with option sets
Override the runModalOpenPanel and wrap the runModalOpenPanel of super with first the setup of the accessory view, and afterwards the evaluation of the options and adding of the options to the dictionary for the associated urls.
When a document is opened, the document can -through the shared NSDocumentController- access the dictionary and retrieve the options.
I am not blown away by this solution, but I also do not see an easier path.
Example code:
struct OptionsAtFileOpen {
let alsoLoadFormat: Bool
}
class DocumentController: NSDocumentController {
var fileOptions: Dictionary<URL, OptionsAtFileOpen> = [:]
var accessoryViewController: OpenPanelAccessoryViewController!
override func runModalOpenPanel(_ openPanel: NSOpenPanel, forTypes types: [String]?) -> Int {
// Load accessory view
let accessoryViewController = OpenPanelAccessoryViewController(nibName: NSNib.Name(rawValue: "OpenPanelAccessoryView"), bundle: nil)
// Add accessory view and make sure it is shown
openPanel.accessoryView = accessoryViewController.view
openPanel.isAccessoryViewDisclosed = true
// Run the dialog
let result = super.runModalOpenPanel(openPanel, forTypes: types)
// If not cancelled, add the files to open to the fileOptions dictionary
if result == 1 {
// Return the state of the checkbox that selects the loading of the formatting file
let alsoLoadFormat = accessoryViewController.alsoLoadFormatFile.state == NSControl.StateValue.on
for url in openPanel.urls {
fileOptions[url] = OptionsAtFileOpen(alsoLoadFormat: alsoLoadFormat)
}
}
return result
}
}
And then in Document
override func read(from data: Data, ofType typeName: String) throws {
...
if let fileUrl = fileURL {
if let dc = (NSDocumentController.shared as? DocumentController) {
if let loadFormat = dc.fileOptions[fileUrl]?.alsoLoadFormat {
...
}
}
}
}

I'm trying to delete a record out of Core Data in xCode 8/Swift 3 & latest core data syntax

I'm trying to delete an entire record out of coreData. I've retrieved the data and placed it in an array for manipulation (I have another function that lets the user edit the data using this method and it works fine) But I can't figure out how to just delete the record. [.remove(at: index)] doesn't work and neither does the code below. I can set all the fields to empty but that's not what I want, I want the record gone completely.
I went through the solutions given for similar problems but to no avail
#IBAction func Delete(_ sender: UIButton) { // The delete function
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "DestinationsOne")
let context = appDelagate.persistentContainer.viewContext
var destArray = [DestinationsOne]() // The data array
do {
try destArray = context.fetch(request) as! [DestinationsOne]} //Fetching the data and placing it in the array
catch{
//error message
}
for index in (0..<destArray.count - 1){ //Go through the records
if destArray[index].destID == IDTitle!{ //Picks the record to edit
let object = destArray[index]
context.delete(object
}
appDelagate.saveContext()
}
I figured this one out. I'm posting the solution in case anyone else has the same question
func deleteRecords() -> Void { //The function to delete the record
let moc = getContext()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "DestinationsOne")
let result = try? moc.fetch(fetchRequest)
let resultdata = result as! [DestinationsOne] // result as entity
for object in resultdata { // Go through the fetched result
if object.destID == self.IDTitle{ // If there is a match
moc.delete(object) // delete the object
}
}
do {
try moc.save() // Save the delete action
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
}
}
func getContext () -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
deleteRecords() // Call the function
Why not applying a predicate to search this particular record. It's much more efficient than looping through a huge list.
func deleteRecords() { //The function to delete the record
let moc = getContext()
let fetchRequest = NSFetchRequest<DestinationsOne>(entityName: "DestinationsOne")
let predicate = NSPredicate(format: "destID == %#", self.IDTitle)
fetchRequest.predicate = predicate
do {
let resultdata = try moc.fetch(fetchRequest) // no type cast needed
if let objectToDelete = resultdata.first {
moc.delete(objectToDelete) // delete the object
try moc.save() // Save the delete action
}
} catch {
print("Could not save error: ", error)
}
}
Here are some issues with your code:
viewContext should be treated as readonly - you should use performBackgroundTask for all changes to core-data
You are fetching ALL of the entities and then then going through each one to find the one you want to delete. It is a lot faster to have core-data only fetch the one you want. You can do this by setting a predicate to the fetch request.
Instead of displaying your records by doing a fetch and using the array as a model, it is better to use a NSFetchedResultsController to do the fetch and manage the results. The fetchedResultsController will keep the data in sync when objects are changed, inserted or deleted. It also has delegate methods that will inform you when there are changes so you can update your view.
remove appDelagate.saveContext from your project. Apple's template code is wrong. You should never be writing to the viewContext so you should never have a reason to save it.
where is IDTitle being set? are you sure it is not nil?
(minor) for index in (0..<destArray.count - 1){ can be replaced with for (index, element) in destArray.enumerated() { which is clearer to read.

Swift2 access component with string-name

Im more familiar with ActionScript3 and see many similarities in Swift2, kind of why i am trying out basic coding in Swift2 and Xcode.
Here's my example:
#IBOutlet weak var b1CurrSpeed: NSTextField!
I want to store b1CurrSpeed as a string so i could access the actual textfield component to set its default value when application is loaded.
I'm aiming for Swift2 for osx apps.
Here is a fictional example, not related to any actual code:
var tf:NSTextField = this.getItem("b1CurrSpeed");
tf.stringValue = "Hello world";
Reason to this approach is following...
I would like to store textfield value in NSUserDefaults, the key for defaults would be name of that textfield. So when looping thru the defaults, i would like to get key as string and when ive got that i'd have access to actual component to set its stringvalue property.
Tho, is that good approach in Swift / xCode ?
If you want to create a function for it, do someting like this:
func getStringForKey(key: String) -> String {
guard let result = NSUserDefaults.standardUserDefaults().objectForKey(key) as! String else { return "" }
return result
}
You can set the TextFields value with myTextField.text
Swift's Mirror type can get you close to it but it is limited to NSObject subclasses, can only access stored properties and is read-only.
Yet, there are ways around these limitations if your requirements will allow.
For example, here's an extension that will save and restore defaults values for all UITextfields on a view controller using the name you gave to each IBOutlet.
extension UIViewController
{
func loadDefaults(userDefaults: NSUserDefaults)
{
for prop in Mirror(reflecting:self).children
{
// add variants for each type/property you want to support
if let field = prop.value as? UITextField,
let name = prop.label
{
if let defaultValue = userDefaults.objectForKey(name) as? String
{ field.text = defaultValue }
}
}
}
func saveDefaults(userDefaults: NSUserDefaults)
{
for prop in Mirror(reflecting:self).children
{
if let field = prop.value as? UITextField,
let name = prop.label
{
if let currentValue = field.text
{ userDefaults.setObject(currentValue, forKey: name) }
}
}
}
}

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.

Resources