My app was working fine without 3Dtouch implementation; But with the 3Dtouch added the app continues to work great and rotates normally until 3D touch is used (peek or pop);I had a tableViewCell handled peek/pop and the preview delegate.The presentation would be done twice and trigger this in the console:#"Warning: Attempt to present on which is already presenting (null)
I have experienced the same problem. The cause of it is that you are registering the table view cell in cellForRowAtIndexPath.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// myTableViewCell is created or reused here
...
registerForPreviewingWithDelegate(self, sourceView: myTableViewCell)
...
}
This causes the cell to be registered multiple times when it is reused by the table view. But registering the same view multiple times with registerForPreviewingWithDelegate is not allowed and leads to the warning and rotation problems you describe.
See the Apple documentation
You can designate more than one source view for a single registered
view controller, but you cannot designate a single view as a source
view more than once.
The solution is simple. Check if the view is already registered before you register it a second time and unregister if necessary.
private var previewingContexts = [MyTableViewCellClass: UIViewControllerPreviewing]()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// myTableViewCell is created or reused here
...
if let previewingContext = previewingContexts[myTableViewCell] {
unregisterForPreviewingWithContext(previewingContext)
}
let previewingContext = registerForPreviewingWithDelegate(self, sourceView: myTableViewCell)
previewingContexts[myTableViewCell] = previewingContext
...
}
You can also use the method described here:
http://krakendev.io/peek-pop/
This uses the location parameter which is passed to the delegate to find out which cell was tapped. In this way you just have to register the whole table view and not every single cell. This might be an ever better solution. In my case this was not possible because I have nested collection views inside tableview cells, so it is way more complex.
Related
I'm implementing an API called Skeleton View on my Xcode Project. Sometimes when the app loads for the first time, it takes a little bit longer to load the UI Elements, because the data come from JSON and if the user is using 4G or even a bad internet, will remain an empty field. In additional, using this API it shows like a gray view animated placeholder on each UIElement that doesn't received data.
However, I don't know how to check when the UIImage received a value to be able to remove the Skeleton effect and present the Image. I'll post some pictures below.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellID") as! TableViewCell
let model = arrCerveja[indexPath.row]
cell.labelName.text = model.name
cell.labelDetail.text = "Teor alcoĆ³lico: \(model.abv)"
let resource = ImageResource(downloadURL: URL(string: "\(model.image_url)")!, cacheKey: model.image_url)
if cell.imageViewCell.image != nil {
cell.imageViewCell.kf.setImage(with: resource)
cell.imageViewCell.hideSkeleton()
}else{
cell.imageViewCell.showAnimatedGradientSkeleton()
//The placeholder still there even when the images already got downloaded
//What I wanted was, while the UIImageView has no value, the placeholder
//persists, but when receive the image the placeholder should be dismissed.
}
return cell
}
That's what I got on UIImages(it has a blur animation passing by):
The problem I'm facing is how do I dismiss this effect after each image load itself?
You will need to start the animation when starting the download (often just before your function that get the data) with:
view.startSkeletonAnimation()
The view property here is the one of your view controller, SkeletonView is recursive and will animate all views inside it.
And then after the download is completed (often in a closure before you reload your table view and I assume that all your images are downloaded and ready to be displayed) stop the animation with:
self.view.stopSkeletonAnimation()
Skeleton view also provide you a delegate for UITableView:
public protocol SkeletonTableViewDataSource: UITableViewDataSource {
func numSections(in collectionSkeletonView: UITableView) -> Int
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
func collectionSkeletonView(_ skeletonView: UITableView, cellIdenfierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier
}
As the documentation says (read the part about Collections to understand how it works):
This protocol inherits from UITableViewDataSource, so you can replace this protocol with the skeleton protocol.
You will need to make your tableView skeletonnable in you viewDidLoad() function:
tableView.isSkeletonable = true
Inside my cell for row at indexPath, I have been using the following code to do most of my work because that is what I have been taught. I was wondering, is it necessary to always use if let to do this work? Because I never find that I ever fall into the else statement.
When would I need to use if let or just let inside cellForRowAtIndexPath?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCellWithIdentifier("myCustomCell") as? myCustomCell {
} else {
return myCustomCell()
}
}
UITableView has two dequeue modes:
dequeueReusableCell(withIdentifier:): The table view tries to dequeue a cell. If there are none, it will try to create one using the cell you registered with the reuseIdentifier. If you didn't register a cell, it will return nil giving you the chance to create one yourself.
This is where the else clause in your code would come into effect. Basically never, since presumably you did register a cell class. Most likely in the Storyboard (by setting the identifier in the inspector) but you can also do it in code.
dequeueReusableCell(withIdentifier:for:), note the additional parameter. The table view tries to dequeue a cell: If there are none, it will try to create one using the cell class you registered using the reuseIdentifier. If you didn't register a cell, it will crash.
Solution
If you can guarantee that a) the cell is correctly registered and b) the type of the cell is set correctly, you would generally use the second option. This avoids the exact issue you're having:
let cell = tableView.dequeueReusableCellWithIdentifier("myCustomCell", forIndexPath: indexPath) as! myCustomCell
(However, it is still perfectly fine to use if let even in this case.)
I have been learning swift through the last few days and I have come across an error that I have been stuck on for quite a while now.
I am attempting to get the selected indexPath so that I can then push data according to which item he selected. I have searched through and tried many different solutions I have found on stack overflow as well as different websites but I am not able to get this figured out still.
The code is below:
#IBOutlet var selectGroceryTable: UITableView!
/* Get size of table */
func tableView(tableView: UITableView, numberOfRowsInSection: Int) ->Int
{
return grocery.count;
}
/* Fill the rows with data */
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let myCell:UITableViewCell = selectGroceryTable.dequeueReusableCellWithIdentifier("groceryListRow", forIndexPath:indexPath) as! UITableViewCell
myCell.textLabel?.text = grocery[indexPath.row];
myCell.imageView?.image = UIImage(named: groceryImage[indexPath.row]);
return myCell;
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
print("Row Selected");
NSLog("Row Selected");
}
Nothing ever prints acting like the function is not being called. However, I do not understand why this would not be called?
Any help would be greatly appreciated!
UPDATE:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
selectGroceryTable.data = self;
selectGroceryTable.delegate = self; //gives error states you can not do this
}
There are a couple of things to check in cases like this:
First, what kind of method is didSelectRowAtIndexPath?
Answer: It's a UITableViewDelegate method. Did you set your view controller up as the delegate of the table view? If not, this method won't get called.
Second, have you made absolutely certain that the method signature is a perfect match for the method from the protocol? A single letter out of place, the wrong upper/lower case, a wrong parameter, and it is a different method, and won't be called. it pays to copy the method signature right out of the protocol header file and then fill in the body to avoid minor typos with delegate methods.
It looks to me like your method signature is correct, so my money is on forgetting to set your view controller up as the table view's delegate.
I have a UITableView with custom cells. Usually when the user taps the cell, it triggers this function:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//Segue to another view
}
If in that view they mark the cell as finished, when they return the code will add the standard check accessory.
When the cell isn't finished, I'd like an empty check that can be tapped (I know I can add a custom image accessory), with the tap allowing the user to skip the segue and quickly mark the cell as finished. But I can't seem to make both work:
Tapping on the main body of the cell should call didSelectRowAtIndexPath
Tapping on the accessory view (empty check mark) should call a different set of code.
I've tried accessoryButtonTappedForRowWithIndexPath but it doesn't seem to even get called.
func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
//Shortcut code to change the cell to "finished"
}
Is it possible to have clicking on the main body trigger one set of code and clicking on the accessory view trigger another? If so, how?
You should add a UIButton to your custom UITableViewCell. You can then add a target for that button called pressedFinished, by saying something like cell.button.addTarget(self, action: "finishedPress:", forControlEvents: .TouchUpInside)
or something then in pressedFinished you can say something like:
func pressedFinished(sender:UIButton)
{
let location = self.tableView.convertPoint(sender.bounds.origin, fromView: sender)
let indexPath = self.tableView.indexPathForRowAtPoint(location)
//update your model to reflect task at indexPath is finished and reloadData
}
It's generally not a great practice to use tags as they have no inherent meaning. Also tags mapped to indexPath.row will only work if the table has one section.
Another option might be using UITableViewRowAction:
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [AnyObject]? {
let rowAction : UITableViewRowAction
if model[indexPath].finished
{
rowAction = UITableViewRowAction(style: .Normal, title: "Mark as Unfinished", handler: {(rowAction:UITableViewRowAction,indexPath:NSIndexPath)->() in
//mark as unfinished in model and reload cell
})
}else{
rowAction = UITableViewRowAction(style: .Normal, title: "Mark as Finished", handler: {(rowAction:UITableViewRowAction,indexPath:NSIndexPath)->() in
//mark as finished in model and reload cell
})
}
return [rowAction]
}
Its a Objective-C solution.
When you have added your own accessoryButton, that won't call tableviewDelegate method of 'accessoryButtonTappedForRowWithIndexPath'.
What you should do is, create a UIButton and addTarget to that method, then add it as a accessoryButton on tableViewCell. Also set tag value to button as index path.row so that you get to know which row is clicked.
I selected the answer that I thought gave a thorough answer, and which helped me out. However, I deviated from that answer a bit and wanted to share this as well:
Making The Accessory View Appear / Disappear
I load several lists into the same storyboard table. Sometimes the list should show indicators, other times no accessory is necessary. To do this, I added a button in the storyboard, centered vertically and trailing = 0 to the right container view. Then I gave it a width of 0 and gave that constraint an IBOutlet in my code. When I want the accessory, I simply give it a width. To make it disappear, I set its width back to 0.
Accessories and Core Data
Accessories are kind of a pain because if the user checks something off then closes the app, they expect it to be remembered. So for every change, you need to save that cells state in CoreData. I added an attribute called "state" and give it a value of "selected" when the accessory should show up filled.
This also means that I have to sort by that attribute when retrieving the list. If you already have a sort descriptor, you'll now need several.
One of my UIViewController with UICollectionView crashes sometime when I swipe very fast. It crashes in let option = self.options[indexPath.row]:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell= collectionView.dequeueReusableCellWithReuseIdentifier("optionCell", forIndexPath: indexPath) as MyCell
let option = self.options[indexPath.row]
return cell
}
I found the problem is that the self.options is not created successfully sometime. I create the self.options in viewWillAppear:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.options = ...
}
There are two possibilities:
The viewWillAppear is not called at all
The viewWillAppear will be called after cellForItemAtIndexPath
Did I miss anything in my code? Thanks
In a default implementation, collectionView:cellForItemAtIndexPath: will get called after viewWillAppear:.
However, collectionView:cellForItemAtIndexPath: is initially triggered by a call to [UICollectionView -layoutSubviews]. You may be doing something else to make this method get called, which explains the behavior you're seeing. If you want, you can set a breakpoint in collectionView:cellForItemAtIndexPath:, and look at the stack trace to see why it's being called.
It appears to me the answer lies in another method for your data source: the number of items you have to present.
I would guess the value you are returning in that data source method is not based on the options array and instead hard coded. That would explain why you are being asked to provide a cell for data you don't have set up yet.
I would work under the assumption that everything needed must be present after viewDidLoad.
For your options array, the safest solution is a getter that calculates the options array when it is not available yet. And of course the count should be taken from the options array, not from another array.