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.
}

iOS地铁图

使用了高德地图的JS API,文档戳这里

原理

通过 WKWebView 加载一个 HTML 字符串,通过注入 JS 在页面加载完毕后加载高德地图的JS库,然后创建地铁地图,同时通过JS库提供的 getCityList 接口获取所有的城市信息,然后把城市数据回传给 Objective-C,OC 在拿到数据后在右上角添加一个切换城市的按钮,当选择所要切换的城市后,通过 WKWebView 调用 JS 代码并把城市 id 传递给 JS,JS 拿到数据后通过 setAdcode 接口来切换地铁数据。

实际上这个 Demo 中可以不考虑 WKUIDelegateWKNavigationDelegate 这两个协议,但是为了熟悉 WKWebView,我还是遵守了这两个协议。

XPWKScriptMessageHandlerObject 这个类的作用只是为了解决 WKScriptMessageHandler 协议由于强引用代理对象从而导致内存泄漏的问题,起到一个中间代理人的作用。

GIF演示效果

GIF

代码

  • .h 头文件
1
2
3
NS_CLASS_AVAILABLE_IOS(9_0) @interface XPSubwayMapViewController : UIViewController
@end
  • .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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#import "XPSubwayMapViewController.h"
#import <WebKit/WebKit.h>
#pragma mark -
@interface XPWKScriptMessageHandlerObject : NSObject<WKScriptMessageHandler>
@property (nonatomic, weak) id<WKScriptMessageHandler> scriptMessageHandler;
@end
@implementation XPWKScriptMessageHandlerObject
- (void)dealloc {
#ifdef DEBUG
NSLog(@"%s", __FUNCTION__);
#endif
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
[self.scriptMessageHandler userContentController:userContentController didReceiveScriptMessage:message];
}
@end
#pragma mark -
@interface XPSubwayMapViewController ()<WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler>
{
UIActivityIndicatorView *indicatorView;
WKWebView *webViwe;
/// 城市列表数据(二维数组,内层数据中第一个元素为id号,第二个元素为城市名称)
NSArray<NSArray<NSString*> *> *cities;
XPWKScriptMessageHandlerObject *scriptMessageHandlerObject;
}
@end
@implementation XPSubwayMapViewController
#pragma mark Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"北京";
scriptMessageHandlerObject = [[XPWKScriptMessageHandlerObject alloc] init];
scriptMessageHandlerObject.scriptMessageHandler = self;
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
[config.userContentController addUserScript:[self subwayJavaScript]];
[config.userContentController addScriptMessageHandler:scriptMessageHandlerObject name:@"showToggleCityButton"];
webViwe = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
webViwe.translatesAutoresizingMaskIntoConstraints = NO;
webViwe.UIDelegate = self;
webViwe.navigationDelegate = self;
[self.view addSubview:webViwe];
[webViwe.topAnchor constraintEqualToAnchor:self.view.topAnchor].active = YES;
[webViwe.rightAnchor constraintEqualToAnchor:self.view.rightAnchor].active = YES;
[webViwe.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES;
[webViwe.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor].active = YES;
indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
indicatorView.hidesWhenStopped = YES;
indicatorView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:indicatorView];
[indicatorView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES;
[indicatorView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES;
[webViwe loadHTMLString:[self html] baseURL:nil];
}
- (void)dealloc {
[webViwe.configuration.userContentController removeScriptMessageHandlerForName:@"showToggleCityButton"];
#ifdef DEBUG
NSLog(@"%s", __FUNCTION__);
#endif
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark <WKNavigationDelegate>
/// 在发送请求之前调用,可以决定是否加载网页
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
decisionHandler(WKNavigationActionPolicyAllow);
}
/// 开始加载网页
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
[indicatorView startAnimating];
}
/// 是否接收服务器返回的数据
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
decisionHandler(WKNavigationResponsePolicyAllow);
}
/// 服务器返回内容时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
}
/// 页面加载完毕
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[indicatorView stopAnimating];
}
/// 页面加载失败
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
}
/// HTTPS时触发该回调(可以在此验证HTTPS证书)
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
#pragma mark <WKUIDelegate>
/// 处理JavaScript原生的alert函数
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}];
[alertController addAction:action];
[self presentViewController:alertController animated:YES completion:nil];
}
/// 处理JavaScript原生的confirm函数
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler(NO);
}];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler(YES);
}];
[alertController addAction:cancelAction];
[alertController addAction:okAction];
[self presentViewController:alertController animated:YES completion:nil];
}
/// 处理JavaScript原生的prompt函数
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
[textField setText:defaultText];
[textField setClearButtonMode:UITextFieldViewModeWhileEditing];
[textField setReturnKeyType:UIReturnKeyDone];
//[textField setDelegate:self];
}];
[alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler(nil);
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSString *text = [alertController.textFields firstObject].text;
completionHandler(text);
}]];
[self presentViewController:alertController animated:YES completion:nil];
}
#pragma mark <WKScriptMessageHandler>
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"showToggleCityButton"]) {
NSArray *cities = message.body;
if ([cities isKindOfClass:[NSArray class]] && cities.count) {
UIBarButtonItem *button = [[UIBarButtonItem alloc] initWithTitle:@"切换城市" style:UIBarButtonItemStylePlain target:self action:@selector(toggleCityButtonAction:)];
self.navigationItem.rightBarButtonItem = button;
self->cities = cities;
}
}
}
#pragma mark Actions
- (void)toggleCityButtonAction:(UIBarButtonItem *)sender {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
__weak __typeof(self) weakSelf = self;
for (NSArray<NSString*> *array in cities) {
NSString *cityId = array.firstObject;
NSString *cityName = array.lastObject;
[alert addAction:[UIAlertAction actionWithTitle:cityName style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
NSString *js = [NSString stringWithFormat:@"toggleCity(%@)", cityId];
[strongSelf->webViwe evaluateJavaScript:js completionHandler:^(id _Nullable resp, NSError * _Nullable error) {
#ifdef DEBUG
NSLog(@"Toggle city fail: %@", error);
#endif
}];
strongSelf.title = cityName;
}]];
}
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark Private
- (NSString *)html {
return @"<html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,shrink-to-fit=no\"/></head><body><div id=\"SubwayContainer\"></div></body></html>";
}
- (WKUserScript *)subwayJavaScript {
NSString *source = @"var script = document.createElement('script');script.setAttribute('src', 'https://webapi.amap.com/subway?v=1.0&key=98cb8314168440c4de1169ffac3a53e7&callback=cbk');document.body.appendChild(script);var mysubway = null;window.cbk = function(){mysubway = subway('SubwayContainer', {easy: 1});mysubway.getCityList(function(resp) {var maps = [];for (var id in resp) {var city = resp[id]['name'];maps.push([id, city]);}window.webkit.messageHandlers.showToggleCityButton.postMessage(maps);});};function toggleCity(id) {mysubway.setAdcode(id);};";
return [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
}
@end

十六进制字符串转UIColor

支持十六进制字符串的格式有:

  • #FFF
  • #FFFF
  • #FFFFFF
  • #FFFFFFFF
  • 0xFFF
  • 0xFFFF
  • 0xFFFFFF
  • 0xFFFFFFFF
  • 可省略#0x符号
  • 不区分大小写
1
2
3
4
5
@interface UIColor (XP)
+ (instancetype)hexColor:(NSString *)hex;
@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
@implementation UIColor (XP)
+ (instancetype)hexColor:(NSString *)hex {
NSString *str = hex;
if ([hex hasPrefix:@"#"]) {
str = [hex substringFromIndex:1];
} else if ([hex.lowercaseString hasPrefix:@"0x"]) {
str = [hex substringFromIndex:2];
}
NSScanner *scanner = [[NSScanner alloc] initWithString:str];
unsigned int hexValue;
if ([scanner scanHexInt:&hexValue]) {
CGFloat r, g, b, a = 1.0;
switch (str.length) {
case 3:
r = ((hexValue & 0xF00) >> 8) / 15.0;
g = ((hexValue & 0x0F0) >> 4) / 15.0;
b = (hexValue & 0x00F) / 15.0;
break;
case 4:
r = ((hexValue & 0xF000) >> 12) / 15.0;
g = ((hexValue & 0x0F00) >> 8) / 15.0;
b = ((hexValue & 0x00F0) >> 4) / 15.0;
a = (hexValue & 0x000F) / 15.0;
break;
case 6:
r = ((hexValue & 0xFF0000) >> 16) / 255.0;
g = ((hexValue & 0x00FF00) >> 8) / 255.0;
b = (hexValue & 0x0000FF) / 255.0;
break;
case 8:
r = ((hexValue & 0xFF000000) >> 24) / 255.0;
g = ((hexValue & 0x00FF0000) >> 16) / 255.0;
b = ((hexValue & 0x0000FF00) >> 8) / 255.0;
a = (hexValue & 0x000000FF) / 255.0;
break;
default:
return nil;
}
if (@available(iOS 10.0, *)) {
return [UIColor colorWithDisplayP3Red:r green:g blue:b alpha:a];
}
return [UIColor colorWithRed:r green:g blue:b alpha:a];
}
return nil;
}
@end

修改DZNEmptyDataSet按钮的宽高

相信没有哪款App是没有使用到滚动视图(UIScrollView、UITableView、UICollectionView)的吧,我相信大家用的最多的就是UITableView了,当需要展示列表数据时,我想大家第一次时间想到的就是UITableView。

如果数据不为空的时候还好,可谁也不能保证一定会有数据展示吧,如果没数据的时候,就会给客户呈现一个空白页面(白板),此时我们可以给用户一点提示信息(图片、文字),这不仅可以提升用户体验,也不至于让用户一脸懵逼。

DZNEmptyDataSet 就是为此而生的,只需设置好数据源和代理即可,然后根据你的需求返回对应的图片或者提示文字。

但是 DZNEmptyDataSet 提供的按钮默认不能调整宽高,如果你实现了该数据源方法 - (UIImage *)buttonBackgroundImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state 并返回对应按钮背景图片后,DZNEmptyDataSet 会将按钮高度自动调整为图片的高度;即便如此,还是无法调整宽度。

由于 DZNEmptyDataSet 并没有提供相应的接口给我们设置按钮的宽高,那我们只能通过曲线救国的方式来达到目的了。

遵守 DZNEmptyDataSetDelegate 协议,实现 - (void)emptyDataSetDidAppear:(UIScrollView *)scrollView 方法,然后修改按钮的约束。

不要在 - (void)emptyDataSetWillAppear:(UIScrollView *)scrollView 这个方法里设置。宽度只能通过调整 leadingtrailing 的约束来达到效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)emptyDataSetDidAppear:(UIScrollView *)scrollView {
UIButton *button = [scrollView valueForKeyPath:@"emptyDataSetView.button"];
if ([button isKindOfClass:[UIButton class]]) {
// Change button width
for (NSLayoutConstraint *constraint in button.superview.constraints) {
if (constraint.firstItem == button && constraint.firstAttribute == NSLayoutAttributeLeading) {
constraint.constant = 130.0;
} else if (constraint.secondItem == button && constraint.secondAttribute == NSLayoutAttributeTrailing) {
constraint.constant = 130.0;
}
}
// Change button height
for (NSLayoutConstraint *constraint in button.constraints) {
if (constraint.firstItem == button && constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.constant = 40.0;
}
}
}
}

