【iOS】多界面传值

发布于:2025-09-11 ⋅ 阅读:(15) ⋅ 点赞:(0)

前言

在先前的的仿写中多次使用到了不同界面的传值,这里作一个总结。

属性传值

属性传值是最常用、最简单的一种控制器间数据传递方式,是通过定义属性并设置值来传递数据的。

以在3gshare关注状态的仿写的内容为例,将要跳转进的视图控制器定义为属性,在每次push时,判断该属性是否已经被初始化,如果没有,则初始化,否则不重复操作初始化,使得push进的界面的状态被保留,而不是反复刷新。

@property(nonatomic, strong) FollowViewController *followVC;
if (!self.followVC) {
  self.followVC = [[FollowViewController alloc] init];
  self.followVC.title = @"新关注的";
  self.followVC.followStatus = self.followStatus;
} else {
  self.followVC.followStatus = self.followStatus;
}
[self.navigationController pushViewController:self.followVC animated:YES];
-(void)pressBtn:(UIButton*)button {
    NSInteger index = button.tag; 
    BOOL selected = ![self.followStatus[index] boolValue];
    self.followStatus[index] = @(selected);
    button.selected = selected;
}

效果不再重复展示。

协议传值

协议传值是通过定义协议和代理方法的方式进行多界面传值,常用于反向传值,也就是从第二个页面把数据回传到第一个页面。实现方法是第二页定义一个协议,第一页遵守并实现协议方法,然后第二页通过代理对象调用协议方法,把数据回去。

这里我们依然以3gshare的仿写中注册界面将账号信息传回给登录界面为例:

  • 定义协议和声明代理属性(一般定义在被传值的界面,也就是发送数据的一方
@protocol InformationDelegate <NSObject>

-(void)setupInformation:(NSMutableDictionary*)dictionary;

@end

@interface RegisterViewControl : UIViewController
  
@property(nonatomic, weak) id<InformationDelegate> dictDelegate;

@end
  • 触发代理方法:也就是调用代理方法,一般在被传值的界面。
NSString *UserName = self.UserNameText.text;
NSString *PassWord = self.PassWordText.text;
NSDictionary *dict = [NSDictionary dictionaryWithObject:PassWord forKey:UserName];
[self.dictionary addEntriesFromDictionary:dict];
//self.dictionary交给代理对象self.dictDelegate执行方法setupInformation
[self.dictDelegate setupInformation:self.dictionary];
[self.navigationController popViewControllerAnimated:YES];
  • 实现代理方法(一般在传值的界面):也就是谁想接受传值的数据,就实现这个方法。
#import "LoginViewControl.h"
#import "RegisterViewControl.h"

@interface LoginViewControl ()<UITextFieldDelegate, UITabBarDelegate, InformationDelegate>
  
@end

@implementation LoginViewControl
  
-(void)setupInformation:(NSMutableDictionary*)dictionary {
  [self.dict addEntriesFromDictionary:dictionary];
}
  • 设置代理:这一步最重要也最容易被漏掉,它一般写在传值方,也就是接受数据的页面。
-(void)pressRegister {
  RegisterViewControl *registerVC = [[RegisterViewControl alloc] init];
  registerVC.dictDelegate = self;
  [self.navigationController pushViewController:registerVC animated:YES];
}

具体的效果展示也不再重复。

block传值

block传值跟协议代理传值的思路相似,也用于后面向前面的传值,但它使用代码块进行传值。

  • 定义block类型和属性
#import <UIKit/UIKit.h>

typedef void(^SendBlock)(NSString *text);

@interface BViewController : UIViewController

@property(nonatomic, copy) SendBlock send;

@end

typedef void(^SendBlock)(NSString *text)

  • typedof:定义类型别名
  • void(^SendBlock)(NSString *text):无返回值,带NSString参数的block类型
  • SendBlock:block名字

这里值得注意的是:属性为什么要用 copy

  1. 首先我们要知道block的存储位置,block 本质上是一个带有上下文的函数对象。根据创建场景不同,block 的存储位置不同:
  • 栈上:默认block分配在栈上,随着作用域结束就会被销毁。
  • 堆上:如果使用copy关键字,那么系统会将block从栈复制到堆上,这样即使超出作用域,block仍然继续存在。
  • 全局区:不捕获外部变量的 block,也叫全局 block,会直接存放在全局区,不需要 copy 就能安全使用。
  1. 然后我们再来考虑为什么属性要用copy而不是strong。
  • strong:赋值时,block可能还是栈上的block,当方法调用完后,栈内存释放,block也随之销毁,但strong修饰的属性还会持有一个悬空指针,访问就会崩溃。
  • copy:copy会把 block 拷贝到堆上,生命周期就不依赖原作用域,直到对象释放才销毁,是安全的。

这里我们再用一个代码来看一下区别:

#import <Foundation/Foundation.h>

typedef void(^TestBlock)(void);

@interface MyClass : NSObject

@property(nonatomic, strong) TestBlock strongBlock;
@property(nonatomic, copy) TestBlock copyBlock;

@end
  
#import <Foundation/Foundation.h>
#import "MyClass.h"

void test(void) {
    MyClass *obj = [[MyClass alloc] init];
    
    int value = 42;
        obj.strongBlock = ^{
            NSLog(@"strongBlock: %d", value);
        };
        obj.copyBlock = ^{
            NSLog(@"copyBlock: %d", value);
        };
        
        NSLog(@"strongBlock class: %@", [obj.strongBlock class]);
        NSLog(@"copyBlock class: %@", [obj.copyBlock class]);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
    }
    return 0;
}

