问题引出

在开发中,涉及价格金额处理,后台会返回Number类型的数据,打印或者经过Json转Model后的NSString可能出现精度丢失的问题,如果涉及到金额的加减乘除运算问题将暴露得更为明显。这里就iOS数据精度处理做一个总结。

问题复现

NSNumber转NSSting

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
NSArray *numbers = @[
                     @99.00,
                     @99.09,
                     @99.19,
                     @99.29,
                     @99.39,
                     @99.49,
                     @99.59,
                     @99.69,
                     @99.79,
                     @99.89,
                     @99.99,
                     ];

for (int i = 0; i < numbers.count; i++) {
    NSNumber *number = numbers[i];
    NSString *strValue = [number stringValue];

    NSLog(@"strValue:%@",strValue);
}

/*
 oldVlue:99.00 strValue:99
 oldVlue:99.09 strValue:99.09
 oldVlue:99.19 strValue:99.19
 oldVlue:99.29 strValue:99.29000000000001
 oldVlue:99.39 strValue:99.39
 oldVlue:99.48 strValue:99.48999999999999
 oldVlue:99.59 strValue:99.59
 oldVlue:99.69 strValue:99.69
 oldVlue:99.79 strValue:99.79000000000001
 oldVlue:99.89 strValue:99.89
 oldVlue:99.99 strValue:99.98999999999999
 */

在这里我们发现将NSNumber转换成NSSting的过程中可能会出现精度丢失。

Json到Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//出现BUG的条件必须是两位数,且带两位小数,类型还必须是float
//两位数:十位必须是7、8、9;个位数随意
//两位小数:个位数随意;十位数必须是0
NSString *jsonStr = @"{\"71.40\":71.40, \"97.40\":97.40, \"80.40\":80.40, \"188.40\":188.40}";
NSLog(@"json:%@", jsonStr);

NSData *jsonData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
NSError *jsonParsingError = nil;
NSDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:[NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonParsingError]];

NSLog(@"dic:%@", dic);
/*
2017-10-14 18:29:19.434 FloatTransferDemo[62722:3093992] dic:{
    "188.40" = "188.4";
    "71.40" = "71.40000000000001";
    "80.40" = "80.40000000000001";
    "97.40" = "97.40000000000001";
}
*/

在这里我们发现将Json解析成Model的过程中可能会出现精度丢失。

问题分析

因为浮点数在计算机中是采用IEEE规定的标准浮点格式,即二进制科学表示法。 在这种表示法中,一个数 S = M * 2 ^ N

其中N表示阶码,M表示位数(有效数字位)。 例如一个float类型的浮点,在32bit位上,占4个字节,字节表示为

【31】N:【30 ~ 23】 M:【22~0】
  • 31位表示符号位: 0正,1负
  • 中间8位是阶码位: 表示范围【-128 ~ 127】,对于float类型数据规定其偏移量为127
  • 后面23位是有效数字位: 因为科学计数法,整数位定死了是1,所以这里记录的是小数点后面的二进制为

指数N决定它的范围,因为M总是一个以1开头的小数,以float来说即是:-2 ^ 128 ~ 2 ^ 128,即float能表示的数的大小的范围。

而它的精度是由位数(也就是有效的数据位)来决定的, 2 ^ 23 = 8388608,总共7位,表示最多能用7位有效数字,最多能表示到.8388708即小数点后7位,由于不能完全表示全部的7位数,所以它的精度范围是6位~7位。

同理可得double的精度是2 ^ 52 = 4503599627370496, 共16位,所以精度为15 ~ 16位。

总结:float/double类型的范围和精度的计算方式

不同机器字节序的规定
公式: S = M * 2 ^ N
二进制在内存中是以补码形式存储,负数要对其二进制绝对值按位取反再加一,正数的补码与原码形式相同

也就是说float和doublel类型数据在计算机中存储可能是不精确的。 当我们需要转换成浮点类型是数据时,最好用double,因为double的精度更高,出现丢精度的概率相对是较小的。

在iOS中提供一个专用的类来处理浮点数据相关的运算:NSDecimalNumber

解决方案

使用NSDecimalNumber来进行浮点数处理。 我们给NSString添加一个分类来处理浮点运算问题

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
///.h
@interface NSString (DecimalNumber)

+ (NSString *)decimalNumberWithNSNumber:(NSNumber * )number;
+ (NSString *)decimalNumberWithDouble:(double)conversionValue;

@end

///.m
#import "NSString+DecimalNumber.h"

@implementation NSString (DecimalNumber)

+ (NSString *)decimalNumberWithNSNumber:(NSNumber * )number
{
    double conversionValue = [number doubleValue];
    NSString *doubleString        = [NSString stringWithFormat:@"%lf", conversionValue];
    NSDecimalNumber *decNumber    = [NSDecimalNumber decimalNumberWithString:doubleString];
    return [decNumber stringValue];
}

+ (NSString *)decimalNumberWithDouble:(double)conversionValue
{
    NSString *doubleString        = [NSString stringWithFormat:@"%lf", conversionValue];
    NSDecimalNumber *decNumber    = [NSDecimalNumber decimalNumberWithString:doubleString];
    return [decNumber stringValue];
}

@end

如何使用

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
NSArray *numbers = @[
                     @99.00,
                     @99.09,
                     @99.19,
                     @99.29,
                     @99.39,
                     @99.49,
                     @99.59,
                     @99.69,
                     @99.79,
                     @99.89,
                     @99.99,
                     ];

for (int i = 0; i < numbers.count; i++) {
    NSNumber *number = numbers[i];
    NSString *strValue = [NSString decimalNumberWithNSNumber:number];

    NSLog(@"strValue:%@",strValue);
}

/*
 oldVlue:99.00 strValue:99
 oldVlue:99.09 strValue:99.09
 oldVlue:99.19 strValue:99.19
 oldVlue:99.29 strValue:99.29
 oldVlue:99.39 strValue:99.39
 oldVlue:99.48 strValue:99.49
 oldVlue:99.59 strValue:99.59
 oldVlue:99.69 strValue:99.69
 oldVlue:99.79 strValue:99.79
 oldVlue:99.89 strValue:99.89
 oldVlue:99.99 strValue:99.99
 */

问题得以解决。☕️

参考资料:

排序是应用常见需求之一,如何正确优雅的实现一个排序,NSSortDescriptor或许是一个非常好的选择。

NSSortDescriptor由3个参数组成:

  • key(键):对于一个给定的集合,将对集合种的每个对象按照该键来进行排序
  • ascending(升序):指定集合按照升序(YES)还是降序(NO)进行排序
  • selector(方法子):排序时的比较函数,当对字符串进行排序时,应当加入localizedStandardCompare:选择器,它将根据语言规则进行排序(例如大小写,变音符号等等的顺序)

