2014년 12월 26일 금요일

[iOS] 원형의 그라디언트 이미지를 그려봅시다. (Rendering a radial gradient)

Radial Gradient

원형으로 배경을 은은하게 채워야하는 경우에, 아래와 같이 그림이 필요합니다.
이미지를 만들어서 넣으면 좋겠지만, Core Graphics를 사용해서 그릴 수 있습니다.
만들 이미지
source code
- (void)drawRadialGradient:(UIColor *)startColor
                  endColor:(UIColor *)endColor
                startPoint:(CGPoint)startPoint
               startRadius:(CGFloat)startRadius
                  endPoint:(CGPoint)endPoint
                 endRadius:(CGFloat)endRadius
                   context:(CGContextRef)context
{
    CGColorRef colorRef = startColor.CGColor;
    CGColorRef endColorRef = endColor.CGColor;
    NSArray *marrColors=[NSArray arrayWithObjects:
                         (__bridge id)colorRef,    //start color
                         (__bridge id)endColorRef, //end color
                         nil];
    CFArrayRef colors =(__bridge CFArrayRef)(marrColors);
    CGColorSpaceRef colorSpc = CGColorSpaceCreateDeviceRGB();
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpc, colors, Nil);
    
    // generates Radial Gradient
    CGContextDrawRadialGradient(context, gradient,
                                startPoint, startRadius,
                                endPoint, endRadius,
                                0);
    CGColorSpaceRelease(colorSpc);
    CGGradientRelease(gradient);
}
 



이 함수를 이용해서, 중심에 그리면, 위 만들이미지와 같은 효과를 얻을 수 있습니다.

Drawing a radial gradient like Lens


그러면, Gradient에 이미지를 추가하면 중간에 이미지가 추가가 됩니다.
source code
- (void)drawRadialGradient:(UIColor *)startColor
                  midColor:(UIColor *)midColor
                  endColor:(UIColor *)endColor
                startPoint:(CGPoint)startPoint
               startRadius:(CGFloat)startRadius
                  endPoint:(CGPoint)endPoint
                 endRadius:(CGFloat)endRadius
                   context:(CGContextRef)context
{
    CGColorRef colorRef = startColor.CGColor;
    CGColorRef midColorRef = midColor.CGColor;
    CGColorRef endColorRef = endColor.CGColor;
    NSArray *marrColors=[NSArray arrayWithObjects:(__bridge id)colorRef,
                         (__bridge id)midColorRef,
                         (__bridge id)endColorRef, nil];
    CFArrayRef colors =(__bridge CFArrayRef)(marrColors);
    CGColorSpaceRef colorSpc = CGColorSpaceCreateDeviceRGB();
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpc, colors, Nil);
    
    // generates Radial Gradient
    CGContextDrawRadialGradient(context, gradient,
                                startPoint, startRadius,
                                endPoint, endRadius,
                                0);
    CGColorSpaceRelease(colorSpc);
    CGGradientRelease(gradient);
}
호출하는 소스
- (void)drawRect:(CGRect)rect {
    
    CGContextRef context = UIGraphicsGetCurrentContext();

    
    // Drawing code
    UIColor *whiteColor = [UIColor whiteColor];
    UIColor *blueColor = [UIColor blueColor];
    UIColor *greenColor = [UIColor greenColor];
    
    CGSize size = self.frame.size;
    CGPoint center = CGPointMake(size.width/2, size.height/2);
    /*/
    [self drawRadialGradient:greenColor endColor:self.backgroundColor
                  startPoint:center startRadius:0
                    endPoint:center endRadius:size.width/2
                     context:context];
    /*/
    [self drawRadialGradient:greenColor midColor:blueColor endColor:whiteColor
                  startPoint:center startRadius:0
                    endPoint:center endRadius:size.width/2
                     context:context];
    CGPoint point2 = CGPointMake(size.width/2-15, size.height/2-15);
    [self drawRadialGradient:whiteColor endColor:greenColor
                  startPoint:point2 startRadius:0
                    endPoint:point2 endRadius:5
                     context:context];
    point2 = CGPointMake(size.width/2+15, size.height/2+15);
    [self drawRadialGradient:whiteColor endColor:greenColor
                  startPoint:point2 startRadius:0
                    endPoint:point2 endRadius:2
                     context:context];
     //*/
}


결과물
3가지 색으로만
3가지 색에, 점 2개 찍은것


나름 랜즈와 비슷한 것이 나온것 같군요.






2014년 12월 10일 수요일

[iOS] NSString으로 된 값을 파일로 직접 저장하자.

현재 App의 디렉토리 Path를 읽어 오고, 거기에 파일 이름을 추가합니다.


 source code
- (NSURL *)urlForFilename:(NSString *)filename {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSArray *urls = [fm URLsForDirectory:NSDocumentDirectory
                               inDomains:NSUserDomainMask];
    NSURL *directoryURL = urls[0];
    NSURL *fileURL = [directoryURL URLByAppendingPathComponent:filename];
    return fileURL;
}

이름을 정하고 나면, NSString에서 바로 저장합니다.


    BOOL status = [string writeToFile:[pathUrl.path stringByAppendingPathExtension:@"txt"]
                           atomically:YES 
                             encoding:NSUTF8StringEncoding 
                                error:&error];
        
    if (error != nil) {
        NSLog(@"save error: %@", [error description]);
    }
    if (status == NO) {
        NSLog(@"save error");
    }


파일로부터 데이터를 읽어 올 경우도, NSString의 카테고리를 이용합니다.

    NSString *string = [NSString stringWithContentsOfURL:pathUrl 
                                                encoding:NSUTF8StringEncoding 
                                                   error:&error];
    if (error != nil || string == nil) {
       NSLog(@"Can't load file: %@, error:%@", pathUrl.path, [error description]);
    }


만약, 문자열이 아닌 Binary데이터를 저장하려면, NSString대신, NSData의 카테고리 함수를 이용하면, 똑같이 사용이 가능합니다.

2014년 10월 9일 목요일

[iOS] User Defined Runtime Attributes

User Defined Runtime Attributes란? 

Xcode의 Interface Builder에서 특정한 UI 객체에 사용자가 정의하는 값을 바로 입력하는 기능을 말합니다.
Storyboard에 정의된 객체에서 값을 설정할 수 있는 것으로, Identity Inspector tab에서 설정할 수 있습니다. 

InterfaceBuilder의 User Defined Runtime Attributes 설정 화면과 시뮬레이터 표시 내용.

위의 그림처럼 Interface Builder에서 UIView를 넣고, Identity Inspector Tab에서 값을 입력하게 되면, m파일에서 UIView의 객체를 연결해서, 직접 입력한 것처럼 동작하게 됩니다.

입력 할 수 있는 값은 어떤 것이 있을까요?

Boolean - BOOL
Number - NSNumber *
String - NSString *
Point - CGPoint
Size - CGSize
Rect - CGRect
Range - NSRange
Color - UIColor *
LocalizedString - NSString *
(Localizable.string 파일에서 Key를 입력하고, 해당 locale에 맞는 string을 입력)
Image - UIImage*




Xcode 6에서는 총 10가지를 입력할 수 있습니다.

위에 첫번째 이미지에서 layer.borderColor는 바로 설정할 수가 없습니다.
전달되는 객채는 UIColor이고, layer.borderColor는 CGColorRef를 사용하기 때문입니다.
가능한 방법은
CALayer의 카테고리를 생성하고, 그 카테고리에 setBorderColorFromUIColor 함수를 만들어 줍니다.
source code
#import "CALayer+extension.h"

@implementation CALayer (extension)
- (void)setBorderColorFromUIColor:(UIColor *)color
{
    self.borderColor = color.CGColor;
}
@end

h파일에도 함수를 선언해 주어야 합니다.
이제, InterfaceBuilder에서 layer.borderColorFromUIColor 를 사용해서, 색상을 설정할 수 있습니다.


위의 시뮬레이터에서 테두리 색상이 설정된 것을 볼 수 있습니다.


[참고]

- iOS-Blog : User Defined Runtime Attributes
- ATOMIC SPIN : Expanding User-Defined Runtime Attributes in Xcode with Objective-C




2014년 9월 12일 금요일

[iOS] 물방울 모양의 UIView를 만들자.

UIView의 모양을 물방울 모양으로 변형을 해보겠습니다.

1. UIBezierPath를 이용해서, 물방울 모양의 Path를 만듭니다.

source code
- (UIBezierPath *)waterDropPath:(CGRect)frame
{
    //NSLog(@"makeWaterDropPathIn: %@", NSStringFromCGRect(frame) );
    float x = frame.origin.x;
    float y = frame.origin.y;
    float hW = frame.size.width/2.0f;
    float H = frame.size.height;
    
    CGPoint sp = CGPointMake(x+hW, y);      //Start point
    CGPoint cp = CGPointMake(x+hW, y+H-hW); //Center point
    
    CGPoint b1 = CGPointMake(x, y+H-hW);    //WaterDrop left point
    CGPoint c1 = CGPointMake(x+hW, y+hW+hW/2); //왼쪽 내려오는 부분에서 사용
    CGPoint c2 = CGPointMake(x, y+H-hW-hW);    
    CGPoint c3 = CGPointMake(x+hW+hW, y+H-hW-hW); //오른쪽 올라가는 부분에서 사용
    
    UIBezierPath *path = [[UIBezierPath alloc] init];
    [path setLineWidth:0.5f];
    
    [path moveToPoint:sp]; //시작점으로 이동
    
    [path addCurveToPoint:b1
             controlPoint1:c1
             controlPoint2:c2]; //왼쪽 물방울 내려오는 부분
    
    [path addArcWithCenter:cp
                     radius:hW//-0.5f
                 startAngle:DEGREE_TO_RADIAN(180)
                   endAngle:DEGREE_TO_RADIAN(0)
                  clockwise:NO]; //물방울 아래부분

    [path addCurveToPoint:sp
             controlPoint1:c3 
             controlPoint2:c1]; //오른쪽 올라가는 부분
    
    return path;
}

2. Path로 부터 현재 뷰의 layer의 mask를 설정합니다.

source code
- (void) setWaterDropClippingArea:(CGRect)frame
{
    //클립핑 영역을 만든다.
    [self setClippingAreaFromPath:[self waterDropPath:frame]];
}
- (void)setClippingAreaFromPath:(UIBezierPath *)path
{
    CAShapeLayer *mask = [CAShapeLayer layer];
    mask.path = path.CGPath;
    
    self.layer.mask = mask;
}

