DZNEmptyDataSet 使用

From: http://www.jianshu.com/p/f70dc3e0f436

                  ⭐️⭐️⭐️ 
以下内容来源于官方源码、 README 文档、测试 Demo 以及个人使用总结 !

[TOC]

DZNEmptyDataSet 是基于 UITableViewUICollectionView 的范畴/扩展(category)类,它可以在空白页面上显示提示信息。

这是 iOS 内建的标准,用于处理空表和集合视图。默认情况下,如果你的表视图是空的,屏幕上什么也不会显示,它给用户的体验不是很好。

使用这个库,你只需要实现一些协议,iOS 就会很好地处理集合视图,然后合理美观地显示出用户信息。

支付宝-查询我的挂号记录

支付宝-查询我的挂号记录

其他效果图参考

使用该框架的项目

将你的项目添加到列表中 并且提供一张(320px)的效果图。

空数据设计模式(The Empty Data Set Pattern)

也被称为 Empty state 或者 Blank Slate

大多数应用程序会显示内容列表、数据集(在 iOS 程序猿眼里,这里通常指的是 UITableViewUICollectionView。),但有些时候这些页面可能是空白的,特别是对于那些刚创建空账户的新用户来说。 空白界面会对用户造成不知道如何进行下一步操作的困惑,因为用户不知道屏幕空白的原因是错误/Bug、网络异常,还是用户应该自己新建内容以恢复APP的正常状态。

请阅读这篇非常有趣的文章 Designing For The Empty States

iOS 9人机界面指南 1.4.2 中也提及过类似的指引:

如果应用中所有的功能当前都不可用,那么应该显示一些内容来解释当前的情形,并建议用户如何进行后续操作。这部分内容给予了用户以反馈,使用户相信你的应用现在没问题。同时这也可以稳定用户情绪,让他们决定是否要采取纠正措施,继续使用应用,还是切换到另一个应用。

Empty Data Sets 有助于:

  • 避免使用空白屏幕,并向用户传达屏幕空白的原因。
  • 用户指引(特别是作为引导页面)。
  • 避免其他中断机制,如显示错误警报。
  • 一致性和改善用户体验。
  • 传递品牌价值。

特性

  • 兼容 UITableViewUICollectionView 。 也兼容 UISearchDisplayControllerUIScrollView
  • 通过显示图片、标题、详细文本、按钮,提供布局外观的多种可能性。
  • 使用 NSAttributedString 类提供更容易定制的外观。
  • 使用 Auto Layout 以自动将内容集中到表格视图,并支持自动旋转。 也接受自定义垂直和水平对齐。
  • 自定义背景颜色。
  • 允许在整个表格矩形上轻敲手势(有助于放弃第一个响应者或类似操作)。
  • 提供更高级的定制,允许自定义视图。
  • 兼容 Storyboard
  • 兼容iOS 6,tvOS 9或更高版本。
  • 兼容iPhone,iPad和Apple TV。
  • 支持 App Store 。

这个库已经被设计为不需要通过扩展(extend) UITableViewUICollectionView 类的方式来实现了。 使用 UITableViewControllerUICollectionViewController 类仍然可以奏效。 只要通过遵循DZNEmptyDataSetSourceDZNEmptyDataSetDelegate 协议,您将能够完全自定义应用程序的空状态的内容和外观。

安装

支持 [CocoaPods][15] 导入

[15]: http://cocoapods.org/?q=DZNEmptyDataSet

pod ‘DZNEmptyDataSet’

使用

完整文档参考:CocoaPods 自动生成文档

导入

#import "UIScrollView+EmptyDataSet.h"

作为框架导入:

#import <DZNEmptyDataSet/UIScrollView+EmptyDataSet.h>

遵循协议

// 遵守 DZNEmptyDataSetSource 、DZNEmptyDataSetDelegate 协议
@interface MainViewController : UITableViewController <DZNEmptyDataSetSource, DZNEmptyDataSetDelegate>

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.tableView.emptyDataSetSource = self;
    self.tableView.emptyDataSetDelegate = self;

    // 删除单元格分隔线的一个小技巧
    self.tableView.tableFooterView = [UIView new];
}

实现数据源协议

DZNEmptyDataSetSource ——实现该协议,可以设置你想要在空白页面显示的内容,并且充分利用 NSAttributedString 功能来自定义文本外观。

  • 空白页显示图片

    • (UIImage )imageForEmptyDataSet:(UIScrollView )scrollView {
      return [UIImage imageNamed:@”lion”];
      }

效果图:


  • 空白页显示标题

    • (NSAttributedString )titleForEmptyDataSet:(UIScrollView )scrollView {
      NSString title = @”狮子王”;
      NSDictionary
      attributes = @{
      NSFontAttributeName:[UIFont boldSystemFontOfSize:18.0f],
      NSForegroundColorAttributeName:[UIColor darkGrayColor]
      };
      
      return [[NSAttributedString alloc] initWithString:title attributes:attributes];
      }

效果图:


  • 空白页显示详细描述

    • (NSAttributedString )descriptionForEmptyDataSet:(UIScrollView )scrollView {
      NSString *text = @”你好,我的名字叫辛巴,大草原是我的家!”;

      NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
      paragraph.lineBreakMode = NSLineBreakByWordWrapping;
      paragraph.alignment = NSTextAlignmentCenter;

      NSDictionary *attributes = @{

      NSFontAttributeName:[UIFont systemFontOfSize:14.0f],
      NSForegroundColorAttributeName:[UIColor lightGrayColor],
      NSParagraphStyleAttributeName:paragraph
      };
      

      return [[NSAttributedString alloc] initWithString:text attributes:attributes];
      }

效果图:


  • 空白页显示按钮:示例1

    • (NSAttributedString )buttonTitleForEmptyDataSet:(UIScrollView )scrollView forState:(UIControlState)state {
      // 设置按钮标题
      NSString buttonTitle = @”喜欢我就点点点点我”;
      NSDictionary
      attributes = @{
      NSFontAttributeName:[UIFont boldSystemFontOfSize:17.0f]
      };
      
      return [[NSAttributedString alloc] initWithString:buttonTitle attributes:attributes];
      }

效果图:

  • 空白页显示按钮:示例2

按钮点击高亮效果

- (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state {
    NSString *text = @"Learn more";
    UIFont   *font = [UIFont systemFontOfSize:15.0];
    // 设置默认状态、点击高亮状态下的按钮字体颜色
    UIColor  *textColor = [UIColor colorWithHex:(state == UIControlStateNormal) ? @"007ee5" : @"48a1ea"];

    NSMutableDictionary *attributes = [NSMutableDictionary new];
    [attributes setObject:font      forKey:NSFontAttributeName];
    [attributes setObject:textColor forKey:NSForegroundColorAttributeName];

    return [[NSAttributedString alloc] initWithString:text attributes:attributes];
}

效果图:

  • 空白页显示按钮:示例3

按钮标题中 点击重试 四个字加粗:

#pragma mark - DZNEmptyDataSetSource

- (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView {
    return [UIImage imageNamed:@"placeholder_No_Network"];
}

- (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state {
    NSString *text = @"网络不给力,请点击重试哦~";

    NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:text];
    // 设置所有字体大小为 #15
    [attStr addAttribute:NSFontAttributeName
                   value:[UIFont systemFontOfSize:15.0]
                   range:NSMakeRange(0, text.length)];
    // 设置所有字体颜色为浅灰色
    [attStr addAttribute:NSForegroundColorAttributeName
                   value:[UIColor lightGrayColor]
                   range:NSMakeRange(0, text.length)];
    // 设置指定4个字体为蓝色
    [attStr addAttribute:NSForegroundColorAttributeName
                   value:HexColor(@"#007EE5")
                   range:NSMakeRange(7, 4)];
    return attStr;
}

- (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView {
    return -70.0f;
}

#pragma mark - DZNEmptyDataSetDelegate

- (void)emptyDataSet:(UIScrollView *)scrollView didTapButton:(UIButton *)button {
    // button clicked...
}

- (void)emptyDataSetWillAppear:(UIScrollView *)scrollView {
    self.tableView.contentOffset = CGPointZero;
}

效果图

  • 空白页显示按钮:示例4

    // 空白页显示返回按钮图片

    • (UIImage )buttonImageForEmptyDataSet:(UIScrollView )scrollView forState:(UIControlState)state {
      return [UIImage imageNamed:@”placeholder_return”];
      }

    • (void)emptyDataSet:(UIScrollView )scrollView didTapButton:(UIButton )button {
      [self.navigationController popViewControllerAnimated:YES];
      }


  • 设置按钮的背景颜色

    • (nullable UIImage )buttonBackgroundImageForEmptyDataSet:(UIScrollView )scrollView forState:(UIControlState)state;

  • 设置按钮图片

    • (UIImage )buttonImageForEmptyDataSet:(UIScrollView )scrollView forState:(UIControlState)state {
      return [UIImage imageNamed:@”Image”];
      }

效果图:


  • 设置图片的 tintColor

    • (UIColor )imageTintColorForEmptyDataSet:(UIScrollView )scrollView {
      return [UIColor yellowColor];
      }

效果图:就是设置图片颜色,脑补中。。。


  • 设置空白页面的背景色

    • (UIColor )backgroundColorForEmptyDataSet:(UIScrollView )scrollView {
      UIColor *appleGreenColor = [UIColor colorWithRed:199/255.0 green:237/255.0 blue:204/255.0 alpha:1.0];
      return appleGreenColor;
      }

效果图:


  • 如果你需要设置更复杂的布局,也可以返回自定义视图

    • (UIView )customViewForEmptyDataSet:(UIScrollView )scrollView {
      UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
      [activityView startAnimating];
      return activityView;
      }

效果图:


  • 设置图片动画

    #pragma mark - DZNEmptyDataSetSource
    #pragma mark 设置空白页图片

    • (nullable UIImage )imageForEmptyDataSet:(UIScrollView )scrollView {
      return [UIImage imageNamed:@”lion”];
      }

    #pragma mark 设置图片动画: 旋转

    • (CAAnimation )imageAnimationForEmptyDataSet:(UIScrollView )scrollView {
      CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath: @”transform”];

      animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
      animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI_2, 0.0, 0.0, 1.0)];

      animation.duration = 0.25;
      animation.cumulative = YES;
      animation.repeatCount = MAXFLOAT;

      return animation;
      }

    #pragma mark - DZNEmptyDataSetDelegate
    // 向代理请求图像视图动画权限。 默认值为NO。
    // 确保从 imageAnimationForEmptyDataSet 返回有效的CAAnimation对象:

    • (BOOL)emptyDataSetShouldAnimateImageView:(UIScrollView *)scrollView {
      return YES;
      }

效果图:

  • 图像视图动画:缩放

    #pragma mark - DZNEmptyDataSetSource
    #pragma mark 设置空白页图片

    • (nullable UIImage )imageForEmptyDataSet:(UIScrollView )scrollView {
      return [UIImage imageNamed:@”computer”];
      }

    #pragma mark 设置图片动画

    • (CAAnimation )imageAnimationForEmptyDataSet:(UIScrollView )scrollView
      {
      CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@”bounds”];
      animation.duration = 1.25;
      animation.cumulative = NO;
      animation.repeatCount = MAXFLOAT;
      animation.toValue = [NSValue valueWithCGRect:CGRectMake(0, 0, 45, 45)];

      return animation;
      }

    #pragma mark - DZNEmptyDataSetDelegate

    • (BOOL)emptyDataSetShouldAnimateImageView:(UIScrollView *)scrollView {
      return YES;
      }

效果图:


  • 我们发现在官方的 Applications Demo 应用中的空白视图中的动画是这样的:

空白视图默认情况下显示一张【静态图片】,当用户点击【静态图片】以后,该图片会被替换成【加载转圈】。

