使用 GPUImage 实现一个简单相机

本文介绍了如何使用 GPUImage 来实现一个简单的相机。具体功能包括拍照、录制视频、多段视频合成、实时美颜、自定义滤镜实现等。

前言

AVFoundation 是苹果提供的用于处理基于时间的媒体数据的一个框架。我们想要实现一个相机,需要从手机摄像头采集数据,离不开这个框架的支持。GPUImage 对 AVFoundation 做了一些封装,使我们的采集工作变得十分简单。

另外,GPUImage 的核心魅力还在于,它封装了一个链路结构的图像数据处理流程,简称滤镜链。滤镜链的结构使得多层滤镜的叠加功能变得很容易实现。

在下面介绍的功能中,有一些和 GPUImage 本身的关系并不大,我们是直接调用 AVFoundation 的 API 来实现的。但是,这些功能也是一个相机应用必不可少的一部分。所以,我们也会简单讲一下每个功能的实现方式和注意事项。

滤镜链简介

在 GPUImage 中,对图像数据的处理都是通过建立滤镜链来实现的。

这里就涉及到了一个类 GPUImageOutput 和一个协议 GPUImageInput 。对于继承了 GPUImageOutput 的类,可以理解为具备输出图像数据的能力;对于实现了 GPUImageInput 协议的类,可以理解为具备接收图像数据输入的能力。

顾名思义,滤镜链作为一个链路,具有起点和终点。根据前面的描述,滤镜链的起点应该只继承了 GPUImageOutput 类,滤镜链的终点应该只实现了 GPUImageInput 协议,而对于中间的结点应该同时继承了 GPUImageOutput 类并实现了 GPUImageInput 协议,这样才具备承上启下的作用。

一、滤镜链起点

在 GPUImage 中,只继承了 GPUImageOutput,而没有实现 GPUImageInput 协议的类有六个,也就是说有六种类型的输入源:

1、GPUImagePicture

GPUImagePicture 通过图片来初始化,本质上是先将图片转化为 CGImageRef,然后将 CGImageRef 转化为纹理。

2、GPUImageRawDataInput

GPUImageRawDataInput 通过二进制数据初始化,然后将二进制数据转化为纹理,在初始化的时候需要指明数据的格式(GPUPixelFormat)。

3、GPUImageTextureInput

GPUImageTextureInput 通过已经存在的纹理来初始化。既然纹理已经存在,在初始化的时候就不会重新去生成,只是将纹理的索引保存下来。

4、GPUImageUIElement

GPUImageUIElement 可以通过 UIView 或者 CALayer 来初始化,最后都是调用 CALayer 的 renderInContext: 方法,将当前显示的内容绘制到 CoreGraphics 的上下文中,从而获取图像数据。然后将数据转化为纹理。简单来说就是截屏,截取当前控件的内容。

这个类可以用来实现在视频上添加文字水印的功能。因为在 OpenGL 中不能直接进行文本的绘制,所以如果我们想把一个 UILabel 的内容添加到滤镜链里面去,使用 GPUImageUIElement 来实现是很合适的。

5、GPUImageMovie

GPUImageMovie 通过本地的视频来初始化。首先通过 AVAssetReader 来逐帧读取视频,然后将帧数据转化为纹理,具体的流程大概是:AVAssetReaderOutput -> CMSampleBufferRef -> CVImageBufferRef -> CVOpenGLESTextureRef -> Texture 。

6、GPUImageVideoCamera

GPUImageVideoCamera 通过相机参数来初始化,通过屏幕比例和相机位置(前后置)来初始化相机。这里主要使用 AVCaptureVideoDataOutput 来获取持续的视频流数据输出,在代理方法 captureOutput:didOutputSampleBuffer:fromConnection: 中可以拿到 CMSampleBufferRef ,将其转化为纹理的过程与 GPUImageMovie 类似。

