My answer is in two parts. In the first part, I would like to discuss your design decision, and in the second, another alternative solution using Obj-C magic.
Design Considerations
It looks like you want ClassB not be able to override your default implementation.
First of all, in this 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 without the possibility of returning additional cells.
Actually this prohibitive behavior is something that I don't like about this design. What if your library user wants to add more sections and cells to the same UITableView ? In this aspect, the design described by Sulthan with ClassA , which provides a default implementation, and ClassB , which wraps it for delegation and, possibly, sometimes changing the default settings, seems to me preferable. I mean something like
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if (section == 0) { return libTableDataSource.tableView(tableView: tableView, numberOfRowsInSection: section) } else {
In addition, this design has another advantage: Obj-C advanced tricks are not needed to work in more complex scripts, such as UITableViewDelegate , because you do not need to implement optional methods that you do not want in ClassA or ClassB and can still add methods that you (the library user) need in ClassB .
Magic Obj-C
Suppose you still want your default behavior to be the only possible choice for the methods you implemented, but you can configure other methods. Suppose also that we are dealing with something like a UITableView that is developed using the Obj-C method, i.e. It relies heavily on optional methods in delegates and does not provide an easy way to name 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 reverse and advanced mode to meet Appleโs default standard on every iOS).
So what is the solution? Using a bit of Obj-C magic, we can create our class that will have our default implementations for the protocol methods that we want so that if we provide it with another delegate with a few additional methods, our object will look like he has them too.
Attempt # 1 (NSProxy)
First, we start with the generic SOMulticastProxy , which is a kind of proxy server that delegates calls to two objects (see SOOptionallyRetainHolder helper sources below).
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 this: find the first delegate that responds to the required selector and redirects there. If none of the delegates knows the selector, say that we do not know this. This is more powerful than just automating the delegation of all methods, because SOMulticastProxy efficiently combines optional methods from both passed objects without having to provide implementations for each of them by default somewhere (optional methods).
Note that this can be done according to several protocols ( UITableViewDelegate + UITableViewDataSource ), but I was not worried.
Now, with the help of this magic, we can simply join two classes that implement the UITableViewDataSource protocol and get the desired object. But I think it makes sense to create a more explicit protocol for the second delegate to show that some methods will not be redirected anyway.
@objc public protocol MyTableDataSource: NSObjectProtocol { @objc optional func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
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 a custom library class that implements the MyTableDataSource protocol, using is simple
let wrappedTableDataSource: UITableViewDataSource = LibTableDataSource.wrap(externalTableDataSource)
Here is the source of the SOOptionallyRetainHolder helper class. SOOptionallyRetainHolder is a class that allows you to control whether an object will be saved or not. This is useful because NSArray saves its objects by default, and in a typical use case you want to save the first delegate and not save the second (thanks to Giuseppe Lance for mentioning this aspect, which I completely forgot 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 (Obj-C class inheritance)
If the dangerous SOMulticastProxy in your code base looks a bit overkill, you can create a 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 use is absolutely identical to use with attempt No. 1. In addition, this solution is even easier to implement two protocols ( UITableViewDelegate + UITableViewDataSource ) at the same time.
A little more about the power of magic Obj-C
In fact, you can use Obj-C magic to make the MyTableDataSource protocol different from UITableDataSource in method names, rather than copying them and even changing parameters, for example, not passing a UITableView at all or passing your own object instead of from a UITableView . I did it once, and it worked, but I do not recommend doing it unless you have a good reason for this.