3. 결과물

 

 위 소스의 sp, cp, b1, c1, c2, c3 위치를 왼쪽에 표시하였습니다.




2014년 7월 13일 일요일

[iOS] 크기에 맞게 Font의 크기 줄이기.

Storyboard에서 문자열을 표시를 해야 되는데...
단어나, 길지 않은 문자열은 크기를 200포인트로 표시하고, 만약 길이가 길다면, 그보다 작은 크기로 크기가 맞을 때까지 폰트의 크기를 줄이는 기능이 필요합니다.


//1
+ (UIFont *) spq_getFontForLabel:(NSString *)string font:(UIFont *)font size:(CGSize)size
{
    BOOL isEnough = NO;
    UIFont *afont = font;    //2
    while (!isEnough) {
        //3
        CGRect aboutRect = [string //높이를 구할 NSString
                            boundingRectWithSize:CGSizeMake(size.width*0.9, CGFLOAT_MAX)
                            options:NSStringDrawingUsesLineFragmentOrigin
                            attributes:@{NSFontAttributeName:font}
                            context:nil];
        //4
        if (aboutRect.size.height > (size.height*0.7)) {
            //5
            font = [UIFont fontWithName:font.fontName size:font.pointSize*0.9];
        }else{
            isEnough = YES;
            afont = font;
        }
    }
    return afont;
}

위 소스에서
1: 대상이 되는 문자열과, 그 문자열에 적용되어 있는 Font, 그리고 어떤 너비에서 계산을 해야 되는지 파라미터로 넘겨 줍니다.
2 : while문을 통해서, 크기가 작어질 때까지 적용 합니다.
3 : NSString의 boundingRectWithSize를 이용해서, 대략적인 rect를 구합니다.
     여기에서, 너비의 90%정도를 고려해서 넣습니다. 100% 크기를 하면 옆으로 너무 딱 붙어서 계산이 되어서, 90%크기로 적용해 줍니다.
4 : 계산된 크기가, 대상 높이의 70%보다 크면, 줄여 줍니다. 70%는 대략적으로 생각한 것으로 높의 70%이하가 되는 것이 적당하였습니다. (적용할 때, 변경해서 보시면 됩니다.)
5 : 아직 커서 줄여야 되면, 현재 font 의 name과 크기의 90%를 적용해서 폰트를 다시 만들고 적용해 줍니다.
    이 폰트를 가지고 while문 안에서 다시 계산을 하게 됩니다.

감사합니다.

2014년 7월 12일 토요일

[iOS] 문자열에 대해서, 언어 알아내기.

문자열에서 언어에 따라서 다른 폰트를 설정해야 하는 경우가 있습니다.
이럴 경우, NSString에서 언어를 알아내서, 적절하게 구분할 수 있습니다.

- (NSString *)languageForString:(NSString *) text{
    return (NSString *)CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage((CFStringRef)text, CFRangeMake(0, MIN(text.length, 100))));
}

참고: How to deal with the iOS detect the NSString language, English can not be detected accurately?

[iOS] 무료사운드 구해서, AVAudioPlayer로 Play하기

사운드 찾기

무료 사운드를 받을 수 있는 곳은 찾아보면 많습니다.

필요한 mp3파일을 찾아서 다운받습니다.

사운드 변경하기

보통 MP3파일을 받게 되는데, 이것을 CAF파일로 변경합니다.
터미널에서 파일(bass_ring.mp3)를 caf로 변경합니다.

afconvert -f caff -d LEI16@22050 brass_ring.mp3 brass_ring.caf

iOS에서 play하기

AVAudioPlayer를 생성하고, 필요한 시점에 play/stop을 실행합니다.

//선언
  AVAudioPlayer *_backgroundMusic;

//viewDidLoad에서 준비
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"brass_ring" withExtension:@"caf"];
    _backgroundMusic = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
    [_backgroundMusic prepareToPlay];

}
//viewWillAppear에서 실행
- (void)viewWillAppear:(BOOL)animated{
    [_backgroundMusic play];
}
//viewWillDisappear에서 중지
- (void)viewWillDisappear:(BOOL)animated
{
    [_backgroundMusic stop];
}



2014년 7월 11일 금요일

[iOS] CoreData관련한 몇가지 정리할 내용들..

개발하면서, CoreData를 사용할 때 필요한 내용을 정리합니다.

RAW SQL

CoreData관련 코드를 작성하고 테스트하다 보면, 실제 SQL이 어떻게 동작되는지 궁금할 때가 있습니다.
어떤 SQL문이 언제 호출이 되어서 실행이 되는지 보면, Commit을 언제 해야 될지 적절할 시기를 알 수 있습니다.
XCode의  Scheme을 드롭다운해서 열고, Edit Scheme을 선택, 'Run {app name}'을 선택하고, Arguments 탭에서 Arguments Passed On Launch에 '+'버튼을 선택해서 아래 내용을 추가합니다.
'-com.apple.CoreData.SQLDebug 1'
그리고 실행을 하면, 실제 SQL문이 어떻게 호출 되는지 표시가 됩니다.


CoreData에서 Object가져오기.

AppDelegate에 추가한 함수를 통해서 managedContext를 받아와야 읽거나, 저장할 수 있습니다.
CoreData에서 데이터를 fetch해서 가져오기 위해서, NSFetchRequest를 만들고, executeFetchRequest를 통해서, NSManagedObject의 형태로 받아 옵니다.
받아올 때, 필터링하기 위해서,  NSPredicate를 설정하고, 정렬해서 보기 위해서 NSSortDescriptor를 설정합니다.

- (void) function
{
    NSError *error;
    SPQAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    NSManagedObjectContext *managedContext = [appDelegate managedObjectContext];
    
    NSFetchRequest *request;
    request = [NSFetchRequest fetchRequestWithEntityName:entityName];
    request.predicate = [NSPredicate predicateWithFormat:@"name = %@ && created=%@", name, (created)?@"YES":@"NO"];
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
    request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
    NSArray *items = [managedContext executeFetchRequest:request error:&error];
    if (error != nil) {
        NSLog(@"executeFetchRequest: %@", [error localizedDescription]);
    }
  //....

}


새로운 Object 만들기

신규로 object를 만들고, 거기에 값을 넣은 후, 저장을 하면, CoreData에 저장이된다.
    item = [NSEntityDescription insertNewObjectForEntityForName:entityName //추가할 entity이름
                                         inManagedObjectContext:managedContext];


기존 Object 지우기.

managedObject를 찾은 다음, 그 object를 manageObjectContext에서 삭제하면 된다.
    [managedContext deleteObject:item]; //item은 NSManageObject 임.

Boolean값을 비교하기

predicate를 만들 때, String은 '=='로 비교를 하지만, Bool인 경우에는 NSNumber를 이용해서 비교를 해야 한다.
    [NSPredicate predicateWithFormat:@"name == %@, created == %@", name, [NSNumber numberWithBool:created]];

제가 필요한 것만 정리를 해 둡니다.

2014년 5월 22일 목요일

[iOS] Poster Scroll View를 만들어 봅니다.

영화관 앱에서 포스터를 옆으로 넘길 때, 현재 페이지의 이미지와, 다음페이지의 이미지가 약간 겹처서 넘어갑니다.
Yahoo 날씨 앱에서도 각 페이지를 양 옆으로 넘길 때, 이미지가 겹쳐서 넘어가게 됩니다.

이것을 스크롤 뷰를 이용해서 구현해 보도록 하겠습니다.

1. 전체 구현방법

 스크롤 뷰에 현재 frame크기의 각 페이지로 커스텀 뷰(DBUMoviePosterView)를 추가하고, 화면이 이동할 때, 커스텀 뷰에서 이미지의 위치를 조정해주게 된다.
 이 커스텀 뷰에서 추가된 내부 뷰가 이동할 때, 배경이 바깥 부분으로 나가지 않도록 레이어의 masksToBound도 설정해 준다.
 오른쪽에서 왼쪽으로 다음페이지의 이미지가 나올 때는, 현재 위치를 계산해서, 중간에서 오른쪽으로 이동하도록 하고,
 왼쪽에서 오른쪽으로 이전 페이지의 이미지가 나올 때, 중간에서 왼쪽으로 이동하도록 한다.

2. 소스 구현

 - DBUScrollView
source code
@interface DBUScollView()
@property (nonatomic, strong) NSMutableArray *posters;
@end

@implementation DBUScollView
- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        self.delegate = self;
    }
    return self;
}
- (NSMutableArray *)posters
{
    if (_posters == nil) {
        _posters = [NSMutableArray array];
    }
    return _posters;
}
- (void)addPoster:(UIImage *)image
{
    CGSize size = self.frame.size;
    //포스터 전체 개수
    NSUInteger posterTotalCount = [self.posters count];
    //content 영역 설정
    self.contentSize = CGSizeMake(size.width * (posterTotalCount+1), size.height);
    // 포스터 뷰 생성 및 추가
    DBUMoviePosterView *posterView = [[DBUMoviePosterView alloc] initWithFrame:CGRectMake(size.width*(posterTotalCount), 0, size.width, size.height)];
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:posterView.bounds];
    imageView.image = image;
    [posterView addView:imageView];
    [self addSubview:posterView];

    //위치 이동을 위해서, Array로 보관
    [self.posters addObject:posterView];
}

#pragma mark - UIScrollViewDelegate
- (void)posterImagePosition:(NSInteger)posterIndex point:(CGPoint)point
{
    if (posterIndex < 0 || posterIndex >= self.posters.count) {
        return; //페이지 수를 벗어난 것이면, 무시한다.
    }
    DBUMoviePosterView *posterView = (DBUMoviePosterView *)self.posters[posterIndex];
    if (posterView == nil) return;
    [posterView moveViewPosition:point]; //각 포스터 뷰에서 내부 뷰를 이동시킨다.
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    //현재 페이지를 계산해서, 연재와 다음 것만 움직이도록 한다.
    NSUInteger currentPage = scrollView.contentOffset.x / scrollView.frame.size.width;
    //계산은 각 포스터 뷰에서 계산한다.
    [self posterImagePosition:currentPage point:scrollView.contentOffset];
    [self posterImagePosition:currentPage+1 point:scrollView.contentOffset];
}
@end



- DBUMoviePosterView
source code
#import "DBUMoviePosterView.h"
@interface DBUMoviePosterView()
{
    UIView *_view;
}
@end

