본문 바로가기
아이폰 개발/ios 개념&튜토리얼

ios Photos framework 로 사진첩만들기

by 인생여희 2021. 1. 9.

ios Photos framework 로 사진첩만들기

 

ios collectionview xib 파일 & customcell 예제

 

 

저번 포스팅에 이어서 이번 포스팅에서는 xib 파일로 collection view를 만들었다면, photos frame work를 이용해서 사진첩에 사진을 불러와서 collection view cell에 뿌려주는 예제를 만들어 보자.

 

 

순서

 

1.custom header 만들기

2.custom cell 만들기

3.collection view 컨트롤러photos 라이브러리 임포트

4.collection view 컨트롤러에서 1번 2번에서 만든 xib 파일 등록

5.collection view 컨트롤러에서 사진 가져와서 뿌려주기

 

 

예제

1.custom header 만들기

 

아래 그림처럼 header xib 파일을 만들고 버튼들을 생성해서 붙여준다. 그리고 인스펙터에서 identifier를 header로 설정해 준다.

 

 

 

Header.h

 

참고로 닫기버튼, 저장버튼을 누르면 collectionview Controller의 함수가 작동되어야 해서, 델리게이트 패턴으로 구현했다. 닫히고, 사진이 저장되는 부분은 Header 클래스가 아니다. collectionview Controller 이다. 

델리게이트 패턴에 대한 설명과 예제는 이곳(ios 델리게이트란 - delegate 예제) 을 참고하자.

 

 

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

//참고: 헤더에서 델리게이트 패턴 이용해서 컬렉션 뷰 함수 호출
//https://stackoverflow.com/questions/31458100/go-to-another-view-by-clicking-a-button-in-custom-cell

//비공식 프로토콜 함수
@protocol HeaderActionDelegate <NSObject>
-(void)closeView;
-(void)saveData;
@end


@interface Header : UICollectionReusableView
@property (nonatomic , weak) id<HeaderActionDelegate> headerDelgate;
@end

NS_ASSUME_NONNULL_END

 

 

Header.m

 

#import "Header.h"

@implementation Header

- (void)awakeFromNib {
    [super awakeFromNib];
    // Initialization code
}
- (IBAction)save:(id)sender {
    
    NSLog(@"저장");
    
    [_headerDelgate saveData];
}

- (IBAction)close:(id)sender {
    NSLog(@"닫기");
    
    [_headerDelgate closeView];
    
}

@end

 

2.custom cell 만들기

 

custom cel 생성을 위한 xib파일을 생성해주고, 아래와 같이 이미지뷰를 넣고 인스펙터에서 indentifier를 Cell로 작성해 준다.  

 

 

 

 

GallaryCell.h

 

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface GallaryCell : UICollectionViewCell

@property (strong, nonatomic) IBOutlet UIImageView *imageView;

@end

NS_ASSUME_NONNULL_END

 

 

GallaryCell.m

 

#import "GallaryCell.h"

@implementation GallaryCell

- (void)awakeFromNib {
    [super awakeFromNib];
    // Initialization code
}

@end

 

 

3.collection view 컨트롤러photos 라이브러리 임포트

 

사진에 접근할려면 info.plist 에서 Privacy - Photo Library Usage Description key를 활성화 시켜주고, 사진을 접근을 허용하겠냐고 값을 입력해야 한다. 그리고 Xcode - Build phases - Link Binary With Libaries 에서 photos framework를 add 해주자. 그리고 GellaryViewController.h 에서 photos framework를 import 해준다. 

 

@import Photos;

 

 

4.collection view 컨트롤러에서 1번 2번에서 만든 custom xib 파일 등록

 

viewDidLoad에서 위에서 만든 cell과 header를 collection view에 등록해준다. 

 

    UINib * nib = [UINib nibWithNibName:@"GallaryCell" bundle:nil];
    [collectionView registerNib:nib forCellWithReuseIdentifier:@"Cell"];
    
    
    UINib *headerNib = [UINib nibWithNibName:@"Header" bundle:nil];
    [collectionView registerNib:headerNib forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header"];

 

 

cell의 초기화는 아래 코드처럼 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 메소드에서 해준다. 

 

//cell 구성
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    
    //1.cell
    GallaryCell *gellarycell =
    [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    
    ....
    
    }

 

header의 초기화는 아래 코드처럼

-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath 에서 해준다. 

 

//커스텀 헤더 설정
//viewDidLoad에서 Header Nib 파일 등록필요.
//컬렉션 뷰 인스펙터 - 엑세서리 - section header 선택 필요
-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
    
    NSLog(@"헤더 셋팅");
    
    if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
        
        Header *rView =
        [self.collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header" forIndexPath:indexPath];
        
        [rView setHeaderDelgate:self];
        
        return  rView;
    }
    
    return nil;
}

 

 

