이번에는 저번 바이오리듬 보다 조금 기능을 추가하여 만들어 보겠습니다. 상단에는 한달 단위로 그래프를 보여주고, 하단에는 해당일의 바이오리듬 정보를 보여 줍니다. 그리고 생일설정을 저장할 수 있도록 하겠습니다. 하단 버튼들의 기능은 아래와 같습니다.

  • << : 한달 전으로 이동
  • < : 하루 전으로 이동
  • > : 하루 후로 이동
  • >> : 한달 후로 이동
  • i : 생일 설정 

한가지 오류가 있습니다. 생일설정 시에 기존의 저장된 날짜가 DatePicker에 설정되지 않습니다. 값은 정확한데 보여지는 부분은 minimumDate로 초기에 선택되어져 있는 것 같습니다. 검색을 해봐도 같은 증상을 겪은 경우는 보았는데, 원인과 해결책은 찾지 못했습니다.


완성된 모습은 아래와 같습니다.

사용자 삽입 이미지


1. 프로젝트 생성
사용자 삽입 이미지
Xcode의 메뉴에서 File/New Project 클릭합니다. iPhone OS의 applicaion에서 좌측과 같은 Unility Application을 선택하고 Choose 버튼을 클릭합니다. 프로젝트 이름에 'iBiorhythm'을 입력한 후에 저장합니다.

Unility Application은 컨텐츠를 보여주는 main view와 설정등을 할 수 있는 flipside view를 제공하며, 두 뷰의 변환시에 아래와 같은 에니메이션 효과를 제공합니다.

사용자 삽입 이미지

사용자 삽입 이미지
Xcode의 Groups & Files를 보시면 좌측과 같이 그룹이 생성되어 있습니다. MainView, FlipsideView 각각에는 view와 view를 관리하는 controller 소스들이 있습니다.


2. 소스코드 
1) Global.h
사용자 삽입 이미지
여러 소스에서 사용할 상수나 변수등을 위해 헤더 파일을 하나 만듭니다. File/New File...을 클릭합니다. Mac OS X에서 C and C++ 항목을 클릭하고 좌측과 같은 Header File이란 아이콘을 선택한 후에 파일명을 Global.h로 입력하고 저장합니다. 생성된 파일에 아래와 같은 내용을 추가 합니다.

#define    MAX_DATATYPE            3

#define US_BIRTHYEAR_KEY        @"birth_year"
#define US_BIRTHMONTH_KEY     @"birth_month"
#define US_BIRTHDAY_KEY          @"birth_day"

#define NM_BIRTHDAYCHANGED   @"NTBirthdayChanged"

#define NAV_PREVMONTH            1
#define NAV_PREVDAY                 2
#define NAV_NEXTDAY                 3
#define NAV_NEXTMONTH            4

#define ONEDAY_SECOND            (24 * 60 * 60)

2) GraphView
그래프를 출력하기 위한 View를 생성합니다. 메뉴에서 File/New File...을 클릭 후에 iPhone OS/Cocoa Touch Classes 항목에서 UIView subclass를 선택한 후에 Next 버튼을 클릭합니다. 파일명에 GraphView.m을 입력한 후에 저장합니다.

* GraphView.h
#import <UIKit/UIKit.h>

@interface GraphView : UIView {
    UIImage                *backgroundImage;
   
    NSDateComponents    *currentDate;
    NSDateComponents    *birthDate;
   
    float biorhythmData[31][MAX_DATATYPE];
}

@property (nonatomic, retain) NSDateComponents *currentDate;
@property (nonatomic, retain) NSDateComponents *birthDate;

- (void)setBirthDate:(int)year atMonth:(int)month atDay:(int)day;
- (void)setCurrentDate:(int)year atMonth:(int)month atDay:(int)day;
- (void)changeDate:(int)pos;

- (NSDateComponents *)currentDate;

- (void)resetBiorhythmData;
- (float *)getBiorhythmDayData:(int)day;

@end

* GraphView.m
#import "Global.h"
#import "GraphView.h"

@implementation GraphView

@synthesize currentDate;
@synthesize birthDate;

- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
    }
    return self;
}

- (void)awakeFromNib {
    currentDate = [[NSDateComponents alloc] init];
   
    /* 배경 이미지로 사용할 bio.png 로드 */
    backgroundImage = [[UIImage alloc] initWithContentsOfFile:
                       [[NSBundle mainBundle] pathForResource:@"bio" ofType:@"png"]];
}

