iOS 清理相似照片、視頻、聯(lián)系人、日歷

最近做了一款清理類的 APP,說(shuō)實(shí)話,iOS 清理真沒(méi)什么可清理的,只能清理下相似照片,重復(fù)聯(lián)系人,日歷事件等

照片、視頻管理類

相似照片統(tǒng)計(jì)

我這里獲取了相冊(cè)的所有信息,并將相似照片,相似視頻,照片大于10M 和 視頻大于 20M 的大文件也整理了處理,可以直接調(diào)用,在屬性上直接獲取,記得刷新 UI 的時(shí)候要在主線程進(jìn)行操作,否則會(huì)崩潰

調(diào)用方法如下

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

-(void)loadPhotoData
{
    // 加載照片數(shù)據(jù)源
    CleanSelf(self);
    
    [CleanOneLoading showWithMessage:CleanLanguage(@"Loading...",@"加載中...")];
    
    [self.clearPhotoManager getAllBigAsset:^(BOOL success) {

       // 這里有很多屬性,自己打印看看,這里列舉了兩個(gè)
        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, // 相冊(cè)變更默認(rèn)處理
    PhotoNotificationStatusClose    = 1, // 相冊(cè)變更不處理
    PhotoNotificationStatusNeed     = 2, // 相冊(cè)變更主動(dòng)處理
};

@protocol ClearPhotoManagerDelegate <NSObject>

@optional
/// 相冊(cè)變動(dòng)代理方法
- (void)clearPhotoLibraryDidChange;

@end

@interface ClearPhotoManager : NSObject

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

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

/// 變更狀態(tài)
@property (nonatomic, assign) PhotoNotificationStatus notificationStatus;

// 這里是 PHASet 對(duì)象
@property (nonatomic, strong, readonly) NSMutableArray *allPhotoArr;
@property (nonatomic, strong, readonly) NSMutableArray *allVideoArr;


/// 相似照片信息:存儲(chǔ)了相似圖片數(shù)量及可以節(jié)省的內(nèi)存空間大小
@property (nonatomic, strong, readonly) NSDictionary *similarInfo;
@property (nonatomic, strong, readonly) NSMutableArray *similarArr;

/// 截圖照片信息:存儲(chǔ)了屏幕截圖數(shù)量及可以節(jié)省的內(nèi)存空間大小
@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;
///  大文件視頻 - 長(zhǎng)視頻
@property (nonatomic, strong, readonly) NSMutableArray *bigVideoArr;
@property (nonatomic, strong, readonly) NSDictionary *bigVideoInfo;


// 獲取相冊(cè)的 PHAsset 對(duì)象
- (void)loadPhotoCompletionHandler:(void (^)(BOOL success))completion isVideo:(BOOL)isVideo;

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

// 獲取相冊(cè)中的大文件
- (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對(duì)象
@property (nonatomic, strong) PHFetchResult *assetArray;

// 這里是 PHASet 對(duì)象
@property (nonatomic, strong, readwrite) NSMutableArray *allPhotoArr;
@property (nonatomic, strong, readwrite) NSMutableArray *allVideoArr;

/// 相似照片信息:存儲(chǔ)了相似圖片數(shù)量及可以節(jié)省的內(nèi)存空間大小
@property (nonatomic, strong, readwrite) NSDictionary *similarInfo;
@property (nonatomic, strong, readwrite) NSMutableArray *similarArr;

/// 截圖照片信息:存儲(chǔ)了屏幕截圖數(shù)量及可以節(jié)省的內(nèi)存空間大小
@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;

// 完成的回調(diào)
@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 - 相冊(cè)變換通知

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        // 相冊(cè)變換通知
        [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
    }
    return self;
}

- (void)dealloc
{
    // 移除相冊(cè)變換通知
    [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}

// 相冊(cè)變換時(shí)候會(huì)調(diào)用
- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
    // 篩選出沒(méi)必要的變動(dòng)
    PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetArray];
   
    if (collectionChanges == nil || self.notificationStatus != PhotoNotificationStatusDefualt)
    {
        return;
    }
    
    // 回到主線程調(diào)用相冊(cè)變動(dòng)代理方法
    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;
}



// 加載照片之前先清除舊數(shù)據(jù)
- (void)resetTagData
{
    // 相冊(cè)清空
    self.allPhotoArr = nil;
    self.similarInfo = nil;
    self.similarArr = nil;
    self.screenshotsInfo = nil;
    self.screenshotsArr = nil;
    self.bigPhotoArr = nil;

    // 視頻數(shù)據(jù)清空
    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
{
    // 有一個(gè)日期為空則直接返回
    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))
    {
        // 如果是同一天,那就計(jì)算兩者的時(shí)間差是不是在一個(gè)小時(shí)之內(nèi)
        NSInteger dateOneSecond = dateComponents1.minute;
        NSInteger dateTwoSecond = dateComponents2.minute;
        NSInteger Difference = labs(dateTwoSecond - dateOneSecond);

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

// NSDate轉(zhuǎn)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:^{
        // 刪除當(dāng)前圖片資源
        [PHAssetChangeRequest deleteAssets:assets];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        // 調(diào)用刪除后的回調(diào)代碼塊
        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 則用于控制請(qǐng)求的圖片質(zhì)量
        _imageRequestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
    }
    return _imageRequestOptions;
}