@implementation DBUMoviePosterView
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        self.layer.masksToBounds = YES;//
    }
    return self;
}
- (void) addView:(UIView *)view
{
    if (_view != nil) {
        [_view removeFromSuperview];
        _view = nil;
    }
    _view = view;
    [self addSubview:view];
}
- (void) moveViewPosition:(CGPoint)point
{
        CGFloat width = self.frame.size.width;
    CGFloat height = self.frame.size.height;
    CGFloat x = point.x - self.frame.origin.x;
    
    if (x > -width && x < width) {
        //현재 위치와 비교한 값의 절반을 x좌표로 한다.
        _view.frame = CGRectMake(x/2, point.y, width, height);
    }
}
- (void) moveViewPositionToInitial
{
    //(0,0)으로 원위치 시킴.
    [self moveViewPosition:CGPointMake(0, 0)];
}


3. 결과물




[iOS] sizeWithFont를 대체하는 함수 사용법

iOS7이전에는 NSString이 특정 가로 크기에서 줄바꿈 등을 통해서, 높이가 얼마나 되는지 알아보기 위해서는 sizeWithFont:constrainedToSize를 사용하였는데, 7.0에서는 Deprecated가 되어서 변경해야 합니다.
iOS7에서는 font뿐만 아니라, 다른 Attribute들도 높이를 구할 때 필요하므로 font만 참고하는 것이 아니라, attributes라고 NSDictionary를 받아서 구하게 됩니다.

sizeWithFont:constrainedToSize (Deprecated)
    //특정 영역 (268, 4000)의 크기에서 myString에 있는 글의 높이를 iOS6에서 구하는 방법
    UIFont *font = [UIFont fontWithName:@"HelveticaNeue-Light" size:10];
    CGSize aboutSize = [myString sizeWithFont:font constrainedToSize:CGSizeMake(268, 4000)];


Xcode5에서는 boundingRectWithSize:options:attributes:context:를 사용하라고 나옵니다.

    //폰트가 따로 정리가 되어 있지 않는 경우는
    UIFont *font = [UIFont fontWithName:@"HelveticaNeue-Light" size:10];
    CGRect aboutRect = [myString //높이를 구할 NSString 
                          boundingRectWithSize:CGSizeMake(268, CGFLOAT_MAX) 
                                       options:NSStringDrawingUsesLineFragmentOrigin 
                                    attributes:@{NSFontAttributeName:font} 
                                       context:nil];

만약 일반 NSString이 아니고, NSAttributedString을 사용하고 있으면, attributes가 없는 함수를 사용하면 됩니다.

    //폰트가 따로 정리가 되어 있지 않는 경우는
    UIFont *font = [UIFont fontWithName:@"HelveticaNeue-Light" size:10];
    NSAttributedString *myAttributedString = [[NSAttributedString alloc] initWithString:myString
               
    CGRect aboutRect = [myAttributedString //높이를 구할 NSAttributedString
                         boundingRectWithSize:CGSizeMake(268, CGFLOAT_MAX) 
                                      options:NSStringDrawingUsesLineFragmentOrigin
                                      context:nil];                                                              

정리해 둡니다.

2014년 5월 17일 토요일

[iOS] Facebook에 글과 이미지, 링크 등록하기.

iOS6 부터 Facebook에 글을 올릴 수 있는 방법을 제공하고 있습니다.
내부 동작방식은 Twitter에 올리는 것과 동일한 방법에 서비스 타입만 변경하면 됩니다.

Twitter에 글 올리기 

위의 트위터에 글 올리기에 설명한 것과 같이, 이미지와 URL을 추가할 수 있습니다.

 source code
- (IBAction)uploadToFacebook:(UIBarButtonItem *)sender {
    if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeFacebook]) {
        SLComposeViewController *composer = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeFacebook];
        [composer setInitialText:@"Initial Text for Facebook :)"];//초기 메시지.
        [composer addImage:self.uploadImage]; //추가할 이미지
        [composer addURL:[NSURL URLWithString:APP_URL_IN_ITUNES]];//추가할 URL
        composer.completionHandler = ^(SLComposeViewControllerResult result){
            switch(result) {
                    //  This means the user cancelled without sending the Tweet
                case SLComposeViewControllerResultCancelled:
                    break;
                    //  This means the user hit 'Send'
                case SLComposeViewControllerResultDone:
                    break;
            }
        };
        [self presentViewController:composer animated:YES completion:^{
            //NSLog(@"present completed");
        }];
    }else{
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Sorry"
                                                            message:@"You can't upload this page to Facebook right now, make sure your device has an internet connection and you have at least one Facebook account setup"
                                                           delegate:self cancelButtonTitle:@"OK"
                                                  otherButtonTitles: nil];
        [alertView show];
    }
}

페이스북에 대한 연결이 되어 있지 않은 경우, 아래와 비슷하게 업로드를 할 수 없다고 알려줘야 합니다.
Upload가 불가능함을 알림.
만약, 아이디가 설정이 되어 있으면, 아래와 같이 Facebook Composer가 표시가 됩니다.
페이스북 타입의 SLComposeViewController
이미지를 어느 앨범에 올릴지, 현재 위치를 추가할지, 공개를 어느 범위까지 할지 표시합니다.
만약, 이 화면에서 Post를 했는데, 인터넷 연결이 안되어 있는 경우, 아래와 같이 표시가 됩니다.

Facebook에 Upload하지 못할 경우 표시화면



참고:
- Integrating Twitter and Facebook into iOS7 Applications
- Mobile and Social Network Technology - Tutorial: How to use inbuilt Facebook/Twitter API in iOS6
- iOS Programming 101: Integrate Twitter and Facebook Sharing in Your App.
- Open Source Control For Creating SLComposeViewController Type Views For Any Social Network



2014년 5월 15일 목요일

[iOS6] Twitter에 이미지와 링크 올리기

iOS6에서 트위터에 이미지와 글을 올릴 수 있는 방법을 자체 Framework으로 제공하고 있습니다.
Social.framework를 추가하고, social/social.h 를 임포트한 후에 실행 시킬 수 있습니다.

1. 트윗을 보낼 수 있는지 확인하는 코드
  SLComposerViewController의 isAvailableForServiceType 함수를 통해서 확인할 수 있으며, 전송하지 못할 경우, 사용자에게 적절한 문구를 보여줘야 합니다.

2. 전송한 결과값을 받아야 하는 경우
  completionHandler에 블록 함수를 설정해서, 결과를 받아올 수 있습니다.

3. 초기 메시지 추가
 - setInitialText를 통해서 초기 메시지를 넣을 수 있습니다.

4. 이미지 추가
 - addImage : 이미지를 추가할 수 있습니다.

5. URL 추가
 - addURL: NSURL객체를 추가할 수 있습니다.

source code
#import 
@import Social;
...
- (IBAction)tweetThisPage:(UIBarButtonItem *)sender
{
    if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]) {
        SLComposeViewController *composer = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter];
        [composer setInitialText:@"This is my first tweet in my app!"];
        [composer addImage:[self loadImage]];
        [composer addURL:[NSURL URLWithString:APP_URL_IN_ITUNES]];
        composer.completionHandler = ^(SLComposeViewControllerResult result){
            switch(result) {
                    //  This means the user cancelled without sending the Tweet
                case SLComposeViewControllerResultCancelled:
                    NSLog(@"Canceled");
                    break;
                    //  This means the user hit 'Send'
                case SLComposeViewControllerResultDone:
                    NSLog(@"Tweet Done");
                    break;
            }
        };
        [self presentViewController:composer animated:YES completion:^{
            NSLog(@"Tweet Composer present completed");
        }];
    }else{
        //트위터 아이디가 설정되어 있지 않거나, 인터넷에 연결이 안되어 있는 경우..
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Sorry"
                                                            message:@"You can't send a tweet right now, make sure your device has an internet connection and you have at least one Twitter account setup"
                                                           delegate:self cancelButtonTitle:@"OK"
                                                  otherButtonTitles: nil];
        [alertView show];
    }
}

설정에 트위터 아이디가 설정이 되어 있지 않으면, 아래와 같이 표시가 됩니다.
Twitter ID가 없는 경우
아이디가 설정화면에서 정상적으로 설정이 되어 있으면, 아래와 같이 표시됩니다.
설정된 트위터 아이디로 트윗을 작성중.
만약 작성 중에, 인터넷 연결이 끊기게 되면, 아래와 같이 Post를 하였을 때, 표시됩니다.
개발자가 해준 부분은 없으며, 시스템에서 자동으로 표시하는 화면입니다.
트윗을 보내지 못했을 때, 표시화면
위 화면이 표시될 경우에도, completionHandler에는 정상적으로 전송이 되었다고 결과 값을 리턴하게 되므로, 구분을 잘할 수 있어야 겠습니다.

참고:

 트위터 개발자 센터 문서: Integrating with Twitter on iOS
 Ray Wenderlich : Beginning Twitter in iOS6 Tutorial

2014년 5월 12일 월요일

[iOS] UIScrollView에서 현재 중심이 되는 페이지 얻어오기.

ScrollView에서 페이지 단위로 뷰를 로딩해서 화면에 보여줄 경우, 현재 페이지의 앞뒤 페이지만 로딩하도록 해서 지금 필요한 페이지만 메모리를 사용할 수 있게 만들어야 됩니다.
100페이지의 ScrollView라 할지라도, 현재는 3페이지만 로딩하도록 하는 것입니다.
이 경우, 현재 페이지가 몇 페이지인지 알아야 합니다.

페이지를 가로로 이동하는 경우에는,  페이지의 너비와 현재 페이지의 x좌표를 이용하여 알아낼 수 있습니다.

사용자가 페이지를 터치해서 이동할 경우, scrollView.contentOffset.x 값이 변하게 됩니다.
source code
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGFloat pageWidth = self.scrollView.frame.size.width;
    NSInteger page = (NSInteger)floor((self.scrollView.contentOffset.x * 2.0f + pageWidth) / (pageWidth * 2.0f));
    NSLog(@"scrollView.contentOffset.x: %f, page:%d", scrollView.contentOffset.x, page);
    
}

페이지를 왼쪽으로 이동시키다가, 현재 화면의 반을 넘어가는 경우, 다음 페이지가 되고, 오른쪽으로 이동시키다가 현재 페이지의 반 이하가 남게 되면, 이전 페이지로 인식해야 합니다.
현재 페이지 = (x좌표 / 페이지너비 + 0.5)값의 반내림
현재 페이지 = ((x좌표 + 1/2*페이지너비) / 페이지너비) 값의 반내림
현재 페이지 = ((x좌표 + 페이지너비) / (2*페이지너비)) 값의 반내림

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGFloat pageWidth = self.scrollView.frame.size.width;
    NSInteger page = (NSInteger)floor((self.scrollView.contentOffset.x * 2.0f + pageWidth) / (pageWidth * 2.0f));
    NSLog(@"scrollView.contentOffset.x: %f, page:%d", scrollView.contentOffset.x, page);
    
}

