Skip to content

Latest commit

 

History

History
959 lines (655 loc) · 53.8 KB

iOS-Good-Practices.md

File metadata and controls

959 lines (655 loc) · 53.8 KB

  跟随iOS开发技术发展的潮流,我将持续维护本文档的中文版,如果你喜欢这个译本,也请给个 Star 鼓励下~~

  本文档的英文原版在这里,感谢Futurice团队卓越的工作,为我们提供这么优质的文档。

知识是人类进步的阶梯

*翻译,喵 ~~*

iOS开发的最佳实践

就像一个软件项目一样,这份文档如果我们不持续维护就会逐渐失效,我们鼓励大家参与到这个项目中来---仅需提交一个 issue 或发送一份 pull request

对其他移动平台感兴趣?我的Andriod开发最佳实践以及Windows App开发最佳实践可能会帮到你哦。

为什么写这个文档

iOS开发要上手比较困难,因为无论是 Objective-C 还是 Swift 在别处都没有广泛被应用,iOS 这个平台似乎对一切都有一套不同的叫法。当你尝试在真机上跑程序时难免会磕磕碰碰。这份持续更新的文档就是你的救星!无论你是Cocoa王国的新手,或是老练到只想知道"最佳做法"是什么,这份文档都值得一读。当然,内容仅供参考,你有理由采取不同的做法只要你愿意!

目录

如果你想阅读指定的小节,可以通过目录直接跳转

  1. 开始吧
  2. 常用库
  3. Architecture 架构
  4. 网络请求
  5. Stores 存储
  6. Assets 资源
  7. 编码风格
  8. Security 安全
  9. 诊断
  10. Analytics 统计分析
  11. 编译构建
  12. Deployment部署
  13. App内购(IAP)
  14. License
  15. More Ideas(计划)
  16. Issues备忘录
  17. 译者

开始吧!

Xcode

Xcode是绝大多数 iOS 开发者选择的 IDE,也是 Apple 唯一一个官方支持的 IDE. 也有一些其他的选择,最著名的可能就是 AppCode了。但除非你已经对 iOS 游刃有余,否则还是用 Xcode 吧,尽管 Xcode 有一些缺点,但它现在还算是相当实用的!

要安装 Xcode ,只需在 Mac 的 AppStore 上下载即可。它自带最新版的 SDK 和 iOS 模拟器,其他版本可以在 Preferences > Downloads处安装。

创建工程

开始一个新的 iOS 项目时,一个常见的问题是:界面用代码写还是用 Storyboard、xib来画?现有的 App 中两种方式都占有相当的市场。就此我们需要考虑以下几点:

用代码写界面有啥好处?

  • Storyboard 的 XML 结构很复杂,所以如果用 Storyboard ,合并代码时很容易冲突,比起用代码来写,麻烦许多。
  • 用代码更容易构建和复用视图,从而使你的代码库更容易遵循 (Don't Repeat Yourself)DRY原则
  • 用代码可以让所有的信息都集中在一处。但使用 Interface Builder 你得到处找对应的检查器,作死地各种点,才能找到你要设置的属性!
  • 代码和 UI 通过 Storyboard 进行耦合可能会导致崩溃,比如一个 IBoutlet 或者 IBAction 没有被正确设置时。类似这种问题编译器可没法检测。

用 Storyboard 画界面有啥好处?

  • 对技术不太熟悉的人也可以画:调整颜色、布局约束等等,为项目作出直接贡献。不过做这些时,需要工程已经建好并了解一些基本的知识。
  • 开发迭代更快,因为不需要 build 工程就能预览到作出的改动,所见及所得:
    • 自定义字体和 UI 控件在 Storyboard 中所见即所得 。这让你在设计时能更好地了解界面的最终外观。
    • 使用SizeClasses(iOS 8开始有效),Interface Builder (界面生成器)为你提供了一个实时的布局预览你所选择的设备,包括 iPad 的分屏多任务处理。

为什么不同时使用两者?

为了结合两者之间的优点,你也可以采用一种混合的方法:使用 Storyboard 绘制最初的设计,对于时不时要做出改变修修补补来说非常合适,你甚至可以邀请设计师参与到这个过程中来。当 UI 的设计更为成熟和可靠时,你再过度到代码层进行配置,这有利于相互合作及代码的维护。

gitignore文件

要为一个项目添加版本控制,最好第一步就添加一个恰当的.gitignore文件。这样一来不需要的文件(如用户配置、临时文件等)就不会进入 repository(版本仓库)了。可喜的是,Github 已经帮我们准备了 Objective-C版Swift版

Cocoapods

如果你准备在工程中引入外部依赖(例如第三方库),Cocoapods提供了快速而便捷的集成方法。安装方法如下:

sudo gem install cocoapods

首先进入你的工程目录,然后运行

pod init

这样会创建一个 Podfile 文件,在这里集中管理所有的依赖。添加你所需要的依赖然后运行

pod install

来安装这些库,并把它们和你的工程一起放进一个 workspace 里。在 commit 的时候,推荐把依赖库在你的 repo 里安装好之后再 commit ,最好不要让每个开发者 checkout后还要自己跑一下 pod install

注意:从此以后要用.workspace打开工程,不要再用.xcproject打开,否则代码编译不通过。

下面这条指令:

pod update

会把所有的 pod 都更新到 Podfile 允许的最新版本。你可以通过一系列的 语法来准确指定你对版本的要求。

项目结构:

把这些数以百计的源文件都保存在同一目录下,不根据工程结构来构建一个目录结构是无法想象的。你可以使用下面的结构:

|- Models
|- Views
|- Controllers
|- Stores
|- Helpers

首先,在 Xcode 的 Project Navigator (左边栏)里,把这些目录建立为group (小小的黄色"文件夹"),建在工程的同名 group 下。然后,把每一个 group 与工程路径下实际的文件夹链接起来,方法是:

  • Step 1、选中 group
  • Step 2、打开右边栏的 File Inspector
  • Step 3、点击小小的灰色文件夹 icon
  • Step 4、在工程目录下创建一个新的子文件夹,名称与 group 相同。

本地化

一开始就应该把所有的文案放在本地化文件里,这不仅有利于翻译,也能让你更快地找到面向用户的文本。你可以在 build scheme 里添加一个 launch 参数,指定在某种语言下启动 App,例如:

-AppleLanguages (Finnish)

对于更复杂的翻译,比如与名词的数量有关的复数形式(如 "1 person" 对应 "3 people"),你应该使用 .stringsdict格式来替换普通的 localizable.strings 文件。只要你能习惯这种奇葩的语法,你就拥有了一个强大的工具,你可以根据需要(如俄语或阿拉伯语的规则)把名词变为"一个"、"一些"、"少数"和"许多"等复数形式。

更多关于本地化的信息,请参考2012年2月的Helsink iOS会议的幻灯片。其中大部分演讲至少到2014年10月为止仍然不过时!

常量

创建被 prefix header 引入的一个 Constants.h 文件 不要用宏定义( #define ),用实际的常量定义

static CGFloat const XYZBrandingFontSizeSmall = 12.0f;
static NSString * const XYZAwesomenessDeliveredNotificationName = @"foo";

常量类型安全并有更明确的作用域(在所有没有引入的文件中不能使用),不能被重定义,并且可以在调试器中使用。

分支模型

App发布的时候把 Release 代码从原有的分支上隔离出来,并且加上适当的tag,是很好的做法,对于向公众分发(比如通过Appstore)的 app 这一点尤其重要。同时,涉及大量 commit 的 feature 应该在独立的分支上完成。 git-flow 是一个帮助你遵守这些规则的工具。它只是在 git 的分支和 tag 命令上简单加了一层包装,就可以帮助维护一套适当的分支结构,对于团队协作尤为有用。所有的开发都应该在 feature 对应的分支上完成(小改动在 develop 分支上完成),给 release 打上 app 版本的 tag,然后 commit 到 master 分支时只能用下面这条命令:

git flow release fininsh <version>

常用库

一般来说,在工程里添加外部依赖要谨慎。当然,眼下某个第三方库能漂亮地解决你的问题,但或许不久之后就陷入了维护的泥淖,最后随着下一版 OS 的发布全线崩溃。另一种情况是,原先只能通过引用外部库来实现的 feature,突然官方 API 也支持了。在设计良好的项目里,把第三方库替换为官方的实现花不了多少功夫,但在将来会大有裨益。永远要优先考虑用苹果官方的框架(也是最好的框架)来解决问题!

因此,这一章有意写得比较简短。下面介绍的第三方库主要用来减少模板代码(例如 Auto Layout)或者用来解决复杂的、需要大量测试的问题,例如计算日期。随着你对 iOS 越来越精通,务必要四处看看它们的源码,熟悉它们所使用的底层框架。你会发现做好这些就能减轻许多重担了。

AFNetworking

99.95% 的 iOS 开发者使用这个库,当 NSURLSession 自己本身也非常完善的时候, AFNetworking 仍然能凭借很多 App 需求的队列请求管理能力立于不败之地。

DateTools 日期工具

总的来说,不要自己计算日期DateTools 是一个经过彻底测试的开源库,你可以放心使用它来做这种事情。

Auto Layout 库

如果你更喜欢用代码写界面,你会用过 Apple 难用的 NSLayoutConstraint的工厂方法或者 Visual Format Language。前者很啰嗦,后者基于字符串不利于编译检查。

masonry 通过他自己的 DSL 来创建、更新和替换约束,利用语言丰富的操作符重载特性较优雅地实现了 AL。Swift 中一个类似的库是 Cartography。如果更加保守的话, FLKAutoLayout 是一个好的选择,它为原生API添加了一层简洁而不奇异的包装。

架构

  • Model-View-Controller-Store (MVCS)

    • 这是苹果默认的架构(MVC)上增加了一个 Store 层,用来吐出 Model, 处理网络请求、缓存等。
    • 每个 Store 暴露给 View Controller 的或是 RACSignal,或是返回值为 void,参数中带有自定义的 completion block的方法。
  • Model-View-ViewModel(MVVM)

    • MVVM 是为了解决“巨大的 view controller”而生,它把 UIViewController 的子类看做 View 层的一部分, 用 ViewModel 维护所有的状态来给 ViewController 瘦身。
    • 对于 Cocoa 开发者是一个很新的概念,但正在引起越来越多的关注。想了解更多请参考:Bob Spry 的 fantastic introduction.
  • View-Interactor-Presenter-Entity-Routing (VIPER).

    • 颇为奇特的架构,但项目大到即使使用 MVVM 都会太凌乱并且需要重点考虑项目可测性的情况下值得参考。

"通知"模式

以下是组建之间互发通知的一些常见手段:

  • Delegate (一对一) : Apple 官方经常用的模式(有些人认为用得太泛滥了)。主要用于回传,比如从模态框回传数据。
  • Callback blocks(一对一) : 耦合更松,同时能让相关联的代码在一起,并且消息发出者数量很多时比 Delegate 更方便。
  • Notification Center(一对多):可能是最常见的对象发送 events 给多个观察者的方法。耦合性非常松 - 没有任何对当前派发对象的引用的情况下,通知也能够在全局范围内被观察到。
  • Key-Value-Observing (KVO) : (一对多)。不需要被观测的对象主动"发出通知",只需要被观测的键(属性)支持 Key-Value Coding (KVC)。这种模式比较含混,而且标准API比较繁复,所以一般不推荐使用。
  • Signal : (一对多)。是ReactiveCocoa的核心,它允许结合你的关键内容进行链式调用,用这种方法逃离回调深渊(嵌套过多的回调)

Models

要确保你的 Model 是不可变的,他们用来把远程 API 的语义和类型转换为 App 适用的语义和类型。对Objective-C来说 Github的Mantle是个不错的选择。在 Swift 中,你可以使用 structs 而非 classes 来确保其不可变性,并使用一个类似 SwiftyJSON或者 Argo的解析库来做 JSON - Model 之间的转换。

Views

今天 Apple 生态系统中丰富的屏幕尺寸及分屏多任务 iPad 的问世,使得设备和它的构成形式之间的界限越来越模糊。就像今天的网站要预先适配不同的窗口尺寸一样,你的 App 也应该以一种优雅的方式来处理各种屏幕的尺寸变化。用户旋转设备或者在你的 App 旁边滑动第二个 iPad App 时(分屏多任务),这种需求简直是必须的。

你应该使用size classes和 AutoLayout 来申明你的视图约束,而不是直接操作视图的 frame。基于这些约束规则,系统将为视图 计算合适的 frame 并在环境改变时(切换设备或者分屏展示等)重新计算他们。

Apple 在设置布局约束的推荐方法中推荐在初始化方法中创建并激活你的布局约束.如果你需要动态地改变某些约束,hold 住他们的引用并在必要的时候关闭或激活他们。这主要用于在你想要系统执行批量更新以获取更好性能的时候, 执行 UIViewupdateConstraints (或者它对应的 UIViewControllerupdateViewContraints )。但这样做的代价是你需要调用 setNeedsUpdateConstraints 方法, 这会增加代码的复杂性。

如果你在自定义的视图中重写 updateConstraints,你应该明确指出你的视图支持基于约束的布局:

Swift:

override class func requiresConstraintBasedLayout() -> Bool {
    return true;
}

Objective-C:

+ (BOOL)requiresConstraintBasedLayout {
    return YES;
}

不然,系统可能不会如期调用 -updateConstraints,而导致奇怪的 bug 。这一点上 Edward Huynh 提供的这个博客有更详细的解释。

Controllers

要使用依赖注入,也就是说,应该把 controller 需要的数据用参数传进来,而非把所有的状态都保持在单例中。后者仅当这些状态的确是全局状态的情况下才适用。

Swift:

let fooViewController = FooViewController(viewModel: fooViewModel)

Objective-C

FooViewController *fooViewController = [[FooViewController alloc] initWithViewModel:fooViewModel];

尽量避免在 view controller 中引入大量的本可以安全地放在其他地方实现的业务逻辑,这会让 view Controller 变得十分臃肿。Soroush Khanlou 有一篇 很好的博客 介绍了如何实现这种机制,而类似 MVVM 这样的程序架构将 view controller 当 views 对待,因此大大地减少了 view controller 的复杂度。

网络请求

传统方法:使用自定义回调 block

//GigStore.h
typedef void (^FetchGigsBlock)(NSArray *gigs, NSError *error);

- (void)fetchGigsForArtist:(Artist *)artist completion:(FetchGigsBlock)completion;

//GigStore.m
[GigStore sharedStore] fetchGigsForArtist:artist completion:^(NSArray *gigs, NSError *error) {
    if(!error) {
        //Do something with gigs
    }
    else {
        // :(
    }
};

这样虽可行,但如果要发起几个链式请求,很容易导致回调深渊。

Reactive 的方法:使用 RACSignal

如果你身陷回调深渊,可以看看 ReactiveCocoa(RAC).这是一个多功能、多用途的库,它可以改变整个 App 的写法。但你也可以仅在适合用它的时候,零散地用一下。

Teehan+lax以及NSHipster很好地介绍了 RAC 概念(以及整个 FRP 的概念)。

//GigStore.h

- (RACSignal *)gigsForArtist:(Artist *)artist;

//GigsViewController.m
[[[GigStore sharedStore] gigsForArtist:artist] subscribeNext:^(NSArray *gigs) {
                            // Do something with gigs
                        } error:^(NSError *error) {
                            // :(
                        }];

在这里我们可以把 gig(演出) 信号与其他信号结合,因此可以在展示 gig 之前做一些修改、过滤等处理。

存储

作为一个可以"在地面上移动"的移动应用,通常有某种存储模型把数据保存在某个地方,如硬盘上、本地数据库中或者远程的服务器上。在把模型对象的任意活动抽象出来的方面,Store 层也非常有用。

抓取数据通常是异步进行的,但它是意味着关闭后台请求还是从硬盘反序列化一个大文件呢?你的 Store 层的 API 必须通过提供某种延期机制反映出这种情况,就像同步返回数据将引起线程阻塞那样。

如果你使用 ReactiveCocoa, 通常会选择 SignalProducer 作为返回类型。举个栗子,获取某个艺术家的演出信息将会产生下面这样 Signature:

Swift + RAC 3:

func fetchGigsForArtist(artist: Artist) -> SignalProducer<[Gig], NSError> {
    //...
}

Objective-C + RAC 2:

- (RACSignal *)fetchGigsForArtist:(Artist *)artist {
    //...
}

这里,返回的 SignalProducer 仅仅是获取演出列表的一个"配方"。仅当被订阅者(如:一个 viewModel )启动时才会执行获取演出列表的实际的动作,在数据返回前取消订阅将会取消该网络请求。

如果你不想使用信号、"期货"或类似的机制来代表你未来的数据,你也可以使用常规的 block 回调。但要记住,block 块嵌套地进行链式调用,如在某个网络请求依赖于另一个的结果的情况下,就会迅速变得非常笨重 --- 这种情况通常被称为“回调深渊"。

资源

Asset catalogs是管理你所有项目可视化资源的最好方式,他们可以同时管理通用的以及设备相关的(iPhoen4-inch,iPhone Retina,iPad 等)资源,并且会通过他们的名字自动分组。告诉你的设计师如何添加它们,(Xcode有内建的 Git 客户端)可以节省很多时间,否则你会很多时间从邮件或者其他渠道把它们复制到代码库中。同时,这样也可以让设计师即刻看到自己的改动,可以根据需求进行迭代。

Using Bitmap Images 使用位图

Asset catalog 只会暴露出一套图片的名字,省略了每张图片实际的文件名。这样类似 [email protected]这类文件的命名空间仅限于 asset 内部,很好地避免了 asset 的命名冲突。然而,命名 asset 时遵循一些原则可以让生活更轻松:

IconCheckmarkHighlighted.png // Universal, non-Retina
[email protected] // Universal, Retina
IconCheckmarkHighlighted~iPhone.png // iPhone, non-Retina
IconCheckmarkHighlighted@2x~iPhone.png // iPone, Retina
IconCheckmarkHighlighted-568@2x~iPhone.png // iPhone, Retina, 4-inch
IconCheckmarkHighlighted~iPad.png // iPad, non-Retina
IconCheckmarkhighlighted@2x~iPad.png // iPad, Retina

其中的 -568h@2x~iPhone以及~iPad这些标示符本省并不是必需的,但如果在文件名里加上它们,把文件拖动到 asset 时就能自动落到正确的"格子"上,因此能避免难以察觉的错误拖放。

Using Vector Images 使用矢量图

你可以把设计师设计的原始图矢量图(PDFs)放进 Asset catalog,让 Xcode 来自动生成位图。这样能减少工程的复杂度(减少文件的个数)。

编码风格

命名

Apple 非常注意在 API 中保持命名一致性,即便是非常冗长的命名也如此。做 cocoa 开发时要遵循 Apple的命名规范, 这样能让加入项目的新人轻松许多。

以下是几条看了就能用上的基本规则: 以动词开头的方法,表示它执行的操作会造成一些影响( 译者注:有时候是函数副作用 ),但是不返回任何值。 - (void)loadView; 或者 - (void)startAnimating;

以下注释来自"维基魔杖"

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。

函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。严格的函数式语言要求函数必须无副作用。

任何以名字开头的方法,应该返回一个对象并且不能造成额外的影响 (即不带函数副作用)。 - (UINavigationItem *)navigationItem; + - (UILabel *)labelWithText:(NSString *)text;

尽可能地区分这两种方法有很多好处。比如当您转换数据的时候就不应该造成额外的影响 ( 译者注:即函数副作用。数据转换的时候即上面那些使用名字开头的方法,实际上是一种数据转换的方法),反过来也一样(没有函数副作用的函数应该返回某个对象,具体可参考严格意义上的函数式语言的要求)。这样的话可以让具有函数副作用的代码保持在一个小的比较集中的区域内,可以帮助理解代码并有利于 Debug.(类似我们的初始化全局变量的方法或者那些设置控制属性的方法等)

代码结构

Pragma marks是给方法分组很好的方法,特别是在 ViewController 中。下面是 swift/Objective-C 语言的一个在 viewController 中常见的结构:

Swift MARK 风格:

import someExternalFramework

class FooViewController : UIViewController, FoobarDelegate {
    let foo: Foo
    
    private let fooStringConstant = "FooConstant"
    private let floatConstant     = 1234.5
    
    //MARK: LifeCycle
    
    //Custom initializers go here
    
    //MARK: View LifeCycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // ...
    }
    
    //MARK: Layout
    private func makeViewConstaints() {
        // ...
    }
    
    //MARK: User Interaction
    
    func foobarButtonTapped () {
        // ...
    }
    
    //MARK:FoobarDelegate
    func foobar(foobar: Foobar didSomethingWithFoo foo: Foo) {
        // ...
    }
    
    //MARK: Additional Helpers
    private func displayNameForFoo(foo: Foo) {
        // ...
    }
}

Objective-C MARK风格:

#import "someModel.h"
#import "someView.h"
#import "someController.h"
#import "someStore.h"
#import "someHelper.h"
#import <someExternalLibrary/someExternalLibraryHeader.h>

static NSString * const XYZFooStringConstant = @"FoobarConstant";
static CGFloat const XYZFooFloatConstant = 1234.5;

@interface XYZFooViewController () <XYZBarDelegate>
@property (nonatomic, readonly, copy) Foo *foo;
@property (nonatomic, strong) UILabel *label; //译者加

@end

@implementation XYZFooViewController

#pragma mark - LifeCycle

- (instancetype)initWithFoo:(Foo *)foo;
- (void)dealloc;

#pragma mark - View LifeCycle

- (void)viewDidLoad;
- (void)viewWillAppear:(BOOL)animated;

#pragma mark - Layout

- (void)makeViewConstraints;

#pragma mark - Public Interface

- (void)startFooing;
- (void)stopFooing;

#pragma mark - User Interface

- (void)foobarButtonTapped;

#pragma mark - XYZFoobarDelegate

- (void)foobar:(Foobar *)foobar didSomethingWithFoo:(Foo *)foo;

#pragma mark - Internal Helpers

- (NSString *)displayNameForFoo:(Foo *)foo;

#pragma mark - Setter / Getter (译者加)

- (UILabel *)label;

@end

最重要的是让这些分块标记在工程里所有的类里保持一致!

其他的编程风格

Futurice(作者所在的公司)并没有公司范围的编码风格指南。不过,仔细研究一下其他开发社区的 Objective-C 风格指南会非常有用,尽管有些部分可能只对特定公司有效或比较主观。

安全

即使在这样一个时代,我们信任我们的便携设备,让其携带自己最私有的数据,但 app 的安全性仍然是一个经常被忽视的主题。尝试对数据安全性的设定找到一个良好的权衡,以下有一些简单的经久耐用的法则。另外,Apple 的 iOS安全指南是一个很好的入门教程。

数据存储

如果你的 app 需要存储敏感数据,比如用户名、密码、认证 "Token" 或者一些个人的用户信息,你需要将它们保存在本地且不允许从 App 外部进行读取。绝不能用 NSUserDefaults或别的存放在闪存的 plist 文件,也不能用 CoreData 来做,因为他们没有加密!绝大多数类似的情况,iOS KeyChain是你的救星。如果不习惯直接使用 C 的 APIs,你可以使用像 SSKeyChain或者 UICKeyChainStore 这样的一些封装。

在保存文件和密码时,确保正确而谨慎地选择恰当的安全等级。如果在设备锁定时(比如后台任务)你还需要访问文件,使用 "accessible after first unlock" 选项即可。其他的情况下,你应该要求设备在解锁之后才能访问数据。仅在需要使用敏感数据时才读取。

网络

确保任何时候与服务端的 HTTP 通信都是 TLS 加密的。为避免中间人攻击窃听你的加密数据,你可以设置证书约束(certificate pinning),像 AFNetworkingAlamofire这种流行的网络库都支持这样通信。

Logging(日志打印)

发布你的 app 之前,应特别小心地设置好合适的日志级别。构建的产品(ipa文件)绝不能(日志)记录登录密码、API的Tokens等类似的敏感信息,因为这很容易导致将他们泄露给公众。另一方面,记录基本的控制流程可以帮你定位用户所遇到的问题。

UserInterface(用户界面)

当使用 UITextField做密码输入时,记住设置它们的 secureTextEntry 属性为 true,以免明文显示密码。同时也应该关闭其"输入自动校正"的功能,并在任何合适的时刻清空密码,比如当 app 退到后台时。

当 app 退到后台时,清空剪切板可以避免密码或其他敏感数据被泄露。由于 iOS 可能需要你 app 的屏幕截图,以显示在 app 切换器中,所以在 applicationDidEnterBackground 方法返回前,应该确保 UI 上显示的所有敏感数据被清空。

诊断

编译警告

建议把编译警告都打开,并且像对待 error 一样对待 warning。这份幻灯片论证了这一点。幻灯片里同时还讲到了如何在特定文件或特定代码段中忽略特定的 warning。 一句话,在 build setting 的 "Other Warning Flags"中至少要加入以下两个值:

  • -Wall (开启非常多的额外的 warning)
  • -Wextra (开启许多额外的 warning)

同时打开 build setting 里的 "Treat warnings as errors"

Clang静态分析器

Clang 编译器 (也就是 Xcode 适用的编译器) 有一个静态分析器(static analyer),用来执行代码控制流和数据流的分析,可以发现许多编译器检查不出来的问题。

你可以在 Xcode 的 Product ---> Analyze里手动运行分析器。

分析器可以运行"shallow"和"deep"两种模式。后者要慢很多,但是有跨方法的控制流分析以及数据流分析,因此能发现更多问题。

建议:

  • 开启分析器的 全部检查(方法是在 build setting 的 "Static Analyzer" 部分开启所有选项)
  • 在 build setting 里,对 release 的 build 配置开启 "Analyzer during 'Build'"。(真的,一定要这样做 --- 你不会记得手动跑分析器的。)
  • 把 build setting 里的 "Model of Analysis for 'Analyze'"设为 Shallow(faster)
  • 把 build setting 里的 "Model of Analysis for 'Build'"设为 Deep

由我们员工Ali Rantakari创作的 Faux Pas 是一个出色的静态 Error 检测工具。它能分析你的代码库,找出你全然不知的错误。在发布任何iOS (或 Mac)App之前务必要运行它一次!

Debugging

当 App 崩溃时,默认情况下 Xcode 不会进入 Debugger。要想进入 Debugger,需要添加一个 Exception Breakpoint (点击 Xcode 的 Debug Navigator 底部的"+"号),遇到 Exception 时就会暂停执行。在大部分情况下,你都能看到导致 Exception 的那行代码。 这种方法会捕捉到任何 Exception,包括已经做了处理的 Exception。如果Xcode经常停在良性的 Exception 上(比如第三方库),选择 Edit Breakpoint然后在 Exception 下拉菜单中选择 Objective-C可以减少这种情况的出现。

在 View 的 Debug 方面, RevealSpark Inspector是两个强大的可视化检查器,可以节约你大量的时间,尤其是用 AutoLayout 时想知道消失的视图去哪儿了的情况。 Xcode 也免费提供了一个类似的东西,不过只支持 iOS8 + ,并且还不够完善。

Profiling评测

Xcode 自带一套评测工具 "Instruments"。它包含了众多的评测工具:评测内存使用、CPU、网络连接、图像等等。它本身是个庞然大物,但一个比较简单直接的用途是用 Allocations Instrument 来检测内存泄露。只需在 Xcode 中选择 Product ---> Profile,选择 Allocations instrument,点击 Record按钮,然后从 Allocation Summary中过滤一些有用的字符串,比如 app 中你自己写的类的类名前缀。在 Persistant一栏中的计数显示了每个对象有多少个实例。如果某个类的实例个数一直胡乱增长,说明有内存泄露。

众所周知的是 Instruments 有一个 Automation 工具可以把 UI 交互录制为 JavaScript 文件并重放。UI Auto Monkey是一个脚本,它可以借助 Automation 在你的 App 上随机点击、清扫、旋转,这对压力测试/浸泡测试很有帮助。

要格外注意的是,你在哪里以何种方式创建了巨耗资源的类。举个栗子,NSDateFormatter创建起来非常耗资源,当快速而连续这么做时,比如在 tableView:cellForRowAtIndePath:方法中,会真正减慢 App 的响应速度。你应该创建一个它的 static 实例,并在需要格式化日期时直接使用该实例。

统计分析

强烈推荐在你的 App 中添加一个统计分析的框架,它能帮助你看到用户实际上是怎么用你的 App 的。X 功能有价值吗?按钮 Y 太难找到了吗?要回答这些问题,可以把点击事件、计时以及其他可测的信息发送到一个能收集并可视化这些信息的服务上,比如 Google Tag Manager。Google Tag Manager 比 Google Analytics 更灵活一些,它在 App 和 Analytics 之间插了一个数据层,因此不须更新 app 就可以通过 web service 更改数据逻辑。

一种好的做法是加一个轻量的辅助类,比如 XYZAnalyticsHelper,用来把 App 内部的 model 和数据格式(XYZModel, NSTimeInterval等)翻译成以字符串为主的数据层。

Swift :

fun pushAddItemEventWithItem(item: Item, editMode: EditMode) {
    let editModeString = nameForEditMode(editMode)
    
    pushToDataLayer([
        "event" : "addItem",
        "itemIdentifier" : item.identifier,
        "editMode" : editModeString
    ])
}

Objective-C :

- (void)pushAddItemEventWithItem:(XYZItem *)item editMode:(XYZEditMode)editMode {
    NSString *editModeString = [self nameForEditMode:editMode];
    
    [self pushToDataLayer:@{
        @"event" : @"addItem",
        @"itemIdentifier" : item.identifier,
        @"editMode" : editModeString
    }];
}

这样做有一个额外的好处:在有必要时,可以清除掉整个统计分析框架,而 App 其余的部分不受任何影响。

CrashLogs崩溃日志

首先应该让 App 把崩溃日志发送到某个服务器上,这样你才能看得到。可以使用 PLCrashReporter结合自己的后台实现这个功能,但推荐使用已有的第三方服务,比如下面这些:

设置好这些之后,每次发布都要确保保存了 Xcode archive(.xcarchive).Archive 里包含编译出的二进制文件以及 Debug symbol( dSYM ),你需要这些数据来解析这个版本 App 的崩溃报告。

编译构建

本节包含了关于这个主题的概述 --- 更全面的信息请参阅这里:

iOS Developer Library: Xcode Concepts

Samantha Marshall: Managing Xcode

编译配置

即使最简单的 App 也有不同的构建方式。 Xcode 提供的最基本的区别是DebugRelease模式。后者的编译时优化要强很多,代价是损失了 Debug 的可能性。苹果建议你开发时使用 Debug模式,提交到 AppStore 的包用 Release模式编译。默认的模式(在 Xcode 里的运行/停止按钮旁边的下拉菜单可以更改)就是这么设置的,Run 用 Debug, Archive 用 Release

不过对于真实的应用,这样还是过于简单。你可以--- 不!是应该 --- 有几套不同的环境,分别用于测试、更新和其他与服务相关的操作。每套环境都可以有自己的 base URL、log 级别、bundle identifier (这样就可以同时安装)、provision profile 等。因此,简单的 Debug/Release 不能满足需求。你可以在 Xcode 工程设置的 "Info" 一栏里添加更多的编译配置。

编译配置的xcconfig文件

编译配置一般是在 Xcode 的界面里设置的,不过你也可以使用配置文件(".xcconfig 文件")来设置。这样做的好处是:

  • 你可以添加注释来进行解释;
  • 你可以 #include其他编译文件,帮助避免重复:
    • 如果你有一些所有配置通用的设置,添加一个 Common.xcconfig 文件,然后把它 #include 到其他文件里;
    • 比如你想要加一个在 "Debug" 基础上开启编译优化的配置,只需 #include "MyApp_Debug.xcconfig",然后覆盖相应的设置
  • 合并和解决冲突更简单一些

更多关于本话题的信息,可以参考这些幻灯片

Targets

Targets 的概念比 project 低一个级别,即一个 project 可以有多个 targets,这些 targets 的设置 可以覆盖它的 project 的设置。粗略地说,每一个 target 对应着代码库上下文中的一个 app。举个栗子,你可能针对不同国家的 Appstore 有不同的 App (都是从同一个代码库编译出来的)。每一个 App 都需要 开发/staging(阶段性成果)/发布 的编译配置,因此用编译配置(build configurations)会比 target 更好一些。一个 App 对应只有一个 target 非常常见。

Schemes

Schemes 告诉 Xcode 在 Run、Test、Profile、Analyze 和 Archive 时分别应该干什么。基本上,以上每个操作的 Scheme 对应一个 target 和 一套编译配置。你也可以传递启动参数,比如 App 运行的语言(对于测试本地化很方便)或者设置一些 Debug 用的诊断标记。

Scheme 推荐的命名方式是 MyApp(<language>) [Environment]:

MyApp (English) [Development]
MyApp (German) [Development]
MyApp [Testing]
MyApp [Staging]
MyApp [AppStore]

大部分环境下,语言是不需要标明的,因为 App 有可能通过 Xcode 之外的途径安装,比如 TestFlight,这样启动参数就会被忽略,这种情况下,只能手动设置设备语言来测试本地化。

部署

将 app 安装到 iOS 设备上并不简单。那么我们在这里会介绍几个核心的概念,理解了这些概念会对你部署 app 有很大帮助。

Signing签名

只要你想把应用跑在真机上,你就需要在编译时用一个 Apple 颁发的 证书来签名。每一个证书对应一对公钥/私钥,私钥保存在你Mac的钥匙串中。证书有两种:

  • 开发证书:团队里的每个开发者都可以通过请求获得自己的开发证书。Xcode 可以自动完成这项工作,不过最好还是不要点击那个神奇的 "Fix issue"按钮,而是自己做一遍来理解这个过程到底做了什么。要把开发环境打的包安装到设备上就需要开发证书。
  • 分发证书:可以有多个,不过最好还是限制为每个组织一个,然后通过内部渠道分享它相关联的密钥。要发布到 AppStore 或者企业的内部 "Appstore",需要这个证书。

Provisioning(证书)配置

除了证书之外,还有 Provisioning profiles(配置文件),它是关联证书与设备的一环。同样有两类,分别用于开发和发布:

  • Development provisioning profile (开发配置文件):它包括被授权安装/运行 App 的设备列表。同时它与一个或多个开发证书相关联,每一个开发证书对应一个可以使用这个 profile (配置文件)的开发者。这种 profile 可以与特定的 App 绑定,但对于开发的用途,大部分用通配的 profile 即可(AppID 以星号*结尾,比如 "net.senink.*")。
  • Distribution provisioning profile (分发配置文件):有三种分发途径,每一种的使用情景都不同。每个 distribution profile 与一个分发证书相关联,证书过期即失效。
    • Ad-Hoc:与开发证书相同,它包含可以安装 App 的设备白名单。这种 profile 可以用来再每年最多100个设备上做 beta 测试(译者注:最近 Apple 放宽了限制:同种设备每年可以各有100个,即iPhone 100 ;iPad 100 ;iPhone touch 100 ...),如果想通过规模更大的测试来改善设计及用户体验,可以使用 Apple 新推出的 TestFlight服务。Supertop 上对它的优势和问题做了很好的总结
    • AppStore:它没有包含设备列表,因为任何人都可以通过 Apple 的官方分发渠道安装。发布到 Appstore需要这种 profile。
    • Enterprise:和 Appstore 属于同一类型,没有设备白名单,任何人都可以通过企业内部的 "AppStore"来安装 App。

要把所有的证书和 profile 同步到你的设备上,在 Xcode 的 Preference 中的 Accounts里添加你的 Apple ID,然后双击团队(team)名称。底部有一个刷新按钮,但有时需要重启 Xcode 才能正常刷新。

DebuggingProvisioning配置文件的调试

有时你需要 Debug 一个 provisioning 问题。比如,Xcode 可能拒绝把包安装到设备上,因为设备不在(development 或 ad-hoc 的) profile 的设备列表上。这种情况下,你可以使用 CraigHockenberry 优秀的 Provisioning插件定位到~/Library/MobileDevice/Provisioning Profiles中,选择.mobileprovision文件然后按空格键启动 Finder 的快速搜索功能,它会展示出非常丰富的信息,包括:设备、授权、证书和 App ID 等。

上传

iTunes Connect是苹果 AppStore 上的 App 管理平台。上传一个包,Xcode 需要一个开发者账户的 Apple ID 来签名。如果你有多个开发者账户,想要分别上传他们的 App,可能遇到一些麻烦,因为不知道为什么 一个特定的 Apple ID只能与一个 iTunes Connect 账户相关联。替代的方法是,为每个 iTunes Connect 账户都创建一个新的 Apple ID,然后使用 Application Loader 代替 Xcode 来上传包。这样就把打包签名与上传 .ipa 文件的过程解耦了。

上传包之后,保持耐心,可能一个小时后这个版本的 App 才会出现在 Builds 一栏,当它出现后,你可以把它与 App 的版本信息关联起来,然后提交审核。

内购

验证 App 内购的收据时,请记得进行以下检查:

  • 真伪性: 购买收据是否确实来自 Apple;
  • 完整性: 收据有没有被篡改;
  • 应用匹配: 收据中的 Bundle ID 是否与你的 App 的 Bundle ID 相符;
  • 产品匹配: 收据的 product ID 是否与你预期的 product ID 相符;
  • 是否最新: 在这之前有没有见过相同的收据ID;

设计你的 IAP 系统时,尽量把售卖的内容存储再 server 端,然后仅当收到有效的、通过以上所有检查的收据后才把内容提供给 client 端。这样的设计防止了常规的盗版机制,并且--- 既然验证是在 server 端进行的 --- 你可以利用 Apple 的 HTTP 收据验证服务,而不是自己解析收据的 PKCS #7 / ASN.1格式文件。

关于这个问题,更多的信息请参考Futurice blog:Validating in-app purchases in your iOS app

授权

Futurice 署名 - 相同方式共享 4.0 国际许可协议(CC BY 4.0)

计划

  • 添加常用的编译警告
  • 添加如何使用Jenkins自动化打包分发
  • 添加一个跟测试相关的小节
  • 添加注意事项

备忘录

组织项目文件目录的一点建议

jpmcgione的建议

在之前的指南中,项目的目录结构是这样的:

|- Models
|- Views
|- Controllers (or ViewModels, if your architecture is MVVM)
|- Stores
|- Helpers

我发现随着 App 的业务量越来越大,特别是当你在多个顶级目录中嵌套具有共同任务特性的文件目录 (如:Views/Login { LoginHeaderView.swift } 和 Controllers/Login/{ LoginViewController.swift })时,我特别推荐下面这样的目录结构:

|- Models
|- Application // Not common views or controls, etc.
    |- Main
    |- Context A
    |- Context B
|- Common
    |- Views/Controls
    |- Extensions
    |- Context X
|- Assets

举个栗子:

|- Models
    |- User.swift // user model
    |- Post.swift // post model
|- Application
    |- Main
        |- AppDelegate.swift
        |- MainViewController.swift // Maybe your app is wrapped in a main view controller, could be a tab bar controller subclass, or some custom view controller that has the neat functionality
    |- Login
        |- LoginManager.swift // business logic. A sington that any controller in the app can interact with
        |- LoginViewController.swift // view controller
        |- LoginHeaderView.swift // a view that only belongs to login
    |- Feed
        |- FeedViewController.swift
        |- FeedTableViewCell.swift
    |- Common
        |- Controls
            |- BouncingButton.swift
        |- Extensions
            |- UIView+Additions.swift //useful addtions to UIView
            |- NSURL+Additions.swift //useful addtions to NSURL
|- Assets
    |- Images.xcassets
    |- Sounds
        |- success.wav
    |- Videos
        |- intro-onboarding.mp4

^ 文件结构浅,突出程序上下文的内容(及一个功能模块会放在一个文件下,或许当这个功能模块所涉及的Views 或 Manager 比较多时 也可以在当前文件目录下追加嵌套一个 Views/ 或者 Manangers/ 类似的目录 )

现在再用你所提出的目录结构来做同样的布局,比较一下:

|- Models
    |- User.swift
    |- Post.swift
|- Views
    |- Login
        |- LoginHeaderView.swift
    |- Feed
        |- FeedTableViewCell.swift
    |- Common
        |- Controls
            |- BouncingButton.swift
|- Controllers (or ViewModel, If your architecture is MVVM)
    |- Login
        |- LoginViewController.swift
    |- Feed
        |- FeedViewController.swift
|- Stores
|- Helpers
    |- Manangers
        |- LoginMananger.swift
    |- Extensions
        |- UIView+Additions.swift
        |- NSURL+Additions.swift

^ 这个文件目录结构很深,显得有点多余。突出了程序的 Class 类型。

不是很确定你的模型中 Assets 资源目录放在何处,但可以想象这跟我的存放位置差不多。

无论是在一个大的团队里面协作,还是团队刚开始很小但迅速发展成一个大团队,我所推荐的结构都更易于管理。

我所推荐的目录结构还有一些额外的优势:

  • 与通过 "用心"地排列文件相比,这种结构更容易理解项目中所有的一切。

对我而言,它是以项目或以文件类型(在这种情况下,模型、视图控制器)来组织的一些差别。想象一下你正在准备一个演讲,这个演讲稿包含一个 .doc ,一个 .ppt 还有一个 .pdf 文件,但你没有把它们都放在一个叫做"给CEO演讲的重要文件"的文件夹下,而是把它们分别放在3个文件夹下面(Documents, Powerpoint, PDF)的子文件夹 ("给 CEO 演讲的重要文件")中。实际上我们从来不会这么处理,在每一个我们创建的其他项目中(指生活中的一些行为),我们按照内容而非类型来组织。那么对 App 来讲,为什么我们不这样做呢?(显然我们没有理由不这么做~)

  • 项目文件的合并冲突可能性会更少。

如果队员正在修改 login 而你正在修改 feed,在我的模板中,你不太可能添加文件到对方的文件目录中。然而,在你的模型中,我们却不得不这么做。相对于每一个功能特性而言,都意味着我们可能添加文件到 Models, Controllers, Views等等,满满的都是合并冲突的机会啊!

你所倡导的模型让有 Ruby on Rails 背景的我想到一个问题:你有 web 前端开发的背景吗?

补充说明

Oh! 在你的模型中,你仍然可以轻松地搜索你的 Models, Views 或者 ViewControllers 如果你使用一个通用的命名规范的话。

如:

将所有的 models 命名为类似 UserModel.swift 和 PostModel.swift

ViewControllers 命名为类似 LoginViewController

Manangers 命名为类似 LoginManager

非视图控制器命名为类似 FeedController

分类扩展命名为类似 'aClass'+Additions

在 Xcode 的左下角的文件搜索框中键入 'Model', 'ViewController' 或者 '+Additions' 应该可以缩小搜索范围来找出相似的文件。

mkauppila评价

非常享受你对自己所建议的项目文件目录组织的全方位的解说,非常棒!

实际上,在一些项目中我使用了相似的面向上下文(context-oriented)的目录结构而且非常好用。我赞同将文件按照他们的上下文而非他们的类型来组织。至少对我而言,当功能特质类似的文件都存放在一个文件目录下时,在 Xcode (或者 AppCode )中工作起来会更加简单.它大大减少了鼠标点击'打开'/'关闭'IDE中的组文件夹的频率。此外,它也有助于将彼此的功能模块的概念分得更加清晰。

虽然我确实发现,举例来说,如果一个视图被多个上下文共用,把它放在一个名为 common 的组文件夹中有一点尴尬,主要是因为很难有足够的理由来说明它到底会被用在何处。但这是不可避免的,虽然在实践中我已经注意到它所带来的小小的不便。

我很乐于添加这个项目文件目录结构到这个文档中。对我而言,比起现在文档中所倡导的方式,它是更好的实践。@richeterre 你认为呢?

jpmcgione 回复

谢谢评价!

是啊,我在浏览一些"最佳实践"的开源库并搜索了很多 blog 文章及相关资料,因为我真的想组建一个自己的最佳实践。我看到许多项目采用了这个'最佳实践'的建议(采用了诸如:views, models, controllers 作为顶级文件目录),而我刚好在使用其他方式来组织文件目录时获得了更好的体验。

但我必须说,整体上我爱这个指南:D 有很多非常棒的技巧,特别是对刚刚进入 iOS 开发领域的团队或个人来说。

哈哈 @jpmcglone ,见证奇迹的时刻!刚好在几天之前我与我亲爱的同事 @richeterre 对这个问题有过激烈的争论,我主张类似你这样的以上下文为基础来组织文件的目录结构。你在这里提供了一些无与伦比的银弹~

我认为最容易出问题的是那些可复用的文件,但你在这里提出的解决方案似乎是一个可行的折衷的办法。在当前的项目中,我们正在向更加类似于 VIPER 架构的方向迁移,这意味着每一个功能特性(上下文亦或模块)都会被包含:

FeatureView
FeatureViewController
FeaturePresenter
FeatureInteractor
FeatureDataManager (still working on a better name for this component)

在这种情况下,基于上下文的树状结构就更有道理了。

(我有一点暗暗地怀疑,我陷入了严重依赖于利用 Cmd+Shift+O 组合键查找文件的最主要原因可能是我们的目录组织方案所造成的。。。)

项目主要负责人之一的richeterre回复

首先,非常感谢你建议 @jpmcglone !我同意当你以任何形式创建功能分组时,我们目前提出的这套目录结构是没有意义的。然而,在一些项目中我们简单地将所有的 views (或者 view models, helpers ...)归纳起来而没有使用更深一层的子目录。我们将所有的这些组链接到硬盘上对应的文件夹,避免所有的文件处在同一个文件目录下。你怎么处理这个的?

不管怎样,看来我们现在真得在一个项目中好好试试你的建议,并在之后在文档中更新我们的研究结果。

点赞 +1

jpmcglone 回复

@richeterre Ah,只有一层深吗。那很有意思!我想它简化了一点(相对于我之前的描述而言)。

你要去试一试我这一套结构,那真是太酷了。我期待着你的汇报:D。我的所有项目都是这样做的,我发现合并多个分支而没有一个合并冲突的感觉真是太棒了。我希望它能够带给你同样的感受!

译者

  KevinHM,喜欢就 Follow 吧,更多精彩将分享给您!