姚翔的部落格

更轻松地页面跳转,Wireframe框架(下)

| Comments

上篇我解释了Wireframe框架的大体思路和跳转逻辑,这次我具体说明一下里面的builder, navigator和transition的概念。

Builder

Builder顾名思义就是用来实例化目标View Controller的工厂。在Wireframe中实例化View Controller有两种方法:1)通过storyboard初始化 2)通过代码。下图是配置文件中相应的配置内容:

File Inspector

可见,如果View Controller被画在了storyboard中,那么只要提供对应的storyboard文件名称,以及对应的view controller id,那么wireframe会自动通过storyboard来实例化。如果要使用代码实例化(包括从xib文件加载页面),那么就需要通过配置builder来实现。首先需要在配置文件中配置对应的builder code,然后通过调用wireframe实例的register方法去注册对应的builder即可,示例代码如下:

1
2
3
4
5
6
7
8
9
register(builderName: "product") { (params) -> UIViewController in
    let productViewController = ProductViewController()
    productViewController.productName = params?[WireframeParam.name.rawValue] as? String
    return productViewController;
}

register(builderName: "seller") { (params) -> UIViewController in
    return SellerViewController(nibName: "SellerViewController", bundle: nil)
}

具体的实例化方式,是根据你具体的业务逻辑需求来的,方法所传递的params参数,是页面跳转时所传的额外参数(optional),你可以根据具体需要来定制目标view controller的初始化过程。目前框架只提供了一个简单的UIAlertController的默认builder。

Navigator

Navigator则是负责具体跳转的接口。它和Builder的使用方法类似,在配置文件中配置对应的name/code,然后通过register方法注册对应的navigator实现到wireframe中去。框架默认提供了7个常用的跳转方式,比如:present(Animated or Not Animated),push(Animated or Not Animated),如果app的跳转方式比较简单,那么默认的navigators就已经足够满足需求了。下面的代码演示了如果注册一个navigator,它的作用就是给目标view controller包上一个navigation controller再做跳转:

1
2
3
4
register(navigatorName: "navigation-wrap") { (sourceViewController, destinationViewController, completion) in
    let navigationController = UINavigationController(rootViewController: destinationViewController)
    sourceViewController.present(navigationController, animated: false, completion: completion)
}

Transition

有些时候,我们需要给跳转过程加特定的动画效果,这就需要我们用到类似UIViewControllerAnimatedTransitioning这样的技术,wireframe也提供了配置的方式,每个Wireframe实例有一个optional的transition接口变量,你可以设置成自己实现的transition接口,每当wireframe完成目标view controller的初始化后,就会调用对应的接口方法来配置需要的transition,你可以通过判断soure和destination的类型来选择对应的动画效果。

目前transition这个配置的方式还不是很完美,会有很多类型判断的代码在里面,如果大家有好的建议欢迎给我留言:)

最后要说的是,对于大型的项目而言,一个项目中可以有多个wireframe存在,每个wireframe负责某个模块内部的调度,然后再由一个总的wireframe来负责模块间的调度,这样可以使结构更清晰,可维护性更强。这个是框架的Github链接,欢迎大家提供宝贵意见和建议。

更轻松地页面跳转,Wireframe框架(上)

| Comments

笔者也已经写过不少的app了,在每个app的开发过程中,一个亘古不变的需求就是页面之间的跳转。虽然这是个非常不起眼的功能,有时候你简单的一行 pushViewController:XXX 代码就搞定了,但它也有着大学问。

试想这样一个场景:项目初期,需求很简单,A界面中点击某个按钮需要跳转到B,所以在按钮点击事件中,我们用代码初始化B的实例,然后调用对应的跳转方法实现了该功能;然后新需求来了,有了一个新的C界面,也需要跳转到B,所以我们就拷贝了相同的代码到C里面,实现需求;迭代还在继续,B界面的内容需要根据入口的不同有一些细节上的不同,所以我们给B添加了初始化参数,然后A和C通过传递不同的参数值来定制B;UI的改版也接踵而来,进入B界面的时候需要添加过渡动画效果,所以我们编写了对应的transition代码,在A和C的入口处添加了相同的动画配置代码;代码越来越多,leader说需要重构一下,B要重命名,传参方式也要改一下,于是我们开始重构,A和C的入口代码也进行了相应修改,但为了保证能覆盖修改到所有调用B的地方,我们还不停search,思考各种使用到的case,但还是不能保证是否都改掉了。

