iOS 清理相似照片、视频、联系人、日历

最近做了一款清理类的 APP,说实话,iOS 清理真没什么可清理的,只能清理下相似照片,重复联系人,日历事件等

照片、视频管理类

相似照片统计

我这里获取了相册的所有信息,并将相似照片,相似视频,照片大于10M 和 视频大于 20M 的大文件也整理了处理,可以直接调用,在属性上直接获取,记得刷新 UI 的时候要在主线程进行操作,否则会崩溃

调用方法如下

- (ClearPhotoManager *)clearPhotoManager
{
    if (!_clearPhotoManager)
    {
        _clearPhotoManager = [ClearPhotoManager shareManager];
    }
    return _clearPhotoManager;
}

-(void)loadPhotoData
{
    // 加载照片数据源
    CleanSelf(self);
    
    [CleanOneLoading showWithMessage:CleanLanguage(@"Loading...",@"加载中...")];
    
    [self.clearPhotoManager getAllBigAsset:^(BOOL success) {

       // 这里有很多属性,自己打印看看,这里列举了两个
        CleanNsLog(@"%@",weakself.clearPhotoManager.similarInfo);
        CleanNsLog(@"%@",weakself.clearPhotoManager.screenshotsInfo);
        
        dispatch_async(dispatch_get_main_queue(), ^{

            [weakself pushHomeVc];
            
            [CleanOneLoading disMiss];
        });
    }];
}

ClearPhotoManager.h

//
//  ClearPhotoManager.h
//
//  Created by Three Project on 21/8/2023.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger, PhotoNotificationStatus)
{
    PhotoNotificationStatusDefualt  = 0, // 相册变更默认处理
    PhotoNotificationStatusClose    = 1, // 相册变更不处理
    PhotoNotificationStatusNeed     = 2, // 相册变更主动处理
};

@protocol ClearPhotoManagerDelegate <NSObject>

@optional
/// 相册变动代理方法
- (void)clearPhotoLibraryDidChange;

@end

@interface ClearPhotoManager : NSObject

/// 单例
+ (ClearPhotoManager *)shareManager;

/// 代理
@property (nonatomic, weak) id<ClearPhotoManagerDelegate> delegate;

/// 变更状态
@property (nonatomic, assign) PhotoNotificationStatus notificationStatus;

// 这里是 PHASet 对象
@property (nonatomic, strong, readonly) NSMutableArray *allPhotoArr;
@property (nonatomic, strong, readonly) NSMutableArray *allVideoArr;


/// 相似照片信息:存储了相似图片数量及可以节省的内存空间大小
@property (nonatomic, strong, readonly) NSDictionary *similarInfo;
@property (nonatomic, strong, readonly) NSMutableArray *similarArr;

/// 截图照片信息:存储了屏幕截图数量及可以节省的内存空间大小
@property (nonatomic, strong, readonly) NSDictionary *screenshotsInfo;
@property (nonatomic, strong, readonly) NSMutableArray *screenshotsArr;

/// short视频信息
@property (nonatomic, strong, readonly) NSDictionary *shortVideoInfo;
@property (nonatomic, strong, readonly) NSMutableArray *shortVideoArr;

/// 相似视频信息
@property (nonatomic, strong, readonly) NSDictionary *duplicateVideoInfo;
@property (nonatomic, strong, readonly) NSMutableArray *duplicateVideoArr;

///  大文件图片
@property (nonatomic, strong, readonly) NSMutableArray *bigPhotoArr;
///  大文件视频 - 长视频
@property (nonatomic, strong, readonly) NSMutableArray *bigVideoArr;
@property (nonatomic, strong, readonly) NSDictionary *bigVideoInfo;


// 获取相册的 PHAsset 对象
- (void)loadPhotoCompletionHandler:(void (^)(BOOL success))completion isVideo:(BOOL)isVideo;

/// 删除照片
+ (void)deleteAssets:(NSArray<PHAsset *> *)assets completionHandler:(void (^)(BOOL success, NSError *error))completion;

// 获取相册中的大文件
- (void)getAllBigAsset:(void (^)(BOOL success))completion;

@end

NS_ASSUME_NONNULL_END

ClearPhotoManager.m

//
//  ClearPhotoManager.m
//
//  Created by Three Project on 21/8/2023.
//

#import "ClearPhotoManager.h"
#import "CleanAlbumModel.h"
#import "ImageCompare.h"

#define REQUEST_VIDEO_QUEUE "com.video.queue"

#define VIDEO_MAXSIZE 20 * 1024 * 1024
#define PHOTO_MAXSIZE 10 * 1024 * 1024


@interface ClearPhotoManager ()<PHPhotoLibraryChangeObserver>

// 获取相簿中的所有PHAsset对象
@property (nonatomic, strong) PHFetchResult *assetArray;

// 这里是 PHASet 对象
@property (nonatomic, strong, readwrite) NSMutableArray *allPhotoArr;
@property (nonatomic, strong, readwrite) NSMutableArray *allVideoArr;

/// 相似照片信息:存储了相似图片数量及可以节省的内存空间大小
@property (nonatomic, strong, readwrite) NSDictionary *similarInfo;
@property (nonatomic, strong, readwrite) NSMutableArray *similarArr;

/// 截图照片信息:存储了屏幕截图数量及可以节省的内存空间大小
@property (nonatomic, strong, readwrite) NSDictionary *screenshotsInfo;
@property (nonatomic, strong, readwrite) NSMutableArray *screenshotsArr;

/// short视频信息
@property (nonatomic, strong, readwrite) NSDictionary *shortVideoInfo;
@property (nonatomic, strong, readwrite) NSMutableArray *shortVideoArr;

/// 相似视频信息
@property (nonatomic, strong, readwrite) NSDictionary *duplicateVideoInfo;
@property (nonatomic, strong, readwrite) NSMutableArray *duplicateVideoArr;

///  大文件图片
@property (nonatomic, strong, readwrite) NSMutableArray *bigPhotoArr;
///  大文件视频
@property (nonatomic, strong, readwrite) NSMutableArray *bigVideoArr;
@property (nonatomic, strong, readwrite) NSDictionary *bigVideoInfo;

// 完成的回调
@property (nonatomic, copy) void (^completionHandler)(BOOL success);
@property (nonatomic, copy) void (^bigCompletionHandler)(BOOL success);


// PHImageManager的requestImageForAsset所需要的options
@property (nonatomic, strong) PHImageRequestOptions *imageRequestOptions;
// PHImageManager的requestImageDataForAsset所需要的options
@property (nonatomic, strong) PHImageRequestOptions *imageSizeRequestOptions;

@property (assign,nonatomic) BOOL isPhotoFinish;
@property (assign,nonatomic) BOOL isVideoFinish;

@end

@implementation ClearPhotoManager

#pragma mark - 单例
+ (ClearPhotoManager *)shareManager
{
    static ClearPhotoManager *clearPhotoManager = nil;
    static dispatch_once_t token;
    dispatch_once(&token, ^{
        clearPhotoManager = [[ClearPhotoManager alloc] init];
    });
    return clearPhotoManager;
}

#pragma mark - 相册变换通知

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        // 相册变换通知
        [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
    }
    return self;
}

- (void)dealloc
{
    // 移除相册变换通知
    [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}

// 相册变换时候会调用
- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
    // 筛选出没必要的变动
    PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetArray];
   
    if (collectionChanges == nil || self.notificationStatus != PhotoNotificationStatusDefualt)
    {
        return;
    }
    
    // 回到主线程调用相册变动代理方法
    dispatch_async(dispatch_get_main_queue(), ^{
        
        if ([self.delegate respondsToSelector:@selector(clearPhotoLibraryDidChange)])
        {
            [self.delegate clearPhotoLibraryDidChange];
        }
    });
}



// 处理图片,获取到需要清理的相似图片和截屏图片
- (void)dealImageWithArray:(NSArray *)assetArray
{
    CleanSelf(self);
    
    
    [self getPhotoDetail:assetArray completion:^(NSArray *assets) {
        
        // 相似图片
        weakself.similarArr = [weakself similarPhotoArray:assets];
        
        for(NSInteger i=0;i<assets.count;i++)
        {
            PHAsset *lastAsset = assets[i][@"asset"];
            
            if (lastAsset.mediaSubtypes == PHAssetMediaSubtypePhotoScreenshot)
            {
                [weakself.screenshotsArr addObject:assets[I]];
            }
            
            NSString *sizeStr = [NSString stringWithFormat:@"%@",assets[i][@"originImageDataLength"]];
                     
            CGFloat sizeFloat = [sizeStr floatValue];
                     
            if (sizeFloat > PHOTO_MAXSIZE)
            {
                [weakself.bigPhotoArr addObject:assets[I]];
            }
        }
                   
        // 处理图片
        weakself.screenshotsInfo = [weakself arangeArray:weakself.screenshotsArr];
            
        weakself.isPhotoFinish = YES;

        weakself.completionHandler(YES);
        
    }];
}




#pragma mark -- 处理视频

- (void)dealVideoWithArray:(NSArray *)assetArray
{
    CleanSelf(self);
    
    [self getVideoDetail:assetArray completion:^(NSArray *assets) {
                
        // 相似视频
        weakself.duplicateVideoArr = [weakself similarVideoArray:assets];

        for(NSInteger i=0;i<assets.count;i++)
        {
            NSString *sizeStr = [NSString stringWithFormat:@"%@",assets[i][@"originImageDataLength"]];

            NSInteger sizeFloat = [sizeStr integerValue];
            
            if (sizeFloat > VIDEO_MAXSIZE)
            {
                [weakself.bigVideoArr addObject:assets[I]];
            }
            else
            {
                [weakself.shortVideoArr addObject:assets[I]];
            }
        }
        
        // 处理视频
        weakself.bigVideoInfo = [weakself arangeArray:weakself.bigVideoArr];
        weakself.shortVideoInfo = [weakself arangeArray:weakself.shortVideoArr];
        
        weakself.isVideoFinish = YES;

        weakself.completionHandler(YES);

    }];

}


