使用CloudKit构建购物清单应用程序:添加记录

本系列第一个教程中 ,我们探索了CloudKit框架和基础架构。 我们还为我们要构建的示例应用程序(购物清单应用程序)奠定了基础。 在本教程中,我们着重于添加,编辑和删除购物清单。

先决条件

正如我在上一教程中提到的那样 ,我将使用Xcode 9Swift 4 。 如果您使用的是Xcode的旧版本,请记住您可能正在使用其他版本的Swift编程语言。

在本教程中,我们将继续处理在第一个教程中创建的项目。 您可以从GitHub下载它(标记adding_records )。

1.设置CocoaPods

购物清单应用程序将使用SVProgressHUD库,该库是Sam Vermette创建的流行库,可轻松显示进度指示器。 您可以将库手动添加到项目中,但是我强烈建议使用CocoaPods来管理依赖项。 您是CocoaPods的新手吗? 阅读此CocoaPods 入门教程,以快速上手。

步骤1:建立Podfile

打开Finder并导航到Xcode项目的根目录。 创建一个新文件,将其命名为Podfile ,并向其添加以下Ruby行。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'Lists' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  pod 'SVProgressHUD', '~> 1.1'

end

第一行指定平台iOS和项目的部署目标iOS 9.0 。 如果您使用的是Swift,则第二行很重要。 Swift不支持静态库,但是CocoaPods从0.36版本开始提供使用框架的选项。 然后,我们为项目的“ 列表”目标指定依赖项。 如果目标名称不同,则用目标名称替换列表

步骤2:安装依赖项

打开终端 ,导航到Xcode项目的根目录,然后运行pod install 。 这将为您做很多事情,例如安装Podfile中指定的依赖以及创建Xcode工作区。

完成CocoaPods设置后,关闭项目并打开为您创建的工作区CocoaPods。 后者非常重要。 打开工作区,而不是项目 。 工作区包括两个项目, Lists项目和一个名为Pods的项目。

2.列出购物清单

步骤1:家政服务

我们准备重新关注CloudKit框架。 但是,首先,我们需要通过重命名 ViewController类改为ListsViewController类。

首先将ViewController.swift重命名为ListsViewController.swift 。 打开ListsViewController.swift并更改其名称 ViewController类改为ListsViewController类。

接下来,打开Main.storyboard ,在左侧的文档大纲中展开View Controller Scene ,然后选择View Controller 。 打开右侧的Identity Inspector并将Class更改为ListsViewController

打开右侧的Identity Inspector并将Class更改为ListsViewController

步骤2:添加表视图

当用户打开应用程序时,将向他们显示他们的购物清单。 我们将以表格视图显示购物清单。 让我们从设置用户界面开始。 在列表视图控制器场景选择列表视图控制器和Xcode的编辑器菜单中选择嵌入>导航控制器

将表视图添加到视图控制器的视图,并为其创建必要的布局约束。 在选择表视图的情况下,打开“ 属性”检查器并将“ 原型单元”设置为1 。 选择原型单元,并将“ 样式”设置为“ 基本” ,将“ 标识符”设置ListCell

选择原型单元,并将“样式”设置为“基本”,将“标识符”设置为ListCell

选择表格视图后,打开“ 连接检查器” 。 将表视图的dataSourcedelegate出口连接到Lists View Controller

步骤3:空状态

即使我们仅创建一个示例应用程序来说明CloudKit的工作方式,但如果出现问题或在iCloud上未找到购物清单,我仍希望显示一条消息。 将标签添加到视图控制器,使其与视图控制器的视图一样大,为其创建必要的布局约束,并将标签的文本居中。

由于我们正在处理网络请求,因此,只要应用程序正在等待来自iCloud的响应,我也希望显示活动指示器视图。 将活动指示器视图添加到视图控制器的视图,并将其置于其父视图的中心。 在“ 属性”检查器中 ,选中标记为“ 停止时隐藏”的复选框。

列表视图控制器

步骤4:连接插座

