Accessing UITableView Header View - ios

I have a UITableView where the section header view contains a UITextField that the user can key in values. When I want to save the value that user keyed in, I need to access the table section header.
I tried using the method headerView(forSection:), and I put headerView(forSection: 0)! as a test to make sure that the section header definitely exists, but every time when I run the programme, the line where the method is returns an error "unexpectedly found nil while unwrapping an optional value".
I have also tried to tag the UITextField, but when I attempt to access the section header using certainTableView.viewWithTag(_ tag:), and cast the resulting UIView to UITextField, there is an error thrown at the line where casting happens, saying "Could not cast value of type 'UITableView' to 'UITextField'."
Both errors look pretty bizarre to me. In the first case, I made sure that the section header is existent, yet the method returned nil. In the second case, the returned tagged view should be of type UITableViewHeaderFooterView, yet the type is UITableView. Can someone suggest any possible reason why this is so and how to correct?
Or alternatively, is there any better approach to access the section header view so that I can save the text keyed in in the UITextField?
Below is the detail that I used in the first case.
In func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?, I returned a UIView that contains a UITextField. In the table view which I need to save the keyed-in user data, I have a button that, when the user taps it, data are saved. Within that selector, I have the following:
//save item
let a = addItemTableView.headerView(forSection: 0)!
let b = a.subviews[0] as! UITextField
let c = b.text ?? ""
someNSManagedObject.text = c
The error I mentioned above appears at the first line.

After reading the post in this question, I got a hint and found out that, in func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? method, I should return a UITableViewHeaderFooterView instead of the previously UIView. As I changed the returned object's type to UITableViewHeaderFooterView, with everything else remaining the same, (i.e. add a UITextField to the UITableViewHeaderFooterView) I managed to access the UITextField in the UITableViewHeaderFooterView via headerView(forSection:) method.
To Be even clearer, given in the apple's documentation that the function declaration is func headerView(forSection section: Int) -> UITableViewHeaderFooterView?, showing that the returned value is UITableViewHeaderFooterView, in order to access the header view using the method, the returned value from func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? should be UITableViewHeaderFooterView instead of any other generic UIView or UIView subclasses.
Hope this helps.

First add a property to your model textFieldData to save textField data in.
Then in your HeaderViewClass add a closure var textFieldDidChange: ((String) -> Void)?
class HeaderViewClass: UITableViewHeaderFooterView {
// MARK:- IBOutlets
#IBOutlet weak var textField: UITextField!
// MARK:- Variable
var textFieldDidChange: ((String) -> Void)?
// MARK:- Functions
func fill(with model: Model) {
textField.delegate = self
textField.text = model.textFieldData
}
}
extension HeaderViewClass: UITextFieldDelegate {
func textFieldDidChange(_ textField: UITextField) {
textFieldDidChange?(textField.text)
}
}
In your viewController class
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: PlaceOrderFooterView.id) as! PlaceOrderFooterView
footerView.fill(with: models[section])
footerView.textFieldDidChange = { [unowned self] text in
self.models[section].textFieldData = text
}
return footerView
}
Now your model object has the updated value whenever any textField data is changed.

In your method
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {}
when you return your custom header containing UITextField , assign it's delegate to your controller, this way you'll get all keyed values by user under UITextFieldDelegate function

Related

UITableViewDiffableDataSource: how to set section header titles?