1.如何使用

NSSortDescriptor主要针对各种集合排序,NSArray,NSMutableArray,NSSet,NSOrderedSet,NSMutableOrderedSet

如果集合是可变的,则对集合本身排序例如

  • NSMutableOrderedSetsortUsingDescriptors:
  • NSMutableArraysortUsingDescriptors:

如果集合是可变的,则返回一个排好序的新集合例如

  • NSArraysortedArrayUsingDescriptors:
  • NSSetsortedArrayUsingDescriptors:
  • NSOrderedSetsortedArrayUsingDescriptors:

注意排序描述是一个数组,也就是排序可以支持按照多个描述综合排序。如果存在多个规则先满足前面的规则再满足后面的规则。例如两个元素按照第一天规则顺序一致,那么如果还存在第二天规则,它们将按照第二条规则继续排序,如果前面的规则已经区分出顺序后面的规则将失效。

2.实例说明

为了更好的描述,假如我们有一个Person对象,它有NSString * 类型的姓和名属性,以及一个NSNumber类型的年龄属性。

1
2
3
4
5
6
7
@interface Person : NSObject

@property (nonatomic, copy  ) NSString *firstName;
@property (nonatomic, copy  ) NSString *lastName;
@property (nonatomic, strong) NSNumber *age;

@end

给定以下数据集:

index 0 1 2 3 4
firstName Alice Bod Charlie Quentin
lastName Smith Jones Smith Alberts
age 24 27 33 31 19

使用不同的NSSortDescriptor的不同组合来将它们排序:

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
NSArray *firstNames = @[@"Alice", @"Bod", @"Charlie", @"Quentin", @""];
NSArray *lastNames = @[@"Smith", @"Jones", @"Smith", @"", @"Alberts"];
NSArray *ages = @[@24, @27, @33, @31, @12];

NSMutableArray *people = [NSMutableArray array];
[firstNames enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    Person *person = [[Person alloc] init];
    person.firstName = [firstNames objectAtIndex:idx];
    person.lastName = [lastNames objectAtIndex:idx];
    person.age = [ages objectAtIndex:idx];

    [people addObject:person];
}];

NSSortDescriptor *firstNameSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"firstName" ascending:YES selector:@selector(localizedStandardCompare:)];
NSSortDescriptor *lastNameSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES selector:@selector(localizedStandardCompare:)];
NSSortDescriptor *ageSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];

NSLog(@"By age: %@",[people sortedArrayUsingDescriptors:@[ageSortDescriptor]]);
/*
 2017-06-06 23:15:02.101 NSSort​Descriptor_Demo[9103:1255482] By age: (
 "Charlie Smith 33",
 "Quentin  31",
 "Bod Jones 27",
 "Alice Smith 24",
 " Alberts 12"
 )
 */

NSLog(@"By first name: %@",[people sortedArrayUsingDescriptors:@[firstNameSortDescriptor]]);
/*
 2017-06-06 23:15:02.102 NSSort​Descriptor_Demo[9103:1255482] By first name: (
 " Alberts 12",
 "Alice Smith 24",
 "Bod Jones 27",
 "Charlie Smith 33",
 "Quentin  31"
 )
*/

NSLog(@"By last name, first name: %@",[people sortedArrayUsingDescriptors:@[lastNameSortDescriptor ,firstNameSortDescriptor]]);
/*
 2017-06-06 23:15:02.102 NSSort​Descriptor_Demo[9103:1255482] By last name, first name: (
 "Quentin  31",
 " Alberts 12",
 "Bod Jones 27",
 "Alice Smith 24",
 "Charlie Smith 33"
 )
 */

3.注意事项

  • 空字符是排在普通字符之前的
  • 基本数据类型需要包装成NSNumber对象来排序
  • 字符串类型排序需要加入localizedStandardCompare:选择器

参考链接

枚举

概述

Swift中枚举和结构体也上升到对象的位置,但是却不具备完整的对象特征,比如说他们不能继承。

在OC中枚举本质其实是整数类型,只是给他们取了一些意义直观的名称而已,而Swift完全摆脱以前的思想的束缚编的更自由,拥有了以下特征。

  • 类型更广,不限于整形,可以是CharacterStringFloatDouble等,这些叫做原始值
  • 可以拥有自己的静态成员变量来储存一些常量
  • 可以拥有自己的方法和变量
  • case条件匹配可以定义参数,可以使用where来过滤
  • 通过rawValue可以拿到枚举的原始值

原始值

如果提供了原始值,就可以在case声明成员时提供默认值,这样跟OC的枚举在结构上一些类似

1
2
3
4
5
6
7
enum WeekDays : Int {
    case Monday    = 0
    case Tuesday   = 1
    case Wednesday = 2
    case Thursday  = 3
    case Friday    = 4
}

1.声明一个枚举值

1
let monday = WeekDays.Monday

2.获取原始值

1
let rawVlaue = WeekDays.Tuesday.rawValue

3.通过原始值构造某个枚举变量

1
let wednesday = WeekDays.init(rawValue: 2)

相关值

和C的联合体有点类似

1.声明

1
2
3
4
enum Figure {
    case Rectangle(Int, Int)
    case Circle(Int)
}

2.使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func printFigure(figure: Figure) {
    switch figure {
    case .Rectangle(let width, let height):
        print("width:\(width), height:\(height)")
    case .Circle(let radius):
        print("the circle's radius is:\(radius)")
    }
}


var figure = Figure.Rectangle(1024, 768)
printFigure(figure)

figure = .Circle(600)
printFigure(figure)

OC与Swift中Options枚举的不同定义

OC版本NS_OPTIONS
1
2
3
4
5
6
7
8
9
typedef NS_OPTIONS(NSUInteger, NSVolumeEnumerationOptions) {
    /* The mounted volume enumeration will skip hidden volumes.
     */
    NSVolumeEnumerationSkipHiddenVolumes = 1UL << 1,

    /* The mounted volume enumeration will produce file reference URLs rather than path-based URLs.
     */
    NSVolumeEnumerationProduceFileReferenceURLs = 1UL << 2
}
Swift版本OPTIONS
1
2
3
4
5
struct NSVolumeEnumerationOptions : OptionSetType {
    init(rawValue rawValue: UInt)
    static var SkipHiddenVolumes: NSVolumeEnumerationOptions { get }
    static var ProduceFileReferenceURLs: NSVolumeEnumerationOptions { get }
}
使用
1
2
3
4
5
UIView.animateWithDuration(0.3,
                           delay: 0.0,
                           options: [.CurveEaseIn, .AllowUserInteraction],
                           animations: {},
                           completion: nil)

