저번 스레드글에 이어서 NSOperation에 대해서 조금 덧붙여 이야기 해보고려고 한다. 자세한 내용은 애플 개발자문서에 너무도 자세히 나와 있으므로 여기서는 그냥 이렇게 움직인다라는 설명정도만 하려고 한다.




Cocoa에서는 스레드 병렬처리를 위한 고수준 인터페이스를 제공하고 있다. 뭐냐하면 바로  GCD와NSOperation/NSOperationQueue이 두가지 이다.

이 두가지 방법으로 멀티스레드처리가 가능한데 이 방법의 장점은 직렬큐를 이용하거나(GCD의 경우) 큐의 태스크처리를 지정하는 방법등으로 간단히 골치아픈 데드락을 피할 수 있다. ( 다만 리얼타임처리 요하는 경우에는 스레드를 이용하는편이 낫다. 아주 빠른속도의 처리를 위한 특수한 경우가 해당될듯 하다.)





NSOperation/NSOperationQueue은 내부적으로는 GCD로 구현되어 있다고 한다. 결론적으로 NSOperation/NSOperationQueue는 GCD의Objective-C랩퍼 클래스 인 것이다.

GCD에 비교해서NSOperation/NSOperationQueue가 고수준처리가 가능하다는것은 이전 글에서도 잠깐 언급했지만 얻는만큼 잃는것도 있는법. 그대신 그만큼 오버해드가 발생한다고 한다. 오버해드가 발생할만한 코드를 짜본 적이 없으므로 아직은 잘 모르겠다.

아무튼 그런점의 유념해서 개발시에 GCD를 사용할 것인지를 판단해야 할것이다.


그럼 먼저 NSOperation/NSOperationQueue 에 대해서 설명을 하자면,


NSOperation/NSOperationQueue의 사용방법



NSOperation/NSOperationQueue의 기본적인 개념은 태스크(NSOperation)를 큐(NSOperationQueue)에 추가하여 실행하는 방식으로 설계되어 있다.

코드를 보는편이 이해가 빠를지도..아래의 코드를 보자.


#import <Foundation/Foundation.h>


NSBlockOperation* createAnOperation()

{

    // NSBlockOperation 복수의 블록을 가질 있고 그것을 병렬로 실행 시킬수 있다.

    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock: ^{

        [NSThread sleepForTimeInterval:1];

        NSLog(@"Done: 1");

    }];

    [op addExecutionBlock: ^{

        [NSThread sleepForTimeInterval:1];

        NSLog(@"Done: 2");

    }];

    [op addExecutionBlock: ^{

        [NSThread sleepForTimeInterval:1];

        NSLog(@"Done: 3");

    }];

    return op;

}


int main(int argc, char* argv[])

