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.
Related
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.
With iOS 8 and Xcode 6, in storyboards we now have the screen size grid letting us select a size class. Where you can select layout formatting for the different screen sizes.
I have found this brilliantly helpful, as it allows me to set the base constraints and then unique ones for each screen size.
My question is, can you do this programmatically? I create my NSLayoutConstraint as normal but I need to be able to specify different constraints for different screen sizes.
iOS 8 introduces the active property on NSLayoutConstraint. It allows you to activate or deactivate a constraint. There are also methods to activate/deactivate multiple constraints.
+ (void)activateConstraints:(NSArray *)constraints
+ (void)deactivateConstraints:(NSArray *)constraints
Keep your constraints in arrays when creating them programmatically.
Create an array for each of the layouts you need.
Activate/Deactivate whatever set of constraints you need from within willTransitionToTraitCollection
To answer your question, you can set the size class programmatically, however, it's a bit of a pain. You must call "setOverrideTraitCollection" but from the parent view controller, not the one you actually wished to make a trait change to.
In my situation, I wanted to change the Master VC of a split view controller on iPad to look differently than the one on the iPhone, however, they are both set to Compact width / Regular height by default. So I subclassed the Master's nav controller and added code to set the Master's traits to Regular width when it's not an iPhone.
Swift code:
class MasterNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
if (self.traitCollection.userInterfaceIdiom != .Phone) {
let newTraitCollection = UITraitCollection(horizontalSizeClass: .Regular)
self.setOverrideTraitCollection(newTraitCollection, forChildViewController: self.topViewController)
}
}
}
I hope this helps someone looking for a similar solution.
It's a bit confusing & hard to find in the documentation because a "size class" isn't actually a "Class" like NSObject. They're really defined in an enum/typedef called: UIUserInterfaceSizeClass
The way to get the horizontal & vertical size class for a view is with a UITraitCollection
Class/Type methods for UITraitCollection allow you to create one based on a particular display scale (e.g. retina or not), from an array of other trait collections, with a UI idiom (iPad/iPhone), or specific horizontal & vertical options (compact, regular), but to be honest I'm not sure yet how you'd use this...
This question discusses updating constraints when the traitCollection changes, using willTransitionToTraitCollection(newCollection: UITraitCollection!,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!)
You're right that both the UITraitCollection and its properties are readonly, but clearly you can create a new collection for some reason, and handle layout changes when the traitCollection changes.
This previous question is pretty similar & links to an Apple article about using Adaptive Layout. Also check the WWDC video "Building Adaptive Apps with UIKit."
Let's say I have a two subclasses of UIViewController called MasterViewController and DetailViewController.
DetailViewController has a property of type NSNumber called level and a UILabel called levelLabel.
MasterViewController has a segue to DetailViewController called ToDetail. MasterViewController's prepareForSegue is like so
- (void)prepareForSegue:(UIStoryboardSegue)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"ToDetail"]) {
DetailViewController *detailVC = (DetailViewController *)segue.destinationViewController;
detailVC.level = [NSNumber numberWithInt:10]; // never mind the literal...pretend there was some algorithm for it
}
}
So then, in DetailViewController we implement the setter for levelLabel like so:
- (void)setLevelLabel:(UILabel *)levelLabel
{
if (levelLabel) {
_levelLabel = levelLabel;
_levelLabel.text = level.stringValue;
}
}
Is this good code design? Also, could you critique my code writing style? I pretty much wrote all this code on the fly so this is pretty much how I write code for the most part.
I thought of this question while showering because this is how I implement the setting of almost all the label texts that depend on a segue.
What follows is my own way of thinking about such relationships. Italics applies to your question.
You have the thing being controlled (the label) the controller (destination view controller) and the context it is being controlled within (the source view controller). This can also be expressed as model-view-controller, but I think thinking about a context can apply to much more specific and localised situations.
You should generally try to keep information flow going in one direction, from the context downwards. Objects should not have to be aware of the context in which they exist, ie they shouldn't have to ask for any information, they should be told everything they need to operate. So the source view controller should push the level to the destination view controller, the destination view controller should push this information to the label. This is what you already have, sort-of.
To build upon the above, not only should information flow in one direction, but I also try to ensure the relationships are causal, ie pushing information from one object to another should cause it to subsequently be pushed to the next object. Your code is not doing this which is probably why you have a bad feeling about it.
A more appropriate thing to do is set the text property of the label within the level setter, so that when you set or change the level, the label will update subsequently. The label may or may not be loaded so you will have to check whether it is using -isViewLoaded; -viewDidLoad is the appropriate place to set the text property upon first load.
(When I say 'push' that's just my way of thinking about setting properties or passing arguments because it implies directionality. It is really dependency injection. An example of pulling information would be delegates and data sources. But note here still the object isn't aware of any context, delegates and data sources are clearly defined as protocols, not classes, and usually within the same header file, and are themselves pushed onto the object from a surrounding context. So yes the object is asking for information, but on its own terms and from a system it has no knowledge of.)
Re coding style:
That's exactly how I write code but note Apple reserves the use of underscore prefixes
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.
I'm starting a small project that displays circles having random radii, random color and random position on the screen. I want to implement this using the MVC paradigm in Objective C.
I have a class Circle that contains the following instance variables:
CGFloat radius
CGPoint center
UIColor radiusColor
This class doesn't contain methods, it just holds data. It is put in a separate file. (Circle.m & Circle.h)
I have a myModel class that is supposed to be the model for my MVC. It contains methods that randomly generate centers inside bound of my view, where the bound dimensions are requested from the View throughout the controller.
Every time a random property (that is center, color and radius) is generated, an instance of the Circle class is created within the myModel class, and stored in an NSMutableArray.
When the generation is done, this NSMutableArray is passed to the controller, which in turn passes it to the view, thus displaying the circles.
My question is that if I am to implement the MVC paradigm correctly, should :
The Model (myModel) hold instances of Circle, or the instances of Circle should be held by the controller?
My model be made of 1 class, or is it legal to be made of several classes?
The model know the bound size of the view or is that something that a violation in the MVC philosophy?
One last question. If I have made the implementation as I have stated above, are myModel and Circle separate models or both classes constitute one model?
Thank you!
[Should] The Model (myModel) hold instances of Circle, or the
instances of Circle should be held by the controller?
The model should hold the data. That's it's job. Imagine what would happen if you wanted to change the interface to your program. Instead of (or in addition to) drawing circles on the screen, you might want to display a list of circles and their locations. You'd might want to change or replace the view controller to do that, but you wouldn't need to change the model that stores the circles. Likewise, you might want to change the way that circles are generated, but keep displaying them the way you are now. In that case, you'd change the model, but the view controller and view could probably stay the same.
[Should] My model be made of 1 class, or is it legal to be made of several classes?
A data model is typically a whole graph of objects, very often of different types. You might have one object that manages the rest (although you don't have to). For example, your MyModel class contains an array that stores Circle objects. You could add Square objects, Group objects, etc.
[Should] The model know the bound size of the view or is that
something that a violation in the MVC philosophy?
The model shouldn't know specifically about the view, but it's fine for the view controller to tell it to produce circles within a given range of coordinates. That way, if the view changes size or orientation, the view controller will likely know about it, and it can in turn give the model new info.
If you have other components to your model than just circles, wrap everything in myModel. Even if you don't, you might still want to do so to allow for future additions.
Depends on your design. If you are writing a "document based" application (regardless of whether you are using UIDocument) you normally would have a single class that contains the others. Even if you aren't, having a single root class for archiving purposes, etc., is usually convenient.
The model should definitely not know anything about the view hierarchy. (Note that this is different from knowing something like "canvas size" - it would be legitimate to store such a property in the model, and let the view display the canvas however it wishes, such as in a UIScrollView.)
Btw, kudos for thinking about this ahead of time!