그리드 뷰 새로고침하기

이제 ViewController.m 에서, 새로고침 IBAction 메소드를 구현을 해야 한다.

우선, 새로고침을 위한 다른 HUD를 만든다. 그래서 이전에 업로딩을 위해서 사용한 다른 HUD를 방해하지 않는다.

refreshHUD = [[MBProgressHUD alloc] initWithView:self.view];
[self.view addSubview:refreshHUD];
     
// HUD 콜백을 등록한다. 그래서 알맞은 시간에 윈도우로 부터 제거할 수 있다.
refreshHUD.delegate = self;
     
// 새로운 스레드에서 메소드를 실행하는 동안 HUD를 보여준다.
[refreshHUD show:YES];

다운로드 쿼리 생성하기

대량의 코드가 있다. 한 단계, 한 단계씩 진행하겠다. 

이미지를 다운로드하려면 PFQuery 를 사용 해야한다. 쿼리는 Parse에서 검색 범위를 좁힐 수 있는 조건으로 객체를 찾을 때 사용된다. 

업로드 한 모든 이미지를 추출하기 위해서  이전에 설정한 클래스 이름에 해당하는 PFQuery 를 만들어야 한다. 또한, 검색 결과를 현재 속해 있는 사용자로 제한해야한다. 

쿼리가 생성되면, 결과를 findObjectsInBackgroundWithBlock: 메소드로 검색할 수 있다. 