scrollView가 가로로 Scroll되었을 때, 현재 페이지를 알 수 있으므로, 페이지가 변경되면, 앞뒤 페이지를 로딩하여, 미리 준비하면 됩니다.


[참조] : http://www.raywenderlich.com/10518/how-to-use-uiscrollview-to-scroll-and-zoom-content

[iOS] CALayer를 이용한 액자 모양 그림자 넣기.


기존의 CALayer를 이용해서, shadow를 설정할 수 있었습니다.
그 방식에 shadowPath를 설정하여서 그림자의 모양을 변형하는 방법을 정리합니다.

참조: NSCookbook - iOS Programming Recipe 10: Adding A Shadow To UIView


ViewController에  UIImageView를 하나 추가하고, 아래와 같이 설정을 합니다.
source code
    CALayer *layer = self.imageView.layer;
    layer.shadowOffset = CGSizeMake(1, 1);
    layer.shadowColor = [[UIColor blackColor] CGColor];
    layer.shadowRadius = 3.0f;
    layer.shadowOpacity = 0.80f;

    CGRect rect = self.imageView.frame;
    CGSize size = rect.size;
    CGFloat offset = 15.0f;
    UIBezierPath *path = [UIBezierPath bezierPath]; //Path를 설정
    [path moveToPoint:CGPointMake(0.0, offset)];
    [path addLineToPoint:CGPointMake(size.width, offset)];
    [path addLineToPoint:CGPointMake(size.width, size.height+offset)];
    [path addCurveToPoint:CGPointMake(0.0, size.height+offset)
            controlPoint1:CGPointMake(size.width-offset,size.height)
            controlPoint2:CGPointMake(offset, size.height)];
    [path closePath];
    layer.shadowPath = [path CGPath];

위의 shadowPath를 bezierPath로 만들어서 넣으면 그 Path를 따라서 그림자가 생기게 됩니다.




[참고]
이미지 URL: http://i.dailymail.co.uk/i/pix/2014/05/04/article-2620040-1D92597400000578-908_306x423.jpg

2014년 5월 4일 일요일

[iOS] TextView의 InnerShadow를 만들어 봅니다.

TextView의 배경에 안쪽 그림자를 만들어봅니다.

Inner Shadow가 적용된 화면

위의 그림처럼 TextView에서 InnerShadow를 이미지를 사용하지 않고 넣도록 합니다.

구현 방법은?

일단 배경이미지를 사용해서, 넣을 수도 있는데, 그렇게 되면, 메인 뷰의 배경이 변경이 되면 그것을 따라가지 못하고, 이미지의 색이 배경과 다르게 흰색 태두리가 생길 수 있습니다.
 그래서, CALayer를 이용해서, 그림자 효과로 만들어 봅니다.

- UIView에 대한 그림자 설정.

일단 일반적인 UIView의 CALayer에 그림자 설정과, 태두리를 둥글게 만들어 봅니다.
source code
    UIView *view;
    UIView *view;
    view = self.testView; //코드를 설명을 위해서 대입함.
    view.backgroundColor = [UIColor clearColor]; //배경을 투명하게 설정
    view.layer.cornerRadius = 10.0f;      //테두리 곡선이 되도록 설정 10.0f는 임의의 수
    view.layer.borderColor = [UIColor whiteColor].CGColor; //테두리 색상 흰색
    view.layer.borderWidth = 2.0f;                         //테두리 두께 2포인트
    view.layer.shadowColor = [UIColor blackColor].CGColor; //그림자 검은색
    view.layer.shadowOffset = CGSizeZero;   //그림자 위치는 뷰의 위치와 동일
    view.layer.shadowOpacity = 1.0f;        //그림자 투명도
    view.layer.shadowRadius = 3.0f;         //그림자의 퍼지는 범위 클수록 넓게 그림자 생김.

Storyboard에서 지정한 UITextField에 대해서 위와 같이 설정을 하면, 아래와 같이 표시가 됩니다.
위에서 코드를 추가합니다.
source code
    view.layer.masksToBounds = YES; //바운드 외부로 나가는 것을 Mask처리함
이렇게 하면 외부로 그림자가 나가는 것은 지워집니다.

이제 내부 테두리 비슷한 것이 설정이 되었습니다.
이것을 UITextView에 적용을 하면 아래와 같아집니다.
source code
    self.textView.backgroundColor = [UIColor clearColor];
    view = self.textView; //UITextView로 변경하고 설정함.
    view.backgroundColor = [UIColor clearColor];
    view.layer.cornerRadius = 10.0f;
    view.layer.borderColor = [UIColor whiteColor].CGColor;
    view.layer.shadowColor = [UIColor blackColor].CGColor;
    view.layer.borderWidth = 2.0f;
    view.layer.shadowOffset = CGSizeZero;
    view.layer.shadowOpacity = 1.0f;
    view.layer.shadowRadius = 3.0f;
    view.layer.masksToBounds = YES;

설정은 UIView에 했던것과 동일하고, 이 설정을 TextView에 한 것입니다.
테두리에 그림자가 생기기는 했지만, 내부의 글자에도 그림자가 표시되는 문제가 발생합니다.
이것을 어떻게 처리할 수가 없어서,
TextView의 자리 배경에 UIView를 두어서 TextView의 주변에 inner shadow가 있는 것처럼 설정을 합니다.

source code
-(void)drawInnerShadowOnViewForTextView:(UIView *)view color:(UIColor *)backgroundColor;
{
    UIEdgeInsets insets = {-10, -15, +20, +30};
    CGFloat cornerRadius = 18.5f;
    CGFloat borderWidth = 2.0f;
    CGFloat shadowRadius = 3.5f;
    CGFloat shadowOpacity = 1.0f;
    
    //그림자가 되는 View를 설정합니다.
    UIView *innerShadowView = [[UIView alloc] initWithFrame:CGRectMake(view.frame.origin.x+insets.top, view.frame.origin.y+insets.left, view.frame.size.width + insets.bottom, view.frame.size.height+ insets.right)];
    [innerShadowView setContentMode:UIViewContentModeScaleToFill];
    [innerShadowView setAutoresizingMask : UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
    [innerShadowView.layer setCornerRadius:cornerRadius]; //그림자의 가장자리 곡선
    [innerShadowView.layer setMasksToBounds:YES];
    [innerShadowView.layer setBorderColor: backgroundColor.CGColor]; //외부 배경을 테두리 색으로
    [innerShadowView.layer setShadowColor:[UIColor blackColor].CGColor]; //그림자 색
    [innerShadowView.layer setBorderWidth:borderWidth];
    [innerShadowView.layer setShadowOffset:CGSizeMake(0, 0)];
    [innerShadowView.layer setShadowOpacity:shadowOpacity];
    [innerShadowView.layer setShadowRadius:shadowRadius];
    
    [view.superview insertSubview:innerShadowView belowSubview:view];
}

위와 같이 inner shadow 역할을 하는 UIView를 만들어서, TextView의 바로 아래에 설정을 합니다.
얼추 비슷하게 나왔습니다.
헌데, View의 배경을 변경해 보면, 이상한 현상이 발생합니다.

inner shadow로 추가된 이미지 뒤에 같은 크기의 흰색 배경의 UIView를 추가해 줍니다.
source code
-(void)drawInnerShadowOnViewForTextView:(UIView *)view color:(UIColor *)backgroundColor;
{
    ...위와 동일...
    [innerShadowView.layer setShadowRadius:shadowRadius];

    //추가 시작
    UIView *innerBackgroundView = [[UIView alloc] initWithFrame:innerShadowView.frame];
    [innerBackgroundView setBackgroundColor:[UIColor whiteColor]];
    [innerBackgroundView.layer setCornerRadius:cornerRadius];
    [innerBackgroundView.layer setBorderWidth:borderWidth];
    [innerBackgroundView.layer setBorderColor:backgroundColor.CGColor];
    [innerBackgroundView setAlpha:0.8f];
    
    [view.superview insertSubview:innerBackgroundView belowSubview:view];
    //추가 끝.
    [view.superview insertSubview:innerShadowView belowSubview:view];

}
뷰의 추가 위치가 textView 밑에 innerShadow, 그 밑에 innerBackView가 추가되어야 합니다.
위와 같이 표시가 됩니다.
이제 배경색에 구애 받지 않고, 흰색으로 유지할 수 있습니다.

그리고, 배경화면이 있을 경우에는, TextView와 innerBackView의 alpha를 변경해서, 아래와 같이 표시할 수도 있습니다.


참고되시길 바랍니다.


[참고]
- StackOverflow: UIView create a inner shadow

2014년 5월 1일 목요일

[iOS] Custom TableViewCell의 Button을 통해, 현재 Cell의 indexPath를 알아내는 방법



1. IBAction 함수로 넘기는 sender의 point를 통해서 알아내는 방법

 TableViewController에 IBAction함수를 정의하고, Cell에 있는 Button에서 Touch Up inside 이벤트에 대해서 TabelViewController의 아래 함수로 연결을 합니다.
 그러면, 특정 Cell에서 버튼이 클릭이 되면, 그 버튼객체를 sender로 해서 아래 함수가 호출이 됩니다.
source code
- (IBAction) btnClicked:(id)sender
{
    // (1)
    CGPoint buttonPoint = [sender convertPoint:CGPointZero toView:self.tableView];
    // 2.
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPoint];
    if (indexPath != nil) {
        //Found Cell
    }
}
 (1) Cell 내에 있는 Button을 sender로 받고, 그 sender의 위치 포인트를 알아냅니다.
 - (CGPoint) convertPoint:(CGPoint)point toView:(UIView *)view
: 자신의 좌표 체계에 있는 point 지점을 view의 좌표 체계에서 어느 지점인지 변환함.
즉, Cell 내에 있는 Button의 특정 지점(0,0)을 TableView에서 어느 지점 인지 변환해서 TableView에서의 좌표로 변환을 합니다.
 (2) 그리고, 그 포인트에 해당하는 IndextPath를 찾기 위해서, UITableView의 - indexPathForRowAtPoint를 사용해서, 현재 포인트에 해당하는 indexPath를 구할 수 있습니다.

2. Cell 을 통해서 알아내는 방법

TableView의 함수 중에서 - indexPathForCell:(UITableCell*)를 이용할 수 도 있습니다.
IBAction이벤트를 받은 함수에서 UITableCell을 가져올 수 있다면, 바로 indexPath를 찾을 수 있을 것입니다.
 하지만, 상위 View에서 UITableViewCell을 찾아서 이용해야 합니다.
