테이블 뷰를 사용할 때마다 가장 손이 많이 가는 부분이 UITableViewCell이다.
이 cell을 대부분 상속을 받아 커스텀으로 사용을 하게 되는데 커스텀의 모양이 생각대로 안나오기 때문이다.

몇 차례 삽질을 한 결과 몇 가지 결론을 얻었다.

우선 cell의 subview는 몇가지 나눠져 있다.

cell.imageView
cell.text
cell.contentView
cell.accessoryView

등으로 구성되어져 있다.

우선 ios5가되면서 imageView와 text 뷰는 deprecate 체크가 되어있어 사용하면 안될 것이다. 그럼 위 imageView와 text뷰를 대체할 서브뷰를 생성하기 위해서 커스텀 뷰내에 UIImageView 객체와 UILabel 객체를 추가하였다.

하지만 여기서 가장 큰 걸림돌이 초기화 할때 위치와 layoutSubviews가 호출될 때하고 cell의 값이 너무 달라져 버려 어느 위치에 서브뷰들을 위치 시켜야 할지 혼란에 빠졌다. 여기에서 몇가지 규칙을 찾아냈다.

1. 초기화될때 cell의 높이는 정해져 있다.

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier

위와 같이 초기화를 시켜줄때 쎌의 높이 넓이 값은 따로 지정하지 않아도 그 값이 정해져 있다. (심지어 tableView의 delegate를 통해 설정된 값 역시 반영되지 않는다. self.bounds의 값을 찍어보면 알수 있을 것이다.)

그러므로 초기화 할때는 각 서브뷰들의 객체 할당만 시켜주면 될것이다.

높이, 넓이 값 뿐만 아니라 다른 값들 역시 쎌의 속성에 제대로 반영되지 않는다. 배경색 이나 기타 cell에 관련된 값이 제대로 반영되지 않는다.
(확실 하지 않지만 subview들의 속성들도 일부 변경된다. opaque 상태등등...)

결론은 초기화 할때 많은 것을 지정할 필요가 없다. 단순히 객체만 생성해주면 된다.

2. 실제 위치는 layoutSubviews에서 결정된다.

위 메소드에서 실제 위치가 결정되는데 보통 일반 뷰에서는 [super layoutSubviews]를 잘 실행하지 않는 경우가 종종있다. 하지만 cell 에서는 위 메소드를 실행 유무에 따라 그 결과 값이 달라진다.

우선 위 메소드를 실행하지 않게 되면 cell의 기본적인 subview들이 제대로 위치를 잡지 못한다. 즉 contentView나 accessoryView, 심지어 cell의 line 역시 제대로 그려지지 않게 된다. 즉 cell의 완전히 커스텀해서 사용하고 싶을때는 상위 클래스의 layoutSubviews를 실행하지 않는다.

하지만 기본적으로 제공하는 accessoryView나 cell의 line을 사용하면 편한 경우가 종종 있다. 이럴 경우 상위 클래스의 layoutSubviews를 꼭 실행해야한다. 하지만 이 메소드를 실행하다고해서 뷰를 마음대로 컨트롤하기는 거의 불가능 하다 (정신건강에도 이롭다.) 왜냐면 사용자에게 노출되지 않은 부분에서 위치 및 높이 넓이 값이 재정의 되기 때문에 아무리 오버라이드를 해도 변경하기 힘들다.

그리고 layoutSubviews가 실행될때에는  tableView에서 설정한 높이 값이 제대로 전달되어 들어온다. 이 높이 넓이 값을 이용하여 초기화에서 만든 서브뷰들의 위치를 재정의 하여 원하는 위치에 뷰를 옮긴다. 여기서 주의해야 할 점은 contentView인데 이 contentView 역시 위 메소드를 실행하면 값이 재정의되어 있는 상태이다. (안쓰이게되는 imageView와 text 값 등에 의해 위치가 변경된듯 하다)

이 재정의된 위치 값은 어차피 imageView 와 text는 안쓰이게 되므로 위치를 변경한다.

또한 배경값이나 기타 쎌에 괄련된 속성 역시 여기서 지정해 준다. (tableView의 reusequeue를 거치면서 초기화때 cell에 지정한 속성 값들이 재정의되어버린듯 하다. 하지만 subview의 속성은 일부 유지된다.)

기본적인 이미지, 텍스트 cell예제


- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier

{

    self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];

    

    if (self) {
        // 서브뷰를 정의한다. 

        UIImageView * iconView = [[UIImageView alloc]init ];

        [self.contentView addSubview:iconView];

        i_FolderIcon = iconView;

        [iconView release];

        

        UILabel * folderName = [[UILabel alloc]init];

        [self.contentView addSubview:folderName];

        i_FolderName = folderName;

        [folderName release];

    }

    return self;

}


- (void) dealloc {

    self.cellColor = nil;

    [super dealloc];

}


- (void) layoutSubviews {

     // 쎌의 위치 및 속성등을 재정의한다.

    [super layoutSubviews];

    

    self.backgroundColor = self.cellColor;

    self.contentView.frame = self.bounds;

    

    CGRect frame = self.bounds;

    CGRect iconFrame = CGRectMake(0, 0, frame.size.height, frame.size.height);

    i_FolderIcon.frame = iconFrame;

    i_FolderIcon.opaque = YES;

    

    CGRect folderNameFrame = CGRectZero;

    folderNameFrame.origin = CGPointMake(iconFrame.size.width+5, 0);

    folderNameFrame.size = CGSizeMake(frame.size.width - (iconFrame.size.width+5), frame.size.height);

    i_FolderName.frame = folderNameFrame;

    i_FolderName.opaque = YES;

}


- (void) setFolderIcon:(UIImage *) iconImage {

    

    i_FolderIcon.image = iconImage;

    

}


- (void) setFolderName:(NSString *) folderName {

    

    NSString * name = [[NSString alloc]initWithString:folderName];

    i_FolderName.text = name;

    [name release];

}








 
tcpdump -i en1 -s 1500 -A 'tcp port 80'
요구사항에 따라 아이폰에 토스트 팝업을 만들일이 생겼다. (아이폰에 토스트 팝업이라니!!!)

그래서 여러 고민 끝에 결론은

어디에서나 화면에 보이는 UIWindow내에 UIView를 삽입하여 화면에 띄우고 애니메이션으로 출력후 종료하도록 하였다.

뷰를 삽입 후 애니메이션으로 출력, 종료까지 잘되었지만 발목을 잡은 것은 바로 회전이였다.

회전을 하게 되면 UIViewController에서 표시되는 뷰와 따로 놀게되고 회전할때도 각각 돌아가게되어 뷰의 모습이 깨지게 되었다.

확인 끝에 중요한 차이점을 알게 되었다

바로 뷰를 삽입하는 방식인다. 기존방식은 새로운 UIWindow를 생성하여 UIView를 입력하는 방식이다.

- (void) makeAlertWindow {

    UIWindow * statusWindow =[[UIWindow alloc]initWithFrame:self.bounds];

    statusWindow.windowLevel = UIWindowLevelAlert;

    statusWindow.hidden = NO;

    statusWindow.backgroundColor = COLOR_CLEAR;

    statusWindow.userInteractionEnabled = NO;

    [statusWindow makeKeyWindow];

    [statusWindow addSubview:self];

    self.alertWindow = statusWindow;

    [statusWindow release];

}


