0%

什么是ABI

应用程序二进制接口(Application Binary Interface,ABI)是指两程序模块间的接口,通常其中一个程序模块会是库或操作系统所提供的服务,而另一边的模块则是用户所运行的程序。
ABI 约定了我们的应用程序怎样获取数据以及操作数据。应用程序就是通过这些ABI跟系统通信的。

ABI稳定

Swift 5.0 版本之前,应用程序打包时,都会将 Swift 标准库导入到二进制包中,原因是不同的 Swift 版本之间差异大,ABI接口不兼容。
Swift 5.0 开始 ABI 以及稳定。从 5.0 版本开始 Swift 共用一个 ABI 接口,所以从 5.0 版本开始打包时就不需要将 SWift 基础库打进二进制包中,此时iOS系统以及包含了这些基础库,这样做的好处是之一就是包体积变小了,启动时间变少了,更省内存了。

1
事实上,打包时还是会包含 Swift 基础库,原因是在 iOS 12.2 之前依然需要 Swift 基础库,但之后的版本 App Store 会自动移除。

模块稳定

Swift 5.1 之前,不同的版本生成的 Module(模块)只能在对应的版本上运行,不然就会出现以下错误

1
Module compiled with Swift 5.0.1 cannot be imported by the Swift 5.1 compiler

而从 5.1 版本开始, Module Stability 允许 5.1 版本打的 Module 在 5.1 之后的任意版本运行。

开启 Module Stability 后,Framework 引入了一个全新的文件夹 .swiftmodule,包含 .swiftinterface.swiftmodule 文件,swiftinterface 文件作为 swiftmodule 的一个补充,用来描述 module 公开接口的文本文件,并且不受编译器版本限制,既通过 swiftinterface 文件可以将 Swift 6 打包的 framework 在 swift 7 版本下运行。

需要设置

1
2
3
4
//BUILD_LIBRARY_FOR_DISTRIBUTION
//Ensures that your libraries are built for distribution.
//For Swift, this enables support for library evolution and generation of a module interface file.
Build Libraries for Distribution = YES

开启 Build Libraries for Distribution 后编译的文件如下

如果不开启 Build Libraries for Distribution ,那么编译后会少了 swiftinterface, 此时的 Module 是受版本限制的。

什么是 Library Evolution

开启 Library Evolution 后,也就是Build Libraries for Distribution,当一个框架依赖另一个框架时,如果另一个框架发生改动,那么不需要重新编译第一个框架。也就是更新第二个框架不需要重新编译第一个框架。
类似于 @frozen ?

参考

Swift ABI 稳定对我们到底意味着什么
ABI Stability and More
What is Module Stability in Swift and why should you care?

Profile 详解

0x00

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
//限制最低版本,不限制的话可能会报错
platform :ios, '9.0'

//不提示第三方库的警告,如果有组件的
inhibit_all_warnings!

//使用静态库
use_modular_headers!
//动态库
use_frameworks!

//支持的swift版本
supports_swift_versions '>= 5.0'


//声明使用的安装方法和选项,install! 函数只能调用一次
install!
//目前只允许通过 cocoapods 安装
install! 'cocoapods',

支持的key ,显示的是默认值

//清理pod没有使用的所有文件
:clean => true

//是否复制pod的target
:deduplicate_targets => true

//是否生成uuid
:deterministic_uuids => true

//是否将安装的pods集成到项目中
//如果设置为false, Pods将被下载并安装到Pods/目录中,但不会集成到项目中
:integrate_targets => true

//锁定pods的源文件
:lock_pod_sources => true

//多个源包含相同名称和版本的Pod时发出警告
:warn_for_multiple_pod_sources => true

//
:share_schemes_for_development_pods => false

//禁用CocoaPods脚本阶段的输入和输出路径(复制框架和复制资源)
//可以解决修改了私有库代码后,无法立即生效的问题
:disable_input_output_paths => false

//是否保留所有pod的文件结构,包括外部pod源。
//默认情况下,Pod源的文件结构仅为开发Pod保留。
//设置:preserve_pod_file_structure为true将始终保存文件结构。
:preserve_pod_file_structure => false