- (void)downloadAllImages
{
    PFQuery *query = [PFQuery queryWithClassName:@"UserPhoto"];
    PFUser *user = [PFUser currentUser];
    [query whereKey:@"user" equalTo:user];
    [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // 만약 사진이 있으면 데이터를 추출한다.
    // 데이터를 추출하는 동안, 객체 ID의 목록을 저장한다.
         
    NSMutableArray *newObjectIDArray = [NSMutableArray array];
    NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
         
    if (objects.count > 0) {
        for (PFObject *eachObject in objects) {
            [newObjectIDArray addObject:[eachObject objectId]];
        }
    }

다운로드를 빠르게 하기위해, NSUserDefaults에서 각 사진의 objectID 를 저장할 것이다. 쿼리 결과가 반환 될 때, 새로운 objectID 가 있는 사진만 다운로드 한다. 

누락된 objectID는 photoScrollView 에서 그것들을 삭제 할 것이다. 데이터베이스에서 항목이 제거되는 경우 발생한다. 예를 들어 Parse의 웹 인터페이스에서 제거 될 때. 삭제하기 위해, 각각 버튼(나중에 설정방법을 보여준다.)의 태그를 확인한다.

// 지난 객체 ID와 새로운 객체 ID를 비교한다.
NSMutableArray *newCompareObjectIDArray = [NSMutableArray arrayWithArray:newObjectIDArray];
NSMutableArray *newCompareObjectIDArray2 = [NSMutableArray arrayWithArray:newObjectIDArray];
NSMutableArray *oldCompareObjectIDArray = [NSMutableArray arrayWithArray:[standardUserDefaults objectForKey:@"objectIDArray"]];
if ([standardUserDefaults objectForKey:@"objectIDArray"]){
    [newCompareObjectIDArray removeObjectsInArray:oldCompareObjectIDArray]; // New objects
             
    // 웹 브라우저를 사용하여 지난 객체를 삭제하는 경우 지난 객체를 삭제한다.
    [oldCompareObjectIDArray removeObjectsInArray:newCompareObjectIDArray2];
    if (oldCompareObjectIDArray.count > 0){
        // objectID 배열에서 위치를 확인하고 삭제한다.
        NSMutableArray *listOfToRemove = [[NSMutableArray alloc] init];
        for (NSString *objectID in oldCompareObjectIDArray){
            int i = 0;
            for (NSString *oldObjectID in [standardUserDefaults objectForKey:@"objectIDArray"]){
                if ([objectID isEqualToString:oldObjectID]){
                    // 삭제한다.
                    for (UIView *view in [photoScrollView subviews]) {
                        if ([view isKindOfClass:[UIButton class]]) {
                            if (view.tag == i){
                                [view removeFromSuperview];
                                NSLog(@"Removing picture at position %i",i);
                            }
                        }
                    }
                             
                    // 삭제하고 싶은 모든 목록을 만들고 마지막에 삭제한다.
                    [listOfToRemove addObject:[NSNumber numberWithInt:i]];
                }
                i++;
            }
        }

이미지가 지나치지 않도록 뒤에서 부터 다시 제거한다.

// 뒤에서 부터 제거
NSSortDescriptor *highestToLowest = [NSSortDescriptor sortDescriptorWithKey:@"self" ascending:NO];
[listOfToRemove sortUsingDescriptors:[NSArray arrayWithObject:highestToLowest]];
                 
for (NSNumber *index in listOfToRemove){                       
    [allImages removeObjectAtIndex:[index intValue]];
    [self setUpImages:allImages];    
}

누락된 객체 제거 외에, 또한 새로운 객체를 추가해야한다. 거꾸로 변환하는 것은 쉽다. 결과에서 각각 PFObject 를 통하여 NSData 로 변환하게 getData 를 호출한다. 다음 로드에 다시 로드 할 수 있도록 NSUserDefautls에 objectIDArrary를 저장하는 것을 잊지마라. 

// 이 메소드는 그리드에서 다운로드된 이미지를 설정하고 배치시킨다.
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSMutableArray *imageDataArray = [NSMutableArray array];
         
        // 모든 이미지를 반복해서 PFFile에서 데이터를 얻는다.
        for (int i = 0; i < images.count; i++) {
            PFObject *eachObject = [images objectAtIndex:i];
            PFFile *theImage = [eachObject objectForKey:@"imageFile"];
            NSData *imageData = [theImage getData];
            UIImage *image = [UIImage imageWithData:imageData];
            [imageDataArray addObject:image];
        }
                    
        // UI 업데이트를 위해 메인 쓰레드에 디스페치.
        dispatch_async(dispatch_get_main_queue(), ^{
            // 지난 그리드 삭제
            for (UIView *view in [photoScrollView subviews]) {
                if ([view isKindOfClass:[UIButton class]]) {
                    [view removeFromSuperview];
                }
            }
             
            // 그리드에서 각 이미지에 필요한 버튼 만들기
            for (int i = 0; i < [imageDataArray count]; i++) {
                PFObject *eachObject = [images objectAtIndex:i];
                UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
                UIImage *image = [imageDataArray objectAtIndex:i];
                [button setImage:image forState:UIControlStateNormal];
                button.showsTouchWhenHighlighted = YES;
                [button addTarget:self action:@selector(buttonTouched:) forControlEvents:UIControlEventTouchUpInside];
                button.tag = i;
                button.frame = CGRectMake(THUMBNAIL_WIDTH * (i % THUMBNAIL_COLS) + PADDING * (i % THUMBNAIL_COLS) + PADDING,
                                          THUMBNAIL_HEIGHT * (i / THUMBNAIL_COLS) + PADDING * (i / THUMBNAIL_COLS) + PADDING + PADDING_TOP,
                                          THUMBNAIL_WIDTH,
                                          THUMBNAIL_HEIGHT);
                button.imageView.contentMode = UIViewContentModeScaleAspectFill;
                [button setTitle:[eachObject objectId] forState:UIControlStateReserved];
                [photoScrollView addSubview:button];
            }
             
            // 그리드에 따른 크기
            int rows = images.count / THUMBNAIL_COLS;
            if (((float)images.count / THUMBNAIL_COLS) - rows != 0) {
                rows++;
            }
            int height = THUMBNAIL_HEIGHT * rows + PADDING * rows + PADDING + PADDING_TOP;
             
            photoScrollView.contentSize = CGSizeMake(self.view.frame.size.width, height);
            photoScrollView.clipsToBounds = YES;
        });
    });

이번 상황에서는 Grand Central Dispatch를 사용하여 getData 를 처리 할 것이다. 이렇게하려면 디스페치 큐를 생성하고 비동기적으로 호출을 수행한다. 완료되면, 전체 photoArray 에 추가한다. 그리고 메인 쓰레드에서 이미지를 깔끔하게 정리하기 위해 setUpImages 를 호출한다. 


그리드 설정하기

setUpImages 메소드에서 이미지가 저장되는 것을 보장하기 위해 이미지를 allImages로 복사한다. 그 후에, 모든 현재 버튼을 삭제하고 새로운 것을 배치시킨다. 


allImages = [images mutableCopy];
     
// 지난 그리드 삭제
for (UIView *view in [photoScrollView subviews]) {
    if ([view isKindOfClass:[UIButton class]]) {
        [view removeFromSuperview];
    }
}

각각의 이미지를 위해서 UIButton 을 만들어서 그리드에 배치시킨다. 나중에 UIButton 을 탭을 했을 때 참조하기 위해 각각의 UIButton 에 태그한다. 또한, 각각의 UIButton 에 이미지를 보여주는 디테일 뷰 컨트롤러를 여는 타겟 메소드를 연결한다.

마지막으로 photoScrollView 에 알맞은 contentSize 를 계산하고 clipsToBounds 에 YES로 설정한다. 그리드에 올바르게 설정한다. 이미지 사이의 패딩값은 4px이다. 썸네일 크기는 75px * 75px이다. 그리고 4개의 컬럼으로 되어있다.

// 이 메소드는 다운로드된 이미지를 설정하고 그것들을 그리드에 알맞게 배치시킨다
UIButton *button;
UIImage *thumbnail;
 
//각각 이미지를 위해 버튼을 생성
for (int i=0; i<images.count; i++) {
    thumbnail = [images objectAtIndex:i];      
    button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setImage:thumbnail forState:UIControlStateNormal];
    button.showsTouchWhenHighlighted = YES;
    [button addTarget:self action:@selector(buttonTouched:) forControlEvents:UIControlEventTouchUpInside];
    button.tag = i;
    button.frame = CGRectMake(THUMBNAIL_WIDTH * (i % THUMBNAIL_COLS) + PADDING * (i % THUMBNAIL_COLS) + PADDING, THUMBNAIL_HEIGHT * (i / THUMBNAIL_COLS) + PADDING * (i / THUMBNAIL_COLS) + PADDING + PADDING_TOP, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
    button.imageView.contentMode = UIViewContentModeScaleAspectFill;
    [photoScrollView addSubview:button];
}
     
int rows = images.count / THUMBNAIL_COLS;
if (((float)images.count / THUMBNAIL_COLS) - rows != 0) {
    rows++;
}
 
int height = THUMBNAIL_HEIGHT * rows + PADDING * rows + PADDING + PADDING_TOP;
     
photoScrollView.contentSize = CGSizeMake(self.view.frame.size.width, height);
photoScrollView.clipsToBounds = YES;

디테일 뷰 컨트롤러 열기

별도의 모달 뷰 컨트롤러에 자세한 사진을 열 수 있다. 여기 할수 있는 방법이 있다.

우선 새로운 UIViewController (File > New > New File > UIViewController 서브클래스)를 PhotoDetailViewController 으로 생성한다. 완료되면, nib을 설정하고 ViewController.m 의 상단에 “PhotoDetailViewController.h”를 import 한다.

UINavigationBarUIBarButtonItem 그리고 자세한 이미지를 보여주는 UIImageView 을 포함하는 nib는 다음과 비슷할 것이다.


UIButton에 해당하는 태그가 있기 때문에, 이전에 저장한 allImages 배열에서 정확한 사진을 얻을 수 있다. 이 이미지로 새로운 디테일 뷰 컨트롤러에 전달하고 컨트롤러를 제시한다. 

- (void)buttonTouched:(id)sender{
    //사진이 터치되었을 때, 이미지를 가지는 뷰 컨트롤러를 연다.
    UIImage *selectedPhoto = [allImages objectAtIndex:[sender tag]];
     
    PhotoDetailViewController *pdvc = [[PhotoDetailViewController alloc] init];
     
    pdvc.selectedImage = selectedPhoto;
    [self presentModalViewController:pdvc animated:YES];
}

이 튜토리얼에서 사진을 PFObjects 로 파일을 랩핑하여 클라우드로 업로드하는 것, PFQueries 를 사용해서 사진을 다운로드 하는 것, 그리드에 사진을 배치하는 것을 다루었다. 또한, 새로운 이미지를 다운로드하고 누락된 것을 제거하는 간단한 캐싱을 했다. 뿐만 아니라 디테일 뷰 컨트롤러 생성도 하였다. 

저작자 표시 비영리
신고
Posted by KraZYeom

원문 : Saving Images


사용자들이 사진을 찍고 Parse에 직접 사진을 올리게 해주는 앱을 만드는 방법을 배운다.

이 튜토리얼의 소스코드 다운로드 링크: .zip | GitHub

사진 앱은 강력하다. 자신의 사진앱을 만들기를 원한다면, Parse를 사용하라. Parse는 파일 저장하기, 파일 찾기 그리고 사용자와 연결하기를 포함한 모든 백-엔드를 다룰 수 있다.

이 튜토리얼에서는 Parse 플랫폼에 여러분의 사진을 서버 사이드 코딩 걱정없이 저장할 수 있는 방법을 보여줄 것이다.

완료된 앱은 아래 이미지와 비슷하게 보일 것이다. 우선 더 나은 아이디어를 얻기 위해 샘플 데모앱을 확인하고 구동해보는 것을 추천한다.




설정하기

view-based 프로젝트로 시작할 것이다. 우선 ViewController.xib에 UINavigationBar과 두 개의 UIBarButtonItem(카메라/새로고침)을 추가한다. 그리고 UIScrollView을 가운데로 배치시킨다. 최종 nib파일의 모습은 다음과 비슷할 것이다.



마쳤으면, IBOutlet을 생성하고 UIScrollView와 연결시킨다. 뿐만아니라 두 개의 IBAction 메소드를 각각 카메라 버튼과 새로고침 버튼에 연결시킨다.

IBOutlets과 IBActions이 올바르게 연결되었는지 확인하라. File's Owner를 클릭하고 Connections 인스팩터를 열어서 확인할 수 있다.(스크린샷 참조)

또한, 올바른 프레임워크를 가져오기를(import)해야 한다. 필수 프레임워크: Parse.framework,AudioToolbox.frameworkCFNetwork.frameworklibz1.1.3.dylibMobileCoreServices.framework,QuartzCore.frameworkSecurity.frameworkSystemConfiguration.frameworkUIKit.frameworkFoundation.frameworkCoreGraphics.framework.



마지막으로, 액티비티 인디게이터 또는 스피너를 나타내기 위해 MBProgressHUD  를 사용할 것이다. MBProgressHUD를 다운로드한다. 그리고 Xcode 프로젝트에 MBProgressHUD.h와 MBProgressHUD.m를 추가한다.

사용자 생성하기

클라우드에 올라간 각각의 파일은 PFUser와 연결될 수 있다. 이 튜토리얼에서는 가입을 위해서 거짓 username과 password를 사용할 것이다. 여기 https://www.parse.com/docs/ios_guide#users 에서 PFUsers에 관해 더 배울수 있다.

앱 델리게이트에서 처음에 아래 코드를 추가한다.

PFUser *currentUser = [PFUser currentUser];
    if (currentUser) {
        [self refresh:nil];
    } else {
        // 거짓 username 와 password
        PFUser *user = [PFUser user];
        user.username = @"Matt";
        user.password = @"password";
        user.email = @"Matt@example.com";
         
        [user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
            if (!error) {
                [self refresh:nil];
            } else {
                [PFUser logInWithUsername:@"Matt" password:@"password"];
                [self refresh:nil];
            }
        }];
    }
}

