使用RAC的基本操作

作为一个iOS开发者,你写的每一行代码几乎都是在相应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。

这里我们来创建一个简单的登录功能界面,当输入的namepassword符合我们的预期就输出success

开始

首先我们创建一个工程起名为FirstRAC,并在里面创建podfile文件:

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, ‘8.0’
use_frameworks!

target 'FirstRAC’ do

    pod 'Masonry'
    pod 'ReactiveObjC', '~> 3.0.0'

end

首先我们创建一个button,两个LabeltextField如下图所示:

这里我们的界面布局使用Masonry来进行约束。

界面元素的属性声明为:

@property(nonatomic,strong) UIView *login;
@property(nonatomic,strong) UILabel *nameLabel;
@property(nonatomic,strong) UILabel *passLabel;
@property(nonatomic,strong) UITextField *name;
@property(nonatomic,strong) UITextField *pass;
@property(nonatomic,strong) UIButton *sign;

元素的约束以及创建为:

self.login = [[UIView alloc] initWithFrame:CGRectZero];
    
    _name = [[UITextField alloc] init];
    _name.borderStyle = UIFontWeightBold;
    _name.font = [UIFont systemFontOfSize:15];
    _name.placeholder = @"Enter Name";
    [_login addSubview:_name];
    
    _pass = [[UITextField alloc] init];
    _pass.borderStyle = UIFontWeightBold;
    _pass.font = [UIFont systemFontOfSize:15];
    _pass.placeholder = @"Enter Pass";
    [_login addSubview:_pass];
    
    _nameLabel = [[UILabel alloc] init];
    _nameLabel.backgroundColor = UIColor.whiteColor;
    _nameLabel.font = [UIFont systemFontOfSize:14.0];
    _nameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
    _nameLabel.text = @"Name";
    [_login addSubview:_nameLabel];
    
    _passLabel = [[UILabel alloc] init];
    _passLabel.backgroundColor = UIColor.whiteColor;
    _passLabel.font = [UIFont systemFontOfSize:14.0];
    _passLabel.lineBreakMode = NSLineBreakByTruncatingTail;
    _passLabel.text = @"Pass";
    [_login addSubview:_passLabel];
    
    _sign = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    _sign.layer.borderWidth = 3;
//    _sign.layer.borderColor = [UIColor blueColor].CGColor;
    _sign.titleLabel.textColor = [UIColor redColor];
//    _sign.backgroundColor = [UIColor greenColor];
    [_sign setTitle:@"Sign" forState:UIControlStateNormal];
    [_sign setTitle:@"Push" forState:UIControlStateHighlighted];
    [_sign setTitleColor:UIColor.blackColor forState:UIControlStateNormal];
    _sign.showsTouchWhenHighlighted = YES;
//    [_sign addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
    [_login addSubview:_sign];
    [self.view addSubview:_login];

    [_login mas_makeConstraints:^(MASConstraintMaker *make){
        make.left.right.and.bottom.equalTo(self.view);
        make.top.equalTo(self.mas_topLayoutGuide).offset(@50);
    }];
    [_nameLabel mas_makeConstraints:^(MASConstraintMaker *make){
        make.top.equalTo(_login.top);
        make.height.equalTo(@20);
        make.left.equalTo(_login.left).with.offset(20);
    }];
    [_passLabel mas_makeConstraints:^(MASConstraintMaker *make){
        make.top.equalTo(_nameLabel.bottom).with.offset(10);
        make.centerX.equalTo(_nameLabel.centerX);
        make.height.equalTo(@20);
    }];
    [_name mas_makeConstraints:^(MASConstraintMaker *make){
        make.left.equalTo(_nameLabel.right).with.offset(5);
        make.centerY.equalTo(_nameLabel.centerY);
        make.height.equalTo(@20);
    }];
    [_pass mas_makeConstraints:^(MASConstraintMaker *make){
        make.left.equalTo(_passLabel.right).with.offset(5);
        make.centerY.equalTo(_passLabel.centerY);
        make.height.equalTo(@20);
    }];
    [_sign mas_makeConstraints:^(MASConstraintMaker *make){
        make.height.equalTo(@50);
        make.width.equalTo(@100);
        make.top.equalTo(_pass.bottom).offset(@50);
        make.centerX.equalTo(self.view.centerX);
    }];

之后我们运用RAC为界面的元素添加各种事件,首先我们可以简单的为nametextField来订阅事件:

[[self.name.rac_textSignal
     filter:^BOOL(id value){
         NSString *text = value;
         return text.length>3;
     }]
     subscribeNext:^(id x){
        NSLog(@"%@",x);
    }];

