想必大家都比较熟悉 UITableView 的 RowAction 了,Apple 在 iOS 2.0 的时候就提供了相关的接口,当时只有 “Delete” 的选项,在 iOS 8.0 之后,增加了 UITableViewRowAction 以及相关的代理方法:tableView(_:editActionsForRowAt:)。但只是提供了改变 tiltlebackgroundColor 的接口,可自定义的空间很小。在如今强调交互的时代,越来越多的 App 开始在 RowAction 上做文章,连 Apple 官方的 Mail 都抛弃其原生的控件,重新实现了 RowAction 的效果,其交互方式也改进了一些。所以我们该如何去自定义 RowAction 呢?

在原生控件上自定义

如果只是要修改下 RowAction 中 text 相关的属性,或者添加 icon 的话,可以用一些偏方来实现。比如可以通过修改 cell Button 的 Appearance 来改变 RA 中 Button 的样式:

1
2
3
let btnAppearance = UIButton.appearance(whenContainedInInstancesOf:[TheCell.self])
let attString = NSAttributedString(string: "Delete", attributes: textAttributes)
btnAppearance.setAttributedTitle(attString, for: .normal)

但这个方法还是有一些据局限性,比如只适合 有且仅有一个 RowAction,并且 cell 中没有需要自定义 Button 的情况。

另一种方法是,通过 patternImage 来设置 RowAction 的 backgroundColor。我们可以将文字或 icon 按我们想要的样式、属性绘制成 UIImage,然后通过 UIColor.init(patternImage:) 来设置 backgroundColor

1
2
3
4
5
6
7
8
9
10
11
12
UIGraphicsBeginImageContextWithOptions(rowActionSize, false, UIScreen.main.nativeScale)
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(bgColor.cgColor)
context.fill(CGRect(origin: .zero, size: rowActionSize))
icon.draw(in: iconRect)
title.draw(in: titleRect, withAttributes: titleAttributes)
let patternImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
backgroundColor = UIColor(patternImage: patternImage)

用这种方法时,需要根据 RowAction 最终的宽度来计算出需要用多少个空格符来组成字符串,给到 RowAction 的 tiltle。另外原生的 RowAction 的宽度是 title (默认的字体是 (UIFont.systemFont(ofSize: 18)) 的宽度加上两边的边距(padding = 15)。

具体的实现可以看我写的 demo

自定义 RowAction

如果想要实现左右滑,或者不满于 RowAction 的交互方式(比如,RowAction 在展开时,会屏蔽 UITableView 上所有的手势,当你想要上滑查看 tableView 上的内容时,RowAction 会截断,并收起 RowAction ,其实这给人感觉很不自然),我们可以自己去实现类似 RowAction 的效果。

其实思路还是比较简单的,主要是处理好自定义的 RowAction 收起和展开时的动效及其连贯性。这里我是将 cell 上要展示的所有内容放到一个新的 contentView 上,然后把这个 contentView 以及 所需的 RowActionButton 根据位置关系放到一个 scrollView 上。之所以用 UIScrollView,主要利用其 setContentOffset(_: animated: ) 方法来达到类似原生收起展开的效果。具体的实现可以看我的 demo

这里主要想说下在交互上的一些处理:

  • 当 RowAction 在展开状态时,用户点击 tableView 上任何区域(包括该 cell),收起该 RowAction。
  • 当用户滑动 tableView 时,不应该像原生那样截断手势,而是默认收起并滚动。
  • 当某个 cell 的 RA 在展开状态时,用户去展开其他 cell,则应收起该 cell 并展开另一个 cell。

上面的需求都可以通过 Notification 去实现。

另外因为我在 contentView 上添加一个 panGesture 用来展开 RowAction,所以需要处理下该手势和 UITableView 的滚动手势以及侧滑手势的冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let ges = gestureRecognizer as? UIPanGestureRecognizer,
ges === panGesture {
let gesLocation = ges.location(in: self)
if gesLocation.x <= 22 { // 处理和侧滑手势的冲突
return false
} else { // 处理和 tableView 滚动手势的冲突
let translation = ges.translation(in: superview)
return fabs(translation.x) > fabs(translation.y)
}
} else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
}

最近有个国外的开发者开源了一个 SwipeCellKit 的项目,其实现思路是,直接改变 cell 的位置,然后利用 iOS 10 推出的新的动画框架 UIViewPropertyAnimator 去实现滑动时的伸缩以及展开收起时的效果。开源的作者是参照 Mail 的 自定义 RowAction 去实现的,并扩展了一些样式的选择,其 API 设计的也很不错,感兴趣的可以看下。

swipeCellKit_demo