iOS2012. 9. 4. 11:02

How to Customize UITabBar on iOS 5

Building the new version of the app Blocos de Rua I was challenged to customize the UITabBar so it meets what the designer wants. In iOS 5 this is pretty easy to do but I haven’t figured out the proper way to do it from the beginning, this post is my findings on how to do it properly by correctly using the new iOS 5 APIs to customize appearance.

The final look:

Keep reading to see the code.

The appearance APIs in iOS 5 are great. They reduce lots of custom drawRect: that used to be necessary to customize the default UIKit components. The first time I tried to customized the tab bar I had some problems with images been offset upwards because I was using the wrong methods.First thing I learned, the setFinishedSelectedImage:finishedUnselectedImage: from UITabBarItem is the tab’s icon not a image for the whole tab with background, icon and label.

Customize the UITabBar is a peace of cake when you understand how the APIs should be used, take a look:

From inside out, the UITabBar

First - usually in your app delegate - set the image for the entire tab bar’s background, which represents the “normal” state of all tabs. The dimension is 320 x 49 points.

1
[[[self tabBarController] tabBar] setBackgroundImage:[UIImage imageNamed:@"background"]];

Then configure the selected state of a tab. This is necessary because in this app I don’t want the default white highlight that represents the selected tab. Pay attention to the image’s width, it must be the same of a single tab. In my case 320/4, 80 points wide.

1
[[[self tabBarController] tabBar] setSelectionIndicatorImage:[UIImage imageNamed:@"selected"]];

Last but not least, the UITabBarItem

Unlike the default behavior the image shouldn’t change when the tab is selected, this is why I set the same image in both states. For each UIViewController that will be part of the tab bar you need to configure the tab image like this:

1
2
3
4
5
6
7
- (id)init {
    self = [super initWithNibName:@"MyNibName" bundle:nil];
    if (self) {
        self.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"The Title" image:nil tag:0];
        [[self tabBarItem] setFinishedSelectedImage:[UIImage imageNamed:@"tab_icon"] withFinishedUnselectedImage:[UIImage imageNamed:@"tab_icon"]];
    }
}

The last detail is the title’s color on the unselected tab, they can’t be the default gray color. To change the color we need a dictionary of attributes whit the UITextAttributeTextColor key:

1
2
3
4
// below the setFinishedSelectedImage:withFinishedUnselectedImage:
[[self tabBarItem] setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
        [UIColor whiteColor], UITextAttributeTextColor,
        nil] forState:UIControlStateNormal];

That’s all folks.

Posted by 다오나무
iOS2012. 9. 3. 17:21

UIImageView를 사용할 경우 UIImageView의 프레임 사이즈에 맞춰 이미지 비율에 따라 이미지 사이즈를 쉽게 재조정할 수 있지만

뷰상에서 특정 UIImage를 drawRect하는 경우에는 비율에 맞춰서 이미지 사이즈나 스케일을 재조정하는게 쉽지 않다.

따라서 다음과 같은 UIImage의 카테고리를 사용해 이미지 비율에 따라 UIImage스케일을 재조정한다.



카테고리의 헤더 파일 

#import <Foundation/Foundation.h>


@interface UIImage (UIImageSizeExtention)


- (UIImage *)fixImageSize;

- (UIImage *)fitToSize:(CGSize)newSize;

- (UIImage *)scaleToSize:(CGSize)newSize;

- (UIImage *)scaleProportionlyToWidth:(CGFloat)width;

- (UIImage *)scaleProportionlyToHeight:(CGFloat)height;

- (UIImage *)cropToRect:(CGRect)newRect;


@end   


카테고리의 실행 파일

#import "UIImage+SizeExtention.h"


@implementation UIImage (UIImageSizeExtention)
 

#pragma mark -

#pragma mark Basic


- (UIImage*)fixImageSize 

{

    // Fix some strange bug

    UIGraphicsBeginImageContext(self.size);

    [self drawInRect:CGRectMake(00self.size.widthself.size.height)];

    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return newImage;

}


#pragma mark -

#pragma mark Fit (Combination)


