Leewin's Blog

Where there's a will, there's a way.


  • Home

  • Archives

iOS-关于继承、分类

Posted on 2018-07-08

一、前言

笔者最近一直忙于开发业务需求,频繁的使用着继承、分类,切身的体会到很多需求用这两种方案都可以解决,这就面临着幸福二选一的问题了!但越到后面,越能感觉到其明显的区别,于是有所感~
继承与分类

二、需求简要

iOS 时间选择器(UIDatePicker)大家都有用过吧,很方便对吧,那性别选择、省市区选择、付款方式选择等功能,我们如何实现类似的效果呢?用UIPickerView实现单选择功能,就能实现类似的用户体验。

三、应用场景

笔者最近遇到的开发需求是产品的属性选择,比如我们现在要发起订单(我们可以类比一下淘宝),选择了佳得乐,这时候我们还要选择口味(蓝莓、鲜橙、哈密瓜等),然后还可以选择净含量等。

demo

四、代码实现

由于在实际开发中,通常都是与UITextField联合实现的,笔者就选择UITextField为最小颗粒度,对其进行处理。

  • 1.使用继承方案

LYTextField.h

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>

@interface LYTextField : UITextField

@property (nonatomic, strong) NSArray *dataArr;

@end

LYTextField.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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#import "LYTextField.h"

@interface LYTextField ()
<UIPickerViewDelegate, UIPickerViewDataSource>

@property (nonatomic, weak) UIPickerView *pickerView;

@end

@implementation LYTextField

- (instancetype)init {
self = [super init];
if (self) {
[self configFoundation];
}
return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self configFoundation];
}
return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configFoundation];
}
return self;
}

- (void)configFoundation {

CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;

UIView *optionView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, screenWidth, 44)];
optionView.backgroundColor = [UIColor orangeColor];

UIButton *cancelBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[cancelBtn setFrame:CGRectMake(0, 0, 70, 44)];
[cancelBtn setTitle:@"取消" forState:UIControlStateNormal];
[cancelBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[cancelBtn.titleLabel setFont:[UIFont systemFontOfSize:17.0]];
[cancelBtn addTarget:self action:@selector(onClickCancelBtn) forControlEvents:UIControlEventTouchUpInside];
[optionView addSubview:cancelBtn];

UIButton *finishBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[finishBtn setFrame:CGRectMake(screenWidth - 70, 0, 70, 44)];
[finishBtn setTitle:@"完成" forState:UIControlStateNormal];
[finishBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[finishBtn.titleLabel setFont:[UIFont systemFontOfSize:17.0]];
[finishBtn addTarget:self action:@selector(onClickFinishBtn) forControlEvents:UIControlEventTouchUpInside];
[optionView addSubview:finishBtn];

UILabel *titleL = [[UILabel alloc]initWithFrame:CGRectMake(screenWidth / 2.0 - 75.0, 0, 150, 44)];
titleL.textAlignment = NSTextAlignmentCenter;
titleL.text = @"请选择类型";
[optionView addSubview:titleL];

self.inputAccessoryView = optionView;

UIPickerView *pickerView = [[UIPickerView alloc] init];
pickerView.dataSource = self;
pickerView.delegate = self;
pickerView.backgroundColor = [UIColor whiteColor];
self.pickerView = pickerView;
self.inputView = pickerView;
}

- (void)onClickCancelBtn {
[self resignFirstResponder];
}

- (void)onClickFinishBtn {

NSInteger index = [self.pickerView selectedRowInComponent:0];
if (index < self.dataArr.count) {
self.text = self.dataArr[index];
}
[self resignFirstResponder];
}

#pragma mark - Picker view data source && delegate
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return self.dataArr.count;
}

- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component {

if (row < self.dataArr.count) {
return self.dataArr[row];
}

return @"";
}
@end

  • 2.使用分类方案

    UITextField+Tool.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #import <UIKit/UIKit.h>

    typedef void(^DidSelectedBlock)(NSString *value);

    @interface UITextField (Tool)
    <UIPickerViewDelegate, UIPickerViewDataSource>

    - (void)configSigleChoiceWith:(NSArray *)preData completion:(DidSelectedBlock)completion;

    - (void)configValue:(NSString *)value;

    @end

UITextField+Tool.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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#import "UITextField+Tool.h"
#import <objc/runtime.h>

@interface UITextField ()

@property (nonatomic, strong) NSArray *choiceData;

@property (nonatomic, copy) DidSelectedBlock didSelectedBlock;

@end

NSString *const KeyChoiceData = @"KeyChoiceData";
NSString *const KeyDidSelectedBlock = @"KeyDidSelectedBlock";

@implementation UITextField (Tool)

- (NSArray *)choiceData {
return objc_getAssociatedObject(self, &KeyChoiceData);
}

