流月


  • 首页

  • 归档

  • 标签

iOS应用加固

发表于 2018-08-17

完整性校验

通过检测SignerIdentity判断是Mach-O文件否被篡改

原理是:SignerIdentity的值在info.plist中是不存在的,开发者不会加上去,苹果也不会,只是当ipa包被反编译后篡改文件再次打包,需要伪造SignerIdentity。

1
2
3
4
5
6
7
NSBundle *bundle = [NSBundle mainBundle];
NSDictionary *info = [bundle infoDictionary];
if ([info objectForKey:@"SignerIdentity"] != nil)
{
return YES;
}
return NO;

参考:https://www.jianshu.com/p/91aa49c45677

越狱检测

参照念茜大神的方法:iOS安全攻防(二十):越狱检测的攻与防,很详细的讲述了检测的办法,不过经过测试,发现有的方法在未越狱的设备上也会检测成越狱,可以在使用的时候过滤掉这些方法。

双击home键后app缩略视图模糊处理

有时候双击home键,app展示启动app记录,会有可能暴露敏感信息,可以在app退至后台时做一个模糊处理,然后进入前台后移除模糊效果。
实现如下:

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
@interface AppDelegate ()
@property(nonatomic,strong)UIVisualEffectView *effectView;
@end

@implementation AppDelegate

- (void)applicationDidEnterBackground:(UIApplication *)application
{
[self.window addSubview:self.effectView];
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
[self.effectView removeFromSuperview];
}

-(UIVisualEffectView *)effectView
{
if (!_effectView)
{
UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
_effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
_effectView.frame = self.window.bounds;
}
return _effectView;
}
...
@end

方法名混淆

也是参照念茜大神的方法:iOS安全攻防(二十三):Objective-C代码混淆.

明文字符串混淆

参考这篇文章:iOS字符串硬编码混淆.

可能作者的步骤描述的不是很清楚,我在这里简单描述一下:
1.在项目的.pch文件中,添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
#include "GolobalCFile.h"


//字符串混淆加密 和 解密的宏开关
//#define ggh_confusion
#ifdef ggh_confusion
#define confusion_NSSTRING(string) [NSString stringWithUTF8String:decryptConstString(string)]
#define confusion_CSTRING(string) decryptConstString(string)
#else
#define confusion_NSSTRING(string) @string
#define confusion_CSTRING(string) string
#endif

其中decryptConstString定义在c文件GolobalCFile中

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* 字符串混淆解密函数,将char[] 形式字符数组和 aa异或运算揭秘
* 如果没有经过混淆,请关闭宏开关
*/
extern char* decryptConstString(char* string)
{
char* origin_string = string;
while(*string) {
*string ^= 0xAA;
string++;
}
return origin_string;
}

2.打开终端Terminal,cd到你的工程根目录;
3.执行如下混淆脚本(执行之前将脚本里面的工程名字改为你的工程名字):

1
python .../confusion.py

将如下脚本写在一个文件里confusion.py。在终端执行即可。

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
#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-
# 本脚本用于对源代码中的字符串进行加密
# 替换所有字符串常量为加密的char数组,形式((char[]){1, 2, 3, 0})

import importlib
import os
import re
import sys


# 替换字符串为((char[]){1, 2, 3, 0})的形式,同时让每个字节与0xAA异或进行加密
def replace(match):
string = match.group(2) + '\x00'
replaced_string = '((char []) {' + ', '.join(["%i" % ((ord© ^ 0xAA) if c != '\0' else 0) for c in list(string)]) + '})'
return match.group(1) + replaced_string + match.group(3)


# 修改源代码,加入字符串加密的函数
def obfuscate(file):
with open(file, 'r') as f:
code = f.read()
f.close()
code = re.sub(r'(confusion_NSSTRING\(|confusion_CSTRING\()"(.*?)"(\))', replace, code)
code = re.sub(r'//#define ggh_confusion', '#define ggh_confusion', code)
with open(file, 'w') as f:
f.write(code)
f.close()