5.collection view 컨트롤러에서 사진 가져와서 뿌려주기

 

이제 사진을 가져오기위해서 GellaryViewController.h 파일에서 사진 조회와 관련된 프로퍼티를 생성해준다. 

 

//사진을 조회하기 위한 이미지 관리자

@property (strong, nonatomic) PHCachingImageManager *imageManager;



//사진을 조회하기 위한 PHFetchResult

@property (strong) PHFetchResult *assetsFetchResults;

 

viewDidLoad에서 위의 프로퍼티들을 초기화 해준다.

 

    //사진 조회를 위한 이미지 관리자 생성 + 초기화
    imageManager  = [[PHCachingImageManager alloc] init];
    
    
    //전체 사진 개수 조회
    PHFetchOptions *option =  [[PHFetchOptions alloc] init];
    option.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
    assetsFetchResults = [PHAsset fetchAssetsWithOptions:option];
    
    
    NSLog(@"사진 전체 개수 : %lu" , (unsigned long)[assetsFetchResults count]);

 

 

마지막으로 collectionview delegate 메소드에서 사진을 호출해준다. 아래에서 사진첩의 개수로 cell 개수로 지정해주었고, 사진을 조회할때 옵션을 지정해서 사진을 조회한 다음 사진을 cell에 할당해 주었다. 

 

/* 컬렉션 뷰 셀 개수 */
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return assetsFetchResults.count;
}



//cell 구성
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    
    //1.cell
    GallaryCell *gellarycell =
    [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    

    NSInteger currentTag = gellarycell.tag + 1;
    gellarycell.tag = currentTag;
    

    CGSize size = gellarycell.imageView.frame.size;;
    
    //사진의 메터 데이터
    PHAsset *asset = assetsFetchResults[indexPath.item];
    
    //이미지 불러올때 옵션
    //setSynchronous 설정안해주면, 사진선택시 cell 흔들림 발생
    PHImageRequestOptions *option = [[PHImageRequestOptions alloc]init];
    [option setSynchronous:YES];
    [option setDeliveryMode:PHImageRequestOptionsDeliveryModeHighQualityFormat];
    [option setResizeMode:PHImageRequestOptionsResizeModeExact];
    
    [imageManager requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeAspectFill options:option resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
        //셀 태그가 변경되지 않은 경우에만 썸네일을 업데이트 하고 그렇지 않으면 셀이 재사용 된것.
        
        if (gellarycell.tag == currentTag) {
        if (result != nil) {
            //사진 cell에 할당
            gellarycell.imageView.image = result;
        }
            
            
        }
    }];
    
        
 
    
    return gellarycell;
}

 

 

6.collection view 컨트롤러 전체 코드

 

GellaryViewController.h

 


#import <UIKit/UIKit.h>
#import "Header.h"

@import Photos;

NS_ASSUME_NONNULL_BEGIN

@interface GellaryViewController : UIViewController <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout , HeaderActionDelegate>


//컬렉션 뷰 객체
@property (strong, nonatomic) IBOutlet UICollectionView *collectionView;

//사진을 조회하기 위한 이미지 관리자
@property (strong, nonatomic) PHCachingImageManager *imageManager;

//사진을 조회하기 위한 PHFetchResult
@property (strong) PHFetchResult *assetsFetchResults;

//선택된 cell 인덱스 담을 배열
@property (nonatomic , strong) NSMutableArray *arrSelectedIndex;

//이미지 저장을위한 딕셔너리
@property (nonatomic,strong) NSMutableDictionary *imageDictionary;

@end

NS_ASSUME_NONNULL_END

 

 

GellaryViewController.m

 


#import "GellaryViewController.h"
#import "GallaryCell.h"

@interface GellaryViewController ()

@end

@implementation GellaryViewController

@synthesize collectionView , imageManager , assetsFetchResults, imageDictionary;