- (UIImage *)fitToSize:(CGSize)newSize {

    float originalProportion = self.size.width/self.size.height;

    float targetProportion = newSize.width/newSize.height;

    float scaleProportion = newSize.width/self.size.width;

    UIImage *targetImage;

    

    if (targetProportion == originalProportion) {

        // Same Proportion

        // Do not have to crop, Direct scale

        targetImage = [self scaleToSize:newSize];

    } else if (targetProportion) {

        // Relative Landscape

        // Crop Rect

        CGFloat originX = self.size.width*scaleProportion/2 - newSize.width/2;

        CGRect cropRect = CGRectMake(originX, 0, newSize.width, newSize.height);

        // Scale to Height, Crop

        targetImage = [[self scaleProportionlyToHeight:newSize.heightcropToRect:cropRect];

    } else {

        // Relative Portrait

        // Scale to Width

        CGFloat originY = self.size.height*scaleProportion/2 - newSize.height/2;

        CGRect cropRect = CGRectMake(0, originY, newSize.width, newSize.height);

        targetImage = [[self scaleProportionlyToWidth:newSize.widthcropToRect:cropRect];

    }

   

   return targetImage;

}

               

#pragma mark -

#pragma mark Scale

               

- (UIImage *)scaleToSize:(CGSize)newSize {

    UIImage *targetImage = [self fixImageSize];

    // Prepare new size context

    UIGraphicsBeginImageContext(newSize);

    // Get current image

    CGContextRef context = UIGraphicsGetCurrentContext();

   

    // Change the coordinate from CoreGraphics (Quartz2D) to UIView

    CGContextTranslateCTM(context, 0.0, newSize.height);

    CGContextScaleCTM(context, 1.0, -1.0);

    // Draw (Scale)

    // The size of this drawRect is for scale

    CGRect drawRect = CGRectMake(00, newSize.width, newSize.height);

    CGContextDrawImage(context, drawRect, targetImage.CGImage);

   

    // Get result and clean

    UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

   

    return scaledImage;

}


- (UIImage *)scaleProportionlyToWidth:(CGFloat)width {

    float originalProportion = self.size.width/self.size.height;

    CGFloat height = width/originalProportion;

    return [self scaleToSize:CGSizeMake(width, height)];

}


- (UIImage *)scaleProportionlyToHeight:(CGFloat)height {

    float originalProportion = self.size.width/self.size.height;

    CGFloat width = height*originalProportion;

    return [self scaleToSize:CGSizeMake(width, height)];

}

               

#pragma mark -

#pragma mark Crop

               

- (UIImage *)cropToRect:(CGRect)newRect {

    UIImage *targetImage = [self fixImageSize];

    // Prepare new rect context

    UIGraphicsBeginImageContext(newRect.size);

    // Get current image

    CGContextRef context = UIGraphicsGetCurrentContext();

   

    // Change the coordinate from CoreGraphics (Quartz2D) to UIView

    CGContextTranslateCTM(context, 0.0, newRect.size.height);

    CGContextScaleCTM(context, 1.0, -1.0);

    // Draw (Crop)

    // This drawRect is for crop

    CGRect clippedRect = CGRectMake(00, newRect.size.width, newRect.size.height);

    CGContextClipToRect(context, clippedRect);

    CGRect drawRect = CGRectMake(newRect.origin.x*(-1), newRect.origin.y*(-1), targetImage.size.width, targetImage.size.height);

    CGContextDrawImage(context, drawRect, targetImage.CGImage);

   

    UIImage *croppedImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return croppedImage;

}    

@end



위의 카테고리를 다음과 같이 사용하였다.

UIImage *image = [UIImage imageNamed:/*Image name*/];


if
 (image.size.width > image.size.height

{

// If the width of a image is longer than the height
// rescale to width

image = [image scaleProportionlyToWidth:/*Image width*/];

}

else

{        

// If the height of a image is longer than the width
// recale to height

image = [image scaleProportionlyToHeight: /*Image height*/];

}


CGRect drawRect = CGRectMake(0, 0, image.size.width, image.size.height);


[image drawInRect:drawRect];


Posted by 다오나무
iOS2012. 9. 3. 14:52

iOS5 업데이트 후 이전에 만들어 놓았던 어플중 Navigation Bar가 초기화 되어서 보여지는 현상을 발견했다.

기존에 대부분의 개발자분들이 사용하던 방법은 UINavigationBar 라는 Class의 Category 형태로 만들어
UIView의 drawRect: 를 override해서 적용하던 방식이였던걸로 알고 있다.

그러나 iOS5로 업데이트 후 위에 명시한 UINavigationBar Category에서 명시한 drawRect 메서드가 호출되지 않는다.
그래서 애플 공식 문서에서 UINavigationBar에 대해서 찾아보았더니 아래와 같은 내용이 보인다.

Prior to iOS v5.0, when used in conjunction with a navigation controller, there are only a handful of direct customizations you can make to the navigation bar. Specifically, it is alright to modify the barStyletintColor, and translucent properties, but you must never directly change UIView-level properties such as the frameboundsalpha, or hidden properties directly. In addition, you should let the navigation controller manage the stack of navigation items and not attempt to modify these items yourself.

In iOS v5.0 and later, you can customize the appearance of the bar using the methods listed in “Customizing the Bar Appearance.” You can customize the appearance of all navigation bars using the appearance proxy ([UINavigationBar appearance]


잘은 모르겠지만.. UINavigation Controller Class에서 UIView-level properties를 직접 변경하지 못한다(??) 라고 적혀 있는 것 같다. 그렇다면 당연히 drawRect로 호출하지 못하는게 당연하다는 결론이 나온다.

해결방법은..
따라서 애플 공식 문서에서 말한 방법으로 “Customizing the Bar Appearance.” 해결 할 수 있다.

(1) iOS5 이하 버젼에서는 기존과 같은 방식으로 Category를 사용하면 되고
(2)  iOS5 이상 버젼에서는 아래와 같은 방식으로 처리한다. (iOS5 이하에서는 실행되지 않는다.)

if([self.navigationController.navigationBar respondsToSelector:@selector(setBackgroundImage:forBarMetrics:)] ) {

        NSLog(@"iphone 5 이상");

        //iOS 5 new UINavigationBar custom background

        UIImage *image = [UIImage imageNamed:@"이미지명"];

        [self.navigationController.navigationBar setBackgroundImage:image forBarMetrics:UIBarMetricsDefault];

    }  



* setBackgroundImage:forBarMetrics:

Sets the background image for given bar metrics.

- (void)setBackgroundImage:(UIImage *)backgroundImage forBarMetrics:(UIBarMetrics)barMetrics

Parameters

backgroundImage

The background image to use for barMetrics.

barMetrics

A bar metrics constant.

Availability

  • Available in iOS 5.0 and later.

Declared In

UINavigationBar.h

Posted by 다오나무
iOS2012. 9. 3. 13:28

For Gigster’s navigation concept it was necessary to implement a custom styled tab bar. Basically the screen is divided into three parts:

  • Navigation Bar
  • Tab Bar
  • Content

The navigation bar has a custom background image and a fixed title with custom font. The tab bar should be directly under the navigation bar and has complete custom styling.

Basic Setup

To start off I created a new Xcode project and added a UITabBarController with three static views. Although I have XIBs for each content view, I added everything else in code. Each view has a UINavigationController so it can easily contain its own navigation stack.

Navigation Bar

An easy technique to get a custom background in a UINavigationBar is to make a cateogory. Add this code (e.g. in YourAppDelegate.m):

@interface UINavigationBar (MyCustomNavBar)
@end
@implementation UINavigationBar (MyCustomNavBar)
- (void) drawRect:(CGRect)rect 
{
    UIImage *barImage = [UIImage imageNamed:@"nav_bg.png"];
    [barImage drawInRect:rect];    
}
@end

To get the fixed title with custom font I added a method that creates a UILabel:

- (UILabel *)_makeTitleLabel
{
    CGRect frame = CGRectMake(0032044);
    UILabel *label = [[[UILabel alloc] initWithFrame:frame] autorelease];
    label.backgroundColor = [UIColor clearColor];
    label.font = [UIFont fontWithName:@"Marker Felt" size:24];
    label.shadowColor = [UIColor colorWithWhite:0.0 alpha:0.5];
    label.textAlignment = UITextAlignmentCenter;
    label.textColor = [UIColor whiteColor];
    label.text = @"Custom Tab Bar";
    
    return label;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    ...
    // add static title labels
    [tab1NavigationController.navigationBar addSubview:[self _makeTitleLabel]];
    [tab2NavigationController.navigationBar addSubview:[self _makeTitleLabel]];
    [tab3NavigationController.navigationBar addSubview:[self _makeTitleLabel]];
    ...
}

Tab Bar

A little bit of frame hackery was necessary to get the tab bar right beneath the navigation bar:

    // disable autosizing of tabbar and move it to correct position
    tabBarController.tabBar.autoresizingMask = 0;
    tabBarController.tabBar.frame = CGRectMake(04432065);
    
    
    // fix frame of tabbarcontroller's view
    CGRect frame = tabBarController.view.frame;
    frame.size.height += 29;
    frame.origin.y = 20;
    tabBarController.view.frame = frame;

To have a custom layout for the tab bar, it is necessary to subclass UITabBarController. We don’t need UIKit’s tab bar at all, so we just hide it and create our own custom tabs:

- (void)viewDidLoad
{
    // find the normal tab bar and hide it
    for(UIView *view in self.view.subviews)
    {
        if([view isKindOfClass:[UITabBar class]])
        {
            view.hidden = YES;
            break;
        }
    }
    
    [self _setupCustomTabBar];
}

First I added a UIImageView containing a background image:

- (void)_setupCustomTabBar
{
    // background image
    self.bgImageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"tabs_bg.png"]] autorelease];
    bgImageView.frame = CGRectMake(04432065);
    [self.view addSubview:bgImageView];

Next we want to display a tab image to indicate which tab is active:

    self.contentView = [[[UIView alloc] initWithFrame:CGRectMake(04464065)] autorelease];
    [self.view addSubview:self.contentView];
    
    // sliding tab image
    self.tabImage = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"tab.png"]] autorelease];
    CGRect frame = self.tabImage.frame;
    frame.origin = CGPointMake(BUTTON_X_OFFSET, BUTTON_Y);
    self.tabImage.frame = frame;
    [self.contentView addSubview:self.tabImage];