{

    @autoreleasepool {

        NSArray *ops = @[createAnOperation(), createAnOperation()];

        NSOperationQueue *queue = [[NSOperationQueue allocinit];

        // 여기서 오퍼레이션을 직렬로 실행하기 위해서 오퍼레이션 동시실행수를 1 설정

        [queue setMaxConcurrentOperationCount:1];

        // waitUntilFinishedYES 하면 대기한다.

        [queue addOperations:ops waitUntilFinished:NO];

        // 큐에 직접 블록오퍼레이션을 추가하는 것도 가능하다.

        [queue addOperationWithBlock: ^{

            NSLog(@"All done!");

        }];

        // 전부 오퍼레이션이 끝날때까지 대기.

        [queue waitUntilAllOperationsAreFinished];

    }

    return 0;

}


결과는 아래와 같이 출력된다.

2015-03-17 16:04:47.026 TestApp3[5652:1449633] Done: 1

2015-03-17 16:04:47.026 TestApp3[5652:1449632] Done: 2

2015-03-17 16:04:47.026 TestApp3[5652:1449638] Done: 3

2015-03-17 16:04:48.032 TestApp3[5652:1449632] Done: 3

2015-03-17 16:04:48.032 TestApp3[5652:1449638] Done: 1

2015-03-17 16:04:48.032 TestApp3[5652:1449633] Done: 2

2015-03-17 16:04:48.032 TestApp3[5652:1449632] All done!




NSBlockOperation는 표준으로 준비되어 있는 NSOperation의 자식클래스이다. 설명에는 NSBlockOperation만 예를 들었지만 NSInvocationOperation이라는 Target, Selector를 지정해 줄 수 있는 타입도 있다. 또한 NSOperation를 상속받아 커스텀할 수 있도 있다. 더 알고싶다면 애플 개발자 문서를 참조하자.


NSOperationQueue은 기본적으로 GCD라는 병렬큐이다. 위의 예에서는 setMaxConcurrentOperationCount:1로 설정함으로써 직렬큐처럼(태스크가 하나만 처리되므로 결과적으로는 직렬큐) 사용할 수도 있다. 그와 반대로 복수개의 오퍼레이션을  추가하여 이것을 병렬로도 실행 할 수 있다. 


하나 더 유용한 기능이 있는데 NSOperation은 오퍼레이션간에 의존관계를 설정하는 것도 가능하다. 위의 코드는 직렬관계의 오퍼레이션을 동시에 실행하는수를 제한했지만 아래의 코드는 의존관계를 이용하여 실행하는것도 가능하다.


#import <Foundation/Foundation.h>


int main(int argc, char* argv[])

{

    @autoreleasepool {

        NSOperation *op1 = [NSBlockOperation blockOperationWithBlock: ^{

            [NSThread sleepForTimeInterval:1];

            NSLog(@"Done: 1");

        }];

        NSOperation *op2 = [NSBlockOperation blockOperationWithBlock: ^{

            [NSThread sleepForTimeInterval:1];

            NSLog(@"Done: 2");

        }];

        [op2 addDependency: op1]; // op2op1 의존

        NSOperationQueue *queue = [[NSOperationQueue alloc] init];

        [queue addOperations:@[op1, op2] waitUntilFinished:NO];

        [queue waitUntilAllOperationsAreFinished];

    }

    return 0;

}


여기서는 설명하지 못했지만 각각의 오퍼레이션의 우선도를 설정한다던지, 완료시에 콜백으로 불려지는등등 블록을 설정하는것도 가능하다. 실제로 이 클래스를 사용해보면 NSThread를 이용하는것보다 훨씬 간편하다는것을 알수 있다.

게다가 멀티스레드에서 제일 골치아픈 배타적제어 문제도 직렬큐를 이용하면 간단히 해결할 수 있으므로 일석이조라고 할 수 있겠다.




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

댓글을 달아 주세요

최근 앱을 만들때 중요도에서는 떨어지지만 반드시 들어가는 기능을 꼽는다면 튜토리얼(Help) 화면일 것이다.

아무도 중요하게 생각안하는 이 화면이 릴리스 거의 막판이되면 디자인때문에 종종 귀찮아지곤 한다.

예를 들어 "이 앱에 대해서 자세히 알고 싶으시면 여기를 클릭하세요" 라는 문구에 "여기"를 클릭하면 화면이동을 하게 한다던지 사파리로 넘겨준다는가 하는 단순한  것들이다.


이런기능들을 예전엔 UIWebView로 구현했었다. (네이티브 화면이동의 경우엔 딜리게이트를 이용하여 후킹하는 방법으로)


그러나 링크기능때문에 UIWebView를 사용한다는건 마치 "Hello World"를 출력하기 위해 멀티스레드로 코드를 짜는것과 같이 미련한 짓이라는 생각이 들어 간단한 방법을 찾아보기로 했다.






역시나! 구현하는 방법이 있었다.


첫번재 방법. UILabel을 이용한 링크 구현이다.

지금까지 조사한 결과 모양만 흉내가능하고 실제 링크터치 이벤트는 못만드는듯 하다.


NSString *text = @"한지민짱! 그럼 여길 클릭하세요.";

    NSRange find_range = [text rangeOfString:@"여길"];

    linkLabel.userInteractionEnabled = YES;

    NSMutableAttributedString *textString = [[NSMutableAttributedString alloc] initWithString:text

                                                                                   attributes:@{ NSForegroundColorAttributeName:[UIColor blackColor],

                                                                                                 NSFontAttributeName:[UIFont systemFontOfSize:17.f]}];

    NSMutableAttributedString *linkString = [[NSMutableAttributedString alloc]

                                             initWithString:[text substringWithRange:find_range]

                                             attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:24.f],

                                                          NSLinkAttributeName:@"native://popuplink/?touched"}];

    

    

    //UILabl 이용하는 방법..그러나 클릭안됨.

    [textString replaceCharactersInRange:find_range withAttributedString:linkString];

     linkLabel.attributedText = textString;