然而,我们在项目中使用的是它的子类 GPUImageStillCamera。 GPUImageStillCamera 在原来的基础上多了一个 AVCaptureStillImageOutput,它是我们实现拍照功能的关键,在 captureStillImageAsynchronouslyFromConnection:completionHandler: 方法的回调中,同样能拿到我们熟悉 CMSampleBufferRef。

简单来说,GPUImageVideoCamera 只能录制视频,GPUImageStillCamera 还可以拍照,因此我们使用 GPUImageStillCamera 。

二、滤镜

滤镜链的关键角色是 GPUImageFilter,它同时继承了 GPUImageOutput 类并实现了 GPUImageInput 协议。GPUImageFilter 实现承上启下功能的基础是「渲染到纹理」,这个操作我们在 《使用 iOS OpenGL ES 实现长腿功能》 一文中已经介绍过了,简单来说就是将结果渲染到纹理而不是屏幕上。

这样,每一个滤镜都能把输出的纹理作为下一个滤镜的输入,实现多层滤镜效果的叠加。

三、滤镜链终点

在 GPUImage 中,实现了 GPUImageInput 协议,而没有继承 GPUImageOutput 的类有四个:

1、GPUImageMovieWriter

GPUImageMovieWriter 封装了 AVAssetWriter,可以逐帧从帧缓存的渲染结果中读取数据,最后通过 AVAssetWriter 将视频文件保存到指定的路径。

2、GPUImageRawDataOutput

GPUImageRawDataOutput 通过 rawBytesForImage 属性,可以获取到当前输入纹理的二进制数据。

假设我们的滤镜链在输入源和终点之间,连接了三个滤镜,而我们需要拿到第二个滤镜渲染后的数据,用来做人脸识别。那我们可以在第二个滤镜后面再添加一个 GPUImageRawDataOutput 作为输出,则可以拿到对应的二进制数据,且不会影响原来的渲染流程。

3、GPUImageTextureOutput

这个类的实现十分简单,提供协议方法 newFrameReadyFromTextureOutput:,在每一帧渲染结束后,将自身返回,通过 texture 属性就可以拿到输入纹理的索引。

4、GPUImageView

GPUImageView 继承自 UIView,通过输入的纹理,执行一遍渲染流程。这次的渲染目标不是新的纹理,而是自身的 layer 。

这个类是我们实现相机功能的重要组成部分,我们所有的滤镜效果,都要依靠它来呈现。

功能实现

一、拍照

拍照功能只需调用一个接口就能搞定,在回调方法中可以直接拿到 UIImage。代码如下:

- (void)takePhotoWtihCompletion:(TakePhotoResult)completion {

    GPUImageFilter *lastFilter = self.currentFilterHandler.lastFilter;

    [self.camera capturePhotoAsImageProcessedUpToFilter:lastFilter withCompletionHandler:^(UIImage *processedImage, NSError *error) {

        if (error && completion) {

            completion(nil, error);

            return;

        }

        if (completion) {

            completion(processedImage, nil);

        }

    }];

}

值得注意的是,相机的预览页面由 GPUImageView 承载,显示的是整个滤镜链作用的结果。而我们的拍照接口,可以传入这个链路上的任意一个滤镜,甚至可以在后面多加一个滤镜,然后拍照接口会返回对应滤镜的渲染结果。即我们的拍照结果不一定要和我们的预览一致。

示意图如下:

二、录制视频

1、单段录制

录制视频首先要创建一个 GPUImageMovieWriter 作为链路的输出,与上面的拍照接口类似,这里录制的视频不一定和我们的预览一样。

整个过程比较简单,当我们调用停止录制的接口并回调之后,视频就被保存到我们指定的路径了。

- (void)setupMovieWriter {
    NSString *videoPath = [SCFileHelper randomFilePathInTmpWithSuffix:@".m4v"];
    NSURL *videoURL = [NSURL fileURLWithPath:videoPath];
    CGSize videoSize = self.videoSize;

    self.movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:videoURL
                                                                size:videoSize];

    GPUImageFilter *lastFilter = self.currentFilterHandler.lastFilter;
    [lastFilter addTarget:self.movieWriter];
    self.camera.audioEncodingTarget = self.movieWriter;
    self.movieWriter.shouldPassthroughAudio = YES;
    self.currentTmpVideoPath = videoPath;

}

