启用与 iOS 移动应用的脱机同步

概述

本教程介绍如何脱机与适用于 iOS 的 Azure 应用服务的移动应用功能同步。 通过脱机同步,最终用户可以与移动应用进行交互,以便查看、添加或修改数据,即使他们没有网络连接也是如此。 更改存储在本地数据库中。 设备重新联机后,更改将与远程后端同步。

如果这是移动应用的第一次体验,则应首先完成教程 “创建 iOS 应用”。 如果不使用下载的快速入门服务器项目,则必须将数据访问扩展包添加到项目。 有关服务器扩展包的详细信息,请参阅 使用适用于 Azure 移动应用的 .NET 后端服务器 SDK

若要了解有关脱机同步功能的详细信息,请参阅 移动应用中的脱机数据同步

查看客户端同步代码

“创建 iOS 应用 ”教程下载的客户端项目已包含支持使用本地核心数据数据库进行脱机同步的代码。 本部分总结了教程代码中已包含的内容。 有关该功能的概念性概述,请参阅 移动应用中的脱机数据同步

使用移动应用的脱机数据同步功能,即使无法访问网络,最终用户也可以与本地数据库进行交互。 要在应用中使用这些功能,您需要初始化 MSClient 的同步上下文,并引用一个本地存储。 然后,通过 MSSyncTable 接口引用表。

QSTodoService.m (Objective-C) 或 ToDoTableViewController.swift (Swift)中,请注意成员 syncTable 的类型为 MSSyncTable。 脱机同步使用此同步表接口而不是 MSTable。 使用同步表时,所有操作都会转到本地存储,并且仅通过显式的推送和拉取操作与远程后端服务器同步。

若要获取对同步表的引用,请使用 syncTableWithName 方法。MSClient 若要删除脱机同步功能,请改用 tableWithName

在执行任何表操作之前,必须初始化本地存储。 下面是相关代码:

  • Objective-C. 在 QSTodoService.init 方法中:

    MSCoreDataStore *store = [[MSCoreDataStore alloc] initWithManagedObjectContext:context];
    self.client.syncContext = [[MSSyncContext alloc] initWithDelegate:nil dataSource:store callback:nil];
    
  • 斯威夫特ToDoTableViewController.viewDidLoad 方法中:

    let client = MSClient(applicationURLString: "http:// ...") // URI of the Mobile App
    let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
    self.store = MSCoreDataStore(managedObjectContext: managedObjectContext)
    client.syncContext = MSSyncContext(delegate: nil, dataSource: self.store, callback: nil)
    

    此方法使用 MSCoreDataStore 移动应用 SDK 提供的接口创建本地存储。 或者,可以通过实现 MSSyncContextDataSource 协议来提供不同的本地存储。 此外, MSSyncContext 的第一个参数用于指定冲突处理程序。 由于我们已通过 nil,所以会获得默认冲突处理程序,而该处理程序在遇到任何冲突时都会失败。