打开ListsViewController.swift并声明标签,表格视图和活动指示器视图的出口。 这也是使ListsViewController类符合UITableViewDataSourceUITableViewDelegate协议的好时机。

请注意,我还为SVProgressHUD框架添加了import语句,并且为我们在情节提要中创建的原型单元的重用标识符声明了静态常量。

import UIKit
import CloudKit
import SVProgressHUD

class ListsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
    static let ListCell = "ListCell"
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var messageLabel: UILabel!
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
    ...
    
}

返回到情节提要,并将插座与“ 列表视图控制器场景”中的相应视图连接。

步骤5:准备表视图

在从iCloud中获取数据之前,我们需要确保表视图已准备好显示数据。 我们首先需要创建一个属性lists ,以保存我们将要获取的记录。 请记住,记录是CKRecord类的实例。 这意味着将保存来自iCloud的数据的属性的类型为[CKRecord] ,这是CKRecord实例的数组。

import UIKit
import CloudKit
import SVProgressHUD

class ListsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
    static let ListCell = "ListCell"
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var messageLabel: UILabel!
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
    
    var lists = [CKRecord]()
    
    ...
    
}

首先,我们需要实现UITableViewDataSource协议的三种方法:

  • numberOfSectionsInTableView(_:)
  • numberOfRowsInSection(_:)
  • cellForRowAtIndexPath(_:)

如果您有使用表视图的经验,那么每种方法的实现都非常简单。 但是, cellForRowAtIndexPath(_:)可能需要一些解释。 请记住, CKRecord实例是键-值对的CKRecord字典。 若要访问特定键的值,请在CKRecord对象上调用objectForKey(_:) 。 那就是我们在cellForRowAtIndexPath(_:)所做的。 我们获取与表视图行相对应的记录,并要求其提供键"name"的值。 如果键值对不存在,我们将显示一个破折号以指示列表还没有名称。

// MARK: -
// MARK: UITableView Delegate Methods
extension ListsViewController{
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return lists.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // Dequeue Reusable Cell
        let cell = tableView.dequeueReusableCell(withIdentifier: ListsViewController.ListCell, for: indexPath)
        
        // Configure Cell
        cell.accessoryType = .detailDisclosureButton
        
        // Fetch Record
        let list = lists[indexPath.row]
        
        if let listName = list.object(forKey: "name") as? String {
            // Configure Cell
            cell.textLabel?.text = listName
            
        } else {
            cell.textLabel?.text = "-"
        }
        
        return cell
    }

}

步骤6:准备用户界面

我们还需要采取进一步的步骤:准备用户界面。 在视图控制器的viewDidLoad方法中,删除fetchUserRecordID方法调用并调用helper方法setupView

override func viewDidLoad() {
    super.viewDidLoad()
    
    setupView()
}

setupView方法准备用于获取记录列表的用户界面。 我们隐藏标签和表格视图,并告诉活动指示器视图开始动画。

// MARK: -
// MARK: View Methods
private func setupView() {
    tableView.hidden = true
    messageLabel.hidden = true
    activityIndicatorView.startAnimating()
}

在设备或iOS模拟器中生成并运行该应用程序。 如果已按照上述步骤进行操作,则应该会看到一个空白视图,中间是旋转活动指示器视图。

忙于假装正在获取数据

步骤7:创建记录类型

在获取任何记录之前,我们需要在CloudKit仪表板中为购物清单创建记录类型。 CloudKit仪表板是一个Web应用程序,使开发人员可以管理存储在Apple iCloud服务器上的数据。

选择在Project Navigator的项目,并从目标列表中选择列表目标。 打开顶部的“ 功能”选项卡,然后展开“ iCloud”部分。 在iCloud容器列表下方,单击标有CloudKit Dashboard的按钮。

打开CloudKit仪表板