I am trying to set up an UITableView with sections using the new UITableViewDiffableDataSource within an UITableViewController.
Everything seems to work fine except setting the section header titles.
According to Apple's documentation, UITableViewDiffableDataSource conforms to UITableViewDataSource, so I was expecting this to be possible.
I have tried:
overriding tableView(_ tableView:, titleForHeaderInSection section:)
in the UITableViewController class
subclassing UITableViewDiffableDataSource and implementing tableView(_ tableView:, titleForHeaderInSection section:) in the subclass
but both ways lead to no result (Xcode 11 and iOS13 beta 3).
Is there currently a way to set the section header titles using UITableViewDiffableDataSource?
Providing code example on #particleman's explanations.
struct User: Hashable {
var name: String
}
enum UserSection: String {
case platinum = "Platinum Tier"
case gold = "Gold Tier"
case silver = "Silver Tier"
}
class UserTableViewDiffibleDataSource: UITableViewDiffableDataSource<UserSection, User> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let user = self.itemIdentifier(for: IndexPath(item: 0, section: section)) else { return nil }
return self.snapshot().sectionIdentifier(containingItem: user)?.rawValue
}
}
Update: Starting in beta 8, you can now implement tableView(_ tableView:, titleForHeaderInSection section:) in a UITableViewDiffableDataSource subclass and it works properly.
The default behavior for populating the header title has always been a little strange to have in the data source. With UITableViewDiffableDataSource, Apple seems to be acknowledging such by not providing the default string-based behavior; however, the UITableViewDelegate methods continue to work as before. Implementing tableView(_:viewForHeaderInSection:) by initializing and returning a UILabel with the desired section title and implementing tableView(_:heightForHeaderInSection:) to manage the desired height works.
Let me suggest quite a flexible universal solution:
Declare a subclass:
class StringConvertibleSectionTableViewDiffibleDataSource<UserSection: Hashable, User: Hashable>: UITableViewDiffableDataSource<UserSection, User> where UserSection: CustomStringConvertible {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sectionIdentifier(for: section)?.description
}
}
Usage example:
class ComitsListViewController: UITableViewController {
private var diffableDataSource = StringConvertibleSectionTableViewDiffibleDataSource<String, Commit>(tableView: tableView) { (tableView, indexPath, commit) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "Commit", for: indexPath)
cell.configure(with: commit)
return cell
}
}
You are not limited just to String thought. You can control what to display as section title by implementing description var of CustomStringConvertible protocol for your section type.
After you init self.dataSource to a UITableViewDiffableDataSource (which sets itself to the tableView.dataSource) set the tableView.dataSource back to self, i.e. the UITableViewController subclass. Now in your numberOfSectionsInTableView and numberOfRowsInSection methods forward those to self.dataSource and return its info (this is the composition pattern). Now your UITableViewController just implements its section titles as normal since it is the table's data source.
I believe UITableViewDiffableDataSource should not be setting itself as the dataSource if one is already set but I guess they designed it to work in the least error prone way because with UITableViewController added to a storyboard its already set.
If you do it this way then it makes sense why the class wasn't open in the early iOS 13 betas.

Crash at tableview.reloaddata with error Unexpectedly found nil

On the click of a create button, I navigate to tableviewcontroller screen. But nothing is populated in the tableview since the array which populates the tableview hasn't been called yet.
Now once the tableview screen is reached, after a few seconds another method is called elsewhere(in another file), which in turn calls a function in this tableviewcontroller screen. This is that method of the tableviewcontroller screen which is called...
func stopIndicator(thegrpName: String) {
stopIndicator()
let realm = try! Realm()
let chatGrp = realm.objects(ChatGroup.self)
chatGroup = chatGrp
tableview.reloadData() //CRASH HAPPENS HERE
}
In this method, once I reach tableview.reloadData() it crashes with the error Unexpectedly found nil while unwrapping an optional value..
I referred this link which seems to have a similar problem...but couldn't get much help out of it...
What could be the reason for this crash...?
EDIT 1: The numberOfRows and cellForRowAt.. is given like so...
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let chatGrp = chatGroup {
return chatGrp.count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: ChatGroupTableViewCell = tableView.dequeueReusableCell(withIdentifier: "chatgroupIdentifier") as! ChatGroupTableViewCell
let groupChatObj = chatGroup![indexPath.row]
cell.chatLabel.text = groupChatObj.lastMessage?.text?.text
return cell
}
It looks you are trying to create delegate method but in another file where you are trying to call delegate method stopIndicator you are calling method on singleton instead which gives you an error.
So, set delegate right. First create protocol
protocol YourProtocol {
func stopIndicator(thegrpName: String)
}
then in another file create delegate property
var delegate: YourProtocol?
now when you need to call delegate method stopIndicator call this
delegate?.stopIndicator(thegrpName: ...)
now add to your ViewController protocol
ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, YourProtocol
and now somewhere set your another file class delegate as your ViewController (if its view set it in viewDidLoad if it is another ViewController set it in prepareForSegue)
fileClass.delegate = self

