「OC」多界面传值
在仿写网易云的过程之中,为了实现多界面传值,我了解了委托,通知和属性等方式,并分别实现了夜间模式的传值,为了后面3Gshared的需要,特此对多界面传值进行学习与总结。
属性传值
属性的传值一般来说是从前往后进行的
这个的实现比较简单,我们可以在视图A之中实现一个事件,然后将信息发送给后一个视图B并且控制视图弹出。以下是我用searchBar
完成的一个界面传值功能,当我们在控制器A之中点击搜索时,就会将searchBar
之中的文字内容通过控制器B的属性传给控制器B,实现了属性传值。
#import "ViewController.h"
#import "ViewController2.h"
@interface ViewController ()<UISearchBarDelegate>
@property (nonatomic, strong)UISearchBar *searchBar;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.searchBar = [[UISearchBar alloc]init];
self.searchBar.frame = CGRectMake(100, 100, 200, 200);
self.searchBar.delegate = self;
[self.view addSubview:_searchBar];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
NSLog(@"1");
ViewController2 *controller2 = [[ViewController2 alloc] init];
controller2.text = searchBar.text;
// 执行搜索操作
[self presentViewController:controller2 animated:YES completion:nil];
}
-(void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//点击空白处,虚拟键盘回收,不再作为第一响应者
[self.searchBar resignFirstResponder];
}
@end
—————————————————————————————————————————————————————————————————————————————————————————————————————————#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ViewController2 : UIViewController
@property (nonatomic,strong) NSString *text;
@end
NS_ASSUME_NONNULL_END
#import "ViewController2.h"
@interface ViewController2 ()
@end
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
UILabel *label = [[UILabel alloc] init];
label.text = self.text;
label.frame = CGRectMake(100, 100, 200, 200);
[self.view addSubview:label];
}
协议/代理传值
协议传值多用于后一个控制器向前一个控制器进行传值。
拿我实现的夜间模式来举例,我们要创建一个协议来规定协议之中的方法,便于后一个视图B调用控制器A之中方法
@protocol NightModeChangeDelegate <NSObject>
-(void)userDidChangeDarkModeSetting:(BOOL)nightModeEnabled;
@end
之后在控制器B之中添加一个代理对象
在视图B之中的UISwitch的状态改变之时,我们就直接触发视图A的相关方法
我们还需要先将视图B的代理对象赋给视图A,我在SceneDelegate.m进行了实现
JCFIrst *first = [[JCFIrst alloc] init];
UINavigationController *nav1 = [[UINavigationController alloc] initWithRootViewController:first];
nav1.navigationBar.tintColor = [UIColor redColor];
nav1.tabBarItem.title = @"主页";
nav1.tabBarItem.image = [UIImage systemImageNamed:@"house"];
JCThird *profileVC = [[JCThird alloc] init];
profileVC.title = @"我的";
profileVC.view.backgroundColor = [UIColor whiteColor];
UINavigationController *nav3 = [[UINavigationController alloc] initWithRootViewController:profileVC];
nav3.tabBarItem.title = @"我的";
nav3.tabBarItem.image = [UIImage systemImageNamed:@"person"];
profileVC.delegate = first;
注:我们还要在JCFIrst.h设置这个视图实现了NightModeChangeDelegate之中的方法
接下来就是在JCFIrst.h写代理方法
- (void)userDidChangeDarkModeSetting:(BOOL)nightModeEnabled
我们就成功的使用协议传值实现了夜间模式
block传值
我感觉block传值与协议传值类似,都是一种回调的思想,与协议不同的是,block的传值可以实现一对多的思路,其实针对简单的单一的传值使用block相较于协议传值来说更加的方便快捷,直接就在对象之间传值,不需要额外的代理
下面是实现过程:
我们先定义一个block,而后在后一个控制器之中声明一个block的属性,
@interface SendingViewController : UIViewController
typedef void (^ValuePassBlock)(NSString *value);
@property (nonatomic, copy) ValuePassBlock valuePassBlock;
@end
@implementation SendingViewController
- (void)sendValueToReceivingViewController {
// 假设要传递的值是一个字符串
NSString *valueToSend = @"Hello, World!";
// 调用 Block 并传递值
if (self.valuePassBlock) {
self.valuePassBlock(valueToSend);
}
}
@end
然后前面的视图做一个接收,就用block完成了一个跨界面传值
@interface FirstViewController : UIViewController
@end
@implementation FirstViewController
- (void)presentSecondViewController {
SendingViewController *secondViewController = [[SendingViewController alloc] init];
SendingViewController.valuePassBlock = ^(NSString *value) {
NSLog(@"%@",value);
};
[self presentViewController:secondViewController animated:YES completion:nil];
}
@end
通知传值
iOS中的NSNotificationCenter是一种广泛使用的通信机制,它允许不同组件、模块或应用程序间以一种松耦合的方式传递信息。通知机制的核心是NSNotificationCenter(通知中心),它作为中介,负责发布(post)和分发(deliver)通知。下面阐述通知的运行步骤原理:
我们仍然以夜间模式的实现来举例
1.在发送者中创建并发送通知
在UISwitch的状态发生变化的时候,事件响应,消息中心发送通知,我们将布尔值通过字典打包起来,通过以下方法发生通知
NSDictionary *userInfo = @{@"boolValue": @(isOn)};
[[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil userInfo:userInfo];
在上述代码中,使用 postNotificationName:object:userInfo:
方法来发送通知。我们需要指定通知的名称(NotificationName)和可选的用户信息(userInfo)。
2.在接收者中注册观察者
我们在需要接受通知的视图控制器之中的 -(void)viewDidLoad
设置观察者,设置监听 name为"NotificationName"的发送者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setNightMode:) name:@"NotificationName" object:nil];
3.在接收者中接受通知
-(void)setNightMode:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
_isOn = [userInfo[@"boolValue"] boolValue];
if (!_isOn) {
//进行夜间模式的相关操作
} else {
}
[self.view setNeedsDisplay];
[self.tableView reloadData];
}
4.在使用完或不使用时销毁通知
一般来说我们在合适的位置将观察值释放,这个位置一般是dealloc
方法之中
// 在对象销毁或不再需要监听通知的地方
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"NotificationName" object:nil];
}
总而言之,通过 NSNotificationCenter
,我们可以在应用程序内的不同对象之间实现松耦合的通信,方便地进行消息传递和处理。它的方便之处其实是在于,一个通知中心信息的发放,每个接收器都能够接受的到,而NSNotificationCenter
之中的name其实就是用来区分不同的发送者的,所以我们在实现消息传值的时候都要注意发送者和接受者name是否相同。
KVO传值
与通知的方法有一点像,我们一共也需要进行观察者的注册,实现监听方法,取消观察。
- 观察者的注册
对于KVO之中观察者的注册我们使用以下方法
(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
其中keyPath
是监听对象之中存在的一个属性名称,NSKeyValueObservingOptions
为一个枚举常量:
NSKeyValueObservingOptionNew
: 当属性的值发生变化时,提供新的属性值作为通知的参数。NSKeyValueObservingOptionOld
: 当属性的值发生变化时,提供旧的属性值作为通知的参数。NSKeyValueObservingOptionInitial
: 在添加观察者时,立即发送一次通知,提供当前属性的值作为通知的参数。NSKeyValueObservingOptionPrior
: 在属性值发生实际变化之前,先发送一次通知,提供旧的属性值作为通知的参数。
context:
这是一个指针类型的参数,用于传递额外的上下文信息。通常情况下可以传入 NULL,表示不需要传递上下文信息。
- 实现监听方法,我们实现监听需要显示监听方法
(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
,这个方法会在被观察到属性发生变化的时候调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"propertyName"]) {
// 处理属性变化的逻辑
// 通过 change 字典获取变化的新值等信息
}
}
注:被观察对象的属性需要符合 KVC(Key-Value Coding)规范,即属性需要使用
@property
声明,并且支持 KVO。你可以使用@objc dynamic
关键字来修饰属性,以确保其支持 KVO。
这个 if
语句用于确保在 observeValueForKeyPath:ofObject:change:context:
方法中处理的是指定的属性变化。
当注册观察者时,可以指定一个或多个要观察的属性名(KeyPath)。当这些属性中的任何一个发生变化时,KVO 会调用观察者对象的 observeValueForKeyPath:ofObject:change:context:
方法,并传递相关的参数。
所以我们可以通过 keyPath
参数来判断触发回调的是哪个属性的变化。这样,可以实现根据不同的属性变化执行不同的逻辑。
- KVO机制只能对继承自NSObject的类的属性进行观察,无法对结构体(struct)等非NSObject子类的属性进行观察。
- 在使用KVO时,需要注意正确地添加和移除观察者,以避免引发内存管理问题,如循环引用。
自动实现KVO监听
一般来说我们监听一个对象的属性,就简单使用监听属性setter
方法,让kvo自动实现就行
至于用KVO的方式实现跨界面传值较为繁琐,以下是实现方法,我们监听控制器2的一个属性,当这个属性发生改变之后,就将改变的值在监听者这里获取。
#import "ViewController.h"
#import "ViewController2.h"
@interface ViewController ()<UISearchBarDelegate>
@property (nonatomic, strong)UISearchBar *searchBar;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.searchBar = [[UISearchBar alloc]init];
self.searchBar.frame = CGRectMake(100, 100, 200, 200);
self.searchBar.delegate = self;
[self.view addSubview:_searchBar];
self.l = [[UILabel alloc] init];
self.l.backgroundColor = [UIColor whiteColor];
self.l.frame = CGRectMake(100, 300, 200, 35);
[self.view addSubview:self.l];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
ViewController2 *controller2 = [[ViewController2 alloc] init];
controller2.text = searchBar.text;
[controller2 addObserver:self forKeyPath:@"context" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// 执行搜索操作
[self presentViewController:controller2 animated:YES completion:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqual:@"context"]) {
self.l.text = [change objectForKey:@"new"];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
-(void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.searchBar resignFirstResponder];
}
@end
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ViewController2 : UIViewController
@property (nonatomic,strong) NSString *context;
@property (nonatomic,strong) NSString *text;
@property (strong, nonatomic) UITextField *textField;
@property (strong, nonatomic) UIButton *backButton;
@end
NS_ASSUME_NONNULL_END
#import "ViewController2.h"
@interface ViewController2 ()
@property (strong, nonatomic) UITextField *textField;
@property (strong, nonatomic) UIButton *backButton;
@end
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
UILabel *label = [[UILabel alloc] init];
label.text = self.text;
label.frame = CGRectMake(100, 100, 200, 200);
[self.view addSubview:label];
self.view.backgroundColor = [UIColor orangeColor];
self.backButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.backButton.frame = CGRectMake(100, 100, 44, 44);
self.backButton.backgroundColor = [UIColor blueColor];
[self.backButton addTarget:self action:@selector(pressBack) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.backButton];
self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 250, 200, 50)];
self.textField.keyboardType = UIKeyboardTypeDefault;
self.textField.borderStyle = UITextBorderStyleRoundedRect;
[self.view addSubview:self.textField];
}
- (void) pressBack {
self.context = self.textField.text;
[self dismissViewControllerAnimated:YES completion:nil];
}
@end
可以看到使用KVO监听实现跨界面回传,相对还是比较麻烦的,所以我们还是多用通知传值
手动触发KVO
虽然KVO主要由setter触发,但也可以通过willChangeValue(forKey:)
和didChangeValue((forKey:)
,
willChangeValueForKey:
:在属性发生变化之前调用,通知KVO开始监听属性的变化。
didChangeValueForKey:
:在属性发生变化后调用,通知KVO属性的变化已经完成。
当我们需要监听可变数组或者可变集合之中的内容变化时(即在可变数组/集合添加元素),KVO并不会进行自动触发,需要我们用手动的方式进行触发,以下是一个简单的例子
#import "ViewController.h"
#import "ViewController2.h"
@interface ViewController () <UISearchBarDelegate>
@property (nonatomic, strong) NSMutableArray *myArray;
@property (nonatomic, strong) UILabel *label;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myArray = [NSMutableArray array];
[self addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
UIButton *addButton = [UIButton buttonWithType:UIButtonTypeSystem];
[addButton setTitle:@"添加元素" forState:UIControlStateNormal];
[addButton addTarget:self action:@selector(addValueToMyArray) forControlEvents:UIControlEventTouchUpInside];
addButton.frame = CGRectMake(20, 100, 200, 40);
[self.view addSubview:addButton];
self.label = [[UILabel alloc] initWithFrame:CGRectMake(20, 200, 200, 40)];
self.label.text = @"";
[self.view addSubview:self.label];
}
- (void)addValueToMyArray {
[self willChangeValueForKey:@"myArray"];
[self.myArray addObject:@(self.myArray.count + 1)];
[self didChangeValueForKey:@"myArray"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"1");
if ([keyPath isEqualToString:@"myArray"]) {
[self updateLabel];
}
}
- (void)updateLabel {
NSString *arrayContent = [self.myArray componentsJoinedByString:@", "];
self.label.text = arrayContent;
}
- (void)dealloc {
[self.myArray removeObserver:self forKeyPath:@"array"];
}
@end
结果如下
有兴趣的读者可以自行尝试如果删去手动触发kvo的那两行代码,就会发现程序没有反应。