Reputation: 5601
I am not sure this can be done, or if it's even unrecommended.
What I am trying to achieve is the following:
I have a 2 classes classA
and classB
that have a reference to the same UITableview
instance. What I want is for classA
to take care of the implementation of the 2 required methods of the UITableViewDataSource
protocol:
numberOfRowsInSection
cellForRowAt
Then I want classB
to be able to implement the other optional methods like titleForHeaderInSection
for example.
So how can classA
have default implementation of some protocol methods, and let classB
be the a class that can build on top of what classB
has done?
In a way, the problem that I am facing is the following: How can multiple classes be the datasource of a single UITableView
?
EDIT:
classA
will be in a library that I am writing that takes care of building the core parts of the tableView. classB
will be used by the 3rd party dev to mainly customise its appearance.
Upvotes: 6
Views: 2400
Reputation: 23788
My answer consists of two parts. In first part I'd like to discuss your design decision and in second provide one more alternative solution using Obj-C magic.
Design considerations
It looks like you want ClassB
to not be able to override your default implementation.
First of all, in such case you probably should also implement
optional public func numberOfSections(in tableView: UITableView) -> Int
in your ClassA
for consistency or ClassB
will be able to return something else there without ability to return additional cells.
Actually this prohibitive behavior is what I don't like in such design. What if the user of your library wants to add more sections and cells to the same UITableView
? In this aspect design as described by Sulthan with ClassA
providing default implementation and ClassB
wrapping it to delegate and probably sometimes change the defaults seems preferable to me. I mean something like
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0) {
return libTableDataSource.tableView(tableView: tableView, numberOfRowsInSection: section)
}
else {
// custom logic for additional sections
}
}
Also such design has another advantage of not needing advanced Obj-C tricks to work in more complicated scenarios such as UITableViewDelegate
because you don't have to implement optional methods you don't want in either of ClassA
or ClassB
and still can add methods you (library's user) need into ClassB
.
Obj-C magic
Suppose that you still do want to make your default behavior to stand as the only possible choice for methods you've implemented but let customize other methods. Assume also that we are dealing with something like UITableView
which is designed in heavily Obj-C way i.e. heavily relies on optional methods in delegates and doesn't provide any simple way to call Apple's standard behavior (this is not true for UITableViewDataSource
but true for UITableViewDelegate
because who knows how to implement something like
optional public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
in backward and forward compatible way to match default Apple's style on every iOS).
So what's the solution? Using a bit of Obj-C magic we can create our class, that will have our default implementations for protocol methods we want such that if we provide to it another delegate that has some another optional methods implemented, our object will look like it has them too.
Attempt #1 (NSProxy)
First we start with a generic SOMulticastProxy
which is kind of proxy that delegates calls to two objects (see sources of helper SOOptionallyRetainHolder further).
SOMulticastProxy.h
@interface SOMulticastProxy : NSProxy
+ (id)proxyForProtocol:(Protocol *)targetProtocol firstDelegateR:(id <NSObject>)firstDelegate secondDelegateNR:(id <NSObject>)secondDelegate;
// This provides sensible defaults for retaining: typically firstDelegate will be created in
// place and thus should be retained while the second delegate most probably will be something
// like UIViewController and retaining it will retaining it will lead to memory leaks
+ (id)proxyForProtocol:(Protocol *)targetProtocol firstDelegate:(id <NSObject>)firstDelegate retainFirst:(BOOL)retainFirst
secondDelegate:(id <NSObject>)secondDelegate retainSecond:(BOOL)retainSecond;
@end
SOMulticastProxy.m
@interface SOMulticastProxy ()
@property(nonatomic) Protocol *targetProtocol;
@property(nonatomic) NSArray<SOOptionallyRetainHolder *> *delegates;
@end
@implementation SOMulticastProxy {
}
- (id)initWithProtocol:(Protocol *)targetProtocol firstDelegate:(id <NSObject>)firstDelegate retainFirst:(BOOL)retainFirst
secondDelegate:(id <NSObject>)secondDelegate retainSecond:(BOOL)retainSecond {
self.targetProtocol = targetProtocol;
self.delegates = @[[SOOptionallyRetainHolder holderWithTarget:firstDelegate retainTarget:retainFirst],
[SOOptionallyRetainHolder holderWithTarget:secondDelegate retainTarget:retainSecond]];
return self;
}
+ (id)proxyForProtocol:(Protocol *)targetProtocol firstDelegate:(id <NSObject>)firstDelegate retainFirst:(BOOL)retainFirst
secondDelegate:(id <NSObject>)secondDelegate retainSecond:(BOOL)retainSecond {
return [[self alloc] initWithProtocol:targetProtocol
firstDelegate:firstDelegate
retainFirst:retainFirst
secondDelegate:secondDelegate
retainSecond:retainSecond];
}
+ (id)proxyForProtocol:(Protocol *)targetProtocol firstDelegateR:(id <NSObject>)firstDelegate secondDelegateNR:(id <NSObject>)secondDelegate {
return [self proxyForProtocol:targetProtocol firstDelegate:firstDelegate retainFirst:YES
secondDelegate:secondDelegate retainSecond:NO];
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
if (self.targetProtocol == aProtocol)
return YES;
else
return NO;
}
- (NSObject *)findTargetForSelector:(SEL)aSelector {
for (SOOptionallyRetainHolder *holder in self.delegates) {
NSObject *del = holder.target;
if ([del respondsToSelector:aSelector])
return del;
}
return nil;
}
- (BOOL)respondsToSelector:(SEL)aSelector {
BOOL superRes = [super respondsToSelector:aSelector];
if (superRes)
return superRes;
NSObject *delegate = [self findTargetForSelector:aSelector];
return (delegate != nil);
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
NSObject *delegate = [self findTargetForSelector:sel];
if (delegate != nil)
return [delegate methodSignatureForSelector:sel];
else
return nil;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
NSObject *delegate = [self findTargetForSelector:invocation.selector];
if (delegate != nil)
[invocation invokeWithTarget:delegate];
else
[super forwardInvocation:invocation]; // which will effectively be [self doesNotRecognizeSelector:invocation.selector];
}
@end
SOMulticastProxy
is basically following: find first delegate that responds to required selector and forward call there. If neither of the delegates knows the selector - say that we don't know it. This is a more powerful than just automation of delegating all methods because SOMulticastProxy
effectively merge optional methods from both passed objects without a need to provide somewhere default implementations for each of them (optional methods).
Note that it is possible to make it conform to several protocols (UITableViewDelegate
+ UITableViewDataSource
) but I didn't bother.
Now with this magic we can just join two classes that both implement UITableViewDataSource
protocol and get an object you want. But I think that it makes sense to create more explicit protocol for second delegate to show that some methods will not be forwarded anyway.
@objc public protocol MyTableDataSource: NSObjectProtocol {
@objc optional func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
// copy here all the methods except the ones you've implemented
}
Now we can have our LibTableDataSource
as
class LibTableDataSource: NSObject, UIKit.UITableViewDataSource {
class func wrap(_ dataSource: MyTableDataSource) -> UITableViewDataSource {
let this = LibTableDataSource()
return SOMulticastProxy.proxy(for: UITableViewDataSource.self, firstDelegateR: this, secondDelegateNR: dataSource) as! UITableViewDataSource
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return your logic here
}
func numberOfSections(in tableView: UITableView) -> Int {
return your logic here
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return your logic here
}
}
Assuming externalTableDataSource
is an object of the library user's class that implements MyTableDataSource
protocol, usage is simply
let wrappedTableDataSource: UITableViewDataSource = LibTableDataSource.wrap(externalTableDataSource)
Here is the source for SOOptionallyRetainHolder helper class. SOOptionallyRetainHolder is a class that allows you to control wether object will be retained or not. This is useful because NSArray
by default retains its objects and in typical usage scenario you want to retain first delegate and not retain the second one (thanks Giuseppe Lanza for mentioning this aspect that I totally forgot about initially)
SOOptionallyRetainHolder.h
@interface SOOptionallyRetainHolder : NSObject
@property(nonatomic, readonly) id <NSObject> target;
+ (instancetype)holderWithTarget:(id <NSObject>)target retainTarget:(BOOL)retainTarget;
@end
SOOptionallyRetainHolder.m
@interface SOOptionallyRetainHolder ()
@property(nonatomic, readwrite) NSValue *targetNonRetained;
@property(nonatomic, readwrite) id <NSObject> targetRetained;
@end
@implementation SOOptionallyRetainHolder {
@private
}
- (id)initWithTarget:(id <NSObject>)target retainTarget:(BOOL)retainTarget {
if (!(self = [super init])) return self;
if (retainTarget)
self.targetRetained = target;
else
self.targetNonRetained = [NSValue valueWithNonretainedObject:target];
return self;
}
+ (instancetype)holderWithTarget:(id <NSObject>)target retainTarget:(BOOL)retainTarget {
return [[self alloc] initWithTarget:target retainTarget:retainTarget];
}
- (id <NSObject>)target {
return self.targetNonRetained != nil ? self.targetNonRetained.nonretainedObjectValue : self.targetRetained;
}
@end
Attempt #2 (inheritance from Obj-C class)
If having dangerous SOMulticastProxy
in your codebase looks a bit like an overkill, you can create more specialized base class SOTotallyInternalDelegatingBaseLibDataSource
:
SOTotallyInternalDelegatingBaseLibDataSource.h
@interface SOTotallyInternalDelegatingBaseLibDataSource : NSObject <UITableViewDataSource>
- (instancetype)initWithDelegate:(NSObject *)delegate;
@end
SOTotallyInternalDelegatingBaseLibDataSource.m
#import "SOTotallyInternalDelegatingBaseLibDataSource.h"
@interface SOTotallyInternalDelegatingBaseLibDataSource ()
@property(nonatomic) NSObject *delegate;
@end
@implementation SOTotallyInternalDelegatingBaseLibDataSource {
}
- (instancetype)initWithDelegate:(NSObject *)delegate {
if (!(self = [super init])) return self;
self.delegate = delegate;
return self;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
[self doesNotRecognizeSelector:_cmd];
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
[self doesNotRecognizeSelector:_cmd];
return nil;
}
#pragma mark -
- (BOOL)respondsToSelector:(SEL)aSelector {
BOOL superRes = [super respondsToSelector:aSelector];
if (superRes)
return superRes;
return [self.delegate respondsToSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
NSMethodSignature *superRes = [super methodSignatureForSelector:sel];
if (superRes != nil)
return superRes;
return [self.delegate methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.delegate];
}
@end
And then make your LibTableDataSource
almost the same as in Attempt #1
class LibTableDataSource: SOTotallyInternalDelegatingBaseLibDataSource {
class func wrap(_ dataSource: MyTableDataSource) -> UITableViewDataSource {
return LibTableDataSource2(delegate: dataSource as! NSObject)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return your logic here
}
func numberOfSections(in tableView: UITableView) -> Int {
return your logic here
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return your logic here
}
}
and the usage is absolutely identical to the one with Attempt #1. Also this solution is even easier to make implement two protocols (UITableViewDelegate
+ UITableViewDataSource
) at the same time.
A bit more on power of Obj-C magic
Actually you can use Obj-C magic to make MyTableDataSource
protocol different from UITableDataSource
in method names rather than copy-paste them and even change parameters such as not passing UITableView
at all or passing your custom object instead of UITableView
. I've done it once and it worked but I don't recommend doing it unless you have a very good reason to do it.
Upvotes: 3
Reputation: 3699
As someone else mentioned we need a proxy. To design a proxy that will be safe in terms of retain cycles it's fairly easy. To make it generic and flexible it is a totally different story. Everybody here knows that the delegate pattern requires the delegate object to be weak to avoid retain cycles (a retains b and b retains a so nobody is deallocated).
The immediate solution is to have N variables in your proxy that are weak of course so that you can forward to those objects the delegate calls
class MyProxy: NSObject, UITableViewDelegate {
weak var delegate1: UITableViewDelegate?
weak var delegate2: UITableViewDelegate?
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate1?.tableView?(tableView, didSelectRowAt: indexPath)
delegate2?.tableView?(tableView, didSelectRowAt: indexPath)
}
}
This of course will work. But it is not flexible at all. You can have just 2 delegates, and clearly if you want more you have to add delegate3 var, remember to update all your methods and so on.
Someone might think "Fine, let's have a delegates array"... Wrong. The array would retain the delegates that will no longer be weak and we will have a retain cycle.
The solution
To get things flexible I've created a weak collection. This code will allow you to have a collection of weak elements by using generics. You will be able to implement as many proxy as you want and these proxy can hold as many delegates you prefer.
public struct WeakContainer<T: NSObjectProtocol> {
public weak var delegate: T?
}
public struct WeakCollection<T: NSObjectProtocol> {
private var delegates: [WeakContainer<T>] = [WeakContainer<T>]()
public init(){}
public init(with delegate: T) {
add(object: delegate)
}
public mutating func add(object: T) {
let container = WeakContainer(delegate: object)
delegates.append(container)
}
public mutating func remove(object: T) {
guard let index = delegates.index(where: {
return object.isEqual($0.delegate)
}) else { return }
delegates.remove(at: index)
}
public mutating func execute(_ closure: ((_ object: T) throws -> Void)) rethrows {
let localDelegates = delegates
try localDelegates.forEach { (weakContainer) in
guard let delegate = weakContainer.delegate else {
cleanup()
return
}
try closure(delegate)
}
}
private mutating func cleanup() {
delegates.sort { (a, b) -> Bool in
return a.delegate == nil
}
while let first = delegates.first, first.delegate == nil {
delegates.removeFirst()
}
}
}
This will allow you to do something like this:
public class TableViewDelegateProxy: NSObject, UITableViewDelegate {
var delegates = WeakCollection<UITableViewDelegate>()
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegates.execute { (delegate) in
delegate.tableView?(tableView, didSelectRowAt: indexPath)
}
}
}
As you can see, these few lines will be safe, as the weakCollection will store a weak reference to the delegates, it will cleanup herself when released delegates are found and it can contain objects of a protocol to be super flexible and bend to your needs.
Upvotes: -1
Reputation: 178
I think the best way to do this is to subclass UIViewController in your ClassA and implement UITableViewDataSource.
To prevent calling of required methods implemented in ClassA, just put final
keyword in the func
implemetation.
Here is my solution:
ClassA
import UIKit
class ClassA: UIViewController, UITableViewDataSource {
// MARK: - Table view data source
final func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
final func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Cell \(indexPath.row) in section \(indexPath.section)"
return cell
}
}
ClassB
import UIKit
class ClassB: ClassA {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "Header \(section)"
}
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return "Footer \(section)"
}
}
Here is what you get:
Upvotes: 1
Reputation: 3355
You can do you things like this. People who write class B
uses extension A
to added UITableViewDataSource
functions.
// file A.swift
class A:NSObject, UITableViewDataSource {
var b:B! = nil
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
return cell
}
}
protocol SectionNameProtocol {
var sectionName:[String] { get set }
}
// file B.swift
class B:SectionNameProtocol {
unowned var a:A
var sectionName: [String] = []
init(a:A) {
self.a = a
a.b = self
}
}
extension A {
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return b.sectionName[section]
}
}
Upvotes: 0
Reputation: 130132
I think that the only solution without manually redirecting everything is to use the default implementation of protocol methods, e.g. :
protocol MyTableViewProtocol : UITableViewDelegate, UITableViewDataSource {
}
extension MyTableViewProtocol {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
}
And then make ClassB
to implement MyTableViewProtocol
instead of UITableViewDelegate
and UITableViewDataSource
.
However, such a solution will not work because protocol extensions are not accessible by Obj-C.
I think a cleaner (and working) solution would be to create the implementation of numberOfRowsInSection
and cellForRowAt
outside the protocol and just let ClassB
to call them inside the delegate method, e.g.:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return MyTable.tableView(tableView: tableView, numberOfRowsInSection: section)
}
Such a solution will be clearer for the user because it will contain less "magic".
Of course, the classic solution is to define your own delegate:
protocol MyTableViewProtocol {
func myTableView(_ tableView: MyTableView, ...)
...
}
and redirect everything to it from your delegate.
This solution makes it impossible for ClassB
to overwrite delegate function you don't want it to overwrite.
Upvotes: 5