이것은 시스템에 사용자가 현재 로그인 되어 있는지 확인한다. 이전에 가입했거나 로그인 되어 있으면 미리 저장되거나 캐쉬된 사용자를 사용한다. 만약 디스크에 캐쉬된 사용자가 없으면, 하나 생성한다.

다시말하면 두 가지의 시나리오에 대한 설명이 필요하다: username이 있을 경우, 로그인을 하면 된다.

헤더 파일 설정하기

구현하기 전에, 헤더 파일을 설정해야한다. 처음으로 메소드를 참조 가능하게 하기위해 ViewController.h 에 <Parse/Parse.h> 를 임포트한다. MBProgressHUD.h 는 엑티비디 인디게이터를 위해 그리고 stdlib.h 는 램덤생성기를 위해서 임포트한다. 또한 델리게이트 메소드를 사용하기 위해서 UINavigationControllerDelegateUIImagePickerControllerDelegate 그리고 MBProgressHUDDelegate 프로토콜을 적용한다. UINavigationControllerDelegate 는 UIImagePickerController 가 UINavigationController 의 서브클래스이기 때문에 추가한다.

다음으로, 인스턴스 변수 photoScrollViewallImagesHUD, 그리고 refreshHUD 추가한다. photoScrollView는 사진을 보여주는 스크롤 뷰를 참조한다. allImages 는 사진을 담고 있는 배열을 참조한다. 그리고 나머지 2개는 곧 사용할 HUDs를 참조한다. 

