Implementing the UITableViewDataSource Protocol in Two Different Classes

I am not sure if this can be done, or if it will not even be canceled.

What I'm trying to achieve is the following:

I have 2 classes classA and classB that have a reference to the same instance of UITableview . I want classA take care of implementing the 2 necessary methods of the UITableViewDataSource protocol:

  • numberOfRowsInSection
  • cellForRowAt

Then I want classB to implement other optional methods, for example titleForHeaderInSection .

So, how classA implement some protocol methods by default, and let classB be a class that can be built on top of what classB did?

In a sense, the problem I am facing is this: how can multiple classes be the data source of a single UITableview ?

EDIT: classA will be in the library I am writing that takes care of creating the main parts of the tableView. classB will be used by a third-party developer to basically customize its appearance.

+5
source share
5 answers

I think that the only solution without manually redirecting everything is to use the default implementation of protocol methods, for example.

 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 available to Obj-C.

I think a cleaner (and working) solution would be to create an implementation of numberOfRowsInSection and cellForRowAt outside the protocol and just let ClassB call them inside the delegate method, for example:

 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return MyTable.tableView(tableView: tableView, numberOfRowsInSection: section) } 

Such a solution will become more understandable 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 to overwrite the delegate's ClassB function that you do not want to overwrite.

+3
source

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 { // custom logic for additional sections } } 

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? // 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 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.

+3
source

You can do it like this. People who write class B use extension A to add 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 ab = self } } extension A { func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return b.sectionName[section] } } 
0
source

I think the best way to do this is to subclass the UIViewController in your classA and implement the UITableViewDataSource. To prevent calling required methods implemented in ClassA, simply put the final keyword in the func implementation.

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

0
source

As already mentioned, we need a proxy. To create a proxy server that is safe from the point of view of saving cycles, it will be quite simple. To make it universal and flexible, this is a completely different story. Everyone here knows that the delegate template requires the delegate object to be weak in order to avoid save loops (a saves b and b saves so that no one is freed).

The immediate solution is to have N variables in your proxy that are, of course, weak, so you can forward the objects that 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) } } 

That, of course, will work. But this is not at all flexible. You can only have 2 delegates, and if you need more, you need to add delegate 3 var, remember to update all your methods, etc.

Someone might think, โ€œWell, let there be an array of delegatesโ€ ... Wrong. The array will save the delegates who will no longer be weak, and we will have a save cycle.

Decision

To make things flexible, I created a weak collection. This code will allow you to have a collection of weak elements using generics. You can implement as many proxies as you want, and this proxy server can contain as many delegates as 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, since weakCollection will keep a weak reference to delegates, it will clear when freed delegates are detected, and may contain protocol objects that need to be super flexible and bend to your needs.

-1
source

Source: https://habr.com/ru/post/1264086/


All Articles