刘浩的技术博客

就当是做个笔记,顺便分享一些知识,更希望业界的交流

实现独特APP特性的策略

实现特定APP特性的策略(Strategies for Implementing Specific App Features)

不同的APP有不同的需要,但是有一些行为对于许多的APP是共同的。接下来的块提供了指导关于在你的APP中实现特定的特性。

隐私策略(Privacy Strategies)

在设计一个APP的时候保护用户的隐私是一个重大的考虑。隐私保护包含了保护用户数据,用户身份和个人信息。系统框架已经提供了关于管理数据的隐私框架,例如联系人,但是你的APP应该采取措施去保护你的本地使用的数据。

保护数据使用在磁盘加密(Protecting Data Using On-Disk Encryption)

数据保护使用内置硬件使用一个加密的格式去存储文件到磁盘,需要它们的时候再解密。在用户锁定屏幕期间,保护文件是访问不到的,即使是创建它们的APP也不行。你个APP能够访问一个它保护的文件必须是这个设备不是屏幕锁定的(通过键入适当的密码解锁)。

数据保护在大部分iOS设备是可用的,但需要服从以下的要求:

  • 在用户设备上的文件系统必须支持数据保护.大部分设备支持这个行为.
  • 这个用户必须有有效的密码用于锁定这个设备.

去保护一个文件,你添加一个特性到这个文件只是所需要的保护等级。添加这个特性使用NSData类或者NSFileManager其中的一个。当写一个新文件,你能使用NSDatawriteToFile:options:error:方法并用一个合适的值作为写选项参数。关于现存的文件,你能使用NSFileManagersetAttributes:ofItemAtPath:error:方法去设置或者改变 NSFileProtectionKey的值。当使用这些方法,给你文件指定一个以下的保护等级。

  • 没有保护——这个文件是加密的但是没有通过密码保护,在这个设备锁定期间是可用的。指定NSDataWritingFileProtectionNone选项(NSData)或者NSFileProtectionNone属性(NSFileManager).
  • 完全的保护——这个文件是加密的,在设备锁定期间是不可访问的。指定 NSDataWritingFileProtectionNone选项(NSData)或者NSFileProtectionNone属性(NSFileManager).
  • 完全保护但是除了已经打开的文件——这个文件是加密的。在设备锁定期间一个关闭的文件是不可访问的。用户解锁这个设备之后,你的APP能够打开这个文件并使用它。在文件打开期间用户锁定了设备你的APP能继续去访问它。指定NSDataWritingFileProtectionCompleteUnlessOpen选项(NSData)或者NSFileProtectionCompleteUnlessOpen属性(NSFileManager).
  • 完全保护直到第一次登录——这个文件是加密的,这个设备启动后文件是不可访问的,用户一旦解锁这个设备文件就可以访问了。指定 NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication选项(NSData)或者NSFileProtectionCompleteUntilFirstUserAuthentication属性(NSFileManager).

如果你保护一个文件,你的APP必须准备去失去对这个文件的访问。当完全的文件保护启用时,当用户锁定屏幕你的APP失去了读写这个文件内容的能力。你能使用接下来的技术跟踪保护文件状态的改变。

  • 实现APP代理的applicationProtectedDataWillBecomeUnavailable:applicationProtectedDataDidBecomeAvailable:方法。
  • 任何对象能够注册UIApplicationProtectedDataWillBecomeUnavailableUIApplicationProtectedDataDidBecomeAvailable通知。
  • 任何对象能够验证UIApplication共享对象的protectedDataAvailable属性值去确定当前文件是否是可访问的。

关于新文件,在写任何数据到这个文件之前你去启用数据保护是被推荐的。如果你使用writeToFile:options:error:方法去写一个NSData对象内容到磁盘,这个是保护自动发生的。关于现存的文件,用一个新的数据版本添加数据保护去替换一个未保护的文件。

识别你的APP的独特用户(Identifying Unique Users of Your App)

仅仅在你去识别你的APP的一个用户能够提供一个明显的好处给你的用户的时候你才应该去做这个。你如果仅仅需要去区分你的APP的一个用户是不是来自另外一个,iOS提供了标识符能够帮助你去做这个。无论如何,如果你需要一个高的安全级别,你可能需要自己做更多的工作。

Important:当识别一个用户时,总是明确的关于你想要用你获得的信息做什么。识别用户是不可接受的,这样你能秘密的跟踪它们.

这里是一些你需要去识别用户的常用的场景,以下是一些关于如何实现它们和一些解决方案.

  • 你想去链接一个用户到一个特定的账户。包含一个登录屏幕需要用户去键入它们的安全账户信息。总是保护你收集来自用户的账户信息通过加密的形式去存储。
  • 你想去区分你的APP实例运行去不同的设备上。使用UIDevice类的identifierForVendor属性去获取一个ID,这个用于区别在不同设备的的同一个用户。这个技术现在允许你去识别特定的用户。当个用户可能有多个设备,在每个设备上有不同的ID值。
  • 你想去识别关于广告目的的用户。使用ASIdentifierManager类的advertisingIdentifier属性去获取关于这个用户的ID

