UITableViewRowAction
以及相关的代理方法:tableView(_:editActionsForRowAt:)
。但只是提供了改变 tiltle
和 backgroundColor
的接口,可自定义的空间很小。在如今强调交互的时代,越来越多的 App 开始在 RowAction 上做文章,连 Apple 官方的 Mail 都抛弃其原生的控件,重新实现了 RowAction 的效果,其交互方式也改进了一些。所以我们该如何去自定义 RowAction 呢?
如果只是要修改下 RowAction
中 text 相关的属性,或者添加 icon 的话,可以用一些偏方来实现。比如可以通过修改 cell Button 的 Appearance 来改变 RA 中 Button 的样式:
|
|
但这个方法还是有一些据局限性,比如只适合 有且仅有一个 RowAction,并且 cell 中没有需要自定义 Button 的情况。
另一种方法是,通过 patternImage
来设置 RowAction 的 backgroundColor
。我们可以将文字或 icon 按我们想要的样式、属性绘制成 UIImage
,然后通过 UIColor.init(patternImage:)
来设置 backgroundColor
:
|
|
用这种方法时,需要根据 RowAction
最终的宽度来计算出需要用多少个空格符来组成字符串,给到 RowAction 的 tiltle
。另外原生的 RowAction 的宽度是 title (默认的字体是 (UIFont.systemFont(ofSize: 18)
) 的宽度加上两边的边距(padding = 15)。
具体的实现可以看我写的 demo。
如果想要实现左右滑,或者不满于 RowAction 的交互方式(比如,RowAction 在展开时,会屏蔽 UITableView 上所有的手势,当你想要上滑查看 tableView 上的内容时,RowAction 会截断,并收起 RowAction ,其实这给人感觉很不自然),我们可以自己去实现类似 RowAction 的效果。
其实思路还是比较简单的,主要是处理好自定义的 RowAction 收起和展开时的动效及其连贯性。这里我是将 cell 上要展示的所有内容放到一个新的 contentView 上,然后把这个 contentView 以及 所需的 RowActionButton 根据位置关系放到一个 scrollView 上。之所以用 UIScrollView,主要利用其 setContentOffset(_: animated: )
方法来达到类似原生收起展开的效果。具体的实现可以看我的 demo。
这里主要想说下在交互上的一些处理:
上面的需求都可以通过 Notification
去实现。
另外因为我在 contentView 上添加一个 panGesture
用来展开 RowAction,所以需要处理下该手势和 UITableView 的滚动手势以及侧滑手势的冲突:
|
|
最近有个国外的开发者开源了一个 SwipeCellKit 的项目,其实现思路是,直接改变 cell 的位置,然后利用 iOS 10 推出的新的动画框架 UIViewPropertyAnimator
去实现滑动时的伸缩以及展开收起时的效果。开源的作者是参照 Mail 的 自定义 RowAction 去实现的,并扩展了一些样式的选择,其 API 设计的也很不错,感兴趣的可以看下。
|
|
当把这个 ViewController push 完在 pop 出去后,控制台输出:
显然,析构函数没有被调用,ViewController 和 SomeButton 的实例都没有被释放,因此,两者之间形成了循环强引用。
我们可以通过 Xcode8 新增的调试工具 Memory Graph 来查看上面的循环强引用,如下图:
在上面的例子中,当我们 push 这 ViewController 的时候,会生成一个 ViewController 的实例(为了方便说明,暂且把这个实例称做 aController)。从图中可以看出,aController 持有了 aButton 的强引用,aButton 的 tapHandler 闭包属性因调用了 aController 的 doSomething() 方法而持有了对 aController 的强引用,从而在 aController 与 aButton 之间形成了循环强引用。
在Swift中,我们可以通过定义闭包的捕获列表(Capture List)来解决闭包和类实例之间的循环强引用。Swift 提供了weak
(弱引用)和unowned
(无主引用) 这两个关键字来声明捕获的引用类型。因此,我们可以将 tapHandler 修改成这样:
|
|
或者这样
|
|
当我们再次运行程序,执行 push 和 pop 操作,输出:
|
|
ViewController 和 SomeButton 的实例都成功释放了,也就是说这两种写法都可以解决循环强引用的问题。
weak
还是unowned
呢?Apple 在 《The Swift Programming Language》中是这么说的:
Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.
Conversely, define a capture as a weak reference when the captured reference may become nil at some point in the future.
意思是说:
在闭包和捕获的实例总是相互引用并且同时销毁时,将闭包内的捕获定义为unowned
(无主引用)。
相反的,在被捕获的引用可能会变为nil时,将闭包内的捕获定义为 weak
(弱引用)。
在上面的例子中,aButton 持有了 tapHandler,tapHandler 又持有了 aController,于是 aButton 和 aController “总是相互引用”,即两者都没有被外部的变量所引用。并且当 aController 被销毁的同时 aButton 肯定也被销毁了。所以,在这种情况下,用unowned
就可以了。
那什么情况下需要用到weak
呢?再举个栗子:
|
|
上面是个延时操作的例子,如果声明为unowned
(无主引用),在还没有到达延迟的时间的时候,我们 pop 掉这个 ViewController 的话,程序就会 crash。因为无主引用是非可选类型,pop 掉这个 ViewController 后,闭包中捕获的 self (也就是ViewController的实例)已经被销毁了,这时候再访问被销毁的实例,程序肯定会奔溃的。
所以这里应该声明为weak
(弱引用),弱引用总是可选类型,当引用的实例被销毁后,弱引用的值会自动置为 nil,并且我们可以在闭包内检查它们是否存在。
当然,还有一些不会产生循环强引用,但需要使用weak
来确保代码的安全性的情况。比如执行一些异步操作的时候,我们需要将闭包内捕获的 self 定义为weak
,来应对 self 被提前释放掉,变为 nil 的情况:
|
|
weak
呢?其实从上面的几个例子看来,我们都使用weak
是不会有什么问题的,而且使用weak
的话,看上去好像会使代码更安全,其实不然。Apple 是这么建议的:
If the captured reference will never become nil, it should always be captured as an unowned reference, rather than a weak reference.
大概意思就是说:
如果我们能够确定所捕获的引用类型在闭包体的整个过程中不会被释放的话,就应该使用无主引用,而不是弱引用。
我想这大概是出于两方面的原因:
unowned
就可以解决的问题,如果使用weak
,就会显得多此一举,有时候甚至还需要额外的强制展开和 strongSelf 的判断。unowned
性能方面会更有优势。这里有篇关于weak
和unowned
在不同情况下的性能分析,Twitter 上也有 Chris Eidhof、 Joe Groff 等人关于这个话题的讨论,有兴趣的同学可以看下。当然,具体用weak
还是unowned
,我们还是需要根据实际情况来判断。但当我们了解了循环强引用的形成原因,以及weak
和unowned
的用法和机制后,处理起这些问题就会更加得心应手,
我们的代码也会更加简洁和安全。
PHAsset
的资源对象 PHAssetResource
,通过其 uniformTypeIdentifier
或 originalFilename
属性来判断是否为 GIF:
|
|
关于PHAssetResource
,每个 PHAsset 对象都会引用一个或多个资源(resource),一个被修改过的图片的PHAsset
对象会包含图片编辑之前和之后的 resource,以及关于描述这次编辑的PHAdjustmentData
对象的 resource。我们可以将一个修改过后的 GIF 的 PHAsset
所包含的 PHAssetResource
打印出来看下:
|
|
从打印出的信息可以看到,在 GIF 被修改之后,UTI 从com.compuserve.gif
变成了 public.jpeg
,所以这里如果还通过 UTI 来判断的话,就会错漏,只能通过 fileName 来判断。
另外, PHAssetResource
类只支持 iOS 9.0+。
PHAsset
的元数据来判断:
|
|
同样,这里也需要考虑到 GIF 被修改的情况,requestOption.version
默认是 current
,即如果图片被修改过的话,返回的就是包含所有调整和修改的图像数据,因此我们需要将其设置为 unadjusted
来获取原始的图像数据。
相比上面的方法,这种方法更快速,用时更少。
localIdentifier
是 PHAsset
的父类 PHObject
的一个属性,是每个图片资源独有的标识符。
我在开发 notGIF 的时候,每次启动时都需要遍历相册中的所有图片来获取其中的 GIF,这个操作非常的耗时,十分影响用户体验。我尝试过将其拆分成多个任务,分发到多个线程同时进行,虽然有些效果,但还是不尽如人意,毕竟随着相册中照片数量的增加,其所消耗的时间是线性增长的。
这时候,localIdentifier
就有了用武之地。因为 localIdentifier
是识别图片资源的唯一标识符,所以,我们可以在第一次获取到相册中的 GIF 的时候,将其获取到的所有 GIF 的 localIdentifier
记录下来。这样,下次启动的时候,就可以通过这些 localIdentifier
来直接获取 GIF 资源:
|
|
如果 localIdentifier
所对应的图片资源被删除或不存在的话,PHAsset.fetchAssets(withLocalIdentifiers: options:)
会自动过滤掉。同时,我们可以在后台去检测相册是否有变化,如果有,则更新 UI 以及 所存储的 localIdentifier
的信息。
在适配 iMessage Extension 的时候,需要通过图片的 url 来获取和发送图片,PhotoKit 也提供了获取图片资源 url 的方法:
|
|
你可以独立地更改frame,bounds,和 center,但改变其中一个会对其他的造成影响:
还有,默认情况下,子视图不会被它的父视图剪切。因此,任何位于父视图的框架外部的子视图都是以其整体呈现。你可以改变这个行为,通过设置父视图的 clipsToBounds
属性为 YES。无论子视图可见部分是否被剪切,触摸事件始终遵从目标的父视图的边界矩形。换句话说,在父视图边界矩形外部的那部分视图发生的触摸事件不会发出给视图。
关于contentMode
各个mode间的区别,这张图足以说明:
还有个就是UIViewContentModeRedraw
,当设置为这个mode时,每一次你改变视图的几何状态时,都会调用drawRect:
来响应视图的变化(如果你设置为其他mode的话,drawRect:
只会第一次显示的时候调用,之后只会在已经显示的视图内容上进行变化,不会重新绘制视图)。因此设置这个mode的话会导致视图变化响应变慢,所以你应该尽量避免使用这个值,除非你想在你的自定义视图中以自己的方式来拉伸和调整尺寸。
文档中提到可以用通过设置contentStretch
来使视图 stretchable,只是这个属性在 iOS6.0 的时候就被废除了(-_-!)。不过可以通过 UIImage
的 resizableImageWithCapInsets:
来实现.
|
|
效果如下:
你也可以直接在 Asset Catalog 里面选择图片,点击Show Slicing来设置:
更详细的设置你可以看下这篇 文章。
UIView的 transform
属性提供了一种简单快速的方式来改变视图,但需要注意的一点就是,如果你对视图进行 transform 操作,那么 frame
属性的值是不可靠的,你只能使用 bounds(不会因为transform而改变) 和 center 来获取视图的尺寸和定位。
我在 demo 中写了个例子:对 imageView 进行 rotate 的操作,然后将它的frame
赋给 grayView,从下图中就可以看出你得到的 ImageView 的 frame 并不是它实际上的位置信息。
之前没涉及过这块,就顺便写了个 demo 熟悉一下。
实现步骤:
1.add observer:
|
|
2.connect 后的设置:
|
|
你可以通过 UIScreenMode 来定义了单屏幕模式的属性。可以从它的 availableModes
属性获取屏幕支持的模式的列表并且遍历列表获取的一个来适配你的需求。
尽量少使用自定义绘制,更多的集成已有的视图控件。比如,UIButton 包含了设置标题和背景图像的方法,不应该再自定义嵌入 UILabel 和 UIImageView
尽量不设置视图为透明。设置 opaque
为不透明可以更少的渲染内容,更加的高效。
调整你的视图在滚动时的绘制行为。滚动时会导致短时间内产生大量的视图更新,如果你的绘制代码没有适当的应对,视图在滚动时的性能将会迟滞。所以应该考虑在滚动操作开始时更改你的视图行为。例如,你可以暂时降低内容渲染的质量或滚动过程中更改内容模式。当滚动停止后,你可以回到视图之前的状态和按需要更新内容。