마지막으로, 오늘 사용할 5개 메소드를 추가한다. 

//ViewController.h
 
#import <UIKit/UIKit.h>
#import <Parse/Parse.h>
#import "MBProgressHUD.h"
#include <stdlib.h>
 
@interface ViewController : UIViewController <UINavigationControllerDelegate, UIImagePickerControllerDelegate, MBProgressHUDDelegate>
{
    IBOutlet UIScrollView *photoScrollView;
    NSMutableArray *allImages;
     
    MBProgressHUD *HUD;
    MBProgressHUD *refreshHUD;
}
 
- (IBAction)refresh:(id)sender;
- (IBAction)cameraButtonTapped:(id)sender;
- (void)uploadImage:(NSData *)imageData;
- (void)setUpImages:(NSArray *)images;
- (void)buttonTouched:(id)sender;

HUD 델리게이트와 Image 사이즈 재조정 메소드 구현하기

HUD 델리게이트 메소드 구현을 해야한다.

- (void)hudWasHidden:(MBProgressHUD *)hud {
    // 숨겨질때 HUD를 화면에서 삭제하기
    [HUD removeFromSuperview];
    HUD = nil;
}

올바른 시간에 메인 뷰에서 HUD를 삭제한다. 

UIImagePickerController 사용하기

다바이스에서 사진을 업로드 할 수 있어야 한다. 만약 카메라를 사용할 수 있으면 사진을 찍고 업로드 한다. 그렇지 않으면, 디바이스 또는 시뮬레이터에 있는 이미지 파일을 올릴 것이다. 5개의 자유롭게 사용할 수 있는 이미지가 있다.