因为用户是允许去运行APP到他们所有的iOS设备上的,Apple不提供一个方法去识别同样的用户在多个设备上,如果你需要去识别一个特定的用户,你必须提供你自己的解决方案使用通用的唯一IDs(UUIDS),一个登录账户,或者一些另外的身份识别类型系统。

关于限制(Respecting Restrictions)

用户能够设置限制,指定想去在你的APP中使用的媒体的等级。如果你的APP播放媒体或者修改基于限制的行为,你需要去确定当前的设置和去响应设置的改变。(访问限制就是用户可以在通用里面设置不同国家和地区的对你的APP所包含内容的访问限制,你可以选择那些内容是可以看到,那些APP是可以运行的,这个是分级制度保护不同阶段的人看到不同的内容,比如儿童可以设置一下限制级的内容限制访问)。

去获得当前的设置,获得共享对象standardUserDefaults和使用它的objectForKey:方法去查看以下键的值:

  • com.apple.content-rating.ExplicitBooksAllowed Boolean。如果这个键的值是NO,明确书是不允许的。
  • com.apple.content-rating.ExplicitMusicPodcastsAllowed Boolean。如果这个键的值是NO,明确音乐内容,电影,和播客是不允许的。
  • com.apple.content-rating.AppRating NSNumber。这个键的值在0到1000的范围内。一个APP的等级比这个当前的键值高是不被允许的。
  • com.apple.content-rating.MovieRating NSNumber。这个键的值在0到1000的范围内。一个电影的等级比这个当前的键值高是不被允许的。
  • com.apple.content-rating.TVShowRating NSNumber。这个键的值在0到1000的范围内。一个电视节目的等级比这个当前的键值高是不被允许的。

Note:关于一个指定的键如果objectForKey:返回nil,它意味着关于这个特定限制的信息是无法拿到的。在这种情况下,你的APP能够使用它自己的策略去确定适当的分级。

去发现用户对限制的改变,注册NSUserDefaultsDidChangeNotification通知。当发现一个持久域中本地偏好设置改变这个共享对象standardUserDefaults发送这个通知到你的APP。

支持多个iOS版本(Supporting Multiple Versions of iOS)

一个APP支持最新的iOS版本加一个或者多个以前的版本必须使用运行时检查去阻止在旧的iOS版本上使用新的APIs.当你的APP尝试使用一个在当前操作系统上不能获得的特性时运行时检查防止你的APP崩溃。

这些是几种你能做的检查类型:

  • 去确定是否一个类存在,看是否Class对象返回nil。链接器对于所有未知的类对象都返回nil,使用一个条件检查就像以下这样:
1
2
3
4
5
6
if ([UIPrintInteractionController class]) {
      // Create an instance of the class and use it.
}
else {
      // The print interaction controller is not available so use an     alternative technique.
}
  • 去确定一个方法在一个现有的类是否可以获取,使用instancesRespondToSelector:类方法或者respondsToSelector:实例方法。
  • 去确定一个基于C的函数是否可以使用,执行这个函数去和NULL比较返回一个布尔值。如果不为NULL,你能调用这个函数。如下:
1
2
3
  if (UIGraphicsBeginPDFPage != NULL) {
      UIGraphicsBeginPDFPage();
  }

保存你的APP的视觉外观(Preserving Your App’s Visual Appearance Across Launches)

即使你的APP支持后台执行,它也不可能永远运行。在一些点,系统可能需要终结你的APP释放内存给当前的前台APP使用。无论如何,用户应该绝不关心一个APP已经运行还是已经终结。从用户的角度看,退出一个APP应该恰恰看起来像是一个临时中断。当用户返回到一个APP时,APP应该总是返回到用户使用这个APP的最后位置,如此用户能继续运行中的任何任务。这个行为提供了一个好体验给用户,状态恢复支持内建到UIKit里面这个获得是相对容易的。 状态保存系统在UIKit里面提供一个简单但是灵活的结构用于保存和恢复你的APP的视图控制器和视图状态。这个结构的工作是在适当的时间去驱动保存和恢复处理。做这个,UIKit需要你的APP的帮助。只有你了解你的APP的内容,如此只有你能写去保存和恢复这个内容的代码。当你更新你的APP用户界面时,只有你知道如何在你的界面去映射旧的保存内容到一个新的对象。

这里是三个地方,在这里你去考虑状态保存在你的APP里。

  • 你的APP代理对象,管理APP的最高级别的状态
  • 你的APP视图控制器对象,管理你APP全部的用户界面状态。
  • 你的APP的自定义视图,可能有一些你需要保存的自定义数据。