该事件对输入的文本进行过滤,将输入文本的长度大于3的文本转化成信号,传递给下一个订阅事件;下一个订阅者输出文本内容。

RACSignal有很多方法可以来订阅不同的事件类型。每个方法都需要至少一个block,当事件发生时就会执行block中的逻辑。在上面的例子中可以看到每次next事件发生时,
subscribeNext:方法提供的block都会执行。

ReactiveCocoa框架使用category来为很多基本UIKit控件添加signal。这样你就能给控件添加订阅了,text field的rac_textSignal就是这么来的。

类型转换

我们运行下面代码就会发现:

[[[self.pass.rac_textSignal
       map:^id(NSString *text){
           return @(text.length);
       }]
      filter:^BOOL(NSNumber *length){
          return [length integerValue]>3;
      }]
     subscribeNext:^(id x){
         NSLog(@"%@",x);
     }];

编译运行,你会发现log输出变成了文本的长度而不是内容。这是因为:新加的map操作通过block改变了事件的数据。map从上一个next事件接收数据,通过执行block把返回值传给下一个next事件。在上面的代码中,map以NSString为输入,取字符串的长度,返回一个NSNumber。

如下图所示:

能看到map操作之后的步骤收到的都是NSNumber实例。你可以使用map操作来把接收的数据转换成想要的类型,只要它是个对象。

创建有效的状态信号

首先我们创建两个信号;

RACSignal *validUsernameSignal =[self.name.rac_textSignal
                                     map:^id(NSString *text){
                                         return @([self isValidUsername:text]);
                                     }];
RACSignal *validPasswordSignal = [self.pass.rac_textSignal
                                      map:^id(NSString *text){
                                          return @([self isValidPassword:text]);
                                      }];

- (BOOL)isValidUsername:(NSString *)username {
    return username.length > 3;
}

- (BOOL)isValidPassword:(NSString *)password {
    return password.length > 3;
}

可以看到,上面的代码对每个输入框的rac_textSignal应用了一个map转换。输出是一个用NSNumber封装的布尔值。

下一步是转换这些信号,从而能为输入框设置不同的背景颜色。基本上就是,你订阅这些信号,然后用接收到的值来更新输入框的背景颜色。下面有一种方法很方便:

    RAC(self.name , backgroundColor) = [validUsernameSignal
                                        map:^id(NSNumber *nameValid){
                                            return [nameValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
                                        }];
    RAC(self.pass , backgroundColor) = [validPasswordSignal
                                        map:^id(NSNumber *nameValid){
                                            return [nameValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
                                        }];

RAC宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。

聚合信号

目前在应用中,登录按钮只有当用户名和密码输入框的输入都有效时才工作。现在要把这里改成响应式的。

现在的代码中已经有可以产生用户名和密码输入框是否有效的信号了——validUsernameSignalvalidPasswordSignal了。现在需要做的就是聚合这两个信号来决定登录按钮是否可用。

把下面的代码添加到viewDidLoad的末尾:

RACSignal *signUpActiveSignal = [RACSignal combineLatest:@[validUsernameSignal,validPasswordSignal] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid){
        return @([usernameValid boolValue]&&[passwordValid boolValue]);
    }];

上面的代码使用combineLatest:reduce:方法把validUsernameSignal和validPasswordSignal产生的最新的值聚合在一起,并生成一个新的信号。每次这两个源信号的任何一个产生新值时,reduce block都会执行,block的返回值会发给下一个信号。

注意:RACsignal的这个方法可以聚合任意数量的信号,reduce block的参数和每个源信号相关。ReactiveCocoa有一个工具类RACBlockTrampoline,它在内部处理reduce block的可变参数。实际上在ReactiveCocoa的实现中有很多隐藏的技巧,值得你去看看。

现在我们的逻辑变为:

上图展示了一些重要的概念,你可以使用ReactiveCocoa来完成一些重量级的任务。

  • 分割——信号可以有很多subscriber,也就是作为很多后续步骤的源。注意上图中那个用来表示用户名和密码有效性的布尔信号,它被分割成多个,用于不同的地方。

  • 聚合——多个信号可以聚合成一个新的信号,在上面的例子中,两个布尔信号聚合成了一个。实际上你可以聚合并产生任何类型的信号。

这些改动的结果就是,代码中没有用来表示两个输入框有效状态的私有属性了。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。

响应式的登录

应用目前使用上面图中展示的响应式管道来管理输入框和按钮的状态。但是按钮按下的处理用的还是action,所以下一步就是把剩下的逻辑都替换成响应式的。

你已经知道了ReactiveCocoa框架是如何给基本UIKit控件添加属性和方法的了。目前你已经使用了rac_textSignal,它会在文本发生变化时产生信号。为了处理按钮的事件,现在需要用到ReactiveCocoaUIKit添加的另一个方法,rac_signalForControlEvents

现在回到ViewController.m中,把下面的代码添加到viewDidLoad的末尾:

[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   subscribeNext:^(id x) {
     NSLog(@"button clicked");
   }];

上面的代码从按钮的UIControlEventTouchUpInside事件创建了一个信号,然后添加了一个订阅,在每次事件发生时都会输出log
你就会发现每次点击按钮后,就会响应输出事件,输出button clicked

现在按钮有了点击事件的信号,下一步就是把它和登录流程连接起来。。那么问题就来了,打开RWSignInService.h,看一下接口:

//RWSignInService.h
#import <Foundation/Foundation.h>

typedef void (^RWSignInResponse)(BOOL);

@interface RWSignInService : NSObject
- (void)signInWithUsername:(NSString *)username
                 password:(NSString *)password
                 complete:(RWSignInResponse)completeBlock;
@end

//RWSignInService.m
#import "RWSignInService.h"

@implementation RWSignInService

- (void)signInWithUsername:(NSString *)username password:(NSString *)password complete:(RWSignInResponse)completeBlock {
    
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        BOOL success = [username isEqualToString:@"user"] && [password isEqualToString:@"password"];
        completeBlock(success);
    });
}

