App crashes when I long press on UITableView Custom Header Cell - ios

I have a simple UITableViewController with a set of data, employee first name and last name. I have created a custom header prototype cell with just one Label to set the header title.
However the issue is that, when ever user "Long Press" on the header cell on the table, the App Crashes.
In the attached screen shot, Header0, Header1, Header3, Header4 when "Long Press"ed, the app crashes.
However, the header section marked in the red oval is a simple UIView for header but without any Label or any control. The strange thing is if the user "Long Press" this empty header, the app won't crash.
The code for header view
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if ( section == 2 ){
var emptyView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 50))
return emptyView
}
let cell = tableView.dequeueReusableCellWithIdentifier("HeaderCell") as! UITableViewCell
let c = cell as! HeaderTableViewCell
c.contentView.backgroundColor = UIColor.darkGrayColor()
c.headerTextLabel.text = "Header" + toString(section)
return c.contentView
}
Wondering what is happening. I have recently updated Xcode to 6.3 which has Swift 1.2. How to fix this issue?
Any help is much appreciated
Within the

I got it fixed by removing gestures on the content view (c.contentView) of cell.
if let recognizers = c.contentView.gestureRecognizers
{
for recognizer in recognizers {
c.contentView.removeGestureRecognizer(recognizer as! UIGestureRecognizer)
}
}

You can also try the solution from here: https://stackoverflow.com/a/31877323/67667 i.e. disable user interactions for the header cell:
c.contentView.userInteractionEnabled = false

Related

Swift TableViewCells not showing up