- (void)setChoiceData:(NSArray *)choiceData {
objc_setAssociatedObject(self, &KeyChoiceData, choiceData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (DidSelectedBlock)didSelectedBlock {
return objc_getAssociatedObject(self, &KeyDidSelectedBlock);
}

- (void)setDidSelectedBlock:(DidSelectedBlock)didSelectedBlock {
objc_setAssociatedObject(self, &KeyDidSelectedBlock, didSelectedBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void)configSigleChoiceWith:(NSArray *)preData completion:(DidSelectedBlock)completion {
self.choiceData = [preData mutableCopy];
self.didSelectedBlock = completion;

[self setSelectedList];
}

- (void)configValue:(NSString *)value {
self.text = value;
if (self.didSelectedBlock) {
self.didSelectedBlock(value);
}
if (value.length == 0) {
return;
}
for (NSInteger i = 0; i < self.choiceData.count; i ++) {
NSString *itemStr = self.choiceData[I];
if ([value isEqualToString:itemStr]) {
UIPickerView *pickerView = (UIPickerView *)self.inputView;
[pickerView selectRow:i inComponent:0 animated:YES];
break;
}
}
}

- (void)setSelectedList {

CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;

UIView *optionView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, screenWidth, 44)];
optionView.backgroundColor = [UIColor orangeColor];

UIButton *cancelBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[cancelBtn setFrame:CGRectMake(0, 0, 70, 44)];
[cancelBtn setTitle:@"取消" forState:UIControlStateNormal];
[cancelBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[cancelBtn.titleLabel setFont:[UIFont systemFontOfSize:17.0]];
[cancelBtn addTarget:self action:@selector(onClickCancelBtn) forControlEvents:UIControlEventTouchUpInside];
[optionView addSubview:cancelBtn];

UIButton *finishBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[finishBtn setFrame:CGRectMake(screenWidth - 70, 0, 70, 44)];
[finishBtn setTitle:@"完成" forState:UIControlStateNormal];
[finishBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[finishBtn.titleLabel setFont:[UIFont systemFontOfSize:17.0]];
[finishBtn addTarget:self action:@selector(onClickFinishBtn) forControlEvents:UIControlEventTouchUpInside];
[optionView addSubview:finishBtn];

UILabel *titleL = [[UILabel alloc]initWithFrame:CGRectMake(screenWidth / 2.0 - 75.0, 0, 150, 44)];
titleL.textAlignment = NSTextAlignmentCenter;
titleL.text = @"请选择类型";
[optionView addSubview:titleL];

self.inputAccessoryView = optionView;

UIPickerView *pickerView = [[UIPickerView alloc] init];
pickerView.dataSource = self;
pickerView.delegate = self;
pickerView.backgroundColor = [UIColor whiteColor];

self.inputView = pickerView;
}

#pragma mark - Actions

- (void)onClickCancelBtn {
[self resignFirstResponder];
}

- (void)onClickFinishBtn {

UIPickerView *pickerView = (UIPickerView *)self.inputView;

NSInteger index = [pickerView selectedRowInComponent:0];

if (index < self.choiceData.count) {

[self configValue:self.choiceData[index]];
}
[self resignFirstResponder];
}

#pragma mark - Picker view data source && delegate
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return self.choiceData.count;
}

- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component {

if (row < self.choiceData.count) {
return self.choiceData[row];
}

return @"";
}

@end

  • 3.使用代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    - (void)configTextFieldDemo {

    CGFloat screenWidth = self.view.bounds.size.width;

    LYTextField *textField1 = [[LYTextField alloc] init];
    textField1.bounds = CGRectMake(0, 0, 200, 44);
    textField1.borderStyle = UITextBorderStyleRoundedRect;
    textField1.center = CGPointMake(screenWidth/2, 100);
    textField1.placeholder = @"请选择口味";
    textField1.dataArr = @[@"鲜橙", @"哈密瓜", @"蓝莓"];
    [self.view addSubview:textField1];

    UITextField *textField2 = [[UITextField alloc] init];
    textField2.bounds = CGRectMake(0, 0, 200, 44);
    textField2.borderStyle = UITextBorderStyleRoundedRect;
    textField2.center = CGPointMake(screenWidth/2, 200);
    textField2.placeholder = @"请选择净含量";
    [self.view addSubview:textField2];
    NSArray *dataArr = @[@"250 mL", @"500 mL", @"1000 mL"];
    [textField2 configSigleChoiceWith:dataArr completion:nil];
    }

五、写在最后

  • 上例中,分类明显的优势在于可以孤立存在,也是最干净的编码。其实就是在一个对象的基础上增加额外的功能形成另外一个对象。
  • 关于继承毫无疑问最大的优点是代码复用。但是很多时候继承也可能会被无止境的滥用,造成代码结构散乱,后期维护困难等,其中有可能带来最大的问题是高耦合,依赖性太强。
  • 实际开发中如果继承超过2层的时候,就要慎重了,因为这可能是滥用继承的开始。
  • 分类是一种平行的架构,相互独立存在,可移植性强,继承是纵向的架构,相互依赖,缺一个,整个架构就断层了。

React Native中的动画过渡! [译]

Posted on 2018-06-09

原文: Animated Transition in React Native!
作者: Jiří Otáhal
站点: Medium

这篇文章有近15k的浏览量。 对于某些人来说,它可能不是什么,但对我来说,这是一个很大的动力。 这就是为什么我决定创建[Pineapple — Financial Manager]。 在仅仅22天的时间里,我完成了iOS版本,Android版本和[网站演示][Pineapple — Financial Manager],花费了300美元,并撰写了一些关于这方面的文章。 不能说这次我很享受。 你也应该尝试一下!

最近我试图从下一个动画挑战中获得灵感。 由此我们去看一下[Ivan Parfenov ][Ivan Parfenov medium ]创建的。 我很好奇,如果我能够使用React Native来实现这种转换效果。 为什么我们甚至需要像这样的动画? 阅读Pablo Stanley的优秀UI动画技巧。

For PLΛTES by Ivan Parfenov

For PLΛTES by Ivan Parfenov

我们可以看到这样一组动画。

  • 工具栏平铺(显示/隐藏)
  • 底部栏(显示/隐藏)
  • 移动选定的项目
  • 隐藏所有其他项目
  • 显示详细信息项目甚至更多。

Timeline of animations

关于转换的难点在于同步所有这些动画。 我们不能真正卸载列表页面并显示详细信息页面,因为我们需要等到所有动画完成。 另外,我很喜欢干净的代码。 易于维护。 如果您曾尝试为您的项目实现动画,那么代码通常会变得混乱。 充满辅助变量,疯狂的计算等。这就是为什么我想介绍react-native-motion。

Timeline of animations.gif

关于react-native-motion的想法

你看到工具栏标题的动画了吗? 您只需将标题移动一点并以动画的方式将透明度设置为0/1。 没什么大不了! 但是正因为如此,你需要编写这样的代码。 甚至在你真正开始为该组件编写UI之前。

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
class TranslateYAndOpacity extends PureComponent {
constructor(props) {
// ...
this.state = {
opacityValue: new Animated.Value(opacityMin),
translateYValue: new Animated.Value(translateYMin),
};
// ...
}
componentDidMount() {
// ...
this.show(this.props);
// ...
}
componentWillReceiveProps(nextProps) {
if (!this.props.isHidden && nextProps.isHidden) {
this.hide(nextProps);
}
if (this.props.isHidden && !nextProps.isHidden) {
this.show(nextProps);
}
}
show(props) {
// ...
Animated.parallel([
Animated.timing(opacityValue, { /* ... */ }),
Animated.timing(translateYValue, { /* ... */ }),
]).start();
}
hide(props) {
// ...
Animated.parallel([
Animated.timing(opacityValue, { /* ... */ }),
Animated.timing(translateYValue, { /* ... */ }),
]).start();
}
render() {
const { opacityValue, translateYValue } = this.state;

const animatedStyle = {
opacity: opacityValue,
transform: [{ translateY: translateYValue }],
};

return (
<Animated.View style={animatedStyle}>{this.props.children}</Animated.View>
);
}
}

现在我们来看看如何使用react-native-motion来实现这一点。 我知道动画通常非常具体。 我知道React Native提供了非常强大的动画API。 无论如何,拥有一个拥有基本动画库会很棒。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { TranslateYAndOpacity } from 'react-native-motion';

class ToolbarTitle extends PureComponent {
render() {
return (
<TranslateYAndOpacity duration={250}>
<View>
// ...
</View>
</TranslateYAndOpacity>
);
}
}

共享元素

这一挑战最大的问题是选中列表Cell的移动。 这个Cell是列表页面和详情页面之间共享的。 当元素实际上没有完全定位时,如何将Cell从FlatList移动到Detail页面的顶部? 用react-native-motion就很容易。

1
2
3
4
5
6
7
8
9
10
11
12
// List items page with source of SharedElement
import { SharedElement } from 'react-native-motion';

class ListPage extends Component {
render() {
return (
<SharedElement id="source">
<View>{listItemNode}</View>
</SharedElement>
);
}
}

我们在List Page中指定了SharedElement的源元素。 现在,我们需要为“详细信息”页面上的目标元素执行几乎相同的操作。 以此知道我们想要移动共享元素的位置。

1
2
3
4
5
6
7
8
9
10
11
12
// Detail page with a destination shared element
import { SharedElement } from 'react-native-motion';

class DetailPage extends Component {
render() {
return (
<SharedElement sourceId="source">
<View>{listItemNode}</View>
</SharedElement>
);
}
}

魔力到底在哪里?

我们如何将相对定位的元素从一个页面移动到另一个页面? 其实,我们不能。 SharedElement的工作原理是这样的:

  • 获取源元素的位置
  • 获取目标元素的位置(显然,如果没有这个步骤,动画将无法启动)
  • 创建共享元素的一个克隆(重点的魔力!)
  • 在屏幕上方渲染一个新图层
  • 渲染将覆盖源元素的克隆元素(在源元素的位置)
  • 开始移动到目标位置
  • 一旦到达目的地位置,移除克隆的元素

The magic

您大概可以想象在同一时刻同一个React节点有3个元素。 这是因为在移动动画期间,List Page被Detail Page覆盖。 这就是为什么我们可以看到所有3个元素。 但是我们想要创造一个视觉效果,即我们实际上是移动了原始的源元素(实际上这只是一种错觉)。

SharedElement timeline

你可以看到A点和B点。这是移动进行的时间段。 你也可以看到SharedElement触发了一些有用的事件。 在这种情况下,我们使用WillStart和DidFinish事件。 当开始移动到目的地时,将源元素和目标元素的透明度设置为0,并在动画完成后将目标元素的透明度恢复为1。

你怎么看?

react-native-motion仍然还在维护和支持中。 这绝对不是这个库的最终稳定版本。 但我希望这是一个好开始😊我很想听听你对此有何看法!

原文: Animated Transition in React Native!
作者: Jiří Otáhal
站点: Medium

位掩码(BitMask)的介绍与使用

Posted on 2018-05-06 | Edited on 2018-06-04

一、前言

位运算在我们实际开发中用得很少,主要原因还是它对于我们而言不好读、不好懂、也不好计算,如果不经常实践,很容易就生疏了。但实际上,位运算是一种很好的运算思想,它的优点自然是计算快,代码更少。

二、基本知识介绍

  • 二进制:
    二进制是由1和0两个数字组成的,它可以表示两种状态,即开和关。所有输入电脑的任何信息最终都要转化为二进制。目前通用的是ASCII码。最基本的单位为bit。
  • 位运算:
    程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算说穿了,就是直接对整数在内存中的二进制位进行操作。比如,and运算本来是一个逻辑运算符,但整数与整数之间也可以进行and运算。举个例子,6的二进制是110,11的二进制是1011,那么6 and 11的结果就是2,它是二进制对应位进行逻辑运算的结果(0表示False,1表示True,空位都当0处理)。

三、问题引用

  • 老鼠试毒
    有1000瓶水,其中有一瓶有毒,小白鼠只要尝一点带毒的水24小时后就会死亡,问至少要多少只小白鼠才能在24小时内鉴别出哪瓶水有毒?

老鼠试毒.png

这里,位掩码的使用就可以巧妙的解决此问题。

我们先将问题简化一下:假设只有8瓶水,其中1瓶有毒。

8杯水分别编号.png

将该矩阵转置,得:

水杯矩阵转置.png

依上述场景,取4只容器,转置后的矩阵数列配组合溶液:
取数位上为1的水,放入相应的容器,即:
第一杯:只包含8号水
第二杯:包含4、5、6、7号水
第三杯:包含2、3、6、7号水
第四杯:包含1、3、5、7号水

取4只老鼠,编号1、2、3、4,分别喝下第一杯…第四杯水,
4只老鼠的生死状态依次记为 w x y z,(w,x,y,z = {0,1})
死亡记作1,非死亡记作0
将二进制数列wxyz转为十进制,则得到有毒水的号码。
假设6号水有毒,那么往回推算,不难看出,第2、3只老鼠会死亡,
得到的wxyz的数列就是0110,转十进制后就是6。

将1000瓶依次编号:1,2,3,4,…,1000; 且都记作二进制;
那我们要用多少位来表示呢?
总数是1000,2^9=512, 2^10=1024,于是至少要10位才够表示,
也就是:0000000001,0000000010,0000000011,…,1111101000;
道理同上。

四、结合实际问题

我们已经见识了二进制的厉害之处了,接下来我们结合代码来看看,在iOS开发中的应用(其实在任何开发中都一样)

  • 在实际开发中,我们常常遇到权限的判断的问题,比如说,不同的用户对系统有不同的操作权限,有的用户可能有多种权限,我们最常规的办法就是每一个权限定义一个BOOL值。
    假设,某系统有4种权限,那么,就有了:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @interface BM_User : NSObject

    @property (nonatomic, assign) BOOL permission1;

    @property (nonatomic, assign) BOOL permission2;

    @property (nonatomic, assign) BOOL permission3;

    @property (nonatomic, assign) BOOL permission4;

    @end

那用户A同时拥有permission1、permission2、permission4怎么表示呢?

1
2
3
4
BM_User *userA = [[BM_User alloc] init];
userA.permission1 = YES;
userA.permission2 = YES;
userA.permission4 = YES;

这样的操作大家见多了吧?那我们来看看另一种写法:

1
2
3
4
5
@interface BM_User : NSObject

@property (nonatomic, assign) OptionPermission permission;

@end

有人就要问了,OptionPermission是什么鬼?来,继续。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
权限枚举

- 1: permission1,二进制第1位,0表示否,1表示是
- 2: permission2,二进制第2位,0表示否,1表示是
- 4: permission3,二进制第3位,0表示否,1表示是
- 8: permission4,二进制第4位,0表示否,1表示是
*/
typedef NS_OPTIONS(NSUInteger, OptionPermission) {
permission1 = 1 << 0,//0001,1
permission2 = 1 << 1,//0010,2
permission3 = 1 << 2,//0100,4
permission4 = 1 << 3,//1000,8
};

那用户A同时拥有permission1、permission2、permission4怎么表示呢?

1
2
BM_User *userA = [[BM_User alloc] init];
userA.permission = permission1 | permission2 | permission4;

是不是神清气爽?

现在我们就具体化4种权限,并给出基础位掩码的表达及运算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef BM_Head_h
#define BM_Head_h

/**
权限枚举

- 1: 是否允许查询,二进制第1位,0表示否,1表示是
- 2: 是否允许新增,二进制第2位,0表示否,1表示是
- 4: 是否允许修改,二进制第3位,0表示否,1表示是
- 8: 是否允许删除,二进制第4位,0表示否,1表示是
*/
typedef NS_OPTIONS(NSUInteger, OptionPermission) {
ALLOW_SELECT = 1 << 0,//0001,1
ALLOW_INSERT = 1 << 1,//0010,2
ALLOW_UPDATE = 1 << 2,//0100,4
ALLOW_DELETE = 1 << 3,//1000,8
};

#endif /* BM_Head_h */

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
#import "BM_Permission.h"
#import "BM_Head.h"

@interface BM_Permission ()

/** 存储目前的权限状态 */
@property (nonatomic, assign) OptionPermission flag;

@end

@implementation BM_Permission


/** 重新设置权限 */
- (void)setPermission:(OptionPermission)permission {
self.flag = permission;
}

/** 添加一项或多项权限 */
- (void)enable:(OptionPermission)permission {
self.flag |= permission;
}

/** 删除一项或多项权限 */
- (void)disable:(OptionPermission)permission {
self.flag &= ~permission;
}

/** 是否拥某些权限 */
- (BOOL)siAllow:(OptionPermission)permission {
return (self.flag & permission) == permission;
}

/** 是否禁用了某些权限 */
- (BOOL)isNotAllow:(OptionPermission)permission {
return (self.flag & permission) == 0;
}

/** 是否仅仅拥有某些权限 */
- (BOOL)isOnlyAllow:(OptionPermission)permission {
return self.flag == permission;
}

五、写在最后

  • 大家还可以自行搜索一下NS_OPTIONS与NS_ENUM的区别,他们都是用来定义枚举的,但其用法是有很大不同。
  • 博主我最近一直在考虑优化代码,正在开发的项目中就有很多权限判断的问题,我也在寻找各种各样更好的写法。
  • 也希望大家重视代码的表达,因此更加优化自己的代码。

iOS分层架构设计-实战篇

Posted on 2018-04-08 | Edited on 2018-06-04

一、前言

iOS的应用一般情况下,无非就是用户与界面间交互,形成操作指令,然后对数据进行增删改查。当业务过于复杂时,我们就可以采用分层架构设计。这样可以最大程度上解耦,这里,我们主要介绍最经典的三层架构设计模型。大体上,分别为:应用层、服务层、数据层。

Tips: 我们常用的MVC、MVP、MVVM等都是属于应用层内的架构,也就是说,它们主要是为应用层解耦等。而本文提到的分层架构是针对整个项目而言。

二、案例分析

本篇主要以代码实例为主,我们将通过一个简单案例来展示分层架构设计。

此例主要是模拟了打印机输入端的业务逻辑。

我们的主要业务逻辑为打印和复印,流程框图如下:👇👇👇

主要逻辑流程框图.png

基本步骤如下:

  • 用户选择服务类型:打印或者复印?

  • 如果用户选择打印,则让用户输入要打印的内容,输入完成后点击提交。

  • 如果用户选择复印,则让用户提供原件,操作完成后点击提交。

  • 提交后,系统生成打印输出预览,用户确认无误后点击立刻打印。

  • 提交数据给打印机,开始打印。

三、代码实现

首先我们看一下目录结构👇👇👇

目录结构.png

  • 应用层 - ApplicationLayer - MVC

数据模型 - Model
PrintModel.h

1
@property (nonatomic, copy) NSString *content;

视图模型 - View
PrintView
CopyView

控制器 - Controller
MainController
PrintController
PreviewController

这一层就不多解释了,采用里MVC架构,主要用于与用户间的交互,接收操作指令等。

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
PrintController

- (void)configClient {

if (self.type == OptionPrint) {
[self configPrintClient];
} else if (self.type == OptionCopy) {
[self configCopyClient];
}
}

- (void)configPrintClient {

NSString *userInput = [self.printView getContent];

PrintModel *model = [[ServiceFactory sharedInstance].printService buildPrintModelWithContent:userInput];

NSLog(@"我要打印了 %@", model);
}

- (void)configCopyClient {

PrintModel *model1 = [[ServiceFactory sharedInstance].copyService getCopyPrintModel1];
PrintModel *model2 = [[ServiceFactory sharedInstance].copyService getCopyPrintModel2];
PrintModel *model3 = [[ServiceFactory sharedInstance].copyService getCopyPrintModel3];

NSLog(@"我要复印了 %@ \n %@ \n %@", model1, model2, model3);
}
  • 服务层 - ServiceLayer

此层的所有服务由ServiceFactory来同一调配。ServiceFactory是一个单例,持有服务层的所有服务。由服务层来接触数据层。应用层则接触服务层。

此处包含了两个服务:打印服务(PrintService)和复印服务(CopyService)
注:如果后续有新的服务,则只需要在ServiceFactory中注册一下即可。

ServiceFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>
#import "PrintService.h"
#import "CopyService.h"

@interface ServiceFactory : NSObject

+ (instancetype)sharedInstance;

@property (nonatomic, strong) PrintService *printService;

@property (nonatomic, strong) CopyService *copyService;

@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
#import "ServiceFactory.h"

@implementation ServiceFactory

+ (instancetype)sharedInstance {

static ServiceFactory *instance = nil;

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [ServiceFactory new];
});
return instance;
}