@end

这个service有3个参数,用户名、密码和一个完成回调block。这个block会在登录成功或失败时执行。你可以在按钮点击事件的subscribeNext: blcok里直接调用这个方法。

注意:本教程为了简便使用了一个假的service,所以它不依赖任何外部API。但你现在的确遇到了一个问题,如何使用这些不是用信号表示的API呢?

创建信号

幸运的是,把已有的异步API用信号的方式来表示相当简单。,还是在ViewController.m中,添加下面的方法:

-(RACSignal *)signInSignal{
    return [RACSignal createSignal:^RACDisposable *(id subscriber){
        [self.signInService signInWithUsername:self.name.text password:self.pass.text complete:^(BOOL success){
            [subscriber sendNext:@(success)];
            [subscriber sendCompleted];
        }];
        return nil;
    }];
}

上面的方法创建了一个信号,使用用户名和密码登录。现在分解来看一下。

上面的代码使用RACSignalcreateSignal:方法来创建信号。方法的入参是一个block,这个block描述了这个信号。当这个信号有subscriber时,block里的代码就会执行。

block的入参是一个subscriber实例,它遵循RACSubscriber协议,协议里有一些方法来产生事件,你可以发送任意数量的next事件,或者用error\complete事件来终止。本例中,信号发送了一个next事件来表示登录是否成功,随后是一个complete事件。

这个block的返回值是一个RACDisposable对象,它允许你在一个订阅被取消时执行一些清理工作。当前的信号不需要执行清理操作,所以返回nil就可以了。

可以看到,把一个异步API用信号封装是多简单!

现在就来使用这个新的信号。把之前添加在viewDidLoad中的代码更新成下面这样的:

[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   map:^id(id x){
     return[self signInSignal];
   }]
   subscribeNext:^(id x){
     NSLog(@"Sign in result: %@", x);
   }];

上面的代码使用map方法,把按钮点击信号转换成了登录信号。subscriber输出log.

编译运行,点击登录按钮,查看Xcode的控制台后发现输出的是一个信号实例,没错,你已经给subscribeNext:block传入了一个信号,但传入的不是登录结果的信号。

当点击按钮时,rac_signalForControlEvents发送了一个next事件(事件的data是UIButton)。map操作创建并返回了登录信号,这意味着后续步骤都会收到一个RACSignal。这就是你在subscribeNext:这步看到的。

上面问题的解决方法,有时候叫做信号中的信号,换句话说就是一个外部信号里面还有一个内部信号。你可以在外部信号的subscribeNext:block里订阅内部信号。不过这样嵌套太混乱啦,还好ReactiveCocoa已经解决了这个问题。

解决的方法很简单,只需要把map操作改成flattenMap就可以了:

[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   flattenMap:^id(id x){
     return[self signInSignal];
   }]
   subscribeNext:^(id x){
     NSLog(@"Sign in result: %@", x);
   }];

这个操作把按钮点击事件转换为登录信号,同时还从内部信号发送事件到外部信号。