通过阅读源码,可以发现它是这样工作的:

  1. 首先在遵循协议的.m文件中声明了一个 BOOL 类型的变量,用来记录空白页面当前的加载状态:

    @property (nonatomic, getter=isLoading) BOOL loading;

  1. 然后为该属性设置 setter 方法,重新加载空数据集视图:

    • (void)setLoading:(BOOL)loading
      {
      if (self.isLoading == loading) {
      return;
      }

      _loading = loading;
      // 每次 loading 状态被修改,就刷新空白页面。
      [self.tableView reloadEmptyDataSet];
      }

  1. 接下来要实现几个关联协议

    #pragma mark - DZNEmptyDataSetSource
    #pragma mark 设置空白页图片

    • (UIImage )imageForEmptyDataSet:(UIScrollView )scrollView {
      if (self.isLoading) {
      // 圆形加载图片
      return [UIImage imageNamed:@"loading_imgBlue_78x78"];
      
      }else {
      // 默认静态图片
      return [UIImage imageNamed:@"staticImage"];
      
      }
      }

    #pragma mark 图片旋转动画

    • (CAAnimation )imageAnimationForEmptyDataSet:(UIScrollView )scrollView
      {
      CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@”transform”];
      animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
      animation.toValue = [NSValue valueWithCATransform3D: CATransform3DMakeRotation(M_PI_2, 0.0, 0.0, 1.0) ];
      animation.duration = 0.25;
      animation.cumulative = YES;
      animation.repeatCount = MAXFLOAT;

      return animation;
      }

    #pragma mark - DZNEmptyDataSetDelegate
    #pragma mark 是否开启动画

    • (BOOL)emptyDataSetShouldAnimateImageView:(UIScrollView *)scrollView {
      return self.isLoading;
      }

    #pragma mark 空白页面被点击时刷新页面

    • (void)emptyDataSet:(UIScrollView )scrollView didTapView:(UIView )view {
      // 空白页面被点击时开启动画,reloadEmptyDataSet
      self.loading = YES;
      // 执行加载任务…
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
      // 任务加载完成后关闭动画,reloadEmptyDataSet
      self.loading = NO;
      
      });
      }

  • 此外,你还可以调整内容视图的垂直对齐(垂直偏移量)方式:

    // 向上偏移量为表头视图高度/2

    • (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView {
      return -self.tableView.tableHeaderView.frame.size.height/2.0f;
      }
      // 或者,返回固定值
    • (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView {
      return -64;
      }

效果图:


  • 最后,你也可以设置所有组件彼此之间的上下间距(默认间距为11 pt):

    • (CGFloat)spaceHeightForEmptyDataSet:(UIScrollView *)scrollView {
      return 25.0f;
      }

效果图:


实现代理协议

DZNEmptyDataSetDelegate ——实现该协议,可以设置你期望从空白页面返回的的行为,并接收用户交互事件。

  • 设置是否 渲染和显示空白页面(默认为YES):

    • (BOOL)emptyDataSetShouldDisplay:(UIScrollView *)scrollView {
      return YES;
      }
  • 设置是否 以淡入方式显示空白页面 。 (默认值为YES)

    • (BOOL)emptyDataSetShouldFadeIn:(UIScrollView *)scrollView {
      return YES;
      }
  • 强制显示空数据集:当项目数量大于0时,请求代理是否仍应显示空数据集。(默认值为NO

    • (BOOL)emptyDataSetShouldBeForcedToDisplay:(UIScrollView *)scrollView;
  • 获取交互权限:是否接收用户点击事件(默认为YES):

    • (BOOL)emptyDataSetShouldAllowTouch:(UIScrollView *)scrollView;
  • 获取滚动权限(默认值为NO):

    • (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView;
  • 获取图像动画权限:是否开启图片动画(默认值为NO):

    • (BOOL)emptyDataSetShouldAnimateImageView:(UIScrollView *)scrollView {
      return YES;
      }
  • 空白数据集 视图被点击 时触发该方法:

    • (void)emptyDataSet:(UIScrollView )scrollView didTapView:(UIView )view {
      // 处理视图点击事件…
      }
  • 空白数据集 按钮被点击时 触发该方法:

    • (void)emptyDataSet:(UIScrollView )scrollView didTapButton:(UIButton )button {
      // 处理按钮点击事件…
      }
  • 空白页将要出现

    • (void)emptyDataSetWillAppear:(UIScrollView *)scrollView {
      // 如果你的空白占位图与需求向左,发生偏移,可如下设置:
      self.tableView.contentOffset = CGPointZero;
      }
  • 空白页已经出现

    • (void)emptyDataSetDidAppear:(UIScrollView *)scrollView;
  • 空白页将要消失

    • (void)emptyDataSetWillDisappear:(UIScrollView *)scrollView;
  • 空白页已经消失

    • (void)emptyDataSetDidDisappear:(UIScrollView *)scrollView;

刷新布局

如果你需要刷新空白页面布局,只需调用:

[self.tableView reloadData];

或者

[self.collectionView reloadData];

这取决于你用的是哪一个。

强制布局更新

你还可以调用 [self.tableView reloadEmptyDataSet] 以使当前空白页面布局无效,并触发布局更新,绕过 -reloadData。 如果你的数据源上有很多逻辑处理,当你不需要或者想避免调用 -reloadData 时这可能很有用。 当使用 UIScrollView 时, [self.scrollView reloadEmptyDataSet] 是刷新内容的唯一方法。

参考文章

YYModel 使用

From: http://www.jianshu.com/p/25e678fa43d3

开篇说明:
虽然网上有很多讲解YYModel使用方法的文章,包括YYModel作者也在github上对其做了使用说明。
但在我实际使用过程中,依然发现文档的不完善,比如对于复杂的模型(如多层嵌套)讲解的仍不透彻,同时本文也会介绍一神器配合YYModel使用,让你感受分分钟搞定模型创建的酸爽。
当然为了减少读者的学习成本,本会对YYModel作者的文档进行丰富和扩展。
可在github上下载Demo,以便更直观了解各种使用场景详细代码。
文章只要包含:

    1. 详解YYModel的多种使用场景
    1. 拓展插件,让你一分钟搞定所有的模型的创建和调用。

一、YYModel的使用场景

1.简单的 Model 与 JSON 相互转换

// JSON:
{
    "uid":123456,
    "name":"Harry",
    "created":"1965-07-31T00:00:00+0000"
}

// Model:
@interface User : NSObject
@property UInt64 uid;
@property NSString *name;
@property NSDate *created;
@end

@implementation User

@end

// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
User *user = [User yy_modelWithJSON:json];

// 将 Model 转换为 JSON 对象:
NSDictionary *json = [user yy_modelToJSONObject];

JSON/Dictionary 中的对象类型与 Model 属性不一致时,YYModel 将会进行如下自动转换。自动转换不支持的值将会被忽略,以避免各种潜在的崩溃问题。

格式自动转换.png

2.Model 属性名和 JSON 中的 Key 不相同

// JSON:
{
    "n":"Harry Pottery",
    "p": 256,
    "ext" : {
        "desc" : "A book written by J.K.Rowing."
    },
    "ID" : 100010
}

// Model:
@interface Book : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@property NSString *bookID;
@end
@implementation Book
//返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"name" : @"n",
             @"page" : @"p",
             @"desc" : @"ext.desc",
             @"bookID" : @[@"id",@"ID",@"book_id"]};
}
@end

你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。
在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。
在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。

3.Model 包含其他 Model

// JSON
{
    "author":{
        "name":"J.K.Rowling",
        "birthday":"1965-07-31T00:00:00+0000"
    },
    "name":"Harry Potter",
    "pages":256
}

// Model: 什么都不用做,转换会自动完成
@interface Author : NSObject
@property NSString *name;
@property NSDate *birthday;
@end
@implementation Author
@end

@interface Book : NSObject
@property NSString *name;
@property NSUInteger pages;
@property Author *author; //Book 包含 Author 属性
@end
@implementation Book
@end

4.容器类属性

@class Shadow, Border, Attachment;

@interface Attributes
@property NSString *name;
@property NSArray *shadows; //Array<Shadow>
@property NSSet *borders; //Set<Border>
@property NSMutableDictionary *attachments; //Dict<NSString,Attachment>
@end

@implementation Attributes
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
+ (NSDictionary *)modelContainerPropertyGenericClass {
    return @{@"shadows" : [Shadow class],
             @"borders" : Border.class,
             @"attachments" : @"Attachment" };
}
@end

在实际使用过过程中,[Shadow class]Border.class@"Attachment"没有明显的区别。
这里仅仅是创建作者有说明,实际使用时,需要对其遍历,取出容器中得字典,然后继续字典转模型。(YYModel的核心是通过runtime获取结构体中得Ivars的值,将此值定义为key,然后给keyvalue值,所以我们需要自己遍历容器(NSArrayNSSetNSDictionary),获取每一个值,然后KVC)。

  • 具体的代码实现如下:

    NSDictionary json =[self getJsonWithJsonName:@”ContainerModel”];
    ContainerModel
    containModel = [ContainerModel yy_modelWithDictionary:json];
    NSDictionary dataDict = [containModel valueForKey:@”data”];
    //定义数组,接受key为list的数组
    self.listArray = [dataDict valueForKey:@”list”];
    //遍历数组
    [self.listArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL
    _Nonnull stop) {

    NSDictionary *listDict = obj;
    //获取数组中得字典
    List *listModel = [List yy_modelWithDictionary:listDict];
    //获取count 和 id
    NSString *count = [listModel valueForKey:@"count"];
    NSString *id = [listModel valueForKey:@"id"];
    

5.黑名单与白名单

@interface User
@property NSString *name;
@property NSUInteger age;
@end

@implementation Attributes
// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性
+ (NSArray *)modelPropertyBlacklist {
    return @[@"test1", @"test2"];
}
// 如果实现了该方法,则处理过程中不会处理该列表外的属性。
+ (NSArray *)modelPropertyWhitelist {
    return @[@"name"];
}
@end

6.数据校验与自定义转换

实际这个分类的目的比较简单和明确。
就是对判断是否为时间戳,然后对时间戳进行处理,调用
_createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
获取时间。

// JSON:
{
    "name":"Harry",
    "timestamp" : 1445534567     //时间戳
}

// Model:
@interface User
@property NSString *name;
@property NSDate *createdAt;
@end

@implementation User
// 当 JSON 转为 Model 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic {
    NSNumber *timestamp = dic[@"timestamp"];
    if (![timestamp isKindOfClass:[NSNumber class]]) return NO;
    _createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue];
    return YES;
}

// 当 Model 转为 JSON 完成后,该方法会被调用。
// 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。
// 你也可以在这里做一些自动转换不能完成的工作。
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic {
    if (!_createdAt) return NO;
    dic[@"timestamp"] = @(n.timeIntervalSince1970);
    return YES;
}
@end
  • 需要注意的时,如果用插件,对时间戳类型或默认创建为NSUInteger类型,需要将其更改为NSDate类型。

7.Coding/Copying/hash/equal/description

以下方法都是YYModel的简单封装,实际使用过程和系统方法区别不大。对其感兴趣的可以点进方法内部查看。

@interface YYShadow :NSObject <NSCoding, NSCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGSize size;
@end

@implementation YYShadow
// 直接添加以下代码即可自动完成
- (void)encodeWithCoder:(NSCoder *)aCoder { 
 [self yy_modelEncodeWithCoder:aCoder]; 
}
- (id)initWithCoder:(NSCoder *)aDecoder {
  self = [super init];
  return [self yy_modelInitWithCoder:aDecoder]; 
}
- (id)copyWithZone:(NSZone *)zone { 
 return [self yy_modelCopy]; 
}
- (NSUInteger)hash { 
 return [self yy_modelHash]; 
}
- (BOOL)isEqual:(id)object { 
 return [self yy_modelIsEqual:object]; 
}
- (NSString *)description { 
 return [self yy_modelDescription]; 
}
@end
  • 二、ESJsonFormat与YYModel的结合使用

彩蛋
给大家介绍一款插件,配合ESJsonFormat

配图:

ESJsonFormat插件使用.gif

使用方法:
快捷键:shift + control + J
插件安装方法比较简单,在此不赘述,不知道可自行google。

好处

    1. 可以直接将json数据复制,ESJsonFormat会根据数据类型自动生成属性。(建议还是要自行检查,比如时间戳,系统会默认帮你生成为NSUInteger,而我们想要的为NSDate类型)
    1. 对于多模型嵌套,不必创建多个文件,ESJsonFormat会自动在一个文件下创建多重类型,极其便捷。

至此YYModel的使用已讲解完毕,关于YYModel的底层核心是运用runtime获取类结构体中Ivars,进行KVC操作,然后根据不同情况进行分别处理
此处只是传递给大家一个概念,不展开讲解,网上有很多源码分析文章,可自学google学习。
文末,做个综述。
建议大家有时间一定要多看底层,分析源码。不要只会用,知其然不知其所以然。
如有错误欢迎指出。

写在最后

我得写作原则:
在技术学习道路上,阅读量和代码量绝不能线性提升你的技术水平。
同样写文章也是如此,作者所写的文章完全是基于自己对技术的理解,在写作时也力求形象不抽象。绝不copy充数,所以也欢迎大家关注和参与讨论。
技术学习绝不能孤胆英雄独闯天涯,而应在一群人的交流碰撞,享受智慧火花的狂欢。
希望我的文章能成为你的盛宴,也渴望你的建议能成为我的大餐。
如有错误请留言指正,对文章感兴趣可以关注作者不定期更新。

github fork 更新作者代码

From: https://my.oschina.net/snail0/blog/776099

我们在github上可以fork别人的代码来阅读,更改,提交修改。这里网上命令或者可视化操作很多博客都有提及,不在赘述。

但是如果源代码更新了,我们怎么同步更新了,网络上好多用的命令行,我试了很久,没同步成功。

原来github上已经支持同步,只需要在需要的时候操作下,就能同步作者的最新代码。

在我们fork后的界面点击

把我们的项目放到左边,原作者的放到右边,即可看到作者的更新

创建一个pull request

合并后就,在你的分支上就能看到原作者的更新了

/// 整理逻辑 自己代码在左边 更新 整理 合并, 最后只更新主分支

iOS SQLite 使用FMDBMigrationManager 进行数据库迁移

From: http://www.jianshu.com/p/6cfc38a6d2c0

FMDBMigrationManager 是与FMDB结合使用的一个第三方,可以记录数据库版本号并对数据库进行数据库升级等操作。
首先要集成FMDB和FMDBMigrationManager,建议使用cocoapods,这里不再多说。
根据官方文档的解释,有两种方法实现升级,我们一个一个的解释。
先说第一种,添加文件的方式进行记录版本和升级操作,新建一个空白的项目,并创建一个数据库,也就是我们将要进行升级操作的数据库。
将数据库与我们的FMDBMigrationManager关联起来

FMDBMigrationManager * manager=[FMDBMigrationManager managerWithDatabaseAtPath:DBPath migrationsBundle:[NSBundle mainBundle]];
//DBPath是要升级的数据库的地址
// [NSBundle mainBundle]是保存数据库升级文件的位置 根据自己放文件的位置定 升级文件是什么下面会说

下面创建版本号表,这个表会保存在我们的数据库中,进行数据库版本号的记录,并将我们的数据库升级到最高的版本。

BOOL resultState=NO;
NSError * error=nil;
if (!manager.hasMigrationsTable) {
 resultState=[manager createMigrationsTable:&error]; 
}

执行完该语句,再去我们的数据库中查看,会发现多了一个表 schema_migrations

这个表就是用来存储版本号的,目前表中的版本号(version)为0。

下面是最重要的升级语句

BOOL resultState=NO; 
resultState=[manager migrateDatabaseToVersion:UINT64_MAX progress:nil error:&error];
//UINT64_MAX 表示升级到最高版本

整体的升级函数就是

FMDBMigrationManager * manager=[FMDBMigrationManager managerWithDatabaseAtPath:DBPath migrationsBundle:[NSBundle mainBundle]]; 
BOOL resultState=NO; 
NSError * error=nil;
 if (!manager.hasMigrationsTable) {
 resultState=[manager createMigrationsTable:&error]; 
}
 resultState=[manager migrateDatabaseToVersion:UINT64_MAX progress:nil error:&error];

此时还没有添加升级文件,所以执行完这段代码数据库并没有什么变化,下面添加升级文件。
所谓升级文件,就是一些sql文件,在里面写入一些对数据库操作的语句

文件名的格式是固定的 (数字)_(描述性语言).sql,前面的数字就是所谓的版本号,官方建议使用时间戳,也可以使用1,2,3,4,5……升级,保持单调递增即可。
文件内写入要对数据库做的操作

然后将文件拖入工程

FMDBMigrationManager 将会根据创建时给入的NSBundle自行寻找sql文件,对比版本号进行操作。添加sql文件之后,重启项目,再运行一次升级代码,查看数据库

发现新增了一个User表,再加入一个新增数据库字段的文件(工程中常用的升级操作就是增加数据库字段)

2_AddEmail.sql 的内容

重启项目运行升级代码,查看数据库

发现email已经添加进来了

存储的版本号也已经记录到2了,以后还想升级的话,再向项目中添加 3_*.sql 文件就好了。
下面使用第二种方法进行升级,使用自定义类的形式。第一种方法,每次升级都要建立一个文件,而且大多数时候,文件里面只写一句话,感情上会难以接受,因此我们介绍一种较为简洁的方式。首先定义一个新的类:Migration

遵循FMDBMigrating协议,与第一种拖入文件的方式相同,name是升级描述,version是版本号,最后一个方法里面,进行操作。
由于name和version都是只读的,因此我们要自定义一个init方法,传入描述 版本号和升级语句,升级语句最好用数组的方式传入,因为可能有多个升级语句。

//
//  Migration.h
//  Sqlite
//
//  Created by LC on 16/10/21.
//  Copyright © 2016年 LC. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "FMDBMigrationManager.h"
@interface Migration : NSObject<FMDBMigrating>

- (instancetype)initWithName:(NSString *)name andVersion:(uint64_t)version andExecuteUpdateArray:(NSArray *)updateArray;//自定义方法

@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) uint64_t version;
- (BOOL)migrateDatabase:(FMDatabase *)database error:(out NSError *__autoreleasing *)error;