// 处理相似视频
-(NSMutableArray *)similarVideoArray:(NSArray *)assets
{
    NSMutableArray *inputArr = [NSMutableArray arrayWithArray:assets];
    
    NSMutableArray *duplicateVideoArr = [NSMutableArray array];
        
    NSInteger totalSize = 0;
    
    for (int i = 0; i < inputArr.count; i++) {
    
        NSMutableArray *tempArr = [NSMutableArray array];
        
        NSDictionary *dict1 = inputArr[I];
        
        NSInteger fileSize1 = [dict1[@"originImageDataLength"] integerValue];
        
        NSInteger duration1 = [dict1[@"duration"] integerValue];
        
        [tempArr addObject:dict1];
        
        for (int j = i + 1; j < inputArr.count; j++) {
        
            NSDictionary *dict2 = inputArr[j];
            
            NSInteger fileSize2 = [dict2[@"originImageDataLength"] integerValue];
            
            NSInteger duration2 = [dict2[@"duration"] integerValue];
            
            if (fileSize1 == fileSize2 && duration1 == duration2) {
            
                [tempArr addObject:dict2];
            }
        }
        
        if (tempArr.count > 1) {
                    
            [duplicateVideoArr addObject:tempArr];
            
            [inputArr removeObjectsInArray:tempArr];
            
            i -= 1;
        }
    }
    
    NSInteger totalCount = 0;
    
    for (NSArray *disArr in duplicateVideoArr) {
        totalCount += disArr.count;
        for (NSDictionary *dict in disArr) {
            NSInteger fileSize = [dict[@"originImageDataLength"] integerValue];
            totalSize += fileSize;
        }
    }
    NSDictionary *duplicateVideosDict = @{@"totalCount":@(totalCount), @"totalSize":@(totalSize), @"dataArr":duplicateVideoArr};
    
    self.duplicateVideoInfo = duplicateVideosDict;
    
    return duplicateVideoArr;
}


// 处理相似图片
-(NSMutableArray *)similarPhotoArray:(NSArray *)assets
{
    NSMutableArray *inputArr = [NSMutableArray arrayWithArray:assets];
    
    NSMutableArray *duplicateVideoArr = [NSMutableArray array];
        
    NSInteger totalSize = 0;
    
    for (int i = 0; i < inputArr.count; i++) {
    
        NSMutableArray *tempArr = [NSMutableArray array];
        
        NSDictionary *dict1 = inputArr[I];
        
        PHAsset *asset1 = dict1[@"asset"];
        
        UIImage *image1 = dict1[@"exactImage"];
  
        [tempArr addObject:dict1];
        
        for (int j = i + 1; j < inputArr.count; j++) {
        
            NSDictionary *dict2 = inputArr[j];
            
            PHAsset *asset2 = dict2[@"asset"];
            
            UIImage *image2 = dict2[@"exactImage"];
            
            BOOL isSameDay = [self isSameDay:asset1.creationDate date2:asset2.creationDate];

            if(isSameDay)
            {
                CleanNsLog(@"%@ === %@",asset1.creationDate,asset2.creationDate);
                
//                double isLike = [SimalPhotoAction getSimilarityValueWithImgA:image1 ImgB:image2];
                BOOL isLike = [ImageCompare isImage:image1 likeImage:image2];

                if(isLike)
                {
                    [tempArr addObject:dict2];
                }
            }
        }
        
        if (tempArr.count > 1) {
                    
            [duplicateVideoArr addObject:tempArr];
            
            [inputArr removeObjectsInArray:tempArr];
            
            i -= 1;
        }
    }
    
    NSInteger totalCount = 0;
    
    for (NSArray *disArr in duplicateVideoArr) {
        totalCount += disArr.count;
        for (NSDictionary *dict in disArr) {
            NSInteger fileSize = [dict[@"originImageDataLength"] integerValue];
            totalSize += fileSize;
        }
    }
    NSDictionary *duplicateVideosDict = @{@"totalCount":@(totalCount), @"totalSize":@(totalSize), @"dataArr":duplicateVideoArr};
    
    self.similarInfo = duplicateVideosDict;
    
    return duplicateVideoArr;
}



-(NSDictionary *)arangeArray:(NSMutableArray *)muArray
{
    CGFloat size = 0;
    
    for(NSDictionary *dict in muArray)
    {
        size += [dict[@"originImageDataLength"] floatValue];
    }

    NSDictionary *dict = @{
      
        @"totalCount":@(muArray.count),
        @"totalSize":@(size),
        @"dataArr":muArray
    };
    
    return dict;
}



// 加载照片之前先清除旧数据
- (void)resetTagData
{
    // 相册清空
    self.allPhotoArr = nil;
    self.similarInfo = nil;
    self.similarArr = nil;
    self.screenshotsInfo = nil;
    self.screenshotsArr = nil;
    self.bigPhotoArr = nil;

    // 视频数据清空
    self.allVideoArr = nil;
    self.shortVideoInfo = nil;
    self.shortVideoArr = nil;
    self.duplicateVideoInfo = nil;
    self.duplicateVideoArr = nil;
    self.bigVideoArr = nil;
    self.bigVideoInfo = nil;
}

#pragma mark - 加载照片:日期

// 是否为同一天
- (BOOL)isSameDay:(NSDate *)date1 date2:(NSDate *)date2
{
    // 有一个日期为空则直接返回
    if (!date1 || !date2)
    {
        return NO;
    }
    
    // 从日历上分别获取date1、date2的年月日
    NSCalendar *calendar = [NSCalendar currentCalendar];
    unsigned unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute;
    NSDateComponents *dateComponents1 = [calendar components:unitFlags fromDate:date1];
    NSDateComponents *dateComponents2 = [calendar components:unitFlags fromDate:date2];
    
    
    // 比较年月日,是否是同一天
    if((dateComponents1.day == dateComponents2.day) && (dateComponents1.month == dateComponents2.month) && (dateComponents1.year == dateComponents2.year))
    {
        // 如果是同一天,那就计算两者的时间差是不是在一个小时之内
        NSInteger dateOneSecond = dateComponents1.minute;
        NSInteger dateTwoSecond = dateComponents2.minute;
        NSInteger Difference = labs(dateTwoSecond - dateOneSecond);

        if(Difference < 30)
        {
            return YES;
        }
        else
        {
            return NO;
        }
    }
        
    return NO;
}

// NSDate转NSString
- (NSString *)stringWithDate:(NSDate *)date
{
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"yyyy-MM-dd"];
    return [dateFormatter stringFromDate:date];
}

#pragma mark - 删除照片

// 删除照片
+ (void)deleteAssets:(NSArray<PHAsset *> *)assets completionHandler:(void (^)(BOOL, NSError * _Nonnull))completion
{
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        // 删除当前图片资源
        [PHAssetChangeRequest deleteAssets:assets];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        // 调用删除后的回调代码块
        if (completion)
        {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(success, error);
            });
        }
    }];
}


#pragma mark - 懒加载

- (NSMutableArray *)allPhotoArr
{
    if (!_allPhotoArr)
    {
        _allPhotoArr = [NSMutableArray array];
    }
    return _allPhotoArr;
}

- (NSMutableArray *)allVideoArr
{
    if (!_allVideoArr)
    {
        _allVideoArr = [NSMutableArray array];
    }
    return _allVideoArr;
}

- (NSMutableArray *)bigPhotoArr
{
    if (!_bigPhotoArr)
    {
        _bigPhotoArr = [NSMutableArray array];
    }
    return _bigPhotoArr;
}


- (NSMutableArray *)bigVideoArr
{
    if (!_bigVideoArr)
    {
        _bigVideoArr = [NSMutableArray array];
    }
    return _bigVideoArr;
}

- (NSMutableArray *)duplicateVideoArr
{
    if (!_duplicateVideoArr)
    {
        _duplicateVideoArr = [NSMutableArray array];
    }
    return _duplicateVideoArr;
}

- (NSMutableArray *)shortVideoArr
{
    if (!_shortVideoArr)
    {
        _shortVideoArr = [NSMutableArray array];
    }
    return _shortVideoArr;
}


- (NSMutableArray *)screenshotsArr
{
    if (!_screenshotsArr)
    {
        _screenshotsArr = [NSMutableArray array];
    }
    return _screenshotsArr;
}

- (NSMutableArray *)similarArr
{
    if (!_similarArr)
    {
        _similarArr = [NSMutableArray array];
    }
    return _similarArr;
}


- (PHImageRequestOptions *)imageRequestOptions
{
    if (!_imageRequestOptions) {
        _imageRequestOptions = [[PHImageRequestOptions alloc] init];
        // resizeMode 属性控制图像的剪裁
        _imageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeNone;// no resize
        // deliveryMode 则用于控制请求的图片质量
        _imageRequestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
    }
    return _imageRequestOptions;
}

- (PHImageRequestOptions *)imageSizeRequestOptions
{
    if (!_imageSizeRequestOptions) {
        _imageSizeRequestOptions = [[PHImageRequestOptions alloc] init];
        // resizeMode 属性控制图像的剪裁
        _imageSizeRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;// exactly targetSize
        // deliveryMode 则用于控制请求的图片质量
        _imageSizeRequestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
    }
    return _imageSizeRequestOptions;
}


#pragma mark -- 获取视频


- (void)phAssetToVideo:(PHAsset *)asset completionHandle:(void(^)(AVURLAsset *urlAsset, int duration, NSNumber *fileSize, UIImage *thumbnailImage))completion {
    @autoreleasepool {
        PHVideoRequestOptions *videoOptions = [[PHVideoRequestOptions alloc] init];
        videoOptions.version = PHVideoRequestOptionsVersionOriginal;
        videoOptions.deliveryMode = PHVideoRequestOptionsDeliveryModeHighQualityFormat;
        videoOptions.networkAccessAllowed = YES;
        [[PHCachingImageManager defaultManager] requestAVAssetForVideo:asset options:videoOptions resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
            if ([asset isKindOfClass:[AVURLAsset class]]) {
                AVURLAsset *urlAsset = (AVURLAsset *)asset;
                CMTime time = [urlAsset duration];
                int seconds = ceil(time.value / time.timescale);
                NSNumber *fileSize;
                [urlAsset.URL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:nil];
                UIImage *thumbnailImage = [self thumbnailImageForVideoAssset:urlAsset];
                completion(urlAsset, seconds, fileSize, thumbnailImage);
            }
        }];
    }
}

