iOS 14适配(模拟器运行报错;UIImageView 做遮罩;FLAnimatedImage GIF播放)

苹果最近发布 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=).

模拟器运行报错.png

看到的第一眼就是,这问题难不倒我哦,添加一下对 x86_64 的支持就好了。网上相关文章也是特别多,不了解 "i386 x86_64 arm64 armv7 armv7s" 这一堆玩意儿的可以自己百度一下哦。

然后我就开始操作:
project targets -> build settings -> architectures -> VALID_ARCHS

当我操作到第三步的时候,我就傻了,Xcode12 项目配置里找不到 VALID_ARCHS。替代的是 Excluded Architectures。啥、啥、啥、这都是啥啊。

Architectures.png

这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 就出现了。

iOS13.4 效果图.png

本来应该出现上图效果的情况下,在 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
SenderTextNodeBkg@3x.png

我这边用的是第二种方式,通过把汽包图片拉伸到合适的尺寸之后,对展示的图片做遮罩实现的。对 layer.mask 不了解的同学,可以自己百度一下哈。

看到这个现象,首先想到的就是 maskImageView.layer 除了问题,通过断点我发现下面的问题。

iOS14 之前的layer.png
iOS14 之后的layer.png

通过对比可以发现,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;

通过上面代码运行出来的结果是这样的:


结果1.png

由此看来,气泡图片的拉伸出了一点点的问题哦,看来拉伸图片的方法只有把图片放在 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;
结果2.png

通过一顿操作,女神回来了,我也安心了。

3、FLAnimatedImage 播放单次循环GIF,跳转新页面回来之后,就不显示东西了。

今天在看自己的APP,发现一个播放GIF的运营位置,是白白一块儿,页面刷新之后,就开始播放GIF,但是播放一次就停止了,最诡异的是,点击进入二级页面再返回的时候,这一块就啥也不显示了,但是不影响点击事件。

看不懂描述,上图。

我们影响中播放GIF是这样的,不停的循环,就算点击到二级页面回来也是在循环的。

罗老师.gif

但是今天我在 iOS14 上发现是这样的,就是 gif 只播放了一次就暂停了,点击跳转二级页面之后回来就啥没了,我的罗老师呢?

罗老师.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 之后,还会执行下面一堆方法:

一堆方法.png

这一堆方法,最终会调到 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;
}

本文就结束了,后续遇到其他奇怪问题在补充。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容