使用您的开发人员帐户登录,并确保在左上方选择了“ 列表”应用程序。 在左侧,从“ 模式”部分中选择“ 记录类型 ”。 默认情况下,每个应用程序都有一个Users记录类型。 要创建新的记录类型,请单击第三列顶部的加号按钮。 我们将遵循Apple的命名约定,并将记录类型命名为Lists ,而不是List

添加新记录类型

请注意,第一个字段是为您自动创建的。 创建一个领域   字段类型设置为字符串 。 不要忘记单击底部的“ 保存”按钮以创建“ 列表”记录类型。 我们将在本系列后面的部分中重新介绍CloudKit仪表板。

接下来,通过转到“ 索引”选项卡并为名称添加一个新的SORTABLE和另一个QUERYABLE索引类型,为文档属性启用索引,然后单击“ 保存”

添加SORTABLE和QUERYABLE索引

最后,转到SECURITY ROLES选项卡,并出于此开发练习的目的,选中所有复选框以确保您的用户可以访问该表。

步骤8:执行查询

创建了Lists记录类型后,终于可以从iCloud中获取一些记录了。 CloudKit框架提供了两个与iCloud进行交互的API:便捷API和基于NSOperation类的API。 在本系列中,我们将同时使用这两个API,但现在我们将使其保持简单,并使用便捷API。

在Xcode中,打开ListsViewController.swift并在viewDidLoad调用fetchLists方法。 fetchLists方法是另一个帮助器方法。 让我们看一下该方法的实现。

override func viewDidLoad() {
    super.viewDidLoad()
    
    setupView()
    fetchLists()
}

由于购物清单记录存储在用户的私有数据库中,因此我们首先获得对默认容器的私有数据库的引用。 要获取用户的购物清单,我们需要使用CKQuery类在私有数据库上执行查询。

private func fetchLists() {
        // Fetch Private Database
        let privateDatabase = CKContainer.default().privateCloudDatabase
        
        // Initialize Query
        let query = CKQuery(recordType: "Lists", predicate: NSPredicate(value: true))
        
        // Configure Query
        query.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        
        // Perform Query
        privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
            records?.forEach({ (record) in
                
                guard error == nil else{
                    print(error?.localizedDescription as Any)
                    return
                }
                
                print(record.value(forKey: "name") ?? "")
                self.lists.append(record)
                DispatchQueue.main.sync {
                    self.tableView.reloadData()
                    self.messageLabel.text = ""
                    updateView()
                }
            })
    
        }
    }

我们通过调用init(recordType:predicate:)指定的初始化程序来初始化CKQuery实例,并传入记录类型和NSPredicate对象。

在执行查询之前,我们先设置查询的sortDescriptors属性。 我们创建一个包含NSSortDescriptor对象的数组,该对象的键为"name" ,升序设置为true

执行查询就像在privateDatabase上调用performQuery(_:inZoneWithID:completionHandler:)一样简单,将query作为第一个参数传入。 第二个参数指定将在其上执行查询的记录区的标识符。 通过传入nil ,查询将在数据库的默认区域中执行,并且我们获得了从查询返回的每个记录的实例。

在方法的最后,我们调用updateView 。 在此帮助程序方法中,我们基于lists属性的内容更新用户界面。

private func updateView(){
        let hasRecords = self.lists.count > 0
        
        self.tableView.isHidden = !hasRecords
        messageLabel.isHidden = hasRecords
        activityIndicatorView.stopAnimating()
    }

生成并运行该应用程序以测试到目前为止的内容。 我们目前没有任何记录,但是我们将在本教程的下一部分中对其进行修复。

没有找到记录

3.添加购物清单

步骤1:创建AddListViewController

由于添加和编辑购物清单非常相似,因此我们将同时实现两者。 创建一个新文件,并将其命名为AddListViewController.swift 。 打开新创建的文件,并创建一个名为AddListViewControllerUIViewController子类。 在顶部,为UIKitCloudKitSVProgressHUD框架添加导入语句。 声明两个出口,其中之一是UITextField!类型UITextField! 和类型UIBarButtonItem! 。 最后但并非最不重要的一点是,创建两个动作: cancel(_:)save(_:)