@end

//
//  Migration.m
//  Sqlite
//
//  Created by LC on 16/10/21.
//  Copyright © 2016年 LC. All rights reserved.
//

#import "Migration.h"

@interface Migration()

@property(nonatomic,copy)NSString * myName;
@property(nonatomic,assign)uint64_t myVersion;
@property(nonatomic,strong)NSArray * updateArray;
@end


@implementation Migration

- (instancetype)initWithName:(NSString *)name andVersion:(uint64_t)version andExecuteUpdateArray:(NSArray *)updateArray
{
    if (self=[super init]) {
        _myName=name;
        _myVersion=version;
        _updateArray=updateArray;
    }
    return self;
}

- (NSString *)name
{
    return _myName;
}

- (uint64_t)version
{
    return _myVersion;
}

- (BOOL)migrateDatabase:(FMDatabase *)database error:(out NSError *__autoreleasing *)error
{
    for(NSString * updateStr in _updateArray)
    {
        [database executeUpdate:updateStr];
    }
    return YES;
}


@end

使用方法也很简单,将自定义类对象添加进manager即可。

NSString *DBPath = [documents stringByAppendingPathComponent:KFMDBName];
NSLog(@"%@",DBPath);
//DBPath是要升级的数据库的地址
// [NSBundle mainBundle]是保存数据库升级文件的位置 根据自己放文件的位置定 升级文件是什么下面会说
FMDBMigrationManager * manager = [FMDBMigrationManager managerWithDatabaseAtPath:DBPath migrationsBundle:[NSBundle mainBundle]];

Migration * migration_1=[[Migration alloc]initWithName:@"新增USer表" andVersion:1 andExecuteUpdateArray:@[@"create table User(name text,age integer,sex text,phoneNum text)"]];
[manager addMigration:migration_1];

Migration * migration_2=[[Migration alloc]initWithName:@"USer表新增字段email" andVersion:2 andExecuteUpdateArray:@[@"alter table User add email text"]];
[manager addMigration:migration_2];

BOOL resultState=NO;
NSError * error=nil;
if (!manager.hasMigrationsTable) {
    resultState=[manager createMigrationsTable:&error];
}

//UINT64_MAX 表示升级到最高版本
resultState=[manager migrateDatabaseToVersion:UINT64_MAX progress:nil error:&error];

NSLog(@"Has `schema_migrations` table?: %@", manager.hasMigrationsTable ? @"YES" : @"NO");
NSLog(@"Origin Version: %llu", manager.originVersion);
NSLog(@"Current version: %llu", manager.currentVersion);
NSLog(@"All migrations: %@", manager.migrations);
NSLog(@"Applied versions: %@", manager.appliedVersions);
NSLog(@"Pending versions: %@", manager.pendingVersions);

以后还想升级,在加入一个新的自定义对象,注意!!!版本号要保持递增

NSString *DBPath = [documents stringByAppendingPathComponent:KFMDBName];
NSLog(@"%@",DBPath);
//DBPath是要升级的数据库的地址
// [NSBundle mainBundle]是保存数据库升级文件的位置 根据自己放文件的位置定 升级文件是什么下面会说
FMDBMigrationManager * manager = [FMDBMigrationManager managerWithDatabaseAtPath:DBPath migrationsBundle:[NSBundle mainBundle]];

Migration * migration_1=[[Migration alloc]initWithName:@"新增USer表" andVersion:1 andExecuteUpdateArray:@[@"create table User(name text,age integer,sex text,phoneNum text)"]];
[manager addMigration:migration_1];

Migration * migration_2=[[Migration alloc]initWithName:@"USer表新增字段email" andVersion:2 andExecuteUpdateArray:@[@"alter table User add email text"]];
[manager addMigration:migration_2];

Migration * migration_3=[[Migration alloc]initWithName:@"USer表新增字段address" andVersion:3 andExecuteUpdateArray:@[@"alter table User add address text"]];
[manager addMigration:migration_3];

BOOL resultState=NO;
NSError * error=nil;
if (!manager.hasMigrationsTable) {
    resultState=[manager createMigrationsTable:&error];
}

//UINT64_MAX 表示升级到最高版本
resultState=[manager migrateDatabaseToVersion:UINT64_MAX progress:nil error:&error];


SQLite清空表数据并将自增ID设为0

SQL标准中有TRUNCATE TABLE语句,用来清空表的所有内容。但SQLite不支持这个语句。在SQLite中直接使用“DELETE FROM TableName”就可以了。对于大多数DBMS来说,用DELETE不如用TRUNCATE 速度快,因为TRUNCATE 不用访问整个表,不用记录数据的变动。

SQLite虽然不支持TRUNCATE,但它对DELETE做了优化:

“When the WHERE is omitted(略去) from a DELETE statement and the table being deleted has no triggers(触发器), SQLite uses an optimization(优化) to erase the entire table content without having to visit each row of the table individually. This “truncate” optimization makes the delete run much faster.”

通常在清空表的时候,还需要把自增列归零。在SQLite中定义自增列的方法如下:

CREATE TABLE TableName ( id INTEGER PRIMARY KEY AUTOINCREMENT, … );

当SQLite数据库中包含自增列时,会自动建立一个名为 sqlite_sequence 的表。这个表包含两个列:name和seq。name记录自增列所在的表,seq记录当前序号(下一条记录的编号就是当前序号加1)。如果想把某个自增列的序号归零,只需要修改 sqlite_sequence表就可以了。

UPDATE sqlite_sequence SET seq = 0 WHERE name = ‘TableName’;
也可以直接把该记录删掉:

DELETE FROM sqlite_sequence WHERE name = ‘TableName’;
要想将所有表的自增列都归零,直接清空sqlite_sequence表就可以了:

DELETE FROM sqlite_sequence;

自定义UISearchBar外观

From: http://www.jianshu.com/p/66b5b777f5dc

最近,在项目过程中遇到要自定义SearchBar的外观,虽然自己觉得用系统默认的外观就行了,不过UI设计师要求不用系统的默认样式,要跟app主题保持一致。

图1:设计效果图

图1:设计效果图

从上图可以看出,我们要做的UISearchBar要有圆角,边框颜色,取消按钮颜色,背景透明等等。

开始以为可能要自己写一个自定义的UISearchBar控件了,后面研究了一番,发现可以设定系统UISearchBar属性来更改,便把经验记录下来跟大家分享一下。

首先,我们看下系统默认的SearchBar的样式,离我们的目标样式确实相差很大, 后面我会一步一步详细说明做成我们的目标样式。

图2:UISearchBar默认样式

图2:UISearchBar默认样式

1. 设置背景色

我以白色的背景色为例,下面看看代码:

//1. 设置背景颜色
    //设置背景图是为了去掉上下黑线
    self.customSearchBar.backgroundImage = [[UIImage alloc] init];
    // 设置SearchBar的颜色主题为白色
    self.customSearchBar.barTintColor = [UIColor whiteColor];

图3:设置SearchBar背景色为白色

图3:设置SearchBar背景色为白色

2. 设置边框颜色和圆角

//2. 设置圆角和边框颜色
    UITextField *searchField = [self.customSearchBar valueForKey:@"searchField"];
    if (searchField) {
        [searchField setBackgroundColor:[UIColor whiteColor]];
        searchField.layer.cornerRadius = 14.0f;
        searchField.layer.borderColor = [UIColor colorWithRed:247/255.0 green:75/255.0 blue:31/255.0 alpha:1].CGColor;
        searchField.layer.borderWidth = 1;
        searchField.layer.masksToBounds = YES;
    }

这段代码有个特别的地方就是通过KVC获得到UISearchBar的私有变量
searchField(类型为UITextField),设置SearchBar的边框颜色和圆角实际上也就变成了设置searchField的边框颜色和圆角,你可以试试直接设置SearchBar.layer.borderColor和cornerRadius,会发现这样做是有问题的。

图4:设置边框颜色和圆角

图4:设置边框颜色和圆角

嗯,离预期效果越来越近了!

3. 设置按钮(取消按钮)的文字和文字颜色

//3. 设置按钮文字和颜色
    [self.customSearchBar fm_setCancelButtonTitle:@"取消"];
    self.customSearchBar.tintColor = [UIColor colorWithRed:86/255.0 green:179/255.0 blue:11/255.0 alpha:1];
    //修正光标颜色
    [searchField setTintColor:[UIColor blackColor]];

//其中fm_setCancelButtonTitle是我写的UISearchBar一个分类的方法
- (void)fm_setCancelButtonTitle:(NSString *)title {
    if (IS_IOS9) {
        [[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UISearchBar class]]] setTitle:title];
    }else {
        [[UIBarButtonItem appearanceWhenContainedIn:[UISearchBar class], nil] setTitle:title];
    }
}

图5:设置按钮文字和颜色

图5:设置按钮文字和颜色

