iOS开发

iOS 开发学习,各种记录。

很多东西都比较简单,但是很多东西都容易忘记,或者自己曾经遇到过但是忘记了当时是如何处理的,使得再次遇到依旧会浪费很多时间,所以不管难易,都记录下来,避免二次伤害。

LLDB

iOS开发过程中Bug不可能少,最重要的如何快速的定位Bug的原因,不能每更改一行代码,运行整个App来查看效果,那样效率太过于低下。

Dancing in the Debugger — A Waltz with LLDB这篇文章很不错,也学到了很多。

chiselfacebook出品必属精品,更加方便的使用lldb。

建议有时间可以看chisel的源码,Objective-C、LLDB、Python一起学。

Xcode

Continue

程序继续执行。

1
2
(lldb) process continue
(lldb) c

‘c’ is an abbreviation for ‘process continue’

step over

以黑盒的形式执行一行代码。

1
2
3
(lldb) thread step-over
(lldb) next
(lldb) n

‘next’ is an abbreviation for ‘thread step-over’

‘n’ is an abbreviation for ‘thread step-over’

step into

进入具体的函数实现。

1
2
3
(lldb) thread step-in
(lldb) step
(lldb) s

‘step’ is an abbreviation for ‘thread step-in’

‘s’ is an abbreviation for ‘thread step-in’

step out

跳出具体的函数实现。

1
(lldb) thread step-out

‘step’ is an abbreviation for ‘thread step-in’

‘s’ is an abbreviation for ‘thread step-in’

Commands

breakpoint

断点管理。

set

Sets a breakpoint or set of breakpoints in the executable.

f

-f 指定文件

l

-l 指定行数

F

Set the breakpoint by fully qualified function names. For C++ this means namespaces and all arguments, and for Objective C this means a full function prototype with class and selector. Can be repeated multiple times to make one breakpoint for multiple names.

-F 指定全名

1
(lldb) breakpoint set -F "-[NSException raise]"

也称之为符号断点。

c

The breakpoint stops only if this condition expression evaluates to true.

-c 指定条件

command

Commands for adding, removing and listing LLDB commands executed when a breakpoint is hit.

add
1
2
3
(lldb) breakpoint command add 1
Enter your debugger command(s). Type 'DONE' to end.
> process continue

expression

-O

-O ( –object-description )

Display using a language-specific description API, if possible.

1
2
(lldb) expression -O -- <expr>
(lldb) po <expr>

‘po’ is an abbreviation for ‘expression -O –’

print

1
2
(lldb) expression -- <expr>
(lldb) print <expr>

‘print’ is an abbreviation for ‘expression –’

二进制、十六进制。。。等完整打印格式

frame

Commands for selecting and examing the current thread’s stack frames.

info

List information about the current stack frame in the current thread.

thread

return

Prematurely return from a stack frame, short-circuiting execution of newer frames and optionally yielding a specified value. Defaults to the exiting the current stack frame. Expects ‘raw’ input (see ‘help raw-input’.)

1
(lldb) thread return [<expr>]

直接返回指定的值。

process

continue

Continue execution of all threads in the current process.

interrupt

Interrupt the current target process.

memory

Commands for operating on memory in the current target process.

read

Read from the memory of the current target process.

