I implemented a custom mechanism to perform reordering using drag and drop in a UICollectionView, using a UILongPressGestureRecognizer.
I have a very simple UICollectionView with a custom UICollectionViewLayout.
My layout class is not a subclass of UICollectionViewFlowLayout.
Here is how it looks like:
When I start lifting a cell to begin the drag, I update a dropProposalIndexPath property in my custom layout object so it knows which cell needs to be hidden.
My custom UICollectionViewLayout subclass returns a UICollectionViewLayoutAttributes with the alpha property set to 0 for the cell which corresponds to dropProposalIndexPath like so:
if self.dropProposalIndexPath == indexPath {
itemLayoutAttributes.alpha = 0
}
It makes the lifted cell hidden thus creating a "hole" in the UICollectionView.
When the user moves its finger, I update the dropProposalIndexPath property of my custom UICollectionViewLayout subclass and invalidate its layout, then move the cell that acts as a hole at the new indexPath:
// Inform layout of the indexPath of the dropProposal
self.reorderableLayout?.dropProposalIndexPath = dropProposalIndexPath
// Move the cell
collectionView.moveItem(at: draggingIndexPath, to: dropProposalIndexPath)
Here is how it looks:
Pretty bad huh?
I implemented the exact same logic in a demo project using a subclass of UICollectionViewFlowLayout and the transition is seamless:
I guess I'm doing something wrong or forget to do something in my UICollectionViewLayout subclass.
I can post more code if needed.
Please note that using the built-in drag & drop mechanism (introduced with iOS 11) to perform the reordering is not an option, I want to use my custom implementation for various reasons.
In case someone encounters the same issue, the solution was to implement the following method in your UICollectionViewLayout subclass:
func initialLayoutAttributesForAppearingItem(at: IndexPath) -> UICollectionViewLayoutAttributes?
You then return a UICollectionViewLayoutAttributes with the alpha property set to 0 in the initialLayoutAttributesForAppearingItem function if the itemIndexPath parameter matches the indexPath of your dragging cell.
Related
I am wondering how did you manage to correctly respect MVC design pattern on your IOS developments with Swift ? What I feel right now is that a view controller mix both the controller part and the view part but it feels wrong right ? To respect MVC, we clearly need to separate the controller and the view. How did you make this ?
Maybe, it's more obvious with other design patterns like MVVM or MVP ?
You can separate your project and create a structure where you have all the logic and models in one place and all the viewControllers in one place.
Like this for example:
Core
Models
Person.swift
Car.swift
Helpers
Extensions.swift
APIHelper.swift
Webservice
Webservice.swift
Controllers
ViewController.swift
SecondViewController.swift
So you basically have all the logics and calculations in your Core and all the views and UI elements in your Controllers. With this way you won´t have to do the same logic code multiple times. You could also create custom views and add them to your Core which you later can call in your Controllers.
Although it might be too broad to answer such a question, I will answer your specific issue about:
view controller mix both the controller part and the view part...
Note that when working an iOS project, it leads you implicitly to apply the MVC pattern. By default, the view controllers represent the Controller part, the .storyboard and .xib files represent the View part and any model objects for encapsulating the data (data templates) represents the Model.
What I feel right now is that a view controller mix both the
controller part and the view part but it feels wrong right ?
The view controller has many responsibilities to be handled, in addition to interprets user actions, it should also have to be the intermediary between view and the model(s), and don't forget about handling the integration with web services... That's the issue of the Massive View Controller.
if you tried to do some researches about solving this issue, you would find many approaches to follow such as applying other structural patterns like MVVM, MVP , VIPER or Clean Architecture or even simpler approaches such as dividing your project files to increase the jobs independency which leads to make it more clear and easy to trace, MVC-N might be a good example.
But for the specific case that are you asking about (mix both the controller part and the view part) to keep it simple: I would recommend to separate the logic of the data representation based on its view, example:
One of the most popular case when building an iOS project is working with table view and its cells, consider the following:
ViewController.swift:
class ViewController: UIViewController {
// ...
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myDataSourceArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCellID") as! MyCustomCell
// ...
return cell
}
}
MyCustomCell.swift:
class MyCustomCell: UITableViewCell {
#IBOutlet weak var lblMessage: UILabel!
}
Now, imagine that -somehow- there is a requirement to change the lblMessage label text color in the cell based on a bunch of complex calculations, for such a case:
Do not do this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCellID") as! MyCustomCell
// do calulations which might needs tens of lines to be achieved, based on that:
cell.lblMessage.textColor = UIColor.red
return cell
}
That leads to make the view controller to be massive and contains alot of jobs, instead do:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCellID") as! MyCustomCell
//...
cell.doCalaulations()
return cell
}
In the cell class:
class MyCustomCell: UITableViewCell {
#IBOutlet weak var lblMessage: UILabel!
func doCalaulations() {
// do calulations which might needs tens of lines to be achieved, based on that:
blMessage.textColor = UIColor.red
}
}
That leads to make the project components to be more encapsulated and the important thing is the view controller does not has to take care of the whole thing. For me, in cases that similar to this one, I would prefer to even make blMessage to be private which guarantees to make it only editable from the owner class (more encapsulation) to handle any needed behavior, thus any view controller should call the class methods instead of direct accessing to its properties and its IBOutlets.
Separate a usual ViewController on two different parts: View and Presenter. View responses only for displaying data and collecting user's interaction. Presenter prepares data for view and process user actions comes from View.
This idea came from Clean Architecture by uncle Bob and realized into VIPER architecture.
View has two protocols:
ViewInput : contains functions for pass data to display like set(labelText: String). This protocol should implement View. Presenter has object of type ViewInput.
ViewOutput : contains functions, which calls when some events happen in view like viewDidLoad() or rowDidSelect(at: IndexPath). This protocol should implement Presenter. View has object of type ViewOutput.
VIPER isn't trivial thing, I spent several days to understand its principles. So read articles and try to implement it in your code. Do not hesitate to ask questions.
I have created a table view with static cells and it looks good in the main storyboard but when I run the application the contents of the table view is empty. I think this is because the numberOfSections and numberOfRowsInSections functions are resetting the table view to 0. When I remove these functions the table view appears as desired.
My questions is am I okay to leave these functions out and are there any other functions I should include to conform to good coding standards?
If you use static cells, you can remove both methods or change values to return the correct number of sections and methods.
You can also define all the content of cells in the storyboard or override the method :
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
It depends on what you are looking for.
I'm a bit lost here:
I created a button acting like a colorPicker: clicking on it shows a collectionView in a popover.
I first did it with a nib fil containing a view + the collectionView (embedded in as scrollView + a clipView).
The stuff works just fine.
As the nib file is very simple (and to improve my coding skills in designing views programmatically), I decided to get rid of the nib file and write the missing part in code.
The thing is, I manage to get the job done except for the content of the collectionView. After deep investigation, it appears that, inside the method:
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem
which is supposed to manage the data source, the method
collectionView.makeItem(withIdentifier: String, for: IndexPath)
doesn't work. In fact, in:
let item = collectionView.makeItem(withIdentifier: ColorPickerPopover.itemIdentifier, for: indexPath)
item is uninitialized, as the debugger says when I step in (not nil, uninitialized). Apparently, the makeItem method never instantiate any collectionViewItem from the subclass I've made.
The identifier is fine and the collectionView.register function is called, just like in the nib version, as both projects are identical in these points. The makeItem function simply doesn't call the loadView method of the NSCollectionViewItem I've subclassed.
Any clue?
Josh
With the collectionView.makeItem(withIdentifier:for:) method, you'll first need to either register the class or the nib file with the collection view:
Using a class
Use register(_:forItemWithIdentifier:) (the first parameter accepts AnyClass?)
collectionView.register(MyCustomCollectionViewItemSubclass.self, forItemWithIdentifier: "SomeId")
Using a Nib file
Use register(_:forItemWithIdentifier:) (the first parameter accepts NSNib?).
let nib = NSNib(nibNamed: "MyCollectionViewItem", bundle: nil)!
collectionView.register(nib, forItemWithIdentifier: "SomeId")
The key thing: On your Nib file, you also have to make sure that you have an NSCollectionViewItem added to the scene. You also have to set the object's class to your subclass in order for it to work (you can set it on the inspector's panel).
Hope this helps!
Case
Normally you would use the cellForRowAtIndexPath delegate method to setup your cell. The information set for the cell is important for how the cell is drawn and what the size will be.
Unfortunatly the heightForRowAtIndexPath delegate method is called before the cellForRowAtIndexPath delegate method so we can't simply tell the delegate to return the height of the cell, since this will be zero at that time.
So we need to calculate the size before the cell is drawn in the table. Luckily there is a method that does just that, sizeWithFont, which belongs to the NSString class. However there is problem, in order to calculate the correct size dynamically it needs to know how the elements in the cell will be presented. I will make this clear in an example:
Imagine a UITableViewCell, which contains a label named textLabel. Within the cellForRowAtIndexPath delegate method we place textLabel.numberOfLines = 0, which basically tells the label it can have as many lines as it needs to present the text for a specific width. The problem occurs if we give textLabel a text larger then the width originally given to textLabel. The second line will appear, but the height of the cell will not be automatically adjusted and so we get a messed up looking table view.
As said earlier, we can use sizeWithFont to calculate the height, but it needs to know which Font is used, for what width, etc. If, for simplicity reasons, we just care about the width, we could hardcode that the width would be around 320.0 (not taking padding in consideration). But what would happen if we used UITableViewStyleGrouped instead of plain the width would then be around 300.0 and the cell would again be messed up. Or what happends if we swap from portrait to landscape, we have much more space, yet it won't be used since we hardcoded 300.0.
This is the case in which at some point you have to ask yourself the question how much can you avoid hardcoding.
My Own Thoughts
You could call the cellForRowAtIndexPath method that belongs to the UITableView class to get the cell for a certain section and row. I read a couple of posts that said you don't want to do that, but I don't really understand that. Yes, I agree it will already allocate the cell, but the heightForRowAtIndexPath delegate method is only called for the cells that will be visible so the cell will be allocated anyway. If you properly use the dequeueReusableCellWithIdentifier the cell will not be allocated again in the cellForRowAtIndexPath method, instead a pointer is used and the properties are just adjusted. Then what's the problem?
Note that the cell is NOT drawn within the cellForRowAtIndexPath delegate method, when the table view cell becomes visible the script will call the setNeedDisplay method on the UITableVieCell which triggers the drawRect method to draw the cell. So calling the cellForRowAtIndexPath delegate directly will not lose performance because it needs to be drawn twice.
Okay so by calling the cellForRowAtIndexPath delegate method within the heightForRowAtIndexPath delegate method we receive all the information we need about the cell to determine it's size.
Perhaps you can create your own sizeForCell method that runs through all the options, what if the cell is in Value1 style, or Value2, etc.
Conclusion/Question
It's just a theory I described in my thoughts, I would like to know if what I wrote is correct. Or that maybe there is another way to accomplish the same thing. Note that I want to be able to do things as flexible as possible.
Yes, I agree it will already allocate the cell, but the heightForRowAtIndexPath delegate method is only called for the cells that will be visible so the cell will be allocated anyway.
This is incorrect. The table view needs to call heightForRowAtIndexPath (if it's implemented) for all rows that are in the table view, not just the ones currently being displayed. The reason is that it needs to figure out its total height to display the correct scroll indicators.
I used to do this by:
Creating a collection objects (array of size information (dictionary, NSNumber of row heights, etc.) based on the collection objects that will be used for the table view.
This is done when we're processing the data either from a local or remote source.
I predetermine the type and size of the font that will be used, when I'm creating this collection objects. You can even store the UIFont objects or whatever custom objects used to represent the content.
These collection objects will be used every time I implement UITableViewDataSource or UITableViewDelegate protocols to determine the sizes of the UITableViewCell instances and its subviews, etc.
By doing it this way you can avoid having to subclass UITableViewCell just to get the various size properties of its content.
Don't use an absolute value for initializing the frames. Use a relative value based on the current orientation and bounds.
If we rotate it to any orientation, just do a resizing mechanism at runtime. Make sure the autoresizingMask is set correctly.
You only need the heights, you don't need all of that unnecessary things inside a UITableViewCell to determine the row height. You may not even need the width, because as I said the width value should be relative to the view bounds.
Here is my approach for solving this
I assume in this solution that only one Label has a "dynamic" height
I also assume if we make the label auto size to stretch the height as the cell grows only the cell height is needed to change
I assume that the nib has the appropriate spacing for where the label will be and how much space is above and bellow it
We dont want to change the code every time we change the font or position of the label in the nib
How to update the height:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// We want the UIFont to be the same as what is in the nib,
// but we dont want to call tableView dequeue a bunch because its slow.
// If we make the font static and only load it once we can reuse it every
// time we get into this method
static UIFont* dynamicTextFont;
static CGRect textFrame;
static CGFloat extraHeight;
if( !dynamicTextFont ) {
DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
dynamicTextFont = cell.resizeLabel.font;
CGRect cellFrame = cell.frame;
textFrame = cell.resizeLabel.frame;
extraHeight = cellFrame.size.height-textFrame.size.height; // The space above and below the growing field
}
NSString* text = .... // Get this from the some object using indexPath
CGSize size = [text sizeWithFont:dynamicTextFont constrainedToSize:CGSizeMake(textFrame.size.width, 200000.f) lineBreakMode:UILineBreakModeWordWrap];
return size.height+extraHeight;
}
Issues:
If you are not using a prototype cell you will need to check if the cell is nil and init it
Your nib / storyboard must have the UILabel autosize and have multi line set to 0
You should have a look at TTTableItemCell.m in the Three20 framework. It follows a different approach, basically by having each cell class (with some predefined settings like font, layout etc.) implement a shared method + tableView: sizeForItem: (or something like that), where it gets passed the text in the item object. When you look up the text for a specific cell, you can as well look up the appropriate font, too.
Regarding the cell height: You can check your tableView's width and, if necessary, subtract the margins by UITableViewStyleGrouped and the width an eventual index bar and disclosure item (which you look for in the data storage for your cells' data). When the width of the tableView changes, e.g. by interface rotation, you have to call [tableView reloadData].
To answer the question the original poster asked which was 'is it ok to call cellForRowAtIndexPath?', it's not. That will give you a cell but it will NOT allocate it to that indexPath internally nor will it be re-queued (no method to put it back), so you'll just lose it. I suppose it will be in an autorelease pool and will be deallocated eventually, but you'll still be creating loads of cells over and over again and that is really pretty wasteful.
You can do dynamic cell heights, you can even make them look quite nice, but it's a lot of work to really make them look seamless, even more if you want to support multiple orientations etc.
I have an idea about dynamic cell height.
Just create one instance of your custom cell as member variable of UITableViewController. In the tableView:heightForRowAtIndexPath: method set the cell's content and return the cell's height.
This way you won't be creating/autoreleasing cell multiple times as you will if you call cellForRowAtIndexPath inside the heightForRowAtIndexPath method.
UPD: For convenience, you can also create a static method in your custom cell class that will create a singleton cell instance for height calculation, set the cell's content and then return it's height.
tableView:heightForRowAtIndexPath: function body will now look like this:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [MyCell cellHeightForContent:yourContent];
}
Here's my solution which I've used to implement some rather slick cells for a chatting app.
Up to this point I've always been really really irritated with heightForCellAtIndexPath: because it leads to violating the DRY principle. With this solution my heightForRowAtIndexPath: costs 1.5ms per cell which I could shave down to ~1ms.
Basically, you want each subview inside your cell to implement sizeThatFits: Create an offscreen cell which you configure then query the root view with sizeThatFits:CGSizeMake(tableViewWidth, CGFLOAT_MAX).
There are a few gotchas along the way. Some UIKit views have expensive setter operations. For example -[UITextView setText] does a lot of work. The trick here is to create a subclass, buffer the variable, then override setNeedsDisplay to call -[super setText:] when the view is about to be rendered. Of course, you'll have to implement your own sizeThatFits: using the UIKit extensions.
Is there any way to access the owning UITableView from within a UITableViewCell?
Store a weak reference to the tableView in the cell, which you'd set in -tableView:cellForRowAtIndexPath: of your table's dataSource.
This is better than relying on self.superview to always be exactly the tableView is fragile. Who knows how Apple might re-organize the view hierarchy of UITableView in the future.
Here's a nicer way to do it, which does not rely on any particular UITableView hierarchy. It will work with any future iOS version, provided that UITableView does not change classname altogether. Not only this is extremely unlikely, but if it does happen you will have to retouch your code anyway.
Just import the category below and get your reference with [myCell parentTableView]
#implementation UIView (FindUITableView)
-(UITableView *) parentTableView {
// iterate up the view hierarchy to find the table containing this cell/view
UIView *aView = self.superview;
while(aView != nil) {
if([aView isKindOfClass:[UITableView class]]) {
return (UITableView *)aView;
}
aView = aView.superview;
}
return nil; // this view is not within a tableView
}
#end
// To use it, just import the category and invoke it like so:
UITableView *myTable = [myTableCell parentTableView];
// It can also be used from any subview within a cell, from example
// if you have a UILabel within your cell, you can also do:
UITableView *myTable = [myCellLabel parentTableView];
// NOTE:
// If you invoke this on a cell that is not part of a UITableView yet
// (i.e., on a cell that you just created with [[MyCell alloc] init]),
// then you will obviously get nil in return. You need to invoke this on cells/subviews
// that are already part of a UITableView.
UPDATE
There is some discussion in the comments about whether keeping a weak reference is a better approach. It depends on your circumstances. Traversing the view hierarchy has some small runtime penalty as you are looping until the target UIView is identified. How deep are your views? On the other hand, keeping a reference on every cell has a minimal memory penalty (a weak reference is a pointer after all), and generally adding object relationships where they are not needed is considered a bad OO design practice for many reasons, and should be avoided (see details in the comments below).
More importantly, keeping table references inside cells adds code complexity and can lead to errors, because UITableViewCells are reusable. It is no coincidence that UIKit does not include a cell.parentTable property. If you define your own you must add code to manage it, and if you fail to do so effectively you can introduce memory leaks (i.e., cells live past the lifetime of their table).
Because typically you'll be using the category above when a user interacts with a cell (execute for a single cell), and not when laying-out the table in [tableView:cellForRowAtIndexPath:] (execute for all visible cells), the runtime cost should be insignificant.
Xcode 7 beta, Swift 2.0
This works fine for me, in my opinion it has nothing to do with the hierarchy or whatever. I had no trouble with this approach so far. I've used this for many async callbacks (ex. when an API request is done).
TableViewCell class
class ItemCell: UITableViewCell {
var updateCallback : ((updateList: Bool)-> Void)? //add this extra var
#IBAction func btnDelete_Click(sender: AnyObject) {
let localStorage = LocalStorage()
if let description = lblItemDescription.text
{
//I delete it here, but could be done at other class as well.
localStorage.DeleteItem(description)
}
updateCallback?(updateList : true)
}
}
Inside table view class that implements the DataSource and Delegate
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: ItemCell = self.ItemTableView.dequeueReusableCellWithIdentifier("ItemCell") as! ItemCell!
cell.updateCallback = UpdateCallback //add this extra line
cell.lblItemDescription?.text = self.SomeList[indexPath.row].Description
return cell
}
func UpdateCallback(updateTable : Bool) //add this extra method
{
licensePlatesList = localStorage.LoadNotificationPlates()
LicenseTableView.reloadData()
}
Ofcourse you can put any variable in the updateCallback and change it's function in the tableView accordingly.
Someone might want to tell me if it is save to use though, just to be sure.
You have to add a reference back to the UITableView when you construct the table view cell.
However, almost certainly what you really want is a reference to your UITableViewController... that requires the same thing, set it as a delegate of the cell when you build the cell and hand it to the table view.
An alternate approach if you are wiring up actions is to build the cells in IB, with the table view controller as the files owner - then wire up buttons in the cell to actions in the table view controller. When you load the cell xib with loadNibNamed, pass in the view controller as the owner and the button actions will be wired back to the table view controller.
If you have custom classes for your UITableViewCells, you can add an id type variable in your cell's header, and synthesize the variable. After you set the variable when you load the cell, you are free to do what you please with the tableview or any other higher view without much hassle or overhead.
cell.h
// interface
id root;
// propery
#property (nonatomic, retain) id root;
cell.m
#synthesize root;
tableviewcontroller.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// blah blah, traditional cell declaration
// but before return cell;
cell.root = tableView;
}
Now you can call any of the tableview's methods from within your cell using the root variable. (e.g., [root reloadData]);
Ah, takes me back to the good old days of flash programming.
The two methods in other answers are: (A) store a reference to the table, or (B) walk up the superviews.
I'd always use something like (A) for model objects and (B) for table cells.
Cells
If you are dealing with a UITableViewCell, then AFAIK you must either have the UITableView at hand (say you are in a table delegate method), or are dealing with a visible cell that is in the view hierarchy. Otherwise, you may well be doing something wrong (please note the "may well").
Cells are liberally reused and if you happen to have one that is not visible then the only real reason that cell exists is because of iOS UITableView performance optimization (a slower iOS version would have released and hopefully dealloc'd the cell when it moved off screen) or because you have a specific reference to it.
I guess this is probably the reason that table cells are not endowed with a tableView instance method.
So (B) gives the right result for all iOS's so far, and all future ones until they radically change how views work.
Though in order to avoid writing generalizable code over and over, I'd use this:
+ (id)enclosingViewOfView:(UIView *)view withClass:(Class)returnKindOfClass {
while (view&&![view isKindOfClass:returnKindOfClass]) view=view.superview;
return(view);
}
and a convenience method:
+ (UITableView *)tableForCell:(UITableViewCell *)cell {
return([self enclosingViewOfView:cell.superview withClass:UITableView.class]);
}
(or categories if you like)
BTW, if you are concerned about the effect of a loop with 20 or so iterations of that size on your app performance,.. don't.
Models
If you are talking about the model object that is displayed in the cell, then definitely that model could/should know about its parent model, which may be used to find, or trigger changes in, the table(s) that the cell's model might be displayed in.
This is like (A), but less brittle with future iOS updates (eg one day they might make the UITableViewCell reuse cache exist per reuseidentifier, rather than per reuseidentifier per tableview, on that day all the implementations that use the weak reference method will break).
Th model method would be used for changes to the data displayed in the cell (i.e. model changes) since changes will propagate wherever the model is displayed (eg. some other UIViewController somewhere else in the app, logging, ...)
The cell method would be used for tableview actions, which would likely always be a bad idea if the cell isn't even a subview of a table (though it's your code, go nuts).
Either way, use a unit test rather than assuming that seemingly cleaner code just works when they update iOS.
UITableView *tv = (UITableView *) self.superview.superview;
UITableViewController *vc = (UITableViewController *) tv.dataSource;