iOS一些小技巧 | 一

一,NSCAssert,断言。传递表达式,如果返回假值,则抛出异常,输出自定义描述。
例如:

- (void)drawRect:(CGRect)rect {
      NSCAssert(!CGRectIntersectsRect(rect, CGRectZero), @"坐标不是0");
}
//抛出异常并处暑:
2020-11-19 16:37:18.590249+0800 xxxxxDemo[32712:1866711] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '坐标不是0'

2,Xcode instrument内存工具。

Leaks,检测运行时内存是否泄漏;
Allocation,检测对象,消息的内存分配情况;
Activity Monitor,CPU ,内存使用详情查看;
NetWork,检测上传,下载的数据流量。

三,控制台查看swift版本:

swift -version
Apple Swift version 5.3 (swiftlang-1200.0.29.2 clang-1200.0.30.1)
Target: x86_64-apple-darwin19.6.0

四,模块分离思想。

案例,例如微信通讯录列表页,首先,此模块在通讯录模块进行展示,然后如果有第三方分享进入微信,需要模态出当前页面通讯录模块,然后点击某一个好友分享给他。

那么如果一个团队是协同开发,有好多其他模块需要用到此模块,并且其他模块的数据跟此模块中的数据不同,但是为了单独分离出来此模块,让其他开发人员可通过开放接口去调用并且赋值此模块数据,就需要进行模块分离。

只是一个简单的思想,可通过不同的业务进行分离。

首先,当前通讯录View是一个tableview,cell都是同一个cell,例如当前cell中的数据是image,name,age,那么其他开发人员的数据可能是imageVlue,nameVlue,ageValue,但是不可能让其他人员在自己cell配置数据的时候,将自己的model传递过来,重新写一个分配方法,所以需要自定义一个公用的model,PublicModel吧,然后整个cell的数据分配就用PublicModel去分配,其他人传递过来的数据只需要转换成PublicModel即可,然后在cell的点击方法里边将PublicModel传递出去做后续的操作。例如:

    PublicModel *pModel = [[PublicModel alloc]init];
    pModel.name = otherModel.nameValue;
    pModel.age = otherModel.ageValue;
    ......
    //
    [cell settingContentModel:pModel];

模块分离一般适用于公用的业务层,例如登陆,公共的视图显示等。

四,面向切面编程。

iOS中实现AOP的核心技术是Runtime,使用Runtime的Method Swizzling黑魔法。
Method Swizzling比较简单的第三方库有,Aspects。
https://github.com/steipete/Aspects

内部实现原理,通过获取对象或者类对象,再获取selector,将当前类里的selector进行交换,在交换的同事,会分先后进行调用block,告诉外部block的操作,block一般都是日志的记录和上报。例如:

    [UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
        NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
    } error:NULL];

获取了UIViewController为类对象,又获取了viewWillAppear方法,然后内部通过method_exchangeImplementations魔法方法进行交换,被交换的方法是viewWillAppear,交换的方法是内部自定义的方法,例如:

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
    if(self.block) self.block(params);
}

当外部调用viewWillAppear的时候,其实在之前已经将xxx_viewWillAppear方法和其进行了交换,所以内部走了xxx_viewWillAppear方法, 然后xxx_viewWillAppear方法已经和viewWillAppear进行了交换,xxx_viewWillAppear方法内部又调用了xxx_viewWillAppear方法, 其实是调用了viewWillAppear方法,然后将block返回,让外部去进行操作。

面向切片,可以不在其类的内部进行操作,让系统底层将业务逻辑部分进行分离,不影响和改变其正常业务逻辑的代码。
面向切片变成主要用于:日志记录,性能统计,安全控制,事务处理,异常处理。

其实原理就是将其事务在内部进行拦截,然后进行了处理,然后再执行该事务。例如数组越界导致崩溃,可以用此形式进行拦截判断。具体看http://www.iashes.com/2020/11/28-716.html 第八项。

简单用消息转发机制实现的一个hook示例:

@interface HookProxy : NSProxy
+ (instancetype)HookProxyWithTarget:(id)target;
- (void)HookWithSelector:(SEL)selector HookBlock:(void(^)(NSString *desc))hookBlock;
@end
//
#import "HookProxy.h"
@interface HookProxy()
@property (nonatomic,copy)void(^hookBlock)(NSString *desc);
@property (nonatomic,strong)id target;
@property (nonatomic,assign)SEL selector;
@end
@implementation HookProxy