Convenience

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 打印视图层级
(lldb) [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
(lldb) pviews


# 强制刷新
(lldb) e (void)[CATransaction flush]
(lldb) caflush

# 实例变量添加断点(涉及地址被修改)
(lldb) p (ptrdiff_t)ivar_getOffset((void*)object_getInstanceVariable([UIView class], "_layer", 0))
(lldb) watchpoint set expression -- (int *)$view + 8
(lldb) wivar $view "_layer"

# 非重写方法的符号断点(用于子类未重写父类方法)
(lldb) bmessage "-[UIViewController viewDidAppear:]"

# 打印引用计数
language swift refcount <expr>

Refrence

Debugging Swift code with LLDB

引用于 Debugging Swift code with LLDB

Signature

整个流程:

  1. 在你的 Mac 开发机器生成一对公私钥,这里称为公钥L,私钥L。L:Local
  2. 苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A,私钥A。A:Apple
  3. 把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书。
  4. 在苹果后台申请 AppID,配置好设备 ID 列表和 APP 可使用的权限,再加上第③步的证书,组成的数据用私钥 A 签名,把数据和签名一起组成一个 Provisioning Profile 文件,下载到本地 Mac 开发机。
  5. 在开发时,编译完一个 APP 后,用本地的私钥 L 对这个 APP 进行签名,同时把第④步得到的 Provisioning Profile 文件打包进 APP 里,文件名为 embedded.mobileprovision,把 APP 安装到手机上。
  6. 在安装时,iOS 系统取得证书,通过系统内置的公钥 A,去验证 embedded.mobileprovision 的数字签名是否正确,里面的证书签名也会再验一遍。
  7. 确保了 embedded.mobileprovision 里的数据都是苹果授权以后,就可以取出里面的数据,做各种验证,包括用公钥 L 验证APP签名,验证设备 ID 是否在 ID 列表上,AppID 是否对应得上,权限开关是否跟 APP 里的 Entitlements 对应等。

上面的步骤对应到我们平常具体的操作和概念是这样的:

  1. 第 1 步对应的是 keychain 里的 “从证书颁发机构请求证书”,这里就本地生成了一对公私钥,保存的 CertificateSigningRequest 就是公钥,私钥保存在本地电脑里。
  2. 第 2 步苹果处理,不用管。
  3. 第 3 步对应把 CertificateSigningRequest 传到苹果后台生成证书,并下载到本地。这时本地有两个证书,一个是第 1 步生成的,一个是这里下载回来的,keychain 会把这两个证书关联起来,因为他们公私钥是对应的,在XCode选择下载回来的证书时,实际上会找到 keychain 里对应的私钥去签名。这里私钥只有生成它的这台 Mac 有,如果别的 Mac 也要编译签名这个 App 怎么办?答案是把私钥导出给其他 Mac 用,在 keychain 里导出私钥,就会存成 .p12 文件,其他 Mac 打开后就导入了这个私钥。
  4. 第 4 步都是在苹果网站上操作,配置 AppID / 权限 / 设备等,最后下载 Provisioning Profile 文件。
  5. 第 5 步 XCode 会通过第 3 步下载回来的证书(存着公钥),在本地找到对应的私钥(第一步生成的),用本地私钥去签名 App,并把 Provisioning Profile 文件命名为 embedded.mobileprovision 一起打包进去。这里对 App 的签名数据保存分两部分,Mach-O 可执行文件会把签名直接写入这个文件里,其他资源文件则会保存在_CodeSignature目录下。

概念:

  1. 证书:内容是公钥或私钥,由其他机构对其签名组成的数据包。
  2. Entitlements:包含了 App 权限开关列表。
  3. CertificateSigningRequest:本地公钥。
  4. p12:本地私钥,可以导入到其他电脑。
  5. Provisioning Profile:包含了 证书 / Entitlements 等数据,并由苹果后台私钥签名的数据包。

引用 iOS App 签名的原理

Device

http://www.blakespot.com/ios_device_specifications_grid.html

UIKit

UIWindow

backgroundColor如果不设置或者为透明色,则不响应不透明事件。

UIView

Touch Events

1.系统将触摸事件打包成UIEvent对象
2.UIApplication将事件传递给UIWindow
3.UIWindow调用hitTest方法。

point(inside:with:)

Returns a Boolean value indicating whether the receiver contains the specified point.

1
func point(inside point: CGPoint, with event: UIEvent?) -> Bool

判断触摸的点在不在该试图范围内。

hitTest(_:with:)

Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.

1
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?

判断哪个视图响应事件

调用当前视图的func point(inside point: CGPoint, with event: UIEvent?) -> Bool判断触摸点是否在当前视图内。

false:直接返回nil,表示触摸点不在当前视图;

true:遍历并调用当前视图的子视图hitTest方法。如果所有的子视图hitTest都返回nil,则返回自身;如果有子视图返回不为空,则返回该子视图。

遍历是从当前视图的最顶层视图遍历到最底层视图,即从subviews最后一个开始向前遍历。

如果isHiddentruealpha0,子视图超过父视图范围,则该视图的hitTest返回nil

当设置视图的backgroundColorclearColor的时候,虽然当前视图也是透明的,但是依旧将事件向子视图传递。

Animation

1
2
3
4
5
6
7
8
9
10
11
12
/// 在一次开发中,当同时改变视图的位置和大小的时候,动画会出现异常。
/// 我希望的动画是以center为中心,改变view的大小,结果动画会先向左上角闪一下,再回到center的位置。
[UIView animateWithDuration:0.3 animations:^{
view.center = <# CGPoint #>;
view.frame = <# CGRect #>
}];

/// 解决方法
[UIView animateWithDuration:0.3 animations:^{
view.center = <# CGPoint #>;
view.transform = CGAffineTransformMakeScale(0.5, 0.5);
}];

当frame改变的时候transform会重设。

UIScrollView

UITableView

1
2
3
4
5
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

/// 配套使用
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section

reloadData

How to keep UITableView contentOffset after calling reloadData

How to keep UITableView contentoffset after calling -reloadData

reloadData并不会调整offset

仅仅在Cell的高度为固定的状态,如果Cell是动态计算出来的就不一定了。如果Cell的高度是不固定的,那么他的contentOffset也是不固定的,而是在滑动的过程中不断的计算出来。

具体的解决方式没有尝试过,别人提出的方式是用字典来缓存高度。

MJRefresh

解决下拉刷新无法回弹的问题

关于iOS11 上下拉刷新无法返回的问题,不关MJRefresh的事,解答

Property Detail Available
automaticallyAdjustsScrollViewInsets 自动调节UIScrollViewcontentInset iOS 7.0 ~ 11.0
contentInsetAdjustmentBehavior 自动调节UIScrollViewcontentInset iOS 11.0+
1
2
3
4
5
6
// 关闭自动调节
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
self.automaticallyAdjustsScrollViewInsets = NO;
}
1
2
3
4
5
6
7
/// 如果有导航栏设置偏移量。
self.tableView.contentInset = UIEdgeInsetsMake(<#CGFloat TopBarHeight#>, 0, 0, 0);
// 或者
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view).offset(<#CGFloat TopBarHeight#>);
make.left.bottom.right.equalTo(self.view);
}];
1
2
3
/// 设置Cell的高度。
self.tableView.estimatedRowHeight = <#CGFloat CellHeight#>;
self.tableView.rowHeight = UITableViewAutomaticDimension;

Summary

一个TableView中多种样式相似的Cell

当一个UITableView中有多种Cell,但是又差不多样式,最优雅的方式是使用继承方式来实现,不要用if、else来判断哪个Cell显示哪些内容。

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
/**
通用样式
*/
@interface BaseSampleTableViewCell : UITableViewCell
@property(nonatomic, strong) UIImageView *avatarImageView;
@property(nonatomic, strong) UILabel *titleLabel;
@end
/**
一个按钮的
*/
@interface OneButtonSampleTableViewCell: BaseSampleTableViewCell
@property(nonatomic, strong) UIButton *buttonOne;
@end
/**
两个按钮的
*/
@interface TwoButtonSampleTableViewCell: OneButtonSampleTableViewCell
@property(nonatomic, strong) UIButton *buttonTwo;
@end
/**
两个按钮带输入框的
*/
@interface TextViewSampleTableViewCell: TwoButtonSampleTableViewCell
@property(nonatomic, strong) UITextView *textView;
@end

