Sep 07

다음과 같은 페이지의 string을 파싱해서 처리하기는 번거로운 일일 것이다.

{
    "firstName": "John",
    "lastName": "Smith",
    "한글이름" : "존 스미스",
    "address": {
        "streetAddress": "21 2nd Street",
        "city": "New York",
        "state": "NY",
        "postalCode": "10021"
    },
    "phoneNumbers": [
        { "type": "home", "number": "212 555-1234" },
        { "type": "fax", "number": "646 555-4567" }
    ]
}

이 경우 (사실 위 페이지는 JSON의 표기법에 따라 key-value형태로 작성한 것이다) JSON (JavaScript Object Notification)을 이용하면 사람도 기계도 읽기가 쉬울 것이다.

그리고 이 것을 iPhoe Library로 만든 json-framework이 있어서 JSON과 json-framework을 이용하는 방법에 대해서 포스팅힌다.

이 튜토리얼에서는 위 HTML 데이터를 json-framework으로 이용해서 파싱해서 터미널로 출력하는 프로그램을 간단히 만들 것이다.

1. 설치 및 프로젝트 설정

json-framework 에서 json framework을 다운로드 받는다. *.dmg 파일로 다운로드 받고 더블 클릭한다.

설치는 Installation Instructions 페이지대로 따라하면 된다. 여기서 간단히 정리하면 다음과 같다.

(아래는 iPhone 개발을 위한 JSON framework 설치이고 Mac Desktop Application 개발을 위한 설치 가이드는 위 문서의 “3″ section을 보면 된다.)

1. ~/Library/SDKs 폴더를 만든다 (존재한다면 만들 필요는 당연히 없다).

2. *.dmg의 디스크 이미지에 있는 SDKs/JSON 폴더를 ~/Library/SDKs 폴더로 복사한다.

3. 아래 그림과 같이 JSON framework을 사용하려는 프로젝트에서 Targets에서 Target Info를 열고 “Build”탭에서 “Configuration”을 “All Configurations”로 선택 후, “Additional SDKs”에 아래를 입력한다.

$HOME/Library/SDKs/JSON/${PLATFORM_NAME}.sdk

입력 화면은 아래와 같고,

그림 8 by you.

입력을 하고 나면 아래와 같이 입력된다.

그림 9 by you.

4. 그리고 “Build” 탭에서 (조금 하단에 있는) “Other Linker Flags”에 아래 두 option을 넣는다.

-ObjC -ljson

그림 7 by you.

5. JSON framework을 사용하려는 코드에서는 아래와 같이 JSON.h를 import 해준다.

#import <JSON/JSON.h>

6. 컴파일 에러가 발생했을 경우

위 JSON.h를 import하고 SBJSON 클래스를 이용해서 객체를 생성하는 코드를 간단히 작성해서 빌드하면 아래와 같은 컴파일 에러를 만날 수 있다 (만났다).

»error: syntax error before ‘AT_NAME‘ token«, »error: syntax error before ‘}’ token« and »fatal error: method definition not in @implementation context«

이 경우는 “Target Info”의 “Build” tab에서 gcc 버전을 4.2로 설정된 경우 (단순히) 4.0을 선택했다가 다시 4.2를 선택하고 창을 닫으면 된다. -__-;

그림 10 by you.

.

2. 테스트 페이지 준비

앱에서 JSON Framework을 읽어올 테스트 웹 페이지를 만들어보자.

위키피디아에 있는 JSON 설명 페이지의 예제 html에 한글까지 테스트하기 위해서 아래와 같이 페이지를 만들어봤다.

물론, 압축 파일에 test.html로 포함이 되어 있다.

{
    "firstName": "John",
    "lastName": "Smith",
    "한글이름" : "존 스미스",
    "address": {
        "streetAddress": "21 2nd Street",
        "city": "New York",
        "state": "NY",
        "postalCode": "10021"
    },
    "phoneNumbers": [
        { "type": "home", "number": "212 555-1234" },
        { "type": "fax", "number": "646 555-4567" }
    ]
}

위 파일을 http://alones.kr/iphone/test/test.html에 올려두었다.

.

3. JSON을 이용해서 test.html URL에 http request 보내서 받은 문자열을 parsing 하기

3-1. URL을 Request할 함수

먼저 URL을 받아서 Http Request를 synchronous하게 처리할 함수를 만들자. URL*을 받아서 http request하고 결과를 NSString*을 반환해주는 함수다.

- (NSString *)stringWithUrl:(NSURL*)url {
	NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:30];
	// Fetch the JSON response
	NSData *urlData;
	NSURLResponse *response;
	NSError *error;

	// Make synchronous request
	urlData = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&response error:&error];

 	// Construct a String around the Data from the response
	return [[[NSString alloc] initWithData:urlData encoding:NSUTF8StringEncoding] autorelease];
}

3-2. applicationDidFinishLaunching 에서 http://alones.kr/iphone/test/test.html 로 위에서 정의한 stringWithUrl:을 이용해서 http request를 보내서 NSString*을 받는다.

- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // Override point for customization after application launch
    [window makeKeyAndVisible];

	// 1. Get the string from the given url.
	NSString *strResponse = [self stringWithUrl:[NSURL URLWithString:@"http://alones.kr/iphone/test/test.html"]];
	NSLog(@"result---\n%@", strResponse);