现在,让我们执行实际的同步作,并从远程后端获取数据:

  • Objective-C. syncData 首先推送新更改,然后调用 pullData 从远程后端获取数据。 反过来, pullData 方法将获取与查询匹配的新数据:

    -(void)syncData:(QSCompletionBlock)completion
    {
         // Push all changes in the sync context, and then pull new data.
         [self.client.syncContext pushWithCompletion:^(NSError *error) {
             [self logErrorIfNotNil:error];
             [self pullData:completion];
         }];
    }
    
    -(void)pullData:(QSCompletionBlock)completion
    {
         MSQuery *query = [self.syncTable query];
    
         // Pulls data from the remote server into the local table.
         // We're pulling all items and filtering in the view.
         // Query ID is used for incremental sync.
         [self.syncTable pullWithQuery:query queryId:@"allTodoItems" completion:^(NSError *error) {
             [self logErrorIfNotNil:error];
    
             // Lets the caller know that we have finished.
             if (completion != nil) {
                 dispatch_async(dispatch_get_main_queue(), completion);
             }
         }];
    }
    
  • Swift

    func onRefresh(sender: UIRefreshControl!) {
        UIApplication.sharedApplication().networkActivityIndicatorVisible = true
    
        self.table!.pullWithQuery(self.table?.query(), queryId: "AllRecords") {
            (error) -> Void in
    
            UIApplication.sharedApplication().networkActivityIndicatorVisible = false
    
            if error != nil {
                // A real application would handle various errors like network conditions,
                // server conflicts, etc. via the MSSyncContextDelegate
                print("Error: \(error!.description)")
    
                // We will discard our changes and keep the server's copy for simplicity
                if let opErrors = error!.userInfo[MSErrorPushResultKey] as? Array<MSTableOperationError> {
                    for opError in opErrors {
                        print("Attempted operation to item \(opError.itemId)")
                        if (opError.operation == .Insert || opError.operation == .Delete) {
                            print("Insert/Delete, failed discarding changes")
                            opError.cancelOperationAndDiscardItemWithCompletion(nil)
                        } else {
                            print("Update failed, reverting to server's copy")
                            opError.cancelOperationAndUpdateItem(opError.serverItem!, completion: nil)
                        }
                    }
                }
            }
            self.refreshControl?.endRefreshing()
        }
    }
    

在 Objective-C 版本中,syncData我们首先在同步上下文中调用 pushWithCompletion。 此方法是(而不是同步表本身)的成员 MSSyncContext ,因为它跨所有表推送更改。 只有以某种方式在本地(通过 CUD作)修改的记录才会发送到服务器。 然后调用帮助程序 pullData ,它调用 MSSyncTable.pullWithQuery 以检索远程数据并将其存储在本地数据库中。

在 Swift 版本中,由于推送操作并不是严格必要的,因此没有调用 pushWithCompletion。 如果在执行推送作的表的同步上下文中挂起任何更改,则拉取始终先发出推送。 但是,如果有多个同步表,最好显式调用推送,以确保所有内容在相关表中保持一致。

在 Objective-C 和 Swift 版本中,可以使用 pullWithQuery 方法指定要检索的记录的查询。 在此示例中,查询检索远程 TodoItem 表中的所有记录。

pullWithQuery 的第二个参数是用于增量同步的查询 ID。增量同步仅使用记录的时间戳(在本地存储中调用updatedAt)检索自上次同步以来修改的UpdatedAt记录。查询 ID 应是应用中每个逻辑查询唯一的描述性字符串。 若要选择退出增量同步,请传递 nil 为查询 ID。 此方法在效率上可能存在问题,因为它在每次拉取操作时都会检索所有记录。

Objective-C 应用在修改或添加数据时、用户执行刷新手势时以及启动时同步。

当用户执行刷新手势和启动时,Swift 应用将同步。

由于应用会在修改数据(Objective-C)或应用启动时(Objective-C 和 Swift)同步,因此应用假定用户处于联机状态。 在后面的部分中,你将更新应用,以便用户即使在脱机时也能进行编辑。

查看核心数据模型

使用核心数据脱机存储时,必须在数据模型中定义特定的表和字段。 示例应用已包含格式正确的数据模型。 在本部分中,我们将讲解这些表的使用方法。

打开 QSDataModel.xcdatamodeld。 四个表是定义的-三个表由 SDK 使用,一个表用于 to-do 项本身:

  • MS_TableOperations:跟踪需要与服务器同步的项。
  • MS_TableOperationErrors:跟踪脱机同步期间发生的任何错误。
  • MS_TableConfig:跟踪所有拉取操作的上次同步操作的更新时间。
  • TodoItem:存储 to-do 项。 系统列createdAtupdatedAtversion是可选的系统属性。

注释

移动应用 SDK 保留以“”``开头的列名称。 不要将此前缀与系统列以外的任何内容一起使用。 否则,使用远程后端时,将修改列名称。

使用脱机同步功能时,请定义三个系统表和数据表。

系统表

MS_TableOperations