到这里我们的大部分内容就结束了。最后就是在subscribeNext步骤里添加登录成功后的逻辑。把代码更新成下面的:

[[[self.sign rac_signalForControlEvents:UIControlEventTouchUpInside]
     flattenMap:^id(id x){
         return [self signInSignal];
     }]
     subscribeNext:^(NSNumber*signedIn){
         BOOL success =[signedIn boolValue];
         if(success){
             NSLog(@"Success!!!");
         }
     }];

但是,你注意到这个应用现在有一些用户体验上的小问题了吗?当登录service正在校验用户名和密码时,登录按钮应该是不可点击的。这会防止用户多次执行登录操作。

这个逻辑应该怎么添加呢?改变按钮的可用状态并不是转换(map)、过滤(filter)或者其他已经学过的概念。其实这个就叫做“副作用”,换句话说就是在一个next事件发生时执行的逻辑,而该逻辑并不改变事件本身。

添加附加操作(Adding side-effects)

把代码更新成下面的:

[[[[self.sign rac_signalForControlEvents:UIControlEventTouchUpInside]
       doNext:^(id x){
           self.sign.enabled = NO;
       }]
     flattenMap:^id(id x){
         return [self signInSignal];
     }]
     subscribeNext:^(NSNumber*signedIn){
         BOOL success =[signedIn boolValue];
         if(success){
             NSLog(@"Success!!!");
         }
     }];

你可以看到doNext:是直接跟在按钮点击事件的后面。而且doNext: block并没有返回值。因为它是附加操作,并不改变事件本身。

上面的doNext: block把按钮置为不可点击,隐藏登录失败提示。然后在subscribeNext: block里重新把按钮置为可点击,并根据登录结果来决定是否显示失败提示。

之前的管道图就更新成下面这样的:

现在所有的工作都已经完成了,这个应用已经是响应式的啦!!

2017/10/19 posted in  iOS

使用Masonry框架来构造iOS布局

之前一直都是在使用storyboards来创建iOS布局,突然某一天看到使用代码布局后,界面元素的清晰易懂,就迷上了。。。

所以这次简单学习一下使用Masonry帮助构建iOS界面元素。

在阅读了Masonrygithub主页之后,学习并安装了框架。

框架的安装

安装框架非常简单,我们只需要在podfile中加上下面一句:

pod 'Masonry'

之后为了语法的缩写以及代码自动补全我们来创建Code Snippets