이렇게 만들고나서 회전문제를 처리하려고하니 위와 같은 많은 문제점이 생겼다. 

찾아본 결과 간단한 해결방법이 있었다.

- (void) makeAlertWindow {

    ///< 뷰를 윈도우에 추가하기 위해서 써야한다!!!!!    

    [[[UIApplication sharedApplication] keyWindow] addSubview:self];

}


이렇게 UIView를 삽입하고 나면 UIViewController의 회전과 UIView의 회전이 일치하게 되어 회전 문제를 처리하기 쉽게 되었다.

첨부파일로 소스 전체를 올린다.

'iOS' 카테고리의 다른 글

UITableViewCell 다루기  (0) 2012.02.06
TCP Dump  (0) 2011.12.29
UITableViewCell Custom 오른쪽 화살표 버튼 이미지  (0) 2011.12.27
유용한 오픈소스 링크  (0) 2011.12.21
ActionSheet 버튼 스타일 변경작업  (0) 2011.12.16

Cell에서 오른쪽 화살표 이미지를 접근하기위해 노력을 해보았지만 initWithStyle에서는 접근이 안되었다.

확인 결과 오른쪽 화살표 이미지는 cell.accessoryView을 통해 변경 가능했다.

하지만 cell의 drawRect 단계에서 기본 accessoryView를 확인하였을때 null이였다. -_-;

그래서 cell의 subview를 찍어본 결과 UIButton 객체가 하나 있었다. (initWithStyle 단계에서는 UIButton 객체가 없었다)

이 객체가 accessoryView가 null일 경우 accessoryView 처럼 작동하는 것 같다.

(accessoryView에 임의 버튼을 삽입할 경우 cell.subview의 버튼 객체가 변경된다)

 

'iOS' 카테고리의 다른 글

TCP Dump  (0) 2011.12.29
아이폰에서 토스트 팝업 만들기  (1) 2011.12.29
유용한 오픈소스 링크  (0) 2011.12.21
ActionSheet 버튼 스타일 변경작업  (0) 2011.12.16
UIView 캡쳐하기  (0) 2011.11.15

요구 사항 중에 ActionSheet의 삭제 버튼을 빨간색 버튼으로 변경해 달라는 요청이 들어왔다.

처음에는 버튼의 스타일을 바꾸면 되겠지라는 생각으로 버튼 스타일을 바꾸었으나 영 이상한 모양으로 나왔다

(배경색을 바꾸니 정말 사각형으로 빨간색이 되어버렸다) 

그래서 헤더 값을 찾아본 결과 눈에 띄는 프로퍼티를 찾았다.

@property(nonatomic) NSInteger destructiveButtonIndex;        // sets destructive (red) button. -1 means none set. default is -1. ignored if only one button

 
설명 중에 red 버튼이라는 말만 보고 한번 인덱스 값을 입력해보았다.

actionSheet.destructiveButtonIndex = 0;   // 삭제 버튼은 0번 인덱스이다.

 
역시 생각대로 빨간 버튼이 출력되었다

그리고 추가로 버튼에 대한 스타일을 변경하는 소스도 올린다.

- (void)willPresentActionSheet:(UIActionSheet *)actionSheet  // before animation and showing view

{    

    if(actionSheet.tag == /* 액션 시트를 구분하기 위한 태그 값*/ ){

         for (UIView* view in [actionSheet subviews]) {


            if ([view isKindOfClass:NSClassFromString(@"UIAlertButton")] == NO) { 

                continue;

            }    

            

            if ([view respondsToSelector:@selector(title)] == NO) {                

                continue;

            }  

            

            NSString * title = [view performSelector:@selector(title)];

            /// 원하는 버튼을 찾기 위하여 버튼의 텍스트 값을 비교하여 찾아낸다.            

            if ([title isEqualToString:@"?????"] && [view respondsToSelector:@selector(setEnabled:)])                

            {     
                // 버튼의 스타일을 지정한다. 

                [(UIButton *)view setBackgroundImage:[UIImage imageNamed:@"bg_effect_title.png"] forState:UIControlStateNormal];

            }            

        }

    }*/

}


 

이미지를 합치는 작업을 하던 중 하나로 합쳐야하는 상황에서 일일히 크롭 붙이기를 반복하려다보니 문제가 많다는걸 느꼈다.

그냥 보이는 화면을 캡쳐하면 될거 같아 찾아보니 역시 있었다.

- (UIImage*)capture:(CGSize) size {
   
    UIGraphicsBeginImageContextWithOptions(size, 1, 0.0);
   
    [self.layer renderInContext:UIGraphicsGetCurrentContext()]; 
   
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext(); 
   
    UIGraphicsEndImageContext(); 
   
    return img;
}

거의 크롭하는 것과 비슷한 구조인데 drawInRect를 쓰는대신 renderInContext를 사용하였다.

하지만! 이렇게 하다보니 저장할수 있는 사이즈는 화면에 보이는 사이즈 그대로일 뿐이다 (320x480) !

그래서 어떻게 할까 고민을 하다가 생각이 난 것이 UIScrollView 이다.

UIView에 원하는 사이즈만큼 View를 만든 다음 이미지를 얹은 후

ScrollView를 사용하여 화면에는 축소된 상태로 출력하니 생각 처럼 원하는 사이즈의 이미지로 저장할수 있었다.

@interface NCLayoutUIView : UIScrollView <UIScrollViewDelegate> {
............
}

......

- (void) makeLayoutView {
   
    CGRect layoutFrame = self.bounds;
    layoutFrame.size.width *= 2;   // 실제 저장될 사이즈는 화면에 보이는 사이즈 x2 크기이다.
    layoutFrame.size.height *= 2;
   
    UIView * layoutView = [[UIView alloc]initWithFrame:layoutFrame];
   
    [self addSubview:layoutView];
   
    self.layoutView = layoutView;
    self.layoutView.backgroundColor = COLOR_CLEAR;
    self.layoutView.layer.borderWidth = 4.0;    // 원래 테두리 굵기는 2px 이다.
    self.layoutView.layer.borderColor = [COLOR_BLACK CGColor];
   
    [layoutView release];
   
    self.delegate = self;
       
    self.maximumZoomScale = 0.5;   // 이미지 사이즈 x2 크기이므로 화면에 보여질때는 2배 축소되서 보여져야한다.
    self.minimumZoomScale = 0.5;
    self.zoomScale = 0.5;
    self.scrollEnabled = NO;
    self.bouncesZoom = NO;
}

하지만 단점은 원래 비율의 1/2크기로 출력되기 때문에 CALayer등으로 그린 선이 만약 0.5단위로 떨어지게 된다면 화면에 제대로 출력이 안되는 단점이 있다. 이를 방지하기 위해 되도록 모든 수치는 짝수로 떨어져야 하며 선 굵기등도 약 2배의 굵기로 그려줘여한다.

