Objective-C协议与委托模式实战
一、Objective-C 协议基础
1.1 协议的定义
在Objective - C中,协议(Protocol)是一种特殊的接口,它定义了一组方法的声明,但并不提供这些方法的实现。协议允许一个类声明它实现了某些方法,即使这些方法的实际实现是在其他类中。协议的定义使用@protocol
关键字,语法如下:
@protocol ProtocolName <NSObject>
// 方法声明
- (void)method1;
- (void)method2:(NSString *)parameter;
@end
在上述代码中,ProtocolName
是协议的名称,<NSObject>
表示该协议继承自NSObject
协议。NSObject
协议定义了一些基础的方法,几乎所有的Objective - C类都遵循NSObject
协议。在协议定义的大括号内,我们声明了两个方法method1
和method2
。
1.2 协议的遵循
一个类要遵循某个协议,需要在类的定义中列出该协议。例如:
@interface MyClass : NSObject <ProtocolName>
// 类的其他属性和方法声明
@end
当一个类遵循某个协议时,理论上它需要实现该协议中声明的所有方法(除非协议中的方法被标记为可选的,后面会详细介绍)。如果一个类没有实现协议中的所有必须方法,编译器会发出警告。
1.3 必须方法和可选方法
在协议中,方法可以分为必须方法和可选方法。默认情况下,协议中声明的方法都是必须方法。如果一个类遵循了包含必须方法的协议,那么它必须实现这些方法。
要声明一个可选方法,需要在方法声明前加上@optional
关键字,如下:
@protocol AnotherProtocol <NSObject>
- (void)requiredMethod;
@optional
- (void)optionalMethod;
@end
在上述协议AnotherProtocol
中,requiredMethod
是必须方法,而optionalMethod
是可选方法。一个遵循AnotherProtocol
的类必须实现requiredMethod
,但可以选择是否实现optionalMethod
。
二、委托模式概述
2.1 委托模式的概念
委托模式(Delegation Pattern)是一种设计模式,它允许一个对象(委托者,delegate)将部分职责委托给另一个对象(代理者,delegatee)。在Objective - C中,委托模式通常通过协议来实现。
委托模式的核心思想是将一些功能的实现从一个对象转移到另一个对象,使得对象之间的耦合度降低,提高代码的可维护性和可扩展性。例如,一个视图控制器(ViewController)可能将处理用户输入的任务委托给另一个专门的类,而不是在视图控制器内部处理所有的逻辑。
2.2 委托模式的优势
- 解耦:委托模式将不同的功能分离到不同的对象中,使得对象之间的依赖关系更加清晰。例如,一个视图类不需要知道具体的业务逻辑如何处理用户输入,它只需要将这个任务委托给一个合适的对象即可。
- 可扩展性:当需要修改或添加功能时,可以通过修改或添加代理对象来实现,而不需要修改委托者的代码。比如,如果有新的业务逻辑需要处理用户输入,只需要创建一个新的代理对象并将其设置为委托者的代理,而不需要在视图类中添加新的代码。
- 代码复用:代理对象可以被多个委托者复用。例如,一个处理用户登录逻辑的代理对象可以被多个不同的视图控制器复用,只要这些视图控制器需要处理用户登录相关的操作。
三、Objective - C 中委托模式的实现
3.1 定义委托协议
在实现委托模式时,首先需要定义一个委托协议。这个协议定义了代理对象需要实现的方法。例如,假设我们有一个简单的按钮点击事件处理场景,我们可以定义如下协议:
@protocol ButtonDelegate <NSObject>
- (void)buttonDidClick:(UIButton *)button;
@end
在上述协议中,buttonDidClick:
方法是当按钮被点击时代理对象需要实现的方法。该方法接受一个UIButton
类型的参数,以便代理对象知道是哪个按钮被点击了。
3.2 委托者类
委托者类是持有代理对象引用并在适当的时候调用代理对象方法的类。在Objective - C中,通常在委托者类中定义一个属性来存储代理对象。例如,我们创建一个简单的MyButton
类作为委托者:
#import <UIKit/UIKit.h>
@protocol ButtonDelegate <NSObject>
- (void)buttonDidClick:(UIButton *)button;
@end
@interface MyButton : UIButton
@property (nonatomic, weak) id<ButtonDelegate> delegate;
@end
@implementation MyButton
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
if ([self.delegate respondsToSelector:@selector(buttonDidClick:)]) {
[self.delegate buttonDidClick:self];
}
}
@end
在MyButton
类中,我们定义了一个delegate
属性,类型为id<ButtonDelegate>
,表示它可以接受任何遵循ButtonDelegate
协议的对象。在touchesEnded:withEvent:
方法中,当按钮被点击时,我们首先调用父类的touchesEnded:withEvent:
方法,然后检查代理对象是否实现了buttonDidClick:
方法。如果实现了,就调用代理对象的该方法,并将自身(即被点击的按钮)作为参数传递过去。
3.3 代理对象
代理对象是实现委托协议中方法的对象。例如,我们创建一个视图控制器作为代理对象:
#import "ViewController.h"
#import "MyButton.h"
@interface ViewController () <ButtonDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
MyButton *myButton = [[MyButton alloc] initWithFrame:CGRectMake(100, 100, 200, 50)];
myButton.delegate = self;
[myButton setTitle:@"点击我" forState:UIControlStateNormal];
[self.view addSubview:myButton];
}
- (void)buttonDidClick:(UIButton *)button {
NSLog(@"按钮被点击了");
}
@end
在视图控制器ViewController
中,我们遵循了ButtonDelegate
协议。在viewDidLoad
方法中,我们创建了一个MyButton
对象,并将视图控制器自身设置为MyButton
的代理。当按钮被点击时,ViewController
的buttonDidClick:
方法会被调用,从而在控制台输出“按钮被点击了”。
四、深入委托模式
4.1 多重委托
在某些情况下,一个委托者可能需要将不同的任务委托给多个代理对象,这就是多重委托。虽然Objective - C本身没有直接支持多重委托的语法,但我们可以通过一些变通的方法来实现。
一种常见的方法是使用数组来存储多个代理对象,并在需要调用代理方法时遍历数组依次调用。例如,我们修改前面的MyButton
类来支持多重委托:
#import <UIKit/UIKit.h>
@protocol ButtonDelegate <NSObject>
- (void)buttonDidClick:(UIButton *)button;
@end
@interface MyButton : UIButton
@property (nonatomic, strong) NSMutableArray<id<ButtonDelegate>> *delegates;
@end
@implementation MyButton
- (instancetype)init {
self = [super init];
if (self) {
self.delegates = [NSMutableArray array];
}
return self;
}
- (void)addDelegate:(id<ButtonDelegate>)delegate {
[self.delegates addObject:delegate];
}
- (void)removeDelegate:(id<ButtonDelegate>)delegate {
[self.delegates removeObject:delegate];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
for (id<ButtonDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(buttonDidClick:)]) {
[delegate buttonDidClick:self];
}
}
}
@end
然后在视图控制器中使用多重委托:
#import "ViewController.h"
#import "MyButton.h"
@interface AnotherDelegate : NSObject <ButtonDelegate>
@end
@implementation AnotherDelegate
- (void)buttonDidClick:(UIButton *)button {
NSLog(@"另一个代理对象处理按钮点击");
}
@end
@interface ViewController () <ButtonDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
MyButton *myButton = [[MyButton alloc] initWithFrame:CGRectMake(100, 100, 200, 50)];
[myButton addDelegate:self];
AnotherDelegate *anotherDelegate = [[AnotherDelegate alloc] init];
[myButton addDelegate:anotherDelegate];
[myButton setTitle:@"点击我" forState:UIControlStateNormal];
[self.view addSubview:myButton];
}
- (void)buttonDidClick:(UIButton *)button {
NSLog(@"视图控制器处理按钮点击");
}
@end
在上述代码中,MyButton
类使用一个NSMutableArray
来存储多个代理对象。addDelegate:
和removeDelegate:
方法用于添加和移除代理对象。当按钮被点击时,会遍历数组依次调用每个代理对象的buttonDidClick:
方法。
4.2 协议继承
协议可以继承自其他协议,就像类可以继承自其他类一样。通过协议继承,一个协议可以获得父协议中声明的所有方法。例如:
@protocol BaseProtocol <NSObject>
- (void)baseMethod;
@end
@protocol SubProtocol <BaseProtocol>
- (void)subMethod;
@end
在上述代码中,SubProtocol
继承自BaseProtocol
。任何遵循SubProtocol
的类不仅需要实现subMethod
,还需要实现baseMethod
。
协议继承在委托模式中也非常有用。例如,我们可能有一个基础的委托协议,然后在不同的场景下有一些继承自该基础协议的子协议,以满足更具体的需求。
4.3 协议的类型检查
在Objective - C中,我们可以使用respondsToSelector:
方法来检查一个对象是否实现了某个协议中的方法。此外,还可以使用conformsToProtocol:
方法来检查一个对象是否遵循某个协议。例如:
id someObject = [[SomeClass alloc] init];
if ([someObject conformsToProtocol:@protocol(SomeProtocol)]) {
NSLog(@"该对象遵循SomeProtocol协议");
if ([someObject respondsToSelector:@selector(someMethodInProtocol)]) {
[someObject someMethodInProtocol];
}
}
在上述代码中,首先使用conformsToProtocol:
方法检查someObject
是否遵循SomeProtocol
协议。如果遵循,再使用respondsToSelector:
方法检查是否实现了someMethodInProtocol
方法,如果实现了则调用该方法。
五、委托模式在iOS开发中的应用场景
5.1 UITableViewDelegate 和 UITableViewDataSource
在iOS开发中,UITableView
是一个常用的视图控件,用于显示列表数据。UITableView
使用委托模式来处理各种与表格显示和交互相关的任务。
UITableViewDelegate
协议定义了一些方法,用于处理表格的行高、选中行的处理等。例如:
@interface ViewController () <UITableViewDelegate>
@end
@implementation ViewController
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 50;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(@"选中了第 %ld 行", (long)indexPath.row);
}
@end
UITableViewDataSource
协议定义了一些方法,用于提供表格显示的数据。例如:
@interface ViewController () <UITableViewDataSource>
@property (nonatomic, strong) NSArray *dataArray;
@end
@implementation ViewController
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.dataArray[indexPath.row];
return cell;
}
@end
通过遵循这两个协议,视图控制器可以有效地控制UITableView
的显示和用户交互。
5.2 UINavigationControllerDelegate
UINavigationController
是iOS中用于管理视图控制器导航栈的控件。UINavigationControllerDelegate
协议定义了一些方法,用于处理导航栏的外观自定义、视图控制器切换动画等。
例如,我们可以通过实现navigationController:willShowViewController:animated:
方法来在视图控制器即将显示时进行一些操作:
@interface ViewController () <UINavigationControllerDelegate>
@end
@implementation ViewController
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (viewController == self) {
navigationController.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName : [UIColor redColor]};
}
}
@end
在上述代码中,当当前视图控制器即将显示时,我们将导航栏标题的颜色设置为红色。
5.3 网络请求的委托
在进行网络请求时,委托模式也经常被使用。例如,我们可以创建一个网络请求类,将网络请求的结果处理委托给其他对象。
首先定义委托协议:
@protocol NetworkRequestDelegate <NSObject>
- (void)networkRequestDidFinishWithData:(NSData *)data;
- (void)networkRequestDidFailWithError:(NSError *)error;
@end
然后创建网络请求类:
#import <Foundation/Foundation.h>
@protocol NetworkRequestDelegate <NSObject>
- (void)networkRequestDidFinishWithData:(NSData *)data;
- (void)networkRequestDidFailWithError:(NSError *)error;
@end
@interface NetworkRequest : NSObject
@property (nonatomic, weak) id<NetworkRequestDelegate> delegate;
- (void)startRequestWithURL:(NSURL *)url;
@end
@implementation NetworkRequest
- (void)startRequestWithURL:(NSURL *)url {
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
if ([self.delegate respondsToSelector:@selector(networkRequestDidFailWithError:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate networkRequestDidFailWithError:error];
});
}
} else {
if ([self.delegate respondsToSelector:@selector(networkRequestDidFinishWithData:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate networkRequestDidFinishWithData:data];
});
}
}
}];
[task resume];
}
@end
在视图控制器中使用网络请求并处理结果:
#import "ViewController.h"
#import "NetworkRequest.h"
@interface ViewController () <NetworkRequestDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NetworkRequest *request = [[NetworkRequest alloc] init];
request.delegate = self;
NSURL *url = [NSURL URLWithString:@"https://example.com/api"];
[request startRequestWithURL:url];
}
- (void)networkRequestDidFinishWithData:(NSData *)data {
NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"网络请求成功,结果:%@", result);
}
- (void)networkRequestDidFailWithError:(NSError *)error {
NSLog(@"网络请求失败,错误:%@", error);
}
@end
在上述代码中,NetworkRequest
类负责发起网络请求,并在请求完成或失败时调用代理对象的相应方法。视图控制器遵循NetworkRequestDelegate
协议,处理网络请求的结果。
六、使用委托模式的注意事项
6.1 避免循环引用
在委托模式中,很容易出现循环引用的问题。例如,如果委托者持有代理对象的强引用,而代理对象又持有委托者的强引用,就会导致循环引用,从而造成内存泄漏。
为了避免循环引用,通常将委托者中的代理属性声明为weak
或assign
(在ARC环境下推荐使用weak
)。例如:
@interface MyClass : NSObject
@property (nonatomic, weak) id<MyProtocol> delegate;
@end
6.2 可选方法的处理
当协议中包含可选方法时,在调用代理对象的可选方法之前,一定要使用respondsToSelector:
方法进行检查,以避免运行时错误。例如:
if ([self.delegate respondsToSelector:@selector(optionalMethod)]) {
[self.delegate optionalMethod];
}
6.3 协议方法的命名规范
为了提高代码的可读性和可维护性,协议方法的命名应该遵循一定的规范。通常,方法名应该清晰地表达该方法的功能,并且与委托模式的上下文相关。例如,在按钮点击的委托协议中,buttonDidClick:
这样的命名就很直观地表明了该方法是在按钮点击时调用。
6.4 代理对象的生命周期管理
要确保代理对象在需要的时候存在,并且在不需要的时候被正确释放。如果代理对象过早被释放,委托者在调用代理方法时可能会导致程序崩溃。例如,在视图控制器作为代理对象的情况下,要注意视图控制器的生命周期,确保在视图控制器被销毁之前,将其从委托者的代理列表中移除(如果支持多重委托)或设置委托者的代理为nil
。
通过深入理解和掌握Objective - C协议与委托模式,并注意上述使用中的注意事项,开发者可以更有效地利用这一强大的设计模式来构建高质量、可维护的iOS应用程序。无论是在简单的用户界面交互,还是复杂的业务逻辑处理中,委托模式都能发挥其独特的优势,帮助我们实现代码的解耦和复用,提高开发效率。