使用 unwind segues 关闭当前页面

我们经常会遇到这样一种需求,在控制器上有一个关闭按钮,点击来关闭当前页面。而我们通常的做法都是在 Storyboard 上拖一个 UIButton 出来,然后将 UIButton 拖线到控制器上,新建一个 IBAction 方法,并在该方法中通过代码来关闭当前控制器。

其实,我们可以不需要这么麻烦,可以通过 Unwind Segue 在 Storyboard 上拖拖线即可达到关闭控制器的效果。

创建一个 Unwind Segue

unwind是一个实例方法,以UIStoryboardSegue作为其唯一的方法参数,返回类型为IBAction,它对方法名称不作要求,也不需要在头文件中进行声明。

需要注意的是,我们需要将 uniwnd action 定义在 UIStoryboardSegue 的 sourceViewController 上,而非当前控制器。例如 A push B 然后从 B 返回 A 这个场景,我们需要将 unwind action 定义在 A 上。

但是我们可以通过扩展 UIViewController 以便所有控制器均可以使用,从而不用关心具体写在哪个控制器上。

1
2
3
- (IBAction)unwindSegue:(UIStoryboardSegue*)sender {
// Pull any data from the view controller which initiated the unwind segue.
}
1
2
3
4
5
6
7
extension UIViewController {
@IBAction func unwindSegue(_ sender: UIStoryboardSegue) {
// Pull any data from the view controller which initiated the unwind segue.
}
}