뷰를 모달로 실행 시킨 후 모달 뷰에서 이미지 처리나 많은 가상메모리를 소비하는 액션(복잡한 UI 처리등..)을 진행할 수 있다.

이럴 때 가상메모리가 없으면 메모리 경고를 받게 되는데 메모리 경고를 받고 난 이후에 모달 창을 닫게 되면 앱이 죽어 버리는 경우가 생긴다.

이런 현상을 방지하기 위해 검색을 해보니

메모리 경고가 발생하게 되면 부모 뷰의 didReceiveMemoryWarning 가 호출되게 되고

didReceiveMemoryWarning가 호출되면 viewUnload 가 실행되어 부모뷰가 해제되게되어 위와 같은 현상이 일어나는 것이었다.

이를 방지하기 위해 부모뷰의 didReceiveMemoryWarning에서  [super didReceiveMemoryWarning]; 를 주석처리하므로써 경고 발생시 해제되는 현상을 수정하므로써 오류를 방지하였다.

'iOS' 카테고리의 다른 글

ActionSheet 버튼 스타일 변경작업  (0) 2011.12.16
UIView 캡쳐하기  (0) 2011.11.15
UIView 와 Category의 만남  (0) 2011.10.26
UIButton Title 위치 정하기  (0) 2011.10.26
UIButon 이벤트 처리  (0) 2011.10.25

UIView 관련된 작업을 많이 하다보니 중복되는 코드들이 너무 많이 생기고 불편했다.

간단한 예로 하나의 이미지를 포함하는 뷰를 생성하려면

    UIImageView * imageView = [[UIImageView alloc]initWithImage:image];
   
    imageView.frame = frame;
   
    [self addSubview:imageView];
   
    [imageView release];
   
    return imageView;

위와 같은 코드가 반복되어야 했다. 이런 코드를 막기위해 다른 소스를 살펴보니 전역 함수를 만들어  좀더 수월하게 함수만 호출하는 방식을 사용하였다.

예로 라벨에 대한 객체를 생성하는 함수이다.


UILabel *NaMakeLabel(CGRect frame, UIColor *backgroundColor, NSString *text, UIColor *textColor, UITextAlignment align, UIFont *font)
{
 UILabel *label = [[UILabel alloc] initWithFrame:frame];
 
 label.text            = text;
 label.textColor       = textColor;
 label.textAlignment   = align;
 label.font            = font;
   
 if(backgroundColor != nil)
  label.backgroundColor = backgroundColor;
 
 return [label autorelease];
}

이런 식으로 변하는 값을 매개변수로 받은 후 생성된 객체를 autorelease 하여 반환하였다.

하지만 이렇게 쓰는 방법에 불편한 점이 있다. 즉 autorelease 이다. 오너쉽을 포기하기 위해 autorelease를 붙이게 되는데 이 함수를 호출한 객체에서는 이 autorelease된 객체가 언제 해제될지 모르는 상황이 겪게 된다. 간단한 뷰 같으면 큰 지장이 없으나 만약 큰 이미지를 포함하게 된다면 메모리 문제가 반드시 발생하게 된다.

좀더 편한 방법이 없을까하고 고민하고있다가 필요에 위해 카테고리를 사용하던 중 UIView에 카테고리를 붙이면 어떨까? 라는 생각이 들었다.

@implementation UIView (UIView_Image)

- (UIImageView *) addImage:(UIImage *)image withFrame:(CGRect)frame {
   
    UIImageView * imageView = [[UIImageView alloc]initWithImage:image];
   
    imageView.frame = frame;
   
    [self addSubview:imageView];
   
    [imageView release];
   
    return imageView;
}



이렇게 사용하자 앞선 함수 방식보다 훨씬 코드량도 줄고 메모리 걱정도 안하게 되었다. 함수 방식의 단점인 autorelease역시 위 카테고리를 사용하게 되면 오너십 자체를 양보하기 위해 autorelease를 사용하지 않고 바로 부모 UIView 넘겨줘 부모 뷰가 소멸 시점에서 같이 소멸된다.

또한 이 객체를 핸들링하기 위해 생성된 객체를 반환을 한다.

이런 카테고리를 사용하게 되니 코드의 양도 줄어들게 되어 매우 쾌적한 코딩을 할수 있게 되었다. 아래는 UIButton을 생성하는 카테고리이다.

물론 Button을 구성하기 위한 매개변수는 다양하기 때문에 입력받은 값이 많긴하지만 여러 줄에 걸쳐 입력한 것을 단 한라인해 해결할 수있게 되었다.

또한 매개변수 외 더 필요한 조합을 위해 추가적인 카테고리 메소드를 임의를 생성해서 쓰게 되어 확장성 및 재활용성이 용이하였다.


// 취소버튼 삽입
    [i_TopMenu addButtonForTarget:self
                       withAction:@selector(onClose)
                         andFrame:CGRectMake(frame.size.width-5-45, 5, 45, 32)
                   andNormalImage:[UIImage imageNamed:@"btn_editor_cancel_normal.png"]
              andHighlightedImage:[UIImage imageNamed:@"btn_editor_cancel_press.png"]
                          andText:@"취소"
                     andTextColor:COLOR_WHITE
          andHighlightedTextColor:COLOR_FFFFFF_A60
                      andTextFont:FONT_12];


UIButton을 사용하는 방법 중에 Title에 글자를 입력하면 항상 중앙에 있는 것이 문제였다.

이 문제를 해결하기 위해 Title Label을 가져와 frame을 바꾸는 삽질을 하였지만 ControlState 값이 바뀌면 다시 원상복구 되어 버리는다.

그래서 UIButton을 자세히 본 결과 재미있는 프로퍼티를 찾았다.

(UIButton).titleEdgeInsets 항목이다. 그리고 타입도 UIEdgeIndests 라는 녀석인데 처음 보는 녀석이였다.

UIEdgeIndests  내용을 보아하니 top, bottom, left, right 이것 뿐이다. 내가 margin이나 padding을 주기위해 쓰던 이름과 동일했다.

그래서 해당 타입의 구조체를 만들어 titleEdgeInsets에 입력을 해보니 역시나 예상대로 해당 구조체의 값 만큼 padding값이 적용되었다. ㅋㅋㅋㅋㅋ

UIButton * button = [[UIButton alloc]initWithFrame:frame];

[button setBackgroundImage:[UIImage imageNamed:@"btn_effect_frame_normal.png"] forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage imageNamed:@"btn_effect_frame_press.png"] forState:UIControlStateHighlighted];
       
[button setTitle:@"테스트" forState:UIControlStateNormal];
[button setTitle:@"테스트2" forState:UIControlStateHighlighted];
       
[button setTitleColor:COLOR_B9B8B8 forState:UIControlStateNormal];
[button setTitleColor:COLOR_WHITE forState:UIControlStateHighlighted];
       
[button.titleLabel setFont:FONT_12];
       
UIEdgeInsets edge;
edge.top = 98;
[button setTitleEdgeInsets:edge];

[button release];

앞으로는 패딩값을 입력할 경우가 생길 때 UIEdgeIndests 구조체를 이용해야겠다. ㅋㅋㅋ