- (void)drawRect:(CGRect)rect {
#define START_X            17    // 그래프 시작 X 위치
#define START_Y            22  // 그래프 시작 Y 위치
#define DAY_WIDTH        10  // 일별 간격
#define BOTTOM_HEIGHT    4   // 하단 공백 높이

    /* 데이터별 색상 테이블 */
    static const CGFloat color[MAX_DATATYPE][5] = {
        { 1.0, 0.0, 0.0, 1.0 }, //red
        { 0.0, 1.0, 0.0, 1.0 }, //green
        { 0.0, 0.0, 1.0, 1.0 }  //blue
    };
   
    CGSize viewSize = rect.size;
    CGContextRef context = UIGraphicsGetCurrentContext();

    /* 배경 이미지 출력 */
    CGPoint point;
    point.x = point.y = 0;
    [backgroundImage drawAtPoint:point];

    /* 오늘 날짜를 가르키는 위치는 노란 선으로 출력 */
    CGContextBeginPath(context);
    CGContextSetLineWidth(context, 1.0);
    CGContextSetRGBStrokeColor(context, 1.0, 0.8, 0.0, 1.0);
    CGContextMoveToPoint(context, START_X + (DAY_WIDTH * ([currentDate day] - 1)), START_Y);
    CGContextAddLineToPoint(context, START_X + (DAY_WIDTH * ([currentDate day] - 1)), viewSize.height - BOTTOM_HEIGHT);
    CGContextStrokePath(context);
   
    /* 바이오리듬 출력 */
    int i, j;
    int lineX = START_X;
   
    int vcenter = START_Y + (viewSize.height - START_Y - BOTTOM_HEIGHT)/2; // 그래프의 Y 중간 좌표
   
    /* 바이오리듬 출력 */
    for (i = 1; i < 31; i++) {
        for(j = 0; j < MAX_DATATYPE; j++) {
            CGContextBeginPath(context);
            CGContextSetLineWidth(context, 2.0);
           
            CGContextSetStrokeColor(context, color[j]);
           
            CGContextMoveToPoint(context, lineX, vcenter - biorhythmData[i-1][j]);
            CGContextAddLineToPoint(context, lineX + DAY_WIDTH, vcenter - biorhythmData[i][j]);
            CGContextStrokePath(context);
        }
        lineX += DAY_WIDTH;
    }
}

- (void)dealloc {
    [backgroundImage release];
    [currentDate release];
    [super dealloc];
}

- (void)resetBiorhythmData {
    static const double s_values[MAX_DATATYPE] = {
        23.0, 33.0, 28.0 // 신체, 지성, 감성
    };
   
    /* 현재 설정된 날짜의 1일 부터 생일 사이의 날수를 구한다. */
    NSDateComponents *firstDate = [[NSDateComponents alloc] init];
    [firstDate setYear: [currentDate year]];
    [firstDate setMonth: [currentDate month]];
    [firstDate setDay:1];
   
    NSDate *tempDay = [[NSCalendar currentCalendar] dateFromComponents:firstDate];
    NSDate *birthDay = [[NSCalendar currentCalendar] dateFromComponents:birthDate];
   
    NSTimeInterval ti = [tempDay timeIntervalSinceDate:birthDay];
    [firstDate release];

    /* 바이오리듬 데이터를 구한다 */
    double days = ceil(fabs(ti) / ONEDAY_SECOND);
   
    for (int i = 0; i < 31; i++) {
        for(int j = 0; j < MAX_DATATYPE; j++) {
            biorhythmData[i][j] = sin((days/s_values[j]) * 2 * 3.14195) * 100;
        }
        days += 1.0;
    }
   
    /* 변경된 데이터로 다시 그림 */
    [self setNeedsDisplay];
}

/* 현재 날짜 변경 */
- (void)setCurrentDate:(int)year atMonth:(int)month atDay:(int)day {
    [currentDate setYear:year];
    [currentDate setMonth:month];
    [currentDate setDay:day];
}

/* 생일 변경 */
- (void)setBirthDate:(int)year atMonth:(int)month atDay:(int)day {
    [birthDate setYear:year];
    [birthDate setMonth:month];
    [birthDate setDay:day];
   
    NSLog(@"SET BIRTHDAY: %d, %d, %d", year, month, day);
}

/* 사용자가 버튼을 클릭했을 때, 날짜를 변경한다 */
- (void)changeDate:(int)pos {
    NSRange monthRange;
   
    NSTimeInterval newValue = 0;
    NSDate *tempDate = [[NSCalendar currentCalendar] dateFromComponents:currentDate];
   
    if (pos == NAV_PREVMONTH) {
        /* 이전 달로 이동을 위해 이전 달의 날수를 구한다 */
        NSDateComponents *prevMonthDate = [[NSDateComponents alloc] init];
        [prevMonthDate setYear: [currentDate year]];
        [prevMonthDate setMonth: [currentDate month]-1];
        [prevMonthDate setDay:1];
       
        monthRange = [[NSCalendar currentCalendar] rangeOfUnit:NSDayCalendarUnit
                    inUnit:NSMonthCalendarUnit
                    forDate:[[NSCalendar currentCalendar] dateFromComponents:prevMonthDate]];
        [prevMonthDate release];
       
        /* 한달 전으로 이동 */
        newValue = ONEDAY_SECOND * monthRange.length;
        newValue = -newValue;
    }
    else if (pos == NAV_PREVDAY) {
        /* 하루 전으로 이동 */
        newValue = -ONEDAY_SECOND;
    }   
    else if (pos == NAV_NEXTDAY) {
        /* 하루 후로 이동 */
        newValue = ONEDAY_SECOND;
    }   
    else if (pos == NAV_NEXTMONTH) {
        /* 다음 달로 이동을 위해 현재 달의 날수를 구한다 */
        monthRange = [[NSCalendar currentCalendar] rangeOfUnit:NSDayCalendarUnit
                                                        inUnit:NSMonthCalendarUnit
                                                       forDate:[[NSCalendar currentCalendar] dateFromComponents:currentDate]];
        /* 한달 후로 이동 */
        newValue = ONEDAY_SECOND * monthRange.length;
    }

    /* 날짜를 변경하고 변경된 날짜를 구한다. */
    NSDateComponents *newDate = [[NSCalendar currentCalendar] components:(NSYearCalendarUnit | NSMonthCalendarUnit |  NSDayCalendarUnit)
                                                            fromDate:[tempDate addTimeInterval:newValue]];
   
    /* 현재 날짜를 변경 */
    [currentDate setYear: [newDate year]];
    [currentDate setMonth: [newDate month]];
    [currentDate setDay: [newDate day]];
   
    /* 바이오리듬을 다시 계산하고 출력한다. */
    [self resetBiorhythmData];
}