如果点击按钮Cell的样式改变直接[tableView dequeueReusableCellWithIdentifier:[TwoButtonSampleTableViewCell reuseIdentifier] forIndexPath:indexPath];对应样式的Cell就OK了,避免过多的if、else。

动态显示

很多时候我们需要根据服务器返回的是否为空来动态显示内容,这个时候依旧用继承重写进行实现,在动态将相应的视图添加到父视图上,并修改约束。

优雅的唯一表示一个Cell

很多人在注册Cell的时候会写很多的字符串来注册,建议采用下面方式来进行。

1
2
3
4
5
6
7
8
9
@interface UITableViewCell (ReuseIdentifier)
+ (NSString *)reuseIdentifier;
@end

@implementation UITableViewCell (ReuseIdentifier)
+ (NSString *)reuseIdentifier {
return [self description];
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
//MARK: - Identifier
protocol ReuseIdentifier {}
extension ReuseIdentifier {
var reuseIdentifier: String { return String(describing: type(of: self)) }
static var reuseIdentifier: String { return String(describing: self) }
}
/// Add reuseIdentifier into UITableViewCell, it also can been using any object to get class name.
///
/// Uniquely an UITableViewCell by class name.
///
/// ABCUITableViewCell.reuseIdentifier // "ABCUITableViewCell"
extension UITableViewCell: ReuseIdentifier {}
extension UICollectionViewCell: ReuseIdentifier {}

是不是优雅了很多呢?

UICollectionView

Horizontal refresh

仿照MJRefresh的结构来写的,至此也大致了解了MJRefresh的设计模式。

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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
#pragma mark - Horizontal Refresh

typedef void (^RefreshBlock)(void);
static NSString *const RefreshKeyPathContentOffset = @"contentOffset";
//static NSString *const RefreshKeyPathContentInset = @"contentInset";
static NSString *const RefreshKeyPathContentSize = @"contentSize";
/** 刷新控件的状态 */
typedef NS_ENUM(NSInteger, RefreshState) {
/** 普通闲置状态 */
RefreshStateNormal = 1,
/** 松开就可以进行刷新的状态 */
RefreshStatePulling,
/** 正在刷新中的状态 */
RefreshStateRefreshing,
/** 即将刷新的状态 */
RefreshStateWillRefresh,
/** 所有数据加载完毕,没有更多的数据了 */
RefreshStateNoMoreData
};

@interface BaseHorizontalRefresh: UIView
@property(nonatomic, weak) UIScrollView *scrollView;
@property(nonatomic, strong) RefreshBlock refreshBlock;
@property(nonatomic) RefreshState state;
@property(nonatomic, readonly) UIEdgeInsets refreshingInset;/// 刷新状态的Inset
@property(nonatomic, readonly) BOOL isCanRefresh;/// 是否到达可以刷新的点
@property(nonatomic, readonly) BOOL isRefreshing;/// 是否正在刷新
@property(nonatomic) UIEdgeInsets originInsets;/// 原始inset
@property(nonatomic, readonly) CGRect defaultFrame;///初始化大小
- (void)beginRefreshing;
- (void)endRefreshing;
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change;
- (void)scrollViewContentSizeDidChange:(NSDictionary *)chang;
@end
@implementation BaseHorizontalRefresh
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.state = RefreshStateNormal;
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];

self.originInsets = self.scrollView.contentInset;
/// 设置初始大小
self.frame = self.defaultFrame;

[self removeObservers];
[self addObservers];
}
- (void)addObservers {
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.scrollView addObserver:self forKeyPath:RefreshKeyPathContentOffset options:options context:nil];
[self.scrollView addObserver:self forKeyPath:RefreshKeyPathContentSize options:options context:nil];
// self.pan = self.scrollView.panGestureRecognizer;
// [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
}
- (void)removeObservers {
[self.superview removeObserver:self forKeyPath:RefreshKeyPathContentOffset];
[self.superview removeObserver:self forKeyPath:RefreshKeyPathContentSize];
// [self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState];
// self.pan = nil;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// 遇到这些情况就直接返回
if (!self.userInteractionEnabled) return;

// 这个就算看不见也需要处理
if ([keyPath isEqualToString:RefreshKeyPathContentSize]) {
[self scrollViewContentSizeDidChange:change];
}

// 看不见
if (self.hidden) return;
if ([keyPath isEqualToString:RefreshKeyPathContentOffset]) {
[self scrollViewContentOffsetDidChange:change];
}
// else if ([keyPath isEqualToString:RefreshKeyPathPanState]) {
// [self scrollViewPanStateDidChange:change];
// }
}
- (BOOL)isRefreshing {
return self.state == RefreshStateRefreshing;
}
#pragma Subclass Override Begin
- (UIEdgeInsets)refreshingInset {
return UIEdgeInsetsZero;
}
- (BOOL)isCanRefresh {
return NO;
}
#pragma Subclass Override Begin
- (void)beginRefreshing {
if (self.isRefreshing)
return;

self.state = RefreshStateRefreshing;
__weak typeof(self) wself = self;
[UIView animateWithDuration:0.25 animations:^{
wself.scrollView.contentInset = self.refreshingInset;
}];

if (self.refreshBlock)
self.refreshBlock();
}
- (void)endRefreshing {
if (!self.isRefreshing)
return;

self.state = RefreshStateNormal;
__weak typeof(self) wself = self;
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.4 animations:^{
wself.scrollView.contentInset = wself.originInsets;
}];
});

}
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change {
if (self.state == RefreshStateRefreshing) {
self.scrollView.contentInset = self.refreshingInset;
return;
}

if (self.scrollView.isDragging) {
self.state = self.isCanRefresh ? RefreshStatePulling : RefreshStateNormal;
} else if (self.state == RefreshStatePulling) {
[self beginRefreshing];
}
}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {}
//- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
@end

#pragma mark Header

