注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

iOS之网络优化

一、正常一个网络请求过程正常一条网络请求需要经过:DNS解析,请求DNS服务器,获取对应的IP地址与服务端建立连接,TCP三次握手,安全协议的同步流程连接建立完成,发送和接受数据,解码数据。优化点:直接使用IP地址,除去DNS解析的流程不要每个请求都重复建立连...
继续阅读 »

一、正常一个网络请求过程

正常一条网络请求需要经过:

  • DNS解析,请求DNS服务器,获取对应的IP地址
  • 与服务端建立连接,TCP三次握手,安全协议的同步流程
  • 连接建立完成,发送和接受数据,解码数据。

优化点:

  • 直接使用IP地址,除去DNS解析的流程
  • 不要每个请求都重复建立连接,复用连接使用同一条连接(长连接)
  • 压缩数据,减少传输数据的大小

二、正常的DNS流程

DNS完整的解析流程:

  1. 先动本地系统缓存获取,偌没有就到最近的DNS服务器获取。

  2. 偌依旧没有,就到主域名服务器获取,每一层都有有缓存。

为了域名解析的实时性,每一层的缓存都有一个过期时间。

缺点:

  1. 缓存时间过长,域名更新不及时,设置的端,单量的DNS解析请求影响速度

  2. 域名劫持,容易被中间人攻击或运营商劫持,把域名解析到第三⽅IP地址,劫持率⽐较⾼。

  3. DNS解析过程不受控制,⽆法保证解析到最快的IP

  4. ⼀次请求只能解析到⼀个域名

处理DNS耗时和防劫持的方式

HTTPDNS原理就是:

⾃⼰做域名解析⼯作,通过HTTP请求后台去拿到域名对应的IP地址,可以解决上述问题。

  • 域名解析与请求分离,所有的请求都直接⽤IP,⽆需DNS解析,APP定时请求HTTPDNS服务器更 新IP地址即可

  • 通过签名等⽅式,保证HTTPDNS请求的安全性,避免被劫持

  • DNS解析有⾃⼰控制,可以确保更具⽤⼾所在地返回就近的IP地址,或者根据客⼾端测速结果使⽤ 速度最快的IP

  • ⼀次请求可以解析多个域名

三、TCP连接耗时优化

解决思路就是:复用接连

  • 不⽤每次重新建⽴连接,⽅案是⾼效的复⽤连接

HTTP1.1版本产生的问题:

  • HTTP1.1 的问题是默认开启的keep-alive,⼀次的连接只能发送接收⼀个请求,同时发起多个请求,会产⽣问题。

  • 若串⾏发送请求,可以⼀直复⽤⼀个连接,但是速度很慢,每个请求都需要等待上⼀个请求完成再发 送,也产⽣了连接的浪费,没⽤充分的利⽤带宽

  • 若并⾏发送请求,那么⾸次请求都要TCP三次握⼿建⽴新的连接,即使第⼆次请求可以复⽤连接池的 连接,但是会导致连接池的连接过多,对服务端资源产⽣浪费,若限制保持的连接数,会有超出的连 接仍要每次建⽴连接。

HTTP2.0 提出了多路复⽤的⽅式解决HTTP1.1的问题:

  • HTTP2 的多路复⽤机制也是复⽤连接,但是它的复⽤的这条连接⽀持同时处理多条请求,所有的请求 都可以并发的在这条连接上进⾏,解决了并发请求需要建⽴多次连接的问题

  • HTTP2 把连接⾥传输的数据都封装成⼀个个stream,每个stream都有⼀个标识,stream的发送和接 收可以是乱序,不依赖顺序,不会有阻塞的问题,接收端可以根据stream的标识区分属于哪个请求, 在进⾏数据拼接,最终得到数据

HTTP2.0 TCP队头阻塞

  • HTTP2还是有问题存在,就是队头阻塞,这是受限于TCP协议,TCP协议为保证数据的可靠性,若传 输过程中有⼀个TCP的包丢失,会等待这个包重传之后,才会处理后续的包.

  • HTTP2的多路复⽤让所 有的请求都在同⼀条连接上,中间有⼀个包丢失,就会阻塞等待重传,所有的请求也会被阻塞

HTTP2.0 TCP队头阻塞解决方案

  • 这个问题需要改变TCP协议,但是TCP协议依赖操作系统实现以及部分硬件的定制,所以改进缓慢。
  • 于是Google提出了QUIC协议,相当于在UDP的基础上在定义⼀套可靠的传输协议,解决TCP的缺陷, 包括队头阻塞,但是客⼾端少有介⼊

四、传输数据优化

传输数据有⼤⼩,数据也会对请求速度有影响。主要优化两个方面:

  • 压缩率,⽽是解压序列化反
    序列化的速度。使⽤Protobuf 可以⽐json的数据量⼩⾄少⼀个数量级

  • 压缩算法的选择,⽬前⽐较好的是Z-Standard HTTP的请求头数据的在HTTP2中也进⾏了压缩。

五、弱⽹优化

根据不同的⽹络设置不同的超时时间

六、数据安全优化

使⽤Https,是基于http协议上的TLS安全协议,安全协议解决了保证安全降低加密成本

1、安全上

  • 使⽤加密算法组合对传输的数据加密,避免被窃听和篡改

  • 认证对⽅⾝份,避免被第三⽅冒充

  • 加密算法保持灵活可更新,防⽌定死算法被破解后⽆法更换,禁⽌已被破解的算法

2、降低加密成本

  • ⽤对称加密算法加密传输的数据,解决⾮对称加密算法的性能低和⻓度限制的问题

  • 缓存安全协议握⼿后的秘钥等数据,加快第⼆次建⽴连接的速度

  • 3、加快握⼿过程2RTT -> 0RTT。加快握⼿的思路,原本客⼾端和服务端需要协商使⽤什么算法后才 可以加密发送数据,变成通过内置的公钥和默认算法,在握⼿的同时,就把数据发送出去,不需要等 待握⼿就开始发送数据,达到0RTT

3.充分利用缓存

  • Get请求可以被缓存,Get请求也是幂等的,
  • 简单的处理缓存的80%需求

使⽤Get请求的代码设置如下:


///objective-c代码 
NSURLCache *urlCache =[[NSURLCache alloc]initWithMemoryCapacity:4 *1024 * 1024
diskCapacity:20 * 1024 *1024 diskPath:nil];
[NSURLCachesetSharedURLCache:urlCache];

4、控制缓存的有效性

1、⽂件缓存:借助ETag或者Last-Modified判断⽂件缓存是不是有效

Last-Modified

  • ⼤多采⽤资源变动后就重新⽣成⼀个链接的做法,但是不排除没有换链接的,这种情况下就需要借助 ETagorLast-Modified判断⽂件的有效性

  • Last-Modified 资源的最后修改时间戳,与缓存时间进⾏对⽐来判断是否过期,请求时返回If- Modified-Since,返回的数据若果没变化http返回的状态码就是403(Not changed),内容为空,节省 传输数据

ETagIf-None-Match

  • HTTP 协议规格说明定义ETag为“被请求变量的实体值” 。另⼀种说法是,ETag是⼀个可以与Web 资源关联的记号(token)。
  • 它是⼀个 hash 值,⽤作 Request 缓存请求头,每⼀个资源⽂件都对应⼀ 个唯⼀的 ETag 值。如果Etag没有改变,则返回状态码304,返回的内容为空

下载的图⽚的格式最好是WebP格式,因为他是同等图⽚质量下最⼩数量的图⽚,可以降低流量损失



作者:枫叶无处漂泊
链接:https://www.jianshu.com/p/66ee6798b99a

收起阅读 »

iOS - 极其强大的性能库DoraemonKit

每一个稍微有点规模的 App,总会自带一些线下的测试功能代码,比如环境切换功能、帧率查看功能等等,这些功能的切换入口往往放在各式各样的入口中,比如一些特殊的手势,双击 statusBar,双击某一个功能区块,或者新建一个 keyWindow 始终至于 App ...
继续阅读 »


每一个稍微有点规模的 App,总会自带一些线下的测试功能代码,比如环境切换功能、帧率查看功能等等,这些功能的切换入口往往放在各式各样的入口中,比如一些特殊的手势,双击 statusBar,双击某一个功能区块,或者新建一个 keyWindow 始终至于 App 最上方等等,而且每一个 App 里面的线下附带功能模块很多是相似的,比如帧率查看、内存和 CPU 监控等等,但是现在基本上都是每个 App 都是自己实现了一份,经历了以上的问题之后,DoKit 就有了它存在的意义。

DoKit 是一个功能平台,能够让每一个 App 快速接入一些常用的或者你没有实现的一些辅助开发工具、测试效率工具、视觉辅助工具,而且能够完美在 Doraemon 面板中接入你已经实现的与业务紧密耦合的一些非通有的辅助工具,并搭配我们的dokit平台,让功能得到延伸,接入方便,便于扩展。


一、平台工具(http://www.dokit.cn)

  1. 【数据Mock】 App接口Mock解决方案,提供一套基于App网络拦截的接口Mock方案,无需修改代码即可完成对于接口数据的Mock。
  2. 【健康体检】 一键式操作,整合DoKit多项工具,数据可视化,快速准确定位问题,让你对app的性能了如指掌。
  3. 【文件同步助手】 通过终端服务,让你的终端空间在平台端完整的展现并提供强大的文件以及数据库操作能力。
  4. 【一机多控】 主从同步,释放人力,让研发测试效率提升看得见

二、常用工具

  1. 【App 信息查看】 快速查看手机信息,App 基础信息、签名相关、权限信息的渠道,避免去手机设置查找或者查看项目源代码的麻烦;
  2. 【开发者选项 Android特有】 一键跳转开发者选项,避免安卓由于平台差异导致的入口不一致
  3. 【本地语言】 一键跳转本地语言,避免安卓由于平台差异导致的入口不一致
  4. 【沙盒浏览】 App 内部文件浏览的功能,支持删除和预览, 并且能通过 AirDrop 或者其他分享方式上传到 PC 中,进行更加细致的操作;
  5. 【MockGPS】 App 能定位到全国各地,支持地图地位和手动输入经纬度;
  6. 【H5任意门】 开发测试同学可以快速输入 H5 页面地址,查看该页面效果;
  7. 【Crash查看】 方便本地打印出出现 Crash 的堆栈;
  8. 【子线程UI】 快速定位哪一些 UI 操作在非主线程中进行渲染,避免不必要的问题;(iOS独有)
  9. 【清除本地数据】 一键删除沙盒中所有数据;
  10. 【NSLog】 把所有 NSLog 信息打印到UI界面,避免没有开发证书无法调试的尴尬;
  11. 【Lumberjack】 每一条 CocoaLumberjack 的日志信息,都在在 App 的界面中显示出来,再也不需要导出日志这么麻烦;(iOS独有)
  12. 【DBView】 通过网页方便快捷的操作应用内数据库,让数据库的调试变得非常优雅;
  13. 【模拟弱网】 限制网速,模拟弱网环境下App的运行情况。(android独有)

三、性能检测

  1. 【帧率】 App 帧率信息提供波形图查看功能,让帧率监控的趋势更加明显;
  2. 【CPU】 App CPU 使用率信息提供波形图查看功能,让 CPU 监控的趋势更加形象;
  3. 【内存】 App 内存使用量信息提供波形图查看功能,让内存监控的趋势更加鲜明;
  4. 【流量监控】 拦截 App 内部流量信息,提供波形图展示、流量概要展示、流量列表展示、流量筛选、流量详情,对流量信息统一拦截,成为我们 App 中自带的 "Charles";
  5. 【卡顿】 锁定 App 出现卡顿的时刻,打印出对应的代码调用堆栈;
  6. 【大图检测】 通过流量监测,找出所有的大小超标的图片,避免下载大图造成的流量浪费和渲染大图带来的CPU消耗。
  7. 【启动耗时】 无侵入的统计出App启动过程的总共耗时;
  8. 【UI层级检查】 检查出每一个页面中层级最深的元素;
  9. 【函数耗时】 从函数级别分析app性能瓶颈;
  10. 【Load】 找出所有的Load方法,并给出耗时分析;(iOS独有)
  11. 【内存泄漏】 找出App中所有的内存泄漏的问题。

四、视觉工具

  1. 【颜色吸管】 方便设计师 UI 捉虫的时候,查看每一个组件的颜色值是否设置正确;
  2. 【组件检查】 可以抓取任意一个UI控件,查看它们的详细信息,包括控件名称、控件位置、背景色、字体颜色、字体大小;
  3. 【对齐标尺】 参考 Android 系统自带测试工具,能够实时捕获屏幕坐标,并且可以查看组件是否对齐;
  4. 【元素边框线】 绘制出每一个 UI 组件的边框,对于组件布局有一定的参考意义。

五、Weex专项工具(CML专项工具)

  1. 【console日志查看】 方便在端上查看每一个Weex文件中的console日志,提供分级和搜索功能;
  2. 【storage缓存查看】 将Weex中的storage模块的本地缓存数据可视化展示;
  3. 【容器信息】 查看每一个打开的Weex页面的基本信息和性能数据;
  4. 【DevTool】 快速开启Weex DevTool的扫码入口。

tips : 如果使用我们滴滴优秀的开源跨端方案 chameleon 也可以集成该工具集合

六、支持自定义的业务工具集成到面板中

统一维护和管理所有的测试模块,详见接入手册

七、微信小程序专项工具


收起阅读 »

微信开源框架-崩溃、卡顿和爆内存Matrix

当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件:WCCrashBlockMonitorPlugin: 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。WCMemoryStatPlu...
继续阅读 »

当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件:

  • WCCrashBlockMonitorPlugin: 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。

  • WCMemoryStatPlugin: 一款性能优化到极致的爆内存监控工具,能够全面捕获应用爆内存时的内存分配以及调用堆栈情况。

特性

WCCrashBlockMonitorPlugin

  • 接入简单,代码无侵入
  • 通过检查 Runloop 运行状态判断应用是否卡顿,同时支持 iOS/macOS 平台
  • 增加耗时堆栈提取,卡顿线程快照日志中附加最近时间最耗时的主线程堆栈

WCMemoryStatPlugin

  • 在应用运行期间获取对象存活以及相应的堆栈信息,在检测到应用爆内存时进行上报
  • 使用平衡二叉树存储存活对象,使用 Hash Table 存储堆栈,将性能优化到极致

使用方法

安装

  • 通过 Cocoapods 安装

    1. 先安装 CocoaPods
    2. 通过 pod repo update 更新 matrix 的 Cocoapods 版本;
    3. 在 Podfile 对应的 target 中,添加 pod 'matrix-wechat',并执行 pod install;
    4. 在项目中使用 Cocoapods 生成的 .xcworkspace运行工程;
    5. 在你的代码文件头引入头文件 #import ,就可以接入微信的性能探针工具了!
  • 通过静态库安装

    1. 获取 Matrix 源码;
    2. 打开命令行,在 matrix/matrix-iOS 代码目录下执行 make 进行编译生成静态库;编译完成后,iOS 平台的库在 matrix/matrix-iOS/build_ios 目录下,macOS 平台的库在 matrix/matrix-iOS/build_macos目录下;
    3. 工程引入静态库:
    • iOS 平台:使用 matrix/matrix-iOS/build_ios 路径下的 Matrix.framework,将 Matrix.framework以静态库的方式引入工程;
    • macOS 平台:使用 matrix/matrix-iOS/build_macos 路径下的 Matrix.framework,将 Matrix.framework 以静态库的方式引入工程。
    1. 添加头文件 #import ,就可以接入微信的性能探针工具了!

启动监控

在以下地方:

  • 程序 main 函数入口;
  • AppDelegate 中的 application:didFinishLaunchingWithOptions:
  • 或者其他应用启动比较早的时间点。

添加类似如下代码,启动插件:

#import 

Matrix *matrix = [Matrix sharedInstance];
MatrixBuilder *curBuilder = [[MatrixBuilder alloc] init];
curBuilder.pluginListener = self; // pluginListener 回调 plugin 的相关事件

WCCrashBlockMonitorPlugin *crashBlockPlugin = [[WCCrashBlockMonitorPlugin alloc] init];
[curBuilder addPlugin:crashBlockPlugin]; // 添加卡顿和崩溃监控

WCMemoryStatPlugin *memoryStatPlugin = [[WCMemoryStatPlugin alloc] init];
[curBuilder addPlugin:memoryStatPlugin]; // 添加内存监控功能

[matrix addMatrixBuilder:curBuilder];

[crashBlockPlugin start]; // 开启卡顿和崩溃监控
// [memoryStatPlugin start];
// 开启内存监控,注意 memoryStatPlugin 开启之后对性能损耗较大,建议按需开启

接收回调获得监控数据

设置 MatrixBuilder 对象中的 pluginListener,实现 MatrixPluginListenerDelegate。

// 设置 delegate

MatrixBuilder *curBuilder = [[MatrixBuilder alloc] init];
curBuilder.pluginListener = <一个遵循 MatrixPluginListenerDelegate 的对象>;

// MatrixPluginListenerDelegate

- (void)onInit:(id)plugin;
- (void)onStart:(id)plugin;
- (void)onStop:(id)plugin;
- (void)onDestroy:(id)plugin;
- (void)onReportIssue:(MatrixIssue *)issue;

各个添加到 MatrixBuilder 的 plugin 会将对应的事件通过 pluginListener 回调。

重要:通过 onReportIssue: 获得 Matrix 处理后的数据,监控数据格式详见:Matrix for iOS/macOS 数据格式说明

Demo

至此,Matrix 已经集成到应用中并且开始收集崩溃、ANR、卡顿和爆内存数据,如仍有疑问,请查看示例:samples/sample-apple/MatrixDemo


常见问题及源码下载:https://github.com/Tencent/matrix#matrix_ios_cn



收起阅读 »

Objective-C & Swift 最轻量级 Hook 方案-SDMagicHook

本文从一个 iOS 日常开发的 hook 案例入手,首先简要介绍了 Objective-C 的动态特性以及传统 hook 方式常见的命名冲突、操作繁琐、hook 链意外断裂、hook 作用范围不可控制等缺陷,然后详细介绍了一套基于消息转发机制的 instanc...
继续阅读 »

本文从一个 iOS 日常开发的 hook 案例入手,首先简要介绍了 Objective-C 的动态特性以及传统 hook 方式常见的命名冲突、操作繁琐、hook 链意外断裂、hook 作用范围不可控制等缺陷,然后详细介绍了一套基于消息转发机制的 instance 粒度的轻量级 hook 方案:SDMagicHook


背景

某年某月的某一天,产品小 S 向开发君小 Q 提出了一个简约而不简单的需求:扩大一下某个 button 的点击区域。小 Q 听完暗自窃喜:还好,这是一个我自定义的 button,只需要重写一下 button 的 pointInside:withEvent:方法即可。只见小 Q 手起刀落在产品小 S 崇拜的目光中轻松完成。代码如下:



次日,产品小 S 又一次满怀期待地找到开发君小 Q:欧巴~,帮我把这个 button 也扩大一下点击区域吧。小 Q 这次却犯了难,心中暗自思忖:这是系统提供的标准 UI 组件里面的 button 啊,我只能拿来用没法改呀,我看你这分明就是故意为难我胖虎!我…我…我.----小 Q 卒。

在这个 case 中,小 Q 的遭遇着实令人同情。但是痛定思痛,难道产品提出的这个问题真的无解吗?其实不然,各位看官静息安坐,且听我慢慢分析:


1. Objective-C 的动态特性

Objective-C 作为一门古老而又灵活的语言有很多动态特性为开发者所津津乐道,这其中尤其以动态类型(Dynamic typing)、动态绑定(Dynamic binding)、动态加载(Dynamic loading)等特性最为著名,许多在其他语言中看似不可能实现的功能也可以在 OC 中利用这些动态特性达到事半功倍的效果。

1.1 动态类型(Dynamic typing)

动态类型就是说运行时才确定对象的真正类型。例如我们可以向一个 id 类型的对象发送任何消息,这在编译期都是合法的,因为类型是可以动态确定的,消息真正起作用的时机也是在运行时这个对象的类型确定以后,这个下面就会讲到。我们甚至可以在运行时动态修改一个对象的 isa 指针从而修改其类型,OC 中 KVO 的实现正是对动态类型的典型应用。

1.2 动态绑定(Dynamic binding)

当一个对象的类型被确定后,其对应的属性和可响应的消息也被确定,这就是动态绑定。绑定完成之后就可以在运行时根据对象的类型在类型信息中查找真正的函数地址然后执行。

1.3 动态加载(Dynamic loading)

根据需求加载所需要的素材资源和代码资源,用户可根据需求加载一些可执行的代码资源,而不是在在启动的时候就加载所有的组件,可执行代码可以含有新的类。

了解了 OC 的这些动态特性之后,让我们再次回顾一下产品的需求要领:产品只想任性地修改任何一个 button 的点击区域,而恰巧这次这个 button 是系统原生组件中的一个子 View。所以当前要解决的关键问题就是如何去改变一个用系统原生类实例化出来的组件的“点击区域检测方法”。刚才在 OC 动态类型特性的介绍中我们说过“消息真正起作用的时机是在运行时这个对象的类型确定以后”、“我们甚至可以在运行时动态修改一个对象的 isa 指针从而修改其类型,OC 中 KVO 的实现正是对动态类型的典型应用”。看到这里,你应该大概有了一些思路,我们不妨照猫画虎模仿 KVO 的原理来实现一下。

2. 初版 SDMagicHook 方案

要想使用这种类似 KVO 的替换 isa 指针的方案,首先需要解决以下几个问题:

2.1 如何动态创建一个新的类

在 OC 中,我们可以调用 runtime 的 objc_allocateClassPairobjc_registerClassPair 函数动态地生成新的类,然后调用 object_setClass 函数去将某个对象的 isa 替换为我们自建的临时类。

2.2 如何给这些新建的临时类命名

作为一个有意义的临时类名,首先得可以直观地看出这个临时类与其基类的关系,所以我们可以这样拼接新的类名[NSString stringWithFormat:@“SDHook*%s”, originalClsName],但这有一个很明显的问题就是无法做到一个对象独享一个专有类,为此我们可以继续扩充下,不妨在类名中加上一个对象的唯一标记–内存地址,新的类名组成是这样的[NSString stringWithFormat:@“SDHook_%s_%p”, originalClsName, self],这次看起来似乎完美了,但在极端的情况下还会出问题,例如我们在一个一万次的 for 循环中不断创建同一种类型的对象,那么就会大概率出现新对象的内存地址和之前已经释放了的对象的内存地址一样,而我们会在一个对象析构后很快就会去释放它所使用的临时类,这就会有概率导致那个新生成的对象正在使用的类被释放了然后就发生了 crash。为解决此类问题,我们需要再在这个临时的类名中添加一个随机标记来降低这种情况发生的概率,最终的类名组成是这样的[NSString stringWithFormat:@“SDHook_%s_%p_%d”, originalClsName, self, mgr.randomFlag]


2.3 何时销毁这些临时类

我们通过 objc_setAssociatedObject 的方式可以为每个 NSObject 对象动态关联上一个 SDNewClassManager 实例,在 SDNewClassManager 实例里面持有当前对象所使用的临时类。当前对象销毁时也会销毁这个 SDNewClassManager 实例,然后我们就可以在 SDNewClassManager 实例的 dealloc 方法里面做一些销毁临时类的操作。但这里我们又不能立即做销毁临时类的操作,因为此时这个对象还没有完全析构,它还在做一些其它善后操作,如果此时去销毁那个临时类必然会造成 crash,所以我们需要稍微延迟一段时间来做这些临时类的销毁操作,代码如下:




好了,到目前为止我们已经实现了第一版 hook 方案,不过这里两个明显的问题:

  1. 每次 hook 都要增加一个 category 定义一个函数相对比较麻烦;
  2. 如果我们在某个 Class 的两个 category 里面分别实现了一个同名的方法就会导致只有一个方法最终能被调用到。

为此,我们研发了第二版针对第一版的不足予以改进和优化。

3. 优化版 SDMagicHook 方案

针对上面提到的两个问题,我们可以通过用 block 生成 IMP 然后将这个 IMP 替换到目标 Selector 对应的 method 上即可,API 示例代码如下:


这个 block 方案看上去确实简洁和方便了很多,但同样面临着任何一个 hook 方案都避不开的问题那就是,如何在 block 里面调用原生的对应方法呢?

3.1 关键点一:如何在 block 里面调用原生方法

在初版方案中,我们在一个类的 category 中增加了一个 hook 专用的方法,然后在完成方法交换之后通过向实例发送 hook 专用的方法自身对应的 selector 消息即可实现对原生方法的回调。但是现在我们是使用的 block 创建了一个“匿名函数”来替换原生方法,既然是匿名函数也就没有明确的 selector,这也就意味着我们根本没有办法在方法交换后找到它的原生方法了!

那么眼下的关键问题就是找到一个合适的 Selector 来映射到被 hook 的原生函数。而目前来看,我们唯一可以在当前编译环境下方便调用且和这个 block 还有一定关联关系的 Selector 就是原方法的 Selector 也就是我们的 demo 中的pointInside:withEvent:了。这样一来pointInside:withEvent:这个 Selector 就变成了一个一对多的映射 key,当有人在外部向我们的 button 发送 pointInside:withEvent:消息时,我们应该首先将 pointInside:withEvent:转发给我们自定义的 block 实现的 IMP,然后当在 block 内部再次向 button 发送 pointInside:withEvent:消息时就将这个消息转发给系统原生的方法实现,如此一来就可以完成了一次完美的方法调度了。

3.2 关键点二:如何设计消息调度方案

在 OC 中要想调度方法派发就需要拿到消息转发的控制权,而要想获得这个消息转发控制权就需要强制让这个 receiver 每次收到这个消息都触发其消息转发机制然后我们在消息转发的过程中做对应的调度。在这个例子中我们将目标 button 的 pointInside:withEvent:对应的 method 的 imp 指针替换为_objc_msgForward,这样每当有人调用这个 button 的 pointInside:withEvent:方法时最终都会走到消息转发方法 forwardInvocation:里面,我们实现这个方法来完成具体的方法调度工作。

因为目标 button 的 pointInside:withEvent:对应的 method 的 imp 指针被替换成了_objc_msgForward,所以我们需要另外新增一个方法 A 和方法 B 来分别存储目标 button 的 pointInside:withEvent:方法的 block 自定义实现和原生实现。然后当需要在自定义的方法内部调用原始方法时通过调用 callOriginalMethodInBlock:这个 api 来显式告知,示例代码如下:



当目标 button 实例收到 pointInside:withEvent:消息时会启用我们自定义的消息调度机制,检查如果 OriginalCallFlag 为 false 就去调用自定义实现方法 A,否则就去调用原始实现方法 B,从而顺利实现一次方法调度。流程图及示例代码如下:




想象这样一个应用场景:有一个全局的 keywindow,各个业务都想监听一下 keywindow 的 layoutSubviews 方法,那我们该如何去管理和维护添加到 keywindow 上的多个 hook 实现之间的关系呢?如果一个对象要销毁了,它需要移除掉之前对 keywindow 的 hook,这时又该如何处理呢?

我们的解决方案是为每个被 hook 的目标原生方法生成一张 hook 表,按照 hook 发生的顺序依次为其生成内部 selector 并加入到 hook 表中。当 keywindow 收到 layoutSubviews 消息时,我们从 hook 表中取出该次消息对应的 hook selector 发送给 keywindow 让它执行对应的动作。如果删除某个 hook 也只需将其对应的 selector 从 hook 表中移除即可。代码如下:




4. 防止 hook 链意外断裂

我们都知道在对某个方法进行 hook 操作时都需要在我们的 hook 代码方法体中调用一下被 hook 的那个原始方法,如果遗漏了此步操作就会造成 hook 链断裂,这样就会导致被 hook 的那个原始方法永远不会被调用到,如果有人在你之前也 hook 了这个方法的话就会导致在你之前的所有 hook 都莫名失效了,因为这是一个很隐蔽的问题所以你往往很难意识到你的 hook 操作已经给其他人造成了严重的问题。

为了方便 hook 操作者快速及时发现这一问题,我们在 DEBUG 模式下增加了一套“hook 链断裂检测机制”,其实现原理大致如下:

前面已经提到过,我们实现了对 hook 目标方法的自定义调度,这就使得我们有机会在这些方法调用结束后检测其是否在方法执行过程中通过 callOriginalMethodInBlock 调用原始方法。如果发现某个方法体不是被 hook 的目标函数的最原始的方法体且这次方法执行结束之后也没有调用过原始方法就会通过 raise(SIGTRAP)方式发送一个中断信号暂停当前的程序以提醒开发者当次 hook 操作没有调用原始方法。



5. SDMagicHook 的优缺点

与传统的在 category 中新增一个自定义方法然后进行 hook 的方案对比,SDMagicHook 的优缺点如下:

优点:

  1. 只用一个 block 即可对任意一个实例的任意方法实现 hook 操作,不需要新增任何 category,简洁高效,可以大大提高你调试程序的效率;
  2. hook 的作用域可以控制在单个实例粒度内,将 hook 的副作用降到最低;
  3. 可以对任意普通实例甚至任意类进行 hook 操作,无论这个实例或者类是你自己生成的还是第三方提供的;
  4. 可以随时添加或去除者任意 hook,易于对 hook 进行管理。

缺点:

  1. 为了保证增删 hook 时的线程安全,SDMagicHook 进行增删 hook 相关的操作时在实例粒度内增加了读写锁,如果有在多线程频繁的 hook 操作可能会带来一点线程等待开销,但是大多数情况下可以忽略不计;
  2. 因为是基于实例维度的所以比较适合处理对某个类的个别实例进行 hook 的场景,如果你需要你的 hook 对某个类的所有实例都生效建议继续沿用传统方式的 hook。

总结

SDMagicHook 方案在 OC 中和 Swift 的 UIKit 层均可直接使用,而且 hook 作用域可以限制在你指定的某个实例范围内从而避免污染其它不相关的实例。Api 设计简洁易用,你只需要花费一分钟的时间即可轻松快速上手,希望我们的这套方案可以给你带来更美妙的 iOS 开发体验。



Github 项目地址:https://github.com/larksuite/SDMagicHook

源码下载:SDMagicHook-master.zip



收起阅读 »

iOS抖音的转场动画

转场调用代码- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { AwemeListV...
继续阅读 »



转场调用代码


- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
AwemeListViewController *awemeVC = [[AwemeListViewController alloc] init];
awemeVC.transitioningDelegate = self; //0

// 1
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
// 2
CGRect cellFrame = cell.frame;
// 3
CGRect cellConvertedFrame = [collectionView convertRect:cellFrame toView:collectionView.superview];

//弹窗转场
self.presentScaleAnimation.cellConvertFrame = cellConvertedFrame; //4

//消失转场
self.dismissScaleAnimation.selectCell = cell; // 5
self.dismissScaleAnimation.originCellFrame = cellFrame; //6
self.dismissScaleAnimation.finalCellFrame = cellConvertedFrame; //7

awemeVC.modalPresentationStyle = UIModalPresentationOverCurrentContext; //8
self.modalPresentationStyle = UIModalPresentationCurrentContext; //9

[self.leftDragInteractiveTransition wireToViewController:awemeVC];
[self presentViewController:awemeVC animated:YES completion:nil];
}

0 处代码使我们需要把当前的类做为转场的代理
1 这里我们要拿出cell这个view
2 拿出当前Cell的frame坐标
3 cell的坐标转成屏幕坐标
4 设置弹出时候需要cell在屏幕的位置坐标
5 设置消失转场需要的选中cell视图
6 设置消失转场原始cell坐标位置
7 设置消失转场最终得cell屏幕坐标位置 用于消失完成回到原来位置的动画
8 设置弹出得vc弹出样式 这个用于显示弹出VC得时候 默认底部使blua的高斯模糊
9 设置当前VC的模态弹出样式为当前的弹出上下文

5~7 步设置的消失转场动画 下面会讲解
这里我们用的是前面讲上下滑的VC对象 大家不必担心 当它是一个普通的UIViewController即可

实现转场所需要的代理

首先在需要实现UIViewControllerTransitioningDelegate这个代理


 #pragma mark -
#pragma mark - UIViewControllerAnimatedTransitioning Delegate
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {

return self.presentScaleAnimation; //present VC
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return self.dismissScaleAnimation; //dismiss VC
}

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
return self.leftDragInteractiveTransition.isInteracting? self.leftDragInteractiveTransition: nil;
}

这里面我们看到我们分别返回了

  • 弹出动画实例self.presentScaleAnimation
  • dismiss动画实例self.dismissScaleAnimation
  • 以及self.leftDragInteractiveTransition实例用于负责转场切换的具体实现

所以我们需要在 当前的VC中声明3个成员变量 并初始化

@property (nonatomic, strong) PresentScaleAnimation *presentScaleAnimation;
@property (nonatomic, strong) DismissScaleAnimation *dismissScaleAnimation;
@property (nonatomic, strong) DragLeftInteractiveTransition *leftDragInteractiveTransition;

并在viewDidLoad:方法中初始化一下

 //转场的两个动画
self.presentScaleAnimation = [[PresentScaleAnimation alloc] init];
self.dismissScaleAnimation = [[DismissScaleAnimation alloc] init];
self.leftDragInteractiveTransition = [DragLeftInteractiveTransition new];

这里我说一下这三个成员都负责啥事
首先DragLeftInteractiveTransition类负责转场的 手势 过程,就是pan手势在这个类里面实现,并继承自UIPercentDrivenInteractiveTransition类,这是iOS7以后系统提供的转场基类必须在interactionControllerForDismissal:代理协议中返回这个类或者子类的实例对象,所以我们生成一个成员变量self.leftDragInteractiveTransition

其次是弹出present和消失dismiss的动画类,这俩类其实是负责简单的手势完成之后的动画.

这两个类都是继承自NSObject并实现UIViewControllerAnimatedTransitioning协议的类,这个协议里面有 需要你复写某些方法返回具体的动画执行时间,和中间过程中我们需要的相关的容器视图以及控制器的视图实例,当我们自己执行完成之后调用相关的block回答告知转场是否完成就行了.

 @implementation PresentScaleAnimation

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 0.3f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
if (CGRectEqualToRect(self.cellConvertFrame, CGRectZero)) {
[transitionContext completeTransition:YES];
return;
}
CGRect initialFrame = self.cellConvertFrame;

UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];

CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
NSTimeInterval duration = [self transitionDuration:transitionContext];

toVC.view.center = CGPointMake(initialFrame.origin.x + initialFrame.size.width/2, initialFrame.origin.y + initialFrame.size.height/2);
toVC.view.transform = CGAffineTransformMakeScale(initialFrame.size.width/finalFrame.size.width, initialFrame.size.height/finalFrame.size.height);

[UIView animateWithDuration:duration
delay:0
usingSpringWithDamping:0.8
initialSpringVelocity:1
options:UIViewAnimationOptionLayoutSubviews
animations:^{
toVC.view.center = CGPointMake(finalFrame.origin.x + finalFrame.size.width/2, finalFrame.origin.y + finalFrame.size.height/2);
toVC.view.transform = CGAffineTransformMakeScale(1, 1);
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
@end

很简单.

消失的动画 同上边差不多

@interface DismissScaleAnimation ()

@end

@implementation DismissScaleAnimation

- (instancetype)init {
self = [super init];
if (self) {
_centerFrame = CGRectMake((ScreenWidth - 5)/2, (ScreenHeight - 5)/2, 5, 5);
}
return self;
}

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 0.25f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
// UINavigationController *toNavigation = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// UIViewController *toVC = [toNavigation viewControllers].firstObject;


UIView *snapshotView;
CGFloat scaleRatio;
CGRect finalFrame = self.finalCellFrame;
if(self.selectCell && !CGRectEqualToRect(finalFrame, CGRectZero)) {
snapshotView = [self.selectCell snapshotViewAfterScreenUpdates:NO];
scaleRatio = fromVC.view.frame.size.width/self.selectCell.frame.size.width;
snapshotView.layer.zPosition = 20;
}else {
snapshotView = [fromVC.view snapshotViewAfterScreenUpdates:NO];
scaleRatio = fromVC.view.frame.size.width/ScreenWidth;
finalFrame = _centerFrame;
}

UIView *containerView = [transitionContext containerView];
[containerView addSubview:snapshotView];

NSTimeInterval duration = [self transitionDuration:transitionContext];

fromVC.view.alpha = 0.0f;
snapshotView.center = fromVC.view.center;
snapshotView.transform = CGAffineTransformMakeScale(scaleRatio, scaleRatio);
[UIView animateWithDuration:duration
delay:0
usingSpringWithDamping:0.8
initialSpringVelocity:0.2
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
snapshotView.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
snapshotView.frame = finalFrame;
} completion:^(BOOL finished) {
[transitionContext finishInteractiveTransition];
[transitionContext completeTransition:YES];
[snapshotView removeFromSuperview];
}];
}



@end
我们重点需要说一下 转场过渡的类DragLeftInteractiveTransition继承自UIPercentDrivenInteractiveTransition负责转场过程,
头文件的声明

@interface DragLeftInteractiveTransition : UIPercentDrivenInteractiveTransition

/** 是否正在拖动返回 标识是否正在使用转场的交互中 */
@property (nonatomic, assign) BOOL isInteracting;


/**
设置需要返回的VC

@param viewController 控制器实例
*/

-(void)wireToViewController:(UIViewController *)viewController;


@end


实现


@interface DragLeftInteractiveTransition ()

@property (nonatomic, strong) UIViewController *presentingVC;
@property (nonatomic, assign) CGPoint viewControllerCenter;
@property (nonatomic, strong) CALayer *transitionMaskLayer;

@end

@implementation DragLeftInteractiveTransition

#pragma mark -
#pragma mark - override methods 复写方法
-(CGFloat)completionSpeed{
return 1 - self.percentComplete;
}

- (void)updateInteractiveTransition:(CGFloat)percentComplete {
NSLog(@"%.2f",percentComplete);

}

- (void)cancelInteractiveTransition {
NSLog(@"转场取消");
}

- (void)finishInteractiveTransition {
NSLog(@"转场完成");
}


- (CALayer *)transitionMaskLayer {
if (_transitionMaskLayer == nil) {
_transitionMaskLayer = [CALayer layer];
}
return _transitionMaskLayer;
}

#pragma mark -
#pragma mark - private methods 私有方法
- (void)prepareGestureRecognizerInView:(UIView*)view {
UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
[view addGestureRecognizer:gesture];
}

#pragma mark -
#pragma mark - event response 所有触发的事件响应 按钮、通知、分段控件等
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {
UIView *vcView = gestureRecognizer.view;
CGPoint translation = [gestureRecognizer translationInView:vcView.superview];
if(!self.isInteracting &&
(translation.x < 0 ||
translation.y < 0 ||
translation.x < translation.y)) {
return;
}
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:{
//修复当从右侧向左滑动的时候的bug 避免开始的时候从又向左滑动 当未开始的时候
CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view];
if (!self.isInteracting && vel.x < 0) {
self.isInteracting = NO;
return;
}
self.transitionMaskLayer.frame = vcView.frame;
self.transitionMaskLayer.opaque = NO;
self.transitionMaskLayer.opacity = 1;
self.transitionMaskLayer.backgroundColor = [UIColor whiteColor].CGColor; //必须有颜色不能透明
[self.transitionMaskLayer setNeedsDisplay];
[self.transitionMaskLayer displayIfNeeded];
self.transitionMaskLayer.anchorPoint = CGPointMake(0.5, 0.5);
self.transitionMaskLayer.position = CGPointMake(vcView.frame.size.width/2.0f, vcView.frame.size.height/2.0f);
vcView.layer.mask = self.transitionMaskLayer;
vcView.layer.masksToBounds = YES;

self.isInteracting = YES;
}
break;
case UIGestureRecognizerStateChanged: {
CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
progress = fminf(fmaxf(progress, 0.0), 1.0);

CGFloat ratio = 1.0f - progress*0.5f;
[_presentingVC.view setCenter:CGPointMake(_viewControllerCenter.x + translation.x * ratio, _viewControllerCenter.y + translation.y * ratio)];
_presentingVC.view.transform = CGAffineTransformMakeScale(ratio, ratio);
[self updateInteractiveTransition:progress];
break;
}
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded:{
CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
progress = fminf(fmaxf(progress, 0.0), 1.0);
if (progress < 0.2){
[UIView animateWithDuration:progress
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGFloat w = [UIScreen mainScreen].bounds.size.width;
CGFloat h = [UIScreen mainScreen].bounds.size.height;
[self.presentingVC.view setCenter:CGPointMake(w/2, h/2)];
self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} completion:^(BOOL finished) {
self.isInteracting = NO;
[self cancelInteractiveTransition];
}];
}else {
_isInteracting = NO;
[self finishInteractiveTransition];
[_presentingVC dismissViewControllerAnimated:YES completion:nil];
}
//移除 遮罩
[self.transitionMaskLayer removeFromSuperlayer];
self.transitionMaskLayer = nil;
}
break;
default:
break;
}
}

#pragma mark -
#pragma mark - public methods 公有方法
-(void)wireToViewController:(UIViewController *)viewController {
self.presentingVC = viewController;
self.viewControllerCenter = viewController.view.center;
[self prepareGestureRecognizerInView:viewController.view];
}

@end


关键的核心代码

[self updateInteractiveTransition:progress];

最后 手势结束

CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
progress = fminf(fmaxf(progress, 0.0), 1.0);
if (progress < 0.2){
[UIView animateWithDuration:progress
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGFloat w = [UIScreen mainScreen].bounds.size.width;
CGFloat h = [UIScreen mainScreen].bounds.size.height;
[self.presentingVC.view setCenter:CGPointMake(w/2, h/2)];
self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} completion:^(BOOL finished) {
self.isInteracting = NO;
[self cancelInteractiveTransition];
}];
}else {
_isInteracting = NO;
[self finishInteractiveTransition];
[_presentingVC dismissViewControllerAnimated:YES completion:nil];
}
//移除 遮罩
[self.transitionMaskLayer removeFromSuperlayer];
self.transitionMaskLayer = nil;


demo及常见问题:https://github.com/sunyazhou13/AwemeDemoTransition



收起阅读 »

UITableView 建模

tableview 是开发中项目中常用的视图控件,并且是重复的使用,布局类似,只是数据源及Cell更改,所以会出现很多重复的内容,并且即使新建一个基础的列表也要重复这些固定逻辑的代码,这对于开发效率很不友好。本文的重点是抽取重复的逻辑代码,简化列表页面的搭建,...
继续阅读 »
tableview 是开发中项目中常用的视图控件,并且是重复的使用,布局类似,只是数据源及Cell更改,所以会出现很多重复的内容,并且即使新建一个基础的列表也要重复这些固定逻辑的代码,这对于开发效率很不友好。
本文的重点是抽取重复的逻辑代码简化列表页面的搭建,达到数据驱动列表

说明:
首先tableview有两个代理delegate 和 datasource(基于单一职责设计规则)
delegate :负责交互事件;
datasource :负责cell创建及数据填充,这也是本文探讨的重点。
(1)基本原则
苹果将tableView的数据通过一个二维数组构建(组,行),这是一个很重要的设计点,要沿着这套规则继续发展,设计模式的继承,才是避免坏代码产生的基础。
(2)组
“组”是这套逻辑的根基先有组再有行,并且列表动态修改的内容都是以为基础,的结构相对固定,因此本文将抽离成一个数据模型而不是接口


#import <Foundation/Foundation.h>
#import "RWCellViewModelProtocol.h"

@interface RWSectionModel : NSObject
/// item数组:元素必须是遵守RWCellViewModel协议
@property (nonatomic, strong) NSMutableArray <id<RWCellViewModel>>*itemsArray;

/// section头部高度
@property (nonatomic, assign) CGFloat sectionHeaderHeight;
/// section尾部高度
@property (nonatomic, assign) CGFloat sectionFooterHeight;
/// sectionHeaderView: 必须是UITableViewHeaderFooterView或其子类,并且遵循RWHeaderFooterDataSource协议
@property (nonatomic, strong) Class headerReuseClass;
/// sectionFooterView: 必须是UITableViewHeaderFooterView或其子类,并且遵循RWHeaderFooterDataSource协议
@property (nonatomic, strong) Class footerReuseClass;

/// headerData
@property (nonatomic, strong) id headerData;
/// footerData
@property (nonatomic, strong) id footerData;
@end

(2)行
最核心的有三大CellCell高度Cell数据
这次的设计参考MVVM设计模式,对于行的要素提取成一个ViewModel,并且ViewModel要做成接口的方式,因为行除了这三个基本的元素外,可能要需要Cell填充的数据,比如titleString,subTitleString,headerImage等等,这样便于扩展。


#ifndef RWCellViewModel_h
#define RWCellViewModel_h

@import UIKit;

@protocol RWCellViewModel <NSObject>
/// Cell 的类型
@property (nonatomic, strong) Class cellClass;
/// Cell的高度: 0 则是UITableViewAutomaticDimension
@property (nonatomic, assign) CGFloat cellHeight;
@end

#endif /* RWCellViewModel_h */

(3)tableView
此处不用使用tableViewController的方式,而使用view的方式,这样嵌入更方便。并且对外提供基本的接口,用于列表数据的获取,及点击事件处理。

备注:
关于数据,这里提供了多组和单组的两个接口,为了减少使用的过程中外部新建RWSectionModel这一步,但是其内部还是基于RWSectionModel这一个模型。


#import <UIKit/UIKit.h>
#import "RWCellViewModelProtocol.h"
#import "RWSectionModel.h"

@protocol RWTableViewDelegate;

@interface RWTableView : UITableView
/// rwdelegate
@property (nonatomic, weak) id<RWTableViewDelegate> rwdelegate;

/// 构建方法
/// @param delegate 是指rwdelegate
- (instancetype)initWithDelegate:(id<RWTableViewDelegate>)delegate;

@end


@protocol RWTableViewDelegate <NSObject>
@optional
/// 多组构建数据
- (NSArray <RWSectionModel*>*)tableViewWithMutilSectionDataArray;

/// 单组构建数据
- (NSArray <id<RWCellViewModel>>*)tableViewWithSigleSectionDataArray;


/// cell点击事件
/// @param data cell数据模型
/// @param indexPath indexPath
- (void)tableViewDidSelectedCellWithDataModel:(id)data indexPath:(NSIndexPath *)indexPath;

RWTableview.m

#pragma mark - dataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
/// 数据源始终保持“二维数组的状态”,即SectionModel中包裹items的方式
if ([self.rwdelegate respondsToSelector:@selector(tableViewWithMutilSectionDataArray)]) {
self.dataArray = [self.rwdelegate tableViewWithMutilSectionDataArray];
return self.dataArray.count;
}
else if ([self.rwdelegate respondsToSelector:@selector(tableViewWithSigleSectionDataArray)]) {
RWSectionModel *sectionModel = [[RWSectionModel alloc]init];
sectionModel.itemsArray = [self.rwdelegate tableViewWithSigleSectionDataArray].mutableCopy;
self.dataArray = @[sectionModel];
return 1;
}
return 0;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:section];
return sectionModel.itemsArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
/// 此处只做Cell的复用或创建
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
if (cell == nil) {
cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}

(4)Cell上子控件的交互事件处理
腾讯QQ部门的大神峰之巅提供了一个很好的解决办法,基于苹果现有的响应链(真的很牛逼),将点击事件传递给下个响应者,而不需要为事件的传递搭建更多的依赖关系。这是一篇鸡汤文章,有很多营养,比如tableview模块化,这也是我接下来要学习的。

#import <UIKit/UIKit.h>
#import "RWEvent.h"

@interface UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event;

@end
#import "UIResponder+RWEvent.h"

@implementation UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event {
[self.nextResponder respondEvent:event];
}

@end


2020年11月18日 更新

鉴于此tableView封装在实际项目遇到的问题进行改善,主要内容如下:
(1)使用分类的方式替换协议
优点:分类能更便捷的扩展原有类,并且使用更方便,不需要再导入协议文件及遵守协议
【RWCellDataSource协议】替换成:【UITableViewCell (RWData)】
【RWHeaderFooterDataSource协议】 替换成:【UITableViewHeaderFooterView (RWData)】

(2)cell高度缓存的勘误
willDisplayCell:中要想获取准确的Cell高度,那么必须在heightForRowAtIndexPath:方法中给Cell赋值,因为系统计算Cell的高度是在这个方法中进行的

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
/// Cell创建
if (cell == nil) {
cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
}
/// Cell赋值
[cell rw_setData:cellViewModel];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
return cellViewModel.cellHeight ? : UITableViewAutomaticDimension;
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
/// 高度缓存
/// 此处高度做一个缓存是为了高度自适应的Cell,避免重复计算的工作量,对于性能优化有些帮助
/// 如果想要在willDisplayCell获取到准确的Cell高度,那么必须在cellForRowAtIndexPath:方法给Cell赋值
/// 同时可以避免由于高度自适应导致Cell的定位不准确,比如置顶或者滑动到某一个Cell的位置
/// 如果自动布局要更新高度,可以将cellViewModel设置为0
cellViewModel.cellHeight = cell.frame.size.height;
}













收起阅读 »

iOS-分页控制器

使用:1、创建方法1.1 导入头文件#import "XLPageViewController.h"1.2 遵守协议@interface ViewController ()<XLPageViewControllerDelegate, XLPageView...
继续阅读 »




使用:

1、创建方法

1.1 导入头文件

#import "XLPageViewController.h"
1.2 遵守协议
@interface ViewController ()<XLPageViewControllerDelegate, XLPageViewControllerDataSrouce>
1.3 创建外观配置类

注:config负责所有的外观配置,defaultConfig方法设定了默认参数,使用时可按需配置。 →Config属性列表

  XLPageViewControllerConfig *config = [XLPageViewControllerConfig defaultConfig];

1.4 创建分页控制器

注:需要把pageViewController添加为当前视图控制器的子视图控制器,才能实现子视图控制器中的界面跳转。

  XLPageViewController *pageViewController = [[XLPageViewController alloc] initWithConfig:config];
pageViewController.view.frame = self.view.bounds;
pageViewController.delegate = self;
pageViewController.dataSource = self;
[self.view addSubview:pageViewController.view];
[self addChildViewController:pageViewController];
2、协议

2.1 XLPageViewControllerDelegate

//回调切换位置
- (void)pageViewController:(XLPageViewController *)pageViewController didSelectedAtIndex:(NSInteger)index;

2.2 XLPageViewControllerDataSrouce

@required

//根据index创建对应的视图控制器,每个试图控制器只会被创建一次。
- (UIViewController *)pageViewController:(XLPageViewController *)pageViewController viewControllerForIndex:(NSInteger)index;
//根据index返回对应的标题
- (NSString *)pageViewController:(XLPageViewController *)pageViewController titleForIndex:(NSInteger)index;
//返回分页数
- (NSInteger)pageViewControllerNumberOfPage;

@optional

//标题cell复用方法,自定义标题cell时用到
- (__kindof XLPageTitleCell *)pageViewController:(XLPageViewController *)pageViewController titleViewCellForItemAtIndex:(NSInteger)index;

3、自定义标题cell

3.1 创建一个XLPageTitleCell的子类

#import "XLPageTitleCell.h"

@interface CustomPageTitleCell : XLPageTitleCell

@end

3.2 注册cell、添加创建cell

//需要先注册cell
[self.pageViewController registerClass:CustomPageTitleCell.class forTitleViewCellWithReuseIdentifier:@"CustomPageTitleCell"];
//自定义标题cell创建方法
- (XLPageTitleCell *)pageViewController:(XLPageViewController *)pageViewController titleViewCellForItemAtIndex:(NSInteger)index {
CustomPageTitleCell *cell = [pageViewController dequeueReusableTitleViewCellWithIdentifier:@"CustomPageTitleCell" forIndex:index];
return cell;
}

3.3 复写cell父类方法

//通过此父类方法配置标题cell是否被选中样式
- (void)configCellOfSelected:(BOOL)selected {

}

//通过此父类方法配置标题cell动画;type:区分是当前选中cell/将要被选中的cell;progress:动画进度0~1
- (void)showAnimationOfProgress:(CGFloat)progress type:(XLPageTitleCellAnimationType)type {

}

4、特殊情况处理

4.1 和子view手势冲突问题

pageViewController的子视图中存在可滚动的子view,例如UISlider、UIScrollView等,如果子view和pageViewController发生滚动冲突时,可设置子view的xl_letMeScrollFirst属性为true。

  UISlider *slider = [[UISlider alloc] init];
slider.xl_letMeScrollFirst = true;
[childVC.view addSubview:slider];

4.2 全屏返回手势问题

pageViewController和全屏返回手势一起使用时,需要将其它手势的delegate的类名添加到respondOtherGestureDelegateClassList属性中。当滚动到第一个分页时,向右滑动会优先响应全屏返回。以FDFullscreenPopGesture为例:

self.pageViewController.respondOtherGestureDelegateClassList = @[@"_FDFullscreenPopGestureRecognizerDelegate"];

5、注意事项

使用时需注意标题不要重复标题是定位ViewController的唯一ID。


源码下载:XLPageViewController-master.zip 

常见问题及demo:https://github.com/mengxianliang/XLPageViewController





收起阅读 »

iOS - 呼吸动画库

先看效果

先看效果



需求和实现思路

具体要求

  • 内部头像呼吸放大缩小 无限循环
  • 每次放大同时需要背景还有一张图也放大 并且透明
  • 点击缩放整个背景视图


实现思路

首先 需要使用创建一个Layer 装第一个无限放大缩小的呼吸的图 背景也需要一个Layer 做 放大+透明度渐变的动画组并且也放置一张需要放大渐变的图片

最后点击触发. 添加一个一次性的缩放动画即可

呼吸动画layer和动画

呼吸layer

CALayer *layer = [CALayer layer];
layer.position = CGPointMake(kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f);
layer.bounds = CGRectMake(0, 0, kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f);
layer.backgroundColor = [UIColor clearColor].CGColor;
layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage);
layer.contentsGravity = kCAGravityResizeAspect;
[self.heartView.layer addSublayer:layer];
复制代码

kHeartSizeHeight 和kHeartSizeWidth 是常量 demo中写好了100

加帧动画

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
animation.values = @[@1.f, @1.4f, @1.f];
animation.keyTimes = @[@0.f, @0.5f, @1.f];
animation.duration = 1; //1000ms
animation.repeatCount = FLT_MAX;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animation setValue:kBreathAnimationKey forKey:kBreathAnimationName];
[layer addAnimation:animation forKey:kBreathAnimationKey];
复制代码

差值器也可以自定义 例如:

[CAMediaTimingFunction functionWithControlPoints:0.33 :0 :0.67 :1]
复制代码

这里我做的持续时常1秒

放大渐变动画group

创建新layer

CALayer *breathLayer = [CALayer layer];
breathLayer.position = layer.position;
breathLayer.bounds = layer.bounds;
breathLayer.backgroundColor = [UIColor clearColor].CGColor;
breathLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage);
breathLayer.contentsGravity = kCAGravityResizeAspect;
[self.heartView.layer insertSublayer:breathLayer below:layer];
//[self.heartView.layer addSublayer:breathLayer];
复制代码

这里用的是放在 呼吸layer后边 如果想放在呼吸layer前边 就把里面注释打开 然后注掉 inert那行代码

动画组 包含 放大 渐变


//缩放
CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.values = @[@1.f, @2.4f];
scaleAnimation.keyTimes = @[@0.f,@1.f];
scaleAnimation.duration = animation.duration;
scaleAnimation.repeatCount = FLT_MAX;
scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
//透明度
CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animation];
opacityAnimation.keyPath = @"opacity";
opacityAnimation.values = @[@1.f, @0.f];
opacityAnimation.duration = 0.4f;
opacityAnimation.keyTimes = @[@0.f, @1.f];
opacityAnimation.repeatCount = FLT_MAX;
opacityAnimation.duration = animation.duration;
opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];

//动画组
CAAnimationGroup *scaleOpacityGroup = [CAAnimationGroup animation];
scaleOpacityGroup.animations = @[scaleAnimation, opacityAnimation];
scaleOpacityGroup.removedOnCompletion = NO;
scaleOpacityGroup.fillMode = kCAFillModeForwards;
scaleOpacityGroup.duration = animation.duration;
scaleOpacityGroup.repeatCount = FLT_MAX;
[breathLayer addAnimation:scaleOpacityGroup forKey:kBreathScaleName];
复制代码

点击缩放动画

跟第一个一样 只不过 执行次数默认一次 执行完就可以了

- (void)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
animation.values = @[@1.0f, @0.8f, @1.f];
animation.keyTimes = @[@0.f,@0.5f, @1.f];
animation.duration = 0.35f;
animation.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[self.heartView.layer addAnimation:animation forKey:@""];
}
复制代码

手势触发的时候 调用一下 

源码及demo地址:https://github.com/sunyazhou13/BreathAnimation



iOS 攻防 - ptrace

在破解一款App的时候,在实际破解之前肯定是在做调试。LLDB之所以能附加进程时因为debugserver,而debugserver附加是通过ptrace函数来trace process的。ptrace是系统函数,此函数提供一个进程去监听和控制另一个进程,并且...
继续阅读 »

在破解一款App的时候,在实际破解之前肯定是在做调试。LLDB之所以能附加进程时因为debugserver,而debugserver附加是通过ptrace函数来trace process的。
ptrace是系统函数,此函数提供一个进程去监听和控制另一个进程,并且可以检测被控制进程的内存和寄存器里面的数据。ptrace可以用来实现断点调试和系统调用跟踪。

一、反调试ptrace

iOS#import 头文件不能直接导入,所以需要我们自己导出头文件引入调用。当然也可以声明ptrace函数直接调用。

1.1 ptrace 头文件

  1. 直接创建一个macOS程序导入#import 头文件,点进去拷贝生成一个.h文件就可以了:


/*
* Copyright (c) 2000-2005 Apple Computer, Inc. All rights reserved.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. The rights granted to you under the License
* may not be used to create, or enable the creation or redistribution of,
* unlawful or unlicensed copies of an Apple operating system, or to
* circumvent, violate, or enable the circumvention or violation of, any
* terms of an Apple operating system software license agreement.
*
* Please obtain a copy of the License at
*
http://www.opensource.apple.com/apsl/
and read it before using this file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_END@
*/

/* Copyright (c) 1995 NeXT Computer, Inc. All Rights Reserved */
/*-
* Copyright (c) 1984, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* @(#)ptrace.h 8.2 (Berkeley) 1/4/94
*/


#ifndef _SYS_PTRACE_H_
#define _SYS_PTRACE_H_

#include
#include

enum {
ePtAttachDeprecated __deprecated_enum_msg("PT_ATTACH is deprecated. See PT_ATTACHEXC") = 10
};


#define PT_TRACE_ME 0 /* child declares it's being traced */
#define PT_READ_I 1 /* read word in child's I space */
#define PT_READ_D 2 /* read word in child's D space */
#define PT_READ_U 3 /* read word in child's user structure */
#define PT_WRITE_I 4 /* write word in child's I space */
#define PT_WRITE_D 5 /* write word in child's D space */
#define PT_WRITE_U 6 /* write word in child's user structure */
#define PT_CONTINUE 7 /* continue the child */
#define PT_KILL 8 /* kill the child process */
#define PT_STEP 9 /* single step the child */
#define PT_ATTACH ePtAttachDeprecated /* trace some running process */
#define PT_DETACH 11 /* stop tracing a process */
#define PT_SIGEXC 12 /* signals as exceptions for current_proc */
#define PT_THUPDATE 13 /* signal for thread# */
#define PT_ATTACHEXC 14 /* attach to running process with signal exception */

#define PT_FORCEQUOTA 30 /* Enforce quota for root */
#define PT_DENY_ATTACH 31

#define PT_FIRSTMACH 32 /* for machine-specific requests */

__BEGIN_DECLS


int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);


__END_DECLS

#endif /* !_SYS_PTRACE_H_ */

  1. 直接声明函数:
int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
  • _request:要处理的事情
  • _pid:要操作的进程
  • _addr_data:取决于_pid参数,要传递的数据地址和数据本身。

1.2 ptrace调用

//告诉系统当前进程拒绝被debugserver附加
ptrace(PT_DENY_ATTACH, 0, 0, 0);
//ptrace(31, 0, 0, 0);

PT_DENY_ATTACH表示拒绝附加,值为31。如果仅仅是声明函数就传31就好了。_pid0表示当前进程。这里不传递任何数据。

分别在以下方法中调用

  1. load方法中调用:
+ (void)load {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
  1. constructor中调用:
__attribute__((constructor)) static void entry() {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
  1. main函数中调用:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;

@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}


  1. didFinishLaunchingWithOptions中调用:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
return YES;
}

123情况下Xcode启动调试后调试直接断开,App能正常操作不能调试。4在调试情况下App直接闪退,正常打开没问题。同时调用的情况下以第一次为准。

也就是说 :ptracemain函数之后调用App会直接闪退,main以及之前调用会停止进程附加,以第一次调用为准。正常打开App没有问题,只影响LLDB调试。

通过上面的验证说明在程序没有加载的时候调用ptrace会设置一个标志,后续程序就不会被附加了,如果在已经被附加了的情况下调用ptrace会直接退出(因为这里ptrace附加传递的pid0主程序本身)。


PT_DENY_ATTACH
This request is the other operation used by the traced process; it allows a process that is not currently being traced to deny future traces by its parent. All other arguments are ignored. If the process is currently being traced, it will exit with the exit status of ENOTSUP; otherwise, it sets a flag that denies future traces. An attempt by the parent to trace a process which has set this flag will result in a segmentation violation in the parent.
ENOTSUP含义如下:
define ENOTSUP 45 //Operation not supported
之前在手机端通过debugserver附加程序直接报错11,定义如下:
PT_DETACH 11 // stop tracing a process

二、 破解ptrace

ptrace的特征:附加不了、Xcode运行闪退/停止附加、使用正常。

既然ptrace可以组织调试,那么我们只要Hook了这个函数绕过PT_DENY_ATTACH的调用就可以了。首先想到的就是fishhook


#import "fishhook.h"

int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);

int hp_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data){
if (_request != 31) {//不是拒绝附加
return ptrace_p(_request, _pid, _addr, _data);
}
return 0;
}

void hp_hook_ptrace() {
struct rebinding ptrace_rb;
ptrace_rb.name ="ptrace";
ptrace_rb.replacement = hp_ptrace;
ptrace_rb.replaced = (void *)&ptrace_p;

struct rebinding bds[] = {ptrace_rb};
rebind_symbols(bds, 1);
}

+ (void)load {
hp_hook_ptrace();
}

这样就能够进行附加调试了。


三、防止ptrace被破解


3.1 提前Hook防止ptrace被Hook


既然ptrace能够被Hook,那么自己先Hookptrace。调用的时候直接调用自己存储的地址就可以了。我们可以在自己的项目中增加一个Framework。这个库在Link Binary With Libraries中尽可能的靠前。这与dyld加载动态库的顺序有关。
这样就可以不被ptrace Hook了。代码逻辑和1.2中相同,只不过调用要换成ptrace_p
记的头文件中导出ptrace_p

CF_EXPORT int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);

创建一个Monkey工程,将3.1生成的.app包拖入工程重签名,这个时候主程序通过调用ptrace已经不能阻止我们调试了,但是调用ptrace_p的地方Monkey Hook不到了。

3.2 修改二进制破解提前Hook ptrace


Monkey的工程中打ptrace符号断点:

这个时候可以看到是didFinishLaunchingWithOptions中调用了ptrace_p函数:
Hopper打开MachO文件找到didFinishLaunchingWithOptions方法:

然后一直点下去找到ptrace_p是属于Inject.framework的:

.appFrameworks中找到Inject.frameworkHopper打开,可以看到_rebind_symbols,上面的参数是ptrace

这里我们可以直接修改ptrace让先Hook的变成另外一个函数,但是有风险点是App内部调用ptrace_p的时候如果没有判断空就crash了。如果判断了可以这么处理。
还有另外一个方式是修改didFinishLaunchingWithOptions代码中的汇编,修改blr x8NOP这样就绕过了ptrace_p的调用。





作者:HotPotCat
链接:https://www.jianshu.com/p/9ed2de5e7497












收起阅读 »

iOS 自定义键盘

很多项目中都使用自定义键盘,实现自定义键盘有很多方法,本文讲的是修改UITextField/UITextView的inputView来实现自定义键盘。如何修改已经知道了,但是怎么修改。有两种思路:自定义CustomTextField/CustomTextVie...
继续阅读 »

很多项目中都使用自定义键盘,实现自定义键盘有很多方法,本文讲的是修改UITextField/UITextView的inputView来实现自定义键盘。
如何修改已经知道了,但是怎么修改。有两种思路:

  1. 自定义CustomTextField/CustomTextView,直接实现如下代码
textField.inputView = customView;   
textView.inputView = customView;

但是这样写有个弊端,就是通用性不强。比如项目中可能要实现某个具体业务逻辑,这个textField/textView是继承ATextField/ATextView,其他地方又有用到的是继承BTextField/BTextView,那我们再写代码时候,可能需要写n个自定义textField/textView,用起来就非常麻烦了,所以这种方法不推荐。

  1. 使用分类来实现自定义键盘
    思路就是在分类中增加一个枚举,这个枚举定义了不同类型的键盘
typedef NS_ENUM(NSUInteger, SJKeyboardType)
{
SJKeyboardTypeDefault, // 使用默认键盘
SJKeyboardTypeNumber // 使用自定义数字键盘
// 还可以根据需求 自定义其他样式...
};

写一个属性,来标记键盘类型

@property (nonatomic, assign) SJKeyboardType sjKeyboardType;
在.m文件中实现getter和setter方法

static NSString *sjKeyboardTypeKey = @"sjKeyboardTypeKey";
- (SJKeyboardType)sjKeyboardType
{
return [objc_getAssociatedObject(self, &sjKeyboardTypeKey) integerValue];
}

- (void)setSjKeyboardType:(SJKeyboardType)sjKeyboardType
{
objc_setAssociatedObject(self, &sjKeyboardTypeKey, @(sjKeyboardType), OBJC_ASSOCIATION_ASSIGN);
[self setupKeyboard:sjKeyboardType];
}

在set方法中来实现自定义键盘视图设置及对应点击方法实现

- (void)setupKeyboard:(SJKeyboardType)sjKeyboardType
{

switch (sjKeyboardType) {
case SJKeyboardTypeDefault:
break;
case SJKeyboardTypeNumber: {
SJCustomKeyboardView *numberInputView = [[[NSBundle mainBundle] loadNibNamed:@"SJCustomKeyboardView" owner:self options:nil] lastObject];
numberInputView.frame = CGRectMake(0, 0, SJSCREEN_WIDTH, SJNumberKeyboardHeight + SJCustomKeyboardBottomMargin);
self.inputView = numberInputView;
numberInputView.textFieldReplacementString = ^(NSString * _Nonnull string) {
BOOL canEditor = YES;
if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) {
canEditor = [self.delegate textField:self shouldChangeCharactersInRange:NSMakeRange(self.text.length, 0) replacementString:string];
}

if (canEditor) {
[self replaceRange:self.selectedTextRange withText:string];
}
};
numberInputView.textFieldShouldDelete = ^{
BOOL canEditor = YES;
if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)] && self.text.length) {
canEditor = [self.delegate textField:self shouldChangeCharactersInRange:NSMakeRange(self.text.length - 1, 1) replacementString:@""];
}
if (canEditor) {
[self deleteBackward];
}
};
numberInputView.textFieldShouldClear = ^{
BOOL canClear = YES;
if ([self.delegate respondsToSelector:@selector(textFieldShouldClear:)]) {
canClear = [self.delegate textFieldShouldClear:self];
}
if (canClear) {
[self setText:@""];
}
};
numberInputView.textFieldShouldReturn = ^{
if ([self.delegate respondsToSelector:@selector(textFieldShouldReturn:)]) {
[self.delegate textFieldShouldReturn:self];
}
};
break;
}
}
}
之后就需要实现自定义键盘视图,这里需要注意一点,就是如果使用新建子类实现自定义键盘,个人感觉按钮响应用代理实现会看起来逻辑更清晰

/* 用代理看的更清楚 但是分类不能实现代理 所以只能用block实现回调 如果自定义textField可以用代理 @protocol SJCustomKeyboardViewDelegate - (void)textFieldReplacementString:(NSString *_Nullable)string; - (BOOL)textFieldShouldDelete; - (BOOL)textFieldShouldClear; - (BOOL)textFieldShouldReturn; @end */

但是分类不能实现代理,所以只能用block来实现回调


@property (nonatomic, copy) void (^textFieldReplacementString)(NSString *string);
@property (nonatomic, copy) void (^textFieldShouldDelete)(void);
@property (nonatomic, copy) void (^textFieldShouldClear)(void);
@property (nonatomic, copy) void (^textFieldShouldReturn)(void);

.m中只需要实现按钮的点击方法和对应的回调方法即可。
这样好处是只需要引入头文件,修改一个属性即可实现自定义键盘,不会影响项目中其他的业务逻辑。

self.textField = [[UITextField alloc] initWithFrame:CGRectMake(20, 100, SJSCREEN_WIDTH - 40, 40)];  
self.textField.placeholder = @"input";
self.textField.borderStyle = UITextBorderStyleBezel;
self.textField.delegate = self;
[self.view addSubview:self.textField];

self.textField.sjKeyboardType = SJKeyboardTypeNumber;





收起阅读 »

iOS 任务调度器:为 CPU 和内存减负

GitHub 地址:YBTaskScheduler支持 cocopods,使用简便,效率不错,一个性能优化的基础组件。前言前些时间有好几个技术朋友问过笔者类似的问题:主线程需要执行大量的任务导致卡顿如何处理?异步任务量级过大导致 CPU 和内存压力过高如何优化...
继续阅读 »

GitHub 地址:YBTaskScheduler
支持 cocopods,使用简便,效率不错,一个性能优化的基础组件。

前言

前些时间有好几个技术朋友问过笔者类似的问题:主线程需要执行大量的任务导致卡顿如何处理?异步任务量级过大导致 CPU 和内存压力过高如何优化?

解决类似的问题可以用几个思路:降频、淘汰、优先级调度。

本来解决这些问题并不需要很复杂的代码,但是涉及到一些 C 代码并且要注意线程安全的问题,所以笔者就做了这样一个轮子,以解决任务调度引发的性能问题。

本文讲述 YBTaskScheduler 的原理,读者朋友需要有一定的 iOS 基础,了解一些性能优化的知识,基本用法可以先看看 GitHub README,DEMO 中也有一个相册列表的应用案例。

一、需求分析

就拿 DEMO 中的案例来说明,一个显示相册图片的列表:


实现图中业务,必然考虑到几个耗时操作:

1、从相册读取图片
2、解压图片
3、圆角处理
4、绘制图片

理所当然的想到处理方案(DEMO中有实现):

1、异步读取图片
2、异步裁剪图片为正方形(这个过程中就解压了)
3、异步裁剪圆角
4、回到主线程绘制图片

一整套流程下来,貌似需求很好的解决了,但是当快速滑动列表时,会发现 CPU 和内存的占用会比较高(这取决于从相册中读取并显示多大的图片)。当然 DEMO 中按照屏幕的物理像素处理,就算不使用任务调度器组件快速滑动列表也基本不会有掉帧的现象。考虑到老旧设备或者技术人员的水平,很多时候这种需求会导致严重的 CPU 和内存负担,甚至导致闪退。

以上处理方案可能存在的性能瓶颈:

从相册读取图片、裁剪图片,处理圆角、主线程绘制等操作会导致 CPU 计算压力过大。
同时解压的图片、同时绘制的图片过多导致内存峰值飙升(更不要说做了图片的缓存)。
任何一种情况都可能导致客户端卡死或者闪退,结合业务来分析问题,会发现优化的思路还是不难找到:

· 滑出屏幕的图片不会存在绘制压力,而当前屏幕中的图片会在一个 RunLoop 循环周期绘制,可能造成掉帧。所以可以减少一个 RunLoop 循环周期所绘制的图片数量。
· 快速滑动列表,大量的异步任务直接交由 CPU 执行,然而滑出屏幕的图片已经没有处理它的意义了。所以可以提前删除掉已经滑出屏幕的异步任务,以此来降低 CPU 和内存压力。

没错, YBTaskScheduler 组件就是替你做了这些事情 ,而且还不止于此。

二、命令模式与 RunLoop

想要管理这些复杂的任务,并且在合适的时机调用它们,自然而然的就想到了命令模式。意味着任务不能直接执行,而是把任务作为一个命令装入容器。

在 Objective-C 中,显然 Block 代码块能解决延迟执行这个问题:

[_scheduler addTask:^{
/*
具体任务代码
解压图片、裁剪图片、访问磁盘等
*/
}];

然后组件将这些代码块“装起来”,组件由此“掌握”了所有的任务,可以自由的决定何时调用这些代码块,何时对某些代码块进行淘汰,还可以实现优先级调度。

既然是命令模式,还差一个 Invoker (调用程序),即何时去触发这些任务。结合 iOS 的技术特点,可以监听 RunLoop 循环周期来实现:

static void addRunLoopObserver() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
taskSchedulers = [NSHashTable weakObjectsHashTable];
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit, true, 0xFFFFFF, runLoopObserverCallBack, NULL);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}

然后在回调函数中进行任务的调度。

三、策略模式

考虑到任务的淘汰策略和优先级调度,必然需要一些高效数据结构来支撑,为了提高处理效率,笔者直接使用了 C++ 的数据结构:deque和priority_queue。

因为要实现任务淘汰,所以使用deque双端队列来模拟栈和队列,而不是直接使用stack和queue。使用priority_queue优先队列来处理自定义的优先级调度,它的缺点是不能删除低优先级节点,为了节约时间成本姑且够用。

具体的策略:

栈:后加入的任务先执行(可以理解为后加入的任务优先级高),优先淘汰先加入的任务。
队列:先加入的任务先执行(可以理解为先加入的任务优先级高),优先淘汰后加入的任务。
优先队列:自定义任务优先级,不支持任务淘汰。
实际上组件是推荐使用栈和队列这两种策略,因为插入和取出的时间复杂度是常数级的,需要定制任务的优先级时才考虑使用优先队列,因为其插入复杂度是 O(logN) 的。

至此,整个组件的业务是比较清晰了,组件需要让这三种处理方式可以自由的变动,所以采用策略模式来处理,下面是 UML 类图:


嗯,这是个挺标准的策略模式。

四、线程安全

由于任务的调度可能在任意线程,所以必须要做好容器(栈、队列、优先队列)访问的线程安全问题,组件是使用pthread_mutex_t和dispatch_once来保证线程安全,同时笔者尽量减少临界区来提高性能。值得注意的是,如果不会存在线程安全的代码就不要去加锁了。

后语

部分技术细节就不多说了,组件代码量比较少,如果感兴趣可以直接看源码。实际上这个组件的应用场景并不是很多,在项目稳定需要做深度的性能优化时可能会比较需要它,并且希望使用它的人也能了解一些原理,做到胸有成竹,才能灵活的运用。

转自:https://www.jianshu.com/p/f2a610c77d26

收起阅读 »

OLLVM代码混淆移植与使用

简介OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。github上地址是https://github.com/obfusc...
继续阅读 »

简介

OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。github上地址是https://github.com/obfuscator-llvm/obfuscator,只不过仅更新到llvm的4.0,2017年开始就没在更新。

移植

OLLVM如果自己想拿最新版的LLVM和Clang进行移植功能其实也并不是很难,整理一下其实改动很小,接下来将会讲一下移植的方法。

个人整理

先放一下个人移植好的版本地址https://github.com/heroims/obfuscator.git,个人fork原版后又加入了llvm5.0,6.0,7.0以及swift-llvm5.0的版本,应该能满足大部分需求了,如果有新版本下面的讲解,各位也可以自己动手去下载自己需要的llvm和clang进行移植。git上的提交每次都很独立如下图,方便各位cherry-pick。


下载LLVM

llvm地址:https://github.com/llvm-mirror
swift-llvm地址:https://github.com/apple
大家可以从上面的地址下载最新的自己需要的llvm和clang

#下载llvm源码
wget https://codeload.github.com/llvm-mirror/llvm/zip/release_70
unzip llvm-release_70.zip
mv llvm-release_70 llvm


#下载clang源码
wget https://codeload.github.com/llvm-mirror/clang/zip/release_70
unzip clang-release_70.zip
mv clang-release_70 llvm/tools/clang

添加混淆代码

如果用git的话只需要执行git cherry-pick xxxx把xxxx换成对应的我的版本上的提交哈希填上即可。极度推荐用git搞定。

如果手动一点点加的话,第一步就是把我改过的OLLVM文件夹里/include/llvm/Transforms/Obfuscation和/lib/llvm/Transforms/Obfuscation移动到刚才下载好的llvm源码文件夹相同的位置。

git clone https://github.com/heroims/obfuscator.git
cd obfuscator
git checkout llvm-7.0
cp include/llvm/Transforms/Obfuscation llvm/include/llvm/Transforms/Obfuscation
cp lib/llvm/Transforms/Obfuscation llvm/lib/llvm/Transforms/Obfuscation

然后手动修改8个文件如下:









编译

mkdir build
cd build
#如果不想跑测试用例加上-DLLVM_INCLUDE_TESTS=OFF
cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_CREATE_XCODE_TOOLCHAIN=ON ../obfuscator/
make -j7

使用

这里原版提供了3种混淆方式分别是控制流扁平化,指令替换,虚假控制流程,用起来都是加cflags的方式。下面简单说下这几种模式。

控制流扁平化

这个模式主要是把一些if-else语句,嵌套成do-while语句

-mllvm -fla:激活控制流扁平化
-mllvm -split:激活基本块分割。在一起使用时改善展平。
-mllvm -split_num=3:如果激活了传递,则在每个基本块上应用3次。默认值:1

指令替换

这个模式主要用功能上等效但更复杂的指令序列替换标准二元运算符(+ , – , & , | 和 ^)

-mllvm -sub:激活指令替换
-mllvm -sub_loop=3:如果激活了传递,则在函数上应用3次。默认值:1

虚假控制流程

这个模式主要嵌套几层判断逻辑,一个简单的运算都会在外面包几层if-else,所以这个模式加上编译速度会慢很多因为要做几层假的逻辑包裹真正有用的代码。

另外说一下这个模式编译的时候要浪费相当长时间包哪几层不是闹得!

-mllvm -bcf:激活虚假控制流程
-mllvm -bcf_loop=3:如果激活了传递,则在函数上应用3次。默认值:1
-mllvm -bcf_prob=40:如果激活了传递,基本块将以40%的概率进行模糊处理。默认值:30

上面说完模式下面讲一下几种使用方式

直接用二进制文件

直接使用编译的二进制文件build/bin/clang test.c -o test -mllvm -sub -mllvm -fla -mllvm -bcf

NDK集成

这里分为工具链的制作和项目里的配置。

制作Toolchains

这里以修改最新的ndk r18为例,老的ndk版本比这更容易都在ndk-bundle/toolchains里放着需要修改的文件。

#复制ndk的toolschain里的llvm
cp -r ndk-bundle/toolchains/llvm ndk-bundle/toolchains/ollvm
#删除prebuilt文件夹下的文件夹的bin和lib64,prebuilt文件夹下根据系统不同命名也不同
rm -rf ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/bin
rm -rf ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/lib64
#把我们之前编译好的ollvm下的bin和lib移到我们刚才删除bin和lib64的目录下
mv build/bin ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/
mv build/lib ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/
#复制ndk-bundle⁩/⁨build⁩/⁨core⁩/⁨toolchains的文件夹,这里根据自己对CPU架构的需求自己复制然后修改
cp -r ndk-bundle⁩/⁨build⁩/⁨core⁩/⁨toolchains/arm-linux-androideabi-clang⁩ ndk-bundle⁩/⁨build⁩/⁨core⁩/⁨toolchains/arm-linux-androideabi-clang-ollvm

最后把arm-linux-androideabi-clang-ollvm里的setup.mk文件进行修改

TOOLCHAIN_NAME := ollvm
TOOLCHAIN_ROOT := $(call get-toolchain-root,$(TOOLCHAIN_NAME))
TOOLCHAIN_PREFIX := $(TOOLCHAIN_ROOT)/bin

config.mk里是CPU架构,刚才是复制出来的所以不用修改,但如果要添加其他的自定义架构需要严格按照格式规范命名最初的文件夹,如mips的需要添加文件夹mipsel-linux-android-clang-ollvm,setup.mk和刚才的修改一样即可。

项目中配置

到了项目里还需要修改两个文件:
在Android.mk 中添加混淆编译参数

LOCAL_CFLAGS += -mllvm -sub -mllvm -bcf -mllvm -fla

Application.mk中配置NDK_TOOLCHAIN_VERSION

#根据需要添加
APP_ABI := x86 armeabi-v7a x86_64 arm64-v8a mips armeabi mips64
#使用刚才我们做好的编译链
NDK_TOOLCHAIN_VERSION := ollvm

Visual Studio集成

编译ollvm的时候,使用cmake-gui选择Visual Studio2015或者命令行选择cmake -G "Visual Studio 14 2015" -DCMAKE_BUILD_TYPE=Release ../obfuscator/
然后cmake会产生一个visual studio工程,用vs编译即可!
至于将Visual Studio的默认编译器换成clang编译,参考https://www.ishani.org/projects/ClangVSX/

Visual Studio2015起官方开始支持Clang,具体做法:
文件->新建->项目->已安装->Visual C++->跨平台->安装Clang with Microsoft CodeGen
Clang是一个完全不同的命令行工具链,这时候可以在工程配置中,平台工具集选项里找到Clang,然后使用ollvm的clang替换该clang即可。

XCode集成

XCode里集成需要看版本,XCode10之前和之后是一个分水岭,XCode9之前和之后有一个小配置不同。

XCode10以前

$ cd /Applications/Xcode.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/
$ sudo cp -r Clang\ LLVM\ 1.0.xcplugin/ Obfuscator.xcplugin
$ cd Obfuscator.xcplugin/Contents/
$ sudo plutil -convert xml1 Info.plist
$ sudo vim Info.plist

修改:

<string>com.apple.compilers.clang</string> -> <string>com.apple.compilers.obfuscator</string>
<string>Clang LLVM 1.0 Compiler Xcode Plug-in</string> -> <string>Obfuscator Xcode Plug-in</string>

执行:

$ sudo plutil -convert binary1 Info.plist
$ cd Resources/
$ sudo mv Clang\ LLVM\ 1.0.xcspec Obfuscator.xcspec
$ sudo vim Obfuscator.xcspec

修改:

<key>Description</key>
<string>Apple LLVM 8.0 compiler</string> -> <string>Obfuscator 4.0 compiler</string>
<key>ExecPath</key>
<string>clang</string> -> <string>/path/to/obfuscator_bin/clang</string>
<key>Identifier</key>
<string>com.apple.compilers.llvm.clang.1_0</string> -> <string>com.apple.compilers.llvm.obfuscator.4_0</string>
<key>Name</key>
<string>Apple LLVM 8.0</string> -> <string>Obfuscator 4.0</string>
<key>Vendor</key>
<string>Apple</string> -> <string>HEIG-VD</string>
<key>Version</key>
<string>7.0</string> -> <string>4.0</string>

执行:

$ cd English.lproj/
$ sudo mv Apple\ LLVM\ 5.1.strings "Obfuscator 3.4.strings"
$ sudo plutil -convert xml1 Obfuscator\ 3.4.strings
$ sudo vim Obfuscator\ 3.4.strings

修改:

<key>Description</key>
<string>Apple LLVM 8.0 compiler</string> -> <string>Obfuscator 4.0 compiler</string>
<key>Name</key>
<string>Apple LLVM 8.0</string> -> <string>Obfuscator 4.0</string>
<key>Vendor</key>
<string>Apple</string> -> <string>HEIG-VD</string>
<key>Version</key>
<string>7.0</string> -> <string>4.0</string>

执行:

$ sudo plutil -convert binary1 Obfuscator\ 3.4.strings

XCode9之后要设置Enable Index-While-Building成NO



XCode10之后

xcode10之后无法使用添加ideplugin的方法,但添加编译链跑的依然可行,另外网上一些人说不能开bitcode,不能提交AppStore,用原版llvm改的ollvm的确有可能出现上述情况,所以我用苹果的swift-llvm改了一版暂时没去试着提交,或许可以,有兴趣的也可以自己下载使用试试obfuscator这版,特别备注由于修改没有针对swift部分所以用swift写的代码没混淆,回头有空的话再弄。

创建XCode的toolchain然后把生成的文件夹放到/Library/Developer/下

cd build
sudo make install-xcode-toolchain
mv /usr/local/Toolchains /Library/Developer/

Toolchains下的.xctoolchain文件就是一个文件夹,进去修改info.plist

<key>CFBundleIdentifier</key>
<string>org.llvm.7.0.0svn</string> -> <string>org.ollvm-swift.5.0</string>

修改完在XCode的Toolchains下就会显示相应的名称

然后如图打开XCode选择Toolchaiins




按这些配置好后就算是可以用了。

最后

简单展示一下混淆后的成果

源码


反编译未混淆代码


反编译混淆后代码


扩展:字符串混淆

原版是没有这功能的本来,Armariris 提供了这个功能,我这也移植过来了,毕竟不难。
首先把StringObfuscation的.h,.cpp文件放到对应的Obfuscation文件夹下,然后分别修改下面的文件。


用法

-mllvm -sobf:编译时候添加选项开启字符串加密
-mllvm -seed=0xdeadbeaf:指定随机数生成器种子

效果

看个添加了-mllvm -sub -mllvm -sobf -mllvm -fla -mllvm -bcf这么一串的效果。

源码


反编译未混淆代码


反编译混淆后代码


转自:https://www.jianshu.com/p/e0637f3169a3

收起阅读 »

汇编-函数本质(上)

栈函数调用栈恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。内存读写指令⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x02和0x03。str(store register)指...
继续阅读 »


栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)


SP和FP寄存器

  • sp寄存器在任意时刻会保存我们栈顶的地址。
  • fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!(没有出现函数嵌套调用的时候不需要fp,相当于分界点)
    ⚠️:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stpARM64里面 对栈的操作是16字节对齐的!!

ARM64是先开辟一段栈空间,fp移动到栈顶再往栈中存放内容(编译期就已经确定大小)。不存在push操作。在iOS中栈是往低地址开辟空间




函数调用栈

常见的函数调用开辟和恢复的栈空间:

//开辟栈空间
sub sp, sp, #0x40 ; 拉伸0x4064字节)空间
stp x29, x30, [sp, #0x30] ;x29\x30 寄存器入栈保护
add x29, sp, #0x30 ; x29指向栈帧的底部
...
//恢复栈空间
ldp x29, x30, [sp, #0x30] ;恢复x29/x30 寄存器的值
add sp, sp, #0x40 ;栈平衡
ret

恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。

内存读写指令

⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x020x03

str(store register)指令
将数据从寄存器中读出来,存到内存中。

ldr(load register)指令
将数据从内存中读出来,存到寄存器中。

ldr 和 str 的变种 ldp 和 stp 还可以操作2个寄存器。


堆栈操作案例

使用32个字节空间作为这段程序的栈空间,然后利用栈将x0x1的值进行交换。

.text
.global _C

_C:
sub sp, sp, #0x20 ;拉伸栈空间32个字节
stp x0, x1, [sp, #0x10] ;sp 偏移 16字节存放 x0和x1 []的意思是寻址。这sp并没有改变
ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0。这里内存相当于temp 交换了 x0 和 x1。寄存器中的值交换了,内存中的值不变。
add sp, sp, #0x20 ;恢复栈空间
ret
这段代码相当于 x0,x1遍历,sp和内存没有变。
栈空间分配:





断点调试

0x102e6e518断点处对x0x1分别赋值0xa0xb。然后单步执行:




拉伸后sp也变了。

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb) register read sp
sp = 0x000000016cf95b10
(lldb)

看下0x000000016cf95b10的空间:




目前还没有写入内存,是脏数据。接着单步执行:



这个时候x0x1的数据完成了交换。内存的数据并没有变化。
继续单步执行:

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb) register read sp
sp = 0x000000016cf95b10
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb)

sp还原了,栈空间释放,这时候0xa0xb还依然存在内存中,等待下次拉伸栈空间写数据覆盖:




bl和ret指令

bl标号

  • 将下一条指令的地址放入lr(x30)寄存器
  • 转到标号处执行指令

b就是跳转,l将下一条指令的地址放入lr(x30)寄存器。


lr相当于保存的”回家的路“。


ret

  • 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!

ret只会看lr


ARM64平台的特色指令,它面向硬件做了优化处理。



x30寄存器

x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!
一个嵌套调用的案例,汇编代码如下:

.text
.global _C, _D

_C:
mov x0,#0xaaaa
bl _D
mov x0,#0xaaaa
ret

_D:
mov x0,#0xbbbb
ret
ViewController.m中调用:

int C();
int D();
- (void)viewDidLoad {
[super viewDidLoad];
printf("C");
C();
printf("D");
}
C();打断点执行,进入C中:






继续执行发现一直在0x104c8e4f80x104c8e4fc中跳转返不回去viewDidLoad中了,发生了死循环。

->  0x104c8e4f8 <+8>:  mov    x0, #0xaaaa
0x104c8e4fc <+12>: ret
那么如果要返回,就必须将viewDidLoad中下一条指令告诉lr,这个时候就必须在bl之前保护lr寄存器(遇到bllr就会改变。需要保护“回家的路”)。那么这个时候能不能把lr保存到其它寄存器?这里我们没法保证其它寄存器不会被使用。这个时候唯一属于当前函数的也就是自己的栈区了。保存到栈区应该就能解决了。
可以看下系统是怎么实现的,写一个c函数断点调试看下:

void c() {
d();
return;;
}

void d() {

}

- (void)viewDidLoad {
[super viewDidLoad];
c();
}

系统的实现如下:



TestDemo`c:
//边开辟空间边写入 x29(fp) x30(lr) 的值。[sp, #-0x10]! !代表赋值给sp,相当于 sp -= 0x10
-> 0x102a21e84 <+0>: stp x29, x30, [sp, #-0x10]!
0x102a21e88 <+4>: mov x29, sp
0x102a21e8c <+8>: bl 0x102a21e98 ; d at ViewController.m:34:1
//将sp所指向的地址读取给x29,x30。[sp], #0x10 等价于 sp += 0x10
0x102a21e90 <+12>: ldp x29, x30, [sp], #0x10
0x102a21e94 <+16>: ret

可以看到系统先开辟栈空间,然后将x29x30寄存器的值存入栈区。在ret之前恢复x29x30的值。

  • stp x29, x30, [sp, #-0x10]!:开辟空间并将x29x30存入栈区。!代表赋值给sp,相当于 sp -= 0x10
  • ldp x29, x30, [sp], #0x10:将栈区的值给x29x30并回收空间。[sp], #0x10 等价于 sp += 0x10

那么对于CD的案例自己实现下保存和恢复lr寄存器。


.text
.global _C, _D

_C:
//sub sp,sp,#0x10
//str x30,[sp] ;等价
str x30, [sp,#-0x10]! ;16字节对齐,必须最小0x10
mov x0,#0xaaaa
bl _D
mov x0,#0xaaaa
//ldr x30,[sp]
//add sp,#0x10 ;等价
ldr x30,[sp],#0x10
ret

_D:
mov x0,#0xbbbb
ret



这个时候进入Dlr值已经发生变化。



继续执行正常返回viewDidload了,这个时候死循环就已经解决了。

⚠️:在函数嵌套调用的时候,需要将x30入栈!开辟空间需要16字节对齐。如果开辟8字节再读的时候会坏地址访问。写的时候没问题。




函数的参数和返回值

先看下系统的实现:

int sum(int a, int b) {
return a + b;
}

- (void)viewDidLoad {
[super viewDidLoad];
sum(10,20);
}




可以看到变量1020分别存入了w0w1
sum调用如下(release模式下编译器会优化):

TestDemo`sum:
//开辟空间
-> 0x100121e68 <+0>: sub sp, sp, #0x10 ; =0x10
//w0 w1 存入栈中
0x100121e6c <+4>: str w0, [sp, #0xc]
0x100121e70 <+8>: str w1, [sp, #0x8]
//从栈中读取参数
0x100121e74 <+12>: ldr w8, [sp, #0xc]
0x100121e78 <+16>: ldr w9, [sp, #0x8]
//参数相加存入w0
0x100121e7c <+20>: add w0, w8, w9
//恢复栈空间
0x100121e80 <+24>: add sp, sp, #0x10 ; =0x10
//返回
0x100121e84 <+28>: ret
从上面可以看出返回值在w0中。那么自己实现sum函数的汇编代码:

.text
.global _suma

_suma:
add x0,x0,x1
ret
调用:

int suma(int a, int b);
- (void)viewDidLoad {
[super viewDidLoad];
printf("%d",suma(10,20));
}

⚠️ARM64下,函数的参数是存放在X0X7(W0W7)这8个寄存器里面的。如果超过8个参数就会入栈。那么oc的方法最好不要超过6个(selfcmd)。
函数的返回值是放在X0寄存器里面的。


参数超过8个


int test(int a, int b, int c ,int d, int e, int f, int g, int h, int i) {
return a + b + c + d + e + f + g + h + i;
}

- (void)viewDidLoad {
[super viewDidLoad];
test(1, 2, 3, 4, 5, 6, 7, 8, 9);
}




可以看到前8个参数分别保存在w0~w7寄存器中,第9个参数先保存在w10中,然后写入x8中(这个时候x8指向sp,相当于第9个参数写入了当前函数栈中)。


TestDemo`-[ViewController viewDidLoad]:
//拉伸栈空间,保存fp lr
0x100f09e5c <+0>: sub sp, sp, #0x40 ; =0x40
0x100f09e60 <+4>: stp x29, x30, [sp, #0x30]

//fp指向 sp+0x30
0x100f09e64 <+8>: add x29, sp, #0x30 ; =0x30
//fp-0x8 存放x0
0x100f09e68 <+12>: stur x0, [x29, #-0x8]
//fp-0x10 存放x1
0x100f09e6c <+16>: stur x1, [x29, #-0x10]
//fp-0x8 给到 x8
0x100f09e70 <+20>: ldur x8, [x29, #-0x8]
//sp+0x10 指针给到 x9
0x100f09e74 <+24>: add x9, sp, #0x10 ; =0x10
//x8写入 sp+0x10
0x100f09e78 <+28>: str x8, [sp, #0x10]

//adrp = address page 内存中取数据
0x100f09e7c <+32>: adrp x8, 4
0x100f09e80 <+36>: add x8, x8, #0x418 ; =0x418
//x8所指向的内容去出来
0x100f09e84 <+40>: ldr x8, [x8]
//x8写入栈中,这个时候x9指向地址,这个时候是一个新的x8
0x100f09e88 <+44>: str x8, [x9, #0x8]
0x100f09e8c <+48>: adrp x8, 4
0x100f09e90 <+52>: add x8, x8, #0x3e8 ; =0x3e8
0x100f09e94 <+56>: ldr x1, [x8]
0x100f09e98 <+60>: mov x0, x9
0x100f09e9c <+64>: bl 0x100f0a568 ; symbol stub for: objc_msgSendSuper2

//sp 一直没有改变过,w0~w7 分别存放前8个参数
0x100f09ea0 <+68>: mov w0, #0x1
0x100f09ea4 <+72>: mov w1, #0x2
0x100f09ea8 <+76>: mov w2, #0x3
0x100f09eac <+80>: mov w3, #0x4
0x100f09eb0 <+84>: mov w4, #0x5
0x100f09eb4 <+88>: mov w5, #0x6
0x100f09eb8 <+92>: mov w6, #0x7
0x100f09ebc <+96>: mov w7, #0x8
//x8 指向 sp
-> 0x100f09ec0 <+100>: mov x8, sp
//参数 9 存入 w10
0x100f09ec4 <+104>: mov w10, #0x9
//w10 存入 x8地址中,也就是sp栈底中
0x100f09ec8 <+108>: str w10, [x8]

0x100f09ecc <+112>: bl 0x100f09de4 ; test at ViewController.m:41
0x100f09ed0 <+116>: ldp x29, x30, [sp, #0x30]
0x100f09ed4 <+120>: add sp, sp, #0x40 ; =0x40
0x100f09ed8 <+124>: ret



接着往下直接跳转到test函数中:

TestDemo`test:
//开辟空间48字节
0x100f09de4 <+0>: sub sp, sp, #0x30 ; =0x30

//从viewDidLoad栈中取数据 第9个参数(读写往高地址)
0x100f09de8 <+4>: ldr w8, [sp, #0x30]

//参数入栈,分别占4个字节
0x100f09dec <+8>: str w0, [sp, #0x2c]
0x100f09df0 <+12>: str w1, [sp, #0x28]
0x100f09df4 <+16>: str w2, [sp, #0x24]
0x100f09df8 <+20>: str w3, [sp, #0x20]
0x100f09dfc <+24>: str w4, [sp, #0x1c]
0x100f09e00 <+28>: str w5, [sp, #0x18]
0x100f09e04 <+32>: str w6, [sp, #0x14]
0x100f09e08 <+36>: str w7, [sp, #0x10]
0x100f09e0c <+40>: str w8, [sp, #0xc]

-> 0x100f09e10 <+44>: ldr w8, [sp, #0x2c]
0x100f09e14 <+48>: ldr w9, [sp, #0x28]
0x100f09e18 <+52>: add w8, w8, w9
0x100f09e1c <+56>: ldr w9, [sp, #0x24]
0x100f09e20 <+60>: add w8, w8, w9
0x100f09e24 <+64>: ldr w9, [sp, #0x20]
0x100f09e28 <+68>: add w8, w8, w9
0x100f09e2c <+72>: ldr w9, [sp, #0x1c]
0x100f09e30 <+76>: add w8, w8, w9
0x100f09e34 <+80>: ldr w9, [sp, #0x18]
0x100f09e38 <+84>: add w8, w8, w9
0x100f09e3c <+88>: ldr w9, [sp, #0x14]
0x100f09e40 <+92>: add w8, w8, w9
0x100f09e44 <+96>: ldr w9, [sp, #0x10]
0x100f09e48 <+100>: add w8, w8, w9
0x100f09e4c <+104>: ldr w9, [sp, #0xc]
//最终相加结果给 w0
0x100f09e50 <+108>: add w0, w8, w9
//栈平衡
0x100f09e54 <+112>: add sp, sp, #0x30 ; =0x30
0x100f09e58 <+116>: ret



最终函数返回值放入w0中,如果在release模式下test不会被调用(被优化掉,因为没有意义,有没有对app没有影响。)

自己实现一个简单有参数并且嵌套调用的汇编:


.text
.global _func,_sum

_func:
//sub sp,sp,#0x10
//stp x29,x30,[sp]
stp x29,x30,[sp, #-0x10]!
bl _sum
//ldp x29,x30,[sp]
//add sp,sp,#0x10
ldp x29,x30,[sp],#0x10
ret
_sum:
add x0,x0,x1
ret


篇幅限制 分为2篇

作者:HotPotCat
链接:https://www.jianshu.com/p/69b9c49b0e71




收起阅读 »

汇编-基本概念

在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app来说,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是由手机上的CPU执行的,静态分析是建立在分析二进制上面。汇编语言的发展机器语言由0...
继续阅读 »


在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app来说,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是由手机上的CPU执行的,静态分析是建立在分析二进制上面。


汇编语言的发展

机器语言

01组成的机器指令。0代表有电,1代表没电。

  • 加:0100 0000
  • 减:0100 1000
  • 乘:1111 0111 1110 0000
  • 除:1111 0111 1111 0000

汇编语言(assembly language)

为了高效的写代码出现了助记符,使用助记符代替机器语言,如:

  • 加:INC EAX 通过编译器 0100 0000
  • 减:DEC EAX 通过编译器 0100 1000
  • 乘:MUL EAX 通过编译器 1111 0111 1110 0000
  • 除:DIV EAX 通过编译器 1111 0111 1111 0000

助记符就是汇编语言的前身,当有专门的编译器出现的时候就有了汇编语言。

高级语言(High-level programming language)

C\C++\Java\OC\Swift,更加接近人类的自然语言。
比如C语言:

  • 加:A + B 通过编译器 0100 0000
  • 减:A - B 通过编译器 0100 1000
  • 乘:A * B 通过编译器 1111 0111 1110 0000
  • 除:A / B 通过编译器 1111 0111 1111 0000

代码在终端设备上的过程:





  • 汇编语言机器语言一一对应,每一条机器指令都有与之对应的汇编指令
  • 汇编语言可以通过编译得到机器语言机器语言可以通过反汇编得到汇编语言
  • 高级语言可以通过编译得到汇编语言 \ 机器语言,但汇编语言\机器语言几乎不可能还原成高级语言(不是一一对应关系,反推出是不准确的,只能大致。)

汇编语言的特点


  • 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
  • 能够不受编译器的限制,对生成的二进制代码进行完全的控制
  • 目标代码简短,占用内存少,执行速度快
  • 汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
  • 开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
  • 不区分大小写,比如movMOV是一样的

汇编的用途

  • 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
  • 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编
  • 软件安全
    1.病毒分析与防治
    2.逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
  • 理解整个计算机系统的最佳起点和最有效途径
  • 为编写高效代码打下基础
  • 弄清代码的本质

汇编语言的种类

目前讨论比较多的汇编语言有:

  • 8086汇编(8086处理器是16bitCPU
  • Win32汇编
  • Win64汇编
  • ARM汇编(嵌入式、MaciOS
  • ......

iPhone里面用到的是ARM汇编,但是不同的设备也有差异(因CPU的架构不同)。

位数架构设备
32armv6iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch
32armv7iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4
32armv7siPhone5, iPhone5C, iPad4(iPad with Retina Display)
64arm64iPhone5s,iPhone6、7、8,iPhone6、7、8 Plus,iPhone X,iPad Air,iPad mini2(iPad mini with Retina Display)
64arm64eXS/XS Max/XR/ iPhone 11, iPhone 11 pro 及以后
64x86_64模拟器64位处理器 (intel)
32i386模拟器32位处理器(intel)

⚠️:苹果A7处理器支持两个不同的指令集:32ARM指令集(armv6|armv7|armv7s)和64ARM指令集(arm64

汇编相关的学习需要了解CPU等硬件结构,最为重要的是CPU/内存。在汇编中,大部分指令都是和CPU与内存相关的。
APP/程序的执行过程:





执行过程:
1.地址总线先去内存地址。
2.控制读取发送读/写命令。
3.数据总线写数据->内存/ 内存发送数据->数据总线

地址总线

  • 它的宽度决定了CPU的寻址能力(也就是寻址范围)
  • 8086的地址总线宽度是20,所以寻址能力是1M( 220)(这里的M是大小,数量单位)




内存中的MB是容量单位。如果内存很大, 地址总线宽度不够怎么处理?以前的cpu是通过2次寻址相加得到一个最终的值来访问内存,现在的cpu没有寻址能力的问题。
数量单位:M,K。1M = 1024K,1K= 1024。比如:10,100
容量单位:字节Byte。 1024B = 1KB,1024KB = 1MB。比如:10个,100只。(大部分计算机都是以1个字节为单位。银行系统的IBM电脑例外是2个字节为单位。)
对于100M 宽带,这里的100M是100Mbps(每秒钟传递多少二进制位,bit位。所以100M带宽理论下载速度12.5MB/s)。

数据总线

  • 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度(吞吐量)
  • 8086的数据总线宽度是16,所以单次最大传递2个字节的数据

我们现在常说的32位,64位cpu说的就是它的数据吞吐量。1次放电分别4字节,8字节数据。

控制总线

  • 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制

案例:
1.一个CPU 的寻址能力为8KB,那么它的地址总线的宽度为____
答案:8KB对应 8192, 213 = 8192 所以为13。

  1. 8080,8088,80286,80386 的地址总线宽度分别为16根,20根,24根,32根。那么他们的寻址能力分别为多少____KB, ____MB,____MB,____GB?
    答案:1kb = 210 = 1024
    1kb * 26 = 64kb
    1kb * 1kb = 1mb
    1mb * 24 = 16mb
    1kb * 1kb * 1kb * 22 = 4gb

  2. 8080,8088,8086,80286,80386 的数据总线宽度分别为8根,8根,16根,16根,32根.那么它们一次可以传输的数据为:____B,____B,____B,____B,____B
    答案:1 、1、2、2、4

4.从内存中读取1024字节的数据,8086至少要读____次,80386至少要读取____次.
答案:8086 数据总线宽度为16。8086一次读2个字节,那么需要512次,80286数据总线宽度为32,一次4个字节,需要256次。

内存




  • 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位220个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB
  • 0x00000~0x9FFFF:主存储器。可读可写
  • 0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
  • 0xC0000~0xFFFFF:存储各种硬件\系统信息。只读

  • 进制

    想学好进制首先要忘掉十进制,也要忘掉进制间的转换。

    进制的定义

    • 八进制由8个符号组成:0 1 2 3 4 5 6 7 逢八进一
    • 十进制由10个符号组成:0 1 2 3 4 5 6 7 8 9逢十进一
    • N进制就是由N个符号组成:逢N进一

    ⚠️:进制的本质是符号。

    案例

    1. 1 + 1 在____情况下等于 3 ?
      除了算错的情况下。在十进制由10个符号组成,假如由: 0 1 3 2 8 A B E S 7组成逢十进一,那么在这种情况下1+1=3

    传统定义的十进制和自定义的十进制不一样。那么这10个符号如果我们不告诉别人这个符号表,别人是没办法拿到我们的具体数据的,可以用于加密!
    ⚠️:十进制由十个符号组成,逢十进一,符号是可以自定义的!!!

    1. 八进制运算:
    • 2 + 3 = __ , 2 * 3 = __ ,4 + 5 = __ ,4 * 5 = __.
      答案:5,6,11,24
    • 277 + 333 = __ , 276 * 54 = __ , 237 - 54 = __ , 234 / 4 = __ .
      答案:632, 20250, 163,47


    八进制加法表
    0 1 2 3 4 5 6 7
    10 11 12 13 14 15 16 17
    20 21 22 23 24 25 26 27
    ...

    1+1 = 2                     
    1+2 = 3 2+2 = 4
    1+3 = 4 2+3 = 5 3+3 = 6
    1+4 = 5 2+4 = 6 3+4 = 7 4+4 = 10
    1+5 = 6 2+5 = 7 3+5 = 10 4+5 = 11 5+5 = 12
    1+6 = 7 2+6 = 10 3+6 = 11 4+6 = 12 5+6 = 13 6+6 = 14
    1+7 = 10 2+7 = 11 3+7 = 12 4+7 = 13 5+7 = 14 6+7 = 15 7+7 = 16

    八进制乘法表
    0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27...

    1*1 = 1                     
    1*2 = 2 2*2 = 4
    1*3 = 3 2*3 = 6 3*3 = 11
    1*4 = 4 2*4 = 10 3*4 = 14 4*4 = 20
    1*5 = 5 2*5 = 12 3*5 = 17 4*5 = 24 5*5 = 31
    1*6 = 6 2*6 = 14 3*6 = 22 4*6 = 30 5*6 = 36 6*6 = 44
    1*7 = 7 2*7 = 16 3*7 = 25 4*7 = 34 5*7 = 43 6*7 = 52 7*7 = 61

    二进制的简写形式

                   二进制: 1 0 1 1 1 0 1 1 1 1 0 0
    三个二进制一组: 101 110 111 100
                    八进制:    5     6     7      4
    四个二进制一组: 1011 1011 1100
                十六进制:     b        b       c

    二进制:从 0 写到 1111
    0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
    这种二进制使用起来太麻烦,改成更简单一点的符号:
    0 1 2 3 4 5 6 7 8 9 A B C D E F 这就是十六进制了

    数据的宽度

    数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃。


    int test() {
    int cTemp = 0x1FFFFFFFF;
    return cTemp;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%x",test());
    }
    输出:

    ffffffff
    数据溢出了。刚开始cTemp默认值1,溢出后变为-1第一位符号位,1代表负数,0代表正数。往后逐位取反,末尾加1。)。

    (lldb) p cTemp
    (int) $0 = 1
    (lldb) p cTemp
    (int) $1 = -1
    (lldb) p &cTemp
    (int *) $2 = 0x000000016b3a9b1c
    (lldb) x 0x000000016b3a9b1c
    0x16b3a9b1c: ff ff ff ff 10 00 00 00 00 00 00 00 ef 98 3a 6b ..............:k
    0x16b3a9b2c: 01 00 00 00 70 a9 f0 59 01 00 00 00 50 d4 a5 04 ....p..Y....P...
    (lldb) p (uint)cTemp
    (uint) $3 = 4294967295

    Debug -> Debug Workflow -> View Memory中也可以查看(这里查看内容更新后需要翻页刷新然后切换回来才能显示新值):




    再看下汇编代码(Debug -> Debug Workflow -> Always Show Disassembly):


    TestDemo`test:
    0x104a59ec8 <+0>: sub sp, sp, #0x10 ; =0x10
    0x104a59ecc <+4>: mov w8, #-0x1
    0x104a59ed0 <+8>: str w8, [sp, #0xc]
    -> 0x104a59ed4 <+12>: ldr w0, [sp, #0xc]
    0x104a59ed8 <+16>: add sp, sp, #0x10 ; =0x10
    0x104a59edc <+20>: ret

    可以看到直接将-1给力w8。指令在内存中占用4字节。

    计算机中常见的数据宽度

    • 位(Bit): 1个位就是1个二进制位。0或者1
    • 字节(Byte): 1个字节由8个Bit组成(8位)。内存中的最小单元Byte
    • 字(Word): 1个字由2个字节组成(16位),这2个字节分别称为高字节低字节
    • 双字(Doubleword): 1个双字由两个字组成(32位)。

    计算机存储数据会分为有符号数和无符号数(对于数据本身内容没有变化,取决于你怎么看):


    无符号数,直接换算!
    有符号数:
    正数: 0 1 2 3 4 5 6 7
    负数: F E D B C A 9 8
    -1 -2 -3 -4 -5 -6 -7 -8

    自定义进制符号

    案例:

    • 现在有10进制数10个符号分别是:2,9,1,7,6,5,4, 8,3 , A 逢10进1 那么: 123 + 234 = ____

    十进制:
    0 1 2 3 4 5 6 7 8 9
    自定义:
    2 9 1 7 6 5 4 8 3 A
    92 99 91 97 96 95 94 98 93 9A
    12 19 11 17 16 15 14 18 13 1A
    72 79 71 77 76 75 74 78 73 7A
    62 69 61 67 66 65 64 68 63 6A
    52 59 51 57 56 55 54 58 53 5A
    42 49 41 47 46 45 44 48 43 4A
    82 89 81 87 86 85 84 88 83 8A
    32 39 31 37 36 35 34 38 33 3A
    922

    转换后加法表:

    9+9 = 1                 
    9+1 = 7 1+1 = 6
    9+7 = 6 1+7 = 5 7+7 = 4
    9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
    9+5 = 4 1+5 = 8 7+5 = 3 6+5 = A 5+5 = 92
    9+4 = 8 1+4 = 3 7+4 = a 6+4 = 92 5+4 = 99 4+4 = 91
    9+8 = 3 1+8 = A 7+8 = 92 6+8 = 99 5+8 = 91 4+8 = 97 8+8 = 96
    9+3 = A 1+3 = 92 7+3 = 99 6+3 = 91 5+3 = 97 4+3 = 96 8+3 = 95 3+3 = 94
    9+A = 92 1+A = 99 7+A = 91 6+A = 97 5+A = 96 4+A = 95 8+A = 94 3+A = 98 A+A = 93

    123 + 234 = 1A6

    • 现在有9进制数 9个符号分别是:2,9,1,7,6,5,4, 8,3 逢9进1 那么: 123 + 234 = __

    十进制:
    0 1 2 3 4 5 6 7 8
    自定义:
    2 9 1 7 6 5 4 8 3
    92 99 91 97 96 95 94 98 93
    12 19 11 17 16 15 14 18 13
    72 79 71 77 76 75 74 78 73
    62 69 61 67 66 65 64 68 63
    52 59 51 57 56 55 54 58 53
    42 49 41 47 46 45 44 48 43
    82 89 81 87 86 85 84 88 83
    32 39 31 37 36 35 34 38 33
    922

    转换后加法表:

    9+9 = 1                 
    9+1 = 7 1+1 = 6
    9+7 = 6 1+7 = 5 7+7 = 4
    9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
    9+5 = 4 1+5 = 8 7+5 = 3 6+5 = 92 5+5 = 99
    9+4 = 8 1+4 = 3 7+4 = 92 6+4 = 99 5+4 = 91 4+4 = 97
    9+8 = 3 1+8 = 92 7+8 = 99 6+8 = 91 5+8 = 97 4+8 = 96 8+8 = 95
    9+3 = 92 1+3 = 99 7+3 = 91 6+3 = 97 5+3 = 96 4+3 = 95 8+3 = 94 3+3 = 98

    123 + 234 = 725

    CPU&寄存器

    内部部件之间由总线连接


    CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储。

    CPU的运算速度是非常快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器

    对于arm64系的CPU来说, 如果寄存器以x开头则表明的是一个64位的寄存器,如果以w开头则表明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用。其中32位的寄存器是64位寄存器的低32位部分并不是独立存在的

    • 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
    • 不同的CPU,寄存器的个数、结构是不相同的

    浮点寄存器

    因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数。

    • 浮点寄存器 64位D0 - D31 32位: S0 - S31

    向量寄存器

    现在的CPU支持向量运算。(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.

    • 向量寄存器 128位:V0-V31

    通用寄存器

    • 通用寄存器也称数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用。
    • ARM64拥有32个64位的通用寄存器x0x30,以及XZR(零寄存器),这些通用寄存器有时也有特定用途。
      1.64位X0-X30, XZR(零寄存器)w0 到 w28 这些是32位的。因为64位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.
      2.32位W0-W30, WZR(零寄存器)。 w0 就是 x0 的低32位!

    ⚠️:了解过8086汇编的都知道,有一种特殊的寄存器段寄存器:CS,DS,SS,ES四个寄存器来保存这些段的基地址,这个属于Intel架构CPU中。在ARM中并没有。

    在"Xcode"中我们可以查看具体寄存器的内容:




    分别看一下x0w0的值:
    x0  unsigned long   0x0000000159f0a970
    w0 unsigned int 0x59f0a970

    验证了w0x0的低32位。

    通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
    假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间:




    pc寄存器

    单步执行汇编代码(pc始终指向下一条指令):




  • 为指令指针寄存器,它指示了CPU当前要读取指令的地址(指向下一条即将执行的指令
  • 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
  • CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义

  • 比如 1110 0000 0000 0011 0000 1000 1010 1010,
    可以当做数据 0xE003008AA。
    也可以当做指令 mov x0, x8

    • CPU根据什么将内存中的信息看做指令?

    CPU将pc指向的内存单元的内容看做指令
    如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过。

    高速缓存

    iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M。

    CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成)。CPU直接从高速缓存依次读取指令来执行。

    bl指令

    bl分位bl:
    b:跳转。
    l:lr寄存器。

    • CPU从何处执行指令是由pc中的内容决定的,我们可以通过改变pc的内容来控制CPU执行目标指令
    • ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如:
      mov x0,#10、mov x1,#20
    • 但是,mov指令不能用于设置pc的值,ARM64没有提供这样的功能
    • ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令,最简单的是bl指令

    案例
    现在有两段代码!假设程序先执行A,请写出指令执行顺序。最终寄存器x0的值是多少?

    _A:
    mov x0,#0xa0
    mov x1,#0x00
    add x1, x0, #0x14
    mov x0,x1
    bl _B
    mov x0,#0x0
    ret

    _B:
    add x0, x0, #0x10
    ret

    分析:


    Xcode中创建Empty文件命名为asm.s.s汇编代码会被Xcode自动识别编译)。


    //asm.s
    .text // 告诉是代码
    .global _A, _B //.global 是标号

    _A:
    mov x0,#0xa0 //a0 给 x0 x0 = 0xa0
    mov x1,#0x00 //00 给x1 x1 = 0x00
    add x1, x0, #0x14 //x0 + 0x14 给 x1 x1 = 0xb4
    mov x0,x1 //x1 的值给 x0 x0 = 0xb4
    bl _B //跳转B
    mov x0,#0x0 //0x0 给 x0 x0 = 0x0
    ret //return 上层调用的地方

    _B:
    add x0, x0, #0x10 //x0 + 0x10 给 x0 x0 = 0xc4
    ret //return A

    oc调用汇编:

    //ViewController.m
    int A();

    - (void)viewDidLoad {
    [super viewDidLoad];
    A();
    }
    swift调用汇编:

    //声明方法A。Swift中C和汇编都可以这么暴露。
    @_silgen_name("A")
    func A()

    class ViewController: UIViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    A();
    }

    }
    答案:0x00



    断点验证了x0最终值为0x00。这里有个问题是发生死循环了。(bl跳转指令导致的,lr寄存器在跳转后需要保护现场还原。)


    总结

    • 汇编概述:
      • 使用助记符代替集齐指令的一种编程语言。
      • 汇编和及其指令是一一对应的关系,拿到二进制就可以反汇编。
      • 由于汇编和CPU指令集是对应的,所以汇编不具备移植性。
    • 总线:是一堆导线的集合
      • 地址总线:地址总线的宽度决定了寻址能力
      • 数据总线:数据总线的宽度决定了CPU的吞吐量
    • 进制
      • 任意进制都是由对应个数的符号组成的。符号可以自定义。
      • 2/8/16是相对完美的集智,他们之间的关系
        • 3个2进制使用一个8进制标识
        • 4个2进制使用一个16进制标识
        • 两个16进制位可以标识一个字节
      • 数量单位
        • 1024 = 1K;1024K = 1M;1024M = 1G
      • 容量单位
        • 1024B = 1KB;1024KB = 1MB; 1024MB = 1GB
        • B:byte(字节)1B = 8bit
        • bit(比特):一个二进制位
      • 数据的宽度
        • 计算机中的数据是有宽度的,超过了就会溢出
    • 寄存器:CPU为了性能,在内部开辟了一小块临时存储区域
      • 浮点向量寄存器
      • 异常状态寄存器
      • 通用寄存器:除了存放数据有时候也有特殊的用途
        • ARM64拥有32个64位的通用寄存器X0—X30以及XZR(令寄存器)
        • 为了兼容32位,所以ARM64拥有W0—W28\WZR 30个32位寄存器
        • 32位寄存器并不是独立存在的,比如W0是X0的低32位
      • PC寄存器:指令指针寄存器
        • PC寄存器里面的值保存的就是CPU接下来需要执行的指令地址!
        • 改变PC的值可以改变程序的执行流程!


    作者:HotPotCat
    链接:https://www.jianshu.com/p/e8ea78cb10f0



    收起阅读 »

    iOS - Path menu 的动画效果

    AwesomeMenu 是一个与Path的故事菜单外观相同的菜单。通过设置菜单项来创建菜单:UIImage *storyMenuItemImage = [UIImage imageNamed:@"bg-menuitem.png"]; UIImage *sto...
    继续阅读 »

    AwesomeMenu 是一个与Path的故事菜单外观相同的菜单

    通过设置菜单项来创建菜单:

    UIImage *storyMenuItemImage = [UIImage imageNamed:@"bg-menuitem.png"];
    UIImage *storyMenuItemImagePressed = [UIImage imageNamed:@"bg-menuitem-highlighted.png"];
    UIImage *starImage = [UIImage imageNamed:@"icon-star.png"];
    AwesomeMenuItem *starMenuItem1 = [[AwesomeMenuItem alloc] initWithImage:storyMenuItemImage
    highlightedImage:storyMenuItemImagePressed
    ContentImage:starImage
    highlightedContentImage:nil];
    AwesomeMenuItem *starMenuItem2 = [[AwesomeMenuItem alloc] initWithImage:storyMenuItemImage
    highlightedImage:storyMenuItemImagePressed
    ContentImage:starImage
    highlightedContentImage:nil];
    // the start item, similar to "add" button of Path
    AwesomeMenuItem *startItem = [[AwesomeMenuItem alloc] initWithImage:[UIImage imageNamed:@"bg-addbutton.png"]
    highlightedImage:[UIImage imageNamed:@"bg-addbutton-highlighted.png"]
    ContentImage:[UIImage imageNamed:@"icon-plus.png"]
    highlightedContentImage:[UIImage imageNamed:@"icon-plus-highlighted.png"]];




    然后,设置菜单和选项:


    AwesomeMenu *menu = [[AwesomeMenu alloc] initWithFrame:self.window.bounds startItem:startItem optionMenus:[NSArray arrayWithObjects:starMenuItem1, starMenuItem2]];
    menu.delegate = self;
    [self.window addSubview:menu];

    您还可以使用菜单选项:

    找到“添加”按钮的中心:

    menu.startPoint = CGPointMake(160.0, 240.0);

    设置旋转角度:

    menu.rotateAngle = 0.0;

    设置整个菜单角度:

    menu.menuWholeAngle = M_PI * 2;

    设置每个菜单飞出动画的延迟:

    menu.timeOffset = 0.036f;

    调整弹跳动画:

    menu.farRadius = 140.0f;
    menu.nearRadius = 110.0f;

    设置“添加”按钮和菜单项之间的距离:

    menu.endRadius = 120.0f;


    常见问题及demo下载:https://github.com/levey/AwesomeMenu

    源码下载:AwesomeMenu-master.zip

    收起阅读 »

    iOS 滑动效果cell - SWTableViewCell

    SWTableViewCell一个易于使用的 UITableViewCell 子类,它实现了一个可滑动的内容视图,它公开了实用程序按钮(类似于 iOS 7 邮件应用程序)在你的 Podfile 中:- (void)tableView:(UITableView ...
    继续阅读 »

    SWTableViewCell

    一个易于使用的 UITableViewCell 子类,它实现了一个可滑动的内容视图,它公开了实用程序按钮(类似于 iOS 7 邮件应用程序)

    在你的 Podfile 中:

    pod 'SWTableViewCell', '~> 0.3.7'


    或者只是克隆这个 repo 并手动将源添加到项目

    当用户向左滑动时,在表格视图单元格右侧可见的实用程序按钮。此行为类似于在 iOS 应用程序邮件和提醒中看到的行为。



    实用程序按钮 当用户向右滑动时,在表格视图单元格左侧可见的实用程序按钮。


    • 动态实用程序按钮缩放。当您向单元格添加更多按钮时,该侧的其他按钮会变小以腾出空间
    • 智能选择:单元格将拾取触摸事件并将单元格滚动回中心或触发委托方法 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

    因此,当实用程序按钮可见时,当用户触摸单元格时,单元格不会被视为选中,相反,单元格将滑回原位(与 iOS 7 邮件应用程序功能相同) * 创建带有标题或图标的实用程序按钮以及RGB 颜色 * 在 iOS 6.1 及更高版本上测试,包括 iOS 7

    用法

    标准表格视图单元格

    在您的tableView:cellForRowAtIndexPath:方法中,您设置 SWTableView 单元格并使用包含的NSMutableArray+SWUtilityButtons类别向其中添加任意数量的实用程序按钮


    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellIdentifier = @"Cell";

    SWTableViewCell *cell = (SWTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil) {
    cell = [[SWTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
    cell.leftUtilityButtons = [self leftButtons];
    cell.rightUtilityButtons = [self rightButtons];
    cell.delegate = self;
    }

    NSDate *dateObject = _testArray[indexPath.row];
    cell.textLabel.text = [dateObject description];
    cell.detailTextLabel.text = @"Some detail text";

    return cell;
    }

    - (NSArray *)rightButtons
    {
    NSMutableArray *rightUtilityButtons = [NSMutableArray new];
    [rightUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:0.78f green:0.78f blue:0.8f alpha:1.0]
    title:@"More"];
    [rightUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:1.0f green:0.231f blue:0.188 alpha:1.0f]
    title:@"Delete"];

    return rightUtilityButtons;
    }

    - (NSArray *)leftButtons
    {
    NSMutableArray *leftUtilityButtons = [NSMutableArray new];

    [leftUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:0.07 green:0.75f blue:0.16f alpha:1.0]
    icon:[UIImage imageNamed:@"check.png"]];
    [leftUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:1.0f green:1.0f blue:0.35f alpha:1.0]
    icon:[UIImage imageNamed:@"clock.png"]];
    [leftUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:1.0f green:0.231f blue:0.188f alpha:1.0]
    icon:[UIImage imageNamed:@"cross.png"]];
    [leftUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:0.55f green:0.27f blue:0.07f alpha:1.0]
    icon:[UIImage imageNamed:@"list.png"]];

    return leftUtilityButtons;
    }

    ###Custom Table View Cells

    Thanks to Matt Bowman you can now create custom table view cells using Interface Builder that have the capabilities of an SWTableViewCell

    The first step is to design your cell either in a standalone nib or inside of a table view using prototype cells. Make sure to set the custom class on the cell in interface builder to the subclass you made for it:

    Then set the cell reuse identifier:

    When writing your custom table view cell's code, make sure your cell is a subclass of SWTableViewCell:

    #import <SWTableViewCell.h>

    @interface MyCustomTableViewCell : SWTableViewCell

    @property (weak, nonatomic) UILabel *customLabel;
    @property (weak, nonatomic) UIImageView *customImageView;

    @end

    If you are using a separate nib and not a prototype cell, you'll need to be sure to register the nib in your table view:

    - (void)viewDidLoad
    {
    [super viewDidLoad];

    [self.tableView registerNib:[UINib nibWithNibName:@"MyCustomTableViewCellNibFileName" bundle:nil] forCellReuseIdentifier:@"MyCustomCell"];
    }

    Then, in the tableView:cellForRowAtIndexPath: method of your UITableViewDataSource (usually your view controller), initialize your custom cell:

    - (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
    {
    static NSString *cellIdentifier = @"MyCustomCell";

    MyCustomTableViewCell *cell = (MyCustomTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier
    forIndexPath:indexPath];

    cell.leftUtilityButtons = [self leftButtons];
    cell.rightUtilityButtons = [self rightButtons];
    cell.delegate = self;

    cell.customLabel.text = @"Some Text";
    cell.customImageView.image = [UIImage imageNamed:@"MyAwesomeTableCellImage"];
    [cell setCellHeight:cell.frame.size.height];
    return cell;
    }

    代理方法

    // click event on left utility button
    - (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerLeftUtilityButtonWithIndex:(NSInteger)index;

    // click event on right utility button
    - (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index;

    // utility button open/close event
    - (void)swipeableTableViewCell:(SWTableViewCell *)cell scrollingToState:(SWCellState)state;

    // prevent multiple cells from showing utilty buttons simultaneously
    - (BOOL)swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:(SWTableViewCell *)cell;

    // prevent cell(s) from displaying left/right utility buttons
    - (BOOL)swipeableTableViewCell:(SWTableViewCell *)cell canSwipeToState:(SWCellState)state;


    常见问题及demo下载:https://github.com/CEWendel/SWTableViewCell

    源码下载:SWTableViewCell-master.zip


    收起阅读 »

    iOS 标签浮动-JVFloatLabeledTextField

    JVFloatLabeledTextFieldJVFloatLabeledTextField是 UX 模式的第一个实现,后来被称为“浮动标签模式”。由于移动设备的空间限制,通常仅依靠占位符来标记字段。这带来了 UX 问题,因为一旦用户开始填写表单,就不会出现任...
    继续阅读 »

    JVFloatLabeledTextField

    JVFloatLabeledTextField是 UX 模式的第一个实现,后来被称为“浮动标签模式”

    由于移动设备的空间限制,通常仅依靠占位符来标记字段。这带来了 UX 问题,因为一旦用户开始填写表单,就不会出现任何标签。

    这个 UI 组件库包括 aUITextFieldUITextView子类,旨在通过将占位符转换为浮动标签来改善用户体验,这些标签在填充文本后悬停在字段上方。

    马特 D. 史密斯的设计


    通过 CocoaPods 

    sudo gem install cocoapods

    Podfile在您的项目目录中创建一个

    pod init

    将以下内容添加到您的Podfile项目目标中:

    pod 'JVFloatLabeledTextField'

    然后运行 CocoaPods pod install

    最后,将JVFloatLabeledTextField.h包含JVFloatLabeledTextView.h在您的项目中。

    Carthage

    brew update
    brew install carthage

    Cartfile在您的项目目录中创建一个包含:

    github "jverdi/JVFloatLabeledTextField"

    然后运行 carthagecarthage updateJVFloatLabeledText.frameworkCarthage/Build/iOS目录中添加到您的项目中

    最后,JVFloatLabeledText.h在您的项目中包含

    #import <JVFloatLabeledText/JVFloatLabeledText.h>


    常见问题及demo下载:https://github.com/jverdi/JVFloatLabeledTextField

    源码下载:JVFloatLabeledTextField-main.zip








    收起阅读 »

    Swift - 第三方日历组件CVCalendar使用详解1(配置、基本用法)

    CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。一、安装配置1. 从 GitHub 上下载最新的代码:https://g...
    继续阅读 »

    CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。

    一、安装配置

    1. 从 GitHub 上下载最新的代码:https://github.com/Mozharovsky/CVCalendar
    2. 将下载下来的源码包中 CVCalendar.xcodeproj 拖拽至你的工程中 


    3. 工程 -> General -> Embedded Binaries 项,把 iOS 版的 framework 添加进来:CVCalendar.framework


    4. 最后,在需要使用 CVCalendar 的地方 import 进来就可以了

    import CVCalendar

    二、基本用法

    1,月视图使用样例 

    ① 效果图
    1. 初始化的时候自动显示当月日历,且“今天”的日期文字是红色的。
    2. 顶部导航栏标题显示当前日历的年、月信息,日历左右滑动切换的时候,标题内容也会随之改变。
    3. 点击导航栏右侧的“今天”按钮,日历又会跳回到当前日期。
    4. 点击日历上的任一日期时间后,该日期背景色会变蓝色(如果是今天则变红色)。同时我们在日期选择响应中,将选择的日期弹出显示。

          

    ② 样例代码
    日历组件分为:CVCalendarMenuView 和 CVCalendarView 两部分。前者是显示星期的菜单栏,后者是日期表格视图。这二者的位置和大小我们可以随意调整设置。
    组件提供了许多代理协议让我进行样式调整或功能响应,我们可以选择使用。但其中 CVCalendarViewDelegate, CVCalendarMenuViewDelegate 这两个协议是必须的。

    import UIKit
    import CVCalendar

    class ViewController: UIViewController {
    //星期菜单栏
    private var menuView: CVCalendarMenuView!

    //日历主视图
    private var calendarView: CVCalendarView!

    var currentCalendar: Calendar!

    override func viewDidLoad() {
    super.viewDidLoad()

    currentCalendar = Calendar.init(identifier: .gregorian)

    //初始化的时候导航栏显示当年当月
    self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

    //初始化星期菜单栏
    self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

    //初始化日历主视图
    self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
    height:450))

    //星期菜单栏代理
    self.menuView.menuViewDelegate = self

    //日历代理
    self.calendarView.calendarDelegate = self

    //将菜单视图和日历视图添加到主视图上
    self.view.addSubview(menuView)
    self.view.addSubview(calendarView)
    }

    //今天按钮点击
    @IBAction func todayButtonTapped(_ sender: AnyObject) {
    let today = Date()
    self.calendarView.toggleViewWithDate(today)
    }

    override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    //更新日历frame
    self.menuView.commitMenuViewUpdate()
    self.calendarView.commitCalendarViewUpdate()
    }

    override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    }
    }

    extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
    //视图模式
    func presentationMode() -> CalendarMode {
    //使用月视图
    return .monthView
    }

    //每周的第一天
    func firstWeekday() -> Weekday {
    //从星期一开始
    return .monday
    }

    func presentedDateUpdated(_ date: CVDate) {
    //导航栏显示当前日历的年月
    self.title = date.globalDescription
    }

    //每个日期上面是否添加横线(连在一起就形成每行的分隔线)
    func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
    return true
    }

    //切换月的时候日历是否自动选择某一天(本月为今天,其它月为第一天)
    func shouldAutoSelectDayOnMonthChange() -> Bool {
    return false
    }

    //日期选择响应
    func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
    //获取日期
    let date = dayView.date.convertedDate()!
    // 创建一个日期格式器
    let dformatter = DateFormatter()
    dformatter.dateFormat = "yyyy年MM月dd日"
    let message = "当前选择的日期是:\(dformatter.string(from: date))"
    //将选择的日期弹出显示
    let alertController = UIAlertController(title: "", message: message,
    preferredStyle: .alert)
    let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
    alertController.addAction(okAction)
    self.present(alertController, animated: true, completion: nil)
    }
    }

    2,周视图使用样例

    同月视图模式相比,周视图日历区域只有一行(每次显示7天日期)。其它方面和月视图相比差别不大,也都是左右滑动切换显示下一周、下一周日期。


    import UIKit
    import CVCalendar

    class ViewController: UIViewController {
    //星期菜单栏
    private var menuView: CVCalendarMenuView!

    //日历主视图
    private var calendarView: CVCalendarView!

    var currentCalendar: Calendar!

    override func viewDidLoad() {
    super.viewDidLoad()

    currentCalendar = Calendar.init(identifier: .gregorian)

    //初始化的时候导航栏显示当年当月
    self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

    //初始化星期菜单栏
    self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

    //初始化日历主视图
    self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
    height:50))

    //星期菜单栏代理
    self.menuView.menuViewDelegate = self

    //日历代理
    self.calendarView.calendarDelegate = self

    //将菜单视图和日历视图添加到主视图上
    self.view.addSubview(menuView)
    self.view.addSubview(calendarView)
    }

    //今天按钮点击
    @IBAction func todayButtonTapped(_ sender: AnyObject) {
    let today = Date()
    self.calendarView.toggleViewWithDate(today)
    }

    override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    //更新日历frame
    self.menuView.commitMenuViewUpdate()
    self.calendarView.commitCalendarViewUpdate()
    }

    override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    }
    }

    extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
    //视图模式
    func presentationMode() -> CalendarMode {
    //使用周视图
    return .weekView
    }

    //每周的第一天
    func firstWeekday() -> Weekday {
    //从星期一开始
    return .monday
    }

    func presentedDateUpdated(_ date: CVDate) {
    //导航栏显示当前日历的年月
    self.title = date.globalDescription
    }

    //每个日期上面是否添加横线(连在一起就形成每行的分隔线)
    func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
    return true
    }

    //切换周的时候日历是否自动选择某一天(本周为今天,其它周为第一天)
    func shouldAutoSelectDayOnWeekChange() -> Bool {
    return false
    }

    //日期选择响应
    func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
    //获取日期
    let date = dayView.date.convertedDate(calendar: currentCalendar)!
    // 创建一个日期格式器
    let dformatter = DateFormatter()
    dformatter.dateFormat = "yyyy年MM月dd日"
    let message = "当前选择的日期是:\(dformatter.string(from: date))"
    //将选择的日期弹出显示
    let alertController = UIAlertController(title: "", message: message,
    preferredStyle: .alert)
    let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
    alertController.addAction(okAction)
    self.present(alertController, animated: true, completion: nil)
    }
    }

    转自:https://www.hangge.com/blog/cache/detail_1504.html#

    收起阅读 »

    iOS-使用SDCycleScrollView定制各种自定义样式的上下滚动的跑马灯

    SDCycleScrollView的优点及实现技巧:1.利用UICollectionView的复用机制,只会创建屏幕可见个cell。2.如果是无限循环 ,会存在100*self.imagePathsGroup.count个item,第一次出现的位置在(100*...
    继续阅读 »

    SDCycleScrollView的优点及实现技巧:

    1.利用UICollectionView的复用机制,只会创建屏幕可见个cell。
    2.如果是无限循环 ,会存在100*self.imagePathsGroup.count个item,第一次出现的位置在(100*self.imagePathsGroup.count)/2的位置。
    3.每次滚动到100*self.imagePathsGroup.count位置的item自动切换到(100*self.imagePathsGroup.count)/2的位置。
    4.使用取余index % self.imagePathsGroup.count确定现在显示的imageView

    缺点:

    手动拖拽到最后、不会跳到初始位置

    原因:

    因为作者设置的100足够大、未对拖拽最后一个item做处理

    解决方法:

    同时监听NSTimer和拖拽,在(100 - 1)*self.imagePathsGroup.count和self.imagePathsGroup.count位置时实现切换到(100*self.imagePathsGroup.count)/2的位置

    使用SDCycleScrollView制作各种自定义样式的上下滚动的跑马灯

    效果图:


    .m

    @interface ViewController () <SDCycleScrollViewDelegate>
    @end
    @implementation ViewController
    {
    NSArray *_imagesURLStrings;
    SDCycleScrollView *_customCellScrollViewDemo;
    }

    - (void)customCellScrollView {

    // 如果要实现自定义cell的轮播图,必须先实现customCollectionViewCellClassForCycleScrollView:和 setupCustomCell:forIndex:代理方法

    _customCellScrollViewDemo = [SDCycleScrollView cycleScrollViewWithFrame:CGRectMake(0, 820, w, 40) delegate:self placeholderImage:[UIImage imageNamed:@"placeholder"]];
    _customCellScrollViewDemo.currentPageDotImage = [UIImage imageNamed:@"pageControlCurrentDot"];
    _customCellScrollViewDemo.pageDotImage = [UIImage imageNamed:@"pageControlDot"];
    _customCellScrollViewDemo.imageURLStringsGroup = imagesURLStrings;
    _customCellScrollViewDemo.scrollDirection = UICollectionViewScrollDirectionVertical;
    _customCellScrollViewDemo.showPageControl = NO;
    [demoContainerView addSubview:_customCellScrollViewDemo];
    }

    // 不需要自定义轮播cell的请忽略下面的代理方法

    // 如果要实现自定义cell的轮播图,必须先实现customCollectionViewCellClassForCycleScrollView:和setupCustomCell:forIndex:代理方法
    - (Class)customCollectionViewCellClassForCycleScrollView:(SDCycleScrollView *)view
    {
    if (view != _customCellScrollViewDemo) {
    return nil;
    }
    return [CustomCollectionViewCell class];
    }

    - (void)setupCustomCell:(UICollectionViewCell *)cell forIndex:(NSInteger)index cycleScrollView:(SDCycleScrollView *)view
    {
    CustomCollectionViewCell *myCell = (CustomCollectionViewCell *)cell;
    //[myCell.imageView sd_setImageWithURL:_imagesURLStrings[index]];

    NSArray *titleArray = @[@"新闻",
    @"娱乐",
    @"体育"];
    NSArray *contentArray = @[@"新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻",
    @"娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐",
    @"体育体育体育体育体育体育体育体育体育体育体育体育"];
    myCell.titleLabel.text = titleArray[index];
    myCell.contentLabel.text = contentArray[index];
    }

    自定义cell-根据不同的cell定制各种自定义样式的上下滚动的跑马灯

    .h
    #import <UIKit/UIKit.h>

    @interface CustomCollectionViewCell : UICollectionViewCell

    @property (nonatomic, strong) UIImageView *imageView;
    @property (nonatomic, strong) UILabel *titleLabel;
    @property (nonatomic, strong) UILabel *contentLabel;

    @end
    .m
    #import "CustomCollectionViewCell.h"
    #import "UIView+SDExtension.h"

    @implementation CustomCollectionViewCell

    #pragma mark - 懒加载
    - (UIImageView *)imageView {
    if (!_imageView) {
    _imageView = [UIImageView new];
    _imageView.layer.borderColor = [[UIColor redColor] CGColor];
    _imageView.layer.borderWidth = 0;
    _imageView.hidden = YES;
    }
    return _imageView;
    }
    - (UILabel *)titleLabel {
    if (!_titleLabel) {
    _titleLabel = [[UILabel alloc]init];
    _titleLabel.text = @"新闻";
    _titleLabel.textColor = [UIColor redColor];
    _titleLabel.numberOfLines = 0;
    _titleLabel.textAlignment = NSTextAlignmentCenter;
    _titleLabel.font = [UIFont systemFontOfSize:12];
    _titleLabel.backgroundColor = [UIColor yellowColor];
    _titleLabel.layer.masksToBounds = YES;
    _titleLabel.layer.cornerRadius = 5;
    _titleLabel.layer.borderColor = [UIColor redColor].CGColor;
    _titleLabel.layer.borderWidth = 1.f;
    }
    return _titleLabel;
    }
    - (UILabel *)contentLabel {
    if (!_contentLabel) {
    _contentLabel = [[UILabel alloc]init];
    _contentLabel.text = @"我是label的内容";
    _contentLabel.textColor = [UIColor blackColor];
    _contentLabel.numberOfLines = 0;
    _contentLabel.font = [UIFont systemFontOfSize:12];
    }
    return _contentLabel;
    }
    #pragma mark - 页面初始化
    - (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
    self.contentView.backgroundColor = [UIColor whiteColor];
    [self setupViews];
    }
    return self;
    }

    #pragma mark - 添加子控件
    - (void)setupViews {
    [self.contentView addSubview:self.imageView];
    [self.contentView addSubview:self.titleLabel];
    [self.contentView addSubview:self.contentLabel];
    }

    #pragma mark - 布局子控件
    - (void)layoutSubviews {
    [super layoutSubviews];
    _imageView.frame = self.bounds;
    _titleLabel.frame = CGRectMake(15, 10, 45, 20);
    _contentLabel.frame = CGRectMake(15 + 45 + 15, 10, 200, 20);
    }

    实际情况自己可下载SDCycleScrollView自行研究。。。

    转自:https://www.jianshu.com/p/641403879f7b

    收起阅读 »

    iOS第三方——JazzHands

    JazzHands是UIKit一个简单的关键帧基础动画框架。可通过手势、scrollView,kvo或者ReactiveCocoa控制动画。JazzHands很适合用来创建很酷的引导页。Swift中的JazzHands想在Swift中使用Jazz Hands?...
    继续阅读 »

    JazzHands是UIKit一个简单的关键帧基础动画框架。可通过手势、scrollView,kvo或者ReactiveCocoa控制动画。JazzHands很适合用来创建很酷的引导页。


    Swift中的JazzHands

    想在Swift中使用Jazz Hands?可以试试RazzleDazzle。

    安装

    JazzHands可以通过CocoaPods安装,在Podfile中加入如下的一行:

    pod "JazzHands"

    你也可以把JazzHands文件夹的内容复制到工程中。

    快速开始

    首先,在UIViewController中加入JazzHands:

    #import <IFTTTJazzHands.h>

    现在创建一个Animator来管理UIViewController中所有的动画。

    @property (nonatomic, strong) IFTTTAnimator *animator;

    // later...

    self.animator = [IFTTTAnimator new];

    为你想要动画的view,创建一个animation。这儿有许多可以应用到view的animation。例如,我们使用IFTTTAlphaAnimation,可以使view淡入淡出。

    IFTTTAlphaAnimation *alphaAnimation = [IFTTTAlphaAnimation animationWithView: viewThatYouWantToAnimate];

    使用animator注册这个animation。

    [self.animator addAnimation: alphaAnimation];

    为animation添加一些keyframe关键帧。我们让这个view在times的30和60之间变淡(Let’s fade this view out between times 30 and 60)。

    [alphaAnimation addKeyframeForTime:30 alpha:1.f];
    [alphaAnimation addKeyframeForTime:60 alpha:0.f];

    现在,让view动起来,要让animator知道what time it is。例如,把这个animation和UIScrollView绑定起来,在scroll的代理方法中来通知animator。

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
    [super scrollViewDidScroll:scrollView];
    [self.animator animate:scrollView.contentOffset.x];
    }

    这样会产生的效果是,view在滚动位置的0到30之间时,view会淡入,变的可见。在滚动位置的30到60之间,view会淡出,变的不可见。而且在滚动位置大于60的时候会保持fade out。

    动画的类型

    Jazz Hands支持多种动画:

    IFTTTAlphaAnimation 动画的是 alpha 属性 (创造的是淡入淡出的效果).
    IFTTTRotationAnimation 动画的是旋转变换 (旋转效果).
    IFTTTBackgroundColorAnimation 动画的是 backgroundColor 属性.
    IFTTTCornerRadiusAnimation 动画的是 layer.cornerRadius 属性.
    IFTTTHideAnimation 动画的是 hidden属性 (隐藏和展示view).
    IFTTTScaleAnimation 应用一个缩放变换 (缩放尺寸).
    IFTTTTranslationAnimation 应用一个平移变换 (平移view的位置).
    IFTTTTransform3DAnimation 动画的是 layer.transform 属性 (是3D变换).
    IFTTTTextColorAnimation 动画的是UILabel的 textColor 属性。
    IFTTTFillColorAnimation 动画的是CAShapeLayer的fillColor属性。
    IFTTTStrokeStartAnimation 动画的是CAShapeLayer的strokeStart属性。(does not work with IFTTTStrokeEndAnimation).
    IFTTTStrokeEndAnimation 动画的是CAShapeLayer的strokeEnd属性。 (does not work with IFTTTStrokeStartAnimation).
    IFTTTPathPositionAnimation 动画的是UIView的layer.position属性。
    IFTTTConstraintConstantAnimation animates an AutoLayout constraint constant.
    IFTTTConstraintMultiplierAnimation animates an AutoLayout constraint constant as a multiple of an attribute of another view (to offset or resize views based on another view’s size)
    IFTTTScrollViewPageConstraintAnimation animates an AutoLayout constraint constant to place a view on a scroll view page (to position views on a scrollView using AutoLayout)
    IFTTTFrameAnimation animates the frame property (moves and sizes views. Not compatible with AutoLayout).

    更多例子

    Easy Paging Scrollview Layouts in an AutoLayout World
    JazzHands的IFTTTAnimatedPagingScrollViewController中的 keepView:onPage:方法,可以非常简单的在scroll view上布局分页。

    调用keepView:onPages: 可以在多个pages上展示一个view,当其它view滚动的时候。

    具体应用的例子

    在开源项目coding/Coding-iOS中的IntroductionViewController有使用到,IntroductionViewController继承自IFTTTAnimatedPagingScrollViewController。

    - (void)configureTipAndTitleViewAnimations{
    for (int index = 0; index < self.numberOfPages; index++) {
    NSString *viewKey = [self viewKeyForIndex:index];
    UIView *iconView = [self.iconsDict objectForKey:viewKey];
    UIView *tipView = [self.tipsDict objectForKey:viewKey];
    if (iconView) {
    if (index == 0) {//第一个页面
    [self keepView:iconView onPages:@[@(index +1), @(index)] atTimes:@[@(index - 1), @(index)]];

    [iconView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.mas_equalTo(kScreen_Height/7);
    }];
    }else{
    [self keepView:iconView onPage:index];

    [iconView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.centerY.mas_equalTo(-kScreen_Height/6);//位置往上偏移
    }];
    }
    IFTTTAlphaAnimation *iconAlphaAnimation = [IFTTTAlphaAnimation animationWithView:iconView];
    [iconAlphaAnimation addKeyframeForTime:index -0.5 alpha:0.f];
    [iconAlphaAnimation addKeyframeForTime:index alpha:1.f];
    [iconAlphaAnimation addKeyframeForTime:index +0.5 alpha:0.f];
    [self.animator addAnimation:iconAlphaAnimation];
    }
    if (tipView) {
    [self keepView:tipView onPages:@[@(index +1), @(index), @(index-1)] atTimes:@[@(index - 1), @(index), @(index + 1)]];

    IFTTTAlphaAnimation *tipAlphaAnimation = [IFTTTAlphaAnimation animationWithView:tipView];
    [tipAlphaAnimation addKeyframeForTime:index -0.5 alpha:0.f];
    [tipAlphaAnimation addKeyframeForTime:index alpha:1.f];
    [tipAlphaAnimation addKeyframeForTime:index +0.5 alpha:0.f];
    [self.animator addAnimation:tipAlphaAnimation];

    [tipView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(iconView.mas_bottom).offset(kScaleFrom_iPhone5_Desgin(45));
    }];
    }
    }
    }

    效果如下:


    转自:https://blog.csdn.net/u014084081/article/details/53610215

    收起阅读 »

    【iOS】自动布局之Purelayout

    masonry这个第三方库件在github上很出名,貌似也很好用,但是我在看过masonry的介绍和使用方法之后,觉得有点隐隐的蛋疼。因为本人工作时间不多,加上一直都用的是Objective-C,看着masonry提供的方法基本上都是点语法,我的[]呢?!!怎...
    继续阅读 »

    masonry这个第三方库件在github上很出名,貌似也很好用,但是我在看过masonry的介绍和使用方法之后,觉得有点隐隐的蛋疼。
    因为本人工作时间不多,加上一直都用的是Objective-C,看着masonry提供的方法基本上都是点语法,我的[]呢?!!怎么不在了?

    于是在github上搜索到另外一个较出名的布局,便有了这段Purelayout的尝试。

    生成一个UIView:

    UIView *view = [UIView newAutoLayoutView];
    + (instancetype)newAutoLayoutView
    {
    ALView *view = [self new];
    view.translatesAutoresizingMaskIntoConstraints = NO;
    return view;
    }

    newAutoLayoutView是UIView的一个扩展方法,其实达到的目的就是生成一个UIView实例,并把该实例的translatesAutoresizingMaskIntoConstraints属性置为NO。这个属性值在默认情况下是YES,如果设置为 NO,那么在运行时,程序不会自动将AutoresizingMask转化成 Constraint。

    1.view相对于父容器间距的位置

    [view autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:10];//相对于父容器顶部距离10
    [view autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:10];//相对于父容器左部距离10
    [view autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:10];//相对于父容器右部距离10
    [view autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10];//相对于父容器底部距离10

    值得注意的是Purelayout对UILabel做了一些人性化的处理:
    在有的国家地区文字是从右至左的,以下代码就是将label的起始位置距离父容器10

    [label autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:10];

    2.相对于父容器的中心位置:

    [view autoCenterInSuperview];//view在父容器中心位置
    [view autoAlignAxisToSuperviewAxis:ALAxisHorizontal];//view在父容器水平中心位置
    [view autoAlignAxisToSuperviewAxis:ALAxisVertical];//view在父容器垂直中心位置

    3.设置大小

    [view autoSetDimensionsToSize:CGSizeMake(300, 300)];//设置view的大小为300*300
    [view autoSetDimension:ALDimensionHeight toSize:300];//设置view的高度为300
    [view autoSetDimension:ALDimensionWidth toSize:300];//设置view的宽度为300

    4.相对位置
    NSLayoutRelation是一个枚举类型:

    typedef NS_ENUM(NSInteger, NSLayoutRelation) {
    NSLayoutRelationLessThanOrEqual = -1,
    NSLayoutRelationEqual = 0,
    NSLayoutRelationGreaterThanOrEqual = 1,
    };

    见名知意,你懂的。

    [view1 autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:view2 withOffset:20 relation:NSLayoutRelationEqual];//view1的顶部在view2的底部的20像素的位置

    5.中心对齐

    [view1 autoAlignAxis:ALAxisVertical toSameAxisOfView:view2];//view1相对于view2保持在同一个垂直中心上

    view1相对于view2保持在同一个垂直中心上

    6.相对大小

    [view1 autoMatchDimension:ALDimensionWidth toDimension:ALDimensionWidth ofView:view2];

    view1的宽度和view2的宽度相等

    在使用purelayout的时候值得注意:
    1.purelayout提供的方法有些是只支持iOS8及以上的,如果iOS7及以下的调用了是会奔溃的,本人就是因为这个被搞得欲仙欲死。好在purelayout在方法中都有介绍。以上介绍的几种使用场景的方法,也都是支持iOS7及以下系统的。
    2.在view父容器为nil的时候,执行purelayout的方法会崩溃。

    有兴趣的可以直接去github下载官方的demo,写的也是相当ok的。

    持续更新~~~

    链接:https://www.jianshu.com/p/15bb1bfec5e9

    收起阅读 »

    SVProgressHUD简单使用以及自定义动画

    SVProgressHUD 是一个干净,易于使用的HUD,旨在显示iOS和tvOS正在进行的任务的进展。常用的还有MBProgressHUD.这两个都是很常用的HUD,大体相似,但是还是有一些不同的.MBProgressHUD和SVProgressHUD的区别...
    继续阅读 »

    SVProgressHUD 是一个干净,易于使用的HUD,旨在显示iOS和tvOS正在进行的任务的进展。
    常用的还有MBProgressHUD.这两个都是很常用的HUD,大体相似,但是还是有一些不同的.
    MBProgressHUD和SVProgressHUD的区别:
    svprogresshud 使用起来很方便,但 可定制 差一些,看它的接口貌似只能添加一个全屏的HUD,不能把它添加到某个视图上面去.
    MBProgressHUD 功能全一些,可定制 高一些,而且可以指定加到某一个View上去.用起来可能就没上面那个方便了.
    具体还要看你的使用场景.
    附上GitHub源码地址:
    SVProgressHUD:https://github.com/SVProgressHUD/SVProgressHUD
    MBProgressHUD:https://github.com/jdg/MBProgressHUD
    今天我们不对二者的区别做详解,有空我会专门写文章对它们的区别做一个详解.
    今天我们主要简单介绍一下SVProgressHUD的使用.


    安装

    通过CocoaPods安装,在Podfile中加入pod 'SVProgressHUD',这里不多做介绍.可以参考文章: CocoaPods的简单使用

    使用

    SVProgressHUD是已经被创建为单例的,所以不需要被实例化了,可以直接使用.调用它的方法[SVProgressHUD method].

    [SVProgressHUD show ];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^ {
    //耗时的任务
    dispatch_async(dispatch_get_main_queue(),^ {
    [SVProgressHUD dismiss ];
    });
    });

    显示HUD

    可以在下拉刷新或者执行其他耗时任务的时候,使用下面方法之一,来显示不确定任务的状态:

    + (void)show;
    + (void)showWithStatus:(NSString*)string;

    效果图分别为:



    如果你希望HUD反应任务的进度,可以使用下面方法的其中一个:

    + (void)showProgress:(CGFloat)progress;
    + (void)showProgress:(CGFloat)progress status:(NSString*)status;

    通过其他方式可以实现进度条的速度把控.比如:

    - (IBAction)clickButtonsShowWithProgress:(id)sender {
    progress = 0.0f;
    [SVProgressHUD showProgress:0 status:@"Loading"];
    [self performSelector:@selector(increaseProgress) withObject:nil afterDelay:0.1f];
    }

    - (void)increaseProgress {
    progress += 0.05f;
    [SVProgressHUD showProgress:progress status:@"xuanhe Loading"];

    if(progress < 1.0f){
    [self performSelector:@selector(increaseProgress) withObject:nil afterDelay:0.1f];
    } else {
    [self performSelector:@selector(dismiss) withObject:nil afterDelay:0.4f];
    }
    }

    效果如下


    还有其他常用的语法:

    +(void)showInfoWithStatus :( NSString *)string;
    +(void)showSuccessWithStatus :( NSString *)string;
    +(void)showErrorWithStatus :( NSString *)string;
    +(void)showImage:(UIImage *)image status :( NSString *)string;

    取消HUD

    HUD可以使用以下方式解除:

    +(void)dismiss;
    +(void)dismissWithDelay :( NSTimeInterval)delay;
    + (void)dismissWithDelay:(NSTimeInterval)delay completion:(SVProgressHUDDismissCompletion)completion;

    可以对这些代码进行改进,比如,在弹框结束后执行其他操作.可以封装一个方法,弹框结束后,执行Block.

    定制

    SVProgressHUD 可以通过以下方法定制:

    + (void)setDefaultStyle:(SVProgressHUDStyle)style;                  // default is SVProgressHUDStyleLight
    + (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType; // default is SVProgressHUDMaskTypeNone
    + (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type; // default is SVProgressHUDAnimationTypeFlat
    + (void)setContainerView:(UIView*)containerView; // default is window level
    + (void)setMinimumSize:(CGSize)minimumSize; // default is CGSizeZero, can be used to avoid resizing
    + (void)setRingThickness:(CGFloat)width; // default is 2 pt
    + (void)setRingRadius:(CGFloat)radius; // default is 18 pt
    + (void)setRingNoTextRadius:(CGFloat)radius; // default is 24 pt
    + (void)setCornerRadius:(CGFloat)cornerRadius; // default is 14 pt
    + (void)setBorderColor:(nonnull UIColor*)color; // default is nil
    + (void)setBorderWidth:(CGFloat)width; // default is 0
    + (void)setFont:(UIFont*)font; // default is [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]
    + (void)setForegroundColor:(UIColor*)color; // default is [UIColor blackColor], only used for SVProgressHUDStyleCustom
    + (void)setBackgroundColor:(UIColor*)color; // default is [UIColor whiteColor], only used for SVProgressHUDStyleCustom
    + (void)setBackgroundLayerColor:(UIColor*)color; // default is [UIColor colorWithWhite:0 alpha:0.4], only used for SVProgressHUDMaskTypeCustom
    + (void)setImageViewSize:(CGSize)size; // default is 28x28 pt
    + (void)setInfoImage:(UIImage*)image; // default is the bundled info image provided by Freepik
    + (void)setSuccessImage:(UIImage*)image; // default is bundled success image from Freepik
    + (void)setErrorImage:(UIImage*)image; // default is bundled error image from Freepik
    + (void)setViewForExtension:(UIView*)view; // default is nil, only used if #define SV_APP_EXTENSIONS is set
    + (void)setGraceTimeInterval:(NSTimeInterval)interval; // default is 0 seconds
    + (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval; // default is 5.0 seconds
    + (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval; // default is CGFLOAT_MAX
    + (void)setFadeInAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
    + (void)setFadeOutAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
    + (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel; // default is UIWindowLevelNormal
    + (void)setHapticsEnabled:(BOOL)hapticsEnabled; // default is NO

    样式

    作为标准SVProgressHUD提供两种预先配置的样式:

    SVProgressHUDStyleLight白色背景黑色图标和文字
    SVProgressHUDStyleDark黑色背景与白色图标和文本
    如果要使用自定义颜色使用setForegroundColor和setBackgroundColor:。这些方法将HUD的风格置为SVProgressHUDStyleCustom。

    触觉反馈

    对于具有较新设备的用户(从iPhone 7开始),SVProgressHUD可以根据显示的HUD来自动触发触觉反馈。反馈图如下:

    showSuccessWithStatus: < - > UINotificationFeedbackTypeSuccess

    showInfoWithStatus: < - > UINotificationFeedbackTypeWarning

    showErrorWithStatus: < - > UINotificationFeedbackTypeError

    要启用此功能,请使用setHapticsEnabled: 。

    具有iPhone 7之前的设备的用户将不会改变功能。

    通知

    SVProgressHUD发布四个通知,NSNotificationCenter以响应被显示/拒绝:

    SVProgressHUDWillAppearNotification 提示框即将出现
    SVProgressHUDDidAppearNotification 提示框已经出现
    SVProgressHUDWillDisappearNotification 提示框即将消失
    SVProgressHUDDidDisappearNotification 提示框已经消失

    每个通知通过一个userInfo保存HUD状态字符串(如果有的话)的字典,可以通过检索SVProgressHUDStatusUserInfoKey。

    SVProgressHUD SVProgressHUDDidReceiveTouchEventNotification当用户触摸整个屏幕或SVProgressHUDDidTouchDownInsideNotification用户直接触摸HUD时也会发布。由于此通知userInfo未被传递,而对象参数包含UIEvent与触摸相关的参数。

    应用扩展

    这里对这个功能不做详解.自行摸索.

    自定义动画

    SVProgressHUD提供了方法可以自定义图片.但是不支持gif格式,直接利用下面的方法依然显示一张静态的图片

    [SVProgressHUD showImage:[UIImage imageNamed:@"loading.gif"] status:@"加载中..."];

    我们可以把gif转化为一个动态的image.
    下面是我在百度上搜的一个方法.仅供参考.

    #import <UIKit/UIKit.h>

    typedef void (^GIFimageBlock)(UIImage *GIFImage);
    @interface UIImage (GIFImage)

    /** 根据本地GIF图片名 获得GIF image对象 */
    + (UIImage *)imageWithGIFNamed:(NSString *)name;

    /** 根据一个GIF图片的data数据 获得GIF image对象 */
    + (UIImage *)imageWithGIFData:(NSData *)data;

    /** 根据一个GIF图片的URL 获得GIF image对象 */
    + (void)imageWithGIFUrl:(NSString *)url and:(GIFimageBlock)gifImageBlock;

    下面是.m的方法实现.

    #import "UIImage+GIFImage.h"
    #import <ImageIO/ImageIO.h>
    @implementation UIImage (GIFImage)
    + (UIImage *)imageWithGIFData:(NSData *)data{

    if (!data) return nil;
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    size_t count = CGImageSourceGetCount(source);
    UIImage *animatedImage;
    if (count <= 1) {
    animatedImage = [[UIImage alloc] initWithData:data];
    } else {
    NSMutableArray *images = [NSMutableArray array];
    NSTimeInterval duration = 0.0f;
    for (size_t i = 0; i < count; i++) {
    // 拿出了Gif的每一帧图片
    CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
    //Learning... 设置动画时长 算出每一帧显示的时长(帧时长)
    NSTimeInterval frameDuration = [UIImage sd_frameDurationAtIndex:i source:source];
    duration += frameDuration;
    // 将每帧图片添加到数组中
    [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
    // 释放真图片对象
    CFRelease(image);
    }
    // 设置动画时长
    if (!duration) {
    duration = (1.0f / 10.0f) * count;
    }
    animatedImage = [UIImage animatedImageWithImages:images duration:duration];
    }

    // 释放源Gif图片
    CFRelease(source);
    return animatedImage;
    }
    + (UIImage *)imageWithGIFNamed:(NSString *)name{
    NSUInteger scale = (NSUInteger)[UIScreen mainScreen].scale;
    return [self GIFName:name scale:scale];
    }

    + (UIImage *)GIFName:(NSString *)name scale:(NSUInteger)scale{
    NSString *imagePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@@%zdx", name, scale] ofType:@"gif"];
    if (!imagePath) {
    (scale + 1 > 3) ? (scale -= 1) : (scale += 1);
    imagePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@@%zdx", name, scale] ofType:@"gif"];
    }
    if (imagePath) {
    // 传入图片名(不包含@Nx)
    NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
    return [UIImage imageWithGIFData:imageData];
    } else {
    imagePath = [[NSBundle mainBundle] pathForResource:name ofType:@"gif"];
    if (imagePath) {
    // 传入的图片名已包含@Nx or 传入图片只有一张 不分@Nx
    NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
    return [UIImage imageWithGIFData:imageData];
    } else {
    // 不是一张GIF图片(后缀不是gif)
    return [UIImage imageNamed:name];
    }
    }
    }
    + (void)imageWithGIFUrl:(NSString *)url and:(GIFimageBlock)gifImageBlock{
    NSURL *GIFUrl = [NSURL URLWithString:url];
    if (!GIFUrl) return;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSData *CIFData = [NSData dataWithContentsOfURL:GIFUrl];
    // 刷新UI在主线程
    dispatch_async(dispatch_get_main_queue(), ^{
    gifImageBlock([UIImage imageWithGIFData:CIFData]);
    });
    });
    }
    #pragma mark - <关于GIF图片帧时长(Learning...)>
    + (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
    float frameDuration = 0.1f;
    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
    NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
    NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
    NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
    if (delayTimeUnclampedProp) {
    frameDuration = [delayTimeUnclampedProp floatValue];
    }
    else {
    NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
    if (delayTimeProp) {
    frameDuration = [delayTimeProp floatValue];
    }
    }
    // Many annoying ads specify a 0 duration to make an image flash as quickly as possible.
    // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify
    // a duration of <= 10 ms. See and
    // for more information.
    if (frameDuration < 0.011f) {
    frameDuration = 0.100f;
    }
    CFRelease(cfFrameProperties);
    return frameDuration;
    }
    @end

    这个是UIimage的分类,在用到的控制器里面调用代码方法即可.这个分类实现我也不太懂.只会用.

    _imgView1.image = [UIImage imageWithGIFNamed:@"xuanxuan"];

    NSString *path = [[NSBundle mainBundle] pathForResource:@"xuanxuan" ofType:@"gif"];
    NSData *imgData = [NSData dataWithContentsOfFile:path];
    _imgView2.image = [UIImage imageWithGIFData:imgData];


    [UIImage imageWithGIFUrl:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1495708809771&di=da92fc5cf3bdd684711ab5124ee43183&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%253D580%2Fsign%3D91bd6cd2d42a60595210e1121835342d%2F212eb9389b504fc215d0301ee6dde71190ef6d1a.jpg" and:^(UIImage *GIFImage) {
    _imgView3.image = GIFImage;
    }];

    转自:https://www.jianshu.com/p/fa22b7c27e1d

    收起阅读 »

    ios中应用Lottie解决动画问题

    Lottie的简单介绍:使用Lottie开发的流程是: 设计师在AE中设计完成你的动画,通过bodymoving插件导出纪录动画信息的JSON文件,然后开发人员使用 Lottie 的Android,iOS,React Native apps开源动画库读取这份J...
    继续阅读 »

    Lottie的简单介绍:

    使用Lottie开发的流程是: 设计师在AE中设计完成你的动画,通过bodymoving插件导出纪录动画信息的JSON文件,然后开发人员使用 Lottie 的Android,iOS,React Native apps开源动画库读取这份JSON文件, 解析动画结构和参数信息并渲染。

    Lottie的优点:

    1、设计即所见: 设计师用AE设计好动画后直接导出Json文件,Lottie 解析Json文件后调Core Animation的API绘制渲染。还原度更好,开发成本更低。
    2、跨平台: 支持iOS、Android、React Native。
    3、性能:Lottie对于从AE导出的Json文件,用Core Animation做矢量动画, 性能较佳。Lottie 对解析后的数据模型有内存缓存。但是对多图片帧动画,性能比较差。
    支持动画属性多:比起脸书的Keyframes,Lottie支持了更多AE动画属性,比如Mask, Trim Paths,Stroke (shape layer)等。
    4、包大小,相比动辄上百K的帧动画,Json文件包大小很小。有图片资源的情况下,同一张图片也可以被多个图层复用,而且运行时内存中只有一个UIImage对象(iOS)。

    Lottie在iOS中的使用

    1、pod 'lottie-ios' 使用cocoaPods来加载Lottie。
    2、在使用的界面添加头文件#import <Lottie/Lottie.h>
    3、简单的使用介绍(要想深入学习,还需要自己点击进入源代码中去深究每一个方法和属性,在此就不一一列举了)

    LOTAnimationView * animation = [LOTAnimationView animationNamed:@"HappyBirthday"];
    animation.loopAnimation = YES; //是否是循环播放
    animation.frame = self.view.bounds;
    [self.view addSubview:animation];
    animation.backgroundColor = [UIColor whiteColor];
    [animation playWithCompletion:^(BOOL animationFinished) {
    //播放完成,循环播放则不进入此方法
    }];
    //可以以动画为北京来添加子控件
    UILabel * newV = [[UILabel alloc]initWithFrame:CGRectMake(100,100,200,100)];
    newV.backgroundColor = [UIColor clearColor];
    newV.textColor = [UIColor blackColor];
    newV.text = @"Lottie的使用教程";
    [animation addSubview:newV];

    另外的创建方法

    /// Load animation by name from the default bundle, Images are also loaded from the bundle
    + (nonnull instancetype)animationNamed:(nonnull NSString *)animationName NS_SWIFT_NAME(init(name:));

    /// Loads animation by name from specified bundle, Images are also loaded from the bundle
    + (nonnull instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle NS_SWIFT_NAME(init(name:bundle:));

    /// Creates an animation from the deserialized JSON Dictionary
    + (nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));

    /// Loads an animation from a specific file path. WARNING Do not use a web URL for file path.
    + (nonnull instancetype)animationWithFilePath:(nonnull NSString *)filePath NS_SWIFT_NAME(init(filePath:));

    /// Creates an animation from the deserialized JSON Dictionary, images are loaded from the specified bundle
    + (nonnull instancetype)animationFromJSON:(nullable NSDictionary *)animationJSON inBundle:(nullable NSBundle *)bundle NS_SWIFT_NAME(init(json:bundle:));

    /// Creates an animation from the LOTComposition, images are loaded from the specified bundle
    - (nonnull instancetype)initWithModel:(nullable LOTComposition *)model inBundle:(nullable NSBundle *)bundle;

    /// Loads animation asynchrounously from the specified URL
    - (nonnull instancetype)initWithContentsOfURL:(nonnull NSURL *)url;

    LOTAnimationView的属性

    /// Flag is YES when the animation is playing
    @property (nonatomic, readonly) BOOL isAnimationPlaying;

    /// Tells the animation to loop indefinitely.
    @property (nonatomic, assign) BOOL loopAnimation;

    /// The animation will play forward and then backwards if loopAnimation is also YES
    @property (nonatomic, assign) BOOL autoReverseAnimation;

    /// Sets a progress from 0 - 1 of the animation. If the animation is playing it will stop and the compeltion block will be called.
    /// The current progress of the animation in absolute time.
    /// e.g. a value of 0.75 always represents the same point in the animation, regardless of positive
    /// or negative speed.
    @property (nonatomic, assign) CGFloat animationProgress;

    /// Sets the speed of the animation. Accepts a negative value for reversing animation.
    @property (nonatomic, assign) CGFloat animationSpeed;

    /// Read only of the duration in seconds of the animation at speed of 1
    @property (nonatomic, readonly) CGFloat animationDuration;

    /// Enables or disables caching of the backing animation model. Defaults to YES
    @property (nonatomic, assign) BOOL cacheEnable;

    /// Sets a completion block to call when the animation has completed
    @property (nonatomic, copy, nullable) LOTAnimationCompletionBlock completionBlock;

    /// Set the amimation data
    @property (nonatomic, strong, nullable) LOTComposition *sceneModel;

    4、简单应用的场景:(1)App的动画引导页。(2)一些特定的动画界面。(3)来作为Tabbar来使用。
    5、这里来介绍下作为Tabbar的使用gitHub上原作者
    6、Lottie动画资源网站
    7、后续有新的学习会更新的。

    链接:https://www.jianshu.com/p/7af085a6a20a

    收起阅读 »

    iOS 音视频编解码基本概念

    内容元素:图像(Image)⾳频(Audio)元信息(Metadata)编码格式: • Video: H264Audio: AAC容器封装: • MP4/MOV/FLV/RM/RMVB/AVI.视频相关基础概念1.视频文件格式相信大家平时接触的word文件后面...
    继续阅读 »



    内容元素:

    • 图像(Image)

    • ⾳频(Audio)

    • 元信息(Metadata)

    • 编码格式: • Video: H264

    • Audio: AAC

    • 容器封装: • MP4/MOV/FLV/RM/RMVB/AVI

    • .视频相关基础概念

      • 1.视频文件格式

        相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.jpg等,我们常见的视频文件后缀有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。这些后缀名通常在操作系统上用相应的应用程序打开,比如.doc用word打开。对于视频来说,为什么会有这么多的文件格式了,那是因为通过了不同的方式来实现了视频这件事---------视频的封装格式。
    • 2.视频的封装格式

      视频封装格式,通常我们把它称作为视频格式,它相当于一种容器,比如可乐的瓶子,矿泉水瓶等等。它里面包含了视频的相关信息(视频信息,音频信息,解码方式等等),一种封装格式直接反应了视频的文件格式,封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.




    • 相关视频封装格式的优缺点:

      • 1.AVI 格式:这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。
      • 2.WMV 格式:可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。
      • 3.MPEG 格式:为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。
      • 4.Matroska 格式:是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。
      • 5.Real Video 格式:用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。
      • 6.QuickTime File Format 格式:是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。
      • 7.Flash Video 格式: Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。
    • 视频的编码格式

    • 视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.在做视频编解码时,需要考虑以下这些因素的平衡:

      • 视频的质量、
      • 用来表示视频所需要的数据量(通常称之为码率)、
      • 编码算法和解码算法的复杂度
      • 针对数据丢失和错误的鲁棒性(Robustness)
      • 编辑的方便性
      • 随机访问
      • 编码算法设计的完美性
      • 端到端的延时以及其它一些因素
    • 常见的编码方式:

    • H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

      • H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
      • H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
      • H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
      • H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
    • 当前不建议用H.265是因为太过于消耗CPU,而且目前H.264已经满足了大多的视频需求,虽然H.265是H.264的升级版,期待后续硬件跟上

    • MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

      • MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
      • MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
      • MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
        其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。
    • 可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

    • 音频编码方式

      • 视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

      • AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。

      • MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。

      • WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

    直播/小视频中的编码格式

    • 视频编码格式

      • H264编码的优势:
        低码率
        高质量的图像
        容错能力强
        网络适应性强
    • 总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
      举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
      音频编码格式:

    • AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

    • LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)

    • HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)

    • 优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码,适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

    关于H264

    • H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

    • 图像

      • H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

    当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

    当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

    「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好




    • 片(Slice),每一帧图像可以分为多个片

    网络提取层单元(NALU, Network Abstraction Layer Unit),
    NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

    宏块(Macroblock),分片是由宏块组成。




    作者:枫紫
    链接:https://www.jianshu.com/p/9602f3c9b82b


    收起阅读 »

    iOS 特效 - iCarousel

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其...
    继续阅读 »

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其他“CoverFlow”库不同,iCarousel 可以处理任何类型的视图,而不仅仅是图像,因此它非常适合在您的应用程序中以流畅且令人印象深刻的方式呈现分页数据。它还使得以最少的代码更改在不同的轮播效果之间切换变得非常容易。

    支持的操作系统和 SDK 版本

    • 支持的构建目标 - iOS 10.0 / Mac OS 10.12(Xcode 8.0,Apple LLVM 编译器 8.0)
    • 最早支持的部署目标 - iOS 5.0 / Mac OS 10.7
    • 最早的兼容部署目标 - iOS 4.3 / Mac OS 10.6

    注意:“支持”表示该库已经过此版本的测试。“兼容”意味着库应该在这个操作系统版本上工作(即它不依赖于任何不可用的 SDK 功能)但不再进行兼容性测试,可能需要调整或错误修复才能正确运行。

    ARC兼容性

    从 1.8 版开始,iCarousel 需要 ARC。如果您希望在非 ARC 项目中使用 iCarousel,只需将 -fobjc-arc 编译器标志添加到 iCarousel.m 类。为此,请转到目标设置中的 Build Phases 选项卡,打开 Compile Sources 组,双击列表中的 iCarousel.m 并在弹出窗口中键入 -fobjc-arc。

    如果您希望将整个项目转换为 ARC,请在 iCarousel.m 中注释掉 #error 行,然后在 Xcode 中运行 Edit > Refactor > Convert to Objective-C ARC... 工具并确保您希望转换的所有文件使用 ARC 进行(包括 iCarousel.m)检查。

    线程安全

    iCarousel 派生自 UIView 并且 - 与所有 UIKit 组件一样 - 它只能从主线程访问。您可能希望使用线程来加载或更新轮播内容或项目,但始终确保一旦您的内容加载完毕,您就可以在更新轮播前切换回主线程。

    安装

    要在应用程序中使用 iCarousel 类,只需将 iCarousel 类文件(不需要演示文件和资产)拖到您的项目中并添加 QuartzCore 框架。您也可以使用 Cocoapods 以正常方式安装它。


    轮播类型

    iCarousel 支持以下内置显示类型:

    • iCarouselTypeLinear
    • iCarouselTypeRotary
    • iCarouselTypeInvertedRotary
    • iCarouselTypeCylinder
    • iCarouselTypeInvertedCylinder
    • iCarouselTypeWheel
    • iCarouselTypeInvertedWheel
    • iCarouselTypeCoverFlow
    • iCarouselTypeCoverFlow2
    • iCarouselTypeTimeMachine
    • iCarouselTypeInvertedTimeMachine

    您还可以使用iCarouselTypeCustomcarousel:itemTransformForOffset:baseTransform:委托方法实现自己的定制轮播样式

    注意:iCarouselTypeCoverFlowiCarouselTypeCoverFlow2类型之间的区别非常微妙,但是 for 的逻辑要iCarouselTypeCoverFlow2复杂得多。如果您轻弹转盘,它们基本上是相同的,但是如果您用手指缓慢拖动转盘,则差异应该很明显。iCarouselTypeCoverFlow2旨在尽可能接近地模拟标准 Apple CoverFlow 效果,并且将来可能会为了该目标而进行微妙的更改。

    显示类型可视化示例

    线性

    线性

    旋转式

    旋转式


    倒转

    倒转


    圆筒

    圆筒

    倒置气缸

    倒置气缸

    Cover Flow功能

    Cover Flow功能



    特性

    iCarousel 具有以下属性(注意:对于 Mac OS,在使用属性时将 NSView 替换为 UIView):

    @property (nonatomic, weak) IBOutlet id dataSource;

    一个支持 iCarouselDataSource 协议并可以提供视图来填充轮播的对象。

    @property (nonatomic, weak) IBOutlet id delegate;

    一个支持 iCarouselDelegate 协议并且可以响应轮播事件和布局请求的对象。

    @property (nonatomic, assign) iCarouselType type;

    用于切换轮播显示类型(详见上文)。

    @property (nonatomic, assign) CGFloat perspective;

    用于调整各种 3D 轮播视图的透视缩短效果。应为负值,小于 0 且大于 -0.01。超出此范围的值将产生非常奇怪的结果。默认值为 -1/500 或 -0.005;

    @property (nonatomic, assign) CGSize contentOffset;

    此属性用于调整轮播项目视图相对于轮播中心的偏移。它默认为 CGSizeZero,这意味着轮播项目居中。更改此值会移动轮播项目而不改变其视角,即消失点随轮播项目移动,因此如果您将轮播项目向下移动,则不会看起来好像您在俯视轮播。

    @property (nonatomic, assign) CGSize viewpointOffset;

    此属性用于调整相对于轮播项目的用户视角。它与调整 contentOffset 有相反的效果,即如果您向上移动视点,则轮播似乎向下移动。与 contentOffset 不同,移动视点也会改变相对于旋转木马项目的透视消失点,因此如果您向上移动视点,它会看起来好像您在俯视旋转木马。

    @property (nonatomic, assign) CGFloat decelerationRate;

    旋转木马在轻弹时减速的速率。较高的值意味着较慢的减速。默认值为 0.95。值应在 0.0(释放时旋转木马立即停止)到 1.0(旋转木马无限期地继续而不减速,除非它到达终点)的范围内。

    @property (nonatomic, assign) BOOL bounces;

    设置旋转木马是应该弹过终点并返回,还是停止不动。请注意,这对设计为包装的轮播类型或 carouselShouldWrap 委托方法返回 YES 的类型没有影响。

    @property (nonatomic, assign) CGFloat bounceDistance;

    未包裹的传送带越过末端时反弹的最大距离。这是以 itemWidth 的倍数来衡量的,因此值 1.0 表示轮播将反弹整个项目宽度,值 0.5 表示项目宽度的一半,依此类推。默认值为 1.0;

    @property (nonatomic, assign, getter = isScrollEnabled) BOOL scrollEnabled;

    启用和禁用用户滚动轮播。如果此属性设置为 NO,则仍然可以通过编程方式滚动轮播。

    @property (nonatomic, readonly, getter = isWrapEnabled) BOOL wrapEnabled;

    如果启用包装,则返回 YES,否则返回 NO。此属性是只读的。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 返回一个值iCarouselOptionWrap

    @property (nonatomic, assign, getter = isPagingEnabled) BOOL pagingEnabled;

    启用和禁用分页。启用分页后,轮播将在用户滚动时在每个项目视图处停止,这与 UIScrollView 的 pagingEnabled 属性非常相似。

    @property (nonatomic, readonly) NSInteger numberOfItems;

    轮播中的项目数(只读)。要设置它,请实现numberOfItemsInCarousel:dataSource 方法。请注意,并非所有这些项目视图都会在给定的时间点加载或可见 - 轮播在滚动时按需加载项目视图。

    @property (nonatomic, readonly) NSInteger numberOfPlaceholders;

    要在轮播中显示的占位符视图的数量(只读)。要设置它,请实现numberOfPlaceholdersInCarousel:dataSource 方法。

    @property (nonatomic, readonly) NSInteger numberOfVisibleItems;

    屏幕上同时显示的轮播项目视图的最大数量(只读)。此属性对于性能优化很重要,并且会根据轮播类型和视图框架自动计算。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 iCarouselOptionVisibleItems 返回一个值。

    @property (nonatomic, strong, readonly) NSArray *indexesForVisibleItems;

    一个数组,包含当前加载和在轮播中可见的所有项目视图的索引,包括占位符视图。该数组包含 NSNumber 对象,其整数值与视图的索引匹配。项目视图的索引从零开始并匹配传递给 dataSource 以加载视图的索引,但是任何可见占位符视图的索引要么是负数(小于零)要么大于或等于numberOfItems此数组中占位符视图的索引等同于与 dataSource 一起使用的占位符视图索引。

    @property (nonatomic, strong, readonly) NSArray *visibleItemViews;

    当前显示在轮播中的所有项目视图的数组(只读)。这包括任何可见的占位符视图。此数组中的视图索引与项目索引不匹配,但是这些视图的顺序与 visibleItemIndexes 数组属性的顺序匹配,即您可以通过从visibleItemIndexes 数组(或者,您可以只使用该indexOfItemView:方法,这要容易得多)。

    @property (nonatomic, strong, readonly) UIView *contentView;

    包含轮播项目视图的视图。如果您想将它们与轮播项目散布,您可以向此视图添加子视图。如果您希望视图出现在所有轮播项目的前面或后面,您应该将其直接添加到 iCarousel 视图本身。请注意,当应用程序运行时, contentView 中的视图顺序会经常发生且未记录的更改。添加到 contentView 的任何视图都应将其 userInteractionEnabled 属性设置为 NO 以防止与 iCarousel 的触摸事件处理发生冲突。

    @property (nonatomic, assign) CGFloat scrollOffset;

    这是轮播的当前滚动偏移量,是 itemWidth 的倍数。这个值,四舍五入到最接近的整数,是 currentItemIndex 值。您可以使用此值在轮播移动时定位其他屏幕元素。如果您希望以编程方式将轮播滚动到特定偏移量,也可以设置该值。如果您希望禁用内置手势处理并提供您自己的实现,这可能很有用。

    @property (nonatomic, readonly) CGFloat offsetMultiplier;

    这是用户用手指拖动轮播时使用的偏移乘数。它不影响编程滚动或减速速度。对于大多数轮播类型,这默认为 1.0,但对于 CoverFlow 风格的轮播默认为 2.0,以补偿它们的项目间隔更近的事实,因此必须进一步拖动以移动相同的距离。您不能直接设置此属性,但可以通过实现carouselOffsetMultiplier:委托方法来覆盖默认值

    @property (nonatomic, assign) NSInteger currentItemIndex;

    轮播中当前居中项目的索引。设置此属性等效于scrollToItemAtIndex:animated:将动画参数设置为 NO进行调用

    @property (nonatomic, strong, readonly) UIView *currentItemView;

    轮播中当前居中的项目视图。此视图的索引匹配currentItemIndex

    @property (nonatomic, readonly) CGFloat itemWidth;

    轮播中项目的显示宽度(只读)。这是从使用carousel:viewForItemAtIndex:reusingView:dataSource 方法传递给轮播的第一个视图自动派生的您还可以使用carouselItemWidth:委托方法覆盖此值,这将更改为轮播项目分配的空间(但不会调整项目视图的大小或缩放)。

    @property (nonatomic, assign) BOOL centerItemWhenSelected;

    当设置为 YES 时,点击轮播中除与 currentItemIndex 匹配的项目之外的任何项目都会使其平滑地动画到中心。点击当前选定的项目将不起作用。默认为是。

    @property (nonatomic, assign) CGFloat scrollSpeed;

    这是用户用手指轻弹轮播时的滚动速度倍增器。默认为 1.0。

    @property (nonatomic, readonly) CGFloat toggle;

    此属性用于iCarouselTypeCoverFlow2轮播变换。它是公开的,以便您可以使用carousel:itemTransformForOffset:baseTransform:委托方法实现自己的 CoverFlow2 样式变体

    @property (nonatomic, assign) BOOL stopAtItemBoundary;

    默认情况下,轮播将在轻弹时停在确切的项目边界处。如果将此属性设置为 NO,它将自然停止,然后 - 如果 scrollToItemBoundary 设置为 YES - 向后或向前滚动到最近的边界。

    @property (nonatomic, assign) BOOL scrollToItemBoundary;

    默认情况下,当轮播停止移动时,它会自动滚动到最近的项目边界。如果将此属性设置为 NO,则轮播在停止后将不会滚动并停留在它所在的位置,即使它在当前索引上没有完全对齐。例外情况是,如果 wrapping 被禁用并bounces设置为 YES,那么无论此设置如何,如果轮播结束后停止,轮播将自动滚动回第一个或最后一个项目索引。

    @property (nonatomic, assign, getter = isVertical) BOOL vertical;

    此属性切换轮播是在屏幕上水平显示还是垂直显示。所有内置的轮播类型都适用于两个方向。切换到垂直会更改轮播的布局以及屏幕上滑动检测的方向。请注意,自定义轮播变换不受此属性影响,但滑动手势方向仍会受到影响。

    @property (nonatomic, readonly, getter = isDragging) BOOL dragging;

    如果用户已开始滚动轮播但尚未释放它,则返回 YES。

    @property (nonatomic, readonly, getter = isDecelerating) BOOL decelerating;

    如果用户不再拖动轮播,但它仍在移动,则返回 YES。

    @property (nonatomic, readonly, getter = isScrolling) BOOL scrolling;

    如果当前正在以编程方式滚动轮播,则返回 YES。

    @property (nonatomic, assign) BOOL ignorePerpendicularSwipes;

    如果是,则轮播将忽略与轮播方向垂直的滑动手势。所以对于水平轮播,垂直滑动不会被拦截。这意味着您可以在旋转木马项目视图中拥有一个垂直滚动的 scrollView,它仍然可以正常工作。默认为是。

    @property (nonatomic, assign) BOOL clipsToBounds;

    这实际上不是 iCarousel 的属性,而是继承自 UIView。它包含在此处是因为它是一个经常被遗漏的功能。将此设置为 YES 以防止轮播项目视图溢出其边界。您可以通过勾选“剪辑子视图”选项在界面生成器中设置此属性。默认为否。

    @property (nonatomic, assign) CGFloat autoscroll;

    此属性可用于设置轮播以恒定速度滚动。值为 1.0 将以每秒一项的速度向前滚动轮播。自动滚动值可以为正也可以为负,默认为 0.0(固定)。如果用户与轮播交互,自动滚动将停止,并在他们停止时恢复。

    方法

    iCarousel 类具有以下方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (void)scrollToItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将使轮播在指定的项目上居中,无论是立即还是平滑的动画。对于包裹式轮播,轮播将自动确定要滚动的最短(直接或环绕)距离。如果您需要控制滚动方向,或者想要滚动一圈以上,请改用 scrollByNumberOfItems 方法。

    - (void)scrollToItemAtIndex:(NSInteger)index duration:(NSTimeInterval)scrollDuration;

    此方法允许您控制轮播滚动到指定索引所需的时间。

    - (void)scrollByNumberOfItems:(NSInteger)itemCount duration:(NSTimeInterval)duration;

    此方法允许您将轮播滚动固定距离,以轮播项目宽度为单位。可以为 itemCount 指定正值或负值,具体取决于您希望滚动的方向。iCarousel 优雅地处理边界问题,因此如果您指定的距离大于轮播中项目的数量,滚动将在到达轮播结束时被限制(如果环绕被禁用)或无缝环绕。

    - (void)scrollToOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollToItemAtIndex:,但允许您滚动到小数偏移量。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则调用此方法后,轮播将自动滚动到最近的项目索引。反正。

    - (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollByNumberOfItems:,但允许您滚动项目的小数部分。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则无论如何调用此方法后,轮播都会自动滚动到最近的项目索引。

    - (void)reloadData;

    这将从数据源重新加载所有轮播视图并刷新轮播显示。

    - (UIView *)itemViewAtIndex:(NSInteger)index;

    返回具有指定索引的可见项视图。请注意,索引与轮播中的位置有关,而不是在visibleItemViews数组中的位置,这可能会有所不同。传递负索引或大于或等于的索引numberOfItems以检索占位符视图。该方法仅适用于可见的项目视图,如果指定索引处的视图尚未加载,或者索引超出范围,则返回 nil。

    - (NSInteger)indexOfItemView:(UIView *)view;

    轮播中给定项目视图的索引。适用于项目视图和占位符视图,但是占位符视图索引与数据源使用的索引不匹配,并且可能为负数(indexesForVisibleItems有关更多详细信息,请参阅上面的属性)。此方法仅适用于可见的项目视图,并且将为当前未加载的视图返回 NSNotFound。要获取所有当前加载的视图的列表,请使用该visibleItemViews属性。

    - (NSInteger)indexOfItemViewOrSubview:(UIView *)view

    此方法为您提供传递的视图或包含作为参数传递的视图的视图的项目索引。它的工作方式是从传递的视图开始沿着视图层次结构向上移动,直到找到一个项目视图并在轮播中返回其索引。如果未找到当前加载的项目视图,则返回 NSNotFound。此方法对于处理嵌入在项目视图中的控件上的事件非常有用。这允许您将所有项目控件绑定到视图控制器上的单个操作方法,然后确定触发操作的控件与哪个项目相关。您可以在Controls Demo示例项目中看到此技术的示例。

    - (CGFloat)offsetForItemAtIndex:(NSInteger)index;

    itemWidth以中心位置的倍数返回指定项索引的偏移量这与用于计算视图变换和 alpha 的值相同,可用于根据它们在轮播中的位置自定义项目视图。每当carouselDidScroll:调用委托方法时,每个视图的这个值都会发生变化

    - (UIView *)itemViewAtPoint:(CGPoint)point;

    返回轮播边界内指定点的最前面的项目视图。用于实现您自己的点击检测。

    - (void)removeItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将从轮播中删除一个项目。其余项目将滑过以填补空白。请注意,调用此方法时数据源不会自动更新,因此后续调用 reloadData 将恢复已删除的项目。

    - (void)insertItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这会将一个项目插入到轮播中。新的item会从dataSource中请求,所以在调用这个方法之前要确保新的item已经添加到数据源data中,否则会在carousel中得到重复的item,或者其他怪事。

    - (void)reloadItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    此方法将重新加载指定的项目视图。将从数据源请求新项目。如果动画参数为 YES,它将从旧项目视图交叉淡入淡出到新项目视图,否则将立即交换。

    协议

    iCarousel 通过提供两个协议接口 iCarouselDataSource 和 iCarouselDelegate 来遵循 Apple 的数据驱动视图约定。iCarouselDataSource 协议具有以下必需的方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel;

    返回轮播中的项目(视图)数。

    - (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示在轮播中指定索引处的视图。reusingView参数的工作方式类似于 UIPickerView,其中先前显示在轮播中的视图被传递回要回收的方法。如果这个参数不是 nil,你可以设置它的属性并返回它,而不是创建一个新的视图实例,这会稍微提高性能。与 UITableView 不同,没有用于区分不同轮播视图类型的重用标识符,因此如果您的轮播包含多个不同的视图类型,那么您应该忽略此参数并在每次调用该方法时返回一个新视图。您应该确保每次carousel:viewForItemAtIndex:reusingView: 方法被调用时,它要么返回 reusingView 要么返回一个全新的视图实例,而不是维护自己的可回收视图池,因为为不同的轮播项目索引返回同一视图的多个副本可能会导致轮播显示问题。

    iCarouselDataSource 协议有以下可选方法:

    - (NSUInteger)numberOfPlaceholdersInCarousel:(iCarousel *)carousel;

    返回要在轮播中显示的占位符视图的数量。当轮播中的项目数量太少而无法填充轮播宽度,并且您希望在空白空间中显示某些内容时,将使用占位符视图。它们与轮播一起移动并且行为与任何其他轮播项目一样,但它们不计入 numberOfItems 值,并且不能设置为当前选定的项目。启用换行时,占位符会隐藏。占位符出现在轮播项目的两侧。对于 n 个占位符视图,前 n/2 个项目将出现在项目视图的左侧,接下来的 n/2 个项目将出现在右侧。您可以有奇数个占位符,在这种情况下,轮播将是不对称的。

    - (UIView *)carousel:(iCarousel *)carousel placeholderViewAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示为占位符视图的视图。工作方式与carousel:viewForItemAtIndex:reusingView:占位符 reusingViews 与用于常规轮播的 reusingViews 存储在单独的池中,因此如果您的占位符视图与项目视图不同,这不是问题。

    iCarouselDelegate 协议具有以下可选方法:

    - (void)carouselWillBeginScrollingAnimation:(iCarousel *)carousel;

    每当轮播开始动画滚动时,都会调用此方法。这可以在用户完成滚动轮播后以编程方式或自动触发,因为轮播会重新对齐自身。

    - (void)carouselDidEndScrollingAnimation:(iCarousel *)carousel;

    当轮播结束动画滚动时调用此方法。

    - (void)carouselDidScroll:(iCarousel *)carousel;

    每当滚动轮播时都会调用此方法。无论轮播是通过编程还是通过用户交互滚动,它都会被调用。

    - (void)carouselCurrentItemIndexDidChange:(iCarousel *)carousel;

    每当轮播滚动到足以改变 currentItemIndex 属性时,就会调用此方法。无论项目索引是以编程方式更新还是通过用户交互更新,都会调用它。

    - (void)carouselWillBeginDragging:(iCarousel *)carousel;

    当用户开始拖动轮播时调用此方法。如果用户点击/点击轮播,或者轮播以编程方式滚动,它不会触发。

    - (void)carouselDidEndDragging:(iCarousel *)carousel willDecelerate:(BOOL)decelerate;

    当用户停止拖动轮播时调用此方法。willDecelerate 参数指示转盘是否行得足够快以至于它在停止之前需要减速(即当前索引不一定是它将停止的索引),或者它是否会在它所在的位置停止。请注意,即使 willDecelerate 为 NO,轮播仍会自动滚动,直到它与当前索引完全对齐。如果您需要知道它何时完全停止移动,请使用 carouselDidEndScrollingAnimation 委托方法。

    - (void)carouselWillBeginDecelerating:(iCarousel *)carousel;

    当轮播开始减速时调用此方法。它通常会在 carouselDidEndDragging:willDecelerate: 方法之后立即调用,假设 willDecelerate 为 YES。

    - (void)carouselDidEndDecelerating:(iCarousel *)carousel;

    当轮播完成减速时调用此方法,您可以假设此时的 currentItemIndex 是最终停止值。与以前的版本不同,在大多数情况下,轮播现在将准确地停在最终索引位置。唯一的例外是启用了弹跳的非包裹式转盘,如果最终停止位置超出转盘的末端,则转盘将自动滚动,直到它与结束索引完全对齐。为了向后兼容,轮播将始终scrollToItemAtIndex:animated:在完成减速后调用如果您需要确定轮播何时完全停止移动,请使用carouselDidEndScrollingAnimation委托方法。

    - (CGFloat)carouselItemWidth:(iCarousel *)carousel;

    返回轮播中每个项目的宽度 - 即每个项目视图的间距。如果未实现该方法,则默认为carousel:viewForItemAtIndex:reusingView:dataSource 方法返回的第一个项目视图的宽度如果从返回的视图carousel:viewForItemAtIndex:reusingView:不正确(例如,如果视图大小不同,或者在其背景图像中包含影响其大小的投影或外部发光),则此方法应仅用于裁剪或填充项目视图- 如果您只是想要将视图隔开一点,那么最好使用该iCarouselOptionSpacing值。

    - (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform;

    此方法可用于为每个轮播视图提供自定义转换。offset 参数是视图与旋转木马中间的距离。当前居中的项目视图的偏移量为 0.0,右侧的偏移值为 1.0,左侧的偏移值为 -1.0,依此类推。要实现线性轮播样式,您只需将偏移值乘以项目宽度并将其用作变换的 x 值。仅当轮播类型为 iCarouselTypeCustom 时才会调用此方法。

    - (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value;

    该方法用于自定义标准轮播类型的参数。通过实施此方法,您可以调整选项,例如圆形转盘中显示的项目数量,或coverflow 转盘中的倾斜量,以及转盘是否应环绕以及是否应在末端淡出等. 对于任何您不想调整的选项,只需返回默认值即可。这些选项的含义在下面的iCarouselOption 值下列出检查选项演示以获取使用此方法的高级示例。

    - (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。如果用户点击当前选定视图中的控件(即作为 UIControl 子类的任何视图),则不会触发此方法。

    - (BOOL)carousel:(iCarousel *)carousel shouldSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。方法的目的是让您有机会忽略轮播上的点击。如果你从方法中返回 YES,或者没有实现它,tap 将正常处理并carousel:didSelectItemAtIndex:调用方法。如果您返回 NO,轮播将忽略点击并继续向上传播视图层次结构。这是防止轮播拦截打算由另一个视图处理的点击事件的好方法。


    检测项目视图上的点击

    在 iOS 上的 iCarousel 中检测点击视图有两种基本方法。第一种方法是简单地使用carousel:didSelectItemAtIndex:委托方法,每次点击项目时都会触发方法。如果您只对点击当前居中的项目感兴趣,您可以将该currentItemIndex属性与此方法的 index 参数进行比较

    或者,如果您想要更多控制,您可以提供 UIButton 或 UIControl 作为项目视图并自己处理触摸交互。有关如何完成此操作的示例,请参阅按钮演示示例项目(不适用于 Mac OS;见下文)。

    您还可以在您的项目视图中嵌套 UIControls,这些将按预期接收触摸(请参阅Controls Demo示例项目以获取示例)。

    如果您希望检测其他类型的交互,例如滑动、双击或长按,最简单的方法是将 UIGestureRecognizer 附加到您的项目视图或其子视图,然后再将其传递给轮播。

    请注意,除了当前选定的项目视图之外,任何项目视图上的点击和手势都将被忽略,除非您将该centerItemWhenSelected属性设置为 NO。

    在 Mac OS 上,目前没有简单的方法可以在 iCarousel 项目视图中嵌入控件。您不能只在项目视图中或在项目视图中提供 NSButton,因为应用于项目视图的转换意味着命中检测无法正常工作。我正在研究可能的解决方案(如果您知道解决此问题的好方法,请与我们联系,或在 github 上 fork 项目)。

    demo及常见问题:https://github.com/nicklockwood/iCarousel

    源码下载:iCarousel-master.zip


    收起阅读 »

    Apple 的xcodebuild的扩展!

    xctool是 Apple 的xcodebuild的扩展,可以更轻松地测试 iOS 和 Mac 产品。它对持续集成特别有帮助。特征xctool是替代品,xcodebuild test它增加了一些额外的功能:更快的并行测试运行。xctool可以选择并行运行所有测...
    继续阅读 »

    xctool是 Apple 的xcodebuild的扩展,可以更轻松地测试 iOS 和 Mac 产品。它对持续集成特别有帮助。

    特征

    xctool是替代品,xcodebuild test它增加了一些额外的功能:

    • 更快的并行测试运行。

      xctool可以选择并行运行所有测试包,从而显着加快测试运行速度。在 Facebook,通过并行运行,我们看到了 2 倍和 3 倍的加速。

      使用-parallelize带有run-teststest选项来启用。有关详细信息,请参阅并行化测试运行

    • 测试结果的结构化输出。

      xctool将所有测试结果捕获为结构化 JSON 对象。如果您正在构建一个持续集成系统,这意味着您不再需要正则表达式解析xcodebuild输出。

      尝试使用Reporters之一自定义输出或使用该-reporter json-stream选项获取完整的事件流

    • 人性化的 ANSI 颜色输出。

      xcodebuild非常冗长,为每个源文件打印完整的编译命令和输出。默认情况下,xctool仅在出现问题时才详细说明,从而更容易确定问题所在。

    • 用Objective-C编写。

      xctool是用 Objective-C 编写的。Mac OS X 和 iOS 开发人员可以轻松提交新功能并修复他们可能遇到的任何错误,而无需学习新语言。我们非常欢迎拉取请求!

    注意:不推荐使用 xctool 构建项目,并且不会更新以支持 Xcode 的未来版本。我们建议移动到 xcodebuild(使用xcpretty)来满足简单的需求,或者使用xcbuild来满足更多的需求。xctool 将继续支持测试(见上文)。

    要求

    • Xcode 7 或更高版本
    • 您需要安装 Xcode 的命令行工具。从 Xcode,通过Xcode → Preferences → Downloads安装

    安装

    brew install xctool

    xctool 的命令和选项主要是 xcodebuild 的超集。在大多数情况下,您只需将xcodebuildxctool交换,事情就会按预期运行,但输出更具吸引力。

    您始终可以通过以下方式获得帮助和完整的选项列表:

    path/to/xctool.sh -help


    在运行测试之前,您需要构建它们。您可以使用xcodebuild、 xcbuildBuck来做到这一点。

    例如:

    xcodebuild \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    build-for-testing


    如果您使用 Xcode 7 进行构建,您可以继续使用 xctool 使用 build-tests 构建测试,或者仅使用测试操作来运行测试。

    例如:

    path/to/xctool.sh \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    build-tests

    并行化测试运行

    xctool可以选择并行运行单元测试,从而更好地利用其他空闲的 CPU 内核。在 Facebook,通过并行化我们的测试运行,我们已经看到了 2 倍和 3 倍的收益。

    要允许测试包同时运行,请使用以下-parallelize 选项:

    path/to/xctool.sh \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    run-tests -parallelize


    常见问题及demo下载:https://github.com/facebookarchive/xctool

    源码下载:xctool-master.zip



    收起阅读 »

    DKNightVersion 的实现 --- 如何为 iOS 应用添加夜间模式

    从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。在正式介绍目前版本的实现之前,我会先简单介绍...
    继续阅读 »

    从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。

    其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。

    在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。

    我们会以对 backgroundColor 为例说明整个框架的工作原理。

    方法调剂的版本

    如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。

    其核心思路就是使用方法调剂修改 backgroundColor 的存取方法。

    使用 nightBackgroundColor

    在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在分类中添加 nightBackgroundColor 属性,并且使用方法调剂改变 backgroundColor 的 setter 方法。

    在当前主题为 DKThemeVersionNormal 时,将颜色保存至 normalBackgroundColor 中,然后再调用原 backgroundColor 的 setter 方法,更新视图的颜色。

    DKNightVersionManager

    这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面。

    整个 DKNightVersion 都是由一个 DKNightVersionManager 的单例来管理的,而它的主要工作就是负责改变应用的主题、并在主题改变时通知其它视图更新颜色:

    - (void)changeColor:(id <DKNightVersionChangeColorProtocol>)object {
    if ([object respondsToSelector:@selector(changeColor)]) {
    [object changeColor];
    }
    if ([object respondsToSelector:@selector(subviews)]) {
    if (![object subviews]) {
    // Basic case, do nothing.
    return;
    } else {
    for (id subview in [object subviews]) {
    // recursive darken all the subviews of current view.
    [self changeColor:subview];
    if ([subview respondsToSelector:@selector(changeColor)]) {
    [subview changeColor];
    }
    }
    }
    }
    }

    如果主题更新,那么就会递归地调用 changeColor 方法,刷新全部的视图颜色,而这个方法的实现比较简单:

    - (void)changeColor {
    if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {
    self.backgroundColor = self.normalBackgroundColor;
    } else {
    self.backgroundColor = self.nightBackgroundColor;
    }
    }

    上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:

    1、在高速滚动的 scrollView 上面来回切换夜间模式,会出现颜色错乱的问题
    2、由于对 backgroundColor 属性进行不合适的方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同
    3、无法适配第三方 UI 控件

    使用色表的版本

    为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 nightBackgroundColor 的使用,并且重新设计底层的实现,转而使用更为稳定、安全的方法实现夜间模式,先看一下效果图:

    <em>新的实现不仅能够支持夜间模式,而且能够支持多主题。</em>

    DKColorPicker

    与上一个版本实现上的不同,在 2.0 中删除了全部的 nightBackgroundColor,使用一个名为 dk_backgroundColorPicker 的属性取代它。

    @property (nonatomic, copy) DKColorPicker dk_backgroundColorPicker;

    这个属性其实就是一个 block,它接收参数 DKThemeVersion *themeVersion,但是会返回一个 UIColor *:

    在第一次传入 picker 或者每次主题改变时,都会将当前主题 DKThemeVersion 传入 picker 并执行,然后,将得到的 UIColor 赋值给对应的属性 backgroundColor 更新视图颜色。

    typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);

    比如下面使用 DKColorPickerWithRGB 创建一个临时的 DKColorPicker:

    1、在 DKThemeVersionNormal 时返回 0xffffff
    2、在 DKThemeVersionNight 时返回 0x343434
    3、在自定义的主题下返回 0xfafafa (这里的顺序与色表中主题的顺序有关)

    cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa);

    同时,每一个对象还持有一个 pickers 数组,来存储自己的全部 DKColorPicker:

    @interface NSObject ()

    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;

    @end

    在第一次使用这个属性时,当前对象注册为 DKNightVersionThemeChangingNotificaiton 通知的观察者。

    在每次收到通知时,都会调用 night_update 方法,将当前主题传入 DKColorPicker,并再次执行,并将结果传入对应的属性 [self performSelector:sel withObject:result]。

    - (void)night_updateColor {
    [self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull selector, DKColorPicker _Nonnull picker, BOOL * _Nonnull stop) {
    SEL sel = NSSelectorFromString(selector);
    id result = picker(self.dk_manager.themeVersion);
    [UIView animateWithDuration:DKNightVersionAnimationDuration
    animations:^{
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector:sel withObject:result];
    #pragma clang diagnostic pop
    }];
    }];
    }

    也就是说,在每次改变主题的时候,都会发出通知。

    DKColorTable

    虽然我们在上面临时创建了一些 DKColorPicker。不过在 DKNightVersion 中,我更推荐使用色表,来减少相同的 DKColorPicker 的创建,并且能够更好地管理整个应用中的颜色:

    NORMAL   NIGHT    RED
    #ffffff #343434 #fafafa BG
    #aaaaaa #313131 #aaaaaa SEP
    #0000ff #ffffff #fa0000 TINT
    #000000 #ffffff #000000 TEXT
    #ffffff #444444 #ffffff BAR

    上面就是默认色表文件 DKColorTable.txt 中的内容,其中,第一行表示主题,NORMAL 主题必须存在,而且必须为第一列,而最右面的 BG、SEP 就是对应 DKColorPicker 的 key。

    self.tableView.dk_backgroundColorPicker =  DKColorPickerWithKey(BG);

    在使用时,上面的代码就相当于返回了一个在 NORMAL 时返回 #ffffff、NIGHT 时返回 #343434 以及 RED 时返回 #fafafa 的 DKColorPicker。

    pickerify

    虽然说,我们使用色表以及 DKColorPicker 解决了,但是,到目前为止我们还没有解决第三方框架的问题。

    比如我们使用了某个第三方框架,或者自己添加了某个 color 属性,比如说:

    @interface DKView ()

    @property (nonatomic, strong) UIColor *weirdColor;

    @end

    weirdColor 并没有对应的 DKColorPicker,但是,我们可以通过 pickerify 在想要使用 dk_weirdColorPicker 的地方生成这个对应的 picker:

    @pickerify(DKView, weirdColor);

    然后,我们就可以使用 dk_weirdColorPicker 属性了:

    view.dk_weirdColorPicker = DKColorPickerWithKey(BG);

    pickerify 其实是一个宏:

    #define pickerify(KLASS, PROPERTY) interface \
    KLASS (Night) \
    @property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \
    @end \
    @interface \
    KLASS () \
    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers; \
    @end \
    @implementation \
    KLASS (Night) \
    - (DKColorPicker)dk_ ## PROPERTY ## Picker { \
    return objc_getAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker)); \
    } \
    - (void)dk_set ## PROPERTY ## Picker:(DKColorPicker)picker { \
    objc_setAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); \
    [self setValue:picker(self.dk_manager.themeVersion) forKeyPath:@keypath(self, PROPERTY)];\
    [self.pickers setValue:[picker copy] forKey:_DKSetterWithPROPERTYerty(@#PROPERTY)]; \
    } \
    @end

    这个宏根据传入的类和属性名,为我们生成了对应 picker 的存取方法,它也可以说是一种元编程的手段。

    这里生成的 setter 方法不是标准意义上的驼峰命名法 dk_setweirdColorPicker:,因为我不知道怎么才能让大写首字母之后的属性添加到这里(如果各位读者有解决方案,欢迎提 PR 或者 issue)。

    嵌入式 Ruby
    由于框架中很多的代码,都是重复的,所以在这里使用了嵌入式 Ruby 模板来生成对应的文件 color.m.irb:

    //
    // <%= klass.name %>+Night.m
    // <%= klass.name %>+Night
    //
    // Copyright (c) 2015 Draveness. All rights reserved.
    //
    // These files are generated by ruby script, if you want to modify code
    // in this file, you are supposed to update the ruby code, run it and
    // test it. And finally open a pull request.

    #import "<%= klass.name %>+Night.h"
    #import "DKNightVersionManager.h"
    #import <objc/runtime.h>

    @interface <%= klass.name %> ()

    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;

    @end

    @implementation <%= klass.name %> (Night)

    <% klass.properties.each do |property| %><%= """
    - (DKColorPicker)dk_#{property.name}Picker {
    return objc_getAssociatedObject(self, @selector(dk_#{property.name}Picker));
    }

    - (void)dk_set#{property.cap_name}Picker:(DKColorPicker)picker {
    objc_setAssociatedObject(self, @selector(dk_#{property.name}Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
    self.#{property.name} = picker(self.dk_manager.themeVersion);
    [self.pickers setValue:[picker copy] forKey:@\"#{property.setter}\"];
    }
    """ %><% end %>

    @end

    这部分的实现并不在这篇文章的讨论范围之内,如果,对这部分看兴趣,可以看一下仓库中的 generator 文件夹,其中包含了代码生成器的全部代码。

    小结

    如果你对 DKNightVersion 的使用有兴趣,可以查看仓库的 README 文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题,AFNetworking、 BlocksKit 也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。

    Git仓库地址

    转自:https://draveness.me/night/

    收起阅读 »

    UIViewController解耦---浅析Three20架构

    前言Three20是一款由Facebook开源的框架,由大神Joe Hewitt创建,曾经风靡一时,被无数开发者观阅。Three20主要提供了UI模块、Network模块以及相关的一些工具。Three20自开源之初就褒贬不一,有人称赞它强大的UI工具,也有人在...
    继续阅读 »

    前言
    Three20是一款由Facebook开源的框架,由大神Joe Hewitt创建,曾经风靡一时,被无数开发者观阅。Three20主要提供了UI模块、Network模块以及相关的一些工具。Three20自开源之初就褒贬不一,有人称赞它强大的UI工具,也有人在诟病Three20各个模块之间的耦合度太高,而且更多人在抱怨Three20极少的开发文档,我想这些大概也是Three20在苹果发布iOS6之后就停止了更新维护的原因吧。大神Joe Hewitt创建的在Github上的源码早已删除,目前只有少数人在GitHub上为自己的项目维护。而我也是有幸在某个项目中见识到了曾经耳闻,却未目睹的Three20框架,因此才有了这篇文章。

    架构
    最近大家都在讨论MVC、MVVM以及MVP三种在移动端开发中常用到的架构模式,究竟是哪种架构最强大,最适合移动开发者使用。这里笔者也阐述一下个人意见,有句方言叫“树挪死,人挪活”,个人认为,架构是死的,开发者是活的,我们不需要局限于哪一种架构的模式之下,看到大家都在用MVVM,于是花大成本将MVC架构模式的老项目重构成了MVVM架构,这种重构个人看来其实并没有意义。更多的架构话题就不想在这里讨论了,笔者推荐几篇大神们关于架构的见解。

    1、被误解的 MVC 和被神化的 MVVM
    这是一篇被早已被翻烂了的文章,起码我个人反复阅读了数次,由家喻户晓的唐巧大神编写。
    2、iOS 架构模式--解密 MVC,MVP,MVVM以及VIPER架构
    最近在Cocoa China上发表的一篇译文,笔者之前看过俩次原文,讲的比较形象。
    3、MVC,MVP 和 MVVM 的图示
    大神阮一峰的博文,以图形展示的方式使得各层结构更加清晰明了。
    4、猿题库 iOS 客户端架构设计
    猿题库 iOS客户端开发者蓝晨钰的博文,以实际项目猿题库详解了架构设计

    UIViewController瘦身

    架构模式并不是限制思维,相反应该是发散思维,我们并不应该为了架构而架构,架构应该是服务于我们的代码逻辑,打造更具有扩展性和健壮的代码结构。就比如,大多数开发者都会遇到一个同样的问题,随着项目一天天的壮大,功能越来越多,需求越来越多,而我们的UIViewController也变得越来越臃肿。在上面推荐的博文中,笔者们都或多或少的阐述了如何打造更轻量级的UIViewController,大都列举了一些共性策略:

    1、将一个界面中的数据获取抽象成一个类,这里面细分一下,包括了网络请求和数据库缓存,我们可以针对这俩点再次封装成俩个类。
    2、将一个界面中的数据处理逻辑抽象成一个类,这里面包含了各种数据转换和算法逻辑,比如数据检索,数据遍历等。
    3、将一个界面中数据传递到UIView视图的过程抽象成一个模型类,这里面就包含了对应到UIView视图的每一个数据的传递,比如icon图标,title标题,comment评论内容等。
    4、将一个界面中所有展示的UIView视图的添加和渲染抽象成一个类,这里包含了添加控件,自定义动画等。这个对视图的封装仍然可以细分,每一个自定义控件都可以单独封装,因为这样可以完美的在其他的UIViewController达到复用的目的。

    而完成了上述抽象之后,就会发现我们需要在UIViewController中完成的工作仅仅是处理视图交互逻辑和数据传递逻辑,这样我们的UIViewController就比较容易维护了。

    Three20架构
    每一种框架的兴起和衰落都有其相应的时势和必然性。虽然Three20饱受诟病,早已跌落神坛,但是它的存在是有一定道理的。虽然它在模块之间的耦合度较高,但是个人认为它对UIViewController的抽象和封装也是一个非常好的借鉴。在这里以Three20中对TTTableViewController的解耦为例,先上图看一下TTTableViewController包含的模块:


    这里根据上面的结构图具体地解释一下解耦的设计方式。TTTableViewController的设计遵从了经典的MVC模式,TTModel负责数据的获取和处理逻辑,TTTableView负责视图展示,TTTableViewController负责TTModel与TTTableView之间的通信逻辑和界面的控件添加渲染。而TTTableViewController在顺应了MVC模式的前提下,也做了一些扩展,它将TTTableViewDatasource接收数据传递的逻辑抽象出来封装成了TTTableItem。而TTTableItem就是关联TTModel传递数据的过程,因而我们也可以把这一层称作是MVVM架构模式中的ViewModel

    根据上面的图示,我们可以看到获取数据的逻辑都在TTModel中,而且界面控件添加和动画渲染这些逻辑仍然都在TTTableViewController中,因此我根据大神们的一些建议,对项目中的Three20进行了一下强化,先上图看一下增加的结构:


    可以清晰地看到,我将TTModel中处理缓存数据的逻辑抽象出来,单独放在了TTCacheModel中,此外还将TTTableViewController中添加控件和渲染动画的逻辑抽象出来,放到了TTViewRender中,这样TTTableViewController就只关心界面交互以及TTModel和TTTableItem之间的数据传递逻辑。

    链接:https://www.jianshu.com/p/a748f6fd99dd

    收起阅读 »

    iOS RESideMenu 侧滑 第三方类库

    下载地址:https://github.com/romaonthego/RESideMenu效果如下:官方案例自己的实现效果具体代码下:AppDelegate.m文件中- (BOOL)application:(UIApplication *)applicati...
    继续阅读 »

    下载地址:https://github.com/romaonthego/RESideMenu
    效果如下:官方案例


    自己的实现效果


    具体代码下:

    AppDelegate.m文件中

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法

    DEMOLeftMenuViewController *leftMenuViewController = [[DEMOLeftMenuViewController alloc] init];

    RESideMenu *sideMenuViewController = [[RESideMenu alloc] initWithContentViewController:[[MainTabBarController alloc]init] leftMenuViewController:leftMenuViewController rightMenuViewController:[UINavigationController new]];
    sideMenuViewController.backgroundImage = [UIImage imageNamed:@"005.jpg"];
    sideMenuViewController.menuPreferredStatusBarStyle = 1; // UIStatusBarStyleLightContent
    sideMenuViewController.delegate = self;
    // sideMenuViewController.parallaxContentMaximumRelativeValue=100;
    // sideMenuViewController.bouncesHorizontally=YES;
    sideMenuViewController.contentViewShadowColor = [UIColor blackColor];
    sideMenuViewController.contentViewShadowOffset = CGSizeMake(0, 0);
    sideMenuViewController.contentViewShadowOpacity = 0.6;
    sideMenuViewController.contentViewShadowRadius = 12;
    // sideMenuViewController.contentViewShadowEnabled = YES;
    // sideMenuViewController.panFromEdge=NO;
    self.window.rootViewController = sideMenuViewController;

    左侧的控制器DEMOLeftMenuViewController.h和DEMOLeftMenuViewController.m

    #import <UIKit/UIKit.h>
    #import "RESideMenu.h"

    @interface DEMOLeftMenuViewController : UIViewController<UITableViewDataSource, UITableViewDelegate, RESideMenuDelegate>


    @end
    #import "DEMOLeftMenuViewController.h"
    #import "HomeViewController.h"
    #import "UIViewController+RESideMenu.h"
    #import "LoginViewController.h"
    #import "resigeViewController.h"

    @interface DEMOLeftMenuViewController ()
    @property (strong, readwrite, nonatomic) UITableView *tableView;

    @end

    @implementation DEMOLeftMenuViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    self.navigationController.title=@"登陆";
    self.tableView = ({
    UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, (self.view.frame.size.height - 54 * 5) / 2.0f, self.view.frame.size.width, 54 * 5) style:UITableViewStylePlain];
    tableView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth;
    tableView.delegate = self;
    tableView.dataSource = self;
    tableView.opaque = NO;
    tableView.backgroundColor = [UIColor clearColor];
    tableView.backgroundView = nil;
    tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    tableView.bounces = NO;
    tableView.scrollsToTop = NO;
    tableView;
    });
    [self.view addSubview:self.tableView];
    }

    #pragma mark -
    #pragma mark UITableView Delegate

    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    switch (indexPath.row) {
    case 0:
    [self presentViewController:[[UINavigationController alloc] initWithRootViewController:[[LoginViewController alloc] init]] animated:YES completion:nil];
    break;
    case 1:
    [self presentViewController:[[UINavigationController alloc] initWithRootViewController:[[resigeViewController alloc] init]] animated:YES completion:nil];
    break;
    default:
    break;
    }
    }

    #pragma mark -
    #pragma mark UITableView Datasource

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    return 54;
    }

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
    return 1;
    }

    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)sectionIndex
    {
    return 5;
    }

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    static NSString *cellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    cell.backgroundColor = [UIColor clearColor];
    cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:21];
    cell.textLabel.textColor = [UIColor whiteColor];
    cell.textLabel.highlightedTextColor = [UIColor lightGrayColor];
    cell.selectedBackgroundView = [[UIView alloc] init];
    }

    NSArray *titles = @[@"Home", @"Calendar", @"Profile", @"Settings", @"Log Out"];
    NSArray *images = @[@"IconHome", @"IconCalendar", @"IconProfile", @"IconSettings", @"IconEmpty"];
    cell.textLabel.text = titles[indexPath.row];
    cell.imageView.image = [UIImage imageNamed:images[indexPath.row]];

    return cell;
    }


    @end

    主页HomeViewController.h和HomeViewController.m实现侧滑的关键代码

    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"个人中心"
    style:UIBarButtonItemStylePlain
    target:self
    action:@selector(presentLeftMenuViewControl

    这个第三番可以实现很多效果

    总结

    优点:

    1.里面的文件较少,不需要使用cocoapods即可运行。

    2.里面自定义API也比较多,可以设置变小的抽屉效果或者不变小。

    3.里面有两个事例程序,一个是纯手码,一个是Storyboard得。可见作者也非常喜欢IB开发,此框架用IB开发应该可以完美兼容。

    4.可以使用手势拖来拖去。

    5.项目里各个文件不需要继承,导入头文件就行。

    缺点:

    1.左边显示的菜单可选项是固定的几个button,暂时想把左边换成tableView还不知道可不可行。

    2.不能实现状态栏右移。

    3.暂时没找到两边控制器的占比怎么自定义。

    转自:https://www.cnblogs.com/qianLL/p/5425738.html

    收起阅读 »

    PNChart:一个简单漂亮的iOS图表库

    PNChart是一个简单漂亮的动画图表库,Piner和CoinsMan的 iOS 客户端中使用了这个框架。你也可以查看 Swift 版本(开源链接:https://github.com/kevinzhow/PNChart-Swift)。要求PNChart 适用...
    继续阅读 »

    PNChart是一个简单漂亮的动画图表库,Piner和CoinsMan的 iOS 客户端中使用了这个框架。你也可以查看 Swift 版本(开源链接:https://github.com/kevinzhow/PNChart-Swift)。

    要求

    PNChart 适用于 iOS 7.0 或更高版本,与 ARC 项目兼容。如果需要支持 iOS 6 ,请使用 0.8.1 版本之前的 PNChart 。注意 0.8.2 版本仅支持 iOS 8.0+ ,0.8.3 及更新版本支持 iOS 7.0+ 。

    PNChart 依赖于下列框架,这些框架已经嵌入了 Xcode 开发工具:

    Foundation.framework

    UIKit.framework

    CoreGraphics.framework

    QuartzCore.framework

    你需要 LLVM 3.0 或更高版本来建立 PNChart 。

    安装

    通过CocoaPods安装(推荐):

    1、在你的 Podfile 文件中添加pod 'PNChart'。

    2、运行pod install进行安装。

    3、按需导入头文件#import "PNChart.h"。

    手动安装:

    拷贝PNChart文件夹到你的工程中。

    使用

    #import "PNChart.h"

    //For Line Chart

    PNLineChart*lineChart=[[PNLineChartalloc]initWithFrame:CGRectMake(0,135.0,SCREEN_WIDTH,200.0)];

    [lineChartsetXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5"]];

    // Line Chart No.1

    NSArray*data01Array=@[@60.1,@160.1,@126.4,@262.2,@186.2];

    PNLineChartData*data01=[PNLineChartDatanew];

    data01.color=PNFreshGreen;

    data01.itemCount=lineChart.xLabels.count;

    data01.getData=^(NSUIntegerindex){

    CGFloatyValue=[data01Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    // Line Chart No.2

    NSArray*data02Array=@[@20.1,@180.1,@26.4,@202.2,@126.2];

    PNLineChartData*data02=[PNLineChartDatanew];

    data02.color=PNTwitterColor;

    data02.itemCount=lineChart.xLabels.count;

    data02.getData=^(NSUIntegerindex){

    CGFloatyValue=[data02Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    lineChart.chartData=@[data01,data02];

    [lineChartstrokeChart];
    #import "PNChart.h"

    //For BarC hart

    PNBarChart*barChart=[[PNBarChartalloc]initWithFrame:CGRectMake(0,135.0,SCREEN_WIDTH,200.0)];

    [barChartsetXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5"]];

    [barChartsetYValues:@[@1,@10,@2,@6,@3]];

    [barChartstrokeChart];



    ``` Objective-C

    #import "PNChart.h"

    //For Circle Chart

    PNCircleChart*circleChart=[[PNCircleChartalloc]initWithFrame:CGRectMake(0,80.0,SCREEN_WIDTH,100.0)total:[NSNumbernumberWithInt:100]current:[NSNumbernumberWithInt:60]clockwise:NOshadow:NO];

    circleChart.backgroundColor=[UIColorclearColor];

    [circleChartsetStrokeColor:PNGreen];

    [circleChart strokeChart];



    ```Objective-C

    # import "PNChart.h"

    //For Pie Chart

    NSArray*items=@[[PNPieChartDataItemdataItemWithValue:10color:PNRed],

    [PNPieChartDataItemdataItemWithValue:20color:PNBluedescription:@"WWDC"],

    [PNPieChartDataItemdataItemWithValue:40color:PNGreendescription:@"GOOL I/O"],

    ];

    PNPieChart*pieChart=[[PNPieChartalloc]initWithFrame:CGRectMake(40.0,155.0,240.0,240.0)items:items];

    pieChart.descriptionTextColor=[UIColorwhiteColor];

    pieChart.descriptionTextFont=[UIFontfontWithName:@"Avenir-Medium"size:14.0];

    [pieChartstrokeChart];
    # import "PNChart.h"

    //For Scatter Chart

    PNScatterChart*scatterChart=[[PNScatterChartalloc]initWithFrame:CGRectMake(SCREEN_WIDTH/6.0-30,135,280,200)];

    [scatterChartsetAxisXWithMinimumValue:20andMaxValue:100toTicks:6];

    [scatterChartsetAxisYWithMinimumValue:30andMaxValue:50toTicks:5];

    NSArray*data01Array=[selfrandomSetOfObjects];

    PNScatterChartData*data01=[PNScatterChartDatanew];

    data01.strokeColor=PNGreen;

    data01.fillColor=PNFreshGreen;

    data01.size=2;

    data01.itemCount=[[data01ArrayobjectAtIndex:0]count];

    data01.inflexionPointStyle=PNScatterChartPointStyleCircle;

    __blockNSMutableArray*XAr1=[NSMutableArrayarrayWithArray:[data01ArrayobjectAtIndex:0]];

    __blockNSMutableArray*YAr1=[NSMutableArrayarrayWithArray:[data01ArrayobjectAtIndex:1]];

    data01.getData=^(NSUIntegerindex){

    CGFloatxValue=[[XAr1objectAtIndex:index]floatValue];

    CGFloatyValue=[[YAr1objectAtIndex:index]floatValue];

    return[PNScatterChartDataItemdataItemWithX:xValueAndWithY:yValue];

    };

    [scatterChartsetup];

    self.scatterChart.chartData=@[data01];

    /***

    this is for drawing line to compare

    CGPoint start = CGPointMake(20, 35);

    CGPoint end = CGPointMake(80, 45);

    [scatterChart drawLineFromPoint:start ToPoint:end WithLineWith:2 AndWithColor:PNBlack];

    ***/

    scatterChart.delegate=self;

    图例

    PNChart 允许在折线图和饼状图中添加图例,图例可以竖向堆叠布置或者横向并列布置。

    #import "PNChart.h"

    //For Line Chart

    //Add Line Titles for the Legend

    data01.dataTitle=@"Alpha";

    data02.dataTitle=@"Beta Beta Beta Beta";

    //Build the legend

    self.lineChart.legendStyle=PNLegendItemStyleSerial;

    self.lineChart.legendFontSize=12.0;

    UIView*legend=[self.lineChartgetLegendWithMaxWidth:320];

    //Move legend to the desired position and add to view

    [legendsetFrame:CGRectMake(100,400,legend.frame.size.width,legend.frame.size.height)];

    [self.viewaddSubview:legend];

    //For Pie Chart

    //Build the legend

    self.pieChart.legendStyle=PNLegendItemStyleStacked;

    self.pieChart.legendFontSize=12.0;

    UIView*legend=[self.pieChartgetLegendWithMaxWidth:200];

    //Move legend to the desired position and add to view

    [legendsetFrame:CGRectMake(130,350,legend.frame.size.width,legend.frame.size.height)];

    [self.viewaddSubview:legend];

    更新数据

    实时更新数据也非常简单。

    Objective-C

    if([self.titleisEqualToString:@"Line Chart"]){

    // Line Chart #1

    NSArray*data01Array=@[@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300)];

    PNLineChartData*data01=[PNLineChartDatanew];

    data01.color=PNFreshGreen;

    data01.itemCount=data01Array.count;

    data01.inflexionPointStyle=PNLineChartPointStyleTriangle;

    data01.getData=^(NSUIntegerindex){

    CGFloatyValue=[data01Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    // Line Chart #2

    NSArray*data02Array=@[@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300)];

    PNLineChartData*data02=[PNLineChartDatanew];

    data02.color=PNTwitterColor;

    data02.itemCount=data02Array.count;

    data02.inflexionPointStyle=PNLineChartPointStyleSquare;

    data02.getData=^(NSUIntegerindex){

    CGFloatyValue=[data02Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    [self.lineChartsetXLabels:@[@"DEC 1",@"DEC 2",@"DEC 3",@"DEC 4",@"DEC 5",@"DEC 6",@"DEC 7"]];

    [self.lineChartupdateChartData:@[data01,data02]];

    }

    elseif([self.titleisEqualToString:@"Bar Chart"])

    {

    [self.barChartsetXLabels:@[@"Jan 1",@"Jan 2",@"Jan 3",@"Jan 4",@"Jan 5",@"Jan 6",@"Jan 7"]];

    [self.barChartupdateChartData:@[@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30)]];

    }

    elseif([self.titleisEqualToString:@"Circle Chart"])

    {

    [self.circleChartupdateChartByCurrent:@(arc4random()0)];

    }

    代理回调

    Objective-C

    #import "PNChart.h"

    //For LineChart

    lineChart.delegate=self;

    动画

    默认绘制图表时使用动画,可以通过设置displayAnimation = NO来禁止动画。

    Objective-C


    #import "PNChart.h"

    //For LineChart

    lineChart.displayAnimation=NO;

    ```Objective-C



    //For DelegateMethod

    -(void)userClickedOnLineKeyPoint:(CGPoint)pointlineIndex:(NSInteger)lineIndexpointIndex:(NSInteger)pointIndex{

    NSLog(@"Click Key on line %f, %f line index is %d and point index is %d",point.x,point.y,(int)lineIndex,(int)pointIndex);

    }

    -(void)userClickedOnLinePoint:(CGPoint)pointlineIndex:(NSInteger)lineIndex{

    NSLog(@"Click on line %f, %f, line index is %d",point.x,point.y,(int)lineIndex);

    }

    开源协议

    PNChart 在MIT开源协议下可以使用,也就是说,只要在项目副本中包含了版权声明和许可声明,用户就可以使用 PNChart 做任何想做的事情,而 PNChart 也无需承担任何责任。可以通过查看 LICENSE 文件来获取更多相关信息。

    开源地址:https://github.com/kevinzhow/PNChart

    链接:https://www.jianshu.com/p/9c162d6f8f14

    收起阅读 »

    iOS 开发的应用内调试和探索工具-FLEX

    FLEX (Flipboard Explorer) 是一套用于 iOS 开发的应用内调试和探索工具。出现时,FLEX 会显示一个位于应用程序上方窗口中的工具栏。从此工具栏上,您可以查看和修改正在运行的应用程序中的几乎所有状态。给自己调试超能力检查和修改层次结构...
    继续阅读 »

    FLEX (Flipboard Explorer) 是一套用于 iOS 开发的应用内调试和探索工具。出现时,FLEX 会显示一个位于应用程序上方窗口中的工具栏。从此工具栏上,您可以查看和修改正在运行的应用程序中的几乎所有状态。

    给自己调试超能力

    • 检查和修改层次结构中的视图。
    • 查看任何对象的属性和变量。
    • 动态修改许多属性和变量。
    • 动态调用实例和类方法。
    • 通过时间、标头和完整响应观察详细的网络请求历史记录。
    • 添加您自己的模拟器键盘快捷键。
    • 查看系统日志消息(例如来自NSLog)。
    • 通过扫描堆访问任何活动对象。
    • 查看应用程序沙箱中的文件系统。
    • 浏览文件系统中的 SQLite/Realm 数据库。
    • 使用 control、shift 和 command 键在模拟器中触发 3D 触摸。
    • 探索您的应用程序和链接系统框架(公共和私有)中的所有类。
    • 快速访问有用的对象,例如[UIApplication sharedApplication]应用程序委托、关键窗口上的根视图控制器等。
    • 动态查看和修改NSUserDefaults值。

    与许多其他调试工具不同,FLEX 完全在您的应用程序内部运行,因此您无需连接到 LLDB/Xcode 或其他远程调试服务器。它在模拟器和物理设备上运行良好。用法

    在 iOS 模拟器中,您可以使用键盘快捷键来激活 FLEX。f将切换 FLEX 工具栏。敲击?快捷键的完整列表。您还可以以编程方式显示 FLEX:

    // Objective-C
    [[FLEXManager sharedManager] showExplorer];

    // Swift
    FLEXManager.shared.showExplorer()

    更完整的版本:

    #if DEBUG
    #import "FLEXManager.h"
    #endif

    ...

    - (void)handleSixFingerQuadrupleTap:(UITapGestureRecognizer *)tapRecognizer
    {
    #if DEBUG
    if (tapRecognizer.state == UIGestureRecognizerStateRecognized) {
    // This could also live in a handler for a keyboard shortcut, debug menu item, etc.
    [[FLEXManager sharedManager] showExplorer];
    }
    #endif
    }


    功能示例

    修改视图

    选择视图后,您可以点击工具栏下方的信息栏以显示有关该视图的更多详细信息。从那里,您可以修改属性和调用方法。



    网络历史

    启用后,网络调试允许您查看使用 NSURLConnection 或 NSURLSession 发出的所有请求。设置允许您调整缓存的响应主体类型和响应缓存的最大大小限制。您可以选择在应用启动时自动启用网络调试。此设置在启动时保持不变。



    堆上的所有对象

    FLEX 查询 malloc 以获取所有实时分配的内存块并搜索看起来像对象的内存块。你可以从这里看到一切。

    堆/活动对象资源管理器


    探索地址

    如果您获得任意地址,您可以尝试探索该地址处的对象,如果 FLEX 可以验证该地址指向有效对象,则会打开它。如果 FLEX 不确定,它会警告您并拒绝取消对指针的引用。但是,如果您更了解,则可以通过选择“不安全探索”来选择探索它

    地址浏览器


    模拟器键盘快捷键

    默认键盘快捷键允许您激活 FLEX 工具、使用箭头键滚动以及使用转义键关闭模式。您还可以通过添加自定义键盘快捷键-[FLEXManager registerSimulatorShortcutWithKey:modifiers:action:description]

    模拟器键盘快捷键


    安装

    CocoaPods

    pod 'FLEX', :configurations => ['Debug']

    Carthage

    1. 不要添加FLEX.framework到目标的嵌入式二进制文件中,否则它会包含在所有构建中(因此也包含在发布版本中)。

    2. 相反,添加$(PROJECT_DIR)/Carthage/Build/iOS到您的目标框架搜索路径(如果您已经在 Carthage 中包含了其他框架,则此设置可能已经存在)。这使得从源文件导入 FLEX 框架成为可能。如果为所有配置添加此设置也无害,但至少应为调试添加此设置。

    3. 向您的目标添加一个运行脚本阶段Link Binary with Libraries例如,在现有阶段之后插入它),并且它只会嵌入FLEX.framework到调试版本中:

    if [ "$CONFIGURATION" == "Debug" ]; then
    /usr/local/bin/carthage copy-frameworks
    fi
    最后,添加
    $(SRCROOT)/Carthage/Build/iOS/FLEX.framework为这个脚本阶段的输入文件。

    手动添加到项目的 FLEX 文件

    在 Xcode 中,导航到Build Settings > Build Options > Excluded Source File Names对于您的Release配置,将其设置为FLEX*这样以排除具有FLEX前缀的所有文件


    常见问题及demo下载:https://github.com/FLEXTool/FLEX







    收起阅读 »

    使用 iOS OpenGL ES 实现长腿功能

    本文介绍了如何使用 OpenGL ES 来实现长腿功能。学习这个例子可以加深我们对纹理渲染流程的理解。另外,还会着重介绍一下「渲染到纹理」这个新知识点。警告: 本文属于进阶教程,阅读前请确保已经熟悉 OpenGL ES 纹理渲染的相关概念,否则强行阅读可能导致...
    继续阅读 »


    本文介绍了如何使用 OpenGL ES 来实现长腿功能。学习这个例子可以加深我们对纹理渲染流程的理解。另外,还会着重介绍一下「渲染到纹理」这个新知识点。

    警告: 本文属于进阶教程,阅读前请确保已经熟悉 OpenGL ES 纹理渲染的相关概念,否则强行阅读可能导致走火入魔。传送门

    注: 下文中的 OpenGL ES 均指代 OpenGL ES 2.0。

    一、效果展示

    首先来看一下最终的效果,这个功能简单来说,就是实现了图片的局部拉伸,从逻辑上来说并不复杂。


    二、思路

    1、怎么实现拉伸

    我们来回忆一下,我们要渲染一张图片,需要将图片拆分成两个三角形,如下所示:


    如果我们想对图片进行拉伸,很简单,只需要修改一下 4 个顶点坐标的 Y 值即可。


    那么,如果我们只想对图片中间的部分进行拉伸,应该怎么做呢?

    其实答案也很容易想到,我们只需要修改一下图片的拆分方式。如下所示,我们把图片拆分成了 6 个三角形,也可以说是 3 个小矩形。这样,我们只需要对中间的小矩形做拉伸处理就可以了。


    2、怎么实现重复调整

    我们观察上面的动态效果图,可以看到第二次的压缩操作,是基于第一次的拉伸操作的结果来进行的。因此,在每一步我们都需要拿到上一步的结果,作为原始图,进行再次调整。

    这里的「原始图」就是一个纹理。换句话说,我们需要将每一次的调整结果,都重新生成一个纹理,供下次调整的时候使用。

    这一步是本文的重点,我们会通过「渲染到纹理」的方式来实现,具体的步骤我们在后面会详细介绍。

    三、为什么要使用 OpenGL ES

    可能有人会说:你这个功能平平无奇,就算不懂 OpenGL ES,我用其它方式也能实现呀。

    确实,在 iOS 中,我们绘图一般是使用 CoreGraphics。假设我们使用 CoreGraphics,也按照上面的实现思路,对原图进行拆分绘制,重复调整的时候进行重新拼接,目测也是能实现相同的功能。

    但是,由于 CoreGraphics 绘图依赖于 CPU,当我们在调节拉伸区域的时候,需要不断地进行重绘,此时 CPU 的占用必然会暴涨,从而引起卡顿。而使用 OpenGL ES 则不存在这样的问题。

    四、实现拉伸逻辑

    从上面我们知道,渲染图片我们需要 8 个顶点,而拉伸逻辑的关键就是顶点坐标的计算,在拿到计算结果后再重新渲染。

    计算顶点的关键步骤如下:

    /**
    根据当前控件的尺寸和纹理的尺寸,计算初始纹理坐标

    @param size 原始纹理尺寸
    @param startY 中间区域的开始纵坐标位置 0~1
    @param endY 中间区域的结束纵坐标位置 0~1
    @param newHeight 新的中间区域的高度
    */
    - (void)calculateOriginTextureCoordWithTextureSize:(CGSize)size
    startY:(CGFloat)startY
    endY:(CGFloat)endY
    newHeight:(CGFloat)newHeight {
    CGFloat ratio = (size.height / size.width) *
    (self.bounds.size.width / self.bounds.size.height);
    CGFloat textureWidth = self.currentTextureWidth;
    CGFloat textureHeight = textureWidth * ratio;

    // 拉伸量
    CGFloat delta = (newHeight - (endY - startY)) * textureHeight;

    // 判断是否超出最大值
    if (textureHeight + delta >= 1) {
    delta = 1 - textureHeight;
    newHeight = delta / textureHeight + (endY - startY);
    }

    // 纹理的顶点
    GLKVector3 pointLT = {-textureWidth, textureHeight + delta, 0}; // 左上角
    GLKVector3 pointRT = {textureWidth, textureHeight + delta, 0}; // 右上角
    GLKVector3 pointLB = {-textureWidth, -textureHeight - delta, 0}; // 左下角
    GLKVector3 pointRB = {textureWidth, -textureHeight - delta, 0}; // 右下角

    // 中间矩形区域的顶点
    CGFloat startYCoord = MIN(-2 * textureHeight * startY + textureHeight, textureHeight);
    CGFloat endYCoord = MAX(-2 * textureHeight * endY + textureHeight, -textureHeight);
    GLKVector3 centerPointLT = {-textureWidth, startYCoord + delta, 0}; // 左上角
    GLKVector3 centerPointRT = {textureWidth, startYCoord + delta, 0}; // 右上角
    GLKVector3 centerPointLB = {-textureWidth, endYCoord - delta, 0}; // 左下角
    GLKVector3 centerPointRB = {textureWidth, endYCoord - delta, 0}; // 右下角

    // 纹理的上面两个顶点
    self.vertices[0].positionCoord = pointLT;
    self.vertices[0].textureCoord = GLKVector2Make(0, 1);
    self.vertices[1].positionCoord = pointRT;
    self.vertices[1].textureCoord = GLKVector2Make(1, 1);
    // 中间区域的4个顶点
    self.vertices[2].positionCoord = centerPointLT;
    self.vertices[2].textureCoord = GLKVector2Make(0, 1 - startY);
    self.vertices[3].positionCoord = centerPointRT;
    self.vertices[3].textureCoord = GLKVector2Make(1, 1 - startY);
    self.vertices[4].positionCoord = centerPointLB;
    self.vertices[4].textureCoord = GLKVector2Make(0, 1 - endY);
    self.vertices[5].positionCoord = centerPointRB;
    self.vertices[5].textureCoord = GLKVector2Make(1, 1 - endY);
    // 纹理的下面两个顶点
    self.vertices[6].positionCoord = pointLB;
    self.vertices[6].textureCoord = GLKVector2Make(0, 0);
    self.vertices[7].positionCoord = pointRB;
    self.vertices[7].textureCoord = GLKVector2Make(1, 0);
    }

    五、渲染到纹理

    上面提到:我们需要将每一次的调整结果,都重新生成一个纹理,供下次调整的时候使用。

    出于对结果分辨率的考虑,我们不会直接读取当前屏幕渲染结果对应的帧缓存,而是采取「渲染到纹理」的方式,重新生成一个宽度与原图一致的纹理。

    这是为什么呢?

    假设我们有一张 1000 X 1000 的图片,而屏幕上的控件大小是 100 X 100 ,则纹理渲染到屏幕后,渲染结果对应的渲染缓存的尺寸也是 100 X 100 (暂不考虑屏幕密度)。如果我们这时候直接读取屏幕的渲染结果,最多也只能读到 100 X 100 的分辨率。

    这样会导致图片的分辨率下降,所以我们会使用能保持原有分辨率的方式,即「渲染到纹理」。

    在这之前,我们都是将纹理直接渲染到屏幕上,关键步骤像这样:

    GLuint renderBuffer; // 渲染缓存
    GLuint frameBuffer; // 帧缓存

    // 绑定渲染缓存要输出的 layer
    glGenRenderbuffers(1, &renderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];

    // 将渲染缓存绑定到帧缓存上
    glGenFramebuffers(1, &frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
    GL_COLOR_ATTACHMENT0,
    GL_RENDERBUFFER,
    renderBuffer);

    我们生成了一个渲染缓存,并把这个渲染缓存挂载到帧缓存的 GL_COLOR_ATTACHMENT0 颜色缓存上,并通过 context 为当前的渲染缓存绑定了输出的 layer 。

    其实,如果我们不需要在屏幕上显示我们的渲染结果,也可以直接将数据渲染到另一个纹理上。更有趣的是,这个渲染后的结果,还可以被当成一个普通的纹理来使用。这也是我们实现重复调整功能的基础。

    具体操作如下:

    // 生成帧缓存,挂载渲染缓存
    GLuint frameBuffer;
    GLuint texture;

    glGenFramebuffers(1, &frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);

    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

    通过对比我们可以发现,这里我们用 Texture 来替换 Renderbuffer ,并且同样是挂载到 GL_COLOR_ATTACHMENT0 上,不过这里就不需要另外再绑定 layer 了。

    另外,我们需要为新的纹理设置一个尺寸,这个尺寸不再受限于屏幕上控件的尺寸,这也是新纹理可以保持原有分辨率的原因。

    这时候,渲染的结果都会被保存在 texture 中,而 texture 也可以被当成普通的纹理来使用。

    六、保存结果

    当我们调整出满意的图片后,需要把它保存下来。这里分为两步,第一步仍然是上面提到的重新生成纹理,第二步就是把纹理转化为图片。

    第二步主要通过 glReadPixels 方法来实现,它可以从当前的帧缓存中读取出纹理数据。直接上代码:

    // 返回某个纹理对应的 UIImage,调用前先绑定对应的帧缓存
    - (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
    int size = width * height * 4;
    GLubyte *buffer = malloc(size);
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL);
    int bitsPerComponent = 8;
    int bitsPerPixel = 32;
    int bytesPerRow = 4 * width;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
    CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);

    // 此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,刚好翻转过来
    UIGraphicsBeginImageContext(CGSizeMake(width, height));
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    free(buffer);
    return image;
    }

    至此,我们已经拿到了 UIImage 对象,可以把它保存到相册里了。

    源码

    请到 GitHub 上查看完整代码。

    参考

    iOS 中使用 OpenGL 实现增高功能
    学习 OpenGL ES 之渲染到纹理
    获取更佳的阅读体验,请访问原文地址【Lyman's Blog】使用 iOS OpenGL ES 实现长腿功能

    链接:https://www.jianshu.com/p/433f13a2945e

    收起阅读 »

    iOS-ijkplayer集成

    ijkplayer是bibiliB站开源的一个三方,封装好了ffmpeg,可以去面向对象去开发。苹果提供了:AVPlayer播放不了直播文件。需要自己去基于ffmpeg播放。1.搜索查找ijkplayer2.克隆ijkplayer到桌面cd Desktop/ ...
    继续阅读 »

    ijkplayer是bibiliB站开源的一个三方,封装好了ffmpeg,可以去面向对象去开发。

    苹果提供了:AVPlayer播放不了直播文件。需要自己去基于ffmpeg播放。

    1.搜索查找ijkplayer





    2.克隆ijkplayer到桌面

    cd Desktop/
    git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-ios



    3.下载ffmpeg


    4.编译ffmpeg


    编译很多情况,64位、32位


    ps: 如果提示错误:

    ./libavutil/arm/asm.S:50:9: error: unknown directive
    .arch armv7-a
    ^
    make: *** [libavcodec/arm/aacpsdsp_neon.o] Error 1
    最新的 Xcode 已经弱化了对 32 位的支持, 解决方法:
    在 compile-ffmpeg.sh 中删除 armv7 , 修改如:
    FF_ALL_ARCHS_IOS8_SDK="arm64 i386 x86_64"
    再重新执行出现错误的命令: ./compile-ffmpeg.sh all

    5.打包framwork并合并

    大家会发现除了IJKMediaFramework这个目标,还有一个叫IJKMediaFrameworkWithSSL,但是不推荐使用这个,因为大部分基于ijkplayer的第三方框架都是使用的前者,你把后者导入项目还是会报找不到包的错误,就算你要支持https也推荐使用前者,然后按照上一步添加openssl即可支持

    5.1,配置释放模式如下图



    5.2,打包真机框架


    如图操作,然后按键命令+ B编译即可

    如果之前的步骤删除了compile-ffmpeg.sh中armv7,这里会报错,我们直接注释掉就好


    用Xcode9可以找到这个 ,但是用Xcode10找不到这个 我只能用Xcode注释完,在用Xcode10编译就没问题了

    5.3,打包模拟器 framework


    如图操作,然后命令+ B编译即可

    5.4,合并框架
    如果只需要真机运行或者模拟器运行,可以不用合并,直接找到对应的框架导入项目即可; 一般我们为了方便会合并框架,这样就同时支持模拟器和真机运行。
    先找到生成框架的目录:



    准备合并:

    打开终端, 先 cd 到 Products 目录下
    然后执行: lipo -create 真机framework路径 模拟器framework路径 -output 合并的文件路径

    lipo -create Release-iphoneos/IJKMediaFramework.framework/IJKMediaFramework Release-iphonesimulator/IJKMediaFramework.framework/IJKMediaFramework -output IJKMediaFramework

    合并完成:
    可以看到这里生成了一个大概两倍大小的文件, 将生成的 IJKMediaFramework 文件替换掉 真机framework 中的 IJKMediaFramework 文件,然后这个替换掉文件的 真机framework 就是我们需要的 通用的framework 了。



    6.集成 framework 到项目中

    1、导入 framework

    直接将 IJKMediaFramework.framework 拖入到工程中即可
    注意记得勾选 Copy items if needed 和 对应的 target

    2、添加下列依赖到工程


    【参考文章】:
    1、ijkplayer 的编译、打包 framework 和 https 支持
    2、armv7 armv7s arm64
    3、iOS IJKPlayer项目集成(支持RTSP)
    4、可用rtmp直播源

    链接:https://www.jianshu.com/p/9a69af13835e

    收起阅读 »

    iOS利用RunTime来实现万能跳转

    1.万能跳转的应用场景:(1)手机App通过推送过来的数据内容来跳转不同的界面,并把界面数据展示出来。(2)手机内部根据不同的cell的点击事件,不同的数据跳转不同的界面。2.工作的流程图:通过动态返回的数据中的class类名,来去查询class是不是存在:(...
    继续阅读 »

    1.万能跳转的应用场景:

    (1)手机App通过推送过来的数据内容来跳转不同的界面,并把界面数据展示出来。
    (2)手机内部根据不同的cell的点击事件,不同的数据跳转不同的界面。

    2.工作的流程图:

    通过动态返回的数据中的class类名,来去查询class是不是存在:(1)存在则获取实例对象然后通过kVC来绑定数据然后去跳转。(2)不存在则动态创建class及其变量,然后手动创建实例对象在通过KVC来绑定数据,最后跳转。


    3.主要方法:

    //创建Class
    objc_allocateClassPair(Class superclass, const char * name, size_t extraBytes)
    //注册Class
    void objc_registerClassPair(Class cls)
    //添加变量
    class_addIvar(Class cls, const char * name,size_t size, uint8_t alignment , const char * types)
    //添加方法
    class_addMethod(Class cls, SEL name, IMP imp, const char * types)
    //获取属性
    class_getProperty(Class cls, const char * name)
    //获取实例变量
    class_getInstanceVariable(Class cls, const char * name)

    4.代码实现:

    1、工程中新建三个控制器,命名为
    FirstViewController
    SecondViewController
    ThredViewController
    每一个控制器的viewDidLoad方法里面的内容为

    self.view.backgroundColor = [UIColor redColor];

    UILabel * titleLab = [[UILabel alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
    titleLab.textColor = [UIColor blackColor];
    [self.view addSubview:titleLab];
    titleLab.text =self.name;

    然后在ViewController模拟根据不同数据跳转不同界面,代码如下

    #import "ViewController.h"
    #import <objc/message.h>

    @interface ViewController ()

    @property (nonatomic, weak) UISegmentedControl * seg;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.view.backgroundColor = [UIColor yellowColor];

    NSArray * array = @[@"消息1",@"消息2",@"消息3",@"消息4"];
    UISegmentedControl * seg = [[UISegmentedControl alloc]initWithItems:array];
    seg.frame = CGRectMake(70, 200, 240, 45);
    [self.view addSubview:seg];
    seg.selectedSegmentIndex = 0;
    self.seg = seg;

    UIButton * jupBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    jupBtn.frame = CGRectMake(100, 250, 60, 45);
    [jupBtn setTitle:@"跳转" forState:UIControlStateNormal];
    [jupBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    jupBtn.backgroundColor = [UIColor redColor];
    [self.view addSubview:jupBtn];
    [jupBtn addTarget:self action:@selector(action) forControlEvents:UIControlEventTouchUpInside];

    //创建Class
    //objc_allocateClassPair(Class superclass, const char * name, size_t extraBytes)
    //注册Class
    //void objc_registerClassPair(Class cls)
    //添加变量
    //class_addIvar(Class cls, const char * name,size_t size, uint8_t alignment , const char * types)
    //添加方法
    //class_addMethod(Class cls, SEL name, IMP imp, const char * types)
    //获取属性
    //class_getProperty(Class cls, const char * name)
    //获取实例变量
    //class_getInstanceVariable(Class cls, const char * name)
    }

    -(void)action{

    NSDictionary * infoDic = nil;

    switch (self.seg.selectedSegmentIndex) {
    case 0:
    infoDic = @{@"class":@"FirstViewController",
    @"property":@{
    @"name":@"尼古拉斯赵四"
    }
    };
    break;
    case 1:
    infoDic = @{@"class":@"SecondViewController",
    @"property":@{
    @"age":@"26",
    @"sex":@"男"
    }
    };
    break;
    case 2:
    infoDic = @{@"class":@"ThredViewController",
    @"property":@{
    @"teacher":@"王老师",
    @"money":@"5000"
    }
    };
    break;
    case 3:
    //NewViewController
    infoDic = @{@"class":@"WorkerController",
    @"property":@{
    @"phoneNumber":@"17710948530"
    }
    };
    break;

    default:
    break;
    }

    [self pushToControllerWithData:infoDic];

    }
    -(void)pushToControllerWithData:(NSDictionary * )vcData{
    //1.获取class
    const char * className = [vcData[@"class"] UTF8String];
    Class cls = objc_getClass(className);
    if(!cls){
    //创建新的类,并添加变量和方法
    Class superClass = [UIViewController class];
    cls = objc_allocateClassPair(superClass, className, 0);
    //添加phoneNumber变量
    class_addIvar(cls, "phoneNumber", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    //添加titleLab控件
    class_addIvar(cls, "titleLab", sizeof(UILabel *), log2(sizeof(UILabel *)), @encode(UILabel *));
    //添加方法,方法交换,执行viewDidLoad加载
    Method method = class_getInstanceMethod([self class], @selector(workerLoad));
    IMP methodIMP = method_getImplementation(method);
    const char * types = method_getTypeEncoding(method);
    class_addMethod(cls, @selector(viewDidLoad), methodIMP, types);
    }
    //2.创建实例对象,给属性赋值
    id instance = [[cls alloc]init];
    NSDictionary * values = vcData[@"property"];
    [values enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
    //检测是否存在为key的属性
    if(class_getProperty(cls, [key UTF8String])){
    [instance setValue:obj forKey:key];
    }
    //检测是否存在为key的变量
    else if (class_getInstanceVariable(cls, [key UTF8String])){
    [instance setValue:obj forKey:key];
    }
    }];

    //2.跳转到对应的界面
    [self.navigationController pushViewController:instance animated:YES];

    }

    -(void)workerLoad{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];
    //初始化titleLab
    [self setValue:[[UILabel alloc]initWithFrame:CGRectMake(100, 100, 200, 40)] forKey:@"titleLab"];
    UILabel * titleLab = [self valueForKey:@"titleLab"];
    //添加到视图上
    [[self valueForKey:@"view"] performSelector:@selector(addSubview:) withObject:titleLab];
    titleLab.text =[self valueForKey:@"phoneNumber"];
    titleLab.textColor = [UIColor blackColor];

    }

    @end

    5.demo的下载地址,喜欢的话给个星,谢谢:

    iOS根据不同数据跳转不同界面,动态添加属性及其控件等界面内容

    转自:https://www.jianshu.com/p/376a3bc7741b

    收起阅读 »

    AVPlayer封装

    说明基于AVPlayer和MVP模式封装的一个视频播放控制器,支持全屏,暂停播放,进度条拖动。Demo地址AVPlayer框架介绍AVPlay既可以用来播放音频也可以用来播放视频,AVPlay在播放音频方面可以直接用来播放网络上的音频。在使用AVPlay的时候...
    继续阅读 »

    说明

    基于AVPlayer和MVP模式封装的一个视频播放控制器,支持全屏,暂停播放,进度条拖动。

    Demo地址

    AVPlayer框架介绍

    AVPlay既可以用来播放音频也可以用来播放视频,AVPlay在播放音频方面可以直接用来播放网络上的音频。在使用AVPlay的时候我们需要导入AVFoundation.framework框架,再引入头文件#import<AVFoundation/AVFoundation.h>。

    主要包括下面几个类

    1.AVPlayer:播放器类
    2.AVPlayerItem:播放单元类,即一个播放源
    3.AVPlayerLayer:播放界面

    使用时,需要先根据NSURL生成一个播放源,[AVPlayerItem playerItemWithURL:],再根据这个播放源获得一个播放器对象,[AVPlayer playerWithPlayerItem:];,此时播放器已经准备完成,但还需要根据AVPlayer生成一个AVPlayerLayer,设置frame,再加入到superView.layer中,[AVPlayerLayer playerLayerWithPlayer:]; self.playerLayer.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.width*0.6); [self.layer addSublayer:self.playerLayer];

    此时一个简单的播放器就已经配置完成。

    暂停播放

    AVPlayer有一个rate属性,可以根据这个属性来判断当前是否在播放,rate == 0.f为暂停,反之视频播放。

    AVPlayerItemStatus
    可以对AVPlayerItem设置kvo,监听视频源是否可播放,系统给了三种状态,如下:

    typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
    AVPlayerItemStatusUnknown,
    AVPlayerItemStatusReadyToPlay,
    AVPlayerItemStatusFailed
    };

    设置KVO监听:

    [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"status"]) {
    AVPlayerItemStatus status = [change[NSKeyValueChangeNewKey] intValue];
    if (status == AVPlayerItemStatusReadyToPlay) {
    isReadyToPlay = YES;
    [self.player play];
    }else{
    //预留
    isReadyToPlay = NO;
    }
    [self.controlView controlItemStatus:status playItem:object];
    }
    }

    全屏操作

    Demo中给出的思路是:

    1.首先将当前竖屏状态下的播放器的view的frame保存下来,方便退出全屏时,布局;
    2.然后新建一个全屏展示View的控制器,重写该控制器的@property(nonatomic, readonly) UIInterfaceOrientation preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;,强制让该控制器旋转;
    3.将当前根控制器present到上述的全屏控制器,在completion:回调中,做个简单的动画过渡一下,然后再将承载AVPlayerLayer的view的frame改成横屏状态,然后再修改AVPlayerLayer的frame;

    退出全屏:

    1.将当前全屏控制器dismiss;
    2.再dismiss的成功回调中,设置View的frame为进入全屏前保存的frame;
    3.再将AVPlayerLayer的frame修改。

    代码如下:

    #pragma mark - 进入全屏和退出全屏的动画和present处理
    - (void)enterFullScreen:(BOOL)rightOrLeft{
    playViewBeforeRect = _playerView.frame;
    playViewBeforeCenter = _playerView.center;

    TBZAVFullViewController *vc = [[TBZAVFullViewController alloc] init];
    vc.type = rightOrLeft;
    self.fullVC = vc;

    __weak TBZAVPlayerViewController *weakSelf = self;

    [self.navigationController presentViewController:vc animated:false completion:^{
    [UIView animateWithDuration:0.25 animations:^{
    weakSelf.playerView.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height);
    } completion:^(BOOL finished) {
    [weakSelf.playerView enterFull];
    [weakSelf.fullVC.view addSubview:weakSelf.playerView];
    [UIApplication.sharedApplication.keyWindow insertSubview:UIApplication.sharedApplication.keyWindow.rootViewController.view belowSubview:vc.view.superview];

    self->isFull = YES;
    }];
    }];
    }

    - (void)exitFullScreen{
    __weak TBZAVPlayerViewController *weakSelf = self;
    [self.fullVC dismissViewControllerAnimated:false completion:^{
    [UIView animateWithDuration:0.25 animations:^{
    weakSelf.playerView.frame = self->playViewBeforeRect;
    } completion:^(BOOL finished) {
    [weakSelf.playerView exitFull];
    [weakSelf.view addSubview:weakSelf.playerView];

    self->isFull = NO;
    }];
    }];
    }

    播放进度

    主要就是需要对AVPlayer添加监听,且注意需要释放该方法返回的对象。AVPlayerItem有两个属性,currentTime和duration,这两个对象都是CMTime类,可以用CMTimeGetSeconds(CMTime t);得到一个float指,秒数。也就是CMTimeGetSeconds(item.currentTime)可以得到当前播放到第几秒,CMTimeGetSeconds(item.duration)可以得到当前视频的总时长。

    /*!
    @method addPeriodicTimeObserverForInterval:queue:usingBlock:
    @abstract Requests invocation of a block during playback to report changing time.
    @param interval
    The interval of invocation of the block during normal playback, according to progress of the current time of the player.
    @param queue
    The serial queue onto which block should be enqueued. If you pass NULL, the main queue (obtained using dispatch_get_main_queue()) will be used. Passing a
    concurrent queue to this method will result in undefined behavior.
    @param block
    The block to be invoked periodically.
    @result
    An object conforming to the NSObject protocol. You must retain this returned value as long as you want the time observer to be invoked by the player.
    Pass this object to -removeTimeObserver: to cancel time observation.
    @discussion The block is invoked periodically at the interval specified, interpreted according to the timeline of the current item.
    The block is also invoked whenever time jumps and whenever playback starts or stops.
    If the interval corresponds to a very short interval in real time, the player may invoke the block less frequently
    than requested. Even so, the player will invoke the block sufficiently often for the client to update indications
    of the current time appropriately in its end-user interface.
    Each call to -addPeriodicTimeObserverForInterval:queue:usingBlock: should be paired with a corresponding call to -removeTimeObserver:.
    Releasing the observer object without a call to -removeTimeObserver: will result in undefined behavior.
    */
    - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;

    /*!
    @method removeTimeObserver:
    @abstract Cancels a previously registered time observer.
    @param observer
    An object returned by a previous call to -addPeriodicTimeObserverForInterval:queue:usingBlock: or -addBoundaryTimeObserverForTimes:queue:usingBlock:.
    @discussion Upon return, the caller is guaranteed that no new time observer blocks will begin executing. Depending on the calling thread and the queue
    used to add the time observer, an in-flight block may continue to execute after this method returns. You can guarantee synchronous time
    observer removal by enqueuing the call to -removeTimeObserver: on that queue. Alternatively, call dispatch_sync(queue, ^{}) after
    -removeTimeObserver: to wait for any in-flight blocks to finish executing.
    -removeTimeObserver: should be used to explicitly cancel each time observer added using -addPeriodicTimeObserverForInterval:queue:usingBlock:
    and -addBoundaryTimeObserverForTimes:queue:usingBlock:.
    */
    - (void)removeTimeObserver:(id)observer;

    - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;其实就是一个Timer,每隔1秒执行block,可以设置常驻子线程,如果设为NULL,就是在主线程。
    主要使用如下:

    __weak AVPlayer *weakAVPlayer = self.player;
    __weak TBZAVPlayerView *weakSelf = self;
    //监听播放进度,需要再destory方法中,释放timeObserve
    self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, NSEC_PER_SEC) queue:NULL usingBlock:^(CMTime time) {
    CGFloat progress = CMTimeGetSeconds(weakAVPlayer.currentItem.currentTime) / CMTimeGetSeconds(weakAVPlayer.currentItem.duration);
    if (progress == 1.0f) {
    //视频播放完毕
    if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(playEnd)]) {
    [weakSelf.delegate playEnd];
    }
    }else{
    [weakSelf.controlView controlPlayItem:weakAVPlayer.currentItem];
    }
    }];

    - (void)destroy{
    if (self.player || self.playerItem || self.playerLayer) {
    [self.player pause];
    if (self.timeObserver) {
    [self.player removeTimeObserver:self.timeObserver];
    }
    [self.playerItem removeObserver:self forKeyPath:@"status"];
    self.playerItem = nil;
    self.player = nil;
    [self.playerLayer removeFromSuperlayer];
    }
    }

    总结

    1.当视频源切换了之后,需要将当前视频源添加的监听都remove掉,重新给新的视频源添加监听;
    2.全屏跟退出全屏,主要是注意AVPlayerLayer的布局,不会跟着superLayer的变动而变动,需要手动再设置一遍;

    具体可以结合Demo来看。

    Demo下载

    觉得有用,请帮忙点亮红心

    Better Late Than Never!
    努力是为了当机会来临时不会错失机会。
    共勉!

    10人点赞
    iOS知识点

    链接:https://www.jianshu.com/p/55825996cb11

    收起阅读 »

    Onboard,迷人的引导页样式制作库

    简介Onboard主要用于引导页制作,源码写的相当规范,值得参考.项目主页: https://github.com/mamaral/Onboard实例下载: https://github.com/mamaral/Onboard/archiv...
    继续阅读 »

    简介




    Onboard主要用于引导页制作,源码写的相当规范,值得参考.

    项目主页: https://github.com/mamaral/Onboard
    实例下载: https://github.com/mamaral/Onboard/archive/master.zip

    样式

    设置背景图片或者背景movie,然后在它们之上生成数个ViewController,默认是顶部一张图片,下面是标题和详细介绍,最下面是按钮和page

    导入

    pod 'Onboard'

    使用

    导入头文件#import “OnboardingViewController.h”

    图片为背景

    蒙板控制器生成方法

    1、title是标题
    2、body是介绍
    3、image是顶部图片
    4、buttonText是按钮文本
    5、block是按钮点击事件

    OnboardingContentViewController *firstPage = [OnboardingContentViewController contentWithTitle:@"What A Beautiful Photo" body:@"This city background image is so beautiful." image:[UIImage imageNamed:@"blue"] buttonText:@"Enable Location Services" action:^{
    }];

    OnboardingContentViewController *secondPage = [OnboardingContentViewController contentWithTitle:@"I'm so sorry" body:@"I can't get over the nice blurry background photo." image:[UIImage imageNamed:@"red"] buttonText:@"Connect With Facebook" action:^{
    }];
    secondPage.movesToNextViewController = YES;
    secondPage.viewDidAppearBlock = ^{
    };

    OnboardingContentViewController *thirdPage = [OnboardingContentViewController contentWithTitle:@"Seriously Though" body:@"Kudos to the photographer." image:[UIImage imageNamed:@"yellow"] buttonText:@"Get Started" action:^{
    }];
    ```


    #### 底部图片控制器

    ```objc
    OnboardingViewController *onboardingVC = [OnboardingViewController onboardWithBackgroundImage:[UIImage imageNamed:@"milky_way.jpg"] contents:@[firstPage, secondPage, thirdPage]];




    <div class="se-preview-section-delimiter"></div>

    底部video控制器

    NSBundle *bundle = [NSBundle mainBundle];
    NSString *moviePath = [bundle pathForResource:@"yourVid" ofType:@"mp4"];
    NSURL *movieURL = [NSURL fileURLWithPath:moviePath];
    OnboardingViewController *onboardingVC = [OnboardingViewController onboardWithBackgroundVideoURL:movieURL contents:@[firstPage, secondPage, thirdPage]];




    <div class="se-preview-section-delimiter"></div>

    定制
    1、默认的会给背景图片或者movie加一层黑色的蒙板,可以去掉它们:

    onboardingVC.shouldFadeTransitions = YES;




    <div class="se-preview-section-delimiter"></div>

    2、可以给图片加上模糊效果(相当漂亮):

    onboardingVC.shouldBlurBackground = YES;




    <div class="se-preview-section-delimiter"></div>

    3、可以给蒙板上的文字加上淡出效果:

    onboardingVC.shouldFadeTransitions = YES;

    转自:https://blog.csdn.net/sinat_30800357/article/details/50016319

    收起阅读 »

    CYLTabBarController的使用

    CYLTabBarController 是一个自定义的TabBarController, 集成非常简单https://github.com/ChenYilong/CYLTabBarController1.首先使用CocoaPods 进行集成: pod...
    继续阅读 »

    CYLTabBarController 是一个自定义的TabBarController, 集成非常简单

    https://github.com/ChenYilong/CYLTabBarController

    1.首先使用CocoaPods 进行集成: 

    pod 'CYLTabBarController'
    在终端上执行: 
    pod install --verbose --no-repo-update

    2. 创建TabBar对应的视图控制器


    3.创建CYLTabBarControllerConfig

    #import <Foundation/Foundation.h>  

    #import "CYLTabBarController.h"
    @interface CYLTabBarControllerConfig : NSObject

    @property (nonatomic, retain) CYLTabBarController * tabBarController;

    @end
    #import "CYLTabBarControllerConfig.h"  

    #import "FirstViewController.h"
    #import "SecondViewController.h"
    #import "ThirdViewController.h"
    #import "FourthViewController.h"


    @implementation CYLTabBarControllerConfig

    - (CYLTabBarController *)tabBarController {
    if (_tabBarController == nil) {
    FirstViewController * firstViewController = [[FirstViewController alloc] init];
    UIViewController * firstNavigationController = [[UINavigationController alloc] initWithRootViewController:firstViewController];

    SecondViewController * secondViewController = [[SecondViewController alloc] init];
    UIViewController * secondNavigationController = [[UINavigationController alloc] initWithRootViewController:secondViewController];

    ThirdViewController * thirdViewController = [[ThirdViewController alloc] init];
    UIViewController * thirdNavigationController = [[UINavigationController alloc] initWithRootViewController:thirdViewController];

    FourthViewController * fourthViewController = [[FourthViewController alloc] init];
    UIViewController * fourthNavigationController = [[UINavigationController alloc] initWithRootViewController:fourthViewController];


    NSArray * tabBarItemsAttributes = [self tabBarItemsAttributes];
    NSArray * viewControllers = @[firstNavigationController, secondNavigationController, thirdNavigationController, fourthNavigationController];

    CYLTabBarController * tabBarController = [[CYLTabBarController alloc] init];

    tabBarController.tabBarItemsAttributes = tabBarItemsAttributes;
    tabBarController.viewControllers = viewControllers;

    _tabBarController = tabBarController;

    }

    return _tabBarController;
    }


    - (NSArray *)tabBarItemsAttributes {
    NSDictionary * tabBarItem1Attribute = @{
    CYLTabBarItemTitle : @"首页",
    CYLTabBarItemImage : @"home_normal",
    CYLTabBarItemSelectedImage : @"home_highlight"
    };
    NSDictionary * tabBarItem2Attribute = @{
    CYLTabBarItemTitle : @"同城",
    CYLTabBarItemImage : @"mycity_normal",
    CYLTabBarItemSelectedImage : @"mycity_highlight"
    };
    NSDictionary * tabBarItem3Attribute = @{
    CYLTabBarItemTitle : @"消息",
    CYLTabBarItemImage : @"message_normal",
    CYLTabBarItemSelectedImage : @"message_highlight"
    };
    NSDictionary * tabBarItem4Attribute = @{
    CYLTabBarItemTitle : @"我的",
    CYLTabBarItemImage : @"account_normal",
    CYLTabBarItemSelectedImage : @"account_highlight"
    };
    NSArray * tarBarItemsAttrbutes = @[tabBarItem1Attribute, tabBarItem2Attribute, tabBarItem3Attribute, tabBarItem4Attribute];

    return tarBarItemsAttrbutes;
    }


    /**
    * 更多TabBar自定义设置:比如:tabBarItem 的选中和不选中文字和背景图片属性、tabbar 背景图片属性
    */
    + (void)customizeTabBarAppearance {

    //去除 TabBar 自带的顶部阴影
    [[UITabBar appearance] setShadowImage:[[UIImage alloc] init]];

    // set the text color for unselected state
    // 普通状态下的文字属性
    NSMutableDictionary *normalAttrs = [NSMutableDictionary dictionary];
    normalAttrs[NSForegroundColorAttributeName] = [UIColor blackColor];

    // set the text color for selected state
    // 选中状态下的文字属性
    NSMutableDictionary *selectedAttrs = [NSMutableDictionary dictionary];
    selectedAttrs[NSForegroundColorAttributeName] = [UIColor blackColor];

    // set the text Attributes
    // 设置文字属性
    UITabBarItem *tabBar = [UITabBarItem appearance];
    [tabBar setTitleTextAttributes:normalAttrs forState:UIControlStateNormal];
    [tabBar setTitleTextAttributes:selectedAttrs forState:UIControlStateSelected];

    // Set the dark color to selected tab (the dimmed background)
    // TabBarItem选中后的背景颜色
    [[UITabBar appearance] setSelectionIndicatorImage:[self imageFromColor:[UIColor colorWithRed:26 / 255.0 green:163 / 255.0 blue:133 / 255.0 alpha:1] forSize:CGSizeMake([UIScreen mainScreen].bounds.size.width / 5.0f, 49) withCornerRadius:0]];

    // set the bar background color
    // 设置背景图片
    // UITabBar *tabBarAppearance = [UITabBar appearance];
    // [tabBarAppearance setBackgroundImage:[UIImage imageNamed:@"tabbar_background_ios7"]];
    }

    + (UIImage *)imageFromColor:(UIColor *)color forSize:(CGSize)size withCornerRadius:(CGFloat)radius {
    CGRect rect = CGRectMake(0, 0, size.width, size.height);
    UIGraphicsBeginImageContext(rect.size);

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, [color CGColor]);
    CGContextFillRect(context, rect);

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    // Begin a new image that will be the new image with the rounded corners
    // (here with the size of an UIImageView)
    UIGraphicsBeginImageContext(size);

    // Add a clip before drawing anything, in the shape of an rounded rect
    [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius] addClip];
    // Draw your image
    [image drawInRect:rect];

    // Get the image, here setting the UIImageView image
    image = UIGraphicsGetImageFromCurrentImageContext();

    // Lets forget about that we were drawing
    UIGraphicsEndImageContext();
    return image;
    }

    4. AppDelegate 设置根视图控制器

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
    // TabBar
    CYLTabBarControllerConfig * TabBarControllerConfig = [[CYLTabBarControllerConfig alloc] init];
    self.window.rootViewController = TabBarControllerConfig.tabBarController;
    [self customizeInterface];

    return YES;
    }
    - (void)customizeInterface {
    [self setUpNavigationBarAppearance];
    }

    /**
    * 设置navigationBar样式
    */
    - (void)setUpNavigationBarAppearance {
    UINavigationBar *navigationBarAppearance = [UINavigationBar appearance];

    UIImage *backgroundImage = nil;
    NSDictionary *textAttributes = nil;
    if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
    backgroundImage = [UIImage imageNamed:@"navigationbar_background_tall"];

    textAttributes = @{
    NSFontAttributeName: [UIFont boldSystemFontOfSize:18],
    NSForegroundColorAttributeName: [UIColor blackColor],
    };
    } else {
    #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0
    backgroundImage = [UIImage imageNamed:@"navigationbar_background"];

    textAttributes = @{
    UITextAttributeFont: [UIFont boldSystemFontOfSize:18],
    UITextAttributeTextColor: [UIColor blackColor],
    UITextAttributeTextShadowColor: [UIColor clearColor],
    UITextAttributeTextShadowOffset: [NSValue valueWithUIOffset:UIOffsetZero],
    };
    #endif
    }

    [navigationBarAppearance setBackgroundImage:backgroundImage
    forBarMetrics:UIBarMetricsDefault];
    [navigationBarAppearance setTitleTextAttributes:textAttributes];
    }

    运行即可实现效果,如果想实现凸起的加号效果需要 CYLPlusButtonSubclass

    #import "CYLPlusButton.h"  

    @interface CYLPlusButtonSubclass : CYLPlusButton <CYLPlusButtonSubclassing>

    @end
    #import "CYLPlusButtonSubclass.h"  

    @interface CYLPlusButtonSubclass ()<UIActionSheetDelegate> {
    CGFloat _buttonImageHeight;
    }

    @end

    @implementation CYLPlusButtonSubclass

    #pragma mark -
    #pragma mark - Life Cycle

    + (void)load {
    [super registerSubclass];
    }

    - (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
    self.titleLabel.textAlignment = NSTextAlignmentCenter;
    self.adjustsImageWhenHighlighted = NO;
    }
    return self;
    }

    //上下结构的 button
    - (void)layoutSubviews {
    [super layoutSubviews];

    // 控件大小,间距大小
    CGFloat const imageViewEdge = self.bounds.size.width * 0.6;
    CGFloat const centerOfView = self.bounds.size.width * 0.5;
    CGFloat const labelLineHeight = self.titleLabel.font.lineHeight;
    CGFloat const verticalMarginT = self.bounds.size.height - labelLineHeight - imageViewEdge;
    CGFloat const verticalMargin = verticalMarginT / 2;

    // imageView 和 titleLabel 中心的 Y 值
    CGFloat const centerOfImageView = verticalMargin + imageViewEdge * 0.5;
    CGFloat const centerOfTitleLabel = imageViewEdge + verticalMargin * 2 + labelLineHeight * 0.5 + 5;

    //imageView position 位置
    self.imageView.bounds = CGRectMake(0, 0, imageViewEdge, imageViewEdge);
    self.imageView.center = CGPointMake(centerOfView, centerOfImageView);

    //title position 位置
    self.titleLabel.bounds = CGRectMake(0, 0, self.bounds.size.width, labelLineHeight);
    self.titleLabel.center = CGPointMake(centerOfView, centerOfTitleLabel);
    }

    #pragma mark -
    #pragma mark - Public Methods

    /*
    *
    Create a custom UIButton with title and add it to the center of our tab bar
    *
    */
    + (instancetype)plusButton {

    CYLPlusButtonSubclass *button = [[CYLPlusButtonSubclass alloc] init];

    [button setImage:[UIImage imageNamed:@"post_normal"] forState:UIControlStateNormal];
    [button setTitle:@"发布" forState:UIControlStateNormal];

    [button setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
    button.titleLabel.font = [UIFont systemFontOfSize:9.5];
    [button sizeToFit];

    [button addTarget:button action:@selector(clickPublish) forControlEvents:UIControlEventTouchUpInside];
    return button;
    }
    /*
    *
    Create a custom UIButton without title and add it to the center of our tab bar
    *
    */
    //+ (instancetype)plusButton
    //{
    //
    // UIImage *buttonImage = [UIImage imageNamed:@"hood.png"];
    // UIImage *highlightImage = [UIImage imageNamed:@"hood-selected.png"];
    //
    // CYLPlusButtonSubclass* button = [CYLPlusButtonSubclass buttonWithType:UIButtonTypeCustom];
    //
    // button.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin;
    // button.frame = CGRectMake(0.0, 0.0, buttonImage.size.width, buttonImage.size.height);
    // [button setBackgroundImage:buttonImage forState:UIControlStateNormal];
    // [button setBackgroundImage:highlightImage forState:UIControlStateHighlighted];
    // [button addTarget:button action:@selector(clickPublish) forControlEvents:UIControlEventTouchUpInside];
    //
    // return button;
    //}

    #pragma mark -
    #pragma mark - Event Response

    - (void)clickPublish {
    UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;
    UIViewController *viewController = tabBarController.selectedViewController;

    UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil
    delegate:self
    cancelButtonTitle:@"取消"
    destructiveButtonTitle:nil
    otherButtonTitles:@"拍照", @"从相册选取", @"淘宝一键转卖", nil nil];
    [actionSheet showInView:viewController.view];
    }

    #pragma mark - UIActionSheetDelegate
    - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
    NSLog(@"index: %ld", buttonIndex);
    }


    #pragma mark - CYLPlusButtonSubclassing
    //+ (NSUInteger)indexOfPlusButtonInTabBar {
    // return 3;
    //}

    + (CGFloat)multiplerInCenterY {
    return 0.3;
    }

    @end

    Demo 下载地址:

    http://download.csdn.net/detail/vbirdbest/9431253

    实现效果如图:


    转自:https://blog.csdn.net/man_liang/article/details/56671353

    收起阅读 »

    iOS 开源项目-FXBlurView

    PurposeFXBlurView is a UIView subclass that replicates the iOS 7 realtime background blur effect, but works on iOS 5 and above. It...
    继续阅读 »

    Purpose
    FXBlurView is a UIView subclass that replicates the iOS 7 realtime background blur effect, but works on iOS 5 and above. It is designed to be as fast and as simple to use as possible. FXBlurView offers two modes of operation: static, where the view is rendered only once when it is added to a superview (though it can be updated by calling setNeedsDisplay or updateAsynchronously:completion:) or dynamic, where it will automatically redraw itself on a background thread as often as possible.
    FXBlurView 是一个 UIView 的子类,复制了 iOS7 的实时背景模糊效果,但是可以运行在 iOS5 以上的版本。它的设计简单易用。FXBlurView 提供两种模式:静态模糊和动态模糊。

    FXBlurView methods

    +(void)setBlurEnabled:(BOOL)blurEnabled;

    This method can be used to globally enable/disable the blur effect on all FXBlurView instances. This is useful for testing, or if you wish to disable blurring on iPhone 4 and below (for consistency with iOS7 blur view behavior). By default blurring is enabled.

    这个方法用来设置全局 使能/不使能 模糊效果在 FXBlurView 的实例上。默认情况下模糊效果是启动。

    +(void)setUpdatesEnabled;
    +(void)setUpdatesDisabled;

    These methods can be used to enable and disable updates for all dynamic FXBlurView instances with a single command. Useful for disabling updates immediately before performing an animation so that the FXBlurView updates don’t cause the animation to stutter. Calls can be nested, but ensure that the enabled/disabled calls are balanced, or the updates will be left permanently enabled or disabled.

    这两个方法用来设置所以的动态 FXBlurView 是否进行更新,通过一条指令执行。在展示动画之前立即对没有用的更新进行更新,使 FXBlurView 更新不会产生动画断断续续的效果。调用可以嵌套,但确保 启用/禁用 调用平衡,否则更新会留下永久 启用/禁用。

    -(void)updateAsynchronously:(BOOL)async completion:(void (^)())completion;

    This method can be used to trigger an update of the blur effect (useful when dynamic = NO). The async argument controls whether the blur will be redrawn on the main thread or in the background. The completion argument is an optional callback block that will be called when the blur is completed.

    这个方法可以用于触发更新模糊效果。(在属性 "dynamic = NO"情况下有用)。异步参数控制是否模糊将要在主线程上或在后台进行重绘。完成参数是一个可供选择的回调块,将在模糊完成的时候进行调用。

    -(void)setNeedsDisplay;

    Inherited from UIView, this method can be used to trigger a (synchronous) update of the view. Calling this method is more-or-less equivalent to calling [view updateAsynchronously:NO completion:NULL].

    继承 UIView,这个方法用于触发一个(同步)更新视图。调用这个方法或多或少等同于调用[view updateAsynchronously:NO completion:NULL].

    FXBlurView properties

    @property (nonatomic, getter = isBlurEnabled) BOOL blurEnabled;

    This property toggles blurring on and off for an individual FXBlurView instance. Blurring is enabled by default. Note that if you disable blurring using the +setBlurEnabled method then that will override this setting.

    这个属性用来切换 FXBlurView 单独实例模糊启动还是关闭。默认情况下模糊是使能的。请注意,如果您禁用模糊方法 setBlurEnabled,那么它将覆盖此设置。

    @property (nonatomic, getter = isDynamic) BOOL dynamic;

    This property controls whether the FXBlurView updates dynamically, or only once when the view is added to its superview. Defaults to YES. Note that if dynamic is set to NO, you can still force the view to update by calling setNeedsDisplay or updateAsynchronously:completion:. Dynamic blurring is extremely cpu-intensive, so you should always disable dynamic views immediately prior to performing an animation to avoid stuttering. However, if you have multiple FXBlurViews on screen then it is simpler to disable updates using the setUpdatesDisabledmethod rather than setting the dynamic property to NO.

    这个属性控制 FXBlurView 是否动态更新,还是只有在视图加入到它的父视图中。默认情况下是 YES ,请注意,如果你设置 dynamic 属性为 NO,你可以强制视图更新通过调用 setNeedsDisplay或者updateAsynchronously:completion:。动态模糊非常消耗 CPU 内存,所以您应该禁用立即执行的动态视图避免出现断断续续的动画。然而,如果您在屏幕上有多个 FXBlurViews ,通过设置方法 setUpdatedsDisabled来禁止更新比用设置动态属性为 NO 更为简单。

    @property (nonatomic, assign) NSUInteger iterations;

    The number of blur iterations. More iterations improves the quality but reduces the performance. Defaults to 2 iterations.

    模糊迭代的次数。更多的迭代提高质量,但会降低性能。默认值为 2 的迭代。

    @property (nonatomic, assign) NSTimeInterval updateInterval;

    This controls the interval (in seconds) between successive updates when the FXBlurView is operating in dynamic mode. This defaults to zero, which means that the FXBlurView will update as fast as possible. This yields the best frame rate, but is also extremely CPU intensive and may cause the rest of your app’s performance to degrade, especially on older devices. To alleviate this, try increasing the updateInterval value.

    此属性控制 FXBlurView 在动态模式下,距离成功更新的时间间隔(以秒计)。默认值为 0 ,这表示 FXBlurView 更新越快越好。 这将生成最佳的帧速率,但是也是非常消耗 CPU内存,导致你的其他 apps 无法无法加载,特别是旧设备。为了减缓这些情况,尝试增加updateInterval 的值。

    @property (nonatomic, assign) CGFloat blurRadius;

    This property controls the radius of the blur effect (in points). Defaults to a 40 point radius, which is similar to the iOS 7 blur effect.

    此属性控制模糊效果的半径 (以像素点计)。默认是半径为40个像素点,这个值与 iOS7 模糊效果相似。

    @property (nonatomic, strong) UIColor *tintColor;

    This in an optional tint color to be applied to the FXBlurView. The RGB components of the color will be blended with the blurred image, resulting in a gentle tint. To vary the intensity of the tint effect, use brighter or darker colors. The alpha component of the tintColor is ignored. If you do not wish to apply a tint, set this value to nil or [UIColor clearColor]. Note that if you are using Xcode 5 or above, FXBlurViews created in Interface Builder will have a blue tint by default.

    这是应用在 FXBlurView 可选的色调选择。颜色的 RGB 分量将会掺入到模糊图像上,导致产生一个柔和的色调。为了验证色调效果,若要改变色调效果的强度,使用更亮或更暗的颜色。颜色的透明参数是被忽略的。如果您不想应用色调,设置[UIColor clearColor]。请注意,如果您现在使用 Xcode5及以上,FXBlurViews 产生一个接口生成器将默认有一个一个蓝色的色调。

    @property (nonatomic, weak) UIView *underlyingView;

    This property specifies the view that the FXBlurView will sample to create the blur effect. If set to nil (the default), this will be the superview of the blur view itself, but you can override this if you need to.

    此属性表明该视图是 FXBlurView 产生模糊效果的子视图。如果设置为 nil(默认),则该视图的父视图是模糊视图本身,但是如果您有需要您可以进行覆盖它。

    总结

    今天通过学习 FXBlurView ,提高对英语文档的理解和翻译能力,增加了自己的学习兴趣,也懂得了如何去使用 FXBlurView 的模糊效果特效。在实际中提升自己的能力,年轻,就是资本!Oh Yeah!

    参考

    https://github.com/cnbin/FXBlurView

    转自:https://cnbin.github.io/blog/2015/05/25/ioskai-yuan-xiang-mu-fxblurview/

    收起阅读 »

    性能超高的UI库-AsyncDisplayKit

    AsyncDisplayKit 已移动并重命名:Texture性能提升AsyncDisplayKit 的基本单位是node. ASDisplayNode 是对 的抽象UIView,而后者又是对 的抽象CALayer。与只能在主线程上使用的视图不同,节...
    继续阅读 »

    AsyncDisplayKit 已移动并重命名:Texture

    性能提升

    AsyncDisplayKit 的基本单位是nodeASDisplayNode 是对 的抽象UIView,而后者又是对 的抽象CALayer与只能在主线程上使用的视图不同,节点是线程安全的:您可以在后台线程上并行实例化和配置它们的整个层次结构。

    为了保持其用户界面流畅和响应迅速,您的应用程序应以每秒 60 帧的速度呈现——这是 iOS 的黄金标准。这意味着主线程有六十分之一秒来推动每一帧。执行所有布局和绘图代码需要 16 毫秒!并且由于系统开销,您的代码在导致丢帧之前的运行时间通常不到 10 毫秒。

    AsyncDisplayKit 允许您将图像解码、文本大小调整和渲染、布局和其他昂贵的 UI 操作移出主线程,以保持主线程可用于响应用户交互。


    随着框架的发展,添加了许多功能,通过消除现代 iOS 应用程序中常见的样板样式结构,可以为开发人员节省大量时间。如果您曾经处理过单元格重用错误,尝试为页面或滚动样式界面高效地预加载数据,或者甚至只是试图防止您的应用丢失太多帧,您都可以从集成 ASDK 中受益。


    详细的api介绍:

    https://texturegroup.org/appledocs.html


    常见问题及demo下载:

    https://github.com/facebookarchive/AsyncDisplayKit


    源码下载:




    收起阅读 »

    ZFPlayer 3.0解析

    详细介绍一下ZFPlayer 3.0的用法,如果你有什么问题或者建议可联系我。在3.0之前版本使用ZFPlayer,是不是在烦恼播放器SDK自定义、控制层自定义等问题。作者公司多个项目分别使用不同播放器SDK以及每个项目控制层都不一样,但是为了统一管理、统一调...
    继续阅读 »

    详细介绍一下ZFPlayer 3.0的用法,如果你有什么问题或者建议可联系我。在3.0之前版本使用ZFPlayer,是不是在烦恼播放器SDK自定义、控制层自定义等问题。作者公司多个项目分别使用不同播放器SDK以及每个项目控制层都不一样,但是为了统一管理、统一调用,我特意写了这个播放器壳子。播放器SDK只要遵守ZFPlayerMediaPlayback协议,控制层只要遵守ZFPlayerMediaControl协议,可以实现自定义播放器和控制层。

    目前支持的功能如下:

    1、普通模式的播放,类似于腾讯视频、爱奇艺等APP;
    2、列表普通模式的播放,包括手动点击播放、滑动到屏幕中间自动播放,wifi网络智能播放等等;
    3、列表的亮暗模式播放,类似于微博、UC浏览器视频列表等APP;
    4、列表视频滑出屏幕后停止播放、滑出屏幕后小窗播放;
    5、优雅的全屏,支持横屏和竖屏全屏模式;

    播放器的主要类为ZFPlayerController,具体API请看下边这张图吧,后边我也会详细介绍。在之前版本收到好多开发朋友的Issues建议也好bug也好,ZFPlayer也是致力于解决这些问题和满足各位的建议。

    ZFPlayerController(播放器的主要类)

    初始化方式:

    /// 普通播放的初始化
    + (instancetype)playerWithPlayerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

    /// 普通播放的初始化
    - (instancetype)initWithPlayerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

    /// UITableView、UICollectionView播放的初始化
    + (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerViewTag:(NSInteger)containerViewTag;

    /// UITableView、UICollectionView播放的初始化
    - (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerViewTag:(NSInteger)containerViewTag;

    /// UIScrollView播放的初始化
    + (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

    /// UIScrollView播放的初始化
    - (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

    属性

    /// 初始化时传递的容器视图,用来显示播放器view,和播放器view同等大小
    @property (nonatomic, strong) UIView *containerView;

    /// 初始化时传递的播放器manager,必须遵守`ZFPlayerMediaPlayback`协议
    @property (nonatomic, strong) id<ZFPlayerMediaPlayback> currentPlayerManager;

    /// 此属性是设置显示的控制层,自定义UIView遵守`ZFPlayerMediaControl`协议,实现相关协议就可以满足自定义控制层的目的。
    @property (nonatomic, strong) UIView<ZFPlayerMediaControl> *controlView;

    /// 通知的管理类
    @property (nonatomic, strong, readonly) ZFPlayerNotification *notification;

    /// 容器的类型(cell和普通View)
    @property (nonatomic, assign, readonly) ZFPlayerContainerType containerType;

    /// 播放器小窗的容器View
    @property (nonatomic, strong, readonly) ZFFloatView *smallFloatView;

    /// 播放器小窗是否正在显示
    @property (nonatomic, assign, readonly) BOOL isSmallFloatViewShow;

    ZFPlayerController (ZFPlayerTimeControl)

    /// 0...1.0,调节系统的声音,要是调节播放器声音可以使用播放器管理类设置
    @property (nonatomic) float volume;

    /// 系统静音,要是调节播放器静音可以使用播放器管理类设置
    @property (nonatomic, getter=isMuted) BOOL muted;

    // 0...1.0, 系统屏幕亮度
    @property (nonatomic) float brightness;

    /// 移动网络下自动播放, default is NO.
    @property (nonatomic, getter=isWWANAutoPlay) BOOL WWANAutoPlay;

    /// 当前播放的下标,只适用于设置了`assetURLs`
    @property (nonatomic) NSInteger currentPlayIndex;

    /// 在 `assetURLs`中是否是最后一个
    @property (nonatomic, readonly) BOOL isLastAssetURL;

    /// 在 `assetURLs`中是否是第一个
    @property (nonatomic, readonly) BOOL isFirstAssetURL;

    /// 当退到后台后是否暂停播放,前提是支持后台播放器模式,default is YES.
    @property (nonatomic) BOOL pauseWhenAppResignActive;

    /// 当播放器在玩播放时,它会被一些事件暂停,而不是用户点击暂停。
    /// 例如,应用程序进入后台或者push到另一个视图控制器
    @property (nonatomic, getter=isPauseByEvent) BOOL pauseByEvent;

    /// 当前播放器控制器消失,而不是dealloc
    @property (nonatomic, getter=isViewControllerDisappear) BOOL viewControllerDisappear;

    /// 自定义AVAudioSession, default is NO.
    @property (nonatomic, assign) BOOL customAudioSession;

    /// 当播放器Prepare时候调用
    @property (nonatomic, copy, nullable) void(^playerPrepareToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

    ///当播放器准备开始播放时候调用
    @property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

    /// 当播放进度改变时候调用.
    @property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval currentTime, NSTimeInterval duration);

    /// 当缓冲进度改变时候调用
    @property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval bufferTime);

    /// 当播放状态改变时候调用
    @property (nonatomic, copy, nullable) void(^playerPlayStateChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerPlaybackState playState);

    /// 当加载状态改变时候调用.
    @property (nonatomic, copy, nullable) void(^playerLoadStateChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerLoadState loadState);

    /// 当播放失败时候调用.
    @property (nonatomic, copy, nullable) void(^playerPlayFailed)(id<ZFPlayerMediaPlayback> asset, id error);

    /// 当播放状态完成时候调用.
    @property (nonatomic, copy, nullable) void(^playerDidToEnd)(id<ZFPlayerMediaPlayback> asset);

    // 当播放器view的尺寸改变时候调用.
    @property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id<ZFPlayerMediaPlayback> asset, CGSize size);

    /// 播放下一个,只适用于设置了`assetURLs`
    - (void)playTheNext;

    /// 播放上一个,只适用于设置了`assetURLs`
    - (void)playThePrevious;

    /// 播放某一个,只适用于设置了`assetURLs`
    - (void)playTheIndex:(NSInteger)index;

    /// 停止播放,并且把播放器view和相关通知移除
    - (void)stop;

    /// 切换当前的PlayerManager,适用场景:播放某一个视频时候使用特定的播放器管理类
    - (void)replaceCurrentPlayerManager:(id<ZFPlayerMediaPlayback>)manager;

    /**
    添加播放器view到cell上
    */
    - (void)addPlayerViewToCell;

    /**
    添加播放器view到容器view上.
    */
    - (void)addPlayerViewToContainerView:(UIView *)containerView;

    /**
    添加播放器到主window上.
    */
    - (void)addPlayerViewToKeyWindow;

    /**
    停止当前在view上的播放并移除播放器view.
    */
    - (void)stopCurrentPlayingView;

    /**
    停止当前在cell上的播放并移除播放器view.
    */
    - (void)stopCurrentPlayingCell;

    ZFPlayerController (ZFPlayerOrientationRotation)

    /// 屏幕旋转管理类
    @property (nonatomic, readonly) ZFOrientationObserver *orientationObserver;

    ///是否支持自动屏幕旋转。
    /// iOS8.1~iOS8.3的值为YES,其他iOS版本的值为NO。
    ///这个属性用于UIViewController ' shouldAutorotate '方法的返回值。
    @property (nonatomic, readonly) BOOL shouldAutorotate;

    ///是否允许视频方向旋转。
    ///默认值是YES。
    @property (nonatomic) BOOL allowOrentitaionRotation;

    /// 是否是全屏状态,当ZFFullScreenMode == ZFFullScreenModeLandscape,当currentOrientation是LandscapeLeft或者LandscapeRight,这个值是YES
    /// 当ZFFullScreenMode == ZFFullScreenModePortrait,当视频全屏后,这个值是YES
    @property (nonatomic, readonly) BOOL isFullScreen;

    /// 锁定当前的屏幕方向,目的是禁止设备自动旋转
    @property (nonatomic, getter=isLockedScreen) BOOL lockedScreen;

    /// 隐藏系统的状态栏
    @property (nonatomic, getter=isStatusBarHidden) BOOL statusBarHidden;

    /// 使用设备方向旋转屏幕, default NO.
    @property (nonatomic, assign) BOOL forceDeviceOrientation;

    /// 播放器view当前方向
    @property (nonatomic, readonly) UIInterfaceOrientation currentOrientation;

    /// 当即将全屏时候会调用
    @property (nonatomic, copy, nullable) void(^orientationWillChange)(ZFPlayerController *player, BOOL isFullScreen);

    /// 当已经全屏时候会调用
    @property (nonatomic, copy, nullable) void(^orientationDidChanged)(ZFPlayerController *player, BOOL isFullScreen);

    /// 添加设备方向的监听
    - (void)addDeviceOrientationObserver;

    /// 移除设备方向的监听
    - (void)removeDeviceOrientationObserver;

    /// 当 ZFFullScreenMode == ZFFullScreenModeLandscape使用此API设置全屏切换
    - (void)enterLandscapeFullScreen:(UIInterfaceOrientation)orientation animated:(BOOL)animated;

    /// 当 ZFFullScreenMode == ZFFullScreenModePortrait使用此API设置全屏切换
    - (void)enterPortraitFullScreen:(BOOL)fullScreen animated:(BOOL)animated;

    /// 内部根据ZFFullScreenMode的值来设置全屏切换
    - (void)enterFullScreen:(BOOL)fullScreen animated:(BOOL)animated;

    ZFPlayerController (ZFPlayerViewGesture)

    /// 手势的管理类
    @property (nonatomic, readonly) ZFPlayerGestureControl *gestureControl;

    /// 禁用哪些手势,默认支持单击、双击、滑动、缩放手势
    @property (nonatomic, assign) ZFPlayerDisableGestureTypes disableGestureTypes;

    ///不支持的平移手势移动方向
    @property (nonatomic) ZFPlayerDisablePanMovingDirection disablePanMovingDirection;

    ZFPlayerController (ZFPlayerScrollView)

    /// 初始化时候设置的scrollView
    @property (nonatomic, readonly, nullable) UIScrollView *scrollView;

    /// 只适用于列表播放时候是否自动播放,default is YES.
    @property (nonatomic) BOOL shouldAutoPlay;

    /// 移动网络自动播放,只有当“shouldAutoPlay”为YES时才支持,默认为NO
    @property (nonatomic, getter=isWWANAutoPlay) BOOL WWANAutoPlay;

    /// 当前播放的indexPath
    @property (nonatomic, nullable) NSIndexPath *playingIndexPath;

    /// 初始化时候设置的containerViewTag,根据此tag在cell上找到播放器view显示的位置
    @property (nonatomic) NSInteger containerViewTag;

    /// 滑出屏幕后是否停止播放,如果设置为NO,滑出屏幕后则会小窗播放,defalut is YES.
    @property (nonatomic) BOOL stopWhileNotVisible;

    /**
    当前播放器滚动滑出屏幕的百分比。
    当`stopWhileNotVisible`为YES时使用的属性,停止当前正在播放的播放器。
    当`stopWhileNotVisible`为NO时使用的属性,当前正在播放的播放器添加到小容器视图。
    范围是0.0~1.0,defalut是0.5。
    0.0是player将会消失。
    1.0是player消失了。
    */
    @property (nonatomic) CGFloat playerDisapperaPercent;

    /**
    当前播放器滚动到屏幕百分比来播放视频。
    范围是0.0~1.0,defalut是0.0。
    0.0是玩家将会出现。
    1.0是播放器确实出现了。
    */
    @property (nonatomic) CGFloat playerApperaPercent;

    /// 如果列表播放时候有多个区,使用此API
    @property (nonatomic, copy, nullable) NSArray <NSArray <NSURL *>*>*sectionAssetURLs;

    /**
    播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

    @param indexPath播放url的indexPath。
    */
    - (void)playTheIndexPath:(NSIndexPath *)indexPath;

    /**
    播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

    @param indexPath播放url的indexPath
    @param scrollToTop使用动画将当前单元格滚动到顶部。
    */
    - (void)playTheIndexPath:(NSIndexPath *)indexPath scrollToTop:(BOOL)scrollToTop;

    /**
    播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

    @param indexPath播放url的indexPath
    @param assetURL播放器URL。
    @param scrollToTop使用动画将当前单元格滚动到顶部。
    */
    - (void)playTheIndexPath:(NSIndexPath *)indexPath assetURL:(NSURL *)assetURL scrollToTop:(BOOL)scrollToTop;

    /**
    播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

    @param indexPath播放url的indexPath
    @param scrollToTop使用动画将当前单元格滚动到顶部。
    @param completionHandler滚动完成回调。
    */
    - (void)playTheIndexPath:(NSIndexPath *)indexPath scrollToTop:(BOOL)scrollToTop completionHandler:(void (^ __nullable)(void))completionHandler;

    ZFPlayerMediaPlayback—播放器SDK遵守的协议

    1、枚举类型:

    ///  播放状态:未知、播放中、暂停、失败、停止
    typedef NS_ENUM(NSUInteger, ZFPlayerPlaybackState) {
    ZFPlayerPlayStateUnknown = 0,
    ZFPlayerPlayStatePlaying,
    ZFPlayerPlayStatePaused,
    ZFPlayerPlayStatePlayFailed,
    ZFPlayerPlayStatePlayStopped
    };
    ///  加载状态:未知、就绪、可以播放、自动播放、播放暂停
    typedef NS_OPTIONS(NSUInteger, ZFPlayerLoadState) {
    ZFPlayerLoadStateUnknown = 0,
    ZFPlayerLoadStatePrepare = 1 << 0,
    ZFPlayerLoadStatePlayable = 1 << 1,
    ZFPlayerLoadStatePlaythroughOK = 1 << 2,
    ZFPlayerLoadStateStalled = 1 << 3,
    };
    ///  播放画面拉伸模式:无拉伸、等比例拉伸不裁剪、部分内容裁剪按比例填充、非等比例填满
    typedef NS_ENUM(NSInteger, ZFPlayerScalingMode) {
    ZFPlayerScalingModeNone,
    ZFPlayerScalingModeAspectFit,
    ZFPlayerScalingModeAspectFill,
    ZFPlayerScalingModeFill
    };

    2、协议属性:

    ///  播放器视图继承于ZFPlayerView,处理一些手势冲突
    @property (nonatomic) ZFPlayerView *view;

    /// 0...1.0,播放器音量,不影响设备的音量大小
    @property (nonatomic) float volume;

    /// 播放器是否静音,不影响设备静音
    @property (nonatomic, getter=isMuted) BOOL muted;

    /// 0.5...2,播放速率,正常速率为 1
    @property (nonatomic) float rate;

    /// 当前播放时间
    @property (nonatomic, readonly) NSTimeInterval currentTime;

    /// 播放总时间
    @property (nonatomic, readonly) NSTimeInterval totalTime;

    /// 缓冲时间
    @property (nonatomic, readonly) NSTimeInterval bufferTime;

    /// 视频播放定位时间
    @property (nonatomic) NSTimeInterval seekTime;

    /// 视频是否正在播放中
    @property (nonatomic, readonly) BOOL isPlaying;

    /// 视频播放视图的填充模式,默认不做任何拉伸
    @property (nonatomic) ZFPlayerScalingMode scalingMode;

    /// 检查视频播放是否准备就绪,返回YES,调用play方法直接播放视频;返回NO,调用play方法内部自动调用prepareToPlay方法进行视频播放准备工作
    @property (nonatomic, readonly) BOOL isPreparedToPlay;

    /// 媒体播放资源URL
    @property (nonatomic) NSURL *assetURL;

    /// 视频的尺寸
    @property (nonatomic, readonly) CGSize presentationSize;

    /// 视频播放状态
    @property (nonatomic, readonly) ZFPlayerPlaybackState playState;

    /// 视频的加载状态
    @property (nonatomic, readonly) ZFPlayerLoadState loadState;

    ///------------------------------------
    ///如果没有指定controlView,可以调用以下块。
    ///如果你指定了controlView,下面的代码块不能在外部调用,只能用于“ZFPlayerController”调用。
    ///------------------------------------

    /// 准备播放
    @property (nonatomic, copy, nullable) void(^playerPrepareToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

    /// 开始播放了
    @property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

    /// 播放进度改变
    @property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval currentTime, NSTimeInterval duration);

    /// 视频缓冲进度改变
    @property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval bufferTime);

    /// 视频播放状态改变
    @property (nonatomic, copy, nullable) void(^playerPlayStatChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerPlaybackState playState);

    /// 视频加载状态改变
    @property (nonatomic, copy, nullable) void(^playerLoadStatChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerLoadState loadState);

    /// 视频播放已经结束
    @property (nonatomic, copy, nullable) void(^playerDidToEnd)(id<ZFPlayerMediaPlayback> asset);

    // 视频的尺寸改变了
    @property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id<ZFPlayerMediaPlayback> asset, CGSize size);

    ///------------------------------------
    /// end
    ///------------------------------------

    3、协议方法:

    ///  视频播放准备,中断除non-mixible之外的任何音频会话
    - (void)prepareToPlay;

    /// 重新进行视频播放准备
    - (void)reloadPlayer;

    /// 视频播放
    - (void)play;

    /// 视频暂停
    - (void)pause;

    /// 视频重新播放
    - (void)replay;

    /// 视频播放停止
    - (void)stop;

    /// 视频播放当前时间的画面截图
    - (UIImage *)thumbnailImageAtCurrentTime;

    /// 替换当前媒体资源地址
    - (void)replaceCurrentAssetURL:(NSURL *)assetURL;

    /// 调节播放进度
    - (void)seekToTime:(NSTimeInterval)time completionHandler:(void (^ __nullable)(BOOL finished))completionHandler;

    ZFPlayerMediaControl—控制层遵守的协议

    1、视频状态相关

    ///  视频播放准备就绪
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer prepareToPlay:(NSURL *)assetURL;

    /// 视频播放状态改变
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer playStateChanged:(ZFPlayerPlaybackState)state;

    /// 视频加载状态改变
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer loadStateChanged:(ZFPlayerLoadState)state;

    2、播放进度

    ///  视频播放时间进度
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer
    currentTime:(NSTimeInterval)currentTime
    totalTime:(NSTimeInterval)totalTime;

    /// 视频缓冲进度
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer
    bufferTime:(NSTimeInterval)bufferTime;

    /// 视频定位播放时间
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer
    draggingTime:(NSTimeInterval)seekTime
    totalTime:(NSTimeInterval)totalTime;

    /// 视频播放结束
    - (void)videoPlayerPlayEnd:(ZFPlayerController *)videoPlayer;

    3、锁屏

    /// 设置播放器锁屏时的协议方法
    - (void)lockedVideoPlayer:(ZFPlayerController *)videoPlayer lockedScreen:(BOOL)locked;

    4、屏幕旋转

    ///  播放器全屏模式即将改变
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationWillChange:(ZFOrientationObserver *)observer;

    /// 播放器全屏模式已经改变
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationDidChanged:(ZFOrientationObserver *)observer;

    /// 当前网络状态发生变化
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer reachabilityChanged:(ZFReachabilityStatus)status;

    5、手势方法

    ///  相关手势设置
    - (BOOL)gestureTriggerCondition:(ZFPlayerGestureControl *)gestureControl
    gestureType:(ZFPlayerGestureType)gestureType
    gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
    touch:(UITouch *)touch;

    /// 单击
    - (void)gestureSingleTapped:(ZFPlayerGestureControl *)gestureControl;

    /// 双击
    - (void)gestureDoubleTapped:(ZFPlayerGestureControl *)gestureControl;

    /// 开始拖拽
    - (void)gestureBeganPan:(ZFPlayerGestureControl *)gestureControl
    panDirection:(ZFPanDirection)direction
    panLocation:(ZFPanLocation)location;

    /// 拖拽中
    - (void)gestureChangedPan:(ZFPlayerGestureControl *)gestureControl
    panDirection:(ZFPanDirection)direction
    panLocation:(ZFPanLocation)location
    withVelocity:(CGPoint)velocity;

    /// 拖拽结束
    - (void)gestureEndedPan:(ZFPlayerGestureControl *)gestureControl
    panDirection:(ZFPanDirection)direction
    panLocation:(ZFPanLocation)location;

    /// 捏合手势变化
    - (void)gesturePinched:(ZFPlayerGestureControl *)gestureControl
    scale:(float)scale;

    6、scrollView上的播放器视图方法

    /**
    scrollView中的播放器视图将要出现的回调
    */
    - (void)playerWillAppearInScrollView:(ZFPlayerController *)videoPlayer;

    /**
    scrollView中的播放器视图已经出现的回调
    */
    - (void)playerDidAppearInScrollView:(ZFPlayerController *)videoPlayer;

    /**
    scrollView中的播放器视图即将消失的回调
    */
    - (void)playerWillDisappearInScrollView:(ZFPlayerController *)videoPlayer;

    /**
    scrollView中的播放器视图已经消失的回调
    */
    - (void)playerDidDisappearInScrollView:(ZFPlayerController *)videoPlayer;

    /**
    scrollView中的播放器视图正在显示的回调
    */
    - (void)playerAppearingInScrollView:(ZFPlayerController *)videoPlayer playerApperaPercent:(CGFloat)playerApperaPercent;

    /**
    scrollView中的播放器视图正在消失的回调
    */
    - (void)playerDisappearingInScrollView:(ZFPlayerController *)videoPlayer playerDisapperaPercent:(CGFloat)playerDisapperaPercent;

    /**
    小窗视图显示隐藏的回调
    */
    - (void)videoPlayer:(ZFPlayerController *)videoPlayer floatViewShow:(BOOL)show;

    代码传送门:https://github.com/renzifeng/ZFPlayer

    转自:https://www.jianshu.com/p/90e55deb4d51

    收起阅读 »

    iOS 优秀框架之TYAttributedLabel(基于coreText的图文混排)

    TYAttributedLabel1、TYAttributedLabel 简单,强大的属性文本控件(无需了解CoreText)2、支持富文本,图文混排显示,支持行间距,字间距,自适应高度,指定行数3、支持添加高度自定义文本属性4、支持添加属性文本,自定义链接,...
    继续阅读 »

    TYAttributedLabel

    1、TYAttributedLabel 简单,强大的属性文本控件(无需了解CoreText)
    2、支持富文本,图文混排显示,支持行间距,字间距,自适应高度,指定行数
    3、支持添加高度自定义文本属性
    4、支持添加属性文本,自定义链接,新增高亮效果显示(文字和背景)
    5、支持添加UIImage和UIView控件

    demo演示


    重点类简介

    TYAttributedLabel

    创建label(可接受文本及富文本)
    设置字体间距
    设置行间距
    设置字体大小
    设置view的位置和宽,会自动计算高度
    设置链接文本,并用代理(TYAttributedLabelDelegate)方法完成点击后需完成的任务

    TYImageStorage

    可创建一个append在TYAttributedLabel后的图片控件,可自定义图片大小,及对齐样式

    TYTextStorage

    文本文件,可设置文本大小及字体颜色

    TYTextContainer

    属性文本生成器(使用 RegexKitLite)
    具体代码及使用细节请看作者的demo(作者是华人),讲的很详细,这里就不再赘述

    链接:TYAttributedLabel

    链接:https://www.jianshu.com/p/5d81bf7e79c8

    收起阅读 »

    iOS 使用Moya网络请求

    Moya最新版本11.0.2由于前段时间写了这篇文章,最新Moya已更新最新版本,故此也更新了下用法,本人已使用,故特意奉上最新的使用demo供参考。Moya11.0.2DemoMoya简介Moya 是你的 app 中缺失的网络层。不用再去想在哪儿(或者如何)...
    继续阅读 »

    Moya最新版本11.0.2

    由于前段时间写了这篇文章,最新Moya已更新最新版本,故此也更新了下用法,本人已使用,故特意奉上最新的使用demo供参考。
    Moya11.0.2Demo

    Moya简介

    Moya 是你的 app 中缺失的网络层。不用再去想在哪儿(或者如何)安放网络请求,Moya 替你管理。

    Moya有几个比较好的特性:

    1、编译时检查正确的API端点访问.

    2、使你定义不同端点枚举值对应相应的用途更加明晰.

    3、提高测试地位从而使单元测试更加容易.

    Swift我们用Alamofire来做网络库.而Moya在Alamofire的基础上又封装了一层,如下流程图说明Moya的简单工作流程图:


    ** Moya**的官方下载地址点我强大的Moya,有具体的使用方法在demo里面有说明。

    本文主要介绍一下Moya的用法

    1、设置请求头部信息
    2、设置超时时间
    3、自定义插件
    4、自签名证书
    注意:以下所出现的NetAPIManager跟官网上demo的** GitHub**是一样类型的文件,都是这个enum实现一个协议TargetType,点进去可以看到TargetType定义了我们发送一个网络请求所需要的东西,什么baseURL,parameter,method等一些计算性属性,我们要做的就是去实现这些东西,当然有带默认值的我们可以不去实现,但是设置头部信息跟超时时间就要修改这些系统默认设置了。

    为了看得更加清楚,贴上NetAPIManager文件的内容

    //
    // NetAPIManager.swift
    // NN110
    //
    // Created by 陈亦海 on 2017/5/12.
    // Copyright © 2017年 陈亦海. All rights reserved.
    //

    import Foundation
    import Moya


    enum NetAPIManager {
    case Show
    case upload(bodyData: Data)
    case download
    case request(isTouch: Bool, body: Dictionary<String, Any>? ,isShow: Bool)
    }


    extension NetAPIManager: TargetType {
    var baseURL: URL {//服务器地址

    switch self {
    case .request( _, _, _):
    return URL(string: "https://www.pmphmall.com")!
    default:
    return URL(string: "https://httpbin.org")!
    }


    }

    var path: String {//具体某个方法的路径
    switch self {
    case .Show:
    return ""
    case .upload(_):
    return ""
    case .request(_, _, _):
    return "/app/json.do"
    case .download:
    return ""
    }
    }

    var method: Moya.Method {//请求的方法 get或者post之类的
    switch self {
    case .Show:
    return .get
    case .request(_, _, _):
    return .post
    default:
    return .post
    }
    }

    var parameters: [String: Any]? {//请求的get post给服务器的参数
    switch self {
    case .Show:
    return nil
    case .request(_, _, _):
    return ["msg":"H4sIAAAAAAAAA11SSZJFIQi7EqPAEgTvf6TP62W7sMoSQhKSWDrs6ZUKVWogLwYV7RjHFBZJlNlzloN6LVqID4a+puxqRdUKVNLwE1TRcZIC/fjF2rPotuXmb84r1gMXbiASZIZbhQdKEewJlz41znDkujCHuQU3dU7G4/PmVRnwArMLXukBv0J23XVahNO3VX35wlgce6TLUzzgPQJFuHngAczl6VhaNXpmRLxJBlMml6gdLWiXxTdO7I+iEyC7XuTirCQXOk4dotgArgkH/InxVjfNTnE/uY46++hyAiLFuFL4cv1Z8WH5DgB2GnvFXMh5gm53Tr13vqqrEYtcdXfkNsMwKB+9sAQ77grNJmquFWOhfXA/DELlMB0KKFtHOc/ronj1ml+Z7qas82L3VWiCVQ+HEitjTVzoFw8RisFN/jJxBY4awvq427McXqnyrfCsl7oeEU6wYgW9yJtj1lOkx0ELL5Fw4z071NaVzRA9ebxWXkFyothgbB445cpRmTC+//F73r1kOyQ3lTpec12XNDR00nnq5/YmJItW3+w1z27lSOLqgVctrxG4xdL9WVPdkH1tkiZ/pUKBGhADAAA="]
    default:
    return nil

    }
    }

    var sampleData: Data { //编码转义
    return "{}".data(using: String.Encoding.utf8)!
    }

    var task: Task { //一个请求任务事件

    switch self {


    case let .upload(data):
    return .upload(.multipart([MultipartFormData(provider: .data(data), name: "file", fileName: "gif.gif", mimeType: "image/gif")]))

    default:
    return .request

    }

    }

    var parameterEncoding: ParameterEncoding {//编码的格式
    switch self {
    case .request(_, _, _):
    return URLEncoding.default
    default:
    return URLEncoding.default
    }

    }
    //以下两个参数是我自己写,用来控制网络加载的时候是否允许操作,跟是否要显示加载提示,这两个参数在自定义插件的时候会用到
    var touch: Bool { //是否可以操作

    switch self {
    case .request(let isTouch, _, _):
    return isTouch
    default:
    return false
    }

    }

    var show: Bool { //是否显示转圈提示

    switch self {
    case .request( _, _,let isShow):
    return isShow
    default:
    return false
    }

    }


    }

    如何设置Moya请求头部信息

    头部信息的设置在开发过程中很重要,如服务器生成的token,用户唯一标识等
    我们直接上代码,不说那么多理论的东西,哈哈

    // MARK: - 设置请求头部信息
    let myEndpointClosure = { (target: NetAPIManager) -> Endpoint<NetAPIManager> in


    let url = target.baseURL.appendingPathComponent(target.path).absoluteString
    let endpoint = Endpoint<NetAPIManager>(
    url: url,
    sampleResponseClosure: { .networkResponse(200, target.sampleData) },
    method: target.method,
    parameters: target.parameters,
    parameterEncoding: target.parameterEncoding
    )

    //在这里设置你的HTTP头部信息
    return endpoint.adding(newHTTPHeaderFields: [
    "Content-Type" : "application/x-www-form-urlencoded",
    "ECP-COOKIE" : ""
    ])

    }

    如何设置请求超时时间

    // MARK: - 设置请求超时时间
    let requestClosure = { (endpoint: Endpoint<NetAPIManager>, done: @escaping MoyaProvider<NetAPIManager>.RequestResultClosure) in

    guard var request = endpoint.urlRequest else { return }

    request.timeoutInterval = 30 //设置请求超时时间
    done(.success(request))
    }

    自定义插件

    自定义插件必须PluginType协议的两个方法willSend与didReceive

    //
    // MyNetworkActivityPlugin.swift
    // NN110
    //
    // Created by 陈亦海 on 2017/5/10.
    // Copyright © 2017年 CocoaPods. All rights reserved.
    //

    import Foundation
    import Result
    import Moya


    /// Network activity change notification type.
    public enum MyNetworkActivityChangeType {
    case began, ended
    }

    /// Notify a request's network activity changes (request begins or ends).
    public final class MyNetworkActivityPlugin: PluginType {



    public typealias MyNetworkActivityClosure = (_ change: MyNetworkActivityChangeType, _ target: TargetType) -> Void
    let myNetworkActivityClosure: MyNetworkActivityClosure

    public init(newNetworkActivityClosure: @escaping MyNetworkActivityClosure) {
    self.myNetworkActivityClosure = newNetworkActivityClosure
    }

    // MARK: Plugin

    /// Called by the provider as soon as the request is about to start
    public func willSend(_ request: RequestType, target: TargetType) {
    myNetworkActivityClosure(.began,target)
    }

    /// Called by the provider as soon as a response arrives, even if the request is cancelled.
    public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
    myNetworkActivityClosure(.ended,target)
    }
    }

    使用自定义插件方法

    // MARK: - 自定义的网络提示请求插件
    let myNetworkPlugin = MyNetworkActivityPlugin { (state,target) in
    if state == .began {
    // SwiftSpinner.show("Connecting...")

    let api = target as! NetAPIManager
    if api.show {
    print("我可以在这里写加载提示")
    }

    if !api.touch {
    print("我可以在这里写禁止用户操作,等待请求结束")
    }

    print("我开始请求\(api.touch)")

    UIApplication.shared.isNetworkActivityIndicatorVisible = true
    } else {
    // SwiftSpinner.show("request finish...")
    // SwiftSpinner.hide()
    print("我结束请求")
    UIApplication.shared.isNetworkActivityIndicatorVisible = false

    }
    }

    自签名证书

    在16年的WWDC中,Apple已表示将从2017年1月1日起,所有新提交的App必须强制性应用HTTPS协议来进行网络请求。默认情况下非HTTPS的网络访问是禁止的并且不能再通过简单粗暴的向Info.plist中添加NSAllowsArbitraryLoads
    设置绕过ATS(App Transport Security)的限制(否则须在应用审核时进行说明并很可能会被拒)。所以还未进行相应配置的公司需要尽快将升级为HTTPS的事项提上进程了。本文将简述HTTPS及配置数字证书的原理并以配置实例和出现的问题进行说明,希望能对你提供帮助。(比心~)

    HTTPS:
    简单来说,HTTPS就是HTTP协议上再加一层加密处理的SSL协议,即HTTP安全版。相比HTTP,HTTPS可以保证内容在传输过程中不会被第三方查看、及时发现被第三方篡改的传输内容、防止身份冒充,从而更有效的保证网络数据的安全。
    HTTPS客户端与服务器交互过程:
    1、 客户端第一次请求时,服务器会返回一个包含公钥的数字证书给客户端;
    2、 客户端生成对称加密密钥并用其得到的公钥对其加密后返回给服务器;
    3、 服务器使用自己私钥对收到的加密数据解密,得到对称加密密钥并保存;
    4、 然后双方通过对称加密的数据进行传输。


    数字证书:
    在HTTPS客户端与服务器第一次交互时,服务端返回给客户端的数字证书是让客户端验证这个数字证书是不是服务端的,证书所有者是不是该服务器,确保数据由正确的服务端发来,没有被第三方篡改。数字证书可以保证数字证书里的公钥确实是这个证书的所有者(Subject)的,或者证书可以用来确认对方身份。证书由公钥、证书主题(Subject)、数字签名(digital signature)等内容组成。其中数字签名就是证书的防伪标签,目前使用最广泛的SHA-RSA加密。
    证书一般分为两种:

    1、一种是向权威认证机构购买的证书,服务端使用该种证书时,因为苹果系统内置了其受信任的签名根证书,所以客户端不需额外的配置。为了证书安全,在证书发布机构公布证书时,证书的指纹算法都会加密后再和证书放到一起公布以防止他人伪造数字证书。而证书机构使用自己的私钥对其指纹算法加密,可以用内置在操作系统里的机构签名根证书来解密,以此保证证书的安全。
    2、另一种是自己制作的证书,即自签名证书。好处是不需要花钱购2买,但使用这种证书是不会受信任的,所以需要我们在代码中将该证书配置为信任证书.

    自签名证书具体实现:

    我们在使用自签名证书来实现HTTPS请求时,因为不像机构颁发的证书一样其签名根证书在系统中已经内置了,所以我们需要在App中内置自己服务器的签名根证书来验证数字证书。首先将服务端生成的.cer格式的根证书添加到项目中,注意在添加证书要一定要记得勾选要添加的targets。这里有个地方要注意:苹果的ATS要求服务端必须支持TLS 1.2或以上版本;必须使用支持前向保密的密码;证书必须使用SHA-256或者更好的签名hash算法来签名,如果证书无效,则会导致连接失败。由于我在生成的根证书时签名hash算法低于其要求,在配置完请求时一直报NSURLErrorServerCertificateUntrusted = -1202错误,希望大家可以注意到这一点。

    那么如何在Moya中使用自签名的证书来实现HTTPS网络请求呢,请期待下回我专门分享......需要自定义一个Manager管理

    综合使用的方法如下

    定义一个公用的Moya请求服务对象

    let MyAPIProvider = MoyaProvider<NetAPIManager>(endpointClosure: myEndpointClosure,requestClosure: requestClosure, plugins: [NetworkLoggerPlugin(verbose: true, responseDataFormatter: JSONResponseDataFormatter),myNetworkPlugin])

    // MARK: -创建一个Moya请求
    func sendRequest(_ postDict: Dictionary<String, Any>? = nil,
    success:@escaping (Dictionary<String, Any>)->(),
    failure:@escaping (MoyaError)->()) -> Cancellable? {

    let request = MyAPIProvider.request(.Show) { result in
    switch result {
    case let .success(moyaResponse):


    do {
    let any = try moyaResponse.mapJSON()
    let data = moyaResponse.data
    let statusCode = moyaResponse.statusCode
    MyLog("\(data) --- \(statusCode) ----- \(any)")

    success(["":""])


    } catch {

    }



    case let .failure(error):

    print(error)
    failure(error)
    }
    }

    return request
    }

    取消所有的Moya请求

    // MARK: -取消所有请求
    func cancelAllRequest() {
    // MyAPIProvider.manager.session.invalidateAndCancel() //取消所有请求
    MyAPIProvider.manager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
    dataTasks.forEach { $0.cancel() }
    uploadTasks.forEach { $0.cancel() }
    downloadTasks.forEach { $0.cancel() }
    }

    //let sessionManager = Alamofire.SessionManager.default
    //sessionManager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
    // dataTasks.forEach { $0.cancel() }
    // uploadTasks.forEach { $0.cancel() }
    // downloadTasks.forEach { $0.cancel() }
    //}

    }

    转自:https://www.jianshu.com/p/38fbc22a1e2b

    收起阅读 »

    日志管理工具 - CocoaLumberjack

    CocoaLumberjackCocoaLumberjack是适用于 macOS、iOS、tvOS 和 watchOS 的快速简单但功能强大且灵活的日志记录框架。首先,通过CocoaPods、Carthage、Swift Package Manager或手动安...
    继续阅读 »

    CocoaLumberjack

    CocoaLumberjack是适用于 macOS、iOS、tvOS 和 watchOS 的快速简单但功能强大且灵活的日志记录框架。

    首先,通过CocoaPodsCarthageSwift Package Manager或手动安装 CocoaLumberjack 然后使用DDOSLoggeriOS 10 及更高版本,或DDTTYLoggerDDASLLogger早期版本开始记录消息。

    CocoaPods

    platform :ios, '9.0'

    target 'SampleTarget' do
    use_frameworks!
    pod 'CocoaLumberjack/Swift'
    end


    platform :ios, '9.0'

    target 'SampleTarget' do
    pod 'CocoaLumberjack'
    end

    Carthage

    github "CocoaLumberjack/CocoaLumberjack"

    用法

    如果您使用 Lumberjack 作为框架,则可以@import CocoaLumberjack;除此以外,#import
    [DDLog addLogger: [DDOSLogger sharedInstance ]]; //使用 os_log

    DDFileLogger *fileLogger = [[DDFileLogger
    alloc ] init ]; //文件记录器
    fileLogger.rollingFrequency =
    60 * 60 * 24 ; // 24 小时滚动
    fileLogger.logFileManager.maximumNumberOfLogFiles =
    7 ;
    [DDLog
    addLogger: fileLogger];

    ...


    DDLogVerbose ( @"详细" );
    DDLogDebug ( @"调试" );
    DDLogInfo ( @"信息" );
    DDLogWarn ( @"警告" );
    DDLogError ( @"错误" );

    Objective-C ARC 语义问题

    将 Lumberjack 集成到现有的 Objective-C 中时,可能会遇到Multiple methods named 'tag' found with mismatched result, parameter type or attributes构建错误。

    #define DD_LEGACY_MESSAGE_TAG 0在导入 CocoaLumberjack 之前添加或添加#define DD_LEGACY_MESSAGE_TAG 0或添加-DDD_LEGACY_MESSAGE_TAG=0Xcode 项目中的其他 C 标志OTHER_CFLAGS

    快速日志后端

    CocoaLumberjack 还附带了swift-log的后端实现只需将 CocoaLumberjack 作为依赖项添加到您的 SPM 目标(见上文),并将CocoaLumberjackSwiftLogBackend产品作为依赖项添加到您的目标。

    然后,您可以将DDLogHandler其用作 swift-log 的后端,它将所有消息转发到 CocoaLumberjack 的DDLog您仍将通过 配置您想要的记录器和日志格式化程序DDLog,但将使用Loggerswift-log完成写入日志消息

    在您自己的日志格式化程序中,您可以使用swiftLogInfoon 属性DDLogMessage来检索通过 swift-log 记录的消息的详细信息。


    在大多数情况下,它比 NSLog 快一个数量级。

    当您的应用程序启动时,只需一行代码即可配置 lumberjack。然后只需将您的 NSLog 语句替换为 DDLog 语句即可。(而且 DDLog 宏的格式和语法与 NSLog 完全相同,因此非常简单。)

    一个日志语句可以发送到多个记录器,这意味着您可以同时记录到一个文件和控制台。想要更多?创建您自己的记录器(这很容易)并通过网络发送您的日志语句。或者到数据库或分布式文件系统。天空才是极限。

    根据需要配置您的日志记录。更改每个文件的日志级别(非常适合调试)。更改每个记录器的日志级别(详细控制台,但简洁的日志文件)。更改每个 xcode 配置的日志级别(详细调试,但简洁发布)。将您的日志语句从发布版本中编译出来。为您的应用程序自定义日志级别的数量。添加您自己的细粒度日志记录。在运行时动态更改日志级别。选择您希望日志文件滚动的方式和时间。将您的日志文件上传到中央服务器。压缩归档日志文件以节省磁盘空间...


    在以下情况下,此框架适合您:

    • 您正在寻找一种方法来追踪该领域不断出现的无法重现的错误。
    • 您对 iPhone 上超短的控制台日志感到沮丧。
    • 您希望将您的应用程序在支持和稳定性方面提升到一个新的水平。
    • 您正在为您的应用程序(Mac 或 iPhone)寻找企业级日志记录解决方案。

    要求

    当前版本的 Lumberjack 要求:

    • Xcode 12 或更高版本
    • Swift 5.3 或更高版本
    • iOS 9 或更高版本
    • macOS 10.10 或更高版本
    • watchOS 3 或更高版本
    • tvOS 9 或更高版本



    收起阅读 »

    视图添加闪烁效果的简单方法 - Shimmer

    ShimmerShimmer 是一种向应用程序中的任何视图添加闪烁效果的简单方法。它作为一个不显眼的加载指示器很有用。Shimmer 最初是为了在Paper 中显示加载状态而开发的。用法要使用 Shimmer,请创建一个FBShimmeringView或FBS...
    继续阅读 »

    Shimmer

    Shimmer 是一种向应用程序中的任何视图添加闪烁效果的简单方法。它作为一个不显眼的加载指示器很有用。

    Shimmer 最初是为了在Paper 中显示加载状态而开发的

    用法

    要使用 Shimmer,请创建一个FBShimmeringViewFBShimmeringLayer并添加您的内容。要开始闪烁,请将shimmering属性设置YES

    使标签闪烁的示例:

    FBShimmeringView *shimmeringView = [[FBShimmeringView alloc ] initWithFrame: self .view.bounds];
    [
    self .view addSubview: shimmeringView];

    UILabel *loadingLabel = [[UILabel
    alloc ] initWithFrame: shimmeringView.bounds];
    loadingLabel.textAlignment = NSTextAlignmentCenter;

    loadingLabel.text = NSLocalizedString(
    @" Shimmer " , nil );
    shimmeringView.contentView = loadingLabel;


    //开始闪烁。
    shimmeringView.shimmering =
    YES ;

    还有一个示例项目。在示例中,您可以水平和垂直滑动以尝试各种闪烁参数,或点击以开始或停止闪烁。(要在本地构建示例,您需要打开FBShimmering.xcworkpace而不是.xcodeproj.)

    安装

    有两种选择:

    1. 微光ShimmerCocoapods 中可用
    2. 手动将文件添加到您的 Xcode 项目中。稍微简单一点,但更新也是手动的。

    Shimmer 需要 iOS 6 或更高版本。


    这个怎么运作

    Shimmer 使用该-[CALayer mask]属性来启用闪烁,类似于 John Harper 2009 年 WWDC 演讲中所描述的内容(不幸的是不再在线)。Shimmer 使用 CoreAnimation 的计时功能在启动和停止微光时平滑过渡“节拍”。


    demo及常见问题:https://github.com/facebookarchive/Shimmer

    源码下载:Shimmer-master.zip



    收起阅读 »

    iOS应用程序瘦身的静态库解决方案

    为什么要给程序瘦身?随着应用程序的功能越来越多,实现越来越复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增长,从而导致应用程序包的体积越来越大。当程序体积变大后不仅会出现编译流程变慢,而且还会出现运行性能问题,会增加应用下载时长和消耗用户的...
    继续阅读 »

    为什么要给程序瘦身?

    随着应用程序的功能越来越多,实现越来越复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增长,从而导致应用程序包的体积越来越大。当程序体积变大后不仅会出现编译流程变慢,而且还会出现运行性能问题,会增加应用下载时长和消耗用户的移动网络流量等等。因此在这些众多的问题下需要对应用进行瘦身处理。

    一个应用程序由众多资源文件和可执行程序文件组成,资源文件的优化不在本文探讨范围。本文主要讨论对可执行程序代码瘦身的方法。

    对可执行程序代码瘦身主要就是想办法让程序中不会被调用的源代码不参与编译或链接。我们可以通过一些源代码分析工具来查找哪些函数或者类方法没有被调用并从代码中删除掉来解决编译链接前的瘦身问题。这些分析工具也不在本文的讨论范围内。应用程序在编译时会对工程中的所有代码都执行编译处理并生成目标文件。而在链接阶段则会根据程序代码中对符号的引用关系来将所有相关的目标文件链接为一个大的可执行程序文件,并且在链接阶段链接器会优化掉所有没被调用的C/C++函数代码,但是对于OC类中的没有调用的方法则不会被优化掉。所以为了对可执行程序在编译链接阶段进行瘦身处理就需要了解源代码的编译链接规则。这也是本文所要介绍的针对工程通过静态库的形式进行编译和链接的方式来减少可执行程序代码的尺寸。您可以从文章:《深入iOS系统底层之静态库介绍》中详细的了解到静态库的编译链接过程,以及相关的技术细节。

    一个瘦身的例子!

    为了验证和具体的实践,我在github上建立了一个项目:YSAppSizeTest。您可以从这个项目中看到如何对工程进行构建以实现程序的瘦身处理。

    在示例项目中同一个Workspace中分别建立ThinApp和FatApp两个工程,这两个工程实现的功能是一样。在整个应用程序中分别定义了CA、CB、CC、CD、CE一共5个OC类,定义了一个UIView(Test)分类,还有定义了两个C函数:libFoo1和libFoo1。

    整个应用程序中只使用了CA和CC两个OC类,以及调用了UIView(Test)分类方法,以及调用了libFoo1函数,并且同时都采用导入静态库的形式。因为这两个工程对文件的定义和分布策略不同使得两个应用程序的最终可执行代码的尺寸是不相同的。

    FatApp中的文件定义和分布策略

    1、FatApp工程依赖并导入了FatAppLib静态库工程。
    2、CA,CB两个类都定义在主程序工程中。
    3、CC,CD,CE三个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在FatAppLib静态库工程中。
    4、CC,CD两个类定义在同一个文件中,CE类则定义在单独的文件中。
    5、FatApp工程的Other Linker Flags中设置了 -ObjC选项。

    ThinApp中的文件定义和分布策略

    1、ThinApp工程依赖并导入了ThinAppLib静态库工程。
    2、主程序工程就是一个壳工程。
    3、CA,CB,CC,CD,CE5个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在ThinAppLib静态库工程中。
    4、上述的5个类都分别定义在不同的文件中。
    5、ThinApp工程的Other Linker Flags中没有设置-ObjC选项。

    上述两个工程的程序被Archive出来后,FatApp可执行程序的尺寸是367KB,而ThinApp可执行程序的尺寸是334KB。通过一些工具比如Mach-O View或者 IDA可以看出:FatApp中5个OC类的代码以及libFoo1函数还有UIView(Test)分类的代码都被链接进可执行程序中;而ThinApp中则只有CA,CC两个类以及libFoo1函数还有UIView(Test)分类的代码被链接进可执行程序中。在ThinApp中虽然没有使用-Objc链接选项,但是静态库中的分类也被链接进可执行程序中。

    应用程序工程构建规则

    根据对项目中的文件定义和引用策略以及相关的理论基础我们可以按照如下的规则来构建您的应用程序:

    1、尽量将所有代码都移植到静态库中,而主程序则保留为一个壳程序。具体操作方法是建立一个Workspace,然后主程序工程就只有默认创建工程时的代码,所有新加入的代码都建立并存放到静态库工程中去,然后通过工程依赖来引入这些静态库工程,或者借助一些工程化工具比如Cocoapods来实现这种拆分和引用处理。主程序工程中只保留AppDelegate的代码,其他代码都一致到静态库中。然后在AppDelegate中的相关代码处调用静态库中定义的业务代码。

    2、按业务组件对工程进行解耦每个组件是一个静态库工程。静态库中的每一个文件中最好只有一个类的实现,并且类的分类实现最好和类实现编写在同一个文件中,相同功能的代码以及可能都会被调用的代码尽量存放在一个文件中。

    3、不要在主程序工程中使用-ObjC和-all_load两个选项而改为用-force_load 来单独指定要执行加载的静态库。-ObjC和-all_load选项会把主程序工程以及所依赖的所有静态库中的工程中的全部代码都链接到可执行程序中而不管代码是否有被调用过或者使用过。而force_load则只会将指定的静态库中的所有代码链接到可执行程序中,当然force_load如果没有必要也尽量不要使用。

    4、尽量减少在静态库中定义OC类的分类方法,如果一定要定义分类方法则可以将分类方法定义在和类定义相同的文件中,或者将分类方法定义在一个一定会被调用和引用的实现文件中。因为根据链接规则静态库中的分类是不会被链接进可执行程序中的,除非使用了上述的三个链接选项。如果将分类代码单独的定义在一个文件中的话则可以通过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把整个分类的实现链接到可执行程序中去。一般情况下我们在静态库中建立分类那就表明一定会被某个文件引用这个分类,从而实现整个文件的链接处理。在分类中定义的这两个函数则因为没有被任何地方调用,因此会在链接优化中将这两个函数给优化掉。这样就使得即使我们不用-ObjC选项也能将静态库中的分类链接到可执行程序中去。最后需要注意的是在每个分类中定义的这两个函数名最好能够唯一这样就不会出现符号重名冲突的问题了。

    //分类文件的头文件UIView+XXX.h
    @interface UIView (XXX)

    //分类中定义的方法

    @end

    /*
    通过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把
    整个分类的实现链接到可执行程序中去。一般情况下我们在静态库中建立分类那就表明一定会被某个文件引用这个分类,从而实现整个文件的链接处理。
    而在分类中定义的这两个函数则因为没有被任何地方调用,因此会在链接优化中将这两个函数给优化掉。这样就使得即使我们不用-ObjC选项也能
    将静态库中的分类链接到可执行程序中去。最后需要注意的是在每个分类中定义的这两个函数名最好能够唯一这样就不会出现符号重名冲突的问题了。
    */
    extern void _cat_UIView_XXX_Impl(void);
    inline void _cat_UIView_XXX_Decl(void){_cat_UIView_XXX_Impl();}


    ------------------------------------------------------------
    //分类文件的实现文件UIView+XXX.m
    #import "UIView+XXX.h"

    @implementation UIView (XXX)

    //分类的实现代码

    @end

    void _cat_UIView_XXX_Impl(void){}


    ---------------------------------------------------------------
    //最后把这个分类头文件放入到某个对外暴露的头文件中,比如本例中将分类代码放入到了ThinAppLib.h文件中
    //ThinAppLib.h

    #import "UIView+XXX.h"
    //其他头文件

    5、除了可以通过-force_load来加载指定静态库中的所有代码外。我们还可以在构建静态库时,在静态库的工程的Build Settings中将Perform Single-Object Prelink 中的开关选项打开。当这个开关打开时,系统会对生成的静态库的所有目标文件执行预链接操作,预链接操作会将所有的目标文件组合成为一个单独的大的目标文件。这样根据以文件为单位的链接规则就会将静态库中的所有代码全部都链接进可执行程序中去,但是这样带来的问题就是最后在dead code stripping时删除不掉已经链接进来的那些没有被任何地方使用过的OC类了。

    6、对于引入的一些第三方静态库或者第三方的开源库来说因为我们无法去改变其实现逻辑。如果这个静态库中没有任何分类代码的定义则正常引用即可,如果静态库中有分类方法的定义则单独对这个静态库采用-force_load选项。

    总之一句话:为了让你的程序瘦身,尽量将代码放到静态库中,不要使用-Objc和-all_load选项

    为了验证上述方法的有效性,笔者对项目中的应用做了一个测试:分别是有带-ObjC选项和没有带-ObjC选项的情况下的应用程序包中可执行程序的大小从115M减少到95M,减少了20M的尺寸。

    链接:https://www.jianshu.com/p/2078e00891fd

    收起阅读 »

    一个围绕 CFNetwork API的网络通讯库,断点续传神器!

    ASIHTTPRequest 是一个围绕 CFNetwork API的易于使用的包装器,它使与 Web 服务器通信的一些更乏味的方面变得更容易。它是用 Objective-C 编写的,适用于 Mac OS X 和 iPhone 应用程序。它适用于执行...
    继续阅读 »

    ASIHTTPRequest 是一个围绕 CFNetwork API的易于使用的包装器,它使与 Web 服务器通信的一些更乏味的方面变得更容易。它是用 Objective-C 编写的,适用于 Mac OS X 和 iPhone 应用程序。

    它适用于执行基本的HTTP请求并与基于REST的服务(GET / POST / PUT / DELETE交互包含的 ASIFormDataRequest 子类使得使用 multipart/form-data提交POST数据和文件变得容易

    它提供:

    • 一个简单的界面,用于向网络服务器提交数据和从网络服务器获取数据
    • 将数据下载到内存或直接下载到磁盘上的文件
    • 在本地驱动器上提交文件作为POST数据的一部分,与HTML文件输入机制兼容
    • 将请求正文直接从磁盘传输到服务器,以节省内存
    • 恢复部分下载
    • 轻松访问请求和响应HTTP标头
    • 进度委托(NSProgressIndicators 和 UIProgressViews)显示有关下载上传进度的信息
    • 操作队列上传下载进度指示器的自动魔法管理
    • 基本、摘要 + NTLM身份验证支持,凭据在会话期间自动重复使用,并且可以存储在钥匙串中以备日后使用。
    • 饼干支持
    • []当您的应用程序移至后台时,请求可以继续运行(iOS 4+)
    • GZIP支持响应数据请求正文
    • 包含的 ASIDownloadCache 类允许请求透明地缓存响应,并且即使在没有可用网络的情况下也允许对缓存数据的请求成功
    • [] ASIWebPageRequest – 下载完整的网页,包括图像和样式表等外部资源。即使没有网络连接,任何大小的页面都可以无限期地缓存,并显示在 UIWebview/WebView 中。
    • 易于使用的 Amazon S3 支持 – 无需自己动手签署请求!
    • 完全支持 Rackspace 云文件
    • []客户端证书支持
    • 支持手动和自动检测代理、验证代理和PAC文件自动配置。内置的登录对话框让您的 iPhone 应用程序可以透明地使用身份验证代理,无需任何额外工作。
    • 带宽限制支持
    • 支持持久连接
    • 支持同步和异步请求
    • 通过委托或 [ NEW ] 块获取有关请求状态更改的通知(Mac OS X 10.6、iOS 4 及更高版本)
    • 带有广泛的单元测试

    ASIHTTPRequest 兼容 Mac OS 10.5 或更高版本,以及 iOS 3.0 或更高版本。


    创建同步请求

    使用 ASIHTTPRequest 的最简单方法。发送startSynchronous 消息将在同一线程中执行请求,并在完成(成功或失败)时返回控制权。

    通过检查error属性来检查问题

    要以字符串形式获取响应,请调用responseString方法。不要将它用于二进制数据 - 使用responseData获取 NSData 对象,或者,对于较大的文件,将您的请求设置为使用downloadDestinationPath属性下载到文件

    - (IBAction)grabURL:(id)sender
    {
    NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
    [request startSynchronous];
    NSError *error = [request error];
    if (!error) {
    NSString *response = [request responseString];
    }
    }

    与前面的示例执行相同的操作,但请求在后台运行。

    - (IBAction)grabURLInBackground:(id)sender
    {
    NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
    [request setDelegate:self];
    [request startAsynchronous];
    }
     
    - (void)requestFinished:(ASIHTTPRequest *)request
    {
    // Use when fetching text data
    NSString *responseString = [request responseString];
     
    // Use when fetching binary data
    NSData *responseData = [request responseData];
    }
     
    - (void)requestFailed:(ASIHTTPRequest *)request
    {
    NSError *error = [request error];
    }

    请注意,我们设置了请求的委托属性,以便我们可以在请求完成或失败时收到通知。

    这是创建异步请求的最简单方法,它将在后台运行在全局 NSOperationQueue 中。对于更复杂的操作(例如跟踪多个请求的进度),您可能希望创建自己的队列,这就是我们接下来将介绍的内容。

    使用块

    从 v1.8 开始,我们可以在支持块的平台上使用块来做同样的事情:

    - (IBAction)grabURLInBackground:(id)sender
    {
    NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
    __block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
    [request setCompletionBlock:^{
    // Use when fetching text data
    NSString *responseString = [request responseString];
     
    // Use when fetching binary data
    NSData *responseData = [request responseData];
    }];
    [request setFailedBlock:^{
    NSError *error = [request error];
    }];
    [request startAsynchronous];
    }

    请注意在我们声明请求时使用__block限定符,这很重要!它告诉块不要保留请求,这对于防止保留循环很重要,因为请求将始终保留块。

    使用队列

    这个例子再次做同样的事情,但我们为我们的请求创建了一个 NSOperationQueue。

    使用您自己创建的 NSOperationQueue(或 ASINetworkQueue,见下文)可以让您更好地控制异步请求。使用队列时,只能同时运行一定数量的请求。如果您添加的请求多于队列的maxConcurrentOperationCount属性,则请求将在开始之前等待其他人完成。

    - (IBAction)grabURLInTheBackground:(id)sender
    {
    if (![self queue]) {
    [self setQueue:[[[NSOperationQueue alloc] init] autorelease]];
    }
     
    NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
    [request setDelegate:self];
    [request setDidFinishSelector:@selector(requestDone:)];
    [request setDidFailSelector:@selector(requestWentWrong:)];
    [[self queue] addOperation:request]; //queue is an NSOperationQueue
    }
     
    - (void)requestDone:(ASIHTTPRequest *)request
    {
    NSString *response = [request responseString];
    }
     
    - (void)requestWentWrong:(ASIHTTPRequest *)request
    {
    NSError *error = [request error];
    }


    demo及常见问题:https://github.com/paytronix/ASIHTTPRequest

    源码下载:ASIHTTPRequest-master.zip


    收起阅读 »

    扁平化 UI 的 iOS 组件-FlatUIKit

    FlatUIKitFlatUIKit 是我们在为 iPhone构建Grouper 时创建的具有“Flat UI”美学风格的 iOS 组件集合。它的设计灵感来自于Flat UI和Kyle Miller。样式是通过替换现有 UIKit 组件的类别来实现的,因此将其...
    继续阅读 »


    FlatUIKit

    FlatUIKit 是我们在为 iPhone构建Grouper 时创建的具有“Flat UI”美学风格的 iOS 组件集合它的设计灵感来自于Flat UIKyle Miller样式是通过替换现有 UIKit 组件的类别来实现的,因此将其集成到您的项目中非常简单。

    安装

    FlatUIKit 可以通过CocoaPods安装只需添加

    pod 'FlatUIKit'

    组件

    FUIButton 是 UIButton 的一个嵌入式子类,它公开了额外的属性 buttonColor、shadowColor、cornerRadius 和 shadowHeight。请注意,如果您设置了其中任何一个,则必须设置所有这些。

    myButton.buttonColor = [UIColor turquoiseColor ];
    myButton.shadowColor = [UIColor
    greenSeaColor ];
    myButton.shadowHeight =
    30f ;
    myButton.cornerRadius =
    60f ;
    myButton.titleLabel.font = [UIFont
    boldFlatFontOfSize: 16 ];
    [myButton
    setTitleColor: [UIColor cloudColor ] forState: UIControlStateNormal];
    [myButton
    setTitleColor: [UIColor cloudColor ] forState: UIControlStateHighlighted];

    FUITextField 是 UITextField 的一个嵌入式子类,它公开了附加属性 edgeInsets、textFieldColor、borderColor、borderWidth 和 cornerRadius。请注意,如果您设置了其中任何一个,则必须设置所有这些。

    myTextField.font = [UIFont flatFontOfSize: 16 ];
    myTextField.backgroundColor = [UIColor
    clearColor ];
    myTextField.edgeInsets = UIEdgeInsetsMake(
    4 . 0f , 15 . 0f , 4 . 0f , 15 . 0f );
    myTextField.textFieldColor = [UIColor
    whiteColor ];
    myTextField.borderColor = [UIColor
    turquoiseColor ];
    myTextField.borderWidth =
    20f ;
    myTextField.cornerRadius =
    30f ;

    FUISegmentedControl 是 UISegmentedControl 的一个嵌入式子类,它公开了附加属性 selectedColor、deselectedColor、selectedFont、deselectedFont、selectedFontColor、deselectedFontColor、dividerColor 和 cornerRadius。请注意,如果您设置了其中任何一个,建议您设置所有这些。

    mySegmentedControl.selectedFont = [UIFont boldFlatFontOfSize: 16 ];
    mySegmentedControl.selectedFontColor = [UIColor
    cloudColor ];
    mySegmentedControl.deselectedFont = [UIFont
    flatFontOfSize: 16 ];
    mySegmentedControl.deselectedFontColor = [UIColor
    cloudColor ];
    mySegmentedControl.selectedColor = [UIColor
    amethystColor ];
    mySegmentedControl.deselectedColor = [UIColor
    silverColor ];
    mySegmentedControl.dividerColor = [的UIColor
    midnightBlueColor ];
    mySegmentedControl.cornerRadius =
    5.0 ;

    FUISwitch 不是 UISwitch 的子类(UISwitch 的子类太不灵活了),而是一个重新实现,它暴露了 UISwitch 的所有方法。此外,它还提供对其底层开/关 UILabels 和其他子视图的访问。

    mySwitch.onColor = [UIColor turquoiseColor ];
    mySwitch.offColor = [UIColor
    cloudColor ];
    mySwitch.onBackgroundColor = [的UIColor
    midnightBlueColor ];
    mySwitch.offBackgroundColor = [UIColor
    silverColor ];
    mySwitch.offLabel.font = [UIFont
    boldFlatFontOfSize: 14 ];
    mySwitch.onLabel.font = [UIFont
    boldFlatFontOfSize: 14 ];

    与 FUISwitch 类似,FUIAlertView 是 UIAlertView 的重新实现,它公开了 UIAlertView 的所有方法(和委托方法,使用 FUIAlertViewDelegate 协议),但在 UI 定制方面具有更大的灵活性。它的所有子 UILabels、UIViews 和 FUIButtons 都可以随意定制。

    FUIAlertView *alertView = [[FUIAlertView alloc ] initWithTitle: @" Hello "
    message: @" This is an alert view "
    delegate: nil cancelButtonTitle: @" Dismiss "
    otherButtonTitles: @" Do Something " , nil ];
    alertView.titleLabel.textColor = [UIColor
    cloudColor ];
    alertView.titleLabel.font = [UIFont
    boldFlatFontOfSize: 16 ];
    alertView.messageLabel.textColor = [UIColor
    cloudColor ];
    alertView.messageLabel.font = [UIFont
    flatFontOfSize: 14 ];
    alertView.backgroundOverlay.backgroundColor = [[UIColor
    cloudColor ] colorWithAlphaComponent: 0.8 ];
    alertView.alertContainer.backgroundColor = [的UIColor
    midnightBlueColor ];
    alertView.defaultButtonColor = [UIColor
    cloudColor ];
    alertView.defaultButtonShadowColor = [UIColor
    asbestosColor ];
    alertView.defaultButtonFont = [UIFont
    boldFlatFontOfSize: 16 ];
    alertView.defaultButtonTitleColor = [UIColor
    asbestosColor ];

    为了提供平面 UISlider、UIProgressViews 和 UISteppers,我们只需在 UISlider/ProgressView/UIStepper 上提供类别,以使用适当的颜色/角半径自动配置它们的外观。这有助于与您现有的项目零摩擦集成:

    [mySlider configureFlatSliderWithTrackColor: [UIColor silverColor ]
    progressColor: [UIColor alizarinColor ]
    thumbColor: [UIColor pomegranateColor ]];

    FUIS滑块

    [myProgressView configureFlatProgressViewWithTrackColor: [UIColor silverColor ]
    progressColor: [UIColor alizarinColor ]];

    [myStepper
    configureFlatStepperWithColor: [的UIColor wisteriaColor ]
    highlightedColor: [的UIColor wisteriaColor ]
    disabledColor: [的UIColor amethystColor ]
    iconColor通过: [的UIColor cloudsColor ]];

    要为整个应用程序自定义栏按钮项(包括后退按钮),UIBarButtonItem+FlatUI 提供了一个类方法,该方法利用 UIBarButtonItem 外观代理一步完成此操作:

    [UIBarButtonItem configureFlatButtonsWithColor: [UIColor peterRiverColor ]
    highlightColor: [UIColor belizeHoleColor ]
    cornerRadius: 3 ];

    但是,这可能会导致从操作表、共享表或 web 视图中的链接推送的控制器出现渲染问题。为防止这种行为,请将自定义栏按钮项的范围限定到您的控制器:

    [UIBarButtonItem configureFlatButtonsWithColor: [UIColor peterRiverColor ]
    highlightColor: [UIColor belizeHoleColor ]
    cornerRadius: 3
    whenContainedIn: [YourViewController class ]];

    您可以修改 UITableViewCell 的 backgroundColor 和 selectedBackgroundColor 而不会丢失圆角。单元格将复制 UITableView 的分隔符颜色。分隔符高度显示为 separatorHeight,半径显示为 cornerRadius。

    UITableViewCell *cell = ...;
    [cell
    configureFlatCellWithColor: [UIColor greenSeaColor ]
    selectedColor: [UIColor cloudColor ]
    roundingCorners: corners];

    cell.cornerRadius =
    50f ; //可选
    cell.separatorHeight =
    2 . 0f ; //可选

    demo下载及常见问题:https://github.com/Grouper/FlatUIKit
    源码下载:FlatUIKit.zip


    收起阅读 »

    模型处理工具不仅仅只有YYModel,还有更强的Mantle

    Mantle 使为您的 Cocoa 或 Cocoa Touch 应用程序编写简单的模型层变得容易Let's use the GitHub API for demonstration~!typedef enum : NSUInteger { ...
    继续阅读 »

    Mantle 使为您的 Cocoa 或 Cocoa Touch 应用程序编写简单的模型层变得容易

    Let's use the GitHub API for demonstration~!

    typedef enum : NSUInteger {
    GHIssueStateOpen,
    GHIssueStateClosed
    } GHIssueState;

    @interface GHIssue : NSObject <NSCoding, NSCopying>

    @property (nonatomic, copy, readonly) NSURL *URL;
    @property (nonatomic, copy, readonly) NSURL *HTMLURL;
    @property (nonatomic, copy, readonly) NSNumber *number;
    @property (nonatomic, assign, readonly) GHIssueState state;
    @property (nonatomic, copy, readonly) NSString *reporterLogin;
    @property (nonatomic, copy, readonly) NSDate *updatedAt;
    @property (nonatomic, strong, readonly) GHUser *assignee;
    @property (nonatomic, copy, readonly) NSDate *retrievedAt;

    @property (nonatomic, copy) NSString *title;
    @property (nonatomic, copy) NSString *body;

    - (id)initWithDictionary:(NSDictionary *)dictionary;

    @end


    typedef enum : NSUInteger {
    GHIssueStateOpen,
    GHIssueStateClosed
    } GHIssueState;

    @interface GHIssue : NSObject <NSCoding, NSCopying>

    @property (nonatomic, copy, readonly) NSURL *URL;
    @property (nonatomic, copy, readonly) NSURL *HTMLURL;
    @property (nonatomic, copy, readonly) NSNumber *number;
    @property (nonatomic, assign, readonly) GHIssueState state;
    @property (nonatomic, copy, readonly) NSString *reporterLogin;
    @property (nonatomic, copy, readonly) NSDate *updatedAt;
    @property (nonatomic, strong, readonly) GHUser *assignee;
    @property (nonatomic, copy, readonly) NSDate *retrievedAt;

    @property (nonatomic, copy) NSString *title;
    @property (nonatomic, copy) NSString *body;

    - (id)initWithDictionary:(NSDictionary *)dictionary;

    @end
    @implementation GHIssue

    + (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
    return dateFormatter;
    }

    - (id)initWithDictionary:(NSDictionary *)dictionary {
    self = [self init];
    if (self == nil) return nil;

    _URL = [NSURL URLWithString:dictionary[@"url"]];
    _HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]];
    _number = dictionary[@"number"];

    if ([dictionary[@"state"] isEqualToString:@"open"]) {
    _state = GHIssueStateOpen;
    } else if ([dictionary[@"state"] isEqualToString:@"closed"]) {
    _state = GHIssueStateClosed;
    }

    _title = [dictionary[@"title"] copy];
    _retrievedAt = [NSDate date];
    _body = [dictionary[@"body"] copy];
    _reporterLogin = [dictionary[@"user"][@"login"] copy];
    _assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]];

    _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];

    return self;
    }

    - (id)initWithCoder:(NSCoder *)coder {
    self = [self init];
    if (self == nil) return nil;

    _URL = [coder decodeObjectForKey:@"URL"];
    _HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
    _number = [coder decodeObjectForKey:@"number"];
    _state = [coder decodeUnsignedIntegerForKey:@"state"];
    _title = [coder decodeObjectForKey:@"title"];
    _retrievedAt = [NSDate date];
    _body = [coder decodeObjectForKey:@"body"];
    _reporterLogin = [coder decodeObjectForKey:@"reporterLogin"];
    _assignee = [coder decodeObjectForKey:@"assignee"];
    _updatedAt = [coder decodeObjectForKey:@"updatedAt"];

    return self;
    }

    - (void)encodeWithCoder:(NSCoder *)coder {
    if (self.URL != nil) [coder encodeObject:self.URL forKey:@"URL"];
    if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURL forKey:@"HTMLURL"];
    if (self.number != nil) [coder encodeObject:self.number forKey:@"number"];
    if (self.title != nil) [coder encodeObject:self.title forKey:@"title"];
    if (self.body != nil) [coder encodeObject:self.body forKey:@"body"];
    if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@"reporterLogin"];
    if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@"assignee"];
    if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"];

    [coder encodeUnsignedInteger:self.state forKey:@"state"];
    }

    - (id)copyWithZone:(NSZone *)zone {
    GHIssue *issue = [[self.class allocWithZone:zone] init];
    issue->_URL = self.URL;
    issue->_HTMLURL = self.HTMLURL;
    issue->_number = self.number;
    issue->_state = self.state;
    issue->_reporterLogin = self.reporterLogin;
    issue->_assignee = self.assignee;
    issue->_updatedAt = self.updatedAt;

    issue.title = self.title;
    issue->_retrievedAt = [NSDate date];
    issue.body = self.body;

    return issue;
    }

    - (NSUInteger)hash {
    return self.number.hash;
    }

    - (BOOL)isEqual:(GHIssue *)issue {
    if (![issue isKindOfClass:GHIssue.class]) return NO;

    return [self.number isEqual:issue.number] && [self.title isEqual:issue.title] && [self.body isEqual:issue.body];
    }

    @end

    哇,对于这么简单的事情来说,这是很多样板!而且,即便如此,这个例子也没有解决一些问题:

    • 无法GHIssue使用来自服务器的新数据更新 a 
    • 没有办法将 a 变GHIssue JSON。
    • GHIssueState不应按原样编码。如果将来枚举更改,现有存档可能会中断。
    • 如果未来的界面发生GHIssue变化,现有的档案可能会中断。


    为什么不使用核心数据?

    Core Data 很好地解决了某些问题。如果您需要对数据执行复杂的查询,处理具有大量关系的巨大对象图,或支持撤消和重做,Core Data 非常适合。

    然而,它确实有几个痛点:

    • 仍然有很多样板。托管对象减少了上面看到的一些样板,但 Core Data 有很多自己的样板。正确设置 Core Data 堆栈(带有持久存储和持久存储协调器)并执行提取可能需要多行代码。
    • 很难做对。即使是有经验的开发人员在使用 Core Data 时也可能会犯错误,而且该框架是不可原谅的。

    如果您只是想访问一些 JSON 对象,那么 Core Data 可能会做大量工作,但收获甚微。

    尽管如此,如果您已经在应用程序中使用或想要使用 Core Data,Mantle 仍然可以作为 API 和托管模型对象之间的方便转换层。


    MTL模型

    输入 MTLModelGHIssue看起来像继承自MTLModel

    typedef enum : NSUInteger {
    GHIssueStateOpen,
    GHIssueStateClosed
    } GHIssueState;

    @interface GHIssue : MTLModel

    @property (nonatomic, copy, readonly) NSURL *URL;
    @property (nonatomic, copy, readonly) NSURL *HTMLURL;
    @property (nonatomic, copy, readonly) NSNumber *number;
    @property (nonatomic, assign, readonly) GHIssueState state;
    @property (nonatomic, copy, readonly) NSString *reporterLogin;
    @property (nonatomic, strong, readonly) GHUser *assignee;
    @property (nonatomic, copy, readonly) NSDate *updatedAt;

    @property (nonatomic, copy) NSString *title;
    @property (nonatomic, copy) NSString *body;

    @property (nonatomic, copy, readonly) NSDate *retrievedAt;

    @end


    @implementation GHIssue

    + (NSDateFormatter *)dateFormatter {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
    return dateFormatter;
    }

    + (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
    @"URL": @"url",
    @"HTMLURL": @"html_url",
    @"number": @"number",
    @"state": @"state",
    @"reporterLogin": @"user.login",
    @"assignee": @"assignee",
    @"updatedAt": @"updated_at"
    };
    }

    + (NSValueTransformer *)URLJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
    }

    + (NSValueTransformer *)HTMLURLJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
    }

    + (NSValueTransformer *)stateJSONTransformer {
    return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
    @"open": @(GHIssueStateOpen),
    @"closed": @(GHIssueStateClosed)
    }];
    }

    + (NSValueTransformer *)assigneeJSONTransformer {
    return [MTLJSONAdapter dictionaryTransformerWithModelClass:GHUser.class];
    }

    + (NSValueTransformer *)updatedAtJSONTransformer {
    return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {
    return [self.dateFormatter dateFromString:dateString];
    } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
    return [self.dateFormatter stringFromDate:date];
    }];
    }

    - (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
    self = [super initWithDictionary:dictionaryValue error:error];
    if (self == nil) return nil;

    // Store a value that needs to be determined locally upon initialization.
    _retrievedAt = [NSDate date];

    return self;
    }

    @end


    值得注意的是,从这个版本是缺席的实现, -isEqual:,和-hash通过检查@property 您在子类中声明,MTLModel可以为所有这些方法提供默认实现。

    原始示例的问题也都被修复了:

    无法GHIssue使用来自服务器的新数据更新 a 

    MTLModel有一个可扩展的-mergeValuesForKeysFromModel:方法,可以很容易地指定应该如何集成新的模型数据。

    没有办法将 a 变GHIssue JSON。

    这是可逆变压器真正派上用场的地方。+[MTLJSONAdapter JSONDictionaryFromModel:error:]可以将任何符合 的模型对象转换 回 JSON 字典。+[MTLJSONAdapter JSONArrayFromModels:error:]是相同的,但将模型对象数组转换为字典的 JSON 数组。

    如果未来的界面发生GHIssue变化,现有的档案可能会中断。

    MTLModel自动保存用于存档的模型对象的版本。取消归档时,-decodeValueForKey:withCoder:modelVersion:如果被覆盖将被调用,为您提供一个方便的挂钩来升级旧数据。


    Mantle 不会自动为您保留对象。但是,MTLModel 确实符合,因此可以使用 将模型对象存档到磁盘 NSKeyedArchiver

    如果你需要更强大的东西,或者想要避免将整个模型一次保存在内存中,Core Data 可能是更好的选择。

    Carthage

    github "Mantle/Mantle"


    CocoaPods

    target 'MyAppOrFramework' do
    pod 'Mantle'
    end


    demo下载及常见问题:https://github.com/Mantle/Mantle

    源码:Mantle-master.zip



    收起阅读 »

    ORCharts:环形图、饼状图、扇形图

    本文为ORCharts:环形图、饼状图、扇形图 部分, 做详细说明相关连接GitHubORChartsORCharts:曲线图、折线图效果预览安装pod 'ORCharts/Ring'使用Use Interface Builder1、 在XIB或Storybo...
    继续阅读 »

    本文为ORCharts:环形图、饼状图、扇形图 部分, 做详细说明

    相关连接

    GitHub
    ORCharts
    ORCharts:曲线图、折线图

    效果预览



    安装

    pod 'ORCharts/Ring'

    使用

    Use Interface Builder

    1、 在XIB或Storyboard拖拽一个 UIView 到你需要展示的位置
    2、 修改Class为 ORRingChartView
    3、 设置 dataSource

    代码

    @property (nonatomic, strong) ORRingChartView *ringChartView;
    _ringChartView = [[ORRingChartView alloc] initWithFrame:CGRectMake(0, 0, 375, 375)];
    _ringChartView.dataSource = self;
    [self.view addSubview:_ringChartView];

    在数据改变或是配置改变的时候reloadData

    [_ringChartView reloadData];

    style

    ORRingChartStyleRing:环形图(默认)
    ORRingChartStylePie:饼状图
    ORRingChartStyleFan:扇形图

    _ringChart.style = ORRingChartStylePie;

    代理相关

    ORRingChartViewDatasource

    1、@required
    必须实现方法,数据个数以及对应数据,类似tableView

    - (NSInteger)numberOfRingsOfChartView:(ORRingChartView *)chartView;
    - (CGFloat)chartView:(ORRingChartView *)chartView valueAtRingIndex:(NSInteger)index;

    2、@optional,对应Index数据视图的渐变色,默认为随机色

    - (NSArray <UIColor *> *)chartView:(ORRingChartView *)chartView graidentColorsAtRingIndex:(NSInteger)index;

    对应Index数据视图的线条颜色,默认为白色

    - (UIColor *)chartView:(ORRingChartView *)chartView lineColorForRingAtRingIndex:(NSInteger)index;

    对应Index数据的信息线条颜色,默认为graidentColors的第一个颜色

    - (UIColor *)chartView:(ORRingChartView *)chartView lineColorForInfoLineAtRingIndex:(NSInteger)index;

    中心视图,默认nil,返回的时候需要设置视图大小

    - (UIView *)viewForRingCenterOfChartView:(ORRingChartView *)chartView;

    对应Index数据的顶部信息视图,默认nil,返回的时候需要设置视图大小

    - (UIView *)chartView:(ORRingChartView *)chartView viewForTopInfoAtRingIndex:(NSInteger)index;

    对应Index数据的底部信息视图,默认nil,返回的时候需要设置视图大小

    - (UIView *)chartView:(ORRingChartView *)chartView viewForBottomInfoAtRingIndex:(NSInteger)index;

    配置相关

    以下是配置中部分属性图解


    配置修改方式

    _ringChart.config.neatInfoLine = YES;
    _ringChart.config.ringLineWidth = 2;
    _ringChart.config.animateDuration = 1;
    [_ringChart reloadData];

    以下为配置具体说明

    1、整体
    clockwise:图表绘制方向是否为顺时针,默认YES
    animateDuration:动画时长 ,设置0,则没有动画,默认1
    neatInfoLine:infoLine 两边对齐、等宽,默认NO
    startAngle:图表绘制起始角度,默认 M_PI * 3 / 2
    ringLineWidth:ringLine宽度,默认2
    infoLineWidth:infoLine宽度,默认2

    2、偏移、边距配置
    minInfoInset:infoView的内容偏移,值越大,infoView越宽,默认0
    infoLineMargin:infoLine 至 周边 的距离,默认10
    infoLineInMargin:infoLine 至 环形图的距离,默认 10
    infoLineBreakMargin:infoLine折线距离,默认 15
    infoViewMargin:infoLine 至 infoView的距离,默认5

    3、其他
    pointWidth:infoline 末尾圆点宽度,默认 5
    ringWidth:环形图,圆环宽度, 如果设置了 centerView 则无效,默认60

    文末

    GitHub传送门
    有任何问题,可在本文下方评论,或是GitHub上提出issue
    如有可取之处, 记得 star

    转自:https://www.jianshu.com/p/317a79890984

    收起阅读 »

    Swift手势密码库,用这一个就够了!

    一个轻量级、面对协议编程、高度自定义的 图形解锁/手势解锁 / 手势密码 / 图案密码 / 九宫格密码相比于其他同类三方库有哪些优势:1、完全面对协议编程,支持高度自定义网格视图和连接线视图,轻松实现各类不同需求;2、默认支持多种配置效果,支持大部分主流效果,...
    继续阅读 »

    一个轻量级、面对协议编程、高度自定义的 图形解锁/手势解锁 / 手势密码 / 图案密码 / 九宫格密码

    相比于其他同类三方库有哪些优势:

    1、完全面对协议编程,支持高度自定义网格视图和连接线视图,轻松实现各类不同需求;
    2、默认支持多种配置效果,支持大部分主流效果,引入就可以搞定需求;
    3、源码采用Swift5编写,通过泛型、枚举、函数式编程优化代码,具有更高的学习价值;
    4、后期会持续迭代,不断添加主流效果;

    Github地址

    JXPatternLock

    效果预览

    1. 箭头


    2. 中间点自动链接


    3. 小灰点


    4. 小白点


    5. 荧光蓝


    6. fill白色


    7. 阴影


    8. 图片


    9. 旋转(鸡你太美)


    10. 破折线


    11. 图片连接线(箭头)


    12. 图片连接线(小鱼儿)


    13. 设置密码


    14. 修改密码


    15. 验证密码


    使用

    初始化PatternLockViewConfig

    方式一:使用LockConfig

    LockConfig是默认提供的类,实现了PatternLockViewConfig协议。可以直接通过LockConfig的属性进行自定义。

    let config = LockConfig()
    config.gridSize = CGSize(width: 70, height: 70)
    config.matrix = Matrix(row: 3, column: 3)
    config.errorDisplayDuration = 1

    方式二:新建实现PatternLockViewConfig协议的类

    该方式可以将所有配置细节聚集到自定义类的内部,外部只需要初始化自定义类即可。详情请参考demo里面的ArrowConfig类。这样有个好处就是,多个地方都需要用到同样配置的时候,只需要初始化相同的类,而不用像使用LockConfig那样,复制属性配置代码。

    struct ArrowConfig: PatternLockViewConfig {
    var matrix: Matrix = Matrix(row: 3, column: 3)
    var gridSize: CGSize = CGSize(width: 70, height: 70)
    var connectLine: ConnectLine?
    var autoMediumGridsConnect: Bool = false
    //其他属性配置!只是示例,就不显示所有配置项,影响文档长度
    }

    配置GridView

    config.initGridClosure = {(matrix) -> PatternLockGrid in
    let gridView = GridView()
    let outerStrokeLineWidthStatus = GridPropertyStatus<CGFloat>.init(normal: 1, connect: 2, error: 2)
    let outerStrokeColorStatus = GridPropertyStatus<UIColor>(normal: tintColor, connect: tintColor, error: .red)
    gridView.outerRoundConfig = RoundConfig(radius: 33, lineWidthStatus: outerStrokeLineWidthStatus, lineColorStatus: outerStrokeColorStatus, fillColorStatus: nil)
    let innerFillColorStatus = GridPropertyStatus<UIColor>(normal: nil, connect: tintColor, error: .red)
    gridView.innerRoundConfig = RoundConfig(radius: 10, lineWidthStatus: nil, lineColorStatus: nil, fillColorStatus: innerFillColorStatus)
    return gridView
    }

    配置ConnectLine

    let lineView = ConnectLineView()
    lineView.lineColorStatus = .init(normal: tintColor, error: .red)
    lineView.triangleColorStatus = .init(normal: tintColor, error: .red)
    lineView.isTriangleHidden = false
    lineView.lineWidth = 3
    config.connectLine = lineView

    初始化PatternLockView

    lockView = PatternLockView(config: config)
    lockView.delegate = self
    view.addSubview(lockView)

    结构


    完全遵从面对协议开发。
    PatternLockView依赖于配置协议PatternLockViewConfig。
    配置协议配置网格协议PatternLockGrid和连接线协议ConnectLine。

    转自:https://www.jianshu.com/p/f8aa805057fc

    收起阅读 »