Skip to content

Latest commit

 

History

History
143 lines (102 loc) · 6.69 KB

Events.md

File metadata and controls

143 lines (102 loc) · 6.69 KB

Reacting to events

DTTableViewManager supports registering closures instead of delegate methods for 4 delegate protocols:

  • UITableViewDelegate
  • UITableViewDataSource(partially)
  • UITableViewDragDelegate
  • UITableViewDropDelegate

With those closures big improvement over delegate methods is that closures are actually strongly typed:

manager.register(PostCell.self) { mapping in
  mapping.didSelect { cell, model, indexPath in
    // cell is of type PostCell
    // model is of type Post
  }
}

Naming

With such huge amount of delegate methods it felt important to not invent new glossary of methods over existing delegate methods. Therefore, all event methods closely follow naming of delegate methods they replace, for example:

// func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)

mapping.willDisplay { cell, model, indexPath in }

// func tableView(_ tableView: UITableView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool

mapping.shouldBeginMultipleSelectionInteraction { cell, model, indexPath in true }

Different kinds of event closures

Whenever cell or reusable view delegate method is referring to can be provided, it will be, and closure accepts three arguments: (View, Model, IndexPath) -> ReturnType. However, for some delegate methods cell or view is not created yet. Such delegate methods are called with signature (Model, IndexPath) -> ReturnType. For example:

// `UITableViewDelegate.tableView(_:heightForRowAt:)`

mapping.heightForCell { model, indexPath in
  return 44
}

You can also register event closures on DTTableViewManager instance directly, but in this case you need to provide Cell/View type again:

  manager.register(PostCell.self)
  manager.didSelect(PostCell.self) { cell, model, indexPath in }

Keep in mind, that in order for this to work, cell needs to be registered prior to registering event closures, because they attach to specific mapping instance(s). Also, this kind of event registration only works for ModelTransfer compatible views.

This may be beneficial for example if you show same type of cell in different section with different appearances, but selection needs to work the same way. Registering event closure through DTTableViewManager attaches this event to all compatible mappings.

If delegate method is unrelated to cells or views, it is available on DTTableViewManager instance directly:

func tableView(_ tableView: UITableView, shouldUpdateFocusIn context: UITableViewFocusUpdateContext)
// `UITableViewDelegate.tableView(_:shouldUpdateFocusIn:)`
manager.shouldUpdateFocus { context in true }

What about performance?

With DTTableViewManager implementing ALL delegate methods, you may wonder about perfomance of all of this. Also you might be concerned, that there are some methods that you don't want to be implemented because they have perfomance implications. Great example of this is func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) method. When used for collections with large amounts of data, it may severely impact performance if implemented. It's much better to use estimated sizes instead.

Well, I'm happy to report that performance is absolutely not an issue with DTTableViewManager. If you don't register closure for a delegate method, UITableView will think that this delegate method is not implemented and will never call it.

Wait, how is that possible? It's actually pretty simple: DTTableViewManager uses dynamic method dispatch, and specifically responds(to selector:) method. If no event closures are registered, DTTableViewManager simply answers that this delegate method is not implemented, and UITableView will never call the implementation.

This means, that even though all delegate methods are implemented, for UITableView implemented methods - are methods you register closures with, which is exactly what you want.

Can I still use delegate methods?

Absolutely. Simply declare protocol conformance and implement method in your view controller:

class PostsViewController: UIViewController, DTTableViewManageable, UITableViewDelegate {
  override func viewDidLoad() {
    super.viewDidLoad()
    manager.register(PostCell.self)
    manager.memoryStorage.setItems(posts)
  }

  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // React to cell selection
  }
}

If your controller implements delegate method, DTTableViewManager detects it and attempts to redirect delegate method to you. It's important to understand priorities on which this is made, generally following happens:

  • Try to execute event, if cell and model type satisfy requirements
  • Try to call delegate or datasource method on DTTableViewManageable instance
  • If two previous scenarios fail, fallback to whatever default UITableView has for this delegate or datasource method

Retain cycles

Because event closures are stored on DTTableViewManager instance, referencing self in those closures will cause reference cycle, so make sure to capture self weakly in those closures:

manager.register(PostCell.self) { [weak self] mapping in
  mapping.didSelect { cell, model, indexPath in
    // self?.didSelect ...
  }
}

There is also one subtle case, that you need to watch for. When you capture self weakly, don't try to make it non-optional in mapping closure, because it will cause a retain cycle:

manager.register(PostCell.self) { [weak self] mapping in
  // Here self is captured weakly
  guard let self = self else { return }
  // Now self is captured strongly for closures below
  mapping.didSelect { cell, model, indexPath in
    // Retain cycle:
    // self.didSelect ...
  }
}

If you want to make self non-optional, you can check this in event closure itself:

manager.register(PostCell.self) { [weak self] mapping in
  // Here self is captured weakly
  mapping.didSelect { cell, model, indexPath in
    guard let self = self else { return }
    // For this closure self is captured weakly, no retain cycle.
    // self.didSelect ...
  }
}

Limitations

While it's possible to register multiple closures for a single event, only first closure will be called once event is fired. This means that if the same event has two closures for the same view/model type, last one will be ignored. You can still register multiple event handlers for a single event and different view/model types.

DTTableViewManager considers most of datasource methods to be handled by provided Storage, and therefore does not provide closure replacement for those. Some of the methods are available though, such as for moving cells, and prodiving index titles.