source code
- (IBAction) btnClicked:(id)sender
{
    NSLog(@"btnClicked : %@", sender);
    UIView *myView = sender;
    UIView *superView = [myView superview];
    while (superView != nil && ![superView isKindOfClass:[DBUTableViewCell class]]) {
        myView = superView;
        superView = [myView superview];
    }
    if (superView != nil) {
        //찾았다.
        NSIndexPath *indexPath = [self.tableView indexPathForCell:(UITableViewCell*)superView];
        if (indexPath != nil) {
            //이제 찾았다.
            [_items removeObjectAtIndex:indexPath.row];
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
    }
}

위와 같이 받은 sender를 통해서 상위에 있는 UITableViewCell을 찾고, TableView의 indexPathForCell을 이용해서 알아 낼 수 있습니다.
그러나, iOS6과 iOS7에서 TableViewCell의 구조가 조금 바뀌었기 때문에 달라질 수 있습니다.


참고하시기 바랍니다.

[참조]
http://code.tutsplus.com/tutorials/blocks-and-table-view-cells-on-ios--mobile-22982

2014년 4월 30일 수요일

[iOS7] UIViewController의 Adjust Scroll View Insets는?

 ScrollView를 전체 화면에 넣고, 이미지를 중앙에 보여주게 하였는데, 아래로 내리면, ScrollView의 위치가, Top Bar밑으로 자동으로 조정이 되어서, 이 기능을 빼는 것을 블로그로 정리합니다.

iOS7에서, NavigationController의 Top Bar가 있을 경우, TableView등의 ScrollView에서는 자동으로 내부 Content영역이 Top Bar밑으로 내려오게 됩니다.

TableView의 경우에는 아래로 내리면 첫번째 Row가 Top Bar 밑으로 표시 되고, 그 밑으로 들어가면 화면에서 가리게 됩니다.
그리고, 위로 올리면, Top Bar뒷면으로 Cell들이 올라가게 됩니다.

어떤경우에 문제가 될까요?
TableView의 경우에는 아주 좋은 기능으로, 첫번째 Row가 잘 표시가 됩니다만, ScrollView의 경우에는 조금 달라집니다.
Top bar밑으로 나왔다가 안 나왔다가 하면 문제가 됩니다.

이 기능을 컨트롤 할 수 있는 방법은
Storyboard의 ViewController에서 Adjust Scroll View Insets를 체크하거나,

automaticallyAdjustsScrollViewInsets를 NO 로 설정하여 기능을 뺄 수 있습니다.
이 기능을 빼면, 자동으로 contentInsets.top 이 설정되지 않으므로 가장 위까지 content영역으로 됩니다.

TableViewController에서 automaticallyAdjustsScrollViewInsets를 NO로 설정하게 되면, row 0, 1은 top bar의 뒷면에 위치하게 되어서, 아래로 내리지 않는 이상을 볼 수도 없고, 선택할 수도 없게 됩니다.

이 기능은 iOS7에서만 지원하는 것이므로, iOS6에서 기능 설정을 무시하게 하려면, 함수가 지원하는지 respondsToSelector를 통해서 확인하고 설정하면 되겠습니다.
if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) {
    self.automaticallyAdjustsScrollViewInsets = NO;
}



[참고]
- iOS 7 UI Transition Guide : AppearanceCustomization

[iOS] Unwind Segue 사용하기.

Segue를 이용해서, Unwind Segue를 사용하는 방법을 간단히 정리합니다.

iOS에서 Storyboard를 이용하여, ViewController 1(이하 VC1)에서 다른 ViewController 2 (이하 VC2)로 이동을 하게 되고, 뒤로는 Back 버튼을 이용해서, 이동하게 됩니다.
어떤 에러로 인해서, 반대로 즉 뒤로 이동해야 할 경우도 있습니다.

이런 경우, Delegate를 사용해서, VC2의 델리게이트로 설정한 VC1의 함수를 호출하여, dismissViewControllerAnimated:complation을 이용해서 화면에 표시된 VC2를 제거하여 Back을 하였습니다.

화면이동으로 Segue를 이용하듯이, Unwind Segue를 이용해서, 화면 Back으로 이동할 수 있습니다.
 그럼, segue와 unwind segue의 차이점은 뭘까요?
 차이점은 Segue는 VC1에서 VC2로 이동하면, 신규로 VC2를 생성하고 NavigationViewController의 presentaion chain에 쌓아 가면서 이동하지만, unwind segue는  기존에 화면에서 표시되었던 VC로만 이동할 수 있어서, 신규로 VC를 만들지 않습니다.
즉, A-B-C-D의 형태로 뷰컨트롤러를 화면에 표시했다면, D에서 A로 Unwind segue로 이동하면서, 중간 VC들을 삭제하게 되는 것입니다.

이용방법

1. back으로 이동할 ViewController1에 함수를 설정합니다.

source code
- (IBAction) exitFromSecondViewController:(UIStoryboardSegue *)segue
{
    //Back으로 올때 호출되는 함수
    // segue를 통해서, 어느 뷰컨트롤러에서 오는 것인지 구분할 수 있다.
    NSLog(@"back from : %@", [segue.sourceViewController class]);
}

이름은 무엇이든 상관없고, 변수로 UIStoryboardSegue를 받아야 하고, 전 프로젝트에 걸쳐, 구분이 되도록 해야 합니다.
 주의 할 것은 VC 1에 위 함수를 추가해야 됩니다. 즉, VC 1으로 이동하고 난 후에 위 함수가 호출이 되는 것입니다.

2. VC2에서 unwind segue를 설정합니다.

Storyboard에서 VC2에서 아래와 같이 VC아이콘에서 exit 아이콘으로 Ctrl+드래그를 선택합니다.
Ctrl+드래그를 합니다.

VC1에 추가하였던 함수가 표시되고, 그것을 선택합니다.
위와 같이 unwind segue를 추가하고, identification을 "UnwindingSegueID"로 설정합니다.

3. Unwind Segue를 호출 합니다.

source code
- (IBAction)backButton:(id)sender //Back버튼은 VC2 화면에 추가된 버튼입니다.
{
    [self performSegueWithIdentifier:@"UnwindingSegueID" sender:self];
}

[추가] 만약, 여러 VC2에서 VC3를 호출하였고, VC3에서 VC1으로 이동하면 어떻게 될까요?

아래와 같이 설정을 하고 테스트를 하면,

VC1 - VC2 - VC3까지 Segue를 통해서 호출을 하면, presentation chain에 순서대로 쌓이게 됩니다.
VC3에서 VC1으로 중간에 VC2를 거치지 않고, unwind segue를 통해서 이동을 하면, VC1으로 이동하고, VC2와 VC3를 차례로 dealloc하게 됩니다.
log
 

[2339:60b] VC1 performSegueWithIdentifier called : GoSegueID
               : VC1에서 segue를 코드로 호출해서 VC2로 이동하도록 호출합니다.
[2339:60b] VC1 prepareForSegue id:GoSegueID to : DBUSecondViewController
               : VC1의 prepareForSegue함수가 호출이됩니다.

[2339:60b] VC2 prepareForSegue id:GoThirdSegueID to : DBUThirdViewController
               : VC2에서 버튼으로 바로 segue 를 호출합니다.

[2339:60b] VC3 performSegueWithIdentifier called : backToOneSegueID
               : VC3에서 unwind segue를 호출합니다.
[2339:60b] VC3 PrepareForSegue id: backToOneSegueID, to DBUViewController
               : prepareForSegue로 segue가 호출 되었음을 알려줍니다.
[2339:60b] VC1 exitFunction called in VC1 from : DBUThirdViewController
               : VC1으로 이동되고, 어디서 왔는지 sourceController를 통해서 알 수 있습니다.
[2339:60b] dealloc : <dbusecondviewcontroller: 0x8e0d670="">
               : VC2가 삭제되고..
[2339:60b] dealloc : <dbuthirdviewcontroller: 0x9947ee0="">
               : VC3가 삭제됩니다.

[참고]

Apple Tech Notes 2298
Using iOS Storyboard Segues

- Segue관련 함수는? (블로그 : [iOS] Segue 관련 함수]



2014년 4월 18일 금요일

[iOS] ZipArchive를 사용해서, Zip파일을 Unzip합니다.

iOS기기에서 zip으로 압축된 데이터를 다운 받고, 그것을 풀어야(Unzip)할 경우가 있습니다.
이 경우, ZipArchive를 이용해서 해결할 수 있습니다.

[ZipArchive는?]

오픈소스 코드인 MinZip을 기반으로 zip파일을 압축/해지를 iOS에서 사용할 수 있도록, Objective-C Class로 만들어 놓은 것입니다.

[어디서 구할 수 있나요?]