@interface HorizontalRefreshHeader: BaseHorizontalRefresh
@property(nonatomic) UIActivityIndicatorView *indicatorView;
+ (instancetype)refreshWithRefreshBlock:(RefreshBlock)block;
@end
@implementation HorizontalRefreshHeader
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
self.indicatorView.hidesWhenStopped = NO;
self.indicatorView.color = UIColor.blackColor;
[self addSubview:self.indicatorView];
}
return self;
}
+ (instancetype)refreshWithRefreshBlock:(RefreshBlock)block {
HorizontalRefreshHeader *header = [[HorizontalRefreshHeader alloc] init];
header.refreshBlock = block;
return header;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.indicatorView.center = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
}
- (void)beginRefreshing {
[super beginRefreshing];

if (!self.indicatorView.isAnimating) {
[self.indicatorView startAnimating];
}
}
- (void)endRefreshing {
[super endRefreshing];

if (self.indicatorView.isAnimating) {
[self.indicatorView stopAnimating];
}
}
- (CGRect)defaultFrame {
return CGRectMake(-54, 0, 54, self.scrollView.bounds.size.height);
}
- (UIEdgeInsets)refreshingInset {
UIEdgeInsets inset = self.scrollView.contentInset;
inset.left = 54;
return inset;
}
- (BOOL)isCanRefresh {
return self.scrollView.contentOffset.x <= -54;
}
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change {
[super scrollViewContentOffsetDidChange:change];
if (!self.isRefreshing && self.scrollView.isDragging && self.scrollView.contentOffset.x < 0) {
CGFloat percent = self.scrollView.contentOffset.x / -54;
if (percent >= 1) {
[self.indicatorView startAnimating];
} else {
self.indicatorView.transform = CGAffineTransformMakeScale(percent, percent);
[self.indicatorView stopAnimating];
}
}
}
@end

#pragma mark Footer

@interface HorizontalRefreshFooter: BaseHorizontalRefresh
@property(nonatomic) UIActivityIndicatorView *indicatorView;
+ (instancetype)refreshWithRefreshBlock:(RefreshBlock)block;
- (void)endRefreshingWithNoMoreData;
@end
@implementation HorizontalRefreshFooter
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
self.indicatorView.hidesWhenStopped = NO;
self.indicatorView.color = UIColor.blackColor;
[self addSubview:self.indicatorView];
}
return self;
}
+ (instancetype)refreshWithRefreshBlock:(RefreshBlock)block {
HorizontalRefreshFooter *footer = [[HorizontalRefreshFooter alloc] init];
footer.refreshBlock = block;
return footer;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.indicatorView.center = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
}
- (void)beginRefreshing {
[super beginRefreshing];

if (!self.indicatorView.isAnimating) {
[self.indicatorView startAnimating];
}
}
- (void)endRefreshing {
[super endRefreshing];

if (self.indicatorView.isAnimating) {
[self.indicatorView stopAnimating];
}

self.indicatorView.hidden = NO;
}
- (void)endRefreshingWithNoMoreData {
[self endRefreshing];
self.state = RefreshStateNoMoreData;
self.indicatorView.hidden = YES;
}
- (CGRect)defaultFrame {
return CGRectMake(self.scrollView.bounds.size.width, 0, 44, self.scrollView.bounds.size.height);
}
- (UIEdgeInsets)refreshingInset {
UIEdgeInsets inset = self.scrollView.contentInset;
inset.right = 44;
return inset;
}
- (BOOL)isCanRefresh {
return self.state == RefreshStateNoMoreData ? NO : self.scrollView.contentOffset.x + self.scrollView.bounds.size.width - self.offsetRevise.x >= 44;
}
/// 当数据量特别小的时候修正offset
- (CGPoint)offsetRevise {
return CGPointMake(MAX(self.scrollView.contentSize.width, self.scrollView.bounds.size.width), 0);
}
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change {

if (self.state == RefreshStateRefreshing) {
self.scrollView.contentInset = self.refreshingInset;
return;
}

if (self.state == RefreshStateNoMoreData)
return;

if (self.scrollView.isDragging) {
self.state = self.isCanRefresh ? RefreshStatePulling : RefreshStateNormal;
} else if (self.state == RefreshStatePulling) {
[self beginRefreshing];
}

if (!self.isRefreshing && self.scrollView.isDragging && self.scrollView.contentOffset.x + self.scrollView.bounds.size.width > self.offsetRevise.x) {
CGFloat percent = (self.scrollView.contentOffset.x + self.scrollView.bounds.size.width - self.offsetRevise.x) / 44;
if (percent >= 1) {
[self.indicatorView startAnimating];
} else {
self.indicatorView.transform = CGAffineTransformMakeScale(percent, percent);
[self.indicatorView stopAnimating];
}
}
}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)chang {
self.frame = CGRectMake(self.offsetRevise.x, self.frame.origin.y, 44, self.frame.size.height);
}
@end

#pragma mark Carthage

@interface UIScrollView (HorizontalRefresh)
@property(nonatomic) HorizontalRefreshHeader *horizontal_header;
@property(nonatomic) HorizontalRefreshFooter *horizontal_footer;
@end

@implementation UIScrollView (HorizontalRefresh)
- (void)setHorizontal_header:(HorizontalRefreshHeader *)horizontal_header {
if (horizontal_header != self.horizontal_header) {
[self.horizontal_header removeFromSuperview];

horizontal_header.scrollView = self;
horizontal_header.tag = 666;
[self insertSubview:horizontal_header atIndex:0];
}
}
- (HorizontalRefreshHeader *)horizontal_header {
return [self viewWithTag:666];
}
- (void)setHorizontal_footer:(HorizontalRefreshFooter *)horizontal_footer {
if (horizontal_footer != self.horizontal_footer) {
[self.horizontal_footer removeFromSuperview];

horizontal_footer.scrollView = self;
horizontal_footer.tag = 999;
[self insertSubview:horizontal_footer atIndex:0];
}
}
- (HorizontalRefreshFooter *)horizontal_footer {
return [self viewWithTag:999];
}
@end
  • 当调用同时调用reloadData和改变contentInset(回弹)的时候会出现抖动的问题,所以需要将改变contentInset放到下一个runloop中调用。

