使用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

KVO的简单实现

之间看有关OCiOS的书都会看到KVO这个名词。所以今天来学习和实现一下。简单的说KVOKey-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。

它来源于设计模式中的观察者模式,其基本思想就是:

一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。

我们一般用到的都是自动实现KVO,所以我这里就不实现手动的KVO了。实现KVO需要两个主要的方法:

//注册观察者
- (void)addObserver:(NSObject *)observer 
        forKeyPath:(NSString *)keyPath 
        options:(NSKeyValueObservingOptions)options 
        context:(void *)context;
        
//当观察的对象属性有所改变就会通知观察者,该方法用来处理变更通知
- (void)observeValueForKeyPath:(NSString *)keyPath 
                     ofObject:(id)object 
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
                        context:(void *)context;

这两个方法在Foundation/NSKeyValueObserving.h中,NSObject,NSArray,NSSet均实现了以上方法,因此我们不仅可以观察普通对象,还可以观察数组或结合类对象。

我们来举例说明:

创建一个观察者类:

//
//  Observer.h
//  KVO机制
//

#import <Foundation/Foundation.h>

@interface Observer : NSObject

@end



//  Observer.m
//  KVO机制
#import "Observer.h"

@implementation Observer

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    //判断发送过来的通知中更改的属性是否是name
    if ([keyPath isEqualToString:@"name"]) {
        //获取更改属性的类的信息
        Class classInfo = (__bridge Class)context;
        NSString *className = [NSString stringWithFormat:@"%s", object_getClassName(classInfo)];
        NSLog(@" >> class : [%@] , Name changed", className);
        NSLog(@" >> old name is %@", [change objectForKey:@"old"]);
        NSLog(@" >> new name is %@", [change objectForKey:@"new"]);
    
    }else{
        /*
         *注意:在实现处理变更通知方法 observeValueForKeyPath 时,
         *要将不能处理的 key 转发给 super 的 observeValueForKeyPath 来处理。
         */
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end

然后创建一个用于观察的Target

//  Target.h
//  KVO机制


#import <Foundation/Foundation.h>

@interface Target : NSObject

@property(nonatomic,assign) NSString *name;

@end


//  Target.m
//  KVO机制


#import "Target.h"

@implementation Target
//给name属性一个初始值,用于检测变化
-(instancetype)init{
    if (self = [super init]) {
        _name = @"yue";
    }
    return self;
}
@end

然后我们在main.m中添加观察者来执行:

//  main.m
//  KVO机制


#import <Foundation/Foundation.h>
#import "Observer.h"
#import "Target.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //分别创建实例
        Observer *observer = [[Observer alloc]init];
        Target *target = [[Target alloc]init];
        
        //target 增加一个观察者,用于观察name属性
        [target addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)([Target class])];
        //更改name属性的值,响应观察者的动作
        [target setName:@"hao"];
        //移除name的观察者,防止内存泄露
        [target removeObserver:observer forKeyPath:@"name"];
    }
    return 0;
}

我们运行后结果如下:

可以发现是Target类发送过来的通知,说明属性已经变更。

下面我们来讨论一下传过来了的参数。其中addObserver方法中的option:参数用于指定应该包含哪种通知。其中主要有以下几种:

  • NSKeyValueObservingOptionNew:指出change字典应该包含有新的属性(如果适用)。
  • NSKeyValueObservingOptionOld:指出change字典应该包含有旧的属性(如果适用)。
  • NSKeyValueObservingOptionInitial:把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
  • NSKeyValueObservingOptionPrior: 分2次调用。在值改变之前和值改变之后。

其中observeValueForKeyPath方法中的change参数是一个字典型数据。会根据option参数的变化来生成不同的数据。一般会包含newold两个key

2017/9/1 posted in  iOS

指针问题

#include <stdio.h>  
   
int main() {  
      
    int num = 10;  
    int *p1 = &num//p1存储num的地址  
    int **p2 = &p1;//p2存储p1的地址  
    int ***p3 = &p2;//p3存储p2的地址  
      
    *p1 = 100;//*p1取出num变量  
    **p2 = 200;//**p2取出num变量  
    ***p3 = 300;//***p3也是取出num变量  
    printf("num = %d\n",num);  
    printf("num = %d\n",***p3);  
      
    return 0;  
}  
2017/8/31 posted in  iOS

消息转发机制的简单实现

第一种

首先我们来实现这两个方法:

+(BOOL)resolveInstanceMethod: (SEL)sel

+(BOOL)resolveClassMethod:(SEL)sel

这两个方法用于方法的动态解析,当一个对象执行一个方法时,发现没有找到所对应的实现。这是就要靠上面的方法来动态添加实现了

举例来说:

//
//  Person.h
//  消息转发

#import <Foundation/Foundation.h>

@interface Person : NSObject
//接口中声明一个方法
-(void)run;

@end

//
//  Person.m
//  消息转发