{
UIButton * button = [[UIButton alloc]initWithFrame:frame];
[button addTarget:self action:@selector(onFrame:withEvent:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button]
[button release];
}


- (void) onFrame:(UIControl *)control withEvent:(UIEvent *)even {
   
}

'iOS' 카테고리의 다른 글

UIView 와 Category의 만남  (0) 2011.10.26
UIButton Title 위치 정하기  (0) 2011.10.26
UIImageView 에 CALayer 로 그림자 효과 넣기  (0) 2011.10.24
UIImage 회전 관련 링크  (0) 2011.10.11
simbolicatecrash 위치  (0) 2011.10.06

이미지 뷰에 그림자 효과를 주기위해 알아본 결과 CALayer 의 Shadow 속성을 사용하면 원하는 형태의 그림자를 만들수 있다.

CALayer * layer = self.imageView.layer;
       
layer.backgroundColor = COLOR_BLACK.CGColor;
layer.shadowOffset = CGSizeMake(10, 10);
layer.shadowRadius = 10.0f;
layer.shadowColor = COLOR_BLACK.CGColor;
layer.shadowOpacity = 0.6f;
[self.layer addSublayer:layer];

imageView에서 layer 프로퍼티를 사용하여 Shadow효과를 입력한 뒤 imageView의 부모 뷰에 AddSubView를 하게되면 그림자 효과를 얻을 수 있다.

ScrollView와 같이 사용하게 되면 줌/팬 이벤트와 동시에 사용할 수 있다.

ScrollView 같은 경우 layoutSubview 메소드에서 이미지뷰의 위치를 정하게 되는데 이때 위의 코드를 함께 넣어주면 그림자 효과를 적용할수 있다.

- (void)layoutSubviews
{
    [super layoutSubviews];
   
    // center the image as it becomes smaller than the size of the screen
   
    CGSize boundsSize = self.bounds.size;
    CGRect frameToCenter = self.imageView.frame;
    frameToCenter.size.width = round(frameToCenter.size.width*100)/100;
    frameToCenter.size.height = round(frameToCenter.size.height*100)/100;
   
    // center horizontally
    if (frameToCenter.size.width < boundsSize.width) {
       
        frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
    }else {
       
        frameToCenter.origin.x = 0.1;
    }
   
    // center vertically
    if (frameToCenter.size.height < boundsSize.height) {
       
        frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
    } else {
       
        frameToCenter.origin.y = 0.1;
    }
   
    self.imageView.frame = frameToCenter;
   
    if(self.shadowState == YES){
        CALayer * layer = self.imageView.layer;
       
        layer.backgroundColor = COLOR_BLACK.CGColor;
        layer.shadowOffset = CGSizeMake(10, 10);
        layer.shadowRadius = 10.0f;
        layer.shadowColor = COLOR_BLACK.CGColor;
        layer.shadowOpacity = 0.6f;
       
        [self.layer addSublayer:layer];
    }
}

'iOS' 카테고리의 다른 글

UIButton Title 위치 정하기  (0) 2011.10.26
UIButon 이벤트 처리  (0) 2011.10.25
UIImage 회전 관련 링크  (0) 2011.10.11
simbolicatecrash 위치  (0) 2011.10.06
SimpleURLConnections ios 5 문제 해결  (0) 2011.09.28

'iOS' 카테고리의 다른 글

UIButon 이벤트 처리  (0) 2011.10.25
UIImageView 에 CALayer 로 그림자 효과 넣기  (0) 2011.10.24
simbolicatecrash 위치  (0) 2011.10.06
SimpleURLConnections ios 5 문제 해결  (0) 2011.09.28
NSOperation 개발팁  (0) 2011.09.27
현재 진행하고 프로젝트의 신규 기능에 대해 설계를 진행하면서 다시 브릿지 패턴에 대해 찾아보다가 예전에는 모르고 지나갔던 부분이 걸려 인터넷을 찾아보았다.

브릿지 패턴을 자세히 보니 패턴의 기본인 Strategy 패턴과 동일한게 아닌가? 라는 의문이 들었다. 실제 UML의 모양도 비슷했다.

실제 두 차이점에 대해 찾아본 결과 차이점은 있었다.

(Stackoverflow 참조 : http://stackoverflow.com/questions/464524/what-is-the-difference-between-the-bridge-pattern-and-the-strategy-pattern)

The UML class diagram for the Strategy pattern is the same as the diagram for the Bridge pattern. However, these two design patterns aren't the same in their intent. While the Strategy pattern is meant for behavior, the Bridge pattern is meant for structure.

The coupling between the context and the strategies is tighter than the coupling between the abstraction and the implementation in the Bridge pattern.

From wikipedia 


즉 브릿지와 스트래지 패턴의 차이점은 스트래지는 행동을 브릿지는 구조의 decoupling를 의미했다.

이 말은 언뜻보기에는 명확하지만 도대체 행동과 구조의 차이점은 무엇일까라는 의문이 들었다. 답변 중의 내용을 좀더 살펴보았다.

  • Strategy: you have more ways for doing an operation; with strategy you can choice the algorithm at run-time and you can modify a single Strategy without a lot of side-effects at compile-time;
  • Bridge: you can split the hierarchy of interface and class join him with an abstract reference (seeexplication)


행동이라는 것은 하나의 동작을 진행할 때 런타임 시점에서 사용자가 알맞는 알고리즘등을 선택하여 진행하는 것이며 구조는 인터페이스의 구조를 나누고 추상적인 참조를 통해 합치는 의미를 가지고 있다. 

즉 스트래지 패턴은 사용자가 사용하고 싶은 정렬 알고리즘을 실행 중에 선택하여 진행할 수 있고 브릿지 패턴은 데이터베이스들의 api를 wrapper 클래스를 추상화 하여 클라이언트가 동일한 api로 같은 동작할 수 있도록 한다는 것이다.

정리.

스트레지 패턴 : 같은 결과이지만 다른 처리방식(알고리즘)을 수행
브릿지 패턴 : 하나의 동작을 추상화하여 서로 다른 구조(데이터베이스, 네트워크 등) 수행




 

'UML,OOP,Pattern, TDD' 카테고리의 다른 글

TDD - 계약에 의한 설계(DbC, Design by Contract)  (0) 2011.04.19
TDD Tip  (0) 2011.04.19
객체지향 개발 관련 팁  (0) 2011.04.19
객체지향 개발 원칙  (0) 2011.04.19
UML Tip  (0) 2011.04.19

/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resources

'iOS' 카테고리의 다른 글

UIImageView 에 CALayer 로 그림자 효과 넣기  (0) 2011.10.24
UIImage 회전 관련 링크  (0) 2011.10.11
SimpleURLConnections ios 5 문제 해결  (0) 2011.09.28
NSOperation 개발팁  (0) 2011.09.27
NSString 조합형, 완성형 문제  (0) 2011.08.31

SimpleURLConnections 샘플 코드를 보게 되면 예제 중 스트림을 사용하여 파일 전송하는 부분이 있다

이때 In/Out 스트림을 연결하기 위해 createBoundInputStream 라는 이름의 메소드를 사용하는데

ios4 에서 정상 동작하는데 ios5 에서는 업로드를 실행하면 크래시를 유발시킨다.

코드 중 CFTypeRef 를 id 타입으로 형변환을 시켜주는 NSMakeCollectable 이라는 함수를 사용하는데

이 함수를 사용하고 난 뒤 autorelease를 붙여 retainCount를 유지시켜준다.

*inputStreamPtr = [NSMakeCollectable(readStream) autorelease];

하지만 ios5에서는 위 구문이 오류를 일으킨다.

참고 : http://andpdas.com/wp-content/uploads/2011/06/ARCProgrammingGuide.pdf

원인은 io5에서 retain/release 의 정책이 변경되면서 위 구문에서 생성된 inputStreamPtr 객체를 release가 한번 더 일어 나는 것이다.

이로 인해 retainCount가 맞지 않아 앱이 종료되어버린다.

이를 해결하기 위해 인터넷 검색 결과

*inputStreamPtr = objc_unretainedObject(readStream);

구문을 사용하면 문제를 해결할 수있다. 실제로 위 구문으로 변경뒤 컴파일 했을때 정상 동작을 확인 하였다.

하지만 여기서 문제가 또 발생되었는데 xcode4.3 이상의 버전에서만 objc_unretainedObject 함수가 정의되어있다. -_-;;

그러다 보니 xcode4.0 이 메인 개발환경인 상태에서는 위 함수가 정의되어있지 않아 컴파일 오류가 발생한다. ㅠㅠ

이를 해결하기 위해 결국 objc_unretainedObject 가 정의되어있는지 #ifdef 구문으로 확인하는 것으로 해결을 하였다

#ifdef objc_unretainedObject
    if (inputStreamPtr != NULL) {
        *inputStreamPtr = objc_unretainedObject(readStream);
    }
    if (outputStreamPtr != NULL) {
        *outputStreamPtr = objc_unretainedObject(writeStream);
    }
   
#else
   
    if (inputStreamPtr != NULL) {
        *inputStreamPtr = [NSMakeCollectable(readStream) autorelease];
    }
    if (outputStreamPtr != NULL) {
        *outputStreamPtr = [NSMakeCollectable(writeStream) autorelease];
    }
   
    if(NaOSVersion() >= 5.0){
        [*inputStreamPtr retain];
    }
   
#endif

'iOS' 카테고리의 다른 글

UIImage 회전 관련 링크  (0) 2011.10.11
simbolicatecrash 위치  (0) 2011.10.06
NSOperation 개발팁  (0) 2011.09.27
NSString 조합형, 완성형 문제  (0) 2011.08.31
SecItemCopyMatching 메모리릭  (0) 2011.08.17


1. NSOperation 생명 주기 및 네트워크 전송 중 NSOperation 상태


가. autorelease pool 생성

NSOperation은 NSOperationQueue객체에서 실행되는 쓰레드 작업 단위의 객체입니다. 초기화 이후 Queue에 입력 된 이후 Queue에 의하 Start를 진행하게 됩니다.

Start단계에서는 객체에 필요한 사전 작업을 진행하고 (네트워크 상태체크, 여유 메모리 체크등) main을 실행하게 됩니다.

main단계는 실제 작업을 해야하는 것을 기술하게 되어있는데 메인쓰레드의 Autorelease Pool의 영역에서 벗어나기 때문에 자체 Autorelease Pool을 생성해야합니다.

만약 제대로 Pool을 생성안할 경우 Autorelease로 해제를 선언한 객체들은 릭을 유발하지 않지만 계속 메모리에 남게 되는 문제를 발생할 수도 있습니다.

 

참조 URL : http://stackoverflow.com/questions/184409/nsautoreleasepool-in-nsoperation-main


나. 비동기 방식 작업시 NSOperation 메모리 확보

일반적인 작업 단위로 NSOperation을 사용하게 될 경우 main이 종료되는 시점에서 객체가 해제됩니다.

하지만 네트워크 작업등 비동기 방식으로 NSOperation을 사용할 경우 주의 할 점이 있습니다.

비동기 방식으로 진행하기 때문에 main이 종료되어도 객체는 계속 살아있어야 합니다. (retainCount가 최소 1로 유지되어야함)

그 이유는 비동기 대상에서 응답을 델리게이트를 통해 처리해야하기 때문입니다.(만약 결과를 처리할 필요가 없으면 NSOperation객체가 살아 있을 필요는 없습니다.)

그럼 main이 종료된 이후에도 메모리에 유지시키기 위해서는 비동기 작업 대상에 자신을 델리게이트 객체로 등록을 해야하는데 보통 델리게이트 객체를 등록할 때는 Property의 assign 키워드를 통해 메모리를 참조만하고 있어야 하지만

위의 경우에는 Property의 retain키워드를 통해 retainCount를 확보해두어야 합니다. (그리고 비동기 객체가 종료 시점에 반드시 해제 시켜야 합니다. 그렇지 않을 경우 메모리릭이 발생 됨)


@interface NCTransOperation : NSOperation {

 NCTransPostManager * i_TransPostManager; // 비동기 대상 객체, 사용할 때 retainCount를 확보한다.
}

@interface NCTransPostManager : NSObject <HttpPostDelegate>{
- id i_Target; ///< 해당 클래스


}

@property (nonatomic, retain)id target;

//  NSOperation의 결과를 처리할 델리게이트 타겟, retain키워드를 사용하여 메모리를 확보한다.

 

- (void) dealloc
{
 self.target = nil; //< 델리게이트 타겟을 반듯이 해제시켜야 한다.

 [super dealloc];
}

 

 

2. NSOperation 취소하기

 

네트워크등으로 인해 NSOperation Queue의 제어를 벗어난 상황에서 취소하는 방법 (노티적용)

NSOperationQueue 내에서 NSOperation이 일방적인 방식으로 처리가 될 경우 isCancelled 및 cancel 메소드를 오버라이드를 통해 취소시 실행될 액션을 정의하면 됩니다.

(isCanceled는 객체의 취소상태를 판별하는 역활을 하며 cancel 메소드는 취소 진행시 실행 될 내용을 기술하면 됩니다.)

하지만 NSOperation이 비동기 방식으로 진행될 경우 비동기 진입 전까지는 (main 메소드내 진행상태) 위의 isCancelled와 cancel메소드가 작동하게 되지만 비동기의 응답을 기다리는 경우

NSOperationQueue의 제어권을 벗어난 상태이기 때문에 두 메소드가 제대로 실행이 안됩니다. 이 경우 제대로 취소 액션을 진행할 수 없어 예외상황을 발생시키는 경우가 발생합니다.

(취소를 발생시킨 객체는 이미지 종료되어 해제되었는데 NSOperation에서 객체에게 결과를 반환하려고 시도할때 Bad Access를 발생시킴)

그러므로 임의로 취소 단계를 돌입할 수 있는 방법을 구현을 해야합니다.

여러 방법이 있겠지만 가장 간편하게 구현할 수 있는 방법은 Notification을 사용한 Observing Pattern을 적용하는 것입니다. NSOperation이 실행될 때 취소 Notification을 등록하게 된면

NSOperationQueue의 제어를 벗어난 상태에서도 취소 단계에 돌입할수 있습니다. (NSOperationQueue의cancelAllOperations을 실행하기 전 Notificaion을 Posting하게 되면 비동기 상태에 있는 객체들도 반응하게되어

취소를 제대로 진행 할수 있게됩니다. 물론 NSOperation이 종료된 이후에는 Notification을 해제 시켜야합니다.

 


3. NSOperation의 Main thread 전환 시점

 

이미지 리사이즈 같은 이미지 프로세싱 작업은 작업단위가 크기 때문에 main Thread에서 처리하기에는 부담이 큽니다. 이때 사용자의 편의성을 유지시키기 위해 작업단위가 큰 작업은 쓰레드로 실행해야 합니다.

이때 쓰레드에서 작업한 결과(이미지 리사이즈 같은 결과물) 화면에 표시하기 위해서는 NSOperation을 main쓰레드로 변환을 시켜야 한다. 현재 작업을 main 쓰레드로 변환하기 위해서는 두 가지 방법이 있습니다.

 

가. NSOperation자체를 main쓰레드로 전환시킴

 

if ([NSThread isMainThread] == NO) {
 [self performSelectorOnMainThread:_cmd withObject:nil waitUntilDone:NO];
 return;
}

 

나. NSOpreation 내 특정 메소드 실행을 main쓰레드로 전환시킴

 

[self performSelectorOnMainThread:@selector(complete:) withObject:outputImage waitUntilDone:NO];


'가' 와 같은 방법은 네트워크 전송등 특정 작업을 하기 위해 NSOperation을 main스레드로 전환시켜야 할 경우에만 사용해야 합니다. 하지만 위 처럼 이미지 변환 작업을 완료 후 결과를 main 쓰레드로 보내기 위해서는

'나'와 같은 방법을 사용하는 것이 좀더 효율적입니다. 만약 아래와 같이 NSOperation을 벗어 난 후 일반 객체에서 메인쓰레드로 전환시킬수도 있긴 하지만 이럴 경우 예외 상황들이 발생할 가능성이 많습니다.


객체1 (main Thread) -> NSOperation 실행 (Thread 2) -> NSOpeartion 종료 (Thread 2) -> 객체 1에 delegate를 통해 결과 반환 (Thread 2) -> 객체 1에서 메인쓰레드로 전환 후 결과를 사용함(performSelectorOnMainThread)


발생될 예외상황은 크게 두가지 인데 첫번째로는 메모리 경고 입니다.

여러가지 이유로 메모리 경고가 발생되는데 위와 같이 NSOperation을 벗어난 상태에서 쓰레드가 동작하면서 그 영향이 main 쓰레드까지 미치게 된다면 실제 가상메모리를 확보 할수 있음에도 불구하고 제대로 확보가 되지 않아

메모리 경고를 유발시키게 됩니다. 심할 경우 앱이 종료될 수도 있습니다. 특히 이미지 변환 같은 작업 단위가 클 경우 더욱 자주 발생됩니다.

두번째로는 비동기 처리와 같이호출 한 객체가 종료된 이후에 결과를 반환하려 할때 객체 검사게 제대로 이뤄지지 않아 Bad Access를 유발 시키게 됩니다. 이런 문제점을 해결하기 위해서는 꼭 쓰레드는 NSOperation 내에서 전환 되어야 합니다.

 

NSOperation은 objective C 프로그램에서 매우 유용한 Concurrency Program을 작성할 수있습니다. 하지만 예외 상황을 유발시키는 경우도 많기 때문에 많은 테스트를 해봐야 합니다.

'iOS' 카테고리의 다른 글

simbolicatecrash 위치  (0) 2011.10.06
SimpleURLConnections ios 5 문제 해결  (0) 2011.09.28
NSString 조합형, 완성형 문제  (0) 2011.08.31
SecItemCopyMatching 메모리릭  (0) 2011.08.17
Delegate를 사용시 주의할 점  (0) 2011.07.21
프로그램 내 NSString으로 한글을 사용하면 완성형 형태로 동작하게 된다.

하지만 디렉토리내 파일명을 읽어올때 조합형으로 문자열을 가져오게되는 경우가 있다.

조합형과 완성형은 최종 결과는 같지만 length의 형태나 인코딩 형태도 달라지게 된다.(비교 역시 틀리다.)

이때 조합형을 완성형으로 변경 시켜야 인코딩 결과도 동일해지며 NSDictionary 의 키로서 사용도 가능하게 된다.

NSDirectoryEnumerator * directoryEnum = [NCAppImageFileManager fileListEnumerator:imagePath];

while ( (file = [directoryEnum nextObject]) !=nil){

            ///< 파일시스템에 파일명을 읽어올때 조합형으로 읽어 제대로 Key로써 역활을 제대로 하지 못하게 된다. 조합형 -> 완성형으로 변경
            file = [file precomposedStringWithCanonicalMapping];
           
            ///< 이미지 정보 생성한다.
            [self imageFileInfoWithFileName:file];
 }

'iOS' 카테고리의 다른 글

SimpleURLConnections ios 5 문제 해결  (0) 2011.09.28
NSOperation 개발팁  (0) 2011.09.27
SecItemCopyMatching 메모리릭  (0) 2011.08.17
Delegate를 사용시 주의할 점  (0) 2011.07.21
iOS 상단 Statusbar 반투명 상태로 사용하기  (0) 2011.07.09

SecItemCopyMatching을 사용할 경우 CFDictionaryRef 의 내용을 CFTypeRef으로 검색하는 구문을 사용시

CFTypeRef가 메모리가 할당된 뒤 해제를 시켜주어야 한다.

그때 CFRelease를 사용하여 해제를 시켜주어야 한다.

예 :

if (SecItemCopyMatching((CFDictionaryRef)genericPasswordQuery, (CFTypeRef *)&attributes) == noErr) {
.......
}

......
CFRelease(attributes);

Objective C로 프로그램을 만들다 보면 Delegate를 많이 사용하게 된다. 이때 주의해야 할 점이 몇가지가 있다.

1. 델리게이트는 Property를 사용해서 관리하라

- 초기화 시점에서 델리게이트 입력 받아 초기화 내에서 델리게이트를 멤버변수에 넣어도 우선 Property를 만들어 Property로 할당하게끔한다.

2. 델리게이트 Property 속성은 대부분 assign이여야 한다.

- retain 속성으로 지정하게 되면 델리게이트를 등록된 부모 클래스에서는 메모리가 해제되지 않고 계속 남아 릭이 발생하는 경우가 생긴다. 델리게이트의 목적은 부모를 참조하는 개념이기 때문에 assign을 사용하여 retain Count를 잡아두지 말아야 한다.

- 하지만 비동기처리시 역활을 수행시 역활을 호출한 이후 자신의 객체가 소멸될 경우가 발생되면 오히려 문제가 생기게 된다.(NSOperation의 네트워크 비동기 접속시)
 이런 경우 자신의 오너십을 역활을 수행하는 대상에게 넘기야 하는데 이때 받아들이는 델리게이트 속성을 retain으로 해야한다. 하지만 비동기 처리가 끝나는 시점에서 반드시 등록했던 오너쉽을 해지해야한다.

3. 사용이 끝난 객체내 델리게이트는 dealloc될 경우 nil로 초기화 시킨다.

A -> B 의 관계에서 A클래스는 B클래스를 호출하여 사용할 때 델리게이트를 등록하게 되는데 A클래스를 해제하는 시점은 dealloc 에서는 delegate를 반드시 초기화 시켜주어야 한다. 이렇게 하지 않을 경우 쓰레드나 performSelector등을 사용할 경우 이미 해제된 delegate에 접근을 시도하여 크래쉬를 발생시키는 원인이 된다. 꼭 dealloc뿐만 아니라 viewDidDisappear등에서 해줘도 무방하다 (대신 viewDidAppear에 다시 delegate를 할당해주어야한다)


UIViewController를 상속받은 커스텀 클래스에서 viewDidLoad 부분에 아래와 같은 코드를 추가한다.

-(void) viewDidLoad{

    

    ///< Status bar영역까지 이미지를 표현하기 위핸  view 설정값을 변경함


    [self setWantsFullScreenLayout:YES];

    

    self.view.frame = [UIScreen mainScreen].bounds;

    

    [[UIApplication sharedApplication]setStatusBarStyle:UIStatusBarStyleBlackTranslucent animated:YES];


    [super viewDidLoad];

}

 
setWantsFullScreenLayout = YES : statusbar가 위치한 부분까지 (0,0) 포인트로 잡는다.
그리고 현재 뷰의 크기를  Statusbar 크기만큼 늘리기 위해 mainScreen의 bounds값으로 크기를 변경한다.
Statusbar의 스타일을 반투명 스타일로 변경한다. 

        /// 싱글탭 설정
        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTapPiece:)];       
        tapGesture.numberOfTapsRequired = 1;
       
        /// 더블탭 설정
        UITapGestureRecognizer *dbTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTapPiece:)];       
        dbTapGesture.numberOfTapsRequired = 2;
       
        /// 더블탭시 싱글탭과 중복을 방지
        [tapGesture requireGestureRecognizerToFail : dbTapGesture];
       
        [dbTapGesture setDelaysTouchesBegan : YES];
        [tapGesture setDelaysTouchesBegan : YES];
       
        /// 싱글탭 뷰에 추가
        [self addGestureRecognizer:tapGesture];
        [tapGesture release];
       
        /// 더블탭 뷰에 추가
        [self addGestureRecognizer:dbTapGesture];
        [dbTapGesture release];
       
        /// 핀치 이벤트 추가
        UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(scalePiece:)];
        [pinchGesture setDelegate:self];
        [self addGestureRecognizer:pinchGesture];
        [pinchGesture release];