//是否为每个pod目标生成一个项目,而不是创建一个Pods.xcodeproj,此选项将为嵌套在Pods.xcodeproj下的每个pod目标生成一个项目。
//使用此选项可以加快编译速度
:generate_multiple_pod_projects => false

//是否仅启用自上次安装以来已更改的重新生成目标及其关联项目。
:incremental_installation => false

//是否跳过生成Pods.xcodeproj,只执行依赖项解析和下载。
:skip_pods_project_generation

0x01 关键字

pod

指定项目的依赖项。依赖项需求由Pod的名称和版本需求列表(可选)定义。

1
2
3
4
5
6
7
8
9
pod 'Objection', '0.9'
//
= 0.1,版本0.1。
> 0.1,任何高于0.1的版本。
>= 0.1,版本0.1和任何更高版本。
< 0.1,任何低于0.1的版本。
<= 0.1,版本0.1和任何较低版本。
~> 0.1.2,版本0.1.2及以上到版本0.2,不含0.2。该操作符基于你在版本需求中指定的最后一个组件工作。这个例子等于>= 0.1.2与< 0.2.0相结合,并且总是匹配与你的需求相匹配的最新已知版本。
~ > 0.1.3-beta.0,Beta版和release发行版本为0.1.3,发行版本为0.2(不包括0.2)。用破折号(-)分隔的组件将不考虑版本要求。

configurations

1
2
pod 'PonyDebugger', :configurations => ['Debug', 'Beta']
pod 'PonyDebugger', :configuration => 'Debug'

modular_headers

模块化,当你使用 use_modular_headers! 属性时,可以从模块头中排除特定的Pod

1
pod 'SSZipArchive', :modular_headers => false

source path

源,默认情况下,在全局级别指定的源按照指定依赖项匹配的顺序进行搜索。这种行为可以通过指定依赖项的来源来改变特定的依赖项

1
2
3
4
5
6
pod 'PonyDebugger', :source => 'https://github.com/CocoaPods/Specs.git'
pod 'AFNetworking', :path => '~/Documents/AFNetworking'
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev'
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :tag => '0.7.0'
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :commit => '082f8319af'
pod 'JSONKit', :podspec => 'https://example.com/JSONKit.podspec'

Subspecs

当通过它的名字安装Pod时,它将安装podspec中定义的所有默认的子规范。

1
2
pod 'QueryKit/Attribute'
pod 'QueryKit', :subspecs => ['Attribute', 'QuerySet']

testspecs

1
2
pod 'AFNetworking', :testspecs => ['UnitTests', 'SomeOtherTests']

abstract_target

定义一个新的抽象目标,可用于方便的目标依赖项继承。

1
2
3
4
5
6
7
8
9
10
11
12
abstract_target 'Networking' do
pod 'AlamoFire'

target 'ShowsiOS' do
pod 'ShowWebAuth'
end

target 'ShowsTV' do
pod 'ShowTVAuth'
end

end

def

预定义模块

1
2
3
4
def debug_pods
pod 'LookinServer', :configurations => ['Debug']
pod 'MLeaksFinder', :configurations => ['Debug']
end

Swift 访问权限修饰符

swift 的设计目标之一就是安全类型语言(Designed for Safety),所以多几个访问修饰符也是合情合理的。
访问修饰符可以用来修饰属性方法结构体

private

只能在当前类里使用,不允许外部或者其子类访问。如果用来修饰类、结构体,那么作用相当于fileprivate

private(set) 表示禁止设置,即只读,可访问

fileprivate

只能在当前的源文件里使用

internal(默认)

可以在源代码所在的模块使用

public

可以在模块之外使用,但不可以在其它模块中重写和继承

open

可以在模块之外使用,也可以在其它模块中重写和继承

补充

在一个 Framework 里,既有 Swift 又有 OC 代码时,编译器会将 Swift 和 OC 分别当成一个 Module 。Swift 代码除了使用关键字 @objc 外,还必须使用 public 级别的关键字才能被同一个 Framework 里的 OC 代码使用