UIKit允许你去选择你想保存的用户界面部分。如果你已经有自定义的代码关于操作状态保存,你能继续使用这个代码和作为需要你能迁移部分代码到UIKit保存系统。

在你的APP中启用状态保存和恢复(Enabling State Preservation and Restoration in Your App)

状态保存和恢复不是一个自动特性,APP必须选择去使用它。APP表明自己支持这个特性通过在它们的APP代理中实现以下的几个方法:

  1. application:shouldSaveApplicationState:
  2. application:shouldRestoreApplicationState:

通常的,你实现这些方法只有返回YES才能表明状态保存和恢复能够发生。当然也能返回NO,不过状态保存和恢复不会发生。例如你的APP无法有效的去恢复状态来自一个以前的版本你就可以在application:shouldRestoreApplicationState:方法里返回NO

保存和恢复过程(The Preservation and Restoration Process)

状态保存和恢复是一个可选的特性,工作需要你的APP的帮助。你的APP标识应该保存的对象,UIKit在合适的时间去做保存和恢复这些对象的工作,因为UIKit处理这么多的过程,它帮你你去明白他在幕后做了什么,如此你知道如何实现你的自定义代码去契合这个总体的方案。

当思考关于状态保存和恢复时,它首先帮助你去分离这两个过程。UIkit在适当的时间保存你的APP状态,例如当你APP从前台移动到后台的时候。当UIkit确定新的状态信息是必要的时候,它审视你的APP视图和视图控制去看哪一个应该被保存。关于这些对象的每一个,UIkit写保存相关的数据到一个磁盘上的加密文件。在下次你的APP重新开始启动,UIkit找到这个文件,如果它是存在的,使用它去尝试恢复你的APP状态。因为这个文件是加密的,只有在这个设备解锁的状态下状态保存和恢复才会发生。

在恢复过程期间,UIkit使用保存数据去重新构造你的界面但是实际对象的创造是通过你的代码操作的。因为你的APP可能自动加载一个对象来自storyboard文件,只有你的代码知道哪一个对象需要去创建和哪一个可能已经存在能够简单的返回。创建每一个对象之后,UIkit用保存的状态信息初始化它们。

在保存和恢复处理期间,你的APP有少数的责任。

  • 在保存期间,你的APP的职责:

    1. 告诉UIkit它支持状态保存。
    2. 告诉UIkit那个视图控制和视图应该保存。
    3. 为任何保存的对象编码相关的数据。
  • 在恢复处理期间,你的APP的职责:

    1. 告诉UIkit它支持状态恢复。
    2. 提供(或创建)UIkit要求的对象。
    3. 解码你保存的对象状态和使用它去返回这个对象到它以前的状态。

你的APP的责任,最重要的是告诉UIkit那些对象要保存和之后的启动提供这些对象。这两个行为是在你设计你的APP的保存和恢复代码的时候应该花费你最多时间的地方。他们同样是你在这个实际过程中有最多控制的地方。去明白为什么是这个样子的。 UIkit只保存已经分配了恢复标识的对象。一个恢复标识是一个字符串,这个标识视图或者视图控制器,这个字符串的值仅仅对于你的代码有意义,但是这个字符串的存在告诉UIkit它需要去保存这个标记的对象。在保存处理期间,UIkit走你的APP视图控制器层级,保存所有的有恢复标识的对象。如果一个视图控制器没有一个恢复标识,这个视图控制器和所有它的视图和子视图控制器是不保存的。

根据你的APP,它可能有或者没有去保存每一个视图控制器的场景。如果一个视图控制器去呈现一个短暂的信息,你可能在恢复的时候不想返回到这个相同的点,而且选择返回到你的界面的一个稳定的点。

关于你选择去保存的每一个视图控制器,你同样需要去决定之后你想如何去恢复它。UIkit提供两个方法去重建对象。你能让你的APP代理重建它或者你能分配一个恢复类给这个视图控制器和让这个类重建它。一个恢复类(restoration class)实现UIViewControllerRestoration协议和它的职责是关于在恢复时间寻找或者创建一个指定的对象。这里有一些小技巧:

  • 如果视图控制器在启动时间总是从你的APP的主storyboard加载,不要分配一个恢复类。作为替代,让你的APP找到这个对象或者利用UIkit支持隐式查找恢复对象。
  • 如果视图控制器在启动时间不从你的APP的主storyboard加载,分配一个恢复类。这个简单的选择是去为每一个视图控制器制作它自己的恢复类。

在保存过程期间,UIkit标识这个对象去保存和写每一个受影响的对象状态到磁盘。每一个视图控制器对象给一个机会去写出任何他想要去保存的数据。