- (PHImageRequestOptions *)imageSizeRequestOptions
{
    if (!_imageSizeRequestOptions) {
        _imageSizeRequestOptions = [[PHImageRequestOptions alloc] init];
        // resizeMode 屬性控制圖像的剪裁
        _imageSizeRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;// exactly targetSize
        // deliveryMode 則用于控制請(qǐng)求的圖片質(zhì)量
        _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;
        
    /*
    如果不需要獲取縮略圖,就設(shè)置為NO,如果需要獲取縮略圖,則maximumSize為獲取的最大尺寸。
    以BBC為例,getThumbnail = NO時(shí),打印寬高數(shù)據(jù)為:1920*1072。
    getThumbnail = YES時(shí),maximumSize為100*100。打印寬高數(shù)據(jù)為:100*55.
    注:不乘[UIScreen mainScreen].scale,會(huì)發(fā)現(xiàn)縮略圖在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;
}



// 獲取相冊(cè)詳細(xì)信息
-(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);
                 }
             }];
        }
        
    });
}


// 獲取視頻詳細(xì)信息
-(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);
                    }
                }
            }];
        }
        
    });
}





// 判斷相冊(cè)授權(quán)狀態(tài)
- (void)getAllBigAsset:(void (^)(BOOL success))completion
{
    // 清除舊數(shù)據(jù)
    [self resetTagData];
    
    self.bigCompletionHandler = completion;

    // 獲取當(dāng)前App的相冊(cè)授權(quán)狀態(tài)
    PHAuthorizationStatus authorizationStatus = [PHPhotoLibrary authorizationStatus];
    // 判斷授權(quán)狀態(tài)
    if (authorizationStatus == PHAuthorizationStatusAuthorized)
    {
        [self greatAllPhotoAndVideos];
    }
    // 如果沒(méi)決定, 彈出指示框, 讓用戶選擇
    else if (authorizationStatus == PHAuthorizationStatusNotDetermined)
    {
        [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
            // 如果用戶選擇授權(quán), 則獲取圖片
            if (status == PHAuthorizationStatusAuthorized)
            {
                [self greatAllPhotoAndVideos];
            }
            else
            {
                // 開(kāi)啟權(quán)限提示
                completion(NO);
            }
        }];
    }
    else
    {
        completion(NO);
    }
}

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

    // 獲取相簿中的PHAsset對(duì)象
    self.allVideoArr = [self greatAllArr:YES];
    self.allPhotoArr = [self greatAllArr:NO];
    // 處理相冊(cè)問(wèn)題
    [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];

    // 獲取相冊(cè)
    PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumUserLibrary options:nil];

    // 獲取所有資源的集合,并按資源的創(chuàng)建時(shí)間排序,這樣就可以通過(guò)和上一張圖片判斷日期來(lái)分組了
    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;
        // 過(guò)濾空相冊(cè)
        if (collection.estimatedAssetCount <= 0){
            return;
        }
        
        

        // 獲取相冊(cè)內(nèi)asset result
            PHFetchResult<PHAsset *> *result = [PHAsset fetchAssetsInAssetCollection:collection options:option];

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

                //資源圖片檢測(cè)
                if (![obj isKindOfClass:[PHAsset class]]) {
                    return;
                }
                PHAsset *asset = (PHAsset *) obj;

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

    return resultArray;
}

@end

相似度的算法使用的是 opencv2 來(lái)計(jì)算的相似度,但是有一點(diǎn),照片過(guò)多時(shí),內(nèi)存會(huì)瘋狂上漲,導(dǎo)致崩潰,原因就是對(duì)比這里出現(xiàn)的問(wèn)題,解決辦法如下

1.通過(guò)時(shí)間,獲取照片(視頻)的拍攝時(shí)間,對(duì)比時(shí)如果在同一天再去對(duì)比,當(dāng)然你也可以再細(xì)致的去判斷,比如判斷相差時(shí)間在幾分鐘之內(nèi)也 ok

// 是否為同一天
- (BOOL)isSameDay:(NSDate *)date1 date2:(NSDate *)date2
{
    // 有一個(gè)日期為空則直接返回
    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))
    {
        // 如果是同一天,那就計(jì)算兩者的時(shí)間差是不是在一個(gè)小時(shí)之內(nèi)
        NSInteger dateOneSecond = dateComponents1.minute;
        NSInteger dateTwoSecond = dateComponents2.minute;
        NSInteger Difference = labs(dateTwoSecond - dateOneSecond);

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

2.修改對(duì)比的方法,減少圖片縮小后的大小,這里是最有效的,這樣會(huì)大大減小內(nèi)存,我減小到了 32 *32,不過(guò)量大肯定也會(huì)崩潰,我測(cè)試在同時(shí)對(duì)比 1500 張照片,210 個(gè)視頻下是不會(huì)崩潰的

// 是否相似
+ (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;
}

下面是對(duì)比的類 和 model ,我這里視頻和圖片共用了一個(gè) Model

CleanAlbumModel.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CleanAlbumModel : NSObject

/// 圖片資源
@property (nonatomic, strong) PHAsset *asset;
/// 圖片
@property (nonatomic, strong) UIImage *exactImage;
/// 圖片數(shù)據(jù)大小
@property (nonatomic, assign) NSUInteger originImageDataLength;
/// 時(shí)長(zhǎng)
@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 圖片相似度對(duì)比

#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 {
    // 當(dāng)前匹配結(jié)果,越接近于0.0匹配度越高
    double dbRst=1.0;
    // 匹配結(jié)果,-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("匹配結(jié)果為:%f\n",dbRst1);
        if (dbRst1<=0.01) {
            // 匹配成功
            tfFound = 1;
            break;
        }
        
        else if(dbRst==1.0 || dbRst1<dbRst) {
            // 本次匹配有進(jìn)步,更新結(jié)果
            cvReleaseImage(&cropImage);
            dbRst = dbRst1;
        }
        
        else if(dbRst1>dbRst) {
            cvReleaseImage(&cropImage);
        }
    }
    
    return dbRst;
}

+ (double)CompareHistWithSmallHeightIpl:(IplImage*)srcIpl withBigHeightIplImg:(IplImage*)srcIpl1 {
    // 當(dāng)前匹配結(jié)果,越接近于0.0匹配度越高
    double dbRst=1.0;
    // 匹配結(jié)果,-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("匹配結(jié)果為:%f\n",dbRst1);
        if (dbRst1<=0.01) {
            // 匹配成功
            tfFound = 1;
            break;
        }
        else if(dbRst==1.0 || dbRst1<dbRst) {
            // 本次匹配有進(jìn)步,更新結(jié)果
            cvReleaseImage(&cropImage);
            dbRst = dbRst1;
        }
        
        else if(dbRst1>dbRst) {
            cvReleaseImage(&cropImage);
        }
    }
    
    return dbRst;
}

+ (double)CompareHistWithSmallIpl:(IplImage*)srcIpl withBigIplImg:(IplImage*)srcIpl1 {
    // 當(dāng)前匹配結(jié)果,越接近于0.0匹配度越高
    double dbRst=1.0;
    // 水平、豎直偏移量
    int xSub=0,ySub=0;
    // 匹配結(jié)果,-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),豎直匹配結(jié)果為:%f\n",j,i,dbRst1);
            if (dbRst1<=0.0375) {
                // 匹配成功
                tfFound = 1;
                break;
            } else if(dbRst==1.0 || dbRst1<dbRst) {
                // 本次匹配有進(jìn)步,更新結(jié)果
                cvReleaseImage(&cropImage);
                dbRst = dbRst1;
            } else if(dbRst1>dbRst) {
                cvReleaseImage(&cropImage);
                // 豎直移動(dòng)到點(diǎn)了,該水平移動(dòng)了
                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),水平移動(dòng)匹配結(jié)果為:%f\n",k,i,dbRst1);
                    if (dbRst1<=0.0375) {
                        // 匹配成功
                        tfFound = 1;
                        xSub = k;
                        break;
                    } else if(dbRst1<dbRst) {
                        // 本次匹配有進(jìn)步,更新結(jié)果
                        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;
}

// 多通道彩色圖片的直方圖比對(duì)
+ (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);
}

// 進(jìn)行膚色檢測(cè)
+ (void)SkinDetect:(IplImage*)src withParam:(IplImage*)dst {
    // 創(chuàng)建圖像頭
    // 用于存圖像的一個(gè)中間變量,是用來(lái)分通道用的,分成hsv通道
    IplImage* hsv = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 3);
    // 通道的中間變量,用于膚色檢測(cè)的中間變量
    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個(gè)通道
    cvSplit(hsv,H,S,V,0);
    
    /*********************膚色檢測(cè)部分**************/
    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類型轉(zhuǎn)換為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類型轉(zhuǎn)換為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;
}
相似度對(duì)比

通訊錄管理類

這里獲取了所有聯(lián)系人,以及手機(jī)號(hào),郵箱,姓名重復(fù)的聯(lián)系人,并進(jìn)行統(tǒng)計(jì)!!
聯(lián)系人獲取

獲取聯(lián)系人的方法


- (CleanPersonManager *)personManager
{
    if (!_personManager)
    {
        _personManager = [CleanPersonManager shareManager];
    }
    return _personManager;
}
// 獲取所有通訊錄里的聯(lián)系人
-(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
            {
                //沒(méi)權(quán)限
            }

        });
        
    }];
   
}

