When is a Cocoa binding 'set' - cocoa

I've got an NSArrayController set in my storyboard where I set the mode to Entity Name with a name of Client, and bound the managed object context, selection indexes, and sort descriptors. My NSPopupButton links to that array controller and when I run I see all the elements I expect on the button.
Now I made a strong #IBOutlet in my code and I'm trying to access the contents:
let objs = clientArrayController.arrangedObjects as! [Client]
print("I have \(objs.count) clients")
I tried that code in viewDidLoad, viewWillAppear and viewDidAppear. They all say 0 clients. Clearly that's not possible as I have the clients showing in the UI.
What am I doing wrong here?

viewDidLoad, viewWillAppear, and viewDidAppear are possibly all called before your array controller collects its data from the core data store - hence they (correctly) report a count of zero. To get word of any changes to your array controller's arrangedObjects array you could use one of the aforementioned methods to install an observer that watches this object and reports any changes:
// MyPopUpController.swift
var ArrayControllerArrangedObjectsObservationContext = "arrayController.arrangedObjects"
func viewDidLoad() {
arrayController.addObserver(self,
forKeyPath: "arrangedObjects",
options: .New | .Old,
context: &ArrayControllerArrangedObjectsObservationContext)
}
You're given the opportunity to respond to changes in observeValueForKeyPath...
// MyPopUpController.swift
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
switch context {
case &ArrayControllerArrangedObjectsObservationContext:
// Check counts here
default:
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
If you only need the observer on start-up, after you've checked the counts, and done whatever you need to do you should then remove the observer:
// MyPopUpController.swift
arrayController.removeObserver(self,
forKeyPath: "arrangedObjects",
context: &ArrayControllerArrangedObjectsObservationContext)

Related

Executing NSApplicationDelegate Code Before ViewController viewDidLoad

My Swift 3, Xcode 8.2 MacOS app loads several tables through web services calls. Since the tables are used by one or more of my seven view controllers, I placed them in the AppDelegate.
The problem is that the AppDelegate methods applicationWillFinishLaunching and applicationDidFinishLaunching run after the ViewController viewDidLoad methods.
As a result the table views show no data. I was able to get it to work correctly by calling the appDelegate method that loads the data from one of the ViewController viewDidLoad methods. Since any of the ViewControllers could be invoked on application start up, I would have to add the call to all of them and some sort of flagging method to prevent redundant loads.
My question is: where can I place code that will execute prior to the ViewControllers loading? The code loads data into multiple arrays of dictionary. These arrays are in the AppDelegate.
I read up on #NSApplicationMain and replacing it with a main.swift. I assume none of application objects would have been instantiated at that point so I couldn't call their methods and don't think my code would be valid outside of a class.
The pertinent part of my appDelegate:
class AppDelegate: NSObject, NSApplicationDelegate {
var artists: [[String:Any]]? = nil
var dispatchGroup = DispatchGroup() // Create a dispatch group
func getDataFromCatBox(rest: String, loadFunction: #escaping ([[String: Any]]?) -> Void) {
let domain = "http://catbox.loc/"
let url = domain + rest
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "Get"
let session = URLSession.shared
var json: [[String:Any]]? = nil
dispatchGroup.enter()
session.dataTask(with: request) { data, response, err in
if err != nil {
print(err!.localizedDescription)
return
}
do {
json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [[String: Any]]
}
catch {
print(error)
}
loadFunction(json)
self.dispatchGroup.leave()
}.resume()
}
func loadArtistTable(array: [[String: Any]]?) {
artists = array
}
}
The ViewController code:
override func viewDidLoad() {
super.viewDidLoad()
appDelegate = NSApplication.shared().delegate as! AppDelegate
appDelegate.getDataFromCatBox(rest: "artists.json", loadFunction: appDelegate.loadArtistTable)
appDelegate.dispatchGroup.wait()
artistTable.reloadData()
}
The code works in that the TableView is populated when the window appears. While it's not a lot of code, I would have to duplicate across all my View Controllers.
This a prototype. The production version will have 14 tables and invocations.
I guess my comment should be an answer. So. Why not just make the window containing the table views not be visible on launch? Then in didFinishLaunching, load the table data and then show the window.
I don't think there is any way to do what I want the way it is structured in the question. The ViewController code could be reduced to
appDelegate = NSApplication.shared().delegate as! AppDelegate
appDelegate.getDataFromCatBox(rest: "artists.json", loadFunction: appDelegate.loadArtistTable
by creating a wrapper function in AppDelegate that had the wait in it. It also could contain a flag that indicated that a given table had already been loaded so as not to make a redundant call.
I ended up going with a different approach: I created a super class with singleton subclasses for each table. Now my viewDidLoad method looks like this:
artists.loadTable() // The sublass
artistTable.reloadData()
If any one comes up with a cleaner solution to the original problem, I'll accept their answer in place of mine.

How to set requestWhenInUseAuthorization location service in app delegate, and get coordinates in a view controller?

All the examples ask for authorization and start updating location in view controller. I want to set that in app delegate and in a view controller return a user's coordinates/location when pressing a button. (I want user coordinates/location accessible in any view controller)
You have few options here.
First of all, I would suggest you to implement or use already implemented wrapper for CLLocationManager and call it from anywhere in your code instead of implementing that functionality directly in your AppDelegate (example of such location manager implementation). Such location manager can communicate with the rest of your program using notification center (each view controller subscribe to specific notification which you define) or, for example, via an array of closures (each view controller pass its own closure for handling location update to the instance of location manager, and whenever a user location is updated, location manager calls each closure in the array hence all view controllers requested access to location data will receive that update).
Other than that, let's consider you already implemented access to user location through CLLocationManager in your AppDelegate. In a delegate function which receive updates about location (where you have actual location data), you can send this data to any view controller in your app by doing the following:
Swift - AppDelegate - using notification center
public func locationManager(manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
let arrayOfLocation = locations as NSArray
let location = arrayOfLocation.lastObject as! CLLocation
let coordLatLon = location.coordinate
var userInfo = [String:AnyObject]()
userInfo["longitude"] = coordLatLon.longitude
userInfo["latitude"] = coordLatLon.latitude
let nc = NSNotificationCenter.defaultCenter()
nc.postNotificationName("userLocationUpdate", object: self, userInfo: userInfo)
}
Swift - ViewController - using notification center (define that in viewDidAppear/viewDidLoad, for instance)
let nc = NSNotificationCenter.defaultCenter()
nc.addObserver(self, selector: #selector(ViewController.locationUpdated(_:)), name: "userLocationUpdate", object: nil)
Swift - ViewController - locationUpdated selector
func locationUpdated(notification: NSNotification) {
if let userInfo = notification.userInfo {
//access your data here
}
}

NSView with a KVC property in Swift

I have a custom NSView class defined as:
class MyView: NSView
{
var someText: NSString
override func didChangeValueForKey(key: String)
{
println( key )
super.didChangeValueForKey( key )
}
// other stuff
}
What I want to be able to do is from outside of this class change the value of someText and have didChangeValueForKey notice that someText has changed so I can, for example, set needsDisplay to true for the view and do some other work.
How an I do this?
Are you sure you need KVC for this? KVC works fine in Swift, but there’s an easier way:
var SomeText: NSString {
didSet {
// do some work every time SomeText is set
}
}
There is no KVC mechanism for this because this isn't what KVC is for.
In Objective-C, you would implement the setter explicitly (or override if the property is originally from a superclass) and do your work there.
In Swift, the proper approach is the didSet mechanism.
didChangeValueForKey() is not part of KVC, it's part of KVO (Key-Value Observing). It is not intended to be overridden. It's intended to be called when one is implementing manual change notification (as a pair with willChangeValueForKey()).
More importantly, though, there's no reason to believe that it will be called at all for a property which is not being observed by anything. KVO swizzles the class in order to hook into the setters and other mutating accessors for those properties which are actually being observed. When such a property is changed (and supports automatic change notification), KVO calls willChangeValueForKey() and didChangeValueForKey() automatically. But for non-observed properties, those methods are not called.
Finally, in some cases, such as the indexed collection mutation accessors, KVO will use different change notification methods, such as willChange(_:valuesAtIndexes:forKey:) and didChange(_:valuesAtIndexes:forKey:).
If you really don't want to use didSet for some reason, you would use KVO to observe self for changes in the someText property and handle changes in observeValueForKeyPath(_:ofObject:change:context:). But this is a bad, clumsy, error-prone, inefficient way of doing a simple thing.
KVO and didSet are not mutually exclusive:
import Foundation
class C: NSObject {
dynamic var someText: String = "" {
didSet {
print("changed to \(someText)")
}
}
}
let c = C()
c.someText = "hi" // prints "changed to hi"
class Observer: NSObject {
init(_ c: C) {
super.init()
c.addObserver(self, forKeyPath: "someText", options: [], context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
print("observed change to \(object!.valueForKeyPath(keyPath!))")
}
}
let o = Observer(c)
c.someText = "test" // prints "changed to test" and "observed change to test"
I would add to Jaanus's answer that to make the property KVC compliant, you should declare it as dynamic var someText: NSString.
But if you don't need all the bells and whistles oh KVC, didSet is the way to go.
Update
As for didChangeValueForKey: – it is intended for the opposite, for you to notify value for key has changed (if it is not due to one of the cases covered by Foundation). You should use addObserver(_:forKeyPath:options:context:) and override observeValueForKeyPath(_:ofObject:change:context:) to be notified of changes.
Alternatively you can use one of many 3rd party solutions such as ReactiveCococa or Facebook's KVOController

NSCollectionView does show nothing

I've tried to follow this guide:
Quick Start for Collection Views
using an NSImageView in the Collection View Item.
Nothing shows up, neither if i set the image with a Image Well neither if i set the array via code.
So i tried to do it programmatically, using
func representedObject(representedObject: AnyObject)
{
super.representedObject = representedObject
photoImageView.image = (representedObject as! NSImage)
println("\(representedObject)")
}
in the Collection View Item (subclassed).
If I don't subclass Collection View Item Xcode tells me that there is no prototype set, if i subclass it it tells that "could not load the nibName"... (it's in the storyboard with correct identity set)
I can't have this Collection View to work :-(
Anyway, i like the bindings... so i'd like to achieve the correct result with bindings..
I checked and rechecked every passage in the document at the link and everything seems fine. the main difference is that the document uses the app delegate, i'm using a view controller.
i translated KVC methods in swift, i think they are correct since i know them have been called. Here them are:
func insertObject(p: ClientPhoto, inClientPhotoArrayAtIndex index: Int) {
images.insertObject(p, atIndex: index)
}
func removeObjectFromClientPhotoArrayAtIndex(index: Int) {
images.removeObjectAtIndex(index)
}
func setClientPhotoArray(a: NSMutableArray) {
images = a
}
func clientPhotoArray() -> NSArray {
return images
}
Their are basically 2 ways to work with NSCollectionView. 1 is to set the itemPrototype property and the other is to override newItemForRepresentedObject. The override method is more flexible and has the advantage that you using the technique below you can create the nscollectionviewitem in storyboard and all the outlets will be set correctly. Here is an example of how I use it:
class TagsCollectionView: NSCollectionView {
// ...
override func newItemForRepresentedObject(object: AnyObject!) -> NSCollectionViewItem! {
let viewItem = MainStoryboard.instantiateControllerWithIdentifier("tagCollectionViewItem") as! TagCollectionViewItem
viewItem.representedObject = object
return viewItem
}

An NSArrayController changes its selection : what is the best way to catch this event?

One can put an observer on the selectedIndex method of NSArrayController. This method has some drawbacks I think :
what will happen when the arrangedObjects is rearranged ? I admit this is not a very important problem
if we ask the observer to remember the old value of selectedIndex, it doesn't work. It is known but I cannot find again the link.
Why doesn't NSArrayController have a delegate ?
Is there another way to achieve what I want to do : launching some methods when the selection changes ?
Observe selection key of the NSArrayController (it is inherited from NSObjectController).
It will return either NSMultipleValuesMarker (when many objects are selected), NSNoSelectionMarker (when nothing is selected), or a proxy representing the selected object which can then be queried for the original object value through self key.
It will not change if rearranging objects did not actually change the selection.
You can also observe selectedObjects; in that case you won't need to deal with markers.
Providing hamstergene's excellent solution, in Swift 4.
In viewDidLoad, observe the key path.
arrayController.addObserver(self, forKeyPath: "selectedObjects", options: .new, context: nil)
In the view controller,
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else { return }
switch keyPath {
case "selectedObjects":
// arrayController.selectedObjects has changed
default:
break
}
}

Resources