- (float *)getBiorhythmDayData:(int)day {
    return biorhythmData[day-1];
}

- (NSDateComponents *)currentDate {
    return currentDate;
}
@end


3) MainView
* MainView.h
#import <UIKit/UIKit.h>

@interface MainView : UIView {
    UIImage     *barFrame[MAX_DATATYPE];
    double        bioData[MAX_DATATYPE];
}

-(void)setData:(int)value1 secondValue:(int)value2 thirdValue:(int)value2;

@end

* MainView.m
#import "Global.h"
#import "MainView.h"

@implementation MainView

- (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
    }
    return self;
}

- (void)awakeFromNib {
    /* 바의 배경을 위한 이미지를 로드한다. */
    for (int i = 1; i <= MAX_DATATYPE; i++) {
        barFrame[i - 1] = [[UIImage alloc] initWithContentsOfFile:
         [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"frame%d", i] ofType:@"png"]];
    }
}

- (void)drawRect:(CGRect)rect {
#define CENTER_X        210  // 바의 중간(0) 위치
#define START_Y            300  // 바가 출력될 Y 위치
#define START_X            120  // 바가 출력될 X 위치
#define BAR_HEIGHT        14.0 // 바의 높이   
#define    BAR_SPACE        40   // 각 바간 간격
   
    // Drawing code
    CGContextRef context = UIGraphicsGetCurrentContext();
   
    int y = START_Y + 15;
    CGPoint point;
   
    point.y = START_Y;
    point.x = START_X;
   
    for (int i = 0; i < MAX_DATATYPE; i++) {
        /* 배경 이미지 출력 */
        [barFrame[i] drawAtPoint:point];
       
        CGContextBeginPath(context);
        CGContextSetLineWidth(context, BAR_HEIGHT);
       
        /* 바이오리듬 값이 0보다 작으면 붉은 색으로 크면 파란색으로 표시 */
        if (bioData[i] > 0)
            CGContextSetRGBStrokeColor(context, 0.0, 0.0, 1.0, 1.0);
        else
            CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
       
        /* 바를 그린다 */
        CGContextMoveToPoint(context, CENTER_X, y);
        CGContextAddLineToPoint(context, CENTER_X + (bioData[i] * 80/100) , y);
       
        CGContextStrokePath(context);

        /* Y 좌표 변경 */
        point.y += BAR_SPACE;
        y += BAR_SPACE;
    }
}

-(void)setData:(int)value1 secondValue:(int)value2 thirdValue:(int)value3 {
    bioData[0] = value1;
    bioData[1] = value2;
    bioData[2] = value3;
}

- (void)dealloc {
    for (int i = 0; i < MAX_DATATYPE; i++) {
        [barFrame[i] release];
    }
   
    [super dealloc];
}

@end


4) MainViewController
* MainViewController.h
#import <UIKit/UIKit.h>

@class GraphView;
@class MainView;

@interface MainViewController : UIViewController {
    IBOutlet MainView        *mainView;   
    IBOutlet GraphView      *graphView;
    IBOutlet UITextField    *value_1;
    IBOutlet UITextField    *value_2;
    IBOutlet UITextField    *value_3;
   
    IBOutlet UIBarButtonItem *currentDate;
}

- (void)updateInfoUI;
- (void)birthdayChanged:(NSNotification *)note;

- (IBAction)navigationButtonClicked:(id)sender;

@end

* MainViewController.m
#import "Global.h"
#import "MainViewController.h"
#import "MainView.h"
#import "GraphView.h"