- (void)viewDidLoad {
    [super viewDidLoad];
   

    NSLog(@"아이폰 스크린 크기 체크");
    CGRect screenRect = [[UIScreen mainScreen] bounds];
    CGFloat screenWidth = screenRect.size.width;
    CGFloat screenHeight = screenRect.size.height;
    
    NSLog(@"스크린 가로 : %f" , screenWidth);
    NSLog(@"스크린 세로 : %f" , screenHeight);
    
    [collectionView setDelegate:self];
    [collectionView setDataSource:self];
    
    
    [collectionView setAllowsMultipleSelection:YES];
    
    UINib * nib = [UINib nibWithNibName:@"GallaryCell" bundle:nil];
    [collectionView registerNib:nib forCellWithReuseIdentifier:@"Cell"];
    
    
    UINib *headerNib = [UINib nibWithNibName:@"Header" bundle:nil];
    [collectionView registerNib:headerNib forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header"];
    
    
    //선택된 cell 인덱스 저장하는 배열
    _arrSelectedIndex = [[NSMutableArray alloc]init];
    
    //사진 전체 저장을 위한 딕셔너리 초기화
    self.imageDictionary = [NSMutableDictionary new];

    //사진 조회를 위한 이미지 관리자 생성 + 초기화
    imageManager  = [[PHCachingImageManager alloc] init];
    
    
    //전체 사진 개수 조회
    PHFetchOptions *option =  [[PHFetchOptions alloc] init];
    option.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
    assetsFetchResults = [PHAsset fetchAssetsWithOptions:option];
    
    
    NSLog(@"사진 전체 개수 : %lu" , (unsigned long)[assetsFetchResults count]);
    
    
    [self checkSaveFolder];
    
}


/* 컬렉션 뷰 셀 개수 */
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return assetsFetchResults.count;
}



//cell 구성
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    
    //1.cell
    GallaryCell *gellarycell =
    [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    

    NSInteger currentTag = gellarycell.tag + 1;
    gellarycell.tag = currentTag;
    

    CGSize size = gellarycell.imageView.frame.size;;
    
    //사진의 메터 데이터
    PHAsset *asset = assetsFetchResults[indexPath.item];
    
    PHImageRequestOptions *option = [[PHImageRequestOptions alloc]init];
    option.synchronous = YES;
    option.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
    
    
    [imageManager requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeAspectFill options:option resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
        //셀 태그가 변경되지 않은 경우에만 썸네일을 업데이트 하고 그렇지 않으면 셀이 재사용 된것.
        
        if (gellarycell.tag == currentTag) {
            
            //사진 cell에 할당
            gellarycell.imageView.image = result;
            
        }
    }];
    
    
    //선택된 indexPath 표시해주기!
    if ([_arrSelectedIndex containsObject:indexPath]) {
        gellarycell.imageView.layer.borderColor = [[UIColor systemBlueColor] CGColor];
        gellarycell.imageView.layer.borderWidth = 3.0;
    } else {
        gellarycell.imageView.layer.borderColor = nil;
        gellarycell.imageView.layer.borderWidth = 0.0;
    }
    
    return gellarycell;
}


//아이템 클릭
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
    
    NSLog(@"선택된 아이템 %ld - %ld" , (long)indexPath.section , (long)indexPath.row);
    
    //이미 인덱스 경로가 있으면
    if ([_arrSelectedIndex containsObject:indexPath]) {
        
        //삭제
        [_arrSelectedIndex removeObject:indexPath];
        
        
        NSString *inStr = [NSString stringWithFormat: @"%ld", (long)indexPath.item];
        [self.imageDictionary removeObjectForKey:inStr];
    
        
    }else{
        
        //등록
        [_arrSelectedIndex addObject:indexPath];
       
 
        PHAsset *asset = assetsFetchResults[indexPath.item];
        CGFloat scale = [UIScreen mainScreen].scale;
        CGSize targetSize =  CGSizeMake(CGRectGetWidth(self.view.bounds) * scale, CGRectGetHeight(self.view.bounds) * scale);
        
        [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:PHImageContentModeAspectFit options:nil resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
            
            if (result) {
                NSData *image =UIImageJPEGRepresentation(result, 1.0);
                
                if (image != nil) {
                    NSLog(@"image : image class %@" , [image class]);
                    NSLog(@"image : image %@" , image);
                    
                    NSString *inStr = [NSString stringWithFormat: @"%ld", (long)indexPath.item];
                    
                    //사진 딕셔너리에 저장
                    [self.imageDictionary setValue:image forKey:inStr];
                    
                    
                }

            }
            
        }];
        
    }
    
    [collectionView reloadData];
    
}


//cell 끼리 가로 간격
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section{

    return 5;
}

//cell 끼리 위아래 간격
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section{
    return 5;
}



//컬렉션 뷰 셀 크기
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{

    CGFloat width  = self.view.frame.size.width;
    
    // in case you you want the cell to be 40% of your controllers view
    return CGSizeMake((width -10) / 3 , (width -10) / 3);
}



/* 색션 개수 설정 */
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
    NSLog(@"색션 개수 설정");
    return 1;
}


//커스텀 헤더 설정
//viewDidLoad에서 Header Nib 파일 등록필요.
//컬렉션 뷰 인스펙터 - 엑세서리 - section header 선택 필요
-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
    
    NSLog(@"헤더 셋팅");
    
    if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
        
        Header *rView =
        [self.collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header" forIndexPath:indexPath];
        
        [rView setHeaderDelgate:self];
        
        return  rView;
    }
    
    return nil;
}