需要特别注意的是设置searchBar的tintColor会使输入框的光标颜色改变,可以通过设置searchField的tintColor来修正。

4. 设置输入框的文字颜色和字体

//4. 设置输入框文字颜色和字体
    [self.customSearchBar fm_setTextColor:[UIColor blackColor]];
    [self.customSearchBar fm_setTextFont:[UIFont systemFontOfSize:14]];

//下面两个方法是UISearchBar分类代码
- (void)fm_setTextColor:(UIColor *)textColor {
    if (IS_IOS9) {
        [UITextField appearanceWhenContainedInInstancesOfClasses:@[[UISearchBar class]]].textColor = textColor;
    }else {
        [[UITextField appearanceWhenContainedIn:[UISearchBar class], nil] setTextColor:textColor];
    }
}

- (void)fm_setCancelButtonTitle:(NSString *)title {
    if (IS_IOS9) {
        [[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UISearchBar class]]] setTitle:title];
    }else {
        [[UIBarButtonItem appearanceWhenContainedIn:[UISearchBar class], nil] setTitle:title];
    }
}

图6:最终对比效果图

图6:最终对比效果图

5. 如何设置搜索图标

下面评论中有简友问我怎么更改默认的搜索图标,我查了下UISearchBar的API,发现有方法可以更改的。

//5. 设置搜索Icon
    [self.customSearchBar setImage:[UIImage imageNamed:@"Search_Icon"]
                  forSearchBarIcon:UISearchBarIconSearch
                             state:UIControlStateNormal];

为了跟系统默认Icon的有个明显的对比,我特殊找了张绿色的搜索Icon,效果见下图:

设置搜索Icon.png

设置搜索Icon.png

Tips: 还可以设置其他的Icon(如清除按钮图标),也是用上面的方法,具体要设置什么,可以去看看UISearchBarIcon这个枚举。

2016-10-20新增

6.实现类似微信的搜索框

图8:提问.png

图8:提问.png

这里有位简友问到怎么实现类似微信的搜索框,下面我将详细说说我的思路。

首先,要告诉大家的是 UISearchBar 是一个由多个控件组合成的比较复杂的控件。用Reveal查看 UISearchBar 组成如下 :

图9:UISearchBar组成图.png

图9:UISearchBar组成图.png

从上图可以看出UISearch的组成结构,下面我总结了一张思维导图,更加的清晰直观:

图10:UISearchBar思维导图.png

图10:UISearchBar思维导图.png

UISearchBar 的主要部分是 UISearchBarTextFieldUISearchBarTextField 又包含好几个subView,其中那个 UIButton 指的是清除按钮(输入文字时会出现)。

好了,上面说了一堆,其实我主要是想表达:既然 UISearchBar 是由这么多个子控件组成,我再往里面加一个按钮又何妨?

最终解决思路就是这样了:在 UISearchBarTextField 添加一个 UIButton,我暂且将它叫做 voiceButton,然后 voiceButton 设置好自动布局以及点击事件处理(点击按钮后,显示录音界面…),最后还要监控文本的变化,有输入了文本时,voiceButton 隐藏,没有文本时, 显示 voiceButton

所有代码如下:

//6. 实现类似微信的搜索框
    UIButton *voiceButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [voiceButton setImage:[UIImage imageNamed:@"Voice_button_icon"] forState:UIControlStateNormal];
    [voiceButton addTarget:self action:@selector(tapVoiceButton:) forControlEvents:UIControlEventTouchUpInside];
    [searchField addSubview:voiceButton];
    self.voiceButton = voiceButton;

    //Autolayout
    voiceButton.translatesAutoresizingMaskIntoConstraints = NO;
    NSDictionary *views = NSDictionaryOfVariableBindings(voiceButton);
    //设置水平方向约束
    [searchField addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[voiceButton(21)]-|" options:NSLayoutFormatAlignAllRight | NSLayoutFormatAlignAllLeft metrics:nil views:views]];
    //设置高度约束
    [searchField addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[voiceButton(21)]" options:NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom metrics:nil views:views]];
    //设置垂直方向居中约束
    [searchField addConstraint:[NSLayoutConstraint constraintWithItem:voiceButton attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:searchField attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];

//按钮触摸事件
- (IBAction)tapVoiceButton:(id)sender {
    NSLog(@"Tap voiceButton");
}

//监控文本变化
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
    self.voiceButton.hidden = searchText.length > 0;
}

好了,打完收工,最终效果图如下:

record.gif

完整代码在这里

本机架设一个RTMP直播流服务器

为了演示直播推流工程, 我们来自己快速架设一个简易的RTMP服务器,用OBS推流,并在播放器中播放,基于Nginx和rtmp模块。
步骤很简单:

  • 安装Homebrew包管理器,以便安装Nginx。
  • 配置服务器的rtmp端口和路径。
  • 用OBS来推流,安装VLC播放器来播放rtmp串流。

详细步骤如下:
1.①如果你安装过cocoapods,就应该会安装好Homebrew,那就跳过此步。如果不确定是否已经安装,可以在终端用man homebrew命令来确定,如果回馈一堆使用说明,那就是安装好了。 否则请用以下命令安装:

1
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

卸载Homebrew:

1
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"

②下载Homebrew上的Nginx项目到本地:

1
brew tap homebrew/nginx

③安装rtmp模块:

brew install nginx-full --with-rtmp-module

④启动Nginx服务器:

nginx (终端)

⑤验证启动(在浏览器中打开以下网址)

http://localhost:8080

如果看到 Welcome to nginx字样,说明服务器启动成功了,成功了一半,oh,yeah!

注 第一个步骤内可能有权限不够,可以按照命令失败的提示修正一下。或者按照提示google 下都有答案

2.①显示nginx的的使用说明

1
brew info nginx-full

②修改 nginx.conf, 一般在

1
/usr/local/etc/nginx/nginx.conf

③配置rtmp:
用任意的文本编辑器打开

1
/usr/local/etc/nginx/nginx.conf

在文件的末尾加上以下并保存:

1
2
3
4
5
6
7
8
rtmp {
server {
listen 1935;
application rtmplive {
live on;
}
}
}

这是一个最小化配置的,在端口1935监听的rtmp服务,名叫rtmplive,启用了直播(live on)。
详见
④重启服务器使配置生效:

1
/usr/local/Cellar/nginx-full/1.10.2/bin/nginx -s reload

这其中的1.10.2是当前的nginx版本. 可以用以下命令查看当前的版本:

1
nginx -v

3.①安装OBS Mac版本,在 设置 > 串流 > 自定义流媒体服务器 > URL 中填入

1
rtmp://192.168.166.172:1935/rtmplive/

确定后退出设置。设置直播场景后,点击“开始串流”,这时OBS状态栏应该可以看到连接状态和推流速率。
②百度一下VLC,安装Mac版本。
③菜单File > Open Network… > 输入:

1
rtmp://192.168.166.172:1935/rtmplive/

把这里的192.168.166.172替换成你自己电脑对外的IP,也就是其他电脑可以访问的IP。

不仅OBS可以用来推流, iPhone/安卓机也可以利用这个自建服务器来推流了。

测试环境搭建

转自这里

//2017-5-4 SRS 服务器搭建

下载

1
2
3
$ git clone https://github.com/ossrs/srs
$ cd srs/trunk
mac 下 和centos 下

编译

1
2
// 支持 hls,flv,rtmp转码,http-server,http-api
$ ./configure --with-ssl --with-http-api --with-hls --with-http-server --with-nginx --with-ffmpeg --with-transcode && make

配置:

我用的是2.0 版本
参考https://github.com/ossrs/srs/wiki/v2_CN_Home
在查看
启动 :
先编辑配置文件,按照每个的单独的配置文件集成到conf/srs.conf
从hls.conf 和http.live.flv.conf 上获取配置数据.

1
2
3
4
5
检查配置文件
./objs/srs -t -c conf/xx.conf
启动
./etc/init.d/srs start

查看当前服务端stream列表

1
http:xxx:9090/api/v1/streams/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"code": 0,
"server": 12323,
"streams": [
{
"id": 12325,
"name": "h5",
"vhost": 12324,
"app": "live",
"live_ms": 1482371950909,
"clients": 1,
"send_bytes": 12576,
"recv_bytes": 29070803,
"kbps": {
"recv_30s": 0,
"send_30s": 0
},
"publish": {
"active": true, // 表示是否在线
"cid": 166
},
"video": {
"codec": "H264",
"profile": "High(422)",
"level": "Other"
},
"audio": null
}
]
}

iOS-Block的详解

iOS-Block的详解
From: http://www.jianshu.com/p/007632bbb01d

学习Block的感悟

Block

一. iOS代码块Block

1.1 概述

代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,Block是一种特殊的数据类型,其可以正常定义变量、作为参数、作为返回值,特殊地,Block还可以保存一段代码,在需要的时候调用,目前Block已经广泛应用于iOS开发中,常用于GCD、动画、排序及各类回调

注: Block的声明与赋值只是保存了一段代码段,必须调用才能执行内部代码

1.2 Block变量的声明、赋值与调用
#1.2.1 Block变量的声明
Block变量的声明格式为: 返回值类型(^Block名字)(参数列表);

// 声明一个无返回值,参数为两个字符串对象,叫做aBlock的Block
void(^aBlock)(NSString *x, NSString *y);

// 形参变量名称可以省略,只留有变量类型即可
void(^aBlock)(NSString *, NSString *);
#1.2.2 Block变量的赋值
Block变量的赋值格式为: Block变量 = ^(参数列表){函数体};

aBlock = ^(NSString *x, NSString *y){
    NSLog(@"%@ love %@", x, y);
};

注: Block变量的赋值格式可以是: Block变量 = ^返回值类型(参数列表){函数体};,不过通常情况下都将返回值类型省略,
因为编译器可以从存储代码块的变量中确定返回值的类型
#1.2.3 声明Block变量的同时进行赋值
int(^myBlock)(int) = ^(int num){
    return num * 7;
};

// 如果没有参数列表,在赋值时参数列表可以省略
void(^aVoidBlock)() = ^{
    NSLog(@"I am a aVoidBlock");
};
#1.2.4 Block变量的调用
// 调用后控制台输出"Li Lei love Han Meimei"
aBlock(@"Li Lei",@"Han Meimei");

// 调用后控制台输出"result = 63"
NSLog(@"result = %d", myBlock(9));

// 调用后控制台输出"I am a aVoidBlock"
aVoidBlock();

二. 使用typedef定义Block类型

在实际使用Block的过程中,我们可能需要重复地声明多个相同返回值相同参数列表的Block变量,如果总是重复地编写一长串代码来声明变量会非常繁琐,所以我们可以使用typedef来定义Block类型

// 定义一种无返回值无参数列表的Block类型
typedef void(^SayHello)();

// 我们可以像OC中声明变量一样使用Block类型SayHello来声明变量
SayHello hello = ^(){
    NSLog(@"hello");
};

// 调用后控制台输出"hello"
hello();

三. Block作为函数参数

3.1 Block作为C函数参数
// 1.定义一个形参为Block的C函数
void useBlockForC(int(^aBlock)(int, int))
{
    NSLog(@"result = %d", aBlock(300,200));
}

// 2.声明并赋值定义一个Block变量
int(^addBlock)(int, int) = ^(int x, int y){
    return x+y;
};

// 3.以Block作为函数参数,把Block像对象一样传递
useBlockForC(addBlock);

// 将第2点和第3点合并一起,以内联定义的Block作为函数参数
useBlockForC(^(int x, int y) {
    return x+y;
});
3.2 Block作为OC函数参数
// 1.定义一个形参为Block的OC函数
- (void)useBlockForOC:(int(^)(int, int))aBlock
{
    NSLog(@"result = %d", aBlock(300,200));
}

// 2.声明并赋值定义一个Block变量
int(^addBlock)(int, int) = ^(int x, int y){
    return x+y;
};

// 3.以Block作为函数参数,把Block像对象一样传递
[self useBlockForOC:addBlock];

// 将第2点和第3点合并一起,以内联定义的Block作为函数参数
[self useBlockForOC:^(int x, int y){
    return x+y;
}];
3.3 使用typedef简化Block
// 1.使用typedef定义Block类型
typedef int(^MyBlock)(int, int);

// 2.定义一个形参为Block的OC函数
- (void)useBlockForOC:(MyBlock)aBlock
{
    NSLog(@"result = %d", aBlock(300,200));
}

// 3.声明并赋值定义一个Block变量
MyBlock addBlock = ^(int x, int y){
    return x+y;
};

// 4.以Block作为函数参数,把Block像对象一样传递
[self useBlockForOC:addBlock];

// 将第3点和第4点合并一起,以内联定义的Block作为函数参数
[self useBlockForOC:^(int x, int y){
    return x+y;
}];

四 Block内访问局部变量

4.1 在Block中可以访问局部变量
// 声明局部变量global
int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 100"
myBlock();
4.2 在声明Block之后、调用Block之前对局部变量进行修改,在调用Block时局部变量值是修改之前的旧值
// 声明局部变量global
int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
global = 101;
// 调用后控制台输出"global = 100"
myBlock();
4.3 在Block中不可以直接修改局部变量
// 声明局部变量global
int global = 100;

void(^myBlock)() = ^{
    global ++; // 这句报错
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 100"
myBlock();

五. Block内访问__block修饰的局部变量

5.1 在局部变量前使用下划线下划线block修饰,在声明Block之后、调用Block之前对局部变量进行修改,在调用Block时局部变量值是修改之后的新值
// 声明局部变量global
__block int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
global = 101;
// 调用后控制台输出"global = 101"
myBlock();
5.2 在局部变量前使用下划线下划线block修饰,在Block中可以直接修改局部变量
// 声明局部变量global
__block int global = 100;

void(^myBlock)() = ^{
    global ++; // 这句正确
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 101"
myBlock();

六. Block内访问全局变量

6.1 在Block中可以访问全局变量
// 声明全局变量global
int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 100"
myBlock();
6.2 在声明Block之后、调用Block之前对全局变量进行修改,在调用Block时全局变量值是修改之后的新值
// 声明全局变量global
int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
global = 101;
// 调用后控制台输出"global = 101"
myBlock();
6.3 在Block中可以直接修改全局变量
// 声明全局变量global
int global = 100;

void(^myBlock)() = ^{
    global ++;
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 101"
myBlock();

七. Block内访问静态变量

7.1 在Block中可以访问静态变量
// 声明静态变量global
static int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 100"
myBlock();
7.2 在声明Block之后、调用Block之前对静态变量进行修改,在调用Block时静态变量值是修改之后的新值
// 声明静态变量global
static int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
global = 101;
// 调用后控制台输出"global = 101"
myBlock();
7.3 在Block中可以直接修改静态变量
// 声明静态变量global
static int global = 100;

void(^myBlock)() = ^{
    global ++;
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 101"
myBlock();

8. Block在MRC及ARC下的内存管理

8.1 Block在MRC下的内存管理
默认情况下,Block的内存存储在栈中,不需要开发人员对其进行内存管理

// 放Block变量出了作用域,Block的内存会被自动释放
void(^myBlock)() = ^{
    NSLog(@"------");
};
myBlock();

在Block的内存存储在栈中时,如果在Block中引用了外面的对象,不会对所引用的对象进行任何操作

Person *p = [[Person alloc] init];

void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();

[p release]; // Person对象在这里可以正常被释放

如果对Block进行一次copy操作,那么Block的内存会被移动到堆中,这时需要开发人员对其进行release操作来管理内存

void(^myBlock)() = ^{
    NSLog(@"------");
};
myBlock();

Block_copy(myBlock);

// do something ...

Block_release(myBlock);

如果对Block进行一次copy操作,那么Block的内存会被移动到堆中,在Block的内存存储在堆中时,如果在Block中引用了外面的对象,会对所引用的对象进行一次retain操作,即使在Block自身调用了release操作之后,Block也不会对所引用的对象进行一次release操作,这时会造成内存泄漏

Person *p = [[Person alloc] init];

void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();

Block_copy(myBlock);

// do something ...

Block_release(myBlock);

[p release]; // Person对象在这里无法正常被释放,因为其在Block中被进行了一次retain操作

如果对Block进行一次copy操作,那么Block的内存会被移动到堆中,在Block的内存存储在堆中时,如果在Block中引用了外面的对象,会对所引用的对象进行一次retain操作,为了不对所引用的对象进行一次retain操作,可以在对象的前面使用下划线下划线block来修饰

__block Person *p = [[Person alloc] init];

void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();

Block_copy(myBlock);

// do something ...

Block_release(myBlock);

[p release]; // Person对象在这里可以正常被释放

如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用

  • 情况一

    @interface Person : NSObject

    @property (nonatomic, copy) void(^myBlock)();

    @end

@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");

    Block_release(_myBlock);
    [super dealloc];
}

@end


Person *p = [[Person alloc] init];

p.myBlock = ^{
    NSLog(@"------%@", p);
};
p.myBlock();

[p release]; // 因为myBlock作为Person的属性,采用copy修饰符修饰(这样才能保证Block在堆里面,以免Block在栈中被系统释放),所以Block会对Person对象进行一次retain操作,导致循环引用无法释放
  • 情况二

    @interface Person : NSObject

    @property (nonatomic, copy) void(^myBlock)();

    • (void)resetBlock;

    @end

@implementation Person

- (void)resetBlock
{
    self.myBlock = ^{
        NSLog(@"------%@", self);
    };
}

- (void)dealloc
{
    NSLog(@"Person dealloc");

    Block_release(_myBlock);

    [super dealloc];
}

@end


Person *p = [[Person alloc] init];
[p resetBlock];
[p release]; // Person对象在这里无法正常释放,虽然表面看起来一个alloc对应一个release符合内存管理规则,但是实际在resetBlock方法实现中,Block内部对self进行了一次retain操作,导致循环引用无法释放

如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用,解决循环引用的办法是在对象的前面使用下划线下划线block来修饰,以避免Block对对象进行retain操作

情况一

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

@end


@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");

    Block_release(_myBlock);
    [super dealloc];
}

@end


__block Person *p = [[Person alloc] init];

p.myBlock = ^{
    NSLog(@"------%@", p);
};
p.myBlock();

[p release]; // Person对象在这里可以正常被释放

情况二

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

- (void)resetBlock;

@end


@implementation Person

- (void)resetBlock
{
    // 这里为了通用一点,可以使用__block typeof(self) p = self;
    __block Person *p = self;
    self.myBlock = ^{
        NSLog(@"------%@", p);
    };
}

- (void)dealloc
{
    NSLog(@"Person dealloc");

    Block_release(_myBlock);

    [super dealloc];
}

@end


Person *p = [[Person alloc] init];
[p resetBlock];
[p release]; // Person对象在这里可以正常被释放

九. Block在ARC下的内存管理

9.1 在ARC默认情况下,Block的内存存储在堆中,ARC会自动进行内存管理,程序员只需要避免循环引用即可
// 放Block变量出了作用域,Block的内存会被自动释放
void(^myBlock)() = ^{
    NSLog(@"------");
};
myBlock();

在Block的内存存储在堆中时,如果在Block中引用了外面的对象,会对所引用的对象进行强引用,但是在Block被释放时会自动去掉对该对象的强引用,所以不会造成内存泄漏

Person *p = [[Person alloc] init];

void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();

// Person对象在这里可以正常被释放
9.2
如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用

情况一

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

@end


@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");
}

@end


Person *p = [[Person alloc] init];

p.myBlock = ^{
    NSLog(@"------%@", p);
};
p.myBlock();

// 因为myBlock作为Person的属性,采用copy修饰符修饰(这样才能保证Block在堆里面,以免Block在栈中被系统释放),所以Block会对Person对象进行一次强引用,导致循环引用无法释放

情况二

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

- (void)resetBlock;

@end


@implementation Person

- (void)resetBlock
{
    self.myBlock = ^{
        NSLog(@"------%@", self);
    };
}

- (void)dealloc
{
    NSLog(@"Person dealloc");
}

@end


Person *p = [[Person alloc] init];
[p resetBlock];

// Person对象在这里无法正常释放,在resetBlock方法实现中,Block内部对self进行了一次强引用,导致循环引用

如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用,解决循环引用的办法是使用一个弱引用的指针指向该对象,然后在Block内部使用该弱引用指针来进行操作,这样避免了Block对对象进行强引用

情况一

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

@end


@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");
}

@end


Person *p = [[Person alloc] init];
__weak typeof(p) weakP = p;

p.myBlock = ^{
    NSLog(@"------%@", weakP);
};
p.myBlock();

// Person对象在这里可以正常被释放

情况二

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

- (void)resetBlock;

@end


@implementation Person

- (void)resetBlock
{
    // 这里为了通用一点,可以使用__weak typeof(self) weakP = self;
    __weak Person *weakP = self;
    self.myBlock = ^{
        NSLog(@"------%@", weakP);
    };
}

- (void)dealloc
{
    NSLog(@"Person dealloc");
}

@end


Person *p = [[Person alloc] init];
[p resetBlock];

// Person对象在这里可以正常被释放

十. Block在ARC下的内存管理的官方案例

在MRC中,我们从当前控制器采用模态视图方式present进入MyViewController控制器,在Block中会对myViewController进行一次retain操作,造成循环引用

MyViewController *myController = [[MyViewController alloc] init];
// ...
myController.completionHandler =  ^(NSInteger result) {
   [myController dismissViewControllerAnimated:YES completion:nil];
};
[self presentViewController:myController animated:YES completion:^{
   [myController release];
}];

在MRC中解决循环引用的办法即在变量前使用下划线下划线block修饰,禁止Block对所引用的对象进行retain操作

__block MyViewController *myController = [[MyViewController alloc] init];
// ...
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
};
[self presentViewController:myController animated:YES completion:^{
   [myController release];
}];