#import "Person.h"
#import <objc/runtime.h>
@implementation Person //在部署的时候我们没有实现接口中声明的run方法


//编写C函数实现代码
void addrun (id self, SEL _cmd){
    NSLog(@"%@ %s",self,sel_getName(_cmd));
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel ==@selector(run)) {
    //如果响应run方法,我们动态将上面实现的addrun方法添加到当前类中。
        class_addMethod(self, sel, (IMP)addrun, "v@:");
    }
    
    return [super resolveInstanceMethod:sel];
}

@end

//  main.m
//  消息转发


#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    //执行代码
        Person *man = [Person new];
        [man run];
    }
    return 0;
}

结果为:
说明执行了我们添加的那个方法,而并没有报错。

第二种

我们可以动态的向类中添加方法来解决无选择子的问题,我们也可以选择转换接受者来选择可以处理该选择子的对象。

首先当我们,没有处理第一种方法的时候,运行期系统会给我们第二个机会,询问是否有备用的接受者。我们就要实现orwardingTargetForSelector

我们举例来看:

首先我们先创建一个新类Car,并且创建一个run方法:

然后在Person.m中,不对resolveInstanceMethod进行任何处理,在forwardingTargetForSelector中返回一个Car实例:

然后继续执行main.m文件,运行为:

说明系统已经将所要实现方法的对象转换为Car的实例对象。

第三种

如果前两种我们都没有实现的话,就会进入第三种,执行完整的消息转发机制。

就会调用两个方法methodSignatureForSelectorforwardInvocation

methodSignatureForSelector方法用来指定方法签名,返回nil表示不处理,否则会进入下一步调用forwardInvocation方法。其中这个签名就是给forwardInvocation中的参数NSInvocation调用的。

开头我们要找的错误unrecognized selector sent to instance原因,原来就是因为methodSignatureForSelector这个方法中,由于没有找到run对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。

所以我们需要做的是自己新建方法签名,再在forwardInvocation中用你要转发的那个对象调用这个对应的签名,这样也实现了消息转发。

关于生成签名的类型"v@:"解释一下。每一个方法会默认隐藏两个参数,self_cmdself代表方法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表返回值为void@表示self:表示_cmd

所以第二个和第三个字符必须是“@:” ,第一个字符是返回类型.

具体的可以看一下了解Type Encodeing

2017/8/31 posted in  iOS

iOS基础概念总结(一)

1. OC的前向声明

由于在编译 Person 类的文件时,不需要知道Dog类的全部细节(Dog类中的方法),若使用 #import "Dog.h" 则必须知道Dog.h的全部细节,而Person类中只需要知道类名Dog就可以了,可用通过以下方式告诉编译器@class Dog;这种方式叫向前声明 (forward declaring),当.m文件中要了解实现细节的时候在引入dog.h

//Person.h
#import <Foundation/Foundation.h>
@class Dog;  //前向声明
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) Dog *dog;

  • 向前声明解决了两个类互相引用的问题。
  • 向前声明可用降低编译时间,除非必要,则可以使用向前申明,并在实现文件中映入头文件,这样做可用尽量降低类之间的耦合。

2. #import和#include的区别?

#importObjective-C导入头文件的语法,可保证不会重复导入。
#include是C/C++导入头文件的语法,如果是objective-c与C/C++混编码,对于C/C++类型的文件,还是使用#include来引入,这种写法需要添加防重复导入的语法。

3. 用NSLog函数输出一个浮点类型,结果四舍五入,并保留一位小数

float money = 1.011;
NSLog(@"%.1f", money);

4.property属性的修饰符有什么样的作用

property是属性访问声明,扩号内支持以下几个属性:

  • getter=getName、setter=setName:设置setter与getter的方法名
  • readwrite、readonly:设置可供访问级别

  • assign:方法直接赋值,不进行任何retain操作,为了解决原类型与环循引用问题

  • retain:其setter方法对参数进行release旧值再retain新值,所有实现都是这个顺序

  • strong:此特质表明该属性定义了一种“拥有关系”(owning relationship)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。

  • weak 此特质表明该属性定义了一种“非拥有关系”(nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

  • unsafe_unretained 此特质的语义和assign相同,但是它适用于“对象类型”(object type),该特质表达一种“非拥有关系”(“不保留”,unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe),这一点与weak有区别

  • copy:其setter方法进行copy操作,与retain处理流程一样,先对旧值release,再copy出新的对象,retainCount为1。这是为了减少对上下文的依赖而引入的机制。

  • nonatomic:非原子性访问,不加同步, 多线程并发访问会提高性能。注意,如果不加此属性,则默认是两个访问方法都为原子型事务访问。

5. self.name=@object和_name=@object有什么不同?

self.name =”object”:会调用对象的setName()方法;name = “object”:会直接把"object"字符串赋值给当前对象的name属性。后者的效率会很高,因为没有调用set方法而是直接方法问内存。

2017/8/30 posted in  iOS