mas_make -> [<#view#> mas_makeConstraints:^(MASConstraintMaker *make) { <#code#> }];

mas_update -> [<#view#> mas_updateConstraints:^(MASConstraintMaker *make) { <#code#> }];

mas_remake -> [<#view#> mas_remakeConstraints:^(MASConstraintMaker *make) { <#code#> }];

将上述语句放到~/Library/Developer/Xcode/UserData/CodeSnippets中,之后我们在写相关代码的时候就会有代码提示了。

我们在要使用Masonry的文件要频繁的导入"Masonry.h"头文件,所以为了方便,我们创建一个Supporting Files文件夹,并在其中创建一个prefix.pch文件。文件内容为:

//
//  MBMasonry-Prefix.pch

#import <Availability.h>

// Include any system framework and library headers here that should be included in all compilation units.
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.

#ifndef __IPHONE_3_0
#warning "This project uses features only available in iOS SDK 3.0 and later."
#endif

#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

//define this constant if you want to use Masonry without the 'mas_' prefix
// 只要添加了这个宏,就不用带mas_前缀
#define MAS_SHORTHAND

//define this constant if you want to enable auto-boxing for default syntax
// 只要添加了这个宏,equalTo就等价于mas_equalTo
#define MAS_SHORTHAND_GLOBALS
//在这里导入头文件。
#import "Masonry.h"

#endif /* MBMasonry_Prefix_pch */

之后我们在使用的时候,就不用每个文件都导入一遍头文件了。

使用方法

原声的iOS代码,对界面布局使用的是NSLayoutAttribute,用了Masonry后,我们使用封装好的MASViewAttribute。具体的属性等价关系如下图所示:

我们来举例说明一下,假入我们想要创建一个登陆界面。界面中要有NamePass两个TextField,并对应两个Label

那么我们可以按照如下方式来写:

//
//  ViewController.m
//  FirstRAC
//
//  Created by 梁中豪 on 2017/10/17.
//  Copyright © 2017年 梁中豪. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()
@property(nonatomic,strong) UIView *login;
@property(nonatomic,strong) UILabel *nameLabel;
@property(nonatomic,strong) UILabel *passLabel;
@property(nonatomic,strong) UITextField *name;
@property(nonatomic,strong) UITextField *pass;


@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.login = [[UIView alloc] initWithFrame:CGRectZero];
    
    _name = [[UITextField alloc] init];
    _name.borderStyle = UIFontWeightBold;
    _name.font = [UIFont systemFontOfSize:15];
    _name.placeholder = @"Enter Name";
    [_login addSubview:_name];
    
    _pass = [[UITextField alloc] init];
    _pass.borderStyle = UIFontWeightBold;
    _pass.font = [UIFont systemFontOfSize:15];
    _pass.placeholder = @"Enter Pass";
    [_login addSubview:_pass];
    
    _nameLabel = [[UILabel alloc] init];
    _nameLabel.backgroundColor = UIColor.whiteColor;
    _nameLabel.font = [UIFont systemFontOfSize:14.0];
    _nameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
    _nameLabel.text = @"Name";
    [_login addSubview:_nameLabel];
    
    _passLabel = [[UILabel alloc] init];
    _passLabel.backgroundColor = UIColor.whiteColor;
    _passLabel.font = [UIFont systemFontOfSize:14.0];
    _passLabel.lineBreakMode = NSLineBreakByTruncatingTail;
    _passLabel.text = @"Pass";
    [_login addSubview:_passLabel];
    [self.view addSubview:_login];
    [self buildElem];
}

//为所创建的控件,创建约束
- (void)buildElem{
    [_login mas_makeConstraints:^(MASConstraintMaker *make){
        make.left.right.and.bottom.equalTo(self.view);
        make.top.equalTo(self.mas_topLayoutGuide);
    }];
    [_nameLabel mas_makeConstraints:^(MASConstraintMaker *make){
        make.top.equalTo(_login.top);
        make.height.equalTo(@20);
        make.left.equalTo(_login.left).with.offset(20);
    }];
    [_passLabel mas_makeConstraints:^(MASConstraintMaker *make){
        make.top.equalTo(_nameLabel.bottom).with.offset(10);
        make.centerX.equalTo(_nameLabel.centerX);
        make.height.equalTo(@20);
    }];
    [_name mas_makeConstraints:^(MASConstraintMaker *make){
        make.left.equalTo(_nameLabel.right).with.offset(5);
        make.centerY.equalTo(_nameLabel.centerY);
        make.height.equalTo(@20);
    }];
    [_pass mas_makeConstraints:^(MASConstraintMaker *make){
        make.left.equalTo(_passLabel.right).with.offset(5);
        make.centerY.equalTo(_passLabel.centerY);
        make.height.equalTo(@20);
    }];

}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

从上面可以看到,我们为元素创建了约束。当然这只是基本的方法。还有更多的API接口可以使用。我们这里就不在过多说明了。

大概知道上面的用法,我们就可以开心的撸代码写界面去了。
:smile:smile:smile:smile:smile:smile:smile:

2017/10/18 posted in  iOS

ReactiveCocoa初步了解

ReactiveObjC是一个受到函数响应式编程启发而开发的OC框架,对应的Swift框架叫做ReactiveCocoa or ReactiveSwift。简称为RAC

不同于使用那些被替代和修改的可编辑变量,RAC提供了一个signals (represented by RACSignal)用于捕获当前或者未来的值。这种做法工作起来类似于KVO,但是却没有那么繁琐。

RAC的一大优势就是提供了一个统一的方法去解决异步表现,这些表现包括:委托方法,回调函数块,target-action机制,通知和KVO。

有如下例子:

//当self.name改变后,输出新的名字到控制台
//
//无论何时改变了值,RACObserve(self, username)都会创造了一个新的RACSignal用于发送当前self.name
// 不管在什么时候signal发送了一个新的值,-subscribeNext: 都将执行块方法.
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

但是不想KVO通知,signals是可以被链接和操作的:

// 只输出以"j"开头的名字.
//
// -filter returns a new RACSignal that only sends a new value when its block
// returns YES.
[[RACObserve(self, username)
    filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }]
    subscribeNext:^(NSString *newName) {
        NSLog(@"%@", newName);
    }];

其实上面代码也可以复杂的写成:

RACSignal *usernameSourceSignal = self.username;
  
RACSignal *filteredUsername =[usernameSourceSignal
  filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }];
  
[filteredUsername subscribeNext:^(NSString *newName) {
        NSLog(@"%@", newName);
    }];

这是因为RACSignal的每个操作都会返回一个RACsignal,这在术语上叫做连贯接口(fluent interface)。这个功能可以让你直接构建管道,而不用每一步都使用本地变量。