聯(lián)系人管理類 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;

/// 所有聯(lián)系人的
@property (nonatomic, strong, readonly) NSDictionary *allPersonInfo;
/// 電話相似信息
@property (nonatomic, strong, readonly) NSDictionary *similarPhoneNumInfo;
/// 姓名相似信息
@property (nonatomic, strong, readonly) NSDictionary *similaNamerInfo;
/// 郵件相似信息
@property (nonatomic, strong, readonly) NSDictionary *similarEmailInfo;


// 獲取權(quán)限,并請(qǐng)求全部聯(lián)系人、相似名字、相似電話、相似郵箱
- (void)loadPerson:(void (^)(BOOL isFinish))completion;

// 獲取所有名字相同的聯(lián)系人
- (void)getDuplicateNameCompleteHandle:(void(^)(NSDictionary *duplicateNameDict))complete;

// 獲取所有電話相同的聯(lián)系人
- (void)getDuplicateNumberCompleteHandle:(void(^)(NSDictionary *duplicateNumberDict))complete;

// 獲取所有郵箱相同的聯(lián)系人
- (void)getDuplicateEmailCompleteHandle:(void(^)(NSDictionary *duplicateEmailDict))complete;

// 合并到第幾個(gè)人下面
- (void)mergeContactWithContacts:(NSArray *)contacts index:(NSInteger)index type:(NSInteger)type;

