SegmentFault 代码与哲学最新的文章
2017-12-09T16:01:04+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
interface和setter,getter
https://segmentfault.com/a/1190000012355200
2017-12-09T16:01:04+08:00
2017-12-09T16:01:04+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
0
<p><a href="https://link.segmentfault.com/?enc=EuatxWAvkCmAt4z8Wqq1Ng%3D%3D.pHhUIwF18EBri2K2h4E8n2MPQmVgbonvdhFGieVWsmHGsKwFjoHaRKWmV2DhvXHe" rel="nofollow">前篇</a>说到我们通过ObjC的Category特性给日常工作增加便捷的实现,这一篇则要从语言设计角度,跟大家分享一些思考。</p>
<h2>不要忽视interface</h2>
<p>ObjC的@interface设计,跟Java和C#真的很像,但又略有不同,相比之下Java和C#则像是一个模子刻出来的。ObjC的特点十分明显,首先是一般不用写<code>@private</code>和<code>@public</code>来区分私有变量,大部分ObjC开发者甚至都不知道还有这两个关键字,其实Cocoa源代码中也基本没有使用过这种设计,即使ObjC是支持的。</p>
<p><!--more--></p>
<p>在@interface 中使用 @private和@public</p>
<pre><code class="objc">@interface Student : NSObject
{
@private
NSString* _name;
@public
NSNumber* _age;
int _height;
}
@end</code></pre>
<p>如上代码中,<code>Student</code>有一个私有变量<code>_name</code>,和两个共有变量<code>_age</code>、<code>_height</code>,但在<code>@interface</code>中声明变量,一定不是Cocoa设计者的初衷,这里有两个方面的考虑。</p>
<p>其一,把内部变量直接暴露在外,会降低整个框架的稳定性,因为增加不同模块之间的耦合,降低了每个类的内聚性。<br>其二,内部变量的变量名,很容易跟局部变量变量名产生冲突。上例中我给每一个变量名前加了下划线,就是为了防止这个问题发生。</p>
<p>所以纵观Cocoa框架的头文件设计,基本没有这样的代码,因为设计者提供了更好的实现方式,就是大家用的更多的<code>@property</code>关键字。</p>
<p>如果用@property声明上面的类,大家都很熟悉</p>
<pre><code class="objc">@interface Student : NSObject
@property (nonatomic, strong) NSString* name;
@property (nonatomic, strong) NSNumber* age;
@property (nonatomic, assign) NSInteger height;
@end</code></pre>
<p>@property这个设计真的很有意思,首先我们不再区分私有公有属性,因为只要写在<code>.h</code>里面的@property,我们都默认是共有的,私有的@property可以写在<code>.m</code>文件里。</p>
<p>其次,配合写在@implementation里面的@synthesize关键字,可以自动生成setter和getter方法,而现在@synthesize关键字都可以省略,除了个别情况有修改内部变量名称的需求。</p>
<pre><code class="objc">@implementation Student
@synthesize name = _name;
@synthesize age = __age;
@end</code></pre>
<p>上面的@synthesize,第一个是可以省略的,在不写的情况下,编译预处理会自动给添加@synthesize代码,所以即使没有合成(synthesize)height属性,我们依然实现了它的setter和getter方法</p>
<pre><code class="objc">//这两个方法可以重写
- (void)setHeight:(NSInteger)height
{
_height = height;
}
- (NSInteger)height
{
return _height;
}</code></pre>
<p>在setter和getter方法均重写的情况下,@synthesize需要手动添加。</p>
<pre><code class="objc">@synthesize height = _height;</code></pre>
<h2>为什么要使用 setter 和 getter</h2>
<p><code>setter</code>和<code>getter</code>的设计的确值得琢磨,我们主要从以下几点分析:</p>
<h3>setter 和 getter 包装了内部变量,整个类对外可以只暴露接口,增强类的内聚性。</h3>
<p>例如上例中的内部变量<code>_name</code>,外部类是无法操作的,只能通过set和get接口来发消息:</p>
<pre><code class="objc">Student* s = [[Student alloc] init];
[s setName:@"Tom"];
[s name];</code></pre>
<h3>通过实现 getter方法,可以避开初始化变量的时机问题</h3>
<p>这也是很实用的一个点,因为ObjC的消息设计机制,导致ObjC很难在初始化(init)方法中传入过多参数(题外话,我给ObjC扩展过依赖注入,详见<a href="https://segmentfault.com/a/1190000004659514">iOS实现依赖注入</a>)。因此新实例的默认属性,放在什么位置实现合适,是大家一定遇到过的问题。</p>
<p>例如最常见的<code>UIViewController</code>,代码初始化走<code>init</code>方法,而通过storyboard实力化则走<code>initWithCoder</code>方法,一些容器属性,通过getter方法初始化,则可避免第一次调用尚未初始化造成的问题。</p>
<p>早期的Cocoa在如果给<code>nil</code>发消息,是会引起异常的,现在的版本给没有alloc的对象发消息不再抛异常,以至于某些时候属性没有初始化造成的问题变得更隐蔽,然而重写getter方法可以有效避免这个问题,例如:</p>
<pre><code class="objc">//班级类
@interface XXClass : NSObject
@property (nonatomic, strong) NSMutableArray* students; //学生
@end
@implementation
//实现getter方法,在内部变量_students没初始化的情况下将其初始化
- (NSMutableArray *)students
{
if (!_students) {
_students = [NSMutableArray array];
}
return _students;
}
@end</code></pre>
<p>如此一来,无论在任何时候,第一次发送<code>[self students]</code>消息的时候,内部变量<code>_students</code>都会初始化。</p>
<blockquote><p>在这里要另外注明一点,在类的内部,不要在setter和getter方法外,直接使用内部变量,遵守这一条会收益很多。</p></blockquote>
<h3>setter 和 getter 可以单独使用,也可以脱离内部变量使用</h3>
<p>这里要说的就是@property的灵活性了,大家知道@property拥有一系列的修饰词,除了常用的<code>nonatomic(非原子化,线程安全)</code>,<code>strong(强引用类型)</code>,<code>weak(弱引用类型)</code>,<code>assign(赋值,用于非对象属性)</code>以外,还有<code>readonly(只读)</code>和<code>readwrite(可读写)</code>两个影响setter和getter方法的属性,<code>readonly</code>修饰的属性,只有getter方法而没有setter方法。</p>
<p><code>readwrite</code>则是一个看起来可有可无的修饰词,因为默认就是可读写。然而它其实有个专门设计的用法,就是在.h中的interface中被<code>readonly</code>修饰的属性,可以在这个类的其他类别(category)或者匿名类别中重新声明这个属性时,修改其读写限制,例如</p>
<pre><code class="objc">//班级类
@interface XXClass : NSObject
@property (nonatomic, strong, readonly) NSMutableArray* students; //学生
@end</code></pre>
<pre><code class="objc">//匿名类别
@interface XXClass()
@property (nonatomic, strong, readwrite) NSMutableArray* students;
@end</code></pre>
<p>这样一来,因为匿名类别一般写在.m文件里(基本没见过写在.h文件里的),所以外部是不能调用<code>students</code>属性的setter方法,而<code>XXClass</code>类内部则可以使用。</p>
<p>还有一种常见情况是用setter和getter来模拟属性(@property),例如:</p>
<pre><code class="objc">//班级类
@interface XXClass : NSObject
@property (nonatomic, strong, readonly) NSMutableArray* students; //学生
@property (nonatomic, assign, readonly) NSUInteger studentsCount; //学生数量
@end</code></pre>
<pre><code class="objc">- (NSUInteger)studentsCount
{
return self.students.count;
}</code></pre>
<p>这里的<code>studentsCount</code>是没有内部变量的,通过getter方法伪造成属性接口。</p>
<h2>小结</h2>
<p>这一篇是ObjC的接口设计模式的一部分,写的比较详细是帮助新手入门,给有经验的朋友带来一些思考,并引出接下来的内容。</p>
认识ObjC,改造Cocoa
https://segmentfault.com/a/1190000012329645
2017-12-07T17:30:16+08:00
2017-12-07T17:30:16+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
0
<p>更好的阅读体验请点击 <a href="https://link.segmentfault.com/?enc=G%2B9GYIwswRYoPXe%2BNefT4w%3D%3D.OAoPc3l1%2F67WCeJ3F0IViDDYQziyxJ70WmE8DDEoIMm8spYmWnMKRPalqdRwF8N1" rel="nofollow">原文</a></p>
<p>接上篇,其实在接触Ruby不久后,我就萌生了改造ObjC的Cocoa框架的想法。为什么要改造?只为能够提高开发OC项目的效率。同时我也完成了一些改造工作,详见<a href="https://link.segmentfault.com/?enc=DF%2B8sARvwjedDCHUG7GfJQ%3D%3D.%2BaUy0vxPl%2BnY206gkS2wvIqTDa%2F3jVItSjyB%2FiXAkq40sD%2Bm2blGNfcadAnu0Gt6" rel="nofollow">像Ruby一样写ObjC,用block实现链式方法调用</a></p>
<p>说到改造这个问题,我想起曾经有人说,合格的程序员都会不断追求自动化,不断追求代码的解耦与复用,不断追求拓展技术的边界。我们也往往会从这三个方向找切入口,例如OC和Python一样充斥了一些C语言函数形式的方法或者宏,例如<code>NSLog()</code>、<code>NSLocalizedString()</code>,或是CoreGraphic框架中一系列的C函数,更有甚者GCD(Grand Central Dispatch)完全是C函数代码,但GCD因为把多线程编程做的跟if/else一样好用,所以用多了也都接受了。</p>
<p><!--more--></p>
<h2>举个栗子</h2>
<p>我们今天就从NSLocalizedString这个宏作为切入口,举一个例子:</p>
<pre><code class="objc">//惯用方法
NSString* str = NSLocalizedString(@"你好,世界",nil);</code></pre>
<p>从OOP的角度思考,我们不难想到字符串的本地化转换,完全可以作为NSString类的实例方法来设计,而不是像NSLocalizedString宏这样的设计,这个设计可以说堪比Python的len()方法。</p>
<p>重新设计的本地化接口</p>
<pre><code class="objc">NSString* str = [@"你好,世界" localizedString];</code></pre>
<p>这样调用不仅更符合我们的思维逻辑,也更符合OOP的理念,并且和NSString其他的接口也保持了一致性。使用ObjC的Category特性,就可以轻松实现</p>
<pre><code class="objc">@interface NSString (add)
- (NSString*)localizedString;
@end
@implementation NSString(add)
- (NSString*)localizedString;{
return NSLocalizedString(self, nil);
}
@end</code></pre>
<p>同样的我们还可以给NSString或者其他类型增加各种各样的类别(Category)进行拓展,例如比较有名集大成框架<a href="https://link.segmentfault.com/?enc=oAwJyJ3sDP0JExmPr6yJ7A%3D%3D.gpreadBMZAnfpfS6kazcNJKvz8D%2FG1hTpQXXBfkYxu743fHjVPuYq9g8xJp%2FvQa3" rel="nofollow">YYKit</a>,在NSString扩展中加入各种摘要算法转换方法,给实际开发带来了极大的便利。</p>
<h2>第二个栗子</h2>
<p>如果第一个栗子不能跟你产生多少共鸣,那就请看接下来的栗子:给NSArray增加高阶函数Map,类似的Filter,Reduce函数在Python、JavaScript、Swift、Ruby中都是标配了,而OC则显得略有落后,但落后并不妨碍我们进行改造,同样给NSArray增加Category方法,实现依赖于OC对block的支持</p>
<pre><code class="objc">@interface NSArray (Functional)
- (NSArray*)map:(id (^)(id x))map;
@end
@implementation NSArray (Functional)
- (NSArray*)map:(id (^)(id))map
{
NSMutableArray* array = [NSMutableArray array];
[self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
id x = map(obj);
if(x) [array addObject:x];
}];
return [array copy];
}
@end</code></pre>
<p>block的出现相当于提高了代码块的身份,虽然它还不是OC的一等公民,但已经可以和实例对象平起平坐,作为参数进行传递了。如果你是一个不太明白block机制的新手,我这里还有一篇<a href="https://link.segmentfault.com/?enc=mIifq244o34eMd6HztHSfw%3D%3D.%2FeidnohI3jMrMigugyQMd1182SBijx6DMEeHi4PWO8LBjGnyc8LjYk%2FLNgiCKAEqW8dLJuNpDu8aRIPQxGYryg%3D%3D" rel="nofollow">教程</a>推荐给你。如果了解block,上面的代码就很好理解了,没有任何优化,仅仅是封装了<code>拿东西</code>和<code>包装</code>这两个步骤。</p>
<p>那,NSArray实现Map方法意味着什么呢?意味着我们加工一组数据时,只要专心数据的加工工作就好。</p>
<pre><code class="objc">NSArray* a = @[@"a",@"b",@"c"];
a = [a map:^id(id x) {
return [x uppercaseString];
}];
NSLog(@"%@",a);</code></pre>
<blockquote><p>(<br> A,<br> B,<br> C<br>)</p></blockquote>
<h2>究竟能否提高开发效率</h2>
<p>这个问题其实不用讨论也知道可以,因为我们都有过复制粘贴重复写代码的经历,而这种代码封装和复用,甚至比复制粘贴更简单,每使用一次,都能节省几秒钟甚至几分钟的时间,一并节省不少精力,长年累月则是受益无穷。</p>
Cocoa改造前篇 - 说在前面的
https://segmentfault.com/a/1190000012329531
2017-12-07T17:25:24+08:00
2017-12-07T17:25:24+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
2
<p>更好的阅读体验请点击 <a href="https://link.segmentfault.com/?enc=aVeUBSKMZZYKE%2F3FID%2Fgww%3D%3D.nDsrnsKHB3Ij%2BhQUgs4hI7yOb1BlC2aEyeeY4XDNa%2FksIYxPsu0kFLjjSqv97Le9" rel="nofollow">原文</a></p>
<h2>从面相对象说起</h2>
<p><code>面向对象的程序设计</code>(Object-Oriented Programming,简记为OOP)这个概念大家都有所耳闻,目前(2017.12),在<a href="https://link.segmentfault.com/?enc=m7%2B%2BdDJWOWdvtkS86gcrRA%3D%3D.%2B20IzLGKUt90IhIb1%2FDocbdHkUsmkh%2FUtQSfOY1nUT%2Bp60SCNqCfH7D1ooQXsffY" rel="nofollow">Tiobe</a>世界语言排行榜上排前十的语言中,C语言和Assembly language(汇编)外的八种语言均原生支持<code>面向对象的程序设计</code>。</p>
<p><!--more--></p>
<p>怎么判断一种编程语言是否支持OOP呢?看看这门语言是否支持类(class)、对象(object)、封装(encapsulation)、继承(inheritance)等功能和特性,支持这些就可以进行面向对象编程。拿Objective-C(OC)来说,类就是<code>Class</code>,对象就是<code>instance</code>,万物的基类是<code>NSObject</code>,这些东西在C语言里并不存在,是OC使用C语言的结构体(struct)抽象出来的产物。</p>
<p>我们从Objective-C的名字上也能看出一些端倪,直译过来是<code>对象化的C语言</code>,当然不仅是OC,排行榜前十中的C++同样是C语言的一个超集;C#和Java同样属于类C语言,把面向对象做的更加彻底;PHP虽然是脚本语言,其解释器是使用C语言写的;而我们常说的Python,其全称则是CPython,也是用C语言实现的解释器,当然Python解释器也有Java和C#实现的版本。</p>
<p>为什么C语言,比其他语言显得更底层呢?接触过的朋友相信都有很深的体会,C语言的程序,是在和图灵机硬件打交道,变量、数组、结构体,声明在堆内存就要为其分配内存空间大小,分配了内存,就要手动回收;数组还要区分静态和动态,每块数据占几个字节,躺在内存的什么位置,一切都按编程人员的安排。所以有人说C语言就是一个高级汇编,想起来确实有一分道理(笑)。但在智能手机、移动计算机计算能力大大提升的今天,计算资源早已不是通用编程首先考虑的问题,相比于C语言强迫编程人员从机器的角度设计程序,抽象程度更高的OOP才更接近人脑的思维方式,才更适合提高软件工程师的编程效率。</p>
<p>即使如此,仍有一部分人至今站在OOP的对立面,从代码复杂度、建模能力要求等方面提出异议,坚持写C++、Python、PHP的时候不构造类,写纯过程的程序。但其实,这些自称为原C党的朋友,并不能说自己没有使用OOP,因为这些语言中变量,跟C语言中的变量,有本质的不同。</p>
<p>就用<code>字符串</code>和<code>数组</code>来举例子,C语言是没有string类型的,只有字符数组,用<code>\0</code>来标记字符串结束;而其他语言中的string则是早已封装好的字符串类(Class),用起来跟整型无异。</p>
<p>C语言中字符串和数字变量声明</p>
<pre><code class="c">char name[] = "Tom\0";
int age = 12;</code></pre>
<p>Python中字符串和数字变量声明</p>
<pre><code class="py">name = "Tom"
age = 12</code></pre>
<p>C++中字符串和数字变量声明</p>
<pre><code class="cpp">string name = "Tom";
int age = 12;</code></pre>
<p>我们在Python和C++中使用字符串,早已不是在直接与设备内存打交道,而C语言中的“字符串”还停留在只是内存中的一段连续空间的阶段。</p>
<p>再来看一看数组,C++虽然也支持C的数组,但我想对比的其实是C++标准库中的向量(Vector),以及Python中的链表(List),这些高级容器同样是基于OOP理念设计的类,仍只有C语言的数组内容直接映射在内存上。</p>
<p>所以即使你不构造Class,在C++、Python、PHP中仍在使用对象和实例的OOP特性,即使开发的是线性程序。</p>
<h2>彻底的OOP</h2>
<p>经常会看到有人抱怨Java把面向对象的理念做的太过头,C#作为Java的仿制品,也同样逃脱不了被诟病的现实,但其稳定性也是有口皆碑。然而真正把OOP理念实现的彻头彻尾彻彻底底的,反而是最早的OOP语言之一的Smarttalk,让我先看一段Samrttalk的代码</p>
<pre><code class="smalltalk">Transcript show: 'Hello world'</code></pre>
<p>这是Smarttalk版本的<code>Hello world</code>程序,<code>Transcript</code>是Squeak(这是Smalltalk语言的一种版本实现)环境里,把信息显示到屏幕上的一个对象。这段代码是用冒号给这个对象发送了一个消息(Message),如果给这段代码加上一对中括号,是不是像极了Ojective-C,没错,因为OC就是参考Smarttalk设计的Runtime。</p>
<p>同样,Samrttalk也支持中括号的写法,我们可以把上面的一段代码段落,赋值给一个变量:</p>
<pre><code class="smalltalk">t := [ Transcript show: 'Hello world']</code></pre>
<p>这个t变量,其实是一个闭包(BlockClosure)对象,相同的概念在C++ 11标准里才出现,相比之下Smarttalk的设计理念真的很前卫。而OC作为Smarttalk的追随者,更是拥有NSOperation类来实现闭包,相比之下,block并不是基于OOP的设计。</p>
<p>C++的blcok和ObjC的NSOperation,这里block写法OC同样支持</p>
<pre><code class="objc">void hello = ^ {
NSLog(@"hello world");
};
hello();
NSBlockOperation* block = [NSBlockOperation blockOperationWithBlock:^{
// 做一些操作
}];
[[NSOperationQueue mainQueue] addOperation:block];</code></pre>
<p>要注意的是<code>NSBlockOperation</code>是在OC支持block以后才出现的类,在此之前要使用NSOpertaion,我们需要继承NSOpertaion类,并重写这个类的<code>-(void)main</code>方法,这无疑是一件十分繁琐的事。</p>
<h2>一切皆对象</h2>
<p>OC作为Smarttalk的追随者,在OOP的理念上是要强于C++、Python和PHP的,<code>interface</code>、<code>implementation</code>、<code>getter</code>、<code>setter</code>的接口设计,和Java、C#相互参考,水平相近,但仍比Smarttalk和Ruby略逊一筹。</p>
<p>熟悉Cocoa框架的朋友都知道,UI绘制框架<code>CoreGraphic</code>中仍然要使用大量的CG开头的C语言函数,点、线、面的容器,依旧是CGPoint,CGSize,CGRect这些C语言结构体;数字变量依然是int、NSInteger、NSNumber(数字类)混着用,相互转换忙的不亦乐乎。当然这一切在OC支持字面量特性(Literals)以后有了好转:</p>
<pre><code class="objc">//通过@符号直接把普通变量转换为数字对象
NSNumber *myIntegerNumber = @8;
//转回来
NSInteger customNumber = [myIntegerNumber integerValue];</code></pre>
<p>相比之下,Smarttalk和Ruby做的更彻底,更好用,下面是用Smarttalk重复输出十次<code>Hello world</code>的代码,给数字10发<code>timesRepeat</code>消息,重复消息参数中的闭包:</p>
<pre><code class="smarttalk">10 timesRepeat: [Transcript show: 'Hello world']</code></pre>
<p>为什么整数类要设计这么方法呢?因为Smarttalk中并没有循环语法,甚至其他语言常见的条件语句if/else在Smarttalk中都是不存在的,而都是使用OOP的理念实现,有兴趣了解更多关于Smarttalk的内容,请来<a href="https://link.segmentfault.com/?enc=6f7bNAPg%2FNPAom8Dgr7ZKg%3D%3D.1Ms2RAEf%2Ba6AMRwxM6vw%2Be4QBAI%2FkFhd%2BL512ezwv2OFg60PCye9iiGFjzCdDSoO" rel="nofollow">这里</a>。</p>
<h2>给对象发消息是更符合人类思维模式的设计</h2>
<p>这里我们从继承Smarttalk理念的Ruby说起,虽然其使用点语法替代了冒号,但仍能看出Ruby中的数字类型,就是数字对象。</p>
<pre><code class="ruby">//将数字对象102转换成字符串对象
102.to_s</code></pre>
<p>用Smarttalk实现则是</p>
<pre><code class="smarttalk">102 printString</code></pre>
<p>相比之下Python则像是一个作者对OOP还处于感性认知阶段设计出来的语言,所以会设计出len()、map()、fliter()这种C语言函数风格的接口,例如我在OC中我们获取数组的长度使用count属性,使用点语法或者中括号消息都可以获取(关于OC中的点语法和中括号语法我们后面再聊)</p>
<pre><code class="objc">NSArray* a = @[@(1),@(2),@(3)];
a.count;
[a count];</code></pre>
<p>这很面向对象,因为我们要获取数量的主体数组实例a,发消息让他返回长度很符合人类的思维逻辑。同样的我们看看Ruby,也是一样的操作</p>
<pre><code class="rb">a = [1,2,3]
a.length
a.size</code></pre>
<p>然而当我使用第一次写Python代码的时候,我经历了很多人都遇到过的情况,不知道字符串或者数组如何获取长度。因为Python中string和list都没有length、size、count、len等属性和方法,然后我们发现Python提供了一个len()方法获取序列长度,这个方法接受一切的对象作为参数。</p>
<pre><code class="py">a = [1,2,3]
len(a)
s = "123"
len(s)</code></pre>
<p>针对这个问题,有一部分人认为不是问题,他们说做OOP不要太教条主义,len在前在后能有很大差别么?我想说真的是有的,这个看似简单的前后问题,其实影响了实际的编程体验,就是是否基于对象思考问题的体验。</p>
<p>一方面,len()方法像一个凭空存在的方法,不依赖于任何类和对象,也不是依附于某个模块,知道它存在,才会去使用它,同样的还有Python中的type()、map()方法等。另一方面,这一类方法到底可以用于什么类型的对象,开发者心里也没底,必须对照接口标明的参数类型使用。</p>
<p>这一切无疑不利于程序开发的思维连贯性,有朋友可能觉得我说的言过其实,我这里举一个例子大家体会一下何为思维连贯性。</p>
<p>需求是将一段英文字符串的单词逆序,<code>How are you</code>处理成<code>you are How</code>。</p>
<p>我们用OC实现如下:</p>
<pre><code class="objc">#import <Foundation/Foundation.h>
NSString* reverse(NSString* text) {
NSArray *words = [text componentsSeparatedByString:@" "];
NSArray *reversed = [[words reverseObjectEnumerator] allObjects];
return [reversed componentsJoinedByString:@" "];
}</code></pre>
<p>Python实现为</p>
<pre><code class="python">def reverse(text):
a = text.split(' ')
a.reverse()
return ' '.join(a)</code></pre>
<p>Ruby的实现为</p>
<pre><code class="ruby">def reverse(string)
return string.split.reverse.join(' ')
end</code></pre>
<p>观察出来区别了了吧,重要的不是Ruby只用了一行代码,而是Ruby相比于OC和Python,省去了很多中间变量,别看只是一点点节省,其实省去我们实际开发中很大一部分无用工作。当然,OC可以通过括号多层嵌套连贯起来写,也能达到同样的效果,但我们并不推荐这样做,因为OC的方法名偏长,如果缩进不当,会让代码更难理解。</p>
<p>相比之下,Python的接口设计更滑稽一些,首先在Ruby中</p>
<pre><code class="ruby">array.reverse!
array.reverse</code></pre>
<p>是两个不同的方法,前者只逆转array,没有返回值。后者则返回一个新的逆转数组对象,Python没有类似设计。</p>
<p>其次Ruby和OC都将join方法设计在array类里,唯独Python将其设为字符串类型的方法,导致了Python没法连贯地将中间参数略去。</p>
像Ruby一样写ObjC,用block实现链式方法调用
https://segmentfault.com/a/1190000009878685
2017-06-21T23:06:42+08:00
2017-06-21T23:06:42+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<p><a href="https://link.segmentfault.com/?enc=oGn%2B2ZBoMjcDpWMWe7bk0Q%3D%3D.p0RYDvhv2lh8cg8nehvyiQLCI71NPkldXVivmfsHxKWPtRf7A19uZJx%2BCei9Z98U" rel="nofollow">Github源码</a></p>
<h2>引言</h2>
<p>一切要从我加入了<code>Codewars</code>网站,开始与世界各地的Coder们一同刷题开始说起。在<code>Codewars</code>中,有许多题目是支持多种不同语言的,比如下面这一道题,把字符串中的所有单词根据空格分割反转:</p>
<pre><code class="ruby">#You need to write a function that reverses the words in a given string. A word can also fit an empty string. If this is not clear enough, here are some examples:
reverse('Hello World') === 'World Hello'
reverse('Hi There.') === 'There. Hi'
#Happy coding!</code></pre>
<p>当我们在<code>Codewars</code>的OJ系统中通过这道题目的时候,可以看到所有答案中,大家评分最高的答案,对应上面这个题目,<code>ObjC</code>的最佳答案是</p>
<pre><code class="objective-c">#import <Foundation/Foundation.h>
NSString* reverse(NSString* text) {
NSArray *words = [text componentsSeparatedByString:@" "];
NSArray *reversed = [[words reverseObjectEnumerator] allObjects];
return [reversed componentsJoinedByString:@" "];
}</code></pre>
<p>这是一段中规中矩的<code>ObjC</code>代码,跟我的答案是一样的。<br>然后我们来看看<code>Ruby</code>版本的最佳答案,虽然是同样的解题思路,但表现出来的视觉效果却完全不同:</p>
<pre><code class="Ruby">def reverse(string)
string.split.reverse.join(' ')
end</code></pre>
<p>这里给没有接触过<code>Ruby</code>的朋友解释一下这段代码,首先,Ruby的方法中可以省略<code>return</code>关键字,把方法中最后一个对象返回;其次<code>split</code>方法不传参数时候默认是以空格符分割,这样就有了这段简介明晰的代码。</p>
<p>当然,<code>ObjC</code>也是可以一句话写完这段代码的嘛:</p>
<pre><code class="objective-c">NSString* reverse(NSString* text) {
return [[[[text componentsSeparatedByString:@" "] reverseObjectEnumerator] allObjects] componentsJoinedByString:@" "];
}</code></pre>
<p>但你会发现,这样书写的<code>ObjC</code>代码可读性大打折扣,一方面<code>ObjC</code>的方法名太长,引起代码折行以后,很难一眼看清整个过程;另一方面,<code>ObjC</code>的消息传递机制使用的中括号嵌套,嵌套多层时会增加额外的匹配括号的工作,有些时候甚是烦人。</p>
<h2>思考</h2>
<p>上面的对比,引发了我对<code>ObjC</code>的种种思考,是否有可能使用<code>ObjC</code>像<code>Ruby</code>一样优雅地写链式函数调用,实际上<code>Swift</code>中就采用了类似<code>Ruby</code>的写法。</p>
<h3>在ObjC中采用 . 调用方法</h3>
<p>我们知道<code>ObjC</code>中,点是用来获取<code>属性(Property)</code>的,例如我们给<code>AppDelegate</code>声明了一个<code>datas</code>的数组属性</p>
<pre><code class="objective-c">@interface AppDelegate ()
@property (nonatomic, strong) NSArray* datas;
@end</code></pre>
<p>当前的编译器会自动给<code>datas</code>生成<code>setter</code>和<code>getter</code>方法,并在没有使用<code>@synthesize</code>关键字合成的前提下,声明了<code>_datas</code>这个内部指针。</p>
<p>这时我们如果用点方法调用<code>self.datas</code>,等同于传递了<code>[self datas]</code>消息,实际是发送的<code>getter</code>消息。如此一来,我们可以用<code>property</code>或者不带参数的方法,来模拟点方法,例如数组反转:</p>
<pre><code class="objective-c">@interface NSArray (Functional)
- (NSArray*)reverse;
@property (nonatomic, strong, readonly) NSArray* reverse;
@end</code></pre>
<p>我们给<code>NSArray</code>增加一个名为Funcional的<code>Category</code>,增加<code>reverse</code>方法或者<code>property</code>都可以,二选一即可。这里的<code>property</code>声明为<code>readonly</code>,从而禁止调用<code>setter</code>方法。</p>
<p>这两种方法都可以实现<code>self.reverse</code>,实际消息发送都是<code>[self reverse]</code>,实现如下:</p>
<pre><code class="objective-c">@implementation NSArray (Functional)
- (NSArray *)reverse //reverse属性的getter方法 和 - (NSArray*)reverse; 相同
{
return [[self reverseObjectEnumerator] allObjects];
}
@end</code></pre>
<h3>使用block闭包</h3>
<p>然而当我们需要改写带参数的方法时,两种方式实现都有些问题了。比如例子中的数组拼接字符串的方法<code>componentsSeparatedByString</code>,我们这里需要用到<code>ObjC</code>的<code>闭包(block)</code>特性。</p>
<p>下面两种方式也是等价的,原理同上面的<code>reverse</code>:</p>
<pre><code class="objective-c">@interface NSArray (Functional)
@property (nonatomic, strong, readonly) NSString* (^join)(NSString* join);
- (NSString* (^)(NSString*))join;
@end</code></pre>
<p>实现代码:</p>
<pre><code class="objective-c">- (NSString *(^)(NSString * j))join
{
return ^ (NSString* j) {
return [self componentsJoinedByString:j];
};
}</code></pre>
<p>另外我们给<code>NSString</code>也增加一个<code>Category</code>实现字符串切割成数组:</p>
<pre><code class="objective-c">
@interface NSString (Functional)
@property (nonatomic, strong, readonly) NSArray* (^split)(NSString* s);
@end
@implementation NSString (Functional)
-(NSArray* (^)(NSString *))split
{
return ^ (NSString* s) {
return [self componentsSeparatedByString:s];
};
}
@end</code></pre>
<p>如此,我们就可以通过点语法来实现链式调用,来实现开篇说的问题。</p>
<pre><code class="objective-c">NSString* reverse(NSString* text) {
return text.split(@" ").reverse.join(@" ");
}</code></pre>
<p>是不是有感觉在用<code>Ruby</code>的错觉。</p>
<h2>扩展,函数式编程</h2>
<p><code>ObjC</code>作为一个比<code>C++</code>还要遥远的语言,在某些方面还是缺少现代编程语言的特性。例如数组的<code>Map</code>、<code>Filter</code>、<code>Flatten</code>等高级函数,<code>Cocoa</code>框架都是没有的。</p>
<p>而这些函数实在是太常用也太好用了,我们完全可以通过前面讨论的方式,为<code>NSArray</code>增加这些方便的高级函数.</p>
<pre><code class="objective-c">//定义block
typedef id (^MapBlock)(id x);
@property (nonatomic, strong, readonly) NSArray* (^map)(MapBlock);
//或者
- (NSArray *(^)(id (^)(id)))map;</code></pre>
<p>实现如下:</p>
<pre><code class="objective-c">- (instancetype)map:(id (^)(id))map
{
NSMutableArray* array = [NSMutableArray array];
[self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
id x = map(obj);
if(x) [array addObject:x];
}];
return [array copy];
}
- (NSArray *(^)(id (^handle)(id)))map
{
return ^(id (^handle)(id)) {
return [self map:handle];
};
}</code></pre>
<p>如此,我们可以通过两种方式来调用<code>map</code>方法,这两种方式是等价的,数组[1,2,3]通过<code>map</code>方法变成了[3,6,9]:</p>
<pre><code class="objective-c">[@[@1,@2,@3] map:^id(id x) {
return @([x integerValue] * 3);
}];
@[@1,@2,@3].map(^id(id x) {
return @([x integerValue] * 3);
}); </code></pre>
<h2>小结</h2>
<p>有人可能认为,这些代码并没有太多的简化我们的开发<code>ObjC</code>的方式,但请不要忽视这些微小的积累。代码的简化和优化,带来的是更低的耦合、更好的可读性和更健壮的构架,我们用了十几分钟的时间来讨论<code>ObjC</code>的链式函数调用方法,必定会在以后的开发中,节省大量的重复劳动时间。</p>
使用 EventKit 向系统日历中添加事件
https://segmentfault.com/a/1190000009661952
2017-06-05T16:11:53+08:00
2017-06-05T16:11:53+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>使用 EventKit 向系统日历中添加事件</h2>
<p>本文主要内容是如何一步一步使用<code>EventKit</code>在iOS设备中添加日历,并在日历中添加事件和提醒事项。 </p>
<p><a href="https://link.segmentfault.com/?enc=1c9yCMcqcQb4%2FVPMyXCjBQ%3D%3D.lzR%2BQsf2gRH1ZZf0N3MstxTj722u%2Bv00e54XZqzbJTdVkZadlxSCMgyAInCYJu2%2B" rel="nofollow">源代码Github</a></p>
<h2>类和属性</h2>
<h4>EKAlarm 提醒操作类</h4>
<p><code>EKAlarm</code>类用于提供操作系统日历通知的相关接口,通知时间既可以是绝对时间,也可以是相对时间。</p>
<p>实例化方法</p>
<pre><code class="objectivec">//绝对时间
+ (EKAlarm *)alarmWithAbsoluteDate:(NSDate *)date;
//相对时间
+ (EKAlarm *)alarmWithRelativeOffset:(NSTimeInterval)offset;</code></pre>
<p>属性相关类</p>
<p><code>EKStructuredLocation</code> 通知的位置属性,包括标题<code>title</code>,地理位置<code>geoLocation</code>和半径<code>radius</code></p>
<p><code>EKAlarmProximity</code> 是一个标记为进入或者离开的枚举类型</p>
<pre><code class="objectivec">typedef NS_ENUM(NSInteger, EKAlarmProximity) {
EKAlarmProximityNone,
EKAlarmProximityEnter, //进入
EKAlarmProximityLeave //离开
};</code></pre>
<p><code>EKAlarmType</code> 记录通知类型的枚举属性</p>
<pre><code class="objectivec">typedef NS_ENUM(NSInteger, EKAlarmType) {
EKAlarmTypeDisplay, //展示信息
EKAlarmTypeAudio, //播放声音
EKAlarmTypeProcedure, //打开网址
EKAlarmTypeEmail //发邮件
};</code></pre>
<h4>EKEventStore 事件管理类</h4>
<p>首先,通过``可以进行系统授权,使用下面的方法</p>
<pre><code class="objectivec">//申请权限
- (void)requestAccessToEntityType:(EKEntityType)entityType completion:(EKEventStoreRequestAccessCompletionHandler)completion NS_AVAILABLE(10_9, 6_0);
//获取当前权限
+ (EKAuthorizationStatus)authorizationStatusForEntityType:(EKEntityType)entityType NS_AVAILABLE(10_9, 6_0);</code></pre>
<p>枚举量包括 实例类型<code>EKEntityType</code> 和 实例蒙版<code>EKEntityMask</code></p>
<h4>EKCalendar 日历操作类</h4>
<p><code>EKCalendar</code> 和 <code>EKEventStore</code> 的关系可以理解为,一个<code>EKEventStore</code>可以包含多个<code>EKCalendar</code>。</p>
<p>这个类的类型枚举变量是</p>
<pre><code class="objectivec">typedef NS_ENUM(NSInteger, EKCalendarType) {
EKCalendarTypeLocal,
EKCalendarTypeCalDAV,
EKCalendarTypeExchange,
EKCalendarTypeSubscription,
EKCalendarTypeBirthday
};</code></pre>
<p>其中,<code>EKCalendarTypeCalDAV</code> 和 <code>EKCalendarTypeExchange</code> 是两种邮箱账户的事件同步类型,<code>EKCalendarTypeBirthday</code>是一个内置的生日日历,<code>EKCalendarTypeLocal</code>和设备同步,<code>EKCalendarTypeSubscription</code>则是用于本地的同步类型。</p>
<h4>EKEvent 事件操作类 与 EKReminder 提醒操作类</h4>
<p><code>EKEvent</code>与<code>EKReminder</code>一样继承于<code>EKCalendarItem</code>。</p>
<h3>业务代码</h3>
<p>上面对于头文件的分析,有助于我们实际编写代码。</p>
<h4>系统授权</h4>
<p><strong>要点一 添加授权描述</strong></p>
<p>首先需要在项目的plist文件中,加入申请系统日历使用的描述,否则无法发起授权请求。键为<code>NSCalendarsUsageDescription</code>,值为提交请求的描述。</p>
<p>然后判断当前的授权状态</p>
<pre><code class="objectivec">+ (BOOL)accessForEventKit:(EKEntityType)type
{
return [EKEventStore authorizationStatusForEntityType:type] == EKAuthorizationStatusAuthorized;
}</code></pre>
<p>当然,可以增加额外的判断,比如当状态为<code>EKAuthorizationStatusDenied</code>的时候,可以提醒用户前往<code>系统设置</code>打开系统日历的授权,我们上面这段代码只是简单的判断是否拥有系统日历的操作权限,当拥有权限时,进行业务代码,如果还没有进行过授权,则用下满的方法吊起授权</p>
<pre><code class="objectivec">+ (void)accessForEventKitType:(EKEntityType)type result:(void(^)(BOOL))result
{
EKEventStore* store = [[EKEventStore alloc] init];
[store requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError * _Nullable error) {
if (!granted) {
NSLog(@"%@",error);
}
if (result) {
result(granted);
}
}];
}</code></pre>
<p>这里是否封装成方法都可以,我这里写成类方法是为了方便读者在任意位置粘贴代码和调用。</p>
<p>授权完成以后就可以进行业务代码的操作了。</p>
<h4>添加日历</h4>
<p>首先使用下面的代码获取系统日历中全部的日历</p>
<pre><code class="objectivec">+ (NSArray*)calendarWithType:(EKEntityType)type
{
EKEventStore* store = [[EKEventStore alloc] init];
return [store calendarsForEntityType:type];
}</code></pre>
<p>这里同样区分事件的种类,获得结果如下,每个苹果账号的日历内容不尽相同</p>
<blockquote><p>"EKCalendar <0x1740a5580> {title = Birthdays; type = Birthday; allowsModify = NO; color = #8295AF;}",<br> "EKCalendar <0x1740a5dc0> {title = Calendar; type = CalDAV; allowsModify = YES; color = #1BADF8;}",<br> "EKCalendar <0x1740a56a0> {title = U65e5U5386; type = CalDAV; allowsModify = YES; color = #1BADF8;}",<br> "EKCalendar <0x1740a5640> {title = U5de5U4f5c; type = CalDAV; allowsModify = YES; color = #63DA38;}",<br> "EKCalendar <0x1740a55e0> {title = U5bb6U5ead; type = CalDAV; allowsModify = YES; color = #FFCC00;}"</p></blockquote>
<p><code>EKCalendar</code> 和 <code>EKCalendarItem</code> 并不是双向的可读取关系,通过<code>EKCalendarItem</code>实例可以获取它的<code>EKCalendar</code>,但我们无法通过<code>EKCalendar</code>获取系统全部的<code>EKCalendarItem</code>,这样不同的应用之间是无法相互操作事件的。</p>
<p>每一个<code>EKCalendarItem</code>拥有独一无二的<code>calendarItemIdentifier</code>标识,是每个事件的id,这个字符串是应用自己分配的,保证了每个应用可以在添加了事件以后,针对性的进行事件修改。</p>
<p>大家注意到上面输出的<code>EKCalendar</code>中有一个日历是禁止应用修改的,就是<code>Birthdays</code>日历,因为这个日历是同步于通讯录中的联系人生日的特殊日历。</p>
<p><code>EKCalendar</code>除了上面获取的系统已有日历,我们也可以添加自己定义的日历</p>
<p><strong>要点二 日历的calendarIdentifier存储</strong></p>
<p>应用需要自己存储自己添加的日历的唯一标识,就是<code>calendarIdentifier</code>,我们这里使用<code>NSUserDefault</code>来存储。</p>
<pre><code class="objectivec">+ (void)createCalendar
{
NSUserDefaults * def = [NSUserDefaults standardUserDefaults];
NSString* calendarIdentifier = [def valueForKey:@"testCalendarIdentifier"];
EKCalendar* birthday = [store calendarWithIdentifier:calendarIdentifier];
//这里如果calendarIdentifier为nil,则EKCalendar也会为nil
if (!birthday) {
birthday = [EKCalendar calendarForEntityType:EKEntityTypeEvent eventStore:store];
birthday.title = @"test日历";
//注意calendarIdentifier自动生成的,这里要保存下来
[def setObject:birthday.calendarIdentifier forKey:@"testCalendarIdentifier"];
[def synchronize];
NSError* error;
[store saveCalendar:birthday coobjectivecit:YES error:&error];
if (error) {
NSLog(@"%@",error);
}
}
}</code></pre>
<p>上面的代码执行将无法添加日历,需要越过两个坑</p>
<p><strong>要点三 坑</strong></p>
<p>坑一是<code>EKCalendar</code>需要设置<code>EKSource</code>才可以添加,所以会得到下面的错误</p>
<blockquote><p>Error Domain=EKErrorDomain Code=14 "Calendar has no source" UserInfo={NSLocalizedDescription=Calendar has no source}</p></blockquote>
<p>然而<code>EKSource</code>并不可以通过EventKit管理,也就是说,系统有哪些日历账户,就只能用哪些,获取的办法如下:</p>
<pre><code class="objectivec">+ (EKSource*)sourceWithType:(EKSourceType)type
{
EKEventStore* store = [[EKEventStore alloc] init];
EKSource *localSource = nil;
NSLog(@"%@",store.sources);
for (EKSource *source in store.sources)
{
if (source.sourceType == type)
{
localSource = source;
break;
}
}
return localSource;
}</code></pre>
<p>我们通过遍历所有的系统Source,来匹配我们需要的Source。</p>
<p>这里就引出坑二,<strong>当系统开启或关闭了iCloud日历功能的时候,Source会有不同</strong>,例如开启iCloud日历的时候,我的设备只有一个<code>EKSourceTypeCalDAV</code>类型的叫iCloud的Source和一个Other类型的Source(每个设备可能不尽相同),但在其他没有开启的设备上,才有<code>EKSourceTypeLocal</code>类型的Source,所以上面一段代码的Source匹配,可能要执行多次,或者按照先后顺序匹配,如果单独只匹配一种类型,会有可能找不到可以使用的Source。</p>
<p>找到Source以后,将其赋值给Calendar,注意<code>EKCalendar</code>的这个属性虽然不是<code>Readonly</code>,但只能在初始化日历的时候进行设置,不能再更改。</p>
<pre><code class="objectivec">birthday.source = [[self class] sourceWithType:EKSourceTypeCalDAV];
if (!birthday.source) {
birthday.source = [[self class] sourceWithType:EKSourceTypeLocal];
}</code></pre>
<p>如此则可顺利添加自定义的日历。</p>
<h4>添加事件</h4>
<p>最后一步添加事件则很简单</p>
<pre><code class="objectivec">+ (void)addEvent
{
EKEvent* event = [EKEvent eventWithEventStore:store];
event.calendar = birthday;
event.title = @"我的生日";
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-objectivec-dd-HH-objectivec"];
event.startDate = [formatter dateFromString:@"2017-11-12-00-00"];
event.endDate = [formatter dateFromString:@"2017-11-12-23-59"];
NSError* error;
[store saveEvent:event span:EKSpanThisEvent error:&error];
if (error) {
NSLog(@"%@",error);
}
NSLog(@"%@",event.eventIdentifier);
} </code></pre>
<p>event设置标题、日历和起始日期即可,保存步骤中用到的<code>EKSpanThisEvent</code>枚举表示只应用于当前事件,这个枚举的另一个值<code>EKSpanFutureEvents</code>表示应用到所有此事件,包括重复事件的未来的事件。</p>
<p>最后保存成功的话,得到的<code>eventIdentifier</code>,应用可以根据需要保存在本地。</p>
<p>以上就是EventKit的基础教程。</p>
教你写一个可以找到.m文件所有接口名的命令行工具
https://segmentfault.com/a/1190000009396667
2017-05-12T17:06:29+08:00
2017-05-12T17:06:29+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<p><a href="https://link.segmentfault.com/?enc=f1zrS0ih2RmE1U6Ja7cmNg%3D%3D.Vc8rv%2BBbMfHABxY5p2zS65cmTLZNlng4VCOOPfJoRC3hFGR1%2FurNzZJtT9YASMn4oP5HKcNg2ERzoiWN5lRRkw%3D%3D" rel="nofollow">项目github</a></p>
<h2>出发点</h2>
<p>今天工作中写了一个工具类,在.m中完成所有功能后,发觉把所有接口从.m中拷贝到.h中声明,好麻烦啊,所以就考虑写个命令行工具来做这些工作。</p>
<h2>想要达到的结果</h2>
<p>我们设计这个小工具,在终端中直接运行,传入一个.m文件路径参数,输出其中所有的方法名。</p>
<pre><code class="objective-c">input:
> fti PWFileController.m
output:
- (NSString *)bytesToAvaiUnit:(long long)bytes;
- (long long) fileSizeAtPath:(NSString*) filePath;
- (long long) folderSizeAtPath:(NSString*) folderPath;
- (void) clearFolderAtPath:(NSString*) folderPath;
- (float)getTotalDiskSpace;
- (NSString *)getHomeDirectory;
</code></pre>
<h2>开始</h2>
<p>第一步新建一个mac的命令行(Command Line Tool)项目,这种项目只有一个<code>main.m</code>文件,内容如下</p>
<pre><code>
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}</code></pre>
<p>这里先分析一下原理,首先.m文件中的C函数方法是不带自动内存池的,所以要在C方法中使用ObjC代码,必须使用<code>@autoreleasepool</code>大括号括起来,这样才能保证在C方法结束后,栈内存能够释放。</p>
<p>其次,main函数中的argc参数,代表命令行中参数的个数,argv这个char数组,是每个参数的内容。</p>
<p>所以我们首先判断argc的个数,这里要注意,shell中的命令本身占一个参数位,所以没有任何参数的时候,argc应该为1。</p>
<pre><code class="objective-c">if(argc<=1) return 0; //当argc<=1直接退出程序</code></pre>
<p>接着我们要获取命令行输入的第二个参数,也就是.m文件路径</p>
<pre><code class="objective-c">NSString* filePath = [[NSString alloc] initWithCString:argv[1] encoding:NSUTF8StringEncoding];</code></pre>
<p>如果文件不存在,则结束程序</p>
<pre><code class="objective-c">if(![[NSFileManager defaultManager] fileExistsAtPath:filePath])
{
NSLog(@"文件不存在");
return 0;
}</code></pre>
<p>接着我们在main函数之前声明一个找接口的方法,这个方法要用C语言方法的格式声明</p>
<pre><code>NSArray* findInterface (NSString* text);</code></pre>
<p>然后实现它,注意要加<code>@autoreleasepool</code></p>
<pre><code class="objective-c">NSArray* findInterface (NSString* text)
{
@autoreleasepool {
NSString *regex = @"-\\s?\\(.*?\\).*?(?=\\n|$|\\{)";
NSString *str = text;
NSError *error;
NSRegularExpression *regular = [NSRegularExpression regularExpressionWithPattern:regex
options:NSRegularExpressionCaseInsensitive
error:&error];
// 对str字符串进行匹配
NSArray *matches = [regular matchesInString:str
options:0
range:NSMakeRange(0, str.length)];
NSMutableArray* result = [NSMutableArray arrayWithCapacity:matches.count];
// 遍历匹配后的每一条记录
for (NSTextCheckingResult *match in matches) {
NSRange range = [match range];
NSString *mStr = [str substringWithRange:range];
[result addObject:mStr];
}
return [result copy];
}
}</code></pre>
<p>这一段正则表达式的搜索没有特别要说明的,关于<code>NSRegularExpression</code>这个类的正则的用法,比较简单,参考上面代码就行,所以我简单说下正则的匹配规则</p>
<pre><code>-\\s?\\(.*?\\).*?(?=\\n|$|\\{)</code></pre>
<p>以<code>-</code>符号开头,在第一个左括号中间有若干空格,然后有若干空格和字符,然后有一个右括号,接下来又是若干个空格和字符,结尾要匹配三个,换行符<code>\n</code>,字符串结尾<code>$</code>和左大括号<code>{</code></p>
<p>这样我们在main方法中读取文件内容,然后调用这个方法即可输出所有的接口名。</p>
<pre><code class="objective-c">NSString* s = [[NSString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
NSString* f = [[findInterface(s) componentsJoinedByString:@";\n"] stringByAppendingString:@";"];
NSLog(@"result:\n%@",f);</code></pre>
<p>完整的代码请参考 <a href="https://link.segmentfault.com/?enc=ruMLpallIsD7Ll9Xn%2Fw%2B0A%3D%3D.BH%2BqSWIWeelKgKC%2Bm3ctVz6PQcfGKCVGogTGtrPZtrg4HTnipBmuV8exiRKBSayKmPJK8Zj6X6uWPIWFUW46PA%3D%3D" rel="nofollow">项目github</a></p>
<h2>使用</h2>
<p>这个项目通过菜单 Product -> Archive 可以发布released版本的运行程序,然后将其拷贝到<code>/usr/local/bin</code>目录下,即可在terminal中直接使用。</p>
<p>注意我为了方便,把Archive出来的运行程序名,简化为<code>fti</code>。</p>
<h2>补充</h2>
<p>类方法匹配,把正则中的<code>-</code>改为<code>(-|\\+)</code>即可。<br>换行的方法,可以根据<code>{</code>来匹配,把<code>(?=\\n|$|\\{)</code>改为<code>[^;]*?(?=\\{)</code>。<br>原理各位自己分析。</p>
<p>因为修改了匹配规则,我们需要对抓取的内容进行一些处理,<br>在findInterface方法中,我们去掉检索内容的换行符和<code>;</code>,用<code>stringByReplacingOccurrencesOfString</code>方法实现</p>
<pre><code class="objective-c"> for (NSTextCheckingResult *match in matches) {
NSRange range = [match range];
NSString *mStr = [str substringWithRange:range];
mStr = [mStr stringByReplacingOccurrencesOfString:@"\n" withString:@""];
mStr = [mStr stringByReplacingOccurrencesOfString:@";" withString:@""];
mStr = format(mStr); //这个方法在下面的内容zh
[result addObject:mStr];
}</code></pre>
<p>然后我们增加一个format方法,来把多行函数,格式化成标准的一行函数。</p>
<pre><code class="objective-c">NSString* format (NSString* string){
@autoreleasepool {
string = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSArray *components = [string componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
components = [components filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self <> ''"]];
string = [components componentsJoinedByString:@" "];
return string;
}
}
</code></pre>
UIView-Maker,实现UIView的clone操作和统一样式模型
https://segmentfault.com/a/1190000009300564
2017-05-05T11:17:20+08:00
2017-05-05T11:17:20+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<p><strong>文中列举的代码并不完整</strong><br><a href="https://link.segmentfault.com/?enc=FuME1BmPeJwEsSNacmdQ8Q%3D%3D.h5kgc6%2F7Q4PGnOGqljftY6uHwoYFcRVJLoTI6BYEhRoKR0mUdJKx%2Fr3UbVgdRwvA" rel="nofollow">源代码</a></p>
<p>在iOS的开发中,对于页面偏多的中大型项目来说,使用纯storyboard进行页面构建是比较痛苦的,所有的困难中,首当其冲的是批量修改控件样式。虽然Apple的工程师提供了像<code>appearance</code>这样的特别技术帮助大家批量配置样式,但面对庞大的项目仍有一定的局限性。</p>
<p>处于这种考虑,我们可以尝试扩展UIView的Category,方便我们在软件启动阶段,将所需要的某种大量使用的控件进行缓存,在使用时直接进行克隆操作,减少了代码中重复设置样式的步骤,同时方便后期进行批量样式修改。</p>
<p>首先第一步,我们要实现UIView的clone(克隆)操作</p>
<h2>如何clone一个UIView? 使用 NSKeyedArchiver/NSKeyedUnarchiver</h2>
<p>核心代码:</p>
<pre><code class="objective-c">+ (__kindof UIView*)duplicate:(__kindof UIView*)view
{
NSData * tempArchive = [NSKeyedArchiver archivedDataWithRootObject:view];
return [NSKeyedUnarchiver unarchiveObjectWithData:tempArchive];
}</code></pre>
<p>接下来,我们可以使用 <code>- (__kindof UIView*)clone;</code> 方法来克隆一个UIIView</p>
<pre><code class="objective-c">- (UIView *)clone
{
return [[self class] duplicate:self];
}</code></pre>
<h2>将UIView注册为标准模版</h2>
<p>我们可以设置key作为模版的标识, 然后使用<code>make block </code> 来配置UIView的样式。</p>
<pre><code class="objective-c">+ (void)registStyle:(NSString*)key make:(void(^)(id view))makeBlock;</code></pre>
<p>举个例子,这里使用的是MDCButton:</p>
<pre><code class="objective-c">[MDCButton registStyle:@"category_button" make:^(MDCButton* _Nonnull view) {
view.frame = CGRectMake(15, 30, 50, 50);
[view setBackgroundColor:[UIColor add_colorWithRGBHexString:@"#178EDA"] forState:UIControlStateNormal];
}];</code></pre>
<p>当我们需要实例化 <code>category_button</code> 的时候:</p>
<pre><code class="objective-c">MDCButton* clone = [MDCButton cloneForKey:@"category_button"];</code></pre>
<h2>实现相同样式多个控件的布局</h2>
<p>使用 <code>copyTimes</code>方法来clone并布局多个相同样式的UIView:</p>
<pre><code class="objective-c">- (void)copyTimes:(NSUInteger)times make:(void(^)(id view, NSUInteger idx))makeBlock;</code></pre>
<p>举个例子,排列10个相同的UILabel:</p>
<pre><code class="objective-c">UILabel* label = [[UILabel alloc] init];
[label copyTimes:10 make:^(id _Nonnull view, NSUInteger idx) {
[self.view addSubview:view];
[view setFrame:CGRectMake(0, idx * 100, 50, 100)];
}];</code></pre>
<h2>小结</h2>
<p>本文中用到了NSKeyedArchiver/NSKeyedUnarchiver序列化/反序列化操作,来实现UIView对象的克隆,然后使用的语言block特性,批量配置样式,内存缓存的代码可以见<a href="https://link.segmentfault.com/?enc=JaC9Nrxg4VME3atlFvLSKg%3D%3D.qh2O3jqwRAD%2F9wVdShAyjaPio2kxjC4Q%2BSsLfFE1KGA%2B0zDDjzueUhaRFsvp8Q4K" rel="nofollow">源代码</a>,欢迎留言讨论。</p>
MMDrawerController和UITableView的手势冲突
https://segmentfault.com/a/1190000009278331
2017-05-03T16:58:26+08:00
2017-05-03T16:58:26+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<p><code>MMDrawerController</code>作为一个在Github上超过6k Star的热门项目,估计不少朋友都有用过。</p>
<p>在<code>MMDrawerController</code>的使用过程中,难免遇到一下手势冲突问题,这里集中记录一下本人的解决办法。</p>
<h2>MMOpenDrawerGestureModeAll造成UITableView不能侧滑打开cell编辑功能的问题</h2>
<p>这个问题解决需要两个步骤,首先,继承<code>MMDrawerController</code>写一个子类,然后在子类中实现UIPanGesture的</p>
<pre><code>- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer</code></pre>
<p>这个回调,如下</p>
<pre><code>- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer* pan = (UIPanGestureRecognizer*)gestureRecognizer;
CGPoint p = [pan translationInView:pan.view];
if(fabs(p.y)<fabs(p.x) && p.x > 0)
{
return NO;
}
return fabs(p.y)<fabs(p.x);
}
return YES;
}</code></pre>
<p>用<code>if(fabs(p.y)<fabs(p.x) && p.x > 0)</code>判断滑屏方向为横向向右时,return NO,禁止tableView上下滚动,如果去掉这段,则在向右打开过程中同时tableView还可以上下滑动。</p>
<p>然后用<code>fabs(p.y)<fabs(p.x)</code>判断是否为向左的水平滑动,我这里只使用了左边侧滑菜单,所以向左划时候,return YES,保证了tableView的cell编辑功能顺利开启。</p>
<h2>MMOpenDrawerGestureModeAll(全打开边栏方式)造成中心VC的UITableView不能点击的问题</h2>
<p>同样的思路,在继承<code>MMDrawerController</code>的子类中添加如下代码</p>
<pre><code class="ObjC">- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]])
{
if(self.openSide == MMDrawerSideNone)
{
return NO;
}
}
return [super gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch];
}</code></pre>
<p>这里解决的原理也很简单,在边栏为关闭状态的时候,禁止<code>MMDrawerController</code>的pan手势接受tap类型的touch。</p>
<h2>UITableViewCell 编辑状态下,右滑关闭时打开左侧栏的问题</h2>
<p>这个问题换一个思路解决,在tableView开始编辑和停止编辑的回调中,调整<code>MMDrawerController</code>的菜单开启属性。</p>
<pre><code>- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
self.mm_drawerController.openDrawerGestureModeMask = MMOpenDrawerGestureModeNone;
}
- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
self.mm_drawerController.openDrawerGestureModeMask = MMOpenDrawerGestureModeAll;
}</code></pre>
<p>完美解决问题。</p>
<p>如有其他问题欢迎留言讨论。</p>
ARC 模式下的循环引用引起内存泄漏
https://segmentfault.com/a/1190000006840077
2016-09-07T13:43:06+08:00
2016-09-07T13:43:06+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<p>自从iOS 5时代自动引用计数(Automatic Reference Counting)技术发布,Cocoa工程师们才扔下了内存管理的包袱,从此在Objective-C修行道路上的一座大山被削平。然而,即使ARC很强大,我们日常搬砖时同样是有内存泄漏风险的,今天我就跟大家聊聊这些你可能还没有注意到的坑。</p>
<h2>测试原理</h2>
<p>我们知道ARC模式下,<code>NSObject</code>的<code>MRC</code>相关方法都不可以使用了,但<code>dealloc</code>方法如果实现了,同样还是会调用的,只是不允许在<code>dealloc</code>方法中调用<code>[super dealloc]</code>,所以我们在<code>dealloc</code>方法中加入log信息就可以跟踪到我们的实例是否释放。</p>
<h2>容易忽视的引用循环</h2>
<p>我们知道<code>引用计数</code>内存管理的设计理念,就是根据实例的计数值来决定是否释放实例内存空间。</p>
<p>例如我们的ViewController拥有一个block类型的property</p>
<pre><code class="objective-c">@property (nonatomic, strong) void (^ testBlock)(void);</code></pre>
<p>我们在viewDidLoad中加入如下代码</p>
<pre><code class="objective-c">[self setTestBlock:^{
self.title = @"测试";
}];</code></pre>
<p>这个代码从表面上看没有什么问题,但编译器会给出warning,</p>
<blockquote><p>Catering 'self' strongly in this block is likely to lead a retain cycle</p></blockquote>
<p>翻译过来意思是在block中使用self指针,可能会引起一个引用循环,导致self无法释放。</p>
<h3>什么是引用循环(retain cycle)</h3>
<p>假设我们有两个实例A和B,B是A的一个strong型的property,则B的引用计数是1,当A的需要释放的时候,A则会调用<code>[B release]</code>来释放B,B的引用计数则减为0,释放。</p>
<p>可如果这时候将B的一个strong型property指向A,则A与B互相为强引用,问题就来了。因为B强引用A,A的引用计数永远不会减为0,当A原本的强引用对象被释放以后,A和B成为了一个相互引用的孤岛,永远不会被释放了,这就会引起内存泄漏。</p>
<p>在上面的例子中,就是一种非常普遍的引用循环情况,加入如上代码的VC在dismiss或者pop以后,并不会执行dealloc方法,证明内存泄漏了。而引起泄漏的原因就是在作为self的property的block中,使用self指针导致self被block强引用,形成引用循环。</p>
<h2>如何解决引用循环问题</h2>
<p>在编译器提示上面的warning的时候一定不要忽视,正确的解决办法如下:</p>
<pre><code class="objective-c">__unsafe_unretained Demo1ViewController * weakSelf = self;
[self setTestBlock:^{
weakSelf.title = @"测试";
}];</code></pre>
<p>或者使用__weak也可以,原理也很简单,就是声明一个弱引用对象在block中替代self,这样在我们测试中,下面代码就能正常输出log,标志着VC被正确释放。</p>
<pre><code class="objective-c">- (void)dealloc
{
NSLog(@"%s",__func__);
}</code></pre>
<blockquote><p>2016-09-07 13:17:38.879 ReactiveCocoaDemo[7473:3432323] -[Demo1ViewController dealloc]</p></blockquote>
<h2>其他会引起引用循环的状况</h2>
<h3>NSTimer</h3>
<p><code>NSTimer</code>在VC释放前,一定要调用<code>[timer invalidate]</code>,不调用的后果就是<code>NSTimer</code>无法释放其target,如果target正好是self(VC本身),则引用循环。</p>
<p>这里要补充一点,引用循环不是只能有两个对象,三个四个更多都是可以的,甚至环数也不一定只有一个,所以要养成良好的代码习惯,在<code>NSTimer</code>停用前调用<code>invalidate</code>方法。</p>
<h3>WKUserContentController</h3>
<p>这个类一般会在使用<code>WKWebView</code>的时候配套使用,如果你发现项目中调用了<code>addScriptMessageHandler</code>方法,就要注意了,检查有没有在VC释放前对称调用<code>removeScriptMessageHandlerForName</code>方法,如果没有则引起引用循环。</p>
<p>调用方法如下:</p>
<pre><code class="objective-c">[self.wkWebView.configuration.userContentController removeScriptMessageHandlerForName:@"qdpay"];</code></pre>
<p>注意<code>WKUserContentController</code>和<code>WKWebView</code>中还有一个<code>WKWebViewConfiguration</code>。</p>
<h2>引用大循环</h2>
<p>就像前面说的,引用循环可能是一个大循环。我遇到过一种情况,就是给<code>UITableViewCell</code>设置block属性响应事件,在block中强引用了self,导致self->tableView->cell->self形成循环。</p>
<h2>改善block写法避免强引用self</h2>
<p>如果要从根本改变这种易发的错误,要从写法开始改变,开始避免。将上面的代码改写如下:</p>
<pre><code class="objective-c">@property (nonatomic, strong) void (^ testBlock)(__kindof UIViewController* sender);
[self setTestBlock:^(__kindof UIViewController * vc) {
vc.title = @"123";
}];
self.testBlock(self);</code></pre>
<p>将self作为参数传入block即可避免强引用,从逻辑角度来看,代码更健壮。</p>
<h2>结束</h2>
<p>上面列举的几个引用循环引起的内存泄漏,编译器是没有任何提示的,并且也不影响App运行,不会crash,但作为严谨的程序猿,我们不能容忍这种的小泄漏,虽然不影响大局,但积少成多终将影响系统的运行速度。</p>
Grand Central Dispatch 1
https://segmentfault.com/a/1190000006657435
2016-08-19T15:33:51+08:00
2016-08-19T15:33:51+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
3
<p>[TOC]</p>
<h2>GCD是什么</h2>
<p>Grand Central Dispatch 是苹果公司发布的一套多核多线程任务分发的解决方案,简称GCD,或者你叫他滚床单也没有人反对,嘿嘿。</p>
<h2>GCD发布</h2>
<p>苹果公司首次发布GCD是伴随Mac OS X 10.6 和 iOS 4系统一起发布的,也正是伴随着block块语法的支持,GCD技术将多线程执行代码,通过block封装成代码块,大大提高了多线程开发的效率,减少了开发难度,也极大增强了代码的可读性。</p>
<h2>GCD之前的黑暗时代</h2>
<p>如果我将GCD技术比喻成普罗米修斯带给人类的火种有一些夸张的话,至少可以将其比作火柴。而在没有生火器的石器时代,人类只能依靠何钻木取火。</p>
<h3>POSIX线程</h3>
<p>POSIX线程(pthread)是一套C语言编写的线程管理API,面向过程,我只在老东家一套C源码库中见别人用过,自己从来没有用过,也不会用,就像我也不会钻木取火一样。</p>
<h3>NSThread</h3>
<p>Cocoa框架中,用OC将pthread对象化封装,就诞生了<code>NSThread</code>操作类,但很可惜至今<code>NSThread.h</code>头文件中一行注释都木有,只能看出这个类早在1994年就已经存在了。</p>
<p>这里就不列举具体事例了,因为如今这个类的使用频率已经非常低了,唯一一种你可能会遇到的使用情境是判断当前执行线程是否为主线程,具体代码如下</p>
<pre><code class="objc"> if([NSThread isMainThread]){
}</code></pre>
<p>但你在GCD和NSOperation出现之前,会在各种需要多线程处理的情况下,使用<code>NSThread</code>的隐式调用方法,也就是NSThread头文件中给<code>NSObject </code>类作为属性方法扩展的一系列接口:</p>
<pre><code class="objc">@interface NSObject (NSThreadPerformAdditions)
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
@end</code></pre>
<p>总计五个API,简易实现了一般开发需要使用的基本线程操作,避免用户自己动手写NSThread调度,引发的一些列莫名的死锁问题,在某种程度上减少了当时的多线程开发难度。</p>
<p>但这些API有一些很直观的问题,例如由于OC语言限制,这些API的参数传递、返回值获取都不易实现,并且实际写出来的代码也会因为逻辑跳转分布在文件的各个位置,影响阅读和纠错,你不相信请看我从教科书上抄下来的例子:</p>
<pre><code class="objc">- (void)launchThreadByNSObject_performSelectorInBackground_withObject
{
[self performSelectorInBackground:@selector(doWork) withObject:nil];
}
- (void) doWork
{
/*
*
* 长时间处理
*
* 例如 图像处理
* 网络数据请求
* 大型数据库操作
* 磁盘操作
*/
//操作结束后调用主线程修改UI
[self performSelectorOnMainThread:@selector(doneWork) withObject:nil waitUntilDone:NO];
}
- (void) doneWork
{
//主线程修改UI
}</code></pre>
<p>这个例子是一个解决关于主线程刷新UI问题的例子,我们同学都知道所有有关UI刷新的方法,务必要在主线程调用,这是个硬性要求,是因为UI渲染就是在主线程循环中完成的,如果在支线程中调用,会出现莫名其妙的错误、UI卡死或者程序崩溃。</p>
<p>所以多线程在我们的日常开发中,用得最多的地方,就是网络数据的异步请求,然后主线程刷新UI。将有延迟和计算量大的操作放在支线程完成,待完成后使用主线程刷新UI,才能有效地防止主线程UI刷新阻塞。</p>
<h3>iOS 4与block</h3>
<p>iOS 4带来的编译器对block块语法的支持,有点像人类发现了磷这种易燃物质一样,带来的是火柴(GCD 和 NSOperation)这个更简易的生火工具。</p>
<p>GCD和NSOperation 可以看作是 pthread(面向过程)和NSThread(面向对象)的block升级版本,带来的多线程编程体验则是质的飞跃。</p>
<p>GCD像是火柴,轻便易用,随用随取。NSOperation则像打火机,一次开发,重复使用。</p>
<h2>GCD实战</h2>
<p>好了,已经说了十几分钟废话了,终要进入主题进行GCD多线程开发实战。在开始之前,希望大家要提前学习block块语法的相关知识,不要求熟练使用,只要求看得懂。</p>
<h3>实战一 异步加载</h3>
<p>还记得我们在上面展示的从教科书上抄下来的例子么,这个例子如果改成GCD的版本,会是什么样子的呢?</p>
<pre><code class="objc">//异步请求Dispatch
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//长时间处理
dispatch_async(dispatch_get_main_queue(), ^{
//主线程更新UI
});
});</code></pre>
<p>这是什么鬼?我来解释一下。GCD使用的是C语言风格的调用接口,栗子中调用了两次<code>dispatch_async</code>方法,第一次将长时间处理操作分拨到支线程处理,在其完成后,跳转回主线程更新UI,操作都在方法的block参数中传入,简单明了,层级分明,没有传参障碍,没有阅读障碍,一气呵成,简直美极了。</p>
<p><code>dispatch_async</code>方法传入的第二个参数是执行block,没啥好说的,第一个参数则是线程。<code>dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)</code>方法获取的Global线程,是非主线程中的一个,具体是哪个不用开发者操心,反正是系统认为这时候不是很忙的那一个。而两个传入参数中的第一个是线程的优先级(共四个优先级),第二个参数则约定为0。<code>dispatch_get_main_queue()</code>这个没有任何参数的方法,返回的则是主线程。 </p>
<p>注意这里返回的参数类型是<code>dispatch_queue_t</code>,是一个普通变量,估计是线程的索引。真是两三句话就能讲明白的方法调用,什么你说听不懂、看不懂。无所谓呀~ 我们将这段代码加入代码片段,需要使用的时候拿出来用就行啦。</p>
<p>比如:</p>
<h4>图片异步加载</h4>
<p>首先放开权限<code>NSAppTransportSecurity</code>,<code>NSAllowsArbitraryLoads</code></p>
<h5>NSURLConnection版本</h5>
<p>不加多线程异步操作</p>
<pre><code class="objc">- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
[cell.imageView setImage:[UIImage new]];
NSURL* url = [NSURL URLWithString:self.static_data[indexPath.row]];
NSData* data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:nil error:nil];
UIImage* image = [UIImage imageWithData:data];
[cell.imageView setImage:image];
[cell setNeedsLayout];
return cell;
}</code></pre>
<p>使用GCD以后</p>
<pre><code class="objc">- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
[cell.imageView setImage:[UIImage new]];
NSURL* url = [NSURL URLWithString:self.static_data[indexPath.row]];
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData* data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:nil error:nil];
UIImage* image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
[cell.imageView setImage:image];
[cell setNeedsLayout];
});
});
return cell;
}</code></pre>
<h5>NSURLSession版本</h5>
<pre><code class="objc">- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
// cell.backgroundColor = [UIColor lightGrayColor];
// Configure the cell...
[cell.imageView setImage:[UIImage new]];
NSURL* url = [NSURL URLWithString:self.static_data[indexPath.row]];
NSURLSessionConfiguration* c = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession* session = [NSURLSession sessionWithConfiguration:c];
NSURLSessionDataTask* task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage* image = [UIImage imageWithData:data];
// NSLog(@"%@",image);
// NSLog(@"%@",cell.imageView);
[cell.imageView setImage:image];
[cell setNeedsLayout];
});
}];
[task resume];
return cell;
}</code></pre>
<p>这次试出来UI刷新的阻塞感受了么?啊?你说没有,那你用真机调试一下,就会有更明显的感受了。</p>
<p>UI阻塞在实际开发中,偶尔会遇到。并且会引起一些莫名其妙的bug,希望大家再遇到时候能及时往这方面思考。比如,我们如果在<code>UIViewController</code>的初始化等一系列加载函数中加入能引起阻塞的代码,整个VC的加载会产生卡顿,还很有可能直接崩溃。</p>
<p>所以将阻塞操作放在支线程处理,是十分必要的。我们只要将下面代码存为代码片段,随用随取。</p>
<pre><code class="objc">//支线程调用
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
<#code#>
});
//主线程调用
dispatch_async(dispatch_get_main_queue(), ^{
<#code#>
});</code></pre>
<h3>实战二 同步操作等待</h3>
<p>多线程操作的第二个常用情景就是并行操作等待。</p>
<pre><code class="objc">dispatch_group_t group = dispatch_group_create();
// 合并汇总结果
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
//并行阻塞操作1
[NSThread sleepForTimeInterval:1.0];
NSLog(@"1");
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
//并行阻塞操作2
[NSThread sleepForTimeInterval:0.5];
NSLog(@"2");
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
//并行阻塞操作3
NSLog(@"3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//3项操作都完成后调用主线程更新UI
NSLog(@"4");
});</code></pre>
<p>在这段演示代码里面,即使你看不懂GCD相关调用,也能猜出最后的输出结果对吧,我解释一下<code>[NSThread sleepForTimeInterval:1.0];</code>这句调用是让线程睡眠1秒中,模拟1秒钟阻塞。</p>
<p>好的告诉我你的答案。</p>
<pre><code class="objc">3
2
1
4</code></pre>
<p>这也是一段可以收藏为代码片段的实用工具,可以起名为<code>并行代码等待</code>。就像<code>异步等待</code>一样,我们现在来举一个简单的实际案例。</p>
<h4>并行操作案例</h4>
<p>还是举一个不是很简单的例子,也可能不是很实用,但绝对能体现这套逻辑的精髓。在讲栗子之前,我们先来学习一下SDWebImage的另外一段代码(对,又是SDWebImage)。</p>
<pre><code class="objc">//SDImageCache.m 608行
- (NSUInteger)getSize {
__block NSUInteger size = 0;
dispatch_sync(self.ioQueue, ^{
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
for (NSString *fileName in fileEnumerator) {
NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
});
return size;
}</code></pre>
<p>这段代码,具体功能是进行文件夹文件大小的统计。对你没有听错,文件夹是无法直接接获取其大小的,需要遍历其中每个文件然后相加统计。</p>
<p>这段代码实用GCD,但使用的方法我们前面并没有讲过,我放在后面再说。目前我们的任务是把这个方法改造一下,让他可以统计任意的文件夹大小。</p>
<pre><code class="objc">- (NSUInteger)getSize:(NSString*)dicPath {
__block NSUInteger size = 0;
NSDirectoryEnumerator *fileEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:dicPath];
for (NSString *fileName in fileEnumerator) {
NSString *filePath = [dicPath stringByAppendingPathComponent:fileName];
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
return size;
}</code></pre>
<p>接下来我们统计一下cache目录和tmp目录的容量</p>
<pre><code class="objc">NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDir = [paths objectAtIndex:0];
NSString *tmpDir = NSTemporaryDirectory();
NSUInteger cacheSize = [self getSize:cachesDir];
NSUInteger tmpSize = [self getSize:tmpDir];
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + tmpSize),@(cacheSize),@(tmpSize));</code></pre>
<blockquote><p>total size : 657060 (657060+0)</p></blockquote>
<p>tmp文件夹是空的,我们换成libiary目录,不过因为cache目录在libiary目下,所以是有重复的,不过无所谓。</p>
<pre><code class="objc">NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDir = [paths objectAtIndex:0];
NSArray * paths2 = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString * libraryPath = paths2[0];
NSUInteger cacheSize = [self getSize:cachesDir];
NSUInteger librarySize = [self getSize:libraryPath];
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + librarySize),@(cacheSize),@(librarySize));</code></pre>
<p>我们在执行这段代码的时候,一般会很顺畅就执行完了,没有任何阻塞。原因是统计的目标目录,文件非常少。如果遇到文件稍多的情况,上面这段代码就出出现阻塞,又因为整个是在主线程操作的,所以必然会影响到UI的刷新,界面会卡顿。好,那让我们运用前面的GCD模版来将这段代码改造成异步执行。</p>
<pre><code class="objc">NSLog(@"1");
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDir = [paths objectAtIndex:0];
NSArray * paths2 = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString * libraryPath = paths2[0];
NSUInteger cacheSize = [self getSize:cachesDir];
NSUInteger librarySize = [self getSize:libraryPath];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + librarySize),@(cacheSize),@(librarySize));
});
});
NSLog(@"2");</code></pre>
<p>上面是我修改的结果,大家来分析一下输出顺序,应该是</p>
<pre><code class="objc">1
2
total size : 1314324 (657060+657264)</code></pre>
<p>前面坐了这么多铺垫,接下来我们进入正题,讲解一下串行和并行。串行很好理解,我们一般写的代码都是一步一步一串一串执行的。并行则是多项任务同时进行,也不难理解,类似于中学物理学的电路的并联合串联。</p>
<p>上面这段代码,我们前后调用两次getSize方法,按顺序分别统计了两个目录的大小,我们统计一下耗时:</p>
<pre><code class="objc">dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
clock_t begin, duration;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDir = [paths objectAtIndex:0];
NSArray * paths2 = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString * libraryPath = paths2[0];
begin = clock();
NSUInteger cacheSize = [self getSize:cachesDir];
NSUInteger librarySize = [self getSize:libraryPath];
duration = clock() - begin;
NSLog(@"%@",@((double)duration/CLOCKS_PER_SEC));
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + librarySize),@(cacheSize),@(librarySize));
});
});</code></pre>
<blockquote><p>0.002481</p></blockquote>
<p>这里单位是秒,其实已经很快。 好,我们把前面的并行模版套进来。</p>
<pre><code class="objc">dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *path = [[NSBundle mainBundle] bundlePath];
NSArray * paths2 = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString * libraryPath = paths2[0];
__block clock_t begin, duration;
__block NSUInteger cacheSize,librarySize;
dispatch_group_t group = dispatch_group_create();
// 合并汇总结果
begin = clock();
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
cacheSize = [self getSize:path];
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
librarySize = [self getSize:libraryPath];
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
duration = clock() - begin;
NSLog(@"%@",@((double)duration/CLOCKS_PER_SEC));
NSLog(@"total size : %@ (%@+%@)",@(cacheSize + librarySize),@(cacheSize),@(librarySize));
});
});</code></pre>
<blockquote><p>0.039834</p></blockquote>
<h5>提问</h5>
<p>结果很让我欣慰,整整大了一个数量级,请你们分析一下原因。</p>
<p>原因也很简单,就是因为统计这种小目录是在耗时太短,短到比创建GCD Group的CPU占用都要少,所以耗时不降反增,呵呵。但一旦这个耗时任务CPU占用大于GCD消耗的时候,并行操作带来的耗时收益就是</p>
<blockquote><p>串行总耗时 - 并行最大耗时</p></blockquote>
<h2>小结</h2>
<p>这节课由于篇幅有限,我们讲的内容并不多,但实用性很高。大家注意到没有,从头到尾我们等于讲任何与GCD有关的接口调用、类型相关的内容,却教会了你进行异步请求和同步等待操作的方法,模版拿过来基本不用修改就能嵌套使用,这就叫知其然。下一章节我们再从API方向讲解GCD的类型和方法调用,这叫知其所以然。</p>
Objective-C进化特性
https://segmentfault.com/a/1190000005739946
2016-06-17T09:31:46+08:00
2016-06-17T09:31:46+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>Objective-C进化特性</h2>
<p>[TOC]</p>
<p>毫无疑问,Objective-C (下称ObjC)在诞生三十年后,因为iOS系统在移动设备领域的制霸,迎来了近五年来一年一波的进化,这些新特性表明ObjC这个年事已高的语言,仍在一步一步追逐现代编程语言的步伐。</p>
<h3>Modern Objective-C 现代Objective-C语言</h3>
<h4>Automatic Reference Counting</h4>
<p>自动引用计数技术,简称<code>ARC</code>,是Objective-C的一项重要进化。编译器在编译过程中,自动分析代码,为对象添加release、retain、dealloc调用,大大减少开发中的内存管理代码,减少了工程师将近三分之一的工作负担,并且更加安全稳定。</p>
<p>如今的Xcode工程,都自动开启了ARC模式,在这种模式下,工程师不可以手动调用release、retain、dealloc、autoreloease等函数,但工程师还是可以灵活处理。</p>
<p>例如:</p>
<pre><code>@property (nonatomic, strong) NSArray* datas;
...
//某处
self.datas = nil; //编译器会自动在这据句调用前执行 [self.datas release];
...</code></pre>
<p>在例如:</p>
<pre><code> @autoreleasepool {
//括号里面分配内存的对象,都会加入大括号包裹的自动释放持,在大括号结束时释放。
}</code></pre>
<p>当然,任何ObjC的方法,每一对大括号都是自带自动释放池的,不是特殊需要不用特别地使用<code>@autoreleasepool</code>。</p>
<h4>Literals 字面量特性</h4>
<p>这个的翻译一直很难确定,可以认为是一种简写。</p>
<p>不使用Literals特性时我们这样写:</p>
<pre><code>NSArray *myArray = [NSArray arrayWithObjects:object1,object2,object3,nil];
NSDictionary *myDictionary1 = [NSDictionary dictionaryWithObject:someObject forKey:@"key"];
NSDictionary *myDictionary2 = [NSDictionary dictionaryWithObjectsAndKeys:object1, key1, object2, key2, nil];
NSNumber *myNumber = [NSNumber numberWithInt:myInt];
NSNumber *mySumNumber= [NSNumber numberWithInt:(2 + 3)];
NSNumber *myBoolNumber = [NSNumber numberWithBool:YES];</code></pre>
<p>使用Literals特性,就可以简写了:</p>
<pre><code>NSArray *myArray = @[ object1, object2, object3 ];
NSDictionary *myDictionary1 = @{ @"key" : someObject };
NSDictionary *myDictionary2 = @{ key1: object1, key2: object2 };
NSNumber *myNumber = @(myInt);
NSNumber *mySumNumber = @(2+3);
NSNumber *myBoolNumber = @YES;
NSNumber *myIntegerNumber = @8;</code></pre>
<p>让基本数据对象,操作起来更便捷,就是一大进步。</p>
<h4>Subscripting</h4>
<p>同样举例比对,首先是不使用Subscripting特性</p>
<pre><code>id object1 = [someArray objectAtIndex:0];
id object2 = [someDictionary objectForKey:@"key"];
[someMutableArray replaceObjectAtIndex:0 withObject:object3];
[someMutableDictionary setObject:object4 forKey:@"key"];</code></pre>
<p>然后是用了Subscripting特性</p>
<pre><code>id object1 = someArray[0];
id object2 = someDictionary[@"key"];
someMutableArray[0] = object3;
someMutableDictionary[@"key"] = object4;</code></pre>
<p>一目了然,更接近现代编程语言的风格。</p>
<h3>技术革新</h3>
<h4>block 闭包</h4>
<p><code>iOS4</code>引入的block特性,看看下面的例子,关于闭包的详细我们将单独写一篇文章进行讲解。</p>
<pre><code>#include <stdio.h>
#include <Block.h>
typedef int (^IntBlock)();
IntBlock MakeCounter(int start, int increment) {
__block int i = start;
return Block_copy( ^ {
int ret = i;
i += increment;
return ret;
});
}
int main(void) {
IntBlock mycounter = MakeCounter(5, 2);
printf("First call: %d\n", mycounter());
printf("Second call: %d\n", mycounter());
printf("Third call: %d\n", mycounter());
/* because it was copied, it must also be released */
Block_release(mycounter);
return 0;
}
/* Output:
First call: 5
Second call: 7
Third call: 9
*/</code></pre>
<h3>Grand Central Dispatch</h3>
<p>简称GCD技术,是Apple开发的用于简化开发<code>多核多线程</code>编程提供的一套底层C接口,详细内容将在单独的章节进行讲解。</p>
<p>//单线程阻塞</p>
<pre><code>- (IBAction)analyzeDocument:(NSButton *)sender {
NSDictionary *stats = [myDoc analyze];
[myModel setDict:stats];
[myStatsView setNeedsDisplay:YES];
}</code></pre>
<p>//多线程异步等待,数据加载完毕后,主线程渲染。</p>
<pre><code>- (IBAction)analyzeDocument:(NSButton *)sender {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSDictionary *stats = [myDoc analyze];
dispatch_async(dispatch_get_main_queue(), ^{
[myModel setDict:stats];
[myStatsView setNeedsDisplay:YES];
});
});
}</code></pre>
Objective-C基本数据类型
https://segmentfault.com/a/1190000005726614
2016-06-15T16:33:54+08:00
2016-06-15T16:33:54+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>Objective-C基本数据类型</h2>
<p>因为Objective-C(下称ObjC)本质是一个C语言的超集,所以所有C语言支持的基本数据类型,ObjC同样支持,并且ObjC还支持一些其它的常用数据类型。</p>
<h3>int 与 NSInteger</h3>
<p>C语言中的int,在ObjC中同样支持,但不建议你用int,而推荐使用Cocoa框架中的NSInteger,</p>
<p>详见NSInteger定义</p>
<pre><code>#if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif</code></pre>
<p>注意,这里主要是为了同时匹配64位和32位处理器,在上面官方框架的代码中我们看到,64位内核中NSInteger为long型,而在32位内核中为int型,使用NSInteger,就不用特意去考虑内核位宽问题。</p>
<h3>bool 与 BOOL</h3>
<p>C语言标准中没有布尔型变量,C++中的bool类型,为true和false,这在许多其他的类C语言中都是一样的,例如java、C#、php等,但在ObjC中,你可以使用bool类型,但更建议使用ObjC专用的BOOL类型,这个基本布尔型的值为YES和NO。</p>
<h3>float 与 CGFloat</h3>
<p>CGFloat 不是Foundation框架的基础变量,而是定义在UIKit框架中,CG代表CoreGraphic(核心绘图框架)。从定义来看,float和CGFloat的区别也是根据系统内核位宽不同,类型不同。</p>
<pre><code>#if defined(__LP64__) && __LP64__
# define CGFLOAT_TYPE double
# define CGFLOAT_IS_DOUBLE 1
# define CGFLOAT_MIN DBL_MIN
# define CGFLOAT_MAX DBL_MAX
#else
# define CGFLOAT_TYPE float
# define CGFLOAT_IS_DOUBLE 0
# define CGFLOAT_MIN FLT_MIN
# define CGFLOAT_MAX FLT_MAX
#endif
/* Definition of the `CGFloat' type and `CGFLOAT_DEFINED'. */
typedef CGFLOAT_TYPE CGFloat;</code></pre>
<p>可以看到,64位中CGFloat是double类型,32位中则是float类型。</p>
<h3>NSString</h3>
<p>char和string类型在ObjC中同样可用,但是基本也可以不用。NSString作为整个Cocoa框架的灵魂类,强大无比,基本能胜任现代编程语言对于字符串的所有基本处理和更复杂的处理。详细使用方法将在后面专门章节介绍。</p>
<p>NSString类,不需要alloc和init,Cocoa框架高度优化了NSString类,让他在实例化时操作起来就好像基本类型一样。例如</p>
<pre><code>NSString* textA = @"123";
NSString* textB = textA;
textA = @"456";
NSLog(@"%@",textA); //输出 456
NSLog(@"%@",textB); //输出 123</code></pre>
<p>注意到没有,按道理说将textB指针指向textA以后,textA值改变,textB应该也跟着变。但实际情况并没有,这是因为对于NSString类型来说,等号赋值,实际是深度拷贝。textA=@"456"这一步textA的指针已经改变,实际操作等同于 textA = [@"456" copy]。<br>textB = textA,实际操作等同于 textB = [textA copy]。<br>这里的copy函数,是NSObject的不可变拷贝方法。</p>
<p>另外NSString类,本身支持与许多基本类型的互转。</p>
<pre><code> //CGPoint 点转字符串
NSString *NSStringFromCGPoint(CGPoint point);
//CGVector 向量转字符串
NSString *NSStringFromCGVector(CGVector vector);
//CGSize 大小转字符串
NSString *NSStringFromCGSize(CGSize size);
//矩形转字符串
NSString *NSStringFromCGRect(CGRect rect);
//矩阵变换转字符串
NSString *NSStringFromCGAffineTransform(CGAffineTransform transform);
//边界转字符串
NSString *NSStringFromUIEdgeInsets(UIEdgeInsets insets);
//位移转字符串
NSString *NSStringFromUIOffset(UIOffset offset);
//上面的逆向方法
CGPoint CGPointFromString(NSString *string);
CGVector CGVectorFromString(NSString *string);
CGSize CGSizeFromString(NSString *string);
CGRect CGRectFromString(NSString *string);
CGAffineTransform CGAffineTransformFromString(NSString *string);
UIEdgeInsets UIEdgeInsetsFromString(NSString *string);
UIOffset UIOffsetFromString(NSString *string);</code></pre>
<p>注意所有上面的互转方法都是C函数,可以在任何地方调用。<br>CG系列的基本类型,则是使用C语言的结构体声明的。</p>
<p>另外NSString也可以转数字</p>
<pre><code> NSString* number = @"1111113";
NSInteger intValue = [number integerValue]; //转整形
CGFloat floatValue = [number doubleValue]; //转浮点</code></pre>
<h3>NSValue</h3>
<p>NSValue是个可以和各种基本数据类型互转的类。包括CGPoint、CGRect、CGSize等等。例如</p>
<pre><code>[NSValue valueWithCGSize:CGSizeMake(100, 100)];
[NSValue valueWithRange:NSMakeRange(0, 10)];</code></pre>
<h3>NSNumber</h3>
<p>NSNumber与上面不同的是,NSNumber不是基本数据类型,而是对象。<br>NSNumber 继承自 NSValue,而NSValue继承自NSObject。<br>NSNumber支持和基本数据类型的互转。</p>
<p>另外NSNumber支持和NSString一样的<code>@</code>符号简写</p>
<pre><code>NSNumber * number = @(123);
NSNumber * number1 = @(3.1415);
NSNumber * number2 = @(YES);
NSInteger intValue = [number integerValue];
CGFloat floatValue = [number1 doubleValue];
BOOL boolValue = [number2 boolValue];</code></pre>
<h3>数组</h3>
<p>在C和C++中常用的基本类型数组,在ObjC中对应的是NSArray类,这个类中存储的数据,也必须是类,而不能是基本数据类型,所以要将基本数据类型转换成对象存储,例如</p>
<pre><code>//注意这里用@[]方式返回NSSArray对象
NSArray* numbers = @[@(1),@(2),@(3)];</code></pre>
<p>这里numbers数组中存储了1,2,3三个NSNumber类型数据。</p>
<p>在ObjC中二维数组或多维数组并不常见,如有需要,建议使用C的数组进行存储。</p>
<h3>小节</h3>
<p>本章中除了基本数据类型,还介绍了部分ObjC的基本容器,例如NSString,NSValue,NSArray等,这些基本容器是Cocoa框架不可或缺的血液,贯穿始终。我们将在后面的章节介绍更多的基本容器,和他们的基本使用方法。</p>
Objective-C数据类型 - NSObject
https://segmentfault.com/a/1190000005725922
2016-06-15T15:45:03+08:00
2016-06-15T15:45:03+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>Objective-C数据类型 - NSObject</h2>
<h3>对象</h3>
<p>Objective-C(下称ObjC)语言是一个C语言的面向对象的封装,从英文名字上就能看出端倪,Object即为对象,等同于C++、Java中的类(Class)。</p>
<p>对象(object),就是ObjC中编程的核心。所谓对象和类,就是抽象出来的一类事物的总称,例如我们要写一个像微信的应用程序,我就要创建<code>联系人</code>这个对象,<code>联系人</code>有自己的属性,在ObjC中称谓<code>Property</code>,包括<code>用户名</code>、<code>头像</code>等内容。</p>
<p>再比如我们的应用程序需要管理全公司车辆,<code>车</code>就可以作为一个类,而<code>轿车</code>、<code>客车</code>则是<code>车</code>的<strong>子类</strong>,在ObjC中,子类继承父类的属性和方法,并且还可以有自己独有的属性和方法。</p>
<h4>NSObject</h4>
<p>在ObjC中,我们使用的框架名为Cocoa,对应iOS的版本叫做Cocoa Touch,这两个框架在的部分代码是相同的,比如最基础也是最重要的<code>Foundation</code>框架。</p>
<p><code>Foundation</code>框架中几乎所有的对象,都是以NS开头,这是由乔布斯离开苹果那几年,创建的计算机公司NEXTSTEP的NEXTSTEP操作系统遗留的代码。</p>
<p>在所有的NS对象中,最为基础的类,就是<code>NSObject</code>类,是Cocoa框架中所有对象的基类,所有其他NS对象,都是<code>NSObject</code>的子类。<code>NSObject</code>包含了NS类中所有的基本属性和方法,例如</p>
<pre><code class="objc">+ (void)load; //当类被引用进程序的时候会执行这个函数
+ (void)initialize;//当类第一次被执行到的时候这个函数会被执行</code></pre>
<p>这两个方法,没有任何关联,没有先后调用顺序只说,一般很少用到,仅有少数几种使用方法,将在后面的章节详细讲解。</p>
<h4>实例化</h4>
<pre><code class="objc">+ (instancetype)alloc;//实例化函数
- (instancetype)init; //初始化函数
+ (instancetype)new; //初始化函数
方法组成为:
+/- (返回值类型)方法名:(参数类型)参数名 :(参数类型2)参数名2 等</code></pre>
<p>在ObjC中,函数分为<code>类方法</code>和<code>实例方法</code>,分别用<code>+</code>和<code>-</code>开头。<br>第一个括号中的类型,是返回值类型,可以是对象,也可以是普通变量类型(int整数,BOOL布尔值等),也可以无返回值(void)。<br>上面三个方法中的<code>instancetype</code>就是代替当前类的类型,例如在NSObject类中,<code>instancetype</code>就是返回NSObject实例。</p>
<h4>类方法与实例方法</h4>
<p>类方法,就是可以直接用类名调用的方法,例如<code>new</code>方法</p>
<pre><code class="objc">NSObject* someObject = [NSObject new];</code></pre>
<p>而<code>init</code>方法则为实例方法,需要先调用</p>
<pre><code>+ (instancetype)alloc; //分配空间,返回实例</code></pre>
<p>返回实例后,用实例调用。</p>
<pre><code>NSObject* someObject = [[NSObject alloc] init];</code></pre>
<p>这里的嵌套写法,等同于</p>
<pre><code>NSObject* someObject = [NSObject alloc];
[someObject init];</code></pre>
<h4>函数参数</h4>
<p>类方法或实例方法,可以传入无限多个参数,也可以不传入参数,书写的格式就是<code>冒号(参数类型)参数名</code>,冒号前面可以有描述,也可以没有,一般我们都写上,例如我们有一个计算机类,他有两个方法</p>
<pre><code>- (BOOL)loginInWithName:(id)name password:(id)password;
- (void)loginOut;</code></pre>
<p>这是我们假设的两个方法,分别对应用户名登陆,和注销。<br>登陆方法,传入了用户名和密码两个参数,返回一个BOOL结果告诉用户是否登陆成功,而注销方法则不需要传入参数,也没有返回值。</p>
<h4>泛型</h4>
<p>ObjC中的泛型,就是指id类型。因为ObjC是运行时链接语言,类和实例的方法的调用,是在真正运行程序时候才进行绑定的。在编译的时候,如果欺骗了编译器也是能通过的,但运行时就会有错误。<br>例如(以下代码不能直接运行,只是示意)</p>
<pre><code>Book* book = [Book new];
id car = book; // 注意这里car是泛型
[car openDoor];// 编译器并不知道car的真实类型,这一步不会报错</code></pre>
<p>但真正执行到<code>openDoor</code>的时候,才会知道原来car是一个book,并没有<code>开门openDoor</code>方法,则会抛出异常。更多泛型的实际用法,将在后面的章节讲解。</p>
<h4>内存管理</h4>
<p>在xcode引入ARC(自动内存管理)以后,NSObjec就不需要手动调用</p>
<pre><code>- (void)dealloc; //释放</code></pre>
<p>来释放内存了。并且编译器会禁止在ARC环境下,编写调用dealloc或其他与引用计数相关的方法,例如</p>
<pre><code>- (instancetype)retain OBJC_ARC_UNAVAILABLE; //引用计数+1
- (oneway void)release OBJC_ARC_UNAVAILABLE; //引用计数-1
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE; //加入自动释放池
- (NSUInteger)retainCount OBJC_ARC_UNAVAILABLE; //返回引用计数</code></pre>
<p>注意这些方法后的宏<code>OBJC_ARC_UNAVAILABLE</code>,就是告诉编译器这些方法ARC下禁用。</p>
<h4>实例比较</h4>
<pre><code>- (BOOL)isEqual:(id)object; //对象比较
@property (readonly) NSUInteger hash; //唯一哈稀值</code></pre>
<p>每一个内存中实例化的对象,都有唯一的hash值,作为比较的参考。<code>isEqual</code>方法就是hash值的比较结果。</p>
<h4>类相关</h4>
<pre><code>@property (readonly) Class superclass; //父类
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead"); //当前类</code></pre>
<p>这两个函数返回的数据类型为Class,是一个C语言的结构体。通过下面两个方法可以判断当前类的类型。</p>
<pre><code>- (BOOL)isKindOfClass:(Class)aClass; //是否为aClass
- (BOOL)isMemberOfClass:(Class)aClass; //是否为aClass或其子类</code></pre>
<h3>小节</h3>
<p><code>NSObjec</code>作为ObjC中的基类,其部分方法的调用,是贯穿整个Cocoa框架开发的必备技能,务必要学会使用。</p>
mysql建表时的PK,NN,UQ,BIN,UN,ZF,AI
https://segmentfault.com/a/1190000005603595
2016-05-31T14:54:05+08:00
2016-05-31T14:54:05+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
0
<p>mysql在建表时,需要给字段设置类型标识</p>
<p>这些标识包括<code>基本字段类型标识</code>(intrinsic column flags)</p>
<pre><code>PK 主键(primary key)
NN 非空(not null)
UQ 唯一(unique)
AI 自增(auto increment)</code></pre>
<p>和<code>扩展数据类型标记</code>(additional data type flags, depend on used data type)</p>
<pre><code>BIN 二进制(binary)
UN 整数(unsigned)
ZF 填充0(zero fill) 例如字段内容是1 int(4), 则内容显示为0001 </code></pre>
如解决NSFetchedResultsController 和 UICollectionView一起使用时产生的崩溃
https://segmentfault.com/a/1190000005022685
2016-04-28T09:29:53+08:00
2016-04-28T09:29:53+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<p><code>NSFetchedResultsController</code>是一个非常好用且强大的数据库绑定类,用来处理<code>CoreData</code>和<code>UIView</code>的数据绑定非常便捷。</p>
<p>例如官方例子中,实用<code>NSFetchedResultsController</code>绑定<code>UITableView</code>,完成绑定后,开发者只要专注处理数据就好,UI会根据数据变化自动更新。</p>
<p>并且<code>NSFetchedResultsController</code>还提供了缓存功能,大大提高了大数据量的<code>CoreData</code>检索效率。</p>
<p>然而这么好的东西,和<code>UICollectionView</code>并不能配合的非常默契。</p>
<p>两个原因:</p>
<h2>坑一</h2>
<p><code>UICollectionView</code>不再有<code>UITableView</code>的<code>-(void)beginUpdates</code>和`-(void)endUpdates`的方法,但却提供了一个令人尴尬的批量处理Cell的外包围方法</p>
<pre><code>- (void)performBatchUpdates:(void (^ __nullable)(void))updates completion:(void (^ __nullable)(BOOL finished))completion; </code></pre>
<p>这个方法把操作包含到了block中执行,但是<code>NSFetchedResultsController</code>的UI改变通知回调是分四个回调方法完成的。所以我被迫使用了<code>NSBlockOperation</code>,把中间回调进行的操作包装到block中,再存到数组中,统一在</p>
<pre><code>- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller </code></pre>
<p>中执行。但在当前页面删除cell时候还是会报错。尴尬的我只好去掉了<code>performBatchUpdates</code>外包围,直接执行block,这次看起来没有问题了。</p>
<p>包装block的方法是这样</p>
<pre><code>@property (nonatomic, strong) NSMutableArray* op;
[self.op addObject:[NSBlockOperation blockOperationWithBlock:^{
[collectionView insertItemsAtIndexPaths:@[newIndexPath]];
}]];</code></pre>
<p>执行block的方法是这样</p>
<pre><code>[self.op enumerateObjectsUsingBlock:^(NSBlockOperation* _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[[NSOperationQueue mainQueue] addOperation:obj];
}];</code></pre>
<h2>坑二</h2>
<p>第二个坑,是UICollectionView,并不可以在幕后之行<code>insert</code>和<code>delete</code>操作,会抛一个莫名其秒的异常。所谓幕后,就是UICollectionView的VC不是当前显示在最前端的VC,这种情况也是很常见的。</p>
<p>所以这个时候,要直接调用<code>[collectionView reloadData]</code>方法。<br>像这样</p>
<pre><code>- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (self.onTop) {
[self.op enumerateObjectsUsingBlock:^(NSBlockOperation* _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[[NSOperationQueue mainQueue] addOperation:obj];
}];
}
else
{
[self.collectionView reloadData];
}
}</code></pre>
<h2>然而这一切还是有问题,问题就出在,数据源上的数据量和执行insertItemsAtIndexPaths方法时的数据量不同。</h2>
<p>这就是<code>performBatchUpdates</code>方法存在的意义。</p>
<p>所以还是需要用<code>performBatchUpdates</code>block住批量修改UI的代码,才能不报错。<br>而这个方法,必须在主线程执行。</p>
<pre><code>dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView performBatchUpdates:^{
这里处理
}];
});</code></pre>
<p>愿好运</p>
iOS实现依赖注入
https://segmentfault.com/a/1190000004659514
2016-03-22T10:51:33+08:00
2016-03-22T10:51:33+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
2
<p><code>依赖注入(Dependency Injection)</code>这个词,源于java,但在Cocoa框架中也是十分常见的。<br>举例来说:<br><strong>UIView的初始化方法initWithFrame</strong></p>
<pre><code class="objc">- (id)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;</code></pre>
<p>这里的frame传入值,就是所谓的<code>依赖(Dependency)</code>,这个View实例化是根据frame注入实现的。<br>但这种用法有很大的局限性</p>
<ol>
<li><p>我们不知道究竟依赖注入的属性有哪些</p></li>
<li><p>不可能无限加长方法长度来满足更多的依赖属性</p></li>
</ol>
<p>所以我们准备采用字典容器对NSObject类进行依赖注入扩展。</p>
<h2>给NSObject类添加一个Category</h2>
<pre><code>@interface NSObject (XXXDependencyInjection)
- (nullable id)initWithParams:(nonnull NSDictionary *)params;
- (void)injection:(nonnull NSDictionary*)params;
@end</code></pre>
<h2>实现注入方法</h2>
<pre><code>- (id)initWithParams:(NSDictionary *)params
{
self = [self init];
if (self) {
[self injection:params];
}
return self;
}
- (void)injection:(NSDictionary*)params
{
[params.allKeys enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:",[[obj substringToIndex:1] uppercaseString],[obj substringFromIndex:1]]);
id value = [params objectForKey:obj];
if ([self respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:selector withObject:value];
#pragma clang diagnostic pop
}
else
{
@try {
[self setValue:value forKeyPath:obj];
}
@catch (NSException *exception) {
NSLog(@"%@",exception);
[exception raise];
}
@finally {
}
}
}];
}</code></pre>
<h2>解释</h2>
<p>我们将需要注入的属性,封装到一个字典里,例如:</p>
<pre><code>UIViewController* controller = [[UIViewController alloc] initWithParams:@{
@"title":@"测试",
@"view.backgroundColor":[UIColor whiteColor]
}];</code></pre>
<p>我们给这个VC注入了两个属性,一个是其title,一个是其View的backgroundColor属性。<br>字典传入以后,我们读区<code>params.allKeys</code>进行遍历,拼装set+参数名的selector,这里用的是NSSelectorFromString方法:</p>
<pre><code>SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:",[[obj substringToIndex:1] uppercaseString],[obj substringFromIndex:1]]);</code></pre>
<p>然后我们判断实例是否可以响应这个set方法,如果可以,则给其赋值。</p>
<pre><code> if ([self respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:selector withObject:value];
#pragma clang diagnostic pop
}</code></pre>
<p>这里的三行clang宏是为了消除编译器的内存泄漏警告,这里因为我们进行了验证,所以不会出现leak。</p>
<h2>KVC实现跨实例赋值</h2>
<p>我们注意到上例中还有一句给VC的View改变背景颜色</p>
<pre><code> @"view.backgroundColor":[UIColor whiteColor]</code></pre>
<p>这里就用到了KVC的点语法特性,在我们判断到实例不能响应<code> if ([self respondsToSelector:selector]) </code>的时候,通过点语法,进行赋值</p>
<pre><code>@try {
[self setValue:value forKeyPath:obj];
}
@catch (NSException *exception) {
NSLog(@"%@",exception);
[exception raise];
}
@finally {
}</code></pre>
<p>这里添加了异常捕获,因为点语法对属性名称拼写要求是全匹配,否则抛异常,所以要注意。</p>
<h2>优缺点</h2>
<p>这样改造过的init方法,优点非常明显,就是绑定更加集中便捷,如果使用的是<code>storyboard</code>则可以轻松实现前后端分离。<br>目前的缺点也很明显,不能告诉开发者哪些属性是必需依赖,另外还不能支持非对象属性的赋值,希望抛砖引玉,大家来改进这段代码。</p>
Animations开源动效分析(二)POP-Stroke动画
https://segmentfault.com/a/1190000004365988
2016-01-26T11:33:24+08:00
2016-01-26T11:33:24+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
3
<ol>
<li><p>本教程源码<a href="https://link.segmentfault.com/?enc=NtcYHciw7s3NlIqKxwTv0w%3D%3D.jKToG0HzltpfYxjEkrCo7ZN%2BRxvmKpPJ7Z%2BLmqmi69UqJkCRqXPVqies6zBaBr4q" rel="nofollow">Animations</a> 作者 <a href="https://link.segmentfault.com/?enc=1F%2FwvqByJIqCR3LjWYYbSA%3D%3D.HtsaI56utfsGzEAvdOmp7G86KLEYM1%2FNHsr18lF%2B3sc%3D" rel="nofollow">YouXianMing</a>,建议配合源码项目食用</p></li>
<li><p>Facebook pop动画框架简易教程请移步 <a href="https://link.segmentfault.com/?enc=gzXMAWagQrH0RBKwuLcLrQ%3D%3D.nBBSZ3CYe3XC%2Fm5jnMQw9dAf0ieqtyeNDmBVRh1AD9xPIwuTzsDYRZm6mYcpP3VB" rel="nofollow">Facebook Pop 使用指南</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=J4r7Y2uU57XUi9Xm0bf2Tg%3D%3D.bAlf1WoIvZjTMhytrRAZbCYDNwCcYWJnMlrTjPqtFdUgsV0XhCoz3bJnWobKtGMidKxEsGWxQDUuma3y22Q0Tg%3D%3D" rel="nofollow">CoreAnimation不简易教程</a></p></li>
<li><p>如果不想看第三条的教程,也要弄明白CALayer的隐式动画,否则看本文会疑惑,请移步<a href="https://link.segmentfault.com/?enc=x8%2Bwlmspdwaimh8RjSBw1w%3D%3D.XoTZO55aqdWW8IMpMPAC0o7r4rHk6d9weMKZJQGCbGhd3acl6o8lVSWxAR0nQms7c75Wc3mcIVymvTaSJkRCZg%3D%3D" rel="nofollow">CALayer的隐式动画和显式动画</a></p></li>
</ol>
<h2>CAMediaTimingFunction</h2>
<p>今天我们来看一下研究一下<code>CAMediaTimingFunction</code>类,它是一个动画的时间线控制类,他所控制的时间线,可以是是一条直线、曲线或者折线,如下:</p>
<p><img src="/img/bVstNN" alt="poster.jpg" title="poster.jpg"></p>
<p>这是用一个开源软件生成的<code>CAMediaTimingFunction</code>,软件地址是<a href="https://link.segmentfault.com/?enc=oPqwesAi7YOp6hbJgxsAsw%3D%3D.GSU2zqLz3mi0QkMnmYOBV05uigXDjyJ1v%2BWJtXHuNRwOSqaW6eoNapF2VM%2BZaK3N" rel="nofollow">keefo/CATweaker</a></p>
<p>可见,一般自定义的<code>CAMediaTimingFunction</code>通过调用</p>
<pre><code>/* Creates a timing function modelled on a cubic Bezier curve. The end
* points of the curve are at (0,0) and (1,1), the two points 'c1' and
* 'c2' defined by the class instance are the control points. Thus the
* points defining the Bezier curve are: '[(0,0), c1, c2, (1,1)]' */
+ (instancetype)functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y;
- (instancetype)initWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y;</code></pre>
<p>两个方法,传入四个位置点参数生成。 注:上图中XY轴区间都是[0,1];</p>
<p>这个类,在什么地方用到呢?</p>
<ol>
<li><p>CALayer的隐式和显式动画,<code>CATransaction</code>有<code>animationTimingFunction</code>设置。</p></li>
<li><p><code>CAKeyframeAnimation</code>有相关设置。</p></li>
<li><p><code>CABasicAnimation</code>是线性的动画,一条直线。</p></li>
<li><p><code>CASpringAnimation</code>弹簧动画是也是有一个特殊的走向,属于<code>CAMediaTimingFunction</code>的特殊封装。</p></li>
<li><p><code>POP</code>也借用了<code>CAMediaTimingFunction</code>类实现非线性动画。</p></li>
</ol>
<p>下面这个网站可以在线调试,<a href="https://link.segmentfault.com/?enc=Yp0Pftmkk%2FjxyDTvI2o5kQ%3D%3D.o1tRiIbWp%2F9edfD4Ui6PaPMl1S3WX8l3aOWbyhSk1Z9hRm8jt4k%2FZd%2Fu0kncon1N" rel="nofollow">cubic-bezier</a>,虽然是给CSS工程师用的,但是通用的。<br><img src="/img/bVstRi" alt="图片描述" title="图片描述"></p>
<p>上图中,蓝色的方块的运动就是线性的,红色方块是非线性的。</p>
<p>iOS7开始,iOS系统大量引入了非线性动画。</p>
<p><img src="/img/bVstR9" alt="0-example-2.gif" title="0-example-2.gif"><br><img src="/img/bVstSc" alt="0-example-1.gif" title="0-example-1.gif"></p>
<p>上图引用自 <a href="https://link.segmentfault.com/?enc=Wj7t1Yi66XjzXahYt5npXw%3D%3D.gP6%2BP%2FTY62B14RnOIfInL6AfxuwVBROat%2BAM3zUzg0nByjBLdTty4J6o0i9u8lgOpEcLrThlDP5ioDVaMnnKzg%3D%3D" rel="nofollow">使用 iOS 8 Spring Animation API 创建动画</a></p>
<h2>Spring动画</h2>
<p>弹簧(Spring)动画是一种特殊曲线的非线性动画,因为用的地方太多,所以无论是CoreAnimation还是POP,都将其进行了封装<code>CASpringAnimation</code>,<code>POPSpringAnimation</code>。</p>
<p>两者有一点区别,参考源码中的<code>CASpringAnimation</code>和<code>POP-Spring动画参数详解</code><br><img src="/img/bVstTb" alt="POPSpringAnimation" title="POPSpringAnimation"></p>
<h2>POP-Stroke动画</h2>
<p><img src="/img/bVstMF" alt="687474703a2f2f696d61676573323031352e636e626c6f67732e636f6d2f626c6f672f3630373534322f3230313531312f3630373534322d32303135313131373131333135333337342d313337303739333939372e676966" title="687474703a2f2f696d61676573323031352e636e626c6f67732e636f6d2f626c6f672f3630373534322f3230313531312f3630373534322d32303135313131373131333135333337342d313337303739333939372e676966"></p>
<p>今天我们来分析一下<code>POP-Stroke动画</code>的源代码,首先interface中声明了一个<code>CAShapeLayer</code>是中心的圆,<code>timer</code>是一个定时器。这个GCDTimer是作者对GCD进行的一层对象化封装。</p>
<pre><code>@interface PopStrokeController ()
@property (nonatomic, strong) CAShapeLayer *circleShape;
@property (nonatomic, strong) GCDTimer *timer;
@end</code></pre>
<p>实现的思路是,定时改变<code>CAShapeLayer</code>的startStoke和endStoke属性,改变圆的绘制弧度,使用POP的Spring动画控制其改变数值。</p>
<pre><code>- (void)setup {
[super setup];
self.circleShape = [CAShapeLayer layer];
self.circleShape.strokeEnd = 0.f;
self.circleShape.lineCap = kCALineCapRound;
StrokeCircleLayerConfigure *config = [StrokeCircleLayerConfigure new];
config.lineWidth = 4.f;
config.startAngle = 0;
config.endAngle = M_PI * 2;
config.radius = 55.f;
config.circleCenter = self.contentView.middlePoint;
config.strokeColor = [UIColor cyanColor];
[config configCAShapeLayer:self.circleShape];
[self.contentView.layer addSublayer:self.circleShape];
_timer = [[GCDTimer alloc] initInQueue:[GCDQueue mainQueue]];
[_timer event:^{
CGFloat value1 = arc4random() % 101 / 100.f;
CGFloat value2 = arc4random() % 101 / 100.f;
POPSpringAnimation *strokeAnimationEnd = [POPSpringAnimation animationWithPropertyNamed:kPOPShapeLayerStrokeEnd];
strokeAnimationEnd.toValue = @(value1 > value2 ? value1 : value2);
strokeAnimationEnd.springBounciness = 12.f;
POPSpringAnimation *strokeAnimationStart = [POPSpringAnimation animationWithPropertyNamed:kPOPShapeLayerStrokeStart];
strokeAnimationStart.toValue = @(value1 < value2 ? value1 : value2);
strokeAnimationStart.springBounciness = 12.f;
POPBasicAnimation *strokeAnimationColor = [POPBasicAnimation animationWithPropertyNamed:kPOPShapeLayerStrokeColor];
strokeAnimationColor.toValue = (__bridge id)([self randomColor].CGColor);
[self.circleShape pop_addAnimation:strokeAnimationEnd forKey:@"layerStrokeAnimation"];
[self.circleShape pop_addAnimation:strokeAnimationStart forKey:@"layerStrokeAnimation1"];
[self.circleShape pop_addAnimation:strokeAnimationColor forKey:@"layerStrokeAnimation2"];
} timeIntervalWithSecs:1];
[_timer start];
}
- (UIColor *)randomColor {
return [UIColor colorWithRed:arc4random() % 101 / 100.f
green:arc4random() % 101 / 100.f
blue:arc4random() % 101 / 100.f
alpha:1];
}</code></pre>
<p>我们可以看到,POP支持了CALayer的所有动画属性,上面代码中用的</p>
<pre><code>NSString * const kPOPShapeLayerStrokeStart = @"shapeLayer.strokeStart";
NSString * const kPOPShapeLayerStrokeEnd = @"shapeLayer.strokeEnd";
NSString * const kPOPShapeLayerStrokeColor = @"shapeLayer.strokeColor";</code></pre>
<p>分别对应CAShapeLayer的绘制颜色,起始比例区间。</p>
<pre><code>/* The color to fill the path's stroked outline, or nil for no stroking.
* Defaults to nil. Animatable. */
@property(nullable) CGColorRef strokeColor;
/* These values define the subregion of the path used to draw the
* stroked outline. The values must be in the range [0,1] with zero
* representing the start of the path and one the end. Values in
* between zero and one are interpolated linearly along the path
* length. strokeStart defaults to zero and strokeEnd to one. Both are
* animatable. */
@property CGFloat strokeStart;
@property CGFloat strokeEnd;</code></pre>
<p>然后通过生成随机数的方式,用定时器定时改变,同时随机改变了颜色。所以总共使用了三个Spring动画。</p>
<p>相同原理的Demo还有<code>Easing-圆环动画</code></p>
<p><img src="/img/bVstMJ" alt="687474703a2f2f696d61676573323031352e636e626c6f67732e636f6d2f626c6f672f3630373534322f3230313531312f3630373534322d32303135313132343130353630383039332d3936373031303436332e676966" title="687474703a2f2f696d61676573323031352e636e626c6f67732e636f6d2f626c6f672f3630373534322f3230313531312f3630373534322d32303135313132343130353630383039332d3936373031303436332e676966"></p>
<p>只是这例子中用了作者自己封装的<code>YXEasing</code>类。</p>
<h2>总结</h2>
<p>非线性动画对于要精益求精交互工程师来说,是一把剑利的刃。放在iOS5时代可能因为设备性能等问题,还是把双刃剑,但现在来说,已经完全是提升APP动效交互的利器了。</p>
<p>相关阅读: <a href="https://link.segmentfault.com/?enc=NvarYy5LuH38N3tVxV3flQ%3D%3D.NRVjt3gDu8ddPQul%2FplbrbysRbAlvr7U68gw3Wb%2BGN8%3D" rel="nofollow">缓动函数速查表</a></p>
Animations开源动效分析(一)POP按钮动画
https://segmentfault.com/a/1190000004360327
2016-01-25T11:43:23+08:00
2016-01-25T11:43:23+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
2
<p>最近Github有一个上很火的开源动画集叫<a href="https://link.segmentfault.com/?enc=g5%2Bs%2FX265gAzhnvXXzDQSw%3D%3D.gMR43WfTd3XAAcRYqCfR9KJMjRS1dP2F%2B26V9eeBWaOVIw%2BQ4yG7LlG3tLb8dwNI" rel="nofollow">Animations</a>。</p>
<p>我也很喜欢做动画动效,特来学习观摩。因为动效的特殊性,很多情况下这个项目里的动效不能直接Copy到我们现有的项目中直接使用,所以搞清楚她们的实现原理就很有必要了。建议配合源码学习。</p>
<h2>POP按钮动画</h2>
<p>没用过的POP的请移步<a href="https://link.segmentfault.com/?enc=CeIM4PSWpRmbdn9Q%2FcsH9g%3D%3D.u4he7J%2F7fo1%2BdgvFM3fod0A7yG59E3q7nGXeQ0hZVjDLN96rL60S32Ok75Yd0FzV" rel="nofollow">Facebook Pop 使用指南</a></p>
<p>效果如下<br><img src="/img/bVssnd" alt="687474703a2f2f696d61676573323031352e636e626c6f67732e636f6d2f626c6f672f3630373534322f3230313531312f3630373534322d32303135313131363230323930373433362d3939323738383338392e676966" title="687474703a2f2f696d61676573323031352e636e626c6f67732e636f6d2f626c6f672f3630373534322f3230313531312f3630373534322d32303135313131363230323930373433362d3939323738383338392e676966"></p>
<p><strong>思路</strong> 整体效果是用三个CAShapeLayer和一个UILabel组合实现的。CAShapeLayer负责绘制两个圆的边和一个实心圆。动画用POP的<code>POPBasicAnimation</code>和<code>POPSpringAnimation</code>。</p>
<p>你的肉眼能看出哪里用得哪种动画么?实心圆的尺寸变化、和空心圆的绘制进度是<code>POPBasicAnimation</code>实现的,基本是一个线性动画,匀速。实心圆的颜色改变用的是<code>POPSpringAnimation</code>弹簧动画。</p>
<h2>关于CAShapeLayer</h2>
<p><code>CAShapeLayer</code>是一中特殊的CALayer用于绘制图形,可以理解成是CoreGraphic的一种对象化封装。一个CAShapeLayer可以绘制一个简单图形。例如例子里的圆</p>
<pre><code> UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.lineWidth + self.radius, self.lineWidth + self.radius)
radius:self.radius + self.lineWidth / 2.f
startAngle:self.startAngle
endAngle:self.endAngle
clockwise:self.clockWise];
shapeLayer.path = path.CGPath;</code></pre>
<p><code>bezierPathWithArcCenter</code>这个方法中传入一系列参数,绘制了一条贝塞尔(bezier)曲线,参数分别是中心点、半径、开始角度、终止角度,是否闭环。</p>
<h2>POP文字动画</h2>
<p>POP实现的文字动画,例子里用的同样是<code>POPBasicAnimation</code>,所以也是线性变化的。</p>
<pre><code>- (void)pop_animationDidApply:(POPAnimation *)anim {
NSLog(@"pop_animationDidApply %@", anim);
NSValue *toValue = (NSValue *)[anim valueForKeyPath:@"currentValue"];
CGSize size = [toValue CGSizeValue];
[CATransaction setDisableActions:YES];
CGFloat percent = (size.height - _math.b) / _math.k;
_circleShape1.strokeEnd = percent;
_circleShape2.strokeEnd = percent;
[CATransaction setDisableActions:NO];
UIColor *color = [UIColor colorWithRed:percent green:percent blue:percent alpha:1.f];
double showValue = fabs(percent * 100);
self.label.text = [NSString stringWithFormat:@"%.f%%", showValue];
self.label.textColor = color;
_blurImageView.alpha = 1 - percent;
}</code></pre>
<p><strong>解释</strong>,这个POP动画的分帧回调,回调中用</p>
<pre><code> NSValue *toValue = (NSValue *)[anim valueForKeyPath:@"currentValue"];</code></pre>
<p>取得当前动画的值,这个动画实际只是改变了实心圆的尺寸。然后开发者通过当前尺寸计算出动画的进度</p>
<pre><code> CGFloat percent = (size.height - _math.b) / _math.k;</code></pre>
<p><code>CATransaction</code>动画开关,禁用了两个外边圆的<code>strokeEnd </code>隐式动画。</p>
<pre><code> [CATransaction setDisableActions:YES]; </code></pre>
<p>如果你不明白CALayer的显式动画和隐式动画的区别,请仿照<a href="https://link.segmentfault.com/?enc=DGt5b6wy6EWWdyG3gHepmA%3D%3D.4%2BMEHHRpnILT%2F7O5%2FmSx7mWo39%2BWVhIfyZNxNGLggf0Wjnd06chHK6xlIAC%2Fy2kcs0yxjx2cpeY7utwjbYuI7Q%3D%3D" rel="nofollow">CALayer的隐式动画和显式动画</a>写一个Demo对比一下。有更多问题可以在下面留言。</p>
<h2>响应链</h2>
<p>Target模式,很容易理解。</p>
<pre><code> // 按住按钮后没有松手的动画
[_button addTarget:self action:@selector(scaleToSmall) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragEnter];
// 按住按钮松手后的动画
[_button addTarget:self action:@selector(scaleAnimations) forControlEvents:UIControlEventTouchUpInside];
// 按住按钮后拖拽出去的动画
[_button addTarget:self action:@selector(scaleToDefault) forControlEvents:UIControlEventTouchDragExit];</code></pre>
<h2>高斯模糊</h2>
<p>高斯模糊(blur)的实现有很多方式,这个源码里使用的<code>UIImage+ImageEffects</code>是一个 <code>UIImage</code>的扩展。</p>
<p>动画效果通过用blur层覆盖在普通层上,通过修改blur层alpha值实现。</p>
UINavigationBar的继承与定制
https://segmentfault.com/a/1190000004305459
2016-01-14T11:21:36+08:00
2016-01-14T11:21:36+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<p>我们在iOS项目开发中,有些时候需要修改标准控件的样式,我们今天就围绕一个具体项目需求,进行<code>UINavigationBar</code>的继承与改造。</p>
<h2>UIApperance协议属性定制</h2>
<p>我们在<code>UINavigationBar.h</code>头文件中,看到如下修改NavigationBar背景颜色的属性</p>
<pre><code>@property(nullable, nonatomic,strong) UIColor *barTintColor NS_AVAILABLE_IOS(7_0) UI_APPEARANCE_SELECTOR; // default is nil</code></pre>
<p>注意到<code>UI_APPEARANCE_SELECTOR</code>这个宏了么,用这个宏标记的属性,都是可以通过<code>UIApperance</code>协议进行全局设置的属性。说的更直白一点,就是可以一次性,修改项目中所有的这个类的默认属性。</p>
<p>例如在iOS6之前,<code>UILabel</code>的默认背景颜色不是透明色,而是白色。我们就可以使用如下方法,修改<code>UILabel</code>的默认背景色</p>
<pre><code> [[UILabel appearance] setBackgroundColor:[UIColor clearColor]];</code></pre>
<p><code>UIApperance</code>协议就是这么神奇,所有的UIKit控件都遵守了这个协议,所有标记了<code>UI_APPEARANCE_SELECTOR</code>宏的属性,都可以使用<code>appearance</code>实例修改默认值,是不是很炫酷。</p>
<h2>项目需求</h2>
<p>上面一段与本文正题无关,下面我们看一下本文的项目需求</p>
<p><img src="/img/bVsd7L" alt="图片描述" title="图片描述"></p>
<h2>分析</h2>
<p>这个页面就是一个标准的<code>NavigationController</code> + <code>TableViewContoller</code>组合实现的设置页面,导航条和Table的样式需要订制。</p>
<p>前面说到的<code>UIApperance</code>协议是可以实现的,我们换一种更为普遍的方式实现,<strong>继承</strong>。</p>
<p>我们继承<code>UINavigationBar</code>,创建子类<code>FWBar</code>。我们使用storyboard实例化大体框架模型,并将<code>NavigationViewController</code>的<code>NavigationBar</code>设置为我们的<code>FWBar</code>类,并将<code>UITableView</code>设置为<code>Static</code>静态模式,直接编辑了<code>Cell</code>的内容。</p>
<p><img src="/img/bVsd8s" alt="图片描述" title="图片描述"></p>
<p>在<code>FWBar.m</code>中加入如下代码</p>
<pre><code>- (void)awakeFromNib
{
[self setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsCompact];
self.shadowImage = [UIImage new];
//把之前的View统统隐藏
[self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[obj setHidden:YES];
}];
[self addSubview:self.fakeBackgroundView];
self.fakeBackgroundView.userInteractionEnabled = NO;
[self sendSubviewToBack:self.fakeBackgroundView];
self.titleTextAttributes = @{
NSFontAttributeName: [UIFont fontWithName:@"NotoSansHans-DemiLight" size:16],
NSForegroundColorAttributeName:[UIColor colorWithRed:57.0/255 green:207.0/255 blue:218.0/255 alpha:1]
};
//rgba(165, 195, 205, 1)
self.tintColor = [UIColor colorWithRed:165.0/255 green:195.0/255 blue:205.0/255 alpha:1];
}</code></pre>
<p><strong>解释</strong> 因为原生的NaviBar背景View下方有一条灰色的边,这条边不是用layer生成的,我没搞明白是怎么实现的,所以直接将这个View隐藏掉了。顺便吧<code>shadowImage</code>也换成空图。</p>
<p>这里的<code>self.fakeBackgroundView</code>是我们添加的背景,颜色是白色。这里我们将它移到最下层,并且触摸属性关掉,<code>userInteractionEnabled</code>设为<code>NO</code>。</p>
<p><code>titleTextAttributes</code>这个属性,是用来修改title的样式的。</p>
<p><code>tintColor</code>这个属性,是用来修改导航条左右按钮颜色的。</p>
<p>这些操作做完,还不够。</p>
<p><strong>我们无法通过暴露出来的接口修改左右按钮的字体和位置。这也是我们选择继承而不是UIApperance的原因</strong></p>
<h2>继承大杀器,高度自定义</h2>
<pre><code>- (void)didAddSubview:(UIView *)subview
{
NSLog(@"%@",subview);
if ([subview isKindOfClass:NSClassFromString(@"UINavigationButton")]) {
if ([subview isKindOfClass:[UIButton class]]) {
[(UIButton*)subview setAttributedTitle:[[NSAttributedString alloc] initWithString:[(UIButton*)subview titleForState:UIControlStateNormal] attributes:@{
NSFontAttributeName: [UIFont fontWithName:@"AvenirNext-Regular" size:17],
NSForegroundColorAttributeName:self.tintColor
}] forState:UIControlStateNormal];
}
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
if ([subview isKindOfClass:NSClassFromString(@"UINavigationButton")]) {
if ([subview isKindOfClass:[UIButton class]] && subview.frame.origin.x < self.frame.size.width/2) {
[subview setFrame:({
CGRect rect = subview.frame;
rect.origin.x = 8;
rect.size.width = 69;
rect;
})];
}
}
}];
}</code></pre>
<p><strong>解释</strong> 重写<code>- (void)didAddSubview:(UIView *)subview</code>方法,检测了系统控件根据<code>NavigationItem</code>向<code>NavigationBar</code>添加按钮这个事件,然后对按钮进行甄别,定制。</p>
<p>我们找到<code>Cancel</code>这个按钮,他虽然是<code>UINavigationButton</code>类型,但是一定是继承了<code>UIButton</code>,所以我们直接强转成她的父类,修改其文字字体和frame。</p>
<p>重写<code>layoutSubviews</code>这个方法,是为了实时更新我们的按钮位置。这个其实也可以不更改的,但是我们的项目需求中,<code>Cancel</code>这个字段太长,字体变大以后导致了显示不全,所以我们将这个做按钮的frame变大了。</p>
<p>注意几点</p>
<ol>
<li><p><code>NSClassFromString(@"UINavigationButton")</code>这个方法是我们无法获取内部类的时候,获取Class类型的方法。<code>UINavigationButton</code>这个类名是NSLog输出时看到的。</p></li>
<li><p>这一段使用了特殊的语法糖,有兴趣了解的参考<a href="https://link.segmentfault.com/?enc=IenCUAkkAO%2F1%2FQLnXVPDdQ%3D%3D.Q7d%2BIXkpAYRpeETVNSPo0iBjcFT0%2BFBXaT2hdUZJGgg4o09VUAsTsTV%2F8A8CMRr1noV2JwG6jCzTwabmqtDu6g%3D%3D" rel="nofollow">这篇sunnyxx大神的博文</a>,全文搜索关键字<code>小括号内联复合表达式</code></p></li>
</ol>
<pre><code>[subview setFrame:({
CGRect rect = subview.frame;
rect.origin.x = 8;
rect.size.width = 69;
rect;
})];</code></pre>
<p>最后的实现效果。</p>
<p><img src="/img/bVseck" alt="图片描述" title="图片描述"></p>
<h2>结语</h2>
<p>截屏的效果不是太好,细心的朋友可能会发现,我们的<code>FWBar</code>在<code>TableView</code>向上滑动的过程中会渐出阴影。</p>
<p>我把这段代码分享给大家,但是这段代码偷懒没用KVO,而是用了<code>ReactiveCocoa</code>这个庞大的庞大框架的小小功能,所以,就没放倒教程里。</p>
<pre><code>- (void)didMoveToSuperview
{
[super didMoveToSuperview];
UIViewController *presentingViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (presentingViewController.presentedViewController) presentingViewController = presentingViewController.presentedViewController;
__block BOOL has = NO;
[[presentingViewController childViewControllers] enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj isKindOfClass:[UINavigationController class]]) {
[[obj childViewControllers] enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull obj2, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj2 isKindOfClass:[UITableViewController class]]) {
has = YES;
UITableViewController* tVC = obj2;
if (self.tableViewOffsetDisposable) {
[self.tableViewOffsetDisposable dispose];
}
self.tableViewOffsetDisposable = [RACObserve(tVC.tableView, contentOffset) subscribeNext:^(id x) {
CGPoint p = [x CGPointValue];
if (p.y <= 0 && p.y >= - 64) {
self.fakeBackgroundView.layer.shadowOpacity = fabs(64 + p.y) / 64 * 0.7;
}
else if (p.y > 0)
{
if (self.fakeBackgroundView.layer.shadowOpacity != 0.7) {
self.fakeBackgroundView.layer.shadowOpacity = 0.7;
}
}
else
{
if (self.fakeBackgroundView.layer.shadowOpacity != 0) {
self.fakeBackgroundView.layer.shadowOpacity = 0;
}
}
}];
}
}];
}
}];
}</code></pre>
iOS自定义控件教程(四)UIControl - 幕后的英雄
https://segmentfault.com/a/1190000004258988
2016-01-06T15:03:14+08:00
2016-01-06T15:03:14+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
2
<p>上一篇文章我们介绍了UIView的触摸事件响应和简单动画,但是并没有将触摸事件封装。我们今天介绍Demo中最后一部分 —— 输出响应事件。</p>
<p><a href="https://link.segmentfault.com/?enc=hcuTs49h8J12ixfaRJmyJw%3D%3D.VAW1mmHOyE31pclYxYO7zHKB1Kz6f4Lh%2FbtoDbaARfMf%2BtnoxyNE0JriAxmqad%2FZ" rel="nofollow">Github下载源码</a></p>
<p>我么知道<code>Objective-C</code>是采用<code>消息机制</code>(messaging)调用方法的,例如我们调用<code>UIView</code>的<code>init</code>方法</p>
<pre><code>UIView * simpleView = [[UIView alloc] init];</code></pre>
<p>简单的描述一下其中的过程:</p>
<ol>
<li><p>程序一运行,所有的类方法(‘+’开头)和实例方法(‘-’开头)的接口内存地址都被写入一张hash表中</p></li>
<li><p>我们向<code>UIView</code>发送类方法<code>alloc</code>消息,runtime(运行时环境)根据前面说的hash表,查找对应类(UIView)的对应类方法(alloc)的内存地址,然后调用</p></li>
<li><p>如果UIView并未实现alloc方法,runtime会转而查找UIView的父类是否实现了alloc方法,如果实现了,就将消息投递给父类的alloc方法;如果没有实现,转而查找UIView父类的父类是否实现,重复这一过程直到将消息投递出去</p></li>
<li><p>如果最终发现投递不出去,则会抛出一个最常见的异常<code>unrecognized selector sent to instance + 内存地址</code>,也就是你调用了一个没有实现的方法</p></li>
</ol>
<p>不过,我们今天遇到的问题单单依靠<code>消息机制</code>并不能很好的解决。</p>
<p><strong>需求</strong> 我们需要将<code>Demo</code>中<code>XXXSegmentView</code>获取的触摸事件,反馈给当前的UIViewContoller,应该怎么做?这个问题就是所谓的<code>反向传值</code>问题。</p>
<h3>1. 直接调用</h3>
<p>我们从最蠢的做法说起,虽然是蠢,但是是可行的,不过不要模仿啊,单纯为了讲原理和作对照</p>
<pre><code class="objc">@interface ViewController ()
@property (strong, nonatomic) XXXSegmentView *segmentView;
- (void)segmentDidSelectIdx:(NSInteger)idx;
@end</code></pre>
<pre><code>@interface XXXSegmentView : UIView
@property (weak, nonatomic) ViewController *viewController;
@end</code></pre>
<p>我们在给<code>XXXSegmentView</code>加上一个<code>viewController</code>属性,然后就可以在获取触摸事件时,通过调用<code>ViewController</code>的<code>segmentDidSelectIdx</code>方法,传递选择标签这个事件。</p>
<p>这样是可行的,但是缺点十分明显:耦合性太高,<code>XXXSegmentView</code>需要引用<code>ViewController</code>头文件,不符合低耦合这个基本原则。</p>
<h3>2. delegate(委托)模式</h3>
<p><code>objc</code>的delegate设计模式,可以解决上面的问题。但根据<code>objc</code>的设计初衷,这个问题用delegate解决真的有种杀鸡用牛刀的感觉。</p>
<pre><code>@interface XXXSegmentView : UIView
@property (nonatomic, weak) id delegate;
@end</code></pre>
<p>这里的delegate属性,是一个<code>id</code>类型的属性,<code>id</code>这个类型就是<code>objc</code>的动态类型,编译器不关心它是什么类型,所以<code>id</code>类型的对象,可以调用所有声明过的类方法和实例方法,而编译器不会报错。</p>
<p>这样我们就可以个把<code>viewController</code>作为<code>XXXSegmentView</code>的<code>delegate</code>属性传入,<code>XXXSegmentView</code>无需知道自己的<code>delegate</code>是什么类,便可以直接调用<code>delegate</code>的实例方法。</p>
<pre><code>if (self.delegate && [self.delegate respondsToSelector:@selector(segmentDidSelectIdx:)]) {
[self.delegate segmentDidSelectIdx:idx];
}</code></pre>
<p>注意我们在调用之前,首先检查<code>self</code>的<code>delegate</code>是否赋值,然后通过<code>respondsToSelector</code>确认<code>delegate</code>实现了<code>segmentDidSelectIdx</code>方法,最后才传递消息。这两步十分重要,<code>delegate</code>作为动态类型,编译器编译阶段是无法发现问题的,所以运行时要进行确认。</p>
<p><strong>注:</strong> 标准的委托模式是要结合协议(Protocol)一起使用的,这里就不多讲了。</p>
<h3>3. Target模式</h3>
<p>这要从一个类说起,他叫<code>UIControl</code>。</p>
<p><code>UIControl</code>是<code>UIView</code>的子类,是<code>UIKit</code>框架中可交互的控件的基类,一般不直接使用。我们用的比较多的例如<code>UIButton</code>、<code>UISwitch</code>、<code>UITextField</code>等都是他的子类。</p>
<p><code>UIControl</code>为<code>iOS</code>的人机交互制定了一系列的标准:</p>
<p>例如最常见的<code>UIControlEvents</code>枚举,定义了<code>iOS</code>交互中的交互方式</p>
<pre><code>typedef NS_OPTIONS(NSUInteger, UIControlEvents) {
UIControlEventTouchDown = 1 << 0, // on all touch downs
UIControlEventTouchDownRepeat = 1 << 1, // on multiple touchdowns (tap count > 1)
UIControlEventTouchDragInside = 1 << 2,
UIControlEventTouchDragOutside = 1 << 3,
UIControlEventTouchDragEnter = 1 << 4,
UIControlEventTouchDragExit = 1 << 5,
UIControlEventTouchUpInside = 1 << 6,
UIControlEventTouchUpOutside = 1 << 7,
UIControlEventTouchCancel = 1 << 8,
UIControlEventValueChanged = 1 << 12, // sliders, etc.
UIControlEventPrimaryActionTriggered NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 13, // semantic action: for buttons, etc.
UIControlEventEditingDidBegin = 1 << 16, // UITextField
UIControlEventEditingChanged = 1 << 17,
UIControlEventEditingDidEnd = 1 << 18,
UIControlEventEditingDidEndOnExit = 1 << 19, // 'return key' ending editing
UIControlEventAllTouchEvents = 0x00000FFF, // for touch events
UIControlEventAllEditingEvents = 0x000F0000, // for UITextField
UIControlEventApplicationReserved = 0x0F000000, // range available for application use
UIControlEventSystemReserved = 0xF0000000, // range reserved for internal framework use
UIControlEventAllEvents = 0xFFFFFFFF
};</code></pre>
<p>又例如<code>UIControlState</code>定义了控件的基本状态</p>
<pre><code>typedef NS_OPTIONS(NSUInteger, UIControlState) {
UIControlStateNormal = 0,
UIControlStateHighlighted = 1 << 0, // used when UIControl isHighlighted is set
UIControlStateDisabled = 1 << 1,
UIControlStateSelected = 1 << 2, // flag usable by app (see below)
UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable only when the screen supports focus
UIControlStateApplication = 0x00FF0000, // additional flags available for application use
UIControlStateReserved = 0xFF000000 // flags reserved for internal framework use
};</code></pre>
<p>同时提供了给控件反馈交互操作的一系列方法,例如我们今天要讲的</p>
<pre><code>- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;</code></pre>
<p>比如我们有一个按钮,当他点击时候,我们执行ViewContollr的<code>-(void)click:(id)sender</code>方法,可以这么写:</p>
<pre><code>UIButton* button = [UIButton buttonWithType:UIButtonTypeSystem];
[button addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];</code></pre>
<p>这里传入的<code>UIControlEventTouchUpInside</code>枚举量,就是在控件frame内按下,然后抬起这样一个事件,<code>UIContol</code>将这个事件作为key,和目标(target)和目标方法(action)存到了自己私有的字典里。当用户点击按钮时,<code>UIControl</code>响应了触摸链的<code>touchesEnded</code>方法,便会根据私有字典,把对应<code>UIControlEventTouchUpInside</code>的目标(target)和目标方法(action)调用,这样完成事件的回传。</p>
<p>这是一个很好的设计,从原则上讲,我们的<code>XXXSegmentView</code>是一个可交互控件,理应继承于<code>UIControl</code>而非<code>UIView</code>,但笔者偷懒了,读者有兴趣可以自己尝试改写。</p>
<h3>4. block(块语法)</h3>
<p>没有继承<code>UIControl</code>,笔者只好祭出终极大杀器,<code>block</code>。block语法特性加入iOS已经有段日子了,因为使用方法篇幅太大,这里就不细说了,推荐一篇相关<a href="https://link.segmentfault.com/?enc=1%2FvYYpkBbkMKnbXaGya2QQ%3D%3D.%2FnBINQW1oK91fMtR87x9KbKg4zbVd%2FuIie4e2%2FycnYW4OGPqkf0MznMl2f60fwaU8unqvtHb0PyFcg22YqFaDg%3D%3D" rel="nofollow">教程</a>。</p>
<p>我们知道block是可以当作对象看待的,所以给<code>XXXSegmentView</code>添加下面这个属性</p>
<pre><code>@property (nonatomic, strong) void (^ didSelectBlock)(NSUInteger idx);</code></pre>
<p>在<code>ViewContoller</code>中,我们给<code>XXXSegmentView</code>的<code>didSelectBlock</code>赋值</p>
<pre><code>@property (weak, nonatomic) IBOutlet XXXSegmentView *segment;
[segment setDidSelectBlock:^(NSUInteger idx) {
NSLog(@"segment select %@",@(idx));
}];</code></pre>
<p>然后在<code>XXXSegmentView</code>中加入<code>block</code>调用</p>
<pre><code>- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
//.....其他代码
if (self.didSelectBlock) {
self.didSelectBlock(touchNumber);
}
}</code></pre>
<p>block的调用方法类似C语言的方法调用,传参格式也相同,注意使用前也要进行非空检测哦。</p>
<h2>不推荐的方法</h2>
<p>有两种反向传值的方法我并不推荐,一种是<code>单例</code>,一种是<code>通知</code>。</p>
<p><code>单例</code>作为一种特殊的设计结构,适合存储全局变量,拿来作为单条响应链的存储媒介,有些大材小用;<code>通知</code>作为另一种神奇的工具,苹果的工程师完全是将其用来深度封装框架所用,自己写代码使用通知,调理不清晰,结构不明朗,不说换个人,自己隔段时间可能就忘了自己在哪里接收了通知。所以能不用尽量不要用。</p>
<h2>小结</h2>
<p><img src="http://upload-images.jianshu.io/upload_images/926561-3e7a48e6c9dc653b.gif?imageMogr2/auto-orient/strip" alt="最终效果" title="最终效果"></p>
<p>至此,我们自制UIKit控件的第一篇教程就结束了,感兴趣的朋友可以从<a href="https://link.segmentfault.com/?enc=Su67pOB3ILI%2BQilTT2fckA%3D%3D.SbhhpLEX65GP7N4Sgd5o%2B40Z6nuCTd50P%2FcIfO1h8opKIAlxnXgRWi1wvp7m5e31" rel="nofollow">Github下载源码</a>对照分析。这几篇教程主要针对一些有objc基础,但UIKit刚入门的初学者,希望能帮到你们。</p>
<p>最后跟大家分享一个最的最新作品:<a href="https://link.segmentfault.com/?enc=uQhyhYxHFiNYoPXMZWyXmw%3D%3D.YlXyKa%2F6jdFL6z8%2BfsTpCcyPlwxLohs7v6D7jqDzwlUa%2FI%2FtTD6vsu64E6NMY%2BnC" rel="nofollow">zsy78191/XXXRoundMenuButton</a></p>
<p><img src="/img/bVr18N" alt="图片描述" title="图片描述"></p>
iOS自定义控件教程(三)UIView动画入门
https://segmentfault.com/a/1190000004250234
2016-01-05T14:07:51+08:00
2016-01-05T14:07:51+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
2
<p>上一篇文章我们介绍了UIView的触摸响应链原理,顺便学习UIView的基本属性和方法。在<a href="https://link.segmentfault.com/?enc=hZFidOkPEJ3SslGkr1E6VA%3D%3D.wE1fknIh%2FAJ4wdteD26IbcT3CdkmSTBbAK%2B2J5P9NMU%2FnUjCNpG2oi5n0yGBoA5P" rel="nofollow">iOS自定义控件教程(二)关于响应链的那些事</a>中我们讲解了触摸原理,但并未具体实现其功能,接下来我们具体讲讲点击效果的实现和响应的动画效果的实现。<br>最终实现的效果:<a href="https://link.segmentfault.com/?enc=lC9%2BMFqaCYMDOLq%2BN8dGYg%3D%3D.ZTN7JhVz0HT%2FEdotq433Cu5cP8IVjHzrjL7sMnCrfD7gv1r1kWq%2BvBnQdpsae6HL" rel="nofollow">Github下载源码</a></p>
<h2>触摸响应链UIResponder</h2>
<p><code>UIView</code>继承自<code>UIResponder</code>(响应链类),继承了相应的响应链方法:</p>
<pre><code>- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet * _Nonnull)touches NS_AVAILABLE_IOS(9_1);
// Generally, all responders which do custom press handling should override all four of these methods.
// Your responder will receive either pressesEnded:withEvent or pressesCancelled:withEvent: for each
// press it is handling (those presses it received in pressesBegan:withEvent:).
// pressesChanged:withEvent: will be invoked for presses that provide an analog value
// (like thumbsticks or analog push buttons)
// *** You must handle cancelled presses to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);</code></pre>
<p>其中<code>touches</code>开头的方法是触摸事件相关的方法;<code>presses</code>开头的方法,是iOS9加入的给iPhone6s等支持<code>Deep Press</code>功能的设备使用的相关方法;<code>motion</code>开头的则是给设备的陀螺仪和加速传感器使用的方法,用于获取晃动等事件。</p>
<p>细心的朋友可能会发现,<code>UIViewController</code>(后面简称VC) 和 <code>UIView</code> 同样继承自<code>UIResponder</code>,这样是为了方便<code>UIViewController</code> 处理他的<code>view</code>属性的响应事件,我们也就不用继承<code>UIView</code>重写他的响应链来处理<code>VC</code>主<code>View</code>的响应链了,<code>VC</code>的<code>View</code>默认将响应链穿给自己的<code>VC</code>,在<code>VC</code>中处理就可以了。</p>
<p><img src="/img/bVr2Ae" alt="strip" title="strip"></p>
<h2>触摸事件</h2>
<p>触摸事件可以通过触摸链响应,也可以使用<code>UIGesture</code>(手势类)来响应。今天我们的例子中用了比较原始的响应链方式。</p>
<p>XXXSegmentView.m</p>
<pre><code>- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
UITouch* touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
NSInteger detla = self.frame.size.width / self.number;
NSInteger touchNumber = point.x / detla;
self.selected = touchNumber;
// [self setNeedsLayout];
[self setSelectItemAtIndex:touchNumber animate:YES];
}</code></pre>
<p><strong>解释</strong> <code>touchesBegan</code>是手指按下事件的方法,这里按下我们不做响应,单写了super方法调用父类方法,还是那句话,父类方法可能是空的不需要调用,单这是一个好习惯,可以避免一些因为继承类不调用父类方法造成的BUG。</p>
<p><code>touchesEnded</code>是手指抬起事件的方法,首先<code>touches</code>这个集合中的UITouch对象,对应的就是触摸的手指。我们用<code>anyObject</code>取出其中之一,用<code>locationInView:</code>方法获取触摸点的位置。这个CGPoint是一个C中的结构体类型,只有x,y两个属性表示位置。</p>
<p>接下来我们根据Label的数量(self.number)来计算每个label的间隔,用总宽度除以个数。</p>
<pre><code>NSInteger detla = self.frame.size.width / self.number;</code></pre>
<p>接下来计算我们触摸的位置,在第几个区域</p>
<pre><code>NSInteger touchNumber = point.x / detla;</code></pre>
<p>这个<code>touchNumber</code>便是我们触摸的按钮的index。</p>
<p>接下来我们调用<code>setSelectItemAtIndex</code>方法来响应触摸这个Label的事件</p>
<pre><code>- (void)setSelectItemAtIndex:(NSUInteger)idx animate:(BOOL)animated
{
self.idx = idx;
[self.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([obj isKindOfClass:[UILabel class]]) {
UILabel* label = obj;
if (self.selected + 981 == label.tag) {
label.textColor = self.tintColor;
}
else
{
label.textColor = self.baseColor;
}
}
}];
//这里是我们下方的那根线,移动的动画,下面详细说
[UIView animateWithDuration:0.2 animations:^{
UILabel* label = (UILabel*)[self viewWithTag:idx + 981];
[self.selectView setFrame:CGRectMake(label.frame.origin.x - self.leftAndRightLineEdge, label.frame.size.height + label.frame.origin.y + 10, label.frame.size.width + self.leftAndRightLineEdge*2, 2)];
}];
}</code></pre>
<p><strong>解释</strong> 每一次触摸,我们都需要遍历所有的Label,将选中的,也就是<code>self.selected + 981 == label.tag</code>的Label改变他的颜色。</p>
<h2>UIView动画基础</h2>
<p>我们在上面代码中,调用self.selectView这个属性,是这么定义的</p>
<p>@interface</p>
<pre><code>@property (nonatomic, strong) UIView* selectView;</code></pre>
<p>@implementation</p>
<pre><code>- (UIView *)selectView
{
if (!_selectView) {
_selectView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 2)];
[self addSubview:_selectView];
_selectView.backgroundColor = self.tintColor;
}
return _selectView;
}</code></pre>
<p>这个View在第一次调用<code>self.selectView</code>的时候会初始化,第二次调用时候,_selectView已经初始化过了,就不会再执行初始化方法了。</p>
<p>注意,关于getter和setter的内容,我们就不细说了,新手只要知道,当前的版本的编译器,自动为@property添加了@synthesize(合成)语句,所以iOS6以后,代码中很少见到</p>
<pre><code>@synthesize selectView = _selectView;</code></pre>
<p>这样的写法了,因为编译器自动帮你做了。所以_selectView是内部变量,单不要直接这样调用,而应该在除了- (UIView *)selectView这个getter方法外的地方,使用<code>self.selectView</code>来调用。</p>
<p>我们再来看看上面触摸代码中的动画代码:</p>
<pre><code> [UIView animateWithDuration:0.2 animations:^{
UILabel* label = (UILabel*)[self viewWithTag:idx + 981];
[self.selectView setFrame:CGRectMake(label.frame.origin.x - self.leftAndRightLineEdge, label.frame.size.height + label.frame.origin.y + 10, label.frame.size.width + self.leftAndRightLineEdge*2, 2)];
}];</code></pre>
<p><strong>解释</strong> 先使用<code>viewWithTag:</code>方法获取Label,注意这里返回的是UIView类,所以需要使用(UILabel*)进行强行类型转换。</p>
<p><code>UIView animateWithDuration:animations: </code>这个方法,是iOS4.0加入的简化版的补帧动画方法。在这个block中去改变一个UIView的frame,系统则会根据传入的Duration(单位:秒),来自动完成补间动画,类似于<code>Flash</code>中的补间动画。</p>
<p>如果不在animateWithDuration中改变view的frame,view则会直接变为新的frame,则不会有中间改变的动画过程了,是不是很简单。</p>
<p>最后<code>self.leftAndRightLineEdge</code>这是一个CGFloat浮点型数值,用来控制下面线两端超出Label的长度,我们demo中第二个例子中这个数值为20。</p>
<p>这样,我们的最终效果就完成。</p>
<p><img src="/img/bVr2Ae" alt="最终效果" title="最终效果"></p>
<p>下一篇教程,我们将着手分析UIView控件响应事件并传递给其他VC或View的具体实现方法,有什么疑问请在下方留言。</p>
iOS自定义控件教程(二)关于响应链的那些事
https://segmentfault.com/a/1190000004243471
2016-01-04T12:23:14+08:00
2016-01-04T12:23:14+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
2
<p>上一次我们一起做一个多段选择的自定义控件,顺便学习UIView的基本属性和方法。在<a href="https://link.segmentfault.com/?enc=F%2BTc2f7m8vJZ73bA3JPV5Q%3D%3D.M7prpFDWxPlvtLCmPdmAU5ODPKz4wUeYyZnq%2FQlrNGGHVnbTkI0DfpgkzSXJUUCM" rel="nofollow">iOS自定义控件教程(一)</a>中我们完成了UILabel布局的工作,接下来我们一起研究一下触摸响应链原理。<br>最终实现的效果:<a href="https://link.segmentfault.com/?enc=bnubrGtpt%2B%2BPsA9i2FwKUg%3D%3D.vhUyNF8FjXP9WR17l0VXqqJ9m2%2FaZusIkBZfBBnwK1vYzb%2FGTsH81pG4IXFMpQUb" rel="nofollow">Github下载源码</a></p>
<p><img src="/img/remote/1460000004843897" alt="" title=""></p>
<h2>链式响应原理</h2>
<p>先简单普及一下响应链原理,我们可以简单地认为iPhone屏幕就是一个容器,我们看到的各种控件(UIView和UIView子类)都是屏幕(UIWindow)这个容器中的子容器,最外层的容器是应用委托(AppDelegate)的属性keyWindow,其实UIWindow也是UIView的子类。</p>
<p>这些容器的相互关系,就是我们最早学数据结构接触的多叉树关系,keyWindow就是这棵树的Root,其它它的子View都是分支。例如上面的例子,我们用xcode进行调试可以得到下图。注意在调试过程中,才有这排功能:</p>
<p><img src="/img/remote/1460000011331323" alt="12.png" title="12.png"></p>
<p>得到下面的层级结构</p>
<p><img src="/img/remote/1460000011331324" alt="屏幕快照 2016-01-04 11.13.53.png" title="屏幕快照 2016-01-04 11.13.53.png"></p>
<p>在我们的Demo里面,总共有两个<code>XXXSegmentView</code>,第一个<code>XXXSegmentView</code>有四个子<code>UILabel</code>,而他的<code>父View</code>是当前<code>ViewContoller</code>的主<code>View</code>,一个背景是纯白色的全屏<code>View</code>。</p>
<p>当<strong>触摸事件</strong>被iPhone硬件接收到时,一个链式的触摸信号就被开启了。最先接收到触摸事件的是<code>Root</code>,也就是我们应用程序的<code>keyWindow</code>,<code>keyWindow</code>再将触摸事件传递给它的一级子View们。这个传递过程不需要开发者用代码实现,如果开发者有需要重写传递,需要使用的是<code>UIView</code>的<code>- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; </code>方法。</p>
<p>这个方法返回的UIView,就是父View决定让哪个孩子作为这个触摸的响应View。这样,触摸事件就不断往下级传递,对应的View才能根据触摸事件改变样式。</p>
<p>新人在开发时,经常会遇到不响应触摸的情况。有两种常见的可能打断触摸链</p>
<p><strong>1. 目标View到Root的分支中,有View没有开启触摸事件,打断了响应链传递。</strong></p>
<p>这里需要知道UIView有一个基本属性叫做<code>userInteractionEnabled</code>,这个属性为<code>YES</code>时才能响应触摸事件。所以检查一下分支中是否有View的触摸开关被关闭。例如,<code>UIImageView</code>这个展示图片的类,默认就是不响应触摸链的,如果你需要继承它写一个新View,并且需要触摸,或者给他添加了需要触摸的子View,请修改他的<code>userInteractionEnabled</code>属性。</p>
<p><strong>2. 目标View的触摸事件,因为层级关系被上层覆盖的View劫走,哪怕这个View可能是透明的</strong></p>
<p>根据链式响应原理,父View会将触摸链传给符合相应条件,并且层级关系最上层的View,所以下层的View接收不到触摸事件,遇到这种情况,你可以根据需要进行处理。如果覆盖在上层View层级顺序有误,通过调用他们父View的两个方法可以轻松交换他们层级覆盖关系。</p>
<pre><code>- (void)bringSubviewToFront:(UIView *)view;
- (void)sendSubviewToBack:(UIView *)view;</code></pre>
<p>如果上层View就是要放在上面,那就关掉上层View的响应链,<code>userInteractionEnabled</code>设为<code>NO</code>,这样父View在进行hitTest时就会抛弃这个View,选择层级更低的View传递。</p>
<h2>关于透明</h2>
<p>这里请注意<code>UIView</code>的几个不同的属性。</p>
<blockquote><p>backgroundColor 背景颜色,UIColor类<br>alpha 颜色中的alpha通道值,这个值范围0-1,等于0时完全透明,等于1时,完全不透明<br>hidden BOOL型,是否隐藏</p></blockquote>
<p>这三个属性,都可以实现UIView隐身的效果。例如一个空的UIView,<code>backgroundColor</code>设为[UIColor clearColor]时,背景色是透明的;alpha设为0时,UIView也是透明的;hidden设为YES时,UIView同样不可见。</p>
<p>但他们的区别也很明显,<code>backgroundColor</code>属性的修改,不影响子View,所以子View不会因为父View的<code>backgroundColor</code>设为[UIColor clearColor]而隐藏,同时,父View同样响应触摸链。</p>
<p>而alpha值则不同,渲染时alpha值时一个叠加属性,例如父View透明度为0.5,子View透明度为0.5,这时渲染出来的真实效果,子View的透明的应该为0.5*0.5 = 0.25,所以当父View的alpha为0时,子View也是完全不可见的。另外,alpha为0的View,是不响应触摸链的。</p>
<p>最后这个hidden属性,直接从根源,决定要不要渲染这个View,不像alpha属性,是不需要渲染时计算最终的渲染效果的,因为这个属性为<code>YES</code>时,根本不进行渲染,所以渲染链都断了,子View也不会渲染了,更不会响应触摸了。</p>
<blockquote><p>响应链hitTest的三个条件,就是userInteractionEnabled = YES,并且alpha != 0, 且hidden = NO。</p></blockquote>
<h2>小结</h2>
<p>树型结构,既是<code>UIView</code>的触摸响应结构,也是渲染链结构,触摸链通过下面四个方法传递,<code>UIView</code>是<code>UIResponder</code>的子类,下面四个方法是触摸链传递的会调用的方法。分别对应<code>开始</code>,<code>移动</code>,<code>结束</code>,<code>取消</code>四种状态。</p>
<pre><code>- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;</code></pre>
<p>渲染树型结构要更加简单一点,调用的是<code>- (void)setNeedsDisplay;</code>方法,也就是在父View的setNeedsDisplay方法中,调用子View的setNeedsDisplay方法,这个方法会通知View在下一个cpu时间里,进行重新渲染,也就是调用他们的<code>- (void)drawRect:(CGRect)rect;</code>方法实现绘制。</p>
<p>但是自从手势(UIGesture)加入之后,触摸链开发用的越来越少,但我们的<a href="https://link.segmentfault.com/?enc=47xKWrKSkO4ITntuttkaqw%3D%3D.LV0I0nIknAPtHkNV8y011JYGI9OTJRXXs7ryd9XJQFDWk%2BRDUbjw1rKCtqRQyTXS" rel="nofollow">Demo</a>还是使用了简单的触摸链。后面的章节我们将介绍功能更为便捷和强大手势(UIGesture)功能。</p>
iOS自定义控件教程(一)看看吧,总会得到你想要的
https://segmentfault.com/a/1190000004231855
2015-12-31T15:56:10+08:00
2015-12-31T15:56:10+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
3
<h2>简介</h2>
<p>本文将是一个关于Cocoa Touch中UIKit框架的自定义控件系列教程,我们将从基础开始,由浅入深,分析讲解自定义控件的相关知识和技巧。</p>
<h2>基础概念,给新人看</h2>
<h3>基础知识</h3>
<ol>
<li><p><code>UIKit</code>是<code>iOS</code>系统使用的界面框架</p></li>
<li><p><code>UIKit</code>中最基本的类是<code>UIView</code>,也就是界面的基础操作类</p></li>
<li><p>iOS使用的渲染框架叫<code>Core Graphics</code>,所以才回有<code>CG</code>开头的一堆基础类型,如CGFloat(浮点),CGPoint(点),CGSize(尺寸),CGRect(矩形)</p></li>
<li><p><code>UIView</code>对象都包含至少一个<code>CALayer</code>对象,<code>CALayer</code>才是最终渲染出效果的对象</p></li>
<li><p><code>UIView</code>和<code>CALayer</code>的层级关系是相同的,他们都是多叉树,同一个<code>父View(superView)</code>的<code>子View们(subViews)</code>是有层级覆盖关系的,上层的View遮挡下层View</p></li>
<li><p>这里所说的<code>层</code>关系和设计软件中的层关系是类似的,如Photoshop,Sketch等</p></li>
<li><p><code>UIView</code>和<code>CALayer</code>的分工是,前者负责<strong>保存属性</strong>和<strong>处理响应链</strong>,后者负责渲染</p></li>
</ol>
<h2>一、 UIView的基本属性和方法</h2>
<p>今天我们一起做一个多段选择的自定义控件,顺便学习UIView的基本属性和方法。先来看一下实现的效果:<a href="https://link.segmentfault.com/?enc=KTWMZou64%2Fd32Es6z4uY%2Bg%3D%3D.oPIIpTJEWiIjhb2XfVlIKUWI6NhzfKUwKSSj7RdWRD6yMu9RB5pmHPXjMATyAR%2F5" rel="nofollow">Github下载源码</a></p>
<p><img src="/img/bVrUXW" alt="图片描述" title="图片描述"></p>
<p><strong>先讲一下思路</strong> 我们继承UIView写一个XXXSegmentView,用<code>UILabel</code>作为XXXSegmentView的子View,显示上图中的标签。</p>
<p>我们的多段选择View,暂且命名为XXXSegmentView,继承自<code>UIView</code>。首先创建一个新文件,选择xcode菜单,File -> New -> File </p>
<p><img src="/img/bVrUYg" alt="图片描述" title="图片描述"><br><img src="/img/bVrUYh" alt="图片描述" title="图片描述"></p>
<p>得到文件<code>XXXSegmentView.h</code>和<code>XXXSegmentView.m</code>,我们给<code>XXXSegmentView</code>写一个初始化配置的方法,<code>- (void)configForTitles:(NSArray<NSString*>*)titles;</code>。</p>
<pre><code>@interface XXXSegmentView : UIView
- (void)configForTitles:(NSArray<NSString*>*)titles;
@property (nonatomic, assign) NSInteger number; //记录titles的个数
@end</code></pre>
<p>这里注明了要使用者传入一个包含字符串类型的数组。这样我们的自定义控件之要两步就能初始化完毕,如下在ViewController中加入初始化代码:</p>
<pre><code>- (void)configSegment
{
XXXSegmentView* segment = [[XXXSegmentView alloc] initWithFrame:CGRect(0,0,300,80)];
//加入三个标题,内容入数组中传入
[segment configForTitles:@[@"Main",@"More",@"Me"]];
[self.view addSubview:segment];
}</code></pre>
<p><strong>解释:CGRect(0,0,300,80)返回一个CGRect数据,是一个矩形,前两个值是起始点,也就是左上角的坐标,后两个数是尺寸,300是宽,80是高</strong></p>
<p>接下来我们需要实现<code>configForTitles</code>这个方法了。来到<code>XXXSegmentView</code>中,我们加入代码:</p>
<pre><code>- (void)cleanAll
{
[self.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([obj isKindOfClass:[UILabel class]]) {
[obj removeFromSuperview];
}
}];
}</code></pre>
<p><strong>解释:cleanAll这个方法,通过self.subviews方法,返回的是包含了segmentView的全部子view的数组,然后用enumerateObjectsUsingBlock方法进行快速枚举,筛选出其中是UILabel的View,调用它的removeFromSuperview方法将其移除</strong></p>
<p>这个函数存在的意义,就是在使用者多次调用<code>configForTitles</code>更改标签的时候,清除掉旧的Label。</p>
<pre><code>- (UILabel*)standLabel
{
UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 50)];
label.font = [UIFont systemFontOfSize:14];
label.textColor = self.tintColor;
return label;
}</code></pre>
<p><strong>解释:UILabel类是UIView的子类,用于显示文字,支持多行显示,支持改变字体和颜色</strong><br><strong>[UIFont systemFontOfSize:14]方法返回一个系统字体对象UIFont</strong><br><strong>label.textColor = self.tintColor;这一步将我们segment的tintColor传给了Label作为字体Color</strong></p>
<p>所谓的<code>TintColor</code>可以理解为控件的高亮颜色,是UIView的默认属性,所以继承于UIView的XXXSegmentView也有这个属性,并且有默认值。</p>
<pre><code>- (void)configForTitles:(NSArray *)titles
{
[self cleanAll];
self.number = titles.count;
[titles enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
UILabel* la = [self standLabel];
[la setText:obj];
[la setBackgroundColor:[UIColor clearColor]];
[self addSubview:la];
la.tag = idx + 981;
}];
}</code></pre>
<p><strong>解释</strong>我们首先记录了标题的个数,然后调用<code>cleanAll</code>方法,清楚旧的Label,然后根据传入的titles数组,给View加入新的Label,并且,我们给label加了文字,和透明色<code>([UIColor clearColor])</code>的背景。</p>
<p>最后我们给每一个Label加了一个<code>Tag</code>编号,这个编号从981(这个数是我随便选的,你也可以选个更大的)开始,用来标记我们的Label们前后顺序。</p>
<h3>布局入门</h3>
<p>代码写到这里,我们运行软件,并不能得到想要的并排展示效果,因为我们所有的label,位置没有做调整。</p>
<p>用什么属性控制UIView的位置呢?答案是<code>Frame</code>属性。 </p>
<p>我们把屏幕比作一个画板,左上角是(0,0)坐标,frame则是一个矩形,如CGRect(0,0,100,50)代表一个从(0,0)点开始,宽100,高50的矩形。当我们重新设置UIView的Frame属性,他们的位置就改变了。</p>
<p>所以我们给XXXSegmentView.m加入如下代码:</p>
<pre><code>- (CGRect)boundsWithLabel:(UILabel*)label
{
return [label.text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, label.frame.size.height) options:NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:label.font} context:nil];
}
</code></pre>
<p>这个方法用来返回一个字符串渲染出来的边界,<code>boundingRectWithSize:CGSizeMake</code>这个方法比较复杂,初学者看不懂直接粘过来用下就行,这里不需要理解。<br>不过我还是要解释一下,这个方法传入的第一个CGSize是,外包围的限制,CGFLOAT_MAX返回的是最大的浮点数,也就是这里在计算宽度时不做限制。NSStringDrawingUsesFontLeading属性表示用自字体作为计算行高的标准,attributes字典传入的是文字的渲染样式,NSFontAttributeName键传入文字的字体和字号,返回的CGRect是文字根据以上要求,渲染出来的外包围。</p>
<pre><code>- (void)layoutSubviews
{
[super layoutSubviews];
[self.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([obj isKindOfClass:[UILabel class]]) {
UILabel* label = obj;
CGRect rect = [self boundsWithLabel:label];
[label setFrame:CGRectMake(0, 0, rect.size.width, rect.size.height)];
label.center = CGPointMake(self.frame.size.width/(self.number*2)*((label.tag-981)*2+1), self.frame.size.height/2);
}
}];
}</code></pre>
<p><strong>解释</strong><code>layoutSubviews </code>方法是UIView的固有方法,这个方法在我们View需要给子View布局的时候会自动调用,我们重写了这个方法,所以要用<code>super</code>指针,先调用父类方法<code>[super layoutSubviews];</code>,虽然<code>UIView</code>的<code>layoutSubviews</code>方法很有可能是空的,但这是好习惯。</p>
<p>然后我们又通过<code>subviews</code>数组快速遍历子View,筛选出<code>UILabel</code>对象,因为我们的子View中以后要加入其它不是<code>UILabel</code>的对象。遍历过程中我们改变Label的外包围,通过上面说的计算外包围的接口,然后通过<code>center(中心点)</code>这个<code>UIView</code>的属性来改变子Label的位置。<strong>label.tag-981</strong>是Label的序号,<strong>self.frame.size.width</strong>是我们SegmentView的宽度,<strong>self.number</strong>是label的个数。</p>
<p>然后我们再运行程序,是不是完成了Label的布局啦,并不复杂,下一篇我们将继续讲解。</p>
一个程序员的理想主义
https://segmentfault.com/a/1190000004229884
2015-12-31T10:24:33+08:00
2015-12-31T10:24:33+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
2
<p>我是一个程序员,也是一个理想主义者。</p>
<p>作为一个五年多的从业者,我回想起当初跨入这一行的初衷,是因为这里是理想主义者的净土。</p>
<p>2011年iPhone蓄势待发准备横扫全球,App Store开始造富,让我们这些非计算机专业的穷学生,都开始转入App行业酌一杯羹。App Store给了全球的开发者一个平台,在这个平台上,人人平等,你不用贿赂巴结苹果的工作人员,也可以常年在榜单上占据一席之地,<strong>只要你的软件足够好</strong>。</p>
<p>后来走入社会的我,靠着自学的技术,在北京的一家IT公司找了一份不错的工作。毕竟不是科班出身,敬畏心很强的我从工作后一直不敢放松,一直学到今天还在不断学习新的知识。因为在程序员这一行,归根结底看的还是工作能力。也许你内向、沉默寡言,也许你人缘超好、很会维系同事关系,也许你脾气很坏,经常跟产品经理吵架,注意,这都无所谓,只要你的技术是公司无法替代的,公司不会亏待你。</p>
<p>虽然现实情况并没有我说的这么好,比如在我的老东家工作,程序员还是没有商务部门的人强势,甚至会被HR部门压一头,单相对于其它行业,已经强很多了吧,反正我没干过拿报纸、端茶倒水这些伺候人的活。</p>
<p>工作几年后,大部分人都会面临一个现实问题,<strong>“要不要转做管理?”</strong></p>
<p>我反正是这么想的,不做管理前,我向技术总监或者部门经理汇报,做管理吧,我就成了汇报中枢了。和人打交道这件事,还是挺复杂的、挺耗时的,我还是省省时间改进代码吧。但毕竟每个人有自己不同的追求,我觉得有能力和爱好的人还是应该尝试管理口的,毕竟管理口的晋升路更宽,有更大的权力,才能做更大的事。像我这种喜欢新事物、喜欢开源、喜欢分享技术的程序员,还是继续搞技术才能让我内心踏实。</p>
<p>我有一些做技术的朋友,小时候崇拜盖茨,工作后推崇老乔,结果他创业做了自己的老板。我觉得他们就不是真的喜欢编程这件工作,因为苹果的技术牛是Steve Wozniak呀。<strong>如果轻视技术(设计也是技术),苹果是不会有今天的成就的。当然这句话也可以套用给Google、脸书等一大票硅谷的技术公司。</strong>所以如果选择了技术这条路,即使你觉得自己站到了巅峰,也会发现巅峰之上原来还有高山,我们要做的,只有相信技术,精益求精。</p>
<p>虽然我是理想主义程序员,但我也是物质的,只是我对单纯的物质没有兴趣罢了。这要怎么理解呢?我举几个例子:<strong>比如我会希望自己的App大卖,挣很多钱,但我不会去买彩票,虽然概率差不多(哈哈哈),因为对我来说,大卖的意义要高于挣了很多钱,我更希望去做点什么事,然后顺便挣了点钱糊口。</strong></p>
<p>再比如我也想写技术书籍,把我积累的经验和知识分享给初学者,顺便挣点钱糊口(理想要和物质结合),但我不会去做代购,虽然后者门槛更低挣的更多。因为我只是想做点事,不是单纯地想挣钱,当然代购的人也可以说他们想让国人用到国外的好商品,顺便挣点钱(哈哈哈)。</p>
<p>我们这些八零后成长的三十多年,是我们这个国家和社会疯狂造富的三十多年,我们身边的很多人,可能连高中都没有读,却能在我们大学毕业的时候,就买了车甚至买了房。这不是教育制度的错,而是社会体制和法制的不完善给了"聪明的人"更多的机会。我不聪明,但我也不仇富,他们用辛苦的劳动和过人的智慧积累的财富,我表示尊敬,但我并不想跟他们一样聪明。我挣的不多,但工作很开心,因为我知道,一个人想把自己喜欢的事情变成工作,真的很难。</p>
<p>但即使有这些理由,我们也要面对亲人朋友甚至爱人的质疑,<strong>“你写代码能写多久?”</strong></p>
<p>好沉重的一个话题,我也看过其他文章中说国外50多岁的老技术,两天可以完成普通程序员两周才能完成的工作;也看到过国外某些技术公司对待老技术这么好那么好的文章,但是,我们毕竟不在国外。</p>
<p>不在国外我们能写一辈子代码么?我还年轻我无法回答,国内可能还没有在民企做这行,且在这行退休的前辈吧。所以我要立个flag,如果我退休时候还在写代码,我会回来告诉你们答案的。</p>
RACSignal的一些常用用法(二)
https://segmentfault.com/a/1190000004223535
2015-12-30T10:50:03+08:00
2015-12-30T10:50:03+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>RACSignal的一些常用用法(二)</h2>
<p>上一篇<a href="https://link.segmentfault.com/?enc=FawzZkCFj4ASPdNLqRMoAQ%3D%3D.AVLmqU2MLVAXr2Bd39ydiVpOsEY%2BcsUt%2FWEUa87MEEQhtqHeQwiZLKjrShCwYWoQ" rel="nofollow">一些常用的RACSignal</a></p>
<p>紧跟上一篇的内容,我们来用一些例子让大家更深刻地理解<code>RACSignal</code>。</p>
<h3>NSData</h3>
<p>尝试这个例子之前,请在Demo工程的info.plist文件中加入<code>App Transport Security Settings</code>键值,并加入<code>Allow Arbitrary Loads</code>:<code>YES</code>键值对,用来开启iOS下的非安全连接。</p>
<p>NSData + RACSupport.h</p>
<pre><code>@interface NSData (RACSupport)
// Read the data at the URL using -[NSData initWithContentsOfURL:options:error:].
// Sends the data or the error.
// 返回一个URL异步请求的信号量
// scheduler 不能为空
+ (RACSignal *)rac_readContentsOfURL:(NSURL *)URL options:(NSDataReadingOptions)options scheduler:(RACScheduler *)scheduler;
@end</code></pre>
<p>可以这样用</p>
<pre><code>NSURL* url = [NSURL URLWithString:@"http://www.jianshu.com"];
RACSignal* getDataSignal = [NSData rac_readContentsOfURL:url options:NSDataReadingUncached
scheduler:[RACScheduler mainThreadScheduler]];
[getDataSignal subscribeNext:^(id x) {
NSLog(@"%@",x); //这里的x就是NSData数据
}];</code></pre>
<h4>map函数</h4>
<p>如果我们使用<code>NSData</code>的<code>rac_readContentsOfURL:url</code>方法下载的是一张图片,我们肯定希望这个Signal最后输出的是<code>UIImage</code>对象,这里就要用到非常常用的<code>map</code>函数。</p>
<p><code>map</code>函数就像<code>signal</code> 管道上的中间处理器,从这里走过的<code>signal</code>都会经过一段处理后,变成新的<code>signal</code>继续传输。而这个处理过程则在<code>map</code>函数中由开发者决定。</p>
<p>将输出<code>NSData</code>的<code>signal</code>转换为输出<code>UIImage</code></p>
<pre><code>NSURL* url = [NSURL URLWithString:@"http://img1.gtimg.com/gamezone/pics/24159/24159840.jpg"];
RACSignal* getDataSignal = [NSData rac_readContentsOfURL:url options:NSDataReadingUncached
scheduler:[RACScheduler mainThreadScheduler]];
//map函数进行转换
RACSignal* getImageSignal = [getDataSignal map:^id(id value) {
if (value) {
return [UIImage imageWithData:value];
}
return nil;
}];
[getImageSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];</code></pre>
<h4>merge方法</h4>
<p>接着我们提出一个新需求,同时请求三张不同的图片,并将它们发送的信号合并成一条信号量。这里就需要用到信号量的<code>merge</code>方法,如下</p>
<pre><code>NSURL* url = [NSURL URLWithString:@"http://img1.gtimg.com/gamezone/pics/24159/24159840.jpg"];
NSURL* url2 = [NSURL URLWithString:@"http://i3.hoopchina.com.cn/blogfile/201306/29/137247593017986.jpg"];
NSURL* url3 = [NSURL URLWithString:@"http://img.youxile.com/pic/1301/25170237170.jpg"];
RACSignal* getImageSignal1 = [[NSData rac_readContentsOfURL:url
options:NSDataReadingUncached
scheduler:[RACScheduler mainThreadScheduler]]
map:^id(id value) {
if (value) {
return [UIImage imageWithData:value];
}
return nil;
}];
RACSignal* getImageSignal2 = [[NSData rac_readContentsOfURL:url2
options:NSDataReadingUncached
scheduler:[RACScheduler mainThreadScheduler]]
map:^id(id value) {
if (value) {
return [UIImage imageWithData:value];
}
return nil;
}];
RACSignal* getImageSignal3 = [[NSData rac_readContentsOfURL:url3
options:NSDataReadingUncached
scheduler:[RACScheduler mainThreadScheduler]]
map:^id(id value) {
if (value) {
return [UIImage imageWithData:value];
}
return nil;
}];
//合并操作
RACSignal* mergeSignal = [RACSignal merge:@[getImageSignal1,getImageSignal2,getImageSignal3]];
[mergeSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];```
输出如下:</code></pre>
<p>2015-12-30 10:19:08.776 Fahu[1354:39991] <UIImage: 0x7f9ce2ac3730>, {450, 600}<br>2015-12-30 10:19:08.891 Fahu[1354:39991] <UIImage: 0x7f9ce2b02600>, {500, 687}<br>2015-12-30 10:19:09.098 Fahu[1354:39991] <UIImage: 0x7f9ce29a4500>, {500, 346}</p>
<pre><code>
###filter函数
网络也会有连不上的时候,三张图片中如果有哪一张请求失败,我们并不想让`signal`发送`nil`值过来,可以使用filter函数对`signal`进行筛选。稍微改造一下上面的`mergeSignal`:
</code></pre>
<p>RACSignal* mergeSignal = [[RACSignal merge:@[getImageSignal1,getImageSignal2,getImageSignal3]] filter:^BOOL(id value) {</p>
<pre><code> return @(!!value);</code></pre>
<p>}];<code>`</code></p>
<p>注意这里的filter函数返回的是NSNumber型的BOOL值,YES是通过,NO时拒绝通过。</p>
<h3>NSURLConnection</h3>
<p>有了<code>RAC</code>,一些简单的网络请求都可以不用AF框架了。</p>
<p>NSURLConnection+RACSupport.h</p>
<pre><code>+ (RACSignal *)rac_sendAsynchronousRequest:(NSURLRequest *)request;```
举个例子,还是请求图片</code></pre>
<p>NSURL* url = [NSURL URLWithString:@"http://img1.gtimg.com/gamezone/pics/24159/24159840.jpg"];</p>
<pre><code>NSURLRequest* request = [NSURLRequest requestWithURL:url];
RACSignal* connectionSignal = [NSURLConnection rac_sendAsynchronousRequest:request];
[connectionSignal subscribeNext:^(id x) {
NSLog(@"%@",x);</code></pre>
<p>}];<code>`</code></p>
<p>输出</p>
<pre><code> <RACTuple: 0x7f826c071c00> ...省略```
注意,这个`signal`发送的信号量是一个`RACTuple`对象。
###RACTuple
`RACTuple`(元组)类,是`RAC`中专门用来返回多个返回值的类,这个设计很像swift中的元组。
上面例子中的`RACTuple`有两个返回值,`x[0]`是http response的头部信息,`x[1]`是请求返回的数据,是一个`NSData`对象。元组类对象的读取方法和数组相同。
下一片文章我们接着聊聊`RAC`对`UIKit`进行的扩展。
</code></pre>
一些常用的RACSignal
https://segmentfault.com/a/1190000004217931
2015-12-29T11:25:35+08:00
2015-12-29T11:25:35+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>一些常用的RACSignal</h2>
<p>如果你没有听说和使用过<code>ReactiveCocoa</code>框架,请阅读sunnyxx写的<a href="https://link.segmentfault.com/?enc=o5BKiSAmU411VhePRURKUQ%3D%3D.nA3M61Q1Gk5gtNZlvyVghc4G2zHuyIF40%2B27yGNLF0HDdnrHc0KVJ1hQ%2F2E3jXxBaS3bLLHxhoGSdc0oS4gq7A%3D%3D" rel="nofollow">入门教程</a>。</p>
<p>本文将罗列一些常用的RACSignal方法,并会不断更新。</p>
<h3>RAC()和RACObserve()</h3>
<p><code>RAC(<#TARGET, ...#>)</code>宏用来将一个对象的属性和信号量绑定,<br><code>RACObserve(<#TARGET#>, <#KEYPATH#>)</code>宏则用来生成一个对象的绑定属性的信号量,<br>这样描述很抽象,上一个例子解释</p>
<pre><code>@property (nonatomic, strong) NSString* testText;
@property (nonatomic, strong) NSString* testText_2;
RAC(self,testText) = RACObserve(self, testText_2);</code></pre>
<p>这个例子中,testText_2属性有任何变动,都会通知给testText变成一样的内容,testText变化不会影响testText_2。RAC宏在等号左边,右边RACObserve宏返回一个传值信号量。</p>
<p>这个用法十分宽泛,可以省去自己添加KVO或添加分散且复杂的绑定代码,并且逻辑更直观。</p>
<h3>RACSingal</h3>
<p>接着我们讲讲<code>RACSignal</code>这个信号量类,他的基类是<code>RACStream</code>,从名字看出,"信号量类"继承自"流类",所以<code>RACSignal</code>支持一些高级,如:<code>flattenMap</code>,<code>flatten</code>,<code>map</code>等等,您看不会用也没关系,本篇文章我们用不到这些函数。</p>
<p>这些函数的用法请参考<a href="https://link.segmentfault.com/?enc=2jYofBg1WiHoaka5XnX%2BKA%3D%3D.CX4W2%2FqsXG83XYiQWkHwSTHV4eMiZr1dADQxLNdmLiY%3D" rel="nofollow">使用ReactiveCocoa实现iOS平台响应式编程</a>博文,此文中使用的<code>RACSequence</code>类和<code>RACSingal</code>同样继承自<code>RACStream</code>,是<code>RAC</code>框架的数组类,<code>RACSequence</code>和<code>NSArray</code>转换也十分简单:</p>
<pre><code>NSArray* array = @[@(1),@(2),@(3),@(4),@(5)];
RACSequence* sequence = [array rac_sequence]; //NSArray -> RACSequence
NSArray* array2 = [sequence array]; //RACSequence -> NSArray</code></pre>
<p>既然<code>RACSignal</code>和<code>RACSequence</code>都是"流"类,那肯定有共同的特性。<code>RACSequence</code>队列,是很形象的流,例如人流车流都是队列。<code>RACSingal</code>则抽象一点,所谓信号,也是一种流,我们用代码来描述。</p>
<pre><code> RACSignal* getRandomSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@(arc4random())];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
}];
}];
[getRandomSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
[getRandomSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];</code></pre>
<pre><code>2015-12-29 10:28:20.611 Fahu[967:365178] 707130803
2015-12-29 10:28:20.613 Fahu[967:365178] 743304158</code></pre>
<p>我们的<code>getRandomSignal</code>信号,是一个用来获取随机数的信号量,用类方法<code>createSignal</code>初始化,<code>subscriber</code>则是这个信号量的订阅者,当有人订阅这个信号量时,<code>[subscriber sendNext:@(arc4random())];</code>信号量返回一个随机数给订阅者,注意这个<code>createSignal</code>的Block,只有在有人订阅时才会执行。</p>
<p>订阅的操作就是信号量调用<code>subscribeNext</code>方法,一次订阅会返回一个值,再订阅一次,返回了一个新的随机数,如上方LOG。</p>
<p>代码并不难懂,出于讲解需要,写了这么一个例子,并不是很合理。应为直接调用<code>arc4random</code>就可以获取随机数,没必要写这么麻烦把它包装成信号量。但随着一项任务的复杂程度增高,信号量写法便会展示出更清晰的逻辑。</p>
<p>举个例子来源于ReactiveCocoa的开源项目,天气类软件<a href="https://link.segmentfault.com/?enc=uVqvQWTKzB2Abdipkt3tMg%3D%3D.4dyFRCb7CUHHChcOyvoi5MXQJExqj5GLS0UNnghVr4LmvR3AQSIap9VEGgzzQ3Lb" rel="nofollow">Tropos</a>。代码中包含了一些高级函数,请根据注释来理解</p>
<p>先普及一下<code>CLLocationManager</code>的回调,这个位置管理器类,开始定位以后,会调用<code>locationManager:didUpdateLocations:</code>和<code>locationManager:didFailWithError:</code>两个回调,前者用来返回定位数据<code>NSArray<CLLocation*></code>,后者用来返回错误。</p>
<pre><code>//返回一个用来更新地理位置的信号量
- (RACSignal *)updateCurrentLocation
{
//已知[self didUpdateLocations]是locationManager:didUpdateLocations:回调的信号量,这个回调返回定位的GPS信息。[self didUpdateLocations]返回的GPS位置数组信号量,进行map操作,map操作简单的说就是遍历+处理,[self didUpdateLocations],每每返回一个位置数组,这一步的map函数都将其变为数组的最后一个对象输出,然后再用filter函数过滤,把请求时间超时的数据去掉,isStale是一个私有属性,这里就不粘出来了。
RACSignal *currentLocationUpdated = [[[self didUpdateLocations] map:^id(NSArray *locations) {
return locations.lastObject; //返回最后一个对象
}] filter:^BOOL(CLLocation *location) {
return !location.isStale; //过滤超时对象
}];
//map函数返回id类型,用来替换。filter函数返回 BOOL值,决定要不要过滤掉,返回NO就过滤。
//locationManager:didFailWithError:的信号量
RACSignal *locationUpdateFailed = [[[self didFailWithError] map:^id(NSError *error) {
return [RACSignal error:error];
}] switchToLatest];
//最后返回的信号量,是上面两股信号量的merge(混合),take:1是不管这两个信号那一个先来,只返回一个,initially是信号量开始时候调用的block,finally则是信号量结束了调用的block。
return [[[[RACSignal merge:@[currentLocationUpdated, locationUpdateFailed]] take:1] initially:^{
[self.locationManager startUpdatingLocation];
}] finally:^{
[self.locationManager stopUpdatingLocation];
}];
}</code></pre>
<p>不知道您感觉如何,相对于传统的<code>delegate</code>方法使用<code>CLLocationManager</code>,<code>RAC</code>信号量是不是让整个获取逻辑更加清晰明了,用一个函数完成了之前最少两段控制代码和两个回调的工作。</p>
<h3>rac_signalForSelector</h3>
<p>再让我们看看上段代码中的<code>didUpdateLocations</code>信号量怎么来的</p>
<pre><code>- (RACSignal *)didUpdateLocations
{
return [[self rac_signalForSelector:@selector(locationManager:didUpdateLocations:) fromProtocol:@protocol(CLLocationManagerDelegate)] reduceEach:^id(CLLocationManager *manager, NSArray *locations) {
return locations;
}];
}</code></pre>
<p><code>self</code>是<code>CLLocationManagerDelegate</code>,是<code>CLLocationManager</code>的委托,通过<code>rac_signalForSelector</code>生成了<code>@selector(locationManager:didUpdateLocations:)</code>的信号量,这个信号量在被订阅后,会在<code>locationManager:didUpdateLocations:</code>回调调用的时候,向订阅者发出信号。并且使用<code>reduceEach</code>函数把回调中多余的信息删除了,只返回了位置信息。</p>
<p><code>rac_signalForSelector</code>这个方法,既可以返回普通Selector的信号量,也可以像上例中返回代理方法的信号量,十分好用。</p>
<h3>RACSignal类簇</h3>
<p><code>RACSignal</code>的设计模式,就是标准的类簇模式,不同的Signal都是继承于RACSignal的子类,但是都封装起来不用开发者关注,和NSArray、UIButton的设计模式相同。</p>
<p>例如下面这个简单的返回定位状态授权的信号量,使用的<code>[RACSignal return:@(authorized)]</code>生成的便是<code>RACReturnSignal</code>类信号量。</p>
<pre><code>- (RACSignal *)authorized
{
BOOL authorized = [self authorizationStatusEqualTo:kCLAuthorizationStatusAuthorizedWhenInUse] || [self authorizationStatusEqualTo:kCLAuthorizationStatusAuthorizedAlways];
return [RACSignal return:@(authorized)];
}</code></pre>
<pre><code>+ (RACSignal *)return:(id)value {
return [RACReturnSignal return:value];
}</code></pre>
NSFetchedResultsController的简单封装 - UITableView与CoreData的完美结合
https://segmentfault.com/a/1190000004201188
2015-12-25T17:06:23+08:00
2015-12-25T17:06:23+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>NSFetchedResultsController的简单封装 - UITableView与CoreData的完美结合</h2>
<p>本文将简单分析NSFetchedResultsController这个控制器类的用法和开源控件<a href="https://link.segmentfault.com/?enc=dmzux2QYxPL%2B%2BJEdRtkq6w%3D%3D.8kVsxfrEwO2GpO4i2OidwceSsRD8HY2vVaCieM%2BRmgsNRKwVlKvDEawsElcm1TdF" rel="nofollow">DEFetchRC</a>的使用方法,文中用于讲解的代码片段,并不能直接粘贴使用,具体细节可以下载<a href="https://link.segmentfault.com/?enc=JH8aZT6RM7AIrQNtZtUq9A%3D%3D.ZCqtlksNz8TLDjAOzWvtaNEB0TwOdJ24FTXlD2MqA2fVx7dFqjxwyy%2Fo4nxKUBan" rel="nofollow">完整代码</a>。</p>
<p>如果您在阅读本文时,对CoreData还没有任何基础概念,这里给您推荐一套来自objcio.cn的<a href="https://link.segmentfault.com/?enc=fgU0LFqfqy6vZjAJuzY6iQ%3D%3D.X83pQuita%2FcssuNsY%2FTYLpxtzLO%2BH5l8V8Usre%2FVTjo%3D" rel="nofollow">CoreData系列教程</a>,相信能对您有所帮助。</p>
<h3>基本概念MVC与MVVC</h3>
<p>从传统意义的MVC到MVVC的转变,相信大家都厌烦了术语套话,这些设计模式概念用自然语言描述显得十分抽象,我们今天用Cocoa的类来简单描述一下这两种模式。</p>
<h4>传统MVC Model - View - Controller</h4>
<p>刚入门Cocoa时,我们接触到的最基本的类,就是UIViewController和UIView类,他们对应的MVC模型中的Controller和View,Model(数据模型)则可以是CoreData的数据,网络请求的数据,或者其他一些格式的数据,MVC的运作模式就是Controller将Model内容填入View,View响应交互让Controller处理数据并且修改View。</p>
<p><img src="/img/bVrPCd" alt="图片描述" title="图片描述"></p>
<h4>MVVC Model - View - Controller - ViewModel</h4>
<p>经过实战我们发现,Controller的代码量庞大不堪,一方面操作数据,一方面操作View,臃肿不堪,难以维护。所以MVVC模型应运而生,在MVC的基础上,加入了ViewModel概念,这个(些)ViewModel可以作为Controller的属性,剥离Controller中复杂的任务,例如:</p>
<ol>
<li><p>替Controller实现TableView和CollectionView的回调。</p></li>
<li><p>剥离数据库操作</p></li>
<li><p>剥离网络请求</p></li>
</ol>
<p>这样让Controller专注于处理View布局、交互和动画,把任务剥离,降低整体项目的耦合性。</p>
<h3>NSFetchedResultsController</h3>
<p>基于高度模块化和低耦合性的设计趋势,<code>NSFetchedResultsController</code>类应运而生。<br>你可能还不知道这个类可以完成什么工作,头文件中如此描述:</p>
<pre><code>This class is intended to efficiently manage the results returned from a Core Data fetch request.
这个类用于高效地响应CoreData数据变化</code></pre>
<p>本文的Demo代码见<a href="https://link.segmentfault.com/?enc=CNflR1BZ72GszSzT7zXC5A%3D%3D.mtlYulBdVmgOpYSj8bEsVAhLB%2FOt3KKQqUc0VHMhHYkiDUC7RVbN9V6UUjKOmwXT" rel="nofollow">zsy78191/DEFetchRC</a><br>初始化代码,这段代码摘自苹果官方模版,我们自建<code>DETableViewFetchRC</code>类用于绑定,这里在DETableViewFetchRC类中实现NSFetchedResultsController相关方法,使用的参数包括</p>
<ol>
<li><p>self.entityName 实体名称</p></li>
<li><p>self.managedContext CoreData的上下文</p></li>
<li><p>self.predicate 检索谓语</p></li>
<li><p>self.sortKey 排序键</p></li>
<li><p>self.ascending 排序升序或降序</p></li>
<li><p>_fetchController.delegate NSFetchedResultsControllerDelegate委托</p></li>
</ol>
<pre><code>@property (nonatomic, strong) NSFetchedResultsController* fetchController;
- (NSFetchedResultsController *)fetchController
{
if (_fetchController != nil) {
return _fetchController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:self.entityName inManagedObjectContext:self.managedContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
[fetchRequest setPredicate:self.predicate];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:self.sortKey ascending:self.ascending];
NSArray *sortDescriptors = @[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
_fetchController= [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedContext sectionNameKeyPath:nil cacheName:nil];
_fetchController.delegate = self;
NSError *error = nil;
if (![_fetchController performFetch:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
////LOGMARK(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return _fetchController;
}</code></pre>
<p>绑定好的NSFetchedResultsController通过KVO观察CoreData上下文变化,并通知我们修改View,具体实现如下:</p>
<h4>绑定tableView的DataSource</h4>
<pre><code>- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [[self.fetchController sections] count];
}</code></pre>
<p><strong>注:NSFetchedResultsController是支持TableView的Section功能的,我们在这个例子中国年并没有使用,读者可以根据需要进行修改。</strong></p>
<pre><code>- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchController sections][section];
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if([self checkDataSourceCanResponse:@selector(tableView:cellAtIndexPath:withData:)])
{
NSManagedObject *data = [self.fetchController objectAtIndexPath:indexPath];
//在这里处理Cell内容
}
return nil;
}</code></pre>
<h4>绑定增删改</h4>
<p>这里<code>NSFetchedResultsController</code>提供了NSFetchedResultsControllerDelegate协议,用于绑定Model和View,如下。</p>
<pre><code>#pragma mark - fetch
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationLeft];
break;
default:
return;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(NSManagedObject*)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
</code></pre>
<p>至此,NSFetchedResultsController的基本使用方法都介绍了,接下来介绍一下我做的一个对NSFetchedResultsController的简单封装,<a href="https://link.segmentfault.com/?enc=G6GsWGRM6DYSbpTZ6l3wKA%3D%3D.Z2PA6%2B6Kheoxde7W2rngVkaBlDgF%2Fv8nf0piRZTe39rdLp4ezZUUN2YGaMET6W%2BT" rel="nofollow">zsy78191/DEFetchRC</a>。</p>
<h2>DEFetchRC</h2>
<p>一个NSFetchedResultsController的简单封装</p>
<p>DETableViewFetchRC将自动绑定CoreData的ManageContext和TableView,绑定好以后,数据的任何变动,包括增删改查,都会自动调用对应Cell的更新,不需要自己写任何代码做通知挂钩等。</p>
<p>Demo中使用Apple的CoreData模版,CoreData的Context在AppDelegate中。</p>
<pre><code>- (NSManagedObjectContext*)managedObjectContext
{
AppDelegate* delegate = [UIApplication sharedApplication].delegate;
return delegate.managedObjectContext;
}</code></pre>
<h3>DETableViewFetchRC初始化</h3>
<pre><code>@property (nonatomic, strong) DETableViewFetchRC* tableFetchResultController;
- (void)viewDidLoad {
[super viewDidLoad];
[self.managedObjectContext setUndoManager:[[NSUndoManager alloc] init]];
self.tableFetchResultController = [DETableViewFetchRC fetchRCWithView:self.tableView
reciver:self
managedContext:self.managedObjectContext
entityName:@"Item"
sortKey:@"date"
ascending:YES
predicate:nil];
}</code></pre>
<p>其中第一参数输入需要绑定(bind)的 TableView(如果使用DECollectionViewFetchRC则传入CollectionView),第二个参数是DETableViewFetchDelegate的委托,主要用来实现Cell内容填充回调:</p>
<pre><code>- (UITableViewCell *)tableView:(UITableView *)tableView cellAtIndexPath:(NSIndexPath *)indexPath withData:(id)data
{
Item* item = data;
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
cell.textLabel.text = item.title;
cell.detailTextLabel.text = [item.date description];
return cell;
}</code></pre>
<p>这里的CoreData实例名字是Item,有title和date两个属性,分别为NSString和NSDate。</p>
<p>这样就完成了一步绑定,CoreData的Context有变动时自动更新调用Cell更新回调,更新Cell,增删改都自动完成。效果如下:</p>
<h3>最终效果</h3>
<p><img src="/img/bVrM4J" alt="DEFetchRCDemo.gif" title="DEFetchRCDemo.gif"></p>
<p>详细的代码请参考<a href="https://link.segmentfault.com/?enc=BNcerl4xarG%2Byva%2FXh0luQ%3D%3D.QgA2D3Masx1IUCunWzzt6JEJGEBnAE3WxgbBErpwvCN85uwn924Arr18lTCB4fHN" rel="nofollow">zsy78191/DEFetchRC</a></p>
<h3>总结</h3>
<p>本文作为<a href="http://segmentfault.com/a/1190000002498637">怎样降低iOS代码耦合性</a>一文的延伸阅读,介绍了<code>NSFetchedResultsController</code>类的基本使用方法,这个类作为CoreData的辅助控制器,在哲学高度强化了CoreData和TableView的绑定,原理上也适用于CoreData和CollectionView或其它第三方控件的绑定,灵活中不失稳重,值得细细研究一番。</p>
UITableView 编辑模式详解
https://segmentfault.com/a/1190000004192662
2015-12-24T10:56:02+08:00
2015-12-24T10:56:02+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
2
<h2>UITableView 编辑模式详解</h2>
<p><code>UITableView</code>的相关编辑操作非常全,今天我们来做一个总结。跟编辑相关的属性和接口有如下,我们一个一个分析,我们先认真阅读一下相关头文件,我根据意思大概翻译了一下注释。</p>
<h3>属性方法</h3>
<pre><code>@property (nonatomic, getter=isEditing) BOOL editing;
// 默认状态是非编辑状态,如果不调用下面接口直接设置,是没有动画的
- (void)setEditing:(BOOL)editing animated:(BOOL)animated;</code></pre>
<h3>DataSource</h3>
<pre><code>// 当增减按钮按下时,用来处理数据和UI的回调。
// 8.0版本后加入的UITableViewRowAction不在这个回调的控制范围内,UITableViewRowAction有单独的回调Block。
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath;
// 这个回调实现了以后,就会出现更换位置的按钮,回调本身用来处理更换位置后的数据交换。
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath;
// 这个回调决定了在当前indexPath的Cell是否可以编辑。
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath;
// 这个回调决定了在当前indexPath的Cell是否可以移动。
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath;</code></pre>
<h3>Delegate</h3>
<pre><code>// 这个回调很关键,返回Cell的编辑样式。
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath;
// 删除按钮的文字
- (nullable NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(3_0) __TVOS_PROHIBITED;
// 8.0后侧滑菜单的新接口,支持多个侧滑按钮。
- (nullable NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED;
// 这个接口决定编辑状态下的Cell是否需要缩进。
- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath;
// 这是两个状态回调
- (void)tableView:(UITableView*)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath __TVOS_PROHIBITED;
- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath __TVOS_PROHIBITED;</code></pre>
<h3>编辑状态</h3>
<p><code>UITableView</code>通过editing属性控制编辑状态,调用<code>- (void)setEditing:(BOOL)editing animated:(BOOL)animated</code>接口,可以决定是否使用原生的变换动画。</p>
<p>当调用这个接口,并将editing设为<code>YES</code>是,<code>UITableView</code>将开始询问代理(Delegate)需要编辑哪些Cell,用什么样的方式编辑。</p>
<p>首先调用回调方法<code>- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath;</code>,这里需要返回YES;</p>
<p>然后依次为各个Cell调用<code>- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath;</code>方法获取编辑样式。</p>
<pre><code>typedef NS_ENUM(NSInteger, UITableViewCellEditingStyle) {
UITableViewCellEditingStyleNone,
UITableViewCellEditingStyleDelete,
UITableViewCellEditingStyleInsert
};</code></pre>
<p>编辑样式枚举有三种,位运算组合则由不同的用途。</p>
<pre><code>UITableViewCellEditingStyleNone 没有编辑样式
UITableViewCellEditingStyleDelete 删除样式 (左边是红色减号)
UITableViewCellEditingStyleInsert 插入样式 (左边是绿色加号)
UITableViewCellEditingStyleDelete|UITableViewCellEditingStyleInsert 多选模式,左边是蓝色对号</code></pre>
<p>特别注意,右边的移动并不是这里控制的,需要实现下面这个回调才会出现。</p>
<pre><code>- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath;</code></pre>
<p>另外对于新手来说,要明白这里的回调都没有对UI和数据进行操作,开发者需要在回调中,完成相应的操作。比如删除或者添加一条数据,应在</p>
<pre><code>- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath;</code></pre>
<p>上面这个回调中,根据editingStyle进行判断,处理对应的UI和数据。</p>
<h3>8.0版本后的多选侧滑菜单</h3>
<p>8.0版本后,短信等原生应用都有了侧滑多按钮选择,原来是苹果的前端团队为TableView加入相关接口,这里给个例子</p>
<pre><code>- (NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath
{
return @[
[UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:NSLocalizedString(@"编辑", nil) handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
}],
[UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:NSLocalizedString(@"删除", nil) handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
}]
];
}
</code></pre>
<h3>数据与UI更新</h3>
<p>数据更新没什么好说的,直接操作数据容器就好,无论是数组、字典还是CoreData数据。UI更新则需要使用TableView的方法,如果需求reloadData无法满足,则必须使用下面的方法</p>
<pre><code>- (void)beginUpdates; // allow multiple insert/delete of rows and sections to be animated simultaneously. Nestable
- (void)endUpdates; // only call insert/delete/reload calls or change the editing state inside an update block. otherwise things like row count, etc. may be invalid.
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0);
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection NS_AVAILABLE_IOS(5_0);
- (void)insertRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
- (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0);
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath NS_AVAILABLE_IOS(5_0);</code></pre>
<p><code>beginUpdates</code>和<code>endUpdates</code>两个方法,在你需要批量处理Cell的时候,用来包裹住你的处理代码,其他方法名字都很直观,不一一介绍了。</p>
<p>最后给大家推荐一个Cocoa框架里的功能强大的类<code>NSFetchedResultsController</code>,用于绑定CoreData数据和<code>UITableView</code>或者<code>UICollectionView</code>,直接封装好所有的UI操作代码,只要数据有变动,UI自动更新,爽的不要不要的,妈妈再也不用担心我的TableView写不好了,下一篇文章我准备详细讲一讲这个有趣的类。</p>
<h3>4月22日更新</h3>
<p>遇到一个有意思的需求,就是编辑模式下,不让cell缩进,要怎么做呢?</p>
<pre><code>cell.shouldIndentWhileEditing = NO;</code></pre>
<p>或者</p>
<pre><code>- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath {
return NO;
}</code></pre>
<p>那一个有效呢?</p>
<p>注意官方注释</p>
<pre><code>// Controls whether the background is indented while editing. If not implemented, the default is YES. This is unrelated to the indentation level below. This method only applies to grouped style table views.
只对grouped的TableView有效。</code></pre>
iOS 本地化进阶教程
https://segmentfault.com/a/1190000004187941
2015-12-23T14:16:38+08:00
2015-12-23T14:16:38+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>iOS 本地化进阶教程</h2>
<p>在<a href="http://segmentfault.com/a/1190000004182437">上一篇</a>入门教程中,我们已经介绍了最基本的本地化方法了,接下来我们要来讲讲两种特殊的本地化情况。</p>
<h3>xib和storyboard本地化</h3>
<p><code>xib</code>和<code>storyboard</code>作为两种不同类型的UI工具,让我们得以摆脱代码布局之苦,他们的本地化,既可以使用上一篇文章中的方法通过代码修改,也可以直接对xib和storyboard文件进行本地化操作,操作类似strings文件的Localize操作,见下图</p>
<p><img src="/img/bVrJBX" alt="图片描述" title="图片描述"></p>
<p>选中文件后,点击右边栏<code>Localize</code>按钮,勾选对应语言。</p>
<p><img src="/img/bVrJB0" alt="图片描述" title="图片描述"></p>
<p>这样我们发现在Storyboard文件中,包含了Chinese版本的Strings文件,内容如下</p>
<pre><code>
/* Class = "UILabel"; text = "用户名"; ObjectID = "8f2-qK-7KS"; */
"8f2-qK-7KS.text" = "用户名";
/* Class = "UIButton"; normalTitle = "登录或注册"; ObjectID = "ENI-bN-wSs"; */
"ENI-bN-wSs.normalTitle" = "登录或注册";
/* Class = "UIButton"; normalTitle = "上一步"; ObjectID = "IGn-o3-iJc"; */
"IGn-o3-iJc.normalTitle" = "上一步";
/* Class = "UILabel"; text = "用户名"; ObjectID = "YHI-In-kPq"; */
"YHI-In-kPq.text" = "用户名";
/* Class = "UIButton"; normalTitle = "登录或注册"; ObjectID = "hzS-mR-s9E"; */
"hzS-mR-s9E.normalTitle" = "登录或注册";
/* Class = "UILabel"; text = "标题"; ObjectID = "rjb-sh-ADV"; */
"rjb-sh-ADV.text" = "标题";
/* Class = "UILabel"; text = "标题"; ObjectID = "wxc-43-QK7"; */
"wxc-43-QK7.text" = "标题";
/* Class = "UIButton"; normalTitle = "用户使用协议"; ObjectID = "xdy-gJ-fv1"; */
"xdy-gJ-fv1.normalTitle" = "用户使用协议";</code></pre>
<p>英文作为项目设置的基础语言,直接使用storyboard内容,中文的本地化,则由这个strings文件控制,是不是很简单。</p>
<h3>info.plist的本地化</h3>
<p>第二种特殊的本地化需求是对info.plist文件进行本地化。具体的最常见的使用场景有:</p>
<ol>
<li><p>应用名称本地化</p></li>
<li><p>定位请求提示语本地化</p></li>
</ol>
<p>我们知道系统默认的本地化文件名,叫做<code>Localizable.strings</code>,info.plist文件也有自己对应的本地化文件,叫做<code>InfoPlist.strings</code></p>
<p>这个文件需要开发者自己添加,我们添加好以后,在info.plist中加入`CFBundleDisplayName`键值,这个键值决定应用显示的名字。</p>
<p>接下来在InfoPlist.strings文件中写入本地化索引,注意<code>CFBundleDisplayName</code>前后没有引号,这个文件是专门用来本地化info.plist文件的。</p>
<pre><code>CFBundleDisplayName = "你的软件名称";
NSLocationAlwaysUsageDescription = "我需要使用手机的定位服务,请授权";</code></pre>
<p><code>NSLocationAlwaysUsageDescription</code>键值则是位置服务授权时候的提示语。如此一来就实现了info.plist文件本地化,是不是很简单?</p>
<p>我们的本地化教学就告一段落了,有其他问题请留言哦。</p>
iOS 本地化入门教程
https://segmentfault.com/a/1190000004182437
2015-12-22T15:07:32+08:00
2015-12-22T15:07:32+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>iOS 本地化入门教程</h2>
<p>iOS应用的本地化原理非常简单,是通过<code>strings</code>类型的文件,为同一种语言的不同翻译设置翻译表,应用再根据用户系统语言自动检索翻译表实现的。</p>
<p>首先普及一些基本概念给新人看。</p>
<h3>NSBundle</h3>
<p>在iOS应用中,bundle的概念非常重要,可以理解为一些文件的集合,bundle中可以嵌套bundle,外在 形式是.bundle格式的文件,同时编译生成的app包,也是bundle。</p>
<p>为此,苹果的工程师们留下了NSBundle这个类,用于操作bundle内的文件。而NSBundle类中的一些特定方法,则是给一些特定的文件实用的。</p>
<p>例如<code>infoDictionary</code>用于读取bundle中的info.plist文件。</p>
<pre><code>@property (nullable, readonly, copy) NSDictionary<NSString *, id> *infoDictionary;</code></pre>
<p>当然还有我们今天要讲的本地化方法</p>
<pre><code>/* Method for retrieving localized strings. */
- (NSString *)localizedStringForKey:(NSString *)key value:(nullable NSString *)value table:(nullable NSString *)tableName NS_FORMAT_ARGUMENT(1);</code></pre>
<p>这个方法就是用来读去bundle中本地化字段的方法。而在日常使用中,我们用的更多的是两个简化的宏:</p>
<pre><code>NSLocalizedString(<#key#>, <#comment#>)
NSLocalizedStringFromTable(<#key#>, <#tbl#>, <#comment#>)</code></pre>
<pre><code>宏定义
#define NSLocalizedString(key, comment) \
[[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]
#define NSLocalizedStringFromTable(key, tbl, comment) \
[[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \
[bundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \
[bundle localizedStringForKey:(key) value:(val) table:(tbl)]</code></pre>
<p><code>key</code>参数必填,<code>comment</code>作为注释参数,是不影响返回值的,可以传nil。</p>
<h3>默认本地化文件</h3>
<p>新工程中是不包含本地化文件的,如果要给应用添加本地化,首先需要添加<code>Localizable.strings</code>文件。</p>
<p><img src="/img/bVrH9C" alt="图片描述" title="图片描述"></p>
<p><code>Localizable.strings</code>文件就是本地化文件中的默认文件,名字不要打错哦,当你使用<code>NSLocalizedString(<#key#>, <#comment#>)</code>宏时,不用输入table名称,便是默认使用<code>Localizable.strings</code>文件作为翻译表。</p>
<p>如果你还有其他的翻译表文件,例如other.strings,你可以这样使用</p>
<pre><code>NSLocalizedStringFromTable(@"你好", @"other", nil)</code></pre>
<p>当然现在并不会将你好翻译成其他语言,下一步,你需要设置你的翻译表。</p>
<h3>Localizable.strings格式</h3>
<p>strings文件的格式非常简单,但如果格式有错,编译器会报错,并且不会提醒你到底哪里错了,所以务必要仔细。如下</p>
<pre><code>"你好" = "你好";</code></pre>
<ol>
<li><p>每一行一句,等号前面是key,等号后面是value,引号前不加<code>@</code>,分号结尾。</p></li>
<li><p>当添加多种语言后,同一个文件将会有不同语言的副本,存在工程目录下的不同本地化文件夹中,但在xcode中还是以同一个文件显示,但可以下拉切换语言进行编辑。</p></li>
</ol>
<p>添加本地化语言的方法如下,先选中<code>strings</code>文件,然后又边点击<code>Localize</code>按钮,然后选项中会有Base和English两个选项,确认后,这里会出现多选框,选取哪种语言,便会自动生成对应的副本。</p>
<p><img src="/img/bVrIaO" alt="图片描述" title="图片描述"></p>
<h4>添加其他语言</h4>
<p>如图选择Project的Info标签,设置Localization选项<br><img src="/img/bVrIa8" alt="图片描述" title="图片描述"></p>
<h3>实战</h3>
<p>这里我们添加了简体中文和日本本地化,对应的Localizable.strings文件多了两个副本。</p>
<p><img src="/img/bVrIbx" alt="图片描述" title="图片描述"></p>
<p>各个文件中的内容如下</p>
<pre><code>//日语Localizable.strings(Japanese)文件
"你好" = "こんにちは";
//英语Localizable.strings(English)文件
"你好" = "hello";
//简体中文Localizable.strings(Simplified)文件
"你好" = "你好";</code></pre>
<p>这样一来,当我们在代码中使用"你好"这个key时,系统便会自动根据我们的系统语言,进行本地化替换,当系统语言并非我们本地化支持的语言时,例如我们并未加入韩语,本地化接口便会把key的默认语言的本地化内容返回,或者根据设备的第二语言进行返回。当使用的key并不存在时,则会把key作为value返回。</p>
<h3>开源工具</h3>
<p>这里给大家分享一个我写的<a href="https://link.segmentfault.com/?enc=F1z1tPcSuAJ0xQqEoUEBug%3D%3D.4qkPRrw49neZZAtdAaY%2BcAY8JKULZhCjun2%2FrWCSj2rFQrQirRxrwDgBkKqy8nQA" rel="nofollow">开源项目</a>,可以自动给.m和.mm文件,生成对应的翻译表。<br>例如代码中使用本地化方法</p>
<pre><code>NSString* a = NSLocalizedString(@"这是一个测试", nil);
NSString* b = NSLocalizedString(@"第一条是普通本地化, 翻译写入Localizable.strings文件", nil);
NSString* c = NSLocalizedString(@"第二条是带注释的语句", @"这里是这条本地化语句的注释");
NSString* d = NSLocalizedStringFromTable(@"第三条是带表名的本地化语句,写在 XXX(表名).strings文件里", @"OtherFile",nil);
NSString* e = NSLocalizedStringFromTable( @"最后一条测试书写规范,空格,特殊符号(\"{@#$)等等" , @"OtherFile" , nil);</code></pre>
<p>只要将文件拖入SimpleLocalizedTool工具,便会自动生成对应的本地化文件。<br>上面的例子中,有使用默认的string文件语句,也有使用<code>OtherFile</code>这个文件名的语句,所以会生成两个文件,内容如下,直接拷贝到工程中吧,是不是很好用呀。</p>
<pre><code>//Localizable.strings
"这是一个测试" = "这是一个测试";
"第一条是普通本地化, 翻译写入Localizable.strings文件" = "第一条是普通本地化, 翻译写入Localizable.strings文件";
"第二条是带注释的语句" = "第二条是带注释的语句";
//OtherFile.strings
"第三条是带表名的本地化语句,写在 XXX(表名).strings文件里" = "第三条是带表名的本地化语句,写在 XXX(表名).strings文件里";
"最后一条测试书写规范,空格,特殊符号(\"{@#$)等等" = "最后一条测试书写规范,空格,特殊符号(\"{@#$)等等";</code></pre>
<p><img src="/img/bVrIc0" alt="图片描述" title="图片描述"></p>
iOS 系统授权开发
https://segmentfault.com/a/1190000004176855
2015-12-21T15:55:28+08:00
2015-12-21T15:55:28+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>iOS系统授权开发</h2>
<p>iOS系统开发中,最常用的系统授权,莫过于<code>系统通知</code>,<code>用户相册</code>,<code>位置服务</code>了,这篇文章将简单讲解这三项功能的开发,并附带我写的一个<a href="https://link.segmentfault.com/?enc=2R1acOSoYZ1CvGCRt76MNA%3D%3D.d48MvKUUdsoZusrg5Ewlys1mfHh0KW9p%2Fnoqa7q7O8QnHiGXF7RneV92O3NSz6LW3Z4B7I5Hx43UAUPJXvxRzA%3D%3D" rel="nofollow">开源项目</a>,统一管理系统授权。</p>
<p>注:本文和项目基于<code>iOS 8.0</code>及以上系统框架,低版本框架接口略有不同。</p>
<p><img src="/img/bVrGHs" alt="截图" title="截图"></p>
<h3>系统通知授权</h3>
<p>系统通知方法在UIApplication类方法中,其中使用<code>isRegisteredForRemoteNotifications</code>获取本地推送授权状态。</p>
<pre><code>+ (UIUserNotificationType)notificationType
{
UIApplication* application = [UIApplication sharedApplication];
return [application currentUserNotificationSettings].types;
}</code></pre>
<p>这里授权状态的枚举类型有</p>
<ol>
<li><p><code>UIUserNotificationTypeNone</code> 无授权</p></li>
<li><p><code>UIUserNotificationTypeBadge</code> 角标</p></li>
<li><p><code>UIUserNotificationTypeSound</code> 声音</p></li>
<li><p><code>UIUserNotificationTypeAlert</code> 通知</p></li>
</ol>
<p>原枚举如下</p>
<pre><code>typedef NS_OPTIONS(NSUInteger, UIUserNotificationType) {
UIUserNotificationTypeNone = 0, // the application may not present any UI upon a notification being received
UIUserNotificationTypeBadge = 1 << 0, // the application may badge its icon upon a notification being received
UIUserNotificationTypeSound = 1 << 1, // the application may play a sound upon a notification being received
UIUserNotificationTypeAlert = 1 << 2, // the application may display an alert upon a notification being received
} NS_ENUM_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED;</code></pre>
<p>授权方法</p>
<pre><code>UIUserNotificationType type = UIUserNotificationTypeBadge | UIUserNotificationTypeAlert | UIUserNotificationTypeSound;
UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes:type categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:setting];</code></pre>
<p>注意,每一项授权,一旦用户拒绝,必须前往<code>设置</code>的相关APP页面开启。APP内跳<code>设置</code>的方法是</p>
<pre><code>[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];</code></pre>
<p>注册本地通知也是有回调的,实现<code>UIApplicationDelegate</code>的<code>didRegisterUserNotificationSettings</code>方法。</p>
<pre><code>- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
}</code></pre>
<p>相应的也有失败的回调。</p>
<h3>系统相册授权</h3>
<p>8.0系统版本以后,框架中加入了<code>Photos.framework</code>框架,当然是用<code>UIImagePickerController</code>同样会提醒用户授权使用相册或相机,这里介绍一下<code>Photos</code>框架的授权。</p>
<p>相册权限状态</p>
<pre><code>+ (PHAuthorizationStatus)photoAccesStatus
{
return [PHPhotoLibrary authorizationStatus];
}</code></pre>
<pre><code>typedef NS_ENUM(NSInteger, PHAuthorizationStatus) {
PHAuthorizationStatusNotDetermined = 0, // User has not yet made a choice with regards to this application
PHAuthorizationStatusRestricted, // This application is not authorized to access photo data.
// The user cannot change this application’s status, possibly due to active restrictions
// such as parental controls being in place.
PHAuthorizationStatusDenied, // User has explicitly denied this application access to photos data.
PHAuthorizationStatusAuthorized // User has authorized this application to access photos data.
} NS_AVAILABLE_IOS(8_0);</code></pre>
<p>这里授权状态有四个状态</p>
<ol>
<li><p><code>PHAuthorizationStatusNotDetermined</code> 未授权</p></li>
<li><p><code>PHAuthorizationStatusRestricted</code> 授权中</p></li>
<li><p><code>PHAuthorizationStatusDenied</code> 拒绝</p></li>
<li><p><code>PHAuthorizationStatusAuthorized</code> 已授权</p></li>
</ol>
<p>授权Block方法</p>
<pre><code> [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
}];</code></pre>
<h3>位置服务授权</h3>
<p>位置服务授权稍微复杂一点点,8.0以后,进行位置服务授权要注意一点是,需要在工程的<code>Info.plist</code>文件中加入<code>NSLocationAlwaysUsageDescription</code>字段。字段中是开发者展示给用户的位置服务的使用场景介绍,或者是请求授权的描述。如果不添加这个字段,授权接口无任何反应。</p>
<p>状态接口</p>
<pre><code>+ (CLAuthorizationStatus)positionAuthorizationStatus
{
return [CLLocationManager authorizationStatus];
}</code></pre>
<p>授权方法</p>
<pre><code>+ (void)authorizedPosition:(CLLocationManager *)manager
{
[manager requestAlwaysAuthorization];
}</code></pre>
<p>注意这里传入的manager一定要是个<code>property</code>,如果是一个局部变量,大括号结束,释放掉了,授权就会消失,就会出现授权框一闪而过的现象。</p>
<p><a href="https://link.segmentfault.com/?enc=cn9AKxY%2FkCuQ6UBxAPIIrQ%3D%3D.jeFQFerZ1lj%2B6WIIuB51YqplEnNUKsGpf7SQwEy9LgOAamYDPte1KnqXjAc2XmlfJ0lREQknECegv1FIMrrP2A%3D%3D" rel="nofollow">开源项目 DeviceAccessViewController</a></p>
<h3>PermissionScope 项目</h3>
<p><code>PermissionScope</code>(<a href="https://link.segmentfault.com/?enc=G4Y5jUO3qvTAzHAJswJ3hg%3D%3D.C%2BxgZusbL68VQDBUXTJh5o5ZFeudUlqeoF%2FatyeEd1naxTdJ5t2Hi9oc4jC0pxVO" rel="nofollow">Github</a>)是一个超级屌,并且好用的开源控件,用来向用户申请系统授权。如果你有使用<code>cocospod</code>管理工具,这样加入<code>use_frameworks!</code>,因为<code>PermissionScope</code>是<code>swift</code>写的,需要编译成Framework才可以给ObjC用。</p>
<pre><code>use_frameworks!
pod 'PermissionScope'</code></pre>
<p>具体用法</p>
<pre><code>PermissionScope* scope = [[PermissionScope alloc] initWithBackgroundTapCancels:YES];
//[scope addPermission:[[CameraPermission alloc] init] message:@"请求使用您的相机"];
//[scope addPermission:[[BluetoothPermission alloc] init] message:@"请求使用您的蓝牙"];
//[scope addPermission:[[ContactsPermission alloc] init] message:@"请求使用您的通讯录"];
//[scope addPermission:[[EventsPermission alloc] init] message:@"请求使用您的日历"];
[scope addPermission:[[LocationAlwaysPermission alloc] init] message:@"请求使用您的位置"];
[scope addPermission:[[MicrophonePermission alloc] init] message:@"请求使用您的麦克风"];
[scope addPermission:[[NotificationsPermission alloc] initWithNotificationCategories:nil] message:@"请求使用通知服务"];
[scope show:^(BOOL s, NSArray<PermissionResult *> * _Nonnull results) {
} cancelled:^(NSArray<PermissionResult *> * _Nonnull canceled) {
}];</code></pre>
<p>这个例子很明了吧,但要注意几点</p>
<ol>
<li><p>最少要添加一个Permission</p></li>
<li><p>最多添加3个Permission</p></li>
<li><p>8.0以后,进行位置服务授权,需要在工程的<code>Info.plist</code>文件中加入<code>NSLocationAlwaysUsageDescription</code>字段。</p></li>
</ol>
iOS后台模式教程 (一)
https://segmentfault.com/a/1190000004155857
2015-12-16T17:27:43+08:00
2015-12-16T17:27:43+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>Background Modes Tutorial: Getting Started</h2>
<p>iOS后台模式教程 (一)</p>
<p><a href="https://link.segmentfault.com/?enc=2i4QQUH4bXtpgYcKVY2sAA%3D%3D.VJac3f55eE59%2BmYju0q0lxZNVdi%2BFJyprbcdWTT%2BTOzVsEJ%2FpNoJsmiXelDrT967RkVvkkFb6mjFv7s3CLet5A%3D%3D" rel="nofollow">原文</a></p>
<h3>使用场景</h3>
<p>在iOS7之前的系统中,当应用被挂起,拥有连续的10分钟时间来处理之前的任务,然后才会被系统终止。</p>
<p>所以,后台模式有一些特殊的使用场景。例如,更新位置,播放视频音频,和更新服务器请求。</p>
<h3>开始</h3>
<p>第一步设置工程中的<code>Capabilities</code>标签栏,打开<code>Background Modes</code>服务。</p>
<p>出现的<code>Modes</code>选项有</p>
<ul>
<li><p>Audio,AirPlay and Picture in Picture 视频音频播放</p></li>
<li><p>Location updates 位置更新</p></li>
<li><p>Voice over IP IP电话</p></li>
<li><p>Newsstand downloads 杂志下载</p></li>
<li><p>External accessory communication 外部附件通信,包括App Watch</p></li>
<li><p>Uses Bluetooth LE accessories 蓝牙LE配件</p></li>
<li><p>Acts as a Bluetooth LE accessory 作为蓝牙LE配件</p></li>
<li><p>Background fetch 后台抓取服务</p></li>
<li><p>Remote notifications 远程通知</p></li>
</ul>
<p>这里介绍几个模式的用法。</p>
<h3>播放音频</h3>
<p>下面这段代码加入viewDidLoad中,程序开始时会按顺序播放两个mp3文件。在勾选<code>Audio,AirPlay and Picture in Picture</code>后,挂起程序时,音乐还是会继续播放 。</p>
<p>引用</p>
<pre><code>#import <AVFoundation/AVFoundation.h></code></pre>
<p>参数</p>
<pre><code>@property (nonatomic, strong) AVQueuePlayer *player;</code></pre>
<p>viewDidLoad</p>
<pre><code> NSError *sessionError = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];
NSArray *queue = @[
[AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"music" withExtension:@"mp3"]],
[AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"pop" withExtension:@"mp3"]]];
self.player = [[AVQueuePlayer alloc] initWithItems:queue];
self.player.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;
[self.player play];</code></pre>
<h3>位置服务</h3>
<p>参数</p>
<pre><code>@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSMutableArray *locations;</code></pre>
<p>初始化LocationManager</p>
<pre><code>self.locations = [[NSMutableArray alloc] init];
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.delegate = self;</code></pre>
<p>启动位置更新服务</p>
<pre><code>[self.locationManager startUpdatingLocation];</code></pre>
<p>记录新位置</p>
<pre><code>- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation
{
// Add another annotation to the map.
if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
{
//前台运行
else
{
//后台运行
NSLog(@"App is backgrounded. New location is %@", newLocation);
}
}</code></pre>
<h4>代码解析</h4>
<p>根据 <code>UIApplication.sharedApplication.applicationState == UIApplicationStateActive</code> 可以判断回调过程中,程序是否挂起。记住要勾选<code>Location updates </code>,当你在制作一个跑步或骑车软件时,需要用到这项功能。</p>
<h3>一般性有限长度任务</h3>
<p>这个宽泛的功能包括上传或者下载任务等。</p>
<pre><code>@property (nonatomic) UIBackgroundTaskIdentifier backgroundTask;</code></pre>
<pre><code>self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
// NSLog(@"Background handler called. Not running background tasks anymore.");
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
self.backgroundTask = UIBackgroundTaskInvalid;
}];</code></pre>
<p>您也可以在任务完成的时候,主动调用<code> [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];</code> 终止任务。<br>程序会在后台运行,但并不是无限时间。</p>
CloudKit 入门贴
https://segmentfault.com/a/1190000004153558
2015-12-16T11:34:59+08:00
2015-12-16T11:34:59+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>本文技术内容转自<a href="https://link.segmentfault.com/?enc=bG6fZIKua6wSSIl81QHUqA%3D%3D.zuRPPkheMgEHFI5F7YWOdX5V7z46UPo3zf76ExxPOLM%3D" rel="nofollow">原帖</a>
</h2>
<p>CloudKit 的基础对象类型有 7 种。这些对象类型可能和你在其他编程领域了解的类似对象类型稍有差别。</p>
<h3>CKContainer</h3>
<p><code>CKContainer</code>: Containers 就像应用运行的沙盒一样,一个应用只能访问自己沙盒中的内容而不能访问其他应用的。Containers 就是最外层容器,每个应用有且仅有一个属于自己的 container。事实上,经过开发者授权配置 <code>CloudKit Dashboard</code> 之后,一个应用也可以访问其他应用的 container。这里和<code>App Group</code>相同,和<code>iCloud Documents</code>也是一样的,允许不同App访问同一个存储空间。</p>
<h4>属性和方法</h4>
<pre><code>+ (CKContainer *)defaultContainer; //默认容器
+ (CKContainer *)containerWithIdentifier:(NSString *)containerIdentifier; //根据identifier获取容器,同一个App有可能有多个iCloud容器。
- (void)addOperation:(CKOperation *)operation; //CKOperation 是CKDatabase操作的基类,继承他,重写main方法。
//私有云数据库
@property (nonatomic, readonly) CKDatabase *privateCloudDatabase;
//公有云数据库
@property (nonatomic, readonly) CKDatabase *publicCloudDatabase;
typedef NS_ENUM(NSInteger, CKAccountStatus) {
/* 无法获取账户*/
CKAccountStatusCouldNotDetermine = 0,
/* 账户可用 */
CKAccountStatusAvailable = 1,
/* 权限禁止使用 */
CKAccountStatusRestricted = 2,
/* 没有登陆iCloud账户 */
CKAccountStatusNoAccount = 3,
} NS_ENUM_AVAILABLE(10_10, 8_0);
//账户更换通知
CK_EXTERN NSString * const CKAccountChangedNotification NS_AVAILABLE(10_11, 9_0);
//获取账户状态,账户通过授权可以让知道你邮箱的人,知道你正在使用这个应用。
- (void)accountStatusWithCompletionHandler:(void (^)(CKAccountStatus accountStatus, NSError * __nullable error))completionHandler;</code></pre>
<p>一个基本使用范例:</p>
<pre><code>[[CKContainer defaultContainer] accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError * _Nullable error) {
void (^requestPremission)(void) = ^{
[[CKContainer defaultContainer] requestApplicationPermission:CKApplicationPermissionUserDiscoverability completionHandler:^(CKApplicationPermissionStatus applicationPermissionStatus, NSError * _Nullable error) {
switch (applicationPermissionStatus) {
case CKApplicationPermissionStatusCouldNotComplete:
{
//无法完成
NSLog(@"CKApplicationPermissionStatusCouldNotComplete");
break;
}
case CKApplicationPermissionStatusDenied:
{
//拒绝
NSLog(@"CKApplicationPermissionStatusDenied");
break;
}
case CKApplicationPermissionStatusGranted:
{
//授权通过
NSLog(@"CKApplicationPermissionStatusGranted");
break;
}
case CKApplicationPermissionStatusInitialState:
{
break;
}
default:
break;
}
}];
};
void (^signPremission)(void) = ^{
[[CKContainer defaultContainer] statusForApplicationPermission:CKApplicationPermissionUserDiscoverability completionHandler:^(CKApplicationPermissionStatus applicationPermissionStatus, NSError * _Nullable error) {
switch (applicationPermissionStatus) {
case CKApplicationPermissionStatusInitialState:
{
requestPremission();
break;
}
case CKApplicationPermissionStatusGranted:
{
//授权过了
break;
}
case CKApplicationPermissionStatusDenied:
{
//已经拒绝
break;
}
case CKApplicationPermissionStatusCouldNotComplete:
{
//无法获取状态
break;
}
default:
break;
}
}];
};
switch (accountStatus) {
case CKAccountStatusAvailable:
{
//账户可用
signPremission();
break;
}
case CKAccountStatusCouldNotDetermine:
{
//无法获取账户信息
break;
}
case CKAccountStatusNoAccount:
{
//当前无登录账户
break;
}
case CKAccountStatusRestricted:
{
//账户被禁用此功能
break;
}
default:
break;
}
}];</code></pre>
<h4>区别</h4>
<p><code>CloudKit</code>和<code>iCloud Documents</code>的区别在于,前者是存储在苹果服务器中的,后者则是存储在用户设备的特定区域中。</p>
<h3>CKDatabase</h3>
<p><code>CKDatabase</code>: Database 即数据库,私有数据库用来存储敏感信息,比如说用户的性别年龄等,用户只能访问自己的私有数据库。应用也有一个公开的数据库来存储公共信息,例如你在构建一个根据地理位置签到的应用,那么地理位置信息就应该存储在公共数据库里以便所有用户都能访问到。</p>
<h3>CKRecord</h3>
<p><code>CKRecord</code>: 即数据库中的一条数据记录。CloudKit 使用 record 通过 k/v 结构来存储结构化数据。关于键值存储,目前值的架构支持 NSString、NSNumber、NSData、NSDate、CLLocation,和 CKReference、CKAsset(这两个下面我们会说明),以及存储以上数据类型的数组。</p>
<h4>增</h4>
<pre><code> CKRecord *postRecrod = [[CKRecord alloc] initWithRecordType:@"item"];
postRecrod[@"password"] = @"asdasdasdasd";
CKDatabase *publicDatabase = [[CKContainer defaultContainer] privateCloudDatabase];
[publicDatabase saveRecord:postRecrod completionHandler:^(CKRecord *record, NSError *error) {
if(error) {
NSLog(@"%@", error);
} else {
NSLog(@"Saved successfully");
}
}];</code></pre>
<p>查</p>
<pre><code> CKContainer *defaultContainer = [CKContainer defaultContainer];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"TRUEPREDICATE"];
CKDatabase *publicDatabase = [defaultContainer privateCloudDatabase];
CKQuery *query = [[CKQuery alloc] initWithRecordType:@"item" predicate:predicate];
[publicDatabase performQuery:query inZoneWithID:nil completionHandler:^(NSArray *results, NSError *error) {
if (!error) {
NSLog(@"%@", results);
} else {
NSLog(@"%@", error);
}
}];</code></pre>
<p>改</p>
<pre><code> CKContainer *defaultContainer = [CKContainer defaultContainer];
CKDatabase *publicDatabase = [defaultContainer publicCloudDatabase];
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:@"1"];
[publicDatabase fetchRecordWithID:recordID completionHandler:^(CKRecord *record, NSError *error) {
if(error) {
NSLog(@"%@", error);
} else {
record[@"postText"] = @"123 Beggers Canyon, Tatooine";;
[publicDatabase saveRecord:record completionHandler:^(CKRecord *record, NSError *error) {
if(error) {
NSLog(@"Uh oh, there was an error updating ... %@", error);
} else {
NSLog(@"Updated record successfully");
}
}];
}
}];</code></pre>
<p>删</p>
<pre><code> CKContainer *defaultContainer = [CKContainer defaultContainer];
CKDatabase *publicDatabase = [defaultContainer publicCloudDatabase];
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:@"1"];
[publicDatabase deleteRecordWithID:recordID completionHandler:^(CKRecordID *recordID, NSError *error) {
if(error) {
NSLog(@"%@", error);
} else {
NSLog(@"Deleted record successfully")
}
}];</code></pre>
<p><em>特别注意,如果出现未知错误,请进入iCloud Dashboard调试</em></p>
<h3>CKRecordZone</h3>
<p><code>CKRecordZone</code>: Record 不是以零散的方式存在于 database 之中的,它们位于 record zones 里。每个应用都有一个 default record zone,你也可以有自定义的 record zone。</p>
<h3>CKRecordIdentifier</h3>
<p><code>CKRecordIdentifier</code>: 是一条 record 的唯一标识,用于确定该 record 在数据库中的唯一位置。</p>
<h3>CKReference</h3>
<p><code>CKReference</code>: Reference 很像 RDBMS 中的引用关系。还是以地理位置签到应用为例,每个地理位置可以包含很多用户在该位置的签到,那么位置与签到之间就形成了这样一种包含式的从属关系。</p>
<h3>CKAsset</h3>
<p><code>CKAsset</code>: 即资源文件,例如二进制文件。还是以签到应用为例,用户签到时可能还包含一张照片,那么这张照片就会以 asset 形式存储起来。</p>
SwizzleMethod 黑魔法
https://segmentfault.com/a/1190000004148124
2015-12-15T11:27:54+08:00
2015-12-15T11:27:54+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<p>首先,请加入runtime头文件 #import <objc/runtime.h></p>
<pre><code> void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// the method might not exist in the class, but in its superclass
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// the method doesn’t exist and we just added one
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}</code></pre>
<p>上面这个C风格函数,就是SwizzleMethod的核心方法,用来交换Runtime中类和对象的方法接口指针。但是这有什么用呢?</p>
<h2>你知道有名的第三方库IQKeyboard么?</h2>
<p>这个吊库,不需要引入头文件,不需要调用任何方法就能使用。怎么做到的呢?<br>答案是NSObject的 <code>+ (void)load</code>方法。</p>
<p>这个类方法,在软件运行时一定会调用一次,并且不需要调用super方法,因为父类的load方法也一定会调用。</p>
<p>IQKeyboard就是在load方法中初始化的。</p>
<h2>SwizzleMethod应用实例 —— 无痛手术</h2>
<p>这个比喻并不准确,准确说应该是无痕手术 —— 对方法的无痕手术<br>首先给AppDelegate添加Category,可以放在新文件中哦,比如AppDelegate+Extension.h & m。</p>
<pre><code>+ (void)load
{
swizzleMethod([AppDelegate class], @selector(application:didFinishLaunchingWithOptions:), @selector(swizzle_application:didFinishLaunchingWithOptions:));
}</code></pre>
<p>这里,我们把AppDelegate的启动方法更换成了我们自己的swizzle_application:didFinishLaunchingWithOptions方法。两个方法指针互换,然后我们在我们的方法中加入我们需要的代码。</p>
<pre><code>- (BOOL)swizzle_application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//我们需要添加的代码
//
return [self swizzle_application:application didFinishLaunchingWithOptions:launchOptions];
}</code></pre>
<p>注意到了么,结尾我们自己调用<code>swizzle_application:application</code>方法,因为这个这个方法指针实际已经指向<code>AppDelegate</code>的<code>application:didFinishLaunchingWithOptions</code>方法。其他地方掉用<code>AppDelegate</code>的<code>application:didFinishLaunchingWithOptions</code>方法则会指向我们的<code>swizzle_application:application</code>方法,这样我们就在人不知不觉中,向<code>AppDelegate</code>插入了一段代码,这一切不需要AppDelegate引入任何头文件,是不是很Cool?</p>
<p>这样一来就可以把需要放在这里面的各种监测代码初始化,都放到我们的<code>swizzle_application:application</code>方法中,可以给这个方法新建一个类,每次新建工程直接拖进来一起编译,分分钟植入,帅爆一切,点个赞吧。</p>
Xcode-Snippets/Objective-C 学习
https://segmentfault.com/a/1190000003014669
2015-07-24T10:40:12+08:00
2015-07-24T10:40:12+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
1
<h2>前言</h2>
<p><code>Xcode-Snippets</code>是github上的一堆开源代码。作者mattt分享了他的Xcode-Snippets(xcode代码片段),今天我们来学习一下。</p>
<h3>片段</h3>
<h4>singleton.m</h4>
<pre><code>+ (instancetype)shared<#name#> {
static <#class#> *_shared<#name#> = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_shared<#name#> = <#initializer#>;
});
return _shared<#name#>;
}
</code></pre>
<p>单例,标准代码。</p>
<h4>stack.m</h4>
<pre><code>NSLog(@"Call Stack: %@", [NSThread callStackSymbols]);
</code></pre>
<p>这个有点意思,输出线程的调用栈?不知道是不是真么翻译。输出如下:</p>
<pre><code>Call Stack: (
0 Countdown 0x00000001000a73e4 -[AppDelegate application:didFinishLaunchingWithOptions:] + 120
1 UIKit 0x0000000186aeb124 <redacted> + 404
2 UIKit 0x0000000186d027e8 <redacted> + 2376
3 UIKit 0x0000000186d0519c <redacted> + 1504
4 UIKit 0x0000000186d0370c <redacted> + 184
5 FrontBoardServices 0x000000018a83d3c8 <redacted> + 32
6 CoreFoundation 0x0000000181fb827c <redacted> + 20
7 CoreFoundation 0x0000000181fb7384 <redacted> + 312
8 CoreFoundation 0x0000000181fb59a8 <redacted> + 1756
9 CoreFoundation 0x0000000181ee12d4 CFRunLoopRunSpecific + 396
10 UIKit 0x0000000186ae43d0 <redacted> + 552
11 UIKit 0x0000000186adef40 UIApplicationMain + 1488
12 Countdown 0x000000010013177c main + 124
13 libdyld.dylib 0x0000000194326a08 <redacted> + 4
)
</code></pre>
<h4>strongself.m && weakself.m</h4>
<pre><code>__strong __typeof(<#weakSelf#>)strongSelf = <#weakSelf#>;
__weak typeof(self)weakSelf = self;
</code></pre>
<p>这个没啥好说的,用于防治block中循环引用。</p>
<h4>tu.m</h4>
<p>醉了</p>
<pre><code>UIControlEventTouchUpInside
</code></pre>
<h4>tvdel.m && tvds.m</h4>
<p>这两个也醉了,不过蛮实用</p>
<pre><code>#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
<#statements#>
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return <#number#>;
}
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return <#number#>;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:<#reuseIdentifier#> forIndexPath:<#indexPath#>];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}
- (void)configureCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath
{
<#statements#>
}
</code></pre>
<h4>xae.m xaf.m xan.m xann.m xat.m</h4>
<p>断言调试宏,分别对应<br>
e-Equel<br>
f-False<br>
n-Nil<br>
nn-NotNil<br>
t-True</p>
<h4>async.m</h4>
<p>GCD的异步等待嵌套。</p>
<pre><code>dispatch_async(dispatch_get_global_queue(<#dispatch_queue_priority_t priority#>, <#unsigned long flags#>), ^(void) {
<#code#>
dispatch_async(dispatch_get_main_queue(), ^(void) {
<#code#>
});
});
</code></pre>
<h4>cdfetch.m</h4>
<p>CoreData的fetch代码,其实官方自带的。</p>
<pre><code>NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:<#entityName#>];
fetchRequest.predicate = [NSPredicate predicateWithFormat:<#predicateFormat#>];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:<#key#> ascending:<#isAscending#> selector:<#selector#>];
fetchRequest.sortDescriptors = @[sortDescriptor];
NSError *error;
NSArray *results = [<#context#> executeFetchRequest:fetchRequest error:&error];
if (error) {
NSLog(@"%@", error);
}
</code></pre>
<p>官方版本</p>
<pre><code>NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"<#Entity name#>" inManagedObjectContext:<#context#>];
[fetchRequest setEntity:entity];
// Specify criteria for filtering which objects to fetch
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"<#format string#>", <#arguments#>];
[fetchRequest setPredicate:predicate];
// Specify how the fetched objects should be sorted
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"<#key#>"
ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:sortDescriptor, nil]];
NSError *error = nil;
NSArray *fetchedObjects = [<#context#> executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
<#Error handling code#>
}
</code></pre>
<h4>checkerror.m</h4>
<p>注释说用于把OSStatus error翻译成人类能看懂的语言。</p>
<pre><code>static void CheckError(OSStatus error, const char *operation) {
if (error == noErr) {
return;
}
char str[20];
*(UInt32 *) (str + 1) = CFSwapInt32HostToBig(error);
if (isprint(str[1]) && isprint(str[2]) && isprint(str[3]) && isprint(str[4])) {
str[0] = str[5] = '\'';
str[6] = '\0';
} else {
sprintf(str, "%d", (int)error);
}
fprintf(stderr, "[Error] %s (%s)\n", operation, str);
exit(1);
}
</code></pre>
<h4>continuation.m</h4>
<p>快速在.m文件添加匿名Category</p>
<pre><code>@interface <#Class Name#> ()
<#Continuation#>
@end
</code></pre>
<h4>cvds.m</h4>
<p>尿性</p>
<pre><code>#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section
{
return <#numberOfItemsInSection#>;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:<#reuseIdentifier#> forIndexPath:indexPath];
[self configureCell:cell forItemAtIndexPath:indexPath];
return cell;
}
- (void)configureCell:(UICollectionViewCell *)cell
forItemAtIndexPath:(NSIndexPath *)indexPath
{
<# statements #>
}
</code></pre>
<h4>documents.m</h4>
<p>取App下Documents文件夹路径,还算实用</p>
<pre><code>NSURL *documentsDirectoryURL = [NSURL fileURLWithPath:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]];
</code></pre>
<h4>frame.m</h4>
<p>这个比较6。用语法糖实现,66666</p>
<pre><code><# view #>.frame = ({
CGRect frame = <# view #>.frame;
<# ... #>
frame;
});
</code></pre>
<h4>frc.m</h4>
<p><code>NSFetchedResultsController</code>是个好东西。</p>
<pre><code><br>NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:<#(NSString *)#>];
fetchRequest.predicate = [NSPredicate predicateWithFormat:<#(NSString *), ...#>];
fetchRequest.sortDescriptors = @[<#(NSSortDescriptor *), ...#>];
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:<#(NSFetchRequest *)#> managedObjectContext:<#(NSManagedObjectContext *)#> sectionNameKeyPath:<#(NSString *)#> cacheName:<#(NSString *)#>];
fetchedResultsController.delegate = <#(id <NSFetchedResultsControllerDelegate>)#>;
NSError *error = nil;
if (![fetchedResultsController performFetch:&error]) {
NSLog(@"Error: %@", error);
}
</code></pre>
<h4>frcd.m</h4>
<p><code>NSFetchedResultsController</code>的一堆回调</p>
<h4>imv.m</h4>
<p>这..</p>
<pre><code>[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"<#image name#>"]]
</code></pre>
<h4>init.m</h4>
<p>顾名思义了,不过是强迫症写法</p>
<pre><code>self = [super init];
if (!self) {
return nil;
}
<#initializations#>
return self;
</code></pre>
<h4>library.m</h4>
<p>Library路径</p>
<pre><code>[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
</code></pre>
<h4>lifecycle.m</h4>
<p>最实用,没有之一</p>
<pre><code>#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
</code></pre>
<h4>mailcomp.m</h4>
<p>调用系统邮件UI</p>
<pre><code>#import <MessageUI/MessageUI.h>
- (void)presentModalMailComposerViewController:(BOOL)animated {
if ([MFMailComposeViewController canSendMail]) {
MFMailComposeViewController *composeViewController = [[MFMailComposeViewController alloc] init];
composeViewController.mailComposeDelegate = self;
[composeViewController setSubject:<#Subject#>];
[composeViewController setMessageBody:<#Body#> isHTML:YES];
[composeViewController setToRecipients:@[<#Recipients#>]];
[self presentViewController:composeViewController animated:animated completion:nil];
} else {
[[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Error", nil) message:NSLocalizedString(@"<#Cannot Send Mail Message#>", nil) delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", nil) otherButtonTitles:nil] show];
}
}
#pragma mark - MFMailComposeViewControllerDelegate
- (void)mailComposeController:(MFMailComposeViewController *)controller
didFinishWithResult:(MFMailComposeResult)result
error:(NSError *)error
{
if (error) {
NSLog(@"%@", error);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
</code></pre>
<h4>mark.m</h4>
<p>短,然并卵</p>
<pre><code>#pragma mark - <#Section#>
</code></pre>
<p>跟直接输入有什么区别。</p>
<h4>nscoding.m</h4>
<p>NSCoding协议实现相关方法</p>
<pre><code>#pragma mark - NSCoding
- (instancetype)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (!self) {
return nil;
}
<# implementation #>
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
<# implementation #>
}
</code></pre>
<h4>nsl.m</h4>
<p>短,然并卵2,本地化字符串</p>
<pre><code>NSLocalizedString(@"<#Message#>", <#Comment#>)
</code></pre>
<h4>pdel.m && pds.m</h4>
<p>UIPickerView的回调</p>
<pre><code>#pragma mark - UIPickerViewDelegate
- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component
{
<#code#>
}
- (void)pickerView:(UIPickerView *)pickerView
didSelectRow:(NSInteger)row
inComponent:(NSInteger)component
{
<#code#>
}
#pragma mark - UIPickerDataSource
- (NSInteger)pickerView:(UIPickerView *)pickerView
numberOfRowsInComponent:(NSInteger)component
{
return <#number#>
}
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return <#number#>
}
</code></pre>
iOS Touch ID 简易开发教程
https://segmentfault.com/a/1190000002516465
2015-01-27T17:18:47+08:00
2015-01-27T17:18:47+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
4
<h2>基础知识</h2>
<h3>支持系统和机型</h3>
<p>iOS系统的指纹识别功能最低支持的机型为<code>iPhone 5s</code>,最低支持系统为<code>iOS 8</code>,虽然安装<code>iOS 7</code>系统的5s机型可以使用系统提供的指纹解锁功能,但由于<code>API</code>并未开放,所以理论上第三方软件不可使用。</p>
<h3>依赖框架</h3>
<p><code>LocalAuthentication.framework</code></p>
<pre><code class="objc">#import <LocalAuthentication/LocalAuthentication.h></code></pre>
<h3>注意事项</h3>
<p>做<code>iOS 8</code>以下版本适配时,务必进行API验证,避免调用相关API引起崩溃。</p>
<h3>使用类</h3>
<p><code>LAContext</code> 指纹验证操作对象</p>
<h2>代码</h2>
<pre><code class="objc">- (void)authenticateUser
{
//初始化上下文对象
LAContext* context = [[LAContext alloc] init];
//错误对象
NSError* error = nil;
NSString* result = @"Authentication is needed to access your notes.";
//首先使用canEvaluatePolicy 判断设备支持状态
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
//支持指纹验证
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:result reply:^(BOOL success, NSError *error) {
if (success) {
//验证成功,主线程处理UI
}
else
{
NSLog(@"%@",error.localizedDescription);
switch (error.code) {
case LAErrorSystemCancel:
{
NSLog(@"Authentication was cancelled by the system");
//切换到其他APP,系统取消验证Touch ID
break;
}
case LAErrorUserCancel:
{
NSLog(@"Authentication was cancelled by the user");
//用户取消验证Touch ID
break;
}
case LAErrorUserFallback:
{
NSLog(@"User selected to enter custom password");
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//用户选择输入密码,切换主线程处理
}];
break;
}
default:
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//其他情况,切换主线程处理
}];
break;
}
}
}
}];
}
else
{
//不支持指纹识别,LOG出错误详情
switch (error.code) {
case LAErrorTouchIDNotEnrolled:
{
NSLog(@"TouchID is not enrolled");
break;
}
case LAErrorPasscodeNotSet:
{
NSLog(@"A passcode has not been set");
break;
}
default:
{
NSLog(@"TouchID not available");
break;
}
}
NSLog(@"%@",error.localizedDescription);
}
}
</code></pre>
<pre><code class="objc">typedef NS_ENUM(NSInteger, LAError)
{
//授权失败
LAErrorAuthenticationFailed = kLAErrorAuthenticationFailed,
//用户取消Touch ID授权
LAErrorUserCancel = kLAErrorUserCancel,
//用户选择输入密码
LAErrorUserFallback = kLAErrorUserFallback,
//系统取消授权(例如其他APP切入)
LAErrorSystemCancel = kLAErrorSystemCancel,
//系统未设置密码
LAErrorPasscodeNotSet = kLAErrorPasscodeNotSet,
//设备Touch ID不可用,例如未打开
LAErrorTouchIDNotAvailable = kLAErrorTouchIDNotAvailable,
//设备Touch ID不可用,用户未录入
LAErrorTouchIDNotEnrolled = kLAErrorTouchIDNotEnrolled,
} NS_ENUM_AVAILABLE(10_10, 8_0);</code></pre>
<h3>操作流程</h3>
<p>首先判断系统版本,<code>iOS 8</code>及以上版本执行<code>-(void)authenticateUser</code>方法,方法自动判断设备是否支持和开启<code>Touch ID</code>。</p>
<h3>iOS 9</h3>
<p>感谢<a href="http://segmentfault.com/u/mohuifen">秋儿</a>指出iOS 9加入了三种新的错误类型。</p>
<pre><code> /// Authentication was not successful, because there were too many failed Touch ID attempts and
/// Touch ID is now locked. Passcode is required to unlock Touch ID, e.g. evaluating
/// LAPolicyDeviceOwnerAuthenticationWithBiometrics will ask for passcode as a prerequisite.
LAErrorTouchIDLockout NS_ENUM_AVAILABLE(10_11, 9_0) = kLAErrorTouchIDLockout,
/// Authentication was canceled by application (e.g. invalidate was called while
/// authentication was in progress).
LAErrorAppCancel NS_ENUM_AVAILABLE(10_11, 9_0) = kLAErrorAppCancel,
/// LAContext passed to this call has been previously invalidated.
LAErrorInvalidContext NS_ENUM_AVAILABLE(10_11, 9_0) = kLAErrorInvalidContext</code></pre>
<p>其中,<code>LAErrorTouchIDLockout</code>是在8.0中也会出现的情况,但并未归为单独的错误类型,这个错误出现,源自用户多次连续使用Touch ID失败,Touch ID被锁,需要用户输入密码解锁,这个错误的交互<code>LocalAuthentication.framework</code>已经封装好了,不需要开发者关心。</p>
<p><code>LAErrorAppCancel</code>和<code>LAErrorSystemCancel</code>相似,都是当前软件被挂起取消了授权,但是前者是用户不能控制的挂起,例如突然来了电话,电话应用进入前台,APP被挂起。后者是用户自己切到了别的应用,例如按home键挂起。</p>
<p><code>LAErrorInvalidContext</code>很好理解,就是授权过程中,LAContext对象被释放掉了,造成的授权失败。</p>
怎样降低iOS代码耦合性
https://segmentfault.com/a/1190000002498637
2015-01-20T11:12:47+08:00
2015-01-20T11:12:47+08:00
秋刀生鱼片
https://segmentfault.com/u/xiaochao_itoytoy
4
<h2>综述</h2>
<p>凡是维护过中型项目的iOS工程师都应该有过类似的体验:<code>ViewController</code>代码繁重、功能复杂、维护困难,整个工程寥寥几个<code>ViewController</code>就完成了整个项目的开发。每个控制器中都囊括了所有的页面布局、委托代理、网络请求、数据库操作和核心功能,这样的代码往往问题重重,修改起来牵一发而动全身,着实令人头疼。</p>
<p>为了应对这一系列的问题,苹果公司的工程师给我们提供了很多选择去更好的在项目工程中贯彻<code>MVC</code>的设计理念,例如使用从前的<code>Interface Builder</code>制作<code>xib</code>可视布局,现在已经内置到<code>xcode</code>里面,并且提供了更为强大<code>Storyboard</code>功能,来减少<code>控制器</code>中的页面样式布局代码量;再例如<code>NSFetchedResultsController</code>这样的类和<code>CoreData</code>he<code>UITableViewController</code>的完美结合,大大减少类似构架项目的代码量,并且稳定高效。</p>
<p>这些技巧在<a rel="nofollow" href="http://objc.io">objc.io</a>上有一个专门的专题,推荐给大家对应中文站<a rel="nofollow" href="http://objcio.cn">objc中国</a>,感谢objc 中国项目组。</p>
<h2>Storyboard与代码耦合性</h2>
<p>如果放在两年前去讨论iOS工程要不要使用<code>Stortboard</code>进行布局,我们可能还会犹豫一下,很多iOS程序猿内心会有一种想把一切化为代码掌控在手中的想法,选择拒绝使用<code>Storyboard</code>或者更早的<code>xib</code>。但事到如今,iPhone、iPad的屏幕尺寸越来越多,工程里为了适配不同屏幕冗余代码越来越长的时候,<code>Storyboard</code>似乎成为了我们必须同时也是苹果公司在引导我们将要实践的方向。</p>
<p>从<code>iOS 6</code>中的<code>Autolayout</code>到<code>iOS 8</code>中的<code>Size Class</code>,新技术的涌现正是为了应对更复杂的布局任务。有人可能会反驳说,自动布局也可以用纯代码完成呀。你说的没错,纯代码是可以完成,但其复杂程度远远不是重写Frame这么简单了,更灵活地将<code>Storyboard</code>和代码结合,才是比较完备的解决方案。</p>
<p>这里通过三个方面介绍通过使用<code>Storyboard</code>减小工程代码耦合性的途径:</p>
<ul>
<li>
<code>IBDesignable</code>和<code>IBInspectable</code>
</li>
<li>预览<code>Storyboard Preview</code>
</li>
<li>
<code>NSObject</code>和<code>Runtime Attributes</code>
</li>
</ul>
<h3>IBDesignable和IBInspectable</h3>
<p><code>IBDesignable</code>和<code>IBInspectable</code>的出现为<code>Storyboard</code>提供了可视化使用高度自定义控件的方法,例子中我们在制作一个双行标签控件,用来显示日期和星期,命名为<code>DateLabel</code>,使用方法如下:</p>
<pre><code>objc</code><code>//IB_DESIGNABLE 标记
IB_DESIGNABLE @interface DateLabel : UIView
//IBInspectable 标记
@property (nonatomic, strong) IBInspectable NSString* dateLabelText;
@property (nonatomic, strong) IBInspectable NSString* weekLabelText;
@end
</code></pre>
<p>其中,<code>IB_DESIGNABLE</code>标记赋予我们的继承类<code>DateLabel</code>可以在界面编辑器里面实时渲染的特权。<code>IBInspectable</code>则赋予让界面编辑器可以设置或者预置<code>View</code>的参数<code>dateLabelText</code>和<code>weekLabelText</code>。具体不多介绍了,有点跑题,大家可以参见<a rel="nofollow" href="http://www.cocoachina.com/industry/20140619/8883.html">如何在iOS 8中使用Swift和Xcode 6制作精美的UI组件</a>,同样适用于<code>Objective-C</code>和<code>Swift</code>。</p>
<blockquote>
<p>引用上文介<code>IBInspectable</code>支持<code>Int</code>,<code>CGFloat</code>,<code>Double</code>,<code>String</code>,<code>Bool</code>,<code>CGPoint</code>,<code>CGSize</code>,<br><code>CGRect</code>,<code>UIColor</code>,<code>UIImage</code>等类型的变量。</p>
</blockquote>
<p>现在在<code>Github</code>上已经有一部分开源的UI控件使用了这项特性,如此一来,很多需要在代码中实现的控件自定义特性,都可以在<code>Storyboard</code>中完成,后者的优势也很明显:</p>
<ol>
<li>所见即所得</li>
<li>剥离了<code>ViewController</code>中的定制<code>View</code>代码,减小耦合</li>
</ol>
<h3>预览Storyboard Preview</h3>
<p><code>Storybord</code>中提供了预览功能,可以预览其界面在各个尺寸设备上的真实显示效果。详见<a rel="nofollow" href="http://www.cocoachina.com/cms/plus/view.php?aid=8814">Xcode 6中学习Swift、CloudKit 和 Testflight</a>,搜索<code>Storyboard Preview</code>。</p>
<h3>NSObject和Runtime Attributes</h3>
<p>大家对这个概念再熟悉不过了,但大家有没有对他作为一个没有界面的控件在<code>Storyboard</code>作用产生过疑问呢。先来看下这篇文章 <a rel="nofollow" href="http://blog.sunnyxx.com/2014/07/17/ios_0code_vc/">0代码ViewController</a>的前言。</p>
<p><code>Storyboard</code>中的<code>NSObject</code>可以是<code>UITableView</code>的<code>DataSource</code>,也可以是<code>MapView</code>的<code>Delegate</code>,连线一下,就能将原本在<code>ViewController</code>中写得最多的代理方法全部移出,并且,当你需要的时候,这些现成的代理方法,可以直接移到其他的项目中使用。</p>
<p><code>Runtime Attributes</code>功能则可以在<code>Storyboard</code>中给参数写好初始值,但这里如果控件没有对应的参数的话,则会出现下面的报错。</p>
<blockquote>
<p><strong>Failed to set (xxx) user defined inspected property on (xxx): [ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxx</strong>.</p>
</blockquote>
<h3>Storyboard小结</h3>
<p>当你了解了<code>Storyboard</code>的基本原理,就会发现<code>Storyboard</code>是一个很好用的工具,是<code>Model-View-Controller</code>模型中<code>Controller</code>跳转逻辑和<code>View</code>初始化的实用载体,从根本上把<code>Controller</code>中的导航代码移出,把页面配置代码、触摸事件甚至协议委托方法分摊到其他实例中,各个类各司其职,整个项目的逻辑也变的更加清晰、更易维护。</p>