prepareLayout method of NSCollcectionViewLayout - objective-c

I have hard time understanding the purpose of prepareLayout method of NSCollectionViewLayout.
According to the official apple documentation it is written
During the layout process, the collection view calls several methods of your layout object to gather information. In particular, it calls three very important methods, whose implementations drive the core layout behavior.
Use the prepareLayout method to perform your initial layout calculations. These calculations provide the basis for everything the layout object does later.
Use the collectionViewContentSize method to return the smallest rectangle that completely encloses all of the elements in the collection view. Use the calculations from your prepareLayout method to specify this rectangle.
Use the layoutAttributesForElementsInRect: method to return the layout attributes for all elements in the specified rectangle. The collection view typically requests only the subset of visible elements, but may include elements that are just offscreen.
The prepareLayout method is your chance to perform the main calculations associated with the layout process. Use this method to generate an initial list of layout attributes for your content. For example, use this method to calculate the frame rectangles of all elements in the collection view. Performing all of these calculations up front and caching the resulting data is often simpler than trying to compute attributes for individual items later.
In addition to the layoutAttributesForElementsInRect: method, the collection view may call other methods to retrieve layout attributes for specific items. By performing your calculations in advance, your implementations of those methods should be able to return cached information without having to recompute that information first. The only time your layout object needs to recompute its layout information is when your app invalidates the layout. For example, you might invalidate the layout when the user inserts or deletes items.
So I naively used this as a guide and rewrote my implementation of custom layout. I computed collectionViewContentSize and precomputed the array used in this method
- (NSArray<__kindof NSCollectionViewLayoutAttributes *>*)layoutAttributesForElementsInRect:(NSRect)rect;
such that in all 3 required methods I just return the cached values. And after this suddenly my collectionView became extremely laggy.
Apparently the method prepareLayout is called on every scroll.
Can anyone clarify what does it mean. Or maybe I do not understand anything?

The usual thing, if you don't want prepare called every time there is a scroll event, is to keep a CGSize instance property and implement shouldInvalidateLayout so that it returns YES only if the new bounds size is different from the stored CGSize property value.
Here's a Swift example, but I'm sure you can translate it into Objective-C:
var oldBoundsSize = CGSize.zero
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
let ok = newBounds.size != self.oldBoundsSize
if ok {
self.oldBoundsSize = newBounds.size
}
return ok
}

So this was my bad. Apparently if
- (BOOL)shouldInvalidateLayoutForBoundsChange:(NSRect)newBounds;
returns YES. Then this method is called. I just changed that this method returns NO and the method prepareLayout has been stopped called all the time.

Related

Does setNeedsDisplay:NO have any use at all?

In Cocoa, when we want to redraw a view, we would send the view a setNeedsDisplay: message telling the view to redraw itself with a parameter of YES. I was wondering if there are any circumstances where you would want to send a view setNeedsDisplay:NO, such as multithreading environments, and if sending a view a setNeedsDisplay:YES, then setting it again immediately after with setNeedsDisplay:NO would make the view redraw itself. If there are no reasons to call setNeedsDisplay:NO, then why create such a tedious method, where they could instead implement something like [view redrawView]
setNeedsDisplay:NO may be used in case you want to discard previously called setNeedsDisplay:YES. E.g. sometimes it is easier to mark all subviews as needing display and then run an algorithm to unmark some of them.
As you perhaps know, the display update is automatic (if necessary) at each pass through the normal event loop. You call setNeedsDisplay: in order to force a display update in between if it is necessary.
From the documentation of NSView:
Discussion
Whenever the data or state used for drawing a view object changes, the view should be sent a setNeedsDisplay: message. NSView objects marked as needing display are automatically redisplayed on each pass through the application’s event loop. (View objects that need to redisplay before the event loop comes around can of course immediately be sent the appropriate display... method.)
The boolean parameter to this function simply specifies if the entire bounds of the view in question is affected or not, not if some property "needsDisplay" is set to true or false. Thus, setNeedsDisplay: does indeed work pretty much like a "redrawView", only with the additional parameter.
Edit
The above was inspired from the same documentation:
flag
If YES, marks the receiver’s entire bounds as needing display; if NO, marks it as not needing display.

