这是iOS Apps with REST APIs系列的最后一篇。在整个翻译过程使用
swift
逐渐开发出了自家APP,还是小有成就的,这个系列的教程也起到很大作用,希望也能够帮到大家。
重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习Swift,看到这个教程对开发一个实际的APP非常有帮助,所以翻译共享给大家。原教程非常长,我会陆续翻译并发布,欢迎交流与分享。
一个很简单的方法可以让App Store审查的时候拒绝你的APP,就是不处理离线情况。对于离线有很多方式可以处理,但这依赖于你的数据及用户想做些什么。从大的方面来说,可以采用下面几种方式:
- 提示用户需要网络连接
- 使用缓存数据并进入只读的状态
- 允许用户在离线的时候可以进行操作,在恢复连接的时候进行同步
当你的APP在离线的时候也别忘记持续检查连接是否已经恢复,不要以为一旦失去连接就永远失去了连接。
当我们开始编写一个新的APP时,最简单的方式就是告诉用户需要互联网连接(并确保在没有连接的情况下不会崩溃)。然后在深入分析在离线时哪些可以进行优化,以提升用户体验。
如何知道用户离线?
当尝试加载并无法加载到数据时,我们知道用户离线了。这时候应该提示用户,让用户知道发生了什么,用户也就会知道现在看到的数据并不是实时加载的。因此,我们首先会在GitHubAPIManager
的getGists
中增加相应的处理。修改后的代码如下:
func getGists(urlRequest: URLRequestConvertible, completionHandler:
(Result<[Gist], NSError>, String?) -> Void) {
alamofireManager.request(urlRequest)
.validate()
.responseArray { (response:Response<[Gist], NSError>) in
if let urlResponse = response.response,
authError = self.checkUnauthorized(urlResponse) {
completionHandler(.Failure(authError), nil)
return
}
guard response.result.error == nil,
let gists = response.result.value else {
print(response.result.error)
completionHandler(response.result, nil)
return
}
// need to figure out if this is the last page
// check the link header, if present
let next = self.getNextPageFromHeaders(response.response)
completionHandler(.Success(gists), next)
}
}
启动APP并尝试把网络关掉,然后刷新看看会有什么错误发生:
{Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline."
UserInfo={NSUnderlyingError=0x7fc8fb403940
{Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)"
UserInfo={_kCFStreamErrorCodeKey=8, _kCFStreamErrorDomainKey=12}},
NSErrorFailingURLStringKey=https://api.github.com/gists/public,
NSErrorFailingURLKey=https://api.github.com/gists/public,
_kCFStreamErrorDomainKey=12, _kCFStreamErrorCodeKey=8,
NSLocalizedDescription=The Internet connection appears to be offline.}
可以看到这里会得到了一个NSURLErrorDomain
错误,错误代码为-1009
,及错误NSURLErrorNotConnectedToInternet
。所以,应该在调用getGists
方法时检查是否有该错误,如果有那么提示用户他们现在离线了。
使用一个非中断式提示,用户交互会更好,因此这里引入一个非常不错的提示库BRYXBanner
。使用CocoaPod将该框架的v0.4.1版本引入到你的工程中,并在MasterViewController
中引入:
import UIKit
import Alamofire
import PINRemoteImage
import BRYXBanner
class MasterViewController: UITableViewController, LoginViewDelegate {
...
}
现在就可以来处理loadGists
的错误了:
if let error = result.error {
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
}
}
这里我们可以像处理NSURLErrorUserAuthenticationRequired
错误一样来处理NSURLErrorNotConnectedToInternet
错误:
if error.domain == NSURLErrorDomain {
if error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
} else if error.code == NSURLErrorNotConnectedToInternet {
...
}
}
当错误发生时显示一个提示栏(Banner)告诉用户现在网络不给力啊。如果现在已经有一个提示栏显示,还需要先把它隐藏,然后再显示新的提示信息。因此,这里我们需要使用一个变量来跟踪之前所显示的提示栏:
...
import BRYXBanner
class MasterViewController: UITableViewController, LoginViewDelegate {
var detailViewController: DetailViewController? = nil
var gists = [Gist]()
var nextPageURLString: String?
var isLoading = false
var dateFormatter = NSDateFormatter()
var notConnectedBanner: Banner?
...
}
显示提示栏:
guard result.error == nil else {
print(result.error)
self.nextPageURLString = nil
self.isLoading = false
if let error = result.error {
if error.domain == NSURLErrorDomain {
if error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
} else if error.code == NSURLErrorNotConnectedToInternet {
// show not connected error & tell em to try again when they do have a connection
// check for existing banner
if let existingBanner = self.notConnectedBanner {
existingBanner.dismiss()
}
self.notConnectedBanner = Banner(title: "No Internet Connection",
subtitle: "Could not load gists." +
" Try again when you're connected to the internet",
image: nil,
backgroundColor: UIColor.redColor())
}
self.notConnectedBanner?.dismissesOnSwipe = true
self.notConnectedBanner?.show(duration: nil)
}
}
return
}
显示的结果如下:
下面修改其它API的调用,让它们也能够处理离线情况。首先是创建处理:
GitHubAPIManager.sharedInstance.createNewGist(description, isPublic: isPublic,
files: files, completionHandler: {
result in
guard result.error == nil, let successValue = result.value
where successValue == true else {
if let error = result.error {
print(error)
}
let alertController = UIAlertController(title: "Could not create gist",
message: "Sorry, your gist couldn't be deleted. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(okAction)
self.presentViewController(alertController, animated:true, completion: nil)
return
}
self.navigationController?.popViewControllerAnimated(true)
})
如果我们想也可以检查错误的域和代码。但,这里不这么做,因为这里不会为不同的域和代码显示不同的错误。在这里只需要提示用户删除失败即可,所以使用UIAlertController
也是可以的。当然,你可以更改为一个提示栏,它也是一个很好的选择。
GitHubAPIManager.sharedInstance.deleteGist(id, completionHandler: {
(error) in
print(error)
if let _ = error {
// Put it back
self.gists.insert(gistToDelete, atIndex: indexPath.row)
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
// tell them it didn't work
let alertController = UIAlertController(title: "Could not delete gist",
message: "Sorry, your gist couldn't be deleted. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(okAction)
// show the alert
self.presentViewController(alertController, animated:true, completion: nil)
}
})
删除的相关处理和创建是非常类似的:中断用户的操作,并告诉他们执行失败。但对于收藏的状态呢?
GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could load starred status",
message: error.description,
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
}
}
if let status = result.value where self.isStarred == nil { // just got it
self.isStarred = status
self.tableView?.insertRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
如果没有网络连接,这里会得到一个错误,并在控制台中打印出来,但用户可不知道啊。这里需要让用户知道发生了什么。因此,这里使用一个橙色的提示栏来提示用户,而不是红色,表示该错误没那么重要。所以,我们需要在DetailViewController
中引入BRYXBanner
并添加相应的变量:
import UIKit
import WebKit
import BRYXBanner
class DetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
var isStarred: Bool?
var alertController: UIAlertController?
var notConnectedBanner: Banner?
...
}
在没有网络连接的时候创建提示栏:
func fetchStarredStatus() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
if error.domain == NSURLErrorDomain {
if error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title:
"Could not get starred status", message: error.description,
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true,
completion: nil)
} else if error.code == NSURLErrorNotConnectedToInternet {
// show not connected error & tell em to try again when they do have a conne\
// check for existing banner
if let existingBanner = self.notConnectedBanner {
existingBanner.dismiss()
}
self.notConnectedBanner = Banner(title: "No Internet Connection",
subtitle: "Can not display starred status. " +
"Try again when you're connected to the internet",
image: nil,
backgroundColor: UIColor.orangeColor())
self.notConnectedBanner?.dismissesOnSwipe = true
self.notConnectedBanner?.show(duration: nil)
}
}
}
if let status = result.value where self.isStarred == nil { // just got it
self.isStarred = status
self.tableView?.insertRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
这里我们有两个Web服务调用收藏和取消收藏。我们选择提示用户当用户进行相应请求出现错误的时候:
func starThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.starGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could not star gist",
message: error.description, preferredStyle: .Alert)
} else {
self.alertController = UIAlertController(title: "Could not star gist",
message: "Sorry, your gist couldn't be starred. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
}
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
} else {
self.isStarred = true
self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
func unstarThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.unstarGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could not unstar gist",
message: error.description, preferredStyle: .Alert)
} else {
self.alertController = UIAlertController(title: "Could not unstar gist",
message: "Sorry, your gist couldn't be unstarred. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
}
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
} else {
self.isStarred = false self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
下面我们就可以运行来检查是否还有其它什么问题了。这时候你可以关闭/开启网络看看是否会发现什么。
啊哈,我找到了两个:第一个如果当前显示了红色提示框,当我选择一个Gist时同时会显示橙色提示栏。但当我们销毁掉橙色的提示栏后,红色的仍然在。因此,我们需要在切换视图的时候销毁提示栏:
override func viewWillDisappear(animated: Bool) {
if let existingBanner = self.notConnectedBanner {
existingBanner.dismiss()
}
super.viewWillDisappear(animated)
}
在MasterViewController
和DetailViewController
中都需要添加该代码。
第二个错误就是,当没有网络连接的时候,如何切换视图显示,此时显示的列表是错误的。因此,我们需要在用户切换不同的列表的时候清除掉原来的列表:
@IBAction func segmentedControlValueChanged(sender: UISegmentedControl) {
// only show add/edit buttons for my gists
...
// clear gists so they can't get shown for the wrong list
self.gists = [Gist]()
self.tableView.reloadData()
loadGists(nil)
}
还有一个需要网络连接的地方就是:登录。为了测试这个功能,我们需要重置模拟器,并重新安装我们的程序。
当我们测试这个功能的时候,我们会发现,当我们点击登录按钮登录视图控制器只是不断出现。这对用户来说是一个非常糟糕的体验,尤其是当他们第一次运行应用程序。下面我们将修改代码使用SFSafariViewControllerDelegate
检查是否有网络连接:
func safariViewController(controller: SFSafariViewController, didCompleteInitialLoad
didLoadSuccessfully: Bool) {
// Detect not being able to load the OAuth URL
if (!didLoadSuccessfully) {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
if let completionHandler =
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler {
let error = NSError(domain: NSURLErrorDomain, code:
NSURLErrorNotConnectedToInternet,
userInfo: [NSLocalizedDescriptionKey: "No Internet Connection",
NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(error)
}
controller.dismissViewControllerAnimated(true, completion: nil)
}
}
只是让完成处理程序再试一次,不论发生了什么错误:
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler = { (error) -> Void in
self.safariViewController?.dismissViewControllerAnimated(true, completion: nil)
if let error = error {
print(error)
self.isLoading = false
// TODO: handle error
// Something went wrong, try again
self.showOAuthLoginView()
} else {
self.loadGists(nil)
}
}
下面让我们使用前面的提示栏来优化用户体验:
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler = { (error) -> Void in
self.safariViewController?.dismissViewControllerAnimated(true, completion: nil)
if let error = error {
print(error)
self.isLoading = false
if error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet {
// show not connected error & tell em to try again when they do have a connection
// check for existing banner
if let existingBanner = self.notConnectedBanner {
existingBanner.dismiss()
}
self.notConnectedBanner = Banner(title: "No Internet Connection",
subtitle: "Could not load gists. Try again when you're connected to the internet",
image: nil,
backgroundColor: UIColor.redColor())
self.notConnectedBanner?.dismissesOnSwipe = true
self.notConnectedBanner?.show(duration: nil)
} else {
// Something went wrong, try again
self.showOAuthLoginView()
}
} else {
self.loadGists(nil)
}
}
如果我们现在进行测试,会发现登录视图控制器依然会弹出来,因为主视图控制器在检测到没有登录的时候总是会弹出登录视图。解决的方法也非常简单,就是当检测到没有网络的时候让APP不再加载OAuth令牌:
func safariViewController(controller: SFSafariViewController, didCompleteInitialLoad
didLoadSuccessfully: Bool) {
// Detect not being able to load the OAuth URL
if (!didLoadSuccessfully) {
// let defaults = NSUserDefaults.standardUserDefaults()
// defaults.setBool(false, forKey: "loadingOAuthToken")
if let completionHandler =
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler {
let error = NSError(domain: NSURLErrorDomain,
code: NSURLErrorNotConnectedToInternet, userInfo: [
NSLocalizedDescriptionKey: "No Internet Connection",
NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(error)
}
controller.dismissViewControllerAnimated(true, completion: nil)
}
}
这样APP将不会再弹出登录视图,直到下拉刷新。
对哪些会调用失败的Web服务进行分析和测试。确保每一个都进行了很好的处理,并根据实际需要添加横幅提示或者弹出对话框,以便让用户知道发生了什么。
我们已经处理了用户无网络的情况,所以Apple不会因为这个拒绝我们的APP上架了。但是如果我们想提供更好的用户体验,比如在无网络的情况下可以让用户查看之前所加载的一些数据,那么我们该如何做呢?不着急,下面我们就来实现这个功能。
本地拷贝
对于一些简单的APP就像这个示例APP一样,当用户离线时以只读的方式显示最后所加载的数据,应该足够了。所以我们需要持久化Gist的列表。当没有网络的时候,用户不能删除、收藏/取消收藏以及查看之前是否已经收藏该Gist。这些功能,在本章前面已经实现。
NSKeyedArchiver
可以用来序列化对象,因此也可以很容易写入到磁盘中。对于数组、字符串都是开箱即用的,但对于我们自己所编写的类,需要整明白该如何进行处理。Gist
和File
这两个类是需要进行处理的。
为了能够将我们自定义的类能够被持久化,需要实现NSCoding
协议,该协议反过来有需要实现NSObject
协议:
class Gist: NSObject, NSCoding, ResponseJSONObjectSerializable {
...
}
NSObject
协议需要我们改变已存在的init
函数:
required override init() {
}
还需要包含一个description
属性,因此我们这里需要修改getDescription
方法:
class Gist: NSObject, NSCoding, ResponseJSONObjectSerializable {
var id: String?
var gistDescription: String?
...
required init(json: JSON) {
self.gistDescription = json["description"].string
...
}
...
}
而且我们还需要修改视图控制器中所使用的地方。下面我们通过搜索.description
找到原来所使用的地方进行更改.
在MasterViewController
中:
cell.textLabel!.text = gist.gistDescription
DetailViewController
中:
func tableView(tableView: UITableView, cellForRowAtIndexPath
indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
if indexPath.section == 0 {
if indexPath.row == 0 {
cell.textLabel?.text = gist?.gistDescription
NSCoding
协议需要实现两个方法,一个是对对象进行序列化,另外一个是进行反序列化:
class Gist: NSObject, NSCoding, ResponseJSONObjectSerializable {
...
// MARK: NSCoding
@objc func encodeWithCoder(aCoder: NSCoder) {
...
}
@objc required convenience init?(coder aDecoder: NSCoder) {
self.init()
...
}
}
这里我们需要对对象的每一个属性进行序列化和反序列化:
@objc func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.id, forKey: "id")
aCoder.encodeObject(self.gistDescription, forKey: "gistDescription")
aCoder.encodeObject(self.ownerLogin, forKey: "ownerLogin")
aCoder.encodeObject(self.ownerAvatarURL, forKey: "ownerAvatarURL")
aCoder.encodeObject(self.url, forKey: "url")
aCoder.encodeObject(self.createdAt, forKey: "createdAt")
aCoder.encodeObject(self.updatedAt, forKey: "updatedAt")
if let files = self.files {
aCoder.encodeObject(files, forKey: "files")
}
}
@objc required convenience init?(coder aDecoder: NSCoder) {
self.init()
self.id = aDecoder.decodeObjectForKey("id") as? String
self.gistDescription = aDecoder.decodeObjectForKey("gistDescription") as? String
self.ownerLogin = aDecoder.decodeObjectForKey("ownerLogin") as? String
self.ownerAvatarURL = aDecoder.decodeObjectForKey("ownerAvatarURL") as? String
self.createdAt = aDecoder.decodeObjectForKey("createdAt") as? NSDate
self.updatedAt = aDecoder.decodeObjectForKey("updatedAt") as? NSDate
if let files = aDecoder.decodeObjectForKey("files") as? [File] {
self.files = files
}
}
对于File
:
class File: NSObject, NSCoding, ResponseJSONObjectSerializable {
var filename: String?
var raw_url: String?
var content: String?
...
// MARK: NSCoding
@objc func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.filename, forKey: "filename")
aCoder.encodeObject(self.raw_url, forKey: "raw_url")
aCoder.encodeObject(self.content, forKey: "content")
}
@objc required convenience init?(coder aDecoder: NSCoder) {
let filename = aDecoder.decodeObjectForKey("filename") as? String
let content = aDecoder.decodeObjectForKey("content") as? String
// use the existing init function
self.init(aName: filename, aContent: content)
self.raw_url = aDecoder.decodeObjectForKey("raw_url") as? String
}
}
下面我们就可以真正实现Gist的保存了。创建一个PersistenceManager.swift
文件来负责数据的保存与加载。这里我们保持简单,只保存和加载Gist数组,而不是其它的类型:
import Foundation
class PersistenceManager {
class func saveArray<T: NSCoding>(arrayToSave: [T], path: Path) {
// TODO: implement
}
class func loadArray<T: NSCoding>(path: Path) -> [T]? {
// TODO: implement
}
}
在保存Gists的时候还有一点,就是数据的保存的路径。这里使用文档路径就可以了:
class PersistenceManager {
class private func documentsDirectory() -> NSString {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,
.UserDomainMask, true)
let documentDirectory = paths[0] as String
return documentDirectory
}
...
}
我们还需要为不同的Gist列表指定不同的保存的路径,以防止被覆盖。让我们定义一个枚举对象来负责。后面如果增加了其它类型的可以进行扩展:
enum Path: String {
case Public = "Public"
case Starred = "Starred"
case MyGists = "MyGists"
}
class PersistenceManager {
...
}
Ok,现在我们就可以真正来实现保存了。这里使用NSKeyedArchiver.archiveRootObject
将对象数组保存到指定的路径下:
class func saveArray<T: NSCoding>(arrayToSave: [T], path: Path) {
let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
NSKeyedArchiver.archiveRootObject(arrayToSave, toFile: file)
}
加载对象数组的方法类似:
class func loadArray<T: NSCoding>(path: Path) -> [T]? {
let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
let result = NSKeyedUnarchiver.unarchiveObjectWithFile(file)
return result as? [T]
}
合在一起PersistenceManager
的代码如下:
import Foundation
enum Path: String {
case Public = "Public"
case Starred = "Starred"
case MyGists = "MyGists"
}
class PersistenceManager {
class private func documentsDirectory() -> NSString {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,
.UserDomainMask, true)
let documentDirectory = paths[0] as String
return documentDirectory
}
class func saveArray<T: NSCoding>(arrayToSave: [T], path: Path) {
let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
NSKeyedArchiver.archiveRootObject(arrayToSave, toFile: file)
}
class func loadArray<T: NSCoding>(path: Path) -> [T]? {
let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
let result = NSKeyedUnarchiver.unarchiveObjectWithFile(file)
return result as? [T]
}
}
那么何时保存呢?就是当它们被加载的时候。在loadGists
中一旦数据被加载成功,我们就需要调用PersistenceManager.saveArray
进行保存:
if let fetchedGists = result.value {
if let _ = urlToLoad {
self.gists += fetchedGists
} else {
self.gists = fetchedGists
}
let path:Path
if self.gistSegmentedControl.selectedSegmentIndex == 0 {
path = .Public
} else if self.gistSegmentedControl.selectedSegmentIndex == 1 {
path = .Starred
} else {
path = .MyGists
}
PersistenceManager.saveArray(self.gists, path: path)
}
当无网络访问的时候我们就可以进行加载了:
if error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
} else if error.code == NSURLErrorNotConnectedToInternet {
let path:Path
if self.gistSegmentedControl.selectedSegmentIndex == 0 {
path = .Public
} else if self.gistSegmentedControl.selectedSegmentIndex == 1 {
path = .Starred
} else {
path = .MyGists
}
if let archived:[Gist] = PersistenceManager.loadArray(path) {
self.gists = archived
} else {
self.gists = [] // don't have any saved gists
}
// show not connected error & tell em to try again when they do have a connection
...
}
再次运行。当加载一些数据后就可以关闭网络,这时候你应该仍然可以看到这些数据。关掉重新运行并保持网络关闭,这时候仍然可以看到这些数据,并且有相应的提示信息。
看看你的APP那个部分可以支持离线只读处理。如果有,使用
NSKeyedArchiver
保存和加载它们,这样用户就可以在离线的时候可以看到这些数据了。
本章的代码.
数据库
也许你的APP非常复杂,希望能够使用一个真正的数据库来做这件事。有关iOS数据库有专门的书来讲解。随着你将数据保存到数据并进行数据同步。那么你需要处理多个用户之间修改造成的冲突,因为他们在修改的时候可能没有获取到最后一个版本的数据。
当没有互联网连接的时候,数据库并不能真正的解决问题。但数据库可以让你方便的处理复杂对象和数据之间的关系。
接下来你可以了解一下Core Data。它是iOS内置的,并且相对于简单的数据库,它能做的更多。但,你仍需要在网络恢复时进行数据的同步处理。
Realm也越来越流行,也是一个很好的选择。如果你愿意,也可以使用SQLite(Core Data就是构建在它之上)。
如果你打算从头构建整个APP,包括后端,可以考虑使用Parse或[Kinvey](http://devcenter.kinvey.com/ios/guides/caching- offline)。它们所提供的SDK都包含了对离线的处理。
考虑怎么样能够提示离线用户的体验。可以考虑使用数据库,这样用户就可以在离线执行一下处理,并在网络恢复时在后台将它们同步。
网友评论