结构体

Swift中加强了结构体的能力,可以定义和使用属性,方法,下标,构造器等,但是不能被继承,没有强制类型抓换,使用析构器和引用计数等能力

通常使用结构体来充当数据模型,常量可以使用静态关键词static标示

循环引用

Swift中解决循环强引用的方式

  • 弱引用 – weak
  • 无主引用 – unowned

解决闭包中循环引用

[unowned 捕获对象]或者 [weak 捕获对象]

使用实例

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
class Person {
    let name: String

    // 写法1
//    lazy var printName: () ->() = {
//        [weak self] in if let strongSelf = self {
//            print("The name is \(strongSelf.name)")
//        }
//    }

    // 写法2
    lazy var printName: () ->() = {
        [unowned self] in print("The name is \(self.name)")
    }

    init(personName: String) {
        name = personName
    }

    deinit {
        print("Person deinit \(self.name)")
    }
}

var xiaoMing: Person? = Person(personName: "xiaoMing")
xiaoMing!.printName
xiaoMing = nil
二者的区别

如果我们可以确定在整个过程中 self 不会被释放的话,我们可以将上面的 weak 改为 unowned,这样就不再需要 strongSelf 的判断。但是如果在过程中 self 被释放了而 printName 这个闭包没有被释放的话 (比如 生成 Person 后,某个外部变量持有了 printName,随后这个 Persone 对象被释放了,但是 printName 已然存在并可能被调用),使用 unowned 将造成崩溃。在这里我们需要根据实际的需求来决定是使用 weak 还是 unowned。

OC和Swift中常用数据类型

字符串

  • OC: NSString, NSMutableString
  • Swift: String

NSString和String的关系:在Swift中,使用字符串可以使用Foundatio中的NSString和Swift中的String。

Swift在底层能够将String与NSString无缝地桥接起来,String可以调用NSString的全部API。

数组类

  • OC: NSArray, NSMutableArray
  • Swift: Array [AnyObject]

NSArray和Array的关系:Swift能在底层将他们自动桥接起来,一个NSArray对象桥接后的结果是[AnyObject]。

字典

  • OC: NSDictionary, NSMutableDictionary
  • Swift: Dictionary [Object: AnyObject]

NSDictionary和Dictionary的关系: 底层自动桥接,一个NSDictionary对象桥接后的结果是[Object: AnyObject]。

类型转换

类的转换使用 as 数据类型加括号 Int()

OC使用 (新的类型)原来的变量

类型判断

is关键词

相当于OC的isKindofClass:

辅助命令

  • #file –> __FILE__
  • #line –> __LINE__
  • #function –> _func__ 或者 _FUNCTION__

退出程序

  • C: exit(0)
  • Swift: fatalError() 或者 fatalError(message: String)

断言

  • OC: NSAssert
  • Swift: assert()

beginTime的妙用

创建一个动画,添加到不同的图层上,可以实现复用,通过调节beginTime可以调节动画开始执行的时间。

例如,我们创建一个水平位移动画,添加到不同的图层对象上,并且让他们轮流执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = 0
flyRight.toValue = 100
flyRight.duration = 0.5
flyRight.fillMode = kCAFillModeBoth

firstView.layer.addAnimation(flyRight, forKey: nil)

// make secondView perform animation on delay 0.3 seconds.
flyRight.beginTime = CACurrentMediaTime() + 0.3
secondView.layer.addAnimation(flyRight, forKey: nil)
secondView.layer.position.x = 100

// make thirdView perform animation on delay 0.4 seconds.
flyRight.beginTime = CACurrentMediaTime() + 0.4
thirdView.layer.addAnimation(flyRight, forKey: nil)
thirdView.layer.position.x = 100

动画的beginTime属性可以设置将要执行的动画的绝对开始时间,通过CACurrentMediaTime()函数获取当前时间再加上你想要延迟执行的时间的,单位秒。

动画代理

动画代理可以监听动画的进行状态,在动画开始和结束的时候回通知代理. 也就是会调用以下两个函数:

1
2
func animationDidStart(anim: CAAnimation)
func animationDidStop(anim: CAAnimation, finished flag: Bool)

CAAnimation

CAAnimation和它的子类遵循KVO,也就意味着你可以把它们当成字典一样使用来添加新的接口在运行时。

例如你可以使用这种机制来给某个动画指定一个名字,以便你能够把它和其它动画区分开。

1
flyRight.setValue("somename", forKey: "name")

CASpringAnimation

Property Default Value
damping 10.0
mass 1.0
stiffness 100.0
initialVelocity 0.0

参数说明:

  • damping - 应用给系统的阻尼
  • mass - 在系统中重物的质量
  • stiffness - 附加在重物上的弹簧的硬度
  • initialVelocity - 附加在重物上的初始速度

使用实例:

1
2
3
4
5
6
7
8
9
10
let jump = CASpringAnimation(keyPath: "position.y")
jump.initialVelocity = 100.0
jump.mass = 10.0
jump.stiffness = 1500.0
jump.damping = 50.0

jump.fromValue = textField.layer.position.y + 1.0
jump.toValue = textField.layer.position.y
jump.duration = jump.settlingDuration
textField.layer.addAnimation(jump, forKey: nil)

备注: 此动画的duration参数可以通过前面几个参数自动计算得出

jump.duration = jump.settlingDuration

使用CAGradientLayer的locations实现滑动解锁

CAGradientLayerlocations属性做动画轻松的实现iPhone自带的滑动解锁效果

a.创建一个渐变图层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let gradientLayer: CAGradientLayer = {
    let gradientLayer = CAGradientLayer()

    // Configure the gradient here

    gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
    gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)

    let colors = [
        UIColor.blackColor().CGColor,
        UIColor.whiteColor().CGColor,
        UIColor.blackColor().CGColor
    ]
    gradientLayer.colors = colors

    let locations = [
        0.25,
        0.5,
        0.75
    ]
    gradientLayer.locations = locations

    return gradientLayer
}()

b.创建一个文本样式字典

1
2
3
4
5
6
7
8
9
let textAttributes : [String: AnyObject] = {
    let style = NSMutableParagraphStyle()
    style.alignment = .Center

    return [
        NSFontAttributeName:UIFont(name: "HelveticaNeue-Thin", size: 28.0)!,
        NSParagraphStyleAttributeName:style
    ]
}()