두번째 방법은 UITextView로 링크 구현하는 것이다. 물론 delegate를 이용하면 UIWebView처럼 이벤트 후킹이 가능하다.


    NSString *text = @"한지민짱! 그럼 여길 클릭하세요.";

    NSRange find_range = [text rangeOfString:@"여길"];

    linkLabel.userInteractionEnabled = YES;

    NSMutableAttributedString *textString = [[NSMutableAttributedString allocinitWithString:text

                                                                                   attributes:@{ NSForegroundColorAttributeName:[UIColor blackColor],

                                                                                                 NSFontAttributeName:[UIFont systemFontOfSize:17.f]}];

    NSMutableAttributedString *linkString = [[NSMutableAttributedString alloc]

                                             initWithString:[text substringWithRange:find_range]

                                             attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:24.f],

                                                          NSLinkAttributeName:@"native://popuplink/?touched"}];

    

    [textString replaceCharactersInRange:find_range withAttributedString:linkString];

     linkTextView.attributedText = textString;


//UITextView 이용한 방법. 클릭이벤트 발생됨.

    linkTextView.scrollEnabled = NO;

    linkTextView.editable = NO;

 linkTextView.selectable = YES;

    linkTextView.textContainer.lineFragmentPadding = 0;

    linkTextView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);

    linkTextView.delegate = self;

    linkTextView.attributedText = textString;

    

    //사이즈 맞춤.

    [linkTextView sizeToFit];

    

    //사이즈를 맞추기 위해 이거 메소드를 호출해줘야 한다고함.

    [linkTextView layoutIfNeeded];

    [linkTextView setTextContainerInset:UIEdgeInsetsMake(0, 0, 0, 0)];

    

    //텍스트뷰는 텍스트를 넣어둔후 폰트설정을 해야 적용된다능...

    linkTextView.font = [UIFont systemFontOfSize:13.f];

}



#pragma mark - UITextView delegate methods

- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)url inRange:(NSRange)characterRange

{

    NSLog(@"%s", __FUNCTION__);

    

    NSString *scheme = [url scheme];

    NSString * query = [url query];

    NSURL *path = [url absoluteURL];

    NSLog(@"scheme : %@   query : %@   path : %@ ", scheme , query, path);

    

    

    return NO;

}








돌려보니 잘 된다. 웹뷰쓰는것보다는 기분상 더 좋아 보인다.




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

댓글을 달아 주세요

많은경우 여러개의 리턴값을 받기위해 참조를 이용한 값전달 받기를 한다. 이러한 방식은 에러를 전달할 때 가장 많이 사용되므로 아마도 익숙할 것이다. 


보통 이런방식으로..


//output parameter를 사용한 메소드

- (BOOL)huga:(NSString*)s error:(NSError**)outError {


    if (outError) {

        *outError = [NSError errorWithDomain:@"domine" code:-1 userInfo:nil];

        return NO;

    }

    return YES;

}



//사용하기

NSError *error = nil;

BOOL success = [self huga:@"hahaha" error:&error];



그런데 유심히보면 포인터를 두개를 사용한 참조를 이용하는데 왜 그럴까? 그 이유를 내 나름대로 해석하자면 이렇다.

만약 hoge라는 메소드를 만들고 출력 파라메타를 이용한다면 이런식으로 구현할 것이다.


- (void)hoge:(NSString **)outValue {

    *outValue = @"Kjcode!";

}


//값을 줄때 요렇게..

NSString *str = nil;

[obj hoge:&str];




이런식으로 구현된 소스는 컴파일러에서 아래와 같이 해석한다고 한다. 


//complier code.