@implementation MainViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad {
    /* 오늘 날짜를 구한다. */
    NSDateComponents *today = [[NSCalendar currentCalendar]
                       components:(NSYearCalendarUnit | NSMonthCalendarUnit |  NSDayCalendarUnit)
                       fromDate:[NSDate date]];
   
    /* 현재 날짜 설정 */
    [graphView setCurrentDate:[today year]
                      atMonth:[today month]
                        atDay:[today day]];

    /* 생년월일 설정 */
    [graphView setBirthDate:[[NSUserDefaults standardUserDefaults] integerForKey:US_BIRTHYEAR_KEY]
                      atMonth:[[NSUserDefaults standardUserDefaults] integerForKey:US_BIRTHMONTH_KEY]
                        atDay:[[NSUserDefaults standardUserDefaults] integerForKey:US_BIRTHDAY_KEY]];

    /* 그래프를 그리고 UI를 설정한다. */
    [graphView resetBiorhythmData];
    [self updateInfoUI];
   
    /* 환경설정에서 생일이 변경되었을 때를 위해 옵저버로 등록 */
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(birthdayChanged:)
                                                 name:NM_BIRTHDAYCHANGED
                                               object:nil];
}

- (void)updateInfoUI {
    /* 하단 타이틀에 현재 날짜를 출력 */
    NSDateComponents *date = [graphView currentDate];
    [currentDate setTitle:[NSString stringWithFormat:@"%d.%02d.%02d",
                           [date year], [date month], [date day]]];
   
    float *bioValue = [graphView getBiorhythmDayData:[date day]];
    int value1 = (int)ceil(bioValue[0]);
    int value2 = (int)ceil(bioValue[1]);
    int value3 = (int)ceil(bioValue[2]);
   
    /* 각 바이오리듬 값 출력 */
    [value_1 setText:[NSString stringWithFormat:@"%d", value1]];
    [value_2 setText:[NSString stringWithFormat:@"%d", value2]];
    [value_3 setText:[NSString stringWithFormat:@"%d", value3]];
   
    /* mainView에 현재 설정된 값을 알려주고, 바가 다시 그려지도록 한다. */
    [mainView setData:value1 secondValue:value2 thirdValue:value3];
    [mainView setNeedsDisplay];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
    // Release anything that's not essential, such as cached data
}

- (void)dealloc {
    [super dealloc];
}

- (void)birthdayChanged:(NSNotification *)note
{
    /* 생일이 변경되었을 때 다시 설정하고 그려지도록 한다. */
    [graphView setBirthDate:[[NSUserDefaults standardUserDefaults] integerForKey:US_BIRTHYEAR_KEY]
                      atMonth:[[NSUserDefaults standardUserDefaults] integerForKey:US_BIRTHMONTH_KEY]
                        atDay:[[NSUserDefaults standardUserDefaults] integerForKey:US_BIRTHDAY_KEY]];
   
    [graphView resetBiorhythmData];
    [self updateInfoUI];
}       

- (IBAction) navigationButtonClicked:(id)sender {
    /* 사용자가 날짜 이동 버튼을 클릭하였을 경우 처리 */
    [graphView changeDate:[sender tag]];
    [self updateInfoUI];
}

@end


5) FlipsideViewController
* FlipsideViewController.h
#import <UIKit/UIKit.h>

@interface FlipsideViewController : UIViewController {
    IBOutlet UIDatePicker    *datePicker;
}

@property (nonatomic, retain) UIDatePicker *datePicker;

@end

* FlipsideViewController.m
#import "FlipsideViewController.h"

@implementation FlipsideViewController

@synthesize datePicker;

- (void)viewDidLoad {
    self.view.backgroundColor = [UIColor viewFlipsideBackgroundColor];       
 }

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
    // Release anything that's not essential, such as cached data
}

- (void)dealloc {
    [super dealloc];
}

@end


5) RootViewController
* RootViewController.m
#import "Global.h"
#import "RootViewController.h"
#import "MainViewController.h"
#import "FlipsideViewController.h"

@implementation RootViewController

@synthesize infoButton;
@synthesize flipsideNavigationBar;
@synthesize mainViewController;
@synthesize flipsideViewController;

- (void)viewDidLoad {
   
    MainViewController *viewController = [[MainViewController alloc] initWithNibName:@"MainView" bundle:nil];
    self.mainViewController = viewController;
    [viewController release];
   
    [self.view insertSubview:mainViewController.view belowSubview:infoButton];
}

- (void)loadFlipsideViewController {
   
    FlipsideViewController *viewController = [[FlipsideViewController alloc] initWithNibName:@"FlipsideView" bundle:nil];
    self.flipsideViewController = viewController;
    [viewController release];
   
    // Set up the navigation bar
   
    UINavigationBar *aNavigationBar = [[UINavigationBar alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 44.0)];
    aNavigationBar.barStyle = UIBarStyleBlackOpaque;
    self.flipsideNavigationBar = aNavigationBar;
    [aNavigationBar release];
   
    UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(toggleView)];
    UINavigationItem *navigationItem = [[UINavigationItem alloc] initWithTitle:@"iBiorhythm"];
    navigationItem.rightBarButtonItem = buttonItem;
    [flipsideNavigationBar pushNavigationItem:navigationItem animated:NO];
    [navigationItem release];
    [buttonItem release];
}