- (UIImage *)thumbnailImageForVideoAssset:(AVURLAsset *)asset {
    NSParameterAssert(asset);
    AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
    imageGenerator.appliesPreferredTrackTransform = YES;
    imageGenerator.apertureMode = AVAssetImageGeneratorApertureModeEncodedPixels;
        
    /*
    如果不需要获取缩略图,就设置为NO,如果需要获取缩略图,则maximumSize为获取的最大尺寸。
    以BBC为例,getThumbnail = NO时,打印宽高数据为:1920*1072。
    getThumbnail = YES时,maximumSize为100*100。打印宽高数据为:100*55.
    注:不乘[UIScreen mainScreen].scale,会发现缩略图在100*100很虚。
    */
    BOOL getThumbnail = YES;
    if (getThumbnail) {
        CGFloat width = [UIScreen mainScreen].scale * 100;
        imageGenerator.maximumSize = CGSizeMake(width, width);
    }
    NSError *error = nil;
    CMTime time = CMTimeMake(1, 1);
    CMTime actucalTime;
    CGImageRef cgImage = [imageGenerator copyCGImageAtTime:time actualTime:&actucalTime error:&error];
    if (error) {
        NSLog(@"ERROR:get video thumb image fail,%@",error.domain);
    }
    CMTimeShow(actucalTime);
    UIImage *image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    return image;
}



// 获取相册详细信息
-(void)getPhotoDetail:(NSArray *)assetArr completion:(void (^)(NSArray* assets))completion
{
    __block NSMutableArray *photos = [NSMutableArray array];
    
    dispatch_queue_t dispatchQueue = dispatch_queue_create(REQUEST_VIDEO_QUEUE, DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(dispatchQueue, ^{

        UIImage *imageResult = [UIImage imageNamed:@"ic_img"];
        
        for (PHAsset *asset in assetArr) {
            
            PHAssetResource *resource = [[PHAssetResource assetResourcesForAsset:asset] firstObject];

            NSInteger assetLength = [[resource valueForKey:@"fileSize"] integerValue];

            PHImageManager *imageManager = [PHImageManager defaultManager];

            // 获取压缩大小后的图片,即缩略图
             [imageManager requestImageForAsset:asset targetSize:CGSizeMake(125, 125) contentMode:PHImageContentModeDefault options:self.imageRequestOptions resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
                 
                 NSString *dateStr = [self stringWithDate:asset.creationDate];

                 if (!result)
                 {
                     result = imageResult;
                 }
                 
                 NSDictionary *itemDictionary = @{
                     
                     @"asset" : asset,
                     @"exactImage" : result,
                     @"originImageDataLength" : @(assetLength),
                     @"duration":@(0),
                     @"dateStr":dateStr,
                     @"type":@"1"
                 };
                 
                 [photos addObject:itemDictionary];

                 if (assetArr.count == photos.count)
                 {
                     completion(photos);
                 }
             }];
        }
        
    });
}


// 获取视频详细信息
-(void)getVideoDetail:(NSArray *)assetArr completion:(void (^)(NSArray* assets))completion
{
    CleanSelf(self);
    
    __block NSMutableArray *videos = [NSMutableArray array];
    
    dispatch_queue_t dispatchQueue = dispatch_queue_create(REQUEST_VIDEO_QUEUE, DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(dispatchQueue, ^{
                
        for (PHAsset *asset in assetArr) {
            
            [weakself phAssetToVideo:asset completionHandle:^(AVURLAsset *urlAsset, int duration, NSNumber *fileSize, UIImage *thumbnailImage) {
                
                if (urlAsset) {
                    thumbnailImage = thumbnailImage == nil ? [UIImage imageNamed:@"img_video_nor"]: thumbnailImage;
                    
                    NSString *dateStr = [self stringWithDate:asset.creationDate];

                    NSDictionary *dict = @{@"asset" : asset,
                                           @"exactImage" : thumbnailImage,
                                           @"originImageDataLength" : fileSize,
                                           @"duration" : @(duration),
                                           @"dateStr":dateStr,
                                           @"type":@"2"

                    };
                                        
                    [videos addObject:dict];
                    
                    if (videos.count == assetArr.count)
                    {
                        completion(videos);
                    }
                }
            }];
        }
        
    });
}





// 判断相册授权状态
- (void)getAllBigAsset:(void (^)(BOOL success))completion
{
    // 清除旧数据
    [self resetTagData];
    
    self.bigCompletionHandler = completion;

    // 获取当前App的相册授权状态
    PHAuthorizationStatus authorizationStatus = [PHPhotoLibrary authorizationStatus];
    // 判断授权状态
    if (authorizationStatus == PHAuthorizationStatusAuthorized)
    {
        [self greatAllPhotoAndVideos];
    }
    // 如果没决定, 弹出指示框, 让用户选择
    else if (authorizationStatus == PHAuthorizationStatusNotDetermined)
    {
        [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
            // 如果用户选择授权, 则获取图片
            if (status == PHAuthorizationStatusAuthorized)
            {
                [self greatAllPhotoAndVideos];
            }
            else
            {
                // 开启权限提示
                completion(NO);
            }
        }];
    }
    else
    {
        completion(NO);
    }
}

-(void)greatAllPhotoAndVideos
{
    self.isPhotoFinish = NO;
    self.isVideoFinish = NO;

    // 获取相簿中的PHAsset对象
    self.allVideoArr = [self greatAllArr:YES];
    self.allPhotoArr = [self greatAllArr:NO];
    // 处理相册问题
    [self dealVideoWithArray:self.allVideoArr];
    [self dealImageWithArray:self.allPhotoArr];
    
    CleanSelf(self);
    
    self.completionHandler = ^(BOOL success)
    {
        if (weakself.isPhotoFinish && weakself.isVideoFinish)
        {
            weakself.bigCompletionHandler(YES);
        }
    };
}

#pragma mark - 加载照片:处理图片

-(NSMutableArray *)greatAllArr:(BOOL)isVideo
{
    NSMutableArray *resultArray = [[NSMutableArray alloc] init];

    // 获取相册
    PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumUserLibrary options:nil];

    // 获取所有资源的集合,并按资源的创建时间排序,这样就可以通过和上一张图片判断日期来分组了
    PHFetchOptions *option = [[PHFetchOptions alloc] init];

    option.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];

    if(isVideo)
    {
        option.predicate = [NSPredicate predicateWithFormat:@"mediaType == %ld", PHAssetMediaTypeVideo];
    }
    else
    {
        option.predicate = [NSPredicate predicateWithFormat:@"mediaType == %ld", PHAssetMediaTypeImage];
    }

    [smartAlbums enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {


        if (![obj isKindOfClass:PHAssetCollection.class]) {
            return;
        }
        PHAssetCollection *collection = (PHAssetCollection *) obj;
        // 过滤空相册
        if (collection.estimatedAssetCount <= 0){
            return;
        }
        
        

        // 获取相册内asset result
            PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsInAssetCollection:collection options:option];

            [result enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

                //资源图片检测
                if (![obj isKindOfClass:[PHAsset class]]) {
                    return;
                }
                PHAsset *asset = (PHAsset *) obj;

                [resultArray addObject:asset];
            }];
        }];

    return resultArray;
}

@end

相似度的算法使用的是 opencv2 来计算的相似度,但是有一点,照片过多时,内存会疯狂上涨,导致崩溃,原因就是对比这里出现的问题,解决办法如下

1.通过时间,获取照片(视频)的拍摄时间,对比时如果在同一天再去对比,当然你也可以再细致的去判断,比如判断相差时间在几分钟之内也 ok

// 是否为同一天
- (BOOL)isSameDay:(NSDate *)date1 date2:(NSDate *)date2
{
    // 有一个日期为空则直接返回
    if (!date1 || !date2)
    {
        return NO;
    }
    
    // 从日历上分别获取date1、date2的年月日
    NSCalendar *calendar = [NSCalendar currentCalendar];
    unsigned unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute;
    NSDateComponents *dateComponents1 = [calendar components:unitFlags fromDate:date1];
    NSDateComponents *dateComponents2 = [calendar components:unitFlags fromDate:date2];
    
    
    // 比较年月日,是否是同一天
    if((dateComponents1.day == dateComponents2.day) && (dateComponents1.month == dateComponents2.month) && (dateComponents1.year == dateComponents2.year))
    {
        // 如果是同一天,那就计算两者的时间差是不是在一个小时之内
        NSInteger dateOneSecond = dateComponents1.minute;
        NSInteger dateTwoSecond = dateComponents2.minute;
        NSInteger Difference = labs(dateTwoSecond - dateOneSecond);

        if(Difference < 30)
        {
            return YES;
        }
        else
        {
            return NO;
        }
    }
        
    return NO;
}

2.修改对比的方法,减少图片缩小后的大小,这里是最有效的,这样会大大减小内存,我减小到了 32 *32,不过量大肯定也会崩溃,我测试在同时对比 1500 张照片,210 个视频下是不会崩溃的

// 是否相似
+ (BOOL)isImage:(UIImage *)image1 likeImage:(UIImage *)image2 {
    IplImage *iplimage1 = [self convertToIplImage:[self OriginImage:image1 scaleToSize:CGSizeMake(32, 32)]];
    IplImage *iplimage2 = [self convertToIplImage:[self OriginImage:image2 scaleToSize:CGSizeMake(32, 32)]];
    double sililary = [self ComparePPKHist:iplimage1 withParam2:iplimage2];
    if (sililary < 0.1) {
        return YES;
    }
    return NO;
}

下面是对比的类 和 model ,我这里视频和图片共用了一个 Model

CleanAlbumModel.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CleanAlbumModel : NSObject

/// 图片资源
@property (nonatomic, strong) PHAsset *asset;
/// 图片
@property (nonatomic, strong) UIImage *exactImage;
/// 图片数据大小
@property (nonatomic, assign) NSUInteger originImageDataLength;
/// 时长
@property (nonatomic, assign) NSUInteger duration;
/// 日期
@property (nonatomic, copy) NSString *dateStr;
/// 类型 1-iamge 2-video
@property (nonatomic, copy) NSString *type;
/// 是否选中
@property (nonatomic, assign) BOOL isSelected;