selected muti-selected

在这里我们使用UICollectionView来实现单选、多选,因为自带单选,我们只需要解决多选就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// UICollectionViewCell
- (void)setSelected:(BOOL)selected {
[super setSelected:selected];

/// 修改选择样式。
}

/// UICollectionView
self.allowsSelection = YES;
self.allowsMultipleSelection = YES/*单选改成NO*/;

/// UICollectionViewDelegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (self.maxSelectableCount > 1 && self.indexPathsForSelectedItems.count >= self.maxSelectableCount) {
/// 多选且大于最大可选数量,则不继续选择。
return NO;
} else {
return YES;
}
}

UICollectionViewFlowLayout

左对齐。

UICollectionViewLeftAlignedLayout

UITextView

UITextView with auto size.

如何在UIScrollView或者其子类中使用UITextView并自动计算高度,自动改变UIScrollView的offset。诸如此类的文章很多,在此总结自己的想法。

  1. 键盘谈起的时候判断是否遮挡UIScrollView中的UITextView,根据需要改变offset。
  2. 当UITextView换行的时候需要判断是否键盘是否再次挡住UITextView。
  • Setting
1
2
3
4
5
6
7
/// 设置约束,从上到下约束都需要是满的。
[self.scrollView addSubview:self.textView];
[self.textView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.right.bottom.left.equalTo(self);
}];
/// 关闭滚动。
textView.scrollEnabled = NO;
  • Listening
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// 监听键盘frame改变。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyBoardChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];

-(void)keyBoardChangeFrame:(NSNotification *)notification {
//获取键盘弹出或收回时frame
CGRect keyBoardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
BOOL isKeyboardHidden = keyBoardFrame.origin.y == self.view.frame.size.height;
/// 防止在输入的时候滑动
self.scrollView.scrollEnabled = isKeyboardHidden;
/// 计算键盘顶部到scrollView顶部的距离。
CGFloat keyboardTopOffset = self.scrollView.bounds.size.height - keyBoardFrame.size.height;

CGPoint newPoint = CGPointZero;
if (!isKeyboardHidden) {
/// 计算scrollView应该移动的距离。
CGFloat moveOffset = <#scrollView中参考点的frame.y#> - keyboardTopOffset;
if (moveOffset > 0) {
newPoint = CGPointMake(0, moveOffset);
}
}
[self.scrollView setContentOffset:newPoint animated:YES];
}

这里的参考点可以是这个textView,在- (void)textViewDidChange:(UITextView *)textView中计算是否需要重新设置scrollView的contentOffset。

1
2
3
4
5
6
7
8
9
10
CGFloat keyboardTopOffset = self.scrollView.bounds.size.height - keyBoardFrame.size.height;
/// 判断是否需要移动
CGFloat isNeedAdjustOffset = <#scrollView中参考点的frame.y#> - keyboardTopOffset;
if (isNeedAdjustOffset > 0) {
CGFloat moveOffset = isNeedAdjustOffset - self.scrollView.contentOffset.y;
if (moveOffset != 0) {
CGPoint newPoint = CGPointMake(0, moveOffset + self.scrollView.contentOffset.y);
[self.scrollView setContentOffset:newPoint animated:NO];
}
}

这样就会让指定的点关键点始终在键盘的上面。

Limit number of character

字符限制在很多场景都会用到,但是除了普通的汉字和ABC之外还有表情其他字符,这个时候单判断字符是会出现以下问题的。

  1. 表情这里比较坑,不同的表情占用的字符数量并不一定。有的占两个字符长度,有的占四个字符长度,有的占11个字符长度,所以单从(text.length)来限制,就会导致最少只能显示几个表情的问题。
  2. 如果检测是在输入的过程中,则会出现最后几个字还没有打出来,就直接截断,导致显示的是拼音。
  3. 复制粘贴也需要根据实际情况进行截断。
  4. 如果单从text.length来截取,可能会造成最后一个如果是表情,可能会出现错误符号。

我个人的解决办法是将ASCII作为一个字符,非ASCII作为两个字符。或者将汉字作为两个字符,其他作为一个字符。这样就不会出现汉字可以很多,表情只能输入几个的问题了。

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
public extension UITextInput {
///
///
/// - Parameters:
/// - limit: 限制字符数量
/// - cut: 超出部分是否截断
/// - Returns: 是否超出限制
@discardableResult
func isOutnumber(limit: Int, cut: Bool = true) -> Bool {
var isOutNumber = false

let noneSelect = markedTextRange?.isEmpty ?? true

guard limit != 0 && noneSelect,
let textRange = textRange(from: beginningOfDocument, to: endOfDocument),
var unicodes = text(in: textRange)?.unicodeScalars else { return isOutNumber }

var ascii: Int = 0//ASCII
var noAscii: Int = 0//汉字,表情

unicodes.forEach {
if $0.isASCII { ascii += 1 }
else { noAscii += 1 }
}

while ascii + noAscii * 2 > limit {
isOutNumber = true
let scalar = unicodes.removeLast()

if scalar.isASCII { ascii -= 1 }
else { noAscii -= 1 }
}

if isOutNumber && cut {
self.replace(textRange, withText: String(unicodes))
}

return isOutNumber
}
}
1
2
3
4
5
[self.text enumerateSubstringsInRange:NSMakeRange(0, self.text.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
/// 这里和Swift的character类似,循环每一个字符,针对字符截断。这样就不会出现粘贴的文字表情被错误截断产生错误符号。
}];

markedTextRange

