iOS 刘海屏适配

今年苹果发布了三款 iPhone,分别为 iPhone XS、iPhone XS Max 以及 iPhone XR,尽管大家都在吐槽刘海屏,但是苹果今年已经是 iPhone 系列标配刘海屏了。

虽然苹果为我们带来了更大屏幕尺寸的 iPhone XS Max 和 iPhone XR,尽管一个是6.5英寸一个是6.1英寸,并且 iPhone XS Max 的分辨率更是达到了 1242px × 2688px,但是对于我们开发者来说,这两者的物理尺寸都一个样,都是 414×896pt,而 6/7/8 Plus 系列的宽度也是 414pt,所以你可以把他们看作是 plus 系列的加长版。


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
/// 是否为异形屏(刘海屏)
BOOL isShapedScreen(void)
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
if (@available(iOS 11.0, *)) {
static BOOL result = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CGFloat width = MIN(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height);
CGFloat height = MAX(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height);
if (@available(iOS 12.0, *)) {
if (width == 414.0 && height == 896.0) {
result = YES; // iPhone XS Max / iPhone XR
}
}
if (width == 375.0 && height == 812.0) {
result = YES; // iPhone X / iPhone XS
}
});
return result;
}
}
return NO;
}
/// 是否为iPhone X / iPhone XS
BOOL iPhoneX(void)
{
if (@available(iOS 11.0, *)) {
static BOOL result = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (isShapedScreen()) {
CGFloat height = MAX(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height);
if (height == 812.0) {
result = YES;
}
}
});
return result;
}
return NO;
}
/// 是否为iPhone XS Max
BOOL iPhoneXSMax(void)
{
if (@available(iOS 12.0, *)) {
static BOOL result = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (isShapedScreen()) {
CGFloat height = MAX(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height);
if (height == 896.0 && UIScreen.mainScreen.scale == 3.0) {
result = YES;
}
}
});
return result;
}
return NO;
}
/// 是否为iPhone XR
BOOL iPhoneXR(void)
{
if (@available(iOS 12.0, *)) {
static BOOL result = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (isShapedScreen()) {
CGFloat height = MAX(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height);
if (height == 896.0 && UIScreen.mainScreen.scale == 2.0) {
result = YES;
}
}
});
return result;
}
return NO;
}

附上 Swift 代码

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
/// 是否为异形屏(刘海屏)
let isShapedScreen : Bool = {
if UI_USER_INTERFACE_IDIOM() == .phone {
if #available(iOS 11.0, *) {
let width = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
let height = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
if #available(iOS 12.0, *) {
if width == 414.0 && height == 896.0 {
return true // iPhone XS Max / iPhone XR
}
}
if (width == 375.0 && height == 812.0) {
return true // iPhone X / iPhone XS
}
}
}
return false
}()
/// 是否为iPhone X / iPhone XS
let iPhoneX : Bool = {
if #available(iOS 11.0, *) {
if isShapedScreen {
let height = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
return (height == 812.0)
}
}
return false
}()
/// 是否为iPhone XS Max
let iPhoneXSMax : Bool = {
if #available(iOS 12.0, *) {
if isShapedScreen {
let height = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
let scale = UIScreen.main.scale
if height == 896.0 && scale == 3.0 {
return true
}
}
}
return false
}()
/// 是否为iPhone XR
let iPhoneXR : Bool = {
if #available(iOS 12.0, *) {
if isShapedScreen {
let height = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
let scale = UIScreen.main.scale
if height == 896.0 && scale == 2.0 {
return true
}
}
}
return false
}()

设备 分辨率 图片资源
12.9” iPad Pro 2048px × 2732px @2x
10.5” iPad Pro 1668px × 2224px @2x
9.7” iPad 1536px × 2048px @2x
7.9” iPad mini 4 1536px × 2048px @2x
iPhone XS Max 1242px × 2688px @3x
iPhone XS 1125px × 2436px @3x
iPhone XR 828px × 1792px @2x
iPhone X 1125px × 2436px @3x
iPhone 8 Plus 1242px × 2208px @3x
iPhone 8 750px × 1334px @2x
iPhone 7 Plus 1242px × 2208px @3x
iPhone 7 750px × 1334px @2x
iPhone 6s Plus 1242px × 2208px @3x
iPhone 6s 750px × 1334px @2x
iPhone SE 640px × 1136px @2x