3-3. SBJSON을 이용해서 response string을 파싱한다.

NSString*을 넘겨주기만 하면 파싱된 NSDictionary* 가 반환된다.

        // 2. Create BSJSON parser.
	SBJSON *jsonParser = [[[SBJSON alloc] init] autorelease];

	// 3. Get the result dictionary from the response string.
	NSDictionary *dic = (NSDictionary*)[jsonParser objectWithString:strResponse error:NULL];

3-4. JSON으로 파싱된 결과는 아래와 같이 NSDictionary를 retrieval하면서 출력할 수 있다.

이 예제에서는 다음과 같이 세 가지 type에 대해서 출력하는 것을 보여주고 있다.

Type 1: key와 vlaue이다.

Key : Value

Type 2: key에 value는 NSDictionary*이다.

Key : {Key : Value, Key: Value, … }

Type 3: key에 value는 NSArray*이고 array element는 NSDictionary*이다.

Key : [ {Key : Value, Key : Value, ...}, {Key: Value, Key: Value, ...}]

NSLog를 이용해서 출력하는 코드는 아래와 같고,

        // 4. Show response
	// key and value type
	NSLog(@"firstName: %@", [dic objectForKey:@"firstName"]);
	NSLog(@"lastName: %@", [dic objectForKey:@"lastName"]);
	// 한글 테스트
	NSLog(@"한글이름: %@", [dic objectForKey:@"한글이름"]);
	// Key and Dictionary as its value type
	NSDictionary *addressDic = [dic objectForKey:@"address"];
	NSLog(@"address: streetAddress: %@", [addressDic objectForKey:@"streetAddress"]);
	NSLog(@"address: city: %@", [addressDic objectForKey:@"city"]);
	NSLog(@"address: state: %@", [addressDic objectForKey:@"state"]);
	NSLog(@"address: postalCode: %@", [addressDic objectForKey:@"postalCode"]);
	// Key and Array as its value type
	// array에는 type과 number로 된 dictionary가 들어 있음
	NSArray *phoneArray = [dic objectForKey:@"phoneNumbers"];
	for(int i = 0; i< [phoneArray count]; i++ ) {
		NSLog(@"phoneNumbers: type: %@", [[phoneArray objectAtIndex:i] objectForKey:@"type"]);
		NSLog(@"phoneNumbers: number: %@", [[phoneArray objectAtIndex:i] objectForKey:@"number"]);
	}

출력 결과는 아래와 같다.

그림 11 by you.

.

4. 소스 코드

Download Sample Code


.

[References]

Ref 1. JSON Framework

Ref 2. JSON Framework Installation Instructions

Ref 3. Issues 33: Installation Instructions Problem

Rer 4. JSON wikipedia

Ref 5 . JSON Framework for iPhone

Ref 6.  JSON Framework for iPhone (Part 2)

Tagged with:
Aug 26

아이폰 3.0에는 여러 가지 유용한 Feature와 Class들이 추가되었다. 그 중 CGGradientLayer를 이용해서 아래 그림과 같이 Shadow 효과가 있는 Gradient Cell을 이용한 Table View를 만들어보자.

.

App to make in this tutorial

아래 그림처럼 Gradient 효과가 들어간 Cell이 있는 TableView가 있는 간단한 앱이다.

그림 3 by you.

.

UI 뼈대 만들기

1. UITableViewController를 상속받는 RootViewController class를 만든다.

2. AppDelegate에 UINavigationController와 RootViewController 변수를 선언한다.

3. applicationDidFinishLaunching 에 아래와 같이 IB (Interface Builder)를 사용하지 않고 TableView를 생성한다.

- (void)applicationDidFinishLaunching:(UIApplication *)application {    

	// create root view controller
	rootViewController = [[[[RootViewController alloc] initWithStyle:UITableViewStylePlain] autorelease] retain];

	navigationController = [[[[UINavigationController alloc] initWithRootViewController:rootViewController] autorelease] retain];

	[window addSubview:[navigationController view]];

    // Override point for customization after application launch
    [window makeKeyAndVisible];
}

.

GradientCell

UITableView의 cell이 될 GradientCell을 UITavleViewCell을 상속받아 만든다.

.

GradientView

GradientView는 GradientCell의 background view가 될 것이고 CAGradientLayer를 이용해서 gradient 효과를 나타낼 것이다.

CAGradientLayer

The CAGradientLayer class draws a color gradient over its background color, filling the shape of the layer (including rounded corners)

즉, CAGradientLayer의 colors property에 원하는 색들을 넣어 줄 것이다.

colors
An array of CGColorRef objects defining the color of each gradient stop. Animatable.

@property(copy) NSArray *colors
Discussion
Defaults to nil.

Availability
Available in iPhone OS 3.0 and later.
Declared In
CAGradientLayer.h

UIView를 상속 받는 GradientView를 만든다.

우리는 UIView.layer를 얻어와서 새 layer를 설정할 것인데 문제는 layer는 아래와 같이 readonly property이다.

@property(nonatomic,readonly,retain)                 CALayer  *layer;