Strange Swift Syntax, What's It Doing?

So I'm following this tutorial for In-App-Purchases. Here are a few things I don't get:
For the table, in the rowAtIndexPath they use a handler, what is that?
They put all the table code in an extension. I don't know why.
There's also a weird "buyButtonHandler?(product!)" call on button tap
I'd appreciate any clarification on any of the above points. Below is the table code where they put the table in an extension:
extension MasterViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return products.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ProductCell
var products = [SKProduct]() //This is actually declared elsewhere
let product = products[(indexPath as NSIndexPath).row]
cell.product = product
cell.buyButtonHandler = { product in
RageProducts.store.buyProduct(product)
}
return cell
}
}
And the above code includes the strange that I'm looking for help understanding:
cell.buyButtonHandler = { product in
RageProducts.store.buyProduct(product)
}
The table cell has a button and in the cell class this is its code:
func buyButtonTapped (_ sender: AnyObject) {
buyButtonHandler?(product!)
}
It references the below line. This button code/reference is gibberish to me:
var buyButtonHandler: ((_ product: SKProduct) -> ())?
I don't get what that buyButtonHandler is doing, it's like 50% parenthesis! Lastly, I'm including the below var declaration, in case it helps for context:
var product: SKProduct? {
didSet {
guard let product = product else { return }
textLabel?.text = product.localizedTitle
if RageProducts.store.isProductPurchased(product.productIdentifier) {
//Setup
} else {
//Alternate setup
}
}
}
The stuff you're seeing is fairly standard Swift.
Bullet #1:
It looks like the table view cells hold a closure, which is a block of code that they save and run later. The IBAction for the cell's button just invokes the handler block. (The term block and closure are interchangeable. Objective-C calls them blocks, Swift calls them closures.)
So the code in cellForRowAtIndexPath is installing a closure into the cell. That lets you configure your cells from outside. It's a neat trick.
Bullet #2:
It's considered good form to place the methods that implement a protocol in an extension. That way they're all grouped together and easy to find. It also makes the extension into a nice modular block of code. The extension is probably for the UITableViewDelegate and/or UITableViewDataSource protocol methods.
Bullet #3:
Same thing as #1. The cell stores a closure (block of code) in a variable, and when the user taps a button, the button's IBAction invokes the stored closure.
Bullet 1 and Bullet 3 mean that in the table view data source's cellForRowAtIndexPath method you can provide a custom block of code for each cell that gets invoked when the cell's button is tapped. The code in the button IBAction invokes the stored closure and passes it the current product.

Swift View Controller with UITableView sections