- (void)hoge:(NSString *__autoreleasing *)value {

    *value = @"Test";




//complier code.

NSString *__strong s = nil;

NSString *__autoreleasing temp = str;

[obj hoge:&temp];

str = temp;




이 이야기는 참조로 넘어가는 값을 __autoreleasing을 해줌으로 인하여 ARC를 적용시켜주는 것이다.

뿐만아니라 시각적으로도 개발자가 포인터에 값을 넣어주어야 하기때문에 반환되는 파라메터라는 것을 금방 눈치챌 수 있을것이다.


만약 포인터 딸랑 하나로 output파라메타를 사용하는 메소드를 만들었다면 문제가 생길소지가 다분하다.

예를 들어 플젝 개발자가 경험이 부족하거나 아니면 경험자라도 메소드안을 꼼꼼히 보지 않는다면 아무생각없이 그 파라메터를 strong프로터피에 값을 전달시킬수도 있을것이다.

그럼 그순간 순환참조가 일어날 것이고 영원히 메모리에서 젖은낙엽처럼 붙어있게 될것이고 짜증나는 야근을 경험할지도 모른다.



휴~ 지금 내가 하는 플젝에서도 출력파라메터를 잔뜩 사용해놓구선 메모리를 잔뜩 잡아먹는 코드를 만들어놔서 고생중인데 물론.. 그 당사자는 이미 그만둔 상태이므로 리팩토링은 남은자의 몫이 되어 버렸다.


암튼 개고생중. (여기서 "개"라는 표현은 욕이 아닌 강조를 위한 최상급 표현임.)



참고사이트

https://developer.apple.com/library/ios/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html




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

댓글을 달아 주세요

  1. addr | edit/del | reply 김기봉 2015.02.03 08:26

    형 이부분
    //complier code.
    NSString *__strong s = nil;
    NSString *__autoreleasing temp = str;
    [obj hoge:&temp];
    str = temp;

    에서
    NSString *__strong s = nil; -> str = nil; 이죠?


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


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

댓글을 달아 주세요


iOS스터디(KJ-CODE) 발표자료.




메모


컨테이너뷰에 있는 컨트롤러를 전환해주는 방법.

코드상에서의 순서


//추가한다면

[self addChildViewController:viewControllerB]; 

[self.view addSubview:viewControllerB.view];

viewControllerB.view.frame = //크기결정해줘야함.

[viewControllerB didMoveToParentViewController:self]; 


//삭제일경우

[viewControllerA willMoveToParentViewController:nil]; 

[viewControllerA.view removeFromSuperview];     

[viewControllerA removeFromParentViewController];


didMoveToParentViewController가 콜백이 아닌 직접호출일까? 그이유는 애니메이션처리 때문에.. 

애니메이션이 언제 끝날지 모르므로...



//toViewController를 컨테이너에 추가

[self addChildViewController:toViewController];


//from부터to로 화면전환시 트랜젝션을 실행

[self transitionFromViewController:fromViewController

                   toViewController:toViewController

                           duration:1.0

                            options:UIViewAnimationOptionTransitionNone

                         animations:^{

                             //애니메이션 처리가 필요하다면...

                         }

                         completion:^(BOOL finished) {

                             // 추가가 완료되면 명시적으로 호출

                             [toViewController didMoveToParentViewController:self];

                         }];



컨테이너뷰

https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/BasicViewControllers/BasicViewControllers.html#//apple_ref/doc/uid/TP40007457-CH101-SW1


라이프사이클문서

https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/RespondingtoDisplay-Notifications/RespondingtoDisplay-Notifications.html#//apple_ref/doc/uid/TP40007457-CH12-SW1


컨테이너뷰 설명

http://qiita.com/paming/items/d8a29d644c994ce60d6a

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

댓글을 달아 주세요

스토리보드가 도입되면서 스토리보드상에서 콘트롤러와 콘트롤러를 연결해주기만 하면 전면전환이 되는 편리한 기능이 추가 되었다.

(모달,푸쉬 그리고 애니메이션 옵션등 약간의 설정이 필요하긴 하다.)


예전에 코드로 전부 써주어야 했던것이 스토리보드로 흡수되면서 기본적인 화면전환의 경우는 코드없이도 간단하게 구현이 가능하다.

물론 몇번의 마우스클릭의 수고는 필요.


만약 Modal, Push 형태 이외의 화면전환이 필요한 경우는 화면전환 전용클래스(UIStoryboardSegue)를 이용하여 정의(커스텀)해 줄수 있는데

화면전환에 관련된 코드가 세그에 정의됨으로써 보다 가독성도 높일 수 있다.


스토리보드 사용법에 대해서는 다른 블로그에도 많이 소개 되어 있으므로 세그에(Segue)를 커스텀하여 애니메애션을 주는 방법을 몇가지 소개햐려 한다.



우선 UIStoryboardSegue를 상속받는 세그에(Segue)클래스를 생성한다.


@interface KJCodeMenuSegue : UIStoryboardSegue


@end




정의에는 perform메소드를 재정의 해준다. 여기에서 애니메이션과 화면전환을 정의 해준다.



- Push, Pop 세그에 를 커스텀 할 경우 (modal의 경우도 마찬가지 방법으로 처리해도 된다.)


- (void)perform {

    

//하나의 클래스에 여러개의 처리를 위해 id 이용하여 분기해도 .

NSLog(@"%@", self.identifier);

//뷰콘트롤러 인스턴스 취득

    UIViewController *sourceViewController = (UIViewController *)self.sourceViewController;

    UIViewController *destinationViewController = (UIViewController *)self.destinationViewController;

    

    if (self.identifier isEqualToString:@"menuPush") {

        

        //화면전환 애니메이션

        [UIView transitionWithView:sourceViewController.navigationController.view

                          duration:0.5f

                           options:UIViewAnimationOptionTransitionCrossDissolve

                        animations:^{

                            //실제로 화면전환코드를 작성. 애니메이션은 겹치지 않도록 NO 설정

                            [sourceViewController.navigationController pushViewController:destinationViewController animated:NO];

                        }

                        completion:nil];

        

    }else if(self.identifier isEqualToString:@"menuPush") {

        

        //화면전환 애니메이션

        [UIView transitionWithView:sourceViewController.navigationController.view

                          duration:0.5f

                           options:UIViewAnimationOptionTransitionCrossDissolve

                        animations:^{

                            //실제로 화면전환코드를 작성. 애니메이션은 겹치지 않도록 NO 설정

                            [sourceViewController.navigationController popViewControllerAnimated:NO];

                        }

                        completion:nil];

        

    }

    

}



- 컨테이너뷰에서의 임베디드 세그에(embedSegue)를 커스텀할 경우


- (void) perform {

    

    NSLog(@"%@", self.identifier);

    

    UIViewController *containerViewController = (UIViewController *) self.sourceViewController;

    UIViewController *nextViewController = (UIViewController *) self.destinationViewController;

    UIViewController *currentViewController = (UIViewController *) containerViewController.currentViewController;

    

    //컨테이너 콘트롤러에 화면전환 뷰콘트롤러를 자식뷰콘트롤러로 추가한다.

    [containerViewController addChildViewController:nextViewController];

    // 현재뷰콘트롤러에 이동(삭제) 라는것을 통지한다.

    [currentViewController willMoveToParentViewController:nil];

    

    // 전환될 뷰콘트롤러와 현재 뷰콘트롤러의 크기를 조정한다.

    nextViewController.view.frame = currentViewController.view.frame;

    

    // 화면전환 애니메이션 정의한다.

    [containerViewController transitionFromViewController:currentViewController

                                         toViewController:nextViewController

                                                 duration:0.1f

                                                  options:UIViewAnimationOptionTransitionCrossDissolve

                                               animations:^{

                                                   

                                               }

                                               completion:^(BOOL finished) {

                                                   //컨터이너뷰 안에서의 화면전환처리를 정의한다.

                                                   containerViewController.currentViewController = nextViewController;

                                                   [currentViewController removeFromParentViewController];

                                                   [nextViewController didMoveToParentViewController:containerViewController];

                                               }];

    

}



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

댓글을 달아 주세요

iPhone 캘린더에 등록된 내용을 보여주거나 표시해주기 위해서는 EventKit을 이용하면 된다. 캘린더 이외에 리마인더도 제어 할 수 있으나 여기선 캘린더 등록 위주로 간단히 설명할 생각이다.



사실 애플도큐먼트만 봐도 굳이 설며이 필요없는 간단한 내용이지만 중요한 포인트는 

EventKit을 사용할 클래스는 싱글톤 클래스로 만들어 사용할 것이라는 내용.

(도큐먼트에 보면 로드하는데 시간이 걸리므로 로드 후 사용하는것이 좋다고 소개되어 있다.)

그러므로 일단 여기서도 싱글톤으로된 매니져클래스를 샘플로 만들어 보았다.


자세한 사용법은 아래의 링크를 참고하면 된다.(일본어임)

https://developer.apple.com/jp/devcenter/ios/library/documentation/EventKitProgGuide.pdf



본론으로 들어가서,

물론, EventKit, EventKitUI 두개의 프레임워크를 먼저 추가해 주어야 한다.

(이름만 봐서도 알겠지만 EventKitUI는 이벤트 편집을 위한 UI프레임워크이다.)






먼저 EventManager.h

필요한 이벤트킷을 임포트 해준 다음 싱글톤 인스턴스 메소드(sharedInstance)와 이벤트 검색을 위한 메소드(fetchEvent...) 그리고 이벤트 추가, 수정을 위한 화면표시용 메소드(eventEditController...)를 선언해 준다.



#import <EventKit/EventKit.h>

#import <EventKitUI/EventKitUI.h>


@interface EventManager : NSObject


+ (EventManager *)sharedInstance;


- (NSArray *)fetchEvent;

- (NSArray *)fetchEventStartDate:(NSDate *)startDate EndDate:(NSDate *)endDate;

- (void)eventEditController:(id)owner event:(EKEvent *)event;


@end




그다음 EventManager.m

싱글톤 클래스를 구현해 주기 위한 메소드들을 추가해 준 다음 헤더파일에 선언된 메소드를 정의해 준다.

그리고 이벤트정보에 접근하기 위한 이벤트스토어를 프로퍼티(EKEventStore)로 선언.

마지막으로 이벤트 추가, 수정을 위한 EventKitUI를 사용하기 위한 프로토콜을 명시해 준다.



@interface EventManager() <EKEventEditViewDelegate>


@property (nonatomicstrongEKEventStore *store;


@end



@implementation EventManager 


static EventManager *_sharedInstance;


#pragma mark - singleton

+ (EventManager *)sharedInstance

{

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        _sharedInstance = [[self alloc] init];

    });

    

    return _sharedInstance;

    

}