- (instancetype)init {
self = [super init];
if (self) {
[self configFoundation];
}
return self;
}

- (void)configFoundation {
self.printService = [PrintService new];
self.copyService = [CopyService new];
}

PrintService

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>

@class PrintModel;

@interface PrintService : NSObject

- (PrintModel *)buildPrintModelWithContent:(NSString *)content;

@end

1
2
3
4
5
6
7
8
9
10
11
#import "PrintService.h"
#import "DataAccessFactory.h"

@implementation PrintService

- (PrintModel *)buildPrintModelWithContent:(NSString *)content {
PrintModel *model = [[DataAccessFactory sharedInstance].printDao buildPrintModelWithContent:content];
return model;
}

@end

CopyService

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>

@class PrintModel;

@interface CopyService : NSObject

- (PrintModel *)getCopyPrintModel1;

- (PrintModel *)getCopyPrintModel2;

- (PrintModel *)getCopyPrintModel3;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import "CopyService.h"
#import "DataAccessFactory.h"

@implementation CopyService

- (PrintModel *)getCopyPrintModel1 {
PrintModel *model = [[DataAccessFactory sharedInstance].copyDao getCopyPrintModel1];
return model;
}

- (PrintModel *)getCopyPrintModel2 {
PrintModel *model = [[DataAccessFactory sharedInstance].copyDao getCopyPrintModel2];
return model;
}