I've been searching for awhile without luck. I am trying to find an example of a View Controller with a UITableView that has sections. The examples I've see are all dealing with a Table View Controller which I cannot use as I have need of buttons in the same view which control the content of the table view. Anyone have an example, know of an example or have an idea about to implement such? Thanks.
Edit
I've got a table view in a view controller, get the data from an api call, separate the sections and data in an array of a struct. I then send this to be bound to the table view. Doing so throws
[UIView tableView:numberOfRowsInSection:]: unrecognized selector sent to instance
but I don't understand where the problem is.
Code for the tablview
//MARK: Tableview delegates
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let count = incidentDataSection?.count{
return count
}
return 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (incidentDataSection?.count)! > 0{
return incidentDataSection![section].incidents.count
}
return 0
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return incidentDataSection?[section].title
}
/*
func tableView(tableView: UITableView, iconForHeaderInSection section: Int) -> UIImage? {
return incidentDataSection?[section].icon
}*/
//if clicked, will openn details view passing in the details
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//let incidentDetails = incidentData?[indexPath.row]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let section = incidentDataSection?[indexPath.section] {
let cell = tableView.dequeueReusableCell(withIdentifier: "IncidentTableViewCell") as! IncidentTableViewCell
cell.roadNameLabel.text = section.incidents[indexPath.row].RoadWay
cell.whenLabel.text = section.incidents[indexPath.row].DateCreated
cell.statusLabel.text = section.incidents[indexPath.row].DateCleared
return cell
}
return UITableViewCell()
}
incidentDataSection is an array of a struct which has the section title and the different items.
Answer
Though I received some fairly good feedback, the cause was actually a typo. Looking closely at
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return incidentDataSection?[section].title
}
you'll notice the problem is that there is no underscore before tableView:. What was happening is that the datasource and delegate were skipping over the functions since with and without call different protocols in swift 3. Thanks to thislink I was able to figure out the cause. My bad for forgetting to mention this was in Swift 3. Might had saved everyone some time.
You need a tableview instance in your view controller.
Implement the protocols UITableViewDelegate, UITableViewDataSource in your view controller as a UITableViewController.
Don't forget bind the tableview in XIB with tableview in the class.
Look this sample:
class Sample01ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
tableView?.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
self.tableView?.reloadData()
}
// ...
You have the required methods implemented, however it sounds like you need to "subclass" or "subcribe" to the UITableView's delegate and dataSource. By using:
class MyViewController : UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView : UITableView!
}
Now that you have those protocols you will need to set your tableView's delegate and dataSource to your viewController. You can do this using storyboard by drag and drop, or inside of your viewDidLoad() which is what I always do because it is easy for other developers to see from the start of opening your code where your delegate and dataSources are assigned to. Using:
#override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
Then your delegate methods and dataSource methods in your viewcontroller will be called for that tableView. Then you can add the IBOutlets to UIButton/UILabel/UISwitch, etc... and do what you will with your ViewController without being limited to simply using a table view inside of that view controller. I Almost always use this methods when using UITableViews/UICollectionViews even if I set the tableView/collectionView to be the size of the whole view because I like the freedom of using a UIViewController over a UITableViewController/UICollectionViewController.
*Note numberOfRows() is not required but I always override it as well, just kind of a habit at this point. Also you sound new to iOS development, so if you aren't already, the next thing I would look into after getting your tableView up and running is pulling your data from your API on a background thread to keep your mainThread open for user response on your UI, DispatchQueue. This is really important if you are displaying images from the API.

configure tableview cell in previous controller

I have got controller 1 with an imageview, uitextview and button. On button click there is an action to show controller with table view. I need to configure this image view and uitextview.text in my table view, but i don't understand how?
I tried to get access
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
var header = tableView.dequeueReusableCellWithIdentifier("customHeader") as CustomHeaderCell
var controller = PreparePhotoViewController()
header.headerLabel.text = controller.textView.text//exc BAD ACCESS - textView is an outlet of controller, it is weak
header.headerPhoto.image = UIImage(named: "heart.png")
return header
}
but it has bad ACCESS exc, cause I think that uitextview with weak property is nil. So any advices how I can do my idea?
Add a property to the view controller you are going to be showing, such as:
var textToDisplay: String?
Then, before you push this new view, set that property, e.g.:
let viewController = [...]
viewController.textToDisplay = "Hello World"
self.presentViewController(viewController, animated: true, completion: nil)
Then, in your pushed view controller's viewDidLoad method, set the value of the UILabel's text property to your new self.textToDisplay value.
Hopefully that's the sort of thing you're thinking of?
I'm not sure about your approach, but I can see issue in your posted code. You are using wrong dequeue function.
Change tableView.dequeueReusableCellWithIdentifier to tableView.dequeueReusableHeaderFooterViewWithIdentifier().
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
var header = tableView.dequeueReusableHeaderFooterViewWithIdentifier("customHeader") as CustomHeaderCell
...
return header
}
And you would probably want to use more accurate name CustomHeaderView, not CustomHeaderCell.

Resources