c.当设置文本时绘制内容,添加@IBInspectable方便看实时效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@IBInspectable var text: String! {
didSet {
    setNeedsDisplay()

    UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
    text.drawInRect(bounds, withAttributes: textAttributes)
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    let maskLayer = CALayer()
    maskLayer.backgroundColor = UIColor.clearColor().CGColor
    maskLayer.frame = CGRectOffset(bounds, bounds.size.width, 0)
    maskLayer.contents = image.CGImage

    gradientLayer.mask = maskLayer
    }
}

d.重新布局gradientLayer

1
2
3
4
5
6
7
override func layoutSubviews() {
    gradientLayer.frame = CGRect(
        x: -bounds.size.width,
        y: bounds.origin.y,
        width: 3 * bounds.size.width,
        height: bounds.size.height)
}

e.添加到window时向渐变层添加动画

1
2
3
4
5
6
7
8
9
10
11
12
13
override func didMoveToWindow() {
    super.didMoveToWindow()

    layer.addSublayer(gradientLayer)

    let gradientAnimation = CABasicAnimation(keyPath: "locations")
    gradientAnimation.fromValue = [0.0, 0.0, 0.25]
    gradientAnimation.toValue = [0.75, 1.0, 1.0]
    gradientAnimation.duration = 3.0
    gradientAnimation.repeatCount = Float.infinity

    gradientLayer.addAnimation(gradientAnimation, forKey: nil)
}

使用场景

给视图对象快速创建约束,可以使用比较冷门的VFL(Visual Format Language),本质是是基于自动布局(AutoLayout)。

解决的问题

以一种直观的方式,为视图创建约束

怎样解决

核心方法:

1
2
3
4
+ (NSArray<__kindof NSLayoutConstraint *> *)constraintsWithVisualFormat:(NSString *)format
                                                                options:(NSLayoutFormatOptions)opts
                                                                metrics:(nullable NSDictionary<NSString *,id> *)metrics
                                                                  views:(NSDictionary<NSString *, id> *)views;

Example:

1
2
3
4
[NSLayoutConstraint constraintsWithVisualFormat:@"|-[button1]-[button2]-[textField(>=20)]-|"
                                        options:0
                                        metrics:metrics
                                          views:views]

参数:

  • format:指定约束的格式。更多信息,在Auto Layout Guide查看Visual Format Language
  • opts:描述在视觉格式化字符串中的布局属性和方向
  • metrics:将出现在视觉格式化字符串的常量字典。字典的Keys必须是在出现在视觉格式化字符串的字符串类型,对应的values必须是NSNumber对象。
  • views:出现的视觉格式化字符串的字典view,所有的Keys必须是使用在视觉格式化字符串的字符串类型,对应的values必须是view对象。

返回值:

一个约束组合的数组,像视觉格式化字符串描述的一样表述了在提供的视图和它们的父视图之间的关系。所有的约束以在视觉格式化字符串被指定的约束顺序一致。

效率和可维护性

需要对VFL理解比较全面和深入,有一定的学习成本,维护起来比较困难,但是如果理解的比较深刻,对于简单的布局效率非常高,比直接创建约束要直观。

最佳实践

  • 每条约束格式字符串分水平(H,可省略)和垂直方向(V)。
  • | 代表父视图。
  • - 代表标准间距,两个子视图直接的值是8,与父视图之间的值是16。
  • [view] 每个视图必须用[]包裹起来,否则语法错误。
  • [view(>=44)] 可以为每个视图设置一些属性或者关系,写一个紧跟view后的括号集合,支持宽高,优先级,和其它视图之间的关系。
  • [view@20] 可以设置视图的约束的优先级,以@开头,取值范围(0 1000]。
  • [view1]-20-[view2] 可以指定view之间的水平或者垂直间距,写在一对 - 即可。
  • [view1][view2] 如果 - 省略则他们之间的距离为0。
  • [flexibleButton(>=70,<=100)] 多个条件之间用,连接并且之间不能有空格。

其它方案

  1. 使用NSLayoutConstraint的类方法创建(iOS96.0及以上可用)
1
2
3
4
5
6
7
+ (instancetype)constraintWithItem:(id)view1
                        attribute:(NSLayoutAttribute)attr1
                        relatedBy:(NSLayoutRelation)relation
                           toItem:(nullable id)view2
                        attribute:(NSLayoutAttribute)attr2
                       multiplier:(CGFloat)multiplier
                         constant:(CGFloat)c;
  1. 使用NSLayoutAnchor的工厂方法创建(iOS9.0及以上可用)
1
2
3
4
5
6
7
- (NSLayoutConstraint *)constraintEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor;
- (NSLayoutConstraint *)constraintGreaterThanOrEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor;
- (NSLayoutConstraint *)constraintLessThanOrEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor;

- (NSLayoutConstraint *)constraintEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor constant:(CGFloat)c;
- (NSLayoutConstraint *)constraintGreaterThanOrEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor constant:(CGFloat)c;
- (NSLayoutConstraint *)constraintLessThanOrEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor constant:(CGFloat)c;

使用注意事项

  • 使用时,必须把view的translatesAutoresizingMaskIntoConstraints属性设置为NO,否则约束可能更预期不一致。这是一个历史遗留的问题,由于在AutoLayout诞生以前一直使用Autoresizing来控制布局.

  • 重写updateConstraints()方法,在里面计算约束,然后调用setNeedsUpdateConstraints()触发更新约束。

  • 系统计算布局顺序:

    • updateConstraints()
    • layoutSubviews()
    • drawRect(_:)

系统计算布局顺序参考

LayoutCycle

具体使用示例,查看下面的Demo,样品工程


Demo地址:VFLDemo

参考链接

翻译自NSHipster的UIActivity​View​Controller

一直非常好奇代码和数据之间的联系。

某些编程语言,例如LispIoMathematica他们是同型性(代码即数据),那就意味着它们的代码是原始数据的呈现形式,它们本身在代码中也能够被维护。然而大多数其它语言,包括Objective-C,然而,在这两者之间有严格的边界,避免eval()和其它有动态引导加载的潜在危险方法。

当数据出现太大和呈现一些东西过于笨重除了使用二进制流的问题时代码和数据之间的不安被推向到了一个全新的高度。自从第一个操作系统出现开始,怎样编码,解码,解释二进制代表的图片,文档,和媒体一直是一个问题。

在OS X上的核心服务框架和iOS上提供标识功能和通过文件拓展和MIMIE type–根据通用数据标示,给数据类型的分类核心服务框架。UTIs提供了一种可拓展,阶梯分层系统,这给开发者在处理大量不同的文件类型提供了巨大的灵活性。例如,一个Ruby的源文件(.rb)被归类为:Ruby Source Code > Source Code > Text > Content > Data;一个QuickTime电影文件(.mov)被归类为 Video > Movie > Audiovisual Content > Content > Data。