MS_TableOperations表属性

特征 类型
id 整数 64
项目编号 字符串
性能 二进制数据
table 字符串
tableKind 整数 16

MS_TableOperationErrors

MS_TableOperationErrors表属性

特征 类型
id 字符串
operationId 整数 64
性能 二进制数据
tableKind 整数 16

MS_TableConfig

特征 类型
id 字符串
钥匙 字符串
键类型 整数 64
table 字符串
价值 字符串

数据表

TodoItem

特征 类型 注释
id 字符串,标记为必需 远程存储中的主键
完成 布尔值 “待办事项”字段
文本 字符串 待办事项字段
createdAt 日期 (可选)映射到 createdAt 系统属性
更新时间 日期 (可选)映射到 updatedAt 系统属性
版本 字符串 (可选)用于检测冲突,并映射到版本。

更改应用的同步行为

在本部分中,将修改应用,以便在应用启动时或插入和更新项目时它不会同步。 仅当执行刷新手势按钮时,它才会同步。

Objective-C

  1. QSTodoListViewController.m 中,更改 viewDidLoad 方法以删除对方法末尾的调用 [self refresh] 。 现在,数据不会与应用启动时的服务器同步。 而是与本地存储的内容同步。

  2. QSTodoService.m 中,修改其定义 addItem ,以便在插入项后不会同步。 删除self syncData块并将其替换为以下内容:

    if (completion != nil) {
        dispatch_async(dispatch_get_main_queue(), completion);
    }
    
  3. 修改前面提到的定义 completeItem 。 删除块 self syncData 并将其替换为以下内容:

    if (completion != nil) {
        dispatch_async(dispatch_get_main_queue(), completion);
    }
    

Swift

viewDidLoadToDoTableViewController.swift 中,注释掉此处显示的两行,以停止在应用启动时同步。 在撰写本文时,Swift Todo 应用不会在某人添加或完成项目时更新服务。 它仅在应用启动时更新服务。

self.refreshControl?.beginRefreshing()
self.onRefresh(self.refreshControl)

测试应用

在本部分中,您将连接到一个无效的 URL,以模拟脱机场景。 添加数据项时,它们保存在本地核心数据存储中,但它们不会与移动应用后端同步。

  1. QSTodoService.m 中的移动应用 URL 更改为无效 URL,然后再次运行该应用:

    Objective-C. 在 QSTodoService.m 中:

    self.client = [MSClient clientWithApplicationURLString:@"https://sitename.azurewebsites.net.fail"];
    

    斯威夫特 在 ToDoTableViewController.swift 中:

    let client = MSClient(applicationURLString: "https://sitename.azurewebsites.net.fail")
    
  2. 添加一些 to-do 项。 退出模拟器(或强行关闭应用),然后重启它。 验证更改是否仍然存在。

  3. 查看远程 TodoItem 表的内容:

    • 对于 Node.js 后端,请转到 Azure 门户 ,在移动应用后端中,单击 “简易表>TodoItem”。
    • 对于 .NET 后端,请使用 SQL 工具(如 SQL Server Management Studio)或 REST 客户端,例如 Fiddler 或 Postman。
  4. 验证新 项是否已与 服务器同步。

  5. 将 URL 更改回 QSTodoService.m 中的正确 URL,然后重新运行应用。

  6. 通过下拉列表进行刷新。
    显示进度指示器。

  7. 再次查看 TodoItem 数据。 现在应显示新的和已更改 to-do 项。

摘要

为了支持脱机同步功能,我们使用 MSSyncTable 接口并使用本地存储进行 MSClient.syncContext 初始化。 在这种情况下,本地存储是一个基于核心数据的数据库。

使用核心数据本地存储时,必须使用 正确的系统属性定义多个表。

移动应用的正常创建、读取、更新和删除(CRUD)作就像应用仍处于连接状态一样,但所有作都针对本地存储进行。

将本地存储与服务器同步时,我们使用 MSSyncTable.pullWithQuery 方法。

其他资源