- (void)recordVideo {
    [self setupMovieWriter];
    [self.movieWriter startRecording];
}

- (void)stopRecordVideoWithCompletion:(RecordVideoResult)completion {
    @weakify(self);
    [self.movieWriter finishRecordingWithCompletionHandler:^{
        @strongify(self);
        [self removeMovieWriter];
        if (completion) {
            completion(self.currentTmpVideoPath);
        }
    }];

}

2、多段录制

在 GPUImage 中并没有提供多段录制的功能,需要我们自己去实现。

首先,我们要重复单段视频的录制过程,这样我们就有了多段视频的文件路径。然后主要实现两个功能,一个是 AVPlayer 的多段视频循环播放;另一个是通过 AVComposition 来合并多段视频,并用 AVAssetExportSession 来导出新的视频。

整个过程逻辑并不复杂,出于篇幅的考虑,代码就不贴了,请到项目中查看。

三、保存

在拍照或者录视频结束后,通过 PhotoKit 保存到相册里。

1、保存图片

- (void)writeImageToSavedPhotosAlbum:(UIImage *)image

                          completion:(void (^)(BOOL success))completion {

    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{

        [PHAssetChangeRequest creationRequestForAssetFromImage:image];

    } completionHandler:^(BOOL success, NSError * _Nullable error) {

        if (completion) {

            completion(success);

        }

    }];

}

2、保存视频

- (void)saveVideo:(NSString *)path completion:(void (^)(BOOL success))completion {

    NSURL *url = [NSURL fileURLWithPath:path];

    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{

        [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url];

    } completionHandler:^(BOOL success, NSError * _Nullable error) {

        dispatch_async(dispatch_get_main_queue(), ^{

            if (completion) {

                completion(success);

            }

        });

    }];

}

四、闪光灯

系统的闪光灯类型通过 AVCaptureDevice 的 flashMode 属性来控制,其实只有三种,分别是:

  • AVCaptureFlashModeOff 关闭
  • AVCaptureFlashModeOn 开启(在拍照的时候会闪一下)
  • AVCaptureFlashModeAuto 自动(系统会自动判断当前的环境是否需要闪光灯)
但是市面上的相机应用,一般还有一种常亮类型,这种类型在夜间的时候会比较适用。这个功能需要通过 torchMode 属性来实现,它其实是指手电筒。

我们对这个两个属性做一下封装,允许这四种类型来回切换,下面是根据封装的类型来同步系统类型的代码:

- (void)syncFlashState {

    AVCaptureDevice *device = self.camera.inputCamera;

    if (![device hasFlash] || self.camera.cameraPosition == AVCaptureDevicePositionFront) {

        [self closeFlashIfNeed];

        return;

    }



    [device lockForConfiguration:nil];



    switch (self.flashMode) {

        case SCCameraFlashModeOff:

            device.torchMode = AVCaptureTorchModeOff;

            device.flashMode = AVCaptureFlashModeOff;

            break;

        case SCCameraFlashModeOn:

            device.torchMode = AVCaptureTorchModeOff;

            device.flashMode = AVCaptureFlashModeOn;

            break;

        case SCCameraFlashModeAuto:

            device.torchMode = AVCaptureTorchModeOff;

            device.flashMode = AVCaptureFlashModeAuto;

            break;

        case SCCameraFlashModeTorch:

            device.torchMode = AVCaptureTorchModeOn;

            device.flashMode = AVCaptureFlashModeOff;

            break;

        default:

            break;

    }



    [device unlockForConfiguration];

}

五、画幅比例

相机的比例通过设置 AVCaptureSession 的 sessionPreset 属性来实现。这个属性并不只意味着比例,也意味着分辨率。