- (void)singleTapPiece:(UITapGestureRecognizer *)gestureRecognizer
{
    if([gestureRecognizer state] == UIGestureRecognizerStateEnded) {
        SALog2(@"tap End");
    }
}
- (void)doubleTapPiece:(UITapGestureRecognizer *)gestureRecognizer
{
    if([gestureRecognizer state] == UIGestureRecognizerStateEnded) {
        SALog2(@"DoubleTap End");
    }
}
// scale the piece by the current scale
// reset the gesture recognizer's rotation to 0 after applying so the next callback is a delta from the current scale
- (void)scalePiece:(UIPinchGestureRecognizer *)gestureRecognizer
{
    [self adjustAnchorPointForGestureRecognizer:gestureRecognizer];
   
    if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged) {
       
        SALog2(@"Pinch Start");
        [gestureRecognizer view].transform = CGAffineTransformScale([[gestureRecognizer view] transform], [gestureRecognizer scale], [gestureRecognizer scale]);
        [gestureRecognizer setScale:1];
       
    }else if([gestureRecognizer state] == UIGestureRecognizerStateEnded) {
        SALog2(@"Pinch End");
    }
}
// shift the piece's center by the pan amount
// reset the gesture recognizer's translation to {0, 0} after applying so the next callback is a delta from the current position
- (void)panPiece:(UIPanGestureRecognizer *)gestureRecognizer
{
    UIView *piece = [gestureRecognizer view];
   
    [self adjustAnchorPointForGestureRecognizer:gestureRecognizer];
   
    if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged) {
       
        SALog2(@"Pan Start");
       
        CGPoint translation = [gestureRecognizer translationInView:[piece superview]];
       
        [piece setCenter:CGPointMake([piece center].x + translation.x, [piece center].y + translation.y)];
        [gestureRecognizer setTranslation:CGPointZero inView:[piece superview]];
       
    } else if([gestureRecognizer state] == UIGestureRecognizerStateEnded) {
       
        SALog2(@"Pan End");
    }
}