参考:

UIButton 被禁用时显示一个菊花提示用户正在加载中

当点击按钮后,可能会执行一个比较耗时的操作,但是又不方便在整个页面进行提示时,此时如果我们仅仅只是把按钮禁用了以防用户重复点击,但是用户并不知道操作是否还在继续,用户体验不够友好;但是如果我们在按钮被禁用时,自动显示一个转圈圈的小菊花进行提示,那用户体验就会好很多,用户一看就知道当前操作正在进行中。

演示

附上我项目中的一个使用场景:重新定位的按钮,点击后重新定位用户的位置,但是不影响页面的其他操作。

gif

要求

  • iOS 9.0+ (使用了 NSLayoutAnchor, 如需要兼容低版本 iOS, 请自行改用 NSLayoutConstraint)

源码

新建一个 UIButton 的分类

  • .h 头文件
1
2
/// 当按钮被禁用时, 是否显示正在加载的状态提示, 默认`NO`
@property (nonatomic, assign, getter=isShowLoadingWhenDisabled) IBInspectable BOOL showLoadingWhenDisabled;
  • .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
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(setEnabled:);
SEL swizllingSelector = @selector(xp_setEnabled:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizllingMethod = class_getInstanceMethod(self, swizllingSelector);
BOOL flag = class_addMethod(self, originalSelector, method_getImplementation(swizllingMethod), method_getTypeEncoding(swizllingMethod));
if (flag) {
class_replaceMethod(self, swizllingSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizllingMethod);
}
});
}
static char const XPButtonLoadingIndicatorKey = '\0';
/// 重写 `setEnabled:` 方法, 当按钮被禁用时显示一个菊花提示
- (void)xp_setEnabled:(BOOL)enabled {
if (self.isShowLoadingWhenDisabled) {
if (enabled) {
UIActivityIndicatorView *indicatorView = objc_getAssociatedObject(self, &XPButtonLoadingIndicatorKey);
[indicatorView stopAnimating];
} else {
// 显示菊花
UIActivityIndicatorView *indicatorView = objc_getAssociatedObject(self, &XPButtonLoadingIndicatorKey);
if (nil == indicatorView) {
indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[indicatorView setTranslatesAutoresizingMaskIntoConstraints:NO];
[indicatorView setHidesWhenStopped:YES];
[self addSubview:indicatorView];
[indicatorView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor].active = YES;
[indicatorView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = YES;
objc_setAssociatedObject(self, &XPButtonLoadingIndicatorKey, indicatorView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 清除禁用状态下的图片/文字(设置`hidden`属性无效)
CGRect rect = (CGRect){CGPointZero, CGSizeMake(1.0, 1.0)};
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, UIColor.clearColor.CGColor);
CGContextFillRect(context, rect);
UIImage *clearImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self setImage:clearImage forState:UIControlStateDisabled];
[self setBackgroundImage:clearImage forState:UIControlStateDisabled];
[self setTitle:NSString.new forState:UIControlStateDisabled];
}
[indicatorView startAnimating];
}
}
[self xp_setEnabled:enabled];
}
- (void)setShowLoadingWhenDisabled:(BOOL)showLoadingWhenDisabled {
objc_setAssociatedObject(self, @selector(isShowLoadingWhenDisabled), @(showLoadingWhenDisabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (showLoadingWhenDisabled && self.state == UIControlStateDisabled) {
[self setEnabled:self.isEnabled];
}
}
- (BOOL)isShowLoadingWhenDisabled {
return [objc_getAssociatedObject(self, _cmd) boolValue];
}

用法

1、 设置按钮的 showLoadingWhenDisabled 属性为 YES

2、 设置按钮的启用/禁用 setEnabled:

注意

  • 不建议将按钮默认为禁用状态

解决 UIButton 快速点击后重复提交数据

有时候我们快速点击按钮多次,会触发多次点击事件,最终导致数据提交多次,这是很没必要的。

说明

  • 默认开启防重复点击功能,默认间隔时间 0.5
  • 一旦开启该功能,则无法同时触发多次 UIControlEvents 的事件,比如按钮同时绑定了 UIControlEventTouchUpInsideUIControlEventTouchDownRepeat 事件,但是只会触发 UIControlEventTouchUpInside 的事件;但是并不影响长按手势以及通过 UITapGestureRecognizer 实现的双击手势

总之,如果你的按钮有特殊的需求,可以通过将 enabledRejectRepeatTap 设置为 NO 来禁用该功能。

代码

  • UIButton+RejectRepeatTapGesture.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
#import <UIKit/UIKit.h>
@interface UIButton (RejectRepeatTapGesture)
/// 重复点击的时间间隔, 默认`0.5s`
@property (nonatomic, assign) IBInspectable double repeatTimeInterval;
/// 是否启用防重复功能, 默认`YES`
@property (nonatomic, assign, getter=isEnabledRejectRepeatTap) IBInspectable BOOL enabledRejectRepeatTap;
@end
/// 回调, 按钮的`target`对象遵守该协议即可接收回调事件
@protocol UIButtonRejectRepeatTapGestureCallback <NSObject>
/**
当重复点击时的回调方法
@param button 被点击的按钮
@param timeInterval 两次点击之间的时间间隔
*/
- (void)rejectRepeatTapGestureFromButton:(UIButton *)button timeInterval:(NSTimeInterval)timeInterval;
@end
  • UIButton+RejectRepeatTapGesture.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
#import "UIButton+RejectRepeatTapGesture.h"
#import <objc/message.h>
static char const kLasttimeTapTimeKey = '\0';
@implementation UIButton (RejectRepeatTapGesture)
#pragma mark - Lifecycle
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizllingSelector = @selector(rrtg_sendAction:to:forEvent:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizllingMethod = class_getInstanceMethod(self, swizllingSelector);
BOOL flag = class_addMethod(self, originalSelector, method_getImplementation(swizllingMethod), method_getTypeEncoding(swizllingMethod));
if (flag) {
class_replaceMethod(self, swizllingSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizllingMethod);
}
});
}
- (void)rrtg_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if (self.isEnabledRejectRepeatTap) {
if (@available(iOS 11.0, *)) {
// Fix: UITableViewCell left-slide the delete button click is invalid on iOS 11.0 or later
if (self.class == NSClassFromString(@"UISwipeActionStandardButton")) {
return [self rrtg_sendAction:action to:target forEvent:event];
}
}
NSTimeInterval lasttimeTapTime = [objc_getAssociatedObject(self, &kLasttimeTapTimeKey) doubleValue];
if (lasttimeTapTime > 0.0) {
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval interval = currentTime - lasttimeTapTime;
if (interval <= self.repeatTimeInterval) {
if ([target conformsToProtocol:@protocol(UIButtonRejectRepeatTapGestureCallback)]) {
id<UIButtonRejectRepeatTapGestureCallback> delegate = (id<UIButtonRejectRepeatTapGestureCallback>)target;
[delegate rejectRepeatTapGestureFromButton:self timeInterval:interval];
}
return;
}
}
objc_setAssociatedObject(self,
&kLasttimeTapTimeKey,
@(NSDate.date.timeIntervalSince1970),
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[self rrtg_sendAction:action to:target forEvent:event];
}
#pragma mark - setter & getter
- (void)setRepeatTimeInterval:(NSTimeInterval)repeatTimeInterval {
objc_setAssociatedObject(self,
@selector(repeatTimeInterval),
@(repeatTimeInterval),
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSTimeInterval)repeatTimeInterval {
NSTimeInterval timeInterval = [objc_getAssociatedObject(self, _cmd) doubleValue];
if (timeInterval >= 0.01) {
return timeInterval;
}
return 0.5; // default.
}
- (void)setEnabledRejectRepeatTap:(BOOL)enabledRejectRepeatTap {
objc_setAssociatedObject(self,
@selector(isEnabledRejectRepeatTap),
@(enabledRejectRepeatTap),
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isEnabledRejectRepeatTap {
id obj = objc_getAssociatedObject(self, _cmd);
if (obj) {
return [obj boolValue];
}
return YES; // default.
}
@end

iOS 导航栏颜色渐变

效果图

PNG

代码

  • 渐变的导航栏
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
@interface XPGradientNavigationBar : UINavigationBar
{
CAGradientLayer *_gradientLayer;
}
@end
@implementation XPGradientNavigationBar
#pragma mark - Lifecycle
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self commonInit];
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self commonInit];
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!_gradientLayer.superlayer) {
UIView *barBackgroundView = self.subviews.firstObject;
if (@available(iOS 10.0, *)) {
UIView *backgroundEffectView = [barBackgroundView valueForKey:@"_backgroundEffectView"];
UIView *gradientView = backgroundEffectView.subviews.lastObject;
[gradientView.layer addSublayer:_gradientLayer];
} else {
for (UIView *subview in barBackgroundView.subviews) {
if ([subview isKindOfClass:NSClassFromString(@"_UIBackdropView")]) {
[subview.layer addSublayer:_gradientLayer];
break;
}
}
}
}
if (_gradientLayer.superlayer) {
_gradientLayer.frame = self.subviews.firstObject.bounds;
}
}
#pragma mark - Private
- (void)commonInit {
self.translucent = YES;
_gradientLayer = [CAGradientLayer layer];
_gradientLayer.colors = @[
(__bridge id)UIColor.purpleColor.CGColor,
(__bridge id)UIColor.orangeColor.CGColor
];
_gradientLayer.locations = @[@0.0, @1.0];
_gradientLayer.startPoint = CGPointMake(0.0, 0.5);
_gradientLayer.endPoint = CGPointMake(1.0, 0.5);
}
#pragma mark - setter & getter
- (void)setTranslucent:(BOOL)translucent {
[super setTranslucent:YES];
}
@end
  • 配合 UINavigationController 使用效果更佳 (非必须)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@implementation XPGradientNavigationController