- (PrintModel *)getCopyPrintModel3 {
PrintModel *model = [[DataAccessFactory sharedInstance].copyDao getCopyPrintModel3];
return model;
}

@end
  • 数据层 - DataAccessLayer

此层由DataAccessFactory来同一调配。DataAccessFactory是一个单例,持有数据层的所有资源。由数据层来直接接触数据。

此处包含了两个数据资源:打印(PrintDAO)和复印(CopyDAO)
注:如果后续有新的数据层,则只需要在DataAccessFactory中注册一下即可。

DataAccessFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>
#import "PrintDAO.h"
#import "CopyDAO.h"

@interface DataAccessFactory : NSObject

+ (instancetype)sharedInstance;

@property (nonatomic, strong) PrintDAO *printDao;

@property (nonatomic, strong) CopyDAO *copyDao;

@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
#import "DataAccessFactory.h"

@implementation DataAccessFactory

+ (instancetype)sharedInstance {

static DataAccessFactory *instance = nil;

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DataAccessFactory new];
});
return instance;
}

- (instancetype)init {
self = [super init];
if (self) {
[self configFoundation];
}
return self;
}

- (void)configFoundation {

self.printDao = [PrintDAO new];

self.copyDao = [CopyDAO new];
}

@end

PrintDAO

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>

@class PrintModel;

@interface PrintDAO : NSObject

- (PrintModel *)buildPrintModelWithContent:(NSString *)content;

@end

1
2
3
4
5
6
7
8
9
10
11
12
#import "PrintDAO.h"
#import "PrintModel.h"

@implementation PrintDAO

- (PrintModel *)buildPrintModelWithContent:(NSString *)content {
PrintModel *model = [[PrintModel alloc] init];
model.content = content;
return model;
}

@end

CopyDAO

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>

@class PrintModel;

@interface CopyDAO : NSObject

- (PrintModel *)getCopyPrintModel1;

- (PrintModel *)getCopyPrintModel2;

- (PrintModel *)getCopyPrintModel3;

@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
#import "CopyDAO.h"
#import "PrintModel.h"

@implementation CopyDAO

- (PrintModel *)getCopyPrintModel1 {
PrintModel *model = [[PrintModel alloc] init];
model.content = @"Hello world";
return model;
}

- (PrintModel *)getCopyPrintModel2 {
PrintModel *model = [[PrintModel alloc] init];
model.content = @"Where there is a will, there is a way";
return model;
}

- (PrintModel *)getCopyPrintModel3 {
PrintModel *model = [[PrintModel alloc] init];
model.content = @"We will, we will rock you";
return model;
}

@end

四、结论分析

  • 分层架构设计最明显的特点就是,结构分层,且不可跨层访问,比如,应用层不能直接访问数据,仅有数据层能直接操作数据,也就是说,应用层必须先经由服务层访问数据层,再由数据层操作数据。这样有人就要问了,为什么要这么复杂?在应用层导入PrintModel.h,直接操作不就行了?

  • 当然是行的,一样可以达到效果,但是,如果业务逻辑发生变动或新增逻辑,我们就必须修改或重写应用层,这是我们最不愿看到的,再或者,另一个模块也要用到类似的业务逻辑,比如有了一台高级打印机,在打印和复印功能上是一样的,那我们是不是要再复制一份普通打印机应用层的逻辑呢?这些都是很严重的问题。

  • 从这里,我们不难看出,首先我们是将可见与不可见分层,再将公用的形成模块沉底,使每层间能相互独立,有清晰的依赖关系,这就是分层架构的目的。

五、写在最后

这样一个简单的案例肯定不能完成的覆盖分层架构,但已经明确的体现了分层架构的核心,在实际开发中,我们要根据实际的业务逻辑来定义服务层和数据层,应用层基本通用。看似简单,但需要大量的实战经验才能完美吸收,也希望读者能多动手。

  • 有需要源码的可以联系我~

iOS分层架构设计-介绍篇

Posted on 2018-03-04 | Edited on 2018-06-04

一、前言

iOS的应用也需要架构设计吗?答案是肯定的,但是并不一定采用分层架构设计。一般情况下,有关信息处理的应用采用分层架构设计,而游戏等应用不会采用分层架构设计。

Tips:游戏开发一般都会采用某个引擎,游戏引擎事实上包含里架构设计解决方案,游戏引擎的架构一般不是分层的而是树形结构的。

  • 表示层:它由UIKit Framework构成,包括视图、控制器、控件和事件处理等。
  • 业务逻辑层:框架的选取是根据业务而定,但一般是具有一定业务处理功能的编程语言封装的类或函数。
  • 数据持有层:提供本地和网络数据访问,它可能是访问SQLite数据的API函数,也可能是Core Data技术,或是访问文件的NSFileManager,或是网络通信技术。
  • 信息系统层:它的信息来源分为本地和网络。本地数据可以放入文件中,也可以放入数据库中,目前iOS本地数据库采用SQLite3。网络可以是某个云服务,也可以是一般的Web服务。

图1:分层架构设计图

二、工程与工作空间

Xcode不仅可以创建工程(Project),还可以创建工作空间(Workspace)。出于方便管理等目的,我们可以将多个相互管理的工程放到一个工作空间中,工作空间是多个工程的集合。工程文件名的后缀是.xcodeproj,工作空间文件名的后缀是.xcworkspace。

三、静态链接库

有时候,我们需要将某一层复用给其他团队、公司或个人,但由于某些原因,我们不能提供源代码,此时就可以将业务逻辑层和数据持有层编写成静态链接库(static library 或 static-linked library)或框架(Framework)。
库是一些没有main函数的程序代码的集合。除了静态链接库,还有动态链接库。它们的区别是:静态链接库可以编译到你的执行代码中,应用程序可以在没有静态链接库的环境下运行;动态链接库不能编译到你的执行代码中,应用程序必须在有链接库文件的环境下运行。
在Xcode中可以创建静态链接库工程,具体过程如下:
File -> New -> Project - > …
Framework & Library -> Cocoa Touch Static Library工程模板。如下图:

图2:创建静态链接库工程

Tips:静态链接库中不能有Swift代码模块,只能是Objective-C代码模块。

四、框架

由于静态链接库比较麻烦,需要给开发者提供.a和.h文件,使用的时候还要配置很多的环境变量。事实上,苹果提供的API(如UIKit、QuartzCoreFoundation)都是框架。为了方便使用,框架会将.a或.h等文件打包在一起。
在Xcode中可以创建框架,具体过程如下:
File -> New -> Project - > …
Framework & Library -> Cocoa Touch Framework

Tips:自定义框架没有静态链接库的限制,Swift代码可以在框架工程中使用。

五、10种分层模式

由于iOS 8 之后可以使用Swift和Objective-C两种语言,开发人员都有四种方式来选择开发语言。

  • 采用纯Swift的改革派方式
  • 采用纯Objective-C的保守派方式
  • 采用Swift调用Objective-C的左倾改良派
  • 采用Objective-C调用Swift的右倾改良派

从技术上讲,无论是否采用分层架构设计,都可以用上述4种方式来选择语言。也就是说,可以在同一层中采用单一语言和搭配混合,也可以在不同层之间采用单一语言和搭配混合。基于图1进行分层,如果只考虑业务逻辑层和数据持有层采用相同语言的情况下,那么可以混合搭配出4种模式,具体如下图:

图3:4种混合搭配

另外,如果考虑到代码的组织形式,可以分为如下3种:

  • 同一工程的分层,用P(Project)表示
  • 基于静态链接库实现的同一个工作空间不同工程的分层,用WL(Workspace Library)表示
  • 基于自定义框架实现的同一个工作空间不同工程的分层,用WF(Workspace Framework)表示

组合后得出如下图10种分层模式

图4:10种分层模式

由于框架实现要先进于静态链接库实现,所以WFOOO、WFSSS、WFSOO和WFOSS是我们优先考虑的,如果从技术和设计上无法实现,可以考虑WLOOO和WLSOO,最后是POOO、PSSS、PSOO和POSS实现。

六、写在最后

本期主要以介绍为主,下一期将更新实战篇,会以具体的项目为主体,详细解析如何使用分层架构。

iOS开发-关于Associated Objects

Posted on 2018-02-04 | Edited on 2018-06-04