'iOS' 카테고리의 다른 글

Delegate를 사용시 주의할 점  (0) 2011.07.21
iOS 상단 Statusbar 반투명 상태로 사용하기  (0) 2011.07.09
UIView 변형 방법  (0) 2011.06.24
델리게이트 성격의 프로퍼티 설정  (0) 2011.06.15
UIImage 카메라 롤에 추가  (0) 2011.06.15

델리게이트를 저장하는 프로퍼티의 설정은 포인터이기 때문에 retain으로 설정하기 쉬운때 이럴경우 호출한 객체가 제대로 해제가 안되는 상황이 되어 메모리 릭 상황이 발생하게 된다.

이유는 호출한 객체를 델리게이트에 저장하면서 retain으로 메모리를 붙잡게되어

호출한 객체가 해제될 때 retain으로 붙잡혀 있어 제대로 해제가 안되는 경우가 생긴다

예를 들어

NCAppImageEffectManager * imageManager = [[NCAppImageEffectManager alloc]initWithDelegate:self];
       
self.effectManager = imageManager;
       
[imageManager release];

위와 같은 소스에서 NCAppImageEffectManager 에 델리게이트 함수로 자신을 보내게 되는데 NCAppImageEffectManager 내에서 델리게이트를 저장할 때 retain을 추가해버리면 제대로 해제가 안된다.