import UIKit
import CloudKit
import SVProgressHUD

class AddListViewController: UIViewController {
    
    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var saveButton: UIBarButtonItem!
    
    @IBAction func cancel(sender: AnyObject) {
        
    }
    
    @IBAction func save(sender: AnyObject) {
        
    }
    
}

步骤2:建立使用者介面

打开Main.storyboard,然后将一个视图控制器添加到情节提要中 。 选择视图控制器后,打开右侧的Identity Inspector并将Class设置为AddListViewController

添加一个View Controller并将其设置为AddListViewcontroller

通过点击列表视图控制器中的按钮,用户将能够导航到添加列表视图控制器。

对象栏按钮项从“ 对象库”拖动到列表视图控制器的导航栏。 选中条形按钮项目后,打开“ 属性”检查器,然后将“ 系统项目”设置为“ 添加” 。 按下Control键并从条形按钮项中拖动到添加列表视图控制器,然后从出现的菜单中选择Show Detail

选择刚刚创建的序列 ,然后在右侧的“ 属性”检查器中将“ 标识符”设置为ListDetail

将ListDetail添加到segue的检查器

在添加列表视图控制器的导航栏中添加两个栏按钮项,一个在左边,另一个在右边。 将左栏按钮项的System Item设置为Cancel ,将右栏按钮项的System Item设置为Save 。 最后,将文本字段添加到添加列表视图控制器。 使文本字段居中,并将其“ 对齐方式”设置为“ 属性”检查器中的中心。

添加列表视图控制器

最后,将您在AddListViewController.swift中创建的出口和动作连接到场景中的相应用户界面元素。

步骤3: AddListViewControllerDelegate协议

在实现AddListViewController类之前,我们需要声明一个协议,该协议将用于从添加列表视图控制器到列表视图控制器进行通信。 该协议定义了两种方法,一种用于添加,一种用于更新购物清单。 这就是协议的样子。

protocol AddListViewControllerDelegate {
    func controller(controller: AddListViewController, didAddList list: CKRecord)
    func controller(controller: AddListViewController, didUpdateList list: CKRecord)
}

我们还需要声明三个属性:一个用于委托人,一个用于创建或更新的购物清单,以及一个帮助程序变量,用于指示我们是在创建新的购物清单还是在编辑现有记录。

import UIKit
import CloudKit
import SVProgressHUD

protocol AddListViewControllerDelegate {
    func controller(controller: AddListViewController, didAddList list: CKRecord)
    func controller(controller: AddListViewController, didUpdateList list: CKRecord)
}

class AddListViewController: UIViewController {
    
    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var saveButton: UIBarButtonItem!
    
    var delegate: AddListViewControllerDelegate?
    var newList: Bool = true
    
    var list: CKRecord?
    
    @IBAction func cancel(sender: AnyObject) {
        
    }
    
    @IBAction func save(sender: AnyObject) {
        
    }
    
}

AddListViewController类的实现很简单。 与视图生命周期相关的方法简短易懂。 在viewDidLoad ,我们首先调用setupView helper方法。 我们稍后将实现此方法。 然后,我们基于list属性的值更新newList帮助器变量的值。 如果list等于nil ,那么我们知道我们正在创建一个新记录。 在viewDidLoad ,我们还将视图控制器添加为UITextFieldTextDidChangeNotification通知的观察者。

