Sunday, February 17, 2019

Add a header for UITableView

I'm developing a pet project, MyCalculator. I'm using a tableview to display the history of Math operations the user has entered. The image below shows how it looks like. (Note that I'm using Xcode 9 and Swift 3 to write this post.) 



At first, I dragged a UILabel and dropped it on the UITableView above the UITableViewCell in the Interface Builder. When I run the app, the label is on top of all cells so it looked like a header. But, when I started scrolling down the tableview, the label moved up along with the cells and disappeared. This is not right. The header is supposed to be fixed on top. Then, I figured that the correct way of adding a header for the tableview is overriding the UITableViewDelegate's method:

tableView(_:UITableView, viewForHeaderInSection:Int) -> UIView?

In this post, I'm gonna create the header view using a XIB file. Some people might create it programmatically.

1. Overriding the UITableViewDelegate's tableView() method


Make sure the ViewController conform to UITableViewDelegate protocol and then override the method as following:

public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    return historyPopup?.dequeueReusableHeaderFooterView(withIdentifier: "HistoryTableViewHeader") as! HistoryTableViewHeader
}
 

HistoryTableViewHeader is a customer class I created in the step below. As the header view will be created using a xib file, we have to register it in the ViewController's viewDidLoad() method as following:

historyPopup?.register(UINib(nibName: "HistoryTableViewHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: "HistoryTableViewHeader")
 

2. Creating HistoryTableViewHeader.swift File


import Foundation
import UIKit

public class HistoryTableViewHeader : UITableViewHeaderFooterView {
    
}
 

3. Creating HistoryTableViewHeader.xib File


a). Select File > New > File... then select View under the User Interface section and click Next. When a dialog shows up, name the file as HistoryTableViewHeader.xib and click Create.

b). find the file in the Project Navigator and open it. Then, drag a UIView from the Object Library and drop it on the view xib, and set appropriate constraints to make the UIView as a container or background view. After that, add a (History) UILabel to the container view and set any constraints needed.


c). make sure the Square icon is selected then open the Identity Inspector and set Class property under Custom Class section to HistoryTableViewHeader.

d). make sure the cube or File's Owner icon is selected then open the Identity Inspector and set Class property under Customer Class section to HistoryTableViewHeader.

e). Control-drag the History label to HistoryTableViewHeader class to create an outlet so that the label is accessible from the ViewController or other classes. When a popup appears, set Name to label and change Object from File's Owner to History Table View Header as shown in the image below (the option shows up in the listbox because of step c above).



Note that setting Object to File's Owner caused the run-time exception:
    
setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xib
 


Friday, February 15, 2019

How to detect if a UITableView has scrolled to the bottom

Concepts


A UIScrollView is a view whose origin (original coordinate) is adjustable over its content view. It clips the content view to its frame and changes its origin according to the finger movements. Then, the content view draws the part of itself based on the new origin.



UITableView is a subclass of UIScrollView so it's scrollable by default. To be able to detect if the scrollview has reached the bottom of its content, we have to understand three properties: contentOffset, frame, and contentSize.

contentOffset is the distance in points between the content view's origin and the scroll view's origin. A view's frame defines its location and size within its superview's coordinate system.




Codes


The ViewController must conform to UIScrollViewDelegate protocol, and set it as the tableView's delegate in viewDidLoad() method:

tableView.delegate = self
 

After that, add below method to the ViewController.
    
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
    if scrollView.frame.size.height >= scrollView.contentSize.height - scrollView.contentOffset.y {
       // add some codes here
    }
}
 

scrollView.frame.size.height does not change at all in this case. scrollView.contentSize is the size of all cells together in the table view.


References









Thursday, February 7, 2019

Create a Custom UITableViewCell in XIB File

I'm using Xcode 9 and Swift 3 to write this post. I'm going to create a custom UITableViewCell in a separate XIB file and then reuse it by a UITableView in the Main.storyboard file.

NIB files
Cocoa and Cocoa Touch objects are saved as .nib files. A nib file is a description of visual elements of an application's user interfaces such as windows, menus, and controls. The system (MacOS and iOS) uses the description at run-time to create the instances of the UI objects.

XIB files
The .storyboard and .xib files are used in Interface Builder to create the user interface's objects and see the result instantly. A .xib file can contain views or controls or even one controller. The .storyboard file contains the main interface of the application; it might include a .xib file and multiple view controllers. The .xib file is compiled by Xcode as a .nib file, which is loaded by the system at run-time. File's Owner is one of the most important objects of a nib file. The application code accesses the contents of the nib file through it.

1). Creating a Custom UITableViewCell with a XIB File


Go to File > New > File... then under iOS pane, select Cocoa Touch Class and click Next. When a dialog shows up, fill in the details as followings:


After clicking the Next button, two files will be created: HistoryTableViewCell.swift and HistoryTableViewCell.xib.

Click on the HistoryTableViewCell.xib file then add two labels (Operations and Result) then set the constraints properly. After that, click on the File Owner icon (cube icon) then open the Identity Inspector, set Class property to HistoryTableViewCell.


Create two outlets in HistoryTableViewCell.swift which reference Operations and Result labels in HistoryTableViewCell.xib.


2). Reuse the Custom UITableViewCell in Main.storyboard


Drag a UITableView control from the Object Library to Interface Builder and then drag a UITableViewCell control to the UITableView.

Select the Table View Cell then the open the Identity Inspector and set Class setting to HistoryTableViewCell.

3). Register the XIB file in the ViewController


Create an outlet for the UITableView (named historyTableView) in the ViewController.swift and add the following codes to the viewDidLoad() method of the ViewContoller.

historyTableView.register(UINib(nibName: "HistoryTableViewCell", bundle: nil), forCellReuseIdentifier: "historyTableViewCell")
historyTableView.dataSource = self
historyTableView.delegate = self
 

The register(nibName:, forCellReuseIdentifier:) method tells the table view how to create the cell in the nib object (XIB file).

The table view needs a datasource to query data for display. In this case, the ViewController is the datasource because I'll extend it to conforms to UITableViewDataSource.

4). Add Sample Data to the Table View


Add an extension to make ViewController adopt two protocols: UITableViewCellDataSource and UITableViewDelegate.
    
extension ViewController : UITableViewDataSource, UITableViewDelegate {
    
    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }
    
    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = historyTableView.dequeueReusableCell(withIdentifier: "historyTableViewCell") as! HistoryTableViewCell
        cell.operationsLabel.text = "1+2="
        cell.resultLabel.text = "3"
        return cell
    }
}
 

The two methods belong to UITableViewDataSource protocol, and they are required for displaying the data. In this case, the table view has only one row.

The dequeueReusableCell(withIdentifier) method creates an instance of HistoryTableViewCell if not exists in the queue and then reuse it for sub sequent requests.



References
https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UsingInterfaceBuilder.html
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html#//apple_ref/doc/uid/10000051i-CH4