//컬렉션뷰 셀 라인 간격 참고!!!!
//https://stackoverflow.com/questions/28872001/uicollectionview-cell-spacing-based-on-device-screen-size

//cell 선택시 indexpath 배열에 담기 참고
//https://stackoverflow.com/questions/52757524/how-do-i-got-multiple-selections-in-uicollection-view-using-swift-4/52759682

//https://stackoverflow.com/questions/18857167/uicollectionview-cell-selection




#pragma HeaderAction delegate Method

- (void)closeView {
    NSLog(@"컬렉션 뷰 닫기");
    [self dismissViewControllerAnimated:NO completion:nil];
}




- (void)saveData {
    NSLog(@"컬렉션 뷰 사진 저장 ");
    
    NSLog(@"딕셔너리 개수 %lu" , (unsigned long)self.imageDictionary.count);
    
    //checkSaveFolder 함수 viewdidload에서 미리 호출하기 !
    
    for(id key in self.imageDictionary){
    
        [self saveDataWithImage:[self.imageDictionary objectForKey:key]];
    
    }

}


//이미지 클릭하면 바로 파일에 저장 + 하나씩 저장
-(void)saveDataWithImage:(NSData *) data{
    
    
        //NSDocumentationDirectory 아님!
        //NSDocumentDirectory (o)
    
        //다큐먼트 디렉토리 - NSDocumentationDirectory(X)
        NSString *documentDirectory = [NSSearchPathForDirectoriesInDomains(
                                             NSDocumentDirectory,
                                             NSUserDomainMask, YES
                                             ) objectAtIndex:0];
    
        //폴더명
        NSDate *todayDate = [NSDate date];
        NSDateFormatter *dateFomatter = [[NSDateFormatter alloc]init];
        dateFomatter.dateFormat = @"yyyyMMdd";
        NSString *today = [dateFomatter stringFromDate:todayDate];
        
        //파일명
        NSDateFormatter *dateFormatter2  = [[NSDateFormatter alloc]init];
        [dateFormatter2 setDateFormat:@"yyyyMMddmmss"];
        NSString *picdateName = [dateFormatter2 stringFromDate:[NSDate date]];
    
        //난수 생성
        int randomIndex = arc4random() % 1000;

        NSString *ramdom = [[NSString alloc]initWithFormat:@"%d" , randomIndex];
    
        //저장경로
        NSString *savePath = [documentDirectory stringByAppendingFormat:@"/Photos/%@/%@_%@.jpg", today,picdateName,ramdom];
        
        NSLog(@"savePath : %@" , savePath);
    
        //저장 -!
        [data writeToFile:savePath atomically:YES];
    
    
}

//저장하기전 폴더 확인 후 생성
-(void) checkSaveFolder{
    
     //다큐먼트 디렉토리
     NSString *documentDirectory = [NSSearchPathForDirectoriesInDomains(
                                          NSDocumentDirectory,
                                          NSUserDomainMask, YES
                                          ) objectAtIndex:0];
    
     NSLog(@"다큐먼트 경로 : %@" , documentDirectory);
     
     //하위폴더 이름 - yyyyMMdd
     NSDate *todayDate = [NSDate date];
     NSDateFormatter *dateFomatter = [[NSDateFormatter alloc]init];
     dateFomatter.dateFormat = @"yyyyMMdd";
     NSString *today = [dateFomatter stringFromDate:todayDate];
     
     //저장 경로
     NSString *savePath = [documentDirectory stringByAppendingFormat:@"/Photos/%@", today];
     
     
     //파일 매니저
     NSFileManager *fileManager = [[NSFileManager alloc]init];
     
     if ([fileManager isWritableFileAtPath:savePath]) {
         
         NSLog(@"이미 %@ 폴더는 존재합니다." , savePath);
         
     }else{
         
         //존재하지않을경우생성
         NSError *error = nil;
         if ([fileManager createDirectoryAtPath:savePath withIntermediateDirectories:YES attributes:nil error:&error]) {
             
             NSLog(@"포폴더 생성 성공");
             NSLog(@"폴더 : %@" , savePath);
         }else{
             
             NSLog(@"폴더생성실패");
             
         }
         
     }
    
}

@end

 

 

 

완성

 

 

 

photocollectionview.zip
0.09MB

 

photocollectionview.zip
0.06MB

참고

 

ios collectionview xib 파일 & customcell 예제

 

 

컬렉션뷰 셀 라인 간격 참고

https://stackoverflow.com/questions/28872001/uicollectionview-cell-spacing-based-on-device-screen-size

 

cell 선택시 indexpath 배열에 담기 참고

https://stackoverflow.com/questions/52757524/how-do-i-got-multiple-selections-in-uicollection-view-using-swift-4/52759682

 

https://stackoverflow.com/questions/18857167/uicollectionview-cell-selection