구글 코드사이트에서 나와 있습니다. (https://code.google.com/p/ziparchive/)
설명도 위 사이트에 자세히 나와 있습니다.


[사용하기 위해서]

1. 라이브러리를 등록합니다.
 프로젝트 설정에서 Framwork 등록하는 부분에서 아래와 같이 libz.dylib를 추가합니다.


2. ZipArchive 파일을 프로젝트에 추가합니다.
 위 사이트에서 download 받아서, 프로젝트에 추가합니다.


[ 압축파일을 풀려면 어떻게 해야 되나요?]

ZipArchive의 인스턴스를 사용해서, 파일을 열고, Unzip 하고, 파일을 닫으면 됩니다.
압축을 푸는 것이 리소스를 많이 잡아먹으니, 파일을 이용하는 것 같습니다.
source code
- (void)UnzipFile:(NSString*)zipFile to:(NSString *)destFile
{
    ZipArchive *zip = [[ZipArchive alloc]init];
    if ([zip unzipOpenFile:zipFile]) {
       BOOL ret = [zip unzipFileTo:destFile overwrite:YES];
       NSAssert(ret, @"unzipfile Error");
       if (![zip unzipCloseFile]) {
           NSLog(@"unzipCloseFile Failed");
       }
    }
}

감사합니다.







2014년 4월 14일 월요일

[iOS] Segue 관련 함수

Storyboard의 Segue관련해서 몇가지 함수를 정리해 둡니다.

[Segue 선언]

- Xcode의 Storyboard에서 Ctrl+Drag를 통해서, 어디에서 어디로 이동할 것인지 Segue를 정의하고, 그 Identifier를 설정해 둡니다.

[Segue 실행]

- Storyboard에서 버튼이나 테이블뷰셀에서 호출되도록 할 수도 있고, ViewController에서 identifier로 호출할 수 있도록 설정할 수 있습니다.
- 버튼에서 Ctrl+Drag로 ViewController로 이동하면, 버튼의 Triggere Segues를 설정할 수 있습니다. 이렇게 되면, 그 버튼이 터치되면, 설정한 Segue를 실행하게 됩니다.
- ViewController에서 Ctrl+Drag를 해서 다른 ViewController로 이동해서 설정하고 id를 설정해 두면, id를 가지고 segue를 호출 할 수 있게 됩니다.
  실행함수는 performSegueWithIdentifier:sender: 함수로 id를 가지고 호출 할 수 있게 됩ㄴ디ㅏ.

[Segue 호출]

Segue가 호출되어 시작될 때, 시작됨을 알리는  -prepareForSegue:sender: 함수가 호출이되고, 이 함수에서 이동할 destinationViewController와 sourceViewController를 알 수 있고, 호출 ID를 통해서, 어떤 Segue인지를 알아서 구분할 수 있게 됩니다.
여기에서 ID에 따라서, destinationViewController에 설정을 추가해서 데이터를 전달 할 수 있게 됩니다.
- destinationViewController가 예상하는 특정 ViewController가 맞는지 확인을 위해서는 NSObject에 정의되어 있는 isKindOfClass: 함수를 이용하여 확인하는 것이 필요합니다.


[참고]
- Unwind Segue는? (블로그 : [iOS] Unwind Segue 사용하기)

2014년 4월 8일 화요일

[iOS7] Dynamic : Attachment, Push, DynamicItem Behavior

Dynamics에 대한 기본적인 구성과 사용에 대해서는 이전 블로그에서 정리를 하였고, 이번은 Dynamics를 가지고, UI를 좀더 사용자 친화적으로 꾸밀 수 있을 지 알아보겠습니다.

모든 UIKit에 물리적 성질을 추가 할 수 있는 것이 iOS7의 Dynamics 입니다.
그것을 이용해서, 초기 화면의 UILabel 들에 Dynamics를 적용해 보도록 하겠습니다.
이전에는 UIView Animation을 이용해서 하던 것을 Dynamics를 활용해서 만드는 것입니다.

버튼 밑에 설명 Label을 붙이고, 이 Label들이 모션에 의해서 흔들리도록 해보겠습니다.
아래에 사용된 이미지는 구글 이미지에서 찾은 것들로, 저작권은 전혀 저에게 있지 않습니다.( 원본은 아래에 링크를 넣었습니다.)

위 이미지에서 Travel 라벨이 그 위의 아이콘의 center와 UIAttachmentBehavior로 연결을 하고, CoreMotion으로 X,Y 값의 변화에 따라서 좌/우로 흔들리도록 UIPushBehavior를 이용했습니다.

각 컨트롤에 대해서, Property 연결 합니다.
source code
@property (strong, nonatomic) IBOutlet UIImageView *travelImageView;
@property (strong, nonatomic) IBOutlet UIImageView *bookImageView;
@property (strong, nonatomic) IBOutlet UIImageView *workoutImageView;
@property (strong, nonatomic) IBOutlet UIImageView *foodImageView;

@property (strong, nonatomic) IBOutlet UILabel *travelLogLabel;
@property (strong, nonatomic) IBOutlet UILabel *bookLogLabel;
@property (strong, nonatomic) IBOutlet UILabel *foodLogLabel;
@property (strong, nonatomic) IBOutlet UILabel *workoutLogLabel;


@property (strong, nonatomic) UIDynamicAnimator *animator;
@property (strong, nonatomic) UIGravityBehavior *gravityBehavior;
@property (strong, nonatomic) UICollisionBehavior *collisionBehavior;
@property (strong, nonatomic) UIPushBehavior *pushBehavior;

그리고, UIDynamicsAnimator, UIGravityBehavior 로 설정합니다.
- (void) setupDynamics
{
    self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
    self.gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[self.travelLogLabel, self.bookLogLabel, self.foodLogLabel, self.workoutLogLabel]];    
    [self.animator addBehavior:self.gravityBehavior];
    ...
}

각 UILabel에 대해서 UIAttachmentBehavior를 연결하고, Damping과 Frequency를 설정합니다.

#pragma mark - Dynamics Setting
- (void) setupDynamics
{
    ...
    
    //Label들에 대해서, 마찰저항을 설정하고, 회전하지 않도록 설정
    UIDynamicItemBehavior *labelItemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.travelLogLabel, self.bookLogLabel, self.foodLogLabel, self.workoutLogLabel]];
    labelItemBehavior.resistance = 1.0f;
    labelItemBehavior.allowsRotation = NO;
    [self.animator addBehavior:labelItemBehavior];

    // Travel Label에 대해서 AttchmentBehavior를 연결함.
    [self addAttachmentBehavior:self.travelLogLabel
                        atPoint:CGPointMake(self.travelImageView.center.x, self.travelImageView.center.y)
                     toAnimator:self.animator];
    
    // Book Label에 대해서 AttachmentBehavior를 연결
    [self addAttachmentBehavior:self.bookLogLabel
                        atPoint:CGPointMake(self.bookImageView.center.x, self.bookImageView.center.y )
                     toAnimator:self.animator];
    
    // Food Label에 대해서 AttachmentBehavior 연결
    [self addAttachmentBehavior:self.foodLogLabel
                        atPoint:CGPointMake(self.foodImageView.center.x, self.foodImageView.center.y )
                     toAnimator:self.animator];
    
    // Workout Label에 대해서 AttachmentBehavior 연결
    [self addAttachmentBehavior:self.workoutLogLabel
                        atPoint:CGPointMake(self.workoutImageView.center.x, self.workoutImageView.center.y )
                     toAnimator:self.animator];
    
    ....
}
- (void) addAttachmentBehavior:(UILabel *)label atPoint:(CGPoint)point toAnimator:(UIDynamicAnimator *)animator
{
    UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:label attachedToAnchor:point];
    attachment.damping = 1.0f;
    attachment.frequency = 100.0f;
    [animator addBehavior:attachment];
}

이 두 값을 설정하더라도 적절하게 멈추는 것이 아니라, 자주 흔들리게 됩니다.
아래의 왼쪽이 Attachment만 적용한 것이고, 오른쪽이 UIDynamicItemBehavior로 마찰저항을 추가한 것입니다.

 


이제, CoreMotion을 추가해서, 기기가 좌우로 흔들릴 때 마다, PushBehavior를 추가하여, Label이 흔들리도록 하겠습니다.

먼저 CoreMotion을 추가합니다.

@import CoreMotion;
...
- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
    
    _motionManager = [[CMMotionManager alloc] init];
    [self startMonitoringAcceleration];
}
...

#pragma mark - Dynamics Setting
- (void) setupDynamics
{
    ...
    
    self.pushBehavior = [[UIPushBehavior alloc] initWithItems:@[self.travelLogLabel, self.bookLogLabel, self.foodLogLabel, self.workoutLogLabel] mode:UIPushBehaviorModeInstantaneous]; //Instantaneous로 설정
    self.pushBehavior.active = NO;
    float angle = arc4random() % 360;
    self.pushBehavior.angle = (angle*M_PI/180.0);
    self.pushBehavior.magnitude = 0.4f;
    [self.animator addBehavior:self.pushBehavior];
}
#pragma mark - CoreMotion
- (void)startMonitoringAcceleration
{
    if (_motionManager.accelerometerAvailable) {
        //[_motionManager startAccelerometerUpdates];
        NSLog(@"accelerometer updates on...");
        [_motionManager setAccelerometerUpdateInterval:0.1f];
        [_motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
                                             withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
                                                 float angle = atan2f(-accelerometerData.acceleration.y, accelerometerData.acceleration.x);
                                                 //NSLog(@"accelerometer:angle:%2.02f, x:%.02f, y:%.02f, z:%.02f", 180.0+angle*180.0/M_PI, accelerometerData.acceleration.x, accelerometerData.acceleration.y, accelerometerData.acceleration.z);
                                                 
                                                 NSLog(@"angle: %0.2f, %02.2f", angle, angle / M_PI * 180.0);
                                                 if (angle > 100*M_PI/180.0) {
                                                     angle = 100*M_PI/180.0;
                                                 }
                                                 if(angle < 80*M_PI/180.0){
                                                     angle = 80*M_PI/180.0;
                                                 }
                                                 self.gravityBehavior.angle = angle;// - 180.0 * M_PI / 180.0;
                                             }
         ];
    }else{
        NSLog(@"accelerometer Unavailable");
    }
}


- (void)stopMonitoringAcceleration
{
    if (_motionManager.accelerometerAvailable && _motionManager.accelerometerActive) {
        [_motionManager stopAccelerometerUpdates];
        NSLog(@"accelerometer updates off...");
    }
}
@end

위에서, accelerometerData의 acceleration의 x,y 좌표 값을 가지고, Angle을 구하고, 그것을 바탕으로 PushBehavior의 Active를 YES로 입력합니다.

실제 Device에서 실행을 하면, 회전을 살짝하면, Label 들이 움직이게 됩니다.


[이미지 출처]
배경화면, 비행기, , 음식, 운동

참고 :
 - WWDC 2013 : Getting started with UIKit Dynamics
      : #206 세션으로, 다이나믹스에 대한 설명이 있음.
 - UIKit Dynamics and iOS 7: Building UIKit Pong
      : 다이나믹스를 이용해서, 바운싱을 예제로 설명하고 있는 곳.
 - RayWenderlich의 UIKit Dynamics 강좌

















2014년 4월 5일 토요일

[iOS7] UIKit Dynamics 사용법

UIKit Dynamics는 iOS7에서 추가된 것입니다.

iOS의 메시지에서 위로 올리다 보면, 마지막 부분에 튕기듯이 멈추는 부분과, Lock Screen에서 카메라 아이콘을 터치하거나, 위로 들었다가, 아래로 내리면 튕기 듯이 반응 하는 것이 UIKit의 Dynamics에서 지원하는 기능입니다.

 - UIKit Dynamics는 UIKit에 통합된 완전한 물리엔진 입니다. 즉, UIKit의 객체에 gravity, attachments, forces와 같은 동작(behavior)를 추가하여 현실처럼 느낄 수 있는 인터페이스를 만들 수 있게 지원하고 있습니다.
 - 물리특성을 정의하면, dynamics엔진이 알아서 처리를 해주고 있습니다.
 - iOS7 전에는 물리적인 시뮬레이션을 위해서, 다른 라이브러리를 사용해야 했었는데, iOS7에서는 간단한 물리적 시뮬레이션을 UIKit에서 해주는 것입니다.
 - 또한, CollectionView에서 아이콘들의 행동을 규정할 수 있습니다. 뭔가 추가되면, 다른 것들이 옆으로 흔들리게 만들 수 있습니다.