그래서 UIView.layer를 가져와서 color를 지정하면 run time error가 발생한다. 그래서 새로운 layer를 만들기 위해서 [UIView layerClass]를 overriding 한다.

즉 GradientView에 아래와 같이 layerClass class method를 overriding 한다.

+ (Class)layerClass
{
	return [CAGradientLayer class];
}

그리고 [UIView initWithFrame:]에 layer를 얻어와서 GradientLayer.colors에 색을 지정한다.

- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        // Initialization code
		CAGradientLayer *gradientLayer = (CAGradientLayer *)self.layer;
		gradientLayer.colors =
		[NSArray arrayWithObjects:
		 (id)[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0].CGColor,
		 (id)[UIColor colorWithRed:0.85 green:0.85 blue:0.85 alpha:1.0].CGColor,
		 nil];
		self.backgroundColor = [UIColor clearColor];

    }
    return self;
}
.

QuartzCore.framework 포함

CAGradientLayer는 QuartzCore.framework에 있기 때문에 이 library를 추가해주고 GradientView.m에 QuartzCore.h를 import한다.

#import <QuartzCore/QuartzCore.h>

.

UITableView의 separate style 지정하기

마지막으로 UITableView의 구분 선을 없애자. 다음과 같이 separate style을 UITableViewCellSeparatorStyleNone 으로 지정하면 된다. title도 하나 붙이고…

- (void)viewDidLoad {
    [super viewDidLoad];

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem;
	self.title = @"Gradient Cell";

	self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
.

Source Code

Download Sample Code

Ref 1. Adding shadow effects to UITableView using CAGradientLayer

Tagged with:
Aug 23

iPhone은 NSURLRequest와 NSURLConnection 를 이용해서 http request와 response를 쉽게 처리할 수 있게 해준다.

첫 아이폰 앱이었던 Make Num을 개발할 때 http request/response를 위한 유틸 클래스를 만들었던 것을 이용해, http request로 이미지를 받아서 보여주는 것에 대해 다루는 튜토리얼이다.

Make Num 때에는 Ranking 서버에서 점수와 해당 정보를 받아오는 것을 string으로 받아올 때 사용했다.

.

App to Make in this tutorial

이번 튜토리얼에서 만들 앱은 아래 Picasa에 있는 이미지를 받아서 UIImageView에 뿌려주는 간단한 앱이다.

http://lh4.ggpht.com/_85hIOLhClFE/SpAB03RUZtI/AAAAAAAAFgs/VdhbDuH8riQ/sample.png

Simulator에서 실행시킨 모습은 아래와 같다 (싱가폴에서 찍은 사진이다. ㅋ).

그림 9 by you.

.

HttpManager

HttpManager는 URL Loading System Guide 문서를 보고 만든 Class이다.

URL을 String으로 주면, 해당 주소의 웹페이지를 읽어서 NSMutableData로 전달해준다. 이 Tutorial의 경우는 웹 페이지가 image이기 때문에 NSMutableData로 UIImage를 만들면 되고, 페이지가 문자열일 경우는 NSString을 만들어서 처리하면 될 것이다.

HttpManager를 사용하기 위해서는 Caller가 HttpManagerDelegate를 Adopt 하게 했다. Http request 시 callback 되는 method들을 처리해주기 위해서이다.

HttpManager를 들여다보자.

.

HttpManager Class 선언 부

HttpManagerDelegate를 받은 Caller를 받기 위한 delegate와 response data를 받기 위한 received data를 가지고 있다.

@protocol HttpManagerDelegate;

@interface HttpManager : NSObject {

	id  delegate;

	NSMutableData *receivedData;
}

@property (nonatomic, assign) id delegate;
@property (nonatomic, retain) NSMutableData *receivedData;

.

HttpManagerDelegate protocol

HttpManagerDelegate protocol은 http request 시 request가 완료되었을 때와 실패했을 때 callback 되는 method들을 정의하고 있다. 그 외 request 시 callback 되는 NSURLConnectionDelegate의 connection method들은 내부적으로 처리했다.

@protocol HttpManagerDelegate

-(void)connectionDidFail:(HttpManager*)aHttpManager;
-(void)connectionDidFinish:(HttpManager*)aHttpManager;

.

[HttpManager initWithUrl: delegate:]

HttpManager를 initWithUrl로 init 하면 바로 주어진 url의 loading을 시작한다. 즉, url에 대해서 NSURLRequest를 만들고, NSURLConnection 생성해서 loading을 시작한다. 여기서 NSString의 URL을 [NSString stringByAddingPercentEscapesUsingEncoding:]에 NSUTF8StringEncoding을 지정해서 encoding하는 것을 잊지 말아야 한다. 공백이나 특수 문자 등을 ‘%’를 써서 encoding 해준다.

-(id)initWithUrl:(NSString*)aURL delegate:(id)aDelegate {

	if( self = [super init] ) {

		// delegate
		self.delegate = aDelegate;

		// URL string을 아래와 같이 %가 필요한 곳에 붙여주는 encoding 해줘야한다.
		NSString *escapedUrl = [aURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

		// create the request
		NSURLRequest *aRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:escapedUrl]
								cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
		// create the connection with the request
		// and start loading the data
		NSURLConnection *aConnection=[[NSURLConnection alloc] initWithRequest:aRequest delegate:self];
		if (aConnection) {
			// Create the NSMutableData that will hold
			// the received data
			// receivedData is declared as a method instance elsewhere
			receivedData = [[NSMutableData data] retain];
		} else {
			// inform the user that the download could not be made
			// [todo] error
		}
	}

	return self;
}

.
[HttpManager connectionDidFinishLoading:]

HttpManager의 다른 method들은 간단하며 HttpManagerDelegate를 adopt 한 caller의 해당 method를 불러주기 위해 caller가 해당 method를 가지고 있는지 체크하는 코드를 보라고 connectionDidFinishLoading method 코드를 보여준다. Caller의 connectionDidFinishLoading method가 불리면 Caller는 HttpManager의 receivedData를 받아서 사용하면 된다.

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {

	if( [self.delegate respondsToSelector:@selector(connectionDidFail:)]) {
		[self.delegate connectionDidFinish:self];
	}

	[connection release];
	if( receivedData != nil) {
		[receivedData release];
		receivedData = nil;
	}
}

.

UI 부분 완성하기

이 번에도 IB (Interface Builder) 없이 간단하게 UI를 만들겠다.

시나리오는 아래와 같다.

1. applicationDidFinishLaunching 에서 UIImageView, UIActivityIndicatorView를 생성하고 Indicator Animation을 start한 후 HttpManager에 Picasa의 이미지를 가져오게 http request를 요청한다.

2. HttpManagerDelegate의 connectionDidFinish 가 callback 되면 HttpManager의 receivedData를 가져와서 UIImage를 만들고 UIImageView에 넣어준다.

.

URLLoadingSystem_imageAppDelegate class 선언

URLLoadingSystem_imageAppDelegate는 HttpManagerDelegate를 adopt해서 아래와 같다.

#import
#import "HttpManager.h"

@interface URLLoadingSystem_imageAppDelegate : NSObject  {
    UIWindow *window;

	UIImageView *imageView;
	UIActivityIndicatorView *indicator;

	HttpManager *httpManager;
	NSMutableData *receivedData;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) UIImageView *imageView;

-(void)getImageFrom:(NSString*)url;

@end

.

UIApplicationDelegate의 applicationDidFinishLaunching

위에서 설명한 것과 같이 UIImageView를 생성해서 UIWindow에 add하고 UIActivityIndicatorView를 생성해서 UIImageView에 add 후 animation을 시작하고 HttpManager를 picasa url로 init해서 URL Loading을  시작한다.

- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // Override point for customization after application launch
	// create UIImageView
	imageView = [[UIImageView alloc] initWithFrame:window.frame];
	[window addSubview:imageView];

	// image를 받을 때까지 indicator를 동작시킨다.
	indicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake( window.frame.size.width/2 - 20, window.frame.size.height/2-20, 40, 40)];
	indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
	[imageView addSubview:indicator];
	[indicator startAnimating];

	// 아래 url에서 이미지를 가져와서 UIImageView에 보여준다.
	[self getImageFrom:@"http://lh4.ggpht.com/_85hIOLhClFE/SpAB03RUZtI/AAAAAAAAFgs/VdhbDuH8riQ/sample.png"];

    [window makeKeyAndVisible];
}