+ (id)allocWithZone:(NSZone *)zone {

    

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        _sharedInstance = [super allocWithZone:zone];

    });

    

    return _sharedInstance;

    

}


- (id)init {

    self = [super init];

    if (self) {

        _store = [[EKEventStore alloc] init];

        

        if([EKEventStore authorizationStatusForEntityType:EKEntityTypeReminder] == EKAuthorizationStatusAuthorized) {

            //접근거부

        } else {

            EKEventStore *eventStore = [[EKEventStore alloc] init];

            [eventStore requestAccessToEntityType:EKEntityTypeReminder

                                       completion:^(BOOL granted, NSError *error) {

                                           if(granted) {

                                               // 처음 접근시 대화장자가 표시되는 경우 OK버튼을 누르면 여기로 들어옴

                                           } else {

                                               dispatch_async(dispatch_get_main_queue(), ^{

                                                   //접근권한 없음

                                                   UIAlertView *alert = [[UIAlertView alloc]

                                                                         initWithTitle:NSLocalizedString(@"This app does not have access to you reminders.", nil)

                                                                         message:NSLocalizedString(@"To display your reminder, enable [YOUR APP] in the \"Privacy\" → \"Reminders\" in the Settings.app.", nil)

                                                                         delegate:nil

                                                                         cancelButtonTitle:NSLocalizedString(@"OK", nil)

                                                                         otherButtonTitles:nil];

                                                   [alert show];

                                               });

                                           }

                                       }

             ];

        }

    }

    

    return self;

}