运行结果:

在这里插入图片描述

我们发现运行结果和我们预想的不一样,无论strong还是copy,block都在堆上。我查阅了一下资料,发现这是编译器ARC模式下,系统自动帮我们做了一次copy操作,因此我们会发现block存储位置是一样的。然而在MRC模式下的输出结果是我们预想的那样。

这里有些超出我现学的知识,后续了解学习后来完善补充这里的内容。

  • 触发block:将要回传的数据作为block的参数传入并执行block。
- (void)backAndSend {
    if (self.send) {
        self.send(self.textField.text);
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}

将输入框输入的内容作为参数传入block时,前一个视图控制器接收的信息就会是我传进去的参数。

  • 接收数据:在需要接受的地方将传回的值进行使用。
- (void)goToB {
    BViewController *bVC = [[BViewController alloc] init];
    bVC.send = ^(NSString *text) {
        self.label.text = [NSString stringWithFormat:@"收到: %@", text];
    };
    [self presentViewController:bVC animated:YES completion:nil];
}

展示一下效果:

在这里插入图片描述

通知传值

通知传值是一种适合一对多传值的传值方式,它使用了通知中心来实现观察者模式,允许一个对象在发生改变时通知其他观察者对象。

首先我们确定我们的目的,从A跳转到B,并且点击B中按钮将我们字典中存入的内容返回给A,也就是在B界面修改并传值给A界面。

  • 发送通知:

将B界面的值传回给A界面,因此在B界面发送通知。

[[NSNotificationCenter defaultCenter] postNotificationName:@"MyNotification"
 object:nil
 userInfo:info];
  • postNotificationName:通知的名称,通过该名称区分不同的通知,以便接收时,监视相应名称的通知。
  • object:通知的发送者,表示是哪个对象发送了这个通知。通常情况下,我们不需要传递发送者,可以传入nil。
  • userInfo:通知的附加信息,可以通过字典传递一些额外的信息给接收通知的对象。通常情况下,我们在发送通知时,将一些需要传递的数据放入这个字典中。
  • 注册监听,注册观察者:

B界面将值传回给A界面,因此在A界面注册监听,也就是A界面愿意接受B界面传回的值。

[[NSNotificationCenter defaultCenter] addObserver:self
 selector:@selector(receiveNotice:)
 name:@"MyNotification"
 object:nil];
  • addObserver:要注册的观察者对象,通常当前对象作为参数接受通知。
  • selector:观察者对象用于处理通知的方法,这个方法必须带有一个参数,通常是 NSNotification 对象,用于接收传递的通知信息。
  • name:要观察的通知名称,与通知名称匹配。
  • object:通知发送者的对象。如果设置为 nil,则会接收任何发送给指定名称的通知。如果设置为特定对象,只有该对象发送的与指定名称匹配的通知才会被发送给观察者。
  • 接收通知:

在A界面接收到传回来的值,也就是这个字典,并修改label

- (void)receiveNotice:(NSNotification *)notification {
    NSDictionary *info = notification.userInfo;
    NSString *msg = info[@"message"];
    self.label.text = msg;
}
  • 移除通知:

最重要且容易遗漏的一步,整个通知传值结束后,一定要移除通知,避免内存泄露

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

效果展示:

在这里插入图片描述

KVO传值

KVO是观察者模式在iOS中的实现,也就是一个对象可以监听另一个对象某个属性的变化,当被监听的属性值发生变化时,会收到通知并执行相应的操作。

  • 注册观察者
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
  • addObserver:观察者对象,即要接受属性变化通知的对象,通常是当前控制器
  • forKeyPath:要监听的属性名,用字符串表示
  • options:枚举值,指定监听选项,使用(|)连接*
    1. NSKeyValueObservingOptionNew:新的属性值
    2. NSKeyValueObservingOptionOld:旧的属性值
    3. NSKeyValueObservingOptionInitial:注册观察者前,立即发送一次通知,尽管属性没有变化,回调中的change提供当前属性值作为新值
    4. NSKeyValueObservingOptionPrior:在属性值真正改变之前,先提前发一次通知,回调提供的是变化前的旧值,为改变做准备或清理
  • context:指针类型的参数,用于传递额外的上下文信息,通常情况下传入NULL
  • KVO回调,实现监听方法
//KVO回调,实现监听方法,当属性改变时调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSNumber *newAge = change[NSKeyValueChangeNewKey];
        self.ageLabel.text = [NSString stringWithFormat:@"观察到 Age: %@", newAge];
    }
}
  • 移除观察者:当观察者对象不再需要监听属性变化时,移除观察者,避免潜在的内存泄漏
