Cell 滑动动作设计和样式自定义

从 iOS 11 开始,iOS 导入了两个新方法来处理表格视图 Cell 中的滑动动作:

tableView(:leadingSwipeActionsConfigurationForRowAt:) 
tableView(:trailingSwipeActionsConfigurationForRowAt:)

这两个方法是被定义在 UITableViewDelegate。第一个方法是处理 “向右滑动” 动作,而第二个是负责 “向左滑动” 动作。下面的案例是

let deleteAction = UIContextualAction(style: .destructive, title: "删除") { (action, sourceView, completionHandler) in

            self.restaurantNames.remove(at:indexPath.row)
                    self.restaurantLocations.remove(at: indexPath.row)
                    self.restaurantTypes.remove(at: indexPath.row)
                    self.restaurantIsVisited.remove(at: indexPath.row)
                    self.restaurantImgs.remove(at: indexPath.row)

                    self.tableView.deleteRows(at: [indexPath], with: .fade)

                    completionHandler(true)
                    //成功完成动作后,需要解除动作按钮
            
        }
        
        let shareAction = UIContextualAction(style: .normal, title: "分享") { (action, sourceView, completionHandler) in
            
            let defaultText = "去预定" + self.restaurantNames[indexPath.row]
            let activityController = UIActivityViewController(activityItems: [defaultText], applicationActivities: nil)
            
            self.present(activityController, animated: true, completion: nil)
            
            completionHandler(true)
            //成功完成动作后,需要解除动作按钮
            
        }

动作设计好后,可以用 UIContextualAction 继续设计它的样式,可以改变它的背景和图标。

UIKit 框架使用一个 UIColor 类别来显示颜色。许多在 UIKit 的方法,要求你使用 UIColor 物件来提供颜色资料。这个类别有内置几个标准颜色,像是 UIColor.blue 与 UIColor.red。如果要使用自己的颜色,可以使用 RGB 分量值来创建 UIColor 物件。这个色彩分量值是落在 0 与 1 之间。RGB 值通常是在 0 至 255 的范围内。为了遵循 UIColor 的要求,在创建 UIColor 物件时,必须要将每一个分量值除以 255。

        deleteAction.backgroundColor = UIColor(red: 231.0/255.0, green: 76.0/255.0, blue: 60.0/255.0, alpha: 1.0)
        deleteAction.image = UIImage(named: "delete")
        
        shareAction.backgroundColor = UIColor(red: 254.0/255.0, green: 149.0/255.0, blue: 38.0/255.0, alpha: 1.0)
        shareAction.image = UIImage(named: "share")
        //自定义删除与分享图标,并手动设置颜色。
        //需在 swipeConfiguration 实例化之前进行设置

这个样式需要在 trailingSwipeActionsConfigurationForRowAt 方法中,swipeConfiguration 实例化之前插入才能生效。

完整的左滑功能实现如下

    override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        
        let deleteAction = UIContextualAction(style: .destructive, title: "删除") { (action, sourceView, completionHandler) in

            self.restaurantNames.remove(at:indexPath.row)
                    self.restaurantLocations.remove(at: indexPath.row)
                    self.restaurantTypes.remove(at: indexPath.row)
                    self.restaurantIsVisited.remove(at: indexPath.row)
                    self.restaurantImgs.remove(at: indexPath.row)

                    self.tableView.deleteRows(at: [indexPath], with: .fade)

                    completionHandler(true)
                    //成功完成动作后,需要解除动作按钮
            
        }
        
        let shareAction = UIContextualAction(style: .normal, title: "分享") { (action, sourceView, completionHandler) in
            
            let defaultText = "去预定" + self.restaurantNames[indexPath.row]
            let activityController = UIActivityViewController(activityItems: [defaultText], applicationActivities: nil)
            
            self.present(activityController, animated: true, completion: nil)
            
            completionHandler(true)
            //成功完成动作后,需要解除动作按钮
            
        }
        
        deleteAction.backgroundColor = UIColor(red: 231.0/255.0, green: 76.0/255.0, blue: 60.0/255.0, alpha: 1.0)
        deleteAction.image = UIImage(named: "delete")
        
        shareAction.backgroundColor = UIColor(red: 254.0/255.0, green: 149.0/255.0, blue: 38.0/255.0, alpha: 1.0)
        shareAction.image = UIImage(named: "share")
        //自定义删除与分享图标,并手动设置颜色。
        //需在 swipeConfiguration 实例化之前进行设置
        
        
        let swipeConfiguration = UISwipeActionsConfiguration(actions: [deleteAction, shareAction])
        //创建 UIContextualAction 物件的阵列,包括了两个动作, deleteAction 与 shareAction
        
        return swipeConfiguration
        //回传 UIContextualAction 物体阵列,告知表格视图在使用者向左滑过 Cell 时创建按钮。
    }
    //左滑功能重做,并实现分享功能