上面的场景在平常的的开发过程中是很普遍的,可见跳转逻辑因为其耦合强的关系导致代码维护成本很高。通常遇到这种情况,我们会把重复的部分抽取出来,包装成对应的模块来减小耦合,但是我们始终没有一套框架性的东西来整体解决这个问题,更多的只是具体问题具体方案解决,所以不是很便利。以前笔者在研究VIPER架构的时候,就对里面Routing的wireframe概念很感兴趣,它给解决这种跳转的问题带来了一个很好的思路,经过多次实践,笔者整理出了一套自己的解决方案,所以把它单独抽取出来作为一个小框架分享出来,希望可以帮助大家能更轻松地维护页面跳转的逻辑。项目Github链接

框架的基本思想很简单:把app里面所有页面之间的跳转关系都抽取出来,统一交由wireframe处理,页面之间不再彼此知道,从而减轻了耦合,减少需求改动时的维护成本。举例来说:传统的方式下,A界面点击一个按钮需要跳转到B,那么A需要知道B这个类的定义,要负责去初始化它,还需要知道当前所在的页面结构,从而决定用什么样的方式去展示B(比如是用push还是present)。采用了wireframe框架后,A只需要调用下面一行代码来完成跳转:

1
Wireframe.sharedWireframe.navigateTo(port: .detail, gate: .product, from: self)

我们把跳转点换成了一个抽象的概念:Port和Gate。Port和Gate都只是简单的String值,用户可以根据自己App的内容来定义,通常来说,Port可以定义为广义功能类型,Gate定义成具体的业务功能。以上面这个代码为例,它的意思就是告诉wirefraem:我现在需要跳转了,要跳转到一个detail类型的界面,具体的内容是product,接下来就交由wireframe去完成所有的工作。那么wireframe是怎么完成这个跳转的呢,它大致的步骤如下:

  1. 通过配置文件找到对于A来说,对应的detail-product功能点的view controller是哪个,比如找到了B
  2. 初始化B,如果有传递参数,则把对应的参数也配置到B中去
  3. 通过配置文件找到此次跳转需要的跳转方式
  4. 如果需要自定义transition方式,则配置对应效果
  5. 执行跳转代码

上面的步骤中,提到了配置文件,它是个什么东西呢?它其实是一个.plist文件,是框架的使用者用于配置整个app中页面关系的文件,wireframe通过读取该文件就能知道所有页面之间的关系,它的内容其实非常简单,我们来看一下示例项目中所带配置文件内容:

File Inspector

整个文件分两个部分:Decodes 和 Destinations

  • Decodes是配置view controller的class和代号(code)之间的关系的,同时还配置了如何实例化该view controller的方式(builder)

  • Destinations则是配置页面跳转的关系的,同时还配置了跳转的方式(navigator)

还是以最前面的那行代码为例,我们来讲解一下具体的流程:

  1. 当HomeViewController调用了对应的跳转方法后,wireframe先通过decodes部分找到了它对应的code是“Home”
  2. 把port和gate同code拼接起来,组成跳转的key,也就是:"Home-Detail-Product"
  3. wireframe通过Destinations部分找到了key所对应的target code:"Product",也就是要跳转到的页面的code
  4. wireframe又通过decodes找到了Product对应的初始化方式(builder):product
  5. 它通过该builder初始化了对应的view controller(builder是什么将在下期介绍)
  6. 配置中本次跳转对应的跳转方式是(navigator):navigation-wrap(navigator是什么将在下期介绍)
  7. Wireframe最终采用对应的跳转方式完成了跳转

可见,Wireframe完成了所有原来耦合部分的工作,同时通过配置文件的方式用户可以很容易地修改跳转关系和方式。而对于页面A来说,它根本不需要知道B的存在,它只需要知道我要跳转到一个什么功能点。

目前这个框架刚完成了第一个版本,完全用swift编写,相信还有很大改进空间。如果大家感兴趣,可以试用一下,里面也带来sample项目可帮助大家理解,希望这个小工具能够帮助到大家。项目Github链接

下期将具体说明一下builder, navigator以及transition的定义和使用方法。

响应式编程入门(下)

| Comments

书接上文,前面我们简单地介绍了响应式编程(Reactive Programming)的一些基本思想,接下来我会通过实际例子,一步步演示如何将RP应用到实际的开发中去。因为RxSwift本身已经提供了非常好的Sample代码,所以我打算这里就通过复刻其中的GitHubSignup项目,来作为本次演示的主题。首先让我们来看看需要达到的效果:

File Inspector

以下是我们将实现的主要功能(我这里省掉了提交表单的逻辑,其实现方法同其他的大同小异):

  • 当用户在输入用户名时,实时去验证用户名是否可用,同时显示对应的验证结果
  • 当用户在输入密码时,要验证是否大于3位,同时显示对应的验证结果
  • 当用户重复密码时,要验证是否和第一次输入一致,同时显示对应的验证结果
  • 当且仅当三个输入区域都验证通过时,提交按钮才可被点击,否则将禁用

因为密码的验证不需要网络请求,完全在本地完成,所以我们先从这个比较简单的入手。验证过程是在每次用户键入密码时触发的,所以我们可以把它看作是一个输入流,用图形表示就是类似这样:

1
2
3
--t----t---t-----t------>

t表示的就是一次键盘输入事件

不过我们不光需要知道输入事件,还需要知道输入事件产生时输入框内的文本内容,我们希望有一个这样的流:

1
2
3
--t----t---t-----t------>
  v    v   v     v
-(1)-(12)-(123)-(1234)-->

大家马上会想到我们可以通过一个简单的map来搞定,不过RxSwift是非常贴心的,这么常用的功能它们已经提供了封装,我们可以直接拿来用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ViewController: UIViewController {

    @IBOutlet weak var passwordTextField: UITextField!

    let password = Variable("")

    override func viewDidLoad() {
        super.viewDidLoad()

        passwordTextField.rx_text <-> password
    }
}

infix operator <-> {
}

func <-> <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable {
    let bindToUIDisposable = variable
        .bindTo(property)
    let bindToVariable = property
        .subscribe(onNext: { n in
            variable.value = n
            }, onCompleted:  {
                bindToUIDisposable.dispose()
        })

    return StableCompositeDisposable.create(bindToUIDisposable, bindToVariable)
}

在上面的代码中,我们申明了一个password的Variable,为的是把UI中的值绑定到它上面,方便后面的使用,所使用的<->绑定操作符是自定义的,可以在底部的方法中看到。可以看到,到目前为止我们其实就用了一行代码生成了需要的数据流,接下来就是验证数据,转化成我们真正需要的结果流了:

1
2
3
4
5
--t----t---t-----t------>
  v    v   v     v
-(1)-(12)-(123)-(1234)-->
  v    v   v     v
--F----F---F-----T------>

其实现的代码如下:

1
2
3
4
5
6
7
8
9
10
11
let passwordValidation = password.map { (password) -> (valid: Bool?, message: String?) in
    let numberOfCharacters = password.characters.count
    if numberOfCharacters == 0 {
        return (false, nil)
    }
    if numberOfCharacters < 4 {
        return (false, "Password must be at least 4 characters")
    }
    return (true, "Password acceptable")
}
.shareReplay(1)

我们的验证逻辑很简单,看一下输入字符的长度,如果大于3位则通过。最后的shareReplay是RxSwift提供的一个流操作函数,作用是为了保证在观察者订阅这个流的时候始终都能回播最后一个(数字1表示回播的数量)流中的值,这样能使界面上正确显示验证的状态。现在我们有了需要的流,接下来就是创建一个观察者来订阅这个流,从而能把结果显示在界面上反馈给用户,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func bindValidationResultToUI(source: Observable<(valid: Bool?, message: String?)>, validationErrorLabel: UILabel) {
    source
        .subscribeNext { v in
            let validationColor: UIColor
  
            if let valid = v.valid {
                validationColor = valid ? UIColor.greenColor() : UIColor.redColor()
            } else {
                validationColor = UIColor.grayColor()
            }
  
            validationErrorLabel.textColor = validationColor
            validationErrorLabel.text = v.message ?? ""
        }
        .addDisposableTo(disposeBag)
}

override func viewDidLoad() {
      
      // ...

    bindValidationResultToUI(
            passwordValidation,
            validationErrorLabel: self.passwordValidationLabel
        )
}

我们定义了一个帮助方法,它会订阅给定的验证流,并把结果显示在指定的Label上,怎么样是不是感觉非常简单,剩下要做的就是把刚才我们创建的密码验证流绑定到需要的标签上即可。依样画葫芦,接下来我们来创建重复密码的验证:

1
2
3
4
5
6
7
8
9
10
11
12
let repeatPasswordValidation = combineLatest(password, repeatedPassword) { (password, repeatedPassword) -> (valid: Bool?, message: String?) in
    if repeatedPassword.characters.count == 0 {
        return (false, nil)
    }
    if repeatedPassword == password {
        return (true, "Password repeated")
    }
    else {
        return (false, "Password different")
    }
}
.shareReplay(1)

这里我只贴出了核心的创建流的代码部分,其余的与验证密码都是雷同的。从上面的代码中可以看到,我们使用了combineLatest的函数来合并两个流,它的作用如下图:

1
2
3
4
5
6
7
X: --1----2------------3--4--|->

Y: ----A-----B---C--D--------|->

combineLatest(X, Y)

   ----1A-2A-2B--2C-2D-3D-4D-|->

可见,它正好试用我们验证密码的场景,接下来就是简单的验证逻辑,如此我们便实现了重复密码的验证过程。现在让我们来看一下稍微复杂一点的逻辑,验证用户名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let usernameValidation = username
    .map { [unowned self] username -> Observable<(valid: Bool?, message: String?)> in
        if username.characters.count == 0 {
            return just((false, nil))
        }

        if username.rangeOfCharacterFromSet(NSCharacterSet.alphanumericCharacterSet().invertedSet) != nil {
            return just((false, "Username can only contain numbers or digits"))
        }

        let loadingValue = (valid: nil as Bool?, message: "Checking availabilty ..." as String?)

        return self.API.usernameAvailable(username)
            .map { available in
                if available {
                    return (true, "Username available")
                }
                else {
                    return (false, "Username already taken")
                }
            }
            .startWith(loadingValue)
    }
    .switchLatest()
    .shareReplay(1)

我们来具体分析一下整个过程,首先用户名的验证分为两个部分,一个本地合法性验证,另一个是远程的可用性验证,因为服务器验证涉及到异步,所以其验证结果本身就是一个流(拥有初始状态,结果状态)。然后我们来想象一下,当用户在输入用户名的时候,每当用户键入一个值,我们都需要去验证,但因为服务器验证是异步的,所以当用户键入下一字母的时候,前面的验证结果其实是没有用了,那我们应该怎么处理呢?switchLatest函数是我们的救星,下面是其官方文档的示意图:

File Inspector

简言之,switchLatest所操作的流其原本的每个值都是一个单独的流,经过处理后,它变成了一个单一的流,其中的每个值都是原本流当中最新流中的最新值。这就完美地解决了我们的问题,每当新的验证触发后,前一个验证流的值虽然还会产生,但是我们已经不去关心它了,从而不会对我们的结果产生影响。最后让我们来处理提交按钮的禁用逻辑,当我们用流的思路去考虑问题时,将发现这个需求简直太简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let signupEnabled = combineLatest(
    usernameValidation,
    passwordValidation,
    repeatPasswordValidation
    ) { username, password, repeatPassword in
        return (username.valid ?? false) &&
            (password.valid ?? false) &&
            (repeatPassword.valid ?? false)
}

signupEnabled
    .subscribeNext { [unowned self] valid  in
        self.submitButton.enabled = valid
        self.submitButton.alpha = valid ? 1.0 : 0.5
    }
    .addDisposableTo(disposeBag)

相信大家现在一看这个代码就已经知道它的意思了,所以就不再赘述了。响应式编程提供给了我们一种完全不同的思维方式,即使不去实际应用,去学习它本身就是一个很有意义的事情,况且在交互越来越复杂的时代,它更是提供了一种清晰的解决方案。我自己还谈不上入门,但已跃跃欲试要好好专研一番,希望此文能给想入门的同道们一些帮助。

另附上示例代码地址

响应式编程入门(上)

| Comments

Reactive Programming(响应式编程,以下简称RP)这个概念最近正火,本人也一直对其非常感兴趣,终于得了些空余时间可以学习下这门新技术,于是顺便整理些相关内容,希望可以帮助到一些想入门的朋友们。

我选择入门的框架是RxSwift,在过了一遍它官方提供的Tutorial playground代码后,发觉对RP的理念仍是一头雾水,一番Google后找到了这篇优秀的文章:The introduction to Reactive Programming you’ve been missing。读完之后醍醐灌顶,所以以下的博文内容我希望通过意译这篇文章结合RxSwift里的注册Sample来阐述RP的“流之世界观”(这个名称是我自己起的,因为一旦步入Reactive后,以往解决问题的思路将完全改变)。

RP的学习曲线是蛮高的,如果缺少好的资源的话就更难了。看library的文档往往使人一头雾水,而一些教程文章往往只是教你如何用相关的库,很少触及到其真正架构的思想,从而使人不得要领。学习RP最难的部分是:如何用Reactive的方式思考。它要求我们摒除掉以往编程时的那种老思路(状态化、命令式),用一种完成不同的视角去考虑问题。那么到底什么是响应式编程(Reactive Programming)?

响应式编程就是面向异步数据流的编程

这并不是什么新东西,事件总线或者典型的点击事件们就是一个异步事件流,你可以监听这个流,同时做一些额外的处理业务。响应式无法就是把这个理念给发扬光大了:不光是点击或者悬停事件,你几乎可以给所有东西都创建数据流。流(Stream)是廉价的、无所不在的,任何东西都可以成为流:变量,用户的输入,属性,缓存,数据结构等等。举得例子:想象一个你的微信朋友圈动态就是同点击事件一样的一个数据流,你可以监听它同时对其进行对应的响应。

在这基础之上,你还拥有一套不可思议工具集去操作这些流:合并、创建、过滤等等。这就是所谓的“函数式(functional)”施展魔力的地方。一个甚至多个流可以被当做另一个流的输入;你可以合并两个流;你可以过滤一个流从而得到一个只包含你感兴趣事件的流;你还可以把一个流中的值映射成其他值从而形成一个新的流。

那让我们来仔细看一下“流”,拿我们熟悉的“点击按钮事件”流来举例:

File Inspector

一个流就是一个按照时间排序的持续事件的序列,它会发出三种不同类型的东西:一个某种类型的值(Value)、一个错误(Error)、以及一个完成(Completed)的信号。比方说这个按钮所在的view或者window被关闭了,那么就会发出一个完成信号。我们可以定义不同的函数,使流分别在发出一个值、或者错误、或者完成信号时,执行不同的函数,从而异步地捕获这些发出的事件。有时候我们可以忽略错误和完成事件,只关心产生值的事件。而这个监听行为,我们称之为“订阅(subscribing)”,那些我们定义的函数,就称为“观察者(Observer)”,那些流则称为“对象(subject)”或者“可被观者物(observable)”。

为了方便绘制,我们可以用ASCII来重绘上图:

1
2
3
4
5
6
--a---b-c---d---X---|->

a, b, c, d 是发出的值(Value)
X 是一个错误(Error)
| 是一个完成(completed)信号
---> 表示的是时间轴

接下来让我们玩点有意思的,我们已经有了一个点击事件的流,现在让我们来建立一个记录点击次数的流。在通用的Reactive库中,每个流都提供了很多有用的方法来使用,比如map、filer、scan等等。当你调用其中一个方法时,比如clickStream.map(f),它会返回一个基于点击事件流的新流,它并不会对原来的流做任何改动。这是一个叫做不可变(immutability)的特性,它和Reactive中的流是比目连枝的。这就使得我们可以通过链式调用来生成新的流,比如:clickStream.map(f).scan(g)

1
2
3
4
5
6
7
8
9
  clickStream: ---c----c--c----c------c-->

                  v  map(c becomes 1) v
  
               ---1----1--1----1------1-->
  
                  v      scan(+)      v

counterStream: ---1----2--3----4------5-->

其中map(f)会根据你提供的f函数把输入流中的每个值转化到新的流中去,在上面的例子里,我们将每个点击映射成了数字1。scan(g)则是把所有早先的值聚合成一个新值的函数,新值的计算公式是:x = g(accumulated, current),当前的例子中,g就是一个简单的add(+)函数。就这样,counterStream变成了一个在每次点击时,都会产生一个点击总数的流。

现在让我们来看下Reactive的真正魔力,比方说我们现在需要一个“双击事件”的流,为了提升点难度,我们再假设“三击事件”也需要同双击一样被监听,或者更狠一点,任意的“多次连击事件”都需要被监听。好,冷静,先让我们来想象一下如果用传统的实现方式你会怎么做?我敢打赌那样的代码一定会恶心到自己,需要定义一些变量来记录状态,同时还要对时间间隔做一些必要的处理。

但是,在Reactive的世界里,这是相当简单的事情。实际上只需要4行代码就可以搞定了,不过先让我们撇开代码,无论你是一个菜鸟还是专家,用时间流图的方式去思考是理解如何创建流的最佳方式:

File Inspector

上图灰框里是把一个流转化成另一个流的函数。第一个函数的作用就是把流中的点击事件按照最大250毫秒间隔累计起来成为一个列表流(不要担心不懂这些函数的功能,目前我们只是在演示响应式的思想)。然后我们再通过map()把结果映射成每个列表中元素数量的流,这样我们就获得了连击次数了。最后我们用一个过滤函数过滤了连击数是1的值,这样我们就订阅(subscribe)/监听(listen)这个结果流来实现我们想要的功能了。

怎么样,这样的解决思路是不是很优美?这个例子只是响应式编程的冰山一角,你可以在任何流上都应用这样的操作,比如说一个API请求相应的流,另一方面,这样的函数有非常之多。

那我们为什么要用响应式编程呢?

RP提升了你代码的抽象级别,所以你只需要专注于那些业务逻辑中相互关联的事件,而不需要一直纠结在所有的实现细节中。RP中的代码会更简明。

在现代那些有着大量UI交互元素,同时每个交互又有其相关联的数据事件的webapp和手机应用中,RP的益处就更大了。10年前,一个网页的交互可能只是简单的提交一份表单给后端,然后在前端展现一些数据;但如今,应用已经进化成更实时的体验:修改表单中某一项的值可能就自动触发了一个后台保存的事件,“给别人点赞”这样一个操作会实时地反馈到被点赞的用户那边,这样的用例非常之多。所以我们需要一些工具来处理这种实时性的需求,响应式编程就是目前最佳的选择。

(待续……)

使用Block时何时需要WeakSelf和StrongSelf?

| Comments

现在我们用 Objective-C 写代码时已经越来越多地用到了block,相比delegate的回调方式,block更直观易用。相信每个使用过block的人都遇到过block中使用self时需要weakself的情况,以下就是非常典型的一段代码:

1
2
3
4
5
__weak __typeof(self)weakSelf = self;
[self.context performBlock:^{
    __strong __typeof(weakSelf)strongSelf = weakSelf;
    [strongSelf doSomething];
}];

对于小白的我一般越到这种情况就是直接拷贝上面这个模板到需要的代码中去,而不知个中原委。但作为一个合格的程序员,是需要完完全全明白自己写的每一行代码是在做什么的,所以现在就简单说明一下这个 WeakSelf 和 StrongSelf 到底是什么。首先看下面这段代码:

1
2
3
[UIView animateWithDuration:0.2 animations:^{
    self.myView.alpha = 1.0;
}];

在ARC环境下的,每个block在创建时,编译器会对里面用到的所有对象自动增加一个reference count,然后当block执行完毕时,再释放这些reference。针对上面的代码,在animations block执行期间,self(假设这里的self是个view controller)的引用数会被加1,执行完后再次减1。但这种情况下为什么我们一般不会去weakify self呢?因为这个block的生命周期是明确可知的,在这个block执行期间当前的view controller一般是不会被销毁的,所以不存在什么风险。现在我们看下面这个例子:

1
2
3
4
5
NSBlockOperation *op = [[[NSBlockOperation alloc] init] autorelease];
[op addExecutionBlock:^ {
    [self doSomething];
    [self doMoreThing];
}];

在这种情况下,我们并不知道这个execution block什么时候会执行完毕,所以很有可能发生的情况是,我在block还没执行完毕时就想销毁当前对象(比方说用户关闭了当前页面),这时就会因为block还对self有一个reference而没有立即销毁,这会引起很多问题,比方说你写在- (void)dealloc {}中的代码并不能马上得到执行。所以为了避免这种情况,我们会在block前加上__weak __typeof(self)weakSelf = self;的定义来使block对self获取一个弱引用(也就是refrence count不会加1)。

那block中的StrongSelf又是做什么的呢?还是上面的例子,当你加了WeakSelf后,block中的self随时都会有被释放的可能,所以会出现一种情况,在调用doSomething的时候self还存在,在doMoreThing的时候self就变成nil了,所以为了避免这种情况发生,我们会重新strongify self。一般情况下,我们都建议这么做,这没什么风险,除非你不关心self在执行过程中变成nil,或者你确定它不会变成nil(比方说所以block都在main thread执行)。

好了,简要的说明到此结束,想要详细了解的可以自行google,这类文章很多。前面代码中的WeakSelf、StrongSelf转换看起来很冗长,不利于阅读代码,下面介绍一个超好用的宏定义代码,它是这个开源库的一部分:libextobjc。把它加入到项目中后,就可以用以下如此简洁的格式来完成转换啦:

1
2
3
4
5
6
7
#import "EXTScope.h"

@weakify(self)
[self.context performBlock:^{
    @strongify(self)
    [self doSomething];
}];

iOS UITableView 使用小贴士

| Comments

UITableView 可以说是搭建应用时必不可少的控件之一,同时也是最常见、遇到最多自定义需求的控件之一,所以今天这里总结了几个平时使用时的小技巧,给自己备忘的同时也分享给大家。

> 去除多余的Separator

有时,我们会使用到table view自带的行分隔线样式,但是会遇到一个问题,就是当列表内没有cell时,或者cell数量不足以填充列表可见高度时,列表仍然会显示额外的分隔线。一个快速的解决方案是给table view设置一个空白的footer view:

1
tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];

> 设置初始显示位置

在一些用例场景中,我们会要求当用户进入到一个列表页面时,他需要首先看到列表最底部的cell(或者其他一些位置不在top的cell)。通常我们第一反应便是在 UIViewController 的 viewWillAppear: 方法中用代码把table view滚动到需要的位置,但实际上这个方案并不奏效(造成这个的原因是,在viewWillAppear的这个阶段,view还没有真正地去layout,所以table view此时还不知道它真正的content size,也就无法滚动到正确的位置);紧接着,我们会尝试把代码移动到 viewDidAppear: 方法中,结果确实起作用了,但是用户会在视觉上看到一个滚动的过程,所以并不理想。这里提供一种比较trick的解决方案来处理这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    if (!self.hasFinishedLayoutView) {
        self.hasFinishedLayoutView = YES; // 这里用一个flag来避免多次执行

        [self.tableView layoutIfNeeded];

        NSIndexPath* indexPath = ...; // 定义要滚动到的位置
        [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];
    }
}

我们把滚动代码移到 viewDidLayoutSubviews 这个生命周期方法中去,这样就可以成功了。注意一点:我这里手动调用了一下table view的layoutIfNeeded方法,因为在我的实践中,我的view都是用autolayout来构建的,如果这里不手动layout一下,table view的内容大小在这个阶段还是不正确的。

> 随着Cell一起滚动的Section Header

UITableView的section header view有一个自带的炫酷功能,就是浮动(floating),当列表滚动的时候,它会自动附着在顶部直到其他Section的内容把它顶掉。但是我们有时候不想要这样的效果,我们希望它能跟着cell一起滚动,那应该怎么处理呢?很不幸的是,官方公开的API中并不提供这种选项,那怎么办呢?我自己选择的一种解决方案就是,用cell的方式来实现header的功能:1)去掉所有section header的定义; 2)自己维护好DataSource的index path关系,每个section的第一行是header cell,接下去的才是真正展示数据的cell。

> 自适应高度的Cell(Self-Sizing Cell)