/// 初始化Model,传入info
- (instancetype)initWithDict:(NSDictionary *)dict;

@end

NS_ASSUME_NONNULL_END

CleanAlbumModel.m

#import "CleanAlbumModel.h"

@implementation CleanAlbumModel


- (instancetype)initWithDict:(NSDictionary *)dict
{
    self = [super init];
    if (self)
    {
        self.asset = dict[@"asset"];
        self.exactImage = dict[@"exactImage"];
        self.duration = [dict[@"duration"] integerValue];
        self.originImageDataLength = [dict[@"originImageDataLength"] unsignedIntegerValue];
        self.dateStr = dict[@"dateStr"];
        self.type = dict[@"type"];
        self.isSelected = NO;

    }
    return self;
}

@end

ImageCompare.h 图片相似度对比

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface ImageCompare : NSObject

/// 是否相似
+ (BOOL)isImage:(UIImage *)image1 likeImage:(UIImage *)image2;

@end

ImageCompare.m

#import "ImageCompare.h"
#import <opencv2/opencv.hpp>

@implementation ImageCompare

// 是否相似
+ (BOOL)isImage:(UIImage *)image1 likeImage:(UIImage *)image2 {
    IplImage *iplimage1 = [self convertToIplImage:[self OriginImage:image1 scaleToSize:CGSizeMake(32, 32)]];
    IplImage *iplimage2 = [self convertToIplImage:[self OriginImage:image2 scaleToSize:CGSizeMake(32, 32)]];
    double sililary = [self ComparePPKHist:iplimage1 withParam2:iplimage2];
    if (sililary < 0.1) {
        return YES;
    }
    return NO;
}