关于 iOS 和 Mac OS 跨平台程序的弹窗 bug

UIActionController 的 .actionSheet 样式,在 iPhone 和 iPad 上会有不同的呈现方式。在 iPad 和 Mac 上不会以强制回应(Modal)对话视窗的方式呈现提示控制器(alert controller),而是使用弹出(popover)的样式。

这个呈现的过程是由 iOS 或 Mac OS 自动处理,当呼叫以下这行程序时,iOS 或 Mac OS 将会检查装置是 iPhone 还是 iPad。

present(optionMenu, animated: true, completion: nil)

如果装置是 iPhone 的话,提示控制器会以强制回应对话视窗来呈现,而在 iPad 和 Mac,提示控制器则会使用储存在 popoverPresentationController 属性中的 UIPopoverPresentationController 物件。

在使用弹出样式来呈现提示视图控制器时,你必须配置 popoverPresentationController 的来源视图,这个来源视图标示了所弹出的矩形视图。如果没有配置的话,在 iPad 运行时会报错闪退(或者卡死)。在把程序设置为可以在 macOS 10.15 上运行,并对 mac 系统进行构建,会发现出现和 iPad 上一样的 bug,会出现卡死。

You must provide location information for this popover through the alert controller’s popoverPresentationController. You must provide either a sourceView and sourceRect or a barButtonItem.

在 iPad 上运行时,会报错如上

现在在 tableView(_:didSelectRowAt:) 的方法中插入以下的程序,将程序放在 optionMenu 的实例之后。

if let popoverController = optionMenu.popoverPresentationController {
    if let cell = tableView.cellForRow(at: indexPath) {
        popoverController.sourceView = cell
        popoverController.sourceRect = cell.bounds
    }
}

当 App 在 iPhone 执行时,提示控制器(也就是 optionMenu)的 popoverPresentationController 属性是设为 nil,反之,在 iPad 执行时,则会存放弹出显示视图控制器(popover presentation controller),因此,我们使用 if let 来检查 popoverPresentationController 属性是否有一个值。如果是的话,我们配置 sourceView 为 Cell,并使用 Cell 来触发动作。另外,我们设定 sourceRect 属性为 Cell 的边界,因此,窗口弹出的原点是来自 Cell 的中心。

这时,这个应用在 iPad 和 macos 上才能无障碍运行。UIActivityViewController 在较大的装置(iPad 和 Mac)会以弹出样式来呈现。这点在开发跨平台应用时应该格外注意。

自适应视图经验总结

标签元素由于 Stack View 设置的元素间距过大,而被截断

为了避免元素被截断,在这个案例中的顶级 Stack View(容纳了一个 Image View 和装着 Name Label 和 Type Label 的 Stack View) 的元素间距设置可以设置为缺省。右图示例就是在 iPhone 11 模拟器下,左右间距为8,元素间距设置为 270 的显示效果。

包含 Name 和 Type 的 Stack View 的元素间距设为缺省
Image View 的显示模式设置为 Aspect Fill
全部的元素和约束关系

Imageview 区块设置了固定高度,并上、左、右和 SuperView 的间距设为0。并和下方的堆叠视图设置好间距。

下方的堆叠视图设置了与 Superview 的左右各8,下部大于5的间距,并在设置时勾选了 「Constrain to margins」。

最后的成品