Ruby
的工具包,为了使用这个工具包我还现学现卖了Ruby
这门语言,算是个入门级选手了吧,其实真的没有想象中的那么复杂。
如果已经是能够熟练使用Xcodeproj
的选手,就可以不用继续往下看了。^_^
先介绍下背景,我们的项目Model
层使用了自己的ORM框架,同时也使用了Protocol Buffer
,这也就导致了每个版本之间有新增接口,或者接口文档有变化的时候,都需要重新生成对应的Model
文件。而这些文件每次都要手动替换Xcode
中的现有文件,这其实是一件很枯燥很麻烦的时间,而且非常容易出错,漏掉一个文件或者少替换一个文件都是很棘手的问题。虽然我现在编写了一部分Shell
脚本,可以直接将新生成的文件替换到Xcode
工程所对应的物理目录中。但是即使如此,还需要对Xcode
的工程文件作出处理,增加新文件的引用,如果不增加引用,只是把文件丢到物理目录上的话,Xcode
工程并不会索引这个文件。同时像PB那些文件还需要添加-fno-objc-arc
这个编译指示符,而这些文件往往有几十个之多,简直要爆炸!(不过还好有搜索批量添加的功能,暂时忍了。。)
后来就想,物理文件通过Shell
脚本搞定了,那剩下的这个能不能也通过工具给搞定?有两个思路。
最近比较流行的就是Xcode Extension
了,它真的是无所不能,各种各样的插件都已经存在了,我就觉得这个一定行。所以开始着手干,后来发现这个需求其实是太复杂了点儿。
首先Xcode
插件的开发需要监测所有Xcode发出的Notifications
,然后摘取出自己需要的,并弄清楚各个参数的类型及关系,同时还需要一定的逆向功底,找到对应Xcode控件的实现方法及如何使用,我自己尝试了下,虽然找到了几个关键的notification,但是Xcode文件管理那里各个元素分别对应何种类,着实让我头大了一把。后来随着Xcode8的发布,苹果禁掉了第三方的插件,所以这一条路自然也就走不下去了。
做过多人协作开发的同学都会遇到代码冲突的情况,而所有与工程相关的冲突都会体现在pbxproj这个文件上,这个文件就处在.xcodeproj这个目录中,这个文件其实就是整个Xcode工程的配置文件,所有的文件引用,group关系,build设置都在这里面能够找到。仔细去看他就是一个plist文件也就是一个特殊的xml文件。它的编写有着一定的规律。
所以通过对这个文件的编写也能够达到同样的目的,所以尝试着手搓一下。然而当我真正分析这个文件的时候就发现,随随便便一个工程这个文件就有着动辄上千行文字,而且内部不同数据之间的格式也都不近相同,然后每个数据之前都有一个24位的16进制数字。这个数字的生成还是一个迷之存在,感觉应该是个UUID。
截取部分文件的内容如图所示:
最终经过尝试,这个文件处理啊起来也不是十分的容易,不过我在写这篇文章的时候看到一篇研究 pbxproj文件的文章,讲的十分深入透彻。Let’s Talk About project.pbxproj,对这个文件感兴趣的同学推荐去阅读。
正在上面两种策略发愁的时候,出现了柳暗花明又一村的事情,最近在阅读一篇博客的时候发现了这么一篇文章使用代码为 Xcode 工程添加文件,介绍的就是这个一个需求,才知道了又Xcodeproj
这样一个前人已经造好的轮子了。后来一想也确实是,CocoaPods能够通过脚本完成项目工程的修改,他们一定已经做好这件事情了。
在发现这个新大陆之后,立马就开始着手准备编写适合自己需求的脚本文件。我在比之前那个作者写起来方便多了,因为现在这个工具有了完善的文档支持。这为开发提供了很大的便捷性。
我自己的需求整体上看其实就需要干两件事,因为所有文件事先已经全部移动到物理目录了,所以首先要把Xcode工程中,对应group下的所有文件删掉,然后在创建相应的group,并将文件添加到对应的group中。最后再根据需要添加编译指示的文件,添加编译指示,就完了。
那么首先就是打开工程,找到Target,一般的项目,target的第一个就是我们所需要的主target了。
1 2 3 4 5 6 |
|
拿到target之后,就要找到们所存放相应目录的group,group在Xcodeproj
中对应这个类PBXGroup
,通过查看文档可以找到他提供了一个find_subpath
的方法,会从它自身这个节点根据提供的path依次向下寻找,最后一个参数为如果没有找到,是否创建这个group。拿到这个group之后,根据我自身项目的需要,因为我的group和文件的物理目录是一一对应的,还需要设置一下它的source_tree
和path
,对应的就是Xcode
中的这个内容。
1 2 3 |
|
获取到了根group之后,就可以拿到他的children,然后就可以递归找到每一个文件,调用remove_form_project
了,但是我在实际编写的过程中,发现一旦对一个文件调用了remove_form_project
之后,那这个循环就break了,也就只执行了一次,尝试了各种方法也没找到解决方案,在Github上还找到了有人提过这个issue,但是好像也没有解决。最后通过再次翻查文档,发现还有clear
这样一个方法,它会直接清空整个group下的所有元素,非常适合我的需求,不需要我自己去遍历了。
1 2 3 |
|
但是使用过之后,还存在问题这仅仅是在Xcode
中左侧的Project Navigator
中把文件引用删除了,但是对于.m
或者资源文件这种需要加入到target中的文件,并不会直接删掉,如此一来在Xcode
的Build Phase
中就会看到这样的情况。文件丢失。
所以针对这种情况,就需要在clear之前,对这些文件特殊处理,在target中将文件引用删掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
接下来是添加新的文件,首先通过find_subpath
将所有的group创建出来,然后在将每个group下对应的文件给引用进去就可以了。这里对.m
和资源文件还是需要单独处理,因为不光工程要引用他们,target也需要引用他们。所以先向工程添加之后拿到返回的PBXFileReference
,在向target对应的build_phase
添加即可。虽然target提供了增加一组文件的方法add_file_references
,但是这样的添加方式并不能设定编译指示,一个Target的Build rule
对应着PBXBuildRule
,从文档中没有找到丝毫设置的方法。后来倒是发现再向target的build_phase
中添加单个文件的时候可以设置compiler flags
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
最后将一切执行完之后,执行一下保存就完事儿了。
1
|
|
这篇文章没有多高深,主要就是介绍一下Xcodeproj
这个轮子,有遇到相同类似需求的同学可以参考下,因为介绍这个轮子的资料确实太少了。。
在ARC下,如果我们需要操作一些底层的库,有时会用到Core Foundation的对象,简称CF对象,例如Core Graphic、Core Text。在ARC下,这些CF的对象的内存是不会被自动管理的,而是需要我们在它的生命周期结束的时候调用CFRelease()释放它。
CF对象与NS对象之间如何相互转换呢?系统提供了bridge,bridge_retained,__bridge_transfer 三个关键字给我们使用。
__bridge只是单纯的对象类型的转换,并没有涉及到对象所有权的转移,所以需要把握好对象的生命周期,否则会出项野指针的情况。
1 2 3 4 5 6 7 8 9 |
|
NS对象转为CF对象会出项野指针,逆过来CF转NS对象则有可能会出现内存泄露的问题,具体见下面的bridge_transfer的介绍。简单来说bridge就是类型强制转换。
__bridge_retained用于将NS对象转为CF对象,这其中有所有权的转移,NS对象会被retain一次再交给CF处理,这样即使原始的NS的对象在被ARC自动处理release一次之后,它的retainCount也不会为0,从而不会被销毁。
1 2 3 4 5 6 7 8 9 10 |
|
看一下引用计数的变化
1 2 3 4 |
|
可以看到引用计数是一个超级大的整数,这是因为在arc下直接创建的Foundation对象的引用计数都被处理过了,无法看到具体的数值。
这一操作系统给我们提供了一个内联函数来干这件事CFBridgingRetain,
1 2 3 |
|
__bridge_transfer用于将CF对象转为NS对象,同样的这其中也有所有权的转移,CF对象会在转换为NS对象后进行一次release操作,即把所有权完全移交给NS对象来处理,看一下引用计数的变化:
1 2 3 4 5 6 |
|
这里的对象是由Core Foundation创建的,所以它的引用计数可以被打印出来,可以看到在ARC环境下,string会被声明成strong类型,所以这个对象的retainCount会被加1,但是转换之后仍然为1,即CF对象已经放弃了它的所有权。
如果是__bridge的话
1 2 3 4 5 6 |
|
转换之后的引用计数是2,即CF和NS对象同时有着持有权,这样在出了当前的作用域后,ARC会自动给NS对象做release,但是CF对象需要手动调用CFRelease(),如果忘记了的话,那就是内存泄露。
同样,这一操作系统给也我们提供了一个内联函数来干这件事CFBridgingRetain,
1 2 3 |
|
这其中的关系可以用下图来直接说明,记住这张图就可以了:
————————————
]]>原文地址:https://developer.apple.com/news/?id=05042016a
首先看下图
其中蓝色部分的高级API,其实都已经帮我们做好了IPv6的支持,我们使用的大多数第三方网络库也都是基于这些高级API的,所以这里我们不需要做什么改动。 需要注意的是下面的红色部分的底层的socket API需要做出适配支持。
- inet_addr()
- inet_aton()
- inet_lnaof()
- inet_makeaddr()
- inet_netof()
- inet_network()
- inet_ntoa()
- inet_ntoa_r()
- bindresvport()
- getipv4sourcefilter()
- setipv4sourcefilter()
测试验证方式就是通过Mac的共享网络共享一个IPv6的无线网,跟已往创建方式不同的是进入共享时需要按住Option
键,不然Create NAT64 Network
的选项不会出现
然后开启无线共享,使iPhone连接上分享出来的热点即可 注:需要将iPhone的蜂窝网络数据关掉,以保证只有通过WiFi在连接网络。
在自己的项目中检查了一下,需要做出修改的都是集中在网路库的Reachability
相关操作中。如AFNetworking
的AFNetworkReachabilityManager
,它已经支持了IPv6,但是在他的支持中加了个一个条件编译的选项,判断了系统的版本,一直不明白它这么做的原因是什么,因此我也提交了一个issue询问了下,等待大神给出解释。
UPDATE:2016年05月11日:
后来发现
Reachability
在iOS9以下的系统上如果适配了IPv6的话则会导致失效,网络监测状态不准,应该是苹果自身的bug,所以在这些系统上还需要使用IPv4的数据结构,AFNetworking
的大神也对我的issue给出了解答,同时在Alamofire
中也有对这个bug描述的issue。
除此之外,我们自己也用到了一个Reachability
的类似物,这就需要对它单独做出IPv6的支持,具体方法可以参照Apple
自己官方提供的Reachability
Demo,这个Demo中的ReadMe中也介绍了一些很有用的信息。
IPv6 Support
Reachability fully supports IPv6. More specifically, each of the APIs handles IPv6 in the following way:
reachabilityWithHostName and SCNetworkReachabilityCreateWithName: Internally, this API works be resolving the host name to a set of IP addresses (this can be any combination of IPv4 and IPv6 addresses) and establishing separate monitors on all available addresses.
reachabilityWithAddress and SCNetworkReachabilityCreateWithAddress: To monitor an IPv6 address, simply pass in an IPv6
sockaddr_in6 struct
instead of the IPv4sockaddr_in struct
.reachabilityForInternetConnection: This monitors the address 0.0.0.0, which reachability treats as a special token that causes it to actually monitor the general routing status of the device, both IPv4 and IPv6.
Removal of reachabilityForLocalWiFi
Older versions of this sample included the method reachabilityForLocalWiFi. As originally designed, this method allowed apps using Bonjour to check the status of “local only” Wi-Fi (Wi-Fi without a connection to the larger internet) to determine whether or not they should advertise or browse.
However, the additional peer-to-peer APIs that have since been added to iOS and OS X have rendered it largely obsolete. Because of the narrow use case for this API and the large potential for misuse, reachabilityForLocalWiFi has been removed from Reachability.
Apps that have a specific requirement can use reachabilityWithAddress to monitor IN_LINKLOCALNETNUM (that is, 169.254.0.0).
Note: ONLY apps that have a specific requirement should be monitoring IN_LINKLOCALNETNUM. For the overwhelming majority of apps, monitoring this address is unnecessary and potentially harmful.
————————————
]]>method swizzling
替换系统方法,或者通过消息转发机制将无法响应的方法都转发给一个可以handle任何消息的对象身上等。通过这些方式都可以将·常见数组越界、unrecognized selector sent to instante 0xXXXXXXXX
等crash类型在App内部自身得到消化处理,从而将程序的crash率降到一个可以接受的范围内。
在我刚刚开始写Objective-C代码的时候,觉得这是一种非常好的编程方式,应该大力推崇,能够极大地降低crash率,想怎么写代码就怎么写代码,妈妈再也不担心我的程序会crash了。但是,后来渐渐发现这并不是一种很好的解决方案,它其实是一把双刃剑。在给我们带来便利的同时也给我们带来了一些不利的地方。这就是本次讨论的主题慎用或者不用SafeKit类似物
。
SateKit的实现一般都是借助于Objective-C的runtime特性,但是这样实现起来就会很黑,也就是我们常说的黑魔法,黑魔法往往能给我们带来意想不到的效果,但是这样一来就将一些本该在程序的编译期就该发现的问题给滞后到了程序的运行时,甚至将本该暴露的问题而隐藏了起来。这种方式其实被我们称为埋车头
的方案,发生了错误,没有响应机制,而是将事故车头埋藏起来,对外表现出一切良好。如此看来这种方式是我们万万不可取的,长期下去,只会产出越来越多的不稳定代码,代码中的问题不能及时暴露出来,久而久之成为编码习惯,后果可想而知。
最近工作中还遇到了一件与使用SafeKit相关的事儿,着实是踩了个坑,拿出来分享下。具体情况是这样,有一个宿主程序,他接收各个业务方以SDK的方式提供给它的静态库以供其正常运行。而我负责开发其中的一个SDK,在开发完成之后也通过了宿主程序接入验证,并由QA验证交付给宿主方后。却得到了宿主方QA的反馈说我们页面展示有问题,无数据展示。这怎么可能呢,在我们自己这里好好的,也自己接入宿主程序验证过,没有任何问题。要来宿主方的测试ipa,安装后发现确实有问题,通过抓包发现有数据请求也有正确数据返回,但是就是页面数据无法展示,真是见了鬼。只好同他们的RD要来最新的宿主程序,测试发现在新的宿主程序上确实有问题,但是老的宿主程序就没有问题,同样的是一份代码,问题肯定就出现在宿主程序上。
虽然明确知道问题出在宿主程序上,但是人家是大爷,你是不能让人家去查找修复问题的,只能用宿主程序和自己的SDK代码去Debug了,最后发现在数据解析时,见鬼了。
1 2 3 |
|
我取首字母转大写的string,最后怎么给我一堆NSNull
对象,也正是因为导致我数据解析失败。KVC
返回NSNull
,难道是我用错了?那我就不用one line of code
,使用另外的方式试试:
1 2 3 4 5 6 7 |
|
这样是正确ok的,那说明我的KVC
没有用错啊,难道是宿主方把KVC
禁用掉了,或者是给黑掉了?联系对方RD检查宿主工程,说没有禁用valueForKeyPath:
,没有找到相关代码,他们自身也在用(自身也在用都没有发现问题么?我觉得随便在宿主工程里建个VC,写个KVC都是会返回NSNull的)。还是那句话人家是大爷,只能自己SDK方修改,去掉这里的KVC
。其实我们的SDK中大量使用了valueForKeyPath:
,只修改这一处肯定是不行的,其他地方同样会出问题,果然第二天,他们又发现的其他的问题,经确认还是KVC
返回NSNull
的问题。
总不能把所有的KVC
都给改掉?最后强烈要求对方去排查,自己也通过增加符号断点-[NSObject valueForKeyPath:]
追查,并未发现任何异常,看来只能是在runtime里干事儿了。最终宿主方通过逐个排除SDK的方式,发现问题就出在另一个业务方的SDK上。
我猜他们一定是使用了SafeKit类似的东西,将valueForKeyPath:
进行了处理,写一段伪码大概就是:
1 2 3 4 5 6 |
|
所以要么是method swizzling
替换了实现,要么是通过category
重写覆盖了原始的系统方法。
所以回到主题上来,SafeKit这种东西还是慎用或者别用的好,一方面把本应该暴露出的问题给隐藏了起来,这会导致开发者过于依赖SafeKit,没有了它,代码的质量将急剧下降。另一方面,如果你的代码还会融入到其他的工程中去的话,例如产品自身是一个SDK,那么使用SafeKit或者runtime Hook了系统方法,那么带来的将是毁灭性的灾难。因为这对使用你SDK的宿主方的开发带来极大的不变。因为这是侵染性的处理方式,不仅仅是在你自己的SDK中SafeKit会起作用,同样的在整个宿主App内都会起作用。如此一来不出问题还好,一旦出了问题,那排查起来简直让人崩溃。
————————————
]]>Part 2
只介绍了第一个技术点alignment
和distribution
的约束如何添加和管理的alignment
这一部分的内容,这一篇继续介绍distribution
的约束添加和管理。
同样的在介绍实现之前,我先介绍一下StackView
的各种distribution
模式都是什么效果的:
arrangedSubviews
填充满整个StackView
,如果设置了spacing,那么这些arrangedSubviews
之间的间距就是spacing。如果减去所有的spacing,所有的arrangedSubview
的固有尺寸(intrinsicContentSize
)不能填满或者超出StackView
的尺寸,那就会按照Hugging
或者CompressionResistance
的优先级来拉伸或压缩一些arrangedSubview
。如果出现优先级相同的情况,就按排列顺序来拉伸或压缩。StackView
的尺寸减去所有的spacing之后均分给arrangedSubviews
,每个arrangedSubview
的尺寸是相同的。arrangedSubviews
,而是根据arrangedSubviews
的intrinsicContentSize
按比例分配。arrangedSubview
之间的spacing相等,但是这个spacing是有可能大于StackView
所设置的spacing,但是绝对不会小于。这个类型的布局可以这样理解,先按所有的arrangedSubview
的intrinsicContentSize
布局,然后余下的空间均分为spacing,如果大约StackView
设置的spacing那这样就OK了,如果小于就按照StackView
设置的spacing,然后按照CompressionResistance
的优先级来压缩一个arrangedSubview
。arrangedSubview
的中心点之间的距离相等,这样没两个arrangedSubview
之间的spacing就有可能不是相等的,但是这个spacing仍然是大于等于StackView
设置的spacing的,不会是小于。这个类型布局仍然是如果StackView
有多余的空间会均分给arrangedSubviews
之间的spacing,如果空间不够那就按照CompressionResistance
的优先级压缩arrangedSubview
。在介绍distribution
的约束创建和管理的过程中也涉及到了第二个知识点spacing
和distribution
的关系及约束的创建的内容,所以这两部都在这里介绍了。
distribution
方向同样也包括4种约束,这4种约束也都是添加到canvas
上的,除此之外它还包括一组通过NSMapTable
维护的FDGapLayoutGuide
。
1 2 3 4 5 6 7 8 |
|
arrangedSubviews
与canvas
之间的约束;arrangedSubviews
之间一个接一个的约束,这里需要注意这些约束的常量是StackView
的spacing,但是关系却不一定是相等。还有就是如果有个arrangedSubview
被hidden
了那么它仍然参与到edgeToEdge
的约束创建及布局中,只不过是把它与后一个arrangedSubview
之间的edgeToEdgeConstraint
的常量由spacing设置为0
。arrangedSubviews
之间distribution
各种相等关系的约束,这里面的管理的约束是StackView
的distribution
布局的精髓所在。如果是UIStackViewDistributionFill
模式的话,是没有relatedDimensionConstraint
的。UIStackViewDistributionFillEqually
与UIStackViewDistributionFillProportionally
使用的是一种类型的约束,而UIStackViewDistributionEqualCentering
与UIStackViewDistributionEqualSpacing
使用的却是另一种类型的约束,后面在详细介绍。arrangedSubviews
有hidden
的时候,该arrangedSubview
的有关dimensionAttribute
的约束;FDGapLayoutGuide
,只用在UIStackViewDistributionEqualCentering
和UIStackViewDistributionEqualSpacing
这两种模式中,FDGapLayoutGuide
用来连接左右两个arrangedSubView
,作为一个辅助view来约束左右两个view的位置关系。spacingOrCenteringGuides
的key是FDGapLayoutGuide
连接的左边的arrangedSubview
。最后说明的就是FDGapLayoutGuide
与arrangedSubView
相连接的约束没有被NSMapTable
所管理,它们就只是被加到了canvas
上。因为当模式改变时,所有的FDGapLayoutGuide
会被移除或者重建,所以跟它们相关的约束也会被一并清楚。
那么以上几种约束的创建顺序是怎样的呢?
canvasConnectionConstraints
;edgeToEdgeConstraints
;arrangedSubviews
,如果有arrangedSubview
被hidden
了,那么就会创建hiddingDimensionConstraints
;relatedDimensionConstraints
,这里如果是UIStackViewDistributionEqualCentering
和UIStackViewDistributionEqualSpacing
这两种模式的话,会先创建出spacingOrCenteringGuides
。下面具体来看,首先canvasConnectionConstraints
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
比较简单,先判断一下不需要创建的情况,然后就是根据axis
选用不同的NSLayoutAttribute
,将第一个和最后一个arrangedSubview
分别与StackView
创建相等的约束。这样一来再加上FDStackViewAlignmentLayoutArrangement
中创建的两个canvasConnectionConstraints
,整个canvas
的上下左右四个方向的约束就都有了,满足了canvas
布局的基本条件。
接下来是edgeToEdgeConstraints
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
先移去旧的相关约束,然后将arrangedSubviews
依次迭代遍历,根据axis
选择正确的NSLayoutAttribute
创建首尾相接的约束,常量为StackView
的spacing,关系则根据distribution
的不同而或等于或大于等于。
这里如前面介绍的一样,如果这个arrangedSubview
是hidden
的那么它仍然参与edgeToEdgeConstraints
的创建,只不过它与后一个arrangedSubview
的约束常量不再是spacing而是0
。还有一个特殊的就是如果是最后一个arrangedSubview
被hidden
了,那么它与前一个arrangedSubview
的约束的常量也同样是0
。
最后再遍历所有arrangedSubviews
,如果有arrangedSubview
被hidden
了,那就根据axis
给这个arrangedSubview
创建一个常量为0
的dimensionConstraint
。
如果是UIStackViewDistributionFill
的话,那么到这里所有distribution
的约束就已经创建完了,已经满足需求了。但是其他几种还要有后续的步骤。
先来看UIStackViewDistributionFillEqually
和UIStackViewDistributionFillProportionally
这两种类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
仍然是先干掉旧的约束,然后跟前面不同的是要取出所有的非hidden的arrangedSubview
添加约束,而不是所有arrangedSubview
。
这两个distribution
类型是将当前axis
所对应的dimensionAttribute
的约束作用在arrangedSubviews
上,如果是UIStackViewDistributionFillEqually
,那么约束的比例(multiplier
)就是1
,如果是UIStackViewDistributionFillProportionally
,那multiplier
就需要通过计算得出,是通过两个arrangedSubview
的intrinsicContentSize
做比值,这样就能保证arrangedSubview
最终会按照intrinsicContentSize
的比例来分配StackView
的空间布局。
再来看UIStackViewDistributionEqualCentering
和UIStackViewDistributionEqualSpacing
这两种类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
先创建spacingOrCenteringGuides
,开始是干掉旧的spacingOrCenteringGuides
。这里使用的仍然是visiableItems。
FDGapLayoutGuide
用来连接左右相连的两个可见arrangedSubview
。
这两个distribution
不同的地方就是UIStackViewDistributionEqualSpacing
的FDGapLayoutGuide
连接的是arrangedSubview
的minAttribute
和maxAttribute
,而UIStackViewDistributionEqualCentering
的FDGapLayoutGuide
连接的却是arrangedSubview
的centerAttribute
。
接下来就是创建relatedDimensionConstraints
,就是根据axis
不同给对应的dimensionAttribute
创建相等的约束即可,这些约束是作用在FDGapLayoutGuide
上的,而与前面那两种distribution
类型不同。这就是一开始说的relatedDimensionConstraints
中的两种类型的约束。
到此整个distribution
方向的约束也都创建完了。加上alignment
方向创建的约束,StackView
已经可以使用了。
介绍完这些再回过头来看本文章Part 1
中后面提到的UIStackView
的第一个bug,当存在spacing的时候UIStackViewDistributionFillProportionally
这个类型的StackView
是烂掉的。我刚才看了一下,苹果仍然没有修复这个bug。
具体的原因那篇文章中已经解释了,现在说下为什么FDStackView
没有这个问题,相信看完前面创建约束的过程,读者朋友应该就能发现我们并没有像UIStackView
那样将canvas
的dimensionAttribute
乘以一个系数作为arrangedSubview
的dimensionConstraint
。我们的arrangedSubview
的dimensionConstraint
是与canvas
无关的,是arrangedSubviews
之间的比例关系,而且spacing在之前的edgeToEdgeConstraints
中就已经创建了,这两者是分开创建的,所以算法不同,自然也就不会出现这个bug。
下面看其余的知识点:
如果一个已经布好局的StackView
,在一个arrangedSubview
被hidden
或者show
之后,那么其余的arrangedSubviews
也要做出相应变化,来相应这种变化。
在FDStackView
这里我们是通过KVO
监测每一个arrangedSubview
的hidden
属性,当任何一个arrangedSubview
属性发生变化后,我们就通过rebuild
的方式重新创建整个StackView
的约束,就是重新布局一遍。这是目前1.0
版本的处理方式,这样势必会带来性能的损失,这也是我们后续优化性能的关键。
intrinsicContentSize
发生变化时如何处理什么叫子视图的intrinsicContentSize
发生变化呢?举个例子,一个已经布好局的StackView
,其中有一个arrangedSubview
是一个UILabel
,但是这个UILabel
被重新setText
了,那么它的intrinsicContentSize
就会发生变化,自然StackView
的布局如果不发生变化的话就是错误的。所以在这种情况下StackView
也要做出处理。
这里我们研究了UIStackView
的实现方式,一个arrangedSubview
的intrinsicContentSize
发生变化如何被捕捉到,是我们未知的,UIKit
并没有暴露任何方法给我们,我们只能通过下符号断点的方式给dump出来的UIStackView
的私有类。
研究发现当一个arrangedSubview
的intrinsicContentSize
发生变化时,UIStackView
总会调用到_intrinsicContentSizeInvalidatedForChildView:
这个私有方法,参数为发生变化的arrangedSubview
。所以我们就把这一私有方法给替换了,借助UIKit
内部的机制来帮我们通知一个arrangedSubview
的intrinsicContentSize
发生变化的这种情况。
1 2 3 4 5 6 7 8 9 |
|
接到这种通知之后,我们目前也是通过rebuild
的方式来重建StackView
的约束的。其实对于这种情况以及上面提到的hidden
的情况,我们都能得到具体发生变化的那个arrangedSubview
,这也将会是后续优化的突破口。
到此整个FDStackView
的设计实现过程都介绍完了,当然还有一些零零碎碎的点没有说,都在源码里了。后续版本会增加Layout Margins
的支持,以及性能优化。
最后在附一张UIStackView
及FDStackView
在不同iOS
系统上加载运行图:
全文完,转载请注明出处,谢谢阅读。
————————————
]]>Part 1
就被接踵而至的新项目和新版本忙的不可开交,转眼间一个季度就已经过去了,而这篇Part 2
却迟迟还没有出现。实在是抱歉没有及时更新。不过有一个好消息就是FDStackView
已经被使用在我们自己的项目中,并且我们的项目也已经经过了两个版本的迭代,FDStackView
可以说还是相当稳定的,并且可以顺利的通过苹果的审核机制,对这方面有顾虑的小伙伴们可以放心大胆的使用了。同时我们也将它的版本号从1.0-alpha
升级到1.0
。在此感谢一下各位热心的小伙伴们在Github
上提出的issue
,以及着重感谢下@里脊串对FDStackView
的重度使用及提出的各种隐晦的bug
。后续我们将会对性能的优化做出改进,以及对Layout Margins
的支持。
回到主题,这篇文章主要介绍StackView
的实现,即如何通过现有AutoLayout
技术实现StackView
这样的一个控件。这里说明一下,当初我们编写FDStackView
的时候,UIStackView
还没有支持Layout Margins
,所以我们也没有添加Layout Margins
的支持,不过目前的iOS SDK
已经增加了这一部分的支持,所以在打开layoutMarginsRelativeArrangement
属性的情况下,StackView
创建出的约束会与我后面所介绍的内容有一些出入,不过问题不大,仅仅是部分约束的firstItem
由StackView
本身变成UILayoutGuide
的区别。
实现StackView
主要包括这几个技术点:
alignment
和distribution
的约束如何添加和管理;spacing
和distribution
的关系及约束的创建;intrinsicContentSize
发生变化时如何处理。我们对
UIStackView
进行了详细的研究,包括dump
出所有UIStackView
的相关私有类,各个类的方法,实例变量等。还需要添加符号断点来跟踪各个方法的调用顺序及各个实例变量的值得变化情况。同时还需要分析各个状态下UIStackView
的约束constraints
的情况,包括约束的个数,连接的方式,及约束所添加到的视图等。经过以上的各种分析之后,我们又通过在IB
中借助UIView
手动连接约束的方式,连出每一个UIStackView
所对应的状态。经过这一番调查与研究我们已经大概摸清的UIStackView
的工作原理与实现方式。
如上篇文章所说,在进行了详尽的研究之后,总结出大概需要攻克的是这几个技术点,以尽可能的与UIStackView
的实现保持一致,在难以完成的地方通过自己的方式实现。在这之前先介绍一下我们使用到的几个私有类。
CATransformLayer
StackView
是一个透明不可见的容器,主要就是因为这个layer
,我们继承了它并重载了两个方法,setOpaque:
和setOpaque:
,用于避免产生警告⚠️。也就是项目中的FDTransformLayer
。
_UILayoutSpacer
这是一个私有类,它的主要作用是用了辅助StackView
创建alignment
方向上的约束,它的父类是UILayoutGuide
,并不是一个UIView的子类,所以我们并不能以熟悉的方式对它添加约束。但是在知道了它的作用之后,我们完全可以使用一个UIView
来代替它,同时它也是不可见的,所以它的layer
自然也是FDTransformLayer
。这是项目中的FDLayoutSpacer
。
_UIOLAGapGuide
与_UILayoutSpacer
相同是UILayoutGuide
的子类,用来辅助distribution
方向上的约束创建,并且只有UIStackViewDistributionEqualSpacing
和UIStackViewDistributionEqualCentering
两种模式下它才会出现。在项目中我们通过UIView
的子类FDGapLayoutGuide
来实现它。
_UILayoutArrangement
同样是一个私有类,用来管理StackView
及其子视图的约束的创建。它是一个父类,在FDStackView
中我们使用FDStackViewLayoutArrangement
来与之对应。
_UIAlignedLayoutArrangement
该类是_UILayoutArrangement
的子类,用来控制alignment
方向上的约束的创建及管理,它维护了一个_UILayoutSpacer
并负责它的生命周期。在FDStackView
中我们以更直接的FDStackViewAlignmentLayoutArrangement
来对它命名。
_UIOrderedLayoutArrangement
与_UIAlignedLayoutArrangement
相对,用来控制distribution
方向上的约束创建及管理,它维护了一组_UIOLAGapGuide
。在FDStackView
中我们以更直接的FDStackViewDistributionLayoutArrangement
来对它命名。
先提前解释几个后面会提到的名词:
canvas
:canvas
是什么?翻译过来是画布的意思,其实就是容器也就是StackView
本身Ambiguity Suppression
:经常Debug``AutoLayout
的同学可能对这个词并木陌生,一般约束产生冲突或者模棱两可的时候,控制台就会输出一组信息,其中就会包含这个词。这里就是抵制模棱两可的约束的意思。StackView
中会创建一些低优先级的约束来完成这件事儿,以防止控制台打出AutoLayout
异常的log
。minAttribute
:是NSLayoutAttribute
一个便捷获取方式,针对不同的axis
会对应不同的NSLayoutAttribute
,可能是NSLayoutAttributeTop
也可能是NSLayoutAttributeLeading
。centerAttribute
:同样针对不同的axis
可能是NSLayoutAttributeCenterY
或者NSLayoutAttributeCenterX
。maxAttribute
:同样针对不同的axis
可能是NSLayoutAttributeBottom
或者NSLayoutAttributeTrailing
。dimensionAttribute
:同样针对不同的axis
可能是NSLayoutAttributeHeight
或者NSLayoutAttributeWidth
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
此外UIStackView
的约束的管理方式也十分的奇妙。除了一个例外的Ambiguity Suppression
的约束,其余不管约束何种关系的约束都是add在canvas
上的。既然约束都加在了canvas
上,那这么多的约束如何区分何管理呢?
这里有个小技巧,那就是用weakToWeak
的NSMapTable
来管理,key
是约束的firstItem
,value
是约束,而且因为NSMapTable
是weakToWeak
的,所以key
和value
所对应的object
并不会增加引用计数,不会带来内存上的管理困难。若要找一个view
所关联约束,直接取view
作为key
的value
就可以了。_UILayoutArrangement
维护了多个这样的NSMapTable
,分别来管理不同作用的约束。不得不说这样的设计真的是太巧妙了。
alignment
和distribution
的约束如何添加和管理先给一张图看一下什么是alignment
和distribution
以及Spacing
:
在介绍实现之前,我先介绍一下StackView
的各种alignment
模式都是什么效果的:
StackView
了,用得比较多。arrangedSubviews
按照firstBaseline
对齐。只能出现在水平的StackView
中。arrangedSubviews
按照lastBaseline
对齐。同样只能出现在水平的StackView
中。下面介绍实现,首先是alignment
方向,alignment
方向的约束主要包括4种
1 2 3 4 5 6 7 8 9 |
|
arrangedSubviews
与canvas
之间的约束;arrangedSubviews
有hidden
的时候,该arrangedSubview
的有关dimensionAttribute
的约束;_UILayoutSpacer
来管理的,它管理了spacer与arrangedSubviews
之间的约束,因为这些约束的firstItem
都是spacer自身,所以就不需要使用NSMapTable
而直接是NSArray
。另外spacer只有在alignment
不是UIStackViewAlignmentFill
的时候才会被创建,所以当alignment
是UIStackViewAlignmentFill
时,是没有systemConstraints的
;arrangedSubviews
之间的约束,它包括两组NSMapTable
,根据alignment
的不同具体的约束也不同,具体的NSMapTable
的key
与alignment
及axis
的关系如下表:可以看到除了UIStackViewAlignmentFill
模式以外,都会有一个Ambiguity Suppression
的key,这个key对应的NSMapTable
的就管理了前面提到的那些低优先级防止布局时出现模棱两可状态的约束。此外Baseline
相关的约束是只有在axis
为Horizontal
时才会有的,并且UIStackViewAlignmentFirstBaseline
和UIStackViewAlignmentTop
,UIStackViewAlignmentLastBaseline
和UIStackViewAlignmentBottom
的key值是相同的。
这个key的名字之所以这么取也是有讲究的,它代表着它所对应的NSMapTable
管理的约束关系。举个例子:axis
为Horizontal
,alignment
为UIStackViewAlignmentFill
时,key为Top
和Bottom
,那么Top
对应的NSMapTable
管理的约束就是arrangedSubviews
之间NSLayoutAttributeTop
相等的约束。同理Bottom
就是NSLayoutAttributeBottom
相等的约束。
这样结合alignment
的效果来看就很容易理解,UIStackViewAlignmentFill
模式需要arrangedSubviews
都充满容器,那么自然他们的NSLayoutAttributeTop
和NSLayoutAttributeBottom
需要都相等,而UIStackViewAlignmentTop
模式需要top
对齐那么只需要NSLayoutAttributeTop
相等就OK了。
这里还有一个点就是arrangedSubviews
之间的约束不是迭代添加的,而是都与第一个arrangedSubview
创建关系。假设有3个view
,那就是view2
与view1
建立约束,view3
同样与view1
建立约束而不是与view2
迭代建立约束。
这4种约束的创建顺序是:
FDLayoutSpacer的systemConstraints
canvasConnectionConstraints
alignmentConstraints
hiddingDimensionConstraints
FDLayoutSpacer的systemConstraints
在FDStackViewAlignmentLayoutArrangement
中被称为spanningLayoutGuideConstraints
,创建方法是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
首先判断一些不需要创建或者不需要更新这组约束的情况,比如之前提到的alignment
为UIStackViewAlignmentFill
或者没有arrangedSubview
的时候。接下来创建一个宽或高为0
的约束给spacer,因为对于后面添加的约束而言,spacer是缺少这样的一个约束以保证它能够正确布局。最后就是把每一个arrangedSubview
与spacer分别建立minAttribute
和maxAttribute
的约束,这些约束的constant
都是0
,但是关系却不一定都是等于,需要根据alignment
的属性不同来动态调整,有可能是大于等于,也有可能是小于等于。这需要查表来得到。
下一步创建canvasConnectionConstraints
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
因为这是alignment
的canvasConnectionConstraints
,所以只需关注它自己的minAttribute
和maxAttribute
两个方向与canvas
的约束即可,其余两个方向会在distributionLayoutArrangement
中创建。
特别的是如果alignment
是UIStackViewAlignmentCenter
的话需要加上一个centerAttribute
的约束。如果是alignment
是baseline
相关的话还要给canvas
添加一个高为0
的低优先级约束,用来满足某些特殊情况下canvas
约束不满足的情况。
具体与canvas
建立约束关系的firstItem
及relation
关系是根据alignment
类型以及NSLayoutAttribute
的不同而不同的,情况比较多我就不一一列举了,同样是根据查表得到,具体可以看代码去查。
最后是alignmentConstraints
和hiddingDimensionConstraints
,虽然前面说它们两个的顺序是一前一后创建,但其实并不是,它们可以说是一起创建的,首先取出第一个arrangedSubview
作为guardView
,然后循环遍历其余arrangedSubview
,先添加alignmentConstraint
,如果这个arrangedSubview
是hidden
的那么就会再添加一个hiddingDimensionConstraint
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
这里的alignmentConstraint
的创建都是guardView
与其余的arrangedSubview
创建relation
关系为相等的约束,而NSLayoutAttribute
的选择仍然是查表法,根据axis
和alignment
的不同而选择不同的NSLayoutAttribute
。
如果alignment
不是UIStackViewAlignmentFill
模式的话,就会给arrangedSubview
创建一个dimensionAttribute
为0
的低优先级约束,称为ambiguitySuppressionConstraint
放在上图中key
为Ambiguity Suppression
的NSMapTable
中。
现在解释一下本文章Part 1
中最后提到的UIStackView
当alignment
为UIStackViewAlignmentFill
时,最高视图隐藏掉,而其余视图没有变成第二个的视图的高度的bug
。原因就是在UIStackView
的中实现中AlignmentLayoutArrangement
是没有管理hiddingDimensionConstraints
的,所以当视图被隐藏了后,那个视图被添加了一个宽为0
的约束,视觉上看不到了,但是高方向的约束仍然存在,所以仍然会撑开StackView
,所以在FDStackView
中我们在alignment
方向上同时增加了hiddingDimensionConstraints
,视图被hidden
后,会在高度方向上也给他加上一个高0
为的约束,而且这个优先级也很有讲究需要跟它的contentCompressionResistancePriority
设为一样,这样才不会在AutoLayout
布局系统中当用户人为添加一个高度约束后产生冲突。
写了这么多,才写完第一个技术点的第一部分,内容确实比较多,我写的也比较乱,时间比较紧所以写作时间是间断的,所以思维也是间断跳跃的,还麻烦各位看官多多包涵。本来打算一篇写完的,但是这么长,还是有必要在分一下的,Part 2
就到这吧,其余的内容就在Part 3
吧。
————————————
]]>forkingdog
开源小组的一员。
近期我们的研究项目是FDStackView
,现如今已经完成了Alpha
版本的开发工作,并将其开源在了Github
上,项目地址。虽然现在已经完成所有的基本功能,但是仍需要在真实的环境中测试试用,欢迎大家将试用之后的问题反馈给我们,提issue
给我们,使我们更好的修复和完善FDStackView
,以便于更好的方便开发者们使用。
FDStackView
究竟是什么呢?在介绍FDStackView
之前,首先你需要知道UIStackView
是什么?UIStackView
是苹果在WWDC上发布iOS9
的时候新推出的一个UIKit
的视图,现在网上可以搜索到很多关于它的资料,关于介绍,如何使用等。简单来说就是可以使用它来做一些流式布局,开发者只需要将需要的视图丢到UIStackView
中,然后设置它的一些属性来展现所需要的布局,因此无需自己再去添加各种约束,所有约束不在由开发者自己去管理,这对于一些还不会使用AutoLayout
的开发者来说是一个福音。复杂来说,因为UIStackView
是可以嵌套使用的,那么再结合上一些简单的约束,那么就可以完成任何复杂的界面了。想想之前需要各种管理约束,而现在有了它只需要将视图丢给它,改几个属性然后界面就做好了,是不是爽到爆,开发效率又提升一个档次啊。下面提供几个介绍UIStackView
的文章,使还不太了解的同学可以了解一下,传送门在此:
iOS 9: Getting Started with UIStackView
介绍完UIStackView
的优势想必大家都已经跃跃欲试了,我自身对于这个控件都是十分的期待,因为在开发中你可以不用去写大段的创建constraints
的代码了,如果你使用xib
或者storyboard
的话,那么在IB
中你也不需要去连接各种约束了,这是多么棒的一种体验,而且在Xcode7
的IB
中右下角往常用来增加约束,修正视图的位置又新增加了一个stack
按钮,可以快速的将所选视图加入到UIStackView
中,可见苹果也是推荐开发者使用UIStackView
的。但是UIStackView
是在iOS9
才推出的,最低支持的系统也是iOS9
,这就蛋疼了,现在能有几个APP
是从iOS9
开始支持的,如此一来这个控件就成了鸡肋般的存在,再低版本下根本无法使用。自己在业务开发中经常会想这个需求用UIStackView
简直就是妙解,而我却还在这里痛苦的连约束……鉴于这个强烈的需求,FDStackView
出现了,它就是为了解决UIStackView
在低于iOS9
的系统下无法使用的问题。在FDStackView
之前也已经有了一些类似的开源项目,比如OAStackView
和TZStackView
,然而他们都不能满足我们的需求,局限性还是比较大的,比如不支持IB
,某些功能还没有实现,类名需要使用非UIStackView
,在我们看来这些对开发者来说都是不友好的,开发者需要的是一款功能完善,支持IB
,使用时完全无感,在Xcode7
上直接使用UIStackView
即可,接下来的事情交给FDStackView
就好,它负责将UIStackView
在低于iOS9
的系统上运行。需要注意的是如果使用IB
的话,那么IB
的Builds for
属性需要设置为iOS 9.0 and later
。如图所示:
这个技术项目有一大部分的时间,我们都是在做调研工作,首先我们需要把UIStackView
玩的很熟练,它的各种属性,各种状态以及他们的组合关系分别是什么样的,其次我们需要解决的问题有:
API
和控件创建一个和UIStackView
一模一样的控件FDStackView
;UIStackView
的时候使用我们的FDStackView
;FDStackView
获得Interface Builder
的支持。解决了以上三个问题后,那么这个项目基本上也就算是完成了,第一个是工作量最大的工程,它又可以拆分为以下几个技术点:
alignment
和distribution
的约束如何添加和管理;spacing
和distribution
的关系及约束的创建;intrinsicContentSize
发生变化时如何处理。首先我们假设在第一个难点已经解决的前提下去攻克其他的难点,毕竟有其他开源方案的存在,说明这个不是不可行的。
至于第二个难点,UIStackView
在低系统版本编译时会报找不到符号的error
,那么解决的思路就是在低系统版本将UIStackView
的符号写进去,然后在runtime
将符号与我们的FDStackView
做关联,从而使低系统版本也能够运行UIStackView
,而实际上在起作用的是我们的FDStackView
。这里使用到的黑魔法
就是汇编语言,网上已经有大神给出了类似的解决方案,对其进行优化和修改之后应该就能满足我们的需求。
最后一个难点就是使FDStackView
获得Interface Builder
的支持,因为我们是IB
的重度使用者,一个不能在IB
上使用的控件一定不是一个好控件。所以一定要让FDStackView
能够在IB
上使用,有一个方案就是直接使用UIView
然后把他的Class
指定为FDStackView
,将Axis
、Alignmen
和Distribution
等属性通过IBInspectable
使其可以在IB
中编辑和设置,但是这样一个是IBInspectable
在IB
中的显示效果很烂,说实话就是不好用,再一个就是用了UIView
没有办法像UIStackView
那样在IB
中可以直接预览布局效果,这就是很差的一种体验了。最好的方案就是在IB
中仍然使用UIStackView
,使其在IB
中有最佳的体验,然后借助上一难点的解决方案,在低系统版本中使用FDStackView
代替UIStackView
。这样就会带来两个其他问题:
IB
的构建版本是根据Project
的部署版本来的,如果项目不是支持iOS9
的话那么会报这样一个error
:”UIStackView before iOS 9.0”
;IB
构建出来的FDStackView
获得在IB
中给UIStackView
所设置的各种属性。
这两个问题,第一个只需要将IB
的构建版本设置为iOS9
及以后即可,目前来看是没有问题的,但是还不知道其他的控件被IB
搞成iOS9
的版本,在低系统版本上会不会有问题,这个还需要后续的验证。第二个问题,由于使用IB
创建的UIKit
控件都会由initWithCoder:
进行初始化,因此弄清楚NSCoder
的decode
过程就能将IB
设置的属性赋值给所创建的对象了。解决完以上两个难点,就可以回过头来研究第一个了,就是创建一个和UIStackView
一模一样的FDStackView
。这里我们对UIStackView
进行了详细的研究,包括dump
出所有UIStackView
的相关私有类,各个类的方法,实例变量等。还需要添加符号断点来跟踪各个方法的调用顺序及各个实例变量的值得变化情况。同时还需要分析各个状态下UIStackView
的约束constraints
的情况,包括约束的个数,连接的方式,及约束所添加到的视图等。经过以上的各种分析之后,我们又通过在IB
中借助UIView
手动连接约束的方式,连出每一个UIStackView
所对应的状态。经过这一番调查与研究我们已经大概摸清的UIStackView
的工作原理与实现方式。
与此同时我们还发现了两个UIStackView
的bug
,本以为在Xcode7
正式发布之后会得到修复,可是遗憾的是从我们开始研究的时候的beta5
到后来的beta6
、GM
和正式版这两个bug
依然存在,后面我会介绍一下这两个bug
。
下面介绍一下具体的实现细节,同样还是从第二个点说起,最终起关键作用的代码是这些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
首先说一下__asm
:
1 2 3 |
|
意思就是说在你的C
或C++
源代码中放入汇编代码用来替换任何C++
的符号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
先来说这一个部分,大神的解决方案给出了英文注释,尝试着直译了一下:
1 2 3 |
|
发现还不如不译,就直接说一下大概的意思吧。
第一行是取得符号所在的区间,之后区分64
和32
位系统,将_OBJC_CLASS_$_UIStackView
这个符号与自定的符号做一个weak
类型的关联。
接下来就是__attribute__((constructor))
这个黑魔法,这个标识的方法会在所有的类load
之后,main
函数调用之前调用。所以此时FDStackView
已经被load
了。再之后就是判断runtime
是否存在UIStackView
,不存在的话就根据不同的系统平台将指向_OBJC_CLASS_$_UIStackView
这个符号的指针存储在stackViewClassLocation
中,接下来通过runtime
创建UIStackView
这个类并作为FDStackView
的子类,并注册进runtime
,最后将UIStackView
作为stackViewClassLocation
这个指针的值。如此一来在低系统版本中UIStackView
就能作为FDStackView
的子类使用了。它没有重载任何方法,因此就跟使用直接FDStackView
一模一样。
接下来的问题是IB
加载出来的UIStackView
如何将属性值设置到我们的FDStackView
上,这个在前面研究是已经有结论,首先需要将IB
的build for
做下修改,然后IB
创建的UIKit
控件都会由initWithCoder:
进行初始化,所以所有的信息都在NSCoder
这个对象中,NSCoder
提供了一系列的decode
方法,由于key
是字符串,所以可以在汇编代码处直接看到,所以通过加符号断点的方式找到这几个key
。
如此一来就可以直接在FDStackView
的initWithCoder:
方法中取到值,再将这几个值赋值即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
最后就是通过系统的API
创建constraints
来实现FDStackView
了,这里涉及的内容比较多,包括几个辅助的私有类,及Alignment
和Distribution
方向上的约束创建,子视图隐藏,intrinsicContentSize
改变如何处理等。这里我们都尽可能的与猜测到的UIStackView
的实现保持一致。这些内容将会在后续的另一篇文章中介绍。
现在来说一下我们在调研UIStackView
时发现的两个bug
,测试的Demo
已经放在Github
上。
这个测试Demo
会借助我们的FDStackView
来演示对比出UIStackView
的bug
,上面是系统原生的UIStackView
,下面是我们的FDStackView
,两者的参数设置是完全相同的。
先来看第一个,当Distribution
设置为UIStackViewDistributionFillProportionally
时,并且存在spacing
时就会出现问题,如图所示:
UIStackViewDistributionFillProportionally
这个属性的意思是子视图的宽度会根据他们内容的宽度比例而在UIStackView
中占据对应的宽度,即他们的实际的宽度比应该是他们的内容固有宽度(intrinsicContentSize
)的比例,Demo
中三个Label
的固有宽度即汉字的宽度是4:1:2
,那么在UIStackView
中他们所占据的宽度也应该是4:1:2
,这在spacing
为0
的情况下是ok的。
如果存在spacing
的话,那么UIStackView
应该先减去子视图之间的spacing
,然后再去按比例分布子视图的宽度。这里可以看到UIStackView
的布局是烂的了,而FDStackView
的布局是ok的。
这里我们通过分析UIStackView
身上的constraints
大概得出UIStackView
出现这个bug
的原因是,他们的算法出了问题,他们这一部分的约束是这样添加的,每一个子视图的宽度等于UIStackView
的宽度乘上一个比例系数,即AutoLayout
计算公式y = m * x + c
中的m
系数,c
的值一直为0
。他们在计算m
的时候出了问题,忽略了spacing
的存在,也就是在计算中没有计算上spacing
的值。
具体拿Demo
来看的话,UIStackView
的最左边的Label
的宽度应该是这样计算的label.width = 4 * UIStackView.width / (4 + 1 + 2)
,这是spacing
为0
时,m
的值就是4 / (4 + 1 + 2)
,这没有问题 ,但是如果有spacing
的话,他们把spacing
也作为了分母的一部分,认为spacing
也是可以按比例显示宽度的,所以m
的值就成为了4 / (4 + 1 + 2 + spacing)
(这里的spacing
不是UIStackView
设置spacing
的值,而应该是实际UIStackView
中出现的所有spacing
的和)。因为spacing
被当作分母计算了进去,那么在布局的时候spacing
也应该按照计算出的系数乘上UIStackView
的宽度来显示,但实际上他们没有这么做,而是把spacing
按固定值来显示了,这样就会因为分母加入了spacing
导致所有子视图计算出的m
偏小,进而显示出来也就会偏小,到了最后一个视图时,由于约束优先级的缘故导致这个宽度的约束不再起作用,从而导致被拉长,出现了上图的效果。
所以这里UIStackView
是算法出了问题而显示时又按正确的样式来显示,所以布局就烂了,其实在有spacing
的状态下就不应该忽略c
的值了,而且spacing
也不应该参与到分母中去计算,正确的约束应该是这个样子的label.width = 4 * UIStackView.width / (4 + 1 + 2) - 4 * spacing / (4 + 1 + 2)
,这时c
就有值了,不再是0
而是-4 * spacing / (4 + 1 + 2)
。
整体来说UIStackView
在处理UIStackViewDistributionFillProportionally
这个属性的时候采取的约束添加方式不是最好的,处理起来是比较复杂的,这样处理会出现很多非整数情况,一个是计算复杂,在一个也会丢失精度。所以我们在FDStackView
中没有使用这种连接方式,而是使用了另外一种方法,后面的文章会介绍到。
另外一个bug
是当Alignment
属性设置为UIStackViewAlignmentFill
时,当一个最高的子视图隐藏掉了时,UIStackView
的高度并没有变化,这时它应该变为第二高的子视图的高度,具体如图所示:
这种情况只有在属性设置为UIStackViewAlignmentFill
时才会出现,具体的出现原因我们也有分析出来的结论,但是涉及到Alignment
方向上约束添加的问题,这个会在后一篇文章中提到,所以这里就先不做解释,之后在说。我们的FDStackView
修复了这个问题,但是在一种情况下也会失去作用就是给这个要隐藏的视图收到添加了一个高优先级的高度约束的情况下,不过一般情况下我们使用UIStackView
基本都不会再给子视图添加约束了。
第一篇文章就介绍这么多,后面我会找时间把第二篇文章(Part 2)整理出来。
————————————
]]>AutoLayout
时,在iOS7
和iOS8
上两者会有很大的不同,iOS8
苹果优化了很多。最近看了一篇bolg
,是Reveal
的工程师写的介绍使用Constraints
以及transform
变幻之后在iOS78
上的异同。
先贴张图说明一下问题:
可以看到iOS7
在使用了autolayout
之后,进行transform
变幻之后view
并没有达到预期效果,而在iOS8
和和纯frame
布局的情况下是正常的。
autolayout
使用的是Top
和leading
与灰色的view
进行约束,而frame
是通过setCenter
来设置位置的。
这种错误的现象会发生在iOS7
及以前的版本中,在iOS8
之后得到了修复。
通过Reveal
查看可以看到:
使用autolayout
的view
跟他的参照View
相比只移动了(-10,-10)
,而且它的布局位置也发生了偏移(10,10)
,在iOS8
下查看,会发现布局位置并没有移动,跟参照View
完全一致
由此可以得到的结论就是,在iOS7
和8
上使用autolayout
布局的view
的center
属性的位置发生了改变。
通过设置断点和重写setFrame
和setCenter
方法研究发现,在iOS7
和8
上setFrame
方法都没有被UIKit
调用到,而只有setCenter
方法被调用。
“If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.”
————UIView’s Class Reference
如果transform
属性不是identity
的,那么他的值就是不确定的而且应该被忽略。因此可以断定setFrame
方法没有没调用,因而view
的transform
属性也就不是identity
的,所以会出现问题。
至于具体的iOS7
和8
在NSISLayoutEngine
里面做了什么改变,可以查看博客原文。
最后说一下结论:如果我们的app
是使用iOS8
或者以后的SDK
编译链接的并且还要支持iOS7
,并在没有identity
的transform
的view
上使用了AutoLayout
。那么就应该注意一下几点:
transform
变换,那么就要使用CenterX/CenterY
约束,来替代Top/Bottom/Left/Right/Trailing/Leading
约束,因为如果transform
的view
是通过它的centre
布局的话,那么结果就有可能是正确的。view
放到一个containerView
里,然后用约束约束containerView
好过直接约束变换的view
。变换的view
可以直接用代码布局,也可以用CentreX/CenterY
约束。但是使用等宽等高与containerView
建立约束将不会达到预期效果constraint
来约束这些View
,使用autosizingMask
,然后设置这些View
的translatesAutoresizingMaskIntoConstraints
为YES
。PS:
最后算是做个广告吧,Reveal
这个工具真的是很NB
很好用,当你使用了之后就会爱不释手。它可以查看view
的层级关系,动态的改变UI
属性,在最近的版本还支持对autolayout
的支持,可以查看constraints
已经对他们进行修改,我们做iOS
开发的更多的是做界面开发工作,那么有了这样一个神器在手,那么必然会达到事半功倍的效果,工欲善其事,必先利其器!
Reveal
还能做更NB的事情就是当你手机越狱后,然后你就可以查看任何app
的视图层级关系了。就说到这里了,至于接下来怎样大家自己脑补吧。
既然这个工具这么强大,我们还是支持一下作者吧,同为开发者,都知道这行挺不容易的还是支持下正版吧,好消息是Reveal对中国的开发者们有个特惠价格:RMB249
就可以拿下了,购买地址,比半价还优惠,我在知道这个消息后第一时间拿下了它,因为之前的价格确实有点贵,对我来说还是有压力的。
————————————
]]>Cocoapods
是非常好用的一个iOS
依赖管理工具,使用它可以方便的管理和更新项目中所使用到的第三方库,以及将自己的项目中的公共组件交由它去管理。Cocoapods
的介绍及优点本文就不在赘述,我开始使用Cocoapods
还是在两年前,那个时候它刚刚出现,网上的资料还非常的少,就连他们自己的HomePage
都十分的简单,我就着手尝试着使用了一下,用它管理起第三方库确实是十分的方便顺手。后来它有了更强大的功能就是自己创建podspec
,更可以设置私有的库。
春节回来上班,一天的工作结束之后,需要充实下自己,正好项目中有一些公共组件需要从庞大的项目体系中剥离出来,而且年前项目终于从SVN
迁移到了Git
,真是喜大普奔,大快人心!这样项目使用Cocoapods
就有了条件,正好学习一下创建私有的podspec
并在项目中部署使用,以及pods
的subspec
的创建及使用。
整体先说明一下创建一个私有的podspec
包括如下那么几个步骤:
Spec Repo
。Pod
的所需要的项目工程文件,并且有可访问的项目版本控制地址。Pod
所对应的podspec
文件。podspec
文件是否可用。Spec Repo
中提交podspec
。Podfile
中增加刚刚制作的好的Pod
并使用。podspec
。在这一系列的步骤中需要创建两个Git仓库
,分别是第一步和第二步(第二步不一定非要是Git仓库
,只要是可以获取到相关代码文件就可以,也可以是SVN
的,也可以说zip包
,区别就是在podspec
中的source
项填写的内容不同),并且第一步只是在初次创建私有podspec
时才需要,之后在创建其他的只需要从第二步开始就可以。本文只介绍在Git
环境下的操作,其他环境其他方式暂不说明。
Spec Repo
先来说第一步,什么是Spec Repo
?他是所有的Pods
的一个索引,就是一个容器,所有公开的Pods
都在这个里面,他实际是一个Git仓库
remote端
在GitHub
上,但是当你使用了Cocoapods
后他会被clone
到本地的~/.cocoapods/repos
目录下,可以进入到这个目录看到master
文件夹就是这个官方的Spec Repo
了。这个master
目录的结构是这个样子的
1 2 3 4 5 |
|
因此我们需要创建一个类似于master
的私有Spec Repo
,这里我们可以fork
官方的Repo
,也可以自己创建,个人建议不fork
,因为你只是想添加自己的Pods
,没有必要把现有的公开Pods
都copy
一份。所以创建一个
Git仓库
,这个仓库你可以创建私有的也可以创建公开的,不过既然私有的Spec Repo
,还是创建私有的仓库吧,需要注意的就是如果项目中有其他同事共同开发的话,你还要给他这个Git仓库
的权限。因为GitHub
的私有仓库是收费的,我还不是GitHub
的付费用户,所以我使用了其他Git
服务,我使用的是CODING
,当然还有其他的可供选择开源中国
、Bitbucket
以及CSDN
创建完成之后在Terminal
中执行如下命令
1 2 |
|
此时如果成功的话进入到~/.cocoapods/repos
目录下就可以看到WTSpecs
这个目录了。至此第一步创建私有Spec Repo
完成。
PS:如果有其他合作人员共同使用这个私有Spec Repo
的话在他有对应Git仓库
的权限的前提下执行相同的命令添加这个Spec Repo
即可。
Pod
项目工程文件这个第二步没有什么好介绍的,如果是有现有的组件项目,并且在Git
的版本管理下,那么这一步就算完成了,可以直接进行下一步了。
如果你的组件还在你冗余庞大的项目中,需要拆分出来或者需要自己从零开始创建一个组件库,那么我建议你使用Cocoapods
提供的一个工具将第二步与第三步结合起来做。
现在来说一下这个工具,相关的文档介绍是Using Pod Lib Create
就拿我创建的podTestLibrary
为例子具体讲一下这里是如何操作的,先cd
到要创建项目的目录然后执行
1
|
|
之后他会问你四个问题,1.是否需要一个例子工程;2.选择一个测试框架;3.是否基于View测试;4.类的前缀;4个问题的具体介绍可以去看官方文档,我这里选择的是1.yes;2.Specta/Expecta;3.yes;4.PTL。
问完这4个问题他会自动执行pod install
命令创建项目并生成依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
以上是项目生成的目录结构及相关介绍。
接下来就是向Pod
文件夹中添加库文件和资源,并配置podspec
文件,我把一个网络模块的共有组件放入Pod/Classes
中,然后进入Example
文件夹执行pod update
命令,再打开项目工程可以看到,刚刚添加的组件已经在Pods
子工程下Development Pods/PodTestLibrary
中了,然后编辑demo工程,测试组件,我并没有使用提供的测试框架进行测试,这里就先不介绍了。
注:这里需要注意的是每当你向Pod
中添加了新的文件或者以后更新了podspec
的版本都需要重新执行一遍pod update
命令。
测试无误后需要将该项目添加并推送到远端仓库,并编辑podspec
文件。
通过Cocoapods
创建出来的目录本身就在本地的Git
管理下,我们需要做的就是给它添加远端仓库,同样去GitHub
或其他的Git
服务提供商那里创建一个私有的仓库,拿到SSH
地址,然后cd
到PodTestLibrary
目录
1 2 3 4 |
|
因为podspec
文件中获取Git
版本控制的项目还需要tag
号,所以我们要打上一个tag
,
1 2 |
|
做完这些就可以开始编辑podspec
文件了,它是一个Ruby
的文件,把编辑器的格式改成Ruby
就能看到语法高亮,下面我贴上我的podspec
文件,并在后面以注释的形式说明每个字段的含义,没有涉及到的字段可以去官方文档查阅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
编辑完podspec
文件后,需要验证一下这个文件是否可用,如果有任何WARNING
或者ERROR
都是不可以的,它就不能被添加到Spec Repo
中,不过xcode
的WARNING
是可以存在的,验证需要执行一下命令
1
|
|
当你看到
1 2 3 |
|
时,说明验证通过了,不过这只是这个podspec
文件是合格的,不一定说明这个Pod
是可以用的,我们需要在本地做一下验证,这就是第四步的内容了,第四步在具体说明。
podspec
文件如果从第二步过来,已经有了现成的项目,那么就需要给这个项目创建一个podspec
文件,创建它需要执行Cocoapods
的另外一个命令,官方文档在这里
1
|
|
执行完之后,就创建了一个podspec
文件,他其中会包含很多内容,可以按照我之前介绍的进行编辑,没用的删掉。编辑完成之后使用验证命令验证一下
1
|
|
验证无误就可以进入下一步了。
podspec
文件我们可以创建一个新的项目,在这个项目的Podfile
文件中直接指定刚才创建编辑好的podspec
文件,看是否可用。
在Podfile
中我们可以这样编辑,有两种方式
1 2 3 4 |
|
然后执行pod install
命令安装依赖,打开项目工程,可以看到库文件都被加载到Pods
子项目中了,不过它们并没有在Pods
目录下,而是跟测试项目一样存在于Development Pods/PodTestLibrary
中,这是因为我们是在本地测试,而没有把podspec
文件添加到Spec Repo
中的缘故。
在项目中编写代码,测试库文件无误后就可以开始下一步了,提交podspec
到Spec Repo
中。
Spec Repo
提交podspec
向Spec Repo
提交podspec
需要完成两点一个是podspec
必须通过验证无误,在一个就是删掉无用的注释(这个不是必须的,为了规范还是删掉吧)。
向我们的私有Spec Repo
提交podspec
只需要一个命令
1
|
|
完成之后这个组件库就添加到我们的私有Spec Repo
中了,可以进入到~/.cocoapods/repos/WTSpecs
目录下查看
1 2 3 4 5 6 |
|
再去看我们的Spec Repo
远端仓库,也有了一次提交,这个podspec
也已经被Push
上去了。
至此,我们的这个组件库就已经制作添加完成了,使用pod search
命令就可以查到我们自己的库了
1 2 3 4 5 6 7 8 |
|
这里说的是添加到私有的Repo
,如果要添加到Cocoapods
的官方库了,可以使用trunk
工具,具体可以查看官方文档
Pod
在完成这一系列步骤之后,我们就可以在正式项目中使用这个私有的Pod
了只需要在项目的Podfile
里增加以下一行代码即可
1
|
|
然后执行pod update
,更新库依赖,然后打卡项目可以看到,我们自己的库文件已经出现在Pods
子项目中的Pods
子目录下了,而不再是Development Pods
。
podspec
最后再来说一下制作好的podspec
文件后续的更新维护工作,比如如何添加新的版本,如何删除Pod
。
我已经制作好了PodTestLibrary
的0.1.0
版本,现在我对他进行升级工作,这次我添加了更多的模块到PodTestLibrary
之中,包括工具类,底层Model
及UIKit
扩展等,这里又尝试了一下subspec
功能,给PodTestLibrary
创建了多个子分支。
具体做法是先将源文件添加到Pod/Classes
中,然后按照不同的模块对文件目录进行整理,因为我有四个模块,所以在Pod/Classes
下有创建了四个子目录,完成之后继续编辑之前的PodTestLibrary.podspec
,这次增加了subspec
特性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
因为我们创建了subspec
所以项目整体的依赖dependency
,源文件source_files
,头文件public_header_files
,资源文件resource
等都移动到了各自的subspec
中,每个subspec
之间也可以有相互的依赖关系,比如UIKitAddition
就依赖于CommonTools
。
编辑完成之后,在测试项目里pod update
一下,几个子项目都被加进项目工程了,写代码验证无误之后,就可以将这个工程push
到远端仓库,并打上新的tag
->1.0.0
。
最后再次使用pod lib lint
验证编辑好的podsepc
文件,没有自身的WARNING
或者ERROR
之后,就可以再次提交到Spec Repo
中了,命令跟之前是一样的
1
|
|
之后再次到~/.cocoapods/repos/WTSpecs
目录下查看
1 2 3 4 5 6 7 8 9 10 |
|
已经有两个版本了,使用pod search
查找得到的结果为
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
完成这些之后,在实际项目中我们就可以选择使用整个组件库或者是组件库的某一个部分了,对应的Podfile
中添加的内容为
1 2 3 4 5 6 7 8 |
|
最后介绍一下如何删除一个私有Spec Repo
,只需要执行一条命令即可
1
|
|
这样这个Spec Repo
就在本地删除了,我们还可以通过
1
|
|
再把它给加回来。
如果我们要删除私有Spec Repo
下的某一个podspec
怎么操作呢,此时无需借助Cocoapods
,只需要cd
到~/.cocoapods/repos/WTSpecs
目录下,删掉库目录
1
|
|
然后在将Git
的变动push
到远端仓库即可
1 2 3 |
|
————————————
]]>@3x
的图片去进行完美适配。即不涉及到某一个模块在iPhone5
及以下的设备上是一个展示样式,在iPhone6
或着iPhone6 Plus
上是另外的一个展示样式。单纯是这样的需求通过Auto Resizing
和AutoLayout
就完全可以应付的了。以前硬编码写的view的frame通过乘上一个屏幕放大的比例系数也可以搞定。
可是目前设计同学提出在一个使用collection view
的页面中,之前是通过各种设备使用同一个大小的cell
,不同的屏幕上拉大的是cell
之间的间距来进行适配。现如今要改成只有在6 Plus
上要将cell
上半部分等比例放大,cell
中下半部分的文字的字号也放大。目前的需求就是这个样子,其实我觉得这样适配就不是苹果的设计规范,按道理说屏幕大了看的内容多才对,这样搞个等比例放大,跟没有适配在兼容模式下运行的效果似地。不过需求来了还是得搞。
这个cell
本来就是使用AutoLayout
做的,但是之前是定高定宽的,图片的宽度被限制死了,即使是在collocation view
的代理方法中将cell
设大,cell
的图片也不会变大。而且还需要把字体一并放大,以及图片左上和右下的两个图片的位置是要成比例设置的,这样之前设置的heading space
和 trailing space
是固定值,也不能达到要求。先来看一下这个cell
,因为项目的保密性我没有展示全部的内容,只是列举了一部分,但足以说明问题。
因为使用的是xcode6
,苹果提供了Size Classes
这样强大的一个功能,为的是制作adaptive UI
。就是只制作一套UI,但是可以适配多个屏幕尺寸以及选装方向,之前没有仔细研究过这个新特性,只是觉得他应该能应付当前的需求,因为使用它可以为每一种size
设置一套constraint
和字体大小。很好很强大!
但是当开始使用时发现,没有一个size
是能够单独区分出6 Plus
的portrait
模式的,到时能区分出landscape
模式。6 Plus
的portrait
模式使用的是Regular height
和Compact width
,其他的设备的portrait
模式使用的也是Regular height
和Compact width
。但是Any height
和Compact width
这个size
对应的是6 Plus
以外其他设备的portrait
和landscape
模式。如图所示,
这里正好不包括6 Plus
,分别设置了这两种size
发现不是预期的效果,所有的手机都使用了Regular height
和Compact width
的这个size
。后来有看见了这个优先级的表格,
一旦设置了这个size
那么就会优先加载这个size
,所以这条路目前是行不通的,我觉得一定有其他的方法来通过Size Classes
解决,或者通过代码修改优先级,或者使用某种方法标识6 Plus
。由于我时间比较紧急,所以就没有继续使用这种方式,有知道的大神求告知啊,感激不尽!
Size Classes
行不通,又想了其他两种方案,一个是为6 Plus
单独制作一个xib,让collection view在6 Plus
上加载这个xib上的cell
;另外一个是修改现有的cell
上的constraints
把固定图片的大小constraint
干掉搞成自适应的,自适应不了的constraint
通过IBOutlet
在代码中进行修改。权衡了一下这两个方案,第一个目前的工作量比较小,但是后期维护很恶心,而且这种做法实在是太low。而第二种虽然开始开上去很复杂,工作量很大,但是搞定之后,维护修改起来也容易很多,毕竟只有一个UI。
所以选择第二种开工,自适应很好搞,把最大的那个圆圈与父view的heading
和trailing
设为0
即可,难的是等比例,虽然用AutoLayout
有几个月了但是用得都是heading
、trailing
、vertical
、horizontal
、top
、bottom
还有width
和height
这几个constraint
,他们就能解决我之前遇到的所有布局问题,连alignment
的那几个constraint
我都很少使用。但是这次需要用到Aspect ratio
了,之前一直不知道这咋用,感谢这次需求让我知道了如何用他,最大的那个圆圈是正方形设置了左右间距就确定了宽度,而高度的确定就用Aspect ratio
设置为1:1
即可,同样圆圈左上和右下的图也是需要等比例放大的,他们的比例也是通过Aspect ratio
来设置,这里的比例按照设计图写一下即可。这里发现xcode
一个很奇怪的问题,就是按住ctrl
从一个view拖向另外一个view选择Aspect ratio
的时候,xcode
为你生成的是一个view的height
与另一个view的width
的比,这个我一直不是很理解,虽然如果你在设置Aspect ratio
之前把他们的frame
设为正确的话,生成的比例是正确的,但是这样真的很奇怪,可读性特别差,为什么要一个高比上另外一个的宽呢?我承认我数学学的不好,这里可能有其他的深刻含义,但是我觉得好奇怪,如果有人知道欢迎留言评论!所以这里我又手动的把一个view的height
改为width
,即宽比上宽,按照设计图修改一下比例。还有一个问题是圆圈的左上有右下的view的位置是要相应改变的,也是按照比例,设置固定的值肯定是不行的,因为值也是会变得,这里再次感谢这个需求,让我又一次加深的AutoLayout
的理解,AutoLayout
中的约束其实是一个二元一次方程,如图所示
First item = Second item * Multiplier + Constant
,
一个值是可以通过另外一个值通过这个方程式计算出来的,而我们所加的约束就是设置了Multiplier
和Constant
,再加上优先级,两者的属性(上下左右宽高等)以及两者的关系(大于小于等于),这些共同组成了一个constraint
。 xcode默认创建的constraint
的Multiplier
为1
,我之前使用的所有constraint
也都是使用的是1
,从没有改变过他,从方程式来看这里是可以按比例设置的。不得不说AutoLayout
真的很强大,自己用到的仅仅是冰山一角。
那圆圈右下角的view举例子,首先设置他的x轴距离,据父view一个是heading
一个是trailing
,因为父view的heading
是0
,无论Multiplier
设置什么相乘都是零,所以只能用trailing
,因为我们事先已经按照设计图将一个尺寸的界面拼出来了,当设置了trailing
之后,xcode
生成了一个Multiplier
为1
,Constant
为一个固定值的constraint
,这样不对,不能够按比例移动x轴位置,所以我们把Constant
设为0
,由公式算出Multiplier
为First item / Second item
并进行设置。确定了x方向的位置还需要y方向的位置,y这里又出现了另外一个坑,y方向父view的top
为0
不能这是比例,bottom
由于有Label
所以bottom
的值是不确定的,所以就不能与父view做约束了,只能选择与圆圈做约束,这里使用的是align bottom
。同理因为是按比例放大,这个constraint
也不能使用定值,所以Constant
设为0
,公式算出Multiplier
并设置。同样圆圈左上的view也这样设置即可。如此一来cell
上半部分等比例放大的问题就搞定了。
cell
下半部分Label
之间的间距这个是不能自适应的,6 Plus
和其他设备是两个不同的值,这样就只能把他们的constraint
拿到代码中去进行修改,label上字体也是只能在代码中cell
第一次加载时判断为6 Plus
就将他们的字体放大。将这一部分逻辑放在了aweakFromNib
中
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这里要说一下,判断6 Plus
不能通过设备号去判断,iPhone7,1
和iPhone7,2
虽然是iPhone6
和iPhone6 Plus
没错,但是我们是在渲染界面,iPhone6
和iPhone6 Plus
是有一个放大模式的,在放大模式下iPhone6 Plus
的屏幕尺寸是iPhone6
的,iPhone6
的屏幕尺寸是iPhone5
的,所以如果通过设备号去判读那么渲染出来就是错误的,正确的方式是通过屏幕的尺寸来判断即[UIScreen mainScreen].bounds.size
。
写的很乱,因为是当天搞的,当天就记录下来,怕以后忘了,文笔不好,就凑合看吧,最后总结一下:
Size Classes
还有待研究,如何区分出iPhone6 Plus
Aspect ratio
,用于标记一个view的宽高比或者两个view的宽高比AutoLayout
中使用Multiplier
进行数值成比例改变的需求————————————
]]>iOS
开发的过程中,对于一些复杂的界面,我们可以通过Interface Builder
这个Xcode
集成的可视化界面编辑工具在完成,这回节省大部分时间以及代码量。它的使用方法这里不做介绍了,这次我要介绍是使用它来实现一个嵌套的自定义视图。解释一下就是,我们使用IB
自定义了一个View
,然后又在其他的xib
文件中使用了这个View
,那么这就是所谓的嵌套自定义视图。之所以要介绍它,是因为我自己在使用它的时候遇到了一些问题,一方面写下来做个记录供自己查看,另一方面我相信大家在使用的时候应该也会遇到这样的问题,方便大家。
下面使用的示例代码我已经放到Github
上了,项目地址,有需要的朋友可以去查看,Demo
非常简单,主要是介绍这个知识点。
首先我们创建一个SingleView
的工程,项目使用StoryBoard
,(使用Xib
也无所谓,因为有些老的项目可能还没有使用到StoryBoard
),然后创建一个CustomView
作为我们的自定义视图。
有时对于复杂的界面我们可能会拆分出来对它进行单独处理,又有可能它的界面布局很复杂,这时我们就会用Interface Builder
对它的布局进行处理。这里的CustomView
就是这样一个视图,所以我们为它创建一个xib
文件,我们通常的作法就是把xib
中的View
的custom class
更改为我们的CustomView
。
接下来对我们的界面进行布局,并连接输出口,编写响应逻辑,这里我放了一个ImageView
和一个Label
在这里,并把View
的背景色设置为浅灰色。
自定义的View
制作完成,回到我们ViewController
的xib
文件,拖入两个View
并把他们的custom class
更改为CustomView
。
这时,我们算是工作做完了,运行程序,结果悲剧了,怎么不是我们想要的结果,为什么只生成了两个空白的视图,我们视图上的图片和文字哪里去了?
在CustomView
中的awakeFromeNib
方法中增加断点调试发现,在CustomView
初始化完成后,ImageView
和Label
并没有被初始化,他们仍然是nil
。这就是在嵌套使用xib
自定义视图时非常容易出现的问题,我们觉得被嵌套的视图能够正常显示出来,但是实际上它并没有被按照我们在xib
上指定的方式被初始化。
那么如何解决这种问题,以及这种问题又是如何出现的呢?其实这主要是由于我们对xib
文件的加载原理不熟悉所导致的,我们以为定义一个View
,创建一个xib
文件并布局好它的子视图,让后将它使用在另外一个xib
文件中,把custom class
改成它,然后xib
的加载系统会自动为我们做好其余的一切。其实并不是这样的。
这样做xib
加载系统只会为我们创建一个CustomView
的对象,但这并不包括CustomView
所对应的xib
文件中的部分,所以只创建了一个空白的View
。
解决他们有两种方式,不过最终的思路都是通过代码强制使CustomView
的xib
部分被加载。第一种是通过代码创建CustomView
的对象,然后addSubview
到viewController
的view
上。第二种是在CustomView
的实现文件里,通过重载一些方法,来完成加载xib
文件。
这两种方法各有利弊,第一种使用起来方便也好理解,但是当嵌套的层级比较多的时候或者一个View
中有多个这样的CustomView
时,这种方式就会显得过于麻烦。而第二种虽然理解起来有些难度,但是当你处理好之后,直接在需要的xib
文件中拖入view
,改个custom class
,就能直接生成需要的对象了,并且也能够在xib
中对他们进行直接布局,不再需要用代码去布局了。
先来介绍第一种方法,很简单,就是找到xib
文件,生成对象,设置属性,addsubview
到视图上。
第二种方法是通过重载initWithCoder
方法来实现,因为通过xib
来创建一个对象会调用到这个方法,所以我们需要在这个方法里做一些处理,把这个CustomView
的xib
中的内容加载进来,这时同样是需要通过代码来来加载,首先附上代码
1 2 3 4 5 6 7 8 9 10 |
|
此外,还要这里的输出口以及设置custom class
的位置跟第一种方式有所不同,这里需要取消掉xib
中view
的custom class
,再将跟它连接的图片与文字的输出口取消掉,在这里这个view
只是被当做一个容器来处理,它跟Customview
没有直接关系,它将来会被addSubview
到CustomView
上,除此之外还要把xib
的File's ower
的custom class
改成CustomView
,表示这个xib
文件的持有者是CustomView
。再把它与图片和文字通过输出口连接起来。
这个时候在运行程序就看到了我们想要的结果了。^_^
其实想要实现第二种解决方案所要的效果,还有一种方式,它是通过重载awakeAfterUsingCoder:
方法来实现的,这个方法的返回值会替换掉真正的加载对象,所以在具体的加载CustomView
的方式又与第一种相同,所以xib
的输出口连接与custom class
的设置也与第一种解决方案相同。不过这种方式是更复杂也更难于理解的,不推荐使用,因为上一个方法就能很好的解决这个问题了,这里只是贴出这个方法的代码,有想仔细研究的请参看文章底部的参考文章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Embedding custom-view Nibs in another Nib: Towards the holy grail
————————————
]]>Blog
好久没有更新了,一个是因为这半年里要忙着毕业好多事情,再一个就是工作上也很忙,基本就没有时间来为我的Blog
增添新的血液了。APPLE
在WWDC 2014
上公布了一门新的编程语言Swift,最近可以说是相当火热,而且在语言热度排名上也是突飞猛进,这是一个集合了N多语言优秀特性于一身的全新语言,它将成为开发iOS
和MAC
的新的选择。并且据说他将会取代Objective-C
,但是我觉得并不是这样,不过这也仅仅是一家之谈,至于会不会这样,我们走着瞧。
既然苹果放出了这样一个利器,身为iOS开发者
的我也不能落下,APPLE
为我们提供了两个文档The Swift Programming Language和Using Swift with Cocoa and Objective-C。他们一个是纯语言角度的介绍Swift
,包括各种细节语法,另一个则是介绍怎么它怎么与Cocoa
交互,如何使用它开发iOS
和MAC
应用,以及如何与现有的程序兼容。学习这样一门全新的技术我还是建议读第一手资料的,虽然现在网络上有好多中文版的资料了吧,但是苹果的文档写的很是通俗易懂,读起来也没有什么困难。而起自己之前有过脚本语言的经验,所以看起来也是很快。
读完两个文档,就做一些实战的内容,先从小程序开始,之前自己写过一个进制转换器
,没啥功能,就是提供一个十进制和十六进制互相转换的功能,主要还是为了方便自己在写一些颜色值的时候使用。之前的版本是用Objective-C
写的,那么这次就用Swift
重写一遍。
这个程序最主要的部分也就是两个进制相互转换的算法了,用Objective-C
实现起来很简单,通过一下字符char
的运算就能搞定。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
但是当我使用Swift
重写的时候,我进入到了一个很深的坑里,发现这项任务是如此的难做,我定义了这样两个方法,注意方法的参数和返回值:
1 2 3 4 |
|
我使用的是Swift提供的String
类型,它是由一系列的Character
类型的字符组成的,但是这种Character
并不是char
,他们在处理进制转换这个问题上让我无从下手,也有可能是我还不够熟悉,不知道正确的用法,如果有人知道欢迎告诉我。下面来说一说我遇到的问题,首先在Swift
中是没有‘A’
这种字符表示方法的,更不用说用它去进行运算了,其次Character
不能进行大小比较,只能进行想等或者不等的比较,这样一来在判断一个字符所在的区间上就遇到了很大的问题。
Swift
的String
提供了uft8
和uft16
方法,返回值为UTF8View
或者UFT16View
,这是一个Array
,使用for-in
遍历他们可以输出他们的数字值,但是这个值也仅仅是能进行输出使用,他们不是Int
也不能进行加减运算。不能进行运算在转换上就无能为力。单单从这里看来,Swift
确实蛋疼,连这么一个小小的问题都不能搞定。
不过还好,Swift
对Cocoa
做了兼容使得String
与NSString
可以无缝转化,在使用了NSString
后,问题的处理就变得简单多了,在Swift
中NSString
使用一系列的unichar
组成的,查看声明可以看到他其实就是UInt
,那么他就可以进行运算,所以把String
改成NSString
完成这两个方法。即使这样,Swift
不支持‘A’
这种字符的特性,也使得我们必须自己把字符转化为数值来运算,使得程序的可读性很差,还就是Swift
的内置类型不支持隐式转换,所以在类型不一致的地方都需要强制转换一下。
这两个方法的实现在文末的代码中有,这个代码虽然完成了功能,但是还有很多需要改进和优化的地方,其次在代码风格上也需要改一下,Raywenderlich的Swift
的代码风格就很不错,非常值得套用。
最后说一下,在iOS8
中UIAlertView
和UIActionSheet
被废除了,而引入的是UIAlertController
,通过preferredStyle
来确定类型,通过UIAlertAction
来增加事件,然后通过presentViewController
来显示出来,在使用上做到了统一,还是蛮方便的。
下面附上完全的代码,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
|
Lua
我就不介绍了,我们都在使用的脚本语言,游戏开发的神器。
而Wax
就是使用Lua
脚本语言来编写ios原生应用
的一个框架,它把Lua脚本语言和原生Objective-C
应用编程接口(API)结合起来。这意味着,你可以从Lua里面,使用任何和全部的Objective-C
类及框架。
苹果在2010年9月就修改条款允许开发者使用脚本语言,不再是只限定开发者只能使用Objective-C
和javascript
两种语言,这也就导致了Wax Lua
的出现。
Wax Lua的优势:
1. 开源、免费,遵循MIT协议。项目地址:[Wax Lua](https://github.com/probablycorey/wax)
2. 可以使用原生API,可以访问所有ios的框架。
3. Lua类型和OC类型自动转化。
4. 自动内存管理。
5. 便捷的Lua模块,使得HTTP请求和JSON解析容易且快速。
6. 简洁的代码,不再有头文件,数组和字典等语句。
7. Lua支持闭包,相当强大的功能。
当年风靡一时的《Angry Birds》就是使用Wax Lua
开发的,不过一个不幸的消息就是Wax Lua
框架在2011年,即两年前原作者就不在对它进行维护了,所以不能确定在如今XCode5
,iOS7
时代它是否依然可以用。我觉得作者不再维护它是有原因的,现在来看他的优势已不再有这么多了,iOS4有了block,就有了Lua的闭包的功能,iOS5有了ARC,也可以自动管理内存,iOS6简化了OC代码,使代码脚本化,再也不用长长的数组与字典语句了,iOS5自带的NSJSONSerialization和强大AFNetworking也使HTTP请求和JSON解析相当便捷。如此看来Wax Lua
的优势也所剩无几了。
说一下Wax
的特点,它支持你在脚本里使用任何OC
的类,同样也支持你创建一个类。
使用一个类时你会这样使用:
1 2 |
|
这样调用其实一个语法糖,实际上他调用的是wax.class[“UIView ”],但是我们在使用的时候不需要知道这些,因为在这个框架里已经通过设置元表的方法实现了这一点。
当定义一个类的时候会是这样:
1
|
|
遵循协议的类:
1
|
|
在你定义这个类的脚本文件里缩写的其他function都将作为这个类的实例方法。且这个方法的第一个参数必须是self
,这就是Wax模仿Objective-C的面向对象的关键所在。
因此在Wax中调用方法要使用冒号,类似这样:
1
|
|
其实他就等同于这样:
1
|
|
在调用含有多个参数的方法时候,使用_
来代替OC中的:,例如:
1
|
|
1
|
|
使用Wax
创建对象不需要你alloc
,因为他会帮你实现内存管理,它是怎么实现的稍后再说。
Wax
不支持属性Property,因此你不能使用OC中的点语法,Wax
要求Lua与OC的通信必须通过方法来完成,就是如果你要访问一个Property的话就只能使用它的setter
和getter
方法。
如果你在脚本中使用了点语法,那么你将为这个对象创建一个实例变量,但这只是在Lua层面的,在OC层面它并不知道你创建了这样一个实例变量。
Wax
会强制的把OC的对象转换成Lua的对象,同时他也支持反向转化,比如一个方法需要NSString
类型的参数,你可以直接传递Lua的字符串进去。
有时你不想让OC对象被强制转化成Lua的,它也提供了相应变回OC对象的方法。
Wax
对枚举和结构的支持并不是很好,就是它需要把你需要用到的枚举和结构都按照他定义好的格式添加到APP_ROOT/wax/stdlib/enums.lua和APP_ROOT/wax/wax-scripts/structs.lua
中,只有这样你才能正常的使用它们。
Wax
对协议的支持也不是很好,有的协议在Wax
中可以正常使用,有的则不可以,你在源文件中会看到ProtocolLoader.h这样一个文件,他需要把不支持的协议预先加载到runtime中,作者自己也不知道这是为什么,也许是一个他不知道的runtime method。
Wax
也是不支持分类的,不过这个使用的比较少,不支持也没有什么。
我们知道OC是一门动态语言,他的runtime
很强大,强大到你可以在运行时动态的创建一个类,而Wax
真是借助于OC的runtime
实现了它一系列的功能。
目前我们在使用的CCLuaObjcBridge
,这个类也是实现了Lua调用OC的方法,他借助的也是runtime
,但是跟Wax
比起来,他就简单了很多,从他的限制就能看出来,它只支持类的静态方法,方法只能有一个参数,不能创建对象,不能调用实例方法。它的实现是这样的:通过类名找到类对象,通过预先定义好的只能包含一个参数或没有参数的方法名生成selector
,再根据类对象和selector
生成NSMethodSignature
,进而由NSMethodSignature生成NSInvocation,进行方法调用,再加上参数和返回值的Lua与OC的类型转换,就完成了一次OC方法的调用。
下面再说一下Wax
。Wax
的源码中有这样一个文件wax_helpers.h/wax_helpers.m
,它提供了一系列的工具方法包括lua与OC的类型之间相互转化,lua中使用_的方法名转化为OC中:的selector,根据lua传递过来的方法名找到对应的selector
等方法,有兴趣的同学可以去看看代码。
Wax
主要是维护了这样的一个结构,基本上所有与对象有关的操作都是在这个基础上完成的:
1 2 3 4 5 6 |
|
第一个instance
就是OC对象的一个指针,isClass
标识这是不是一个类对象,isSuper
用来标识他的父对象,类似以OC中的isa
指针,这么做是为了在方法调用时子类如果找不到的话就会由此去父类查找,actAsSuper
用来标识这个对象是不是被当做父类来使用,Wax
中一个对象智能被当做父类一次。
Wax
中还维护了两个表,一个UserDataTable
一个StrongUserDataTable
。这两个表中都存储的是Wax_instance_userdata->instance
为key
,Wax_instance_userdata
为值的键值对。 UserDataTable
是一个值为wake的弱表,他用来存储所有创建的对象,是一个弱引用,他其中就存储了通过lua创建的OC对象,因为是弱表,所以当不在使用时会调用__gc
这个元方法,进而将该OC对象销毁。StrongUserDataTable
是一个强引用表他保存的是所有通过Wax
创建的对象,他不是一个弱表所以需要手动管理内存。也就是说使用Wax
创建的对象除了会在UserDataTable
中保存一份以外还会在StrongUserDataTable
保存一份。
说到这里就在说一下Wax
的内存管理,Wax
的内存管理也是基于引用计数的,而且他没有使用AutoReleasePool
。所有引用计数的操作都在框架里为你实现好了,所以在lua里你不能调用alloc
方法,而要直接使用init
方法,因为他会判断你的方法是不是init
初始化方法,如果是的话Wax
会帮你调用alloc
方法。对象的release
有两种一种是UserDataTable
中的对象会在__gc
元方法中release
,另外一种就是在Wax
运行的时候有一个定时器timer,不停地轮询StrongUserDataTable
中的对象的引用计数如果小于2,那么就会release
。
Wax
创建类和对象以及方法调用都是通过元方法来实现的。
先来说创建类,就是通过定义的类名以及父类,在运行时通过字符串以及运行时的API创建一个类,通过class_addMethod()
函数给创建的这个类注册方法,而这个方法的实现就是一个IMP(函数指针),Wax中IMP是这样的一类方法,方法包括lua中用户自己写的function,在OC的层面又对这个function的参数和返回值进行了OC与lua的互转,这两部分组合起来构成一个方法。也就是当调用一个用lua写的方法的时候会首先把参数转化为lua类型然后由lua_pcall()
调用lua中的方法,完成后再把返回值转换成OC类型的。
最后说一下Wax
的方法调用,无论是OC自己的方法还是用户自己写的方法最终都是去调用这个IMP(函数指针),所以在这之前无论是调用OC原生的方法和用户自己定义的方法,处理的方式都是一样的。在元方法__index
里将方法的调用作为一个closure push到lua中,在元方法__newindex
中进行方法的override。在closure中的方法调用就和CCLuaObjcBridge
一样了,都是先获取到selector
,生成NSMethodSignature
,然后生成NSInvocation
,然后调用。与CCLuaObjcBridge
不同的地方就是由于这个对象是wax_instance_userdata
中的instance
,而不是由类名生成的类对象,所以他可以调用实例方法。
以上仅是个人一些理解,自身对Lua的C API和OC的runtime的API不是很熟悉,Wax
中使用了大量的这些API,所以有不对的地方还请指出来。
shell
编写脚本来简化操作。我使用过众多别人编写的脚本,但是自己从来没有写过。今天正好碰到一个问题,不想繁琐的去挨个操作,那么就尝试着写一个脚本来方便操作一下吧。
首先说一下自己遇到的问题,今年去参加了CocoaChina开发者大会
,会上的PPT
很好,今天在网上把他们下载了下来,但是问题是,他们的命名很长而且前一部分都是一样的,类似于"CocoaChina2013开发者大会-……",前一部分都是重的,在Finder
里看起来很是不方便,根本看不到有用的文件名信息,所以我决定要把这十多个文件重命名去掉前缀。
从来没有编写过shell
脚本,向来都是使用shell
命令的我,先从学习编写shell
脚本文件开始吧!
在文本编辑器中第一行(必须是第一行)首先键入#!/bin/sh
,符号#!
用来告诉系统它后面的参数是用来执行该文件的程序。
在shell
中#
代表注释,直到这一行结束。
在shell
中,变量都由字符串组成,变量名无需提前声明,写了就可以直接赋值,变量名=值
(这里等号两边一定不能有空格)。
取变量时要使用$
符号。有时在一长串字符串中包含变量,可以对变量名加上{}
来区分。例如有一个变量num=2
echo "this is the $numnd"
,这样会有问题,我们要写成echo "this is the ${num}nd"
。
只写一下本次用到的for
循环,其他的流程控制以后用到时在学习。
for-loop表达式查看一个字符串列表 (字符串用空格分隔) 然后将其赋给一个变量:
for var in ….; do
….
done
在下面的例子中,将分别打印ABC到屏幕上:
1 2 3 4 |
|
这里参考的文章是Linux shell脚本编写基础
在我写的shell
脚本中则是使用循环输出当前文件夹下文件的名字:
1 2 3 4 5 |
|
接下来就是对获取到的每一个文件名的字符串进行截取,删掉不需要的部分:
1 2 3 4 5 6 7 |
|
这里学习了一下shell
字符串的操作知识,:
选取子串 #
正向截取子串 %
逆向截取子串 ##
正向最长匹配 %%
逆向最长匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
这里参考的文章Bash Shell字符串操作小结
最后就是将文件重命名了:
1 2 3 4 5 6 7 8 |
|
运行时发现只有部分文件被成功重命名了,其余的都不成功,而这些不成功的文件名中都包含空格,空格是很大一个问题,在获取原文件名时文件名就被空格截断了,导致文件名不全。因而重命名也是失败的,找不到源文件。
参考这篇文章SHELL技巧:处理文件名中的那些空格
找到了解决方法,对变量添加""
使空格被正确处理,这不是最好的方法,但是在我这个小小的脚本中完全可以了。
文章中还介绍了一种终极解决方法就是设置IFS(the Internal Field Separator)
,但是在设置之前先保存当前的IFS
,操作完成之后在设置回去。
1 2 3 4 5 6 7 |
|
但是我这样使用之后经过测试发现,并不能解决问题,文件名被读取成其他怪异的形式了,空格是被正常读取了但是-
也被读取成空格了,我不知道这是为什么,有知道的欢迎告诉我。
最后附上自己写的完整的shell
脚本,虽然很短,但这是第一次写,也算是个入门吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
shell
脚本时不能延续写其他代码乱加空格的习惯,空格在shell
中很重要,随便加空格会导致shell
脚本执行失败。basename
命令时,获取到的文件名如果有空格的话将不会获取之后部分,可以对变量添加""
解决问题。cp
mv
等命令要求文件命中同样不能有空格,也可以在脚本中对变量添加""
解决。shell
脚本时最好写一句测试一句,要严谨不能想当然。策略模式(Strategy Pattern)
,相比之下是一种比较简单的模式。它也叫政策模式(Policy Pattern)
。
策略模式
使用的就是面向对象的继承
和多态
机制,其他的没有什么玄机。策略模式
适合使用在:
1. 多个类只有在算法或行为上稍有不同的场景。
2. 算法需要自由切换的场景。
3. 需要屏蔽算法规则的场景。
使用策略模式
当然也有需要注意的地方,那么就是策略类不要太多,如果一个策略家族的具体策略数量超过4个,则需要考虑混合模式
,解决策略类膨胀和对外暴露问题。在实际项目中,我们一般通过工厂方法模式
来实现策略类的声明。
下面我们就来具体讲解一下策略模式
。
Define a family of algorithms, encapsulate each one, and make them interchangeable.(定义一组算法,将每个算法都封装起来,并且是它们之间可以互换。)
迪米特法则
是相违背的。1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
UDID
之前在iOS
开发中广泛使用的一串字符串,用来标示唯一的设备,它本身并不包含用户信息,但是广告商却可以利用他获取到个人信息,从而发送广告,因为他涉及到隐私问题,所以苹果早在2011年就提出了将不再使用它。然而就在前天,苹果的开发者网站,发出新闻:
Starting May 1, the App Store will no longer accept new apps or app updates that access UDIDs. Please update your apps and servers to associate users with the Vendor or Advertising identifiers introduced in iOS 6. You can find more details in the UIDevice Class Reference.
“自5月1日起,App Store将不再接受任何使用UDID的app,苹果建议开发者转用iOS 6 引进的Vendor 或者 Adverstising Identifier(‘广告标识符’)”
这次苹果打出了强制通知,再使用UDID
的话,那么应用将不会审核通过,也就无法发布。
不过俗话说的好“上有政策,下有对策”。虽然苹果自己也给出了替代的方法,但他们都不是最好的,UUID
每次获取都不同,所以使用时必须要把他们存文件,存数据库或者存UserDefault
。当应用被删除重装,那么这个UUID
也就不同了。Vendor
更是同一个设备上的同一个开发商的应用的id
都是相同的,也没有办法使用。Advertising identifier
也并不是固定的。那要如何才能达到我们的需求呢,既能方便获取又能保证唯一呢?
答案就是MAC
地址,MAC
地址在网络上用来区分设备的唯一性,接入网络的设备都有一个MAC
地址,他们肯定都是不同的,是唯一的。一部iPhone
上可能有多个MAC
地址,包括WIFI
的、SIM
的等,但是iTouch
和iPad
上就有一个WIFI
的,因此只需获取WIFI
的MAC
地址就好了,也就是en0
的地址。直接把MAC
地址拿出来使用是不安全的,因此对他们做一次hash
计算,MD5
就是一种哈希算法,对得到的MAC
地址计算一下他的MD5
值就好了,那么这样拿到的就是这个设备唯一的ID
了。有时我们为了区分设备上的应用,也可以获取到应用的bundleID
,在和MAC
地址结合起来计算一下MD5
,那么该值就是可以区分设备上应用的ID了。
首先是MD5
,为NSString
添加MD5
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
接下来是获得唯一ID,为UIDevice
添加方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
|
如此一来,在使用时就相当方便了。
]]>适配器模式(Adapter Pattern)
,适配器模式又叫做变压器模式
,也叫做包装模式(Wrapper)
,但是包装模式
却不止一个,装饰模式
也是包装模式
,以后会介绍到。适配器模式是一种补救模式,他可以让你从因业务扩展而系统无法迅速适应的苦恼中解脱出来。我们在进行系统开发时,不管之前的可行性分析、需求分析、系统设计处理的多么完美,总会在关键时候、关键场合出现一些“意外”。这些“意外”,该来的还是要来,躲是躲不过的,而这时就是我们的适配器模式
的用武之地。适配器模式
最好在设计阶段不要考虑它,它不是为了解决还处在开发阶段的问题,而是解决正在服役的项目问题,没有一个系统分析师会再做详细设计时考虑使用适配器模式
。
适配器模式
包含两种,一种是类适配器
,另一种是对象适配器
。类适配器
是通过类的继承实现的适配,而对象适配器
是通过对象间的关联关系,组合关系实现的适配。二者在实际项目中都会经常用到,由于对象适配器
是通过类间的关联关系进行耦合的,因此在设计时就可以做到比较灵活,而类适配器
就只能通过覆写源角色的方法进行拓展,在实际项目中,对象适配器
使用到的场景相对较多。在iOS
开发中也推荐多使用组合关系,而尽量减少继承关系,这是一种很好的编程习惯,因此我在这里只介绍对象适配器
,想了解更多的关于类适配器
的话,请自行Google
之。
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.(将一个类的接口变成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。)
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
观察者模式(Observer Pattern)也叫做发布订阅模式(Publish/subscribe)。
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.(定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于他的对象都会得到通知并被自动更新。)
观察者模式需要考虑开发效率和运行效率的问题。
观察者模式在iOS中的应用我知道主要有两个,一个是消息中心(NSNotificitonCenter
),他并不是严格意义上的观察者模式,但是很类似,首先将观察者注册到消息中心,被观察者可以向消息中心发送消息通知给观察者,从而产生相应的逻辑。再有一个就是Cocoa
的KVO
机制,也就是Key Value Observer
,他可以检测一个对象的某一个属性,当他发生改变时,观察者自动的去调用相应的方法。
下面附上一个用objective-C
写的类似的一个观察者模式的小程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
例子很简单就是老师将电话号码留给同学们,当老师电话号码改变时,同学们自动的也会将老师的电话号码改变。其实写的并不好,最好还是定义两个Protocol
,一个是Observer
一个是Subject
,然后老师和学生分别遵循Observer
和Subject
协议,这样的话就比较好了。
iOS
开发,所以基本上这23种设计模式我都通过objective-C
来实现了。此系列文章的类图都是来自《设计模式之禅》,有兴趣的同学可以去买这本书看。
话说,在编码编到一定的程度以后,由于代码体系的庞大,结构的复杂,自然就会上升到设计模式高度,而现在的软件设计又基本都是面向对象的,所以有了设计模式作支持,可以使软件更加的稳定安全,也更易于维护与拓展。
首先来介绍最常用最简单的单例模式(Singleton),在以后的文章中再依次介绍其他的模式。
Ensure a class has only one instance, and provide a global point of access to it. (确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)
单例模式确保在一个应用中只产生一个实例,这是很有必要的,因为在我们做软件设计的时候,有很多对象都是只需要一个就可以了,而不需要创建众多的对象,这样最显而易见的就是节省了内存空间。而且避免了这个类的频繁的初始化与销毁。有时为了实现某一种功能与操作而创建的类(工具类)往往也不需要多个对象,使用单例模式再合适不过。再延伸一点,有时为了节省内存对一个对象进行复用的话也可以通过单例来实现,这在手机软件的开发中用得比较多,因为手机的内存实在是少得可怜。
单例模式在iOS
开发中的使用还是蛮多的,许多Foundation
、Cocoa
和UIKit
中的类都实现了单例模式,比如应用程序本身UIApplication
、文件操作类NSFileManager
、消息中心NSNotificitonCenter
等系统都已经给我们实现单例,我们只需要使用就好了。在iOS
中使用单例模式要使用类方法,通过类方法返回该类的唯一对象。
我知道的在iOS
开发中实现单例模式主要有以下三种方式:
该方法是苹果的官方文档中写的一种方式,通过覆盖NSObject
的部分方法实现,使该类无法alloc
、retain
、release
。这是最麻烦的一种方法,也是最不好的一种方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
可以看到这种方式,使用静态成员维持了一个永久存在的对象,而且覆盖了alloc
方法(alloc
方法会调用allocWithZone:
方法),并且也覆盖了所有与引用技术有关的方法,这都使这个对象不会被销毁。这样看上去基本实现了我们需要的,但是写起来麻烦不说,还有很大的一个问题,那就是多线程问题,如果是在多线程中那么该种方法就不能保证只产生一个对象了。所以这种方式只是介绍一下,并不推荐使用。
第二种跟第一种差不多,也是通过覆盖NSObject
的方法实现的,但是它在第一种的基础上增加了多线程的处理,所以即使在多线程下,该种方法创建的对象也是唯一的。这种方法已经有大牛为我们写好了,全都都是通过C
的宏定义#define
出来了。现给出该头文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 |
|
使用时也非常方便,该头文件也已给出使用方法,在这里我在说一下,供那些E文不好的同学使用。
使用这种方式首先把该头文件加到我们的项目中,然后直接使用就可以了:
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 |
|
如此一来在使用时,通过[Singleton sharedInstance]
就可以获得该类的单例对象了。
这种方法由于有了这个头文件的支持,所以使得使用单例方便多了,而且也避免了多线程的问题。
这是最后一种也是我最推荐的一种。iOS
在4.0以后推出了block
和GCD
,这两个特性给iOS
开发带来的很大的便利,也使开发变得更加趣味话。那么如何通过GCD
+block
来实现单例模式呢,这主要归功于dispatch_once(dispatch_once_t *predicate, ^(void)block)
这个GCD
的函数,他有两个参数第一参数是一个指向dispatch_once_t
类型结构体的指针,用来测试block
是否执行完成,该指针所指向的结构体必须是全局的或者静态的,第二个参数是一个返回值与参数均为空的block
,在block
体中进行对象的初始化即可。dispatch_once
在程序的生命周期中保证只会被调用一次,所以在多线程中也不会有问题。
该种方法使用方法:
1 2 3 4 5 6 7 8 9 10 11 |
|
使用该种方法只需要这简单的几句代码就可以实现单例了。使用起来非常方便,但是这种创建单例的方法也不是完美的,它并不能阻止人们通过alloc
方法来实例化一个对象,所以这并不是严格意义上的单例模式,但是一般程序都是我们自己写,我们自己记得就好了,这也没什么可担心的,从这一点上来说第二种方法又是比较好的,具体使用的时候呢,根据实际情况来吧,各取所需就好了。
Xcode
不得不说,很好用的一款IDE
,他集成了很多功能,但惟独没有发现为一个方法添加注释的功能。尤其是在当有大量的方法需要添加注释,而且注释的格式还要统一的时候,真的让人头疼。
在Xcode 3.2
版本的时候,还可以找到appledoc
插件,很方便的生成注释。但是到了Xcode 4.0
以上的版本就找不到这个功能,虽然appledoc
仍然可以用,但是需要使用命令行,而且生成的是html
文件。就没有再仔细研究,继续寻找更简便的方法。
最终找到一位大神写的一段ruby
脚本,使用它为系统添加了一项服务,使用此可以很方便为指定的方法生成指定格式的注释。
不过,测试发现这段ruby
脚本还是有一点点问题的,在生成注释后会把当前生成注释的方法的声明删掉。我只好凭着多年的编程经验对这段脚本进行了一点修改(第一次接触到ruby
代码。o(╯□╰)o),现在已经很好使用了,基本上没有啥问题了。分享给大家。
先展示个效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
所需文件:下载
1. ruby脚本 Doxygen.rb
原始的 DoxygenNew.rb
我修改的
2. 添加服务的应用程序 ThisService.app
首先打开ThisService.app
,加载DoxygenNew.rb
可以通过Add option
增加一些自定义设置,这里只添加了应用程序filter,添加的该服务只有Xcode
能使用
点Test Service
测试服务,可以粘过一些代码过来测试。
测试无误后,添加服务就好了。
然后就可以在Xcode
的服务里找到添加的这个服务了。
为了方便使用再为这个服务设置一个快捷键,往后在使用时,只需要选中要生成注释的方法名,按下快捷键,注释就会自动给生成了。
最后贴上我改过的ruby
代码,希望大家根据自己的需要再进行编辑,拿出来与大家分享。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 |
|