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.