UTIs表现得非常好在桌面的抽象文件系统中。然而,在一个移动范例中这就崩溃得很快,因为文件和目录是对用户隐藏的。还有,随着云服务和社交媒体扮演着越来越重要的角色在远程实体通过本地文件。因此,UTIs和URLs之间很紧张。

我们非常清楚我们需要一些别的东西。UIActivityViewController能否成为我们正在急切寻找的解决方案呢?


UIActivityViewController,在iOS6中被引入,提供了一套分享和在应用内执行数据操作的统一服务界面。

考虑到可操作的数据集合,一个UIActivityViewController实例通过如下方式创建:

1
2
3
4
5
6
7
8
9
10
NSString *string = ...;
NSURL *URL = ...;

UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[string, URL]
                                    applicationActivities:nil];
[navigationController presentViewController:activityViewController
                                   animated:YES
                                 completion:^{
  // ...
}];

这将在屏幕底部呈现如下界面:

activity sharing img

默认情况下,UIActivityViewController将会显示所有支持的可用服务类型,但是某些活动类型也可以排除:

1
activityViewController.excludedActivityTypes = @[UIActivityTypePostToFacebook];

活动类型被划分“动作”(action)和“分享”(share)类型:

UIActivityCategoryAction

  • UIActivityTypePrint
  • UIActivityTypeCopyToPasteboard
  • UIActivityTypeAssignToContact
  • UIActivityTypeSaveToCameraRoll
  • UIActivityTypeAddToReadingList
  • UIActivityTypeAirDrop

UIActivityCategoryShare

  • UIActivityTypeMessage
  • UIActivityTypeMail
  • UIActivityTypePostToFacebook
  • UIActivityTypePostToTwitter
  • UIActivityTypePostToFlickr
  • UIActivityTypePostToVimeo
  • UIActivityTypePostToTencentWeibo
  • UIActivityTypePostToWeibo

每种活动类型,支持一系列不同的数据类型。例如,一个Tweet可能由一个NSString并附带一个image和或者URL

不同活动类型支持的数据类型

Activity Type String Attributed String URL Data Image Asset Other
Post To Facebook
Post To Twitter
Post To Weibo
Message ✓* ✓* sms:// NSURL
Mail ✓+ ✓+ ✓+
Print ✓+ ✓+ UIPrintPageRenderer, UIPrintFormatter, & UIPrintInfo
Copy To Pasteboard UIColor, NSDictionary
Assign To Contact
Save To Camera Roll
Add To Reading List
Post To Flickr
Post To Vimeo
Post To Tencent Weibo
AirDrop

UIActivityItemSource & UIActivityItemProvider

当必要时和一个粘贴板项目能用来提供的数据类似,为了避免过度的内测开销或处理时间,活动类型可以是一种自定义类型。

任何遵循<UIActivityItemSource>协议的对象,包括内构UIActivityItemProvider类,可以用来动态提供不同类型的数据根据活动的类型。

UIActivityItemSource

获取数据项目:

  • activityViewControllerPlaceholderItem:
  • activityViewController:itemForActivityType:

提供数据项目信息:

  • activityViewController:subjectForActivityType:
  • activityViewController:dataTypeIdentifierForActivityType:
  • activityViewController:thumbnailImageForActivityType:suggestedSize:

下面是一个例子,根据是否分享到FaceBook或者Twitter来展示自定义一条消息是怎样使用的:

1
2
3
4
5
6
7
8
9
10
11
- (id)activityViewController:(UIActivityViewController *)activityViewController
         itemForActivityType:(NSString *)activityType
{
    if ([activityType isEqualToString:UIActivityTypePostToFacebook]) {
        return NSLocalizedString(@"Like this!");
    } else if ([activityType isEqualToString:UIActivityTypePostToTwitter]) {
        return NSLocalizedString(@"Retweet this!");
    } else {
        return nil;
    }
}

创建一个自定义UIActivity

除了上述的系统提供的活动,你也可以创建你自己的活动。

例如,让我们创建一个自定义活动类型,它能使一个URL中的图片添加上胡须通过使用mustache.me

Before:

Before

After:

After

首先,我们定义一个反向域名解析的标识符给活动类型:

1
static NSString * const HIPMustachifyActivityType = @"com.nshipster.activity.Mustachify";

然后指定它的分类为UIActivityCategoryAction并且提供一个本地化标题和iOS版本相应的图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma mark - UIActivity

+ (UIActivityCategory)activityCategory {
    return UIActivityCategoryAction;
}

- (NSString *)activityType {
    return HIPMustachifyActivityType;
}

- (NSString *)activityTitle {
    return NSLocalizedString(@"Mustachify", nil);
}

- (UIImage *)activityImage {
    if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
        return [UIImage imageNamed:@"MustachifyUIActivity7"];
    } else {
        return [UIImage imageNamed:@"MustachifyUIActivity"];
    }
}

下一步,我们创建一个辅助函数,HIPMatchingURLsInActivityItems,通过它返回一个支持类型的图片URL的数组。

1
2
3
4
5
6
7
8
9
10
11
12
static NSArray * HIPMatchingURLsInActivityItems(NSArray *activityItems) {
    return [activityItems filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:
    ^BOOL(id item, __unused NSDictionary *bindings) {
        if ([item isKindOfClass:[NSURL class]] &&
            ![(NSURL *)item isFileURL]) {
            return [[(NSURL *)item pathExtension] caseInsensitiveCompare:@"jpg"] == NSOrderedSame ||
            [[(NSURL *)item pathExtension] caseInsensitiveCompare:@"png"] == NSOrderedSame;
        }

        return NO;
    }]];
}

这个函数让后被用在-canPerformWithActivityItems:prepareWithActivityItems:来获取小胡子的第一张PNG或JPEG图片的URL,假如有的话。

1
2
3
4
5
6
7
8
9
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems {
    return [HIPMatchingURLsInActivityItems(activityItems) count] > 0;
}

- (void)prepareWithActivityItems:(NSArray *)activityItems {
    static NSString * const HIPMustachifyMeURLFormatString = @"http://mustachify.me/%d?src=%@";

    self.imageURL = [NSURL URLWithString:[NSString stringWithFormat:HIPMustachifyMeURLFormatString, self.mustacheType, [HIPMatchingURLsInActivityItems(activityItems) firstObject]]];
}

我们的网站服务提供了各种胡须选项,它被定义在一个枚举中:

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSInteger, HIPMustacheType) {
    HIPMustacheTypeEnglish,
    HIPMustacheTypeHorseshoe,
    HIPMustacheTypeImperial,
    HIPMustacheTypeChevron,
    HIPMustacheTypeNatural,
    HIPMustacheTypeHandlebar,
};