但是上述方法在ARC下行不通,因为下划线下划线block在ARC中并不能禁止Block对所引用的对象进行强引用,解决办法可以是在Block中将myController置空(为了可以修改myController,还是需要使用下划线下划线block对变量进行修饰)

__block MyViewController *myController = [[MyViewController alloc] init];
// ...
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
    myController = nil;
};
[self presentViewController:myController animated:YES completion:^{}];

上述方法确实可以解决循环引用,但是在ARC中还有更优雅的解决办法,新创建一个弱指针来指向该对象,并将该弱指针放在Block中使用,这样Block便不会造成循环引用

MyViewController *myController = [[MyViewController alloc] init];
// ...
__weak MyViewController *weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    [weakMyController dismissViewControllerAnimated:YES completion:nil];
};
[self presentViewController:myController animated:YES completion:^{}];

虽然解决了循环引用,但是也容易涉及到另一个问题,因为Block是通过弱引用指向了myController对象,那么有可能在调用Block之前myController对象便已经被释放了,所以我们需要在Block内部再定义一个强指针来指向myController对象

MyViewController *myController = [[MyViewController alloc] init];
// ...
__weak MyViewController *weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;
    if (strongMyController)
    {
        [strongMyController dismissViewControllerAnimated:YES completion:nil];
    }
    else
    {
        // Probably nothing...
    }
};
[self presentViewController:myController animated:YES completion:^{}];

这里需要补充一下,在Block内部定义的变量,会在作用域结束时自动释放,Block对其并没有强引用关系,且在ARC中只需要避免循环引用即可,如果只是Block单方面地对外部变量进行强引用,并不会造成内存泄漏

注: 关于下划线下划线block关键字在MRC和ARC下的不同

__block在MRC下有两个作用
1. 允许在Block中访问和修改局部变量 
2. 禁止Block对所引用的对象进行隐式retain操作

__block在ARC下只有一个作用
1. 允许在Block中访问和修改局部变量

小笨狼

iOS 开发之照片框架详解之二 —— PhotoKit 详解(上)

iOS 开发之照片框架详解之二 —— PhotoKit 详解(上)

一. 概况

这里引用一下前文中对 PhotoKit 基本构成的介绍:

  • PHAsset: 代表照片库中的一个资源,跟 ALAsset 类似,通过 PHAsset 可以获取和保存资源
  • PHFetchOptions: 获取资源时的参数,可以传 nil,即使用系统默认值
  • PHAssetCollection: PHCollection 的子类,表示一个相册或者一个时刻,或者是一个「智能相册(系统提供的特定的一系列相册,例如:最近删除,视频列表,收藏等等,如下图所示)
  • PHFetchResult: 表示一系列的资源结果集合,也可以是相册的集合,从?PHCollection 的类方法中获得
  • PHImageManager: 用于处理资源的加载,加载图片的过程带有缓存处理,可以通过传入一个 PHImageRequestOptions 控制资源的输出尺寸等规格
  • PHImageRequestOptions: 如上面所说,控制加载图片时的一系列参数

这里还有一个额外的概念?PHCollectionList,表示一组?PHCollection,它本身也是一个?PHCollection,因此?PHCollection 作为一个集合,可以包含其他集合,这使到 PhotoKit 的组成比 ALAssetLibrary 要复杂一些。另外与 ALAssetLibrary 相似,一个 PHAsset 可以同时属于多个不同的 PHAssetCollection,最常见的例子就是刚刚拍摄的照片,至少同时属于“最近添加”、“相机胶卷”以及“照片 - 精选”这三个 PHAssetCollection。关于这几个概念的关系如下图:

二. ?PhotoKit 的机制

  1. 获取资源

在 ALAssetLibrary 中获取数据,无论是相册,还是资源,本质上都是使用枚举的方式,遍历照片库取得相应的数据,并且数据是从 ALAssetLibrary(照片库) - ALAssetGroup(相册)- ALAsset(资源)这一路径逐层获取,即使有直接从 ALAssetLibrary 这一层获取 ALAsset 的接口,本质上也是枚举 ALAssetLibrary 所得,并不是直接获取,这样的好处很明显,就是非常符合实际应用中资源的显示路径:照片库 - 相册 - 图片或视频,但由于采用枚举的方式获取资源,效率低而且不灵活。

