苹果最近发布 iOS14 正式版本,作为 iOS 开发者,必须要进行一波适配了。网上关于 iOS14 适配的文章已经有挺多的了,通过这些文章我们就可以解决大多数的问题了,比如说:定位、相册、UITableViewCell这些问题。本文就不对这些问题赘述了,大家百度一下就解决了。
本文就记录一下,我在更新 Xcode 之后,项目运行在 iOS14 上的一些问题。主要就是下面的三点问题:
1、Xcode 12 在模拟器上运行项目时报错。
2、使用 UIImageView 的 layer 做遮罩时,不显示任何东西。
3、FLAnimatedImage 播放单次循环GIF,跳转新页面回来之后,就不显示东西了。
1、Xcode 12 在模拟器上运行项目时报错
当我更新完 Xcode 迫不及待的想看一下项目在 iOS14 上是否可以完美运行时,就得到了下面的提示。
No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arch=x86_64, VALID_ARCHS=).
看到的第一眼就是,这问题难不倒我哦,添加一下对 x86_64 的支持就好了。网上相关文章也是特别多,不了解 "i386 x86_64 arm64 armv7 armv7s" 这一堆玩意儿的可以自己百度一下哦。
然后我就开始操作:
project targets -> build settings -> architectures -> VALID_ARCHS
当我操作到第三步的时候,我就傻了,Xcode12 项目配置里找不到 VALID_ARCHS。替代的是 Excluded Architectures。啥、啥、啥、这都是啥啊。
这TMD不是我熟悉的 Xcode,内心一万头曹尼玛奔腾过后,理智告诉自己问题还是要解决的。经过一堆百度、Google 终于找到解决方案。
project targets -> build settings -> user-defined
在 build settings 里,有个 user-defined 这个玩意儿,VALID_ARCHS 现在在这里设置一下就OK了。有的朋友可能又会问了,啊啊啊,我在 user-defined 里也没有找到 VALID_ARCHS 啊,这时候你会看到 Xcode 界面顶上有一个 + ,点击一下,就会恍然大悟。
2、使用 UIImageView 的 layer 做遮罩时,不显示任何东西。
项目终于运行起来,点点看看吧,点着点着 bug 就出现了。
本来应该出现上图效果的情况下,在 iOS14 上竟然空白一片,最骚包的是对点击事件竟然没有影响。研究一下:
上面这种效果应该 iOS 开发老司机应该都知道怎么搞的,就是通过 layer.mask 来实现的。具体来说可以通过下面两种方式:
1、通过 UIBezierPath 画出气泡的样式来做遮罩。
// 绘制气泡,得到对应的layer
UIBezierPath *path = [UIBezierPath bezierPath];
/*
绘制气泡的代码
*/
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
//要展示的图片
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 200, 355.2)];
[imageView setImage:[UIImage imageNamed:@"lock_wallpaper.jpg"]];
[self.view addSubview:imageView];
//设置layer.mask
imageView.layer.mask = layer
2、通过 UIImageView 的 layer 做做遮罩。
//做 mask 的图片
UIImageView *maskImageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 200, 355.2)];
maskImageView.image = [[UIImage imageNamed:@"SenderTextNodeBkg"] stretchableImageWithLeftCapWidth:50 topCapHeight:30];//遮盖图片
//要展示的图片
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 200, 355.2)];
[imageView setImage:[UIImage imageNamed:@"lock_wallpaper.jpg"]];
[self.view addSubview:imageView];
//设置layer.mask
imageView.layer.mask = maskImageView.layer
我这边用的是第二种方式,通过把汽包图片拉伸到合适的尺寸之后,对展示的图片做遮罩实现的。对 layer.mask 不了解的同学,可以自己百度一下哈。
看到这个现象,首先想到的就是 maskImageView.layer 除了问题,通过断点我发现下面的问题。
通过对比可以发现,layer 中少了 contents 的信息。这点不知道是bug,还是官方特意为之。毕竟咱也没看相关的文档。
这样的话,就不能用 UIImageView 的 layer 啦, 要自己搞出一个 layer 了。首先想到的就是通过 UIBezierPath 来绘制,通过这种方案来实现完全是可行的,但是这个气泡里面的圆角、拐点什么的太多了,搞起来就太麻烦,放弃喽。。。
想其它办法,只要肯动脑办法总比困难多。通过查找资料,发现 layer 可以设置 contents,而这个玩意儿可以接收 UIImage 的对象。开搞:
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = imageView.bounds;
[maskLayer setContents:(id)[[UIImage imageNamed:@"SenderTextNodeBkg"] stretchableImageWithLeftCapWidth:50 topCapHeight:30].CGImage];
imageView.layer.mask = maskLayer;
通过上面代码运行出来的结果是这样的:
由此看来,气泡图片的拉伸出了一点点的问题哦,看来拉伸图片的方法只有把图片放在 UIImageView 上才好使哦,那我们就来自己弄个适合尺寸的图片吧。
/// 对当前图片拉伸到对应尺寸的图片
/// @param originImage 原始图片
/// @param newSize 新的尺寸
/// @param leftCapWidth
/// @param topCapHeight
- (UIImage *)stretchImageWithOriginImage:(UIImage *)originImage newSize:(CGSize)newSize leftCapWidth:(CGFloat)leftCapWidth topCapHeight:(CGFloat)topCapHeight{
UIImage *newImage;
newImage = [originImage stretchableImageWithLeftCapWidth:leftCapWidth topCapHeight:topCapHeight];
UIGraphicsBeginImageContextWithOptions(newSize, false, 0);
[newImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = imageView.bounds;
[maskLayer setContents:(id)[self stretchImageWithOriginImage:[UIImage imageNamed:@"SenderTextNodeBkg"] newSize:CGSizeMake(200, 355.2) leftCapWidth:50 topCapHeight:30].CGImage];
imageView.layer.mask = maskLayer;
通过一顿操作,女神回来了,我也安心了。
3、FLAnimatedImage 播放单次循环GIF,跳转新页面回来之后,就不显示东西了。
今天在看自己的APP,发现一个播放GIF的运营位置,是白白一块儿,页面刷新之后,就开始播放GIF,但是播放一次就停止了,最诡异的是,点击进入二级页面再返回的时候,这一块就啥也不显示了,但是不影响点击事件。
看不懂描述,上图。
我们影响中播放GIF是这样的,不停的循环,就算点击到二级页面回来也是在循环的。
但是今天我在 iOS14 上发现是这样的,就是 gif 只播放了一次就暂停了,点击跳转二级页面之后回来就啥没了,我的罗老师呢?
经过我查找资料和实验得出以下几点:
1、GIF 在中做出来的时候是可以选择循环次数的,而罗老师这个 gif 的循环次数设置的就是1次。
2、在 FLAnimatedImage 类中,会通过下面的方法获取循环次数。
_loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
有意思的是同样一张 GIF,iOS14 系统获取的次数是1,而之前的系统获取的次数是0,也就是无线循环。
3、当我把循环次数写死为1的时候,不论哪个版本的系统都会出现上述问题。
我们先不考虑为啥之前获取循环次数不对,就探索一下为啥单次循环时,会出现 UIImageView 不显示内容的问题。
通过观察 FLAnimatedImageView 源码可以发现。
// 当我们设置动图的时候会执行这个方法。
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
/*
省略的代码
*/
if (animatedImage) {
// 当我们设置动图的时候,会把 ImageView 的 image 置为 nil
super.image = nil;
// Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
super.highlighted = NO;
// UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image.
[self invalidateIntrinsicContentSize];
} else {
// Stop animating before the animated image gets cleared out.
[self stopAnimating];
}
/*
省略的代码
*/
}
// gif 动画的实现追踪依赖这个方法
- (void)displayDidRefresh:(CADisplayLink *)displayLink{
/*
省略的代码
*/
//取出对应帧的图片
UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
if (image) {
FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
//记录当前帧的图片
self.currentFrame = image;
if (self.needsDisplayWhenImageBecomesAvailable) {
//通过不停的重绘layer,实现 GIF 效果。会触发 displayLayer 这代理方法。
[self.layer setNeedsDisplay];
self.needsDisplayWhenImageBecomesAvailable = NO;
}
/*
省略的代码
*/
}
//绘制layer
- (void)displayLayer:(CALayer *)layer
{
//取出当前图片,绘制 layer.contents
layer.contents = (__bridge id)self.image.CGImage;
}
// 重写 image getter 方法
- (UIImage *)image
{
UIImage *image = nil;
if (self.animatedImage) {
//如果是动图,就返回当前帧图片
image = self.currentFrame;
} else {
//不然就是 imageview 的 image。
image = super.image;
}
return image;
}
上面这一堆逻辑看起来都是没有问题的,在 gif 为无限循环时,也是可以完美运行的。那么就要看一下程序对循环次数固定的 gif 是怎么处理的吧。
- (void)displayDidRefresh:(CADisplayLink *)displayLink{
/*
省略的代码
*/
while (self.accumulator >= delayTime) {
self.accumulator -= delayTime;
self.currentFrameIndex++;
if (self.currentFrameIndex >= self.animatedImage.frameCount) {
// If we've looped the number of times that this animated image describes, stop looping.
self.loopCountdown--;
if (self.loopCompletionBlock) {
self.loopCompletionBlock(self.loopCountdown);
}
//这里我们可以发现,当 loopCountdown == 0 的时候,我们的 GIF 就会停掉。
if (self.loopCountdown == 0) {
[self stopAnimating];
return;
}
self.currentFrameIndex = 0;
}
// Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
// Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
self.needsDisplayWhenImageBecomesAvailable = YES;
}
/*
省略的代码
*/
}
看上去上面对 gif 停止的操作也是没有问题的,GIF 停止时所展示的画面是 GIF 最后一帧的图片,所展示的内容是通过 layer.contents 绘制出来的。
但是事实告诉我们当页面发生跳转时,内容就消失掉了,具体为啥我这边也没有查到相关的资料。当时我们可以自己实验:
当我写下下面代码的时候,我的预想的结果应该是会显示图片的
let imageView:FLAnimatedImageView = FLAnimatedImageView.init()
self.view.addSubview(imageView)
imageView.layer.contents = UIImage.init(named: "yjk_header_refresh")?.cgImage
但事实是毛都没有,为啥呢?经过我的研究发现这么一个问题,就是当我们设置过 layer.contents 之后,还会执行下面一堆方法:
这一堆方法,最终会调到 UIImageView image 的 getter 方法,而这个时候 image 就是 nil,然后 UIImageView 没有东西就解释的过去了。
当我们设置 layer.contents 时做个延迟,果然图片就显示了。
dispense_after({
imageView.layer.contents = UIImage.init(named: "yjk_header_refresh")?.cgImage
}, 5)
按照这个思路,我以为页面跳转的时候也是因为 image 被赋值为 nil 而导致的内容消失的,可事实并不是,因为 UIImageView image 的 getter 方法并没有执行。
我发现在页面 push 时,didMoveToWindow 会执行3次,再 pop 时,又会执行一次。对于 didMoveToWindow 解释,官方文档是这样的。
The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the window changes.
The [`window`](https://developer.apple.com/documentation/uikit/uiview/1622456-window) property may be `nil` by the time that this method is called, indicating that the receiver does not currently reside in any window. This occurs when the receiver has just been removed from its superview or when the receiver has just been added to a superview that is not attached to a window. Overrides of this method may choose to ignore such cases if they are not of interest.
大概意思是窗口变化就会执行,其实我没太明白,查了资料也啥都没有查到,在 didMoveToWindow 执行第二次的时候,打印 layer.contents 发现这个玩意儿没东西了。具体在哪里被干掉了也没整明白。。。
当我把 UIImageView 换成 UIView 时,就不存在跳转时 layer.contents 被置空的问题了。应该是 UIImageView 内部对 layer 又搞了什么玩意儿,没有源码太难了。所以到最后也没整明白为啥。。。
知道问题所在我们就可以有解决方案了,方案不止一种,我用的是下面的方法:
在动画结束的时候,重新给 image 赋值。
if (self.loopCountdown == 0) {
super.image = image;
[self stopAnimating];
return;
}
我自己又做了一些探究,发现了另外一个问题:
//当页面跳转发生跳转时,会执行这个方法
- (void)didMoveToWindow{
[super didMoveToWindow];
// 更新是否需要动画的方法,
[self updateShouldAnimate];
// 即使GIF已经到达最大次数而停止, self.shouldAnimate 该字段仍然时 ture,因为它值与循环次数没有关系。这样就导致 displayDidRefresh
if (self.shouldAnimate) {
[self startAnimating];
} else {
[self stopAnimating];
}
}
//更新是否需要动画的方法
- (void)updateShouldAnimate
{
BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
self.shouldAnimate = self.animatedImage && isVisible;
}
本文就结束了,后续遇到其他奇怪问题在补充。