需求

下拉刷新某个主页面,需要刷新这个主页面里的所有子页面,每个子页面都是一个独立的模块,也就是要等到所有的模块都请求完成时,主页面的刷新才能结束,并执行一系列相应的操作。

以上就是一个完成的需求,OK,开始写代码,第一版的代码思路是这样的

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
@property (nonatomic, assign) NSInteger dataCallbackCount;

- (void)refreshView {
self.dataCallbackCount = 0;

[self.viewModel fetchData0lWithComplete:^{
self.dataCallbackCount++;
[self shouldEndRefresh:dataCallbackCount];
}];

[self.viewModel fetchData1lWithComplete:^{
self.dataCallbackCount++;
[self shouldEndRefresh:dataCallbackCount];
}];

[self.viewModel fetchData2lWithComplete:^{
self.dataCallbackCount++;
[self shouldEndRefresh:dataCallbackCount];
}];
}

- (void)shouldEndRefresh:(NSInteger)count {
if (count >= 3) {
[self.refreshView endRefresh];
}
}

第一版的代码有个问题,dataCallbackCount 属性和 shouldEndRefresh 方法 都是刷新方法内部才需要使用的,现在都变成类内部全局的了,其他的方法也可以使用这个属性和方法,而且 dataCallbackCount 还一直占用内存,这是不应该的。
我们知道block可以捕获变量,方法也可以用block代替。那么第二版改动如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)refreshView {
__block NSInteger dataCallbackCount = 0;

void (^shouldEndRefresh)(void) = ^(void) {
if (++dataCallbackCount >= 3) {
[self.refreshView endRefresh];
}
};

[self.viewModel fetchData0lWithComplete:^{
shouldEndRefresh();
}];

[self.viewModel fetchData1lWithComplete:^{
shouldEndRefresh();
}];

[self.viewModel fetchData2lWithComplete:^{
shouldEndRefresh();
}];
}

第二版我们将所有的代码都放到一个方法内了,同时也将 self.dataCallbackCount++; 放到了block内部,避免多次调用。dataCallbackCount 这个变量也会在结束刷新后释放内存,对于外部来说只需要调用 refreshView 就可以了,这样显得内聚很多。
但是仔细看的话,又会发现现在的 dataCallbackCount 也只是 shouldEndRefresh 内部需要使用而已 对于 [self.viewModel fetchxxx] 方法来说也应该是不可见的,但显然目前的这种写法是可见的。于是第三版改动如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)refreshView {
void (^_shouldEndRefresh)(void) = ^(NSInteger dataCallbackCountMax) {
__block NSInteger dataCallbackCount = 0;
return ^{
if (++dataCallbackCount >= dataCallbackCountMax) {
[self.refreshView endRefresh];
}
};
}(3);

[self.viewModel fetchData0lWithComplete:^{
_shouldEndRefresh();
}];

[self.viewModel fetchData1lWithComplete:^{
_shouldEndRefresh();
}];

[self.viewModel fetchData2lWithComplete:^{
_shouldEndRefresh();
}];
}

优化结束,如果需要优化的话可以将 [self.refreshView endRefresh]; 方法抽出来。最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)refreshViewWithCallBack:(void(^)(void))callback {
void (^_shouldEndRefresh)(void) = ^(NSInteger dataCallbackCountMax) {
__block NSInteger dataCallbackCount = 0;
return ^{
if (++dataCallbackCount >= dataCallbackCountMax) {
callback ? callback() : nil;
}
};
}(3);

[self.viewModel fetchData0lWithComplete:^{
_shouldEndRefresh();
}];

[self.viewModel fetchData1lWithComplete:^{
_shouldEndRefresh();
}];

[self.viewModel fetchData2lWithComplete:^{
_shouldEndRefresh();
}];
}

-END-

基本类型数据去重(4种方法)