而在 PhotoKit 中,则是采用“获取”的方式拉取资源,这些获取的手段,都是一系列形如 class func fetchXXX(…, options: PHFetchOptions) -> PHFetchResult 的类方法,具体使用哪个类方法,则视乎需要获取的是相册、时刻还是资源,这类方法中的 option 充当了过滤器的作用,可以过滤相册的类型,日期,名称等,从而直接获取对应的资源而不需要枚举。例如在前文中列举个的几个小例子:

1
2
3
4
5
6
7
8
9
10
// 列出所有相册智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
// 列出所有用户创建的相册
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
// 获取所有资源的集合,并按资源的创建时间排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];

如前面提到过的那样,从?PHAssetCollection 获取中获取到的可以是相册也可以是资源,但无论是哪种内容,都统一使用?PHFetchResult 对象封装起来,因此虽然 PHAssetCollection 获取到的结果可能是多样的,但通过?PHFetchResult 就可以使用统一的方法去处理这些内容(即遍历 PHFetchResult)。例如扩展上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 列出所有相册智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
// 这时 smartAlbums 中保存的应该是各个智能相册对应的 PHAssetCollection
for (NSInteger i = 0; i < fetchResult.count; i++) {
// 获取一个相册(PHAssetCollection)
PHCollection *collection = fetchResult[i];
if ([collection isKindOfClass:[PHAssetCollection class]]) {
PHAssetCollection *assetCollection = (PHAssetCollection *)collection;
// 从每一个智能相册中获取到的 PHFetchResult 中包含的才是真正的资源(PHAsset)
PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions];
else {
NSAssert(NO, @"Fetch collection not PHCollection: %@", collection);
}
}
// 获取所有资源的集合,并按资源的创建时间排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];
// 这时 assetsFetchResults 中包含的,应该就是各个资源(PHAsset)
for (NSInteger i = 0; i < fetchResult.count; i++) {
// 获取一个资源(PHAsset)
PHAsset *asset = fetchResult[i];
}

?2. 获取图像的方式与坑点

经过了上面的步骤,已经可以了解到如何在 PhotoKit 中获取到代表资源的 PHAsset 了,但与 ALAssetLibrary 中从 ALAsset 中直接获取图像的方式不同,PhotoKit 无法直接从 PHAsset 的实例中获取图像,而是引入了一个管理器?PHImageManager 获取图像。PHImageManager 是通过请求的方式拉取图像,并可以控制请求得到的图像的尺寸、剪裁方式、质量,缓存以及请求本身的管理(发出请求、取消请求)等。而请求图像的方法是 ?PHImageManager 的一个实例方法:

1
2
3
4
5
- (PHImageRequestID)requestImageForAsset:(PHAsset *)asset
targetSize:(CGSize)targetSize
contentMode:(PHImageContentMode)contentMode
options:(nullable PHImageRequestOptions *)options
resultHandler:(void (^)(UIImage *__nullable result, NSDictionary *__nullable info))resultHandler;

这个方法中的参数坑点不少,下面逐个参数列举一下其作用及坑点:

  • asset,图像对应的 PHAsset。
  • targetSize,需要获取的图像的尺寸,如果输入的尺寸大于资源原图的尺寸,则只返回原图。需要注意在 PHImageManager 中,所有的尺寸都是用 Pixel 作为单位(Note that all sizes are in pixels),因此这里想要获得正确大小的图像,需要把输入的尺寸转换为 Pixel。如果需要返回原图尺寸,可以传入 PhotoKit 中预先定义好的常量?PHImageManagerMaximumSize,表示返回可选范围内的最大的尺寸,即原图尺寸。
  • contentMode,图像的剪裁方式,与?UIView 的 contentMode 参数相似,控制照片应该以按比例缩放还是按比例填充的方式放到最终展示的容器内。注意如果 targetSize 传入?PHImageManagerMaximumSize,则 contentMode 无论传入什么值都会被视为?PHImageContentModeDefault。
  • options,一个?PHImageRequestOptions 的实例,可以控制的内容相当丰富,包括图像的质量、版本,也会有参数控制图像的剪裁,下面再展开说明。
  • resultHandler,请求结束后被调用的 block,返回一个包含资源对于图像的 UIImage 和包含图像信息的一个 NSDictionary,在整个请求的周期中,这个 block 可能会被多次调用,关于这点连同 options 参数在下面展开说明。

(1)PHImageRequestOptions 与 iCloud 照片库

PHImageRequestOptions 中包含了一系列控制请求图像的属性。

resizeMode 属性控制图像的剪裁,不知道为什么 PhotoKit 会在请求图像方法(requestImageForAsset)中已经有控制图像剪裁的参数后(contentMode),还在 options 中加入控制剪裁的属性,但如果两个地方所控制的剪裁结果有所冲突,PhotoKit 会以 resizeMode 的结果为准。另外,resizeMode 也有控制图像质量的作用。如?resizeMode?设置为?PHImageRequestOptionsResizeModeExact 则返回图像必须和目标大小相匹配,并且图像质量也为高质量图像,而设置为 PHImageRequestOptionsResizeModeFast 则请求的效率更高,但返回的图像可能和目标大小不一样并且质量较低。

在 PhotoKit 中,对 iCloud 照片库有很好的支持,如果用户开启了 iCloud 照片库,并且选择了“优化 iPhone/iPad 储存空间”,或者选择了“下载并保留原件”但原件还没有加载好的时候,PhotoKit 也会预先拿到这些非本地图像的 PHAsset,但是由于本地并没有原图,所以如果产生了请求高清图的请求,PHotoKit 会尝试从 iCloud 下载图片,而这个行为最终的表现,会被?PHImageRequestOptions 中的值所影响。PHImageRequestOptions 中常常会用的几个属性如下:

networkAccessAllowed 参数控制是否允许网络请求,默认为 NO,如果不允许网络请求,那么就没有然后了,当然也拉取不到 iCloud 的图像原件。deliveryMode 则用于控制请求的图片质量。synchronous 控制是否为同步请求,默认为 NO,如果?synchronous 为 YES,即同步请求时,deliveryMode 会被视为 PHImageRequestOptionsDeliveryModeHighQualityFormat,即自动返回高质量的图片,因此不建议使用同步请求,否则如果界面需要等待返回的图像才能进一步作出反应,则反应时长会很长。

还有一个与 iCloud 密切相关的属性?progressHandler,当图像需要从 iCloud 下载时,这个 block 会被自动调用,block 中会返回图像下载的进度,图像的信息,出错信息。开发者可以利用这些信息反馈给用户当前图像的下载进度以及状况,但需要注意?progressHandler 不在主线程上执行,因此在其中需要操作 UI,则需要手工放到主线程执行。

上面有提到,requestImageForAsset 中的参数?resultHandler 可能会被多次调用,这种情况就是图像需要从 iCloud 中下载的情况。在?requestImageForAsset 返回的内容中,一开始的那一次请求中会返回一个小尺寸的图像版本,当高清图像还在下载时,开发者可以首先给用户展示这个低清的图像版本,然后 block 在多次调用后,最终会返回高清的原图。至于当前返回的图像是哪个版本的图像,可以通过 block 返回的 NSDictionary info 中获知,PHImageResultIsDegradedKey 表示当前返回的 UIImage 是低清图。如果需要判断是否已经获得高清图,可以这样判断:

1
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];

另外,当我们使用?requestImageForAsset 发出对图像的请求时,如果在同一个 PHImageManager 中同时对同一个资源发出图像请求,请求的进度是可以共享的,因此我们可以利用这个特性,把 PHImageManager 以单例的形式使用,这样在切换界面时也不用担心无法传递图像的下载进度。例如,在图像的列表页面触发了下载图像,当我们离开列表页面进入预览大图界面时,并不用担心会重新图像会重新下载,只要没有手工取消图像下载,进入预览大图界面下载图像会自动继续从上次的进度下载图像。

如果希望取消下载图像,则可以使用?PHImageManager 的 ?cancelImageRequest 方法,它传入的是请求图像的请求 ID,这个 ID 可以从?requestImageForAsset 的返回值中获得,也可以从前面提到的包含图像信息的?NSDictionary info 中获得,当然前提是这个这个接收取消请求的 PHImageManager 与刚刚发出请求的 PHImageManager 是同一个实例,如上面所述使用单例是最为简单有效的方式。

最后,还要介绍一个?PHImageRequestOptions 的属性 versions,这个属性是指获取的图像是否需要包含系统相册“编辑”功能处理过的信息(如滤镜,旋转等),这一点比 ALAssetLibrary 要灵活很多,ALAssetLibrary 中并不能灵活地控制获取的图像是否带有“编辑”处理过的效果,例如在 ALAsset 中获取原图的接口 fullResolutionImage 获取到的是不带“编辑”效果的图像,要想获取带有“编辑”效果的图像,只能自行处理获取这些滤镜效果,并手工叠加上去。在我们的 UI 框架 QMUI 中就有对获取原图作出这样的封装,整个过程也较为繁琐,而框架中处理 PhotoKit 的部分则灵活很多,这也体现了 PhotoKit 相比 ALAssetLibrary 的最主要特点——复杂但灵活。文章的第三部分也会详细列出如何处理这个问题。

(2)获取图像的优化

PHImageManager 提供了一个子类?PHImageCachingManager 用于处理图像的缓存,但是这个子类并不只是图像本身的缓存,而是更加实用——处理图像的整个加载过程的缓存。例如要在一个?collectionView 上展示图像列表这类大量的资源图像的缩略图时,可以利用 PHImageCachingManager?预先将一些图像加载到内存中,这对优化 collectionView 滚动时的表现很有帮助。然而,这只是官方说法,实际上由于加载图像的过程并不确定,每个业务加载图像的实际需求都可能不一样,因此?PHImageCachingManager 也采用比较松散的方法去控制这些缓存,其中的关键方法:

1
- (void)startCachingImagesForAssets:(NSArray<PHAsset *> *)assets targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options;

需要传入一组 PHAsset,以及 targetSize,contentMode,以及一个?PHImageRequestOptions,如上面所述,这些参数之间的有着互相影响的作用,因此实际上不同的场景对于每个参数要求都不一样,而这些参数的最佳取值也只能通过实际在场景中测试所得。因此,比起使用?PHImageCachingManager,我总结了一些更为简易可行的缓存方法:

  • 获取图片时尽量获取预览图,不要直接显示原件,建议获取与设备屏幕同样大小的图像即可,实际上系统相册预览大图时使用的也是预览图,这也是系统相册加载速度快的原因。
  • 获取图片使用异步请求,如上面所述,当请求为异步时返回图像的 block 会被多次调用,先返回低清图,再返回高清图,这样一来可以大大减少 UI 的等待时间。
  • 获取到高清图后可以缓存下来,简单地使用变量缓存即可,尽量在获取到高清图后避免再次发起请求获取图像。因为即使图像原件已经下载下来,重新请求高清图时因为图片的尺寸比较大,因此系统生成图像和剪裁图像也会花费一些时间。
  • 预先加载图像,如像预览大图这类情景中,用户同时只会看到一张大图,因此在观看某一张图片时,预先请求其邻近两张图片,对于加快 UI 的响应很有帮助

经过实际测试,如果请求的是缩略图(即尺寸小的图像),那么即使请求的图像很多,仍不会产生任何不流畅的表现,但如果请求的是高清大图,那么即使只是同时请求几张图都会产生不流畅的状况。如上面提到过的那样,这些的状况的出现很可能是请求大图时由图片元数据产生图像,以及剪裁图像的过程耗时较多。所以按实际表现来看,即使 PhotoKit 有自己的缓存策略,仍然很难避免这部分耗时。因此上面几点优化获取图像的策略重点也是放在减少图像大小,异步请求以及做缓存几个方面。

iOS 开发之照片框架详解之二 —— PhotoKit 详解(下)

三. 常用方法的封装

虽然 PhotoKit 的功能强大很多,但基于兼容 iOS 8.0 以下版本的考虑,暂时可能仍无法抛弃 ALAssetLibrary,这时候一个比较好的方案是基于 ALAssetLibrary 和 PhotoKit 封装出一系列模拟系统 Asset 类的自定义类,然后在其中封装好兼容 ALAssetLibrary 和 PhotoKit 的方法。

这里列举了四种常用的封装好的方法:原图,缩略图,预览图,方向,下面直接上代码,代码中有相关注释解释其中的要点。其中下面的代码中常常出现的 [[QMUIAssetsManager sharedInstance] phCachingImageManager] 是 QMUI 框架中封装的类以及单例方法,表示产生一个 PHCachingImageManager 的单例,这样做的好处是 PHCachingImageManager 需要占用较多的资源,因此使用单例可以避免无谓的资源消耗,另外请求图像等方法需要基于用一个 PHCachingImageManager 实例才能进行进度续传,管理请求等操作。

  1. 原图