Right way to create a customizable uiview

this question is about "style", because i think this is a very common problem and i'm looking for an elegant solution.
I have created some "advanced" UIView and i try to make them very customizable.
Usually i create the UIView structure inside a custom init method, but i need to know the value of all customizable parameter inside init method so sometimes i need a very long init method like:
initWithFrame:color:font:verticalspace:verylonglist:
I tried to use delegate design pattern but i need also to pass delegate inside init method.
My actual best solution is to leave empty the init method and move everything about layout inside a "configure" method. everytime i chance a property like background color or font i will call this method and i will rebuild the view.
I think there is a best way to solve this problem...
I'd be curious to see the code of UITableView Class, because with that class you can pass a delegate outside init method.
Check out something like a UIButton or UILabel. They both have tons of configurable aspects, however to simply create an instance of one of those objects, they need very little information.
In general, provide init methods that allow the consumer of your class to specify the least amount of information for the class to work.
If you do want to give the consumer a way to initialize the class with a bunch of values, consider using some sort of initWithDictionary: method that takes an NSDictionary of parameters. This keeps your method names short and allows the user to customize an arbitrary number of settings for your class.
You could also consider providing a way for the consumer to request an instance with some standard set of values. UITableViewCell, for example, has an initWithStyle:reuseIdentifier: method. The important part is the style - UITableViewCell provides several default styles like UITableViewCellStyleDefault and UITableViewCellStyleSubtitle.
I don't know if it is the standard/best practices way but I use a dictionary in cases like this and pass that to an initWithDictionaryinitializer. Would be possible too to create a class method that returns a 'default settings' type dictionary which can then be customized (and delegate set), so that not every param needs to be specified whenever the class is used.

NSBrowser setRowHeight, not supported for browsers with matrix delegates

I have an NSBrowser and try to use setRowHeight, but I get the error:
"setRowHeight: is not supported for browsers with matrix delegates."
I really don't understand what this means, and if someone could help me out by either telling me how to fix it or even just what a matrix delegate is, it would be much appreciated.
A delegate is a helper object that you tell the NSBrowser instance about either by using -setDelegate: in code or hooking the delegate outlet up in IB (the NIB editor). It is commonly used to fill the browser's data, determine programmatically the layout options, etc.
If you have a delegate assigned in your NSBrowser instance, you are expected (required) to give the row height using the delegate method:
- (CGFloat)browser:(NSBrowser *)browser heightOfRow:(NSInteger)row inColumn:(NSInteger)columnIndex
Which will allow you to optionally set the row height on a per-row basis, but in your case, you can safely return a constant.
A NSBrowser creates one instance of NSMatrix per column. The browser itself only manages the columns and leaves row management entirely to the matrices. The matrices display cell objects (NSCell or subclasses of it). A cell is a simple displayable object that knows nothing but how to draw itself. Unlike a view (NSView and subclasses), it never manages an own drawing context, it also doesn't belong to a window (and thus won't ever directly access the window's drawing context) and it knows nothing about the "view hierarchy" (superviews, subviews, view order, constraints, etc.), it just manages some properties, some state and knows how to draw itself to a drawing context provided.
If your delegate works with items (the docs speak about "the item delegate methods"), the developers of NSBrowser thought it's unlikely that you ever want to deal with matrices directly, thus the browser will control all the display aspects for you. You are only supposed to hand out items (basically arbitrary objects) in your delegate and answer questions about them by implementing various delegate methods. E.g.: What is the root item? Is item x a leaf item or has children? How many children does it have? What's child number n of item x? What display object (string, image, etc.) shall I use to display that item?
If you don't work with items, you have to work directly with the cells (NSBrowserCell, a subclass of NSCell) that the matrix owns and is going to display. In that case you are said to be a "browser matrix delegate". That means the browser will only ask you how many rows a column has, setup a matrix with cells for you for that column and finally pass every cell once to your delegate, so you can do something meaningful with it, e.g. fill it with displayable content, otherwise the cell would just be empty, and teach it all the stuff the browser has to know (e.g. setting the leaf property).
As a matrix based delegate has to deal with cells directly, it can as well also deal with the matrices directly and in fact that his exactly what you have to do here. E.g. if you want that all rows of your browser have a height of 50 points, implement the following NSBrowser delegate method:
- (void)browser:(NSBrowser *)browser
didChangeLastColumn:(NSInteger)oldLastColumn
toColumn:(NSInteger)column
{
NSMatrix * matrix = [browser matrixInColumn:column];
CGSize cellSize = [matrix cellSize];
if (cellSize.height != 50) {
cellSize.height = 50;
matrix.cellSize = cellSize;
[matrix sizeToCells];
}
}
Every matrix created by the browser is passed to that method at least once before it gets displayed on screen. In case that matrix is not using a cell height of 50 already, we change that and finally tell the matrix to re-layout; that is important as otherwise it won't recalculate its total height. The if is only to avoid that we call sizeToCells more often than necessary as this call may be rather expensive. In this example all columns get equal row height but of course you can set a different row heights per column.