I got a ViewController, with two UITableView in it.
They both get data from the same object, but from different arrays inside that object.
My second Tableview doesn't show me all the items, only 5.
The TableViews are both dynamic and size themself.
Here is my cellForRowAt function:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == tableViewSteps, //This is the table that doesn't show right
let cell = tableView.dequeueReusableCell(withIdentifier: "stepsCell", for: indexPath) as? StepsTableViewCell {
let text = detailItem?.itemStepAr[indexPath.row] as! String
cell.textAreaOutlet.text = "Schritt \(indexPath.row + 1) \n\(text)"
let textAsNSString = cell.textAreaOutlet.text as NSString
let lineBreakRange = textAsNSString.range(of: "\n")
let newAttributedText = NSMutableAttributedString(attributedString: cell.textAreaOutlet.attributedText)
let boldRange: NSRange
if lineBreakRange.location < textAsNSString.length {
boldRange = NSRange(location: 0, length: lineBreakRange.location)
} else {
boldRange = NSRange(location: 0, length: textAsNSString.length)
}
newAttributedText.addAttribute(NSAttributedString.Key.font, value: UIFont.preferredFont(forTextStyle: UIFont.TextStyle.headline), range: boldRange)
cell.textAreaOutlet.attributedText = newAttributedText
let fixedWidth = cell.textAreaOutlet.frame.size.width
let newSize = cell.textAreaOutlet.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
cell.textAreaOutlet.frame.size = CGSize(width: max(newSize.width, fixedWidth), height: newSize.height)
cell.textAreaOutlet.isScrollEnabled = false
tableStepsHeight = tableStepsHeight + Int(newSize.height)
cell.textAreaOutlet.textColor = UIColor.lightGray
tableViewSteps.frame = CGRect(x:0, y: currentViewHeight + 20, width: 375, height: tableStepsHeight)
changeScrollHeight()
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "ingredsCell")! //1.
let text = detailItem?.itemIngredAr[indexPath.row]
cell.textLabel?.text = text as? String
cell.textLabel?.font = UIFont(name: "Futura-Medium", size: 17)
cell.textLabel?.textColor = UIColor.lightGray
return cell
}
}
as commented, the first tableview is the one, that doesn't work properly.
It only runs 5 times, when I watch it with the debugger, but there are more Items in the Array.
Edit:
Here is my numberOfRowsInSection function
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (tableView == tableViewSteps) {
return detailItem?.itemStepAr.count ?? 0
}
else {
return detailItem?.itemIngredAr.count ?? 0
}
}
Edit 2:
When I enable scrolling in my TableView and then try to scroll, all the items appear. But the scrolling should be disabled, because it automatic resizes (which works)
Final edit:
I added like some more height to the tableViewand enabled scroll (but disabled the bounce, so u don't see anything from the scroll)
Now it loads properly and everything works fine
A UITableView works with recycling UITableViewCells. This means that a tableview will only load the visible cells first, and only when you scroll, will it load more cells (by recycling the old cells).
The issue I can see is that you may try to get the height of the tableview, to update a height constraint (so the tableview shows in its entirety). But that may be an incorrect calculation (as said before, it only instantiates the first x cells, so it can't know the height of the remaining cells). Also, this calculation should never be performed from cellForRowAt, as that method should never do things outside the cell (unless you want to know when a cell has become visible).
You can fix this by making your cells all have a predefined height, and then multiplying that by the number of cells.
If you have self-sizing cells, it can't be done (read: it can be done, but it's too unwieldy and inefficient to be practical).
You could try to look into taking all other views that should scroll above and below the tableview, into tableviewcells. Then you can just use a regular tableview, no fancy height constraints and calculations needed.

UITableView called many times when the cell is out of the screen

I have a problem when i use the function tableView. I show you my code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cardCell", for: indexPath) as! OpsCardCell
cell.workTest(drawCard: .selectOpponent)
return cell
}
In the exemple I have 4 cell and when I scroll in the simulator the cell who are out of the screen and come back, the cell is again called. And since I draw the card dynamically, the card was drawn several times and the shadow I adds too many times. I show you the screen before and after:
after few scroll down and scroll up:
this is because the function tableView called many times the cell [0] and [3]
This is my code to draw the card:
func drawBasiqCard(){
let cardView = UIView()
self.addSubview(cardView)
cardView.frame = CGRect(marginCardWidth,
marginCardHeight,
self.bounds.size.width - (marginCardWidth*2),
self.bounds.size.height - (marginCardHeight*2))
cardView.layer.cornerRadius = 10
let rounding = CGFloat.init(10)
let shadowLayer = CAShapeLayer.init()
shadowLayer.path = UIBezierPath.init(roundedRect: cardView.bounds, cornerRadius: rounding).cgPath
shadowLayer.fillColor = UIColor(rgb: 0xaaccbb).cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowRadius = 5
shadowLayer.shadowOpacity = 0.2
shadowLayer.shadowOffset = CGSize.init(width: 0, height: 0)
cardView.layer.insertSublayer(shadowLayer, at: 0)
}
So my question is, what is wrong with my code? And there is another way to solve my problem ?
thanks to your reply !
You should try to add sublayer at once in your awakefromnib method. Tableviewcell reuse the same cell using cell identifier that why multiple shadows added to your cell.
Your problem is cell recycling. When you scroll, the same cell gets re-used to display data at a different indexPath.
You should create a custom subclass of UITableViewCell. Give it an optional property shadowLayer. In your cellForRow(at:) method, dequeue a cell and cast it to the correct custom class. Then check to see if its shadowLayer property is nil. If it is nil, add a shadow layer. If it's not nil, create a shadow layer and install it in the cell (cell.shadowLayer = shadowLayer).
In you code you have taken new card each time that card drawing method called, so this will work fine for the first time, but after that this will create problem because you haven't removed that view or you have not checked is that card is already added in cell or not.
So you can either remove below line from you code and take one parent view to design your card inside your storyboard or xib, if you are going to design your cell using xib or storyboard.
let cardView = UIView()
Or if you are going to design your cell programatically then first check if that cardview is added in cell or not, and then cardview is not added then add new one else skip that code.
So I add to the top of my class :
private var ShadowLayerCard: CAShapeLayer?
and I compare if the ShadowLayerCard is not nil :
if(self.ShadowLayerCard == nil){
let shadowLayer = CAShapeLayer.init()
self.ShadowLayerCard = shadowLayer
// here I have my code to add the shadowLayer and other parameters of the shadow...
}
this solve my problem, thanks to duncan-c