在下次APP启动后,UIkit照例加载APP的主storyboard或者nib文件,调用APP代理application:willFinishLaunchingWithOptions:方法,尝试去恢复APP以前的状态。这个第一件事是询问你的APP提供的视图控制器对象集合用于去匹配那个是保存了的。如果给的视图控制器有一个指定的恢复类,询问这个类去提供这个对象;否则,询问APP的代理提供这个对象。

保存的处理流程(Flow of the Preservation Process)

保存甚至发生之前,UIkit询问你的APP代理如果它应该发生就通过调用application:shouldSaveApplicationState:方法。如果这个方法返回值为YES,UIkit开始收集和编码你的APP视图和视图控制器。当这个过程结束,它写编码后的数据到磁盘。 在下次你的APP启动,系统自动找寻一个保存状态文件,如果存在,使用它去恢复你的界面。因为这个状态信息是只与你的APP在以前状态和当前启动周期相关的,这个文件在你的APP结束启动之后通常会被丢弃。这个文件同样在任何时间恢复你的APP有一个错误发生时被丢弃。例子,如果你的APP在恢复处理期间崩溃,系统自动扔掉这个状态信息避免在下一次启动周期再次发生崩溃。

恢复的处理流程(Flow of the Restoration Process)

在标准初始化和UI加载完成之后,UIkit询问你的APP代理,如果状态恢复应该发生通过调用application:shouldRestoreApplicationState:方法。这个是你的APP代理去检查保存数据和确定状态恢复是否可能的机会。如果它是,UIkit使用APP代理和恢复类去获取到你的APP视图控制器的引用。每一个对象去提供它需要的数据去恢复它自己到它以前的状态。 尽管UIkit帮助恢复单独的视图控制器,它不自动恢复这些视图控制器之间的联系。作为替代,每一个视图控制器有责任去编码足够多的状态信息用于返回它自己到以前的状态。例子,一个导航控制器编码信息关于在它的导航栈上视图控制器的顺序。它使用这个信息在之后去返回这些视图控制器到它们以前在导航栈的位置。其他的视图控制器有嵌入子视图控制器有同样的责任关于编码任何它们需要的信息用于之后去恢复它们的子类。

Note:不是所有的视图控制器需要去编码它们的子视图控制器,例如tab bar控制器就不.作为替代,它假设你的APP遵循通常的模式,在创建tab bar控制器之前创建恰当的子视图控制器.

因为你的的职责是关于重建你的APP视图控制器,在恢复处理期间你有一定的灵活性趣改变你的界面。例如,你能够重排序一个tab bar控制器中的tab和仍然使用保存数据去返回每一个tab到它以前的状态。当然,如果你对你的视图控制器层级做了巨大的更改,例如在一个应用程序更新期间,你可能不能使用保存数据。

当你被逐出了视图控制器组时发生了什么(What Happens When You Exclude Groups of View Controllers?)

当一个视图控制器的标识符为nil,这个视图控制器和它管理的任何子视图控制器是不自动保存的。

即使你决定不保存视图控制器,这么做不意味着这些所有的视图控制器完全从视图层级消失。在启动时间,你的APP可能仍然创建这个视图控制器作为默认设置的一部分。例如,如果任何视图控制器是自动加载来自于你的APP的storyboard文件,他们将仍然出现,虽然是在他们默认的配置(或者说状态).

有些事需要了解的是,即使一个视图控制器没有自动保存,你还可以编码对这个视图控制器的引用然后手动保存它。

实现状态保存和恢复清单(Checklist for Implementing State Preservation and Restoration)

支持状态保存和恢复需要修改你的APP的代理和视图控制器对象去编码和解码这个状态信息。如果你的APP有任何自定义的视图并且同样有可保存的状态信息,你也需要去修改这些对象。

当添加状态保存和恢复到你的代码的时,使用以下列表来提醒你需要些的代码.

  • (必须)在你的APP代理实现application:shouldSaveApplicationState:application:shouldRestoreApplicationState:方法.
  • (必须)分配恢复标识符到每一个你想去保存的视图控制器,做法是分配一个非空字符串到它们的restorationIdentifier属性.如果你也想去保存指定视图的状态,分配非空字符串到它们的restorationIdentifier属性.
  • (必须)在你的APP代理application:willFinishLaunchingWithOptions:方法中显示你的window.这个状态恢复系统需要这个window,如此它能恢复滚动位置和另外的与你的界面相关的一些东西。
  • 指定对恰当视图控制器的恢复类.(如果没有这个,你的APP代理在恢复时间被询问去提供相应的视图控制器).
  • (推荐)编码和解码你的视图和视图控制器的状态,使用这些对象的encodeRestorableStateWithCoder:decodeRestorableStateWithCoder:方法.
  • 对于你的APP的任何版本信息或者额外的状态信息进行编码和解码,使用你的APP的代理application:willEncodeRestorableStateWithCoder:application:didDecodeRestorableStateWithCoder:方法.
  • 作为table viewcollection view数据源的对象应该实现UIDataSourceModelAssociation协议.尽管这不是必须的,这个协议帮助保存在这些视图类型中的选中项和显示项.