- (IBAction)toggleView {   
    if (flipsideViewController == nil) {
        [self loadFlipsideViewController];
    }
   
    UIView *mainView = mainViewController.view;
    UIView *flipsideView = flipsideViewController.view;
   
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:1];
    [UIView setAnimationTransition:([mainView superview] ? UIViewAnimationTransitionFlipFromRight : UIViewAnimationTransitionFlipFromLeft) forView:self.view cache:YES];
   
    if ([mainView superview] != nil) {
        [flipsideViewController viewWillAppear:YES];
        [mainViewController viewWillDisappear:YES];
        [mainView removeFromSuperview];
        [infoButton removeFromSuperview];
        [self.view addSubview:flipsideView];
        [self.view insertSubview:flipsideNavigationBar aboveSubview:flipsideView];
        [mainViewController viewDidDisappear:YES];
        [flipsideViewController viewDidAppear:YES];
       
        /* DataPicker 날짜를 저장된 날짜로 설정 (현재 동작하지 않음) */
        NSDateComponents *curDate = [[NSDateComponents alloc] init];
       
        int year = [[NSUserDefaults standardUserDefaults] integerForKey:@"birth_year"];
        int month = [[NSUserDefaults standardUserDefaults] integerForKey:@"birth_year"];
        int day = [[NSUserDefaults standardUserDefaults] integerForKey:@"birth_year"];

        [curDate setYear: year];
        [curDate setMonth: month];
        [curDate setDay: day];
   
        NSDate *tempDate = [[NSCalendar currentCalendar] dateFromComponents:curDate];
        [[flipsideViewController datePicker] setDate:tempDate animated:YES];
           
        [curDate release];
    } else {
        [mainViewController viewWillAppear:YES];
        [flipsideViewController viewWillDisappear:YES];
        [flipsideView removeFromSuperview];
        [flipsideNavigationBar removeFromSuperview];
        [self.view addSubview:mainView];
        [self.view insertSubview:infoButton aboveSubview:mainViewController.view];
        [flipsideViewController viewDidDisappear:YES];
        [mainViewController viewDidAppear:YES];
       
        /* 사용자가 설정한 날짜(생일)를 저장 한다 */
        NSDateComponents *newDate = [[NSCalendar currentCalendar]
              components:(NSYearCalendarUnit | NSMonthCalendarUnit |  NSDayCalendarUnit)
              fromDate:[[flipsideViewController datePicker] date]];
       
        [[NSUserDefaults standardUserDefaults] setInteger:[newDate year] forKey:US_BIRTHYEAR_KEY];
        [[NSUserDefaults standardUserDefaults] setInteger:[newDate month] forKey:US_BIRTHMONTH_KEY];
        [[NSUserDefaults standardUserDefaults] setInteger:[newDate day] forKey:US_BIRTHDAY_KEY];
       
        /* 바이오리듬이 변경되도록 메시지를 보낸다. */
        [[NSNotificationCenter defaultCenter]
                   postNotificationName:NM_BIRTHDAYCHANGED
                   object:self];
    }
    [UIView commitAnimations];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
    // Release anything that's not essential, such as cached data
}

- (void)dealloc {
    [infoButton release];
    [flipsideNavigationBar release];
    [mainViewController release];
    [flipsideViewController release];
    [super dealloc];
}

@end

다음 포스팅에선 인터페이스 빌더에서 작업을 하고 어플리케이션이 동작하도록 완성시켜 보겠습니다. 동작은 하는데 제대로 만든 것인지는 모르겠습니다.
AND

간단하게 아이폰용 바이오리듬 어플리케이션을 만들어 보겠습니다. 저도 처음 아이폰 SDK를 사용하여 만들어 보았기 때문에, 과정을 가능한 상세하게 설명할려고 합니다. 바이오리듬은 이전에 데쉬보드 위젯에서의 방식을 사용 하였습니다. 역시나 제대로 된 방법인지는 잘 모르겠습니다.




1. 프로젝트 생성
Xcode를 실행하여 파일 메뉴에서 New Project를 클릭합니다. iPhone OS의 Application 항목에서 가장 마지막에 있는 'Window-Based Application'을 선택한 후 Choose... 버튼을 클릭합니다.

프로젝트명을 'iBio'로 입력하고 Save 버튼을 클릭합니다.


2. iBioView
1) 클래스생성
바이오리듬을 막대그래프 형식으로 보여줄 View 클래스를 만들어 보겠습니다. 메뉴에서 파일/New File...(또는 단축키: ⌘+N)을 클릭합니다. IPhone OS/Cocoa Touch Classes를 클릭하고 아래와 같이 UIView subclass를 선택한 다음 Next 버튼을 클릭합니다.


다음 창에서 아래와 같이 File Name에 iBioView.m을 입력한 후에 Finish 버튼을 클릭합니다.

2) iBioView.h
iBioView.h 파일에 아래와 같이 내용을 추가 합니다.
#import <UIKit/UIKit.h>

#define MAX_DATATYPE        3

@interface iBioView : UIView {
   double bioData[MAX_DATATYPE];
}

- (void)showBioData: (double)days;
- (int)getBioDataAt: (int)index;

@end

MAX_DATATYPE은 바이오리듬 항목을 의미하며, 신체/지성/감성 등 3개의 항목을 가지고 있습니다. bioData에는 각 항목별로 오늘의 바이오리듬 값이 저장됩니다.

