원문 : Todo App with JavaScript 


사용자 계정 및 데잍를 유지하기 위하여 백엔드로 Parse를 사용하여 정통적인 백본 todo 애플리케이션만드는 방법을 배워보자. 

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

자바스크립트 SDK를 사용하여 우리는 디바이스간에 사용자 인증 및 데이터 지속성으로 정통적인 백본 todo 애플리케이션을 확장할 수 있다. 우리의 SDK는 Backbone을 기반으로하기 때문에 Parse를 사용하여 이 애플리케이션을 확장하는 것은 쉬운 일이다. 

이 튜토리얼은 todo 애플리케이션 코드를 통해서 여러분을 안내 할 것이다. 여러분은 여기에서 실제 애플리케이션을 가지고 놀수 있다. 전체 애플리케이션은 간단한 정적인 파일로 구성되어 있다. 자바스크림트 앱에 대한 백엔드를 Parse가 처리해준다. 

종속관계

앱의 필수 요소:

  • Underscore
  • jQuery

Parse 자바스크립트 SDK는 Backbone을 포크(fork)해서 만들어졌고 다른 자바스크립트 라이브러리는 필요하지 않기 때문에 Backbone을 필요로하지 않는다. 여기서는 우리는   이 앱의 특정 요소를 위해서 jQuery와 Underscore를 사용한다. 그러나 원하는 다른 라이브러리를 선택할 수도 있다.

템플릿

우리는 모든 뷰를 위해서 Underscore 템플릿을 사용한다. index.html의 스크립트 블록에 직접 위치하고 있다. 예를 들면, 이 탬플릿은 하나의 todo 아이템을 위한 것이다. 

<script type="text/template"  id="item-template">
  <li class="<%= done ? 'completed' : '' %>">
    <div class="view">
      <input class="toggle" type="checkbox" <%= done ? 'checked="checked"' : '' %>>
      <label class="todo-content"><%= content %></label>
      <button class="todo-destroy"></button>
    </div>
    <input class="edit" value="<%= content %>">
  </li>
</script>

사용자 인증

우리는 전형적인 todo앱에 로그인을 허용하고 todo 아이템을 여러 디바이스에서 관리 할 수 있게 하는 사용자 인증을 확장했다. 다행이도 이 기능을 Parse를 통해서 충가하는 것은 쉽다. 

처음으로, 2개의 서브 뷰(LoginView와 ManageTodosView)를 관리하는 매인 뷰를 AppView 만든다. 기본적으로, 일반적인 todo 앱의 앞에 로그인 화면을 추가한다. 만약에 사용자가 로그인되어 있다면 어떤 뷰를 보여줄지 결정하는 것을 간단하게 테스트한다. 

if (Parse.User.current()) {
  new ManageTodosView();
} else {
  new LogInView();
}

로그인 뷰는 2개의 폼을 보여준다. 하나는 로그인이고 또 다른 하나는 새로운 계정을 위한 가입이다. 

<form class="login-form">
  <h2>Log In</h2>
  <div class="error" style="display:none"></div>
  <input type="text" id="login-username" placeholder="Username" />
  <input type="password" id="login-password" placeholder="Password" />
  <button>Log In</button>
</form>
<form class="signup-form">
  <h2>Sign Up</h2>
  <div class="error" style="display:none"></div>
  <input type="text" id="signup-username" placeholder="Username" />
  <input type="password" id="signup-password" placeholder="Create a Password" />
  <button>Sign Up</button>
</form>

뷰는 각각 유저의 로그인과 가입 폼의 서브밋 이벤트에 바인딩된다.  로그인 코드는 다음과 같다. 

Parse.User.logIn(username, password, {
  success: function(user) {
    new ManageTodosView();
    self.undelegateEvents();
    delete self;
  },
  error: function(user, error) {
    self.$(".login-form .error").html("Invalid username or password. Please try again.").show();
    this.$(".login-form button").removeAttr("disabled");
  }
});

일단 유저가 로그인 되면, Parse SDK는 자동적으로 세션을 유지하는 처리를 한다. 그래서 Parse.User.current()는 항상 로그인 된 사용자의 인스턴스가 될 것이다. 로그인 후에는 즉시 ManageTodosView를 보여준다.

가입 코드는 다음과 비슷하다. 

Parse.User.signUp(username, password, { ACL: new Parse.ACL() }, {
  success: function(user) {
    new ManageTodosView();
    self.undelegateEvents();
    delete self;
  },
  error: function(user, error) {
    self.$(".signup-form .error").html(error.message).show();
    this.$(".signup-form button").removeAttr("disabled");
  }
});

필드로부터 가져온 사용자이름과 비밀번호으로 사용자 계정을 생성한다. 또한 사용자에게 빈 ACL을 적용한다. 이작업은 만약 로그인 한 사용자가 아닌경우 User 클래스로 부터 데이터를 읽는 것을 방지한다. 

두 개의 뷰에서, 콜백시 div에 오류 메세지를 표시하여 오류를 처리한다. 

Todo 아이템 유지하기

애플리케이션의 대부분은 원조 Backbone.js todo 앱과 매우 유사하다. Backbone 모델과 콜렉션을 사용하여 todo 아이템을 표시하는 방법의 의미를 얻기 위해서 원조 주석이 달린 소스코드 살펴 볼 수 있다. 

적용할 주요 변경사항은 Backbone 클래스 대신에 Parse 클래스로 교체하는 것이. 다음과 같다. 

var Todo = Parse.Object.extend("Todo", {
  // ...
});
var TodoList = Parse.Collection.extend({
  // ...
});

이것은 자동적으로 Todo 모델의 자신을 원조 앱의 로컬 스토리지 대신에 Parse 플랫폼에 유지하는 것을 만들어 준다. 여기서 기존 Backbone 앱을 Parse로 변환하는 것에 대해서 더 살펴 볼 수 있다.

여기, 사용자가 소유하고 있는 모든 todo 아이템을 얻고 TodoList인스턴스를 위한 query로 설정하기 위해서 Parse.Query를 구축한다. 

// Todo의 콜랙션 생성
this.todos = new TodoList;
 
// 현재 사용자에서 todo을 살펴보기 위해 콜랙션에 대한 쿼리 설정
this.todos.query = new Parse.Query(Todo);
this.todos.query.equalTo("user", Parse.User.current());
 
// ...
 
// Parse로 부터 이 유저에 대한 모든 todo 아이템 가져오기
this.todos.fetch();

이것이 전부이다. 이제 todo 아이템을 저장, 삭제 또는 생성할 수 있다. 이 값들의 상태는 Parse에 지속된다. 

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

그리드 뷰 새로고침하기

이제 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{