被标记的文本,这个东西基本上没次都不一样,由于输入的内容不一样,可能是中文,可能是英文,但是在回调中这些标记的文字也在self.text中,这部分文字由于可能存在没有输入完成,所以并不计入最终的内容。

1
2
/// 获取被标记的文本。
[self textInRange:self.markedTextRange];

Placeholder

UITtextView没有Placeholder着实难受,那就只能自己实现一个了。

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
- (void)awakeFromNib {
[super awakeFromNib];
[self customInit];
}

- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer {
self = [super initWithFrame:frame textContainer:textContainer];
if (self) {
[self customInit];
}
return self;
}

- (void)customInit {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChange:) name:UITextViewTextDidChangeNotification object:nil];
}

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)layoutSubviews {

UIEdgeInsets inset = self.textContainerInset;

if (self.placeholder) {
[self.placeholderLabel sizeToFit];
CGRect newFrame = self.placeholderLabel.frame;
/// 这里5测试出来的,直接是inset.left会和光标不对齐。
newFrame.origin = CGPointMake(inset.left + 5, inset.top);
self.placeholderLabel.frame = newFrame;
}
[super layoutSubviews];
}

- (void)setText:(NSString *)text {
[super setText:text];
self.placeholderLabel.hidden = self.hasText;
}

- (void)setFont:(UIFont *)font {
[super setFont:font];
if (self.placeholder) {
self.placeholderLabel.font = font;
}
}

- (void)textDidChange:(NSNotification *)notification {
self.placeholderLabel.hidden = self.hasText;
}

- (UILabel *)placeholderLabel {
if (!_placeholderLabel) {
_placeholderLabel = [UILabel new];
_placeholderLabel.font = self.font;
[self customPlaceholderLabel:_placeholderLabel];
[self addSubview:_placeholderLabel];
}
return _placeholderLabel;
}

- (void)customPlaceholderLabel:(UILabel *)placeholderLabel {
placeholderLabel.textColor = <#Custom Color#>;
}

- (void)setPlaceholder:(NSString *)placeholder {
_placeholder = placeholder;
self.placeholderLabel.text = placeholder;
}

UISearchBar

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation CustomSearchBar
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {

}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
/// ... custom search bar size.
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
self.searchBar = [CustomSearchBar new];
self.searchBar.placeholder = @"";
self.searchBar.returnKeyType = UIReturnKeySearch;
self.searchBar.tintColor = UIColor.blackColor;
self.searchBar.showsCancelButton = YES;
[self.searchBar setImage:<#(nullable UIImage *)#> forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal];
[self.searchBar setImage:<#(nullable UIImage *)#> forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
UITextField *searchTextField = [self.searchBar valueForKey:@"_searchField"];
/// ...
UILabel *placeholderLabel = [searchTextField valueForKey:@"_placeholderLabel"];
/// ...
UIButton *cancelButton = [self.searchBar valueForKey:@"_cancelButton"];
/// ...
UIImageView *backgroundImageView = [self.searchBar valueForKey:@"_background"];
/// ...
/// Add searchBar to navigationItem.
self.navigationItem.titleView = self.searchBar;

iOS11更改了搜索栏的样式,与其兼容新老版本不如自己写SearchBar

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
@class SearchBar;
@protocol SearchBarDelegate <NSObject>
@optional
/// 搜索框内部样式

/// Default self.bounds
- (CGRect)searchBar:(SearchBar *)searchBar frameForSearchField:(UITextField *)searchField;
/// Default CGRectZero
- (CGRect)searchBar:(SearchBar *)searchBar frameForCancelButton:(UIButton *)cancelButton;
- (CGRect)searchBar:(SearchBar *)searchBar frameForSearchButton:(UIButton *)searchButton;
- (CGRect)searchBar:(SearchBar *)searchBar frameForClearButton:(UIButton *)clearButton;

/// 搜索框各种事件

- (BOOL)searchBarShouldBeginEditing:(SearchBar *)searchBar;
//- (void)searchBarTextDidBeginEditing:(SearchBar *)searchBar;
- (BOOL)searchBarShouldEndEditing:(SearchBar *)searchBar;
//- (void)searchBarTextDidEndEditing:(SearchBar *)searchBar;
- (void)searchBar:(SearchBar *)searchBar textDidChange:(NSString *)searchText;
- (BOOL)searchBar:(SearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;

- (void)searchBarSearchButtonClicked:(SearchBar *)searchBar;
//- (void)searchBarBookmarkButtonClicked:(SearchBar *)searchBar;
- (void)searchBarCancelButtonClicked:(SearchBar *)searchBar;
//- (void)searchBarResultsListButtonClicked:(SearchBar *)searchBar;

//- (void)searchBar:(SearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope;
@end


@interface SearchBar: UIView
@property (nonatomic) UITextField *searchField;
@property (nonatomic) NSString *placeholder;
/// 直接使用_searchField.text会导致无法正确的显示和隐藏clearButton,算是一个小Bug吧(解决方式:使用KVO监听_searchField.text)
@property (nonatomic) NSString *text;
@property (nonatomic, weak) id<SearchBarDelegate> delegate;
/// 取消
@property (nonatomic) UIButton *cancelButton;
/// Only support UISearchBarIconSearch and UISearchBarIconClear
- (void)setImage:(UIImage *)iconImage forSearchBarIcon:(UISearchBarIcon)icon state:(UIControlState)state;
@end

@interface SearchBar() <UITextFieldDelegate> {
struct {
unsigned int searchBar_frameForSearchField_ : 1;
unsigned int searchBar_frameForCancelButton_ : 1;
unsigned int searchBar_frameForSearchButton_ : 1;
unsigned int searchBar_frameForClearButton_ : 1;

unsigned int searchBarShouldBeginEditing_ : 1;
unsigned int searchBarShouldEndEditing_ : 1;
unsigned int searchBar_textDidChange_ : 1;
unsigned int searchBar_shouldChangeTextInRange_replacementText_ : 1;
unsigned int searchBarSearchButtonClicked_ : 1;
unsigned int searchBarCancelButtonClicked_ : 1;
} _delegateFlags;
}
/// 搜索
@property (nonatomic) UIButton *searchTextFieldLeftButton;
/// 清除
@property (nonatomic) UIButton *searchTextFieldRightButton;
@end

@implementation SearchBar
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = UIColor.clearColor;

_searchTextFieldLeftButton = [UIButton buttonWithType:UIButtonTypeCustom];
_searchTextFieldRightButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_searchTextFieldRightButton addTarget:self action:@selector(clearButtonAction:) forControlEvents:UIControlEventTouchUpInside];

_searchField = [[UITextField alloc] initWithFrame:self.bounds];
_searchField.returnKeyType = UIReturnKeySearch;
_searchField.delegate = self;
_searchField.leftView = _searchTextFieldLeftButton;
_searchField.leftViewMode = UITextFieldViewModeAlways;
_searchField.rightView = _searchTextFieldRightButton;
_searchField.rightViewMode = UITextFieldViewModeAlways;
_searchTextFieldRightButton.hidden = YES;
[self addSubview:_searchField];

_cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_cancelButton addTarget:self action:@selector(cancelButtonAction:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_cancelButton];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldTextDidChangeNotificationAction:) name:UITextFieldTextDidChangeNotification object:nil];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];

