Guy Daher
Guy Daher

Reputation: 5601

Implementing UITableViewDataSource protocol in two different classes

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:

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

Answers (5)

SergGr
SergGr

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

Giuseppe Lanza
Giuseppe Lanza

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

Shkelzen
Shkelzen

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:

enter image description here

Upvotes: 1

Owen Zhao
Owen Zhao

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

Sulthan
Sulthan

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

Related Questions