#读取源码路径下的所有.h和.m 文件
def openSrcFile(path):
print("开始处理路径: "+ path +" 下的所有.h和.m文件")
# this folder is custom
for parent,dirnames,filenames in os.walk(path):
#case 1:
# for dirname in dirnames:
# print((" parent folder is:" + parent).encode('utf-8'))
# print((" dirname is:" + dirname).encode('utf-8'))
#case 2
for filename in filenames:
extendedName = os.path.splitext(os.path.join(parent,filename))
if (extendedName[1] == '.h' or extendedName[1] == '.m'):
print("处理源代码文件: "+ os.path.join(parent,filename))
obfuscate(os.path.join(parent,filename))


#源码路径
srcPath = '../StringDecodeDemo'

if __name__ == '__main__':
print("本脚本用于对源代码中被标记的字符串进行加密")

if len(srcPath) > 0:
openSrcFile(srcPath)
else:
print("请输入正确的源代码路径")
sys.exit()

执行完成后查看你的代码,会发现用confusion_NSSTRING和confusion_CSTRING 写的明文字符串都被编码。这样就达到了混淆的效果。

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
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
#!/usr/bin/env python
# encoding=utf8
# -*- coding: utf-8 -*-
# 本脚本用于对源代码中的字符串进行解密
# 替换所有加密的char数组为字符串常量,""

import importlib
import os
import re
import sys


# 替换((char[]){1, 2, 3, 0})的形式为字符串,同时让每个数组值与0xAA异或进行解密
def replace(match):
string = match.group(2)
decodeConfusion_string = ""
for numberStr in list(string.split(',')):
if int(numberStr) != 0:
decodeConfusion_string = decodeConfusion_string + "%c" % (int(numberStr) ^ 0xAA)

# replaced_string = '\"' + "".join(["%c" % ((int© ^ 0xAA) if int© != 0 else '\0') for c in string.split(',')]) + '\"'
replaced_string = '\"' + decodeConfusion_string + '\"'
print("replaced_string = " + replaced_string)

return match.group(1) + replaced_string + match.group(3)


# 修改源代码,加入字符串加密的函数
def obfuscate(file):
with open(file, 'r') as f:
code = f.read()
f.close()
code = re.sub(r'(confusion_NSSTRING\(|confusion_CSTRING\()\(\(char \[\]\) \{(.*?)\}\)(\))', replace, code)
code = re.sub(r'[/]*#define ggh_confusion', '//#define ggh_confusion', code)
with open(file, 'w') as f:
f.write(code)
f.close()


#读取源码路径下的所有.h和.m 文件
def openSrcFile(path):
print("开始处理路径: "+ path +" 下的所有.h和.m文件")
# this folder is custom
for parent,dirnames,filenames in os.walk(path):
#case 1:
# for dirname in dirnames:
# print((" parent folder is:" + parent).encode('utf-8'))
# print((" dirname is:" + dirname).encode('utf-8'))
#case 2
for filename in filenames:
extendedName = os.path.splitext(os.path.join(parent,filename))
#读取所有.h和.m 的源文件
if (extendedName[1] == '.h' or extendedName[1] == '.m'):
print("处理代码文件:"+ os.path.join(parent,filename))
obfuscate(os.path.join(parent,filename))


#源码路径
srcPath = '../StringDecodeDemo'
if __name__ == '__main__':
print("字符串解混淆脚本,将被标记过的char数组转为字符串,并和0xAA异或。还原代码")
if len(srcPath) > 0:
openSrcFile(srcPath)
else:
print("请输入正确的源代码路径!")
sys.exit()

即可解码明文。

为了不那么麻烦,可以打包前将工程拷贝一份,这样就可以只需要编码,不用解码。

以上脚本和代码均在文末Demo中。

反编译

8DD95EE6-2236-4BBF-886B-982214D0DBFB

在混淆了函数名以后,当然要检验一下成果了,这里就需要反编译我们的app了,有个很方便的工具Class-dump,这里有篇文章详细的描述了安装和使用方法:Objective-C Class-dump 安装和使用方法(原创).

另外还有一个反编译、反汇编和调试神器:Hopper。可以查看源码。

疑问

1.执行脚本文件时报如下错误

1
confusion.py: Permission denied

解决方法:
1、打开终端。
2、cd到目标文件夹。
3、输入 chmod 755 你的文件名.sh。

Demo

本文demo:iOS_CodeEncrypt

