DTTableViewManager

repository·main·Indexed 19 days ago

https://github.com/dentelezhkin/dttableviewmanager

A library for managing UITableView data sources and delegates, featuring a type-safe mapping system between data models and cells. It supports automated synchronization and multiple storage options including Memory, CoreData, and Realm. The library allows replacing traditional delegate methods with strongly typed closures and provides tools for handling anomalies and conditional mappings.

Tokens
20K
Snippets
69
Records
88
Agent score
65%

What's inside DTTableViewManager

  1. Use the new Events system in DTTableViewManager 5.0

    main

    The Events system has been rewritten to support 37 UITableViewDelegate and UITableViewDataSource methods. Events are categorized into two types based on whether the view is available at runtime:

    1. View-available events: These provide both the cell (view) and the model in the closure. Use these for interactions like selection or deselection.
    2. Data-only events: These occur when the view has not yet been created (e.g., calculating row height). These closures only provide the model and the IndexPath.

    Execution Logic: DTTableViewManager uses responds(to:) to manage delegate methods. It follows this priority:

    1. Execute the registered event if types match.
    2. Call the delegate/datasource method on the DTTableViewManageable instance.
    3. Fallback to default UITableView behavior.

    This allows you to use self-sizing cells safely; if you don't call heightForCell(withItem:_:), the manager won't intercept the height request, letting UITableView handle it.

    // Example: Reacting to cell deselection (View-available event)
    manager.didDeselect(FooCell.self) { cell, model, indexPath in
      print("did deselect FooCell at \(indexPath), model: \(model)")
    }
    
    // Example: Height calculation (Data-only event)
    manager.heightForCell(withItem: FooModel.self) { item, indexPath in
      return 44.0
    }
  2. Update Supplementary Providers (Headers and Footers)

    main

    In 7.0, header and footer APIs have been rewritten to be closure-based. Note the following behavioral changes:

    1. Manual Reload Required: Setting header or footer models no longer automatically triggers UITableView.reloadData(). You must call reloadData() manually if you update them after the table view is on screen.
    2. Section Creation: Setting header/footer models no longer automatically creates sections in storage. If you need a section with 0 items (to show a header, for example), you must explicitly set items in the storage (e.g., memoryStorage.setItems([Int](), forSectionAt: index)).
    3. Protocol Removal: The SupplementaryAccessible protocol and its extensions (tableHeaderModel, tableFooterModel) have been removed. To retrieve a model, use storage.header(for:) or storage.footer(for:) directly.
  3. Transfer models to cells using ModelTransfer

    main

    To automate the mapping between a data model and a cell, implement the ModelTransfer protocol in your UITableViewCell subclass. When you register the cell, DTTableViewManager uses this protocol to infer the relationship, enabling type-safe delegate closures and automatic model updates.

    If you do not want to use the protocol, you can provide a manual handler during registration to update the cell with the model.

    // Using ModelTransfer protocol (Recommended)
    class VideoPostCell: UITableViewCell, ModelTransfer {
        func update(with model: VideoPost) {
            // update cell logic
        }
    }
    
    // Manual handler (Alternative for simple cells)
    manager.register(UITableViewCell.self, for: MenuItem.self) {
        mapping in
        mapping.didSelect { cell, model, indexPath in
            // handle selection
        }
    } handler: { cell, model, indexPath in
        cell.textLabel.text = model
    }
  4. Map views to data models using ModelTransfer

    main

    The recommended way to establish a mapping between a UITableViewCell/UITableViewHeaderFooterView and a data model is by conforming your view to the ModelTransfer protocol. This protocol requires implementing a single update(with:) method to handle data transfer.

    When you register a cell that conforms to ModelTransfer, DTTableViewManager automatically establishes the mapping between the model type and the cell type.

    class FoodTableViewCell : UITableViewCell, ModelTransfer {
        func update(with model: Food) {
            // Display food in a cell
        }
    }
    
    // Registration
    manager.register(FoodTableViewCell.self)
  5. Use unsubclassed UITableViewCell and UITableViewHeaderFooterView

    main

    Previously, all cell and view subclasses were required to conform to the ModelTransfer protocol. DTTableViewManager 8.0 removes this restriction, allowing you to use standard, unsubclassed views.

    Note: For views that do not conform to ModelTransfer, you must use the new event registration style (via the mapping closure).

    // Registering an unsubclassed UITableViewCell
    manager.register(UITableViewCell.self, for: String.self) { mapping in
        // customize mapping
    } handler: { cell, model, indexPath in
        cell.textLabel.text = model
    }
    
    // Registering an unsubclassed UITableViewHeaderFooterView
    manager.registerHeader(UITableViewHeaderFooterView.self, for: String.self) { mapping in
        // customize mapping
    } handler: { header, model, indexPath in
        // ...
    }
  6. Understand event closure signatures

    main

    Event closures in DTTableViewManager follow two main signature patterns depending on whether the delegate method provides access to the cell/view:

    1. With View/Cell access: (View, Model, IndexPath) -> ReturnType. Used when the cell or reusable view is available.

      • Example: mapping.didSelect { cell, model, indexPath in ... }
      • Example: mapping.willDisplay { cell, model, indexPath in ... }
    2. Without View/Cell access: (Model, IndexPath) -> ReturnType. Used when the cell/view is not yet created or required by the delegate method.

      • Example: mapping.heightForCell { model, indexPath in return 44 }
  7. Use DTModelStorage for data source abstractions

    main

    Data source management is handled by the DTModelStorage framework. It abstracts data sources via the Storage protocol.

    Available implementations include:

    • MemoryStorage: For storing arrays of data models in memory (the default).
    • CoreDataStorage: For displaying models from CoreData via NSFetchedResultsController.
    • RealmStorage: For displaying models from Realm.
    • SingleSectionEquatableStorage: For single section diffable datasources (compatible with Changeset, Dwifft, etc.).
    • ProxyDiffableDataSourceStorage: For UITableViewDiffableDataSource on iOS/tvOS 13+.

    To show an array of models using the default MemoryStorage, use manager.memoryStorage.setItems(items).

    // Using MemoryStorage
    manager.memoryStorage.setItems(posts)
    
    // Configuring Diffable Data Source (iOS 13+)
    dataSource = manager.configureDiffableDataSource { indexPath, model in
       model
    }
  8. Handle cell state and tap events in SwiftUI cells

    main

    Standard UITableView selection and SwiftUI interaction (like Button) can clash.

    Best Practices:

    • Avoid SwiftUI Buttons: Do not use SwiftUI.Button inside a cell if you also rely on tableView(_:didSelectRowAt:). They do not play well together.
    • Use Gestures: Instead, use the .onTapGesture modifier on your SwiftUI view and communicate the event through your data model/view model.
    • Selection Style: HostingTableViewCell sets UITableViewCell.SelectionStyle = .none by default to prevent visual clashes.
  9. Implement Custom Storage

    main

    If the provided storage options do not meet your needs, you can implement your own. You can either subclass one of the five existing storage types or create a new one from scratch.

    Requirements:

    • Implement the Storage protocol.
    • (Optional) Implement the SupplementaryStorage protocol if you need to manage header/footer models.

    DTTableViewManager provides optional access to supplementaryStorage. This accessor is available for all five built-in storages and any custom storage that implements the SupplementaryStorage protocol.

    manager.supplementaryStorage?.setSectionHeaderModels([1])
  10. Handle anomalies in DTTableViewManager

    main

    DTTableViewManager detects common usage errors (e.g., registering an empty XIB) and emits anomalies. By default, these are non-fatal errors logged to the console.

    You can customize the behavior by setting an anomaly handler to send errors to analytics providers, or you can silence specific or all anomalies.

  11. Quick start with DTTableViewManager

    main

    To display an array of data models in a UITableView, follow these steps:

    1. Prepare your Cell: Create a UITableViewCell subclass and adopt the ModelTransfer protocol. Implement the update(with:) method to map your model data to the cell's UI components.
    2. Configure your View Controller: Your view controller must adopt the DTTableViewManageable protocol.
    3. Register and Populate: In viewDidLoad, use manager.register(_:) to register your cell class and manager.memoryStorage.setItems(_:) to provide the data.

    Note: If you use a .xib file for your cell, the manager will automatically handle dequeueing using that XIB.

    class PostCell : UITableViewCell, ModelTransfer {
        func update(with model: Post) {
            // Fill your cell with actual data
        }
    }
    
    class PostsViewController: UITableViewController, DTTableViewManageable {
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Register PostCell to be used with this controller's table view
            manager.register(PostCell.self)
    
            // Populate datasource
            manager.memoryStorage.setItems(posts)
        }
    }
  12. Use SingleSectionEquatableStorage with a custom differ

    main

    SingleSectionEquatableStorage is designed for a single section and calculates UI updates using a provided differ. Since DTModelStorage does not provide a built-in differ, you must provide an adapter for your chosen differ (e.g., Changeset, Dwifft, or HeckelDiff).

    To set it up, initialize the storage with your items and a differ, then assign it to the manager's storage property.

    let storage = SingleSectionEquatableStorage(items: arrayOfPosts, differ: ChangesetDiffer())
    storage.setItems(startingItems)
    manager.storage = storage
    
    storage.addItems(newItems)