保存你的视图控制器状态(Preserving the State of Your View Controllers)

保存你的APP视图控制器状态应该是你主要的目标.视图控制器定义你的用户界面的结构.它们管理需要在界面上呈现的视图和协调支持的视图的数据的获取和设置.去保存一个单独的视图控制器状态,你必须做以下这些:

  • (必须)分配一个恢复标识符到这个视图控制器.
  • (必须)在启动时间提供代码去创建或者定位新的视图控制器.
  • (可选)实现encodeRestorableStateWithCoder:decodeRestorableStateWithCoder:方法去编码和恢复在下一次启动期间不能再现的状态信息.

标记你要保存的视图控制器(Marking Your View Controllers for Preservation)

UIkit仅仅保存在视图控制器restorationIdentifier属性包含一个有效字符串对象的视图控制器.你知道你想去保存的视图控制器,当你初始化这个视图控制器对象时设置这个属性值.如果你的视图控制器是加载来自一个storyboard或者nib文件,你能在哪里设置这个恢复标识.

选择一个恰当的值作为恢复标识(restoration identifiers)是重要的.当这个恢复处理期间,你的代码使用这个恢复标识去确定那个视图控制器去寻回(retrieve)或者创建.如果每一个视图控制器对象是基于一个不同的类,你能使用类名作为恢复标识。无论如何,如果你的视图控制器层级包含了一个相同类的多个实例,你可能需要对每个视图控制器使用去选择不同的名字。

当它请你去提供一个视图控制器时,UIkit提供给你视图控制器对象的恢复路径.一个恢复路径是在根视图控制器开始的恢复标识序列,它沿着视图控制器层级向下直到你的当前的对象。例如,假如你有一个tab bar controller其标识符是TabBarControllerID,这个第一个tab包含了一个导航控制器其标识符是NavControllerID并且其根视图控制器标识是MyViewController.这个根视图控制器完整的恢复路径将是TabBarControllerID/NavControllerID/MyViewController.

每一个对象的恢复路径是唯一的。如果一个视图控制器有两个子视图控制器,每一个子视图控制器必须有一个不同的恢复标识。然而,两个视图控制器有两个不同的父对象可以使用相同的恢复标识符,因为这个恢复路径的剩余部分提供了需要的唯一性。一些UIkit视图控制器,例如导航控制器,自动消除它们子视图控制器的歧义,允许你对每一个子视图控制器使用相同的恢复标识。更多关于一个给定视图控制器的行为,去看相应的类参考。

在恢复时间,你使用提供的恢复路径去确定那些视图控制器返回给UIkit。

在启动时间恢复你的视图控制器(Restoring Your View Controllers at Launch Time)

在恢复处理期间,UIkit要求你的APP去创建(或者定位)视图控制器对象这些由你保存的用户界面组成。当尝试去定位视图控制器时UIkit坚持以下的过程:

  1. 如果视图控制器有一个恢复类,UIkit要求这个类去提供这个视图控制器.UIkit调用这个关联的恢复类的viewControllerWithRestorationIdentifierPath:coder:方法去找回这个视图控制器。如果这个方法返回nil,他是假定APP不想去重建这个视图控制器然后UIkit停止寻找它。
  2. 如果视图控制器没有一个恢复类,UIkit请求APP代理去提供这个视图控制器.UIkit调用你的APP代理的application:viewControllerWithRestorationIdentifierPath:coder:方法寻找一个没有恢复类的视图控制器.如果这个方法返回nil,UIkit视图去找到隐式视图控制器.
  3. 如果一个视图控制器的正确的恢复路径已经存在,UIkit使用这个对象.如果你的APP在启动时间创建视图控制器(以编程的方式或者通过加载它们来自一个资源文件)和分配了恢复标识符给它们,UIkit会通过它们的恢复路径隐式的找到它们。
  4. 如果视图控制器最初从一个storyboard文件加载,UIkit使用保存的storyboard信息去定位和创建它.UIkit保存关于在恢复档案内部存在的视图控制器相关的storyboard信息.在恢复时间,它使用这个信息去定位相同的storyboard文件和如果有视图控制器通过任何另外的方法没有被找到那么实例化相应的视图控制器.

值得注意的是,如果你为一个视图控制器指定一个恢复类,UIkit不会试图隐式的去寻找你的视图控制器.如果你的恢复类的viewControllerWithRestorationIdentifierPath:coder:方法返回nil,UIkit停止尝试去定位你的视图控制器。这给你控制是否真的想去创建这个视图控制器。如果你不指定一个恢复类,UIkit会做它能够找到这个视图控制器给你的任何事,当必要时从你的APP的storyboard文件创建它。

