앱개발자가 혼자서 개발 할 경우에 고민되는 문제중 하나가 서버개발이다. 그런문제를 한방에 해결해 줄수 있는 서비스가 있는데 그게 바로  Parse라는 서비스이다.

https://parse.com/



물론 앱개발쪽에 관심이 있는 개발자라면 다 아는곳이겠지만, 노티피케이션 서버라던가 API서버, 클라우드등등.. 여러가지 기능을 쉽게 구현해준다.







제공해 주는 많은 기능중에 이번에 포스팅할 내용은 Core(데이타베이스)이다. Parse에 도큐먼트와 튜토리얼을 보면 너무나도 자세히 설명되어 있지만 간단히 소개를 하자면,



모델클래스(PFObject)의 인스턴스를 생성하는것 만으로도 테이블이 생성되며 그인스턴스를 save..라는 메소드를 이용하여 저장할 수 있다. (모델과 테이블에 영속화 되어 있는개념이다.)


게다가 사전(NSDictionary)처럼 사용하면 된다.

PFObject *campSite = [PFObject objectWithClassName:@"campSites"];

campSite[@"name"] = @"テストコード";

campSite[@"access"] = @"hogehoge";

campSite[@"address"] = @"fugafuga";

    

[campSite saveInBackground];



그리고 저장된 데이타를 검색하는 여러가지 메소드를 지원해 주는데 이것또한 직관적으로 알기 쉽게 되어 있다.


PFQuery *query1 = [PFQuery queryWithClassName:@"CampSites"];

[query1 whereKey:@"access" equalTo:@"Dan Stemkoski"];

    

    [query1 findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {

        if (!error) {

            // The find succeeded.

            NSLog(@"Successfully retrieved %d scores.", (int)objects.count);

            // Do something with the found objects

            for (PFObject *object in objects) {

                NSLog(@"name[%@]", object[@"name"]);

            }

        } else {

            // Log details of the failure

            NSLog(@"Error: %@ %@", error, [error userInfo]);

        }

    }];


위의 샘플소스는 도큐먼트에 나와있는 방법으로 코딩한 것이다.

그러나 한가지 불만스러운 점이 있었는데 문자열을 키로 사용한다는것이 뭔가 이펙티브하지 않다는 생각이 들었다. 뭔가 방법이 있을텐데.. PFObject를 상속받아 각각의 매핑된 모델클래스를 만들수 있을것 같다는 강력한 의구심이 생겼다.


어쨋든 Stackflow와 Parse블로그 등등 조금 둘러보니 역시나 이펙티브한 방법이 있었다.





먼저 테이블에 매핑될 모델클래스를 만들어준다.

클래스를 만들때에 PFSubclassing라는 프로토콜을 선언해 줘야 한다.


#import <Parse/Parse.h>


@interface CampSites : PFObject<PFSubclassing>


+ (NSString *)parseClassName;


@property (retain) NSString *name;


@end



#import "CampSites.h"

#import <Parse/PFObject+Subclass.h>


@implementation CampSites 


@dynamic name;


+ (NSString *)parseClassName {

    return @"CampSites";

}


@end



그다음 모델을 이용하여 테이블 생성 및 데이타 저장을 할 수 있다.

//effective!!

CampSites *obj = [CampSites object];

obj.name = @"effecitve!!!";


[obj saveInBackground];



검색한 결과도 모델로 넘어온다. 한번 해보자.


PFQuery *query1 = [PFQuery queryWithClassName:[CampSites parseClassName]];

    

[query1 findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {

    if (!error) {

        // The find succeeded.

        NSLog(@"Successfully retrieved %d scores.", (int)objects.count);

        // Do something with the found objects

        for (CampSites *object in objects) {

            NSLog(@"name[%@]", object.name);

        }

    } else {

        // Log details of the failure

        NSLog(@"Error: %@ %@", error, [error userInfo]);

    }

 }];



한가지 주의해야 할 점은 이 모델클래스를 사용하기전에 반드시 등록을 해줘야 한다는 점이다.

한번만 등록을 해주면 되므로 AppDelegate.m 에 코딩해 주자.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // Override point for customization after application launch.

     [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];

    

    //regist PFObject subClassing

    [CampSites registerSubclass];

    

    return YES;

}


근데 모델을 등록해주려면 AppDelegate.m에다가 모델을 써줘야 하고 임포트까지 해줘야 하는데 이러면 AppDelegate가 너무 지저분해지잖아! 참을수 없다.



각각의 모델클래스에서 사용되어지기전에 한번만 실행되도록 바꾸어 주자.