-(void)getImageFrom:(NSString*)url {
	if( httpManager ) {
		[httpManager release];
		httpManager = nil;
	}

	httpManager = [[HttpManager alloc] initWithUrl:url delegate:self];
}

.

HttpManagerDelegate의 connectionDidFinish를 adopt

connectionDidFinish: 는 URL Loading이 완료되었을 때 callback 된다. 여기서 HttpManager의 receivedData를 가져와서 아래와 같이 UIImage를 생성해서 UIImageView에 넣어주면 완료된다.

#pragma mark HttpManagerDelegate
-(void)connectionDidFinish:(HttpManager*)aHttpManager {
	[indicator stopAnimating];

	receivedData = httpManager.receivedData;
	imageView.image = [[UIImage alloc] initWithData:receivedData];
}

.

Http Request Fail 처리

HttpManagerDelegate의 connectionDidFail은 Http Request가 실패했을 때 callback 된다. 아래와 같이 사용자에게 실패했다는 메시지를 UIAlertView로 보여준다.

-(void)connectionDidFail:(HttpManager*)aHttpManager {
	[indicator stopAnimating];

	UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:@"Fail" message:@"Couldn't connect" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil, nil] autorelease];
	[alertView show];
}

.

Source Code

이 Tutorial의 코드는 아래에서 받을 수 있다.

Download Sample Code

.

Ref 1. URL Loading System

Tagged with:
Aug 19

현재 진행 중인 앱엔 Core Data만 사용해도 될 것을 SQLite의 데이터를 Core Data로 import해보고 싶어서 손을 댔다가 며칠을 보냈는지 모른다.

iPhone OS 3.0부터, 애플은 iPhone Dev Center에서 SQLite 관련 Tutorial과 Sample 코드를 내릴 만큼 Core Data를 전격 적으로 밀고 (?) 있다.

iPhone은 그동안 Property list, Settings, plist, Archive 등의 여러 가지 데이터 store, restore 방법을 제공하고 SQLite를 DB로 제공해왔다.

