NSCollectionView does show nothing - cocoa

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
}

Related

UICollectionViewDelegate in RxCocoa

I write an extension for UICollectionView which will listen the delegate's
shouldHighlightItemAt method,but it don't call.
public var shouldHighlightItem: ControlEvent<IndexPath> {
let source = self.delegate.methodInvoked(#selector(UICollectionViewDelegate.collectionView(_:shouldHighlightItemAt:)))
.map { a in
return try self.castOrThrow(IndexPath.self, a[1])
}
return ControlEvent(events: source)
}
}
how to write an extension for UICollectionView of rx shouldHighlightItemAt?
You cannot use methodInvoked(_:) with a delegate method that has a non void return type.
collectionView(_:shouldHighlightItemAt:) expects you to return a Bool value. So you cannot use methodInvoked(_:).
If you have a look at the implementation of methodInvoked(_:) it gives you an explanation why this does not work:
Delegate methods that have non void return value can't be observed
directly using this method
because:
those methods are not intended to be used as a notification mechanism, but as a behavior customization mechanism
there is no sensible automatic way to determine a default return value
There is however a suggestion how you could achieve what you are trying to do:
In case observing of delegate methods that have return type is
required, it can be done by
manually installing a PublishSubject or BehaviorSubject and implementing delegate method.
In your case it would work like this:
In RxCollectionViewDelegateProxy you add the 'PublishSubject' and implement the UICollectionViewDelegate method:
let shouldHighlightItemAtIndexPathSubject = PublishSubject<IndexPath>
public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
shouldHighlightItemAtIndexPathSubject.on(.next(indexPath))
return self._forwardToDelegate?.collectionView(collectionView, shouldHighlightItemAt: indexPath) ?? true // default value
}
In your UICollectionView RxExtension you can expose the desired Observable like this:
public var property: Observable<IndexPath> {
let proxy = RxCollectionViewDelegateProxy.proxy(for: base)
return proxy.shouldHighlightItemAtIndexPathSubject.asObservable()
}
I have not tested this, I merely took it from the RxCocoa source code and modified it to fit your needs. So in theory this should work, but you might have to tweak it a little bit ;-)

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.

When is a Cocoa binding 'set'

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)

NSCache() is not working properly

I think I am tired of NSCache(). Could not understand what's the problem behind this. Trying to save an array of [AnyObject] to NSCahce(), which I have done using this following line of code.
NSCache().setObject(data, forKey: "News")
And tried to get it back using this way.
if let news = NSCache()("News") as? [AnyObject]
{
}
else
{
// I am always here :)
}
So I was thinking what's the problem with this. After searching a bit in Google I could see that setting totalCostLimit and countLimit will help you solve this problem. So I have set it like this.
NSCache().totalCostLimit = 50000
NSCache().countLimit = 50000
After setting this also, it was not working. So I thought of running this code in main thread, which I have done like this.
dispatch_async(dispatch_get_main_queue()) {
NSCache().setObject(data, forKey: "News")
}
Still it returned nil. Now last but not the least I have created one global instance of NSCache() and called all these operations using that instance. Well, doing like that also didn't give the expected result. It always gave me nil.
What's happening here? I know that NSCache() can store AnyObject values. I am saving lot of images in the same project without any problem, when I am trying to save this it returns nil.
Well this AnyObject contains some custom classes. Is that can be a problem? If yes, how will I save it locally without using CoreData or NSUserdefaults.
How I created an instance globally and accessed these. Created one instance of NSCache in the AppDelegate.swift file but outside of AppDelegate class
let mainCache = NSCache()
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(aNotification: NSNotification)
{
mainCache.totalCostLimit = 50000
mainCache.countLimit = 50000
}
}
Later I have used it like this.
mainCache.setObject(data, forKey: "News")
And getting the data back like this.
if let news = mainCache.objectForKey("News") as? [AnyObject]
{
}
else
{
// Always here :)
}
When you write NSCache() you are creating a new NSCache instance. You're doing this on just about every line.
What you need to do is create one instance, let myCache = NSCache(), and then reuse it: myCache.setObject(data, forKey: "News").

'#selector' refers to a method that is not exposed to Objective-C

The new Xcode 7.3 passing the parameter via addTarget usually works for me but in this case it's throwing the error in the title. Any ideas? It throws another when I try to change it to #objc
Thank you!
cell.commentButton.addTarget(self, action: #selector(FeedViewController.didTapCommentButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
The selector it's calling
func didTapCommentButton(post: Post) {
}
In my case the function of the selector was private. Once I removed the private the error was gone. Same goes for fileprivate.
In Swift 4
You will need to add #objc to the function declaration. Until swift 4 this was implicitly inferred.
You need to use the #objc attribute on didTapCommentButton(_:) to use it with #selector.
You say you did that but you got another error. My guess is that the new error is that Post is not a type that is compatible with Objective-C. You can only expose a method to Objective-C if all of its argument types, and its return type, are compatible with Objective-C.
You could fix that by making Post a subclass of NSObject, but that's not going to matter, because the argument to didTapCommentButton(_:) will not be a Post anyway. The argument to an action function is the sender of the action, and that sender will be commentButton, which is presumably a UIButton. You should declare didTapCommentButton like this:
#objc func didTapCommentButton(sender: UIButton) {
// ...
}
You'll then face the problem of getting the Post corresponding to the tapped button. There are multiple ways to get it. Here's one.
I gather (since your code says cell.commentButton) that you're setting up a table view (or a collection view). And since your cell has a non-standard property named commentButton, I assume it's a custom UITableViewCell subclass. So let's assume your cell is a PostCell declared like this:
class PostCell: UITableViewCell {
#IBOutlet var commentButton: UIButton?
var post: Post?
// other stuff...
}
Then you can walk up the view hierarchy from the button to find the PostCell, and get the post from it:
#objc func didTapCommentButton(sender: UIButton) {
var ancestor = sender.superview
while ancestor != nil && !(ancestor! is PostCell) {
ancestor = view.superview
}
guard let cell = ancestor as? PostCell,
post = cell.post
else { return }
// Do something with post here
}
Try having the selector point to a wrapper function, which in turn calls your delegate function. That worked for me.
cell.commentButton.addTarget(self, action: #selector(wrapperForDidTapCommentButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
-
func wrapperForDidTapCommentButton(post: Post) {
FeedViewController.didTapCommentButton(post)
}
As you know selector[About] says that Objective-C runtime[About] should be used. Declarations that are marked as private or fileprivate are not exposed to the Objective-C runtime by default. That is why you have two variants:
Mark your private or fileprivate method declaration by #objc[About]
Use internal, public, open method access modifier[About]

Resources