- (instancetype)initWithRootViewController:(UIViewController *)rootViewController {
self = [self initWithNavigationBarClass:nil toolbarClass:nil];
if (self) {
if (rootViewController) {
[super pushViewController:rootViewController animated:NO];
}
}
return self;
}
- (instancetype)initWithNavigationBarClass:(Class)navigationBarClass toolbarClass:(Class)toolbarClass {
return [super initWithNavigationBarClass:XPGradientNavigationBar.class toolbarClass:toolbarClass];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
NSAssert([self.navigationBar isKindOfClass:XPGradientNavigationBar.class], @"self.navigationBar must be XPGradientNavigationBar");
}
return self;
}
@end

如果你是通过可视化方式创建的导航栏控制器,则需要在 Storyboard/xib 中手动将 Navigation Bar 的 Class 指定为 XPGradientNavigationBar,如果是通过代码创建 UINavigationController,则使用 XPGradientNavigationController 即可。

UITableViewHeaderFooterView 修改背景色/文字

注意点:

  • 修改背景色只能通过修改 contentViewbackgroundView 的背景色实现,修改 UITableViewHeaderFooterView 自身背景色无效果
  • 只能在 tableView:willDisplayHeaderView:forSection: 代理方法中进行设置,在 tableView:didEndDisplayingHeaderView:forSection: 设置无效