@property (nonatomic, retain) id<NCAppImageEffectDelegate>  controllerDelegate;

-(id) initWithDelegate:(id<NCAppImageEffectDelegate>) delegate{
   
    self=[super init];
   
    if(self != nil) {
        self.controllerDelegate = delegate;
    }
   
    return self;
}

즉 델리게이트 성격의 프로퍼티를 지정할 때는 호출한 상위객체가 해제 안된다는 가정하에 실행이 되어야 하며 assign 속성으로 지정하여 따로 retainCount를 늘리지 말아야한다.

'iOS' 카테고리의 다른 글

싱글 탭 / 더블 탭 / 핀치 GestureRecognizer 추가  (0) 2011.07.08
UIView 변형 방법  (0) 2011.06.24
UIImage 카메라 롤에 추가  (0) 2011.06.15
NSFileManager 디렉토리 위치 정의  (0) 2011.06.11
objective C (xcode4) gcov 설정방법  (0) 2011.06.07

Saving an image to the camera roll on the iPhone is as close as a method call in the UIKit. However, it takes a few steps to wrap together code to manage error handling and notification that the image has been saved. Let’s see how this works.

Let’s begin with the method description to save an image:

  void UIImageWriteToSavedPhotosAlbum(UIImage *image,                   id completionTarget, SEL completionSelector, void *contextInfo);