Signals也可以被使用去获取状态。区别于观察属性和设置其他属性来反应新的值,RAC可以按照信号和操作来表达属性:

// Creates a one-way binding so that self.createEnabled will be
// true whenever self.password and self.passwordConfirmation
// are equal.
//
// RAC() is a macro that makes the binding look nicer.
//
// +combineLatest:reduce: takes an array of signals, executes the block with the
// latest value from each signal whenever any of them changes, and returns a new
// RACSignal that sends the return value of that block as values.
RAC(self, createEnabled) = [RACSignal
    combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]
    reduce:^(NSString *password, NSString *passwordConfirm) {
        return @([passwordConfirm isEqualToString:password]);
    }];

还有许多用法这就不举例了,详细用例可以查看官方文档

使用时机

刚开始,可能很难理解RAC,因为这个ReactiveObjC很抽象,很难了解什么时机应该使用它,怎样去解决问题。这里有几个推荐的使用时机:

  • 解决异步或者时间驱动的数据源
  • 链接依赖操作(尤其在网络处理方面)
  • 并行独立的工作
  • 简化集合的转变
2017/10/17 posted in  iOS

多用块枚举,与快速遍历少用for循环

我们在编程时,经常会需要遍历某个collection,例如NSArrary,NSDictionary,NSSet等。

我们经常使用for循坏来遍历,这样对于数组来说还好,但是根据定义,字典与set都是“无序的"(imoniered)所以无法根据特定的整数下标来直接访问其中的值。于是,就需要先获取字典里的所有键或是set里的所有对象,这两种情况下,都可以在获取到的有序数组上遍历,以便借此访问原字典及原set中的值。

但是创建这个附加数组会有额外开销,而且还会多创建一个数组对象,它会保留collection中的所有元素对象。 当然了,释放数组时这些附加对象也要释放,可是要调用本来不需执行的方法。其他各种遍 历方式都无须创建这种中介数组。

例如:

// Dictionary
NSDictionary *aDictionary = /* .... */;
NSArray *keys = [aDictionary allKeys]; 
for (int i = 0; i < keys.count; i++) { 
    id key = keys[i]; 
    id value = aDictionary[key];
    //Do something with 'key' and 'value'
}

//Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allObjects]; 
for (int i = 0; i < objects.count; i++)
{ 
    id object = objects [i];
    //Do something with 'object'
} 

上述代码实现遍历比较麻烦,所以我们推荐使用快速遍历与块循环。

快速遍历

快速遍历是OC 2.0所引入的一个新功能。它语法更简洁,它为for循环开设了in关键字。这个关键字大幅简化了遍历collection所需的语法,比方说要遍历数组,就可以这么写:

NSArray *anArray = /* ••• */; 
for (id object in anArray) {
    //Do something with 'object'
}

这样写简单多了。如果某个类的对象支持快速遍历,那么就可以宣称自己遵从名为 NSFastEmimeraticm的协议,从而令开发者可以采用此语法来迭代该对象。

遍历字典与set为:

// Dictionary
NSDictionary *aDictionary = /* ... */; 
for (id key in aDictionary) {
    id value = aDictionary[key];
    //Do something with 'key' and 'value'
}

//Set
NSSet *aSet = /* ... */; 
for (id object in aSet) {
    //Do something with 'object'
}

由于NSEnumerator对象也实现了 NSFastEnumeration协议,所以能用来执行反向遍历。 若要反向遍历数组,可采用下面这种写法:

NSArray *anArray = /* ... */;
for (id object in [anArray reverseObjectEnumerator]){
    //Do something with 'object'
}

在目前所介绍的遍历方式中,这种办法是语法最简单且效率最髙的,然而如果在遍历字典时需要同时获取键与值,那么会多出来一步。而且,与传统的for循环不同,这种遍历方式无法轻松获取当前遍历操作所针对的下标。遍历时通常会用到这个下标,比如很多算法都需要它。

基于块的遍历

在当前的Objective-C语言中,最新引人的一种做法就是基于块来遍历。NSArray中定义了下面这个方法,它可以实现最基本的遍历功能:

-(void)enumerateObjectsUsingBlock:
        (void(^)(id object, NSUInteger idx, BOOL *stop))block

此之外,还有一系列类似的遍历方法,它们可以接受各种选项,以控制遍历操作,稍后将会讨论那些方法。