由于原图的尺寸通常会比较大,因此建议使用异步拉取,但这里仍同时列举同步拉取的方法。这里需要留意如前文中所述,ALAssetRepresentation 中获取原图的接口 fullResolutionImage 所得到的图像并没有带上系统相册“编辑”(选中,滤镜等)的效果,需要额外获取这些效果并手工叠加到图像上。

.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// Asset 的原图(包含系统相册“编辑”功能处理后的效果)
- (UIImage *)originImage;
/**
* 异步请求 Asset 的原图,包含了系统照片“编辑”功能处理后的效果(剪裁,旋转和滤镜等),可能会有网络请求
*
* @param completion 完成请求后调用的 block,参数中包含了请求的原图以及图片信息,在 iOS 8.0 或以上版本中,
* 这个 block 会被多次调用,其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直接获取到高清图,
* 获取到高清图后 QMUIAsset 会缓存起这张高清图,这时 block 中的第二个参数(图片信息)返回的为 nil
* @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。
*
* @wraning iOS 8.0 以下中并没有异步请求预览图的接口,因此实际上为同步请求,这时 block 中的第二个参数(图片信息)返回的为 nil
*
* @return 返回请求图片的请求 id
*/
- (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler;

.m 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
- (UIImage *)originImage {
if (_originImage) {
return _originImage;
}
__block UIImage *resultImage;
if (_usePhotoKit) {
PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init];
phImageRequestOptions.synchronous = YES;
[[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset
targetSize:PHImageManagerMaximumSize
contentMode:PHImageContentModeDefault
options:phImageRequestOptions
resultHandler:^(UIImage *result, NSDictionary *info) {
resultImage = result;
}];
} else {
CGImageRef fullResolutionImageRef = [_alAssetRepresentation fullResolutionImage];
// 通过 fullResolutionImage 获取到的的高清图实际上并不带上在照片应用中使用“编辑”处理的效果,需要额外在 AlAssetRepresentation 中获取这些信息
NSString *adjustment = [[_alAssetRepresentation metadata] objectForKey:@"AdjustmentXMP"];
if (adjustment) {
// 如果有在照片应用中使用“编辑”效果,则需要获取这些编辑后的滤镜,手工叠加到原图中
NSData *xmpData = [adjustment dataUsingEncoding:NSUTF8StringEncoding];
CIImage *tempImage = [CIImage imageWithCGImage:fullResolutionImageRef];
NSError *error;
NSArray *filterArray = [CIFilter filterArrayFromSerializedXMP:xmpData
inputImageExtent:tempImage.extent
error:&error];
CIContext *context = [CIContext contextWithOptions:nil];
if (filterArray && !error) {
for (CIFilter *filter in filterArray) {
[filter setValue:tempImage forKey:kCIInputImageKey];
tempImage = [filter outputImage];
}
fullResolutionImageRef = [context createCGImage:tempImage fromRect:[tempImage extent]];
}
}
// 生成最终返回的 UIImage,同时把图片的 orientation 也补充上去
resultImage = [UIImage imageWithCGImage:fullResolutionImageRef scale:[_alAssetRepresentation scale] orientation:(UIImageOrientation)[_alAssetRepresentation orientation]];
}
_originImage = resultImage;
return resultImage;
}
- (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler {
if (_usePhotoKit) {
if (_originImage) {
// 如果已经有缓存的图片则直接拿缓存的图片
if (completion) {
completion(_originImage, nil);
}
return 0;
} else {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络
imageRequestOptions.progressHandler = phProgressHandler;
return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
// 排除取消,错误,低清图三种情况,即已经获取到了高清图时,把这张高清图缓存到 _originImage 中
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined) {
_originImage = result;
}
if (completion) {
completion(result, info);
}
}];
}
} else {
if (completion) {
completion([self originImage], nil);
}
return 0;
}
}
  1. 缩略图

相对于在拉取原图时 ALAssetLibrary 的部分需要手工叠加系统相册的“编辑”效果,拉取缩略图则简单一些,因为系统接口拉取到的缩略图已经带上“编辑”的效果了。

.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Asset 的缩略图
*
* @param size 指定返回的缩略图的大小,仅在 iOS 8.0 及以上的版本有效,其他版本则调用 ALAsset 的接口由系统返回一个合适当前平台的图片
*
* @return Asset 的缩略图
*/
- (UIImage *)thumbnailWithSize:(CGSize)size;
/**
* 异步请求 Asset 的缩略图,不会产生网络请求
*
* @param size 指定返回的缩略图的大小,仅在 iOS 8.0 及以上的版本有效,其他版本则调用 ALAsset 的接口由系统返回一个合适当前平台的图片
* @param completion 完成请求后调用的 block,参数中包含了请求的缩略图以及图片信息,在 iOS 8.0 或以上版本中,这个 block 会被多次调用,
* 其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直接获取到高清图,获取到高清图后 QMUIAsset 会缓存起这张高清图,
* 这时 block 中的第二个参数(图片信息)返回的为 nil。
*
* @return 返回请求图片的请求 id
*/
- (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *, NSDictionary *))completion;

.m 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
- (UIImage *)thumbnailWithSize:(CGSize)size {
if (_thumbnailImage) {
return _thumbnailImage;
}
__block UIImage *resultImage;
if (_usePhotoKit) {
PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init];
phImageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
// 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片
[[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset
targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale)
contentMode:PHImageContentModeAspectFill options:phImageRequestOptions
resultHandler:^(UIImage *result, NSDictionary *info) {
resultImage = result;
}];
} else {
CGImageRef thumbnailImageRef = [_alAsset thumbnail];
if (thumbnailImageRef) {
resultImage = [UIImage imageWithCGImage:thumbnailImageRef];
}
}
_thumbnailImage = resultImage;
return resultImage;
}
- (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *, NSDictionary *))completion {
if (_usePhotoKit) {
if (_thumbnailImage) {
if (completion) {
completion(_thumbnailImage, nil);
}
return 0;
} else {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
// 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片
return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale) contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
// 排除取消,错误,低清图三种情况,即已经获取到了高清图时,把这张高清图缓存到 _thumbnailImage 中
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined) {
_thumbnailImage = result;
}
if (completion) {
completion(result, info);
}
}];
}
} else {
if (completion) {
completion([self thumbnailWithSize:size], nil);
}
return 0;
}
}
  1. 预览图

与上面的方法类似,不再展开说明。

.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Asset 的预览图
*
* @warning 仿照 ALAssetsLibrary 的做法输出与当前设备屏幕大小相同尺寸的图片,如果图片原图小于当前设备屏幕的尺寸,则只输出原图大小的图片
* @return Asset 的全屏图
*/
- (UIImage *)previewImage;
/**
* 异步请求 Asset 的预览图,可能会有网络请求
*
* @param completion 完成请求后调用的 block,参数中包含了请求的预览图以及图片信息,在 iOS 8.0 或以上版本中,
* 这个 block 会被多次调用,其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直接获取到高清图,
* 获取到高清图后 QMUIAsset 会缓存起这张高清图,这时 block 中的第二个参数(图片信息)返回的为 nil
* @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。
*
* @wraning iOS 8.0 以下中并没有异步请求预览图的接口,因此实际上为同步请求,这时 block 中的第二个参数(图片信息)返回的为 nil
*
* @return 返回请求图片的请求 id
*/
- (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler;

__.m 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
- (UIImage *)previewImage {
if (_previewImage) {
return _previewImage;
}
__block UIImage *resultImage;
if (_usePhotoKit) {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.synchronous = YES;
[[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset
targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT)
contentMode:PHImageContentModeAspectFill
options:imageRequestOptions
resultHandler:^(UIImage *result, NSDictionary *info) {
resultImage = result;
}];
} else {
CGImageRef fullScreenImageRef = [_alAssetRepresentation fullScreenImage];
resultImage = [UIImage imageWithCGImage:fullScreenImageRef];
}
_previewImage = resultImage;
return resultImage;
}
- (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler {
if (_usePhotoKit) {
if (_previewImage) {
// 如果已经有缓存的图片则直接拿缓存的图片
if (completion) {
completion(_previewImage, nil);
}
return 0;
} else {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络
imageRequestOptions.progressHandler = phProgressHandler;
return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT) contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
// 排除取消,错误,低清图三种情况,即已经获取到了高清图时,把这张高清图缓存到 _previewImage 中
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined) {
_previewImage = result;
}
if (completion) {
completion(result, info);
}
}];
}
} else {
if (completion) {
completion([self previewImage], nil);
}
return 0;
}
}
  1. 方向(imageOrientation)

比较奇怪的是,无论在 PhotoKit 或者是 ALAssetLibrary 中,要想获取到准确的图像方向,只能通过某些 key 检索所得。

1
2
3
4
.h 文件
1
- (UIImageOrientation)imageOrientation;

.m 文件

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    - (UIImageOrientation)imageOrientation {
    UIImageOrientation orientation;
    if (_usePhotoKit) {
    if (!_phAssetInfo) {
    // PHAsset 的 UIImageOrientation 需要调用过 requestImageDataForAsset 才能获取
    [self requestPhAssetInfo];
    }
    // 从 PhAssetInfo 中获取 UIImageOrientation 对应的字段
    orientation = (UIImageOrientation)[_phAssetInfo[@"orientation"] integerValue];
    } else {
    orientation = (UIImageOrientation)[[_alAsset valueForProperty:@"ALAssetPropertyOrientation"] integerValue];
    }
    return orientation;
    }

转自http://kayosite.com/ios-development-and-detail-of-photo-framework-part-three.html

iOS 开发之照片框架详解

iOS 开发之照片框架详解

一. 概要

在 iOS 设备中,照片和视频是相当重要的一部分。最近刚好在制作一个自定义的 iOS 图片选择器,顺便整理一下 iOS 中对照片框架的使用方法。在 iOS 8 出现之前,开发者只能使用 AssetsLibrary 框架来访问设备的照片库,这是一个有点跟不上 iOS 应用发展步伐以及代码设计原则但确实强大的框架,考虑到 iOS7 仍占有不少的渗透率,因此 AssetsLibrary 也是本文重点介绍的部分。而在 iOS8 出现之后,苹果提供了一个名为 PhotoKit 的框架,一个可以让应用更好地与设备照片库对接的框架,文末也会介绍一下这个框架。

另外值得强调的是,在 iOS 中,照片库并不只是照片的集合,同时也包含了视频。在 AssetsLibrary 中两者都有相同类型的对象去描述,只是类型不同而已。文中为了方便,大部分时候会使用「资源」代表 iOS 中的「照片和视频」。

二. AssetsLibrary 组成介绍

  • AssetsLibrary 的组成比较符合照片库本身的组成,照片库中的完整照片库对象、相册、相片都能在 AssetsLibrary 中找到一一对应的组成,这使到 AssetsLibrary 的使用变得直观而方便。

  • AssetsLibrary: 代表整个设备中的资源库(照片库),通过 AssetsLibrary 可以获取和包括设备中的照片和视频

  • ALAssetsGroup: 映射照片库中的一个相册,通过 ALAssetsGroup 可以获取某个相册的信息,相册下的资源,同时也可以对某个相册添加资源。
  • ALAsset: 映射照片库中的一个照片或视频,通过 ALAsset 可以获取某个照片或视频的详细信息,或者保存照片和视频。
  • ALAssetRepresentation: ALAssetRepresentation 是对 ALAsset 的封装(但不是其子类),可以更方便地获取 ALAsset 中的资源信息,每个 ALAsset 都有至少有一个 ALAssetRepresentation 对象,可以通过 defaultRepresentation 获取。而例如使用系统相机应用拍摄的 RAW + JPEG 照片,则会有两个 ALAssetRepresentation,一个封装了照片的 RAW 信息,另一个则封装了照片的 JPEG 信息。

    三. AssetsLibrary 的基本使用

    AssetsLibrary 的功能很多,基本可以分为对资源的获取/保存两个部分,保存的部分相对简单,API 也比较少,因此这里不作详细介绍。获取资源的 API 则比较丰富了,一个常见的使用大量 AssetsLibrary API 的例子就是图片选择器(ALAsset Picker)。要制作一个图片选择器,思路应该是获取照片库-列出所有相册-展示相册中的所有图片-预览图片大图。
1
2
3
4
5
6
7
8
9
10
NSString *tipTextWhenNoPhotosAuthorization; // 提示语
// 获取当前应用对照片的访问授权状态
ALAuthorizationStatus authorizationStatus = [ALAssetsLibrary authorizationStatus];
// 如果没有获取访问授权,或者访问授权状态已经被明确禁止,则显示提示语,引导用户开启授权
if (authorizationStatus == ALAuthorizationStatusRestricted || authorizationStatus == ALAuthorizationStatusDenied) {
NSDictionary *mainInfoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString *appName = [mainInfoDictionary objectForKey:@"CFBundleDisplayName"];
tipTextWhenNoPhotosAuthorization = [NSString stringWithFormat:@"请在设备的\"设置-隐私-照片\"选项中,允许%@访问你的手机相册", appName];
// 展示提示语
}

