薛永伟

种瓜得瓜,种豆得豆


  • 首页

  • 标签

  • 分类

  • 归档

Commitizen的安装和使用

发表于 2018-04-13 | 分类于 iOS开发

一、为什么需要标准化的commit message

先来看两张图,一张来自国际知名项目,一张是国内某知名项目(笑)。

国际知名项目AngularJS

image

国内知名项目MingGeJS

image

从上面两张图中,我想你已经能看出一些端倪来了吧,相较而言哪种更加能促进开发不言自明。当然,我举这个国内知名项目比较极端,不过整体而言都普遍存在Commit Message比较随意的现象。

Commit规范

顺着思路,这一步应该给方案了,方案就是上图AngularJS项目中用到的Git Commit Guidelines。

Commit Message 格式

1
2
3
4
5
<type>(<scope>): <subject>
<空行>
<body>
<空行>
<footer>

上面是一次Commit后Message格式规范,分成标题,内容详情,结尾三个部分,各有各的用处,没有多余项。

头部即首行,是可以直接在页面中预览的部分,入上面图中所示,一共有三个部分,,,含义分别如下

  • feat:新功能(feature)
  • fix:修补bug
  • docs:文档(documentation)
  • style: 格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动
Type
  • feat:新功能(feature)

  • fix:修补bug

  • docs:文档(documentation)

  • style: 格式(不影响代码运行的变动)

  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)

  • test:增加测试

  • chore:构建过程或辅助工具的变动

Scope

用来说明本次Commit影响的范围,即简要说明修改会涉及的部分。这个本来是选填项,但从AngularJS实际项目中可以看出基本上也成了必填项了。

Subject

用来简要描述本次改动,概述就好了,因为后面还会在Body里给出具体信息。并且最好遵循下面三条:

  • 以动词开头,使用第一人称现在时,比如change,而不是changed或changes

  • 首字母不要大写

  • 结尾不用句号(.)

Body

里的内容是对上面subject里内容的展开,在此做更加详尽的描述,内容里应该包含修改动机和修改前后的对比。

Footer

footer里的主要放置不兼容变更和Issue关闭的信息,参考下面两个例子

image

image

Revert

此外如果需要撤销之前的Commit,那么本次Commit Message中必须以revert:开头,后面紧跟前面描述的Header部分,格式不变。并且,Body部分的格式也是固定的,必须要记录撤销前Commit的SHA值。

实践利器

上面就是AngularJS目前的Commit规范,相信第一次接触的话不免会有些头大,这时如果有什么能Step by Step的提醒或者可视化的演示就好了。OK,你来对地儿了,现在就来说说如何把规范变为可执行的具体步骤!

二、工欲善其事,必先利其器 - Commitizen

大量的代码提交,必然会产生大量的commit log,而每一次commit是阶段性的Ending,应记录着这一阶段所完成的事以及关注点,尽可能详细具体;且提供更多的历史信息,方便快速浏览;可以过滤某些commit(比如文档改动),便于快速查找信息;可以直接从commit生成Change log。所以log的格式就是关键所在,而Commitizen可以完美的解决这些问题。

Commitizen是什么?

是一个格式化commit message的工具。它的安装需要NPM的支持,NPM是Node.js的包管理工具,所以首先安装node.js,下载对应系统的包,安装即可。
命令安装Node.js:

1
brew install node

Commitizen安装

1
npm install -g commitizen

安装changelog,是生成changelog的工具

1
2
npm install -g conventional-changelog
npm install -g conventional-changelog-cli

执行

1
npm ls -g -depth=0

检验上面两个工具是否安装成功,得到结果如下,表示成功:

1
2
3
4
5
/usr/local/lib
├── commitizen@2.9.6
├── conventional-changelog@1.1.7
├── conventional-changelog-cli@1.3.5
└── npm@5.5.1

然后,运行下面命令,使其支持Angular的Commit message格式。

1
commitizen init cz-conventional-changelog --save --save-exact

但是注意,因为commitizen工具是基于Node.js的,而我们iOS项目工程目录下是没有package.json文件,所以会报错:

1
2
npm WARN saveError ENOENT: no such file or directory, open '/Users/Elite/package.json'
npm WARN enoent ENOENT: no such file or directory, open '/Users/Elite/package.json'

对于此种错误,创建一个空的package.json文件,然后进入到项目目录,执行

1
npm init --yes