AppDelegate에 있는 등록소스를 각각의 모델클래스로 옮겨준다.

#import "CampSites.h"

#import <Parse/PFObject+Subclass.h>


@implementation CampSites 


@dynamic name;

// 모델클래스가 로드되면 클래스를 먼저 등록해준다. dispatch_once를 이용하여 한번만 등록되도록 한다.

+ (void)load {

    __weak typeof(self) weakSelf = self;

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        [weakSelf registerSubclass];

    });

}


+ (NSString *)parseClassName {

    return @"CampSites";

}


@end





확인해본결과 잘 동작하는것을 알수 있다.






더자세한 내용을 공부하려면 아래의 도큐먼트를 참조하면된다.


https://parse.com/docs/kr/ios_guide#objects




Posted by 악당잰 트랙백 0 : 댓글 0

댓글을 달아 주세요

앱 개발시 국제화 대응을 생각한다면 국가별언어처리와 국가별 시간처리를 미리 생각해 둘 필요가 있다.

특히 시간처리경우 서버 클라이언트 환경 혹은 다른 디바이스간의 통신환경일 경우 GMT를 계산해 넣지 않으면 데이타의 시간이 뒤죽박죽 되기 때문이다.

이번프로젝트에서도 이런 상황이 또 발생하여 대규모의 리팩토링을 했기에 이번에 확실히 정리해 두려고 한다.




이 이슈에 대하서 먼저 이해해야 할 두가지 단어가 있다. 


타임존(TimeZone)
말그대로 시간대 지역이다. 지구상의 모든국가는 나라,지역별로 시간대를 가지고 있는데 인간이 생활편의를 위한 시간개념이다. 예를들어 오전 10시면 해가 떠있는 아침이고 어둑어둑 해지면 오후 6시면 저녁이라는 시간개념이라고 해두자.

표준시(GMT)
지구상에서 기준이 되는 시간이다. 인간은 생활하는데 각자 편리하게 시간대를 정했지만 기준이 없다면 어느시간이 늦는지 빠른지 알수 없게 된다. 그래서 만든것이 표준시(GMT)이다.(일것이다.) 예를 들어 동경은 표준시보다 9시간 시차가 있으므로 GMT+09:00 의 형태로 표현 할 수 있다.

그럼 코딩에 세계에서는 어떻게 활용되는지 보자.

NSDate
NSDateはGMT에서 1970년1월1일 00:00:00부터 경과한 초를 날짜로 관리한다. 그러므로 타임존이 적용되지 않은 시간인것이다. 그러니까 당연히 날짜에 관련된 메소드도 없다. getYear, getMinute, getWeekday등등.. 간단히 NSDate는 GMT 시간이라고 알아두면 되겠다.


NSDate에서 타임존이 적용된 날짜를 얻는 방법은?
타임존이 적용된 날짜를 얻기 위해선 물론 현재 시간대(Timezone)의 달력이 달력이 있어야 한다. 

그것을 코드로 표현하면 

NSDate *date = [NSDate date];

NSCalendar *calendar = [NSCalendar currentCalendar];

NSUInteger flags = NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay;

NSDateComponents *comps =[calendar components:flags fromDate:date];

[comps setCalendar:calendar];

//    NSDate data1 = comp.date;



이렇게 얻은 데이타는 언어포멧에 맞추어 출력을 할 수도 있다.


NSString *dateTimeString = [NSString stringWithFormat:@"%d%d%d %d%d%d",

                                comps.year, comps.month, comps.day,

                                comps.hour, comps.minute, comps.second];




그러나 위의 방법 보다는  NSDateFormatter 로컬라이즈를 적용하여 문자열을 얻는 방법이 더 유용하다.


NSDate *date = [NSDate date];

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];

[dateFormatter setDateFormat:@"yyyy/M/d H:mm"];

NSString *dateString = [dateFormatter stringFromDate:date];

      

NSDate *date = [NSDate date];

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];

// 카렌다를 생략하면 언어설정에 의해 양력이외에 표기로 되는수도 있으므로 꼭 설정해 주자. 

// 예를들어 일본의 경우 연호를 사용한 일본달력이 되는 수도 있다.


dateFormatter.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];

dateFormatter.dateStyle = NSDateFormatterLongStyle;

dateFormatter.timeStyle = NSDateFormatterShortStyle;

NSString *dateString = [dateFormatter stringFromDate:date];