[_cancelButton sizeToFit];
_cancelButton.frame = _delegateFlags.searchBar_frameForCancelButton_ ? [_delegate searchBar:self frameForCancelButton:_cancelButton] : CGRectZero;

_searchField.frame = _delegateFlags.searchBar_frameForSearchField_ ? [_delegate searchBar:self frameForSearchField:_searchField] : self.bounds;

[_searchTextFieldLeftButton sizeToFit];
_searchTextFieldLeftButton.frame = _delegateFlags.searchBar_frameForSearchButton_ ? [_delegate searchBar:self frameForSearchButton:_searchTextFieldLeftButton] : _searchTextFieldLeftButton.frame;

[_searchTextFieldRightButton sizeToFit];
_searchTextFieldRightButton.frame = _delegateFlags.searchBar_frameForClearButton_ ? [_delegate searchBar:self frameForClearButton:_searchTextFieldRightButton] : _searchTextFieldRightButton.frame;
}
- (BOOL)resignFirstResponder {
return [_searchField resignFirstResponder];
}
- (void)clearButtonAction:(UIButton *)sender {
sender.hidden = YES;
_searchField.text = @"";

if (_delegateFlags.searchBar_textDidChange_) {
[_delegate searchBar:self textDidChange:@""];
}
}
- (void)cancelButtonAction:(UIButton *)sender {
if (_delegateFlags.searchBarCancelButtonClicked_) {
[_delegate searchBarCancelButtonClicked:self];
}
}
- (void)setImage:(UIImage *)iconImage forSearchBarIcon:(UISearchBarIcon)icon state:(UIControlState)state {
switch (icon) {
case UISearchBarIconSearch: {
[_searchTextFieldLeftButton setImage:iconImage forState:state];
}
break;
case UISearchBarIconClear: {
[_searchTextFieldRightButton setImage:iconImage forState:state];
}
break;
default:
break;
}
}
- (void)setDelegate:(id<SearchBarDelegate>)delegate {
_delegate = delegate;

_delegateFlags.searchBar_frameForSearchField_ = [_delegate respondsToSelector:@selector(searchBar:frameForSearchField:)];
_delegateFlags.searchBar_frameForCancelButton_ = [_delegate respondsToSelector:@selector(searchBar:frameForCancelButton:)];
_delegateFlags.searchBar_frameForSearchButton_ = [_delegate respondsToSelector:@selector(searchBar:frameForSearchButton:)];
_delegateFlags.searchBar_frameForClearButton_ = [_delegate respondsToSelector:@selector(searchBar:frameForClearButton:)];


_delegateFlags.searchBarShouldBeginEditing_ = [_delegate respondsToSelector:@selector(searchBarShouldBeginEditing:)];
_delegateFlags.searchBarShouldEndEditing_ = [_delegate respondsToSelector:@selector(searchBarShouldEndEditing:)];
_delegateFlags.searchBar_textDidChange_ = [_delegate respondsToSelector:@selector(searchBar:textDidChange:)];
_delegateFlags.searchBar_shouldChangeTextInRange_replacementText_ = [_delegate respondsToSelector:@selector(searchBar:shouldChangeTextInRange:replacementText:)];
_delegateFlags.searchBarSearchButtonClicked_ = [_delegate respondsToSelector:@selector(searchBarSearchButtonClicked:)];
_delegateFlags.searchBarCancelButtonClicked_ = [_delegate respondsToSelector:@selector(searchBarCancelButtonClicked:)];
}
#pragma mark selecotr delegate
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
return _delegateFlags.searchBarShouldBeginEditing_ ? [_delegate searchBarShouldBeginEditing:self] : YES;
}
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
return _delegateFlags.searchBarShouldEndEditing_ ? [_delegate searchBarShouldEndEditing:self] : YES;
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
return _delegateFlags.searchBar_shouldChangeTextInRange_replacementText_ ? [_delegate searchBar:self shouldChangeTextInRange:range replacementText:string] : YES;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if (_delegateFlags.searchBarSearchButtonClicked_) {
[_delegate searchBarSearchButtonClicked:self];
}
return YES;
}
- (void)textFieldTextDidChangeNotificationAction:(NSNotification *)sender {
_searchTextFieldRightButton.hidden = _searchField.text.length == 0;
if (_delegateFlags.searchBar_textDidChange_) {
[_delegate searchBar:self textDidChange:_searchField.text];
}
}
#pragma mark easy property
- (NSString *)placeholder {
return _searchField.placeholder;
}
- (void)setPlaceholder:(NSString *)placeholder {
_searchField.placeholder = placeholder;
}
- (NSString *)text {
return _searchField.text;
}
- (void)setText:(NSString *)text {
_searchField.text = text;
_searchTextFieldRightButton.hidden = text.length == 0;
}
@end