Finally I added three buttons to trigger tab selection:

    // Make custom tab buttons and add them to the content view
    [self.contentView addSubview:[self _makeTabButtonWithTitle:@"Tab 1" atIndex:0]];
    [self.contentView addSubview:[self _makeTabButtonWithTitle:@"Tab 2" atIndex:1]];
    [self.contentView addSubview:[self _makeTabButtonWithTitle:@"Tab 3" atIndex:2]];
}

Button creation method:

- (UIButton *)_makeTabButtonWithTitle:(NSString *)title
                              atIndex:(NSInteger)index
{
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(BUTTON_X_OFFSET + index*BUTTON_WIDTH, BUTTON_Y, BUTTON_WIDTH, BUTTON_HEIGHT);
    button.tag = index;
    button.titleLabel.font = [UIFont fontWithName:@"Marker Felt" size:18];
    button.titleLabel.shadowColor = [UIColor colorWithWhite:0 alpha:0.5];
    button.titleLabel.shadowOffset = CGSizeMake(0-1);
    [button setTitle:title forState:UIControlStateNormal];
    [button addTarget:self action:@selector(_buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
    
    return button;
}

Note that the buttons’ action is _buttonClicked. There we need to set the currently selected tab index in the UITabBarController.

- (void)_buttonClicked:(id)sender
{
    self.selectedIndex = [sender tag];
    [self _updateTabImage];
}

We also want to update the tab image’s position after a tab was selected. This is what _updateTabImage is for:

- (void)_updateTabImage
{
    CGRect targetRect = CGRectMake(BUTTON_X_OFFSET + self.selectedIndex*BUTTON_WIDTH, BUTTON_Y, BUTTON_WIDTH, BUTTON_HEIGHT);
    
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.3];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    
    [self.tabImage setFrame:targetRect];
    
    [UIView commitAnimations];
}

Now we have a fully functioning tab bar with custom layout. You can download the full Xcode project here:MyCustomTabBar_final.zip.

Gigster in the App Store: Jump

Posted by 다오나무
iOS2012. 8. 29. 17:04

맥이나 iOS에서 소켓 프로그래밍을 구현하기 위해 직접 BSDSocket 혹은 CFSocket을 사용해서 만드는 방법이 있습니다. 그러나 저처럼 처음부터 쌓기 싫어하는 사람을 위해서 잘 포장된 라이브러리가 있는데 그것이 바로 CocoaAsyncSocket입니다.

https://github.com/robbiehanson/CocoaAsyncSocket

예전에는 구글 코드에 있었는데 현재는 github에서 진행되고 있습니다. 오픈소스이고, 간혹 버그가 생기는 경우도 있습니다만 (직접 경험) 개발자에게 알려주니 며칠 뒤에 고쳐져 있더군요.


이 라이브러리의 사용법은 생각외로 간단합니다.


다운 받는 방법


다른 프로그램을 사용할 필요 없이 해당 버튼만 누르면 zip 형태로 받을 수 있습니다. 


압축을 풀면 폴더와 파일이 나옵니다. 다른 폴더는 신경쓰지 않고, 소켓 라이브러리만 본다면 두 폴더로 나눌 수 있습니다
RunLoop : AsyncSocket(.h/.m) AsyncUdpSocket(.h/.m)
GCD : GCDAsyncSocket(.h/.m) GCDAsyncUdpSocket(.h/.m)

tcp와 udp는 보면 구별가실테고, GCD는 모르겠으면 검색해 봅시다.
런루프 폴더의 라이브러리는 업데이트가 중단된지 꽤 되었습니다. 안정성도 보장할 수 없습니다. 코드를 보고 어떤 구조인지 파악하기에는 문제 없을지도 모르나, 실 사용에서는 권장하지 않습니다.
GCD 폴더에는 GCD를 사용해 thread-safe하게 설계되었습니다. 실제 사용할 목적이라면 이 쪽을 사용하도록 합시다.


기본적인 사용 방법은 아주 간단합니다. 그냥 프로젝트에 해당 파일을 넣으면 됩니다. 가령 UDP 소켓 통신을 원한다면 GCDAsyncUdoSocket라는 이름의 두 파일만 추가해서 사용하면 됩니다.

예제는 폴더 안에 들어있는 예제들을 참고하시면 됩니다.


라고 하고 끝나면 부실하니 간단하게 사용하는 설명을 해보죠. 저는 UDP를 구현한지라 UDP를 중심으로 설명하겠습니다. TCP와 큰 차이는 없을테니 다른 포함된 예제를 보시고 비교하면서 공부해보도록 합시다.


사용하기 전에 우선 헤더 파일에 해당되는 GCDAsyncUdpSocket.h를 해당 헤더 파일에 포함시킵니다.
그리고 소켓을 선언해 주셔야 되는데 대략 이런 방식으로 하시면 됩니다.

@interface UDPSocket : UIViewController {

    long tag;
    GCDAsyncUdpSocket *udpSocket;

}

long 타입은 특별한 경우가 아니면 하나로도 충분하고, 소켓의 경우에는 필요에 따라서 여러개를 사용할 수도 있습니다. 물론 여러개를 사용하려고 한다면 선언도 그에 맞춰서 해줘야겠죠.



소스 코드에서의 처리는 다음과 같습니다.

- (void) viewDidLoad() 
{
    //.......
    udpSocket = [[GCDAsyncUdpSocket allocinitWithDelegate:self delegateQueue:dispatch_get_global_queue(00)];
 
   
if (![udpSocket bindToPort:0 error:&error]) {
      NSLog(@"Error binding: %@", error);
       
return;
    
}

   if
 (![udpSocket beginReceiving:&error]
   {
      NSLog(@"Error receiving: %@", error);
       
return;
   }
}

viewDidLoad에서 구현하는 이유는 미리 선언을 해주어 이후에 사용하기 편하게 하기 위해서입니다.
딱히 따로 설명할 것은 없고, 코드를 보면 무슨 기능을 하는지 대충 감이 오리라 생각됩니다.


위 작업을 행한 다음에 실제로 데이터를 주고 받을때는 이렇게 합니다.
[udpSocket sendData:data toHost:host port:port withTimeout:0.5 tag:tag];

 data는 NSData형식입니다. 다른 데이터 전송 방식이 있는지는 잘 모르겠네요.
 host는 보낼 대상의 ip, port는 포트 번호입니다. 
 withTimeout은 타임아웃 시간 설정입니다. 0일때는 타임아웃을 하지 않고, 이외의 양수의 경우에는 해당 시간(초)만큼 기다려도 응답이 오지 않으면 그만두는거죠.

 
보내는 것은 위와 같이 하고, 받는 것은 아래와 같이 합니다.
TCP와 UCP가 다른 것은 이 부분입니다만, 약간의 함수명 차이만 있고 사용법은 비슷합니다.

- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag
{
}

- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError *)error 
{
}

- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data 
    fromAddress:(NSData *)address withFilterContext:(id)filterContext
{
}

UDP에서는 위의 3가지를 포함시켜야 합니다. 실제 구현내용이 없어도 되니 꼭 넣어주도록 합시다.

UDP의 특성상 보냈는지 확인하는 2개의 함수는 별 내용이 없어도 되더군요. TCP라면 신경써서 체크해줘야겠지만 말입니다.
세번째가 데이터를 받았을 때 처리하는 함수입니다.


 라이브러리가 쉬워서 별로 설명할건 없네요. 기본적인건 위 수준에서 원하는걸 조금 고치는 정도로도 충분합니다. 

Posted by 다오나무
iOS2012. 8. 28. 09:47

iOS5 이전에는 JSON API가 private API가 되어서 사용할 수 없었기 때문에, yajl, json-framework, jsonTouch 등 써드파티 json 라이브러리들을 사용해야했다. 써드파티 라이브러리이기 때문에 외부 라이브러들을 불러와아야했고, ilnk를 걸어줘야하는 등 개발에 필요한 것들이 많았었다. 하지만 iOS5에서 부터는 NSJSONSerialization API를 사용할수 있게 되었다. 

NSJSONSerialization은 다른 써더파티 라이브러리와 마찬가지로 NSDicationary와 NSArray로 형태의 객체로 바로 매핑할 수 있다. 또한 모든 객체는 NSString, NSNumber, NSArray, NSDictionary 또는 NSNull로 매핑된다. 그리고 모든 객체의 키이름은 NSString으로 사용된다.간단히 

NSJSONSerialization을 사용하는 방법에 대한 예제를 준비했다. 
{
   "name" : "saltfactory",
   "e-mail" : "saltfactory@gmail.com"
}
이라는 간단한 JSON을 파싱하는 테스트를 위해서 UnitTest로 준비했다.

JSON 문자열을 NSDictionary 객체로 매핑하기 위해서는 다음과 같은 단계가 필요하다. NSJSONSerialization에서 에러를 담을 NSError가 필요하기 때문에 NSError 변수를 하나를 만든다. 그리고 JSON문자열을 NSJSONSerialization에서 사용할 수 있는 NSData로 변형시켜야한다. 이렇게 준비된 data와 error를 JSONObjectWithData:options:error: 메소드를 이용해서 NSDictionary로 바로 매핑할 수 있다. 예제는 JSON문자열을 NSJSONSerialization을 이용해서 NSDictionary로 파싱해서 name이라는 키의 값과 @"saltfactory"라는 문자열이 같은지 단위테스트를 한 것이다. 결과는 올바르게 해석되어 매핑되었기 때문에 단위테스트가 성공적으로 끝나게 된다.

 - (void)testParsingJSON

{

    NSString *jsonString = @"{\"name\":\"saltfactory\",\"e-mail\":\"saltfactory@gmail.com\"}";

    NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];

    NSError *error;

    NSDictionary *jsonInfo = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:&error];

    

    NSAssert([[jsonInfo valueForKey:@"name"] isEqualToString:@"saltfactory"], @"not equals");

}