- (id)copyWithZone:(NSZone *)zone

{

    return self;

}


- (void)dealloc {

    

}


#pragma mark - Event fetch

- (NSArray *)fetchEvent {

    

    //기본값은 일주일로 잡아준다.

    

    NSCalendar *calendar = [NSCalendar currentCalendar];


    // 시작시간 작성

    NSDateComponents *oneWeekAgoComponents = [[NSDateComponents alloc] init]; oneWeekAgoComponents.day = -7;

    NSDate *oneDayAgo = [calendar dateByAddingComponents:oneWeekAgoComponents

                                                  toDate:[NSDate date]

                                                 options:0];

    // 종료시간 작성

    NSDateComponents *oneYearFromNowComponents = [[NSDateComponents alloc] init]; oneYearFromNowComponents.year = 1;

    NSDate *oneYearFromNow = [calendar dateByAddingComponents:oneYearFromNowComponents

                                                       toDate:[NSDate date]

                                                      options:0];

    

    return [self fetchEventStartDate:oneDayAgo EndDate:oneYearFromNow];

    

}


- (NSArray *)fetchEventStartDate:(NSDate *)startDate EndDate:(NSDate *)endDate {

    

    // 조건생성

    NSPredicate *predicate = [_store predicateForEventsWithStartDate:startDate

                                                            endDate:startDate

                                                          calendars:nil];

    // 조건에 따른 패치 실행.

    return [_store eventsMatchingPredicate:predicate];

    

}


#pragma mark - Event editing controller