如果你选择去使用恢复类,你实现viewControllerWithRestorationIdentifierPath:coder:方法应该创建恢复类的一个新实例,执行一些最小的初始化,返回所得的对象。以下代码是一个例子显示了你可能如何使用这个方法去从一个storyboard加载一个视图控制器。因为视图控制器是最初从storyboard加载的,这个方法使用UIStateRestorationViewControllerStoryboardKey键去获得来自档案文件(归档)的storyboard。注意此方法不试图去配置这个视图控制器的数据字段。当视图控制状态解码后这一步会发生。

1
2
3
4
5
6
7
8
9
10
11
+ (UIViewController*) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents
                      coder:(NSCoder *)coder {
   MyViewController* vc;
   UIStoryboard* sb = [coder decodeObjectForKey:UIStateRestorationViewControllerStoryboardKey];
   if (sb) {
      vc = (PushViewController*)[sb instantiateViewControllerWithIdentifier:@"MyViewController"];
      vc.restorationIdentifier = [identifierComponents lastObject];
      vc.restorationClass = [MyViewController class];
   }
    return vc;
}

如上当创建一个新的视图控制器时去重新分配一个恢复标识和恢复类是一个好的习惯。最简单的恢复这个恢复标识的方法是去抓住identifierComponents数据的最后一项把这项分配给你的新的视图控制器(如上代码所示).

关于在启动时间对象已经从你的APP主storyboard加载了,就不要为每一个对象创建一个新实例了。作为替代,实现你APP的application:viewControllerWithRestorationIdentifierPath:coder:方法和使用这个方法返回恰当的对象或者让UIkit隐式的找到这些对象。

编码和解码你的视图控制器状态(Encoding and Decoding Your View Controller’s State)

对于每一个计划保存的对象,UIkit调用对象的encodeRestorableStateWithCoder:去给它一个机会保存它的状态。在这个解码处理期间,UIKit调用decodeRestorableStateWithCoder:方法去解码这个状态和应用状态到这个对象。这些方法的实现是可选的,但是是被推荐的,对于你的视图控制器。你可以使用它们去保存和恢复以下的信息类型:

  • 对任何被展示的数据的引用(不是数据本身)
  • 用于一个容器视图控制器,对它子视图控制器的引用
  • 关于当前选择的信息
  • 用于具有用户可配置视图的视图控制器,关于这个可配置视图的当前配置信息。

在你的编码和解码方法里面,你能编码由编码器支持的任何值,包括其他的对象。对于除视图和视图控制器之外的所有对象,这个对象必须采用NSCoding协议和使用这个协议的方法去写它的状态。对于视图和视图控制器,编码器不使用NSCoding协议的方法去保存这个对象状态对象。作为代替,编码器保存这个对象的恢复标识和添加它到可保存的对象列表,这结果在该对象的encodeRestorableStateWithCoder:方法里面被调用。

你的视图控制器的encodeRestorableStateWithCoder:decodeRestorableStateWithCoder:方法应该总是在它们实现的一些点调用super。调用super给父类一个机会去保存和恢复任何额外的信息。以下的代码简单的实现了这些方法,其保存一个用于去识别指定视图控制器的数字值。

1
2
3
4
5
6
7
8
9
10
11
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
   [super encodeRestorableStateWithCoder:coder];

   [coder encodeInt:self.number forKey:MyViewControllerNumber];
}

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
   [super decodeRestorableStateWithCoder:coder];

   self.number = [coder decodeIntForKey:MyViewControllerNumber];
}

在编码和解码处理期间编码器对象不是共享的。每个对象的可保存状态接收它自己的编码器,如此它能使用去读或者写数据。这个独特的编码器使用方式使你不用去担心在你自己的对象之间关键的命名空间冲突。然而,你必须仍然避免使用一些UIkit提供的特定键名。具体的,每一个编码器包含UIApplicationStateRestorationBundleVersionKeyUIApplicationStateRestorationUserInterfaceIdiomKey键,它们提供了关于包版本(bundle version)和当前用户界面风格(idiom).编码器关联的视图控制器可能同样包含了UIStateRestorationViewControllerStoryboardKey键,这个识别视图控制器来源于哪一个storyboard.

保存你的视图的状态(Preserving the State of Your Views)

如果视图有状态值得保存,你能够保存你的APP视图控制器的剩余部分状态。因为它通常是由拥有它的视图控制器配置,大部分视图不需要保存状态信息。你唯一需要保存一个视图状态的时间是当视图能够被用户通过一种方式改变,并且这个改变是独立于它的数据和拥有它的视图控制器的。例如,scroll视图保存了当前的滚动位置,这个信息对于视图控制器是没有趣的但是这个会影响这个视图如何去呈现它自己。