最终,我们提供了一个UIViewController来显示图片。在这个例子中,一个简单的UIWebView控制器就够用了。

1
2
3
4
5
6
7
8
9
10
11
12
@interface HIPMustachifyWebViewController : UIViewController <UIWebViewDelegate>
@property (readonly, nonatomic, strong) UIWebView *webView;
@end

- (UIViewController *)activityViewController {
    HIPMustachifyWebViewController *webViewController = [[HIPMustachifyWebViewController alloc] init];

    NSURLRequest *request = [NSURLRequest requestWithURL:self.imageURL];
    [webViewController.webView loadRequest:request];

    return webViewController;
}

为了使用我们加了新胡须的活动,我们简单把它传递到UIActivityViewController构造器中:

1
2
3
4
HIPMustachifyActivity *mustacheActivity = [[HIPMustachifyActivity alloc] init];
UIActivityViewController *activityViewController =
  [[UIActivityViewController alloc] initWithActivityItems:@[imageURL]
                                    applicationActivities:@[mustacheActivity]];

手动执行动作

现在是一个提醒的好时机,当UIActivityViewController允许用户执行他们选择的动作,分享也能通过手动执行,当场景出现时。

所有为了完整性,下面是怎样手动执行这些动作示例:

Open URL

1
2
NSURL *URL = [NSURL URLWithString:@"http://nshipster.com"];
[[UIApplication sharedApplication] openURL:URL];

系统支持的URL协议包括:mailto:tel:sms:maps:

添加到Safari阅读列表

1
2
3
4
5
6
7
@import SafariServices;

NSURL *URL = [NSURL URLWithString:@"http://nshipster.com/uiactivityviewcontroller"];
[[SSReadingList defaultReadingList] addReadingListItemWithURL:URL
                                                        title:@"NSHipster"
                                                  previewText:@"..."
                                                        error:nil];

保存到相册

1
2
3
4
5
UIImage *image = ...;
id completionTarget = self;
SEL completionSelector = @selector(didWriteToSavedPhotosAlbum);
void *contextInfo = NULL;
UIImageWriteToSavedPhotosAlbum(image, completionTarget, completionSelector, contextInfo);

发送 SMS

1
2
3
4
5
6
7
8
9
@import MessageUI;

MFMessageComposeViewController *messageComposeViewController = [[MFMessageComposeViewController alloc] init];
messageComposeViewController.messageComposeDelegate = self;
messageComposeViewController.recipients = @[@"mattt@nshipster•com"];
messageComposeViewController.body = @"Lorem ipsum dolor sit amet";
[navigationController presentViewController:messageComposeViewController animated:YES completion:^{
    // ...
}];

发送 Email

1
2
3
4
5
6
7
8
9
10
@import MessageUI;

MFMailComposeViewController *mailComposeViewController = [[MFMailComposeViewController alloc] init];
mailComposeViewController.mailComposeDelegate = self;
[mailComposeViewController setToRecipients:@[@"mattt@nshipster•com"]];
[mailComposeViewController setSubject:@"Hello"];
[mailComposeViewController setMessageBody:@"Lorem ipsum dolor sit amet" isHTML:NO];
[navigationController presentViewController:mailComposeViewController animated:YES completion:^{
    // ...
}];

发送 Tweet

1
2
3
4
5
6
7
8
9
@import Social;

SLComposeViewController *tweetComposeViewController = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter];
[tweetComposeViewController setInitialText:@"Lorem ipsum dolor sit amet."];
[self.navigationController presentViewController:tweetComposeViewController
                                        animated:YES
                                      completion:^{
                                          // ...
                                      }];

IntentKit

当所有的这些都是印象深刻和非常有用,它在iOS中的活动范例特别缺乏,当和在Android中的丰富的Intents Model相比。

在Android中,apps能够注册不同的intents,来指示它们能够用在Maps,或者充当一个浏览器,能够被涉及到相关活动的默认app选中,比如指引方向,或者一个书签的URL。

然而iOS缺乏来支持这些的可拓展的基础架构,有一个叫IntentKit第三方库,作者@lazerwalker(因f*ingblocksyntax.com成名),这是一个有趣的例子关于我们怎样缩小这方面的差距。

IntentKit demo

通常,一个开发者可能已经做了很多第一步的工作,来决定是否某个app是否安装,然后怎样构造一个URL来支持某个特殊的活动。

IntentKit加强了连结到大多数流行的Web,Maps,Mail,Twitter,Facebook,和Google+客户的的逻辑,以和UIActivityViewController相似的UI。

任何寻找提升他们的分享体验到下一个级别的人都应该仔细看看这个。

在iOS长期生存能力作为一个平台取决于像UIActivityViewController类型的分享机制存上在很大的争论。正如俗语所言:“资讯应该免费”。任何阻碍方式统一的东西,最终都会失去一些没有的东西。

将来的景象是公共的远程视图控制器APIs带给我希望,对于将来在iOS平台上的分享功能。然而现在我们做的和UIActivityViewController相比糟糕得多。

前言

2016对于我来说的意义,就像10年前的今天,iPhone之余乔布斯,iPhone发布后10年后的今天,小程序之余张小龙。它跨越了一道深深的鸿沟,并向自己理想的方向坚定前行。

一直相信努力就会有回报,付出就会有收获,是金子总会发光。

  • 2016是我加入客路一周年的时间,认识了很多有趣的朋友和同事的一
  • 2016是我迈出自我,走出中国探索未知的世界的一年。
  • 2016是旅行,工作,生活,学习,社交一样都没落下的一年。

曾经的铁哥们也即将回归东莞,这样我们的距离又进了一步,以后有更多的时间聊理想谈人生,想想突然好激动。

业绩

2016-2017年间一共发布17个版本,多次被Apple推荐,崩溃率小于0.2%,这就是给自己和公司交出最满意的答卷。

下面是Klook App上Apple的推荐页截图:

klook Apple App recommend

下面是Klook iMessage App上Apple的推荐页截图:

klook Apple iMessage App recommend

有时发现互联网真的很奇妙,他能让很多人迅速积累大量的财富成名走上人生的巅峰,过上自己想要的生活,只要你一个好的idea和一个强大的执行力。

这一年来个人技术博客一共写了46遍文章,也算一点小积蓄,希望来年继续坚持,能够影响和帮助更多的人,因为技术这条道路上坑实在是太多。

Github405次commit还算中规中矩,希望来年分享更多的有价值的东西给有需要的小伙伴,也留给自己方便以后查阅。

github 2016 commit record

当然现在Github上的内容Fork和博客居多,原创开源项目还比较少,希望来年多输出一点东西。

未来