自动化打包工具集Fastlnae

发表于 2018-08-13 | 分类于 工具 //在此处输入这篇文章的分类。

什么是Fastlane

Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布App.

如何安装Fastlane

  1. 安装xcode命令行工具
    1
    xcode-select --install

如果没有安装,会弹出对话框,点击安装;

如果已经安装,就会提示

xcode-select: error: command line tools are already installed, use “Software Update” to install updates。

  1. 安装Fastlane

使用下面的命令

1
sudo gem install fastlane -NV

或是

1
brew cask install fastlane

来安装Fastlane。

安装完成后,可以执行下面命令,检查是否安装好。

1
fastlane --version

  1. 初始化Fastlane

cd到你的项目目录执行

1
fastlane init

过一会会出现如下提示,让你选择一个选项:

我这里希望打包上传到app store,所以选择了3.

如果你的工程是用cocoapods的那么可能会提示让你勾选工程的Scheme,步骤就是打开你的xcode,点击Manage Schemes,在一堆三方库中找到你的项目Scheme,在后面的多选框中进行勾选,然后rm -rf fastlane文件夹,重新fastlane init一下就不会报错了。

接着会提示你输入开发者账号和密码。

[20:48:55]: Please enter your Apple ID developer credentials
[20:48:55]: Apple ID Username:

登录成功后会提示你是否需要下载你的App的metadata。点y等待就可以。

初始化成功以后,就会生成一个如下图所示的fastlane文件夹:
fastlane

其中metadata和screenshots分别对应App元数据和商店应用截图。

Appfile

Appfile用来存放app_identifier,apple_id和team_id。 它的格式是这样的:

1
2
3
4
5
app_identifier "com.xxx.xxx" # app的bundle identifier
apple_id "xxx@xxx.com" # 你的Apple ID

team_id "XXXXXXXXXX" # Team ID
···

你也可以为每个lane(后面会讲到)提供不同的 app_identifier, apple_id 和 team_id,例如:

1
2
3
4
5
6
7
8
9
app_identifier "com.aaa.aaa"
apple_id "aaa@aaa.com"
team_id "AAAAAAAAAA"

for_lane : release do
app_identifier "com.bbb.bbb"
apple_id "bbb@bbb.com"
team_id "AAAAAAAAAA"
end

这里就是为Fastfile中定义的: release设置单独的信息。

Deliverfile

Deliverfile中为发布的配置信息,一般情况用不到。

Fastfile

Fastfile是我们最应该关注的文件,也是我们的工作文件。
下面是我的fastfile文件中上传至app store的lane:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
platform :ios do
#------------------------------APP STORE-----------------------------------
desc "Push a new release build to the App Store"
lane :to_appStore do
#gym用来编译ipa
gym(clean:true,# 编译前执行 clean,可减少 ipa 文件大小
scheme: "MobileChecking",#要编译的scheme名称
export_method: "app-store", # Xcode 9增加export_method标签
silent: true, # 隐藏没有必要的信息
output_directory: "./fastlane/appstoreIPA", # ipa输出目录
output_name:"mobileChecking", #输出的ipa名称
archive_path:"./fastlane/appstoreIpaArchive", #archive文件导出地址
export_xcargs: "-allowProvisioningUpdates"
)
upload_to_app_store
end

其中一个lane就是一个任务,里面是一个个的action组成的工作流。

export_method:指定打包所使用的输出方式,目前支持app-store, package, ad-hoc, enterprise, development, 和developer-id,即xcodebuild的。

如何使用Fastlane

定义完lane之后怎么执行呢?打开终端,切换到项目的根目录:执行fastlane [lane’name]就可以了。比如我的lane名称叫appStore,那么久执行如下命令:

1
fastlane  appStore

或者采用下面的命令会更快:

1
bundle exec fastlane  appStore

成功之后会在相应的路径下生成ipa文件,并会自动上传至app store。

如何配置Fastfile

上传到fir

####安装fir
使用如下命令,安装fir:

1
sudo gem install fir-cli

安装完成以后,登录fir:

1
fir login

会让你输入你的fir.im API Token,去fir网站即可获得此token。
安装fir:

1
fastlane add_plugin firim

