I have a tableView with two custom cells. One for the header and one for the cells inside the tableView.
The Header cell looks like this:
Label Label ScrollView(inside scrollView is an imageView)
The other cell looks like this:
Label TextField ScrollView(inside scrollView is an imageView)
When I scroll one cell horizontally all other cells (including header cell) will get the same contentOffset. This works like a charm.
I added another function which adds another imageView inside the scrollView on top of the existing imageView for all cells except the header cel. Inside the new imageView I add a line which is draggable. This is realized by a longPressureGesture. Inside this gesture I need to do a tableView.reloadData() so that each time the line moves it will be updated for all cells. LongPressureGesture allows me to do this without loosing the control of the line while doing a reloadData().
But here is my problem:
The contentOffset of the scrollView inside the headerCell is reseted after calling reloadData() inside the longPressureGesture. But the contentOffset for all other scrollViews in the cells are still the same.
I tried to add the contentOffset in the headerCell so that each time the reloadData is called the contentOffset will be set. But this is not working because the contentOffset will be called to early and has no effect.
If I add a delay and then set the contentOffset again it is working. But this ends up in flickering which is not good.
Edit: Tried to use only one cell (used cell for header instead of specific header cell). Ended up with the same result. So this is an general issue for headers in tableView?
My code for the cells is:
/*
* This method fills all the given information for each signal to a custom cell by type SignalCell.
* Each cell has a signalName, a value at specific time and a wave image.
*/
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! SignalCell
cell.delegate = self
// Get the signal for this row
let signal = VCDHelper.sharedInstance.signals[indexPath.row] as! VCDSignal
// Set the name of the signal to the label
cell.signalLabel.text = signal.name
cell.name = signal.name
// cellContainer is set inside renderWave every time a new Cell appears
renderTimePicker(cell, signal: signal, indexPath: indexPath)
renderWave(cell, signal: signal, indexPath: indexPath)
cellContainer.setObject(cell, forKey: signal.name)
return cell
}
Code for header:
// Display header cell
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCellWithIdentifier(cellHeaderIdentifier) as! HeaderCell
// Add object to renderer dictionary
var headerRenderer = headerToRender.objectForKey("header") as? TimeRenderer
// Render waves
if let headerRenderer = headerRenderer {
cell.timeImageView.image = headerRenderer.renderedTime()
} else {
// There is no renderer yet
headerRenderer = TimeRenderer()
headerToRender.setObject(headerRenderer!, forKey: "header")
// Creates and returns an NSBlockOperation object with block
let operation = NSBlockOperation(block: { () -> Void in
let renderedHeaderImage = headerRenderer!.renderedTime()
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
cell.timeImageView.image = renderedHeaderImage
cell.cellContainer = self.cellContainer
})
})
queue.addOperation(operation)
headerToRenderOperations.setObject(operation, forKey: "header")
}
cellContainer.setObject(cell, forKey: "header")
return cell
}
And here the code for saving/setting the contentOffset for each Cell realized in SignalCell class:
/*
* This method allows only horizontal scrolling.
* It also handles that scrolling out of bounds is not allowed
*/
func scrollViewDidScroll(scrollView: UIScrollView) {
// Set contentOffsetX to minimum bound value because we are out of range
if scrollView.contentOffset.x < 0 {
scrollCells(scrollView, contentOffsetX: 0.0)
VCDHelper.sharedInstance.waveScrollContentOffsetX = 0.0
}
if scrollView.contentOffset.x > 0 {
// ContentOffsetX is the point at the beginning of the scrollView
// We know the total size of the image and need to subtract the size of the scrollView
// This will result in the max contentOffset.x for scrolling
if scrollView.contentOffset.x <= CGFloat(VCDHelper.sharedInstance.imageWidth) - scrollView.bounds.width {
scrollCells(scrollView, contentOffsetX: scrollView.contentOffset.x)
VCDHelper.sharedInstance.waveScrollContentOffsetX = Float(scrollView.contentOffset.x)
} else {
// Set contentOffsetX to maximal bound value because we are out of range
scrollCells(scrollView, contentOffsetX: CGFloat(VCDHelper.sharedInstance.imageWidth) - scrollView.bounds.width)
}
}
}
/*
* This method scrolls all visible images inside the scrollView at once
*/
func scrollCells(scrollView: UIScrollView, contentOffsetX: CGFloat) {
for (key, cell) in self.cellContainer {
if key as! String == "header" {
let headerCell = cell as! HeaderCell
let scrollContentOffsetY = headerCell.timeScrollView.contentOffset.y
// Dont use setContentOffset because this will call scrollViewDidScroll each time
headerCell.timeScrollView.contentOffset = CGPoint(x: contentOffsetX, y: scrollContentOffsetY)
} else {
let signalCell = cell as! SignalCell
let scrollContentOffsetY = signalCell.signalScrollView.contentOffset.y
// Dont use setContentOffset because this will call scrollViewDidScroll each time
signalCell.signalScrollView.contentOffset = CGPoint(x: contentOffsetX, y: scrollContentOffsetY)
}
}
}
Related
In an UITableViewCell, I have an UIView container, which contains a UIImageView on top and an UILabel on the bottom. When the UILabel does not contain text, I want to remove the UILabel and resize the UImageView to the bottom of my UIView container.
In my TableViewCell, I have this:
let x = imageViewPostPreview.bottomAnchor.constraint(equalTo: viewPreviewOverlay.topAnchor, constant: 0)
if postText == "" {
x.isActive = true
imageViewPostPreview.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMinXMaxYCorner]
self.layoutSubviews()
self.layoutIfNeeded()
} else if postText != "" {
x.isActive = false
imageViewPostPreview.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
self.layoutSubviews()
self.layoutIfNeeded()
}
}
This piece of code works on when the initial UITableViewCell is loaded, but when I scroll my UITableView, the cell loses the initial layout.
How do I setup the layout correctly, so the layout is kept on scroll?
Edit:
Initial cell, which is correct:
After scrolling, incorrect layout:
Add this to prepareToReuse method
override func prepareForReuse() {
super.prepareForReuse()
let x = imageViewPostPreview.bottomAnchor.constraint(equalTo: viewPreviewOverlay.topAnchor, constant: 0)
if postText.isEmpty {
x.isActive = true
imageViewPostPreview.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMinXMaxYCorner]
} else {
x.isActive = false
imageViewPostPreview.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
}
self.layoutIfNeeded()
}
Note: Dont call layoutSubview directly ... always try to call layoutifNeeded or setNeeedsLayout
A way you can have the cell calculate its size automatically, is to have constraints set inside the cell view for all directions on vertical axis. So, in your case the image view will have top, leading and trailing set to superview, and bottom to the label. The label will have leading and bottom to superview, and the top will be to the image view you set. It's important to set for vertical axis, since for table view we need it to calculate the height. This way the table view will know what to expect and can set height automatically.
After that you set tableView.rowHeight = UITableView.automaticDimension. Or you can also set in the delegate method.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
After all this is in place, you will only need to give the right values to the cell subviews(in this case set image and text). You can do this inside cell for row function. A sample code will look something like this.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath)
let model = self.models[indexPath.row]
cell.imageViewPostPreview.image = model.image ?? nil
cell.postText.text = model.text ?? nil
return cell
}
This should work without any further need to calculate constraints and calling layouts to update. Hope this helps! You can also check this question.
In My application I have top navigation bar and a tableview below the navigation bar. I have CollectionViewCell with two rows which added inside the UITableViewHeader programmatically. When ever I scroll the the TableView to top, i want the header to stop just below the navigation bar, and update the TableView Header height so I can show only one row. I just want to do an animation (like Shrinked)when the TableViewHeader sticks to the navigationbar the two collectionview rows should turn into one row by decreasing the Header Height. How can I do it programmatically
Below is my code for showing CustomHeaderView
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView.init(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 183))
let headerCell = tableView.dequeueReusableCell(withIdentifier: kLastPlayedidentifier) as! LastPlayedTVC
headerCell.frame = headerView.frame
headerCell.category = lastPlayedData
headerView.addSubview(headerCell)
return headerView
}
Also i'm checking for the scroll position to set the tableview header height progmmatically which isn't successful for me.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
if scrollView.contentOffset.y > 237 //This value is to check when the header reached the top position {
//Condition to check and animate the headerview height to make collectionview cell two rows into one rows.
}
How can I achieve the TableViewHeader height update when header sticks on top while scrolling.
Any help is appreciated.
What you are looking for is "sticky header"
and you want to change the header as well.
Sticky part is built in automatically I think if you just use UITableViewController(style: .plain), if that doesn't work for you, you can just google sticky header and there are lots of answers.
the part about changing the height or animating it. you are doing it right, just do something like:
// update your viewForHeader method to account for headerRows variable above
// update your viewForHeader method to account for headerRows variable above
// default 2, you modify this in your scroll
var headerRows = 2
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let height = headerRows == 2 ? 183 : 91
let headerView = UIView.init(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: height))
let headerCell = tableView.dequeueReusableCell(withIdentifier: kLastPlayedidentifier) as! LastPlayedTVC
headerCell.frame = headerView.frame
headerCell.category = lastPlayedData
headerView.addSubview(headerCell)
return headerView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
if scrollView.contentOffset.y > 237 {
updatedHeader.frame.size.height = 40
self.tableviewObj.tableHeaderView = updatedHeader
headerRows = 1 } else {
headerRows = 2
}
self.tableView.reloadSectionHeaders()
}
If you want to do some animating instead, what you would do is store reference to your headerView in a variable of your view controller and inside your scrollViewDidScroll animate it using UIView.animate{...}
hope this helps man.
I am trying to figure out why my custom styling for table cells disappears after scrolling down in a table view and them back up. What do I need to do to have the style persist?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : CustomTrendingCell = trendingTableView.dequeueReusableCell(withIdentifier: "cell") as! CustomTrendingCell
cell.selectionStyle = .none
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(searchForTrendingTerm(sender:)))
cell.iconView_search.isUserInteractionEnabled = true
cell.iconView_search.tag = indexPath.row
cell.iconView_search.addGestureRecognizer(tapGesture)
cell.trendingLabel.text = trendingSearchTerms[indexPath.row]
cell.elevate(elevation: 4.0) //where my style is being set
return cell
}
extension UIView {
func elevate(elevation: Double) {
self.layer.masksToBounds = false
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: elevation)
self.layer.shadowRadius = abs(CGFloat(elevation))
self.layer.shadowOpacity = 0.4
}
}
The top 2 items in the screenshot below have been scrolled down and up. The drop shadow styling has been removed. The bottom 2 have the correct styling and have not been scrolled down.
Screenshot example
One possible solution here is to explicitly specify the zPosition of each cell's layer.
You would want to ensure that the upper cell has the higher position so that it's content (shadow) lies over the lower cell.
In your cell for row at function add:
cell.layer.zPosition = CGFloat(numberOfRows - indexPath.row)
I'm working on a simple flashcard app. The cards are loaded into a collection view. When someone clicks on the card I want a rotation animation to occur and the text label to change to the answer.
func animateRotationY(view: UIImageView) {
let animation = CABasicAnimation(keyPath: "transform.rotation.y")
let angleRadian = CGFloat.pi * 2
animation.fromValue = 0
animation.byValue = angleRadian
animation.toValue = angleRadian
animation.duration = 1.0
animation.repeatCount = 1
view.layer.add(animation, forKey: nil)
}
Im calling this animation function in the didSelect method as such
extension FlashCardsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = flashCardsView.collectionView.dequeueReusableCell(withReuseIdentifier: "FlashCardCell", for: indexPath) as! FlashCardCollectionViewCell
let row = indexPath.row
let selectedCard = flashCards[row]
animateRotationY(view: cell.imageView)
selectedIndexPath = row
cell.textLabel.text = selectedCard.answer
self.flashCardsView.collectionView.reloadData()
}
Problem is, not only will the animation not show, but the textLabel wont even update even though the breakpoints are hitting those lines. Does anyone have any idea how I can update UI in the didSelect?
When you call dequeueReusableCell(withReuseIdentifier:for:), you're not getting the cell that is already shown and you want to change.
You should call flashCardsView.collectionView.cellForItem(at: indexPath) instead.
Also, calling reloadData() is causing collectionView(_:cellForItemAt:) to be called for every index path, which might be overriding whatever changes you made after selecting the cell.
I have a custom inline date picker that expands and collapses just like the Apple Calendar events date pickers. The problem is that I need the Date Picker to in a UITableViewCell that is at the bottom of the UITableView, but when it expands, you cannot see the Picker without scrolling the view down manually. For a similar issue (UITextField disappears behind keyboard when it's being edited if it's below the height of the keyboard) I was able to fix it with an animation in the beginEditing and endEditing delegate functions:
let keyboardHeight: CGFloat = 216
UIView.animateWithDuration(0.25, delay: 0.25, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
self.view.frame = CGRectMake(0, (self.view.frame.origin.y - keyboardHeight), self.view.bounds.width, self.view.bounds.height)
}, completion: nil)
Does anyone know of a similar animation that would work for maybe placing in didSelectRowAtIndexPath, and if it's a date picker the screen will adjust by scrolling down to show the full date picker expanded, and then it will adjust back when the row is not selected anymore - or something along these lines?
I'm not sure if the fact that there's already an animation occurring to expand/collapse the date picker would conflict with this or if it's just a matter of precedence for the animations to occur. Anyways I will post some code below:
When the DatePicker is selected in the tableView:
/**
Used to notify the DatePickerCell that it was selected. The DatePickerCell will then run its selection animation and expand or collapse.
*/
public func selectedInTableView() {
expanded = !expanded
UIView.transitionWithView(rightLabel, duration: 0.25, options:UIViewAnimationOptions.TransitionCrossDissolve, animations: { () -> Void in
self.rightLabel.textColor = self.expanded ? self.tintColor : self.rightLabelTextColor
}, completion: nil)
}
didSelectRowAtIndexPath function:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Deselect automatically if the cell is a DatePickerCell.
let cell = self.tableView(tableView, cellForRowAtIndexPath: indexPath)
if (cell.isKindOfClass(DatePickerCell)) {
let datePickerTableViewCell = cell as! DatePickerCell
datePickerTableViewCell.selectedInTableView()
tableView.deselectRowAtIndexPath(indexPath, animated: true)
} else if(cell.isKindOfClass(PickerCell)) {
let pickerTableViewCell = cell as! PickerCell
pickerTableViewCell.selectedInTableView()
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
tableView.beginUpdates()
tableView.endUpdates()
}
I think I came up with a solution that seems to work as needed, I wrote this code in the didSelectRowAtIndexPath delegate function after the call to selectedInTablView():
if(datePickerTableViewCell.isExpanded() && datePickerTableViewCell.datePicker == billDatePicker.datePicker) {
tableView.contentOffset = CGPoint(x: 0, y: self.view.frame.height + datePickerTableViewCell.datePickerHeight())
}