愿望这东西还真神奇,许一个试试吧,或许它真的实现了呢,就非常有趣了。 给自己定下的旅行清单,学习清单,阅读清单,电影清单基本都完成,这是一件非常棒的事情,为自己点个赞。单反已经从风光转人像,希望来年多出点片,新年即将来临,希望自己坚持一周2次的锻炼,保持精力充沛以便好投入到感情,生活中以及学习中,毕竟互联网是一门终生学习的职业,也是容易产生奇迹的职业,说不定一下个就是你呢。

来年希望和自己喜欢的人旅拍,多尝试一些没尝试过的事情,交更多有趣的朋友。干巴爹😁😁😁

主要涉及到Tips:

  • 获取设备音量
  • 静音模式失效
  • 监听音量改变
  • 设置设备音量
  • 监听静音按钮
  • 监听耳机拔插

1.获取设备音量

播放音频可以通过:

1
2
MPMusicPlayerController *iPod = [MPMusicPlayerController systemMusicPlayer];
float volumeLevel = iPod.volume;

播放视频可以通过:

1
float outputVolume = [[AVAudioSession sharedInstance] outputVolume];

推荐下面的方法,上面的在某些版本可能有问题,下面的方法兼容iOS6及以上

2.静音模式失效

通过设置音频会话的category实现:

1
2
3
4
5
NSError *setCategoryError = nil;
BOOL success = [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback
                                                    error: &setCategoryError];

if (!success) { /* handle the error in setCategoryError */ }

这样App就不会随着手机静音键打开而静音,可在手机静音下播放声音😁

3.监听音量改变

监听音频改变私有通知:

1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(volumeChanged:) name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];

实现通过回调:

1
2
3
4
5
6
7
- (void)volumeChanged:(NSNotification *)notification
{
    float volume = [[[notification userInfo] objectForKey:@"AVSystemController_AudioVolumeNotificationParameter"]
     floatValue];

     // do something
}

4.设置设备音量

使用MPVolumeView类,便利它的子views找到类为MPVolumeSlider的滑竿。

1
2
3
4
5
6
7
8
MPVolumeView *volumeView = [[MPVolumeView alloc] init];
UISlider *volumeViewSlider = nil;
  for (UIView *view in [volumeView subviews]){
      if ([view.class.description isEqualToString:@"MPVolumeSlider"]) {
          volumeViewSlider = (UISlider *)view;
          break;
      }
  }

然后再通过设置volumeViewSlidervalue即可。

1
_volumeViewSlider.value = someVolume;

5.监听静音按钮

参考Sound Switch - Sharkfood的实现。

使用很简单,判断是否为静音模式:

1
2
3
if ([SharkfoodMuteSwitchDetector shared].isMute) {
    // do something
}

动态监听,通过block回调:

1
2
3
[SharkfoodMuteSwitchDetector shared].silentNotify = ^(BOOL silent){
      // do something
};

6.监听耳机拔插

监听AVAudioSessionRouteChangeNotification通知:

1
2
3
[[NSNotificationCenter defaultCenter] addObserver:self
                                        selector:@selector(audioRouteChangeListenerCallback:)                                               name:AVAudioSessionRouteChangeNotification
                                          object:nil];

实现回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification
{
    NSDictionary *interuptionDict = notification.userInfo;

    NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
    switch (routeChangeReason) {
        case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
            // 耳机插入
            break;

        case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
            // 耳机拔掉
            break;

        case AVAudioSessionRouteChangeReasonCategoryChange:
            // called at start - also when other audio wants to play
            NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange");
            break;
    }
}

前言

UIImageView应该是iOS中使用最频换的控件,就如日常吃饭一样,天天都在重复,有时或许应该反思一下,怎么使用这个控件,达到低能耗,最佳用户体验。

针对单张图片来说,常见的处理是在图片准备显示时增加一个淡出动画,能使图片显示闲的很平滑。

多张图片也一样,在第一张图片的基础上淡出原来的图片,淡入新的图片。也可以说是溶解效果。

很多人喜欢对图片的alpha做淡出动画,使alpha从0到1动画改变。这种动画有一点不好的是,在动画结束后,图片会明显的出现一闪,这样使动画看起来有点突兀。比较好的做法时,在将要显示时给图片做一个转场动画。

淡出动画实现

下面是其中一种简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation UIImageView (RFWebImage)

- (void)animatedChangeToImage:(UIImage *)img
{
    [UIView transitionWithView:self
                      duration:0.3f
                       options:UIViewAnimationOptionTransitionCrossDissolve
                    animations:^{
                        self.image = img;
                    } completion:NULL];
}

@end

思路:在ImageView将要显示是使用转场动画函数来实现淡出动画效果,体验应该是是各种动画中最好的了,而且使用起来很简单。

在淡出显示的动画基础上,我们引出今天的主角,动画切换Image。

思路:单张图片淡出我们已经实现,现在做的就是在切换一张新的图片时同时再加入淡出或者说溶解效果即可。

动画切换Image

比较常见的有下面3种实现:

  • CATransition类实现
  • UIView动画转场API实现
  • CABasicAnimation类实现

CATransition实现

CATransition类是iOS中很好用的控制转场动画的类,通过简单的配置可以实现常见而炫酷的动画效果,变换类型通过type字段控制,subtype可以很细化控制动画的方向(比如动画开始的上下左右方向)。CATransition继承至CAAnimation可以对动画设置动画曲线(timingFunction),可以通过代理获取动画状态(是已经开始,还是已经停止,已经是否完成)。

type支持四种类型:

  • kCATransitionFade // 淡入淡出
  • kCATransitionMoveIn // 从某个方向向终点平移知道覆盖在上方
  • kCATransitionPush // 把原来的推出去,自己推出去
  • kCATransitionReveal // 把原来的从正上方解开,自己在下面

下面是样板代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)animatedSwichImageMethodOne {

    UIImage *toImage = [self getRadomImage];

    CATransition *transition = [CATransition animation];
    transition.duration = 0.3f;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    transition.type = kCATransitionFade;
    transition.subtype = kCATransitionFromTop;

    [self.imageViewOne.layer addAnimation:transition forKey:nil];
    [self.imageViewOne setImage:toImage];
}

UIView动画转场

1
+ (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);

通过上面的函数实现,其实是对第一种的高级封装。通过设置optionsUIViewAnimationOptionTransitionCrossDissolve即可。

下面是样板代码:

1
2
3
4
5
6
7
8
9
10
11
- (void)animatedSwichImageMethodTwo {

    UIImage *toImage = [self getRadomImage];

    [UIView transitionWithView:self.imageViewTwo
                      duration:0.3f
                       options:UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionCurveEaseInOut
                    animations:^{
                        self.imageViewTwo.image = toImage;
                    } completion:nil];
}