指定一个视图状态应该保存,你要做以下的:

  • 分配一个有效的字符串到视图的restorationIdentifier属性
  • 使用来自视图控制器的视图同样有一个有效的恢复标识符
  • 对于table viewscollection views,分配一个采用了UIDataSourceModelAssociation协议的数据源

与视图控制器一样,分配一个恢复标识符到一个视图告诉系统这个视图有你的APP想去保存的状态。之后这个恢复标识符能同样被使用去定位这个视图。

像视图控制器,视图定义方法关于编码和解码它们自定义状态。如果你创建一个它的状态值得保存的视图,你能使用这些方法去读和写任何相关的数据。

有可保存状态的UIKit视图(UIKit Views with Preservable State)

为了保存任何视图的状态,包括自定义和标准系统视图两种,你必须分配一个恢复标识符给这个视图。视图没有一个恢复标识符UIKit是不会把它加入一个可保存对象列表的。

以下的UIKit视图有能被保存的状态信息:

  • UICollectionView
  • UIImageView
  • UIScrollView
  • UITableView
  • UITextField
  • UITextView
  • UIWebView

其他的框架可能同样有可保存状态的视图。关于视图是否保存状态信息和保存什么状态信息,可以看相应类的参考。

保存自定义视图的状态(Preserving the State of a Custom View)

如果你实现一个有可恢复状态的自定义视图,实现encodeRestorableStateWithCoder:decodeRestorableStateWithCoder:方法和使用它们去编码和解码这个状态。使用这些方法去保存只有在通过其他的方式不能很容易的被重新配置的数据。例如,使用这些方法去保存用户与视图交互被修改的数据。不要使用这些方法去保存由视图去呈现的数据或者所属视图控制器能够很容易配置的任何数据。

如下代码显示如何去保存和恢复一个包含可编辑文本的自定义视图的选择。在这个例子里,这个选择范围是使用selectionRangesetSelectionRange去访问,这些是自定义方法,视图使用这些方法去管理选择范围。编码这些数据只需要写它们到一个提供的编码器(coder)对象。恢复数据需要读取这个编码器(coder)对象然后应用数据到这个视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Preserve the text selection
- (void) encodeRestorableStateWithCoder:(NSCoder *)coder {
    [super encodeRestorableStateWithCoder:coder];

    NSRange range = [self selectionRange];
    [coder encodeInt:range.length forKey:kMyTextViewSelectionRangeLength];
    [coder encodeInt:range.location forKey:kMyTextViewSelectionRangeLocation];
}

// Restore the text selection.
- (void) decodeRestorableStateWithCoder:(NSCoder *)coder {
   [super decodeRestorableStateWithCoder:coder];
   if ([coder containsValueForKey:kMyTextViewSelectionRangeLength] &&
           [coder containsValueForKey:kMyTextViewSelectionRangeLocation]) {
      NSRange range;
      range.length = [coder decodeIntForKey:kMyTextViewSelectionRangeLength];
      range.location = [coder decodeIntForKey:kMyTextViewSelectionRangeLocation];
      if (range.length > 0)
         [self setSelectionRange:range];
   }
}

实现友好保存数据源(Implementing Preservation-Friendly Data Sources)

因为通过table或者collection视图显示的数据可以更改,只有这两个类实现了UIDataSourceModelAssociation协议才会保存当前选中项和当前显示的cell信息。这个协议提供了一个关于table或者collection视图去识别它所包含的不依赖该内容的索引路径(index path)的内容。如此,在下一个启动周期期间不管这个数据源(data source)放置一个项(item)在哪里,这个视图仍然有所有的它需要去定位相应项(item)的信息。

为了成功的实现UIDataSourceModelAssociation协议,你的数据源对象必须能确定在APP随后启动之间的项(items).这个意味着你设计的任何识别方案对于一个给定的数据块必须是不变的。这个是必不可少的,因为数据源(data source)每次请求同样的标识符必须能寻回同样的数据块。实现这个协议本身是一个数据项到它唯一ID的关系映射,之后通过这种映射再次返回相应项。

APP使用Core Data利用对象标识符能实现这个协议。每一个对象在Core Data里储存有一个唯一对象标识符,这个能够被转换成一个URL,之后可以使用这个URL去定位这个对象。如果你的APP不使用Core Data,如果你想去支持你视图的状态保存那么你需要去设计你自己唯一标识的形式。

Note:记住实现UIDataSourceModelAssociation协议是只必须(only necessary)去保存属性(attributes)例如在一个table或者collection视图的当前选择项。这个协议不用于保存由你的数据源(data source)管理的实际数据。这个是你APP的责任,去确保它的数据在恰当的时间保存。