fir上传的lane:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#-----------------------------------FIR-------------------------------

desc "Push a new release build to the FIR"
lane :to_fir do
#build_app(workspace: "MobileChecking.xcworkspace", scheme: "MobileChecking")
#gym用来编译ipa
gym(clean: true, # 编译前执行 clean,可减少 ipa 文件大小
scheme: "MobileChecking", # 要编译的scheme名称
export_method: "ad-hoc", # Xcode 9增加export_method标签
silent: true, # 隐藏没有必要的信息
output_directory: "./fastlane/firIPA", # ipa输出目录
output_name:"mobileChecking", # 输出的ipa名称
archive_path:"./fastlane/firIpaArchive", #archive文件导出地址
export_xcargs: "-allowProvisioningUpdates",
)
# 上传ipa到fir.im服务器,在fir.im获取firim_api_token
firim(firim_api_token: "451d867c8860da31e5e46062b1ecea57")

end

如果你想上传到蒲公英,可以参考这篇文章:使用 fastlane 实现 iOS 持续集成(二)。

常见错误处理

  1. FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT
    1
    2
    3
    4
    5
    6
    7
    [14:57:59]: fastlane finished with errors

    [!] xcodebuild -showBuildSettings timed out after 4 retries with a
    base timeout of 3. You can override the base timeout value with the
    environment variable FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT, and the
    number of retries with the environment variable
    FASTLANE_XCODEBUILD_SETTINGS_RETRIES

在遇到这个错误之后,在命令窗口运行下面代码来更新timeout时间:

1
2
#更新timeout
export FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120

参考

  • iOS效率神器fastlane自动打包
  • 小团队的自动化发布-Fastlane带来的全自动化部署
  • iOS中使用Fastlane实现自动化打包和发布
  • 当Fastlane遇到Xcode9打包出来不一定是ipa而是坑
  • Fastlane的gym指令
  • 使用Fastlane实现iOS项目自动打包
  • (译)iOS自动化打包发布(fastlane)

响应链之Hit-Testing

发表于 2017-05-22 | 分类于 响应链 //在此处输入这篇文章的分类。

Hit-Testing 是什么

Hit-Testing 是一个决定一个点(比如一个触摸点)是否落在一个给定的物理对象上(比如绘制在屏幕上的UIView)的一个过程。

Hit-Testing 执行时机

Hit-Testing是在每次手指触摸时执行的。并且是在任何视图或者手势收到UIEvent(代表触摸属于的事件)之前。

Hit-Testing 的实现

实现:Hit-Testing采用深度优先的反序访问迭代算法(先访问根节点然后从高到低访(从离用户近的视图或者说是后添加的视图)问低节点)。这种遍历方法可以减少遍历迭代的次数。

结束条件:一旦找到最深的包含触摸点的后裔视图就停止遍历(注意,是最深的)。

下面举例说明:

图1

如上图所示,视图A\B\C依次添加到视图上,比如View B的添加比A晚比C早,而自视图B.1比B.2添加的要早。

图2

通过深度优先的反向遍历允许一旦找到第一个最深的后裔包含触摸点的视图就停止遍历,如上图所示,找到B.1后就停止,不会继续遍历A视图。

遍历算法以向UIWindow(视图层次结构的根视图)发送hitTest:withEvent:消息开始。这个方法返回的值就是包含触摸点的最前面的视图。
下面流程图说明了hit-test逻辑:

图3

下面的代码展示了原生hitTest:withEvent:可能的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}

hitTest:withEvent:方法首先检查视图是否允许接收触摸事件。视图允许接收触摸事件的四个条件是:

  • 视图不是隐藏的: self.hidden == NO
  • 视图是允许交互的: self.userInteractionEnabled == YES
  • 视图透明度大于0.01: self.alpha > 0.01
  • 视图包含这个点: pointInside:withEvent: == YES

图2遍历顺序为:UIWindow->MainView->View C->ViewB->View B.2->View B.1.