如果已经获取授权,则可以获取相册列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_assetsLibrary = [[ALAssetsLibrary alloc] init];
_albumsArray = [[NSMutableArray alloc] init];
[_assetsLibrary enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if (group) {
[group setAssetsFilter:[ALAssetsFilter allPhotos]];
if (group.numberOfAssets > 0) {
// 把相册储存到数组中,方便后面展示相册时使用
[_albumsArray addObject:group];
}
} else {
if ([_albumsArray count] > 0) {
// 把所有的相册储存完毕,可以展示相册列表
} else {
// 没有任何有资源的相册,输出提示
}
}
} failureBlock:^(NSError *error) {
NSLog(@"Asset group not found!\n");
}];

上面的代码中,遍历出所有的相册列表,并把相册中资源数不为空的相册 ALAssetGroup 对象的引用储存到一个数组中。这里需要强调几点:

  • iOS 中允许相册为空,即相册中没有任何资源,如果不希望获取空相册,则需要像上面的代码中那样手动过滤
  • ALAssetsGroup 有一个 setAssetsFilter 的方法,可以传入一个过滤器,控制只获取相册中的照片或只获取视频。一旦设置过滤,ALAssetsGroup 中资源列表和资源数量的获取也会被自动更新。
  • 整个 AssetsLibrary 中对相册、资源的获取和保存都是使用异步处理(Asynchronous),这是考虑到资源文件体积相当比较大(还可能很大)。例如上面的遍历相册操作,相册的结果使用 block 输出,如果相册遍历完毕,则最后一次输出的 block 中的 group 参数值为 nil。而 stop 参数则是用于手工停止遍历,只要把 *stop 置 YES,则会停止下一次的遍历。关于这一点常常会引起误会,所以需要注意。

现在,已经可以获取相册了,接下来是获取相册中的资源:

1
2
3
4
5
6
7
8
_imagesAssetArray = [[NSMutableArray alloc] init];
[assetsGroup enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result) {
[_imagesAssetArray addObject:result];
} else {
// resultnil,即遍历相片或视频完毕,可以展示资源列表
}
}];

跟遍历相册的过程类似,遍历相片也是使用一系列的异步方法,其中上面的方法所输出的 block 中,除了 result 参数表示资源信息,stop 用于手工停止遍历外,还提供了一个 index 参数,这个参数表示资源的索引。一般来说,展示资源列表都会使用缩略图(result.thumbnail),因此即使资源很多,遍历资源的速度也会相当快。但如果确实需要加载资源的高清图或者其他耗时的处理,则可以利用上面的 index 参数和 stop 参数做一个分段拉取资源。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NSUInteger _currentIndex; // 当前 index,每次拉取资源时从这个值开始
_targetIndex = 50;
_currentIndex = 0;
- (void)loadAssetWithAssetsGroup:(assetsGroup *)assetsGroup {
[assetsGroup enumerateAssetsAtIndexes:[NSIndexSet indexSetWithIndex:_currentIndex] options:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
_currentIndex = index;
if (index > _targetIndex) {
// 拉取资源的索引如果比目标值大,则停止拉取
*stop = YES;
} else {
if (result) {
[_imagesAssetArray addObject:result];
} else {
// result 为 nil,即遍历相片或视频完毕
}
}
}];
}
// 之前拉取的数据已经显示完毕,需要展示新数据,重新调用 loadAssetWithAssetsGroup 方法,并根据需要更新 _targetIndex 的值

最后一步是获取图片详细信息,例如:

1
2
3
4
// 获取资源图片的详细资源信息,其中 imageAsset 是某个资源的 ALAsset 对象
ALAssetRepresentation *representation = [imageAsset defaultRepresentation];
// 获取资源图片的 fullScreenImage
UIImage *contentImage = [UIImage imageWithCGImage:[representation fullScreenImage]];

对于一个 ALAssetRepresentation,里面包含了图片的多个版本。最常用的是 fullResolutionImage 和 fullScreenImage。fullResolutionImage 是图片的原图,通过 fullResolutionImage 获取的图片没有任何处理,包括通过系统相册中“编辑”功能处理后的信息也没有被包含其中,因此需要展示“编辑”功能处理后的信息,使用 fullResolutionImage 就比较不方便,另外 fullResolutionImage 的拉取也会比较慢,在多张 fullResolutionImage 中切换时能明显感觉到图片的加载过程。因此这里建议获取图片的 fullScreenImage,它是图片的全屏图版本,这个版本包含了通过系统相册中“编辑”功能处理后的信息,同时也是一张缩略图,但图片的失真很少,缺点是图片的尺寸是一个适应屏幕大小的版本,因此展示图片时需要作出额外处理,但考虑到加载速度非常快的原因(在多张图片之间切换感受不到图片加载耗时),仍建议使用 fullScreenImage。

系统相册的处理过程大概也是如上,可以看出,在整个过程中并没有使用到图片的 fullResolutionImage,从相册列表展示到最终查看资源,都是使用缩略图,这也是 iOS 相册加载快的一个重要原因。

三. AssetsLibrary 的坑点

作为一套老框架,AssetsLibrary 不但有坑,而且还不少,除了上面提到的资源异步拉取时需要注意的事项,下面几点也是值得注意的:

  1. AssetsLibrary 实例需要强引用
    实例一个 AssetsLibrary 后,如上面所示,我们可以通过一系列枚举方法获取到需要的相册和资源,并把其储存到数组中,方便用于展示。但是,当我们把这些获取到的相册和资源储存到数组时,实际上只是在数组中储存了这些相册和资源在 AssetsLibrary 中的引用(指针),因而无论把相册和资源储存数组后如何利用这些数据,都首先需要确保 AssetsLibrary 没有被 ARC 释放,否则把数据从数组中取出来时,会发现对应的引用数据已经丢失(参见下图)。这一点较为容易被忽略,因此建议在使用 AssetsLibrary 的 viewController 中,把 AssetsLibrary 作为一个强持有的 property 或私有变量,避免在枚举出 AssetsLibrary 中所需要的数据后,AssetsLibrary 就被 ARC 释放了。
  1. AssetsLibrary 遵循写入优先原则
    写入优先也就是說,在利用 AssetsLibrary 读取资源的过程中,有任何其它的进程(不一定是同一个 App)在保存资源时,就会收到 ALAssetsLibraryChangedNotification,让用户自行中断读取操作。最常见的就是读取 fullResolutionImage 时,用进程在写入,由于读取 fullResolutionImage 耗时较长,很容易就会 exception。
  2. 开启 Photo Stream 容易导致 exception

本质上,这跟上面的 AssetsLibrary 遵循写入优先原则是同一个问题。如果用户开启了共享照片流(Photo Stream),共享照片流会以 mstreamd 的方式“偷偷”执行,当有人把相片写入 Camera Roll 时,它就会自动保存到 Photo Stream Album 中,如果用户刚好在读取,那就跟上面说的一样产生 exception 了。由于共享照片流是用户决定是否要开启的,所以开发者无法改变,但是可以通过下面的接口在需要保护的时刻关闭监听共享照片流产生的频繁通知信息。

1
[ALAssetsLibrary disableSharedPhotoStreamsSupport];

四. PhotoKit 简介

PhotoKit 是一套比 AssetsLibrary 更完整也更高效的库,对资源的处理跟 AssetsLibrary 也有很大的不同。
首先简单介绍几个概念:

  • PHAsset: 代表照片库中的一个资源,跟 ALAsset 类似,通过 PHAsset 可以获取和保存资源
  • PHFetchOptions: 获取资源时的参数,可以传 nil,即使用系统默认值
  • PHFetchResult: 表示一系列的资源集合,也可以是相册的集合
  • PHAssetCollection: 表示一个相册或者一个时刻,或者是一个「智能相册(系统提供的特定的一系列相册,例如:最近删除,视频列表,收藏等等,如下图所示)
  • PHImageManager: 用于处理资源的加载,加载图片的过程带有缓存处理,可以通过传入一个 PHImageRequestOptions 控制资源的输出尺寸等规格
  • PHImageRequestOptions: 如上面所说,控制加载图片时的一系列参数

再列出几个代码片段,展示如何获取相册以及某个相册下资源的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 列出所有相册智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
// 列出所有用户创建的相册
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
// 获取所有资源的集合,并按资源的创建时间排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];
// 在资源的集合中获取第一个集合,并获取其中的图片
PHCachingImageManager *imageManager = [[PHCachingImageManager alloc] init];
PHAsset *asset = assetsFetchResults[0];
[imageManager requestImageForAsset:asset
targetSize:SomeSize
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// 得到一张 UIImage,展示到界面上
}];

结合上面几个代码片段上看,PhotoKit 相对 AssetsLibrary 主要有三点重要的改进:

  • 从 AssetsLibrary 中获取数据,无论是相册,还是资源,本质上都是使用枚举的方式,遍历照片库取得相应的数据。而 PhotoKit 则是通过传入参数,直接获取相应的数据,因而效率会提高不少。
  • 在 AssetsLibrary 中,相册和资源是对应不同的对象(ALAssetGroup 和 ALAsset),因此获取相册和获取资源是两个完全没有关联的接口。而 PhotoKit 中则有 PHFetchResult 这个可以统一储存相册或资源的对象,因此处理相册和资源时也会比较方便。
  • PhotoKit 返回资源结果时,同时返回了资源的元数据,获取元数据在 AssetsLibrary 中是很难办到的一件事。同时通过 PHAsset,开发者还能直接获取资源是否被收藏(favorite)和隐藏(hidden),拍摄图片时是否开启了 HDR 或全景模式,甚至能通过一张连拍图片获取到连拍图片中的其他图片。这也是文章开头说的,PhotoKit 能更好地与设备照片库接入的一个重要因素。

转自http://kayosite.com/ios-development-and-detail-of-photo-framework.html

IOS 图标以及启动界面

From: https://my.oschina.net/hqc17/blog/735282

在苹果应用的开发后期,我们需要对应用设置显示的图标以及启动界面(启动界面也可以省略),Xcode是一个非常方便的工具,到了7.3版本,已经在这块非常成熟了,不再需要plist,只需xcasset文件就可以,也不需要再关心具体文件的命名了。

在开始之前,我们先要了解需要用到的分辨率,以下归纳的分辨率是universal的,不是iPhone-only或者iPad-only,如果只需要其中一种可自行调整。

29pt: 1x=2929,2x=5858,3x=87*87

40pt: 1x=4040,2x=8080,3x=120*120

50pt: 1x=5050,2x=100100

57pt: 1x=5757,2x=114114

60pt: 2x=120120,3x=180180

72pt: 1x=7272,2x=144144

76pt: 1x=7676,2x=152152

83.5pt: 2x=167*167

其中,iphone使用到的是29,57,58,80,87,114,120,180;

ipad使用到的是29,40,50,58,72,76,80,100,144,152,167。

在开始设置之前,请确保已经将图标生成对应的像素大小,便于调用。

portrait:

1x=320*480

2x=640*960

Retina 4=640*1136

Retina HD 4.7=750*1334

Retina HD 5.5=1242*2208

landscape:

1x=1024*768

2x=2048*1536

1x=1024*748(Without Status Bar)

2x=2048*1496(Without Status Bar)

Retina HD 5.5=2208*1242

其中,iphone使用到的landscape只有Retina HD 5.5,portrait全用;

ipad使用到的landscape除了Retina HD 5.5外都有,portrait只有1x和2x。

在开始设置之前,请确保已经将启动图像生成对应的像素大小,便于调用。

裁剪好图片资源后,我们接下来对xcasset文件操作,添加图片资源。

![][1]

在工程设置general里头,找到这个选项,如果是don’t use asset catalogs,就单击选取,migrate到xcasset里,而后会在左边的树状结构出现image.xcassets![][2]

单击后,在这空白处右键,选择App Icons & Launch Images,会看到New iOS App Icon(新建图标)以及New iOS Launch Image(新建启动图像)

我们先来看Icon的制作,很简单,单击AppIcon后,右侧出现如图所示:

将我们之前做好的图标,按照我上述总结的表,一个个对应拖进去就可以了,不需要其他操作。拖完后Xcode自动会重命名一份到工程里,不用担心删掉制作的Icon或者移动文件后会出问题。同时,在同级下的Contents.json会记录相应的文件信息。这样就做好图标了。

LaunchImage的制作类似,但是需要注意,Icon是必须都要添加,而启动界面只需要添加需要的即可。单击LaunchImage如图所示:

对于很多游戏,我们需要横屏显示,而iPad的设置非常方便,只需要将对应的Landscape图拖到对应的框内即可,而iPhone非常坑,只有一个Landscape,还是Retina HD 5.5的,这是6plus用的,其他的都需要通过portrait设置。我第一次设置的时候以为苹果会自动把高清的切成低分辨的,结果打开是黑屏。查阅后才明白原来是要设置portrait,而为了横屏显示,portrait图片需要调整,将本来正常显示的landscape图像转动90度后在拖入,这样读取的时候横过来显示,就是正常的。