1
2
3
4
5
6
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section {
UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
headerView.contentView.backgroundColor = [UIColor brownColor];
headerView.textLabel.font = [UIFont systemFontOfSize:14.0];
headerView.textLabel.textColor = [UIColor orangeColor];
}

iOS JSON解析换行符(\r、\n、\r\n)

项目需要对JSON返回的换行符进行解析,系统提供了解析JSON的方法[NSJSONSerialization JSONObjectWithData:options:error:]
AFNetworking 内部也是采用该方法来解析服务器返回的内容。所以,我只需要 hook 该系统方法,当解析失败的时候进行处理就行了,这样就可以一劳永逸,也无需修改其他地方的代码。

服务器采用的是 Windows + C# 组合,通过后台添加的数据回车符为:\r\n,通过 Android 应用提交的回车符则是:\n,而 iOS 平台则是:\r,我统一将回车符替换成 \r ,进行转义后再次尝试解析JSON。

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
#import <objc/message.h>
@interface NSJSONSerialization (SpecialCharacters)
@end
@implementation NSJSONSerialization (SpecialCharacters)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getClassMethod(self, @selector(JSONObjectWithData:options:error:));
Method swizlledMethod = class_getClassMethod(self, @selector(sc_JSONObjectWithData:options:error:));
method_exchangeImplementations(originalMethod, swizlledMethod);
});
}
+ (id)sc_JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError * _Nullable __autoreleasing *)error {
if (!data.length) return nil;
NSError *serializationError = nil;
id responseObject = [self sc_JSONObjectWithData:data options:opt error:&serializationError];
if (!responseObject) {
if (error) {
*error = serializationError;
}
if (serializationError && serializationError.code == 3840) {
NSString *serializationString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (!serializationString) {
return nil;
}
serializationString = [serializationString stringByReplacingOccurrencesOfString:@"(\\r\\n|\\r|\\n)" withString:@"\\\\r" options:NSRegularExpressionSearch range:NSMakeRange(0, serializationString.length)];
NSData *serializationData = [serializationString dataUsingEncoding:NSUTF8StringEncoding];
responseObject = [self sc_JSONObjectWithData:serializationData options:opt error:nil];
#ifndef DEBUG
if (responseObject && error) {
*error = nil;
}
#endif
}
}
return responseObject;
}
@end

