在两个不同的 类 中实现 UITableViewDataSource 协议
Implementing UITableViewDataSource protocol in two different classes
我不确定是否可以这样做,或者是否不推荐这样做。
我要实现的目标如下:
我有 2 个 classes classA
和 classB
引用同一个 UITableview
实例。我想要的是 classA
负责 UITableViewDataSource
协议的 2 个必需方法的实现:
numberOfRowsInSection
cellForRowAt
然后我希望 classB
能够实现其他可选方法,例如 titleForHeaderInSection
。
那么 classA
如何能够默认实现某些协议方法,并让 classB
成为可以构建在 classB
已经完成的之上的 class ?
在某种程度上,我面临的问题如下:多个class如何成为单个UITableView
的数据源?
编辑:
classA
将在我正在编写的库中,该库负责构建 tableView 的核心部分。 classB
将被第 3 方开发人员使用,主要用于自定义其外观。
我认为无需手动重定向所有内容的唯一解决方案是使用协议方法的默认实现,例如:
protocol MyTableViewProtocol : UITableViewDelegate, UITableViewDataSource {
}
extension MyTableViewProtocol {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
}
然后使 ClassB
实现 MyTableViewProtocol
而不是 UITableViewDelegate
和 UITableViewDataSource
。
但是,这样的解决方案将不起作用,因为 Obj-C 无法访问协议扩展。
我认为更简洁(和有效)的解决方案是在协议之外创建 numberOfRowsInSection
和 cellForRowAt
的实现,然后让 ClassB
在委托方法中调用它们, 例如:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return MyTable.tableView(tableView: tableView, numberOfRowsInSection: section)
}
这样的解决方案对用户来说会更清晰,因为它包含的内容更少"magic"。
当然,经典的解决方案是定义自己的委托:
protocol MyTableViewProtocol {
func myTableView(_ tableView: MyTableView, ...)
...
}
并将所有内容从您的委托重定向到它。
此解决方案使 ClassB
无法覆盖您不想覆盖的委托函数。
你可以这样做。写 class B
的人使用 extension A
来添加 UITableViewDataSource
功能。
// 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]
}
}
我的回答包括两部分。在第一部分中,我想讨论您的设计决策,在第二部分中,我想提供一个使用 Obj-C 魔法的替代解决方案。
设计注意事项
您似乎希望 ClassB
无法覆盖您的默认实现。
首先,在这种情况下,您可能还应该实施
optional public func numberOfSections(in tableView: UITableView) -> Int
在您的 ClassA
中保持一致性或 ClassB
将能够 return 其他内容而无法 return 其他单元格。
实际上这种禁止行为是我不喜欢这样的设计。如果您图书馆的用户想要向同一个 UITableView
添加更多部分和单元格怎么办?在这方面的设计中,如 Sulthan 所述,ClassA
提供默认实现,ClassB
将其包装为委托,有时可能会更改默认值,这对我来说似乎更可取。我的意思是
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0) {
return libTableDataSource.tableView(tableView: tableView, numberOfRowsInSection: section)
}
else {
// custom logic for additional sections
}
}
此外,这种设计还有另一个优点,即无需高级 Obj-C 技巧即可在更复杂的场景(例如 UITableViewDelegate
中工作,因为您不必在任何一个场景中实现您不想要的可选方法ClassA
或 ClassB
并且仍然可以将您(图书馆的用户)需要的方法添加到 ClassB
.
Obj-C 魔法
假设您仍然希望将默认行为作为已实现方法的唯一可能选择,但让自定义其他方法。还假设我们正在处理类似 UITableView
的东西,它是以大量 Obj-C 方式设计的,即严重依赖委托中的可选方法,并且不提供任何简单的方法来调用 Apple 的标准行为(这不适用于UITableViewDataSource
但对于 UITableViewDelegate
是正确的,因为谁知道如何实现
optional public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
以向后和向前兼容的方式在每个 iOS) 上匹配默认的 Apple 样式。
那么解决方法是什么?使用一点 Obj-C 魔法,我们可以创建我们的 class,它将具有我们想要的协议方法的默认实现,这样如果我们向它提供另一个实现了一些其他可选方法的委托,我们的对象将看起来好像也有。
尝试#1 (NSProxy)
首先,我们从一个通用的 SOMulticastProxy
开始,它是一种将调用委托给两个对象的代理(进一步参见助手 SOOptionallyRetainHolder 的来源)。
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
基本上如下:找到第一个响应所需选择器的委托并在那里转发调用。如果两个代表都不知道选择器 - 就说我们不知道。这比委托所有方法的自动化更强大,因为 SOMulticastProxy
有效地合并来自两个传递对象的可选方法,而不需要为每个方法(可选方法)提供默认实现。
请注意,可以使其符合多种协议 (UITableViewDelegate
+ UITableViewDataSource
),但我没有费心。
现在有了这个魔法,我们可以加入两个都实现 UITableViewDataSource
协议的 classes 并获得你想要的对象。但我认为为第二个委托创建更明确的协议以表明某些方法无论如何都不会被转发是有意义的。
@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
}
现在我们可以将 LibTableDataSource
作为
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
}
}
假设externalTableDataSource
是实现MyTableDataSource
协议的库用户class的一个对象,用法很简单
let wrappedTableDataSource: UITableViewDataSource = LibTableDataSource.wrap(externalTableDataSource)
这是 SOOptionallyRetainHolder 助手 class 的源代码。 SOOptionallyRetainHolder 是一个 class 允许你控制对象是否被保留。这很有用,因为 NSArray
默认情况下会保留其对象,并且在典型的使用场景中,您希望保留第一个委托而不保留第二个委托(感谢 Giuseppe Lanza 提到我最初完全忘记的这个方面)
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
尝试 #2(继承自 Obj-C class)
如果你的代码库中有危险的 SOMulticastProxy
看起来有点矫枉过正,你可以创建更专业的基础 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
然后让你的 LibTableDataSource
与尝试 #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
}
}
并且用法与尝试 #1 的用法完全相同。此外,此解决方案更容易同时实现两个协议(UITableViewDelegate
+ UITableViewDataSource
)。
更多关于 Obj-C 魔法的力量
实际上你可以使用 Obj-C 魔法使 MyTableDataSource
协议在方法名称中不同于 UITableDataSource
而不是复制粘贴它们甚至更改参数,例如不传递 UITableView
完全或传递您的自定义对象而不是 UITableView
。我已经做过一次并且很有效,但我不建议这样做,除非你有充分的理由这样做。
我认为最好的方法是在您的 ClassA 中子类化 UIViewController 并实现 UITableViewDataSource。
为了防止调用在 ClassA 中实现的所需方法,只需将 final
关键字放在 func
实现中。
这是我的解决方案:
A 类
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)"
}
}
这是你得到的:
正如其他人提到的,我们需要一个代理。设计一个在保留周期方面是安全的代理是相当容易的。要使其通用和灵活,这是一个完全不同的故事。
这里的每个人都知道委托模式要求委托对象是弱的以避免保留循环(a 保留 b 和 b 保留 a 所以没有人被释放)。
直接的解决方案是在你的代理中有 N 个弱变量,当然这样你就可以转发给委托调用的那些对象
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)
}
}
这当然行得通。但它一点也不灵活。你可以只有 2 个代表,如果你想要更多,你必须添加 delegate3 var,记得更新你所有的方法等等。
有人可能会认为"Fine, let's have a delegates array"……错了。该数组将保留不再弱的委托,我们将有一个保留周期。
解决方法
为了让事情变得灵活,我创建了一个 weak 集合。
此代码将允许您通过使用泛型来拥有一组弱元素。您将能够实现任意数量的代理,并且这些代理可以容纳您喜欢的任意数量的委托。
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([=11=].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()
}
}
}
这将允许您执行如下操作:
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)
}
}
}
如您所见,这几行是安全的,因为 weakCollection 将存储对委托的弱引用,当找到已释放的委托时它会自行清理,并且它可以包含协议的对象非常灵活并满足您的需求。
我不确定是否可以这样做,或者是否不推荐这样做。
我要实现的目标如下:
我有 2 个 classes classA
和 classB
引用同一个 UITableview
实例。我想要的是 classA
负责 UITableViewDataSource
协议的 2 个必需方法的实现:
numberOfRowsInSection
cellForRowAt
然后我希望 classB
能够实现其他可选方法,例如 titleForHeaderInSection
。
那么 classA
如何能够默认实现某些协议方法,并让 classB
成为可以构建在 classB
已经完成的之上的 class ?
在某种程度上,我面临的问题如下:多个class如何成为单个UITableView
的数据源?
编辑:
classA
将在我正在编写的库中,该库负责构建 tableView 的核心部分。 classB
将被第 3 方开发人员使用,主要用于自定义其外观。
我认为无需手动重定向所有内容的唯一解决方案是使用协议方法的默认实现,例如:
protocol MyTableViewProtocol : UITableViewDelegate, UITableViewDataSource {
}
extension MyTableViewProtocol {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
}
然后使 ClassB
实现 MyTableViewProtocol
而不是 UITableViewDelegate
和 UITableViewDataSource
。
但是,这样的解决方案将不起作用,因为 Obj-C 无法访问协议扩展。
我认为更简洁(和有效)的解决方案是在协议之外创建 numberOfRowsInSection
和 cellForRowAt
的实现,然后让 ClassB
在委托方法中调用它们, 例如:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return MyTable.tableView(tableView: tableView, numberOfRowsInSection: section)
}
这样的解决方案对用户来说会更清晰,因为它包含的内容更少"magic"。
当然,经典的解决方案是定义自己的委托:
protocol MyTableViewProtocol {
func myTableView(_ tableView: MyTableView, ...)
...
}
并将所有内容从您的委托重定向到它。
此解决方案使 ClassB
无法覆盖您不想覆盖的委托函数。
你可以这样做。写 class B
的人使用 extension A
来添加 UITableViewDataSource
功能。
// 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]
}
}
我的回答包括两部分。在第一部分中,我想讨论您的设计决策,在第二部分中,我想提供一个使用 Obj-C 魔法的替代解决方案。
设计注意事项
您似乎希望 ClassB
无法覆盖您的默认实现。
首先,在这种情况下,您可能还应该实施
optional public func numberOfSections(in tableView: UITableView) -> Int
在您的 ClassA
中保持一致性或 ClassB
将能够 return 其他内容而无法 return 其他单元格。
实际上这种禁止行为是我不喜欢这样的设计。如果您图书馆的用户想要向同一个 UITableView
添加更多部分和单元格怎么办?在这方面的设计中,如 Sulthan 所述,ClassA
提供默认实现,ClassB
将其包装为委托,有时可能会更改默认值,这对我来说似乎更可取。我的意思是
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0) {
return libTableDataSource.tableView(tableView: tableView, numberOfRowsInSection: section)
}
else {
// custom logic for additional sections
}
}
此外,这种设计还有另一个优点,即无需高级 Obj-C 技巧即可在更复杂的场景(例如 UITableViewDelegate
中工作,因为您不必在任何一个场景中实现您不想要的可选方法ClassA
或 ClassB
并且仍然可以将您(图书馆的用户)需要的方法添加到 ClassB
.
Obj-C 魔法
假设您仍然希望将默认行为作为已实现方法的唯一可能选择,但让自定义其他方法。还假设我们正在处理类似 UITableView
的东西,它是以大量 Obj-C 方式设计的,即严重依赖委托中的可选方法,并且不提供任何简单的方法来调用 Apple 的标准行为(这不适用于UITableViewDataSource
但对于 UITableViewDelegate
是正确的,因为谁知道如何实现
optional public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
以向后和向前兼容的方式在每个 iOS) 上匹配默认的 Apple 样式。
那么解决方法是什么?使用一点 Obj-C 魔法,我们可以创建我们的 class,它将具有我们想要的协议方法的默认实现,这样如果我们向它提供另一个实现了一些其他可选方法的委托,我们的对象将看起来好像也有。
尝试#1 (NSProxy)
首先,我们从一个通用的 SOMulticastProxy
开始,它是一种将调用委托给两个对象的代理(进一步参见助手 SOOptionallyRetainHolder 的来源)。
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
基本上如下:找到第一个响应所需选择器的委托并在那里转发调用。如果两个代表都不知道选择器 - 就说我们不知道。这比委托所有方法的自动化更强大,因为 SOMulticastProxy
有效地合并来自两个传递对象的可选方法,而不需要为每个方法(可选方法)提供默认实现。
请注意,可以使其符合多种协议 (UITableViewDelegate
+ UITableViewDataSource
),但我没有费心。
现在有了这个魔法,我们可以加入两个都实现 UITableViewDataSource
协议的 classes 并获得你想要的对象。但我认为为第二个委托创建更明确的协议以表明某些方法无论如何都不会被转发是有意义的。
@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
}
现在我们可以将 LibTableDataSource
作为
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
}
}
假设externalTableDataSource
是实现MyTableDataSource
协议的库用户class的一个对象,用法很简单
let wrappedTableDataSource: UITableViewDataSource = LibTableDataSource.wrap(externalTableDataSource)
这是 SOOptionallyRetainHolder 助手 class 的源代码。 SOOptionallyRetainHolder 是一个 class 允许你控制对象是否被保留。这很有用,因为 NSArray
默认情况下会保留其对象,并且在典型的使用场景中,您希望保留第一个委托而不保留第二个委托(感谢 Giuseppe Lanza 提到我最初完全忘记的这个方面)
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
尝试 #2(继承自 Obj-C class)
如果你的代码库中有危险的 SOMulticastProxy
看起来有点矫枉过正,你可以创建更专业的基础 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
然后让你的 LibTableDataSource
与尝试 #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
}
}
并且用法与尝试 #1 的用法完全相同。此外,此解决方案更容易同时实现两个协议(UITableViewDelegate
+ UITableViewDataSource
)。
更多关于 Obj-C 魔法的力量
实际上你可以使用 Obj-C 魔法使 MyTableDataSource
协议在方法名称中不同于 UITableDataSource
而不是复制粘贴它们甚至更改参数,例如不传递 UITableView
完全或传递您的自定义对象而不是 UITableView
。我已经做过一次并且很有效,但我不建议这样做,除非你有充分的理由这样做。
我认为最好的方法是在您的 ClassA 中子类化 UIViewController 并实现 UITableViewDataSource。
为了防止调用在 ClassA 中实现的所需方法,只需将 final
关键字放在 func
实现中。
这是我的解决方案:
A 类
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)"
}
}
这是你得到的:
正如其他人提到的,我们需要一个代理。设计一个在保留周期方面是安全的代理是相当容易的。要使其通用和灵活,这是一个完全不同的故事。 这里的每个人都知道委托模式要求委托对象是弱的以避免保留循环(a 保留 b 和 b 保留 a 所以没有人被释放)。
直接的解决方案是在你的代理中有 N 个弱变量,当然这样你就可以转发给委托调用的那些对象
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)
}
}
这当然行得通。但它一点也不灵活。你可以只有 2 个代表,如果你想要更多,你必须添加 delegate3 var,记得更新你所有的方法等等。
有人可能会认为"Fine, let's have a delegates array"……错了。该数组将保留不再弱的委托,我们将有一个保留周期。
解决方法
为了让事情变得灵活,我创建了一个 weak 集合。 此代码将允许您通过使用泛型来拥有一组弱元素。您将能够实现任意数量的代理,并且这些代理可以容纳您喜欢的任意数量的委托。
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([=11=].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()
}
}
}
这将允许您执行如下操作:
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)
}
}
}
如您所见,这几行是安全的,因为 weakCollection 将存储对委托的弱引用,当找到已释放的委托时它会自行清理,并且它可以包含协议的对象非常灵活并满足您的需求。