一、前言

Associated Objects(关联对象)是什么?什么时候用?为什么要用?怎么用?
最开始用到关联对象是源于一个需求(废话,肯定是源于需求)。
大家都知道,Button的点击事件,一定是将本身传入参数:

1
2
3
4
5
6
7
8
9
10
- (void)setupFoundationUI {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.frame = (CGRect){0, 0, 30, 30};
[self.view addSubview:btn];
[btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)btnDidClick:(UIButton *)sender {
NSLog(@"Btn did click ...");
}

如果想要传入一个特定的参数呢?

  • 当时我想传的参数是整型,于是我想到了tag(那时的我还不知道关联对象)
  • tag其实是用来标记不同的Button对象,但此时,我也很无奈。。。就先借用一下吧,哈哈
    也就是这样👇👇👇
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - (void)setupFoundationUI {
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    btn.frame = (CGRect){0, 0, 30, 30};
    btn.tag = 110;
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
    }

    - (void)btnDidClick:(UIButton *)sender {
    NSLog(@"Btn did click ...tag = %zd", sender.tag);
    }

后来又有需求,要传的参数是字符串,甚至是对象。。。
就在此时我注意到了关联对象(Associated Objects)

二、关联对象的介绍

1.关联对象解决的问题

我们知道,在 Objective-C 中可以通过 Category (类别、分类,反正你们懂得)给一个现有的类添加属性,但是却不能添加实例变量,这似乎成为了 Objective-C 的一个明显短板,关联对象就可以解决这个问题。

2.如何用关联对象

  • 首先要引入 runtime

    1
    #import <objc/runtime.h>
  • API主要就是(来自系统文件runtime.h的介绍)👇👇👇

    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
    /** 
    * Sets an associated value for a given object using a given key and association policy.
    *
    * @param object The source object for the association.
    * @param key The key for the association.
    * @param value The value to associate with the key key for object. Pass nil to clear an existing association.
    * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
    *
    * @see objc_setAssociatedObject
    * @see objc_removeAssociatedObjects
    */
    OBJC_EXPORT void
    objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
    id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

    /**
    * Returns the value associated with a given object for a given key.
    *
    * @param object The source object for the association.
    * @param key The key for the association.
    *
    * @return The value associated with the key \e key for \e object.
    *
    * @see objc_setAssociatedObject
    */
    OBJC_EXPORT id _Nullable
    objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

    /**
    * Removes all associations for a given object.
    *
    * @param object An object that maintains associated objects.
    *
    * @note The main purpose of this function is to make it easy to return an object
    * to a "pristine state”. You should not use this function for general removal of
    * associations from objects, since it also removes associations that other clients
    * may have added to the object. Typically you should use \c objc_setAssociatedObject
    * with a nil value to clear an association.
    *
    * @see objc_setAssociatedObject
    * @see objc_getAssociatedObject
    */
    OBJC_EXPORT void
    objc_removeAssociatedObjects(id _Nonnull object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

官方的解释已经很清晰了,就不过多解读了(绑定、获取、移除),值得注意的一点是:objc_removeAssociatedObjects是移除一个对象的所有关联对象,将该对象恢复成“原始”状态,这样的操作风险太大,所以一般的做法是通过给 objc_setAssociatedObject 函数传入 nil 来移除某个已有的关联对象。如下这样👇👇👇

1
objc_setAssociatedObject(self, &key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

  • 关于key的问题

    • 声明 static char kAssociatedObjectKey; 使用 &kAssociatedObjectKey 作为 key 值;
    • 声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; 使用 kAssociatedObjectKey 作为 key 值;
    • 用 selector ,使用 getter 方法的名称作为 key 值。
  • 关于policy(关联策略)的问题

OBJC_ASSOCIATION_ASSIGN
等价属性@property (assign) or @property (unsafe_unretained)
弱引用关联对象

OBJC_ASSOCIATION_RETAIN_NONATOMIC
等价属性@property (strong, nonatomic)
强引用关联对象,且为非原子操作

OBJC_ASSOCIATION_COPY_NONATOMIC
等价属性@property (copy, nonatomic)
复制关联对象,且为非原子操作

OBJC_ASSOCIATION_RETAIN
等价属性@property (strong, atomic)
强引用关联对象,且为原子操作

OBJC_ASSOCIATION_COPY
等价属性@property (copy, atomic)
复制关联对象,且为原子操作

具体内容可以参考官方文档,这里就不copy了

三、用关联对象解决上述问题

  • 传整型数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    NSString *const kButtonKey = @"kButtonKey";

    - (void)setupFoundationUI {
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    objc_setAssociatedObject(btn, &kButtonKey, @110, OBJC_ASSOCIATION_ASSIGN);
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
    }

    - (void)btnDidClick:(UIButton *)sender {
    NSInteger value = [objc_getAssociatedObject(sender, &kButtonKey) integerValue];
    NSLog(@"btn did click ...value = %zd", value);
    }
  • 传对象数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    NSString *const kButtonKey = @"kButtonKey";

    - (void)setupFoundationUI {
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    btn.frame = (CGRect){0, 0, 30, 30};
    Person *person = [[Person alloc] init];
    person.name = @"LiMing";
    objc_setAssociatedObject(btn, &kButtonKey, person, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
    }

    - (void)btnDidClick:(UIButton *)sender {
    Person *person = objc_getAssociatedObject(sender, &kButtonKey);
    NSLog(@"person`s name = %@", person.name);
    }

四、关联对象用于Category

以实现UIBarButtonItem的扩展为例子,为其增加红点的功能,其中大量的使用了关联对象
需求:

1.显示小红点
2.显示数字红点
3.即有小红点又有数字红点时,优先显示数字红点
4.可自定义红点颜色(默认是红色[UIColor redColor])
5.数字红点数目大于99时,显示99+

具体代码如下:👇👇👇

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>

@interface UIBarButtonItem (Badge)

@property (assign, nonatomic) UIColor *badgeColor;

- (void)configBadgeWithBigNum:(NSInteger)bigNum small:(BOOL)isOn;

@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
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#import "UIBarButtonItem+Badge.h"
#import <objc/runtime.h>

NSString *const ZYBarButtonItem_hasBadgeKey = @"ZYBarButtonItem_hasBadgeKey";
NSString *const ZYBarButtonItem_badgeKey = @"ZYBarButtonItem_badgeKey";
NSString *const ZYBarButtonItem_badgeSizeKey = @"ZYBarButtonItem_badgeSizeKey";
NSString *const ZYBarButtonItem_badgeOriginXKey = @"ZYBarButtonItem_badgeOriginXKey";
NSString *const ZYBarButtonItem_badgeOriginYKey = @"ZYBarButtonItem_badgeOriginYKey";
NSString *const ZYBarButtonItem_badgeColorKey = @"ZYBarButtonItem_badgeColorKey";
NSString *const ZYBarButtonItem_badgeSizeWKey = @"ZYBarButtonItem_badgeSizeWKey";

@interface UIBarButtonItem ()

@property (nonatomic, assign) CGFloat badgeSizeW;
@property (strong, nonatomic) UILabel *badge;
@property (assign, nonatomic) CGFloat badgeOriginX;
@property (assign, nonatomic) CGFloat badgeOriginY;
@property (assign, nonatomic) CGFloat badgeSize;
@property BOOL hasBadge;

@end

@implementation UIBarButtonItem (Badge)


- (void)initBadge {
UIView *superview = nil;

if (self.customView) {
superview = self.customView;
superview.clipsToBounds = NO;
} else if ([self respondsToSelector:@selector(view)] && [(id)self view]) {
superview = [(id)self view];
}
[superview addSubview:self.badge];

// 默认设置 default configure
self.badgeColor = [UIColor redColor];
self.badgeSize = 10;
self.badgeSizeW = 10;
self.badgeOriginX = 28;
self.badgeOriginY = 8;
self.badge.hidden = YES;
self.badge.layer.masksToBounds = YES;
self.badge.font = [UIFont boldSystemFontOfSize:12];
self.badge.textAlignment = NSTextAlignmentCenter;
self.badge.textColor = [UIColor whiteColor];
}

- (void)showBadge {
self.badge.hidden = NO;
}

- (void)hideBadge {
self.badge.hidden = YES;
}

- (void)refreshBadge {
self.badge.frame = (CGRect){self.badgeOriginX,self.badgeOriginY,self.badgeSizeW,self.badgeSize};
self.badge.backgroundColor = self.badgeColor;
self.badge.layer.cornerRadius = self.badgeSize/2;
}


#pragma mark ---------- badge getter & setter function -----------

- (UILabel *)badge {
UILabel *badge = (UILabel *)objc_getAssociatedObject(self, &ZYBarButtonItem_badgeKey);
if (!badge) {
badge = [[UILabel alloc] init];
[self setBadge:badge];
[self initBadge];
}
return badge;
}

- (void)setBadge:(UILabel *)badge {
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeKey, badge, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIColor *)badgeColor {
return objc_getAssociatedObject(self, &ZYBarButtonItem_badgeColorKey);
}

- (void)setBadgeColor:(UIColor *)badgeColor {
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeColorKey, badgeColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}

-(CGFloat)badgeSize {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_badgeSizeKey);
return number.floatValue;
}

-(void)setBadgeSize:(CGFloat)badgeSize {
NSNumber *number = [NSNumber numberWithDouble:badgeSize];
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeSizeKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}

- (CGFloat)badgeSizeW {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_badgeSizeWKey);
return number.floatValue;
}

- (void)setBadgeSizeW:(CGFloat)badgeSizeW {
NSNumber *number = [NSNumber numberWithDouble:badgeSizeW];
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeSizeWKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}

-(CGFloat)badgeOriginX {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_badgeOriginXKey);
return number.floatValue;
}

-(void)setBadgeOriginX:(CGFloat)badgeOriginX {
NSNumber *number = [NSNumber numberWithDouble:badgeOriginX];
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeOriginXKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}

-(CGFloat)badgeOriginY {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_badgeOriginYKey);
return number.floatValue;
}