하지만 Core Data에서 object graph를 완성시키고, UI (View)과 data (Model)을 거의 완벽하게 연결해줌으로써 iPhone의 진정한 MVC를 실현 시킨 것 같다 (그동안 dictionary나 array로 table view를 처리했던 일에서 벗어날 수 있게 되었다).

하지만 애석하게도 Core Data는 그 자체만으로는 pre population data set (default data set)을 준비할 수 없다. xml, plist, SQLite 등으로 데이터를 준비해야 한다. 즉, “오늘의 격언”이라는 앱을 만든다고 가정하면, 격언들을 xml이나 plist, SQLite에 저장해두고 앱에서는 Core Data로 그 것을  읽어야 한다. 데이터가 많고 relation이 복잡하며 binary까지  다뤄야 한다면 SQLite를 default data set으로 이용해야 할 것이다. 하지만 애석하게도 이 부분에 대해 자세히 다룬 문서는 애플  dev center의 문서고에서 찾지 못했다.


이 경우 아래 그림과 같이 Persistent Store Coordinator를 이용해서 SQLite의 데이터를 하나의 persistent store로 취급해서 데이터를 가져오고 쓸 수 있다. 이것이 이 Tutorial에서 다룰 내용이다.

그림 1 by you.

서론이 너무 길었다. Tutorial을 시작해보자.

.

Premise

- 이 Tutorial은 앱을 만들 때 Core Data를 만드는 것에 대해서 하나하나 다루지 않는다. 이를 위해서는 Core Data Tutorial for iPhone OS을 보는 것이 좋을 것이다.

- 마찬가지로, TableView 등의 UI에 대해서도 자세히 다루지 않는다.

.

App to make

- AppInfo라는 앱을 만들 것이고 ‘name’, ‘author’, ‘price’의 field를 가지는 SQLite table을 준비해서 이용할 것이다.

- UI는 간단하게 다음과 같이 TableView에 위 정보를 보여줄 것이다.

.

SQLite Manger FireFox pluging 설치

SQLite를 편하게 사용하기 위해서 FireFox Addon인 SQLite Manger를 설치하자. 아주 멋진 녀석인 것 같다. 설명서가 필요 없을 만큼 intuitive한 GUI를 제공하고 table 생성, 데이터 입력뿐만 아니라 query를 직접 날릴 수도 있어 아주 편하다.

SQLite Manger Addon 페이지에서 설치하면 아래 그림과 같이 멋진 녀석이 파폭에 addon된다.

그림 2 by you.

.

AppInfo project 만들기

“AppInfo”라는 프로젝트를 만들자. Use Core Data for storage 옵션 체크를 잊지 말자!

.

SQLite DB 준비하기

사실 상 이 부분이 이 Tutorial의 핵심일 것이다.

AppInfo라는 이름으로 SQLite DB를 만들자. 저장소는 project 폴더 위치로 정한다.

일반적인 SQLite는 Core Data의 persistent store로 사용될 수 없다. Core Data에 import되기 위해서는 다음 두 테이블이 있어야 한다.

M_METADATA, M_PRIMARYKEY

이 부분에 시행착오가 많아서 어떤 사람들은 CoreDataBooks 의 SQLite 파일을 가져와서 수정해서 써라는 사람도 있다.

step 1. Z_METADATA와 Z_PRIMARYKEY table을 만들고 아래와 같이 field를 추가한다.

Z_METADATA는 다음과 같이 만든다.

Z_METADATA

- Z_VERSION: PRIMARY KEY, INTEGER

- Z_UUID: VARCHAR(255)

- Z_PLIST: BLOB

아래 Query를 SQLite Manger에서 바로 실행 시켜도 될 것이다.

CREATE TABLE Z_METADATA (Z_VERSION INTEGER PRIMARY KEY, Z_UUID VARCHAR(255), Z_PLIST BLOB)

M_PRIMARYKEY는 다음과 같이 만든다.

Z_PRIMARYKEY

- Z_ENT: PRIMARY KEY, INTEGER

- Z_NAME: VARCHAR

- Z_SUPER: INTEGER

- Z_MAX: INTEGER

마찬가지로 다음과 같은 Query를 바로 실행시켜도 될 것이다.

CREATE TABLE Z_PRIMARYKEY (Z_ENT INTEGER PRIMARY KEY, Z_NAME VARCHAR, Z_SUPER INTEGER, Z_MAX INTEGER)

step 2. AppInfo table을 만들자.

모든 사용자 table은 Core Data에 import되기 위해서 다음 사항을 지켜줘야 한다.

1. Table 이름은 ‘Z’ prefix를 붙인다. 실제 Core Data의 table 이름에는 Z가 없다. 즉, AppInfo table은 SQLite에서 ZAppInfo로 만들어야 한다.

2. 모든 field도 Z로 시작해야 한다. 즉, Core Data에서는 AppInfo가 ‘name’이라는 attribute를 가져도 SQLite에서는 ‘Zname’으로 field를 맞춰야 한다.

3. 다음 세 field를 가져야 한다.

Z_PK: PRIMARY KEY, INTEGER

Z_ENT: INTEGER

Z_OPT: INTEGER