completionTarget refers to the object in which the completionSelector can be found. In other words, what object has the method that will be called once the file write is complete? contextInfo is a void * that you can use to specify content to be passed to the completionSelector.

Obviously, the image is a required paramater. The other three are only needed if you prefer to be notified asynchronously when the write is complete. Here’s how a call to the above method might look:

  // Image to save  UIImage *img = [UIImage imageNamed:@"ImageName.png"];     // Request to save the image to camera roll  UIImageWriteToSavedPhotosAlbum(img, self,               @selector(image:didFinishSavingWithError:contextInfo:), nil);

Here we are specifying that the selector to be called upon completion is within this object (self). A template for this selector could look as follows:

  - (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error              contextInfo:(void *)contextInfo  {    // Was there an error?    if (error != NULL)    {      // Show error message...     }    else  // No errors    {      // Show message image successfully saved    }  }

Add code for displaying the appropriate message based on success or failure of the image save, and you’re good to go.

NSApplicationDirectory: /var/mobile/Applications/{APPID}/Applications
NSDemoApplicationDirectory: /var/mobile/Applications/{APPID}/Applications/Demos
NSDeveloperApplicationDirectory: /var/mobile/Applications/{APPID}/Developer/Applications
NSAdminApplicationDirectory: /var/mobile/Applications{APPID}/Applications/Utilities
NSLibraryDirectory: /var/mobile/Applications/{APPID}/Library
NSDeveloperDirectory: /var/mobile/Applications/{APPID}/Developer
NSUserDirectory: Not found
NSDocumentationDirectory: /var/mobile/Applications{APPID}/Library/Documentation
NSDocumentDirectory: /var/mobile/Applications{APPID}/Documents
NSCoreServiceDirectory: Not found
NSDesktopDirectory: /var/mobile/Applications/{APPID}/Desktop
NSCachesDirectory: /var/mobile/Applications/{APPID}/Library/Caches
NSApplicationSupportDirectory: /var/mobile/Applications/{APPID}/Library/Application Support
NSDownloadsDirectory: /var/mobile/Applications/{APPID}/Downloads
NSAllApplicationsDirectory: /var/mobile/Applications/{APPID}/Applications
NSAllLibrariesDirectory: /var/mobile/Applications{APPID}/Library


1. 테스트가 되는 타겟의 빌드 세팅을 변경한다.

Other Linker Flags: add "-lgcov"

GCC_GENERATE_TEST_COVERAGE_FILES: Set to YES (안해도 작동됨)

GCC_INSTRUMENT_PROGRAM_FLOW_ARCS: Set to YES (안해도 작동됨)

C/C++ Compiler Version: GCC 4.2 (if you are on XCode 4) iOS deployment target: 4.2 (LLVM은 작동안됨)

Precompile prefix header: NO (안해도 작동됨)


2. 별도의 테스트 타켓은 build phases 부분의 Compliler Flags에 속성값을 입력한다..

-fprofile-arcs -ftest-coverage


3. objective c의 gcovr의 버전은 현재(2011.06) 2.0 에서 정상 동작한다. 만약 제대로 실행이 안될경우 버전을 확인하도록함 (2.1버전에서 작동안함)

gcovr --version


4. 아래의 스크립트로 정상 동작하는지 확인

xcodebuild -configuration Debug -target <타겟명> -sdk iphonesimulator
<gcovr설치된 경로>/gcovr -r <프로젝트 경로>/ -x


gcovr는 xcodebuild를 통해 생성된 build 내 gcda파일을 분석하는 방식이다. 즉 첫번째로 xcodebuild를 통해 gcda파일이 제대로 생성되었는지 확인 후 gcda가 생성되었다면 gcovr를 통해 분석이 될텐데 제대로 결과값이 안나오게 되면 gcovr의 문제일 가능성이 크다


보통 plist를 읽어올때 [NSBundle mainBundle] 을 사용하여 읽어오는데 단위 테스트를 할때 제대로 읽어오지 못하는 현상이 생긴다

이때 mainBundle 부분을 bundleForClass:[self class] 로 변경해주면 단위테스트를 할때도 제대로 plist의 path를 읽어온다



NSString * path = [[NSBundle bundleForClass:[self class]] pathForResource:@"ErrorMessage" ofType:@"plist"];

KeychainItemWrapper 의 역할은 아이폰내 키체인에 보다 쉽게 계정을 저장하기 위한 wrapper이다.

사용 방법은 생각보다 간단한데 주의해야 할점은 초기화 할때 입력하는 identifier 값이 키체인의 영역을 구분하는 키 역할을 하는 부분인데

이때 입력되는 키체인의 속성에 따라 충돌이 발생되 오류가 나는 현상이 있다.

만약 한 앱에 2개 이상에 대한 패스워드를 키체인에 저장할 때는 반드시 주의해서 사용해야한다.

SAKeychainItemWrapper * item = [[SAKeychainItemWrapper alloc] initWithIdentifier:key accessGroup:nil];

    [item setObject:value forKey:(id)kSecValueData];

    [item release];



key : 키체인의 영역을 구분짓는 키
value : 저장되는 값

문제는 2개 이상의 계정에 대해 저장할 때 key 체인의 값이 계정에 따라 다르더라도 두번째 계정 비밀번호를 입력할 때 오류가 발생되었다.

(동일 아이템이 존재하여 입력이 안된단다)

분명 identifier 키가 다른데 왜 충돌이 날까? 그렇다고 중복된것이 있다면서 업데이트 역시 제대로 되지 않았다.

구글링을 한 결과 알아낸 사실은 kSecAttrAccount, kSecValueData 있었다.

wrapper를 사용하여 초기화할 때 kSecClassGenericPassword 속성이 들어가게 되는데 이 속성은

kSecAttrAccount가 유니크해야한다는 것이다. 이 사실을 모르고 아무리 비밀번호를 입력하니 오류가 발생된것이었다.

보통 아이디와 비밀번호 둘다 키체인에 저장하기 때문에 문제가 발생이 안되겠지만

현재 개발하는 프로젝트는 동일 아이디에 비밀번호가 다른 경우도 발생할수 있기 때문에 비밀번호만 키체인에 저장하려고 하니 문제가 발생한 것이었다

즉 해결하기 위해서 kSecAttrAccount 값에 패스워드의 identifier 값을 입력하므로써 각 계정간 유니크성을 유지하여 해결 할수 있었다.

SAKeychainItemWrapper * item = [[SAKeychainItemWrapper alloc] initWithIdentifier:key accessGroup:nil];
    [item setObject:key forKey:(id)kSecAttrAccount];
    [item setObject:value forKey:(id)kSecValueData];
    [item release];


참고 : http://useyourloaf.com/blog/2010/4/28/keychain-duplicate-item-when-adding-password.html


 /Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates
 
위치에있는 Cocoa Touch 디렉토리를 백업한 후에
 
첨부한 파일을 해당 디렉토리에 설치 후 클래스 생성시 Doxygen용 템플레이트가 적용 됨


 

+ Recent posts