为 NSArray/NSDictionary 优雅地过滤 nil 值

作为一名 iOS 开发者,肯定知道 NSArray/NSDictionary 不能存储 nil 值,如果你试图往数组/字典中存储 nil,那么 App 也将毫不客气的为你闪退。

尽管在日常的编码中,我们都会小心翼翼的处理 nil,但是总会有纰漏,毕竟大部分数据都是从服务器下发的,我们很难彻底把控。作为一名码农,肯定是想着怎么偷懒的,既能自动规避 nil,又能够不影响现有代码,最好不用引入第三方方法。得益于 Objective-C 的 runtime 机制,我们可以很优雅地通过 Method Swizlling 来解决上述问题。

但是我们需要注意一点的是,我们不能直接对 NSArray/NSMutableArray、NSDictionary/NSMutableDictionary 这些类进行 method swizlling 操作,因为它们底层是通过 Class cluster 来实现的,我们需要对隐藏在它们背后的真实的类进行 method swizlling,否则没任何效果。

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
#import <Foundation/Foundation.h>
#import <objc/message.h>
void safe_swizzle_method(Class originalClass, Class swizzledClass, SEL originalSelector, SEL swizzledSelector)
{
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);
class_replaceMethod(originalClass, swizzledSelector, originalIMP, originalType);
class_replaceMethod(originalClass, originalSelector, swizzledIMP, swizzledType);
}
#pragma mark - Array
@interface XPSafeArray : NSObject
@end
@implementation XPSafeArray
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray<NSString *> *array1 = @[
NSStringFromSelector(@selector(objectAtIndex:)),
NSStringFromSelector(@selector(objectAtIndexedSubscript:))
];
for (NSString *str in array1) {
safe_swizzle_method(NSClassFromString(@"__NSArrayI"), self,
NSSelectorFromString(str),
NSSelectorFromString([@"safe_" stringByAppendingString:str]));
}
NSArray<NSString *> *array2 = @[
NSStringFromSelector(@selector(objectAtIndex:)),
NSStringFromSelector(@selector(objectAtIndexedSubscript:)),
NSStringFromSelector(@selector(insertObject:atIndex:)),
NSStringFromSelector(@selector(setObject:atIndexedSubscript:)),
NSStringFromSelector(@selector(insertObjects:atIndexes:))
];
for (NSString *str in array2) {
safe_swizzle_method(NSClassFromString(@"__NSArrayM"), self,
NSSelectorFromString(str),
NSSelectorFromString([@"safe_" stringByAppendingString:str]));
}
});
}
- (id)safe_objectAtIndex:(NSUInteger)index {
NSUInteger count = [(NSArray*)self count];
if (count == 0 || index >= count) {
return nil;
}
return [self safe_objectAtIndex:index];
}
- (id)safe_objectAtIndexedSubscript:(NSUInteger)index {
NSUInteger count = [(NSArray*)self count];
if (count == 0 || index >= count) {
return nil;
}
return [self safe_objectAtIndexedSubscript:index];
}
- (void)safe_insertObject:(id)anObject atIndex:(NSUInteger)index {
if (anObject == nil) return;
[self safe_insertObject:anObject atIndex:index];
}
- (void)safe_setObject:(id)obj atIndexedSubscript:(NSUInteger)idx {
if (obj == nil) return;
[self safe_setObject:obj atIndexedSubscript:idx];
}
- (void)safe_insertObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexes {
if (objects && objects.count == indexes.count) {
[self safe_insertObjects:objects atIndexes:indexes];
}
}
@end
#pragma mark - Dictionary
@interface XPSafeDictionary : NSDictionary
@end
@implementation XPSafeDictionary
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
safe_swizzle_method(NSClassFromString(@"__NSPlaceholderDictionary"),
self,
@selector(initWithObjects:forKeys:count:),
@selector(safe_initWithObjects:forKeys:count:));
safe_swizzle_method(NSClassFromString(@"__NSDictionaryM"),
self,
@selector(setObject:forKey:),
@selector(safe_setObject:forKey:));
safe_swizzle_method(NSClassFromString(@"__NSDictionaryM"),
self,
@selector(removeObjectForKey:),
@selector(safe_removeObjectForKey:));
});
}
- (id)safe_initWithObjects:(id _Nonnull const [])objects forKeys:(id<NSCopying> _Nonnull const [])keys count:(NSUInteger)cnt {
id safeObjects[cnt];
id safeKeys[cnt];
NSUInteger count = 0;
for (NSUInteger idx=0; idx<cnt; idx++) {
id key = keys[idx];
id obj = objects[idx];
if (!key) {
continue;
}
if (!obj) {
obj = [NSNull null]; // 可根据你项目需求,将`NSString`作为默认值
}
safeKeys[count] = key;
safeObjects[count] = obj;
count++;
}
return [self safe_initWithObjects:safeObjects forKeys:safeKeys count:count];
}
- (void)safe_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
if (anObject && aKey) {
[self safe_setObject:anObject forKey:aKey];
}
}
- (void)safe_removeObjectForKey:(id)aKey {
if (aKey) {
[self safe_removeObjectForKey:aKey];
}
}
@end