1、利用 NSDictionary key 的唯一性
2、利用 NSSet 的特性,数据不能重复
3、利用 NSArray 的 containsObject 方法
4、利用 NSArray 的 valueForKeyPath 方法,传入的 keyPath 为 @distinctUnionOfObjects.self

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NSArray *dataArray = @[@"a",@"b",@"c",@"d",@"e",@"f",@"g",
@"b",@"d",@"a"];

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for(NSString *str in dataArray) {
[dict setValue:str forKey:str];
}
NSLog(@"dict = %@",[dict allKeys]);

NSSet *set = [NSSet setWithArray:dataArray];
NSLog(@"set = %@",[set allObjects]);

NSMutableArray *array = [NSMutableArray array];
for (NSString *str in dataArray) {
if (![array containsObject:str]) {
[array addObject:str];
}
}
NSLog(@"array = %@",array);

NSArray *keyPathArray = [dataArray valueForKeyPath:@"@distinctUnionOfObjects.self"];
NSLog(@"keyPathArray = %@",keyPathArray);

通过上面4中方法均可过滤掉重复数据。

Read more »

需求

将一个视图对象绕设定点旋转

旋转

当我们需要将一个 View 旋转90°时,可以这样做

1
self.view.transform = CGAffineTransformMakeRotation(M_PI_2);

这时,这个 view 就会绕着“旋转中心”旋转,那么这个“旋转中心”能否自定义呢?肯定是可以的,继续往下看。。。

我们知道每一个 UIView 内部都关联着一个 CALayer ,而 CALayer 中有一个属性

1
2
3
4
5
/* Defines the anchor point of the layer's bounds rect, as a point in
* normalized layer coordinates - '(0, 0)' is the bottom left corner of
* the bounds rect, '(1, 1)' is the top right corner. Defaults to
* '(0.5, 0.5)', i.e. the center of the bounds rect. Animatable. */
@property CGPoint anchorPoint;

这个 anchorPoint 俗称“锚点”,其实也就是上面所说的“旋转中心”。
从注释中我们可以看懂,anchorPoint 是一个已经“归一化”的数据,也就是默认值是中心(0.5, 0.5),左上角是(0, 0),右下角是(1, 1)。anchorPoint 是一个以自身为参考系的“归一化”坐标。
所以当我们想绕某个点旋转时,只需要将这个点相对于自身的“归一化”坐标赋值给 anchorPoint 就行了。但,事实上当我们改动 anchorPoint 时,发现 layer 也改动了。也就是 anchorPoint 和 frame 之间存在某种关系。

Read more »

需求

点击 UITextField 没有成为第一响应者,而是触发一个事件。

常规做法 UITapGestureRecognizer

1
2
3
4
5
6
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
[self.searchTextField addGestureRecognizer:tap];

- (void)tapGesture:(UIGestureRecognizer *)gesture {
NSLog(@"--%@", gesture);
}

这样做可以触发一个事件,同时也成为了第一响应者。那么怎样不让 UITextField 成为第一响应者呢?可以通过委托事件完成

1
2
3
4
5
self.searchTextField.delegate = self;

- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
return NO;
}
Read more »

前言

由于某些原因,国内的经纬度坐标系并不是使用通用的经纬度坐标系。当坐标定位到国内时就会出现偏差,所以我们需要先判断经纬度坐标是否在国内,如果是,则需要先转成国内坐标。

常用坐标系

WGS84坐标系

国际坐标,Google地图、苹果地图使用的坐标系。GPS信号直接解析的经纬度信息也是这个坐标系的。

GCJ02坐标系

中国坐标,高德地图、腾讯地图使用这个坐标系。国内基站定位、WIFI定位获取到的也是 GCJ02 的坐标。

国内的其他坐标系

还有一些国内的地图,在 GCJ02 坐标的基础上再进行一次加密。例如:百度坐标(BD09)。

iOS 苹果地图开发

苹果地图在国内使用的是高德地图的数据(GCJ02坐标系),在国外则使用自己的数据(WGS84坐标系)。那么我们应该怎么区分国内外呢?这里有几种方法

1、国家区域判断

用大量的点将将中国圈出来(港澳台地区要单独出来),然后判断经纬度是否在这个区域内。边界点越多越精确。
缺点:数据越多运算量越大,数据少精确度就低。

Read more »