会生成项目对应项目的package.json,将项目目录下产生的package.json的内容写入到自己建的package.json(/User/Elite/package.json)中,如果有多个项目,将各项目生成的package.json内容写入到package.json中,下面是我的配置(/User/Elite/package.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
[{
"name": "salary",
"version": "1.0.0",
"description": "> v1.0 涵盖所有老师(非中教)的基本工资、奖励工资、惩罚工资。",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@***.***.com:erp/salary.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cz-conventional-changelog": "^2.1.0"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"dependencies": {}
},
{
"name": "ats",
"version": "1.0.0",
"description": "composer.json composer配置 vendor 第三方类库",
"main": "index.js",
"directories": {
"test": "tests"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@***.***.com:ats/ats.git"
},
"keywords": [],
"author": "",
"license": "ISC"
}]

然后进入到你要操作的项目目录,执行

1
conventional-changelog -p angular -i CHANGELOG.md -s

此时项目中多了CHANGELOG.md文件,表示生成 Change log成功了。以后,凡是用到git commit 命令的时候统一改为git cz,然后就会出现选项,生成符合格式的Commit Message。实例如下:

1
2
3
4
5
6
7
8
? Select the type of change that you're committing: (Use arrow keys)
? feat: A new feature
fix: A bug fix
docs: Documentation only changes
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
refactor: A code change that neither fixes a bug nor adds a feature
perf: A code change that improves performance
test: Adding missing tests or correcting existing tests

然后按操作执行,即可产生change log。如果最后产生一个这样的错误:

1
2
Error: Could not resolve /Users/Elite/web/node_modules/cz-conventional-changelog. 
Cannot find module '/Users/Elite/web/node_modules/cz-conventional-changelog'

只需做个软连接即可:

1
ln -s /Users/Elite/node_modules /Users/Elite/web/node_modules

使用

在代码更改后,提交commit message的时候,不再使用git commit -m方法,而是git cz,将会出现交互式选项,让你选择或者输入信息,给你一个完善的commit message。示例动图:
相当犀利

OK。

你真的了解iOS中控制器的present和dismiss吗?

发表于 2018-02-07 | 分类于 iOS开发
一、了解present和dismiss

一个iOS开发,这个控制器的打开和关闭,应该是接触UIKit所接触的第一个关于UIViewController的API,然而,你真的了解它吗?
同样的,本文默认你已经了解UIViewController的present和dismis方法,并多次运用。另外本文不再加OC语言的代码,毕竟都能读得懂swift。

二、谁来调用dismiss方法?

咱们从最基本的开始:一个控制器A,和一个控制器B。
A present B
让A打开B,在A中是这样写:

1
2
3
//A
let B = ViewController()
self.present(B, animated: true, completion: nil)

在B中需要关闭B的时候在B中这么写:

1
2
//B
self.dismiss(animated: true, completion: nil)

难道?这样写有问题?这….可能大多数开发者都是这么写,也没有出现过什么问题。但是…
YES,这样写在“理论”讲,是错误的。
别急,在实际上,这么写大多数情况是没有问题的,因为iOS系统帮我们做了很多。

三、presentingViewController和presentedViewController

想了解为什么说那么写在“理论上”是错误的,先从presentingViewController和presentedViewController说起。

为了不使本文枯燥,不引用大段描述和说明,只针对案例解释。当从A中弹出B后:

  1. self.presentingViewController: 在A中,就是nil;在B中,就是A
  2. self.presentedViewController:在A中,就是B;在B中,就是nil

那为什么说在B中调用dismiss在“理论上”是错的呢?因为有这么一个规则一定要记住!

谁污染,谁治理!

很熟悉对不对?MRC的法则,谁创建谁释放!所以A打开了B,当然是A来负责关闭!

正确写法是在A里调用self.dismiss(animated: true, completion: nil)

等等,之前都是B里调用?没错,dismiss方法系统会自动优化,当B视图控制器调用dismiss时,它并没有打开任何界面,就将dismissViewController方法会自动交给B的presentingViewController执行,也就是A来执行。
如果现在A没有打开B的话,调用dismissViewController时它没有presentedViewController,转交给它的presentingViewController,但是它也没有presentingViewController,所以dismiss就不执行了。swift中可选型能很好的解释了:

1
self.presentingViewController?.dismiss(animated: true, completion: nil) //presentingViewController为nil,后面的不执行

扩展:关于实例为空的时候方法不执行,可百度“iOS消息转发”了解。

既然这样,说这个有毛用?
如果你从A打开了B,从B打开了C,现在怎么直接回到A?
你很可能会通过一些手段让B执行dismiss方法,但你会得到错误的结果,因为你这里如果交给B执行dismiss方法,和直接在C里面执行dismiss方法的效果是一样的,也就是说你到了B的界面并没有到A。
SO,dismiss方法必须让需要回到的这个控制器来执行。那么一个控制器的presentingViewController一定是打开它的那个控制器吗?继续往后看。

四、presentingViewController是打开它的那个控制器吗?

从上文,A打开了B,A是B的presentingViewController,那是不是所有的控制器的presentingViewController都是调用presnet方法打开自己的那个控制器呢?
NO!但是刚才A打开了B,不是说A是B的presentingViewController吗?当然,不是说一个控制器A弹出一个控制器B,A就一定是B的presentingViewController。
吃屁吧你

别着急,看几个图片娱乐一下:
第一种
这时箭头指向的控制器的presenting是谁?
第二种
这时箭头指向的控制器的presenting是谁?

第一个图片是navigationController,第二个是tabbarController。又跟你想的不一样了?看来你真的了解的太少,这时咱们得了解一下子控制器了。

四、子控制器childViewControllers

刚才在了解presentingViewController和presentedViewController的时候,如果你用代码试了一下,会发现parentViewController(OC中,swift叫parent),父控制器。有父就有子,每个控制器都有一个属性,叫做childViewControllers,存放它的子控制器。这里对childViewControllers的使用方法使用场景使用优势不做探讨。我们假设你了解并多次使用了子控制器来显示复杂界面的。

但是实际上只要你学习iOS的UIKit,你就肯定已经多次,甚至经常使用了这个东西。
我们知道,在一个有导航控制器的控制器P中加入了子控制器Q,然后添加了Q的view,这个Q就可以使用self.navigationController进行跳转,Q是利用的P的导航控制器进行跳转。也就是说Q的关于控制器的操作都被P给处理了。

那新加入一个问题:如果在Q中present出来一个界面R,那这个R的presentingViewController是谁呢?
是的,是P。此时已经出现了刚才说的问题,Q打开了R,但是R的presentingViewController却是P。这时因为Q是P的子控制器。

那么就可以解释上面两个图片的结果为什么是navi控制器和tabbar控制器了。因为使用标签控制器和导航控制器,都是显示的子控制器的内容,一个带导航栏,一个带标签栏。
所有的控制器都有childViewControllers属性,保存了所有的子控制器,但是有些特殊的控制器,比如UIPageViewController、UINavigationController、UITabBarController等,还有一个viewControllers属性,实际内容和childViewControllers一样。这些特殊的控制器实际上不展示主要内容,主要内容由子控制器展示。
所以刚才说,只要你学了UIKit用了UINavigationController和UITabBarController,你就在使用childViewControllers在做界面的管理了。

因此,当某个控制器有父控制器的时候,它的presentingViewController是父控制器的presentingViewController。

说这么多,又有毛用?

image.png

比如你做过转场动画,会用到这么个方法animateTransition(using transitionContext: UIViewControllerContextTransitioning),当然你首先要做的,就是从context里获取toViewcontroller和fromViewController。如果你的这个转场是个modalPresent,此时你获取到的fromViewController可不一定是调用present的那个控制器,所以你要根据这个VC来做一些动画,可能就要有问题了。

如果是带UINavigationController,需要通过nav.visibleViewController获取nav当前的控制器,如果是带UITabBarController,需要通过tab.selectedViewController获取tab当前的控制器。

四、如何强制指定presentingViewController就是打开自己的那个?

我们需要了解一下这两个属性:

1
2
@property(nonatomic,assign) BOOL definesPresentationContext;
@property(nonatomic,assign) UIModalPresentationStyle modalPresentationStyle;

modalPresentationStyle属性决定了将要present的控制器以何种方式展现,默认值为UIModalTransitionStyleCoverVertical。如果把一个控制器的definesPresentationContext属性设置为YES,那么在需要进行UIModalPresentationCurrentContext类型的跳转的时候,UIKit会使用视图层级内的这个控制器来进行跳转。

1
2
avc.definesPresentationContext = false
avc.modalPresentationStyle = .currentContext

大功告成!现在presentingViewController能够获取到我们期望的对象了。

FMDB从理论到实战(swift4)

发表于 2018-02-07 | 分类于 iOS开发

理论篇

本文默认你已经在项目中继承了FMDB,如果是swift项目,已经在swift中引入桥接文件并添加了FMDB。
iOS数据库常用的库有CoreData、FMDB、Realm等。对于三种方式的优缺点及速度等本文不做讨论,由于FMDB的受欢迎程度很高,本文对FMDB进行探讨和梳理。

首先三种数据库操作方式的简介:

  1. CoreData是Apple对sqlite的封装,一种面向对象的数据库操作方式。包含
    [认识CoreData-基础使用](https://www.jianshu.com/p/0ddfa35c7898)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    2. FMDB是开源的对sqlite的封装,使用sql语句进行对数据库的操作,灵活方便,本文主要讨论这个。了解更多[SQLite的常见问题](http://www.sqlite.org/faq.html)
    3. Realm是由Y Combinator孵化的创业团队开源出来的一款可以用于iOS(同样适用于Swift&Objective-C)和Android的跨平台移动数据库。目前最新版是Realm 2.0.2,支持的平台包括Java,Objective-C,Swift,React Native,Xamarin。提供可视化工具。Realm与sqlite无关。了解更多[Realm数据库 从入门到“放弃”](https://www.jianshu.com/p/50e0efb66bdf)

    ###### FMDB主要类

    1. FMDatabase-代表一个独立的SQLite数据库,执行SQL语句。
    2. FMResultSet-代表FMDatebase查询的结果集,
    3. FMDatabaseQueue-如果你想要在多线程中查询和更新,你应该使用这个类,

    ###### FMDB创建数据库

    FMDatabase通过一个SQLite数据库文件的路径创建。这个路径可以有以下三种样式:
    1. 一个系统文件路径.硬盘上之前不存在的,如果它不存在,FMDB会为你新建。
    2. 一个空字符串,一个空数据库在临时文件中创建。在数据库连接关闭的时候,这个数据库会被删除。
    3. NULL(空)。一个在内存中的数据库将被创建。在数据库连接关闭的时候,这个数据库会被销毁。
    eg:

//OC
FMDatabase *db = [FMDatabase databaseWithpath:@”/tmp/tmp.db”];
//想了解更多关于临时或内存数据库,请阅读sqlite文档:http://www.sqlite.org/inmemorydb.html

1
2
3
```
//swift
let db = FMDatabase.init(path: "/tmp/tmp.db")

FMDB打开数据库

和数据库建立连接之前,应该确保它是打开的,在内存不足、禁止开启、创建数据库的时候会打开失败。

1
2
3
4
5
//OC
if(![db open]){
//打开数据库失败
return;
}

1
2
3
4
5
6
//Swift
if (db?.open())! {
//打开成功
}else{
//打开失败
}
FMDB执行更新

除了SELECT格式的数据库执行语句都是更新。包括CREATE,UPDATE,INSERT,ALTER,COMMIT,BEGIN,DETACH,DELETE,DROP,END,EXPLAIN,VACUUM,还有replace语句等等。基本上,只要你的SQL语句不是以SELECT开头,都是更新语句。
执行更新使用

1
2
3
4
5
6
7
```
//OC
BOOL sucess = [db executeUpdate:@"DELETE FROM person WHERE person_id = ?",person.ID];
if sucess {
}else{
NSLog(@"执行出错了:%@",[db laseErrorMessage])
}

1
2
3
4
5
6
7
8
9
//swift
let sql = String.init(format: "DELETE FROM person WHERE person_id = %d'", person.ID)
guard let sucess = db?. executeUpdate(sql, withArgumentsIn: nil) else{
debugPrint("\(String(describing: db?.lastErrorMessage()))")
}
if sucess {
}else{
debugPrint("执行出错了")
}
FMDB执行查询

一个SELECt语句是一个查询语句并且通过-executrQuery方法执行。
执行查询成功返回FMResultSet对象,失败返回nil。你应该使用

1
为了循环访问查询结果集,你可以使用```while()```循环.你也需要从一个纪录到另一条。在FMDB中,最简单的办法是这样:

//OC
FMResultSet s = [db executeQuery:@”SELECT FROM myTable”];
while ([s next]){
//retrieve values for each record
}

1
2
3
4
5
6
7
8
9
10
```
//swift
let sql = "SELECT *FROM myTable"
guard let result = db?.executeQuery(checksql, withArgumentsIn: nil) else {
debugPrint("\(String(describing: db?.lastErrorMessage()))")
return
}
while result.next() {
//retrieve values for each record
}

通常你在使用查询结果的返回值前必须先调用

1
2
3
4
5
6
7
8
9
```
//OC
FMResultSet *s = [db executeQuery:@“SELECT COUNT(*) FROM myTable”];

if ([s next]) {

int totalCount = [s intForColumnIndex:0];

}

FMResultSet 拥有许多方法去获取适当类型的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
intForColumn:

longForColumn:

longLongIntForColumn:

boolForColumn:

doubleForColumn:

stringForColumn:

dateForColumn:

dataForColumn:

dataNoCopyForColumn:

UTF8StringForColumnName:

objectForColumnName:

其中时间date类型取出时使用

1
2
3
4
5
6
7
8
9
10
11
这里的每一个方法都有一个对应的{type}ForColumnIndex:表达式。基于字段在结果集中的位置可以被用来获取数据,和字段名一一对应。
特别的,你在这里不需要```close```一个FMResultSet,直到结果集都被释放或者父数据库关闭了。

###### FMDB关闭(Close)

当你完成了数据的查询和更新,你应该```close```这个FMDatabase的连接让SQLite释放那些操作过程中占用的资源。

###### FMDB 事务(Transactions)

FMDatabase可以通过调用合适的方法或者执行开始和结束事务型语句开始并提交一个事务。多条语句和批量添加你可以使用```FMDatabase```的```executeStatements:withResultBlock:```去做。
一个多条SQL语句在一个字符串中:

//OC
NSString *sql = @”create table bulktest1 (id integer primary key autoincrement, x text);”

“create table bulktest2 (id integer primary key autoincrement, y text);”

“create table bulktest3 (id integer primary key autoincrement, z text);”

“insert into bulktest1 (x) values (‘XXX’);”

“insert into bulktest2 (y) values (‘YYY’);”

“insert into bulktest3 (z) values (‘ZZZ’);”;

success = [db executeStatements:sql];

sql = @”select count(*) as count from bulktest1;”

“select count(*) as count from bulktest2;”

“select count(*) as count from bulktest3;”;

success = [self.db executeStatements:sql withResultBlock:^int(NSDictionary *dictionary) {

NSInteger count = [dictionary[@”count”] integerValue];

XCTAssertEqual(count, 1, @”expected one record for dictionary %@”, dictionary);

return 0;

}];

1
OC中对于长字符串的处理,对每一段字符串用双引号```"```括起来,可以换行,直到以分号```;```结尾,表示字符串结束。

NSString *aa = @”长字符串的第一段”
“这是第二段”
“出现分号则表示字符串结束”
;

1
2
3
4
5
6
###### FMDB线程安全(FMDatabaseQueue)

在多线程使用同一个FMDatabase的单例是一个坏主意。通常为每一个线程创建FMDatabase对象都是OK的,但千万不要跨线程使用数据库单例。在多线程同时使用,你可能会遇到异常或闪退。
如果需要线程安全,用FMDatabaseQueue替代。
实例化一个FMDatabaseQueue的单例,并且在多线程中使用这个单例,将通过队列管理,**同步执行**来自多线程的命令。
这里是如何使用,第一步,创建你的队列。

//OC
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];

1
2
3
```
//swift
let queue = FMDatabaseQueue.init(path: aPath)

然后这样使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//OC
[queue inDatabase:^(FMDatabase *db) {

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @1];

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @2];

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @3];

FMResultSet *rs = [db executeQuery:@"select * from foo"];

while ([rs next]) {

…

}

}];

扩展:在swift中使用queue的单例的示例代码:

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
//swift
class DBHelper: NSObject {
static let dbQueue:FMDatabaseQueue? = {
let dbPath = XYWSandBox.getDocumentDirectory() + "/114la.db"
let dbq = FMDatabaseQueue.init(path: dbPath)
return dbq
}()

//类方法,dbQueue是DBHelper单例(static)
class func dbQueryAlldownload() -> [DownloadData]{
let resultArray = [DownloadData]()
self.dbQueue?.inDatabase({ (db) in
let sql = "select * from \(self.downloadsTable) ORDER BY id Desc"
guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {
return
}
var resultArray = [DownloadData]()
while result.next() {
//获取数据并创建download对象
resultArray.append(download)
}
})
return resultArray
}
}

将简单的多任务包装在一个事务中,使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
```
//OC
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @1];

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @2];

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @3];

if (whoopsSomethingWrongHappened) {

*rollback = YES;

return;

}

// etc…

[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @4];

}];

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
//swift
queue.inTransaction { db, rollback in
do {

try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [1])

try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [2])

try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [3])

if whoopsSomethingWrongHappened {

rollback.pointee = true//早版本swift使用rollback.memory = true

return
}
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [4])

} catch {

rollback.pointee = true//早版本swift使用rollback.memory = true

print(error)

}
}

FMDatabaseQueue将在队列中顺序执行代码块中的任务,因此你可以同时调用在多线程的多FMDatabaseQueue的方法,在顺序排到的时候它们将被执行。需要注意的是,在一个queue的block中,不能再次使用queue的block。

实战篇

实战场景

我们做一个app,这个app中可以下载网络内容,我们的下载管理界面中需要显示一些信息,那么大概需要记录下载任务的内容有:下载地址,下载文件名,文件大小,已下载大小,创建下载任务的源网页地址,创建时间。
我们自己也有个网站,是个说明书大全网站,因为说明书是由我们自己提供的内容,所以要能通过已下载的文件,分享这个文件的H5在线阅读的url地址,在第三方分享的时候要有封面图的远程url地址,并且在已下载文件里这个说明书可以进行“反馈”、“举报”。那么我们就需要知道这个说明书的“id”等信息。因此我们创建一个表单独记录这个说明书的信息。

在此功能完成中,需要“下载管理类”、“数据库管理类”、“沙盒文件管理类”相互配合。
其中通过文件的下载url作为关联标识符,以此url获取下载的信息,数据的信息,以及文件路径等信息。
下载管理类主要负责文件的下载,暂停,恢复,断点下载等;沙盒文件管理类主要负责下载文件的管理;数据库管理类负责记录下载的信息,以及其他需要持久化的信息。

因此关于下载后的文件路径地址,并不在数据库中存储,而是使用了另外的文件管理类,通过url地址等来获取相应的下载后的路径。此类不在此文探讨范围,暂且不表。

下载的步骤如下:

  1. 根据发起请求的header中的Content-Type检测打开的是个可下载文件。
  2. 创建下载任务,并在下载数据表中记录这个下载。
  3. 检查是否是检查网址是否来自自家网站,如果是,在说明书表中记录信息。
  4. 文件下载过程更新下载数据表中的信息。

显示步骤如下:

  1. 从下载数据表中查询所有内容,或者根据需求查询某些内容。
  2. 已下载文件的封面为各种格式的默认图,显示大小、时间等其他信息。
  3. 根据下载的url从说明书数据表中查询信息。
  4. 如果2没有找到说明书,不显示反馈、分享等工具栏,只显示文件的打开方式。
  5. 如果2有结果,则显示此下载关于说明书的封面图等其他信息。
实战代码

在项目应用中,个人推荐所有关于数据库的操作都放在一个类中。
通过swift的extension,将不同表的操作放在不同的区域,并且通过文档注释(//MARK: -)的方法,建立快速索引。
这样就很容易管理数据库,并且此类以外所有地方都不在与数据库交互。

这里我们创建一个数据库管理类DBHelper :

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
//
// DBManager.swift
// Browser
//
// Created by 西方 on 2018/1/24.
// Copyright © 2018年 114la.com. All rights reserved.
//

import UIKit


/// 数据库管理类,可在需要操作数据库的时候,通过扩展的模式添加方法,统一管理便于维护
class DBHelper: NSObject {


/// 单例的dbQueue
static let dbQueue:FMDatabaseQueue? = {
let dbPath = XYWSandBox.getDocumentDirectory() + "/114la.db"
let dbq = FMDatabaseQueue.init(path: dbPath)
return dbq
}()

/// 下载信息的表
static let downloadsTable:String = {
return "downloads"
}()


/// 说明书信息的表
static let instructionsCacheTable:String = {
return "instructionsCache"
}()


}

//MARK: - 下载表的数据库操作
extension DBHelper {

/// 创建下载表
class func createDownloadsTable() {
self.dbQueue?.inDatabase({ (db) in
let sql = "create table if not exists \(self.downloadsTable) (id integer primary key autoincrement,url text,weburl text,title text,downloadsize long,filesize long,createtime datetime)"

let result = db?.executeUpdate(sql, withArgumentsIn: nil)
if result! {
//表创建成功
debugPrint("createDownloadsTable sucess")
}else{
debugPrint("downloads table create faild!")
MBProgressHUD.showFailImage("downloads table create faild!")
}
})
}

typealias DownloadResultCompleteHandle = ([DownloadData]) -> ()

/// 查询所有的下载数据
///
/// - Parameter complete: 完成回调
class func quryAllDownloads(complete:@escaping DownloadResultCompleteHandle){

self.dbQueue?.inDatabase({ (db) in
let sql = "select * from \(self.downloadsTable) ORDER BY id Desc"
guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {

return
}

var resultArray = [DownloadData]()
while result.next() {
let url = result.string(forColumn: "url") ?? ""
let webUrl = result.string(forColumn: "webUrl") ?? ""
let title = result.string(forColumn: "title") ?? "下载出错"
let downloadsize = result.double(forColumn: "downloadsize")
let filesize = result.double(forColumn: "filesize")
let createtime = result.date(forColumn: "createtime")

let download = DownloadData.init()
download.url = url
download.sourceUrl = webUrl
download.title = title
download.downloadsize = downloadsize
download.filesize = filesize
download.createTime = createtime
resultArray.append(download)
}
complete(resultArray)
return
})
}


/// 查询某个url是否已经下载过
///
/// - Parameter url: URL地址
/// - Returns: 是否下载过
class func isDownloadExit(_ url:String)->Bool{
var exit = false
self.dbQueue?.inDatabase({ (db) in
let checksql = String.init(format: "select * from %@ where url = '%@'",self.downloadsTable, url.urlEncoded())

guard let exitresult = db?.executeQuery(checksql, withArgumentsIn: nil) else {

debugPrint("\(String(describing: db?.lastErrorMessage()))")
return

}
if exitresult.next() {
debugPrint("下载已存在!")
exit = true
}
})

return exit
}

/// 添加一个下载任务
///
/// - Parameters:
/// - url: 下载地址
/// - title: 任务的标题
class func dbAddDownload(_ url:String,webUrl:String,title:String){

self.dbQueue?.inDatabase({ (db) in

debugPrint("准备添加新的下载数据!")
let now = Date.init().timeIntervalSince1970

let sql = String.init(format: "insert into %@ (url,weburl,title,downloadsize,filesize,createtime) values ('%@','%@','%@',0,1,%f)" ,self.downloadsTable,url.urlEncoded(),webUrl.urlEncoded(),title.urlEncoded(),now)

let result = db?.executeUpdate(sql, withArgumentsIn: nil)

if result! {
debugPrint("dbAddDownload - \(url)")
}else{
MBProgressHUD.showFailImage("db add download falid!")
debugPrint("db add download falid! - \(url) \n \(String(describing: db?.lastErrorMessage()))")
}
})

}


/// 更新下载的进度信息
///
/// - Parameter data: 下载数据data
class func updateDownloadStatus(data:DownloadData) {
let sql = "update \(self.downloadsTable) set downloadsize = \(data.downloadsize),filesize = \(data.filesize) where url = '" + data.url.urlEncoded() + "'"
self.dbQueue?.inDatabase({ (db) in
let result = db?.executeUpdate(sql, withArgumentsIn: nil)
if !result! {
debugPrint("db update download falid!")
}
})
}

}


//MARK: - 说明书的数据库操作
extension DBHelper {

/// 创建缓存的说明书表
class func createInstructionsCacheTable() {
self.dbQueue?.inDatabase({ (db) in
let sql = "create table if not exists \(self.instructionsCacheTable) (id integer primary key autoincrement,url text,title text,thumPath text,imgUrl text,tid integer,articletype integer,articeTitle text)"
let result = db?.executeUpdate(sql, withArgumentsIn: nil)
if result! {
//表创建成功
debugPrint("createInstructionsCacheTable sucess")
}else{
debugPrint("instructionsCacheTable create faild!")
MBProgressHUD.showFailImage("instructionsCacheTable create faild!")
}
})
}


/// 添加说明书下载
///
/// - Parameter download: 说明书信息
class func dbAddInstructionDownload(_ download:InstructionDownloadData){
self.dbQueue?.inDatabase({ (db) in

let imgPath = download.imgPath ?? ""
let imgUrl = download.imgUrl ?? ""
let sql = String.init(format: "insert into %@ (url,title,thumPath,imgUrl,tid,articletype,articeTitle) values ('%@','%@','%@','%@',%d,%d,'%@')", self.instructionsCacheTable,download.url.urlEncoded(),download.title.urlEncoded(),imgPath.urlEncoded(),imgUrl.urlEncoded(),download.tid ?? 0,download.articletype ?? 0,download.articeTitle ?? "unknown")

let result = db?.executeUpdate(sql, withArgumentsIn: nil)
if result! {
debugPrint("dbAddInstructionDownload - \(download.url)")
}else{
MBProgressHUD.showFailImage("db add instructionCache falid!")
debugPrint("db add instructionCache falid! - \(download.url)")
}

})
}


/// 查询说明书的信息
///
/// - Parameters:
/// - url: 下载地主
/// - complete: 完成回调
class func dbSearchInstructionInfo(by url:String,complete:@escaping (InstructionDownloadData)->()) {
self.dbQueue?.inDatabase({ (db) in
let sql = String.init(format: "select * from %@ where url = '%@' ORDER BY id asc",self.instructionsCacheTable,url)

guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {

return
}

if result.next(){
inst.imgPath = result.string(forColumn: "thumPath")
inst.imgUrl = result.string(forColumn: "imgUrl")

inst.tid = Int(result.int(forColumn: "tid"))
inst.articletype = Int(result.int(forColumn: "articletype"))
inst.articeTitle = result.string(forColumn: "articeTitle")

complete(inst)
}
result.close()
})
}

}

这里我使用了闭包进行数据的回传,实际上更应该通过返回值的方法,这样能完全隔离数据库操作。由于数据操作是同步执行,所以不必担心返回的时候数据库还没有执行完导致数据不全或者为空的问题。
我们改写一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class func dbSearchInstructionInfo(by url:String,complete:@escaping (InstructionDownloadData)->()) {
self.dbQueue?.inDatabase({ (db) in
let sql = String.init(format: "select * from %@ where url = '%@' ORDER BY id asc",self.instructionsCacheTable,url)

guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {

return
}

if result.next(){
inst.imgPath = result.string(forColumn: "thumPath")
inst.imgUrl = result.string(forColumn: "imgUrl")

inst.tid = Int(result.int(forColumn: "tid"))
inst.articletype = Int(result.int(forColumn: "articletype"))
inst.articeTitle = result.string(forColumn: "articeTitle")

complete(inst)
}
result.close()
})
}

改写后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class func dbSearchInstructionInfo(by url:String) -> InstructionDownloadData?{

var data:InstructionDownloadData? = nil

self.dbQueue?.inDatabase({ (db) in
let sql = String.init(format: "select * from %@ where url = '%@' ORDER BY id asc",self.instructionsCacheTable,url)

guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {

return
}

if result.next(){
let inst = InstructionDownloadData.init()
inst.imgPath = result.string(forColumn: "thumPath")
inst.imgUrl = result.string(forColumn: "imgUrl")

inst.tid = Int(result.int(forColumn: "tid"))
inst.articletype = Int(result.int(forColumn: "articletype"))
inst.articeTitle = result.string(forColumn: "articeTitle")
data = inst
}

result.close()
})
return data
}

从此类之外所有地方,都不在与数据库进行交互。

UITableViewCell选中后分割线消失问题

发表于 2018-01-16 | 分类于 iOS开发
问题:分割线消失了

遇到一个问题,需求是tableview在编辑模式可以多选cell,多选时没有选中背景色,但是有选中按钮🔘。非编辑状态有选中style为灰色。结果发现在选中某个cell时,cell上的分割线消失了。
消失的分割线

自定义cell的时候,我是这么做的,首先将tableView的系统分割线去除

1
[tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];

然后在cell的初始化方法中自定义了一个UIView,并加到了contentView的底部。其中分割线高度,有些是要求是1像素(不是一个点),所以可以使用 1.0/UIScreen.main.scale 表示一个像素的高度。

1
2
3
4
5
6
7
8
9
UIView *line = [UIView new];
[line setBackgroundColor:[UIColor greenColor]];
[self.contentView addSubview:line];
[line mas_makeConstraints:^(MASConstraintMaker *make){
make.left.equalTo(self.contentView);
make.right.equalTo(self.contentView);
make.height.mas_equalTo(@1);
make.bottom.equalTo(self.contentView);
}];

再然后设置了cell的选中背景为透明

1
2
3
UIView *v = [UIView new];
[v setBackgroundColor:[UIColor clearColor]];
[self setSelectedBackgroundView:v];

完美实现,既可以自定义分割线,又可以自定义选中背景颜色,很灵活。

分析:消失的原因

但是,为什么选中后分割线消失了?而且只是分割线消失了,cell上的其他label、button都还在。
看视图的层次图,发现并不是SelectedBackgroundView挡住了分割线,也不是约束问题导致分割线被挤出了可视范围。
首先是觉得SelectedBackgroundView出了问题,就自定义SelectedBackgroundView为带有分割线的view,再设置为SelectedBackgroundView,发现问题解决了。
然而,分割线在编辑状态和非编辑状态的leading约束是不一样的,手动设置55(40+15,原cell的分割线leading是15)发现iPhone上可以对齐,但是在ipad上测试是发现了其他问题:有些cell上分割线还是消失了。
失灵的解决方法

很显然,解决问题的方向错了。
我故意加了另一个很大的很蓝的view,看是否还不显示。结果这个很大很蓝的view在选中时仍不显示。再次查看层次图,放大发现被选中的cell的分割线背景色是w:0 a:0,其他cell的是正常的。
背景色没了
OK,还是百度一下,原来并非是分割线消失了,而是contentView上所有的view的背景颜色被清除了,或者说被设置成透明了。包括UIView以及所有基于UIView的UILabel等。

分析:解决方法

那如何解决该问题?既然在选中的时候被系统清空了cell上的view的背景色,那么只需要再设置回来就好了。
1. 重写cell的setSelected方法,恢复分割线的颜色

1
2
3
4
-(void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
[line setBackgroundColor:[UIColor greenColor]];
}

这个方法就更加灵活了,还可以根据你的选中状态,分别设置不同的颜色。

那么问题来了,既然是选中时,系统默认清除了所有子控件的背景色,那么我不实现父类的方法可不可以?

1
2
3
4
-(void)setSelected:(BOOL)selected animated:(BOOL)animated {
//[super setSelected:selected animated:animated];
//我不实现super的选中方法,那是不是就不会自动清除颜色了呢?
}

结果证明我依旧想多了。不实现super的选中方法,cell将不会保持选择状态,但是点上去的时候,依旧会清除掉子控件的背景色。

有人说把分割线加在cell上而不是contentview上可以解决,实际测试结果:直接加在cell上仍然是会被清除背景色

2. 使用imageView
创建一个imageView,让UI给图,或者通过颜色创建一个图片,设置imageView的图片,就不存在由于背景色被清理而没有分割线的问题了。
3. 给这个分割线单独定义个类
比如:GlobleSeparatorLine,只需要在xib或者storyboard里,更改分割线的类型为GlobleSeparatorLine。然后重写layoutSubviews,在方法里设置背景颜色即可。

WKWebview:JS交互、Cookie的注入与清除

发表于 2018-01-16 | 分类于 iOS开发

一. WKWebView与JS交互

首先使用WKWebView.你需要导入WebKit。关于WKWebView其他基础使用不在本篇研究范围。

1. iOS端执行一段js代码。

使用

1
2
3
4
5
```
//OC代码
[webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable response, NSError * _Nullable error) {

}];

1
2
3
4
//swift代码
webview.evaluateJavaScript(jsStr) { (response, error) in

}

一般使用以上方法,是在网页加载完成(

1
2
###### 2. 通过使用userContentController向网页注入js。
注入的js可以取名字,将会在```WKScriptMessageHandler```的代理方法```didReceiveScriptMessage```中被回掉。

//OC代码
NSString js = @”I am JS Code”;
//初始化WKUserScript对象
//WKUserScriptInjectionTimeAtDocumentEnd为网页加载完成时注入
WKUserScript
script = [[WKUserScript alloc] initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
//根据生成的WKUserScript对象,初始化WKWebViewConfiguration
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
[config.userContentController addUserScript:script];
//设置ScriptMessageHandler为self
[config.userContentController addScriptMessageHandler:self name:@”APPJS”];
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
```
//swift代码
//从js文件加载js代码
let path = (bundlePath) + ("/" + "Contents/Resources/ContextMenu.js")
let source = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String

let path2 = (bundlePath) + ("/" + "Contents/Resources/JSBridge.js")
let source2 = try! NSString(contentsOfFile: path2, encoding: String.Encoding.utf8.rawValue) as String
let js = source + source2

let userScript = WKUserScript(source: js, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: false)
configuration!.userContentController.addUserScript(userScript)
//设置ScriptMessageHandler为self
configuration.userContentController.add(TabManager.sharedInstance, name: "APPJS")
let newWebView = WKWebView(frame: CGRect.zero, configuration: configuration)
self.webView = newWebView

3. 在网页里,使用js代码进行与原生交互。

比如js需要app已经登陆了用户,js调用原生的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//js代码
function callLogin() {
// APPJS是我们所注入的对象
window.webkit.messageHandlers.APPJS.postMessage("shouldLogin");
}

//复杂点的js方法,参数中约定好格式。
//比如:fun代表方法名,arg代表参数,callback是原生处理完需要回掉js的方法名
function callLogin() {
// APPJS是我们所注入的对象
window.webkit.messageHandlers.APPJS.postMessage({
fun: 'notifyLoginOut',
arg: {
callback: ""
}
});
}

4. 原生处理js内容。

通过在

1
2
3
4
5
6
7
8
9
10
11
12
13
```
//OC代码
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"APPJS"]) {
// 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray,
// NSDictionary, and NSNull类型
//do something
NSLog(@"%@", message.body);
}else if ([message.name isEqualToString:@"AppModel"]){
NSLog(@"%@", message.body);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//swift代码,这个对应上面比较复杂js的处理,按约定格式
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){
if message.name == "APPJS" {
if let dic = message.body as? NSDictionary, dic["fun"] != nil,
let fun = (dic["fun"] as AnyObject).description{
// print("dic: \(dic)")
if let arg = dic["arg"] as? NSArray {
if fun == "homeList" && arg.count == 1{
self.homeList(arg[0] as? String ?? "")
}

else if fun == "homeDelete" && arg.count == 1{
self.homeDelete(arg[0] as? String ?? "")
}
}
}
}
5. 原生调用js方法。

上面的这些足够应付大多数的js和原生交互了。
当然,你还有一个使用场景,想要原生某些控件去调用网页里的某个js方法。比如:点击toolBar上的目录按钮,让网页左侧显示出章节目录。
已知js的显示章节目录的js方法名为

1
2


//OC代码
NSString jsStr = @”window.toggleCatalog();”;
[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable, NSError
_Nullable error) {

}]

1
2
3
4
```
//swift代码
let js ="window.toggleCatalog();"
webView?.evaluateJavaScript(js, completionHandler: nil)

二. WKWebView的cookie注入与清除

WKWebView与UIWebview的一个区别,就是WKWebView实例将会忽略任何的默认网络存储器(NSURLCache, NSHTTPCookieStorage, NSCredentialStorage) 和一些标准的自定义网络请求类(NSURLProtocol,等等.).
WKWebView实例不会把Cookie存入到App标准的的Cookie容器(NSHTTPCookieStorage)中,因为 NSURLSession/NSURLConnection等网络请求使用NSHTTPCookieStorage进行访问Cookie,所以不能访问WKWebView的Cookie,现象就是WKWebView存了Cookie,其他的网络类如NSURLSession/NSURLConnection却看不到
与Cookie相同的情况就是WKWebView的缓存,凭据等。WKWebView都拥有自己的私有存储,因此和标准cocoa网络类兼容的不是那么好

NSHTTPCookieStorage 实现管理cookie的单利,每个cookie都是NSHTTPCookie类的实例,做为一个规则,cookie在所有应用 之间共享并在不同进程之间保持同步。
上面引入了网页需要用户登陆,然后让app跳转登陆界面进行登陆,app登陆之后自然要向网页注入cookie,来让网页继续剩下的功能。

1. 在webview发起请求的时候附带cookie。

这个适用首次发起网页请求,同样适用点击,在webview代理方法里,判断是否需要注入cookie的域名,如果是,截断请求,重新发起注入了cookie的请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//oc代码
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
[cookieDic setObject:cookie.value forKey:cookie.name];
}

// cookie重复,先放到字典进行去重,再进行拼接
for (NSString *key in cookieDic) {
NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
[cookieValue appendString:appendString];
}
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.url]];
[request addValue:cookieValue forHTTPHeaderField:@"Cookie"];
NSLog(@"添加cookie");
[self.webView loadRequest:request];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//swift代码
guard let cookies = HTTPCookieStorage.shared.cookies else {
return
}
var cookieDic = Dictionary<String, Any>()
var cookieValue = ""
for cookie in cookies{
cookieDic[cookie.name] = cookie.value
}
for (key,value) in cookieDic {
let appendString = "\(key)=\(value)"
cookieValue.append(appendString)
}
let request = URLRequest.init(url: URL.init(string: "url")!)
request.addValue(cookieValue, forHTTPHeaderField: "Cookie")
2. 在webview创建的时候js注入cookie。

其中js的写法问题,有可能有多个写法是cookie之间用

1
2


//OC代码
WKUserContentController* userContentController = WKUserContentController.new;

WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @”document.cookie =’TeskCookieKey1=TeskCookieValue1’;document.cookie = ‘TeskCookieKey2=TeskCookieValue2’;”injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];

[userContentController addUserScript:cookieScript];

WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;

webViewConfig.userContentController = userContentController;

WKWebView webView = [[WKWebView alloc] initWithFrame:CGRectMake(/set your values*/) configuration:webViewConfig];

1
2
3
4
5
6
7
8
9
10
11
```
//swift代码
let userContent = WKUserContentController()
let jsStr = "document.cookie ='TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"

let cookieScript = WKUserScript.init(source: jsStr, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: false)
userContent.addUserScript(cookieScript)
let webViewConfig = WKWebViewConfiguration()
webViewConfig.userContentController = userContent

let webview = WKWebView.init(frame: CGRect(x: 0, y: 0, width: 300, height: 300), configuration: webViewConfig)

3. 在webview加载内容时js注入cookie。
1
2
3
4
5
6
7
8
9
//swift代码
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
if let laToken = UserCenter.shared().user?.laToken {
let cookie = "115token=\(oofToken)"
webView?.evaluateJavaScript("function setCookie(e,o){document.cookie=e+\"=\"+escape(o)+\";path=/;domain=.115.com\"}for(var cookieTem= \"\(cookie)\",cookieArr=cookieTem.split(\";\"),i=0;i<cookieArr.length;i++){var temArr=cookieArr[i].split(\"=\");setCookie(temArr[0],temArr[1])}", completionHandler: {
(object, error) -> Void in
})
}
}

经过验证,最好是第一点和第三点同时使用,第二点每次截断请求总觉得浪费资源 -。-

4. 清理注入的cookie。

当用户退出登陆的时候,需要清理已经注入的cookie。在iOS9之前,wkwebview是没有清理cookie的方法的,所以需要对不同的版本进行不同的操作。
那iOS9之前的如何操作?可以预见,既然是缓存,肯定是放在沙盒里的。找到沙盒的目录,删除文件即可。
```
//swift代码
/// 清理cookie缓存数据
func ClearCache() {

if #available(iOS 9.0, *) {
    let websiteDataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
    let dateFrom = Date.init(timeIntervalSince1970: 0)

// NSDate.init(timeIntervalSince1970: 0)
// let websiteDataTypes: NSSet = WKWebsiteDataStore.allWebsiteDataTypes()
WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: dateFrom, completionHandler: {
//done
})

} else {

    let libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)[0]
    let cookiesFolderPath = libraryPath+"/Cookies"
    try? FileManager.default.removeItem(atPath: cookiesFolderPath)