-(void)setBadgeOriginY:(CGFloat)badgeOriginY {
NSNumber *number = [NSNumber numberWithDouble:badgeOriginY];
objc_setAssociatedObject(self, &ZYBarButtonItem_badgeOriginYKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (self.badge) {
[self refreshBadge];
}
}

- (void)setHasBadge:(BOOL)hasBadge {
if (hasBadge) {
[self showBadge];
}else{
[self hideBadge];
}

NSNumber *number = [NSNumber numberWithBool:hasBadge];
objc_setAssociatedObject(self, &ZYBarButtonItem_hasBadgeKey, number, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)hasBadge {
NSNumber *number = objc_getAssociatedObject(self, &ZYBarButtonItem_hasBadgeKey);
return number.boolValue;
}

#pragma mark - Public

- (void)configBadgeWithBigNum:(NSInteger)bigNum small:(BOOL)isOn {

if (bigNum > 0) {
self.hasBadge = YES;
self.badgeSize = 18;
self.badgeOriginY = 6;
NSString *numStr = [NSString stringWithFormat:@"%zd", bigNum];
if (bigNum < 10) {
self.badgeSizeW = 18;
} else if (bigNum < 100) {
self.badgeSizeW = 25;
} else {
self.badgeSizeW = 30;
numStr = @"99+";
}
self.badge.text = numStr;
} else if (isOn) {
self.hasBadge = YES;
self.badgeSizeW = 10;
self.badgeSize = 10;
self.badgeOriginY = 8;
self.badge.text = nil;
} else {
self.hasBadge = NO;
}
}
@end

五、写在最后

  • 关联对象与被关联对象本身的存储并没有直接的关系,它是存储在单独的哈希表中的;
  • 关联对象的五种关联策略与属性的限定符非常类似,在绝大多数情况下,我们都会使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的关联策略,这可以保证我们持有关联对象;
  • 关联对象的释放时机与移除时机并不总是一致,比如用关联策略 OBJC_ASSOCIATION_ASSIGN 进行关联的对象,很早就已经被释放了,但是并没有被移除,而再使用这个关联对象时就会造成 Crash 。
    Associated.jpg

iOS-关于Block,你不得不知

Posted on 2018-01-01 | Edited on 2018-06-04

一、前言

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

二、Block常规使用

1.Block属性

1
2
3
4
5
// 格式
@property (nonatomic, copy) return_type (^blockName) (var_type);

// 例如 用于发送本类中的异常时
@property (nonatomic, copy) void(^postErrorMessageHandler)(NSString *errorMsg, CGFloat offsetY);
1
2
3
4
5
6
// 实现如下
- (void)postErrorMsgWith:(NSString *)errorMsg offsetY:(CGFloat)offSetY {
if (self.postErrorMessageHandler) {
self.postErrorMessageHandler(errorMsg, offSetY);
}
}
1
2
3
4
5
6
// 调用
__weak typeof(self)weakSelf = self;
obj.postErrorMessageHandler = ^(NSString *errorMsg, CGFloat offsetY) {
__strong typeof(weakSelf)strongSelf = weakSelf;
[strongSelf showToastWith:errorMsg offsetY:offsetY];
};

2.Block作为形参

1
2
3
4
5
// 格式
- (void)yourMethod:(return_type (^)(var_type))blockName;

// 例如 用于获取用户信息时
- (void)fetchUserInfoWithCompletion:(void (^)(User *userInfo, GetInfoError *error))completion;
1
2
3
4
5
6
7
8
9
// 实现如下
- (void)fetchUserInfoWithCompletion:(void (^)(User *userInfo, GetInfoError *error))completion {

if (self.isHaveCompletionInfo == NO) {
completion(nil, [GetInfoError errorWithMessage:@"请完善用户信息" success:NO]);
} else {
completion(self.userInfo, nil);
}
}
1
2
3
4
5
6
7
8
9
10
// 调用
__weak typeof(self)weakSelf = self;
[obj fetchUserInfoWithCompletion:^(User *userInfo, GetInfoError *error) {
__strong typeof(weakSelf)strongSelf = weakSelf;
if (error) {
[strongSelf postErrorMsgWith:error.message];
} else {
SaveInfo(userInfo);
}
}];

3.使用typedef定义Block类型

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

1
2
// 格式
typedef return_type(^BlockName)(var_type);
1
2
3
4
// 例如(由上例变形)
typedef void(^MessageHandler)(NSString *errorMsg, CGFloat offsetY);

@property (nonatomic, copy) MessageHandler postErrorMessageHandler;
1
2
3
4
// 再例如(由上例变形)
typedef void(^Completion)(User *userInfo, GetInfoError *error);

- (void)fetchUserInfoWithCompletion:(Completion)completion;

三、在Block内部访问局部变量

1.在Block中可以访问局部变量

1
2
3
4
5
6
7
8
9
// 声明局部变量age
int age = 18;

void(^logAgeBlock)() = ^{
NSLog(@"current age = %d", age);
};

// 调用后控制台输出"current age = 18"
logAgeBlock();

2.Block内局部变量的值在声明Block确定,不会随后面修改而改变,在调用Block时局部变量值是声明Block时的旧值

1
2
3
4
5
6
7
8
9
10
int age = 18;

void(^logAgeBlock)() = ^{
NSLog(@"current age = %d", age);
};

age = 24;

// 调用后控制台输出"current age = 18"
logAgeBlock();

3.在Block中不可以直接修改局部变量

1
2
3
4
5
6
int age = 18;

void(^logAgeBlock)() = ^{
age ++; //这句报错: Variable is not assignable(missing __block type specifier)
NSLog(@"current age = %d", age);
};

4.该如何修改局部变量?

1
2
3
4
5
6
7
8
9
// 声明局部变量global
__block int age = 18;

void(^logAgeBlock)() = ^{
NSLog(@"current age = %d", age);
};
age = 24;
// 调用后控制台输出"current age = 24"
logAgeBlock();

或者

1
2
3
4
5
6
7
8
__block int age = 18;

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

四、Block引发的内存泄露

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

1
2
3
4
5
6
7
8
User *user = [[User alloc] init];

void(^invokingBlock)() = ^{
NSLog(@"log user : %@", user);
};
invokingBlock();

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface User : NSObject

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

@end

@implementation User

- (void)dealloc {
NSLog(@"%s dealloc", __func__);
}

@end

User *user = [[User alloc] init];

user.invokingBlock = ^{
NSLog(@"log user : %@", user);
};
user.invokingBlock();

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

另一种情况👇👇👇

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
@interface User : NSObject

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

- (void)resetBlock;

@end

@implementation User

- (void)resetBlock {
self.invokingBlock = ^{
NSLog(@"log user : %@", self);
};
}

- (void)dealloc {
NSLog(@"%s dealloc", __func__);
}

@end

User *user = [[User alloc] init];
[user resetBlock];

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface User : NSObject

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

@end


@implementation User

- (void)dealloc {
NSLog(@"%s dealloc", __func__);
}

@end

User *user = [[User alloc] init];
__weak typeof(user) weakUser = user;

user.invokingBlock = ^{
NSLog(@"log user : %@", weakUser);
};
user.invokingBlock();

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

另一种情况👇👇👇

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
@interface User : NSObject

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

- (void)resetBlock;

@end


@implementation User

- (void)resetBlock {
// 这里为了通用一点,可以使用__weak typeof(self) weakUser = self;
__weak User *weakUser = self;
self.invokingBlock = ^{
NSLog(@"log user : %@", weakUser);
};
}

- (void)dealloc {
NSLog(@"%s dealloc", __func__);
}

@end


User *user = [[User alloc] init];
[user resetBlock];

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

五、写在最后

  • 本文旨在浅析Block的常规使用及注意事项(主要是循环引用导致的内存泄露),内容不深,还是以举例用法为主;
  • 本人经历了 ->不太会用 -> 经常会有 -> 内存泄露 -> 正确使用,由衷的喜欢Block;
  • Block是OC里很优秀的东西,捕获变量、代码传递、代码内联等特性赋予了它多于代理机制的功能和灵活性;
  • 希望读者有所收获!
    磁湖.png

iOS 地图开发-那些年我们遇到的坑~

Posted on 2017-12-04 | Edited on 2018-06-04

一、前言

地图开发的重要性就不讲了,大家看一下自己手机里的App就知道了,10个App有9个包含了位置服务功能,而且发展到今天,精准度已经非常高了。balabala……

我是地图.png

二、关于地图的Info.plist文件配置

iOS 10 和iOS 11 的设置分别是这样配置的(如下图)
注:每次Apple有大版本更新,都得检查一下plist文件的权限配置

iOS 10 位置服务权限配置.png

iOS 11 位置服务权限配置.png

如果配置不完整,在部分系统上是无法加载地图的(显示为一片蓝色),且在相应App的设置里都找不到『位置』这一项。反之,如果你发现设置里没有『位置』,大概就是权限配置有问题了。

『位置』.png

三、我们用那种地图更好?

常用的地图:高德地图、百度地图、腾讯地图、Google地图、Apple地图

1.基于功能的强大,个人首推高德地图(如果不需要国际化)

我认为高德地图是我大天朝最好的地图供应商(仅限于国内),主要原因当然是里面的代理方法相当的丰富,文档也很清晰。

2.如果需要国际化,就只能考虑Apple地图和Google地图了

Google地图当然很好(可能是地球上最好的地图服务商),但免费版只提供了基础的定位服务,兴趣点等稍高级的服务都是收费的,所以,基于『勤俭节约』,就放弃吧!

3.重点来了,Apple地图

其实,Apple地图的数据并不是Apple公司提供的。大家打开自带的地图App,看看左下角,有一个『高德地图』的标志,那地处歪果的iPhone里地图App是什么呢?答案是『TomTom』,大家番羽墙出去,再打开看看就知道了。也就是说,Apple只拿着别人家的数据,优化了一下UI,就成了Apple地图了,它自己是没有地图数据的,就更不能提供地图服务了。Apple地图是根据当前网络所处的国家,自动请求不同服务商的数据(国内是高德,国外则是Tom Tom)。Apple地图的API完全没有高德地图的API丰富。

注:其实如果业务逻辑不那么复杂,直接用Apple地图就行了。毕竟原生,简单、好用。(文档中都有)

四、原生地图的坑

『附近的兴趣点』!!!(兴趣点 学名:POI - Point of Interest)

『关键字检索POI』就大家都有,就不说了。这里主要说一下『检索周边POI』。

1.什么叫『附近的兴趣点』?

在地图表达中,一个POI可代表一栋大厦、一家商铺、一处景点等等。通过POI搜索,完成找餐馆、找景点、找厕所等等的功能。
比如:附近的停车场、附近的建筑、附近的公司、附近的电影院。

2.高德地图如何实现?

高德地图的POI类别共20个大类,分别为:(InterestKey)

  • 汽车服务、汽车销售、汽车维修、摩托车服务、餐饮服务、
  • 购物服务、生活服务、体育休闲服务、医疗保健服务、住宿服务、
  • 风景名胜、商务住宅、政府机构及社会团体、科教文化服务、交通设施服务、
  • 金融保险服务、公司企业、道路附属设施、地名地址信息、公共设施

注:不设置POI的类别,默认返回“餐饮服务”、“商务住宅”、“生活服务”这三种类别的POI

  • 高德地图的附近POI检索(AMapPOIAroundSearchRequest)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)loadAroundPOIsInAMapWithCoordinate:(CLLocationCoordinate2D)coordinate {

AMapPOIAroundSearchRequest *request = [[AMapPOIAroundSearchRequest alloc]init];
request.location = [AMapGeoPoint locationWithLatitude:coordinate.latitude longitude:coordinate.longitude];
// exp : @"公司企业|政府机构及社会团体|道路附属设施|地名地址信息"
request.types = InterestKey;
request.sortrule = 0;
request.requireExtension = YES;

[self.search AMapPOIAroundSearch:request];
}

- (void)onPOISearchDone:(AMapPOISearchBaseRequest *)request response:(AMapPOISearchResponse *)response {

for (AMapPOI *poi in response.pois) {
// 检索的结果
}
}
  • 高德地图的关键字检索是另一套方法(AMapInputTipsSearchRequest)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    - (void)searchInAMapWith:(NSString *)searchKey {

    AMapInputTipsSearchRequest *request = [[AMapInputTipsSearchRequest alloc]init];
    request.keywords = searchKey;
    request.types = @"公司企业|政府机构及社会团体";
    [self.search AMapInputTipsSearch:request];
    }

    - (void)onInputTipsSearchDone:(AMapInputTipsSearchRequest *)request response:(AMapInputTipsSearchResponse *)response {

    NSMutableArray *resultArrM = [NSMutableArray array];
    for (AMapTip *tip in response.tips) {
    // 检索的结果
    }
    }

3.Apple地图如何实现?

『臣妾』做不到!!!我也很无奈!!!

  • 好吧,其实也有一个比较凑合的方式(非常凑合):

那就是有关键字检索的方式,固定检索事先设置好的关键字,比如:
Company|Community|cafe|supermarket|village|
Shop|Restaurant|School|hospital|Street|
Convenience store|Shopping Centre|Place names|Hotel|Grocery store
经过实测,有一定的效果(毕竟,有总比没有好~)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)loadAroundPOIsInAppleMapWithCoordinate:(CLLocationCoordinate2D)coordinate {

MKCoordinateSpan span = {0.005, 0.005};
MKCoordinateRegion region = {coordinate, span}; //MKCoordinateRegionMakeWithDistance(coordinate,1000, 1000);
MKLocalSearchRequest *req = [[MKLocalSearchRequest alloc] init];
req.region = region;
req.naturalLanguageQuery = @"Company";
MKLocalSearch * ser = [[MKLocalSearch alloc] initWithRequest:req];
//开始检索,结果返回在block中
[ser startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
if (error) {
return ;
}
//兴趣点节点数组
for (MKMapItem *item in response.mapItems) {

}
}];
}

  • 如果是真正的关键字检索呢?大家自行对比一下(其实是一模一样,唯一不同就是naturalLanguageQuery的不同使用,注:非官方做法,慎重!)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    - (void)searchInAppleMapWithNaturaLanguage:(NSString *)searchKey {

    MKCoordinateRegion region = self.mkMapView.region;
    MKLocalSearchRequest *req = [[MKLocalSearchRequest alloc] init];
    req.region = region;
    req.naturalLanguageQuery = searchKey;
    MKLocalSearch * ser = [[MKLocalSearch alloc] initWithRequest:req];
    //开始检索,结果返回在block中
    [ser startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
    if (error) {
    return ;
    }
    //兴趣点节点数组
    for (MKMapItem *item in response.mapItems) {

    }
    }];
    }

