Skip to content

Instantly share code, notes, and snippets.

@thejohnlima
Last active February 2, 2023 00:10
Show Gist options
  • Save thejohnlima/ebca08a6009bcf3ad160970dfaefc709 to your computer and use it in GitHub Desktop.
Save thejohnlima/ebca08a6009bcf3ad160970dfaefc709 to your computer and use it in GitHub Desktop.
Extensions to help on UITableView implementation
import UIKit
extension NSObject {
public var identifier: String {
String(describing: type(of: self))
}
public static var identifier: String {
String(describing: self)
}
}
extension UITableView {
/// Register nib cell
/// - Parameter cell: nib cell to register
public func register(_ cell: UITableViewCell.Type) {
let nib = UINib(nibName: cell.identifier, bundle: nil)
register(nib, forCellReuseIdentifier: cell.identifier)
}
/// Register nib cell for header or footer
/// - Parameter reusableView: reusable view to register
public func registerHeaderFooter(_ reusableView: UITableViewHeaderFooterView.Type) {
let nib = UINib(nibName: reusableView.identifier, bundle: nil)
register(nib, forHeaderFooterViewReuseIdentifier: reusableView.identifier)
}
/// Register nib cell for header or footer
/// - Parameters:
/// - nibName: nib name of the cell
/// - identifier: nib identifier
public func registerHeaderFooter(nibName: String, identifier: String? = nil) {
let nib = UINib(nibName: nibName, bundle: nil)
if let identifier = identifier {
register(nib, forHeaderFooterViewReuseIdentifier: identifier)
} else {
register(nib, forHeaderFooterViewReuseIdentifier: nibName)
}
}
/// Returns a reusable table-view cell object for the specified reuse identifier and adds it to the table.
/// - Parameters:
/// - class: object type
/// - indexPath: current index path
/// - configure: handler for configuration
/// - Returns: result cell
public func dequeueReusableCell<T: UITableViewCell>(of class: T.Type,
for indexPath: IndexPath,
configure: ((T) -> Void)? = nil) -> UITableViewCell {
let cell = dequeueReusableCell(withIdentifier: T.identifier, for: indexPath)
if let typedCell = cell as? T {
configure?(typedCell)
}
return cell
}
/// Returns a reusable view object for the specified reuse identifier and adds it to the table.
/// - Parameter class: object type
/// - Returns: result view for header or footer
public func dequeueReusableHeaderFooterView<T: UITableViewHeaderFooterView>(of class: T.Type) -> T? {
let view = dequeueReusableHeaderFooterView(withIdentifier: T.identifier)
if let typedView = view as? T {
return typedView
}
return nil
}
/// Returns a reusable table-view cell object for the specified reuse identifier and adds it to the table.
/// - Parameters:
/// - identifier: A string identifying the cell object to be reused. This parameter must not be nil
/// - indexPath: The index path specifying the location of the cell. Always specify the index path provided to you by your data source object. This method uses the index path to perform additional configuration based on the cell’s position in the table view.
/// - configure: The handler for configuration
/// - Returns: Result cell
public func genericDequeueReusableCell(of identifier: String,
for indexPath: IndexPath,
configure: ((UITableViewCell) -> Void)? = nil) -> UITableViewCell {
let cell = dequeueReusableCell(withIdentifier: identifier, for: indexPath)
configure?(cell)
return cell
}
/// Set the height of the table header view
/// - Parameter height: The height value to be set
public func setHeightTableHeaderView(height: CGFloat) {
DispatchQueue.main.async { [weak self] in
guard let headerView = self?.tableHeaderView else { return }
var newFrame = headerView.frame
newFrame.size.height = height
UIView.animate(withDuration: 0.1) {
self?.beginUpdates()
headerView.frame = newFrame
self?.endUpdates()
}
}
}
/// Set the height of the table footer view
/// - Parameter height: The height value to be set
public func setHeightTableFooterView(height: CGFloat) {
DispatchQueue.main.async { [weak self] in
guard let footerView = self?.tableFooterView else { return }
var newFrame = footerView.frame
newFrame.size.height = height
UIView.animate(withDuration: 0.1) {
self?.beginUpdates()
footerView.frame = newFrame
self?.endUpdates()
}
}
}
/// Get the cell for specific row and section
/// - Parameters:
/// - row: row of the cell
/// - section: section of the cell
/// - numberOfRows: number of rows
/// - Returns: result cell
public func cellForRow(row: Int, section: Int, numberOfRows: Int) -> UITableViewCell? {
let indexPath = IndexPath(row: row, section: section)
guard let cell = self.cellForRow(at: indexPath) else { return nil }
let rowCell = row < numberOfRows - 1 ? row + 1 : row
self.scrollToRow(at: IndexPath(row: rowCell, section: 0), at: .top, animated: false)
return cell
}
/// Reload table view data
/// - Parameter completion: completion handler
public func reloadData(completion: @escaping () -> Void) {
UIView.animate(withDuration: 0, animations: {
self.reloadData()
}, completion: { _ in
completion()
})
}
/// Update row for specific index path with animation
/// - Parameters:
/// - indexPath: current index path
/// - withAnimation: animation handler
public func updateRow(at indexPath: IndexPath, withAnimation: UITableView.RowAnimation) {
beginUpdates()
reloadRows(at: [indexPath], with: withAnimation)
endUpdates()
}
/// Remove row for specific index path
/// - Parameters:
/// - indexPath: index path to remove the cell
/// - withAnimation: animation handler
public func removeRow(at indexPath: IndexPath, withAnimation: UITableView.RowAnimation) {
beginUpdates()
deleteRows(at: [indexPath], with: withAnimation)
endUpdates()
}
/// Remove the separator of the last cell
public func setLastCellSeparatorHidden() {
tableFooterView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 0, height: 0.001)))
}
}
import UIKit
// MODELS
struct Profile: Codable {
let name: String?
let username: String?
static func getProfile(_ data: Data?) -> Profile? {
guard let data = data else { return nil }
return try? JSONDecoder().decode(Profile.self, from: data)
}
}
enum ProfileListType {
case header(Profile?)
case body(_ options: [Any] = [])
case footer
}
// VIEW MODEL
class ProfileViewModel {
// MARK: - Properties
private(set) var list: [ProfileListType] = []
private var profileMock: Profile? {
let data = """
{"name": "John Lima", "username": "@thejohnlima"}
""".data(using: .utf8)
return Profile.getProfile(data)
}
// MARK: - Initializers
init() {
list = [
.header(profileMock)
]
}
}
// VIEWS
class ProfileCell: UITableViewCell {
// MARK: - Constants
static let estimatedHeight: CGFloat = 72
// MARK: - Properties
@IBOutlet private weak var nameLabel: UILabel!
@IBOutlet private weak var usernameLabel: UILabel!
// MARK: - Public Actions
func configure(_ item: Profile?) {
nameLabel.text = item?.name
usernameLabel.text = item?.username
}
}
class ProfileViewController: UIViewController {
// MARK: - Properties
@IBOutlet private weak var tableView: UITableView!
let viewModel = ProfileViewModel()
// MARK: - View LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - Private Actions
private func configureUI() {
// The .xib files should have the same name as the class file
tableView.register(ProfileCell.self)
tableView.dataSource = self
tableView.delegate = self
}
}
// MARK: - UITableViewDataSource and UITableViewDelegate
extension ProfileViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let type = viewModel.list[indexPath.row]
switch type {
case .header(let profile):
return tableView.dequeueReusableCell(of: ProfileCell.self, for: indexPath) { cell in
cell.configure(profile)
}
default:
return UITableViewCell()
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let type = viewModel.list[indexPath.row]
switch type {
case .header:
return ProfileCell.estimatedHeight
default:
return 0
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
@tangzzz-fan
Copy link

Hi,
let nib = UINib(nibName: cell.identifier, bundle: nil)
If the extension and the xib files are in different bundles, how should I pass the parameters?

@thejohnlima
Copy link
Author

What kind of error are you seeing?

If you wish, you can do something like this:

public func register(_ cell: UITableViewCell.Type, bundle: Bundle? = nil) {
   let nib = UINib(nibName: cell.identifier, bundle: bundle)
   register(nib, forCellReuseIdentifier: cell.identifier)
}

Just in case, I ran a test using a local package and it works fine.
The table view extensions are in DemoKit file.
Have a look:

Screenshot 2022-10-31 at 9 44 37 AM

@tangzzz-fan
Copy link

Hi thejohnlima, thanks for your response, I solved the issue with the code blow:

func register<T: UITableViewCell>(_:T.Type) where T: NibLoadableView {
        let bundle = Bundle(for: T.self)
        let nib = UINib(nibName: T.nibName, bundle: bundle)
        register(nib, forCellReuseIdentifier: T.defaultReuseIdentifier)
    }

In this case, we dont need to concern about the right bundle of the cell.

@thejohnlima
Copy link
Author

Sounds good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment