nswebfrog 2017-01-24T03:39:34+00:00 ccf.developer@gmail.com 开始尝试 Swift server 开发 2017-01-23T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2017/01/23/swift-server-attempt Vapor

Swift 语言发展的非常迅速,官方最近还专门成立了一个 Server APIs 的 group。今天尝试着使用 Swift 来写了一个服务端的小程序,我本身并没有太多服务端的开发经验,但还是为 Swift 能做服务器的开发而感到兴奋不已。

之所以会有写 server 端的程序的需求,是因为最近购买了一个自己的 vps,于是开始着手将博客迁移到自己服务器上。由于使用 jekyll 的博客系统,最终生成的都是静态的 html 文件,于是在服务器上安装了 nginx 来做服务器和网关。配置好 nginx 后,并将博客的域名服务地址改为指向 vps 的 ip 后,博客就已经正常运行了,但是接下来就碰到了另外一个问题。

问题


jekyll 是集成在 github 里的。由于之前是使用了 github 的服务,每当有新的博客文章写好,并以 git commit 形式 push 到相应的 github 仓库后,github 会做自动处理,然后生成最新的文章。但是换到自己的服务器后,这一切就需要自己来做了。如何能够自动化做到,每当有文章更新后,服务器上对应的博客内容也同样更新,便成了需要解决的问题。

解决思路及框架选择


明确了问题,那么就可以想办法来解决问题了。

解决思路如下:

  1. 当 git 库发生 push 后,通过 github 的 webhook 功能发出一个 http 调用到服务器。
  2. 服务器收到这个 http 调用后,更新本地对应的博客 git 仓库内容,然后调用 jekyll 命令,生成最新的博客文件。

这就需要搭建一个 http server ,该 server 在收到 hook 请求之后,执行相应的更新脚本,完成 git 仓库更新以及 jekyll 命令的调用。我决定用 Swift 来完成这些工作。

server 框架选择


Swift 发展到现在,已经出现了不少的 server 端框架,比较知名的有 PerfectVapor,也已经有文章比较详细的对比了这些框架的区别。对我来说, Vapor 的代码风格更加符合我的口味,于是便选择了 Vapor 来完成这次工作。

Vapor 提供的文档很详细,根据文档,新建一个对应的工程,接收一个指定链接 post 的 http 请求,并调用脚本完成任务。

完成主要功能的代码非常少:

import Vapor
import Foundation

#if os(Linux)
    typealias CommandProcess = Task
#else
    typealias CommandProcess = Process
#endif

let drop = Droplet()
drop.post("/hooker/updateBlog") { _ in
    let task = CommandProcess()
    task.launchPath = "/carl/bin/blogRefresh"
    task.launch()
    return JSON([:])
}
drop.run()

/carl/bin/blogRefresh 是我在服务上写好的一个 shell 脚本,功能就是更新 git 仓库,然后执行 jekyll build 命令生成最新的博客内容。

这里有一个坑,由于我是在 Mac 上开发,当我把写好的工程在 linux 服务器上编译时,就报错了,提示找不到 Process。最后才知道原来 linux 上的 swift 最新 release 版本的完成度并没有 Mac 平台上高。所以代码中使用了编译指令加上 typealias 来屏蔽了这个差异。

相应工程已经在我的 github 上:https://github.com/webfrogs/blogHooker.在工程路径下执行以下命令,可以编译出 release 环境的可执行文件

swift build -c release

文件位于工程跟路径下的 .build/release 文件夹下。

环境配置


Swift 工程编译完之后,直接执行编译好的可执行文件便可以启动服务。由于 Vapor 本身项目的限制,如果 http server 的服务不是启动在 80 端口或者是 443 端口(https 默认端口),则外界无法直接访问。需要用 nginx 做一个代理,在 nginx 的配置中设置相应的 http 代理。

nginx 代理配置 ``` server { listen 80; server_name domain;

location /hooker/updateBlog {
	      proxy_pass http://127.0.0.1:8080;
    	proxy_pass_header Server;
    	proxy_set_header Host $host;
    	proxy_set_header X-Real-IP $remote_addr;
    	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    	proxy_pass_header Server;
    	proxy_connect_timeout 3s;
    	proxy_read_timeout 10s;
} } ```

将 http://domain/hooker/updateBlog 这个请求代理到本地的 8080 端口服务,这个端口正是 hooker 这个 http 服务所启动的端口。

由于做为服务需要一直启动着,这里使用了 Vapor 官方推荐的 Supervisor 来管理 http 服务,具体可以查看相应文档

在这里我碰到了一些障碍,服务器上使用 rvm 来管理 ruby 相应的版本,在调用 jekyll 这个 ruby 的库时,碰到了一些环境上的问题,最终在 Supervisor 的服务配置上加上了下面配置得以解决

environment=PATH="/usr/local/rvm/gems/ruby-2.3.3/bin:/usr/local/rvm/gems/ruby-2.3.3@global/bin:/usr/local/rvm/rubies/ruby-2.3.3/bin:%(ENV_PATH)s", GEM_HOME="/usr/local/rvm/gems/ruby-2.3.3", GEM_PATH="/usr/local/rvm/gems/ruby-2.3.3:/usr/local/rvm/gems/ruby-2.3.3@global"

所有设置没问题后,到 github 仓库的 setting 中将这个 webhook 地址填好,最后测试通过。

总结


这次只是写了一个非常简单的 Swift server 程序,虽然感觉 Swift 在 server 端只是在起步阶段,很多方面还不够成熟。但是可以使用自己熟悉的语言来写一些完成自己需求的服务器程序,就已经很激动人心了嘛~~

]]>
ENJOY 工程 Swift 3 适配 2016-11-03T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2016/11/03/swift3-adaption 虽然 Swift 3 正式版出来很久了,但我们 ENJOY 工程迟迟未升级到 Swift 3。期间一直在用 Swift 2.3 版本进行过渡。但 Swift 3 是大势所趋,而且 Apple 也已经表示在 Xcode 8.2 将会是最后一个支持 Swift 2.3 的版本。虽然留给开发者升级的时间并不算短,但适配这个事情还是越早越好。我们项目组经过最近一段时间的奋战,终于在今天基本完成了 Swift 3 版本的适配工作。

Swift 3 做为一个 Grand API Change 的语言版本,对我们来说,适配并不是一个轻松的工作,毕竟我们工程是从 Swift 1.1 版本开始一直开发到现在的一个纯 Swift 工程,代码量还是相当大的。下面是我们工程在适配之前的代码统计,其中还不包括我们自己的 framework 代码。

适配步骤


在 Swift 3 还是 beta 1 版本时,我们就有过一次适配的经历,那次在将所有代码的编译器错误改完之后,工程并不能运行起来,随之作罢。后续的 beta 版本陆续还有语法更新,也就没有继续随之更新了。在 Swift 3 正式版出来之后,由于这段时间内业务相关代码变动蛮大,代码也做了很多重构工作,之前的适配代码已经基本不可用。为了完成整个工程的适配,我将适配工作拆分成下面的步骤:

  1. 第三方库及自己 Swift framework 库的适配
  2. 解决 ENJOY 工程编译错误
  3. 解决 ENJOY 工程的编译警告
  4. Swift 3 风格代码适配

适配过程


基于之前的适配经验,我并不相信 Xcode 自带的代码转换工具,因为这个工具会在某些地方将代码修改为不想要的逻辑,而这种修改又很难被第一时间发现。于是只使用这个工具将工程的配置文件更新到 Swift 3,其余的源代码均是手动更改。

首先,将 ENJOY 工程依赖的几个自己的 framework 工程切出 swift3 分支适配 Swift 3。这个过程很顺利。然后,对 Swift 3 依赖的第三方 Swift 库进行升级,这时就发现了一个比较坑的事情,也是导致了我们 Swift 3 完全适配拖延的问题,就是 Alamofire 这个库的 Swift 3 版本的分支最低支持的版本变成了 iOS 9!!!!这个结果是 Alamofire 官方组织刻意为之,然后他们也有一个完整的说明来解释这么做的原因。我看了之后表示,嗯,他们说的好有道理。但,我们还是很可能要支持 iOS 8 的啊(事实是最后确实还是要支持 iOS 8)。那好,暂且决定不管它,先去适配别的。

三方库的工作搞定后,下面要解决 Swift 3 下的编译错误。从 ENJOY 工程切出了一个 swift3 分支。将工程编译环境改为 Swift 3,并将所有第三方库依赖切换到 Swift 3 去,然后就开始了根据代码提示来修改语法错误的漫漫长路。由于我们的工程组织结构还是很清晰的。于是制定了以下计划,先修改工程的基础部分代码,而不适配与业务紧密相关的 UI 和 model 代码。由于要保证 swift3 分支每天与 develop 分支的同步,为了避免代码冲突的情况,在适配期间内基本不做公用部分代码的重构工作。而且这个阶段的目标很明确,就是优先解决语法错误,将工程先跑起来。

那次 beta 版本适配工作虽然最终都被丢弃,但我们也收获了一些经验。比如在那之后,我们对工程做了一些针对 Swift 3 适配的重构,最主要的就是重构掉 CGRectMake 这类的 Objective-C 风格的代码。这样的函数虽然在 Swift 2.3 中可以编译,但在 Swift 3 中已经被删除了,而对应的 Swift 版本代码在两个版本都是可以使用的。并且新写的代码不使用这种 API,这些都是我们在 code review 时重点关注的地方。

在经过漫长的时间后(其实是在评估要不要舍弃 iOS 8),除了业务代码之外的部分已经适配完毕。到了适配业务代码的时候了,事实证明,我们还不能抛弃 iOS 8。看了以下 Alamofire 的代码,支持 iOS 9 的原因就是因为使用了 iOS 9 下的 stream 相关的网络 API, 而这部分功能我们工程并没有用到,那么就 fork 了一份 Alamofire 库,屏蔽掉这部分功能,并将最低支持版本改为 iOS 8。然后修改工程的 Alamofire 依赖地址。这个 fork 库的地址 点这里 ,可以将工程依赖指向这个库的 serious 分支即可使用 iOS 8 版本的 Alamofire。解决掉这个障碍之后,我们于上周启动了整个 Swift 3 适配工作。由于 Swift 3 语法的变化,适配中很重要的一个准则就是,如果函数带有参数,那么就在函数定义的第一个参数的 label 前加上 _ 来解决函数签名问题,目的就是优先让工程跑起来。其余的根据代码提示来修改就好(这里严重吐槽一下 Xcode 对 Swift 语言的语法高亮速度,不过也可能是机器太慢)。一个小技巧:熟练使用文本替换可以节省不少时间,比如可以将一个文件中所有的 private 替换为 fileprivate 来解决作用域改变的问题。

好了,解决完工程中所有的 Error 之后,由于已经有了一次适配经验,所以对工程能够一下运行起来并没有报太大期望。果然,cmd + r 之后,不负众望的出现了一个下面的编译器错误:

segment fault, exit code xxxx.

查阅资料之后,总结起来就是,出现这种情况,大部分都是因为 Swift 类型推断出了问题。最后定位到问题,是由于工程中使用了 RxCocoa 的一个 bindTo 函数的 closure 中,使用了 $0 这种参数缩写方式。老老实实去掉这种写法,然后明确写上 closure 的参数类型后,工程终于跑起来了!!!!!

嗯,虽然跑起来了,但是 Xcode 显示的 warning 有 999+。warning 主要集中以下几个方面:

  • 函数返回值未使用
  • RxSwift 的 API 变化
  • SnapKit 的 API 变化

集中解决一下。然后就是对将已经运行起来的工程做回归性功能测试了。

适配中的坑:

  • nib 的事件对应的是有 @IBAction 方法签名第一个参数记得加上 _, 不然会 crash。
  • 自定义 present 动画的地方,由于方法签名变化导致自定义动画失效。(delegate 方法都是 optional 的,所有编译器不会提示)
  • 工程调用加密方法的地方,由于操作二进制数据的方式改变,适配起来还是蛮坑的
  • SnapKit 库 update 约束函数实现机制的更改,如果 update 一个不存在的约束,不会跟之前版本一样自动添加这个约束,而是会 crash

到这里,也只是完成到了适配步骤的第三步。接下来,根据 Swift API Design Guideline 的思想进行重构才是一个持久的工作。

总结


适配总的来说技术难度不大,但工作量不小,而且由于改动基本涉及所有 Swift 文件,回归测试是避免不了的。而且适配工作还要找业务开发的间隙来做,这里如何把控是个关键。

如果你的工程还未做适配,如果在写代码时多考虑一下适配情况,可以减少之后适配时的工作量。

最后,祝自己在 Swift 适配工程师这条道路上越走越远。不说了,继续去做适配去了😂。嗯,我还没去看 Swift 4 的相关变化。

]]>
Apple Pay 应用内支付流程分析 2016-02-20T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2016/02/20/apple-pay-analysis

简介


Apple Pay 已经正式上线,ENJOY 作为国内首批接入 Apple Pay 应用内支付的 App,并且是为数不多的直接使用 PassKit Framework 和银联接口的形式接入的应用,我们在产品上与 Apple Pay 做了深度集成。接下来把我们在接入过程中的一些经验分享一下。本文并不涉及具体代码,也不是讲解 Apple Pay 如何接入的教程,只是结合我们的开发过程,介绍下在接入过程中需要注意的地方。

Apple Pay 是 Apple 推出的支付服务,支持线上支付和线下支付,对我们开发者而言,线下支付跟我们没有太多关系。而线上支付的接入也并不复杂,关键在于要理解 Apple Pay 的整个工作流程。

接入方式


因为 Apple Pay 在国内是跟银联合作的,所以在接入方式的选择上有两种。一种是使用 CUP SDK(CUP 就是 China Union Pay)等第三方的 SDK。另外一种就是使用 iOS 的 PassKit Framework 和银联的接口来接入。本质上来说,第三方 SDK 就是对 PassKit Framework 和传输信息的加密解密过程做了一层封装,让开发者可以轻松完成 Apple Pay 的接入。

两种接入方式对比:第一种使用第三方 SDK 接入的方式开发成本较低,但缺点在于对 Payment Sheet 定制化程度不够。而第二种形式的缺点就是开发成本较高。不仅 iOS 端要处理好 Payment Sheet 的显示和隐藏的逻辑,还要对各种异常情况做好相应的 UI 处理。同时在后台也需要处理好以下情况:支付信息的解密,银联接口的交互,以及订单状态的处理。

支付流程分析


要理解 Apple Pay 的支付流程,其中最关键一点就是:Apple 不处理跟扣款相关的逻辑,它只负责支付信息的传递。Apple 通过 Touch ID 来验证银行卡卡持有者身份。实际的扣款行为则是发生在银联端,接入了 Apple Pay 的商户组织好 Apple 返回的支付信息,向银联发出扣款请求之后,该笔交易才会真正发生扣款。所以,商户还是要跟银联进行结算的,Apple Pay 只是提供了一种支付渠道。

Apple Pay 应用内支付流程如下

  1. App 根据使用场景显示 Payment Sheet。
  2. 用户选择需要进行支付的卡以及支付需要的个人信息后,进行指纹验证,之后根据情况,有些银行卡还需要输入卡对应的密码(PIN 码)
  3. iOS 将支付相关信息发送到 Apple 的服务器,进行加密。然后通过回调函数将加密后的支付信息返回给对应 App。
  4. App 在收到回调之后,将对应信息发送到自己的服务器。
  5. 服务器在收到 App 发送来的支付信息后,对数据进行解密操作,提取其中需要的信息,组织银联接口报文,调用银联的接口,完成扣款

下面对过程中的关键地方做一些说明。

App 收到的 Payment sheet 回调信息中,包含了一个 PKPayment 的对象,该对象包含了所有跟 Apple Pay 支付相关所有信息。比如用户的手机号或者收货地址等等,其中最重要的就是 payment token,它的 paymentData 字段数据就是需要发送给服务器的内容。用户信息部分是明文的,而支付信息也就是 paymentData 部分则是被加密过的。

paymentData 的内容是 Json 格式的二进制流,服务器在收到这个数据之后进行解析,其中的 header.wrappedKey 是使用非对称加密算法加密过的对称秘钥。使用在苹果开发者后台配置 merchant 时的私钥进行解密,会得到这个对称秘钥。然后用这个对称秘钥对 data 字段所包含的加密数据进行解密,可以得到 Apple 返回的与支付相关的信息。此支付信息是加密过的,包含了用户支付的卡号和 PIN 码等信息,理论上只有银联才能解析出来真正的内容,我们作为商户是看不到具体信息的。服务器端需要将这些解密过的信息组织成银联所需的报文内容,然后调用银联的扣款接口,完成扣款。

以上的服务器端对 paymentData 的解密流程,我们后台的同学近期会整理并开源出来,方便大家使用。有一点需要特别注意:paymentData 里的有一个交易金额字段,但该字段返回的数据并不是实际支付的金额。在组织银联报文的时候一定要注意不要直接使用该字段的内容作为扣款金额的值。

调用银联接口时也有一些需要注意的事项。拿调用银联扣款接口举例,在组织好报文并调用银联接口发送给银联之后,银联的接口返回结果同时有同步和异步两种形式。注意:如果同步结果返回成功,说明银联成功收到并开始处理扣款请求,并不是代表扣款成功。扣款是否成功,是通过异步形式来通知的。扣款不成功的原因可能有很多,比如卡被冻结,PIN 码错误,余额不足等等。为了保证交易状态的准确,推荐的做法是这样:在调用扣款接口后,如果 3 秒内没有收到本次调用的异步结果回调,则使用银联的流水号,开始轮询银联的交易状态接口来确保拿到确切的交易结果。

总结


Apple Pay 是很重视数据安全的。从上面的流程可以看到,为了保证整个交易的安全,Apple Pay 对每个关键流程都有加密处理。同时对每个绑定了 Apple Pay 的银行卡生成一个虚拟卡号,这个卡号的部分信息可以在 wallet 里绑定的卡片详情里看到。在实际支付中是用的这个卡号来做交易,这样可以在一定程度上保证我们银行卡的信息安全。

最后,如何将 Apple Pay 接入到 App 中,要结合自身的产品做出选择。如果只是将 Apple Pay 作为现有支付手段的一种补充,那么使用第三方 SDK 是一种省时省力的选择。如果需要跟 Apple Pay 做深度集成以及 Payment Sheet 的高度定制化,那么就需要使用 PassKit Framework 和银联接口方式来接入。

参考资料


]]>
动手搭建 iOS CI 环境之「了解 xcodebuild 命令」 2015-10-31T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2015/10/31/xcodebuild CI 也就是持续集成,是一种软件开发实践。通过自动化构建来将软件系统集成后尽早交付测试来发现问题。

在 iOS 开发中,如果需要把工程打包成 ipa 文件,通常的做法就是在 Xcode 里点击 「Product -> Archive」,等待整个工程 archive 后,然后在自动弹出的 「Organizer」 中进行选择,根据需要导出 ad hoc,enterprise 类型的 ipa 包。虽然 Xcode 通过提供的 GUI 操作已经屏蔽了大部分的细节,但作为一个偷懒的程序员,当然只是想简单的执行一个命令,然后坐下来惬意地喝一杯茶,静静等待整个过程的完成。那 xcodebuild 这个命令就是来完成这个事情的。

目前已经有不少封装得很不错的第三方打包服务可以用,比如最出名的有 AFNetworking 的作者 mattt 大大的 shenzhen (没错,就是深圳),还有一个名叫 gym 也很不错。这些库都是对 xcodebuild 这个命令做了一些封装,更方便使用而已。

xcodebuild 使用方法

xcodebuild 命令是 Xcode Command Line Tools 的一部分。通过调用这个命令,可以完成 iOS 工程的编译,打包和签名过程。这个命令随着 Xcode 的版本不同使用方法上也会有所不同。打开命令行,调用以下命令查看使用方法

xcodebuild --help

虽然这个命令屏蔽了大量编译器的细节,实际使用起来还是较为繁琐,使用的话最好还是对其做一层封装,来更方便地使用,这个之后的文章里再细说。

image

上面截图中显示了命令执行结果。当前环境是目前最新的 Xcode 7.1。

Archive 工程

我们就根据显示的帮助信息来调用命令完成整个打包的过程吧。首先需要了解的是,archive 工程后,实际上我们是把整个工程编译,然后签名,变成了一个后缀名为 xcarchive 的文件。通过调用以下命令,我们将整个工程编译,签名,然后将生成的 xcarchive 文件放到工程根路径下的 build 文件夹里。

xcodebuild -scheme <schemeName> -archivePath build/<schemeName>.xcarchive archive

参数说明:SchemeName 就是工程的 scheme 的名字,在 Xcode 中可以看到。如下图

image

如果工程使用了 Cocoapods 来管理第三方库,那么命令的执行方式上有所不同,因为 Cocoapods 会把工程变成一个 workspace。将 workspace 工程 archive 的命令如下。

xcodebuild -workspace <workspaceName> -scheme <schemeName> -archivePath build/<schemeName>.xcarchive archive

这个命令和上面的命令区别就在于需要指定 workspace 的文件名。

上面这两个命令适用于不同类型的工程: workspace 或者非 workspace。执行中读取的配置都是在工程的 Target 或者 Scheme 中设置好的配置。比如,archive 进行编译的是 release 环境,这个在工程的 scheme 中可以看到。而签名的证书默认也是在各个 Target 的 Build Setting 中的配置。

命令中可以添加一些参数实现在命令执行时配置不同的环境。比如,如果想 archive 出 Debug 环境的包,那么就可以在命令中添加 -configuration Debug 参数。对于非 workspace 的工程,还可以通过添加 PROVISIONING_PROFILE=<profileuuid> 的参数来指定签名所用的 Profile 文件,参数的值为 profile 文件的 UUID。

注意:如果下一步中需要导出的是 enterprise 类型的 ipa 包,则导出过程中使用的 profile 文件中指定的 identifier 一定要和相应的 target 的 Bundle Identifier 完全一样,不能使用包含通配符 * 的 profile

小提示:如果 archive 命令无法执行成功, 则可以在 Xcode 中通过点击「Product -> Archive」来查看产生问题的原因。如果 Xcode 中操作没有问题,那么 archive 命令也是可以正确执行的。

导出 IPA

接下来就是将上一步生成的 xcarchive 文件导出,生成 ipa 文件。这里需要用到的命令是 xcodebuild -exportArchive

从上面的帮助文件中可以看到命令的使用方法。这里需要注意的是,目前的 Xcode 7 中此命令的使用已经变成使用 -exportOptionsPlist 的参数形式了。这种形式支持 bitcode 的功能。但在实际的使用中,我发现使用这种形式我们工程并不能正确导出 ipa 包。原因我暂未找到,关于 xcodebuild 的资料也真是少的可怜。我猜测是我们工程未使用 bitcode 导致。所以我改为使用了 Xcode 6 时代的 -exportFormat IPA 参数来完成导出 ipa 包的功能。

xcodebuild  -exportArchive -exportFormat IPA -archivePath <archivePath> -exportPath <exportPath>

参数说明:archivePath 就是上一步生成的 xcarchive 文件的路径。exportPath 就是导出的 ipa 文件路径。

如果导出的 ipa 中要更换 profile 文件,也就是需要修改上一步中 archive 文件用于签名的 profile 文件,有两种方式可以完成这个任务。

一种方式是直接指定新的 profile 文件,用法是添加参数 -exportProvisioningProfile <profile name> 这种用法适用于只有一个 target 的项目。

如果工程中有多于一个的 tagert, 比如提供了 watchkit 的功能。由于 watchkit 和 watchkitextension 的 target 也分别需要不同的 profile,需要使用第二种形式的参数, -exportSigningIdentity <signing identity name> 只要相关的 profile 文件已经正确安装,那么就可以导出需要的 ipa 文件。 identity 可以在 Keychain 中找到。如下图

image

实例

以一个实际工程举例,该工程的名字叫 Tyrion。Scheme 名字也是 Tyrion。 那么 archive 的命令如下

xcodebuild -scheme Tyrion  -archivePath build/Tyrion.xcarchive archive

导出 ipa 包的命令如下

xcodebuild  -exportArchive -exportFormat IPA -archivePath build/Tyrion.xcarchive -exportPath build/Tyrion.ipa

依次执行完这两个命令后,工程根路径下的 build 文件夹内容如下图。

image

后续

导出 ipa 包后,就可以利用 iFunBox 之类的软件直接安装到对应的 iPhone ,或者利用 items-service 协议来远程安装。

就整个 CI 环境搭建来说,能够通过命令行正确导出 ipa 包只是完成了第一步,要完成整个自动化构建,还需要能够自动将 ipa 包发布出去。这篇文章就到此为止啦~~

]]>
AutoLayout 布局技巧-等宽子视图 2015-01-08T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2015/01/08/autolayout-trick 所谓等宽子视图,也就是对一个有 n 个子视图的父视图来说,无论父视图的宽度怎么变化,所有子视图的宽度是相等的。这里给出一种等宽子视图布局的情况(n = 3),如下图:

图片

分析一下这种情况的视图约束规则, view1,view2,view3 这三个子视图具有相同的宽度,子视图之间的 padding 为 10,view1 的左边与父视图距离为 20 ,view3 的右边与父视图的距离也为 20。简单起见,所有子视图的高度都为 50(不考虑高度的影响)。

接下来将上面的约束一一实现,其中用到的关键的约束条件就是’等宽约束’。

下图是在 Storyboard 中的约束实现:

图片

下面是 Swift 代码实现的约束:

let view1 = UILabel()
view1.backgroundColor = UIColor.redColor()
view1.textAlignment = .Center
view1.text = "view1"
view1.textColor = UIColor.whiteColor()
view1.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(view1)

let view2 = UILabel()
view2.backgroundColor = UIColor.blueColor()
view2.textAlignment = .Center
view2.text = "view2"
view2.textColor = UIColor.whiteColor()
view2.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(view2)

let view3 = UILabel()
view3.backgroundColor = UIColor.purpleColor()
view3.textAlignment = .Center
view3.text = "view3"
view3.textColor = UIColor.whiteColor()
view3.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(view3)

let viewDic = [
"view1": view1,
"view2": view2,
"view3": view3,

]

var constraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-20-[view1]-10-[view2(==view1)]-10-[view3(==view2)]-20-|", options: NSLayoutFormatOptions(0), metrics: nil, views: viewDic)
self.view.addConstraints(constraints)

constraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|-50-[view1(50)]", options: NSLayoutFormatOptions(0), metrics: nil, views: viewDic)
self.view.addConstraints(constraints)

constraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|-50-[view2(50)]", options: NSLayoutFormatOptions(0), metrics: nil, views: viewDic)
self.view.addConstraints(constraints)

constraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|-50-[view3(50)]", options: NSLayoutFormatOptions(0), metrics: nil, views: viewDic)
self.view.addConstraints(constraints)

如果将例子中的所有的水平 padding 都去掉的话,则三个子视图就将父视图的宽度三等分了。

]]>
Swift 脚本编写 2014-12-22T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2014/12/22/swift-script 作为苹果新一代的编程语言,Swift 不仅可以用来开发 iOS 应用,还可以用来编写脚本,来完成 OS X 下的一些自动化的工作。终于可以用我们熟悉的语言来写自动化脚本了,想想是不是就觉得心里有点小激动呢^_^。让我们开始编写脚本吧。

编写脚本


老规矩,从 Hello World 开始。

新建一个 Swift 文件,就叫 Hello.swift 吧。使用文本编辑器打开输入以下内容,并保存:

#!/usr/bin/env xcrun swift
println("Hello World")

打开终端,切换到保存 Hello.swift 文件的路径下,为该文件添加可执行权限,命令如下:

chmod +x Hello.swift

然后运行一下这个文件:

./Hello.swift

终端上输出了熟悉的 Hello World

执行 shell 脚本


在Swift 脚本里,你可以尽情使用 Swift 语法特性。但有些情况下还是需要调用一些 shell 脚本的。这里就需要用到 Foundation 库中的 NSTask 类了。

首先,在脚本中引入 Foundation 库。

import Foundation

调用 shell 脚本的关键代码:

let shell = "pwd"
let task = NSTask()
task.launchPath = "/bin/bash"
task.arguments = ["-c", shell]

task.launch()

字符串常量 shell 可以是任意能在 bash 中执行的 shell 脚本文本内容

优缺点


对我们 iOS 开发者来说,使用 Swift 来编写脚本,优势很明显,那就是不需要另外学习脚本语言。缺点就是不能够跨平台,还有就是目前 Xcode 对编写 Swift 脚本的支持不够好(好吧,Xcode 对 Swift 开发 iOS 的支持也是不怎么样)。不过,多一种选择总是好的嘛。

]]>
2014 年终总结 2014-12-19T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2014/12/19/summary 已经一年没有更新博客了。这一年的时间里,我的主要精力都集中在了 iOS 开发上,并且开始带团队了。上半年的时间主要花费在重写并维护一个 Objective-c 项目上。最近这两个月左右又负责了一个用 Swift 作为主要开发语言的项目。并于昨天刚刚向 App Store 提交了第一个正式的版本(佛祖保佑一定要审核通过……)。今年真的是忙碌而又有收获的一年。

前几天抽了一些时间对博客做了一些调整。一年的时间没管,就有变得年久失修的感觉。主要的调整如下:第一,关闭了博客的评论系统,我发现对于个人博客来讲,评论系统真的是一点用处都没有,索性就将其关闭了。第二,更换了一个 .com 的域名。之前的博客所用的 .me 域名的管理权不在我手里,而且那时买域名的时候没有经验,只买了一年的,现在发现续费真的是太贵,于是干脆直接买了一个 10 年的 .com 域名来更换。博客目前依旧是托管在 github pages 上,对于目前博客的这点流量来说足够了。

今夏的 WWDC 最令苹果开发者激动的一件事情莫过于 Swift 语言的横空出世了。从发布的那刻起,整个社区都在兴致勃勃讨论与这门新语言相关的种种话题。老实说,我并没有在第一时间去学习 Swift 这门语言。因为作为一门刚发布的语言,各个 beta 版本语法的变化经常导致之前写的程序无法编译,还记得当时社区里流行的一个段子就是,开发者每周一的问候语是:“今天你的代码编译通过了么?”^_^ 。真正开始学习 Swift 是在 1.0 版本发布之后,又恰逢公司启动新项目,作为技术负责人的我做了一个大胆的决定:新项目用 Swift 作为主要编程语言来做。这种在项目中来学习一门语言绝对是最佳的方法。渡过过初期的不适应期之后,发现自己已经喜欢上了这门语言。当然这种离开自己的 comfort zone 后提升的感觉真的让人上瘾啊!

最近养成了听播客的习惯,最初的起因是越来越觉得时间不够用,听播客可以将上下班路上的时间充分利用起来。后来逐渐就喜欢上了,而且发现播客对我的影响蛮大,启发了我学会从不同角度看待一件事情。中文播客里听的最多的就是 IPN 的《IT 公论》 和 《内核恐慌》。最近也在慢慢开始去听一些英文播客,不过自己目前的听力水平还是不太好,要加强锻炼啊。

总的来说, 2014年,是收获的一年,技术上有了很大的提升,同时也积累了团队管理的经验。编程之路漫漫,学到的太少,不懂的太多,共勉。期待即将到来的2015.

]]>
使用Jenkins搭建iOS开发的CI服务器 2013-12-31T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2013/12/31/jenkins-ios-ci 本文为webfrogs原创,转载请注明作者及出处!

目录


    简介
    下载并运行
    Jenkins配置
        安装git插件
        E-mail设置
    自动化构建
        远程仓库设置
        触发条件设置
        编译设置
        编译后行为设置
    单元测试
    最后

## 简介 —-

持续集成CI(continuous integration)是一种可以增加项目可见性,降低项目失败风险的开发实践。iOS开发中CI的选择有很多,比如可以使用Apple提供的Bots来完成自动化构建和单元测试,其优点就是和Xcode深度集成,只需几步配置就可以完成,缺点就是不够灵活,可定制化程度不高。这篇文章主要讲解如何使用开源社区的一个CI工具Jenkins来搭建iOS开发的CI环境。如果是搭建单独CI服务器的话,就需要一台单独的mac机器了。

## 下载并运行 — 打开Jenkins的官网,在页面的右侧,点击下载最新版本的Jenkins的war包。

下载完成后,打开terminal,进入到war包所在目录,执行命令:

java -jar jenkins.war --httpPort=8888

httpPort指定的就是Jenkins所使用的http端口,这里指定8888,可根据具体情况修改。待Jenkins启动后,打开浏览器输入地址:

http://localhost:8888/

便可以打开Jenkins的管理界面了。

## Jenkins配置 — #### 安装git插件

Jenkins默认没有安装git插件,需要手动选择安装。进入Jenkins的管理界面,依次选择Manage Jenkins->Manage Plugins,选中“Available”选项,在页面的右上角的“Filter”中输入git过滤条件,在所有列出的结果中,选中“Git Client Plugin”和“Git Server Plugin”这两个选项,然后点击按钮“Download now and install after restart”。等待插件下载安装成功后,重启Jenkins。如下图所示:

图片

#### E-mail设置 Jenkins可以在适当的时机发送邮件通知,比如自动化构建失败时。这就需要对E-mail的发送进行相关的设置。

发送邮件使用的是SMTP协议,首先要设置Jenkins的管理员邮箱,在Manage Jenkins->Configure System的“Jenkins Location”中设置“System Admin e-mail address”为需要的邮箱,也就是Jenkins发送邮件的发件人。

接下来设置邮件SMTP的相关信息,在“E-mail Notification”区域中,点击“Advanced…”按钮,然后进行设置,首先填写SMTP服务器地址,选中“Use SMTP Authentication”的复选框,然后输入用户名和密码,最后在“Test configuration by sending test e-mail”中输入一个测试邮箱来测试邮件是否能发送成功。如果成功,会有相关提示,如下图所示。

图片

注意: 在设置邮箱时,Jenkins管理员邮箱要与SMTP中设置的发送邮箱为同一个邮箱,否则在使用比如qq邮箱或者是163邮箱时,就会报错。

### 自动化构建 —- 在Jenkins中,所有的任务都是以“Job”为单位的。下面以新建一个iOS项目Daily Build的自动化构建Job为例来做一个演示。

在Jenkins管理的首页左侧,点击“New Job”,接下来输入Job的名字,这里输入“Dailybuild”,选择“Build a free-style software project”然后点击“OK”,进入下一个页面。

#### 远程仓库设置

首先进行版本控制的相关设置,这里我们选择git。输入git的仓库地址,然后选择需要build的分支,另外,在“Additional Behaviours”中,还可以选择一些额外的git操作。如下图。

图片

提示: Jenkins使用当前用户.ssh目录下的公私钥来进行git的相关操作。

#### 触发条件设置

下一步,设置build的触发条件,由于是做Daily Build,所以在“Build Triggers”中,选择“Build periodically”,然后在输入框中输入build的规则,这里,我们的规则是每个工作日的下午6点25到30分之间进行build,所以在输入框中输入“H(25-30) 18 * * 1-5”(点击输入框右边的问号,会有详细的规则编写说明),如下图。 图片

#### 编译设置

然后,进行对工程编译的相关设置。这里,可以使用Jenkins自带的xcode插件(需要安装,参考上面git插件的安装方法)来完成,也可以自己编写脚本来完成。编写脚本时,可以直接使用Xcode的xcodebuild来写,也可以使用Facebook提供的xctool来做。但在本例中使用的是本人遍写的makefile来完成编译打包。这个makefile的功能有:指定Provisioning Profile打包编译,生成itms-services协议相关文件并以scp或者ftp方式上传到服务器来实现ota功能,发送邮件通知和iMessage通知。使用的makefile的github地址在这里,里面有使用说明。

点击“Add build step”按钮,选择“Execute shell”,在command中输入一下内容:

#!/bin/zsh
make clean
make
make upload
make sendEmail
make sendIMsg

如图所示: 图片

说明: 如果不使用iMessage通知,可以去掉第一行和最后一行,否则,Jenkins默认的shell会导致iMessage通知不能正常发送。

#### 编译后行为设置 工程成功编译以后,我们可以设置编译出来的ipa文件(甚至可以直接是ota文件),将其与本次build的相关结果放到一起,提供下载。也可以在build失败时,发送邮件提醒。设置如下。

点击“Add post-build action”选择“Archive the artifacts”,在输入框中输入“build/*.ipa”,就可以将编译打包后的ipa文件集成。点击“Add post-build action”选择“E-mail Notification”,在输入框中输入编译失败后邮件的通知者邮箱,如有多个,以空白字符分隔,如下图: 图片

至此,一个Daily Build的Job基本设置完成,点击“Save”按钮保存设置。在Job中,点击“Build Now”,测试下我们刚才的配置。如果build失败,可以点击“Console Output”查看log来查找错误的地方。如果成功,在相应的build中,可以看到如下图的内容: 图片

### 单元测试 —- 在本例中,iOS工程的单元测试选择xcode自带的XCTest框架(Xcode5之前叫做OCUnit)。创建单元测试Job和自动化构建的Job过程一样,只在触发构建规则,build的脚本和编译后的规则有些不同。以下只说明不同的地方。

单元测试的触发规则应该在git仓库的每次有新提交时就触发执行,所以在”Build Triggers”中,选择“Poll SCM”,在规则中写入“H/10 * * * *”,意思是每十分钟轮询一次远程仓库,如果有新的提交,则开始构建。可以根据自己需求来设置轮询的时间间隔。

接下来是在build中输入单元测试脚本。这里需要有一些准备,首先,由于Jenkins只接收Junit的单元测试报告,这里要安装一个将脚本执行结果的ocunit格式的测试报告转化为JUnit报告格式的脚本,该项目名叫OCUnit2JUnit,项目地址点这里。安装非常简单,命令行下执行gem install ocunit2junit(可能需要sudo权限)。第二步,需要在当前项目工程中,将项目schemes共享,并上传到远程仓库。在工程中选择“Manage Schemes”在弹出的菜单中勾选“Shared”,然后在git中将相应的shared shceme添加到版本控制中并上传到远程仓库。如下图

图片

“Build”配置中,依然选择“Execute shell”,shell的内容如下:

xcodebuild test -scheme testCI -sdk iphonesimulator7.0 -destination OS=7.0,name="iPhone Retina (4-inch)" -configuration Debug  2>&1 | ocunit2junit

这里的单元测试是在模拟器中进行,如果测试服务器连接着iOS设备,也可以设置在iOS设备中进行,只需修改上述shell的参数即可。

最后是编译后行为的设置,这里要将测试报告加入。点击“Add post-build action”选择“Publish JUnit test result report”,输入内容test-reports/*.xml保存设置。

接下来在单元测试的Job中,点击“Build Now”来测试一下Job的配置,如果配置正确,则会看到模拟器启动,然后运行了一下程序,之后在build的结果里,可以看到相应的测试报告,点击进去会有详细的信息,如下图:

图片

## 最后 —- 芈峮在《豆瓣ios自动化测试实践和经验》(视频地址PPT地址)中提到Jenkins还可以集成UIAutomation来进行iOS的UI方面的自动化测试,并且还发布了他们自己封装的UI测试工具ynm3k,项目地址点这里。待研究之后再写下相关的经验吧。

]]>
objc之利用runtime为category添加成员变量 2013-10-22T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2013/10/22/add-member-to-category 背景

我们知道利用category可以在不知道某个类内部实现的情况下,为改类增加方法。但默认的情况下是不能添加成员变量的。但作为一门动态语言,我们可以利用objc的runtime来实现为其添加成员变量的功能,并且完成对KVO的支持。

实现


选取iOS下比较知名的下拉刷新开源库SVPullToRefresh库为例,在其对UIScrollView的扩展中,就利用了这种技术为UIScrollView添加了成员变量。

关键代码如下:

- (void)setPullToRefreshView:(SVPullToRefreshView *)pullToRefreshView {
    [self willChangeValueForKey:@"SVPullToRefreshView"];
    objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView,
                             pullToRefreshView,
                             OBJC_ASSOCIATION_ASSIGN);
    [self didChangeValueForKey:@"SVPullToRefreshView"];
}

- (SVPullToRefreshView *)pullToRefreshView {
    return objc_getAssociatedObject(self, &UIScrollViewPullToRefreshView);
}

其中UIScrollViewPullToRefreshView是一个静态的char变量。在文件的头部有如下的定义:

static char UIScrollViewPullToRefreshView;

从上面代码可以看出,通过objc_setAssociatedObject和objc_getAssociatedObject这两个函数,完成了为UIScrollView添加SVPullToRefreshView类型变量的功能。同时,willChangeValueForKey:didChangeValueForKey:这两个方法完成了KVO的功能。

]]>
Mac OS X开发之Custom Sheets 2013-10-18T00:00:00+00:00 nswebfrog http://blog.nswebfrog.com/2013/10/18/osx-sheet-window 前言

最近在准备写一个Cocoa软件,便开始学习一些Mac开发的知识。相比于iOS开发,Mac开发的资料真的是很少,而中文就更少了。于是决定将自己开发中的碰到的一些问题记录下来,提供一些借鉴。这篇博文记录了我在实现Custom Sheets时碰到的一些问题和相关解决办法。

Mac开发和iOS开发还是有一定的区别的,如果有iOS开发基础,那么看完参考链接里的《How to Make a Simple Mac App on OS X 10.7 Tutorial》系列文章你就已经可是上手开发Mac应用了。

Sheets介绍


大家对sheet应该不会感到陌生,iOS中就有UIActionSheet这个控件。苹果官方文档中对sheet是这么定义的。

A sheet is simply a dialog attached to a specific window, ensuring that a user never loses track of which window the dialog belongs to.*

Mac中的NSOpenPanel的beginSheetModalForWindow:completionHandler:方法和NSAlert的beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:方法都提供了sheet的显示方式,Custom Sheets的显示效果如下图,这也正是我们要实现的。 sheetDemo

注意事项


首先提供上面这个图片对应的Demo工程的源码:点击下载

其中的几个细节要特别注意下:

  1. 作为自定义Sheet的NSWindow的nib中的“Visible At launch”选项一定要取消选择。(如下图)
  2. “Title Bar”这个选项如果不选中的话,弹出的自定义Sheet里的NSTextField无法输入内容。我也搞不清楚是为什么,谁了解的话,望不吝赐教。

## 参考链接 —

]]>