以上只给出部分实现,但是已基本满足日常使用,你可以自己根据自己的需求进行完善。

iOS Haptics Feedback (触感反馈)

要求

  • iOS 10.0+
  • iPhone 7/7P 或更新机型
  • 打开”设置–声音与触感–系统触感反馈”选项

相关类说明

UIFeedbackGenerator:一个抽象类,不要子类化或创建此类的实例。我们应该使用它的三个子类 (UIImpactFeedbackGenerator, UISelectionFeedbackGenerator, UINotificationFeedbackGenerator)。

说明
UIImpactFeedbackGenerator 提供物理体验,补充动作或任务的视觉反馈。 例如,当视图滑入到位或两个对象发生碰撞时,用户可能会感到砰的一声。
UISelectionFeedbackGenerator 表示选择正在积极更改。 例如,用户在滚动拾取轮时感觉轻敲。
UINotificationFeedbackGenerator 表示任务或操作(例如存入支票或解锁车辆)已完成、失败或产生某种警告。

使用姿势

  1. 实例化Generator (UIImpactFeedbackGenerator, UISelectionFeedbackGenerator, UINotificationFeedbackGenerator)
  2. 准备工作,调用 -prepare 方法
  3. 触发触觉反馈,调用相应的触发方法(impactOccurred, selectionChanged, notificationOccurred:)
  4. 释放Generator

其中第2、4步可省略。

如有兴趣,可 点此 前往查看官方的示例代码。

代码示例

  • UIImpactFeedbackGenerator
    -

    1
    2
    UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
    [feedbackGenerator impactOccurred];

    可以在实例化 UIImpactFeedbackGenerator 时指定震动反馈的力度,有三个枚举值:

    • UIImpactFeedbackStyleLight 轻微震动
    • UIImpactFeedbackStyleMedium 中度震动
    • UIImpactFeedbackStyleHeavy 重度震动
  • UISelectionFeedbackGenerator
    -

    1
    2
    UISelectionFeedbackGenerator *feedbackGenerator = [[UISelectionFeedbackGenerator alloc] init];
    [feedbackGenerator selectionChanged];
  • UINotificationFeedbackGenerator
    -

    1
    2
    UINotificationFeedbackGenerator *feedbackGenerator = [[UINotificationFeedbackGenerator alloc] init];
    [feedbackGenerator notificationOccurred:UINotificationFeedbackTypeSuccess];

    UINotificationFeedbackTypeSuccess 和 UINotificationFeedbackTypeWarning 均是震动两次,两者差异不是很大,就我个人感觉而已,UINotificationFeedbackTypeSuccess 的第一下震动力度稍微轻点;UINotificationFeedbackTypeError 则会震动三下,但震动的力度(频率)和每次震动之间的时间间隔也不相同。