덧붙여 블루투스 장비의 경우는 Unix시간을 쓰는 경우가 많은데 이경우에는 아래와 같은 코드로 변환해 주면된다. - Unix시간도 GMT기준이다.


//NSDate → UNIX시간

NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970];

NSLog(@"timestamp: %f", timestamp);

    

//UNIX시간 → NSDate

NSTimeInterval interval = [timestamp doubleValue] / 1000; // ms -> sec

NSDate* expiresDate = [NSDate dateWithTimeIntervalSince1970:interval];

NSLog(@"expiresDate: %@", expiresDate);

    

//NSDate → NSString

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];

[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"];

NSString *dateString = [dateFormatter stringFromDate:expiresDate];

NSLog(@"dateString: %@", dateString);





이번프로젝트의 경우는 CoreData에 저장하는 데이타는 모조리 UnixTime(UInt64)으로 변환하여 저장하고  표시할 경우에만 타임존을 적용하였다. 

모두 유틸클래스로 공통화하는 리팩토링도 같이했는데 다수의 개발자가 작업하는 플젝에서는 엉뚱한 짓을 못하게 하는것도 중요한것 같다.


Posted by 악당잰 트랙백 0 : 댓글 0

댓글을 달아 주세요

Assets Catalog를 사용하고 있는 앱안에서 아이콘 이미지(AppIcon라고 되어 있는 놈)를 사용하고 싶을 경우엔 아래와같이 해주면 될줄 알았다..


UIImage *appIcon = [UIImage imageNamed"AppIcon"];


그러나 값을 확인하니 정확하게 nil을 반환하는것이 아닌가..




스택플로우 검색결과 이미지사이즈를 포함한 네이밍 문자열을 넘겨줘야 한다는 사실을 알았다.

요렇게..


UIImage *appIcon = [UIImage imageNamed:@"AppIcon40x40"];



참고한곳.

http://stackoverflow.com/questions/22808416/how-to-get-uiimage-of-appicon




나만몰랐나?

Posted by 악당잰 트랙백 0 : 댓글 1

댓글을 달아 주세요

  1. addr | edit/del | reply BlogIcon 차동수 2015.02.03 09:47

    저도 몰랐어요!

작년에 만든 앱(Hakenman)에 기능 몇개와 버그를 대응한 후 새해첫날에 업데이트 레뷰를 제출했었다.

그러나 생각치도 않던 리젝(메타데이터)이 되어 이유를 읽어보니 사용방법을 동영상으로 보내라는 내용이었다. (게다가 요구하는 동영상화면은 예전부터 있던 기능이었다능...)





정말 기능 설명도 필요없는 간단한 앱인데 말이지... 진정한 갑질은 애플이 아닐까..

어쩔수 없이 동영상을 만들어야 했는데 기왕 만드는거 앱프리뷰 동영상을 만들어 버리기로 했다.

일단 작업전에 앱프리뷰에 대해 사전조사를 한 결과 아래와 같은 정보를 얻을 수 있었다.


App Previews로컬라이즈 안됨. (하나 등록하면 전부 적용되버림)
그러나 표시할지 말지 국가별로 제한이 가능함.
3.5인치 이외에 디바이스 별로 사이즈 맞추어서 동영상을 올려야함. 사이즈가 맞지 않으면 에러남.

동영상은 화면캡춰영상 이어야 함. (사람손이 보이거나하면 안됨.)
음악, 자막, 나레이션 등은 OK.
동영상은 30초 이내, 그리고 30fps.


참고링크.

http://www.apptamin.com/blog/app-previews/








이제 위에 조건이 맞게 동영상을 만들어야 하는데 상당히 까다롭다. 다행히 저 귀찮은 작업들을 어느정도 해소해 줄 몇가지 툴을 찾을 수 있었는데 그중에서도 전혀 디자인초보인 개발자가 간단히 작업할 수 있는 AppShow라는 툴을 찾았다.


AppShow링크.

http://appshow.techsmith.com


물론 무료인데다가 인터페이스가 직관적이다. 게다가 위의 애플에서 요구하는 귀찮은 조건들을 한방에 클리어 할 수 있는 툴이다.








어쨋든 이걸 이용하여 만들어 보았다. 만져본 소감으로는 상당히 다루기 쉬웠고 Movie정도를 다룰수준이면 메뉴얼도 필요없을 수준이다.

조금 아쉬운점은 각각의 동영상 씬추가는 간단하지만 동영상에 텍스트를 넣거나 편집하는 기능은 없었다.

이 문제를 궁여지책으로 키노트(Keynote)로 슬라이드를 만든 다음 그걸 이미지로 출력해서 동영상에 삽입하는 꼼수를 썻다. 


요렇게..








게다가 만든 동영상은 디바이스별로 사이즈를 조정하여 출력이 가능하기에 전부 iTuens Connect에 무사히 등록까지 마쳤다. 

어쨋든 까칠한 애플덕분에 3시간정도 시간을 소비하기는 했지만 쓸모있는 경험을 할 수 있었다.








마지막으로 Hakenman Priview 영상은 유투브에 업로드 링크.









Posted by 악당잰 트랙백 0 : 댓글 0

댓글을 달아 주세요


제목그대로 네비게이션바에다가 커스텀뷰를 붙이는 방법에는 두가지가 있다.


1. 네비게이션바에서 제공하는 titleView 메소드를 이용하는 방법


UIView *customView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 120.f, 44.f)];

UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(10.f, 10.f, 100.f, 24.f)];

lbl.backgroundColor = [UIColor whiteColor];

lbl.text = @"custom label~";

lbl.font = [UIFont systemFontOfSize:12.f];

customView.backgroundColor = [UIColor blueColor];

[customView addSubview:lbl];


[self.navigationItem setTitleView:customView];


2. 네비게이션바에다가 직접  addSubview로 붙이는 방법


self.customButton = [UIButton buttonWithType:UIButtonTypeSystem];

    [_customButton setTitle:@"Click!" forState:UIControlStateNormal];

    _customButton.frame = CGRectMake(10.f, 10.f, 50.f, 24.f);

    [_customButton addTarget:self action:@selector(clicked:) forControlEvents:UIControlEventTouchUpInside];

    [self.navigationController.navigationBar addSubview:_customButton];

    

    self.iconImageView = [[UIImageView alloc] initWithFrame:CGRectMake(60.f, 0.f, 40.f, 40.f)];

    _iconImageView.image = [UIImage imageNamed:@"hanjimin"];

    [self.navigationController.navigationBar addSubview:_iconImageView];

    

    

    self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(_iconImageView.frame.origin.x + 40.f,

                                                                10.f, 100.f, 24.f)];

    _titleLabel.text = @"jimin jjang!!";

    [_titleLabel sizeToFit];

    _titleLabel.textColor = [UIColor whiteColor];

    _titleLabel.backgroundColor = [UIColor orangeColor];

    [self.navigationController.navigationBar addSubview:_titleLabel];



위의 두가지 방법 다 유효하다. 다만 붙이는 커스텀된 뷰가 레이아웃이 가변적일때(유저조작에 따라 변경되야 할 경우)는 고민을 해 봐야 한다.


이 문제로 3시간 삽질을 했는데 삽질결과 알아낸 사실은..


- 1.번의 방법으로는 레이아웃 변경자체가 안되고 그냥 가운데로 고정이 되버림.


- 2.번의 방법으로 레이아웃은 조정 가능하지만 만약 커스텀뷰 안에 하위 뷰가 존재할 경우 하위 뷰는 레이아웃 변경이 안된다.(오토레이아웃, 오토리사이즈, 코드상 변경 모두 불가)

 네비게이션에 직접 붙인 뷰(하위1단계레벨)만이 레이아웃 조정가능했다.


버튼이벤트에 레이아웃을 움직여보면.. 잘움직인다.


- (void)clicked:(id)sender {

    

    //update layout!

    _iconImageView.frame = CGRectMake(arc4random()%200 + 60.f,

                                      _iconImageView.frame.origin.y,

                                      _iconImageView.frame.size.width,

                                      _iconImageView.frame.size.height);

    

    _titleLabel.frame = CGRectMake(_iconImageView.frame.origin.x + 40.f,

                                   _titleLabel.frame.origin.y,

                                  _titleLabel.frame.size.width,

                                  _titleLabel.frame.size.height);

    

}





결론은..


네비게이션뷰에다가 복수개의 뷰를 붙일경우엔 편의상 컨테이너뷰를 만들고 뷰 하나만 붙이면 된다고 생각하지만 그렇게 되면 레이아웃 조정이 불가능해지므로 따라서 각각 하나하나 붙여야 한다.


예를들어.. 내가 삽질을 하게 된 이유이지만, 네비게이션뷰에 드롭다운 메뉴를 만들고 드롭다운 메뉴에서 선택된 항목으로 인하여 라벨크기가 가변적으로 변해야 한다면 라벨, 버튼, 이미지 각각 따로 붙여야 한다.








Posted by 악당잰 트랙백 0 : 댓글 0

댓글을 달아 주세요