关于这个话题,网上已经有很多教程了,我这里就不再赘述,而是列出一些关键点以供备忘用。因为要能灵活运用这个功能,了解其原理是很重要的,tips只是为了不违背“好记性不如烂笔头”这个亘古不变的告诫,所以如果大家对其仍一知半解的话,建议看WWDC官方视频深入了解一下。视频链接请戳这里

下面是一些要点:

  • 把UITableView的rowHeight属性设置成UITableViewAutomaticDimension

  • 把UITableView的estimatedRowHeight属性设置成适合实际情况的估算值

  • 如果使用Autolayout约束的方式来定义cell的高度,一定要加足够多的约束,从而能让cell通过这些约束计算出对应的高度。比如:一个只显示一个UILabel的cell,我们要给label同时加上距离ContentView top和bottom两个约束,这样cell才能通过label内容的高度,再加上上下距离来确定cell需要的实际高度是多少。

  • 一般涉及到动态高度的情况都会有UILabel元素的加入,所以如果想要通过label的内容来扩大cell高度的话,记得要把numberOfLines属性设置成0

为 UIActivityViewController 添加自定义的 UIActivity 选项

| Comments

在现今开发应用的过程中,为其添加分享功能几乎成了一个不可或缺的需求。而 iOS 里的 UIActivityViewController 为我们提供了一种非常简单、快速的实现方案,因此往往成为初期快速迭代中的第一选择。但是,其自带的 Activity Type 毕竟有限,所以有时我们需要添加一些自定义的类型来满足需求。这里我就简单说明一下如何通过继承 UIActivity 来添加自定义选项。

首先,让我们再简单认识一下UIActivityViewController,它到底是用来做什么的呢?(UIActivityViewController自从 iOS6 开始便被加入到 API 中,要具体了解请看此文)总得来说,它主要做两件事:

  • 它从你的应用接收各种对象数据,可以是 NSString, NSAttributedString, NSURL, UIImage 等等,它们被称为 Activity Items。

  • 它管理着所有的 Activities,它把接收到的数据传递给每个 Activity,同时展示给用户;这些 Activities 包括系统自带的,用户自定义的,以及来自 Share and Action Extensions 的。

它展示的界面效果是这样的:

File Inspector

它把 Activities 分成了上下两部分,上面的都是属于 Share 类别的,而下面的则是 Action 类别。UIActivityViewController 的 UI 界面是系统生成的,不支持自定义,所以如果对界面有特殊要求的话,恐怕只能自己去实现相应的功能了(这里有一个开源库可以参考),但是你也失去了其他应用实现的 Share Extensions 和 Action Extensions 的支持了。

言归正传,我们来看一下如何添加一个自定义的分享选项。其实过程非常简单,你只需要新建一个类,把它作为 UIActivity 的子类,然后重载以下这些方法:

  • + (UIActivityCategory)activityCategory 这个方法就是告诉系统这个 Activity 是 Action 类型还是 Share 类型,默认是 Action,所以我们这里要返回 UIActivityCategoryShare。

  • - (NSString *)activityType 用来区分不同 activity 的字符串,用你的 bundle id 做前缀就会避免冲突

  • - (NSString *)activityTitle 显示在选项图标下的文字

  • - (UIImage *)activityImage 图标素材,这里要注意的是,目前只有 iOS 8 才支持显示彩色的图标,在这之前,你所提供的素材其实是作为 mask 来使用的,显示的则是黑白效果

  • - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems 这里就是你通过 items 来判断当前 Activity 是否支持,如果不支持(返回No),则当前选项不会在界面中显示出来

  • - (void)prepareWithActivityItems:(NSArray *)activityItems 为分享做准备,你必须在这里把这些 items 保存下来,然后做一些适当的准备工作

  • - (void)performActivity 真正执行 share 动作的地方,这里要注意的是,不管分享成功与否,都要在结束后调用 - (void)activityDidFinish:(BOOL)completed 这个方法来通知系统分享结束了

实现好我们自定义的选项后,使用起来就非常简单了:

1
2
NSArray* activities = @[ [CustomActivity1 new], [CustomActivity2 new] ];
[[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:activities]

更多详细内容请点击这篇参考文章继续阅读