在遍历数组及set时,每次迭代都要执行由block参数所传人的块,在遍历数组及set时,每次迭代都要执行由block参数所传人的块,这个块有三个参数, 分别是当前迭代所针对的对象、所针对的下标,以及指向布尔值的指针。前两个参数的含义不言而喻。而通过第三个参数所提供的机制,开发者可以终止遍历操作。

例如,下面这段代码用此方法来遍历数组:

NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:
        ^(id object, NSUInteger id, BOOL *stop){
    //Do something with 'object' 
    if (shouldStop) {
        *stop = YES;
}

这种写法稍微多了几行代码,不过依然明晰,而且遍历时既能获取对象,也能知道其下标。此方法还提供了一种优雅的机制,用于终止遍历操作,开发者可以通过设定stop变最值来实现,当然,使用for等遍历方式时,也可以通过break来终止循环,那样做也很好。

此方式不仅可用来遍历数组。NSSet里面也有同样的块枚举方法,NSDictionary也是这样, 只是略有不同:

-(void)enumerateKeysAndObjectsUsingBlock:
        (void(^)(id key, id object, BOOL *stop))block

因此,遍历字典与set也同样简单:

// Dictionary
NSDictionary *aDictionary = /* ... */;
[aDietionary enumerateKeysAndObjectsUsingBlock: 
        ^(id key, id object, BOOL *stop)){
            //Do something with 'key' and 'object' 
            if (shouldStop) {
            *stop = YES;
};

//Set
NSSet *aSet = /* ... */;
[aSet enumerateObjectsUsingBlock:
            ^(id object, BOOL *stop){
    //Do something with 'object' 
        if (shouldStop) {
            *stop = YES;
        }
  }];

此方式大大胜过其他方式的地方在于:遍历时可以直接从块里获取更多信息。在遍历数组时,可以知道当前所针对的下标。遍历有序set(NSOrderedSet)时也一样。而在遍历字典时,无须额外编码,即可同时获取键与值,因而省去了根据给定键来获取对应值这一步。用这种方式遍历字典,可以同时得知键与值,这很可能比其他方式快很多,因为在字典内部的数据结构中,键与值本来就是存储在一起的。

另一个好处就是:能够修改块的方法签名,以免进行类型转换操作,从效果上讲,相当于把本来需要执行的类型转换操作交给块方法签名来做。

比方说,要用“快速遍历法”来遍 历字典。若已知字典中的对象必为字符串,则可以这样编码:

for (NSString *key in aDictionary) {
    NSString *object = (NSString*)aDictionary[key];
    //Do something with 1 key1 and 1 object1
)

如果改用基于块的方式来遍历,那么就可以在块方法签名中直接转换:

NSDictionary ^aDictionary = /* ••• */;
[aDictionary enumerateKeysAndObjectsUsingBlock:
        ^(NSString *key, NSString *obj, BOOL *stop){
         //Do something with 'key' and 'obj'
}];

之所以能如此,是因为id类型相当特殊,它可以像本例这样,为其他类型所覆写。要是原来的块签名把键与值都定义成NSObject*,那这么写就不行了。此技巧初看不甚显眼,实 则相当有用。指定对象的精确类型之后,编译器就可以检测出开发者是否调用了该对象所不 具备的方法,并在发现这种问题时报错。如果能够确知某collection里的对象是什么类型, 那就应该使用这种方法指明其类型。

用此方式也可以执行反向遍历。数组、字典、set都实现了前述方法的另一个版本,使开发者可向其传入“选项掩码”(option mask):

-(void)enumerateObjectsWithOptions:
            (NSEnumerationOptions)options 
            usingBlock: 
            (void(^)(id obj, NSUInteger id, BOOL *stop))block

-(void)enumerateKeysAndObjectsWithOptions:
            (NSEnumerationOptions)options
            usingBlock:
            (void(^)(id key, id obj, BOOL *stop))block

NSEnumerationOptions类型是个enum,其各种取值可用“按位或”(bitwiseOR)连接,用以表明遍历方式。具体选项不再过多介绍。

总体来看,块枚举法拥有其他遍历方式都具备的优势,而且还能带来更多好处。与快速遍历法相比,它要多用一些代码,可是却能提供遍历时所针对的下标,在遍历字典时也能同时提供键与值,而且还有选项可以开启并发迭代功能,所以多写这点代码还是值得的。

要点

  • 遍历collection有多方式。最基本的办法是for循环,其次是NSEnumerator遍历法及快速遍历法,最新、最先进的方式则是“块枚举法”。
  • “块枚举法”本身就能通过GCD来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点。
  • 若提前知道待遍历的collection含有何种对象,则应修改块签名,指出对象的具体类型。
2017/10/11 posted in  第七章 熟悉系统框架

第三十四条 : 以“自动释放池降低内存峰值”

Objective-C的引用计数架构中,有一项特性叫做“自动释放池”(autoreleasepool)。释放对象有两种方式:一种是 调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加人“自动 释放池”中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会向其中的对象发送release消息。

创建自动释放池的语句为:

@autoreleasepool{
    // ....
}

我们来看下面这个例子:

@autoreleasepool {
    NSString *string = [NSString stringWithFormat: @"1 = %i", 1]; 
    @autoreleasepool {
        NSNumber *number = [NSNumber numberWithlnt:1];
        }
}

上面这个例子展示了基本的用法,自动释放池于左花括号处创建, 并于对应的右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到release消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。

autoreleasepool的常见用法为降低程序的内存峰值,比如下面这个代码:

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
    EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
    [people addObject:person];
}

上述代码,因为for语句会不断的创建person对象,造成应用程序内存不断上涨,在执行完for语句后又会将所有临时对象都释放,造成内存的突然上涨与下降。这些临时对象本可以提前回收的,所以我们应该这么写:

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
    @autoreleasepool{
        EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
        [people addObject:person];
    }
}