override func viewDidLoad() {
        super.viewDidLoad()
        
        self.setupView()
        
        // Update Helper
        self.newList = self.list == nil
        
        // Add Observer
        let notificationCenter = NotificationCenter.default
        notificationCenter.addObserver(self, selector: #selector(AddListViewController.textFieldTextDidChange(notification:)), name: NSNotification.Name.UITextFieldTextDidChange, object: nameTextField)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        nameTextField.becomeFirstResponder()
    }

viewDidAppear(_:) ,我们在文本字段上调用becomeFirstResponder以将键盘呈现给用户。

setupView ,我们调用两个帮助器方法, updateNameTextFieldupdateSaveButton 。 如果list不为nil ,则在updateNameTextField填充文本字段。 换句话说,如果我们正在编辑现有记录,那么将使用该记录的名称填充文本字段。

updateSaveButton方法负责启用和禁用右上角的条形按钮项目。 如果购物清单的名称不是空字符串,我们仅启用保存按钮。

private func setupView() {
        updateNameTextField()
        updateSaveButton()
    }
    
    // MARK: -
    private func updateNameTextField() {
        if let name = list?.object(forKey: "name") as? String {
            nameTextField.text = name
        }
    }
    
    // MARK: -
    private func updateSaveButton() {
        let text = nameTextField.text
        
        if let name = text {
            saveButton.isEnabled = !name.isEmpty
        } else {
            saveButton.isEnabled = false
        }
    }

步骤4:执行动作

cancel(_:)操作非常简单。 我们从导航堆栈中弹出顶视图控制器。 save(_:)操作更有趣。 在这种方法中,我们从文本字段中提取用户的输入,并获得对默认容器的私有数据库的引用。

如果要添加新的购物清单,则通过调用init(recordType:)并传入RecordTypeLists作为记录类型来创建新的CKRecord实例。 然后,我们通过设置键"name"的记录值来更新购物清单的"name"

由于保存记录涉及网络请求,并且可能花费很短的时间,因此我们显示了进度指示器。 要保存新记录或对现有记录的任何更改,我们在privateDatabase上调用saveRecord(_:completionHandler:) ,并将记录作为第一个参数传入。 第二个参数是另一个完成处理程序,保存记录成功完成或失败后将调用该完成处理程序。

完成处理程序接受两个参数,一个可选的CKRecord和一个可选的NSError 。 如前所述,可以在任何线程上调用完成处理程序,这意味着我们需要对此进行编码。 我们通过在主线程上显式调用processResponse(_:error:)方法来实现。

@IBAction func cancel(sender: AnyObject) {
        self.dismiss(animated: true, completion: nil) 
    }
    
    @IBAction func save(sender: AnyObject) {
        
        // Helpers
        let name = self.nameTextField.text! as NSString
        
        // Fetch Private Database
        let privateDatabase = CKContainer.default().privateCloudDatabase
        
        if list == nil {
            list = CKRecord(recordType: "Lists")
        }
        
        // Configure Record
        list?.setObject(name, forKey: "name")
        
        // Show Progress HUD
        SVProgressHUD.show()
        
        // Save Record
        privateDatabase.save(list!) { (record, error) -> Void in
            DispatchQueue.main.sync {
                // Dismiss Progress HUD
                SVProgressHUD.dismiss()
                
                // Process Response
                self.processResponse(record: record, error: error)
            }

        }
    }

processResponse(_:error:) ,我们验证是否引发了错误。 如果确实遇到问题,则会向用户显示警报。 如果一切顺利,我们将通知委托并从导航堆栈中弹出视图控制器。

// MARK: -
    // MARK: Helper Methods
    private func processResponse(record: CKRecord?, error: Error?) {
        var message = ""
        
        if let error = error {
            print(error)
            message = "We were not able to save your list."
            
        } else if record == nil {
            message = "We were not able to save your list."
        }
        
        if !message.isEmpty {
            // Initialize Alert Controller
            let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
            
            // Present Alert Controller
            present(alertController, animated: true, completion: nil)
            
        } else {
            // Notify Delegate
            if newList {
                delegate?.controller(controller: self, didAddList: list!)
            } else {
                delegate?.controller(controller: self, didUpdateList: list!)
            }
            
            // Pop View Controller
            self.dismiss(animated: true, completion: nil)

        }
    }

最后但并非最不重要的一点是,当视图控制器收到UITextFieldTextDidChangeNotification通知时,它将调用updateSaveButton来更新保存按钮。

// MARK: -
// MARK: Notification Handling
func textFieldTextDidChange(notification: NSNotification) {
    updateSaveButton()
}

步骤5:将所有内容捆绑在一起

ListsViewController类中,我们仍然需要注意一些事项。 让我们从使类符合AddListViewControllerDelegate协议开始。

class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AddListViewControllerDelegate {
...

这也意味着我们需要实现AddListViewControllerDelegate协议的方法。 在controller(_:didAddList:)方法中,我们将新记录添加到CKRecord对象的数组中。 然后,我们对记录数组进行排序,重新加载表视图,然后在视图控制器上调用updateView

// MARK: -
// MARK: Add List View Controller Delegate Methods
func controller(controller: AddListViewController, didAddList list: CKRecord) {
    // Add List to Lists
    lists.append(list)
    
    // Sort Lists
    sortLists()
    
    // Update Table View
    tableView.reloadData()

    // Update View
    updateView()
}

sortLists方法非常基本。 我们在记录数组上调用sortInPlace ,根据记录的名称对数组进行排序。

private func sortLists() {
        self.lists.sort {
            var result = false
            let name0 = $0.object(forKey: "name") as? String
            let name1 = $1.object(forKey: "name") as? String
            
            if let listName0 = name0, let listName1 = name1 {
                result = listName0.localizedCaseInsensitiveCompare(listName1) == .orderedAscending
            }
            
            return result
        }
    }

AddListViewControllerDelegate协议的第二个方法controller(_:didUpdateList:)看起来几乎相同。 因为我们不添加记录,所以我们只需要对记录数组进行排序并重新加载表视图。 不需要在视图控制器上调用updateView ,因为根据定义,记录数组不是空的。

func controller(controller: AddListViewController, didUpdateList list: CKRecord) {
    // Sort Lists
    sortLists()
    
    // Update Table View
    tableView.reloadData()
}

要编辑记录,用户需要点击表格视图行的附件按钮。 这意味着我们需要实现UITableViewDelegate协议的tableView(_:accessoryButtonTappedForRowWithIndexPath:)方法。 在实现此方法之前,请声明一个助手属性selection ,以存储用户的选择。

class ListsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    static let ListCell = "ListCell"
    
    @IBOutlet weak var messageLabel: UILabel!
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
    
    var lists = [CKRecord]()
    
    var selection: Int?
    
    ...
    
}

tableView(_:accessoryButtonTappedForRowWithIndexPath:) ,我们将用户的选择存储在selection并告诉视图控制器执行导致添加列表视图控制器的任务。

// MARK: -
    // MARK: Segue Life Cycle
    func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
       
        tableView.deselectRow(at: indexPath as IndexPath, animated: true)
        
        // Save Selection
        selection = indexPath.row
        
        // Perform Segue
        performSegue(withIdentifier: "ListDetail", sender: self)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        
        // Fetch Destination View Controller
        let addListViewController = segue.destination as! AddListViewController
        
        // Configure View Controller
        addListViewController.delegate = self
        
        if let selection = selection {
            // Fetch List
            let list = lists[selection]
            
            // Configure View Controller
            addListViewController.list = list
        }
    }