+ (instancetype)HookProxyWithTarget:(id)target;
{
    HookProxy *proxy = [HookProxy alloc];
    if(target) proxy.target = target;
    return proxy;
}
- (void)HookWithSelector:(SEL)selector HookBlock:(void(^)(NSString *desc))hookBlock
{
    if(hookBlock) self.hookBlock = hookBlock;
    if(selector) self.selector = selector;
    [self performSelector:self.selector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    //注册到target中,去接受当前的消息(执行当前的方法)
    return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
    //唤醒target接受消息
    if(self.hookBlock) self.hookBlock(invocation.description);
    [invocation invokeWithTarget:self.target];
}
//使用:
- (void)viewDidLoad {
    [super viewDidLoad];
    HookProxy *hookProxy = [HookProxy HookProxyWithTarget:self];
    [hookProxy HookWithSelector:@selector(testHook) HookBlock:^(NSString * _Nonnull desc) {
        NSLog(@"%@",desc);
    }];
}
- (void)testHook
{
    NSLog(@"This is hook Simulation!");
}

五,关联对象。

objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)
四个参数:
1,要关联的类;
2,关联标识符,可通过当前key找到被关联的对象,一一对应关系;
3,被关联的对象;
4,关联策略,也就是当前被关联对象修饰符类型,策略有,
OBJC_ASSOCIATION_RETAIN_NONATOMIC ,类似于(nonatomic, strong);
OBJC_ASSOCIATION_COPY_NONATOMIC,类似于(nonatomic, copy);
OBJC_ASSOCIATION_RETAIN,类似于(atomic, strong);
OBJC_ASSOCIATION_COPY,类似于(nonatomic, copy);

示例:

@interface UIButton (Block)
- (void)eventWithBlock:(void(^)(id sender))block;
@end
//
#import "UIButton+Block.h"
#import <objc/runtime.h>