// 缩小尺寸
+ (UIImage *)OriginImage:(UIImage *)image scaleToSize:(CGSize)size {
    // size 为CGSize类型,即你所需要的图片尺寸
    UIGraphicsBeginImageContext(size);
    [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
    UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return scaledImage;
}

// 获取相似度
+ (float)isImageFloat:(UIImage *)image1 likeImage:(UIImage *)image2 {
    IplImage *iplimage1 = [self convertToIplImage:image1];
    IplImage *iplimage2 = [self convertToIplImage:image2];
    double sililary = [self ComparePPKHist:iplimage1 withParam2:iplimage2];
    return sililary;
}

// 比较
+ (double)ComparePPKHist:(IplImage *)srcIpl withParam2:(IplImage *)srcIpl1 {
    if (srcIpl->width==srcIpl1->width && srcIpl->height==srcIpl1->height) {
        return [self CompareHist:srcIpl withParam2:srcIpl1];
    }
    else if (srcIpl->width<srcIpl1->width && srcIpl->height==srcIpl1->height) {
        return [self CompareHistWithSmallWidthIpl:srcIpl withBigWidthIplImg:srcIpl1];
    }
    else if (srcIpl->width>srcIpl1->width && srcIpl->height==srcIpl1->height) {
        return [self CompareHistWithSmallWidthIpl:srcIpl1 withBigWidthIplImg:srcIpl];
    }
    else if (srcIpl->width==srcIpl1->width && srcIpl->height<srcIpl1->height) {
        return [self CompareHistWithSmallHeightIpl:srcIpl withBigHeightIplImg:srcIpl1];
    }
    else if (srcIpl->width==srcIpl1->width && srcIpl->height>srcIpl1->height) {
        return [self CompareHistWithSmallHeightIpl:srcIpl1 withBigHeightIplImg:srcIpl];
    }
    else if (srcIpl->width<srcIpl1->width && srcIpl->height<srcIpl1->height) {
        return [self CompareHistWithSmallIpl:srcIpl withBigIplImg:srcIpl1];
    }
    else if (srcIpl->width>srcIpl1->width && srcIpl->height>srcIpl1->height)
    {
        return [self CompareHistWithSmallIpl:srcIpl1 withBigIplImg:srcIpl];
    }

    return 1.f;
}

+ (double)CompareHistWithSmallWidthIpl:(IplImage*)srcIpl withBigWidthIplImg:(IplImage*)srcIpl1 {
    // 当前匹配结果,越接近于0.0匹配度越高
    double dbRst=1.0;
    // 匹配结果,-1表示正在匹配,0表示匹配失败,1表示匹配成功
    int tfFound = -1;
    // 裁剪后的图片
    IplImage *cropImage;
    for (int j=0; j<srcIpl1->width-srcIpl->width; j++) {
        // 裁剪图片
        cvSetImageROI(srcIpl1, cvRect(j, 0, srcIpl->width, srcIpl->height));
        cropImage = cvCreateImage(cvGetSize(srcIpl), IPL_DEPTH_8U, 3);
        cvCopy(srcIpl1, cropImage);
        cvResetImageROI(srcIpl1);
        
        // 匹配图片
        double dbRst1 =[self CompareHist:srcIpl withParam2:cropImage];
//        printf("匹配结果为:%f\n",dbRst1);
        if (dbRst1<=0.01) {
            // 匹配成功
            tfFound = 1;
            break;
        }
        
        else if(dbRst==1.0 || dbRst1<dbRst) {
            // 本次匹配有进步,更新结果
            cvReleaseImage(&cropImage);
            dbRst = dbRst1;
        }
        
        else if(dbRst1>dbRst) {
            cvReleaseImage(&cropImage);
        }
    }
    
    return dbRst;
}

+ (double)CompareHistWithSmallHeightIpl:(IplImage*)srcIpl withBigHeightIplImg:(IplImage*)srcIpl1 {
    // 当前匹配结果,越接近于0.0匹配度越高
    double dbRst=1.0;
    // 匹配结果,-1表示正在匹配,0表示匹配失败,1表示匹配成功
    int tfFound = -1;
    // 裁剪后的图片
    IplImage *cropImage;
    for (int j=0; j<srcIpl1->height-srcIpl->height; j++) {
        // 裁剪图片
        cvSetImageROI(srcIpl1, cvRect(0, j, srcIpl->height, srcIpl->height));
        cropImage = cvCreateImage(cvGetSize(srcIpl), IPL_DEPTH_8U, 3);
        cvCopy(srcIpl1, cropImage);
        cvResetImageROI(srcIpl1);
        
        // 匹配图片
        double dbRst1 =[self CompareHist:srcIpl withParam2:cropImage];
//        printf("匹配结果为:%f\n",dbRst1);
        if (dbRst1<=0.01) {
            // 匹配成功
            tfFound = 1;
            break;
        }
        else if(dbRst==1.0 || dbRst1<dbRst) {
            // 本次匹配有进步,更新结果
            cvReleaseImage(&cropImage);
            dbRst = dbRst1;
        }
        
        else if(dbRst1>dbRst) {
            cvReleaseImage(&cropImage);
        }
    }
    
    return dbRst;
}

+ (double)CompareHistWithSmallIpl:(IplImage*)srcIpl withBigIplImg:(IplImage*)srcIpl1 {
    // 当前匹配结果,越接近于0.0匹配度越高
    double dbRst=1.0;
    // 水平、竖直偏移量
    int xSub=0,ySub=0;
    // 匹配结果,-1表示正在匹配,0表示匹配失败,1表示匹配成功
    int tfFound = -1;
    // 裁剪后的图片
    IplImage *cropImage;
    
    // 遍历方式:先竖后横
    for (int j=0; j<srcIpl1->width-srcIpl->width; j++) {
        for (int i=ySub; i<srcIpl1->height-srcIpl->height; i++) {
            // 裁剪图片
            cvSetImageROI(srcIpl1, cvRect(j, i, srcIpl->width, srcIpl->height));
            cropImage = cvCreateImage(cvGetSize(srcIpl), IPL_DEPTH_8U, 3);
            cvCopy(srcIpl1, cropImage);
            cvResetImageROI(srcIpl1);
            
            // 匹配图片
            double dbRst1 =[self CompareHist:srcIpl withParam2:cropImage];
            
//            printf("(x=%d,y=%d),竖直匹配结果为:%f\n",j,i,dbRst1);
            if (dbRst1<=0.0375) {
                // 匹配成功
                tfFound = 1;
                break;
            } else if(dbRst==1.0 || dbRst1<dbRst) {
                // 本次匹配有进步,更新结果
                cvReleaseImage(&cropImage);
                dbRst = dbRst1;
            } else if(dbRst1>dbRst) {
                cvReleaseImage(&cropImage);
                // 竖直移动到点了,该水平移动了
                ySub = i-1;
                for (int k=j+1;k<srcIpl1->width-srcIpl->width; k++) {
                    // 裁切图片
                    cvSetImageROI(srcIpl1, cvRect(k, i, srcIpl->width, srcIpl->height));
                    cropImage = cvCreateImage(cvGetSize(srcIpl), IPL_DEPTH_8U, 3);
                    cvCopy(srcIpl1, cropImage);
                    cvResetImageROI(srcIpl1);
                    
                    // 匹配图片
                    double dbRst1 =[self CompareHist:srcIpl withParam2:cropImage];
//                    printf("(x=%d,y=%d),水平移动匹配结果为:%f\n",k,i,dbRst1);
                    if (dbRst1<=0.0375) {
                        // 匹配成功
                        tfFound = 1;
                        xSub = k;
                        break;
                    } else if(dbRst1<dbRst) {
                        // 本次匹配有进步,更新结果
                        cvReleaseImage(&cropImage);
                        xSub = k;
                        j = xSub;
                        dbRst = dbRst1;
                    } else {
                        cvReleaseImage(&cropImage);
                        xSub = k;
                        j = xSub;
                        break;
                    }
                }
            }
            
            if (tfFound==1 || tfFound==0) {
                break;
            }
        }
        
        if (tfFound==1 || tfFound==0) {
            break;
        }
    }
    
    return dbRst;
}

// 多通道彩色图片的直方图比对
+ (double)CompareHist:(IplImage*)image1 withParam2:(IplImage*)image2 {
    int hist_size = 256;
    IplImage *gray_plane = cvCreateImage(cvGetSize(image1), 8, 1);
    cvCvtColor(image1, gray_plane, CV_BGR2GRAY);
    
    CvHistogram *gray_hist = cvCreateHist(1, &hist_size, CV_HIST_ARRAY);
    cvCalcHist(&gray_plane, gray_hist);
    
    IplImage *gray_plane2 = cvCreateImage(cvGetSize(image2), 8, 1);
    cvCvtColor(image2, gray_plane2, CV_BGR2GRAY);
    CvHistogram *gray_hist2 = cvCreateHist(1, &hist_size, CV_HIST_ARRAY);
    cvCalcHist(&gray_plane2, gray_hist2);

    return cvCompareHist(gray_hist, gray_hist2, CV_COMP_BHATTACHARYYA);
}

// 单通道彩色图片的直方图
+ (double)CompareHistSignle:(IplImage*)image1 withParam2:(IplImage*)image2 {
    int hist_size = 256;
    CvHistogram *gray_hist = cvCreateHist(1, &hist_size, CV_HIST_ARRAY);
    cvCalcHist(&image1, gray_hist);
    
    CvHistogram *gray_hist2 = cvCreateHist(1, &hist_size, CV_HIST_ARRAY);
    cvCalcHist(&image2, gray_hist2);
    
    return cvCompareHist(gray_hist, gray_hist2, CV_COMP_BHATTACHARYYA);
}

// 进行肤色检测
+ (void)SkinDetect:(IplImage*)src withParam:(IplImage*)dst {
    // 创建图像头
    // 用于存图像的一个中间变量,是用来分通道用的,分成hsv通道
    IplImage* hsv = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 3);
    // 通道的中间变量,用于肤色检测的中间变量
    IplImage* tmpH1 = cvCreateImage( cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage* tmpS1 = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage* tmpH2 = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage* tmpS2 = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage* tmpH3 = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage* tmpS3 = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage* H = cvCreateImage( cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage* S = cvCreateImage( cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage* V = cvCreateImage( cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage* src_tmp1=cvCreateImage(cvGetSize(src),8,3);
    
    // 高斯模糊
    cvSmooth(src,src_tmp1,CV_GAUSSIAN,3,3);
    
    // hue色度,saturation饱和度,value纯度
    cvCvtColor(src_tmp1, hsv, CV_BGR2HSV );
    // 分为3个通道
    cvSplit(hsv,H,S,V,0);
    
    /*********************肤色检测部分**************/
    cvInRangeS(H,cvScalar(0.0,0.0,0,0),cvScalar(20.0,0.0,0,0),tmpH1);
    cvInRangeS(S,cvScalar(75.0,0.0,0,0),cvScalar(200.0,0.0,0,0),tmpS1);
    cvAnd(tmpH1,tmpS1,tmpH1,0);
    
    // Red Hue with Low Saturation
    // Hue 0 to 26 degree and Sat 20 to 90
    cvInRangeS(H,cvScalar(0.0,0.0,0,0),cvScalar(13.0,0.0,0,0),tmpH2);
    cvInRangeS(S,cvScalar(20.0,0.0,0,0),cvScalar(90.0,0.0,0,0),tmpS2);
    cvAnd(tmpH2,tmpS2,tmpH2,0);
    
    // Red Hue to Pink with Low Saturation
    // Hue 340 to 360 degree and Sat 15 to 90
    cvInRangeS(H,cvScalar(170.0,0.0,0,0),cvScalar(180.0,0.0,0,0),tmpH3);
    cvInRangeS(S,cvScalar(15.0,0.0,0,0),cvScalar(90.,0.0,0,0),tmpS3);
    cvAnd(tmpH3,tmpS3,tmpH3,0);
    
    // Combine the Hue and Sat detections
    cvOr(tmpH3,tmpH2,tmpH2,0);
    cvOr(tmpH1,tmpH2,tmpH1,0);
    cvCopy(tmpH1,dst);
    cvReleaseImage(&hsv);
    cvReleaseImage(&tmpH1);
    cvReleaseImage(&tmpS1);
    cvReleaseImage(&tmpH2);
    cvReleaseImage(&tmpS2);
    cvReleaseImage(&tmpH3);
    cvReleaseImage(&tmpS3);
    cvReleaseImage(&H);
    cvReleaseImage(&S);
    cvReleaseImage(&V);
    cvReleaseImage(&src_tmp1);
}

// UIImage类型转换为IPlImage类型
+ (IplImage*)convertToIplImage:(UIImage*)image {
    CGImageRef imageRef = image.CGImage;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    IplImage *iplImage = cvCreateImage(cvSize(image.size.width, image.size.height), IPL_DEPTH_8U, 4);
    CGContextRef contextRef = CGBitmapContextCreate(iplImage->imageData, iplImage->width, iplImage->height, iplImage->depth, iplImage->widthStep, colorSpace, kCGImageAlphaPremultipliedLast|kCGBitmapByteOrderDefault);
    CGContextDrawImage(contextRef, CGRectMake(0, 0, image.size.width, image.size.height), imageRef);
    CGContextRelease(contextRef);
    CGColorSpaceRelease(colorSpace);
    
    IplImage *ret = cvCreateImage(cvGetSize(iplImage), IPL_DEPTH_8U, 3);
    cvCvtColor(iplImage, ret, CV_RGB2BGR);
    cvReleaseImage(&iplImage);
    
    return ret;
}

// IplImage类型转换为UIImage类型
+ (UIImage*)convertToUIImage:(IplImage*)image {
    cvCvtColor(image, image, CV_BGR2RGB);
    
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    NSData *data = [NSData dataWithBytes:image->imageData length:image->imageSize];
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)data);
    CGImageRef imageRef = CGImageCreate(image->width, image->height, image->depth, image->depth * image->nChannels, image->widthStep, colorSpace, kCGImageAlphaNone | kCGBitmapByteOrderDefault, provider, NULL, false, kCGRenderingIntentDefault);
    
    UIImage *ret = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpace);
    return ret;
}
相似度对比

通讯录管理类

这里获取了所有联系人,以及手机号,邮箱,姓名重复的联系人,并进行统计!!
联系人获取

获取联系人的方法


- (CleanPersonManager *)personManager
{
    if (!_personManager)
    {
        _personManager = [CleanPersonManager shareManager];
    }
    return _personManager;
}
// 获取所有通讯录里的联系人
-(void)getAllPersonArr:(BOOL)isSend
{
    CleanSelf(self);
    
    [self.personManager loadPerson:^(BOOL isFinish) {
       
        dispatch_async(dispatch_get_main_queue(), ^{

            weakself.isLoadAllPerson = isFinish;
            
            if (isFinish)
            {
                CleanNsLog(@"%@",weakself.personManager.allPersonInfo);
                CleanNsLog(@"%@",weakself.personManager.similaNamerInfo);
                CleanNsLog(@"%@",weakself.personManager.similarPhoneNumInfo);
                CleanNsLog(@"%@",weakself.personManager.similarEmailInfo);
            }
            else
            {
                //没权限
            }

        });
        
    }];
   
}

联系人管理类 CleanPersonManager.h

//
//  CleanPersonManager.h
//  CleanOne
//
//  Created by iOS Clean on 2023/8/25.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CleanPersonManager : NSObject

+ (CleanPersonManager *)shareManager;

/// 所有联系人的
@property (nonatomic, strong, readonly) NSDictionary *allPersonInfo;
/// 电话相似信息
@property (nonatomic, strong, readonly) NSDictionary *similarPhoneNumInfo;
/// 姓名相似信息
@property (nonatomic, strong, readonly) NSDictionary *similaNamerInfo;
/// 邮件相似信息
@property (nonatomic, strong, readonly) NSDictionary *similarEmailInfo;


// 获取权限,并请求全部联系人、相似名字、相似电话、相似邮箱
- (void)loadPerson:(void (^)(BOOL isFinish))completion;

// 获取所有名字相同的联系人
- (void)getDuplicateNameCompleteHandle:(void(^)(NSDictionary *duplicateNameDict))complete;

// 获取所有电话相同的联系人
- (void)getDuplicateNumberCompleteHandle:(void(^)(NSDictionary *duplicateNumberDict))complete;

// 获取所有邮箱相同的联系人
- (void)getDuplicateEmailCompleteHandle:(void(^)(NSDictionary *duplicateEmailDict))complete;

// 合并到第几个人下面
- (void)mergeContactWithContacts:(NSArray *)contacts index:(NSInteger)index type:(NSInteger)type;

// 删除对应的联系人
- (void)deleteContact:(CNContact *)contact;

@end

NS_ASSUME_NONNULL_END

CleanPersonManager.m

//
//  CleanPersonManager.m
//  CleanOne
//
//  Created by iOS Clean on 2023/8/25.
//

#import "CleanPersonManager.h"
#import "PersonModel.h"
#import "AlertChoosPersonModel.h"

@interface CleanPersonManager ()

@property (nonatomic, strong) CNContactStore  *contactStore;

@property (nonatomic, strong) NSMutableArray *allContacts;

/// 所有联系人的
@property (nonatomic, strong, readwrite) NSDictionary *allPersonInfo;
/// 电话相似信息
@property (nonatomic, strong, readwrite) NSDictionary *similarPhoneNumInfo;
/// 姓名相似信息
@property (nonatomic, strong, readwrite) NSDictionary *similaNamerInfo;
/// 邮件相似信息
@property (nonatomic, strong, readwrite) NSDictionary *similarEmailInfo;

@end

@implementation CleanPersonManager


+ (CleanPersonManager *)shareManager {
    static CleanPersonManager *manager = nil;
    static dispatch_once_t token;
    dispatch_once(&token, ^{
        manager = [[CleanPersonManager alloc] init];
    });
    return manager;
}


// 判断通讯录授权状态
- (void)loadPerson:(void (^)(BOOL isFinish))completion
{
    // 获取当前App的通讯录授权状态
    CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
    // 判断授权状态
    if (status == CNAuthorizationStatusAuthorized)
    {
        // 如果已经授权, 获取通讯录信息
        [self getAllPerson:^(BOOL isFinish) {
           
            completion(YES);
        }];
    }
    // 如果没决定, 弹出指示框, 让用户选择
    else if (status == CNAuthorizationStatusNotDetermined)
    {
        // 如果用户选择授权, 则获取图片
        CNContactStore *store = [[CNContactStore alloc] init];
       
        [store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
            
            if (!error) {
                
                // 如果已经授权, 获取通讯录信息
                [self getAllPerson:^(BOOL isFinish) {
                   
                    completion(YES);
                }];
            }
            else
            {
                // 开启权限提示
                completion(NO);
            }
            
        }];
    }
    else
    {
        // 开启权限提示
        completion(NO);
    }
}