我们快到了。 当执行带有标识符ListDetail的segue时,我们需要配置被推送到导航堆栈的AddListViewController实例。 我们在prepareForSegue(_:sender:)

搜索向我们提供了对目标视图控制器AddListViewController实例的引用。 我们设置了delegate属性,如果购物清单被更新,我们将视图控制器的list属性设置为所选记录。

// MARK: -
// MARK: Segue Life Cycle
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    guard let identifier = segue.identifier else { return }
    
    switch identifier {
    case SegueListDetail:
        // Fetch Destination View Controller
        let addListViewController = segue.destinationViewController as! AddListViewController
        
        // Configure View Controller
        addListViewController.delegate = self
        
        if let selection = selection {
            // Fetch List
            let list = lists[selection]
            
            // Configure View Controller
            addListViewController.list = list
        }
    default:
        break
    }
}

生成并运行该应用程序以查看结果。 现在,您应该能够添加新的购物清单并编辑现有购物清单的名称。

4.删除购物清单

添加删除购物清单的功能并不需要太多的工作。 用户应该能够通过从右向左滑动表格视图行并点击显示的删除按钮来删除购物清单。 为了使之成为可能,我们需要实现UITableViewDataSource协议的另外两种方法:

  • tableView(_:canEditRowAtIndexPath:)
  • tableView(_:commitEditingStyle:forRowAtIndexPath:)