参考:

UISearchController使用示例(push到下一个页面)

GIF效果图

GIF

源码

  • FirstViewController,展示数据

    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
    @interface FirstViewController : UITableViewController<UISearchResultsUpdating>
    {
    UISearchController *searchController;
    NSArray<NSString *> *titles;
    }
    @end
    @implementation FirstViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"UISearchController";
    SearchResultViewController *resultVC = [[SearchResultViewController alloc] initWithStyle:UITableViewStylePlain];
    searchController = [[UISearchController alloc] initWithSearchResultsController:resultVC];
    searchController.searchResultsUpdater = self;
    searchController.searchBar.keyboardType = UIKeyboardTypeNumberPad;
    self.tableView.tableHeaderView = searchController.searchBar;
    self.definesPresentationContext = YES; // 这是push成功的关键
    NSMutableArray<NSString *> *temp = [NSMutableArray array];
    for (int i=0; i<40; i++) {
    NSString *title = [NSString stringWithFormat:@"%d-%u", i,arc4random()];
    [temp addObject:title];
    }
    titles = [NSArray arrayWithArray:temp];
    }
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return titles.count;
    }
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString * const identifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    }
    cell.textLabel.text = titles[indexPath.row];
    return cell;
    }
    - (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
    NSString *keyword = searchController.searchBar.text;
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[c] %@", keyword];
    NSArray<NSString *> *results = [titles filteredArrayUsingPredicate:predicate];
    SearchResultViewController *resultVC = (SearchResultViewController*)searchController.searchResultsController;
    resultVC.resultTitles = results;
    [resultVC.tableView reloadData];
    }
    @end
  • SearchResultViewController 源码,用于展示搜索结果内容

    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
    @interface SearchResultViewController : UITableViewController
    @property (nonatomic, strong) NSArray<NSString*> *resultTitles;
    @end
    @implementation SearchResultViewController
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.resultTitles.count;
    }
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString * const identifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    }
    cell.textLabel.text = self.resultTitles[indexPath.row];
    return cell;
    }
    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    // 点击搜索结果内容push到下一个页面
    UIViewController *vc = [[UIViewController alloc] init];
    vc.view.backgroundColor = [UIColor brownColor];
    [self.presentingViewController.navigationController pushViewController:vc animated:YES];
    }
    @end
  • AppDelegate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    FirstViewController *firstVC = [[FirstViewController alloc] initWithStyle:UITableViewStylePlain];
    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:firstVC];
    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    self.window.rootViewController = nav;
    [self.window makeKeyAndVisible];
    return YES;
    }

说明

1、definesPresentationContext 属性必须设置为YES,否则跳转不成功或者跳转后新页面处于搜索结果页面之下

1
self.definesPresentationContext = YES;

2、正确的 push 姿势,使用 self.presentingViewController.navigationController 进行跳转

1
[self.presentingViewController.navigationController pushViewController:vc animated:YES];

WKWebView在iOS9上崩溃

最近项目重构,所以采用了 WKWebView 来展示网页内容,但是发现在 iOS9 上程序会出现 crash,以前也用过 WKWebView 但是都没遇到这么坑的情况啊。

具体操作:通过 push 网页控制器,然后在 pop 之后,程序就崩了。

打断点也定位不到 crash 代码行,打开 Xcode 的僵尸模式之后,控制台也仅输出以下信息:

1
-[XPWebViewController retain]: message sent to deallocated instance 0x7fa1974a33f0

经过排查发现是因为 WKWebView 的 UIScrollView 代理惹的祸,因为我需要实时监听网页的滚动区域来处理一些事情,所以我把 WKWebView.scrollView.delegate 设置为当前控制器。

1
self.webView.scrollView.delegate = self;

既然知道了症结所在,那么只需要在控制器销毁的时候及时清掉代理就行了:

1
2
3
- (void)dealloc {
self.webView.scrollView.delegate = nil; // Fix iOS9 crash.
}