-(void)getAllPerson:(void(^)(BOOL isFinish))complete
{
    // 如果已经授权, 获取通讯录信息
    [self.allContacts removeAllObjects];
    [self.contactStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
        CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactFamilyNameKey,
                                                                                              CNContactGivenNameKey,
                                                                                              CNContactPhoneNumbersKey,
                                                                                              CNContactEmailAddressesKey,
                                                                                              CNContactPostalAddressesKey,
                                                                                              CNContactOrganizationNameKey]];
        NSError *conError = nil;
        [self.contactStore enumerateContactsWithFetchRequest:request error:&conError usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
           
            PersonModel *contactModel = [[PersonModel alloc] init];
            
            contactModel.contact = contact;
            // 匹配电话号码
            NSMutableArray *phoneArray = [NSMutableArray array];
            NSCharacterSet *numSet = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet];
            NSString *phoneNumber = @"";
            for(NSInteger i=0;i<contact.phoneNumbers.count;i++)
            {
                phoneNumber = [[contact.phoneNumbers[i].value.stringValue componentsSeparatedByCharactersInSet:numSet] componentsJoinedByString:@""];;

                [phoneArray addObject:phoneNumber];
            }
                        
            contactModel.mobileNumberArr = phoneArray;
            contactModel.mobileNumber = phoneNumber;
            
            NSString *contactName = @"";
            if ([NSString stringWithFormat:@"%@%@", contact.familyName, contact.givenName]) {
                contactName = [NSString stringWithFormat:@"%@%@", contact.familyName, contact.givenName];
            }
            contactModel.userName = contactName;
            
            NSString * emailAddress = @"";
            NSMutableArray *emailArr = [NSMutableArray array];
            for (NSInteger i=0;i<contact.emailAddresses.count;i++) {
                
                CNLabeledValue *emaileVale = contact.emailAddresses[I];
                emailAddress = emaileVale.value;
                [emailArr addObject:emailAddress];
            }
            contactModel.email = emailAddress;
            contactModel.emailArr = emailArr;

            
            NSString *companyName = @"";
            if (contact.organizationName.length > 0) {
                companyName = contact.organizationName;
            }
            contactModel.company = companyName;
            [self.allContacts addObject:contactModel];
        }];
        NSInteger count = self.allContacts.count > 0 ? self.allContacts.count : 0;
        NSDictionary *dict = @{@"dataArr":self.allContacts, @"totalCount":@(count), @"contactType":@0};
        self.allPersonInfo = dict;
    }];
        
    [self getDuplicateNameCompleteHandle:^(NSDictionary * _Nonnull duplicateNameDict) {
        
        self.similaNamerInfo = duplicateNameDict;
        
        if (self.similarPhoneNumInfo && self.similarEmailInfo)
        {
            complete(YES);
        }
    }];
    
    [self getDuplicateEmailCompleteHandle:^(NSDictionary * _Nonnull duplicateEmailDict) {
        
        self.similarEmailInfo = duplicateEmailDict;
        
        if (self.similaNamerInfo && self.similarPhoneNumInfo)
        {
            complete(YES);
        }
    }];
    
    [self getDuplicateNumberCompleteHandle:^(NSDictionary * _Nonnull duplicateNumberDict) {
        
        self.similarPhoneNumInfo = duplicateNumberDict;
        
        if (self.similaNamerInfo && self.similarEmailInfo)
        {
            complete(YES);
        }        
    }];
}

- (void)getDuplicateNameCompleteHandle:(void(^)(NSDictionary *duplicateNameDict))complete {
    NSArray *duplicateNameArr = [self analyseDuplicateNameContact:self.allContacts];
    NSInteger count = 0;
    if (duplicateNameArr.count > 0) {
        for (NSArray *arr in duplicateNameArr) {
            count = count + arr.count;
        }
        NSDictionary *dict = @{@"dataArr":duplicateNameArr, @"totalCount":@(count), @"contactType":@1};
        self.similaNamerInfo = dict;
        complete(dict);
    } else {
        NSDictionary *dict = @{@"dataArr":@[], @"totalCount":@(0), @"contactType":@1};
        self.similaNamerInfo = dict;
        complete(dict);
    }
    
}

- (void)getDuplicateNumberCompleteHandle:(void(^)(NSDictionary *duplicateNumberDict))complete{
    NSArray *duplicateNumberArr = [self analyseDuplicateNumberAndEmailContact:self.allContacts type:@"1"];
    NSInteger count = 0;
    if (duplicateNumberArr.count > 0) {
        for (NSArray *arr in duplicateNumberArr) {
            count = count + arr.count;
        }
        NSDictionary *dict = @{@"dataArr":duplicateNumberArr, @"totalCount":@(count), @"contactType":@2};
        self.similarPhoneNumInfo = dict;
        complete(dict);
    } else {
        NSDictionary *dict = @{@"dataArr":@[], @"totalCount":@(0), @"contactType":@2};
        self.similarPhoneNumInfo = dict;
        complete(dict);
    }
}

- (void)getDuplicateEmailCompleteHandle:(void(^)(NSDictionary *duplicateEmailDict))complete {
    NSArray *duplicateEmailArr = [self analyseDuplicateNumberAndEmailContact:self.allContacts type:@"2"];
    NSInteger count = 0;
    if (duplicateEmailArr.count > 0) {
        for (NSArray *arr in duplicateEmailArr) {
            count = count + arr.count;
        }
        NSDictionary *dict = @{@"dataArr":duplicateEmailArr, @"totalCount":@(count), @"contactType":@3};
        self.similarEmailInfo = dict;
        complete(dict);
    } else {
        NSDictionary *dict = @{@"dataArr":@[], @"totalCount":@(0), @"contactType":@3};
        self.similarEmailInfo = dict;
        complete(dict);
    }
}

// 处理相似名字的联系人
- (NSArray *)analyseDuplicateNameContact:(NSArray *)contactArr {
    NSMutableArray *inputArr = [NSMutableArray arrayWithArray:contactArr];
    NSMutableArray *outputArr = [NSMutableArray array];
    for (int i = 0; i < inputArr.count; i++) {
        PersonModel *model1 = inputArr[i];
        NSMutableArray *tempArr = [NSMutableArray array];
        [tempArr addObject:model1];
        for (int j = i + 1; j < inputArr.count; j++) {
            PersonModel *model2 = inputArr[j];
            if ([model1.userName isEqualToString:model2.userName]) {
                [tempArr addObject:model2];
            }
        }
        if (tempArr.count > 1) {
            [outputArr addObject:tempArr];
            [inputArr removeObjectsInArray:tempArr];
            i -= 1;
        }
    }
    return outputArr;
}


// 处理相似手机的联系人 1:电话 2:邮箱
- (NSArray *)analyseDuplicateNumberAndEmailContact:(NSArray *)contactArr type:(NSString *)type{
   
    NSMutableArray *inputArr = [NSMutableArray arrayWithArray:contactArr];
   
    NSMutableArray *outputArr = [NSMutableArray array];
   
    for (int i = 0; i < inputArr.count; i++) {
       
        PersonModel *model1 = inputArr[i];
       
        NSMutableArray *tempArr = [NSMutableArray array];
       
        [tempArr addObject:model1];
      
        for (int j = i + 1; j < inputArr.count; j++) {
           
            PersonModel *model2 = inputArr[j];
           
            if ([type isEqualToString:@"1"])
            {
                NSDictionary *dataDict = [self contrastArr:model1.mobileNumberArr twoArr:model2.mobileNumberArr type:type];
                
                if ([dataDict[@"isSame"] isEqualToString:@"1"]) {
                  
                    model2.mobileNumber = dataDict[@"sameStr"];
                    [tempArr addObject:model2];
                }
            }
            else
            {
                NSDictionary *dataDict = [self contrastArr:model1.emailArr twoArr:model2.emailArr type:type];
                
                if ([dataDict[@"isSame"] isEqualToString:@"1"]) {
                  
                    model2.email = dataDict[@"sameStr"];
                    [tempArr addObject:model2];
                }
            }
            
            
        }
        
        if (tempArr.count > 1) {
            
            [outputArr addObject:tempArr];
            
            [inputArr removeObjectsInArray:tempArr];
            
            i -= 1;
        }
    }
    
    return outputArr;
}


///  是否有相同的数据
/// - Parameters:
///   - oneArr: 对比 1 数组
///   - twoArr: 对比 2 数组
///   - type: 1:对比电话 2:对比邮箱
-(NSDictionary *)contrastArr:(NSArray *)oneArr twoArr:(NSArray *)twoArr type:(NSString *)type
{
    NSDictionary *dataDict = @{@"isSame":@"0",@"sameStr":@""};
    
    BOOL isSame = NO;
    
    for(NSInteger i=0;i<oneArr.count;i++)
    {
        NSString *contrastContent1 = oneArr[i];
        
        for(NSInteger j=0;j<twoArr.count;j++)
        {
            NSString *contrastContent2 = twoArr[j];
            
            if ([contrastContent1 isEqualToString:contrastContent2] && contrastContent1.length > 1 && contrastContent2.length > 1)
            {
                isSame = YES;
                
                dataDict = @{@"isSame":@"1",@"sameStr":contrastContent1};
                
                break;
            }
        }
        
        if (isSame)
        {
            break;
        }
    }
    
    return dataDict;
}



