preface

I’ve been looking for an easy to use communication tool that can replace message notifications and avoid writing tedious Delegate interfaces. By chance, I came into contact with RAC framework (ReactiveCocoa) in the project. Reactive programming + chain programming did have powerful functions, but I also encountered many problems and stumbles. The cost of solving each problem was very high, and the learning cost was too high. RAC is also a large framework that is very intrusive to project code. In addition, it is not suitable for creating external interfaces. If you want to develop an SDK for other projects, you should not require all users to use RAC in the same way. However, I could not find a simple lightweight communication tool, so I wrote a small tool XKVO.

If you don’t want to continue using a large framework like RAC and want to get rid of the onerous communication interfaces, the XKVO tool described in this article may be the answer.

XKVO introduction

Lightweight communication tools use Block way to achieve one-to-many event notification, using Block encapsulation system KVO method, so that monitoring events and attribute changes become very simple.

XKVO is divided into two parts, actually only two classes to be exact:

  1. XSimpleSignal XSimpleSignal – A lightweight signal Notification tool that uses blocks instead of delegates and notifications and supports one-to-many communication.

    Signal-block management, which is used to create an XSimpleSignal object. Listeners take this object and register a listening Block with the signal. A signal object (XSimpleSignal) can register multiple listeners. The Owner of the signal object calls signal.emitSignal(ID,…). Send a signal, each listener can receive notification, first register first arrive.

  2. XSimpleKVO uses XSimpleSignal to encapsulate the system’s KVO methods. Users can use blocks to listen for KVO callbacks, which is easy to use.

Usage, for example,

Install the tutorial

1.Introduce XKVO: pod in Podfile'XKVO', :git => 'https://gitee.com/fireice2048/xkvo.git', :branch => 'master'
2.Perform pod installCopy the code

The KVO attribute listens

  1. #import <XKVO/XKVO.h>
  2. Add KVO listening code
@interface ViewController (a)

@property (nonatomic, assign) NSInteger testCount;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [XKVO xkvo_addObserver:self object:self property:@"testCount" changedBlock:^(id  _Nonnull oldValue, id  _Nonnull newValue, BOOL isInitialNotify) {
        NSLog(@"Listen for testCount attribute change %@ ==> %@", oldValue, newValue);
    }];
    
    self.testCount ++;
}

@end

Copy the code
  1. Run the program and you will see the following log output:
Listen for the testCount property change 0 ==> 1Copy the code
  1. Add the listener code to initialNotify:YES,
    [XKVO xkvo_addObserver:self object:self property:@"testCount" changedBlock:^(id  _Nonnull oldValue, id  _Nonnull newValue, BOOL isInitialNotify) {
        NSLog(@"Listen for testCount attribute change %@ %@ ==> %@", isInitialNotify ? @"Initial value" : @"", oldValue, newValue);
    } initialNotify:YES];
Copy the code

Run the program and you will see the following log output:

Listen for testCount change initial value 0 ==> 0 Listen for testCount change 0 ==> 1Copy the code
  • InitialNotify: Indicates whether to call back block notifications. The default value is NO. Because iOS’s KVO does not get the new value that is about to change until it listens for the change, there is no need to implement priorNotify
  • IsInitialNotify: YES Indicates that this listening callback is the current value, not the change event. In this case, newValue and oldValue are the same value
  • When the added observer object is destroyed, the blocks maintained within XKVO are automatically cleaned up

Signal transmission and monitoring

  1. #import <XKVO/XKVO.h>
@property (nonatomic, strong) XSimpleSignal * loginSignal; // Login signal
Copy the code
  1. Creating a signal object
    _loginSignal = [[XSimpleSignal alloc] init];
Copy the code
  1. Add signal listening code
    [_loginSignal addBlock:^(NSString * nick, int age){
        NSLog(@"Listener 1 received account login event Nick :%@, age:%@", nick, @(age));
    } observer:self];
    
    [_loginSignal addBlock:^(NSString * nick, int age){
        NSLog(@"Listener 2 received account login event Nick :%@, age:%@", nick, @(age));
    } observer:self];
    
    [_loginSignal addBlock:^(NSString * nick, int age){
        NSLog(@"Listener 3 received account login event Nick :%@, age:%@", nick, @(age));
    } observer:self];
Copy the code
  1. Add signal sending code
    if (_loginSignal)
    {
        _loginSignal.emitSignal(@"Cabbage".20);
    }
Copy the code

Run the program and you will see the following log output:

Listener 1 receives the account login event Nick: cabbage, age:20 listener 2 receives the account login event Nick: cabbage, age:20 Listener 3 receives the account login event Nick: cabbage, age:20Copy the code