将 Unwind Segue 添加到故事板

  • 方法1

a. 选中退出图标,右击弹出 Segues 列表

b. 将 unwindSegue: 拖线到控制器图标上

c. 设置一个 identifier

经过这样的拖线绑定之后,我们就可以在代码中通过发送一个 performSegue(withIdentifier:sender:) 消息来关闭当前控制器。

  • 方法2

a. 选中按钮,然后按住 control 拖线到退出图标上

b. 在弹出的 Action Segues 中选择 unwindSegue:

这样,当我们点击按钮的时候,就会自动关闭当前控制器了。

Technical Note TN2298 Using Unwind Segues

在Markdown中创建表格

越来越多人采用Markdown进行写作,而表格作为一种比较常见的需求,Markdown也支持快速创建表格。

语法:

1
2
3
4
| Name | Score |
| :- | :-: |
| zhangsan | 90 |
| lisi | 100 |

效果:

Name Score
zhangsan 90
lisi 100

语法说明:

  • 第一行为表头,第二行为分割表头和表格内容,第三行开始每一行表示一个表格行
  • 列与列之间用|进行分割
  • 第二行中可以指定内容对其模式
    • - 默认对其
    • :- 左对齐
    • -: 右对齐
    • :-: 居中对齐

博客搬迁至阿里云