- (void)getAllContactsCompleteHandle:(void(^)(NSDictionary *allcontactsDict))complete {
    [self.allContacts removeAllObjects];
    [self.contactStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
        CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactFamilyNameKey,
                                                                                              CNContactGivenNameKey,
                                                                                              CNContactPhoneNumbersKey,
                                                                                              CNContactEmailAddressesKey,
                                                                                              CNContactPostalAddressesKey,
                                                                                              CNContactOrganizationNameKey]];
        NSError *conError = nil;
        [self.contactStore enumerateContactsWithFetchRequest:request error:&conError usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
            PersonModel *contactModel = [[PersonModel alloc] init];
            contactModel.contact = contact;
            NSCharacterSet *numSet = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet];
            NSString *phoneNumber = @"";
            if (contact.phoneNumbers.count > 0) {
                phoneNumber = [[[contact.phoneNumbers firstObject].value.stringValue componentsSeparatedByCharactersInSet:numSet] componentsJoinedByString:@""];
            }
            contactModel.mobileNumber = phoneNumber;
            
            NSString *contactName = @"";
            if ([NSString stringWithFormat:@"%@%@", contact.familyName, contact.givenName]) {
                contactName = [NSString stringWithFormat:@"%@%@", contact.familyName, contact.givenName];
            }
            contactModel.userName = contactName;
            
            NSString * emailAddress = @"";
            if (contact.emailAddresses.count > 0) {
                CNLabeledValue *emaileVale = contact.emailAddresses[0];
                emailAddress = emaileVale.value;
            }
            contactModel.email = emailAddress;
            
            NSString *companyName = @"";
            if (contact.organizationName.length > 0) {
                companyName = contact.organizationName;
            }
            contactModel.company = companyName;
            [self.allContacts addObject:contactModel];
        }];
        NSInteger count = self.allContacts.count > 0 ? self.allContacts.count : 0;
        NSDictionary *dict = @{@"dataArr":self.allContacts, @"totalCount":@(count), @"contactType":@0};
        self.allPersonInfo = dict;
        complete(dict);
    }];
}

- (void)mergeContactWithContacts:(NSArray *)contacts index:(NSInteger)index type:(NSInteger)type {
    [self addContactWithModels:contacts index:index type:type];
    CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
    for (AlertChoosPersonModel *model in contacts) {
        CNMutableContact *contact = (CNMutableContact *)[model.contact mutableCopy];
        [saveRequest deleteContact:contact];
    }
    [self.contactStore executeSaveRequest:saveRequest error:nil];
}

- (void)deleteContact:(CNContact *)contact {
    CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
    CNMutableContact *mutContact = (CNMutableContact *)[contact mutableCopy];
    [saveRequest deleteContact:mutContact];
    [self.contactStore executeSaveRequest:saveRequest error:nil];
}

- (void)addContactWithModels:(NSArray *)models index:(NSInteger)index type:(NSInteger)type {
    CNMutableContact *contact = [[CNMutableContact alloc] init];
    CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
    switch (type) {
        case 2: {
            AlertChoosPersonModel *nameModel = models.firstObject;
            contact.familyName = nameModel.contact.familyName;
            contact.givenName = nameModel.contact.givenName;
            NSMutableArray *numbers = [NSMutableArray array];
            NSMutableArray *emails = [NSMutableArray array];
            for (AlertChoosPersonModel *model in models) {
                if (model.contact.emailAddresses.count > 0) {
                    CNLabeledValue *emaileValue = model.contact.emailAddresses[0];
                    [emails addObject:emaileValue];
                }
                if (model.contact.phoneNumbers.count > 0) {
                    CNLabeledValue *numberValue = model.contact.phoneNumbers[0];
                    [numbers addObject:numberValue];
                }
            }
            contact.emailAddresses = emails;
            contact.phoneNumbers = numbers;
            [saveRequest addContact:contact toContainerWithIdentifier:nil];
        }
            break;
        case 1: {
            AlertChoosPersonModel *numberModel = models[index];
            contact.familyName = numberModel.contact.familyName;
            contact.givenName = numberModel.contact.givenName;
            if (numberModel.contact.emailAddresses.count > 0) {
                CNLabeledValue *emaileValue = numberModel.contact.emailAddresses[0];
                contact.emailAddresses = @[emaileValue];
            }
            if (numberModel.contact.phoneNumbers.count > 0) {
                CNLabeledValue *numberValue = numberModel.contact.phoneNumbers[0];
                contact.phoneNumbers = @[numberValue];
            }
            
            
            
            [saveRequest addContact:contact toContainerWithIdentifier:nil];
        }
            break;
        case 3: {
            AlertChoosPersonModel *emailModel = models[index];
            contact.familyName = emailModel.contact.familyName;
            contact.givenName = emailModel.contact.givenName;
            if (emailModel.contact.emailAddresses.count > 0) {
                CNLabeledValue *emaileValue = emailModel.contact.emailAddresses[0];
                contact.emailAddresses = @[emaileValue];
            }
            if (emailModel.contact.phoneNumbers.count > 0) {
                CNLabeledValue *numberValue = emailModel.contact.phoneNumbers[0];
                contact.phoneNumbers = @[numberValue];
            }
            
            [saveRequest addContact:contact toContainerWithIdentifier:nil];
        }
            break;
        default:
            break;
    }
    [self.contactStore executeSaveRequest:saveRequest error:nil];
}

- (CNContactStore *)contactStore {
    if (!_contactStore) {
        _contactStore = [[CNContactStore alloc] init];
    }
    return _contactStore;
}

- (NSMutableArray *)allContacts {
    if (!_allContacts) {
        _allContacts = [NSMutableArray array];
    }
    return _allContacts;
}

@end

重复姓名、联系人等,合并的时候要选择合并至哪个人底下,这里选择的意义在于放弃未选中人的所有信息,将需要合并的值进行合并,这里是有优化的点的,可以把两个人的所有信息都同步到一个人的身上,然后相同的过滤掉,我嫌麻烦所以没做

合并界面

提示选择的 model
、、、

import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface AlertChoosPersonModel : NSObject

// 联系人
@property (nonatomic, strong) CNContact *contact;
// 姓名
@property (nonatomic, copy) NSString *userName;
// 电话
@property (nonatomic, copy) NSString *mobileNumber;
// 邮箱
@property (nonatomic, copy) NSString *email;
// 公司
@property (nonatomic, copy) NSString *company;
// 是否选择
@property (nonatomic, assign) BOOL isSelected;

@end

NS_ASSUME_NONNULL_END

、、、

PersonModel.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface PersonModel : NSObject
// 联系人
@property (nonatomic, strong) CNContact *contact;
// 姓名
@property (nonatomic, copy) NSString *userName;
// 重复的电话
@property (nonatomic, copy) NSString *mobileNumber;
// 重复的邮箱
@property (nonatomic, copy) NSString *email;
// 公司
@property (nonatomic, copy) NSString *company;
// 是否选择
@property (nonatomic, assign) BOOL isSelected;
// 电话
@property (nonatomic, copy) NSArray *mobileNumberArr;
// 邮箱
@property (nonatomic, copy) NSArray *emailArr;

@end

NS_ASSUME_NONNULL_END

日历管理类

这就是获取了日历上的事件,方便进行清理

日历事件管理

日历获取使用方法

- (CleanDateManager *)dateManager
{
    if (!_dateManager)
    {
        _dateManager = [CleanDateManager shareManager];
    }
    return _dateManager;
}


-(void)requestData
{
    
    CleanSelf(self);
    
    NSDate *startDate = [NSDate dateWithTimeInterval:-24*3600*365*3 sinceDate:[NSDate date]];

    [self.dateManager getLocalCalendarAuthorization:startDate dataArray:^(NSArray * _Nonnull dataArray) {
               
        NSLog(@"%@",dataArray);
    }];
}

CleanDateManager.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CleanDateManager : NSObject

/// 所有日历的
@property (nonatomic, strong, readonly) NSArray *allDateArray;

+ (CleanDateManager *)shareManager;
// 判断权限
- (void)getLocalCalendarAuthorization:(NSDate *)startDate dataArray:(void (^)(NSArray *dataArray))completion;
// 获取数据
- (NSArray *)getLocalSystemCalendarEventWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate;
// 删除数据
- (BOOL)deleteCalendarEventIdentifier:(NSString *)eventIdentifier;


@end

NS_ASSUME_NONNULL_END

CleanDateManager.m

//
//  CleanDateManager.m
//  CleanOne
//
//  Created by iOS Clean on 2023/8/28.
//

#import "CleanDateManager.h"
#import "CleanDateModel.h"

@interface CleanDateManager ()

@property (nonatomic, strong) EKEventStore *eventStore;

/// 所有日历的
@property (nonatomic, strong, readwrite) NSArray *allDateArray;

@end

@implementation CleanDateManager

+ (CleanDateManager *)shareManager {
    static CleanDateManager *manager = nil;
    static dispatch_once_t token;
    dispatch_once(&token, ^{
        manager = [[CleanDateManager alloc] init];
    });
    return manager;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        
    }
    return self;
}

/**
 EKAuthorizationStatusNotDetermined = 0,     // 未进行授权选择
 EKAuthorizationStatusRestricted,                   // 未授权,且用户无法更新,如家长控制情况下
 EKAuthorizationStatusDenied,        // 用户拒绝App使用
 EKAuthorizationStatusAuthorized,         // 已授权,可使用
 */
- (void)getLocalCalendarAuthorization:(NSDate *)startDate dataArray:(void (^)(NSArray *dataArray))completion{
    
    EKAuthorizationStatus eventStatus = [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent];
        
    if (eventStatus == EKAuthorizationStatusNotDetermined) {
      
        // 用户尚未授权,提示用户授权。下边的requestAccessToEntityType:方法可以调出系统授权弹窗
        [self.eventStore requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError * _Nullable error) {
            
            if (granted) {
                // 允许
               
                NSArray *array = [self getLocalSystemCalendarEventWithStartDate:startDate endDate:[NSDate date]];
                
                completion(array);
                
            } else {
                // 不允许
                completion(nil);
            }
        }];
    } else if (eventStatus == EKAuthorizationStatusAuthorized) {
        // 用户已经允许授权。作相应处理,比如查询日历里今天的所有事件..
        NSArray *array = [self getLocalSystemCalendarEventWithStartDate:startDate endDate:[NSDate date]];
        
        completion(array);
    }
    else
    {
        completion(nil);
    }
}