// try? NSFileManager.defaultManager().removeItemAtPath(cookiesFolderPath)
}
}

iPhone X超简单适配(适配实战)

发表于 2018-01-16 | 分类于 iOS开发

[已在线上项目使用此方法适配,暂无问题出现。]
日常开发会经常使用xib或者sb,所以本次实验也是使用了xib或者sb。理论纯代码会更简单。
image.png

新建一个sb的界面:
image.png
一个顶部的button中间一个tableView。现在基本还需要适配iOS 8,所以更改项目支持iOS8,并且去掉”Use Safe Area Layout Guides”。
image.png

如果约束基于layout guides,tableView好像自动可以自动多开刘海,而顶部的button就不行了。
实际上,tableview多开齐刘海只是tableviewcell的contentView多开,tableviewCell仍然不在安全区域:
image.png
直接在tableViewCell上添加一个测试的label:
image.png
显然这个tableView并不安全(上面cell内容之所以躲开了刘海,是contentView调整了自己的布局)。
解决这个问题,使用

OR layout Guides)到.m文件中,然后在```viewSafeAreaInsetsDidChange```判断是否支持safeAreaInsets,然后更新这个约束即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
```
//OC
-(void)viewSafeAreaInsetsDidChange
{
[super viewSafeAreaInsetsDidChange];
if ([UIDevice currentDevice].systemVersion.doubleValue>10.0) {
self.topConst.constant = self.view.safeAreaInsets.top;
self.leadingConst.constant = self.view.safeAreaInsets.left;
self.trallingConst.constant = self.view.safeAreaInsets.right;
self.bottomConst.constant = self.view.safeAreaInsets.bottom;
}
}
//swift
override func viewSafeAreaInsetsDidChange() {
if #available(iOS 11, *){
super.viewSafeAreaInsetsDidChange()
self.topConst.constant = self.view.safeAreaInsets.top;
self.leadingConst.constant = self.view.safeAreaInsets.left;
self.trallingConst.constant = self.view.safeAreaInsets.right;
self.bottomConst.constant = self.view.safeAreaInsets.bottom;
}
}

躲开了刘海

```
1
2
3
4
5
6
7
8
9
10
11
12
13

如果是一个复杂的view,比如
![复杂的界面](http://upload-images.jianshu.io/upload_images/1966717-b97c51c692201824.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

在这里就要拖动所有和```self.view```这个```view```相关的约束,然后在```viewSafeAreaInsetsDidChange```方法里进行修改约束。
既然iOS9之前是没有```safeArea```,那咱们自己加一个*safeView*吧。。。
![safeView](http://upload-images.jianshu.io/upload_images/1966717-15ee323a98fe7b06.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

1. 新建一个view,取名为safeView,safeView的约束,**基于LayoutGuide**,然后safeView的上下左右均为0.
2. 此view上所有的view都不再放在self.view上,而是self.safeView上。
3. 在viewSafeAreaInsetsDidChange修改safeView的约束即可。

如果是使用Snapkit,则remake约束即可:

override func viewSafeAreaInsetsDidChange() {
if #available(iOS 11.0, *) {
super.viewSafeAreaInsetsDidChange()
let safeArea = self.view.safeAreaInsets
self.tableView.snp.remakeConstraints { (make) in
make.bottom.top.equalToSuperview()
make.leading.equalTo(safeArea.left)
make.top.equalTo(safeArea.top)
make.tralling.equalTo(-safeArea.right)
} else {
// Fallback on earlier versions
}
}
```
OC使用masonry则同理。

遇到tableView需要底部不用躲避在iPhoneX上的小黑线,则在viewSafeAreaInsetsDidChange里底部约束为0而不是safeArea。
适配使用了autoLayout的老项目,同样适用此方法,稍微麻烦点。
如果需要适配横竖屏,且使用frame布局的,我恐怕你应该考虑跳槽了。

Hello World

发表于 2017-12-06

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

xueyongwei

xueyongwei

7 日志
1 分类
12 标签
© 2018 xueyongwei
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.3