3) iBioView.m
iBioView.m 파일에 아래와 같이 내용을 추가 합니다. 설명은 소스내의 간단한 주석으로 대신하겠습니다.
#import "iBioView.h"

@implementation iBioView

- (id)initWithFrame:(CGRect)frame {
   if (self = [super initWithFrame:frame]) {
          /* 바이오리듬 값을 0으로 초기화 */

           for (int i = 0; i < MAX_DATATYPE; i++) {
               bioData[i] = 0.0;

       }
   }
   return self;
}

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
   
   /* 세개의 바이오리듬 값 막대 출력 */
    for (int i = 0; i < MAX_DATATYPE; i++) {
        CGContextBeginPath(context);
        CGContextSetLineWidth(context, 15.0);
       
        int y = 34 + (i * 40);
      
       /* 바이오리듬 값이 0보다 작으면 붉은 색으로 크면 파란색으로 표시 */
        if (bioData[i] > 0)
            CGContextSetRGBStrokeColor(context, 0.0, 0.0, 1.0, 1.0);
        else
            CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
      
       /* 선을 그린다 */
        CGContextMoveToPoint(context, 200, y);
        CGContextAddLineToPoint(context, 200 + (bioData[i] * 80/100) , y);
       
        CGContextStrokePath(context);
    }
}

- (void)dealloc {
   [super dealloc];
}

- (void)showBioData: (double)days {
    static const double s_values[MAX_DATATYPE] = {
        23.0, 28.0, 33.0
    };
   
   /* 각 바이오리듬 설정 */
    for (int i = 0; i < MAX_DATATYPE; i++) {
        bioData[i] = sin((days/s_values[i]) * 2 * 3.14195) * 100;
    }
   
   /* 변경된 값으로 view가 다시 그려지도록 한다 */
    [self setNeedsDisplay];
   
}

- (int)getBioDataAt: (int)index {
    return (int)bioData[index];
}

4) 프레임 워크 추가
CGContextBeginPath와 같이 코어그래픽스 프레임워크의 모듈을 사용하기 위해 해당 프레임워크를 추가해야 합니다. 아래와 같이 Xcode 좌측의 Frameworks에서 마우스를 우클릭하여 Add/Existing Frameworks... 메뉴를 선택합니다.


선택창이 열리면 /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS2.0.sdk/System/Library/Frameworks  디렉토리에서 /CoreGraphics.framework를 선택합니다.

* 확인
완료되면 좌측과 같이 CoreGraphics.framework가 추가되어 있는 것을 확인할 수 있습니다.




3. iBioAppDelegate 변경
1) iBioAppDelegate.h
iBioAppDelegate.h 파일에 아래의 내용을 추가합니다. 추가된 항목은 인터페이스 빌더에서 각각의 오브젝트와 액션으로 연결됩니다.

#import <UIKit/UIKit.h>

@class iBioViewController;
@class iBioView;

@interface iBioAppDelegate : NSObject <UIApplicationDelegate> {
   IBOutlet UIWindow *window;

    IBOutlet iBioView *view;
    IBOutlet UIDatePicker *datePicker;
   
    IBOutlet UITextField *text_1;
    IBOutlet UITextField *text_2;
    IBOutlet UITextField *text_3;
}

@property (nonatomic, retain) UIWindow *window;

- (IBAction) viewButtonClicked:(id)sender;

@end


2) iBioAppDelegate.m
iBioAppDelegate.m 파일에 아래의 내용을 추가합니다.
#import "iBioAppDelegate.h"
#import "iBioView.h"

@implementation iBioAppDelegate

@synthesize window;

- (void)applicationDidFinishLaunching:(UIApplication *)application {   
   
    // Override point for customization after app launch   
    [window makeKeyAndVisible];
}


- (void)dealloc {
    [window release];
    [super dealloc];
}

/* 사용자가 보기 버튼을 클릭시에 메시지를 받는 메소드 */
- (IBAction)viewButtonClicked:(id)sender {
    /* 사용자가 DatePicker에서 입력한 생일과 현재 날짜로 부터 차이를 구함 */
    NSTimeInterval ti = [[datePicker date] timeIntervalSinceNow];
    double days = ceil(fabs(ti) / (24 * 60 * 60));
   
   /* view에서 days를 기준으로 바이오리듬을 구하고 출력되도록 한다 */
    [view showBioData: days];
   
   /* 각 텍스트필드에 새로 계산된 바이오리듬 값으로 설정 */ 
    [text_1 setText: [NSString stringWithFormat: @"%d", [view getBioDataAt:0]]];
    [text_2 setText: [NSString stringWithFormat: @"%d", [view getBioDataAt:1]]];
    [text_3 setText: [NSString stringWithFormat: @"%d", [view getBioDataAt:2]]];
}

@end

* 인터페이스 빌더 오픈
변경된 모든 파일들을 저장하고 좌측과 같이 MainWindow.xib를 더블클릭하여 인터페이스 빌더를 실행합니다.



