Last active
February 2, 2023 00:10
-
-
Save thejohnlima/ebca08a6009bcf3ad160970dfaefc709 to your computer and use it in GitHub Desktop.
Extensions to help on UITableView implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
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:
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.
Sounds good
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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?