解释一下为什么顺序是这样:

  1. 首先遍历UIWindow,然后MainView
  2. MainView 有三个子视图ABC,根据算法描述中所讲,首先遍历离用户最近的视图,所以,先遍历View C
  3. 每次遍历时需要判断接收触摸的四个条件,由于落点不在C中,所以在hitTest遍历C时返回nil,然后继续遍历View B,
  4. 然后遍历View B的两个子视图,与第2点判断条件一样,先遍历View B.2
  5. 由于落点不在B.2中,所以继续遍历B.1,由于此时满足结束条件,即接收触摸事件并且B.1没有子视图,遍历到此结束。

覆盖hitTest:withEvent:的一些用途

hitTest:withEvent:可以被覆盖,那么覆盖它可以做些什么呢?

1.增加视图的触摸区域

hit-test-increase-touch-area.png

如上图所示,蓝色按钮太小,如果采用设置UIButton的image来放大点击区域,调整按钮坐标的代码就很不好看了,如果用覆盖hitTest:withEvent:的方法来解决这个方法,就要优雅一些,自定义UIButton,覆盖hit-testing方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
CGRect touchRect = CGRectInset(self.bounds, -10, -10);
if (CGRectContainsPoint(touchRect, point)) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}

2.传递触摸事件给父视图

有的时候对于一个视图忽略触摸事件并传递给下面的视图是很重要的。例如,假设一个透明的视图覆盖在应用内所有视图的最上面。覆盖层有子视图应该相应触摸事件的一些控件和按钮。但是触摸覆盖层的其他区域应该传递给覆盖层下面的视图。为了完成这个行为,覆盖层需要覆盖hitTest:withEvent:方法来返回包含触摸点的子视图中的一个,然后其他情况返回nil,包括覆盖层包含触摸点的情况:

1
2
3
4
5
6
7
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitTestView = [super hitTest:point withEvent:event];
if (hitTestView == self) {
hitTestView = nil;
}
return hitTestView;
}

3.传递触摸事件给子视图

hit-test-pass-touches-to-subviews.png

蓝色方框是一个图片浏览器,在蓝色方框内滑动,可以翻动图片,但是在方框之外是无法响应的,因为手指落点不在图片浏览器的bounces里面,那么如何让手指落在上图位置时,也可以滚动图片呢?方法是在图片浏览器的父视图中,重载hitTest:withEvent:方法,当触摸到图片浏览器自视图之外的视图时,返回图片浏览器即可:

1
2
3
4
5
6
7
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitTestView = [super hitTest:point withEvent:event];
if (hitTestView) {
hitTestView = self.scrollView;
}
return hitTestView;
}

4.响应子view超出了父view的bounds事件

比如自定义Tabbar中间的大按钮,点击超出Tabbar bounds的区域也需要响应,此时重载父view的hitTest: withEvent:方法,去掉点击必须在父view内的判断,然后子view就能成为 hit-test view用于响应事件了。这篇文章详细的描述了如何实现。

参考:iOS中的Hit-Testing

UITabBar 自定义中间大按钮

发表于 2017-05-22 | 分类于 UI //在此处输入这篇文章的分类。

在项目中经常会有这种需求:在tabBar的正中间放置一个大按钮,有时候会超出tabBar的可点击范围。如下图的闲鱼:

闲鱼TabBar

主要思路就是:

  • 自定义UITabBarController
  • 自定义UITabBar
  • 在自定义的UITabBarController中用自定义的UITabBar替换掉原有的tabbar。

首先,利用KVC在自定义的UITabBarController中用自定义的UITabBar替换掉原有的tabbar:

1
2
CustomTabBar *myTabBar = [[CustomTabBar alloc] init];
[self setValue:myTabBar forKey:@"tabBar"];

大按钮好解决,用一张大点的图片即可。但是你会发现点击超出tabbar顶部的部分是不能响应的。如果你知道hit-test的工作原理,就会知道为什么超出部分不能响应。响应的四个条件是:

  • 视图不是隐藏的: self.hidden == NO
  • 视图是允许交互的: self.userInteractionEnabled == YES
  • 视图透明度大于0.01: self.alpha > 0.01
  • 视图包含这个点: pointInside:withEvent: == YES
    在这里就是由于不满足第四个条件,即点击的point没有落在父视图的bounce之内,所以无法响应。

系统的hit-test方法实现大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01)
{
return nil;
}

if ([self pointInside:point withEvent:event])
{
for (UIView *subview in [self.subviews reverseObjectEnumerator])
{
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView)
{
return hitTestView;
}
}
}
return nil;
}