4. 사용자 인터페이스 생성
1) DatePicker
* Library 윈도우
사용자가 생일을 입력할 수 있도록 DatePicker를 설정합니다.


위와 같이 라이브러리 윈도우의 Cocoa Touch Plugin/Data Views에서 Date Picker를 클릭한 후에 드래그 하여 윈도우에 가져다 놓습니다. 아래가 윈도우에 위치한 모습니다.


* 속성 변경
위의 윈도우에서 DatePicker가 선택된 상태에서 속성창에서 좌측과 같이 설정합니다.

속성창이 안보이면 선택된 상태에서  단축키(⌘+1)를 이용합니다.










2) ToolBar
다시 라이브러리 윈도우의 Windows, Views & Bars 항목에서 좌측과 같은 Toolbar 아이콘을 드래그 하여 윈도우의 하단에 아래와 같이 배치합니다.



* 타이틀 변경
툴바의 Item 버튼을 더플클릭하면 좌측과 같이 입력 가능한 상태로 변경됩니다. '보기'라고 입력합니다.



3) 안내문구
다시 라이브러리 윈도우의 Inputs & Values 항목에서 Label을 윈도우의 DatePicker 위로 드래그 해서 놓습니다.


* 속성 변경
좌측과 같이 속성창에서 Text에  "생일을 선택한 후 보기를 클릭하세요" 라고 입력합니다.

하단의 Layout에서 정렬을 중앙 정렬로 놓습니다.






* 확인
현재까지 윈도우의 모습은 아래와 같습니다.
사용자 삽입 이미지


4) View
다시 라이브러리 윈도우의 Windows, Views & Bars 항목에서 좌측과 같은 View 아이콘을 드래그 하여 크기를 조절하여 윈도우의 상단에 아래와 같이 배치합니다.
* 배경 색상 변경
속성창의 Background의 색상선택 버튼을 클릭하여 좌측과 같이 검정색으로 설정합니다.








* 클래스 변경
Class에 Xcode에서 만들어 두었던 iBioView를 선택합니다.





5) 제목, 텍스트 필드
* 제목 라벨
라이브러리 윈도우에서 Label을 세번 드래그 해서 좌측과 같이 배치합니다. 

라벨을 더블클릭하여 입력모드가 되면 각각 '신체:', '감성:', '지성:'으로 입력합니다.










* 텍스트 필드
라이브러리 윈도우에서 좌측과 같은 TextFiled도 똑같이 세번 드래그 해서 이전에 배치한 라벨의 옆에 아래와 같이 배치합니다.



* 속성 변경
⌘를 누른 상태에서 마우스로 클릭하여 좌측과 같이 세개의 텍스트 필드를 선택합니다. ⌘+1로 속성창을 엽니다.







사용자로 부터 입력에 반응하지 않기 위해 'User Interaction Eanbled'를 체크를 해제합니다.



5. 연결
MainWindow.xib 윈도우에서 I Bio App Delegate를 마우스로 우클릭합니다. 아래와 같이 iBioAppDelegate.h에서 생성해 놓은 Outlet들과 'viewButtonCliecked' Action이 있습니다. 이제 실제 오브젝트들과 연결해 보겠습니다.


1) Date Picker 연결
dataPicker 우측의 원 모양의 아이콘을 클릭하면 좌측과 같이 +로 모양이 변경됩니다. 이 상태에서 마우스 버튼을 클릭한 채로 아래와 같이 윈도우의 DatePicker에 드래그해서 놓습니다.


* 확인
완료되면 좌측과 같이 연결된 오브젝트가 표시되며, 우측의 원 모양 아이콘이 on으로 표시되어 연결되어 있슴을 알려 줍니다. x 모양의 아이콘을 클릭하면 연결이 해제됩니다.

2) TextFiled 연결
위와 같은 방법으로 아래와 같이 TextField를 text_1, text_2, text3과 위에서 부터 순서대로 연결합니다.

3) View 연결
같은 방법으로 아래와 같이 view도 연결합니다.

4) viewButtonClicked 메소드 연결
사용자가 보기 버튼을 클릭하면 viewButtonClicked가 실행되도록 위와 같은 방법으로 viewButtonClicked를 툴바의 보기 버튼과 연결합니다.

5) 연결 확인

좌측과 같이 모든 항목들이 정확하게 연결이 되어 있는 것을 확인합니다.

이제 모두 완료되었습니다. 저장 후에 빌드를 한후 실행하여 동작을 확인해 봅니다.









이상 간단하게 첫 아이폰 어플리케이션을 만들어 보았습니다. DatePicker도 동작과 설정이 조금 이상한 것 같고, 처음 갑작스레 만들어 본 것이라 맞는 방법인지 의심스럽습니다. 혹시 잘못된 부분이 있으면 댓글로 알려 주시면 감사하겠습니다.

기본 UI들이 멋있어서 맞출려면 디자인이나 이미지에 신경을 써야 할 것 같습니다. 나중에 조금 더 알아본 후에 월별로 출력하도록 수정하고 몇가지 변경해 본 후에 다시 한번 올려 보겠습니다.
AND