- (void)eventEditController:(id)owner event:(EKEvent *)event {


    //event가 nil 일 경우에는 신규이벤트 등록이 된다.

    

    EKEventEditViewController *eventEditViewController = [EKEventEditViewController new];

    eventEditViewController.editViewDelegate = self;

    eventEditViewController.event = event;

    eventEditViewController.eventStore = _store;

    

    [owner presentViewController:eventEditViewController animated:YES completion:nil];

}


#pragma mark - EKEventEditViewDelegate

- (void)eventEditViewController:(EKEventEditViewController *)controller didCompleteWithAction:(EKEventEditViewAction)action

{

//UI에서 이벤트를 받을경우 처리할 딜리게이트 메소드.


NSError *error = nil;

switch (action) {

case EKEventEditViewActionCanceled:

break;

case EKEventEditViewActionSaved:

[controller.eventStore saveEvent:controller.event span:EKSpanThisEvent error:&error];

break;

case EKEventEditViewActionDeleted:

[controller.eventStore removeEvent:controller.event span:EKSpanThisEvent error:&error];

break;

default:

break;

}

    

[controller dismissViewControllerAnimated:YES completion:nil];

}


@end



기술한 샘플코드는 전혀 테스트를 하지 않았기 때문에 동작안할 수도 있으므로 참고만 하시길...






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

댓글을 달아 주세요

요즘 너무 자주사용하므로 메모.


애플공식 참고링크

https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/BinaryData/Tasks/WorkingBinaryData.html


NSData 를 NSString 으로

NSString *str= [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];


NSString 을 NSData 로

NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];



unsigned char array 를 NSData 로

NSUInteger size = // some size

unsigned char array[size];

NSData* data = [NSData dataWithBytes:(const void *)array length:sizeof(unsigned char)*size];


NSData 를 unsigned char array 로

NSUInteger size = [data length] / sizeof(unsigned char);

unsigned char* array = (unsigned char*) [NSData bytes];


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

댓글을 달아 주세요

Objective-c에서 KVO를 구현해야 할 경우가 있어 메모해 둔다.

KVO를 간단히 설명하자면 오브젝트의 값(프로퍼티)를 감시하여 이벤트를 받을 수 있는 기능이다.

감시할때 감시하는 옵션에는 여러가지가 있으나 그중 제일 많이 사용하는 값변경에 대한 감시이다.


값을 감시할 때.

// NSYunjiClass 인스턴스 생성하여 "onAir" 값을 감시할 경우에는

NSYunji *yunjilove = [[NSYunji alloc] init];

[yunjilove addObserver:self forKeyPath:@"onAir" options:NSKeyValueObservingOptionNew context:nil];


감시값이 변경될경우 콜백 되는 메소드

//값이 변경되었을때 콜백되는 메소드

- (void)observerValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context


    {

        NSLog(@"keyPath:[%@] change:[%@]",keyPath, [change description]);

        

        //변화한 값을 판별

        if ([keyPath isEqual:@"onAir"]) {

            //처리할 내용을 코딩.

            

        }

    }


파라메터 설명


keyPath 갑시할 키.(프로퍼티)

object         감시대상 오브젝트 (NS윤지러브)

change        변화한 값정보.

context 임의의 오브젝트 nil

(여기서 context는 nil을 넣어주면된다. 감시자에게 참조값을 전달할 수 있다는데 지금 설명에서는 패스.)



값 감시 삭제할 때.

감시(Observer)를 했으면 반드시 삭제를 해야 한다. 물론 중복해서 삭제를 하거나 하면 에러가 발생하므로 주의.


[yunjilove removeObserver:self forKeyPath:@"onAir"];


수동통지 할때.

감시대상이 된 프로퍼티는 전부 통지되므로 통지가 많아지는 경우가 발생한다.

이 경우엔 해당 프로퍼티를 수동통지로 바꾸어서 한번에 통지하던지 하는 방법도 가능하다.


수동으로 통지하는경우에는 automaticallyNotiviesObserversForKey: 메소드를 실행하여 수동통지 되도록 코딩해 준다.


+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey

    {

        BOOL automatic = NO;

        if ([theKey isEqualToString:@"onAir"]) {

            automatic = NO;

        }

        else {

            automatic = [super automaticallyNotifiesObserversForKey:theKey];

        }

        

        return automatic;

    }



덛붙여,willChangeValueForKey: didChangeValueForKey: 의 메소드를 setter에다가 구현해 줄 필요가 있다.