@implementation UIButton (Block)
- (void)eventWithBlock:(void(^)(id sender))block
{
    if(block){
        //等同于自动为当前button绑定了一个成员变量,也就是block
        objc_setAssociatedObject(self, @selector(btnAction:), block, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    [self addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)btnAction:(id)sender
{
    //获取当前block
    void(^btBlock)(id sender) =  objc_getAssociatedObject(self, _cmd);
    if(btBlock) btBlock(sender);
}

@end
//使用:
    UIButton *button = [[UIButton alloc]initWithFrame:CGRectMake(20, 100, 70, 70)];
    button.backgroundColor = [UIColor orangeColor];
    [button eventWithBlock:^(id  _Nonnull sender) {
        NSLog(@"%@",[sender description]);
    }];
    [self.view addSubview:button];

objc_getAssociatedObject,通过key获取当类的关联对象,key也就是objc_setAssociatedObject中定义的key。
在objc_setAssociatedObject中,直接用selector作为key,所以在selector内部获取关联对象时直接用_cmd可直接得到key,然后获取关联对象,也可以这么操作:

//关联
objc_setAssociatedObject(self, "this is button block key", block, OBJC_ASSOCIATION_COPY_NONATOMIC);
//获取
void(^btBlock)(id sender) =  objc_getAssociatedObject(self, "this is button block key");
//这里的key必须是一一对应的,才能够找到关联的对象。类似于map中的valueForKey:。

5,debug和release期间的代码调试。

#ifdef DEBUG
    NSLog(@"debug");//debug时候运行
#else
    NSLog(@"release");//上线时候运行
#endif

七,__sync_synchronize();此函数让上下代码顺序执行,会阻碍当前线程。

八,class_addMethod。

此方法判断当前绑定的类中是否实现了某个SEL,如果没有实现,则返回True,实现了则返回False。
参数:
1,对应查询的类(目标类);
2,被查询的SEL;
3,如果没有实现SEL,那么传递的当前要添加新的IMP去实现SEL;
4,替换的IMP一些信息,返回值和参数等。
如果返回True,说明SEL在当前类没有,那么参数3中IMP会添加到当前类。
参数信息是一个char类型的指针,”v@:@”,v标示当前添加的新方法无返回值,@标示返回的对象类型,具体如下图:
method_getTypeEncoding可以获取另外一个method的参数及返回值。

    SEL runSelector = @selector(run);
    //更换成
    SEL runs_Selector = @selector(runs);
    Method runs_Method = class_getInstanceMethod(self.class, runs_Selector);
    bool addMethod = class_addMethod(self.class, runSelector, method_getImplementation(runs_Method), method_getTypeEncoding(runs_Method));

class_replaceMethod,将方法进行了替换,其内部执行了两个方法,class_addMethod和method_exchangeImplementations方法,class_addMethod绑定其方法,method_exchangeImplementations进行方法交换。

class_replaceMethod(Class cls, SEL, imp, types);
四个参数:
1,目标类;
2,被替换的方法SEL(旧);
3,替换的方法IMP,(新);
4,替换的方法信息(新),返回值和参数等;
如下示例:

    SEL runSelector = @selector(run);
    //更换成
    SEL runs_Selector = @selector(runs);
    Method runs_Method = class_getInstanceMethod(self.class, runs_Selector);
//    bool addMethod = class_addMethod(self.class, runSelector, method_getImplementation(runs_Method), method_getTypeEncoding(runs_Method));
    class_replaceMethod(self.class, runSelector, method_getImplementation(runs_Method), method_getTypeEncoding(runs_Method));
    objc_msgSend(self,@selector(run));
//
- (void)run
{
    NSLog(@"run");
}
- (void)runs
{
    NSLog(@"runs_Selector");
}
//输出
2020-12-07 22:51:35.200855+0800 demo[9703:8295268] runs_Selector

九,两个子线程,让第一个线程等待第二个线程执行完再去执行第一个线程里的任务。

    //让第一个线程等待第二个线程执行完再去执行,创建当前信号数量为0
    dispatch_semaphore_t dsema = dispatch_semaphore_create(0);
    [NSThread detachNewThreadWithBlock:^{
        //等待其信号为1的时候,执行wait后的操作,wait执行完之后,信号量变为0
        dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER);
        NSLog(@"1-%@",[NSThread currentThread].description);
    }];
    [NSThread detachNewThreadWithBlock:^{
        NSLog(@"2-%@",[NSThread currentThread].description);
        sleep(2);
        //发送信号,信号量变为1,然后等待线程才去执行。
        dispatch_semaphore_signal(dsema);
    }];

信号量也是一种锁,所以可以让其消息内部只能有一个线程进行操作,例如:

{
    dispatch_semaphore_t _dsema;//成员变量
}
//
- (void)runSemaphoreTextWithInt:(NSInteger)value
{
    //让其内部只能有一个线程对其进行操作,
    //执行完wait后,信号-1变为0,其他线程进来之后会进行等待,直到信号量大于0
    dispatch_semaphore_wait(_dsema, DISPATCH_TIME_FOREVER);
    NSLog(@"Semaphore - %ld",value);
    sleep(2);
    //发送一个信号,说明当前任务已经执行完,信号量+1
    dispatch_semaphore_signal(_dsema);
}
//
- (void)viewDidLoad {
    [super viewDidLoad];
    //创建当前信号为1
    _dsema = dispatch_semaphore_create(1);
//
    dispatch_queue_global_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (NSInteger i = 1; i <= 100; i++) {
        dispatch_async(global, ^{
            [self runSemaphoreTextWithInt:i];
        });
    }
}

输出的数据,间隔都是两秒

十,数据缓存工具,如果读写任务有加锁操作,为什么要写成单例?

首先,数据缓存时,为了数据安全和完整性,所以写数据是必须要进行加锁,保证当前任务只有一个线程进行写操作,多个线程操作会导致数据丢失或者不完整。
要保证对”写任务”进行加锁,必须是同一队列。试想一下,如果不是同一队列,只是在一个子线程中对其进行了加锁,那么外部多个地方调用了”写任务”,其他地方也会进入写操作,数据也是不完整的,例如:
其他任务进入writeOperateWithData方法后,会重新创建子线程,例如第一个创建的线程number为1,第二个为2,那么pthread加锁的分别是1和2,并不是同一个线程任务,所以多个地方调用,会造成多个线程同事操作”写任务”。

- (void)writeOperateWithData:(NSData *)data
{
    [NSThread detachNewThreadWithBlock:^{
        pthread_rwlock_wrlock(&_lock);
        //write operate...
        NSLog(@"%s",__func__);
        pthread_rwlock_unlock(&_lock);
    }];
}

那么如果换成以下操作,会如何:

- (void)writeOperateWithData:(NSData *)data
{
    pthread_rwlock_wrlock(&_lock);
    [NSThread detachNewThreadWithBlock:^{
        //write operate...
        NSLog(@"%s",__func__);
    }];
    pthread_rwlock_unlock(&_lock);
}

这样是可以实现当前写任务,保证其只有一个线程对写任务进行操作,但是弊端就是,会阻碍当前NSThread以外的线程。

那么如果将写任务换成GCD队列进行操作会如何?

- (void)writeOperateWithData:(NSData *)data
{
    //async会创建一个新的线程
    dispatch_async(_sCacheQueue, ^{
        //加锁
        pthread_rwlock_wrlock(&_lock);
        //write operate...
        //解锁
        pthread_rwlock_unlock(&_lock);
    });
}

以上也会创建一个新的线程,也会对新的线程进行加锁,但是每次运行writeOperateWithData方法, 都会创建一个新的线程,(或许也不会创建新的,因为GCD按照内核和网络的分布,最大好像能够创建4-5个线程),那么pthread只会为当前任务进行加锁,也就是说下次再调用写方法,如果再次创建一个新线程,跟之前创建的没有关系,做不到写任务安全。如果在异步任务外部加锁,跟NSThread中类似,会阻碍外部线程。

所以具体实现方式,利用栅栏函数进行加锁比较靠谱:

{
    dispatch_queue_t _sCacheQueue;
}
//
- (instancetype)init
{
    self = [super init];
    if(self){
        _sCacheQueue = dispatch_queue_create("com.iashes.sqliteCache", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}
//
- (void)writeOperateWithData:(NSData *)data
{
    //_sCacheQueue队列中调用栅栏函数
    dispatch_barrier_async(_sCacheQueue, ^{
        //write operate...
    });
}

dispatch_barrier_async,栅栏函数能够对当前队列中的任务进行分割,如果当前任务在栅栏函数任务之前,那么栅栏函数会等待其执行完再去执行,如果栅栏函数在其他任务之前那么先执行栅栏函数,然后执行其他任务,不管其他任务是并发还是非并发。
说白了,栅栏函数可以对当前队列中的任务进行一个加锁,并且不会阻碍其加锁线程以外的其他线程。

以上_sCacheQueue队列是在单例里边,如果写操作也是通过当前队列中的任务去操作,那么如果同时调用读写,那么按照顺序,栅栏函数会在之前或者之后执行,不会同时执行。
dispatch_barrier_async任务在子线程中执行;
dispatch_barrier_sync任务在主线程中执行。

那么归到原题,缓存中如果有多读单写的任务,为什么要写成单例?

首先反转一下, 如果不写成单例,那么每次创建的对象不同,那么队列(也就是当前对象里边的成员列表队列)也不同,所以如果有多个地方调用此实例的写方法,那么会导致栅栏函数只会对当前队列任务进行加锁。
例如第一次创建obj,会有一个_sCacheQueue,然后又创建了一个obj_2,那么也会有新的_sCacheQueue,所以当前栅栏函数只针对每个obj中的_sCacheQueue队列中的任务进行加锁。并不会让其队列中的所有任务进行同步。

所以具体实现为:

@interface SqliteCacheManager : NSObject
+ (instancetype)defaultManager;
@end
//
#import "SqliteCacheManager.h"

@interface SqliteCacheManager()
{
    dispatch_queue_t _sCacheQueue;
}
@end
@implementation SqliteCacheManager
- (instancetype)init
{
    self = [super init];
    if(self){
        //并发队列
        _sCacheQueue = dispatch_queue_create("com.iashes.sqliteCache", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}
+ (instancetype)defaultManager
{
    static dispatch_once_t onceQueue;
    static SqliteCacheManager *cacheManager = nil;
    dispatch_once(&onceQueue, ^{
        cacheManager = [[self alloc] init];
    });
    return cacheManager;
}
- (void)writeOperateWithData:(NSData *)data
{
    //_sCacheQueue队列中调用栅栏函数,这里是异步
    dispatch_barrier_async(_sCacheQueue, ^{
        //write operate...
    });
}
- (void)readOperateWithPath:(NSString *)path
{
    //异步任务读取。
    dispatch_async(_sCacheQueue, ^{
        //read operate...
    });
}

@end
//这里读写会按照顺序进行操作,因为栅栏函数将队列中的任务进行了分割。
//dispatch_barrier_async在前先执行_sCacheQueue中的此任务,如果dispatch_barrier_async在后,后执行_sCacheQueue中的此任务。保证其线程同步,

延展,栅栏函数可以充当队列组group中的notify去使用,例如:

    dispatch_queue_t queue = dispatch_queue_create("com.iashes.testQueue", DISPATCH_QUEUE_CONCURRENT);
    for (NSInteger i = 0; i < 3; i++) {
        dispatch_async(queue, ^{
            NSLog(@"0 - %@ - 同步栅栏,不创建子线程,只是在当前线程中执行",[NSThread currentThread].description);
            //这里可以去请求图片
        });
    }
    //同步栅栏,不创建子线程,只是在当前线程中执行,
    dispatch_barrier_sync(queue, ^{
        //这里可以去刷新view,这里自身就是当前线程,也就是主线程
        NSLog(@"1 - %@ - 这里可以去刷新view,这里自身就是当前线程,也就是主线程",[NSThread currentThread].description);
    });
//输出
2020-12-08 22:43:56.373411+0800 xxxx[12979:8606802] 0 - <NSThread: 0x600001ab10c0>{number = 3, name = (null)} - 同步栅栏,不创建子线程,只是在当前线程中执行
2020-12-08 22:43:56.373473+0800 xxxx[12979:8606802] 0 - <NSThread: 0x600001acdb40>{number = 6, name = (null)} - 同步栅栏,不创建子线程,只是在当前线程中执行
2020-12-08 22:43:56.373497+0800 xxxx[12979:8606801] 0 - <NSThread: 0x600001ab5b00>{number = 2, name = (null)} - 同步栅栏,不创建子线程,只是在当前线程中执行
2020-12-08 22:43:56.373946+0800 xxxx[12979:8606704] 1 - <NSThread: 0x600001afc080>{number = 1, name = main} - 这里可以去刷新view,这里自身就是当前线程,也就是主线程
    [NSThread detachNewThreadWithBlock:^{
        NSLog(@"ns - %@",[NSThread currentThread]);
        dispatch_barrier_sync(queue, ^{
            NSLog(@"br - %@",[NSThread currentThread]);
        });
    }];
//但是这样的话,dispatch_barrier_sync任务就不是主线程任务,它其实就是与dispatch_barrier_sync外层的线程进行同步任务,外层是什么线程,这里的任务就是在哪个线程执行。
读写锁:
//读
pthread_rwlock_rdlock(&_lock);
pthread_rwlock_unlock(&_lock);
//写
pthread_rwlock_wrlock(&_lock);
pthread_rwlock_unlock(&_lock);

十一,宏定义区分模拟器还是设备。

#if TARGET_IPHONE_SIMULATOR
    NSLog(@"run on simulator");
#else
    NSLog(@"run on device");
#endif

十二,读取string文件。

NSString *str  = NSLocalizedStringFromTable(@”skyzizhuTextByTest”, @”File”, @””);

十三,设置动画事物,等待动画结束执行block。

    CALayer *layer = [[CALayer alloc]init];
    layer.bounds = CGRectMake(0, 0, 100, 100);
    layer.position = CGPointMake(120, 400);
    layer.backgroundColor = [UIColor greenColor].CGColor;
    [self.view.layer addSublayer:layer];
    self.layer = layer;
    //
    //设置变化动画过程是否显示,默认为YES不显示
    [CATransaction setValue:(id)kCFBooleanFalse forKey:kCATransactionDisableActions];
    [CATransaction setValue:[NSNumber numberWithFloat:1.0f]forKey:kCATransactionAnimationDuration];
    [CATransaction setCompletionBlock:^{
        //下边事务中的动画执行结束后,执行此block。
        NSLog(@"执行完了");
    }];
    [CATransaction begin];//事务开始
      //设置圆角会有动画过程
    self.layer.cornerRadius = (self.layer.cornerRadius == 0.0f) ? 30.0f : 0.0f;
      //设置透明度会有动画过程
    self.layer.opacity = (self.layer.opacity == 1.0f) ? 0.5f : 1.0f;
    [CATransaction commit];//事务结束

可通过CATransaction为button等控件做一些点击动画效果等。

十四,通过block执行其任务,发现一个好处,主要是思想。

首先一个controller里边定义了一个block类型的属性。
controller的viewDidDisappear方法中,去执行一个方法,但是为了保证当前是被pop或者dismiss掉的,也就是move掉,因为viewDidDisappear方法的执行不仅仅是在当前页面pop或者dismiss的时候,或许是其他页面push进来。
所以用了一个全局的block在当前方法中执行,为了保证当前方法执行完,可以执行dealloc方法,这样就可以置空了block,具体查询SkyQRCodeController类。
也就是说,self可以控制当前block的生命周期。但是如果是-method,那么就不行了。

十五,Masonry优先级,将某一个约束的约束级别进行调整。可通过设置优先级,修复自动布局中的冲突布局。

[self.contentBgView mas_updateConstraints:^(MASConstraintMaker *make) {
    CGFloat height = is ? ScaleForSize(408.0) : ScaleForSize(324.0);
    make.height.mas_equalTo(height).priorityHigh();
}];
分别有:
priorityLow,priorityMedium,priorityHigh三个级别。

十六,UICollectionView获取当前显示内容的大小区域,可根据当前区域调节UICollectionView的大小。
collectionView.collectionViewLayout.collectionViewContentSize。

十七,intrinsicContentSize.height

参考:http://www.iashes.com/2021/02/02-1197.html

十八,masonry设置内边距:

[paddingView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(fistView).insets(UIEdgeInsetsMake(5, 5, 5, 5));
}];

十九,定时器模拟暂停和开始:
//暂停计时器
[weakSelf.timer setFireDate:[NSDate distantFuture]];
//立马执行
[weakSelf.timer setFireDate:[NSDate date]];

二十,为某个view添加标识:
view.accessibilityIdentifier = @””;

二十一,获取当前字符串的高度,并且换取固定行数字符串的高度,以下为2行。

- (CGFloat)getTextHeight:(NSString *)text width:(CGFloat)width fontSize:(CGFloat)fontSize
{
    if(text.length <= 0) return 0.0;
    CGSize newSize = [text boundingRectWithSize:CGSizeMake(width, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:BoleFontSize(fontSize)} context:nil].size;
    CGSize size = [@"*\n*" sizeWithAttributes: @{
                      NSFontAttributeName: BoleFontSize(fontSize)
                  }];
    if(newSize.height >= size.height) return size.height;
    return newSize.height;
}

二十二,系统选取相册模态上来之后,视图有向上偏移,解决办法为:
UIScrollViewContentInsetAdjustmentAutomatic

    if (@available(iOS 11, *)) {
            UIScrollView.appearance.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
    }
    UIImagePickerController *ipc = [[UIImagePickerController alloc] init];
    ipc.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
    ipc.modalPresentationStyle = UIModalPresentationFullScreen;
    ipc.delegate = self;
    ipc.allowsEditing = YES;
    [self presentViewController:ipc animated:YES completion:^{
        UIScrollView.appearance.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    }];

二十三,设置view的圆角4个值都不同:

#import "UIView+Radius.h"
- (void)setCornerRadiusLeftTop:(CGFloat)leftTop rightTop:(CGFloat)rightTop leftBottom:(CGFloat)leftBottom rightBottom:(CGFloat)rightBottom
{
    CGFloat width = self.bounds.size.width;
    CGFloat height = self.bounds.size.height;
    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:CGPointMake(0.0, leftTop)];
    [path addQuadCurveToPoint:CGPointMake(leftTop, 0.0) controlPoint:CGPointZero];
    [path addLineToPoint:CGPointMake(width - rightTop, 0.0)];
    [path addQuadCurveToPoint:CGPointMake(width, rightTop) controlPoint:CGPointMake(width, 0.0)];
    [path addLineToPoint:CGPointMake(width, height - rightBottom)];
    [path addQuadCurveToPoint:CGPointMake(width - rightBottom, height) controlPoint:CGPointMake(width, height)];
    [path addLineToPoint:CGPointMake(leftBottom, height)];
    [path addQuadCurveToPoint:CGPointMake(0.0, height - leftBottom) controlPoint:CGPointMake(0.0, height)];
    [path closePath];
    //
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.frame = self.bounds;
    maskLayer.path = path.CGPath;
    self.layer.mask = maskLayer;
}

Leave a Reply

Required fields are marked *