다음은 NSDictionary의 객체로 JSON 문자열을 생성하는 방법이다. 이 방법은 JSON의 문자열을 NSDictionary로 매핑하는 방법과 반대로 실행하면 된다. 우선 키이름과 NSString, NSNumber, NSArray, NSDictionary, NSNull 중에 하나의 객체를 값으로 가지는 NSDictionary를 만든다. 그리고 그 NSDictionary 객체를 NSData로 생성하는데 이때 NSJSONSerialization의 dataWithJSONObject:options:error: 메소드를 사용해서 객체를 JSON data로 생성한다. 이때 옵션으로 NSJSONWritingPrettyPrinted라는 옵션을 사용했는데, 이 옵션을 사용한 이유는 JSON 문자열을 만들때 whitespace가 포함되어 가독석을 높이기 위해서 이 옵션을 사용했다. 마지막으로 이 data를 이용해서 NSString을 만들면 된다. 이렇게 생성된 JSON 문자열을 출력해보면 처음 테스트하려던 JSON과 동일한 형태의 JSON이 출력되는 것을 확인할 수 있다. 

- (void)testGeneratingJSON

{

    NSError *error;

    NSString *jsonInfo = [NSMutableDictionarydictionaryWithObjectsAndKeys:@"saltfactory",@"name",@"saltfactory@gmail.com",@"e-mail"nil];

    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonInfo options:NSJSONWritingPrettyPrintederror:&error];

    NSString *jsonString = [[NSString allocinitWithData:jsonData encoding:NSUTF8StringEncoding];

    NSLog(@"jsonString => %@", jsonString);


Posted by 다오나무
iOS2012. 8. 27. 17:52

UITableView에서 각 cell 마다 이미지를 로드하거나 특정 시점이 아닌 비동기적으로 이미지를 로드하기 위한 방법이 필요할 때가 있다. 이미지 로드가 완료될때까지 기다렸다가 다음 프로세스를 진행하게 되면 UI가 멈추거나 인터렉티브한 프로그램을 만드는데 많은 제약이 생기기 때문이다. 이러한 이유로 이전부터 비동기 형식으로 이미지를 로드하는 방법과 예제가 블로그와 책에 소개가 많이 되어왔다. 이러한 비동기 방식으로 이미지 로드는 iPhone 로컬의 이미지를 로드하기보다 원격지에 있는 URL을 이용해서 이미지를 로드할때 그 필요성이 더 필요하다. 네트워크를 통해서 이미지를 가져온다는 것은 바이너리 이미지를 로드하는 시간보다 네트워크에서 데이터를 전송하는 시간이 더 많이 걸리기 때문이다. 만약 원격 이미지를 비동기 방식으로 로드하지 않게 되면 UI가 멈추어 버리기 때문에 사용자들은 앱이 죽었다고 생각하거나 답답해서 그 앱을 두번다시 사용하지 않을지도 모른다.

비동기 방식으로 이미지를 로드하는 예제들
http://developer.apple.com/library/ios/#samplecode/LazyTableImages/index.html
http://www.markj.net/iphone-asynchronous-table-image/
http://gazapps.com/wp/2011/06/29/asynchronous-images-on-ios/ 
http://avishek.net/blog/?p=39

위에서 사용하는 방법은 NSURLConnection을 이용해서 delegate method를 이용하는 방법을 보통 사용한다. delegate pattern은 작업 처리를 하는 객체에게 위임을하거나 비동기적으로 객체에 메소드를 호출할때 매우 유용한 방법이다. 하지만 delegate를 사용하면 delegate 메소드가 필요하게 되고 delegate 메소드 안에서 처리할 작업을 정의해 두어야 한다. 

이미지를 비동기 방식으로 로드하고 비동기 방식으로 로드하면서 외부에서 그 이미지를 처리하는 메소드까지 선언해줄수 있는 방법으로 block과 GCD (grand centeral dispatch)를 이용하는 방법을 참조하였다. block은 Python이나 Ruby 등에서 사용하는 closure 개념과 비슷하다. Block은 C 레벨의 문법과 런타임 특징이다. 이것은 C의 기본 함수와 비슷하지만 실행가능한 코드를 추가할 수가 있고 stack 또는 heap 메모리를 바인딩하는 변수를 포함할 수 있다. 이러한 특징 대문에 Block은 실행되는 동안에 어떠한 행위를 위해서 데이터의 상태를 유지할 수 있는 특성을 가진다. Block에 관해서는 다시 한번 Block의 주제에 관해서 다시 포스팅을 할 예정이다. GCD는 애플에서 개발한 기술로써 멀티코어 프로세서에 최적화된 어플리케이션을 지원하기 위해서 만들어진 기술이다. GCD는 병렬처리와 쓰레드 풀을 기반한 기술이다. GCD에 대한 상세한 내용역시 다른 포스팅으로 준비 중이다. Block과 GCD를 이용하면 기존에 사용하던 방법(delegate로 처리하는 방법)과는 다르게 원하는 방법을 구현할 수 있을 것이라는 생각을 가지고 검색해서 다음 블로그를 찾을 수 있게 되었다.  http://www.geekygoodness.com/2011/06/17/uiimage-from-url-simplified/  이 블로그에서는 아주 간단한 설명만 남겨 두었지만 Block과 GCD를 이용해서 이미지를 로드하는 방법을 소스코드로 잘 표현해 주고 있다. Block은 iOS4 이상에서만 사용이 가능하다.

void UIImageFromURL( NSURL * URL, void (^imageBlock)(UIImage * image), void (^errorBlock)(void) )

{

    dispatch_asyncdispatch_get_global_queueDISPATCH_QUEUE_PRIORITY_DEFAULT0 ), ^(void)

                   {

                       NSData * data = [[NSData allocinitWithContentsOfURL:URL];

                       UIImage * image = [[UIImage allocinitWithData:data];

                       dispatch_asyncdispatch_get_main_queue(), ^(void){

                           if( image != nil )

                           {

                               imageBlock( image );

                           } else {

                               errorBlock();

                           }

                       });

                   });

}

 

