Skip to content

Instantly share code, notes, and snippets.

@bojan
Last active July 4, 2022 13:32
Show Gist options
  • Save bojan/b376f94578c27f694cb55116c2ed2225 to your computer and use it in GitHub Desktop.
Save bojan/b376f94578c27f694cb55116c2ed2225 to your computer and use it in GitHub Desktop.
IB auto-magical loading
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <[email protected]>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
import Foundation
import UIKit
// MARK: - Protocol declaration
/// A utility protocol for auto-magically loading of nib-backed views.
/// In order to use the protocol, you need to conform your `UIView` subclass to it.
/// Also, make sure to set the class name in the nib (.xib).
///
/// - Important:
/// - When the view is loaded in **code**, then you have to set the class name for the **View** property.
/// - When the view is loaded in **a storyboard**, you have to set the class name in the **File's owner** property.
///
/// If the view is intended to be used in a storyboard, you'll need to create an intermediate view which will contain the nib content.
/// The following sample should be used only as a basis.
/// ```
/// class FooView: UIView, NibBackedView {
/// private var contentView: UIView?
///
/// required init?(coder aDecoder: NSCoder) {
/// super.init(coder: aDecoder)
/// configure()
/// }
///
/// override init(frame: CGRect) {
/// super.init(frame: frame)
/// configure()
/// }
///
/// private func configure() {
/// let view = loadFromNib(useInStoryboard: true)
/// view.frame = self.bounds
/// addSubview(view)
/// contentView = view
/// }
/// }
/// ```
public protocol NibBackedView: Reusable {
/// Returns the nib name, equal to the class name.
static var nibName: String { get }
/// Returns the nib from the current bundle.
static var nib: UINib { get }
/// Returns the size of the view as set in the nib.
/// - Note:
/// When loading from nib, the size of the view is the one set in the actual nib.
static var estimatedSize: CGSize { get }
/// A shorthand method for loading the view from its associated nib for the current instance.
/// - Parameter useInStoryboard: When used in a storyboard, the nib needs the **File's owner** set.
/// - Important:
/// - Intended for loading views both in code and storyboards.
func loadFromNib(useInStoryboard: Bool) -> UIView
/// A static method for loading the view from its associated nib.
/// - Important:
/// - Intended for loading views in code.
static func loadFromNib() -> Self
}
// MARK: - Default protocol implementation
extension NibBackedView {
public static var nibName: String {
reuseIdentifier
}
public static var nib: UINib {
UINib(nibName: nibName,
bundle: Bundle(for: self))
}
public func loadFromNib(useInStoryboard: Bool = false) -> UIView {
// The owner needs to be passed only when we use the view from storyboards.
Self.loadFromNib(owner: useInStoryboard ? self : nil)
}
public static func loadFromNib() -> Self {
// The owner should be nil for views loaded in code.
loadFromNib(owner: nil)
}
/// Load a view from a nib safely or throw a fatal error in debug builds.
/// - Parameters:
/// - owner: An owner object of the loaded view.
private static func loadFromNib<T>(owner: Any?) -> T {
// When a view is loaded from a nib and used in a storyboard, we need to pass the owner to the nib instantiating method.
// If an instance cannot be loaded, it is usually due to configuration.
guard let view = nib.instantiate(withOwner: owner, options: nil).first as? T else {
let errorMessage = (owner != nil)
? "Check if the File's owner has been to set to \(nibName) in the nib itself."
: "Check if the class of the root view has been set to \(nibName) in the nib itself."
fatalError("Could not instantiate a view from \(nibName)! \(errorMessage)")
}
// Return the loaded view.
return view
}
public static var estimatedSize: CGSize {
loadFromNib().bounds.size
}
}
// MARK: - Table view cells
extension NibBackedView where Self: UITableViewCell {
// MARK: - Table cell registration
/// Registers this cell for use in the passed table view.
/// - Parameter tableView: The table view which should register the cell.
public static func register(for tableView: UITableView) {
tableView.register(nib,
forCellReuseIdentifier: reuseIdentifier)
}
}
// MARK: - Table view header and footer views
extension NibBackedView where Self: UITableViewHeaderFooterView {
// MARK: - Table cell registration
/// Registers this view for use in the passed table view as a header or footer.
/// - Parameter tableView: The table view which should register the view.
public static func registerHeaderFooter(for tableView: UITableView) {
tableView.register(nib,
forHeaderFooterViewReuseIdentifier: reuseIdentifier)
}
}
// MARK: - Collection view cells
extension NibBackedView where Self: UITableViewHeaderFooterView {
// MARK: - Collection cell registration
/// Registers this cell for use in the passed collection view.
/// - Parameter collectionView: The collection view which should register the cell.
public static func register(for collectionView: UICollectionView) {
collectionView.register(nib,
forCellWithReuseIdentifier: reuseIdentifier)
}
}
// MARK: - Collection view reusable views
extension NibBackedView where Self: UICollectionReusableView {
// MARK: - Reusable view registration
/// Registers this view for use in the passed table view as a header.
/// - Parameter collectionView: The collection view which should register the view.
public static func registerHeader(for collectionView: UICollectionView) {
registerSupplementaryView(for: collectionView,
of: UICollectionView.elementKindSectionHeader)
}
/// Registers this view for use in the passed table view as a header.
/// - Parameter collectionView: The collection view which should register the view.
public static func registerFooter(for collectionView: UICollectionView) {
registerSupplementaryView(for: collectionView,
of: UICollectionView.elementKindSectionFooter)
}
/// Registers this view for use in the passed table view as a header or a footer.
/// - Parameters:
/// - collectionView: The collection view which should register the view.
/// - kind: The kind of supplementary view to register.
private static func registerSupplementaryView(for collectionView: UICollectionView,
of kind: String) {
collectionView.register(nib,
forSupplementaryViewOfKind: kind,
withReuseIdentifier: reuseIdentifier)
}
}
import Foundation
import UIKit
// MARK: - Protocol declaration
/// Describes any view that uses the reusability mechanism.
public protocol ReusableView: UIView, StoryboardIdentifiable {
/// Returns the reuse identifier, defaults to the class name.
static var reuseIdentifier: String { get }
}
// MARK: - Default protocol implementation
extension ReusableView {
public static var reuseIdentifier: String {
identifier
}
}
// MARK: - Default Conformance
extension UITableViewCell: ReusableView {}
extension UITableViewHeaderFooterView: ReusableView {}
extension UICollectionReusableView: ReusableView {}
// MARK: - Table view cells
extension ReusableView where Self: UITableViewCell {
// MARK: - Dequeuing
/// Returns a reusable cell of this class. Replaces the `UITableView.dequeueReusableCell(withIdentifier:for:)` method.
/// - Parameters:
/// - tableView: The table view which should dequeue the cell.
/// - indexPath: The index path specifying the location of the cell.
/// - Note:
/// Make sure the cell is registered for use in the passed table, use the `register(for:)` method.
public static func dequeue(from tableView: UITableView,
at indexPath: IndexPath) -> Self {
guard let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier,
for: indexPath) as? Self
else {
fatalError("Could not dequeue a cell for \(reuseIdentifier)! For nib-backed cells, check if the class name has been set in the nib itself.")
}
return cell
}
}
// MARK: - Table view header and footer views
extension ReusableView where Self: UITableViewHeaderFooterView {
// MARK: - Dequeuing
/// Returns a reusable header or footer view of this class. Replaces the `UITableView.dequeueReusableHeaderFooterView(withIdentifier:)` method.
/// - Parameter tableView: The table view which should dequeue the view.
/// - Note:
/// Make sure the cell is registered for use in the passed table, use the `registerHeaderFooter(for:)` method.
public static func dequeueHeaderFooterView(from tableView: UITableView) -> Self {
guard let cell = tableView.dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier) as? Self else {
fatalError("Could not dequeue a header/footer view from \(reuseIdentifier)! For nib-backed cells, check if the class name has been set in the nib itself.")
}
return cell
}
}
// MARK: - Collection view cells
extension ReusableView where Self: UICollectionViewCell {
// MARK: - Dequeuing
/// Returns a reusable cell of this class. Replaces the `UICollectionView.dequeueReusableCell(withIdentifier:for:)` method.
/// - Parameters:
/// - collectionView: The collection view which should dequeue the cell.
/// - indexPath: The index path specifying the location of the cell.
public static func dequeue(from collectionView: UICollectionView,
at indexPath: IndexPath) -> Self {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier,
for: indexPath) as? Self else {
fatalError("Could not dequeue a cell from \(reuseIdentifier)! For nib-backed cells, check if the class name has been set in the nib itself.")
}
return cell
}
}
// MARK: - Collection view reusable views
extension ReusableView where Self: UICollectionReusableView {
// MARK: - Dequeuing
/// Returns a reusable header view of this class. Replaces the `UITableView.dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:)` method.
/// - Parameters:
/// - collectionView: The collection view which should dequeue the view.
/// - indexPath: The index path specifying the location of the view.
public static func dequeueHeader(from collectionView: UICollectionView,
at indexPath: IndexPath) -> Self {
dequeueSupplementaryView(from: collectionView,
at: indexPath,
of: UICollectionView.elementKindSectionHeader)
}
/// Returns a reusable footer view of this class. Replaces the `UITableView.dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:)` method.
/// - Parameters:
/// - collectionView: The collection view which should dequeue the view.
/// - indexPath: The index path specifying the location of the view.
public static func dequeueFooter(from collectionView: UICollectionView, at indexPath: IndexPath) -> Self {
dequeueSupplementaryView(from: collectionView,
at: indexPath,
of: UICollectionView.elementKindSectionFooter)
}
/// Returns a reusable supplementary view of this class. Replaces the `UITableView.dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:)` method.
/// - Parameters:
/// - collectionView: The collection view which should dequeue the view.
/// - indexPath: The index path specifying the location of the view.
/// - kind: The kind of supplementary view to dequeue.
private static func dequeueSupplementaryView(from collectionView: UICollectionView,
at indexPath: IndexPath,
of kind: String) -> Self {
guard let cell = collectionView.dequeueReusableSupplementaryView(ofKind: kind,
withReuseIdentifier: reuseIdentifier,
for: indexPath) as? Self else {
fatalError("Could not dequeue a supplementary view from \(reuseIdentifier)! For nib-backed cells, check if the class name has been set in the nib itself.")
}
return cell
}
}
import Foundation
import UIKit
// MARK: - Protocol declaration
/// A protocol describing types that have a storyboard identifier.
public protocol StoryboardIdentifiable {
/// Returns a storyboard identifier used by view controller and table/collection cell subclasses.
///
/// By convention, the identifier (set in Interface Builder) and the class name **should be identical**.
///
///- Important: For nib-backed cells, the filename should also be equal to this identifier.
static var identifier: String { get }
}
// MARK: - Default Implementation
extension StoryboardIdentifiable {
public static var identifier: String {
String(describing: self)
}
}
// MARK: - Default Conformance
extension UIViewController: StoryboardIdentifiable {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment