I am trying to do something like this:
When the user clicks "Add Page," a new grouping shows up below it. Now, I decided to use Table View cells in order to achieve this. After following various tutorials and looking up similar Q&As, I am able to add cells on button click with UILabel and have the cell height be dynamic depending on the content but now I am trying to figure out how to add ImageViews and place buttons within a cell.
I've created a custom cell class:
class PageCell : UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = UITableViewCellSelectionStyle.none
setupViews()
}
...
... // other random code here
let imgView : UIImageView = {
let imgview = UIImageView()
imgview.frame = CGRect(x: 100, y: 150, width: 150, height: 140)
imgview.tintColor = UIColor(red: 0.73, green: 0.2, blue: 0.3, alpha: 1.0)
imgview.translatesAutoresizingMaskIntoConstraints = false
return imgview
}()
func setupViews() {
addSubview(pageLabel) // the label that I got working
addSubview(imgView) // can't get this working
...
// constraint info here
}
}
And back in my TableViewController:
class TakePhotosVC: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(PageCell.self, forCellReuseIdentifier: "cellID")
}
// return the actual view for the cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let pagecell = tableView.dequeueReusableCell(withIdentifier: "cellID", for: indexPath) as! PageCell
// set more stuff here
}
... // more code
}
My issue is that I am trying to get a box showing where the ImageView is that the user can click on to load in a picture. I am unsure how to do that and place all the relevant buttons as well (Trash, X, etc.)
Any help would be greatly appreciated.
EDIT
Okay, I was trying to follow this tutorial and I can't quite get it to work. In my prototype cell, I see this:
But the result is this:
I made the UIImageView have a background so I can see it. I have two constraints for the UIImageView which are: width = 240, height = 128 and two constraints for the Page Label which are: width = 240, height = 21. Two questions: why are my elements not placed correctly even though I have it correctly placed in the Storyboard? And why is the cell height not dynamically resizing to accommodate the elements?
I have these two lines in my viewDidLoad method of the TakePhotosVC but it doesn't seem to do anything.
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 200
If it's relevant, I get this warning when I run the Simulator.
Warning once only: Detected a case where constraints ambiguously
suggest a height of zero for a tableview cell's content view. We're
considering the collapse unintentional and using standard height
instead.
EDIT 2
I got it to work. For any poor souls reading this after me, you have to click on those dotted pink lines in the Constraints window editor and then click "Add X Constraints" in order to get the ImageView to center and stuff.
My issue is that I am trying to get a box showing where the ImageView
is that the user can click on to load in a picture. I am unsure how to
do that and place all the relevant buttons as well (Trash, X, etc.)
I am not sure if I understand you correctly. You have a cell with an UIImageView. But you want to show a box to visually indicate where the user should touch to add an UIImage (?)
Why not simply add a UIButton on top of the UIImageView with the exact same frame size, and on touch, you fire your action to add the image, and once the image is successfully added, you can set the UIButton to hidden.
If the user deletes the image with the trash button, you simply show the UIButton again by hidden = NO.
Other solution:
Add a border to the UIImageView with custom colors and add a UITapGestureRecognizer to fire an action. (Make sure you set the UIImageView to userInteractionEnabled = YES;
You can allso add a Placeholder image to the UIImageView when there is no image set, with your custom design.
The easiest approach would be to use a xib instead of placing the buttons programmatically. To do this, add a new file and select xib. In a xib, you can pre-make a tableviewcell with the image view, and you buttons placed for you already with constraints. Then, you can subclass this table view cell and connect the image view and buttons with ib outlets and ib actions to access the buttons and image view. Then, in your cellForRow function, load the xib like this:
Bundle.main.loadNibNamed("NameOfNib", owner: self, options: nil).first as! NameOfSubclass
I would advise to read more on xibs and nibs.
I have a UITableView with dynamically populated rows, but also there's a section at the top that contains one special cell (with a different identifier) which is always the same.
I've added two buttons to this cell and they do work, however they react poorly. That is, the highlighting occurs only after about 0.25s.
I'm using the following slightly customized button:
import UIKit
class HighlightingButton: UIButton {
override var isHighlighted: Bool {
didSet {
if isHighlighted {
backgroundColor = UIColor.lightGray
} else {
backgroundColor = UIColor.white
}
}
}
}
It's important that the user gets a clear feedback that they tapped the button. However with the slow highlighting this isn't satisfying, although the events seem to be triggered quickly (juding by printing some output).
In a normal view this HighlightingButton works as expected and the highlighting flashes as quickly as I can tap.
Is there something in the event handling of the UITableViewCell that leads to this slowness?
Update
I created a minimalistic example project that demonstrates the problem.
There aren't any GestureRecognizers and still there's this very noticable delay.
Take a look at delaysContentTouches property of UIScrollView.
I fixed your problem by setting it to false on tableView and all of it's scrollview subviews.
So you should just add a tableView IBOutlet and override viewDidLoad method like this:
override func viewDidLoad() {
super.viewDidLoad()
tableView.delaysContentTouches = false
for case let subview as UIScrollView in tableView.subviews {
subview.delaysContentTouches = false
}
}
I have a UIButton at the bottom layer of the UIViewController.
And I have a UITableView (full screen size) on top of the UIButton, the UITableView has a header (UIView) which has a transparent background which could be able to see through and show the UIButton.
The UIButton is not clickable even when the button appear behind the tableview header.
My tableView's cell and the header of the tableView has buttons on it, so I could not set headerView.userInteraction = true or tableView.userInteraction = true
I have tried to use pointInside:withEvent: and hitTest:withEvent:, UIButton is still not clickable in both cases.
Any suggestions? Thanks
You could use UITapGestureRecognizer for example like this:
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
...
let headerViewGesture = UITapGestureRecognizer(target:self, action:#selector(MyClass.headerViewTap(_:)))
headerView.addGestureRecognizer(mainViewGesture)
...
}
func headerViewTap(recognizer: UITapGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.Recognized
{
let touchLocation = recognizer.locationInView(recognizer.view)
print(touchLocation)
if CGRectContainsPoint(myButton.frame, touchLocation) {
print("button was touched")
}
}
}
Please try bringSubviewToFront to get the button clickable.
self.view.bringSubviewToFront(yourButtton)
Or you can check it in storyboard whether the button is inside some view, And add make sure the button is placed properly in storyboard. The button should be below the table view like in the sample picture.
You can try both the ways and let me know if you have any doubt.
I am in the process of updating my app to iOS10 with Swift 2.3 and Xcode 8 Beta 1 and I have found that there is a UITableViewHeaderFooterContentView which is blocking touches to the UIButton on my subclass of UITableViewHeaderFooterView.
On the Xcode 8 Beta 1 simulator the UIButton works on iOS9.3 but not iOS10.
1) Is there any documentation for this?
2) How can I ensure my UI elements are on top of the new Content View in iOS10? (or allow touches through the UITableHeaderFooterContentView)
Thanks!
Table Header
import UIKit
class TableHeader: UITableViewHeaderFooterView {
#IBOutlet weak var dayLabel: UILabel!
#IBOutlet weak var dateLabel: UILabel!
#IBOutlet weak var addNewEventButton: UIButton!
}
Code In View Controller
dateCell.addNewEventButton is the UIButton that is no longer receiving touches in iOS10
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let tintColor = TintManager().getTintColour()
let dateCell:TableHeader = tableView.dequeueReusableHeaderFooterViewWithIdentifier("TableHeader") as! TableHeader
//dateCell.bringSubviewToFront(dateCell.addNewEventButton)
dateCell.dayLabel.text = Dates.day.uppercaseString
dateCell.dateLabel.text = Dates.date
dateCell.backgroundView = UIView(frame: dateCell.frame)
dateCell.backgroundView!.backgroundColor = tintColor
dateCell.dayLabel.textColor = UIColor.whiteColor()
dateCell.dateLabel.textColor = UIColor.whiteColor()
dateCell.addNewEventButton.backgroundColor = tintColor
dateCell.addNewEventButton.tag = section
dateCell.addNewEventButton.layer.cornerRadius = 20.0
if (savedEventView.superview === self.view) {
dateCell.addNewEventButton.removeTarget(nil, action: nil, forControlEvents: .AllEvents)
dateCell.addNewEventButton.addTarget(self, action: #selector(ViewController.userPressedAddButtonToInsertSavedEvent(_:)), forControlEvents:.TouchUpInside)
} else {
dateCell.addNewEventButton.removeTarget(nil, action: nil, forControlEvents: .AllEvents)
dateCell.addNewEventButton.addTarget(self, action: #selector(ViewController.userPressedAddNewEventOnTableViewHeader(_:)), forControlEvents:.TouchUpInside)
}
return dateCell
}
The offending view is in fact the contentView of the UITableViewHeaderFooterView (see the Apple Docs). So you should be able just to use sendSubview(toBack:) in order to stop it interfering with touches.
However, it seems that under iOS9 the UITableViewHeaderFooterView fails to correctly initialise the contentView if the view is loaded from a NIB. Although the contentView property is not optional, it is in fact nil, and you get a BAD ACCESS error if you try to access it. Nor can you set a value for contentView (either in code or as an outlet in IB) because it's a read only property (*). So the only solution I can think of is to use #available to conditionally include code to move the contentView to the back, if you are running on iOS 10 or newer. I would put the relevant code into your subclass:
override func awakeFromNib() {
if #available(iOS 10, *) {
self.sendSubview(toBack: contentView)
}
}
(*) Indulging in wild speculation, my guess is that Apple based the UITableViewHeaderFooterView code heavily on UITableViewCell. Since IB has UITableViewCells in its object library (and notice these include the cell's contentView), it can ensure that the cell's contentView is correctly instantiated. But since there is no UITableViewHeaderFooterView in the object library, there's no way to get the contentView loaded correctly. Looks like they fixed it in iOS10 by instantiating an empty contentView. Pity they didn't also add UITableViewHeaderFooterView to the library.
I can create simple custom viewForHeaderInSection in programmatically like below. But I want to do much more complex things maybe connection with a different class and reach their properties like a tableView cell. Simply, I want to see what I do.
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if(section == 0) {
let view = UIView() // The width will be the same as the cell, and the height should be set in tableView:heightForRowAtIndexPath:
let label = UILabel()
let button = UIButton(type: UIButtonType.System)
label.text="My Details"
button.setTitle("Test Title", forState: .Normal)
// button.addTarget(self, action: Selector("visibleRow:"), forControlEvents:.TouchUpInside)
view.addSubview(label)
view.addSubview(button)
label.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
let views = ["label": label, "button": button, "view": view]
let horizontallayoutContraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-10-[label]-60-[button]-10-|", options: .AlignAllCenterY, metrics: nil, views: views)
view.addConstraints(horizontallayoutContraints)
let verticalLayoutContraint = NSLayoutConstraint(item: label, attribute: .CenterY, relatedBy: .Equal, toItem: view, attribute: .CenterY, multiplier: 1, constant: 0)
view.addConstraint(verticalLayoutContraint)
return view
}
return nil
}
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
Is there anyone to explain how can I create a custom tableView header view using xib? I have encountered with old Obj-C topics but I'm new with Swift language. If someone explain as detailed, It would be great.
1.issue: Button #IBAction doesn't connect with my ViewController. (Fixed)
Solved with File's Owner, ViewController base class (clicked left outline menu.)
2.issue: Header height problem (Fixed)
Solved adding headerView.clipsToBounds = true in viewForHeaderInSection: method.
For constraint warnings this answer solved my problems:
When I added ImageView even same height constraint with this method in viewController, it flow over tableView rows look like picture.
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 120
}
If I use, automaticallyAdjustsScrollViewInsets in viewDidLoad, In this case image flows under navigationBar. -fixed-
self.automaticallyAdjustsScrollViewInsets = false
3.issue: If button under View (Fixed)
#IBAction func didTapButton(sender: AnyObject) {
print("tapped")
if let upView = sender.superview {
if let headerView = upView?.superview as? CustomHeader {
print("in section \(headerView.sectionNumber)")
}
}
}
The typical process for NIB based headers would be:
Create UITableViewHeaderFooterView subclass with, at the least, an outlet for your label. You might want to also give it some identifier by which you can reverse engineer to which section this header corresponds. Likewise, you may want to specify a protocol by which the header can inform the view controller of events (like the tapping of the button). Thus, in Swift 3 and later:
// if you want your header to be able to inform view controller of key events, create protocol
protocol CustomHeaderDelegate: class {
func customHeader(_ customHeader: CustomHeader, didTapButtonInSection section: Int)
}
// define CustomHeader class with necessary `delegate`, `#IBOutlet` and `#IBAction`:
class CustomHeader: UITableViewHeaderFooterView {
static let reuseIdentifier = "CustomHeader"
weak var delegate: CustomHeaderDelegate?
#IBOutlet weak var customLabel: UILabel!
var sectionNumber: Int! // you don't have to do this, but it can be useful to have reference back to the section number so that when you tap on a button, you know which section you came from; obviously this is problematic if you insert/delete sections after the table is loaded; always reload in that case
#IBAction func didTapButton(_ sender: AnyObject) {
delegate?.customHeader(self, didTapButtonInSection: section)
}
}
Create NIB. Personally, I give the NIB the same name as the base class to simplify management of my files in my project and avoid confusion. Anyway, the key steps include:
Create view NIB, or if you started with an empty NIB, add view to the NIB;
Set the base class of the view to be whatever your UITableViewHeaderFooterView subclass was (in my example, CustomHeader);
Add your controls and constraints in IB;
Hook up #IBOutlet references to outlets in your Swift code;
Hook up the button to the #IBAction; and
For the root view in the NIB, make sure to set the background color to "default" or else you'll get annoying warnings about changing background colors.
In the viewDidLoad in the view controller, register the NIB. In Swift 3 and later:
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "CustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: CustomHeader.reuseIdentifier)
}
In viewForHeaderInSection, dequeue a reusable view using the same identifier you specified in the prior step. Having done that, you can now use your outlet, you don't have to do anything with programmatically created constraints, etc. The only think you need to do (for the protocol for the button to work) is to specify its delegate. For example, in Swift 3:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "CustomHeader") as! CustomHeader
headerView.customLabel.text = content[section].name // set this however is appropriate for your app's model
headerView.sectionNumber = section
headerView.delegate = self
return headerView
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44 // or whatever
}
Obviously, if you're going to specify the view controller as the delegate for the button in the header view, you have to conform to that protocol:
extension ViewController: CustomHeaderDelegate {
func customHeader(_ customHeader: CustomHeader, didTapButtonInSection section: Int) {
print("did tap button", section)
}
}
This all sounds confusing when I list all the steps involved, but it's really quite simple once you've done it once or twice. I think it's simpler than building the header view programmatically.
In matt's answer, he protests:
The problem, quite simply, is that you cannot magically turn a UIView in a nib into a UITableViewHeaderFooterView merely by declaring it so in the Identity inspector.
This is simply not correct. If you use the above NIB-based approach, the class that is instantiated for the root view of this header view is a UITableViewHeaderFooterView subclass, not a UIView. It instantiates whatever class you specify for the base class for the NIBs root view.
What is correct, though, is that some of the properties for this class (notably the contentView) aren't used in this NIB based approach. It really should be optional property, just like textLabel and detailTextLabel are (or, better, they should add proper support for UITableViewHeaderFooterView in IB). I agree that this is poor design on Apple's part, but it strikes me as a sloppy, idiosyncratic detail, but a minor issue given all the problems in table views. E.g., it is extraordinary that after all these years, that we still can't do prototype header/footer views in storyboards at all and have to rely on these NIB and class registration techniques at all.
But, it is incorrect to conclude that one cannot use register(_:forHeaderFooterViewReuseIdentifier:), an API method that has actively been in use since iOS 6. Let’s not throw the baby out with the bath water.
See previous revision of this answer for Swift 2 renditions.
Rob's answer, though it sounds convincing and has withstood the test of time, is wrong and always was. It's difficult to stand alone against the overwhelming crowd "wisdom" of acceptance and numerous upvotes, but I'll try to summon the courage to tell the truth.
The problem, quite simply, is that you cannot magically turn a UIView in a nib into a UITableViewHeaderFooterView merely by declaring it so in the Identity inspector. A UITableViewHeaderFooterView has important features that are key to its correct operation, and a plain UIView, no matter how you may cast it, lacks them.
A UITableViewHeaderFooterView has a contentView, and all your custom subviews must be added to this, not to the UITableViewHeaderFooterView.
But a UIView mysteriously cast as a UITableViewHeaderFooterView lacks this contentView in the nib. Thus, when Rob says "Add your controls and constraints in IB", he is having you add subviews directly to the UITableViewHeaderFooterView, and not to its contentView. The header thus ends up incorrectly configured.
Another sign of the issue is that you are not permitted to give a UITableViewHeaderFooterView a background color. If you do, you'll get this message in the console:
Setting the background color on UITableViewHeaderFooterView has been deprecated. Please set a custom UIView with your desired background color to the backgroundView property instead.
But in the nib, you cannot help setting a background color on your UITableViewHeaderFooterView, and you do get that message in the console.
So what's the right answer to the question? There's no possible answer. Apple has made a huge goof here. They have provided a method that allows you to register a nib as the source of your UITableViewHeaderFooterView, but there is no UITableViewHeaderFooterView in the Object Library. Therefore this method is useless. It is impossible to design a UITableViewHeaderFooterView correctly in a nib.
This is a huge bug in Xcode. I filed a bug report on this matter in 2013 and it is still sitting there, open. I refile the bug year after year, and Apple keeps pushing back, saying "It has not been determined how or when the issue will be resolved." So they acknowledge the bug, but they do nothing about it.
What you can do, however, is design a normal UIView in the nib, and then, in code (in your implementation of viewForHeaderInSection), load the view manually from the nib and stuff it into the contentView of your header view.
For example, let's say we want to design our header in the nib, and we have a label in the header to which we want to connect an outlet lab. Then we need both a custom header class and a custom view class:
class MyHeaderView : UITableViewHeaderFooterView {
weak var content : MyHeaderViewContent!
}
class MyHeaderViewContent : UIView {
#IBOutlet weak var lab : UILabel!
}
We register our header view's class, not the nib:
self.tableView.register(MyHeaderView.self,
forHeaderFooterViewReuseIdentifier: self.headerID)
In the view xib file, we declare our view to be a MyHeaderViewContent — not a MyHeaderView.
In viewForHeaderInSection, we pluck the view out of the nib, stuff it into the contentView of the header, and configure the reference to it:
override func tableView(_ tableView: UITableView,
viewForHeaderInSection section: Int) -> UIView? {
let h = tableView.dequeueReusableHeaderFooterView(
withIdentifier: self.headerID) as! MyHeaderView
if h.content == nil {
let v = UINib(nibName: "MyHeaderView", bundle: nil).instantiate
(withOwner: nil, options: nil)[0] as! MyHeaderViewContent
h.contentView.addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
v.topAnchor.constraint(equalTo: h.contentView.topAnchor).isActive = true
v.bottomAnchor.constraint(equalTo: h.contentView.bottomAnchor).isActive = true
v.leadingAnchor.constraint(equalTo: h.contentView.leadingAnchor).isActive = true
v.trailingAnchor.constraint(equalTo: h.contentView.trailingAnchor).isActive = true
h.content = v
// other initializations for all headers go here
}
h.content.lab.text = // whatever
// other initializations for this header go here
return h
}
It's dreadful and annoying, but it is the best you can do.
Create a UITableViewHeaderFooterView and its corresponding xib file.
class BeerListSectionHeader: UITableViewHeaderFooterView {
#IBOutlet weak var sectionLabel: UILabel!
#IBOutlet weak var abvLabel: UILabel!
}
Register the nib similarly to how you register a table view cell. The nib name and reuse identifier should match your file names. (The xib doesn't have a reuse id.)
func registerHeader {
let nib = UINib(nibName: "BeerListSectionHeader", bundle: nil)
tableView.register(nib, forHeaderFooterViewReuseIdentifier: "BeerListSectionHeader")
}
Dequeue and use similarly to a cell. The identifier is the file name.
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "BeerListSectionHeader") as! BeerListSectionHeader
let sectionTitle = allStyles[section].name
header.sectionLabel.text = sectionTitle
header.dismissButton?.addTarget(self, action: #selector(dismissView), for: .touchUpInside)
return header
}
Don't forget the header height.
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return BeerListSectionHeader.height
}
I don't have enough reputation to add comment to Matt answer.
Anyway, the only thing missing here is to remove all subviews from UITableViewHeaderFooterView.contentView before adding new views. This will reset reused cell to initial state and avoid memory leak.