수동통지를 구현하는 setter메소드

- (void)setOnAir:(NSString *)onAir

    {

        [self willChangeValueForKey:@"onAir"];

        _onAir = onAir;

        [self didChangeValueForKey:@"onAir"];

    }




의외로 유용하게 쓸일이 많은 기능이다.



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

댓글을 달아 주세요

아이폰에서 챠트 또는 그래프를 그려야 하는 경우가 종종 있다. 개인적으로 만드는거라면 오픈소스를 가져다 쓰는게 가장 편하지만 디자인너의 요구사항이 까다롭다면 어쩔수 없이 직접 그려줘야 한다.


그리는 방법은 다음과 같은 순서로 작성한다.


1. UIView를 상속받은 차트용 뷰를 작성

2. 제대로 만들었다면 주석처리된 drawRect 메소드가 나오는데 이걸 주석을 해제.

3. drawRect메소드에 그리고 싶은 코드를 넣어줌


*참고로 drawRect메소드는 첫 화면에 뿌려질때 불려지는 콜백함수이다.

인위적으로 불러줘야 할 경우에는 setNeedDisplay라는 메소드를 호출해 준다.



그릴때 자주쓰는 기본적인 메소드를 만들어 실장해 보았다.


// Only override drawRect: if you perform custom drawing.

// An empty implementation adversely affects performance during animation.

- (void)drawRect:(CGRect)rect

{

    // Drawing code

    CGContextRef ctx = UIGraphicsGetCurrentContext();

    

    [self drawLine:ctx

         color:[UIColor grayColor]

         width:1.0f

        startPoint:CGPointMake(10, 20)

          endPoint:CGPointMake(100, 40)];

    

    [self drawCircle:ctx

           fillColor:[UIColor blackColor]

         strokeColor:[UIColor grayColor]

              radius:30.0f

         CenterPoint:CGPointMake(250, 40)];

    

    [self drawText:ctx text:@"Graph TEST!!" size:13.0f point:CGPointMake(100, 100)];

}


- (void)drawLine:(CGContextRef)ctx color:(UIColor *)c width:(float)w startPoint:(CGPoint)sp endPoint:(CGPoint)ep {


    CGContextSetStrokeColorWithColor(ctx, c.CGColor);

    CGContextBeginPath(ctx);

    CGContextMoveToPoint(ctx, sp.x, sp.y);

    CGContextAddLineToPoint(ctx, ep.x, ep.y);

    

    CGContextStrokePath(ctx);


}


- (void)drawCircle:(CGContextRef)ctx fillColor:(UIColor *)fc strokeColor:(UIColor *)sc radius:(float)r CenterPoint:(CGPoint)cp {

    

    // Set the width of the line

    CGContextSetLineWidth(ctx, 2.0);

    

    //Make the circle

    // 150 = x coordinate

    // 150 = y coordinate

    // 100 = radius of circle

    // 0   = starting angle

    // 2*M_PI = end angle

    // YES = draw clockwise

    CGContextBeginPath(ctx);

    CGContextAddArc(ctx, cp.x, cp.y, r, 0, 2*M_PI, YES);

    CGContextClosePath(ctx);

    

    //color

    CGContextSetFillColorWithColor(ctx, fc.CGColor);

    CGContextSetStrokeColorWithColor(ctx, sc.CGColor);

    

    // Note: If I wanted to only stroke the path, use:

    // CGContextDrawPath(context, kCGPathStroke);

    // or to only fill it, use:

    // CGContextDrawPath(context, kCGPathFill);

    

    //Fill/Stroke the path

    CGContextDrawPath(ctx, kCGPathFillStroke);

    

}


- (void)drawCircle:(CGContextRef)ctx color:(UIColor *)c radius:(float)r CenterPoint:(CGPoint)cp {

    

    [self drawCircle:ctx fillColor:c strokeColor:c radius:r CenterPoint:cp];

}


- (void)drawText:(CGContextRef)ctx text:(NSString *)t size:(float)s point:(CGPoint)p {

    

    CGContextSetTextDrawingMode(ctx, kCGTextFill); // This is the default

    

    [[UIColor redColor] setFill]; // This is the default

    

    [t drawAtPoint:CGPointMake(p.x, p.y)

               withAttributes:@{NSFontAttributeName:[UIFont fontWithName:@"Helvetica"

                                                                    size:s]}];

    

    

    

}





출력결과




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

댓글을 달아 주세요