UI CollectionView in UICollectionView Cell Programmatically

Hi I am trying to make a home feed like facebook using UICollectionView But in each cell i want to put another collectionView that have 3 cells.
you can clone the project here
I have two bugs the first is when i scroll on the inner collection View the bounce do not bring back the cell to center. when i created the collection view i enabled the paging and set the minimumLineSpacing to 0
i could not understand why this is happening. when i tried to debug I noticed that this bug stops when i remove this line
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
but removing that line brings me this error
The behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values
because my cell have a dynamic Height
here is an example
my second problem is the text on each inner cell dosent display the good text i have to scroll until the last cell of the inner collection view to see the good text displayed here is an example
You first issue will be solved by setting the minimumInteritemSpacing for the innerCollectionView in the OuterCell. So the definition for innerCollectionView becomes this:
let innerCollectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
let cv = UICollectionView(frame :.zero , collectionViewLayout: layout)
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .orange
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
cv.isPagingEnabled = true
cv.showsHorizontalScrollIndicator = false
return cv
}()
The second issue is solved by adding calls to reloadData and layoutIfNeeded in the didSet of the post property of OuterCell like this:
var post: Post? {
didSet {
if let numLikes = post?.numLikes {
likesLabel.text = "\(numLikes) Likes"
}
if let numComments = post?.numComments {
commentsLabel.text = "\(numComments) Comments"
}
innerCollectionView.reloadData()
self.layoutIfNeeded()
}
}
What you are seeing is related to cell reuse. You can see this in effect if you scroll to the yellow bordered text on the first item and then scroll down. You will see others are also on the yellow bordered text (although at least with the correct text now).
EDIT
As a bonus here is one method to remember the state of the cells.
First you need to track when the position changes so in OuterCell.swft add a new protocol like this:
protocol OuterCellProtocol: class {
func changed(toPosition position: Int, cell: OutterCell)
}
then add an instance variable for a delegate of that protocol to the OuterCell class like this:
public weak var delegate: OuterCellProtocol?
then finally you need to add the following method which is called when the scrolling finishes, calculates the new position and calls the delegate method to let it know. Like this:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let index = self.innerCollectionView.indexPathForItem(at: CGPoint(x: self.innerCollectionView.contentOffset.x + 1, y: self.innerCollectionView.contentOffset.y + 1)) {
self.delegate?.changed(toPosition: index.row, cell: self)
}
}
So that's each cell detecting when the collection view cell changes and informing a delegate. Let's see how to use that information.
The OutterCellCollectionViewController is going to need to keep track the position for each cell in it's collection view and update them when they become visible.
So first make the OutterCellCollectionViewController conform to the OuterCellProtocol so it is informed when one of its
class OutterCellCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, OuterCellProtocol {
then add a class instance variable to record the cell positions to OuterCellCollectionViewController like this:
var positionForCell: [Int: Int] = [:]
then add the required OuterCellProtocol method to record the cell position changes like this:
func changed(toPosition position: Int, cell: OutterCell) {
if let index = self.collectionView?.indexPath(for: cell) {
self.positionForCell[index.row] = position
}
}
and finally update the cellForItemAt method to set the delegate for a cell and to use the new cell positions like this:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OutterCardCell", for: indexPath) as! OutterCell
cell.post = posts[indexPath.row]
cell.delegate = self
let cellPosition = self.positionForCell[indexPath.row] ?? 0
cell.innerCollectionView.scrollToItem(at: IndexPath(row: cellPosition, section: 0), at: .left, animated: false)
print (cellPosition)
return cell
}
If you managed to get that all setup correctly it should track the positions when you scroll up and down the list.

Make UIButton stick on bottom of UITableView