UIDatePicker

日期选择器

1
2
3
4
5
6
self.picker = [[UIDatePicker alloc] initWithFrame:CGRectMake(0, 40, KScreenWidth, 180)];
self.picker.backgroundColor = [UIColor whiteColor];
self.picker.datePickerMode = UIDatePickerModeDate;

/// 获取日期。
self.picker.date

UINavigationBar

UINavigationItem

UIVisualEffectView

1
2
3
4
5
6
/// 修改模糊背景颜色
for (UIView *view in <#(UIVisualEffectView)#>.subviews) {
if ([view isMemberOfClass:NSClassFromString(@"_UIVisualEffectSubview")]) {
view.backgroundColor = [UIColor colorWithRed:<#(CGFloat)#> green:<#(CGFloat)#> blue:<#(CGFloat)#> alpha:<#(CGFloat)#>];
}
}

UIViewController

Enable interactive pop gesture recognizer with custom back button

自定义返回按钮导致系统滑动手势失败解决。

1
2
3
4
5
6
7
8
9
10
11
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
[backButton setImage:[UIImage imageNamed:<#(nonnull NSString *)#>] forState:UIControlStateNormal];
[backButton addTarget:self action:@selector(backAction:) forControlEvents:UIControlEventTouchUpInside];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
self.navigationItem.hidesBackButton = YES;
self.navigationController.interactivePopGestureRecognizer.delegate = self;


- (void)backAction:(UIButton *)sender {
[self.navigationController popViewControllerAnimated:YES];
}

Transition

presente transition的默认过程是创建一个UIPresentationController,将prestatingViewController的rootViewController的subViewControllers压栈,将prestationController

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
/// x.h
@interface TransitionManager : NSObject <UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning>
@end

///x.m
@interface PresentationController: UIPresentationController
@property(nonatomic, strong) UIView *dimmingView;
@end

@implementation PresentationController

- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController {
self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController];
if (self) {
self.dimmingView = [UIView new];
self.dimmingView.backgroundColor = <#Custom Dimming Background Color#>;
[self.dimmingView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissAction)]];
}
return self;
}

- (void)presentationTransitionWillBegin {
self.dimmingView.frame = self.containerView.bounds;
self.dimmingView.alpha = 0;
[self.containerView addSubview:self.dimmingView];

/// 动画同时进行。
[self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
self.dimmingView.alpha = 0.6;
} completion:nil];
}

- (void)dismissalTransitionWillBegin {
[self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
self.dimmingView.alpha = 0;
} completion:nil];
}

- (void)dismissAction {
[self.presentedViewController dismissViewControllerAnimated:YES completion:nil];
}

@end



@implementation TransitionManager

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
UIView *containerView = transitionContext.containerView;
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

CGRect endFrame = [transitionContext finalFrameForViewController:toVC];
CGRect startFrame = endFrame;
startFrame.origin.y = containerView.frame.size.height;


if (toVC.isBeingPresented) {
[containerView addSubview:toVC.view];
toVC.view.frame = startFrame;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toVC.view.frame = endFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:finished];
}];
} else {
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromVC.view.frame = startFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:finished];
}];
}

}

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.2;
}

#pragma mark - UIViewControllerTransitioningDelegate

- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source {
return [[PresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return self;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return self;
}
@end

iOS 视图控制器转场详解

  1. 如何动态弹出高度?意味着弹出的高度不是固定的,在弹出之前就应该算好高度,这无疑需要提前渲染,然后计算内容高度,然后弹出。在这里我想到了一种简便的方法:
1
2
3
4
5
6
7
8
/// 在layoutsubviews中修改view的y值。
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
/// 在这里仅改变view的frame.origin.y,来达到自定义弹出高度。
CGRect newFrame = self.view.frame;
newFrame.origin.y = self.view.bounds.size.height - self.markedView.frame.origin.y;
self.view.frame = newFrame;
}

UIAlertController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 简单弹出一个提示框
UIAlertController *alert = [UIAlertController alertControllerWithTitle:<#(nullable NSString *)#> message:<#(nullable NSString *)#> preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *leftAction = [UIAlertAction actionWithTitle:<#(nullable NSString *)#> style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
}];
UIAlertAction *rightAction = [UIAlertAction actionWithTitle:<#(nullable NSString *)#> style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
}];

[leftAction setValue:[UIColor blackColor] forKey:@"titleTextColor"];
[rightAction setValue:[UIColor redColor] forKey:@"titleTextColor"];

[alert addAction:leftAction];
[alert addAction:rightAction];

[controller presentViewController:alert animated:YES completion:^{
}];

UIImagePickerController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_imagePickerController = [[UIImagePickerController alloc] init];
_imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
/// 是否可编辑
_imagePickerController.allowsEditing = YES;
_imagePickerController.delegate = self;

/// UIImagePickerControllerDelegate

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary<NSString *,id> *)editingInfo {
[picker dismissViewControllerAnimated:YES completion:nil];
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[picker dismissViewControllerAnimated:YES completion:nil];
}

Summary

if、else Vs Inherit

开发中遇到的很多相似的界面,很多开发者都会用复用,并用if、else来区分当前界面处理的是那种情况,在开发前期基本没有问题。但是一旦别人接收你的代码,再改需求的时候就会出现非常混乱的代码。所以这种情况建议分开,相同的逻辑可以放在基类中实现(业务代码除外,这里的通用处理指的是下拉刷新逻辑、谈提示的样式、Loading的弹出等和业务无关的代码)。