우리의 AppInfo table은 Zname (VARCHAR), Zauthor (VARCHAR), Zprice (FLOAT)의 field를 가져서 다음과 같은 query 문으로 생성될 수 있을 것이다.

CREATE TABLE “ZAppInfo” (“Z_PK” INTEGER PRIMARY KEY  NOT NULL , “Z_ENT” INTEGER, “Z_OPT” INTEGER, “Zname” VARCHAR, “Zauthor” VARCHAR, “Zprice” FLOAT)

GUI를 이용한 모습은 아래와 같다.

그림 3 by you.

step 3. Z_PRIMARYKEY에 다음 record를 넣어줘야 한다.

Z_ENT: ’1′, Z_NAME: ‘AppInfo’, Z_SUPER: ’0′, Z_MAX: ’22′.

아래 그림과 같다.

그림 4 by you.

step 4. AppInfo에 record를 넣는다. primary key를 넣어주는 것을 잊지 말자. 아래와 같이 내가 만든 app들을 넣어봤다. -_-; Z_ENT와 Z_OPT는 비워둔다.

그림 5 by you.

.

UI 뼈대 만들기

IB (Interface Builder)를 직접 사용하지 않고 아래와 같이 코드로 UITableViewController를 만든다. Class 이름은 RootViewController로 하자.

아래는 간단히 과정을 설명한 것이고, 자세한 것은 첨부한 코드를 참조하면 될 것이다. 이것을 다루려는 Tutorial이 아니니…

1. RootViewController를 UITableViewController를 상속받아 만든다.

2. AppInfoAppDelegate에 UINavigationController와 RootViewController를 추가하고 applicationDidFinishLaunching에 아래와 같이 코드를 넣어준다.

- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // Override point for customization after app launch
	rootViewController = [[[[RootViewController alloc] initWithStyle:UITableViewStylePlain] autorelease] retain];
	rootViewController.managedObjectContext = self.managedObjectContext;

	navigationViewController = [[[[UINavigationController alloc] initWithRootViewController:self.rootViewController] autorelease] retain];

	[window addSubview:[self.navigationViewController view]];

	[window makeKeyAndVisible];
}

3. RootViewController에는 NSManagedObjectContext와 NSFetchedResultsController를 추가한다. 뒤에서 다루겠지만, NSFetchedResultsController는 정말 멋진 녀석으로 UITableView를 태어났다.

4. 빌드해서 수행하면 빈 Table View가 나타날 것이다.

.

CoreData Model 만들기

SQLite에서 만든 AppInfo와 동일한 scheme를 가진 Model을 만든다. 물론 ‘Z’ prefix는 빼고 만들어야 할 것이다.

Managed Object Class까지 만든 model 은 아래와 같다.

그림 6 by you.

.

Persistent Store Coordinator에서 SQLite DB를 Persistent Store로 가져오기

AppInfo.sqlite 파일을 Project의 “Resouces” 아래에 추가하고, 자동 생성된 AppInfoAppDelegate의 persistentStoreCoordinator method를 아래와 같이 작성한다.

/**
 Returns the persistent store coordinator for the application.
 If the coordinator doesn't already exist, it is created and the application's store added to it.
 */
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

    if (persistentStoreCoordinator != nil) {
        return persistentStoreCoordinator;
    }

	NSString *storePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"AppInfo.sqlite"];
	/*
	 Set up the store.
	 For the sake of illustration, provide a pre-populated default store.
	 */
	NSFileManager *fileManager = [NSFileManager defaultManager];
	// If the expected store doesn't exist, copy the default store.
	if (![fileManager fileExistsAtPath:storePath]) {
		NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:@"AppInfo" ofType:@"sqlite"];
		if (defaultStorePath) {
			[fileManager copyItemAtPath:defaultStorePath toPath:storePath error:NULL];
		}
	}

	NSURL *storeUrl = [NSURL fileURLWithPath:storePath];

	NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
    persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];

	NSError *error;
	if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
		// Update to handle the error appropriately.
		NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
		exit(-1);  // Fail
    }    

    return persistentStoreCoordinator;
}

.

NSFetchedResultsController 사용하기

UITableView를 위해 태어난 NSFetchedResultsController를 적용해서 SQLite의 데이터를 TableView에 나타내보자.

Step1. NSFetchedResultsControllerDelegate를 적용한다.

NSFetchedResultsControllerDelegate를 RootViewController가 받아 아래와 같이 fetchedResultsController를 구현한다. Fetch는 AppInfo table을 가지고 올 것이고, name에 따라 sorting할 것이고 section은 사용하지 않아서 아래와 같다.

#pragma mark Fetched results controller

/**
 Returns the fetched results controller. Creates and configures the controller if necessary.
 */
- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

	// Create and configure a fetch request with the Book entity.
	NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
	NSEntityDescription *entity = [NSEntityDescription entityForName:@"AppInfo" inManagedObjectContext:managedObjectContext];
	[fetchRequest setEntity:entity];

	// Create the sort descriptors array.
	NSSortDescriptor *nameDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES] autorelease];
	NSArray *sortDescriptors = [[[NSArray alloc] initWithObjects:nameDescriptor, nil] autorelease];
	[fetchRequest setSortDescriptors:sortDescriptors];

	// Create and initialize the fetch results controller. We don't make section.
	NSFetchedResultsController *aFetchedResultsController = [[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil] autorelease];
	self.fetchedResultsController = aFetchedResultsController;
	fetchedResultsController.delegate = self;

	return fetchedResultsController;
}