I have an UITableView which consists of prototype cells. I want to put an UIButton inside the bottom of the UITableView using Interface Builder.
I added the UIButton in the footer of the UITableView:
I added a purple background for the Footer View and a green background colour for the UITableView. In the picture above it shows the Button at the bottom of the footer. However this isn't equal to the bottom of the UITableView.
The GIF below displays that the button is placed bellow the cells but not inside the bottom of the UITableView. I want it to appear at the bottom in the UITableView. Not under the UITableView. The following GIF displays this problem:
My question is: How do I set an UIButton inside an UITableView at the bottom of the UITableView using Interface Builder?
This is what I want to achieve (From Apple's ResearchKit):
Edit: The UIButton should be inside the UITableView. Suggestions where the UIButton is placed outside the TableView and pinned underneath don't achieve my goal.
You are setting footer width wrong.Set it fixed height so that button sticks to that particular height(Should be Fixed like 60px)
Check Demo Code for Storyboard structure and constraints
So I had to slightly swizzle it, but got it working by doing the below things:
Pull the UIButton out to the same level in the view heirarcy as
the tableview.
Embed the tableview and the button inside a view
Embed the above view inside another view
Pin edges of view #3 (Pinned View) to superview
Pin top, left & right edges of view #2 (Resizing View) to view #3 edges. And set a constraint of equal height to view #3.
Set an outlet in the view controller for the equal height constraint
The view heirarcy in IB should look like this:
Now in the view controller code, you need to do the following things:
Create instance var for the keyboard offset value
var keyboardOffset: CGFloat = 0
set notifications and observers for the keyboard willShow and
willHide
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(_:)), name:NSNotification.Name.UIKeyboardWillHide, object: nil)
In keyboardWillShow, cache the keyboard height value.
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
keyboardOffset = keyboardSize.height
}
Create didSet method on the keyboardOffset var, and animate the height of the view by that value each time it is set
var keyboardOffset: CGFloat = 0 {
didSet {
resizingViewHeight.constant = -keyboardOffset
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
}
}
Make sure you set the offset back to 0 in keyboardWillHide
keyboardOffset = 0
Every time the keyboard now appears, the view that is containing the tableview will reduce in size and therefore pull the contents up with it, providing the shrinking tableview effect that you are hoepfully looking for!
Add a view that contains the UIButton to the bottom of the UIViewController where the UITableView is. Give it the constraints to attach to left, right and bottom side of super view and probably a fixed height.
Then attach the UITableView's bottom constraint to the top of the view that contains the UIButton.
You should get the effect you're looking for.
NOTE: For the button you can give centered Y and X in superview constraints to keep it centered.
Footer is apperead always after the last cell of your table view so your output is correct.
If you wanted the button bottom of tableview then add button below the tableview in hierarchy not as a footer. But it makes your button static that means it didn't matter how much cells you have, button is always button of the tableView but it is not a scrollable like as it is now.
I tried the accepted answer, but couldn't get it to work. I found that the footer view always stayed pinned to the bottom of the screen, regardless of the size of the TableView (just as if it were a sibling of the TableView). I ended up following an approach suggested here: https://stackoverflow.com/a/18047772/5778751 The basic idea is that you programmatically determine the height of the TableView and depending on the result, you EITHER display a footer internal to the TableView OR display a view which is a sibling of the TableView.
I have a perfect solution for this problem. Using default was never that meaningful in my life.
The button under the view is also a table view cell from another section but its configuration of header height and interior design is just different from the above cells.
So I have five different sections. The first three of them are standard table view cells(SettingTableViewCell) but the last two(cache and version) are custom buttons. In the header title, I init for those empty titles.
enum Section: Int {
case adjustSettings
case about
case agreements
case cache
case version
static var numberOfSections: Int { return 5 }
var reuseIdentifier: String { return "SettingTableCell" }
var headerTitle: String? {
switch self {
case .adjustSettings: return "settings.adjust.section.title".localized
case .about: return "settings.headertitle.about".localized
case .agreements: return "agreement.title".localized
case .cache: return ""
case .version: return ""
}
}
Then I configured with cell will be in which section with below code. Cache and version have only one cell which will be our buttons.
var cells: [CellType] {
switch self {
case .adjustSettings:return [.notification,.language ]
case .about: return [.rate, .contact, .invite]
case .agreements: return [.membership, .kvkk, .illuminate]
case .cache: return [.cache]
case .version: return [.version]
}
}
I have three different set functions inside my settingsTableViewCell.
For setting up standard table view cell -> .setDefault(text: text)
For setting up my clean cache button -> .setCache(text: text)
Last for shoving version info -> .setVersion(version: version)
with the above cellForRowAt, I am switching rows and setting them up accordingly. My default is .setDefault
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else {
assertionFailure()
return UITableViewCell()
}
let row = section.cells[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: section.reuseIdentifier) as! SettingTableCell
switch row {
case .version:
cell.setVersion(version: getVersion())
case .cache:
ImageCache.default.calculateDiskCacheSize(completion: { size in
if size == 0 {
cell.setCache(text: "settings.clear.data".localized)
} else {
let byte = Int64(size)
let fileSizeWithUnit = ByteCountFormatter.string(fromByteCount: byte, countStyle: .file)
cell.setCache(text: "settings.cler.data.with.string".localized + "(\(String(describing: fileSizeWithUnit)))")
}
})
default:
cell.setDefault(text: row.text)
}
return cell
}
You can adjust button heights as below by switching section.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let section = Section(rawValue: indexPath.section) else { return 0 }
switch section {
case .cache: return 44
case .version: return 44
default: return 56.0
}
You can adjust the gap between each button as below.
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard let section = Section(rawValue: section) else { return 0 }
switch section {
case .adjustSettings: return 46
case .about: return 46
case .agreements: return 46
case .cache: return 9
case .version: return 0.5
default: return 46
}
And finally, this is my cell where I set .set functions to customize each cell as I pleased.
class SettingTableCell: UITableViewCell {
#IBOutlet weak var line: UIView!
#IBOutlet weak var content: UIView!
#IBOutlet weak var arrowView: UIView!
#IBOutlet weak var labelSetting: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
func setVersion(version: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = .clear
labelSetting.label(textStr: version, textColor: KSColor.neutral400.getColor(), textFont: .sfProTextRegular(size: 13), fontSize: 13, lineSpacing: -0.13, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setCache(text: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = KSColor.neutral100.getColor()
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: .sfProTextMedium(size: 14), fontSize: 14, lineSpacing: -0.14, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setDefault(text: String) {
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: UIFont.sfProTextMedium(size: 16), fontSize: 16, lineSpacing: -0.16, paragraphStyle: NSMutableParagraphStyle())
}
}
And the outcome is I have 5 sections but the last two are buttons.

Separator line unexpectedly appears when swiping UITableViewRowActions away

In my app I'm using a UITableView which consists of multiple headers and one or two rows for each header. Each cell features two UITableViewRowActions. My problem is, that whenever these row actions are animated out, a strange separator appears between the cell and the header underneath.
I've tried turning off the default separators, and using a custom separator instead, but for some reason I actually need to use the default separators.
In the next step I tried to disable the separator only for those cells which are on top of the next header, like this, but it didn't work:
if #available(iOS 9.0, *) {
tableView.cellLayoutMarginsFollowReadableWidth = false
}
cell.layoutMargins = UIEdgeInsetsZero
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset.left = self.view.frame.size.width
To help you gain an impression of the issue, I've created three screenshots that show the strange behaviour:
Before:
While displaying row actions:
After dismissing row actions using tableView.setEditing(false, animated: true):
Do you have any idea how I can remove this strange separator line?
I finally found a relatively clean solution to fix this issue - I just add a "line fix" view to every header view, that will overlay the strange separator. Since it seems to be an Apple bug, that's probably the best workaround.
Here's the code:
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = UITableViewHeaderFooterView()
let lineFix = UIView(frame: CGRect(x: 0, y: -0.5, width: tableView.frame.size.height, height: 0.5))
lineFix.backgroundColor = UIColor.groupTableViewBackgroundColor()
view.addSubview(lineFix)
return view
}

Resources