五、关于地图坐标系的问题

  • 地球坐标(WGS84) - 是国际标准,GPS坐标(Google Earth使用、或者GPS模块)- 苹果的CLLocationManager
  • 火星坐标(GCJ-02) - 中国坐标偏移标准,Google地图、高德、腾讯使用
  • 百度坐标(BD-09) - 百度坐标偏移标准,Baidu地图使用

我们经常在常用的这几种地图中进行坐标转换,或用于第三方地图的导航,或用于后台下发地址的打点等等场景。虽然百度和高德都提供相应的API,但是他们都只提供向自家坐标系转化的API,需要连网请求才能得到转化后的结果。

提供一下坐标系的转换代码 👇👇👇

CLLocation+Sino.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <CoreLocation/CoreLocation.h>

@interface CLLocation (Sino)

/**地球坐标转火星坐标*/

- (CLLocation*)locationMarsFromEarth;

/**火星坐标转百度坐标*/

- (CLLocation*)locationBearPawFromMars;

/**百度坐标转火星坐标*/

- (CLLocation*)locationMarsFromBearPaw;

@end

CLLocation+Sino.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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#import "CLLocation+Sino.h"

void transform_earth_2_mars(double lat, double lng, double* tarLat, double* tarLng);

void transform_mars_2_bear_paw(double lat, double lng, double* tarLat, double* tarLng);

void transform_bear_paw_2_mars(double lat, double lng, double* tarLat, double* tarLng);

@implementation CLLocation (Sino)