通常解决这个问题的做法是将响应区域扩大,这个我还没有研究。

不过我的思路特殊一点:在自定义的UITabBar中重写hit-test方法,将点击的point进行修改,让它落在可点击范围之内。

由于是点击的point的y坐标超出了tabbar范围,那么只要修改这个y坐标,让他落在tabbar的可点击范围内即可。

修改y坐标的代码如下所示:

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
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01)
{
return nil;
}

if ([self pointInside:point withEvent:event])
{
return [self mmHitTest:point withEvent:event];
}
else
{
CGPoint otherPoint = CGPointMake(point.x, point.y + self.effectAreaY);
return [self mmHitTest:otherPoint withEvent:event];
}
return nil;
}

- (UIView *)mmHitTest:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *subview in [self.subviews reverseObjectEnumerator])
{
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return nil;
}

其中effectAreaY是超出tabbar的垂直距离,比如大按钮顶部超出20,那么这个值就是20,你可以根据需求进行调整。

到这里,超出tabbar区域的大按钮点击问题就得到解决了,但是我反复点了一下,发现一个小问题,就是每个tabbar的item超出部分都能点击,这是我们不想要的。

我们想要的是落点在中间按钮范围内时才去修改这个落点,所以,加一个判断即可,只要点击的point的x坐标在中间大按钮范围之内就去修改,代码如下:

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
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01)
{
return nil;
}

if ([self pointInside:point withEvent:event])
{
return [self mmHitTest:point withEvent:event];
}
else
{
CGFloat tabBarItemWidth = self.bounds.size.width/self.items.count;
CGFloat left = self.center.x - tabBarItemWidth/2;
CGFloat right = self.center.x + tabBarItemWidth/2;

if (point.x < right &&
point.x > left)
{//当点击的point的x坐标是中间item范围内,才去修正落点
CGPoint otherPoint = CGPointMake(point.x, point.y + self.effectAreaY);
return [self mmHitTest:otherPoint withEvent:event];
}
}
return nil;
}

- (UIView *)mmHitTest:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *subview in [self.subviews reverseObjectEnumerator])
{
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return nil;
}

另外,如果想调整图片和文字位置,调整tabbarItem的属性:

1
2
[vc.tabBarItem setImageInsets:UIEdgeInsetsMake(-30, 0, 30, 0)];//修改图片偏移量,上下,左右必须为相反数,否则图片会被压缩
[vc.tabBarItem setTitlePositionAdjustment:UIOffsetMake(0, -30)];//修改文字偏移量

vc是每个根控制器。

到这里,就完美的解决了自定义大按钮及其点击问题,相当简单。

总的来说要点就是:

  • 替换系统的UITabBar
  • 修改点击point的落点
  • 判断什么时候才去修改落点。

最后附上Demo:HitTesting

end~ 如果觉得有用,请点个赞👍,谢谢~

iOS常用开源库

发表于 2017-04-09 | 分类于 开源库

❋Objective-C库❋

网络请求

1. AFNetworking 🔥

浏览器

1. SVWebViewController 🔥

阅读全文 »

UITableView-Auto-Layout-(iOS 8+)

发表于 2017-04-07 | 分类于 自动布局

翻译自stack overflow的一个回答,对于iOS自动布局很有用处。翻译水平有限,且现在公司的app都是iOS 8+,所以只翻译了iOS 8+的部分。如有错误,欢迎指正!谢谢!

阅读全文 »

《Auto-Layout-Guide》笔记

发表于 2017-04-07 | 分类于 自动布局

概念

约束优先级

所有约束都有1-1000的优先级。优先级为1000的约束是必须的。其它约束都是可选的。

阅读全文 »

详解自动布局(Masonry)实现九宫格

发表于 2017-04-07 | 分类于 自动布局

以前写TimeLine中照片九宫格布局是直接计算frame,今天想用自动布局实现。

阅读全文 »

Hello World

发表于 2017-04-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.

阅读全文 »
flowyears

flowyears

9 日志
5 分类
3 标签
RSS
Creative Commons
© 2018 flowyears
由 Hexo 强力驱动
主题 - NexT.Muse