前因

清明假期前,收到阿里云发来的邮件,说我的域名未指向阿里云国内节点(不含香港)服务器,根据工信部相关法规规定属于空壳网站,需要我尽快处理,否则将删除网站备案接入信息,甚至网站备案号有可能会被注销。

email

我的域名之前确实是指向了国外服务器,我在 GitHub 通过 pages 服务搭建了一个 hexo 博客,并通过 CNAME 解析将域名绑定到了 GitHub pages 上

收到邮件当天,我就将 hexo 博客从 GitHub 迁移到了码云上,后来发现码云无法绑定域名,遂将博客继续搬迁到了Coding上,然后绑定了我的域名并修改了域名解析,将域名指向了 Coding Pages;至此,我心想Coding.net毕竟是国内公司,我把博客托管至它提供的服务器上,应该没事了吧。

后果

不曾想,几个工作日后,在4月9日收到工信部备案系统发来的短信,说我的网站备案号粤ICP备15020585已被收回,网站信息被注销了,域名也访问不了了。

容我悲伤一会 :(

痛定思痛

发现将博客托管在第三方提供的服务器上已无望,没办法,只好花点软妹币购买阿里云的服务器了。在经过两天的思量后(其实还不是因为穷),花了RMB199软妹币购买了阿里云的弹性Web托管服务。

接下来就是重新备案了,这个没什么好说的,先进入弹性Web托管控制台,然后去申请一个备案服务号,然后拿着这个备案服务号去申请备案,填写网站基本信息,上传资料,提交审核。一周左右,我的备案就通过了。

重新上线我的博客

服务器买好了,备案也审核通过了,下面就开始将我的博客重新上线了。

  • 上传博客内容文件

hexo 博客生成的静态页面,全部存储在 public 目录下,我们只需将该目录下的文件上传到服务器的 htdocs 目录下即可。除了用FTP软件上传之外,还可以通过 hexo 提供的一键部署功能,将网站部署到服务器上。

  • 绑定域名

进入弹性Web托管控制台,分别绑定 www.0daybug.com0daybug.com

进入域名解析控制台,添加如下两条域名解析,其中记录值为弹性Web托管中的测试域名

记录类型 主机记录 解析线路(isp) 记录值
CNAME @ 默认 ew-*.aliapp.com
CNAME www 默认 ew-*.aliapp.com

至此,我的博客又重新上线了~~~

Swift选项集合(OptionSet)

Objective-C中的NS_OPTION宏

对于iOS开发者来说,NS_OPTION这个宏应该都不会陌生吧,这个宏给我们的开发带来了极大的便利。

1
2
3
4
5
6
7
8
9
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

Objective-C中可以通过|来组合选项,通过&来判断给定的选项中是否包含某个成员。

1
2
3
4
UIViewAutoresizing autoresizing = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (autoresizing & UIViewAutoresizingFlexibleWidth) {
...
}

通过|=来追加选项。

1
autoresizing |= UIViewAutoresizingFlexibleLeftMargin;

OptionSet协议

Swift使用struct来遵从OptionSet协议,以引入选项集合,而非enum。为什么这样处理呢?当枚举成员互斥的时候,比如说,一次只有一个选项可以被选择的情况下,枚举是非常好的。但是和 C 不同,在 Swift 中,你无法把多个枚举成员组合成一个值,而 C 中的枚举对编译器来说就是整型,可以接受任意整数值。

1
2
3
4
5
6
7
8
9
10
11
struct Hobbies: OptionSet {
var rawValue: UInt
static let none = Hobbies(rawValue: 0) // 这人很懒,居然没任何爱好😂
static let basketball = Hobbies(rawValue: 1 << 0)
static let football = Hobbies(rawValue: 1 << 1)
static let baseball = Hobbies(rawValue: 1 << 2)
static let swim = Hobbies(rawValue: 1 << 3)
static let badminton = Hobbies(rawValue: 1 << 4)
static let all: Hobbies = [.basketball, .football, .baseball, .swim, .badminton]
}

和C一样,Swift中的选项集合结构体使用了高效的位域来表示,但是这个结构体本身表现为一个集合,它的成员则为被选择的选项。这允许你使用标准的集合运算来维护位域,比如使用contains(_:)来检验集合中是否有某个成员,或者是用union(_:)来组合两个位域,通过remove(_:)来移除某个成员。另外,由于OptionSet继承于ExpressibleByArrayLiteral,你可以使用数组字面量来生成一个选项集合。

1
2
3
4
5
6
7
var hobbies: Hobbies = [.football, .swim]
_ = hobbies.contains(.football) // true
_ = hobbies.contains(.baseball) // false
// 增加`.badminton`选项
hobbies = hobbies.union(.badminton)
// 移除`.football`选项
hobbies.remove(.football)

源码

你可以在GitHub上查看到OptionSet的源码swift/stdlib/public/core/OptionSet.swift,如果你有兴趣。

UICollectionView瀑布流布局

仅作为学习交流之用

环境

  • Xcode9.0+
  • Swift4.0+

TODO

  • 废弃CollectionViewWaterfallLayoutcontentInset属性,改用UICollectionView的contentInset属性。

一开始是采用UICollectionView.contentInset来做四周留白的,但是后来发现一旦设置了UICollectionView.contentInset.left的值且>0的时候,整个UICollectionView就会一片空白,但是top、bottom、right属性均不受此影响。测试了网上的一些Demo也发现存在此问题,具体原因不详,如有知道的还望赐教。

鉴于此,暂时给CollectionViewWaterfallLayout增加了一个contentInset属性。

源码

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
protocol CollectionViewDelegateWaterfallLayout {
/// 返回每个cell的高度
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, heightForItemAt indexPath: IndexPath, itemWidth width: CGFloat) -> CGFloat
}
class CollectionViewWaterfallLayout: UICollectionViewLayout {
/// 纵列数量
var column: Int = 2
/// 每一行cell之间的间距
var minimumLineSpacing: CGFloat = 5.0
/// 每一纵列cell之间的间距
var minimumInteritemSpacing: CGFloat = 5.0
///
var delegate: CollectionViewDelegateWaterfallLayout?
/// 内容偏移
var contentInset: UIEdgeInsets = .zero
/// 每个纵列的Y值偏移量
private var maxYForColumn = [CGFloat]()
/// 布局数据
private var layoutAttributes = [UICollectionViewLayoutAttributes]()
override var collectionViewContentSize: CGSize {
guard let max = maxYForColumn.max() else { return .zero }
return CGSize(width: 0.0, height: max + contentInset.bottom)
}
override func prepare() {
super.prepare()
maxYForColumn = Array(repeating: contentInset.top, count: column)
layoutAttributes.removeAll()
guard let collectionView = collectionView else { return }
guard let delegate = delegate else {
assertionFailure("delegate can'not be nil.")
return
}
let contentWidth = collectionView.bounds.width - contentInset.left - contentInset.right
let width = (contentWidth - CGFloat(column - 1) * minimumInteritemSpacing) / CGFloat(column)
let itemCount = collectionView.numberOfItems(inSection: 0)
for item in 0..<itemCount {
let indexPath = IndexPath(item: item, section: 0)
let height = delegate.collectionView(collectionView, layout: self, heightForItemAt: indexPath, itemWidth: width)
// 计算当前cell应该展示在哪一列上
var targetColumn: Int
if item < column {
targetColumn = item
} else {
let min = maxYForColumn.min()!
targetColumn = maxYForColumn.index(of: min)!
}
// 计算frame
let x = contentInset.left + width * CGFloat(targetColumn) + CGFloat(targetColumn) * minimumInteritemSpacing
let y = minimumLineSpacing + maxYForColumn[targetColumn]
let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attribute.frame = CGRect(x: x, y: y, width: width, height: height)
layoutAttributes.append(attribute)
// 更新当前列的Y值偏移量
maxYForColumn[targetColumn] = y + height
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return layoutAttributes[indexPath.item]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let collectionView = collectionView else { return false }
return !collectionView.bounds.size.equalTo(newBounds.size)
}
}