tableView(_:canEditRowAtIndexPath:)很简单,如下所示。

func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}

tableView(_:commitEditingStyle:forRowAtIndexPath:) ,我们从记录数组中提取正确的记录,并在视图控制器上调用deleteRecord(_:) ,传入需要删除的记录。

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {    guard editingStyle == .delete else { return }
    
    // Fetch Record
    let list = lists[indexPath.row]
    
    // Delete Record
    deleteRecord(list)
}

现在, deleteRecord(_:)方法应该看起来很熟悉。 我们显示一个进度指示器,并在默认容器的私有数据库上调用deleteRecordWithID(_:completionHandler:) 。 请注意,我们传递的是记录标识符,而不是记录本身。 完成处理程序接受两个参数,一个可选的CKRecordID和一个可选的NSError

private func deleteRecord(_ list: CKRecord) {
        // Fetch Private Database
        let privateDatabase = CKContainer.default().privateCloudDatabase
        
        // Show Progress HUD
        SVProgressHUD.show()
        
        // Delete List
        privateDatabase.delete(withRecordID: list.recordID) { (recordID, error) -> Void in
            DispatchQueue.main.sync {
                SVProgressHUD.dismiss()
                
                // Process Response
                self.processResponseForDeleteRequest(list, recordID: recordID, error: error)
            }
        }
    }

在完成处理程序中,我们关闭进度指示器并在主线程上调用processResponseForDeleteRequest(_:recordID:error:) 。 在这种方法中,我们检查CloudKit API给我们的recordID值和error ,并相应地更新message 。 如果删除请求成功,则我们将更新用户界面和记录数组。

private func processResponseForDeleteRequest(_ record: CKRecord, recordID: CKRecordID?, error: Error?) {
        var message = ""
        
        if let error = error {
            print(error)
            message = "We are unable to delete the list."
            
        } else if recordID == nil {
            message = "We are unable to delete the list."
        }
        
        if message.isEmpty {
            // Calculate Row Index
            let index = self.lists.index(of: record)
            
            if let index = index {
                // Update Data Source
                self.lists.remove(at: index)
                
                if lists.count > 0 {
                    // Update Table View
                    self.tableView.deleteRows(at: [NSIndexPath(row: index, section: 0) as IndexPath], with: .right)
                    
                } else {
                    // Update Message Label
                    messageLabel.text = "No Records Found"
                    
                    // Update View
                    updateView()
                }
            }
            
        } else {
            // Initialize Alert Controller
            let alertController = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
            
            // Present Alert Controller
            present(alertController, animated: true, completion: nil)
        }
    }

而已。 现在该使用一些数据正确测试应用程序了。 在设备上或iOS模拟器中运行该应用程序,然后添加一些购物清单。 您应该能够添加,编辑和删除购物清单。

结论

即使本文篇幅相当长,也要记住我们只是与CloudKit API进行了短暂的交互。 CloudKit框架的便捷API轻巧且易于使用。

但是,本教程还说明了您作为开发人员的工作不仅限于与CloudKit API进行交互。 重要的是要处理错误,在请求进行中向用户显示,更新用户界面并告诉用户发生了什么。

在本系列的下一篇文章中,我们将通过添加商品填充购物清单的功能来仔细研究关系。 一个空的购物清单没有多大用处,而且肯定不是很有趣。 留下您在以下评论中的任何问题,或通过Twitter与我联系。

翻译自: https://code.tutsplus.com/tutorials/building-a-shopping-list-application-with-cloudkit-adding-records--cms-24731

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值