加上这个自动释放池之后,应用程序在执行循环时的内存峰值就会降低,不再像原来那么高了。内存峰值(high-memory waterline)是指应用程序在某个特定时段内的最大内存用量 (highest memory footprint)。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。

自动释放池机制就像“栈”(stack) 一样。系统创建好自动释放池之后,就将其推入栈中, 而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。

现在我们创建一个iOS程序之后,系统会默认有一个main.m文件其中有代码:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

我们用自动释放池来包裹应用程序的主入口点(main application entry point),除了上述主动添加了一个释放池,我们一般不需要主动添加,系统创建的线程一般默认都有自动释放池。

@autordeaSepool语法还有个好处:每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已为系统所回收的对象。例如:

@autoreleasepool {
    id object = [self createObject];
}
[self useObject:object];

上述代码无法编译,因为object变量出了自动释放池块的外围后就不可用了(已经被释放),所以在调用“useObject:”方法时不能用它做参数。

要点

  • 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
  • 合理运用自动释放池,可降低应用程序的内存峰值。
  • @autoreleasepool这种新式写法能创建出更为轻便的自动释放池。
2017/10/9 posted in  第五章 内存管理

第四十五条:使用dispatch_once来执行只需运行一次的代码

单例模式顾名思义,就是一个类一般只会同时存在一个实例,常见的实现方式为在类中编写名为sharedInstance的类方法。例如:

+(id)sharedImstance{
    static EOCClass *sharedlnstance = nil;
        @synchronized(self){
            if (!sharedlnstance) {
                sharedlnstance = [[self alloc] init];
            }
    }
    return sharedlnstance;
}

该方法只会返回全类共用的单例实例,而不会在每次调用时都创建新的实例。

为保证线程安全,上述代码将创建单例实例的代码包裹在同步块里。

GCD中的一个函数可以更简单的执行这种只需执行一次的代码。为:

void dispatch_once (dispatch_once_t *token,
                    dispatch_block_t block);

此函数接受类型为的dispatch_once_t的特殊参数token,此外还接受块参数,在块里面执行我们所需要运行一次的代码。对于给定的token来说,该函数保证相关的块必定会执行,且仅执行一次。首次调用该函数时,必然会执行块中的代码,最重要的一点在于,此操作完全是线程安全的。请注意,对于只需执行一次的块来说,每次凋用函数时传入的标记都必须完全相同。因此,开发 者通常将标记变量声明在staticglobal作用域里。

将上面的代码改为dispatch_once来执行,就可以换为:

+ (id)sharedInstance {
    static EOCClass ^sharedInstance = nil; 
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedlnstance = [[self alloc] initJ;
    });
    return sharedInstance;
}

使用dispatch_once可以简化代码并且彻底保证线程安全,开发者根本无须担心加锁或同步。所有问题都由GCD在底层处理。

相比于同步机制的繁琐,dispatch_once更高效,此函数采用“原子访问”(atomic access)来査询token,以判断其所对应的代码原来是否已经执行过。速度一般是同步机制的两倍。

要点

  • 经常需要编写“只需执行一次的线程安全代码”(thread-safe single-code execution)。通过GCD所提供的dispatch_once函数,很容易就能实现此功能。
  • 标记应该声明在staticglobal作用域中,这样的话,在把只需执行一次的块传给dispatch once函数时,传进去的标记也是相同的。
2017/9/28 posted in  Effective OC2.0