Step 2. View가 load 되고 나면 아래와 같이 Core Data에 대해서 fetch를 실행하자.

- (void)viewDidLoad {
    [super viewDidLoad];

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem;
	self.title = @"App Info";

	NSError *error;
	if (![[self fetchedResultsController] performFetch:&error]) {
		// Update to handle the error appropriately.
		NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
		exit(-1);  // Fail
	}
}

Step 3. 아래와 같이 NSFetchedResultsController를 이용해서 간단하게 Table View를 완성하자!

#pragma mark Table view methods

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
	return [[fetchedResultsController sections] count];
}

// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    id  sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
	return [sectionInfo numberOfObjects];
}

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

	AppInfo *appInfo = [fetchedResultsController objectAtIndexPath:indexPath];
	cell.textLabel.text = [NSString stringWithFormat:@"%s | %s | $%.2f USD", [appInfo.name UTF8String], [appInfo.author UTF8String], [appInfo.price floatValue]];
	cell.textLabel.font = [UIFont systemFontOfSize:13];
    return cell;
}

.

모든 과정이 끝났다. 빌드 후 실행 시키면 아래와 같이 SQLite의 DB를 Core Data가 persistent store로 import해서 table view에 DB 내용을 뿌려준다.

그림 7 by you.

.

Source Code

이 Tutorial의 코드는 아래에서 받을 수 있다.

Download Sample Code

.
Tips

- CoreData는 update 등으로 scheme등이 변경되면 별도의 작업을 해줘야 한다. 즉, 작업 중 persistent store를 제대로 import 해서 XCode가 필요한 entity를 못 찾겠다고 토악질을 하면 Clean 뿐만 아니라 설치된 app도 지우고 다시 빌드하는 것이 좋다. 이것 때문에 정상 코드와 SQLite data를 가지고도 몇 시간 삽질을 했다. -_-;

Ref 1. Core Data Tutorial for iPhone OS

Ref 2. SQLite Tutorial – Saving images in the database

Ref 3. iPhone SQLite Database Basics

Ref 4. Using a Pre-Populated SQLite Database with Core Data on iPhone OS 3.0

Ref 5. Core Data Error: Can’t Find Model for Source Store

Ref 6. NSFetchedResultsController Class Reference

Tagged with:
Aug 13

iPone Dev Tutorial을 시작해보려고 한다. 여러 앱들에 흩어져 있는 code snippet들도 정리하고 고견들도 듣기 위해서이다.

iPhone Dev 관련 포스트와 Tutorial의 차이는 코드가 제공 여부로 보면 좋겠다.

첫 번째 Tutorial은 UIImage를 resizing하는 것이다.

Resizing 없이 UIImageView에 content mode를 UIViewContentModeScaleAspectFit으로 설정하고 UIImageView.image에 UIImage를 줘버리면 끝나겠지만, thumbnail image를 제대로 resize해서 메모리 측면에서 효율적인 코딩을 해보자 (Core Data의 faulting에도 Original Image와 Resized Thumbnail Image를 이용하면 더 의미가 있을 것이다.).

이 Tutorial에서는 두 가지 방법을 제공할 것이다.

첫 번째는 UIGraphicsBeginImageContext()과 UIGraphicsGetImageFromCurrentImageContext() 을 이용하는 방법이고, 두 번째는 Using CGContextDrawImage(), CGBitmapContextCreateImage()과 UIImage imageWithCGImage을 이용하는 방법이다.

이 Tutorial에서는 아래 Screen shot과 같이 Image Picker로 앨범에서 사진을 하나 선택하고 이 것을 위 두 가지 방식으로 resizing해서 thumbnail image를 보여주는 Sample App을 제공한다.

그림 3 by you.

#1 . UIGraphicsBeginImageContext()과 UIGraphicsGetImageFromCurrentImageContext() 이용 방법

UIGraphicsBeginImageContext()는 아래 설명과 같이 주어진 size로 GC (Graphics Context)를 만드는 것이다.

UIGraphicsBeginImageContext
Creates a bitmap-based graphics context and makes it the current context.

void UIGraphicsBeginImageContext(CGSize size);
Parameters
size
The size of the new bitmap context. This represents the size of the image returned by the UIGraphicsGetImageFromCurrentImageContext function.

UIGraphicsGetImageFromCurrentImageContext 는 아래 설명과 같이 현재 CG에 있는 contents의 image (UIImage)를 반환해준다.

UIGraphicsGetImageFromCurrentImageContext
Returns an image based on the contents of the current bitmap-based graphics context.

UIImage* UIGraphicsGetImageFromCurrentImageContext(void);
Return Value
An autoreleased image object containing the contents of the current bitmap graphics context.

위 두 함수의 설명만으로 resizing은 간단히 다음과 같이 이루어질 수 있다.

1. UIGraphicsBeginImageContext 를 이용해서 resizing할 rect 크기의 CG를 만든다.

