前情回顾
上周利用周末时间开源了ZSYFrameWork,后来自己在使用过程中,还是发现了不足之处。总结如下:
- 自定义程度虽然很高,但也带来了负面影响,那就是使用起来过于麻烦了
- 只支持传Frame的方式,导致我们使用的时候需要自己去计算Frame,其实整个功能这一步就是最麻烦的一步,很多小伙伴要用库就是想省略这一步。我后来反思,如果这一步都不做,这个库也就失去了最大的意义
- 遮罩不支持调节边距的属性,当然因为之前设计的是传Frame,如果能支持传View的话,那就需要支持调节边距
优化之前,感受一下传Frame的痛苦过程。。。
if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? BigButtonTableViewCell {
let cellRect = tableView.convert(cell.frame, to: view)
maskFrame[0] = [CGRect(x: cellRect.origin.x + leftMargin, y: cellRect.origin.y, width: cellRect.width / 3 - leftMargin * 2, height: cellRect.height), CGRect(x: cellRect.origin.x + cellRect.width / 3 * 2 + leftMargin, y: cellRect.origin.y, width: cellRect.width / 3 - leftMargin * 2, height: cellRect.height)]
maskFrame[1] = [CGRect(x: cellRect.origin.x + cellRect.width / 3 + leftMargin, y: cellRect.origin.y, width: cellRect.width / 3 - leftMargin * 2, height: cellRect.height)]
}
if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 1)) as? TitleTableViewCell {
let cellRect = tableView.convert(cell.frame, to: view)
maskFrame[2] = [CGRect(x: cellRect.origin.x, y: cellRect.origin.y, width: cellRect.width, height: noEventTodayHeight)]
if let events = calendarDetail?.events, events.count > todayItemCount {
maskFrame[2] = [CGRect(x: cellRect.origin.x, y: cellRect.origin.y, width: cellRect.width, height: overTwoEventTodayHeight)]
}
if let events = calendarDetail?.events, events.count > 0, events.count <= todayItemCount {
maskFrame[2] = [CGRect(x: cellRect.origin.x, y: cellRect.origin.y, width: cellRect.width, height: todayTitleHeight + todayItemHeight + todayItemHeight * CGFloat(integerLiteral: events.count))]
}
}
if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 2)) as? TitleTableViewCell {
let cellRect = tableView.convert(cell.frame, to: view)
if cellRect.origin.y > view.height - customerCenterHeight - tabbarHeight - 42 {
maskFrame[3] = [CGRect(x: cellRect.origin.x, y: view.height - customerCenterHeight - tabbarHeight - 42, width: cellRect.width, height: customerCenterHeight)]
} else {
maskFrame[3] = [CGRect(x: cellRect.origin.x, y: cellRect.origin.y, width: cellRect.width, height: customerCenterHeight)]
}
}
制定目标
基于自己的糟糕使用体验,我为自己重新定了一个目标。
- 使用者最好能一行代码调用,傻瓜式集成,只需要传递相应View就能实现完美遮罩效果。
提出问题
想要达到上面的目标,有几个场景需要思考。静态页面初步一想应该没啥问题,问题在于动态View,比如ScrollView或者TableView,当然我这边的实际案例就是基于TableView。
- ScrollView,需要遮罩的View已经接近底部,一半在屏幕内,一半在屏幕外,遮罩是正确的,但用户体验不完美,如果能实现上滑到完全可见,再进行遮罩会更完美。
- ScrollView,需要遮罩的View已经完全在屏幕之外看不见,我们能否帮助使用者将View滑动到屏幕内可见,并进行遮罩。
- TableView,需要遮罩的View在屏幕以外,但TableView跟ScrollView不同,因为存在复用机制,使用者都拿不到那个还未渲染出来的View,我们能否通过其它方式实现滚动后遮罩。
思考问题
思考问题的过程其实就是理清思路的过程,是一个库完整设计的过程,我的理解应该是核心过程。
-
动态View,我们就需要判断遮罩View是否在屏幕内,如果超出屏幕,我们需要滚动到屏幕内再进行遮罩。这样想来ScrollView可能问题不是很大,毕竟可以通过判断View的origin.y和屏幕高度的大小,来确认是否在屏幕以外,只是我需要验证一下scrollRectToVisible这个方法在View一半可见一半不可见时能否起作用,后来实践证明是可行的。
-
但是scrollRectToVisible这个方法需要ScrollView去调用,我们库很难去判断使用者需要遮罩的View是否存在于滚动视图,使用者有可能嵌套了很多View,我们去拿它的superView一层层去遍历不现实,对性能也多多少少会有影响,设计上不应该这样。那么最后我们采取一个折中方案,用户在明确是一个ScrollView的时候,把它传进来,这样对于他使用体验而言其实并无多大差别。
-
滚动完成后的重新绘制可以放在UIScrollView的代理方法
extension ZSYMaskViewController: UIScrollViewDelegate {
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
///重新绘制
}
}
- TableView的难点在于复用机制,使用的时候可能连View都拿不到。库能做到的是通过IndexPath实现滚动遮罩。所以能确认拿到的可以继续传View,有可能拿不到的可以传IndexPath。
- 有了IndexPath,用TableView的scrollToRow方法,滚到相应IndexPath。
open func scrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool)
- 滚动完成,用上面的UIScrollView代理监听,继续重新绘制即可。
解决问题
解决问题的过程其实就是编码的过程了,这里贴出使用案例,详细内容可去ZSYMaskView查看Demo代码
静态页面调用:
let vc = MaskForStaticViewController()
vc.maskView = [0: [greenView, orangeView], 1: [purpleView]]
present(vc, animated: true, completion: nil)
ScrollView页面调用:(多了一个scrollView参数)
let vc = ZSYMaskViewController()
vc.maskView = [0: [greenView], 1: [blueView], 2: [yellowView]]
vc.scrollView = scrollView
present(vc, animated: true, completion: nil)
TableView页面调用:(多了一个tableView参数、invisibleIndexPath参数)
let vc = ZSYMaskViewController()
if let cellView = tableView.cellForRow(at: IndexPath(row: 3, section: 0)) as? CustomTableViewCell {
vc.maskView = [0: [sender], 1: [tableView.cellForRow(at: IndexPath(row: 1, section: 0))!], 2: [cellView.cellView2]]
vc.invisibleIndexPath = [3: IndexPath(row: 7, section: 0)]
vc.tableView = tableView
present(vc, animated: true, completion: nil)
}
通过key-value的形式传入相应View数组,key代表遮罩步骤,因为同一页面可能有多步遮罩,value设计成数组是因为,单步遮罩可能有多个View需要镂空,就比如我们的案例

案例完整展示

总结
还是就像我上一篇说的,优化之路永无止境,一个库要能真正做到能用、易用是一个持续思考的过程,很庆幸,我暂时很享受这个过程。当然要是能开源给更多人使用,给大家带来实质性的便利,才是开源的真正意义。
最后
Github地址:ZSYMaskView
如果这篇文章能给到你们启发,就点个赞👍👍👍吧
如果这个库有帮到你们,就点个star🌟🌟🌟吧
最后的最后,还是我最爱的Tony Stark