// 刪除對(duì)應(yīng)的聯(lián)系人
- (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;

/// 所有聯(lián)系人的
@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;
}


// 判斷通訊錄授權(quán)狀態(tài)
- (void)loadPerson:(void (^)(BOOL isFinish))completion
{
    // 獲取當(dāng)前App的通訊錄授權(quán)狀態(tài)
    CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
    // 判斷授權(quán)狀態(tài)
    if (status == CNAuthorizationStatusAuthorized)
    {
        // 如果已經(jīng)授權(quán), 獲取通訊錄信息
        [self getAllPerson:^(BOOL isFinish) {
           
            completion(YES);
        }];
    }
    // 如果沒(méi)決定, 彈出指示框, 讓用戶選擇
    else if (status == CNAuthorizationStatusNotDetermined)
    {
        // 如果用戶選擇授權(quán), 則獲取圖片
        CNContactStore *store = [[CNContactStore alloc] init];
       
        [store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
            
            if (!error) {
                
                // 如果已經(jīng)授權(quán), 獲取通訊錄信息
                [self getAllPerson:^(BOOL isFinish) {
                   
                    completion(YES);
                }];
            }
            else
            {
                // 開(kāi)啟權(quán)限提示
                completion(NO);
            }
            
        }];
    }
    else
    {
        // 開(kāi)啟權(quán)限提示
        completion(NO);
    }
}