CABasicAnimation实现

CABasicAnimation是核心动画一个重要的类,继承至CAPropertyAnimation,可以对所有的可动画属性做动画,可以通过fromValuetoValuebyValue字段控制动画的进度。 在这里我们是对CALayercontents属性做动画,在改变图片时,创建一个CABasicAnimation对象添加到ImageView的图层上即可。

下面是样板代码:

1
2
3
4
5
6
7
8
9
10
11
- (void)animatedSwichImageMethodThree {

    UIImage *toImage = [self getRadomImage];

    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"contents"];
    animation.toValue = toImage;
    animation.duration = 0.3f;

    [self.imageViewThree.layer addAnimation:animation forKey:@"contentsAnimationKey"];
    [self.imageViewThree setImage:toImage];
}

更多内容请下载Demo查看(🤔Bonus: Flip效果🤔

Toll-Free Bridged Types

Core Foundation框架和Foundation框架中有很多数据类型可以交替转换。能够被交替转换的数据类型也被叫做Toll-Free Bridged数据类型。这意味着你能像参数一样使用相同的数据结构对一个Core Foundation的函数进行调用,或者像Objective-C的消息接受模式一样执行。例如,NSLocale(查看NSLocale Class Reference)可以与在Core Foundation中对应的CFLocale(查看CFLocale Reference)之间互相转换。

不是所有数据类型都是Toll-Free Bridged,即使它们的名字可能让你认为它们是。例如,NSRunLoop是没有对应的桥接类型CFRunLoop,NSBundle也没有对应的桥接类型CFBundleNSDateFormatter同样没有对应的桥接类型CFDateFormatter

文章末尾表1提供了一份支持无缝桥接的数据类型的列表。

注意:假如你使用一个自定义回调在一个Core Foundation框架的集合中,包含一个NULL回调,当使用Objective-C的方式接入它,它的内存管理方式是未定义的。

类型转换和对象语义周期声明

通过无缝桥接技术,在一个你以NSLocale *做为一个参数的方法的例子中,你能传递一个CFLocaleRef结构体,并且当你看到有一个CFLocaleRef参数的函数中,你能够传递一个NSLocale实例对象。当然你也必须提供给编译器相关的一些其它信息:第一,你必须转换一种类型成其它;第二,你可能必须指明对象的语义生命周期。

编译器理解Objective-C的方法并且返回Core Foundation数据类型,下面是Cocoa命名转换的历史(查看Advanced Memory Management Programming Guide)。例如,编译器知道,在iOS中,通过UIColorCGColor方法返回的CGColor并不应该被持有。你必须使用恰当的类型转换,像下面例子演示的那样:

1
2
NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]];
[colors addObject:(id)[[UIColor lightGrayColor] CGColor]];

编译器并不会自动管理Core Foundation对象的生命周期。你必须告诉编译器对象的语义所属关系通过使用一种转换(定义在objc/runtime.h)或者Core Foundation风格的宏(定义在 NSObject.h):

  • __bridge关键字表示转换指针在Objective-CCore Foundation之间而不会转换所属关系。
  • __bridge_retained关键字或者CFBridgingRetain表示转换指针在Objective-CCore Foundation之间并且把所属权交给你。你负责调用CFRelease或者相关的函数来交出对象的所属权。
  • __bridge_transfer关键字或者CFBridgingRelease表示转换一个非Objective-C的指针到Objective-C并且转换所属权给ARCARC负责交出对象的所属权。

下面是一些例子:

1
2
3
4
5
6
7
8
9
10
11
NSLocale *gbNSLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
CFLocaleRef gbCFLocale = (__bridge CFLocaleRef)gbNSLocale;
CFStringRef cfIdentifier = CFLocaleGetIdentifier(gbCFLocale);
NSLog(@"cfIdentifier: %@", (__bridge NSString *)cfIdentifier);
// Logs: "cfIdentifier: en_GB"

CFLocaleRef myCFLocale = CFLocaleCopyCurrent();
NSLocale *myNSLocale = (NSLocale *)CFBridgingRelease(myCFLocale);
NSString *nsIdentifier = [myNSLocale localeIdentifier];
CFShow((CFStringRef)[@"nsIdentifier: " stringByAppendingString:nsIdentifier]);
// Logs identifier for current locale

下面的例子显示了使用口述的Core Foundation内存管理规则来管理Core Foundation内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)drawRect:(CGRect)rect {
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
    CGFloat locations[2] = {0.0, 1.0};
    NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]];
    [colors addObject:(id)[[UIColor lightGrayColor] CGColor]];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
    CGColorSpaceRelease(colorSpace);  // Release owned Core Foundation object.

    CGPoint startPoint = CGPointMake(0.0, 0.0);
    CGPoint endPoint = CGPointMake(CGRectGetMaxX(self.bounds), CGRectGetMaxY(self.bounds));
    CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint,
                                kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
    CGGradientRelease(gradient);  // Release owned Core Foundation object.
}

无缝桥接类型

表1提供了一个在Core FoundationFoundation中可以交替转换数据类型列表。对每一对桥接类型,表也列举出了这些无缝桥接类型在OS X中的可用版本。

Core Foundation 类型 Foundation 类型 可用性
CFArrayRef NSArray OS X v10.0
CFAttributedStringRef NSAttributedString OS X v10.4
CFCalendarRef NSCalendar OS X v10.0
CFCharacterSetRef NSCharacterSet OS X v10.4
CFDataRef NSData OS X v10.0
CFDateRef NSDate OS X v10.4
CFDictionaryRef NSDictionary OS X v10.0
CFErrorRef NSError OS X v10.4
CFLocaleRef NSLocale OS X v10.0
CFMutableArrayRef NSMutableArray OS X v10.4
CFMutableAttributedStringRef NSMutableAttributedString OS X v10.0
CFMutableCharacterSetRef NSMutableCharacterSet OS X v10.4
CFMutableDataRef NSMutableData OS X v10.0
CFMutableDictionaryRef NSMutableDict OS X v10.4
CFMutableSetRef NSMutableSet OS X v10.0
CFMutableStringRef NSMutableString OS X v10.4
CFNumberRef NSNumber OS X v10.0
CFReadStreamRef NSInputStream OS X v10.4
CFRunLoopTimerRef NSTimer OS X v10.0
CFSetRef NSSet OS X v10.4
CFStringRef NSString OS X v10.0
CFTimeZoneRef NSTimeZone OS X v10.4
CFURLRef NSURL OS X v10.0
CFWriteStreamRef NSOutputStream OS X v10.4

表1

参考资料