첫 번째 스텝은 cameraButtonTapped 소드에서 UIImagePickerController 를 생성하는 것이다. 우선 카메라 소스가 사용가능한지 검증부터 해야한다. 그리고 UIImagePickerController 객체를 생성한다. 소스 타입과 델리게이트를 설정해야한다.

if ([UIImagePickerController isSourceTypeAvailable:
UIImagePickerControllerSourceTypeCamera] == YES){
        // 이미지 피커 컨트롤러 생성
        UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
         
        // 카메라 소스 설정
        imagePicker.sourceType =  UIImagePickerControllerSourceTypeCamera;
         
        // Delegate is self
        imagePicker.delegate = self;
         
        // 이미지 피커 보여주기
        [self presentModalViewController:imagePicker animated:YES];
    }

카메라 미지원 디바이스를 다루기 전에 이미지 피커 델리게이트 메소드를 마쳐야 한다. 반환되는 JPEG를 크기를 재조정하고 압축한다. 그리고 uploadImage 메소드를 호출하고 imageData 를 전달한다.


- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    // 인포 딕셔너리(info Dictionary)에서 크롭되지 않은 이미지에 접근한다.
    UIImage *image = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
     
    // 컨트롤러를 닫는다.
    [picker dismissModalViewControllerAnimated:YES];
     
    // 이미지 크기 재조정
    UIGraphicsBeginImageContext(CGSizeMake(640, 960));
    [image drawInRect: CGRectMake(0, 0, 640, 960)];
    UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();  
 
    // 이미지 업로드
    NSData *imageData = UIImageJPEGRepresentation(image, 0.05f