기타 2008. 9. 1. 13:21
1. nib 파일이란?
Nib는 코코아 어플리케이션에서 사용하는 파일로 인터페이스빌더에서 생성한 윈도우, 메뉴, 컨트롤과 사용자가 만든 오브젝트들의 레이아웃과 속성등의 정보와 오브젝트간의 연결과 바인딩등의 정보를 포함하고 있는 파일입니다.

윈도우즈의 rc 파일과 개념이 비슷하지만 소스코드에서 처리하던 메시지 처리함수 연결과 바인딩등의 더 포괄적인 정보를 가지고 있습니다.

Xcode 3.1에서는 개발과정에선 nib 대신 xib란 XML 포멧의 파일을 사용합니다. (개발이 완료되고 릴리즈된 후에 nib의 역활은 이전과 동일합니다.) 3.1 이전의 자료에서는 Xcode에서 nib를 더블클릭하여 관련된 nib를 인터페이스 빌더로 실행하는 것으로 나와 있지만, 현 3.1 버젼에서는 xib를 더블클릭하여 실행합니다.


2. MainMenu.nib
사용자 삽입 이미지
MainMenu.nib는 어플리케이션에서 사용하는 메뉴, 메인윈도우, Application등 기본적인 오브젝트를 포함하고 있습니다. MainMenu.nib의 File's Owner는 NSApplication이며 또는 사용자가 생성한 NSApplication의 서브클래스를 지정할 수 있습니다. 그렇기 때문에 MainMenu.nib는 NSApplication에 의해 실행 초기(이벤트 루프에 들어 가기 전)에 자동으로 로딩됩니다.

그러므로 가능하면 다른 오브젝트나 컨트롤, 윈도우등은 모듈별로 각각의 nib를 만들어 작게 유지하며, 해당 모듈의 실행 시에만 메모리에 올라 오도록 하는 것이 좋습니다. MainMenu.nib가 많은 오브젝트나 리소를 가지고 있으면, 초기 런칭 속도가 느려지며 처음부터 많은 메모리를 사용하게 됩니다. 

MainMenu 외에 사용자가 직접 nib를 로딩해야 할 경우에는 NSBundle을 사용하여 아래와 같이 호출합니다.

[NSBundle loadNibNamed:@"[nib file name]" owner:self];


3. nib 파일 로딩
nib 파일은 로딩 시에 아래와 같은 순서로 처리됩니다.

1) 메모리 로딩
해당 Nib에 등록된 오브젝트, 관련된 리소스(이미지, 사운드 파일등)를 메모리 또는 캐쉬로 불러 옵니다.

2) 오브젝트 Unarchive
nib에 냉동포장 되어있던 오브젝트들을 해동합니다. 인터페이스 빌더의 오브젝트들에게는 initWithCoder 메시지를 발송하며, NSView의 서브클래스들에게는 initWithFrame, 그외 오브젝트들에게는 init 메시지를 발송합니다. 그리고 인터페이스 빌더에서 설정한 정보에 따라 오브젝트들간의 연결(actions, outlet)과 바인딩을 설정합니다.

3) awakeFromNib
nib내의 모든 오브젝트들에게 awakeFromNib 메시지를 발송하여, 오브젝트가 생성이 완료되었음을 알립니다. nib내의 오브젝트들은 이 메시지를 이용하여 초기화와 관련된 처리를 할 수 있습니다.

사용자 삽입 이미지
그리고 nib내의 윈도우중에 인터페이스 빌더의 Window/Behavior내의 'Visible at Launch' 속성이 체크되어 있는 윈도우를 오픈합니다.



4. 로컬라이징
nib 파일의 로컬라이징은 매우 쉽습니다. Xcode 내에서 해당 xib를 마우스 우클릭하여 나온 메뉴중에 'Get Info'를 클릭하여 정보창을 오픈합니다.
사용자 삽입 이미지
하단의 Add Localization 버튼을 클릭하여 해당언어를 추가합니다.

사용자 삽입 이미지
XCode에서 좌측과 같이 Korean이 추가되어 있는 것을 확인할 수 있습니다. 프로젝트 디렉토리내에는 Korean.lproj 디렉토리가 추가되고 그 안에 한국어 버젼의 MainMenu.xib이 위치합니다. 각 nib마다 컨트롤의 타이틀과 텍스트를 해당언어로 변경하면 됩니다.

어플리케이션이 실행되면 시스템에 설정된 현재언어를 우선으로 해당 nib가 존재할 경우에는 그 언어의 nib 파일이 로드됩니다.

nib에 대해 자세한 사항은 ADCResource Programming Guide 문서를 참조하시기 바랍니다.

'기타' 카테고리의 다른 글

아이팟 터치 2세대  (8) 2008.10.10
OS X 루트계정 활성화  (6) 2008.10.08
OS X의 번들(Bundle) 둘러보기  (2) 2008.08.28
맥 OS X에서 CHM 파일 보기  (4) 2008.08.19
ClassDumper - 응용프로그램 Class Viewer  (0) 2008.08.07
AND