이 코드를 iOS에서 사용하면 다음과 같은 warning이 나타난다.


이것은 UIImageFromURL가 미리 선언되어 있지 않아서 발생하는 경고인데 .h 파일 안에 미리 선언해주면 이 경고는 사라진다.

#import <UIKit/UIKit.h>


@interface ViewController : UIViewController


void UIImageFromURL( NSURL * URL, void (^imageBlock)(UIImage * image), void (^errorBlock)(void) );

@end


사용방법은 참조한 블로그에 나온 방법대로 사용하면 되는데 다음과 같이 사용하면 된다.

- (void)viewDidLoad

{

    [super viewDidLoad];

// Do any additional setup after loading the view, typically from a nib.

    

    UIImageFromURL( [NSURL URLWithString:@"https://t1.daumcdn.net/cfile/tistory/186A3A384EEAE04331"], ^(UIImage * image )

    {

        [self.view addSubview:[[UIImageView allocinitWithImage:image]];

    }, ^(void){

        NSLog(@"%@",@"error!");

    });


}



우리는 이 코드를 좀더 Objective-C에 익숙한 메소드와 파라미터 방식으로 변경하고 싶다고 생각했다. Objective-C의 메소드 선언 방법은 개발할때 파라미터에 대한 이름과 타입을 참조하는데 더 유용하기 때문이다. 그래서 이 코드를 다음과 같이 변경하여 인스턴스 메소드로 만들어서 사용할 수 있다. 

#import <UIKit/UIKit.h>


@interface ViewController : UIViewController


//void UIImageFromURL( NSURL * URL, void (^imageBlock)(UIImage * image), void (^errorBlock)(void) );


- (void) loadAsyncImageFromURL:(NSURL *)url  imageBlock:(void (^) (UIImage *image))imageBlock errorBlock:(void(^)(void))errorBlock;

@end


- (void) loadAsyncImageFromURL:(NSURL *)url  imageBlock:(void (^) (UIImage *image))imageBlock errorBlock:(void(^)(void))errorBlock

{

    dispatch_asyncdispatch_get_global_queueDISPATCH_QUEUE_PRIORITY_DEFAULT0 ), ^(void)

                   {

                       NSData * data = [[NSData allocinitWithContentsOfURL:url];

                       UIImage * image = [[UIImage allocinitWithData:data];

                       dispatch_asyncdispatch_get_main_queue(), ^(void){

                           if( image != nil )

                           {

                               imageBlock( image );

                           } else {

                               errorBlock();

                           }

                       });

                   });

}