由于不是所有的设备都支持高分辨率,所以这里只使用 AVCaptureSessionPreset640x480 和 AVCaptureSessionPreset1280x720 这两个分辨率,分别用来作为 3:4 和 9:16 的输出。

市面上的相机除了上面的两个比例外,一般还支持 1:1 和 Full (iPhoneX 系列的全屏)比例,但是系统并没有提供对应比例的 AVCaptureSessionPreset 。

这里可以通过 GPUImageCropFilter 来实现,这是 GPUImage 的一个内置滤镜,可以对输入的纹理进行裁剪。使用时通过 cropRegion 属性来传入一个归一化的裁剪区域。

切换比例的关键代码如下:

- (void)setRatio:(SCCameraRatio)ratio {

    _ratio = ratio;



    CGRect rect = CGRectMake(0, 0, 1, 1);

    if (ratio == SCCameraRatio1v1) {

        self.camera.captureSessionPreset = AVCaptureSessionPreset640x480;

        CGFloat space = (4 - 3) / 4.0; // 竖直方向应该裁剪掉的空间

        rect = CGRectMake(0, space / 2, 1, 1 - space);

    } else if (ratio == SCCameraRatio4v3) {

        self.camera.captureSessionPreset = AVCaptureSessionPreset640x480;

    } else if (ratio == SCCameraRatio16v9) {

        self.camera.captureSessionPreset = AVCaptureSessionPreset1280x720;

    } else if (ratio == SCCameraRatioFull) {

        self.camera.captureSessionPreset = AVCaptureSessionPreset1280x720;

        CGFloat currentRatio = SCREEN_HEIGHT / SCREEN_WIDTH;

        if (currentRatio > 16.0 / 9.0) { // 需要在水平方向裁剪

            CGFloat resultWidth = 16.0 / currentRatio;

            CGFloat space = (9.0 - resultWidth) / 9.0;

            rect = CGRectMake(space / 2, 0, 1 - space, 1);

        } else { // 需要在竖直方向裁剪

            CGFloat resultHeight = 9.0 * currentRatio;

            CGFloat space = (16.0 - resultHeight) / 16.0;

            rect = CGRectMake(0, space / 2, 1, 1 - space);

        }

    }

    [self.currentFilterHandler setCropRect:rect];

    self.videoSize = [self videoSizeWithRatio:ratio];

}

六、前后置切换

通过调用 GPUImageVideoCamera 的 rotateCamera 方法来实现。

另外,由于前置摄像头不支持闪光灯,如果在前置的时候去切换闪光灯,只能修改我们封装的类型。所以在切换到后置的时候,需要去同步一下系统的闪光灯类型:

- (void)rotateCamera {

    [self.camera rotateCamera];

    // 切换摄像头,同步一下闪光灯

    [self syncFlashState];

}

七、对焦

AVCaptureDevice 的 focusMode 用来设置聚焦模式,focusPointOfInterest 用来设置聚焦点;exposureMode 用来设置曝光模式,exposurePointOfInterest 用来设置曝光点。

前置摄像头只支持设置曝光,后置摄像头支持设置曝光和聚焦,所以在设置之前要先判断是否支持。

需要注意的是,相机默认输出的图像是横向的,图像向右偏转。而前置摄像头又是镜像,所以图像是向左偏转。我们从 UIView 获得的触摸点,要经过相应的转化,才是正确的坐标。关键代码如下:

- (void)setFocusPoint:(CGPoint)focusPoint {

    _focusPoint = focusPoint;



    AVCaptureDevice *device = self.camera.inputCamera;



    // 坐标转换

    CGPoint currentPoint = CGPointMake(focusPoint.y / self.outputView.bounds.size.height, 1 - focusPoint.x / self.outputView.bounds.size.width);

    if (self.camera.cameraPosition == AVCaptureDevicePositionFront) {

        currentPoint = CGPointMake(currentPoint.x, 1 - currentPoint.y);

    }



    [device lockForConfiguration:nil];



    if ([device isFocusPointOfInterestSupported] &&

        [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {

        [device setFocusPointOfInterest:currentPoint];

        [device setFocusMode:AVCaptureFocusModeAutoFocus];

    }

    if ([device isExposurePointOfInterestSupported] &&

        [device isExposureModeSupported:AVCaptureExposureModeAutoExpose]) {

        [device setExposurePointOfInterest:currentPoint];

        [device setExposureMode:AVCaptureExposureModeAutoExpose];

    }



    [device unlockForConfiguration];

}

八、改变焦距

改变焦距简单来说就是画面的放大缩小,通过设置 AVCaptureDevice 的 videoZoomFactor 属性实现。

值得注意的是,这个属性有最大值和最小值,设置之前需要做好判断,否则会直接崩溃。代码如下:

- (void)setVideoScale:(CGFloat)videoScale {

    _videoScale = videoScale;



    videoScale = [self availableVideoScaleWithScale:videoScale];



    AVCaptureDevice *device = self.camera.inputCamera;

    [device lockForConfiguration:nil];

    device.videoZoomFactor = videoScale;

    [device unlockForConfiguration];

}



- (CGFloat)availableVideoScaleWithScale:(CGFloat)scale {

    AVCaptureDevice *device = self.camera.inputCamera;



    CGFloat maxScale = kMaxVideoScale;

    CGFloat minScale = kMinVideoScale;

    if (@available(iOS 11.0, *)) {

        maxScale = device.maxAvailableVideoZoomFactor;

    }



    scale = MAX(scale, minScale);

    scale = MIN(scale, maxScale);



    return scale;

}

九、滤镜

1、滤镜的使用

当我们想使用一个滤镜的时候,只需要把它加到滤镜链里去,通过 addTarget: 方法实现。来看一下这个方法的定义:

- (void)addTarget:(id<GPUImageInput>)newTarget;

可以看到,只要实现了 GPUImageInput 协议,就可以成为滤镜链的下一个结点。

2、美颜滤镜

目前美颜效果已经成为相机应用的标配,我们也来给自己的相机加上美颜的效果。

美颜效果本质上是对图片做模糊,想要达到比较好的效果,需要结合人脸识别,只对人脸的部分进行模糊处理。这里并不去探究美颜算法的实现,直接找开源的美颜滤镜来用。

目前找到的实现效果比较好的是 LFGPUImageBeautyFilter ,虽然它的效果肯定比不上现在市面上的美颜类 APP,但是作为学习级别的 Demo 已经足够了。

效果展示:

3、自定义滤镜

打开 GPUImageFilter 的头文件,可以看到有下面这个方法:

- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString 

            fragmentShaderFromString:(NSString *)fragmentShaderString;

很容易理解,通过一个顶点着色器和一个片段着色器来初始化,并且可以看到是字符串类型。

另外,GPUImageFilter 中还内置了简单的顶点着色器和片段着色器,顶点着色器代码如下:

NSString *const kGPUImageVertexShaderString = SHADER_STRING

(

 attribute vec4 position;

 attribute vec4 inputTextureCoordinate;



 varying vec2 textureCoordinate;



 void main()

 {

     gl_Position = position;

     textureCoordinate = inputTextureCoordinate.xy;

 }

);

这里用到了 SHADER_STRING 宏,看一下它的定义:

#define STRINGIZE(x) #x

#define STRINGIZE2(x) STRINGIZE(x)

#define SHADER_STRING(text) @ STRINGIZE2(text)

在 #define 中的 # 是「字符串化」的意思,返回 C 语言风格字符串,而 SHADER_STRING 在字符串前面加了一个 @ 符号,则 SHADER_STRING 的定义就是将括号中的内容转化为 OC 风格的字符串。

我们之前都是为着色器代码单独创建两个文件,而在 GPUImageFilter 中直接以字符串的形式,写死在代码中,两种方式本质上没什么区别。

当我们想自定义一个滤镜的时候,只需要继承 GPUImageFilter 来定义一个子类,然后用相同的方式来定义两个保存着色器代码的字符串,并且用这两个字符串来初始化子类就可以了。

作为示例,我把之前实现的 抖音滤镜 也添加到这个工程里,来看一下效果:

总结

通过上面的步骤,我们实现了一个具备基础功能的相机。之后会在这个相机的基础上,继续做一些有趣的尝试,欢迎持续关注~

源码

请到 GitHub (https://github.com/lmf12/SimpleCam) 上查看完整代码。

Vue中Axios的封装和API接口的管理

在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御XSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。如果还对axios不了解的,可以移步axios文档。

发布于:6天以前  |  52次阅读  |  详细内容 »

iOS 持续集成:更完备的 App Store Connect API

时隔两年 App Store Connect API 有了更新,WWDC 2018 推出了 App Store Connect API ,用于自动化一些 App Store Connect 后台操作。这次更新包含了 app 元数据相关的API,补上了原来缺失的重要一环, 使得几乎可以通过 App Store Connect API 完成 App Store Connect 上的所有操作。今后开发、证书配置、用户管理、测试、发布全流程都可以通过 API 完成。

发布于:9天以前  |  69次阅读  |  详细内容 »

iOS 性能优化:优化 App 启动速度

苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote[1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%

发布于:10天以前  |  68次阅读  |  详细内容 »

让你的应用远离越狱:iOS 14 App Attest 防护功能

当越狱在 iOS 设备第一次流行起来时,iOS 开发人员会尝试各种方法来保护自己的应用程序,以让应用免受盗版等不确定因素的困扰。有许多方法可以做到这一点,包括检查 Cydia 是否存在、检测应用程序是否可读取自身沙箱之外的文件、在检测到调试器时让应用程序崩溃等等。

发布于:17天以前  |  92次阅读  |  详细内容 »

探秘 iOS 14 的 WidgetKit

Widget Extension 提供了 small, medium, large 三个尺寸,不同尺寸可以展示不同的数据、不同的界面,开发者也可以锁定自己APP的 Widget 只有某类尺寸,相同的widget也能重复添加。作为添加在主屏幕上的控件,苹果用了 “At a glance” 来形容 widget ,所以 widget extension 是无法交互的,它能做的只有展示一些信息与点击两个作用,点击后就会引导至app,同时为了性能与耗电量的考虑,Widget extension 也不能展示视频和动态图像。

发布于:19天以前  |  111次阅读  |  详细内容 »

iOS14 Widget 万字指北,先人一步获得顶级流量

2020 年 6 月 22 日,苹果召开了第一次线上的开发者大会 - WWDC20。这次发布会上宣布了ARM架构Mac芯片(拳打Intel)、iOS 14 ATT(脚踢Facebook),可谓是一次载入史册(我是爸爸)的发布会了,当然还发布了被称为下一个顶级流量入口的Widget。踩着八月的尾巴,本次我们就来探究一下Widget。本文会从Widget初窥和Widget开发两个维度和章节来探究一下Widget, 其中初窥章节会带您简单的了解一下Widget,适合应用决策者阅读; 开发章节会带着您一步一步的完成设计开发Widget,适合程序员阅读。

发布于:19天以前  |  113次阅读  |  详细内容 »

OCRunner:完全体的iOS热修复方案

为了能够实现一篇文章的思路:Objective-C源码 -> 二进制补丁文件 ->热更新(具体是哪篇我忘了)。当时刚好开始了oc2mango翻译器的漫漫长路(顺带为了学习编译原理,嘻嘻),等基本完成以后,就开始肝OCRunner:完全兼容struct,enum,系统C函数调用,魔改libffi,生成补丁文件等,尽可能兼容Objective-C,为了做一个直接运行OC的快乐人。

发布于:19天以前  |  109次阅读  |  详细内容 »

iOS APP图标版本化

在我们的项目开发过程中,需要频繁打包给测试人员去测试,有时候我们都不知道测试机上安装的版本是否是最新的,这样会造成很多不必要的麻烦和成本。因此我们需要将buildNumber以水印的方式打在APPIcon上,可以很直观的知道当前是哪一个版本。

发布于:19天以前  |  99次阅读  |  详细内容 »

百度App iOS工程化实践: EasyBox破冰之旅

百度App从单一的搜索工具发展到今天以搜索和Feed流为双引擎的综合性内容消费服务平台,其复杂程度已然不可同日而语矣。 作为一个日活过亿的超级App,业务规模庞大,相关技术人员超过千人,客户端支持主流的移动技术,涉及近百业务方,技术形态复杂,各种组件近三百个,代码百万量级,由此带来的工程化问题是技术团队的一个极大挑战。

发布于:19天以前  |  116次阅读  |  详细内容 »

百度APP iOS暗黑模式适配的完美解决方案

在2019WWDC的开场演讲中,苹果公布了即将推出的iOS13 DarkMode的新特性。此新特性不仅可以在夜晚保护视力,而且对于使用OLED的最新一代设备而言,也可以帮助用户节省电量消耗。不过此特性只支持iOS13以上的系统,为了给全系统所有用户最好的体验,研发出了一套皮肤主题框架,不仅可以全系统支持DarkMode,还可以扩展多套皮肤主题;

发布于:19天以前  |  104次阅读  |  详细内容 »

iOS 14 - 使用 PHPicker 选择照片和视频

绝大多数的 App 都要和相册打交道,选择照片或者视频,要么用来发个朋友圈,要么是放到什么地方做个背景。从 AssertLibrary 到 Photos 框架,苹果已经在多年之前就给相册相关的 API 做过一次大升级了。

发布于:21天以前  |  131次阅读  |  详细内容 »

iOS开发体验优化方案

随着Flutter等跨端框架的出现,业务开发同学经常需要在Android/iOS上跨端进行业务开发,问题定位等。新的不熟悉的环境的搭建总会遇到各种各样的问题,导致搭建失败,特别是iOS开发环境,是最复杂的,不仅环境搭建繁琐,而且切分支后的打包速度很慢,所以我们设计实现了两个工具,用于优化闲鱼iOS开发体验。

发布于:1月以前  |  161次阅读  |  详细内容 »

iOS14:再见了,“流氓”APP!

最近和苹果有关的重大消息可能就是从8月1日开始,AppStore中国区火速下架未获版号的游戏APP,数量超过30000款,之前小智就和大家说过,这未必不是一件好事,众多低质和“流氓”APP将被最大限度隔绝在iOS系统之外。

发布于:1月以前  |  197次阅读  |  详细内容 »

最多阅读

快速配置 Sign In with Apple 1年以前  |  3244次阅读
开篇 关于iOS越狱开发 1年以前  |  2350次阅读
给数组NSMutableArray排序 1年以前  |  2349次阅读
APP适配iOS11 1年以前  |  2274次阅读
在越狱的iPhone设置上使用lldb调试 1年以前  |  2253次阅读
UITableViewCell高亮效果实现 1年以前  |  2177次阅读
App Store 审核指南[2017年最新版本] 1年以前  |  2104次阅读
使用 GPUImage 实现一个简单相机 1年以前  |  2077次阅读
所有iPhone设备尺寸汇总 1年以前  |  2035次阅读
使用ssh访问越狱iPhone的两种方式 1年以前  |  1952次阅读
关于Xcode不能打印崩溃日志 1年以前  |  1950次阅读
使用ssh 访问越狱iPhone的两种方式 1年以前  |  1817次阅读
UIDevice的简单使用 1年以前  |  1678次阅读
为对象添加一个释放时触发的block 1年以前  |  1637次阅读
使用最高权限操作iPhone手机 1年以前  |  1582次阅读