-(void)dealloc {
    [self.person removeObserver:self forKeyPath:@"age"];
}

展示一下效果:

在这里插入图片描述

可以看的出,通过KVO传值,实现了两个界面之间类似于3gshare仿写中点赞同步的传值。

这里我们再学习一下KVO的自动触发与手动触发。

  • 自动触发:常用于监听对象属性的变化(例如字符串、数字等)。例如上面demo中的age,该属性值发生变化时,KVO会自动发送通知给观察者;使用@synthesize合成属性时,如果指定了观察者,则合成的属性会自动触发KVO通知。
  • 手动触发:
    • 用于监听集合属性的变化,集合属性不支持自动触发KVO通知。修改集合属性之前调用willChangeValueForKey:方法,在修改完成后调用didChangeValueForKey:方法。(这两个方法必须配套出现
    • 用于监听非对象类型(例如C语言基本数据类型)属性,由于它们不是对象,因此无法自动触发KVO通知。

这里展示一个demo:

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property(nonatomic, strong) NSMutableArray *friends;

-(void)addFriends:(NSString *)name;
-(void)removeFriends:(NSString *)name;

@end
  
-(void)addFriends:(NSString *)name {
    [self willChangeValueForKey:@"friends"];
    [self.friends addObject:name];
    [self didChangeValueForKey:@"friends"];
}

-(void)removeFriends:(NSString *)name {
    [self willChangeValueForKey:@"friends"];
    [self.friends removeObject:name];
    [self didChangeValueForKey:@"friends"];
}

!非常重要的

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"friends"]) {
        return NO;//关闭自动触发
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

这是KVO通知开关,这个方法必须有,因为只有关闭自动触发后才会调用willChangeValueForKey:didChangeValueForKey:两个方法,否则观察者不会收到任何通知,程序就会报错。

在这里插入图片描述

// 观察回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"friends"]) {
        NSLog(@"friends 改变了 old=%@ new=%@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
        NSLog(@"当前好友列表:%@", self.person.friends);
    }
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"friends"];
}

输出结果:

在这里插入图片描述

我们会发现点击删除时输出均为nil,这是因为willChangeValueForKey:didChangeValueForKey:这两个方法实际是告诉程序属性将要改变了,属性已经改变了,但是不会知道数组中具体增加了还是删除了具体哪个元素。为解决这个问题,willChange:valuesAtIndexes:forKey:didChange:valuesAtIndexes:forKey:这两个方法可以找到数组里具体哪个元素,这样change中便有了具体值。

总结

在仿写项目的过程中,多界面传值是很重要的部分,笔者会在学习后继续补充完善。


网站公告

今日签到

点亮在社区的每一天
去签到