버튼들이 화면에 있다가 특정 순간이 되면, 아래로 떨어지도록 할 수도 있습니다.
이것을 가능하게 하는 것이 iOS7의 UIKit Dynamics입니다.


Dynamics는 UIKit내부에 내장되어 있으므로, 별도의 Framework를 import 할 필요가 없습니다.
위에 그림에서 처럼 Button 두개와, TextField를 움직이도록 해 보겠습니다.
먼저 Single View Application을 만들고, storyboad에서 위와 같이 객체를 등록하고, 각각 Property를 설정합니다.

UIDyanmicsAnimator를 현재 Main으로 등록된 View를 레퍼런스로 만듭니다.
Source Code

@interface DBUViewController ()

//아래로 떨어질 TextField
@property (strong, nonatomic) IBOutlet UITextField *textField; 
//아래로 떨어질 Button
@property (strong, nonatomic) IBOutlet UIButton *fallingButton; 
//스프링처럼 매달려 있을 Button
@property (strong, nonatomic) IBOutlet UIButton *danglingButton; 
//매달려 있을 버튼이 고정되어 있는 곳
@property (strong, nonatomic) IBOutlet UIView *redSquare; 
//버튼이 터치되면 결과를 알려줄 곳
@property (strong, nonatomic) IBOutlet UITextView *resultTextView; 

// 여기에 행동(Behavior)를 등록합니다.
@property (strong, nonatomic) UIDynamicAnimator *animator; 
//아래로 중력 행동을 만들것.
@property (strong, nonatomic) UIGravityBehavior* gravityBehavior; 

//원래 자리로 돌아가는 스냅 행동을 정의 돌아갈 위치가 달라지므로, 두개가 필요합니다.
@property (strong, nonatomic) UISnapBehavior * textFieldSnapBehavior; 
// 버튼 자리로 돌아갈 스냅 행동
@property (strong, nonatomic) UISnapBehavior *fallingButtonSnapBehavior;
@end

@implementation DBUViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    //
    self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
    
    UIAttachmentBehavior *springBehavior = [[UIAttachmentBehavior alloc] initWithItem:self.danglingButton offsetFromCenter:UIOffsetMake(/*20.0f*/0, /*self.danglingButton.frame.size.height/2*/0) attachedToAnchor:CGPointMake(self.redSquare.center.x, self.redSquare.center.y)];
    [springBehavior setFrequency:1.0f];
    [springBehavior setDamping:0.1f];
    
    [self.animator addBehavior:springBehavior];
}

시작버튼을 누르면, Gravity Behavior를 만들고, 등록을 시킨다.
source code
- (IBAction)startButton:(UIButton *)sender
{
    //gravity behavior가 없는 경우 새로 만들고 등록한다.
    if (!self.gravityBeahvior) {
        [sender setTitle:@"Stop" forState:UIControlStateNormal];
        [self.animator removeBehavior:self.textFieldSnapBehavior];
        [self.animator removeBehavior:self.fallingButtonSnapBehavior];
        
        //중력 행동을 만들때, 어느 Item을 행동에 넣을지 넣습니다.
        self.gravityBeahvior = [[UIGravityBehavior alloc] initWithItems:@[self.fallingButton, self.textField]];
        //만들고 난 다음에 추가할 수도 있습니다.
        [self.gravityBeahvior addItem:self.danglingButton];
        //Dynamic Animator에 행동을 추가하면, 실제 동작하게 됩니다.
        [self.animator addBehavior:self.gravityBeahvior];
    }else{
        //원래 위치로 돌아가기 위해서, 중력 행동을 빼고, 스냅 행동을 넣습니다.
        [sender setTitle:@"Start" forState:UIControlStateNormal];
        [self.animator removeBehavior:self.gravityBeahvior];
        self.gravityBeahvior = nil;
        [self.animator addBehavior:self.textFieldSnapBehavior];
        [self.animator addBehavior:self.fallingButtonSnapBehavior];
    }
}

스냅은 특정 Item이 어디로 이동할 지 넣는 것입니다.
이 스냅이 호출 될 때는 이미 아이템 2개가 아래로 떨어지고 난 다음입니다. 그래도, 원래의 위치는 그대로 저장되어 있는 상태로, Dynamics 엔진 내부에 위치만 달라진 것이므로, 자시느이 위치로 돌아오도록 center 값을 읽을 수 있습니다.
source code
- (UISnapBehavior *)textFieldSnapBehavior
{
    if (_textFieldSnapBehavior == nil) {
        _textFieldSnapBehavior = [[UISnapBehavior alloc] initWithItem:self.textField 
                                                          snapToPoint:self.textField.center];
    }
    return _textFieldSnapBehavior;
}
- (UISnapBehavior *)fallingButtonSnapBehavior
{
    if (_fallingButtonSnapBehavior == nil) {
        _fallingButtonSnapBehavior = [[UISnapBehavior alloc] initWithItem:self.fallingButton 
                                                   snapToPoint:self.fallingButton.center];
    }
    return _fallingButtonSnapBehavior;
}

감사합니다.

[소스코드: https://github.com/davidbae/iOS7-Dynamics-Test.git ]


참고 :
 - WWDC 2013 : Getting started with UIKit Dynamics
      : #206 세션으로, 다이나믹스에 대한 설명이 있음.
 - UIKit Dynamics and iOS 7: Building UIKit Pong
      : 다이나믹스를 이용해서, 바운싱을 예제로 설명하고 있는 곳.
 - Apple의 Dynamics 예제
 - RayWenderlich의 UIKit Dynamics 강좌
 -

2014년 3월 7일 금요일

[iOS7] Background Fetch in iOS7

오늘 정리할 내용은, Background Fetch에 대한 것입니다.
이 기능은 iOS7에서 추가 되었고, Background Fetch 는 앱이 백그라운드에 있을 때, 간헐 적으로 iOS에서 호출해서, 앱이 실행 될 수 있도록 해주는 것입니다.

그럼 얼마나 자주 호출이 될까요?

- iOS의 문서에는 "the system uses available information to determine the best time to launch or wake apps. For example, it does so when networking conditions are good or when the device is already awake. "라고 되어 있습니다.
- iPhone 5s에서 테스트를 해보니, 대략 8~10분 정도에서 호출이 되기도 하고,
- 8~10분 정도 지난 후에 파워 키로 화면을 켜고, 패스워드를 입력해서, Main화면이 표시되면, Background Fetch가 호출 되기도 합니다.
- 일정한 시간으로 주기적으로 호출되지 않습니다. 간헐적으로 시스템에서 지금이 최적이다라고 생각하는 시간, 즉 배터리를 사용하기 좋은 시간이라고 생각할 때 호출이 됩니다.

호출 된 후에 얼마나 계속 실행이 될까요?

실행 될 때,  completionHandler를 파라미터로 받습니다.
모든 패치를 끝내면, 이 함수를 호출 해서, Fetch가 끝났음을 시스템에 알려줘야 합니다.
약 30초 안에, 모든 패치를 끝내고, handler를 호출해줘야 합니다. 만약 호출하지 않거나, 시간이 많이 걸리다던지 하는 것에 의해서, 다음 Fetch가 언제 호출 될지 영향을 받는 것 같습니다.
 간단하게 끝날 것 같으면, NSURLRequest를 사용하고, 시간이 걸리면, NSURLSession을 사용하라고 권고하고 있습니다.

Background Fetch를 사용하려면?


1. App에 Background Mode에 설정을 추가합니다.
- Helloworld-Info.plist 에 "Required background modes"항목을 추가하고, Item으로 "App downloads content from the network"를 추가하면 된다.

- 다른 방법으로 Project의 Capability에서 Background Mode를 On하고 background fetch를 체크합니다.

2. 앱이 시작될 때, 시스템에 background fetch시간을 설정합니다.
- 일정 간격을 설정할 수는 없고, Minimum과 Never를 선택할 수 있습니다.
source code
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    
    //Background Fetch 초기화 함. 가장 짧은 주기로 설정함.
    [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

    return YES;
}

3. Fetch가 실행되는 시간을 알아보기 위해서, Background로 들어갈 때와, Fetch가 실행될 때, Local Notification을 보내도록 합니다.
source code
- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    UILocalNotification *noti = [[UILocalNotification alloc] init];
    if(noti)
    {
        noti.repeatInterval = 0.0f;
        noti.alertBody = [NSString stringWithFormat:@"Backgournd:%@", [NSDate date]];
        NSLog(@"%@", noti.alertBody);
        [[UIApplication sharedApplication] presentLocalNotificationNow:noti];
    }
    [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
}

4. Fetch가 호출 되면, 웹사이트에 접속합니다.
source code
#pragma mark - Background Fetch
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    NSLog(@"Received Background Fetch");
    //데이터를 받아오거나 호출할 URL
    NSURL *url = [NSURL URLWithString:@"http://hidavidbae.blogspot.kr"];
    
    //URL객체가 잘 만들어졌을 경우
    if (url != nil) {
        // URL로 호출할 때 사용할 request객체임.
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        //데이터 로드 실패 시 주소를 받아올 에러 포인터
        NSError *error = nil;
        //해당 URL의 데이터를 가져옴
        NSData *data = [NSURLConnection sendSynchronousRequest:request
                                             returningResponse:nil error:&error];
        //데이터가 로딩이 되었을 경우
        if (data != nil) {
            //사용자에게 notification을 보냄.
            UILocalNotification *noti = [[UILocalNotification alloc] init];
            if(noti)
            {
                noti.repeatInterval = 0.0f;
                noti.alertBody = [NSString stringWithFormat:@"Fetch Success:(%d):%@",
                                  (int)data.length, [NSDate date]];
                [[UIApplication sharedApplication] presentLocalNotificationNow:noti];
            }            
            //성공했음을 알림
            completionHandler(UIBackgroundFetchResultNewData);
        }else{
            //에러를 로그로 찍음.
            NSLog(@"%@", error);
            //실패했음을 알림.
            completionHandler(UIBackgroundFetchResultFailed);
        }
    }else{
        //실패했음을 알림.
        completionHandler(UIBackgroundFetchResultFailed);
    }
}


실제 동작은 어떻게 될까요?