- (CLLocation*) locationMarsFromEarth {

double lat = 0.0;

double lng = 0.0;

transform_earth_2_mars(self.coordinate.latitude, self.coordinate.longitude, &lat, &lng);

return [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(lat, lng)

altitude:self.altitude

horizontalAccuracy:self.horizontalAccuracy

verticalAccuracy:self.verticalAccuracy

course:self.course

speed:self.speed

timestamp:self.timestamp];

}

- (CLLocation*) locationBearPawFromMars {

double lat = 0.0;

double lng = 0.0;

transform_mars_2_bear_paw(self.coordinate.latitude, self.coordinate.longitude, &lat, &lng);

return [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(lat, lng)

altitude:self.altitude

horizontalAccuracy:self.horizontalAccuracy

verticalAccuracy:self.verticalAccuracy

course:self.course

speed:self.speed

timestamp:self.timestamp];

}

- (CLLocation*) locationMarsFromBearPaw {

double lat = 0.0;

double lng = 0.0;

transform_bear_paw_2_mars(self.coordinate.latitude, self.coordinate.longitude, &lat, &lng);

return [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(lat, lng)

altitude:self.altitude

horizontalAccuracy:self.horizontalAccuracy

verticalAccuracy:self.verticalAccuracy

course:self.course

speed:self.speed

timestamp:self.timestamp];

}

const double a = 6378245.0;

const double ee = 0.00669342162296594323;

bool transform_sino_out_china(double lat, double lon) {

if (lon < 72.004 || lon > 137.8347)

return true;

if (lat < 0.8293 || lat > 55.8271)

return true;

return false;

}

double transform_earth_2_mars_lat(double x, double y) {

double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(fabs(x));

ret += (20.0 * sin(6.0 * x * M_PI) + 20.0 * sin(2.0 * x * M_PI)) * 2.0 / 3.0;

ret += (20.0 * sin(y * M_PI) + 40.0 * sin(y / 3.0 * M_PI)) * 2.0 / 3.0;

ret += (160.0 * sin(y / 12.0 * M_PI) + 320 * sin(y * M_PI / 30.0)) * 2.0 / 3.0;

return ret;

}

double transform_earth_2_mars_lng(double x, double y) {

double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(fabs(x));

ret += (20.0 * sin(6.0 * x * M_PI) + 20.0 * sin(2.0 * x * M_PI)) * 2.0 / 3.0;

ret += (20.0 * sin(x * M_PI) + 40.0 * sin(x / 3.0 * M_PI)) * 2.0 / 3.0;

ret += (150.0 * sin(x / 12.0 * M_PI) + 300.0 * sin(x / 30.0 * M_PI)) * 2.0 / 3.0;

return ret;

}

void transform_earth_2_mars(double lat, double lng, double* tarLat, double* tarLng) {

if (transform_sino_out_china(lat, lng)) {

*tarLat = lat;

*tarLng = lng;

return;

}

double dLat = transform_earth_2_mars_lat(lng - 105.0, lat - 35.0);

double dLon = transform_earth_2_mars_lng(lng - 105.0, lat - 35.0);

double radLat = lat / 180.0 * M_PI;

double magic = sin(radLat);

magic = 1 - ee * magic * magic;

double sqrtMagic = sqrt(magic);

dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * M_PI);

dLon = (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * M_PI);

*tarLat = lat + dLat;

*tarLng = lng + dLon;

}

const double x_pi = M_PI * 3000.0 / 180.0;

void transform_mars_2_bear_paw(double gg_lat, double gg_lon, double *bd_lat, double *bd_lon) {

double x = gg_lon, y = gg_lat;

double z = sqrt(x * x + y * y) + 0.00002 * sin(y * x_pi);

double theta = atan2(y, x) + 0.000003 * cos(x * x_pi);

*bd_lon = z * cos(theta) + 0.0065;

*bd_lat = z * sin(theta) + 0.006;

}

void transform_bear_paw_2_mars(double bd_lat, double bd_lon, double *gg_lat, double *gg_lon) {

double x = bd_lon - 0.0065, y = bd_lat - 0.006;

double z = sqrt(x * x + y * y) - 0.00002 * sin(y * x_pi);

double theta = atan2(y, x) - 0.000003 * cos(x * x_pi);

*gg_lon = z * cos(theta);

*gg_lat = z * sin(theta);

}

@end

六、写在最后

本文没有涉及详细开发步骤,主要就是『点一下狼坑』而已,常规的开发网上有很多,而且还有各自的官方开发文档。

目的是介绍地图开发中一些可能出现的问题,也都是我在开发中的『心路历程』。有不对的地方,欢迎指正!

就是想配个图.png

监测那些常规方法捕捉不到的变化-观察者模式

Posted on 2017-10-31 | Edited on 2018-06-09

一、你可能遇到过的问题

Question 1.使用UITextField时;

当你想时时监测textField.text的变化,以获取最新的text,你是用这样的代理方法吗?

1
2
3
4
5
6
7
8
9
10
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {

if (textField == self.textField) {
NSString *futureStr = [NSString stringWithFormat:@"%@%@", textField.text, string];
if ([futureStr isEqualToString:@"000"]) {
NSLog(@"----------The text value is 000----------");
}
}
return YES;
}

Question 2.使用UIWebView或WKWebView时;

由于网页端用Ajax,导致我们并不能用常规的网页加载代理方法来检测webView的title的值~比如这样

1
2
3
4
5
6
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

if (webView.title.length > 0) {
self.title = webView.title;
}
}

二、这里我们就需要用上观察者模式了

KVO,即key-value-observing,利用一个key来找到某个属性并监听其值得改变。其实这也是一种典型的观察者模式。

  • 1,添加观察者
  • 2,在观察者中实现监听方法,observeValueForKeyPath: ofObject: change: context:(因为这个方法属于NSObject,所以,可以用于检测任何对象的任何属性的变化)
  • 3,移除观察者
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
/**
用万能的User类来煮个栗子~
*/
- (void)addObserverForUserName {

User *user = [[User alloc] init];

/* Whether the change dictionaries sent in notifications
should contain NSKeyValueChangeNewKey and NSKeyValueChangeOldKey entries, respectively.
-------------------------------------------------------------
NSKeyValueObservingOptionNew = 0x01,新值
NSKeyValueObservingOptionOld = 0x02,旧值
*/
[user addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}

/**
监测 user 的 name 属性 发生变化的情况

@param keyPath 被监听的属性
@param object 被监听的对象
@param change 新值和旧值
@param context 附加的额外数据
*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {

if ([keyPath isEqualToString:@"name"] && [object isEqual:user]) {
// Do Something
NSLog(@"----------user'name has changed----------");
}
}

- (void)dealloc {

[user removeObserver:self forKeyPath:@"name"];
}

三、解决上述及类似的问题

Answer 1

1
2
3
4
5
6
7
8
9
[self.textField addTarget:self action:@selector(textFieldDidChange:) 
forControlEvents:UIControlEventEditingChanged];

- (void)textFieldDidChange:(UITextField *)textField {
// TODO
if ([textField.text isEqualToString:@"000"]) {
NSLog(@"----------The text value is 000----------");
}
}

Answer 2

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
/**
为webView配置监听者
*/
- (void)configWebViewObserver {

[webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
[webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {

if ([keyPath isEqualToString:@"title"] && [object isEqual:webView]) {
self.title = self.webView.title;
} else if ([keyPath isEqualToString:@"estimatedProgress"] && [object isEqual:webView]) {
self.progressView.progress = self.webView.estimatedProgress;
}
}

- (void)dealloc {

[self.webView removeObserver:self forKeyPath:@"title"];
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
}

附:再来一发监听UITableView的contentOffset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)configObserverForTableView {

[tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {

if ([keyPath isEqualToString:@"contentOffset"] && object == tableView) {

CGFloat offsetY = self.contentOffset.y;
CGFloat offsetX = self.contentOffset.x;
NSLog(@"----------offsetX = %f, offsetY = %f----------", offsetX, offsetY);
}
}

- (void)dealloc {

[tableView removeObserver:self forKeyPath:@"contentOffset"];
}

看到这里大家一定就能明白观察者模式的基本套路了吧,心里突然响起了音乐:”其实很简单,其实很自然”~

四、写在最后

  • 此方法可用于监听一切对象的属性变化,包括系统的类(UITableView、UIWebView等)和自定义的类(User、Car等)
  • 上面的User类是通例,属于基本模型,实际情况有需求而定
  • 还可以用于监听动画的特殊时刻,比如:frame、color等变化
  • 一定记得在析构函数里移除监听;
  • 一定记得在析构函数里移除监听;
  • 一定记得在析构函数里移除监听……

我是与文无关的图.JPG

Leewin

9 posts
© 2018 Leewin
Powered by Hexo v3.7.1
|
粤ICP备2024282274号
|
Theme — NexT.Muse v6.3.0