-(void)getAllPerson:(void(^)(BOOL isFinish))complete
{
    // 如果已經(jīng)授權(quán), 獲取通訊錄信息
    [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;
            // 匹配電話號(hào)碼
            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);
    }
}

// 處理相似名字的聯(lián)系人
- (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;
}


// 處理相似手機(jī)的聯(lián)系人 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;
}


///  是否有相同的數(shù)據(jù)
/// - Parameters:
///   - oneArr: 對(duì)比 1 數(shù)組
///   - twoArr: 對(duì)比 2 數(shù)組
///   - type: 1:對(duì)比電話 2:對(duì)比郵箱
-(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

重復(fù)姓名、聯(lián)系人等,合并的時(shí)候要選擇合并至哪個(gè)人底下,這里選擇的意義在于放棄未選中人的所有信息,將需要合并的值進(jìn)行合并,這里是有優(yōu)化的點(diǎn)的,可以把兩個(gè)人的所有信息都同步到一個(gè)人的身上,然后相同的過(guò)濾掉,我嫌麻煩所以沒(méi)做

合并界面

提示選擇的 model
、、、

import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface AlertChoosPersonModel : NSObject

// 聯(lián)系人
@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
// 聯(lián)系人
@property (nonatomic, strong) CNContact *contact;
// 姓名
@property (nonatomic, copy) NSString *userName;
// 重復(fù)的電話
@property (nonatomic, copy) NSString *mobileNumber;
// 重復(fù)的郵箱
@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

日歷管理類

這就是獲取了日歷上的事件,方便進(jìn)行清理

日歷事件管理

日歷獲取使用方法

- (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;
// 判斷權(quán)限
- (void)getLocalCalendarAuthorization:(NSDate *)startDate dataArray:(void (^)(NSArray *dataArray))completion;
// 獲取數(shù)據(jù)
- (NSArray *)getLocalSystemCalendarEventWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate;
// 刪除數(shù)據(jù)
- (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,     // 未進(jìn)行授權(quán)選擇
 EKAuthorizationStatusRestricted,                   // 未授權(quán),且用戶無(wú)法更新,如家長(zhǎng)控制情況下
 EKAuthorizationStatusDenied,        // 用戶拒絕App使用
 EKAuthorizationStatusAuthorized,         // 已授權(quán),可使用
 */
- (void)getLocalCalendarAuthorization:(NSDate *)startDate dataArray:(void (^)(NSArray *dataArray))completion{
    
    EKAuthorizationStatus eventStatus = [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent];
        
    if (eventStatus == EKAuthorizationStatusNotDetermined) {
      
        // 用戶尚未授權(quán),提示用戶授權(quán)。下邊的requestAccessToEntityType:方法可以調(diào)出系統(tǒng)授權(quán)彈窗
        [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) {
        // 用戶已經(jīng)允許授權(quán)。作相應(yīng)處理,比如查詢?nèi)諝v里今天的所有事件..
        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(標(biāo)題 NSString),
     location(位置NSString),
     startDate(開(kāi)始時(shí)間 2016/06/07 11:14AM),
     endDate(結(jié)束時(shí)間 2016/06/07 11:14AM),
     addAlarm(提醒時(shí)間 2016/06/07 11:14AM),
     notes(備注類容NSString)
     */
    
    /// 06.07 時(shí)間格式
    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);
    
    /// 創(chuàng)建事件
    EKEvent *event = [EKEvent eventWithEventStore:eventStore];
    event.title = [NSString stringWithFormat:@"寫入日歷事件-%@",dateStr];
    event.location = @"北京app";
    
    /// 開(kāi)始時(shí)間(必須傳)
    event.startDate = [date dateByAddingTimeInterval:60 * 2];
    /// 結(jié)束時(shí)間(必須傳)
    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];
    /// 將日歷事件添加到默認(rèn)的日歷源中
    [event setCalendar:[eventStore defaultCalendarForNewEvents]];
    /// 保存日歷事件
    NSError *err;
    [eventStore saveEvent:event span:EKSpanThisEvent error:&err];
}

/// 刪除日歷事件(刪除單個(gè))
/// @param eventIdentifier 事件ID(標(biāo)識(shí)符)
- (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;
    }
    
}

/// 刪除日歷事件(可刪除一段時(shí)間內(nèi)的事件)
/// @param startDate 開(kāi)始時(shí)間
/// @param endDate 結(jié)束時(shí)間
- (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];
    }
    //一次提交所有操作到事件庫(kù)
    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;
// 日歷標(biāo)題
@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

下面再說(shuō)下多語(yǔ)言的切換,我搜了下,應(yīng)用內(nèi)的切換是獲取 storyboard,然后進(jìn)行重新加載的,我只是在界面給了個(gè)提示,設(shè)置成功后下次啟動(dòng)生效

多語(yǔ)言切換界面

語(yǔ)言設(shè)置管理類

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;
// 獲取當(dāng)前的語(yǔ)言
- (NSString *)getIPhoneLanguage;
// 設(shè)置語(yǔ)言
- (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

國(guó)際化語(yǔ)言添加的方式說(shuō)一下

提前創(chuàng)建這個(gè)文件,名字不可更改


創(chuàng)建多語(yǔ)言文件
添加多語(yǔ)言

當(dāng)然你也可以對(duì) Info.Plist文件中的提示信息進(jìn)行設(shè)置

//比如說(shuō)日歷權(quán)限獲取就這樣寫,在切換語(yǔ)言后,權(quán)限的提示語(yǔ)也會(huì)跟著變

NSCalendarsUsageDescription = "應(yīng)用程序可以幫助用戶分析日歷中已過(guò)期或未使用的日程信息,以便用戶可以查看或刪除這些信息";

設(shè)置
// 這是我定義的宏
#define CleanLanguage(key,comment) NSLocalizedStringFromTable(key,@"",comment)
// 使用方法如下
self.label.text = CleanLanguage(@"Charging", @"這里只是為了記錄是什么");

清理類的就這么多了,由于是公司項(xiàng)目,不能放到網(wǎng)上,有問(wèn)題自行研究吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容