2. Original Image (UIImage)를 UIImage::drawInRect:를 이용해서 축소된 CG에 그린다.

3. UIGraphicsGetImageFromCurrentImageContext를 이용해서 현재 CG의 UIImage를 가지고 온다.

그래서 origianl UIImage를 rect에 맞게 resizing 해주는 함수는 다음과 같이 만들어질 수 있다.

-(UIImage*)resizedImage1:(UIImage*)inImage  inRect:(CGRect)thumbRect {
	// Creates a bitmap-based graphics context and makes it the current context.
	UIGraphicsBeginImageContext(thumbRect.size);
	[inImage drawInRect:thumbRect];

	return UIGraphicsGetImageFromCurrentImageContext();
}

# 2. Using CGContextDrawImage(), CGBitmapContextCreateImage()과 UIImage imageWithCGImage을 이용한 방법

이 방법도 첫 번째 방법과 비슷하나 다른 점은 resized Bitmap을 만들고 그 Bitmap에 image를 그리고, 그 Bitmap을 이용해서 UIImage를 만드는 것이다.

즉, 다음과 같은 절차를 가진다. 역시 bitmap이 들어가니 코드가 너저분해진다.

1. CGBitmapContextCreate()을 이용해서 resized된 bitmap을 만든다. 이때, rowbytes를 Original Image가 가로냐 세로냐에 따라 맞춰줘야 한다.

2. CGContextDrawImage()로 bitmap에 UIImge를 그린다.

3. CGBitmapContextCreateImage()으로 위 bitmap의 CGImageRef를 만든다.

4. 위 CGImageRef로 UIImage::imageWithCGImage로 UIImage를 만든다.

그래서 resizing 함수는 다음과 같다.

-(UIImage*)resizedImage2:(UIImage*)inImage  inRect:(CGRect)thumbRect { 

	CGImageRef			imageRef = [inImage CGImage];
	CGImageAlphaInfo	alphaInfo = CGImageGetAlphaInfo(imageRef);

	// There's a wierdness with kCGImageAlphaNone and CGBitmapContextCreate
	// see Supported Pixel Formats in the Quartz 2D Programming Guide
	// Creating a Bitmap Graphics Context section
	// only RGB 8 bit images with alpha of kCGImageAlphaNoneSkipFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaPremultipliedFirst,
	// and kCGImageAlphaPremultipliedLast, with a few other oddball image kinds are supported
	// The images on input here are likely to be png or jpeg files
	if (alphaInfo == kCGImageAlphaNone)
		alphaInfo = kCGImageAlphaNoneSkipLast;

	// Build a bitmap context that's the size of the thumbRect
	CGFloat bytesPerRow;

	if( thumbRect.size.width > thumbRect.size.height ) {
		bytesPerRow = 4 * thumbRect.size.width;
	} else {
		bytesPerRow = 4 * thumbRect.size.height;
	}

	CGContextRef bitmap = CGBitmapContextCreate(
                NULL,
                thumbRect.size.width,		// width
                thumbRect.size.height,		// height
                8, //CGImageGetBitsPerComponent(imageRef),	// really needs to always be 8
                bytesPerRow, //4 * thumbRect.size.width,	// rowbytes
                CGImageGetColorSpace(imageRef),
                alphaInfo
);

	// Draw into the context, this scales the image
	CGContextDrawImage(bitmap, thumbRect, imageRef);

	// Get an image from the context and a UIImage
	CGImageRef	ref = CGBitmapContextCreateImage(bitmap);
	UIImage*	result = [UIImage imageWithCGImage:ref];

	CGContextRelease(bitmap);	// ok if NULL
	CGImageRelease(ref);

	return result;
}

# 3. 가로/세로 이미지 고려하기

DIYPuzzle을 만들 때 삽질 좀 했었지만, 가로가 더 길거나 세로가 더 긴 이미지를 resizing 시에도 고려해주어야 한다. 가로가 길면 width로 resizing ratio를 세로가 길면 height로 resizing ratio를 만들어줘야할 것이다.

그래서 아래와 같이 이미지의 width/height를 체크 해서 thumbnail image의 rect를 만들어서 resizing 함수를 불러야 한다.

// Create a thumbnail image
	// resizedImage1View의 크기에 맞추어서 ori image을 resize한다.
	CGFloat ratio = 0;
	if( oriImage.size.width > oriImage.size.height ) {
		// 가로가 더 길면
		// resizedImage1View.frame은 57 * 57 이다.
		ratio = resizedImage1View.frame.size.width / oriImage.size.width;
	} else {
		// 세로가 더 길면
		ratio = resizedImage1View.frame.size.width / oriImage.size.height;
	}

	// image의 가로 세로가 다르기 때문에 ori image에 ratio를 적용해서 rect만들기
	CGRect rect = CGRectMake(0, 0, ratio*oriImage.size.width, ratio*oriImage.size.height);

Ref1. PhotoLocations 첫 번째 방법이 사용된 sample code이다.

Ref2. Resize Image High Quality alones로 위 두 함수를 댓글을 달기도 했다. bitmap을 이용한 방법은 PhoneyDev의 함수를 개선한 것이다.

Donwload Sample Code

# Revision History
v1.0.0 2009.08.13 Initial version.
Tagged with:
preload preload preload