- Simulator에서 동작하는지 먼저 테스트를 할 수 있습니다.
 시뮬레이터에 앱을 설치해서 동작시키고, Home키로 앱을 Background로 보냅니다.
 이후, Xcode의 debug > Simulate background fetch 를 선택해서 동작을 바로바로 테스트해 볼 수 있습니다.

- 실제 iPhone 5s 디바이스에서는 아래와 같이, 8~10분 쯤에, 아이폰을 켜고, 비밀번호 력후 Main화면에 들어가면, Fetch가 실행이 되었습니다.
 이렇게 몇번 화면이 동작하고 Fetch된 이후 약 8분 정도에 자동으로 Fetch도 된 경우가 있었습니다.
 그리고는 2~3시간 후, 한번, 그리고 1시간 후 한번 정도 실행이 되었습니다.
 특별하게 화면이 꺼져 있는데, 동작한 적은 없는것 같고, 내가 화면을 켜고, Home화면에 들어가면, 위치 정보를 읽고 있음이 화면에 표시되고, Fetch가 실행되는 것으로 보입니다.


마치며..


- Fetch는 언제 일어날까요? 
  - 사용자가 iPhone을 어떤 형태로 사용하는 가에 따라서 달라지는 것 같습니다.
  - 밤에는 Fetch를 하지 않고, 낮에 사용자가 자주 iPhone을 켜면, Fetch도 일어나는 것 같습니다.

- Fetch는 사용자의 편의를 위해서 만들어졌습니다.
   패치가 필요한 이유는 사용자가 iPhone을 켜고, 앱을 실행하기 전에, 데이터가 딱!!! 들어와 있기를 위해서 만들어진 것입니다.
  즉, 필요 없는 시간, 혹은 사용자가 많이 안쓰는 앱에 대해서 불 필요하게 자주 Fetch를 하지 않습니다.
 그리고, 패치하는데 시간이 오래 걸리거나, 응답이 잘 없는 앱에 대해서는 Fetch 스케쥴을 뒤로 미뤄버리는 것 같습니다.

- 어떻게 사용하면 좋을까요?
  실시간은 아니지만, 사용자가 앱을 실행하기 전에, 데이터가 미리 다운로드 받으면, 사용자가 편리하게 이용할 수 있도록 장치를 마련해 놓는 용도로 사용하면 되겠습니다.

[소스코드]

https://github.com/davidbae/iOS7-background-fetch

[추가 분석 자료]

3월 9일 새벽부터, 오후 9시쯤 까지 Fetch가 실행 된 시간을 분석해 보았습니다.
제 iPhone 5s에서는 오전 3시~ 오후1시까지 9시간 동안은 Fetch가 진행이 되지 않았고, 그외의 시간에는 약 8~20분 사이를 오락가락 하며 Background Fetch가 실행이 된 것을 볼 수 있습니다.
 아마도 제가 오후에 iPhone을 많이 사용해서, 점심먹고부터 Fetch가 주기적으로 실행이 되는 것 같습니다.

 2014-03-09 01:42:47 (Background Fetch Start)
 2014-03-09 01:43:12 (00:00:25)
 2014-03-09 01:43:42 (00:00:29)
 2014-03-09 01:44:11 (00:00:29)
 2014-03-09 01:54:26 (00:10:14)
 2014-03-09 02:16:57 (00:22:31)
 2014-03-09 02:39:17 (00:22:20)
 2014-03-09 02:49:21 (00:10:04)
 2014-03-09 03:01:36 (00:12:14)
 2014-03-09 03:31:21 (00:29:44)
 2014-03-09 13:09:56 (09:38:34
      ==>오전에는 한번도 실행이 안되었습니다.
      ==>제가 오전에 사용을 잘 안해서 그런지, 아니면, 중간에 내가 앱을 종료했는지 잘 모르겠네요.
      ==> 며칠 더 자료를 모아보면 알 수 있을 것 같습니다.
 2014-03-09 13:19:40 (00:09:44)
 2014-03-09 13:30:16 (00:10:35)
 2014-03-09 13:40:01 (00:09:45)
 2014-03-09 13:49:46 (00:09:45)
 2014-03-09 13:58:52 (00:09:05)
 2014-03-09 14:07:52 (00:09:00)
 2014-03-09 14:21:36 (00:13:43)
 2014-03-09 14:29:17 (00:07:41)
 2014-03-09 14:30:02 (Background Fetch Start)
 2014-03-09 14:30:22 (Background Fetch Start)
 2014-03-09 14:54:38 (00:24:16)
 2014-03-09 15:02:17 (00:07:38)
 2014-03-09 15:12:01 (00:09:44)
 2014-03-09 15:26:43 (00:14:42)
 2014-03-09 15:41:27 (00:14:43)
 2014-03-09 15:50:51 (00:09:24)
 2014-03-09 16:02:04 (00:11:12)
 2014-03-09 16:15:32 (00:13:28)
 2014-03-09 16:23:53 (00:08:21)
 2014-03-09 16:36:04 (00:12:11)
 2014-03-09 16:45:46 (00:09:41)
 2014-03-09 16:57:40 (00:11:54)
 2014-03-09 17:05:34 (00:07:54)
 2014-03-09 17:18:53 (00:13:19)
 2014-03-09 17:31:16 (00:12:22)
 2014-03-09 17:41:01 (00:09:44)
 2014-03-09 17:50:46 (00:09:45)
 2014-03-09 18:04:46 (00:13:59)
 2014-03-09 18:15:11 (00:10:24)
 2014-03-09 18:23:30 (00:08:19)
 2014-03-09 18:34:16 (00:10:45)
 2014-03-09 18:45:21 (00:11:04)
 2014-03-09 18:53:31 (00:08:10)
 2014-03-09 19:03:46 (00:10:15)
 2014-03-09 19:13:31 (00:09:44)
 2014-03-09 19:21:03 (00:07:32)
 2014-03-09 19:30:26 (00:09:22)
 2014-03-09 19:45:31 (00:15:05)
 2014-03-09 19:55:39 (00:10:07)
 2014-03-09 20:03:54 (00:08:14)
 2014-03-09 20:13:23 (00:09:29)
 2014-03-09 20:23:06 (00:09:42)
 2014-03-09 20:30:56 (00:07:49)
 2014-03-09 20:50:39 (00:19:42)
 2014-03-09 21:08:18 (00:17:38)
 2014-03-09 21:28:46 (00:13:37)
 2014-03-09 21:38:31 (00:09:45)
 2014-03-09 21:48:16 (00:09:44)
 2014-03-09 22:15:36 (00:27:20)

 2014-03-09 23:10:19 (00:54:43)

아래는 3월 10일 자료 입니다.
 2014-03-10 00:30:58 (01:20:38)
 2014-03-10 04:45:58 (04:15:00
 2014-03-10 12:33:41 (07:47:43) //오후 1시이후, 약10분단위로 실행
  ==> 이것으로 미뤄봐서, iOS7에서는 사용자의 사용 형태에 따라서, Fetch를 적당한 시간에 실행하는 것으로 보입니다.

 2014-03-10 12:45:00 (00:11:18)
 2014-03-10 12:53:26 (00:08:26)
 2014-03-10 13:05:01 (00:11:35)
 2014-03-10 13:14:45 (00:09:44)
 2014-03-10 13:31:21 (00:16:35)
 2014-03-10 13:40:11 (00:08:50)
 2014-03-10 13:55:26 (00:15:14)
 2014-03-10 14:05:11 (00:09:45)
 2014-03-10 14:14:56 (00:09:44)
 2014-03-10 14:27:24 (00:12:28)
 2014-03-10 14:35:57 (00:08:32)
 2014-03-10 14:46:11 (00:10:14)
 2014-03-10 14:55:56 (00:09:45)
 2014-03-10 15:05:27 (00:09:30)
 2014-03-10 15:14:24 (00:08:57)
 2014-03-10 15:22:26 (00:08:01)
 2014-03-10 15:32:12 (00:09:46)
 2014-03-10 15:41:46 (00:09:34)
 2014-03-10 15:51:31 (00:09:44)
 2014-03-10 16:01:16 (00:09:45)
 2014-03-10 16:11:01 (00:09:44)
 2014-03-10 16:22:00 (00:10:59)
 2014-03-10 16:33:16 (00:11:15)
 2014-03-10 16:48:01 (00:14:45)
 2014-03-10 16:57:46 (00:09:44)
 2014-03-10 17:07:31 (00:09:45)
 2014-03-10 17:16:40 (00:09:09)
 2014-03-10 17:26:26 (00:09:45)
 2014-03-10 17:36:11 (00:09:44)
 2014-03-10 17:44:43 (00:08:31)
 2014-03-10 17:54:56 (00:10:13)
 2014-03-10 18:02:50 (00:07:53)
 2014-03-10 18:12:31 (00:09:41)
 2014-03-10 18:22:16 (00:09:44)
 2014-03-10 18:31:23 (00:09:07)
 2014-03-10 18:40:01 (00:08:38)
 2014-03-10 18:51:46 (00:11:44)
 2014-03-10 19:01:30 (00:09:44)
 2014-03-10 19:12:47 (00:11:17)
 2014-03-10 19:20:18 (00:07:30)
 2014-03-10 19:30:20 (00:10:01)
 2014-03-10 19:39:49 (00:09:28)
 2014-03-10 19:49:01 (00:09:12)
 2014-03-10 19:56:45 (00:07:44)
 2014-03-10 20:13:16 (00:16:30)
 2014-03-10 20:24:36 (00:11:20)
 2014-03-10 20:33:41 (00:09:05)
 2014-03-10 20:41:34 (00:07:53)
 2014-03-10 20:49:46 (00:08:12)
 2014-03-10 21:02:32 (00:12:46)
 2014-03-10 21:12:22 (00:09:49)
 2014-03-10 21:20:39 (00:08:17)
 2014-03-10 21:30:46 (00:10:06)
 2014-03-10 21:38:47 (00:08:00)
 2014-03-10 21:48:56 (00:10:08)
 2014-03-10 22:00:01 (00:11:05)
 2014-03-10 22:08:20 (00:08:19)
 2014-03-10 22:15:57 (00:07:36)
 2014-03-10 22:28:51 (00:12:54)

 2014-03-10 22:43:56 (00:15:05)

[참고자료]

- iOS7 : What's new in iOS7
- [iOS] :iOS Programming Guide - App State and Multitasking
- [Double Encore]: http://www.doubleencore.com/2013/09/ios-7-background-fetch/
- [Hayageek] : http://hayageek.com/ios-background-fetch/
- [Tuts+] : http://code.tutsplus.com/tutorials/ios-7-sdk-working-with-background-fetch--mobile-20520