- (void)viewDidLoad

{

    [super viewDidLoad];

// Do any additional setup after loading the view, typically from a nib.


    [self loadAsyncImageFromURL:[NSURLURLWithString:@"https://t1.daumcdn.net/cfile/tistory/1349CD374EA43DFB2E"

                     imageBlock:^(UIImage *image){ 

                         [self.view addSubview:[[UIImageView allocinitWithImage:image]];

                     } 

                     errorBlock:^(void){

                         NSLog(@"%@"@"error!");

                     }];


    

//    UIImageFromURL( [NSURL URLWithString:@"https://t1.daumcdn.net/cfile/tistory/186A3A384EEAE04331"], ^( UIImage * image )

//    {

//        [self.view addSubview:[[UIImageView alloc] initWithImage:image]];

//    }, ^(void){

//        NSLog(@"%@",@"error!");

//    });



이 방법을 이용해서 테이블에 이미지를 비동기 방식으로 로드하는 예제를 작성하면 다음과 같이 될 것이다. 간단하게 테이블에 URL 문자열을 가지는 items 배열을 가지고 사용하였지만, 실제 사용할때는 이 데이터 역시 json이나 xml에서 받아와서 만들 것으로 예상이 된다. tableView:cellForRowAtIndexPath: 메소드에서 cell의 작업을 할때 우리는 비동기 방식으로 처리하는 이 코드로 작업을 처리할 수 있다. 
(이 예제는 정말 단순하고 간단한 예제 이다. 더욱 좋은 코드를 만들기 위한 방법을 이 포스트에서는 소개하지 않는다.)

- (void)viewDidLoad

{

    [super viewDidLoad];


    items = [[NSArray allocinitWithObjects:@"https://t1.daumcdn.net/cfile/tistory/186A3A384EEAE04331"

             @"https://t1.daumcdn.net/cfile/tistory/186A3A384EEAE04331",

             @"https://t1.daumcdn.net/cfile/tistory/186A3A384EEAE04331",

             @"https://t1.daumcdn.net/cfile/tistory/186A3A384EEAE04331",

             @"https://t1.daumcdn.net/cfile/tistory/186A3A384EEAE04331",

             @"https://t1.daumcdn.net/cfile/tistory/186A3A384EEAE04331",

             @"https://t1.daumcdn.net/cfile/tistory/186A3A384EEAE04331",

             nil];

}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

    static NSString *CellIdentifier = @"Cell";

    

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil) {

        cell = [[UITableViewCell allocinitWithStyle:UITableViewCellStyleDefaultreuseIdentifier:CellIdentifier];

    }

    

    // Configure the cell...

    [self loadAsyncImageFromURL:[NSURL URLWithString:[items objectAtIndex:indexPath.row]] 

                     imageBlock:^(UIImage *image){ 


                         [[cell.contentView viewWithTag:999+indexPath.rowremoveFromSuperview];

                         

                         UIImageView *imageView = [[UIImageView allocinitWithImage:image];

                         imageView.frame = CGRectMake(04.0f.0f44.0f44.0f);

                         imageView.tag = 999+indexPath.row;

                         

                         [cell.contentView addSubview:imageView];

                     } 

                     errorBlock:^(void){

                         NSLog(@"%@"@"error!");

                     }];

    cell.textLabel.text = [items objectAtIndex:indexPath.row];

    return cell;

}

 


참조 원문 : http://www.geekygoodness.com/2011/06/17/uiimage-from-url-simplified/
코드의 저작권은 http://www.geekygoodness.com 에 있기 때문에 코드 사용시 원 저작권자에게 사용 요청을 받기 바랍니다.

Posted by 다오나무
iOS2012. 8. 27. 11:00

As a newcomer to IOS programming this particular issue on my first app took a bit of working out so this is a tutorial for others struggling with the same issue.

The ultimate objective of this code is to get a out like shown below

Firstly things to note.

      UI components need to stay on the main thread, the main thread needs to perform the UI updates. (Cocoa Fundamentals Guide, page 164: “All UIKit objects should be used on the main thread only.”)
      This tutorial/code sample is about adding a UIActivityIndicatorView programmatically
      You will need #import adding to your code file
      in this code activityIndicator is a property

I am not entirely sure this is the best way as I have only been doing IOS for a short while but it works for me.

So I suggest

Firstly start by creating a method where you start the whole thing off (probably triggered from a button press- in this sample it is showNearby.

Secondly create a NSTimer and start it immediately calling the show activity selector

Thirdly start your marker/pin loading method using the performSelectorInBackground method.

Finally remove the indicator when you have completed your marker/pin showing / loading
That’s it you will have a activity indicator that appears whilst loading and is removed once done.

The code for this is shown here.

-(IBAction)showNearby:(id)sender
{
    
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(ShowActivityIndicator) userInfo:nil repeats:NO];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];   

    [self performSelectorInBackground:@selector(loadMarkers) withObject: nil];
   
    
}

-(void)  ShowActivityIndicator
{
    
    activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge] ;
    CGRect frame = activityIndicator.frame;
    frame.origin = CGPointMake(290, 12);
    frame.size = CGSizeMake(100, 100);
   
    activityIndicator.backgroundColor=[UIColor blackColor]; 
    CALayer *layer =[activityIndicator layer];
    layer.cornerRadius = 8.0;
    activityIndicator.alpha=0.7;
    activityIndicator.frame = frame;
    activityIndicator.center = self.view.center; 
    activityIndicator.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    activityIndicator.tag = 2;
    
    [self.view addSubview:activityIndicator];
    [self.view bringSubviewToFront:activityIndicator];  
    [activityIndicator startAnimating];
}

-(void) loadMarkers
{
  //code to load markers then remove activityIndicator

[activityIndicator stopAnimating];
[activityIndicator removeFromSuperview];
}

Posted by 다오나무
iOS2012. 8. 27. 10:51

UIProgressHUD / 어플 실행 및 , 데이터 호출시 "로딩중" 메세지처리



아이폰 어플은 거의 대부분 로딩중이라는 메세지가 나타나길레.. 대부분 자동으로 처리되는줄 알았습니다..
그런데;;; 아니더군요 -_-;;

UIActivaityIndicatorView 의 경우 별도 스타일이나 지원되는 것이 있는듯하나..


모두다 호출전 출력 / 호출후 출력.. 노가다를 통해 진행한듯합니다
아직 잘은 모르겠지만. 

앱을 이용하는 이용자가 마냥 기다리지않도록 처리해야하는 로딩메세지는 상당히 중요한 UI중에 하나인데요


여러책에도 나왔지만 구체적으로 어떤 방법으로 처리되는지 나와있는게 없어 포스팅합니다.



보통 "Delegate -> 메인뷰 -> 서브뷰" 형태로 진행이되는데요

우선 메인뷰에 적용한 소스입니다.

해당 소스를 Delegate 에 동일하게 추가하셔도 이용이 가능합니다

============= 헤더파일 ==================
#import <UIKit/UIKit.h>

//프로그래스 - 로딩메세지
@interface UIProgressHUD : NSObject
- (UIProgressHUD *) initWithWindow: (UIView*)aWindow; 
- (void) show: (BOOL)aShow;
- (void) done;
- (void) hide;
- (void) setText: (NSString*)aText; 
@end

@interface MainView : UIViewController{
    UIProgressHUD *hud;
}


@end
=====================================
상위 @interface UIProgressHUD : NSObject 부분부터 @end 까지 별도 해당 소스가 추가되며.



============= 메인파일 ==================

//프로그레스 - 로딩메세지
-(void)mostraProgressHUD:(BOOL)done{
    hud = [[UIProgressHUD alloc] initWithWindow:self.view];
   //hud = [[UIProgressHUD alloc] initWithWindow:self.window];    //Delegate 에 추가시 해당 부분을 self.window로 변경하여 이용

 
    if(done){
        [hud setText:@"로딩완료"];
    }else{
        [hud setText:@"로딩중"];
    }
    [hud show:YES];
    if(done){
        [hud done];
    }
}

//프로그래스 로딩메세지
-(void)escondeProgressHUD{
    if (hud != nil){
        [hud hide];
        hud = nil;
    }
}

=====================================
메인파일은 mostraProgressHUD / escondeProgressHUD 두개가 추가하시고.


팝업호출은 [self mostraProgressHUD:NO]; 과 

 [self mostraProgressHUD:YES]; 로 호출하시면 됩니다.
프로그레스의 좀더 구체적이고 자동화되어있는것이 있으면 별도 포스팅을 하겠습니다.


기타 다른 함수는

[hud show];
[hud done];
[hud hide];
[hud setText:@"하이욤"];

과같이 이용하시면 됩니다.



P.S : 아시죠?? 해당 소스는 특정 저자의 소스를 인용한것이 아닙니다.
        애플개발자 레퍼런스와, 외국 개발자들의 소스를 인용한것이며.

        누구를 가르키기 위함이 아니라.
        제가 개발중 까먹는것을 대비해서 포스팅하는것입니다.

Posted by 다오나무
iOS2012. 8. 26. 13:02

안녕하세요. 팬텀입니다. 
첨부된 파일은 이번 정모에 발표했던 'App과 Server의 은밀한 대화'에 리뷰했던 코드 예제 입니다.
그날 리뷰했던 코드에 urlencoding / urldecoding 관련된 부분이 추가되었습니다.

첨부된 파일을 xcode에서 실행시키면, 뷰에 버튼이 하나 있습니다. 
이 버튼이 parameter와 security token을 만들고, 서버로 request를 보내고,
서버로부터의 결과를 콘솔에 남기는 일을 합니다. 콘솔에서 다음과 같이 확인할 수 있습니다.

 [Session started at 2010-07-21 00:57:01 +0900.]
 -------------------------------------------
[app]
1) param : id=2&point=450&name=%EB%A7%A5%EB%B6%80%EA%B8%B0&nonce=16807
2) st : PysueoNho2uhis%2B8%2FQjpWuSuYdGYA0m4HM969zadxBZIMNwe%2BJle
 -------------------------------------------
[server]
1) result : id=2&point=450&name=맥부기&nonce=16807