- (NSArray *)getLocalSystemCalendarEventWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate {
    NSArray *eventArray = [self.eventStore calendarsForEntityType:EKEntityTypeEvent];
    NSMutableArray *calendar = [NSMutableArray array];
    for (int i = 0; i < eventArray.count; i++) {
        EKCalendar *temp = eventArray[I];
        EKCalendarType type = temp.type;
        if (type == EKCalendarTypeLocal || type == EKCalendarTypeCalDAV) {
            [calendar addObject:temp];
        }
    }
    NSPredicate *predicate = [self.eventStore predicateForEventsWithStartDate:startDate endDate:endDate calendars:calendar];
    NSArray *events = [self.eventStore eventsMatchingPredicate:predicate];
    events = [events sortedArrayUsingSelector:@selector(compareStartDateWithEvent:)];
    NSMutableArray *results = [NSMutableArray array];
    for (EKEvent *event in events) {
        CleanDateModel *model = [[CleanDateModel alloc] init];
        model.eventTitle = event.title;
        model.eventDate = event.startDate;
        model.event = event;
        [results addObject:model];
    }
    return results;
}

- (void)writeToLocalCalendarAction {
    EKEventStore *eventStore = [[EKEventStore alloc] init];

    /**
     事件保存到日历
     06.07 元素
     title(标题 NSString),
     location(位置NSString),
     startDate(开始时间 2016/06/07 11:14AM),
     endDate(结束时间 2016/06/07 11:14AM),
     addAlarm(提醒时间 2016/06/07 11:14AM),
     notes(备注类容NSString)
     */
    
    /// 06.07 时间格式
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setAMSymbol:@"AM"];
    [dateFormatter setPMSymbol:@"PM"];
    [dateFormatter setDateFormat:@"hh:mm"];
    NSDate *date = [NSDate date];
    NSString *dateStr = [dateFormatter stringFromDate:date];
    NSLog(@"%@",dateStr);
    
    /// 创建事件
    EKEvent *event = [EKEvent eventWithEventStore:eventStore];
    event.title = [NSString stringWithFormat:@"写入日历事件-%@",dateStr];
    event.location = @"北京app";
    
    /// 开始时间(必须传)
    event.startDate = [date dateByAddingTimeInterval:60 * 2];
    /// 结束时间(必须传)
    event.endDate = [date dateByAddingTimeInterval:60 * 5 * 24];
    ///  event.allDay = YES;//全天
    
    /// 添加提醒
    /// 第一次提醒  (几分钟后)
    [event addAlarm:[EKAlarm alarmWithRelativeOffset:60.0f * -1.0f]];
    /// 第二次提醒  ()
    /// [event addAlarm:[EKAlarm alarmWithRelativeOffset:60.0f * -10.0f * 24]];
    
    /// 06.07 add 事件类容备注
    NSString *str = @"这是备注";
    event.notes = [NSString stringWithFormat:@"%@:%@", str, dateStr];
    /// 将日历事件添加到默认的日历源中
    [event setCalendar:[eventStore defaultCalendarForNewEvents]];
    /// 保存日历事件
    NSError *err;
    [eventStore saveEvent:event span:EKSpanThisEvent error:&err];
}

/// 删除日历事件(删除单个)
/// @param eventIdentifier 事件ID(标识符)
- (BOOL)deleteCalendarEventIdentifier:(NSString *)eventIdentifier {
   
    EKEvent *event;
    
    NSError *error = nil;
    
    if (eventIdentifier && ![eventIdentifier isEqualToString:@""]) {
       
        event = [self.eventStore eventWithIdentifier:eventIdentifier];
        
        BOOL isSuccess = [self.eventStore removeEvent:event span:EKSpanThisEvent commit:YES error:&error];
        
        return isSuccess;

    }
    else
    {
        return NO;
    }
    
}

/// 删除日历事件(可删除一段时间内的事件)
/// @param startDate 开始时间
/// @param endDate 结束时间
- (BOOL)deleteCalendarStartDate:(NSDate *)startDate addEndDate:(NSDate *)endDate {
    // 获取到此事件
    NSArray * eventArray = [self.eventStore calendarsForEntityType:EKEntityTypeEvent];
    NSMutableArray *onlyArray = [NSMutableArray array];
    for (int i = 0; i < eventArray.count; i++) {
        EKCalendar *tempCalendar = eventArray[I];
        EKCalendarType type = tempCalendar.type;
        if (type == EKCalendarTypeCalDAV) {
            [onlyArray addObject:tempCalendar];
        }
    }
    
    NSPredicate *predicate = [self.eventStore predicateForEventsWithStartDate:startDate endDate:endDate calendars:onlyArray];
    NSArray *events = [self.eventStore eventsMatchingPredicate:predicate];
    
    for (int i = 0; i < events.count; i ++) {
        // 删除这一条事件
        EKEvent *event = events[I];
        NSError *error = nil;
        
        // commit:NO:最后再一次性提交
        [self.eventStore removeEvent:event span:EKSpanThisEvent commit:NO error:&error];
    }
    //一次提交所有操作到事件库
    NSError *errored = nil;
    BOOL commitSuccess = [self.eventStore commit:&errored];
    return commitSuccess;
}


- (EKEventStore *)eventStore {
    if (!_eventStore) {
        _eventStore = [[EKEventStore alloc] init];
    }
    return _eventStore;
}

@end

日历的 Model

CleanDateModel.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CleanDateModel : NSObject
// 日历
@property (nonatomic, strong) EKEvent *event;
// 日历标题
@property (nonatomic, copy) NSString *eventTitle;
// 日期
@property (nonatomic, strong) NSDate *eventDate;
// 是否选择
@property (nonatomic, assign) BOOL isSelected;

@end

NS_ASSUME_NONNULL_END

CleanDateModel.m

#import "CleanDateModel.h"

@implementation CleanDateModel

- (instancetype)init {
    
    if (self = [super init]) {
        
        self.isSelected = NO;
    }
    return self;
}

@end

下面再说下多语言的切换,我搜了下,应用内的切换是获取 storyboard,然后进行重新加载的,我只是在界面给了个提示,设置成功后下次启动生效

多语言切换界面

语言设置管理类

JHLanguage.h

//
//  JHLanguage.h
//  CleanOne
//
//  Created by iOS Clean on 2023/9/27.
//

#import <Foundation/Foundation.h>

#define kAppLanguage            @"cleanLanguage"
#define kAppLanguage_CH         @"zh-Hans"
#define kAppLanguage_EN         @"en"
#define kAppLanguage_FR         @"fr-CN"
#define kAppLanguage_DE         @"de-CN"
#define kAppLanguage_Je         @"ja-CN"

#define kJHCurrentLanguage \
[[JHLanguage language] getIPhoneLanguage]

#define kJHSetLanguage(lan) \
[[JHLanguage language] setLanguageWith:lan]

#define kJHLocalizedString(key,tab) \
[[JHLanguage language] jh_stringForKey:key table:tab]

typedef NS_ENUM(NSUInteger, LanagueType) {
   
    English,
    Chinese,
    German,
    Japanese,
    French
    
};

@interface JHLanguage : NSObject

+ (instancetype)language;
// 获取当前的语言
- (NSString *)getIPhoneLanguage;
// 设置语言
- (void)setLanguageWith:(LanagueType)language;

- (NSString *)jh_stringForKey:(NSString *)key table:(NSString *)table;

@end

JHLanguage.m

#import "JHLanguage.h"

@interface JHLanguage()
@property (nonatomic,  strong) NSString *currentLanguage;
@property (nonatomic,  strong) NSBundle *bundle;
@end

@implementation JHLanguage

+ (instancetype)language{
    static JHLanguage *lan = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        lan = [[JHLanguage alloc] init];
    });
    return lan;
}

- (instancetype)init{
    if (self = [super init]) {
        _currentLanguage = [[NSUserDefaults standardUserDefaults] objectForKey:kAppLanguage];
        if (!_currentLanguage) {
            _currentLanguage = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"][0]; // system default.
        }
        _bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:_currentLanguage ofType:@"lproj"]];
    }
    return self;
}

- (NSString *)getIPhoneLanguage{
    
    NSString *language = [[NSUserDefaults standardUserDefaults] objectForKey:kAppLanguage];
    
    if (language == nil) {
        
        language = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"][0];
    }
    return language;
}

- (void)setLanguageWith:(LanagueType)language{
   
    if (language == Chinese) {
        
        _currentLanguage = kAppLanguage_CH;
        
    }else if (language == English){
        
        _currentLanguage = kAppLanguage_EN;
        
    }else if (language == German){
        
        _currentLanguage = kAppLanguage_DE;
        
    }else if (language == French){
        
        _currentLanguage = kAppLanguage_FR;
    }
    else if (language == Japanese)
    {
        _currentLanguage = kAppLanguage_Je;
    }
    
    _bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:_currentLanguage ofType:@"lproj"]];
    
    [[NSUserDefaults standardUserDefaults] setObject:_currentLanguage forKey:kAppLanguage];
    [[NSUserDefaults standardUserDefaults] setValue:@[_currentLanguage] forKey:@"AppleLanguages"];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

- (NSString *)jh_stringForKey:(NSString *)key table:(NSString *)table{
    
    if (_bundle) {
        
        return NSLocalizedStringFromTableInBundle(key, table, _bundle, nil);
    }
    return NSLocalizedStringFromTable(key, table, nil);
}

@end

国际化语言添加的方式说一下

提前创建这个文件,名字不可更改


创建多语言文件
添加多语言

当然你也可以对 Info.Plist文件中的提示信息进行设置

//比如说日历权限获取就这样写,在切换语言后,权限的提示语也会跟着变

NSCalendarsUsageDescription = "应用程序可以帮助用户分析日历中已过期或未使用的日程信息,以便用户可以查看或删除这些信息";

设置
// 这是我定义的宏
#define CleanLanguage(key,comment) NSLocalizedStringFromTable(key,@"",comment)
// 使用方法如下
self.label.text = CleanLanguage(@"Charging", @"这里只是为了记录是什么");

清理类的就这么多了,由于是公司项目,不能放到网上,有问题自行研究吧。

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

推荐阅读更多精彩内容