這是iOS Apps with REST APIs系列的最后一篇砌庄。在整個翻譯過程使用
swift
逐漸開發(fā)出了自家APP羹唠,還是小有成就的,這個系列的教程也起到很大作用鹤耍,希望也能夠幫到大家。
重要說明: 這是一個系列教程验辞,非本人原創(chuàng)稿黄,而是翻譯國外的一個教程。本人也在學(xué)習(xí)Swift跌造,看到這個教程對開發(fā)一個實際的APP非常有幫助杆怕,所以翻譯共享給大家族购。原教程非常長,我會陸續(xù)翻譯并發(fā)布陵珍,歡迎交流與分享寝杖。
一個很簡單的方法可以讓App Store審查的時候拒絕你的APP,就是不處理離線情況互纯。對于離線有很多方式可以處理瑟幕,但這依賴于你的數(shù)據(jù)及用戶想做些什么。從大的方面來說留潦,可以采用下面幾種方式:
- 提示用戶需要網(wǎng)絡(luò)連接
- 使用緩存數(shù)據(jù)并進(jìn)入只讀的狀態(tài)
- 允許用戶在離線的時候可以進(jìn)行操作只盹,在恢復(fù)連接的時候進(jìn)行同步
當(dāng)你的APP在離線的時候也別忘記持續(xù)檢查連接是否已經(jīng)恢復(fù),不要以為一旦失去連接就永遠(yuǎn)失去了連接兔院。
當(dāng)我們開始編寫一個新的APP時殖卑,最簡單的方式就是告訴用戶需要互聯(lián)網(wǎng)連接(并確保在沒有連接的情況下不會崩潰)。然后在深入分析在離線時哪些可以進(jìn)行優(yōu)化坊萝,以提升用戶體驗孵稽。
如何知道用戶離線?
當(dāng)嘗試加載并無法加載到數(shù)據(jù)時十偶,我們知道用戶離線了菩鲜。這時候應(yīng)該提示用戶,讓用戶知道發(fā)生了什么扯键,用戶也就會知道現(xiàn)在看到的數(shù)據(jù)并不是實時加載的睦袖。因此,我們首先會在GitHubAPIManager
的getGists
中增加相應(yīng)的處理荣刑。修改后的代碼如下:
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并嘗試把網(wǎng)絡(luò)關(guān)掉馅笙,然后刷新看看會有什么錯誤發(fā)生:
{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
董习。所以,應(yīng)該在調(diào)用getGists
方法時檢查是否有該錯誤爱只,如果有那么提示用戶他們現(xiàn)在離線了皿淋。
使用一個非中斷式提示,用戶交互會更好恬试,因此這里引入一個非常不錯的提示庫BRYXBanner
窝趣。使用CocoaPod將該框架的v0.4.1版本引入到你的工程中,并在MasterViewController
中引入:
import UIKit
import Alamofire
import PINRemoteImage
import BRYXBanner
class MasterViewController: UITableViewController, LoginViewDelegate {
...
}
現(xiàn)在就可以來處理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 {
...
}
}
當(dāng)錯誤發(fā)生時顯示一個提示欄(Banner)告訴用戶現(xiàn)在網(wǎng)絡(luò)不給力啊。如果現(xiàn)在已經(jīng)有一個提示欄顯示,還需要先把它隱藏吊洼,然后再顯示新的提示信息蓖捶。因此,這里我們需要使用一個變量來跟蹤之前所顯示的提示欄:
...
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
}
顯示的結(jié)果如下:
下面修改其它API的調(diào)用军援,讓它們也能夠處理離線情況绍些。首先是創(chuàng)建處理:
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
也是可以的丹弱。當(dāng)然,你可以更改為一個提示欄贡翘,它也是一個很好的選擇蹈矮。
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)
}
})
刪除的相關(guān)處理和創(chuàng)建是非常類似的:中斷用戶的操作,并告訴他們執(zhí)行失敗鸣驱。但對于收藏的狀態(tài)呢泛鸟?
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)
}
})
如果沒有網(wǎng)絡(luò)連接,這里會得到一個錯誤踊东,并在控制臺中打印出來北滥,但用戶可不知道啊。這里需要讓用戶知道發(fā)生了什么闸翅。因此再芋,這里使用一個橙色的提示欄來提示用戶,而不是紅色坚冀,表示該錯誤沒那么重要济赎。所以,我們需要在DetailViewController
中引入BRYXBanner
并添加相應(yīng)的變量:
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?
...
}
在沒有網(wǎng)絡(luò)連接的時候創(chuàng)建提示欄:
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服務(wù)調(diào)用收藏和取消收藏记某。我們選擇提示用戶當(dāng)用戶進(jìn)行相應(yīng)請求出現(xiàn)錯誤的時候:
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)
}
})
}
}
下面我們就可以運行來檢查是否還有其它什么問題了司训。這時候你可以關(guān)閉/開啟網(wǎng)絡(luò)看看是否會發(fā)現(xiàn)什么。
啊哈液南,我找到了兩個:第一個如果當(dāng)前顯示了紅色提示框壳猜,當(dāng)我選擇一個Gist時同時會顯示橙色提示欄。但當(dāng)我們銷毀掉橙色的提示欄后滑凉,紅色的仍然在统扳。因此,我們需要在切換視圖的時候銷毀提示欄:
override func viewWillDisappear(animated: Bool) {
if let existingBanner = self.notConnectedBanner {
existingBanner.dismiss()
}
super.viewWillDisappear(animated)
}
在MasterViewController
和DetailViewController
中都需要添加該代碼畅姊。
第二個錯誤就是咒钟,當(dāng)沒有網(wǎng)絡(luò)連接的時候,如何切換視圖顯示若未,此時顯示的列表是錯誤的朱嘴。因此,我們需要在用戶切換不同的列表的時候清除掉原來的列表:
@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)
}
還有一個需要網(wǎng)絡(luò)連接的地方就是:登錄陨瘩。為了測試這個功能腕够,我們需要重置模擬器,并重新安裝我們的程序舌劳。
當(dāng)我們測試這個功能的時候帚湘,我們會發(fā)現(xiàn),當(dāng)我們點擊登錄按鈕登錄視圖控制器只是不斷出現(xiàn)甚淡。這對用戶來說是一個非常糟糕的體驗大诸,尤其是當(dāng)他們第一次運行應(yīng)用程序。下面我們將修改代碼使用SFSafariViewControllerDelegate
檢查是否有網(wǎng)絡(luò)連接:
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)
}
}
只是讓完成處理程序再試一次贯卦,不論發(fā)生了什么錯誤:
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)
}
}
下面讓我們使用前面的提示欄來優(yōu)化用戶體驗:
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)
}
}
如果我們現(xiàn)在進(jìn)行測試资柔,會發(fā)現(xiàn)登錄視圖控制器依然會彈出來,因為主視圖控制器在檢測到?jīng)]有登錄的時候總是會彈出登錄視圖撵割。解決的方法也非常簡單贿堰,就是當(dāng)檢測到?jīng)]有網(wǎng)絡(luò)的時候讓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將不會再彈出登錄視圖,直到下拉刷新啡彬。
對哪些會調(diào)用失敗的Web服務(wù)進(jìn)行分析和測試羹与。確保每一個都進(jìn)行了很好的處理,并根據(jù)實際需要添加橫幅提示或者彈出對話框庶灿,以便讓用戶知道發(fā)生了什么纵搁。
我們已經(jīng)處理了用戶無網(wǎng)絡(luò)的情況,所以Apple不會因為這個拒絕我們的APP上架了往踢。但是如果我們想提供更好的用戶體驗腾誉,比如在無網(wǎng)絡(luò)的情況下可以讓用戶查看之前所加載的一些數(shù)據(jù),那么我們該如何做呢峻呕?不著急利职,下面我們就來實現(xiàn)這個功能。
本地拷貝
對于一些簡單的APP就像這個示例APP一樣山上,當(dāng)用戶離線時以只讀的方式顯示最后所加載的數(shù)據(jù)眼耀,應(yīng)該足夠了。所以我們需要持久化Gist的列表佩憾。當(dāng)沒有網(wǎng)絡(luò)的時候哮伟,用戶不能刪除、收藏/取消收藏以及查看之前是否已經(jīng)收藏該Gist妄帘。這些功能楞黄,在本章前面已經(jīng)實現(xiàn)。
NSKeyedArchiver
可以用來序列化對象抡驼,因此也可以很容易寫入到磁盤中鬼廓。對于數(shù)組、字符串都是開箱即用的致盟,但對于我們自己所編寫的類碎税,需要整明白該如何進(jìn)行處理尤慰。Gist
和File
這兩個類是需要進(jìn)行處理的。
為了能夠?qū)⑽覀冏远x的類能夠被持久化雷蹂,需要實現(xiàn)NSCoding
協(xié)議伟端,該協(xié)議反過來有需要實現(xiàn)NSObject
協(xié)議:
class Gist: NSObject, NSCoding, ResponseJSONObjectSerializable {
...
}
NSObject
協(xié)議需要我們改變已存在的init
函數(shù):
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
找到原來所使用的地方進(jìn)行更改.
在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
協(xié)議需要實現(xiàn)兩個方法责蝠,一個是對對象進(jìn)行序列化,另外一個是進(jìn)行反序列化:
class Gist: NSObject, NSCoding, ResponseJSONObjectSerializable {
...
// MARK: NSCoding
@objc func encodeWithCoder(aCoder: NSCoder) {
...
}
@objc required convenience init?(coder aDecoder: NSCoder) {
self.init()
...
}
}
這里我們需要對對象的每一個屬性進(jìn)行序列化和反序列化:
@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
}
}
下面我們就可以真正實現(xiàn)Gist的保存了萎庭。創(chuàng)建一個PersistenceManager.swift
文件來負(fù)責(zé)數(shù)據(jù)的保存與加載霜医。這里我們保持簡單,只保存和加載Gist數(shù)組驳规,而不是其它的類型:
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的時候還有一點肴敛,就是數(shù)據(jù)的保存的路徑。這里使用文檔路徑就可以了:
class PersistenceManager {
class private func documentsDirectory() -> NSString {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,
.UserDomainMask, true)
let documentDirectory = paths[0] as String
return documentDirectory
}
...
}
我們還需要為不同的Gist列表指定不同的保存的路徑吗购,以防止被覆蓋值朋。讓我們定義一個枚舉對象來負(fù)責(zé)。后面如果增加了其它類型的可以進(jìn)行擴展:
enum Path: String {
case Public = "Public"
case Starred = "Starred"
case MyGists = "MyGists"
}
class PersistenceManager {
...
}
Ok巩搏,現(xiàn)在我們就可以真正來實現(xiàn)保存了昨登。這里使用NSKeyedArchiver.archiveRootObject
將對象數(shù)組保存到指定的路徑下:
class func saveArray<T: NSCoding>(arrayToSave: [T], path: Path) {
let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
NSKeyedArchiver.archiveRootObject(arrayToSave, toFile: file)
}
加載對象數(shù)組的方法類似:
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]
}
}
那么何時保存呢?就是當(dāng)它們被加載的時候贯底。在loadGists
中一旦數(shù)據(jù)被加載成功丰辣,我們就需要調(diào)用PersistenceManager.saveArray
進(jìn)行保存:
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)
}
當(dāng)無網(wǎng)絡(luò)訪問的時候我們就可以進(jìn)行加載了:
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
...
}
再次運行。當(dāng)加載一些數(shù)據(jù)后就可以關(guān)閉網(wǎng)絡(luò)禽捆,這時候你應(yīng)該仍然可以看到這些數(shù)據(jù)笙什。關(guān)掉重新運行并保持網(wǎng)絡(luò)關(guān)閉,這時候仍然可以看到這些數(shù)據(jù)胚想,并且有相應(yīng)的提示信息琐凭。
看看你的APP那個部分可以支持離線只讀處理。如果有浊服,使用
NSKeyedArchiver
保存和加載它們统屈,這樣用戶就可以在離線的時候可以看到這些數(shù)據(jù)了。
本章的代碼.
數(shù)據(jù)庫
也許你的APP非常復(fù)雜牙躺,希望能夠使用一個真正的數(shù)據(jù)庫來做這件事愁憔。有關(guān)iOS數(shù)據(jù)庫有專門的書來講解。隨著你將數(shù)據(jù)保存到數(shù)據(jù)并進(jìn)行數(shù)據(jù)同步孽拷。那么你需要處理多個用戶之間修改造成的沖突吨掌,因為他們在修改的時候可能沒有獲取到最后一個版本的數(shù)據(jù)。
當(dāng)沒有互聯(lián)網(wǎng)連接的時候,數(shù)據(jù)庫并不能真正的解決問題膜宋。但數(shù)據(jù)庫可以讓你方便的處理復(fù)雜對象和數(shù)據(jù)之間的關(guān)系窿侈。
接下來你可以了解一下Core Data。它是iOS內(nèi)置的秋茫,并且相對于簡單的數(shù)據(jù)庫棉磨,它能做的更多。但学辱,你仍需要在網(wǎng)絡(luò)恢復(fù)時進(jìn)行數(shù)據(jù)的同步處理。
Realm也越來越流行环形,也是一個很好的選擇策泣。如果你愿意,也可以使用SQLite(Core Data就是構(gòu)建在它之上)抬吟。
如果你打算從頭構(gòu)建整個APP萨咕,包括后端,可以考慮使用Parse或[Kinvey](http://devcenter.kinvey.com/ios/guides/caching- offline)火本。它們所提供的SDK都包含了對離線的處理危队。
考慮怎么樣能夠提示離線用戶的體驗「婆希可以考慮使用數(shù)據(jù)庫茫陆,這樣用戶就可以在離線執(zhí)行一下處理,并在網(wǎng)絡(luò)恢復(fù)時在后臺將它們同步擎析。