endpoint는 코드에 들어있는 http://rockk.org/mcbugi/test.php 입니다. 2주 정도 열어 놓도록 할께요.
서버쪽 코드는 다음과 같구요. 간단하게 st를 복호화 한 값을 그대로 echo 합니다.
app과 key값을 꼭 맞춰주어야 합니다. 아래 붉게 강조된 부분에요. '123456789abcdef'

 <?php
// base64 decode st
$tmp_st = base64_decode(urldecode($_POST['st']));

// decrypt st (key is '123456789abcdef')
$st = urldecode(trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, '123456789abcdef', $tmp_st, MCRYPT_MODE_ECB, mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_ECB), MCRYPT_RAND))));

// echo decrypted data
echo $st;
exit;
?>


실제로 사용할 땐, $st 값을 &로 파싱하고, key & value로 활용하면 되지 않을까 싶습니다.

CryptoHelper 관련 참고 url은 아래와 같구요.
http://pastie.org/297563.txt
http://stackoverflow.com/questions/1235171/iphone-how-to-encrypt-a-string

php의 mcrypt와 padding을 다루는 부분에서 차이가 있어CryptoHelper.m의 다음 부분에 kCCOptionECBMode를 추가했습니다.
참고하세요.

 // Create and Initialize the crypto reference.
    ccStatus = CCCryptorCreate(    encryptOrDecrypt, 
                               kCCAlgorithmAES128, 
                               kCCOptionPKCS7Padding | kCCOptionECBMode
                               (const void *)[theSymmetricKey bytes], 
                               kCCKeySizeAES128, 
                               (const void *)iv, 
                               &thisEncipher
                               );


혹시나 해서 발표자료도 공유합니다- :D

App과 Server의 은밀한 대화
View more presentations from rockk.




한글 잘림현상- 아래수정 부분 확인해서 사용하삼
- (NSString*)encryptString:(NSString*)string
{
NSRange fullRange;
fullRange.length = [string length];
fullRange.location = 0;

uint8_t buffer[[string length]];

[string getBytes:&buffer maxLength:[string length] usedLength:NULL encoding:NSUTF8StringEncoding options:0 range:fullRange remainingRange:NULL];

// NSData *plainText = [NSData dataWithBytes:buffer length:[string length]];// 한글 잘림
NSData *plainText = [string dataUsingEncoding:NSUTF8StringEncoding];

NSData *encryptedResponse = [self doCipher:plainText key:symmetricKey context:kCCEncrypt padding:&pad];

return [self base64EncodeData:encryptedResponse];
}

Posted by 다오나무