保存你的APP高等级状态(Preserving Your App’s High-Level State)

除了由你的APP的视图控制器和视图保存数据之外,UIkit提供钩子给你去保存任何你的APP所需要的各种各样的数据。具体的,UIApplicationDelegate协议包含了以下的方法给你覆盖:

  • application:willEncodeRestorableStateWithCoder:
  • application:didDecodeRestorableStateWithCoder:

如果你的APP包含的状态不被视图控制器所拥有,但是需要去保存,你能使用前面的方法去保存和恢复它。application:willEncodeRestorableStateWithCoder:方法在保存过程一开始被调用,如此你能写出任何高级别的APP状态,例如你的用户界面的当前版本。application:didDecodeRestorableStateWithCoder:方法在恢复状态结束时被调用,如此你能解码任何数据和执行任何你的APP需要的最后的清理。

保存和恢复状态信息技巧(Tips for Saving and Restoring State Information)

当你添加状态保存和恢复到你的APP,考虑以下的指导:

  • 编码版本信息以及你的APP状态的其余部分。当保存过程期间,建议你编码一个版本字符串或者数字作为标识你的APP用户界面的当前版本。你能在你的APP代理application:willEncodeRestorableStateWithCoder:方法中编码这个状态。当你的APP代理 application:shouldRestoreApplicationState:方法被调用,你能从提供的编码器(coder)找回这个信息和使用它去确定是否状态保存是可能的。
  • 在你的APP状态里不要包含来自你数据模型(data model)的对象.APP应该继续去保存它们的数据分别在云(iCloud)或者在磁盘上的本地文件。绝不使用状态恢复系统去保存这个数据。在一个恢复操作期间如果有问题发生保存的界面数据可能被删除。因此,任何你写到磁盘的保存相关的数据应该考虑清除。
  • 状态保存系统期望你按照这种方式去使用视图控制器,它们是设计去被使用的。视图控制器层级是通过一个视图控制器包含的组合创建的,并由另一个视图控制器去呈现一个视图控制器。如果你的APP通过另外一种方式显示一个视图控制器的视图,例如添加它到一个另外的视图且没有在相应的视图控制器中间创建包含关系——这个保存系统将不能去找到你的视图控制器去保存它。
  • 记住,你可能不想去保存所有的视图控制器。有时候,它可能没有去保存一个视图控制器的意义。例如,当显示一个视图控制器去改变用户密码的时候,如果用户离开了你的APP,你可能想去取消这个操作和恢复APP到以前的屏幕。在这种情况,你将不保存那个要求新密码信息的视图控制器。
  • 在恢复过程期间避免交换视图控制器类。状态保存系统编码它保存的视图控制器类。在恢复期间,如果你的APP返回一个对象,这个对象的类和(或者不是一个子类)这个原来的对象不匹配,这个系统不要求视图控制器去解码任何状态信息。如此,将旧的视图控制器换成一个完全不同的视图控制器并且不恢复这个对象的全部状态。
  • 当用户强制退出这个APP时系统自动删除APP保存的状态。当APP被终结(killed)时删除保存的状态信息是一个安全的预防措施。(作为一种安全的预防措施,如果APP在启动期间崩溃两次系统同样删除保存的状态),如果你想去测试你的APP恢复状态的能力,在调试时(debugging)你不应该使用多任务栏(multitasking bar)去终结这个APP。作为替代,使用XcodeKill这个APP或者通过安装一个临时命令以编程的方式kill这个APP或者手势去调用exit根据需要.

一个VoIP App开发技巧(Tips for Developing a VoIP App)

一个因特网上的音频协议(VoIP)APP允许用户使用一个互联网连接打电话而不是设备移动服务。在iOS8或者之后的系统,你能使用苹果推送通知(APNs)和PushKit框架的APIs去创建一个VoIP APP。依赖这个推送通知去启用VoIP功能意味着你的APP不要去保持(或维护)一个到所关联服务的持久的网络连接或者配置一个套接字(socket)给VoIP使用。当一个VoIP推送通知抵达,你的APP是给定时间去处理这个通知,即使你的APP现在是被终结的。

Note:VoIP推送通知是只发送给设备上运行的是iOS8或者这之后的系统。如果你需要 去支持运行以前iOS版本的设备,你有责任去维护一个兼容的实现.

和任何后台音频(audio)APP一样,关于VoIP APP的音频会话(audio session)必须配置正确去确保这个APP和另外的基于音频的APP工作顺利。因为一个VoIP APP的音频播放(playback)和录音不在所有时间都使用,只有在它需要的时候去创建和配置你APP的音频会话这个尤其重要。例如,你要创建音频会话去通知用户有电话进来或者用户实际上正在打电话。一到电话结束,你将接下来移除对这个音频会话的引用去给另外的音频APP机会去播放它们的音频。