All three listeners received account login events.

Matters needing attention

  • All blocks to be added must be of the same type
  • When sending signals, the parameters must be consistent with the added blocks
  • The first argument must be of type ID, so the block must have at least one argument

Program source code

Gitee XKVO source

Design Suggestions

Since XSimpleSignal is positioned as a simple communication tool, not a framework, it is not recommended to use it as an interface. Simply put, don’t require your users to use XSimpleSignal, so you don’t need to expose XSimpleSignal in your interface files. Just use XSimpleSignal internally. For example, a project has a login service, designed as a singleton, that many other businesses may rely on to listen for login events and login status changes.

Wrong examples:

#import <XKVO/XSimpleSignal.h>

typedef NS_ENUM(NSInteger, XLoginState) {
    XLoginStateNone = 0./ / not logged in
    XLoginStateLogged,            / / have to log in
    XLoginStateLoggedOut,         // Logged out
};

typedef NS_ENUM(NSInteger, XLoginEvent) {
    XLoginEventNone = 0./ / unknown
    XLoginEventLogging,           // Logging in
    XLoginEventSuccess,           // Login succeeded
    XLoginEventFailed,            // Login failed
};

@interface XLoginService : NSObject

@property (nonatomic, strong) XSimpleSignal * loginEventSignal;
@property (nonatomic, strong) XSimpleSignal * loginStateSignal;

+ (instancetype)sharedInstance;

@end
Copy the code

However, XSimpleSignal is a general-purpose tool, and users do not know how to design listening blocks. XSimpleSignal can also design a general-purpose callback interface like RAC, passing only an ID parameter, to solve this problem. But exposing XSimpleSignal everywhere in the interface isn’t really recommended, so it’s not designed that way.

Correct case:

@interface XLoginService : NSObject

+ (instancetype)sharedInstance;

- (void)doLogin;

- (void)addEventBlock:(void (^)(id service, XLoginEvent event))block;
- (void)addStateBlock:(void (^)(id service, XLoginState state))block;

@end
Copy the code
@interface XLoginService (a)

@property (nonatomic, strong) XSimpleSignal * loginEventSignal;
@property (nonatomic, strong) XSimpleSignal * loginStateSignal;

@end

@implementation XLoginService

/** Zen and Objective-C programming art, singleton elegant writing method, copy and paste can be used */
+ (instancetype)sharedInstance
{
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if(nil == instance) { instance = [[[self class] alloc] init]; }});return instance;
}

- (id)init
{
    self = [super init];
    if (self)
    {
        _loginEventSignal = [[XSimpleSignal alloc] init];
        _loginStateSignal = [[XSimpleSignal alloc] init];
    }
    
    return self;
}

- (void)doLogin
{
    if(! _loginEventSignal || ! _loginStateSignal) {return;
    }
    
    _loginEventSignal.emitSignal(self, XLoginEventLogging);

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.loginStateSignal.emitSignal(self, XLoginStateLogged);
        self.loginEventSignal.emitSignal(self, XLoginEventSuccess);
    });
}

- (void)addEventBlock:(void (^)(id service, XLoginEvent event))block
{
    [_loginEventSignal addBlock:block];
}

- (void)addStateBlock:(void (^)(id service, XLoginState state))block
{
    [_loginStateSignal addBlock:block];
}

@end
Copy the code

After this encapsulation, the user is very simple, with no additional learning costs:

#import "XLoginService.h"

- (void)testLogin
{
    [[XLoginService sharedInstance] addEventBlock:^(id  _Nonnull service, XLoginEvent event) {
        NSLog(@"Received login event callback event:%@", @(event));
    }];
    
    [[XLoginService sharedInstance] addStateBlock:^(id  _Nonnull service, XLoginState event) {
        NSLog(@"Received logon status callback state:%@", @(event));
    }];
    
    [[XLoginService sharedInstance] doLogin];
}
Copy the code

Run the program and you will see the following log output:

Event :1 Login status callback is received state:1 Login event callback is received Event :2Copy the code

Note: When using XSimpleSignal, the first argument to a Block must be of type ID. If the target argument does not need an ID type, then pass nil to place space.

WeakSelf = self; weakSelf = self; weakSelf = self; weakSelf = self;

__weak typeof(self) weakSelf = self;
[[XLoginService sharedInstance] addEventBlock:^(id _Nonnull service, XLoginEvent event) {
    __strong typeof(self) self = weakSelf;
    NSLog(@"%@ received logon event callback event:%@", self, @(event));
}];
Copy the code

Because XLoginService is a singleton and does not destroy itself, the Block added by its signal member will not be released. If the business Block holds its own self, the memory leak will occur.