iOS storyboard: how to handle dynamic, floating content?

My App contains a news-section. I'm trying to create a View in my storyboard which will show an news item. A news contains of a title, a date, an image, and some text.
Content like this would be easy to display in HTML since it's content is floating, but as far as I understand iOS elements have a fixed size. I think a UILabel could be fine for title and date, UIImage for the image and a UITextView for the text. I think all elements should be inside a scroll view. The problem is, that the title for some news will fill one line but for other news multiple lines.
How can I handle a setup like this in a storyboard? A link to a guide, tutorial would be fine.
There are a lot of different ways you could approach this. Ultimately you have control of the size of any of these elements at runtime. From a UX point of view you'd probably want to keep at least some of the elements fixed in size (e.g., the image and the scrollview representing the total size of the news item). That would leave playing with the relative sizes of the title and the detail. If you want to show the entire title you can use the sizeWithFont method (or one of its variations) of the NSString class to calculate how much space to use for the title, and allocate space from the remaining amount to the detail as desired.
UPDATED: I might add that I intended this as a general strategy, not specifically aimed at the new iOS 5 Storyboard functionality.
UPDATED: Well, no tutorial but here are some general guidelines (at least about the way I might tackle it).
I'd set up a NewsItem class that encapsulates the elements that you describe here, similar to what I've laid out in the example image. How you expose NewsItem is something of a matter of taste; you might want to start with a UITableView as a container as it would make managing a NewsItem collection simpler. I'd subclass UITableViewCell as NewsItemTableViewCell, and generate a nominal layout for a NewsItem using Interface Builder. So in the example image, a news item would correspond to one table row.
The main part that you are asking about is identified by the red and blue frames in the image. Using the above approach, I would add logic in the NewsItemTableViewCell to calculate the extent of the title -- this assumes that the View (in this case, the NewsItemTableViewCell) has explicit knowledge of the Model (the underlying data for the news item) in the form of a reference (i.e, an instance of a NewsItem class
that contains the items you've described retrieved from a web service). If the width of the title is greater than the default width of the red frame, I'd adjust the height of the red frame (increasing it) at the same time decreasing the height of the blue frame. This kind of approach avoids having to include extra logic for differing cell heights within the table.
An alternative I often use is to have a Controller class mediate between the View and the Model. This can be useful when you have a requirement that your table contain different kinds of cells; it also makes tableview cell reuse much simpler. In this case, declare a base class. This might look something like:
#protocol GenericCellController
- (UITableViewCell*) tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath;
#optional
- (void) tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath;
#end
and then:
#interface NewsItemTableCellController : NSObject<GenericCellController> {
NewsItem* newsitem;
}
You then set up a collection of table cell controller objects. This has the advantage that when you implement tableView:cellForRowAtIndexPath: in your tableview controller code, you only have to delegate to the cell controller object at the appropriate row. What's more, you can refer to it using a superclass designation; e.g., in a hypothetical NewsItemTableViewController class you might have:
- (UITableViewCell*) tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*) path {
NSObject<GenericCellController>* cellController = [cellControllerArray objectAtIndexPath:indexPath.row];
UITableViewCell* cell = [cellController tableView:tableView cellForRowAtIndexPath:indexPath];
return cell;
}
This approach allows you to have any class derived from NSObject in the collection and polymorphically use delegation to have the derived class handle the rendering logic. The derived Controller class has its own implementation of tableView:cellForRowAtIndexPath: with a corresponding UITableViewCell-derived subclass as described above.
If you decide to go this way, there are numerous examples of the basics of tableviews and their controllers that you can use as a preface to implementing these ideas.

How can I load data for an NSTableView without blocking the interface?

I'm initializing a simple interface, with an NSTableView bound to an array controller (which manages an array of dictionaries). I want to load the content for the array in the background (it's a very time-consuming process), updating the table view every 100 or 1000 elements. The idea is that the interface is available and responsive. I can't figure out how to also trigger an update / refresh afterwards. The table remains empty. Can anyone offer pointers?
My current approach is:
// In init for my app controller. This seems to work well, but I've tried other methods here.
[self performSelectorInBackground:#selector(loadTable) withObject:nil];
- (void)loadTable {
tracks = [[NSMutableArray alloc] initWithCapacity:[masters count]];
// ... create each object one-by-one. Add it to tracks.
for (... in ...) {
[tracks addObject:newObject];
}
// Now I don't know what to do next. The table remains empty.
// Things I've tried (though possibly not in all combinations with the
// method above):
// 1. With a suitably-defined reloadData method, which just reloads
// the table view and sets needs display.
[self performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES];
// 2. Reload directly.
[tv reloadData];
[tv setNeedsDisplay];
}
If I just load the data directly, and don't try to do that in the background, everything works fine, but it takes almost 30s.
You have the table columns (I assume you meant) bound to an array controller, so that's where the table view gets its data from. The table view may very well be asking for updated arrays, but it's asking the array controller, which doesn't know anything has changed.
The array controller won't simply turn around and ask you for fresh data; that would imply it exists solely to make it harder for you to bind the table view to your array, and that isn't the case. It's a controller; its job is to own (a copy of) the array and maintain its order and the user's selection of some subset of its objects.
Therefore, you need the array controller to find out when you add items to your array. The best way to make this happen is to bind the array controller's contentArray to a property of your controller, and update that property in a KVO-compliant manner.
That means:
Create the mutable array in your init method. (And, of course, release it in dealloc.)
Implement the array accessor methods, plus addTracksObject: and removeTracksObject: (which are technically set accessor methods, so KVO will ignore them for an array property) for your convenience.
To add a track, send yourself an addTracksObject: message. You should respond to that by sending yourself an insertObject:inTracksAtIndex: message (with [self countOfTracks] for the index, unless you want to do an insort), and you should respond to insertObject:inTracksAtIndex: by sending your tracks array an insertObject:atIndex: message.
As I mentioned, KVO will ignore addFooObject: and removeFooObject: when foo is an NSArray property, considering those only NSSet-property accessors, so you need to implement them on top of insertObject:inFooAtIndex: and removeObjectFromFooAtIndex: because those are array accessors, which means KVO will react to them.
Step 3, as I just described it, will be pretty slow, because it will cause the array controller to re-fetch your property and the table view to re-fetch the array controller's arrangedObjects at least once each for every row you add.
So, you should maintain your batch-adding behavior with this alternate step 3:
Implement insertTracks:atIndexes:, and pass it an array of one batch of (e.g., 100 or 1000) tracks and an index set formed by [NSIndexSet indexSetWithRange:(NSRange){ [self countOfTracks], countOfBatch }]. You'll also need to implement removeTracksAtIndexes:, only because KVO will ignore each insert method if you don't also have its counterpart.
You probably should have the array controller set to attempt to preserve the selection, so as not to frustrate the user too much while you're still bringing in rows.
Also, you may want to create the objects on a background thread, periodically sending yourself another batch to add using a main-thread perform. I'm ordinarily an advocate of doing things on the main thread run loop whenever possible, but this sort of thing could easily make your interface laggy while your periodic load builds up another batch.
You need to call setNeedsDisplay:YES on your table view on the main thread. Don't call it from a background thread. All Cocoa UI calls must be done on the main thread or weird things happen.