ㄱ본 글은 자바스크립트 카테고리의 "하이브리드 앱에서의 함수 실행" 글과 이어집니다. 



Objective C 네이티브로 하이브리드 앱을 만들때에 웹에서 Objective C 함수를 실행하기 위해서 자바스크립트단에도 준비를 해야하지만, 실제로 실행될 Obj C 함수도 어플리케이션단에 준비되어있어야한다. 이때, 기존의 UIWebView와 iOS8부터 사용되고 있는 WKWebView에서의 처리가 각각 다르기에 각 클래스에 맞게 정리해둔다.


1. 자바스크립트 -> iOS


1.1. UIWebView

 

 기존에 많이 사용되던 클래스이다. 자바스크립트 사이드에서의 구현은 간단하지만 어플리케이션 레벨에서 약간 까다로운 준비를 해야한다. 

아래의 코드는 하나의 뷰 컨트롤러에서 webView:shouldStartLoadWithRequest:navigationType: 함수를 사용하여 웹뷰의 request를 가로채 

필요한 값들만 뽑아낸다. 요청의 프로토콜이 기존에 약속되어 있던 코드인지 확인 한 후 host는 함수명으로 querystring은 키 / 변수 쌍으로 사용하고, querystring을 사용할때 Dictionary로 변환하여 선언해둔 함수로 넘겨준다. 넘겨받은 함수명으로 바로 함수를 사용할 경우 오류가 발생할 수 있으므로 RespondToSelector: 함수로 넘겨받은 함수가 실제로 존재하는지 확인한 후 실행하도록 했다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#import <UIKit/UIKit.h>
 
// UIWebView의 Delegate함수를 실행해야 하므로 Delegate를 추가해준다.
@interface UIWVController: UIViewController<UIWebViewDelegate>
 
// 사용할 웹뷰를 선언해준다.
@property(nonatomic) IBOutlet UIWebView* webView;
 
@end
 
@implementation FrontViewController
 
 
- (void)viewDidLoad {
    [super viewDidLoad];
    // 웹뷰를 초기화 해준다.    
    webView = [[UIWebView allocinit];
    // 해당 웹뷰의 Delegate를 현재 ViewController로 지정해준다.
    [webView setDelegate:self];
}
 
#pragma mark - UIWebView Delegate Methods
 
// 웹뷰의 리퀘스트가 시작되면 (주소가 바뀌면) 실행되는 함수
// UIWebView일때 location.href 혹은 <a href=''.../> 를 사용하면 아래 함수가 실행된다.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request 
navigationType:(UIWebViewNavigationType)navigationType{
    if([request.URL.scheme rangeOfString:@"ioscall"].location != NSNotFound){
        // web -> app 통신을 걸러낸다
        // 이 예제에서는 ioscall이라는 프로토콜을 사용하는 것을 전제로 함
        
        // scheme : 요청의 프로토콜명 보통은 http / https가 넘어온다.
        // host : 함수의 이름 
        // query : 파라미터
        
        // 넘겨받은 함수를 확인 및 실행하기 위한 NSSelector 변수
        SEL _selector_from_web;
        // 변수가 있는지 없는지 확인한다.
        // 변수가 존재하는 경우에는 Selector에 : 가 붙으므로 나누어 선언해준다.
        if(request.URL.query){
            NSLog(@"변수가 있습니다. %@" , request.URL.query);
            _selector_from_web = NSSelectorFromString([NSString stringWithFormat:@"%@:" , request.URL.host]);
        }
        else{
            NSLog(@"변수가 없습니다. %@" , request.URL.query);
            _selector_from_web = NSSelectorFromString(request.URL.host);
        }
        
        if([self respondsToSelector:_selector_from_web]){
            if(request.URL.query){
                // 변수가 존재하는 경우
                Boolean err_occurred = false;     
                // 넘겨받은 쿼리스트링을 &를 기준으로 나눕니다           
                NSArray *queries_arr = [[request.URL.query stringByRemovingPercentEncoding] 
componentsSeparatedByString:@"&"];
                NSMutableDictionary *queries = [[NSMutableDictionary alloc]init];
                for(int idx = 0 ; idx < queries_arr.count ; idx++){
                    if([[queries_arr objectAtIndex:idx] rangeOfString:@"="].location == NSNotFound){
                        // 형식이 잘못되었을 경우
                        err_occurred = true;
                        break;
                    }
                    NSArray *separated = [[queries_arr objectAtIndex:idx] componentsSeparatedByString:@"="];
                    [queries setObject:[separated objectAtIndex:1] forKey:[separated objectAtIndex:0]];
                }
                if(err_occurred){
                    // 에러가 생겼다.
                    NSLog(@"올바르지 않은 형태의 쿼리입니다. %@" , request.URL.query);
                    return NO;
                }
                else{
                    // 2. method call
                    // 함수가 컨트롤러에 존재할때만 실행되도록 되어있습니다.
                    [self performSelector:_selector_from_web withObject:queries];
                }            }
            else {
                // 변수가 존재하지 않는 경우
                [self performSelector:_selector_from_web];
            }
        }
        else{
            // 넘겨받은 함수가 존재하지 않는 경우 
            NSLog(@"셀렉터가 존재하지 않습니다");
            return NO;
        }
    }    
 
    return YES;    
}
 
 
#pragma mark - JS -> APP Methods
 
- (void) mockup_method:(NSDictionary *)params{
    // 스크립트에서 location.href = ioscall://mockup_method?value=1을 실행할 경우
    // 이 함수가 실행됩니다.
    NSLog(@"mockup method called ! value = [%@]" , [params objectForKey:@"value"]);
}
 
@end

cs



1.2. WKWebView


 WKWebView는 UIKit이외에 WebKit을 추가해주어야한다. 좀 더 추가해주어야 하는 요소들이 많고 상속받아야 하는 것들이 많지만 그만큼 더 세밀한 

작업을 지원한다. 스토리보드에서는 WKWebView를 추가해 사용할 수 없으므로 크기를 지정해주고 직접 뷰에 추가해주어야 하는 불편함이 있다. request를 다루는 부분과 스크립트를 다루는 부분이 분리되어 있어 코드를 보기에 더 편해졌다. UIWebView와 마찬가지로 함수가 존재할때만 실행하도록 한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#import <UIKit/UIKit.h>
// WkWebView를 사용하기 위해 WebKit을 참조합니다.
@import WebKit;
 
// WkWebView에서 주소가 변경되거나 스크립트를 사용할 경우 사용할 Delegate함수를 사용하기 위해
// 아래의 Delegate들을 참조합니다
@interface MainViewController : UIViewController<WKUIDelegate , WKNavigationDelegate , WKScriptMessageHandler>
 
@property(nonatomic) WKWebView* webView;
 
@end
 
@implementation MainViewController{
    // 지역 변수를 선언합니다. 
    // 웹뷰의 랜더 속도 및 각종 설정들을 담당하는 클래스입니다.
    WkWebViewConfiguration *config;
    // 자바스크립트에서 메시지를 받거나 자바스크립트를 실행하는데 필요한 클래스입니다.
    WKUserContentController *jsctrl;
}
 
- (void) viewDidLoad{
    [super viewDidLoad];
 
    // WkWebViewConfiguration과 WKUserContentController를 초기화해줍니다. 
    config = [[WKWebViewConfiguration alloc]init];
    jsctrl = [[WKUserContentController alloc]init];
    
    // 자바스크립트 -> ios에 사용될 핸들러 이름을 추가해줍니다.
    // 본 글에서는 핸들러 및 프로토콜을 ioscall로 통일합니다.
    [jsctrl addScriptMessageHandler:self name:@"ioscall"];
    // WkWebView의 configuration에 스크립트에 대한 설정을 정해줍니다.
    [config setUserContentController:jsctrl];
    
    // 웹뷰의 딜리게이트들을 새로 초기화해줍니다.
    [webView setUIDelegate:self];
    [webView setNavigationDelegate:self];
    
    CGRect frame = [[UIScreen mainScreen]bounds];
    // WkWebView는 IBOutlet으로 제공되지 않아 스토리보드에서 추가할 수 없습니다.
    // 웹뷰의 크기를 정해준 후 초기화하고 본 ViewController의 뷰에 추가합니다.
    webView = [[UIWKWebView alloc] initWithFrame:frame configuration:config];
    [[self view]addSubView:webView];
}
 
#pragma mark - delegate method
 
// WKScriptMessageHandler에 의해 생성된 delegate 함수입니다. 
// 자바스크립트에서 ios에 wekkit핸들러를 통해 postMessage함수를 사용한 경우 실행됩니다.
- (void)userContentController:(WKUserContentController *)userContentController 
        didReceiveScriptMessage:(WKScriptMessage *)message{
 
    // message의 body에 전달받은 객체가 NSDictionary형식으로 들어있습니다.
    // 본 예제에서는 함수이름을 "name"으로, 파라미터 묶음을 "params"로 정합니다.
    if([[[message body] allKeys] count] > 0 && [[message body]objectForKey:@"name"]){
        SEL _selector = nil;
        if([[message body] objectForKey:@"params"]){
            // 변수를 가진 경우
            _selector = NSSelectorFromString([NSString stringWithFormat:@"%@:"
, [[message body] objectForKey:@"name"]]);
            
            if([self respondsToSelector:_selector]){
                // 현재 뷰 컨트롤러에 함수가 존재하는지 확인합니다.
                [self performSelector:_selector withObject:[[message body]objectForKey:@"params"]];
            }
            else{
                NSLog(@"셀렉터가 존재하지 않습니다");
            }
        }
        else{
            _selector = NSSelectorFromString([NSString stringWithFormat:@"%@",
[[message body]objectForKey:@"name"]]);
            
            if([self respondsToSelector:_selector]){
                // 현재 뷰 컨트롤러에 함수가 존재하는지 확인합니다. 
               [self performSelector:_selector];
            }
            else{
                NSLog(@"셀렉터가 존재하지 않습니다.");
            }
        }
    }
    else{
        NSLog(@"잘못된 javascript 요청입니다.");
    }
}
 
#pragma mark - JS -> ios Method
 
- (void) mockup_method:(NSDictionary *)params{
    // 스크립트에서 webkit.messageHandlers.ioscall.postMessage 함수를 실행할 경우
    // 이 함수가 실행됩니다.
    NSLog(@"mockup method called ! value = [%@]" , [params objectForKey:@"value"]);
}
@end
 
 
cs



2. iOS -> 자바스크립트


 iOS상에서 자바스크립트를 실행하는 것도 UIWebView와 WKWebView간 차이가 있다. 반대의 경우보다는 비교적 간단하다.


2.1. UIWebView

 

 UIWebView에서는 뷰 컨트롤러 내에서 stringByEvaluatingJavascriptFromString함수를 이용해 자바스크립트를 호출한다. 다만 스크립트를 직접 

문자열로 만들어 호출하는만큼 문자열을 조작하는 함수를 따로 만들어 두는 것이 편할 것이다. 


1
[webView stringByEvaluatingJavascriptFromString:@"window.alert('Hello World')"];
cs

2.2. WKWebView


 WKWebView에서의 스크립트 호출도 UIWebView와 비슷한 방법으로 실행된다. 함수는 evaluateJavasScript:completionHandler: 이며 

completionHandler에 블록함수를 넣어 자바스크립트가 실행된 뒤의 이벤트를 캐치할 수 있다. 



1
2
3
[webView evaluateJavaScript:@"window.alert('Hello World');" completionHandler:^{
    NSLog(@"evaluate Completed");
}];
cs


즉각적으로 자바스크립트를 실행할 경우 UIWebView와 사용법이 크게 다르지 않지만, WKWebView에서는 차별화된 기능을 갖고 있는데, WKUserScript

클래스를 이용해 문서 최상단 / 최하단에 스크립트를 직접 삽입해주는 addScript: 함수가 그것이다. 사용법은 아래와 같다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void) viewDidLoad{
    [super viewDidLoad];
 
    // WkWebViewConfiguration과 WKUserContentController를 초기화해줍니다. 
    config = [[WKWebViewConfiguration alloc]init];
    jsctrl = [[WKUserContentController alloc]init];
    
    // 자바스크립트 -> ios에 사용될 핸들러 이름을 추가해줍니다.
    // 본 글에서는 핸들러 및 프로토콜을 ioscall로 통일합니다.
    [jsctrl addScriptMessageHandler:self name:@"ioscall"];
    // WkWebView의 configuration에 스크립트에 대한 설정을 정해줍니다.
    [config setUserContentController:jsctrl];
    
    // 스크립트를 초기화 합니다.
    WKUserScript *cookie_script = [[WKUserScript alloc]initWithSource:@"alert('load!')" 
                                    injectionTime:WKUserScriptInjectionTimeAtDocumentStart 
                                    forMainFrameOnly:NO];
    // UserContentController에 스크립트를 삽입합니다.
    [jsctrl addUserScript:cookie_script];
        
    // 웹뷰의 딜리게이트들을 새로 초기화해줍니다.
    [webView setUIDelegate:self];
    [webView setNavigationDelegate:self];
    
    CGRect frame = [[UIScreen mainScreen]bounds];
    // WkWebView는 IBOutlet으로 제공되지 않아 스토리보드에서 추가할 수 없습니다.
    // 웹뷰의 크기를 정해준 후 초기화하고 본 ViewController의 뷰에 추가합니다.
    webView = [[UIWKWebView alloc] initWithFrame:frame configuration:config];
    [[self view]addSubView:webView];
}
cs


소스는 위의 WKWebView 사용부의 viewDidLoad 부분을 잘라낸 것인데 , ln14 ~ 18이 추가되었다. WKUserScript클래스로 문서의 최상단에 둘지

최하단에 둘지 결정하고 (injectionTime) 모든 프레임에 적용할 것인지 (forMainFrameOnly)결정한 후 WKUserContentController에 해당 스크립트를

추가해준다. 이렇게하면 웹뷰가 로드되었을때 미리 삽입한 스크립트들이 자동으로 실행되게 된다. 



안드로이드 , iOS에서 네이티브로 하이브리드 앱을 만들 경우 웹(JS)와  앱(ios , android)양쪽에서 서로의 함수를 실행해야 하는 경우가 생긴다. 


본 글에는 웹 <-> 앱간 함수를 실행해야 될 경우 자바스크립트에서 준비해야할 함수를 정리해둔다.


(앱에서 자바스크립트를 실행하는 코드와 자바스크립트로 앱 함수를 실행할 경우 앱에서 선언해야 하는 앱(ios / android)의 함수는 따로 다루도록 한다.)




1. 자바스크립트에서 iOS 함수 실행


 자바스크립트에서 iOS의 함수를 실행하는 방법은 두가지이다. 이는 기존에 사용되던 웹뷰 UIWebView에 더해 iOS8부터 WKWebView가 추가되었기 때문이다. 두 함수의 자바스크립트 코드와 objective c (OR Swift)코드가 다르기 때문에 유의하여야 한다. 


1.1 UIWebView

location.href = YOUR_PROTOCOL://YOUR_METHOD_NAME?PARAM_1_NAME=PARAM_2_VALUE&PARAM_2_NAME=PARAM_2_VALUE....

 UIWebView에서는 주소를 호출하는 방식을 사용한다. http 대신 임의로 지정한 이름을 프로토콜로 사용하고 도메인이 위치하는 곳에는 함수 이름을 , 쿼리스트링으로 함수의 변수들을 넘겨준다. 이렇게 실행 후 iOS 코드 내에서는 웹뷰의 주소가 변경되는 것을 감지하는 함수내에서 각 값들을 분리하여 사용하도록 한다. 


1.2 WKWebview 

 

window.webkit.messageHandlers[YOUR_HANDLER_NAME].postMessage(PARAMS)


 WKWebView에서는 전역변수로서 자동으로 webkit이라는 변수를 생성한다. 그리고 웹 -> 앱간 통신이 필요한 경우 위의 webkit 변수를 통해 이루어진다. webkit.messageHandlers에 앱에서 임의로 결정한 값이 추가로 생성되며, 그 자식 함수로 postMessage라는 함수가 생성된다. 여기서 postMessage라는 함수를 스크립트에서 실행할 경우 자바스크립트 함수를 처리하는 WKWebView의 딜리게이트 함수에서 'PARAMS'을 dictionary형태로 넘겨 받게 된다. PARAMS는 자바스크립트 객체 형태로 넘겨주어도 무방하다. 


2. 자바스크립트에서 Android 함수 실행


window[YOUR_HANDLER_NAME][YOUR_METHOD_NAME](PARAMS)

 안드로이드에서는 임의로 설정한 HANDLER이름으로 전역 변수를 생성하고 그 아래에 함수를 선언하는 형식이 된다. 일반적으로 전역 변수로서 네임스페이스로 함수들을 묶어두는 방식과 동일하다. 





위의 기능들을 사용하는데 매번 문자열을 더하거나 확인해가면서 작업할 수는 없는 노릇이라, 웹 <-> 앱간의 통신을 담당하는 스크립트 함수들을 묶음으로 만들어두고 실제로 사용하였다. 가지고 있는 기능은 아래와 같다. 

// 0. 최초 로드시

// 반드시 HY_BRIDGE.config 함수로 ios 및 안드로이드의 핸들러 / 프리픽스를 초기화 해주어야 합니다.

HY_BRIDGE.config({

ios : { prefix : 'PREFIX' , handler : 'HANDLER' },

android : { handler : 'HANDLER' }

})


// 1.1 기기 타입 가져오기

HY_BRIDGE.tools.device() // iOS / Android / Unknown

// 1.2 iOS 확인

HY_BRDIGE.tools.isiOS() // true or false

// 1.3 Android 확인

HY_BRIDGE.tools.isAndroid() // true or false



// 2 웹-> 어플리케이션 함수

// 2.1 생성

HY_BDIRGE.app.extend({

method_1 : function(foo,bar){

HY_BDRIDGE.app.dispatch({

method : "METHOD_NAME_IN_APP",

params : { foo : 0 , bar : 1 }

});

},

method_2 : function(){

..

}

})

// 2.2 실행

HY_BRIDGE.app.call(method_1 , foo , bar);


// 2.3 특정 이벤트일때

// 2.3.1 생성

HY_BRIDGE.app.evt.insert(HY_BRIDGE_app.IDENTIFIER.ON_LOAD_FINISHED, function(){

// 함수 내용

})

// 2.3.2 (앱에서) 실행 (선언된 순서대로 실행됩니다.)

HY_BRIDGE.app.evt.flush(HY_BRIDGE.app.IDENTIFIER.ON_LOAD_FINISHED);


// 3 어플리케이션 -> 웹

// 어플리케이션에서 자바스크립트는 쉽게 실행할 수 있지만, 파편화 되는 것을 방지하기 위해 선언 및 사용부를 통일해두었다.

// 3.1.1 함수 생성

HY_BRIDGE.web.extend({

method_1 : function(foo){

console.log("Hello " + foo)

}

});

// 3.1.2 함수 실행

HY_BRIDGE.web.call('method_1' , foo)

// 3.2.1 변수 생성

HY_BRIDGE.web.declare({ value_1 : 0 , value_2 : 1 })

// 3.2.2 변수 가져오기

HY_BRIDGE.web.get('value_1')




'DEV > JavaScript' 카테고리의 다른 글

문자열 관련 함수 묶음  (0) 2017.10.17
쿠키 관리 함수 묶음  (0) 2017.10.17
주소의 쿼리스트링을 조작하는 함수 모음  (0) 2017.10.17

 개인작업을 하다보니 여러 앱에 빠지지않은 페이징기능을 구현해야할 경우가 생겨, 여러 방법을 찾아봤는데 어떻게된것이 페이지 컨트롤러를 분명 XCode에서 지원하는데도, 직접 구현하는 방법을 먼저 찾게되어버렸다. 

그런고로 이번 게시물에서 다룰것은 그 페이징기능을 ScrollView와 PageControl을 이용해 구현하는 방법이다. 사용해보지는 않았지만, 페이지 컨트롤러로 페이징을 구현하는 것보다 귀찮은 짓일것이라고 생각된다.



샘플소스는 아래의 링크로 다운 !

PageWithScrollView.zip



 

           



 스크롤뷰를 이용해 페이징을 구현하기에 앞서 스크롤뷰의 속성에 대해서 하나 알아둘 것이있다. 바로 ContentSize라는 개념인데, 이는 스크롤뷰가 보여줄 수 있는 내용, 즉 컨텐츠의 크기를 말하는 것으로 SIze와는 완전히 다른개념이다. 예컨데, 4inch 아이폰의 화면(320 x 568)만한 스크롤 뷰가 있을때 페이징 기능을 이용해 가로로 아이폰 화면만한 5개의 뷰를 보여주려고한다면 , 스크롤 뷰 Frame의 Size는 320x568이되겠지만 ContentSize는 스크롤 뷰의 가로길이 * 페이지 수인 1600x568이 되는것이다. 

 다시 요약하자면, 이 ContentSize는 스크롤뷰가 보여줄 수 있는 실제 내용의 크기로 이 사이즈가 설정되어야 스크롤뷰가 제 역할을 할 수 있는 것이다.


 이제 본격적으로 예제를 작성하기 위해 간단한 프로젝트를 하나 만들었다. Empty Project로 프로젝트를 생성하고 뷰컨트롤러와 스토리보드를 만들어준다. 스크린샷에는 나와있지 않지만 예제용 이미지도 프로젝트에 포함해두었다. 샘플을 확인!



# 클래스 / 파일명 일람 

  - MainStoryboard : 현재 예제를 구성할 뷰를 그려놓을 스토리보드

  - PageScrollViewController : 페이징을 구현할 ScrollView를 가지는 뷰 컨트롤러

  - PageViewController : ScrollView에서 보여줄 각 페이지의 내용 뷰 컨트롤러


본격적인 코딩에 들어가기 앞서, 스토리보드에서 간단한 구성요소들을 만들어주자 스토리보드에서 각 뷰가 가지는 요소들은, 스크린샷에서 확인하면된다. 구성요소들을 모두 위치시키고나면 PageScrollViewController의 Storyboard Identifier에는 PageScrollView , PageViewController에는 SinglePageView라는 값을 주자. 나중에 뷰를 불러올때 사용할 수 있다.

 스토리보드에서 구성요소들을 위치시킬때 주의해야할 점은 PageControl이 ScrollView의 하위에 속하면 안된다는 것이다. 그럴 경우 예제의 방법으로 구현했을때 정상적인 스크롤 이벤트가 발생하지 않으니 유의할 것! 




 가장 먼저 , 현재 프로젝트는 빈 프로젝트였으므로 AppDelegate의 루트뷰를 설정해주어야한다. 


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // 현재 스토리보드를 불러온다.
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
    // RootViewController로 설정할 PageScrollView를 불러온다.
    UIViewController *rootViewController = [storyboard instantiateViewControllerWithIdentifier:@"PageScrollView"];
    
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    self.window.rootViewController = rootViewController;
    
    [self.window makeKeyAndVisible];
    return YES;
}

#PageScrollViewController.h

 RootViewController의 설정이 끝나면 이제 본격적으로 코딩에 들어가보자 ! 먼저 PageScrollViewController부터 작업해준다. PageScrollViewController클래스의 헤더는 아래와 같이 작성한다. 헤더의 각 프로퍼티들과 요소들을 선언한후에는 스토리보드의 요소들과 매칭시켜주어야한다.

#import 

@interface PageScrollViewController : UIViewController<UIScrollViewDelegate>

// IBOUTLET
@property (weak, nonatomic)IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic)IBOutlet UIPageControl *pageControl;
@property (weak, nonatomic)IBOutlet UIButton *btn_Prev;
@property (weak, nonatomic)IBOutlet UIButton *btn_Next;

// 컨트롤러들이 저장될 배열
@property (strong, nonatomic)NSMutableArray *controllers;

// 페이지 수동 전환용 IBACTION
-(IBAction)btn_ScrollMove:(id)sender;

@end

 PageScrollViewController가 가지는 변수는 위의 소스와 같은데 (화면 구성을 위한 IBOUTLET들의 설명은 생략) 이 중 주목해야될 객체는 NSMutableArray형인 controllers다. 이 배열은 페이징을 구성하는 뷰 컨트롤러들을 가지고있는 배열로, 뷰 컨트롤러들을 로드해서 이 배열에 저장해두었다가 필요할때 꺼내서 쓰는 방식으로 사용한다. 페이징을 구성하는 실질적인 DataSource라고 할 수 있다.


#PageScrollViewController.m

 이 파일에서는 PageViewController를 미리 import시켜두어야한다. 페이징 뷰를 구성할 뷰 컨트롤러들을 저장해두기 위함이다. 각 프로퍼티들을 Synthesize 해준 뒤 본격적인 코딩에 들어간다.

 먼저 스크롤 뷰 와 페이지 컨트롤, 그리고 데이터소스가될 controllers등을 초기화해줄 함수를 구성해준다. 소스는 아래와 같다. 


-(void)initScrollViewAndPageControl{
    // 스크롤뷰와 페이지 컨트롤에 관련된 변수들을 초기화합니다.
    
    // 임의로 표시할 페이지 수를 설정합니다.
    int numbersOfPage = 4;
    
    // 컨트롤러들을 저장할 배열을 초기화합니다.
    _controllers = [[NSMutableArray alloc]init];
    
    // 최초에 값을 넣어줄때는 배열에 Null을 넣어줍니다.
    for(int idx = 0 ; idx < numbersOfPage ; idx++){
        [_controllers addObject:[NSNull null]];
    }
    
    // 스크롤 뷰의 컨텐츠 사이즈를 미리 만들어둡니다.
    CGSize contentSize = _scrollView.frame.size;
    contentSize.width = _scrollView.frame.size.width * numbersOfPage;
    
    // 스크롤 뷰의 컨텐츠 사이즈를 설정합니다.
    [_scrollView setContentSize:contentSize];
    // 스크롤 뷰의 Delegate를 설정합니다. ScrollView Delegate 함수를 사용하기 위함입니다.
    [_scrollView setDelegate:self];
    // 스크롤 뷰의 페이징 기능을 ON합니다.
    [_scrollView setPagingEnabled:YES];
    // 스크롤 뷰의 Bounce를 Disabled합니다 첫 페이지와 마지막 페이지에서 애니메이션이 발생하지않습니다.
    [_scrollView setBounces:NO];
    [_scrollView setScrollsToTop:NO];
    [_scrollView setScrollEnabled:YES];
    
    // 스크롤 바들을 보이지 않습니다.
    _scrollView.showsHorizontalScrollIndicator = NO;
    _scrollView.showsVerticalScrollIndicator = NO;
    
    // 현재 페이지 컨트롤의 페이지 숫자와 현재 페이지를 설정합니다.
    [_pageControl setNumberOfPages:numbersOfPage];
    [_pageControl setCurrentPage:0];
    
    // 최초에 보여줄 페이지를 미리 2개만 로드합니다.
    [self loadScrollViewDataSourceWithPage:0];
    [self loadScrollViewDataSourceWithPage:1];
}

 각 줄의 설명은 소스의 주석으로 대체하기로하고, 위의 함수를 보면 loadScrollViewDataSourceWithPage:라는 함수가 있는데, 이 함수는 스크롤뷰를 구성할 각 페이지를 로드하는 함수로 지정한 페이지에 해당하는 뷰 컨트롤러를 미리 불러와 스크롤뷰의 SubView로 추가해주는 핵심적인 함수라고 할 수 있다. 

 위의 함수에서는 0페이지와 1페이지만 불러왔는데 이는 최초 페이지가 0일 경우 이전페이지는 존재하지않고 당장에 스크롤로 보여주여야할 뷰는 현재 페이지와 다음페이지 즉, 0과 1페이지 뿐이므로 0페이지와 1페이지에 해당하는 뷰컨트롤러만 미리 로드해두도록한 것이다. 이 loadScrollViewDataSourceWithPage:의 내용은 아래와 같다. 


- (void)loadScrollViewDataSourceWithPage:(NSInteger)page{
    // 스크롤뷰에서 표시할 뷰를 미리 로드합니다.
    
    // 페이지가 범위를 벗어나면 로드하지않습니다.
    if(page >= _controllers.count)
        return;

    // 페이지의 뷰컨트롤러를 배열에서 읽어옵니다.
    PageViewController *controller = [_controllers objectAtIndex:page];
    
    // 현재 컨트롤러가 비어있다면, 컨트롤러를 초기화해줍니다.
    // (initScrollViewAndPageControl 참조)
    if((NSNull *)controller == [NSNull null]){
        NSLog(@"Page %d Controller Init..",page);
        
        // 현재 스토리보드에서 SinglePageView라는 StoryboardIdentifier를 가진 뷰를 읽어옵니다.
        controller = [[self storyboard] instantiateViewControllerWithIdentifier:@"SinglePageView"];
        // 현재 컨트롤러의 뷰에 Frame을 초기화해줍니다.
        [controller.view setFrame:_scrollView.frame];
        // 컨트롤러에 이미지와 텍스트들을 설정합니다.
        [controller initPageViewInfo:page];
        // 현재 컨트롤러와 배열에 들어있는 객체를 교체합니다.
        [_controllers replaceObjectAtIndex:page withObject:controller];
    }
    
    // 현재 컨트롤러의 뷰가 superview를 가지지 못했을 경우(현재 스크롤뷰의 서브뷰가 아닌 경우)
    // 스크롤 뷰의 서브뷰로 추가해줍니다.
    if(controller.view.superview == nil){
        NSLog(@"Page %d Controller Add On ScrollView..",page);
        
        // 현재 컨트롤러의 뷰가 위치할 frame을 잡아줍니다.
        // Page에 따라 Origin의 x값이 달라집니다.
        CGRect curFrame = _scrollView.frame;
        curFrame.origin.x = CGRectGetWidth(curFrame) * page;
        curFrame.origin.y = 0;
        controller.view.frame = curFrame;
        
        // 컨트롤러를 현재 컨트롤러의 ChildViewController로 등록하고 컨트롤러의 뷰를 스크롤뷰에 Subview로 추가해줍니다.
        [self addChildViewController:controller];
        [_scrollView addSubview:controller.view];
        [controller didMoveToParentViewController:self];
    }
}

 이렇게 페이징을 구성할 데이터 소스들을 다루는 함수의 구현을 끝마치고나면, UIScrollViewDelegate의 Delegate함수인 scrollViewDidEndDecelerating:를 구현해주어야한다. 이 Delegate 함수는 스크롤 뷰의 스크롤링이 완전히 끝마쳤을 경우 실행되는 함수로 이 함수에서 현재 페이지를 계산해 PageControl에 넘겨준후 현재 페이지와 이전페이지, 다음 페이지에 해당하는 뷰 컨트롤러들을 미리 로드해두게된다. 

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    
    // ScrollView의 드래그가 멈춘경우
    CGFloat pageWidth = CGRectGetWidth(_scrollView.frame);
    // 현재 페이지를 구합니다. floor는 소수점 자리를 버리는 함수입니다
    NSUInteger page = floor((_scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
    // 현재 페이지를 계산된 페이지로 설정해줍니다.
    _pageControl.currentPage = page;

    // 보여줄 페이지들을 미리 로드합니다.
    [self loadScrollViewDataSourceWithPage:page - 1];
    [self loadScrollViewDataSourceWithPage:page];
    [self loadScrollViewDataSourceWithPage:page + 1];
}

 

 Delegate 함수의 구현이 끝나면 스크롤 뷰의 스크롤이 아닌 방법으로(버튼 동작,페이지 컨트롤 조작)현재 페이지를 변경하는 함수도 구현해주도록하자 gotoPage:AtPage:함수는 함수에서 전달받은 페이지로 페이지를 이동시켜주는 함수로, 애니메이션의 여부를 결정하는 bool값을 별도로 입력받게된다. 이 함수와 이것을 사용하는 IBAction함수는 아래와 같다. 

- (void)gotoPage:(BOOL)animated AtPage:(NSInteger)page{
    // 스크롤을 임의로 원하는 페이지로 이동시킵니다.
    
    // 페이지 컨트롤의 현재 페이지를 넘겨받은 페이지로 설정합니다.
    [_pageControl setCurrentPage:page];
    
    // 미리 뷰를 로드합니다.
    [self loadScrollViewDataSourceWithPage:page - 1];
    [self loadScrollViewDataSourceWithPage:page];
    [self loadScrollViewDataSourceWithPage:page + 1];
    
    // 보여줄 스크롤뷰의 ContentOffset을 설정합니다.
    // 스크롤 뷰의 Width * Page입니다.
    CGRect bounds = _scrollView.bounds;
    bounds.origin.x = CGRectGetWidth(bounds) * page;
    bounds.origin.y = 0;
    
    // 정해진 부분으로 스크롤뷰를 스크롤합니다.
    [_scrollView scrollRectToVisible:bounds animated:animated];
    
}

#pragma mark - IBACTION

-(IBAction)btn_ScrollMove:(id)sender{
    // 버튼을 누르거나, 페이지컨트롤의 값이 변경될 경우 스크롤을 이동시킵니다.
    
    // 버튼은 Tag로 구분하며, pageControl은 태그를 설정해두지않았습니다.
    if([sender tag] == 0){
        if(_pageControl.currentPage < 3){
            [self gotoPage:YES AtPage:_pageControl.currentPage + 1];
        }
        else{
            [self alertViewShowsWithMessage:@"마지막 페이지입니다"];
        }
    }
    else if([sender tag] == 1){
        if(_pageControl.currentPage > 0){
            [self gotoPage:YES AtPage:_pageControl.currentPage - 1];
        }
        else{
            [self alertViewShowsWithMessage:@"첫 페이지입니다"];
        }
    }
    else{
        [self gotoPage:YES AtPage:_pageControl.currentPage];
    }
}

 위의 함수들을 모두 구현하고 나면 최초의 View가 로드되었을때 아래와 같이 스크롤뷰와 페이지 컨트롤러등을 초기화하기 위한 initScrollViewAndPageControl함수를 실행해주고 , 각 버튼들에 Tag를 세팅해주어야한다. 

 만약 페이징을 구현할때, 최초에 보여주어야하는 페이지가 0페이지가 아니라 다른 임의의 페이지가 되어야한다면 위에서 구현한 gotoPage:AtPage함수를 아래 소스의 주석과 같이 실행해주면된다.

- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view.
    
    // 버튼을 구분하기 위해 버튼에 tag를 설정해줍니다.
    [_btn_Prev setTag:1];
    [_btn_Next setTag:0];
    
    // 스크롤 뷰와 페이지 컨트롤들을 초기화해줍니다.
    [self initScrollViewAndPageControl];
    
    // 만약 최초에 보여줄 페이지를 변경하려면 아래줄의 주석을 풀어주면 됩니다.
    // Page는 원하는 페이지로 설정할 수 있습니다.
    // [self gotoPage:FALSE AtPage:2];
}


#PageViewController.h

 PageScrollViewcController의 구현이 모두 끝났다면, 사실상 PageViewController에서 구현할 것은 그렇게 많지않다. 물론 예제이기때문이겠지만.. 해당 파일의 내용은 아래의 소스와 같다. 여기서 굳이 중요한 부분을 꼽자면 initPageViewInfo가 그나마 중요하다고 할 수있겠다

 이 함수는 현재 페이지를 입력받고 보여줄 Text와 이미지를 초기화한다. 페이지를 구성하는 내용을 초기화하는 함수인셈.

@interface PageViewController : UIViewController

@property (strong,nonatomic)IBOutlet UIImageView *imageView;
@property (strong,nonatomic)IBOutlet UILabel *titleLabel;

-(void)initPageViewInfo:(NSInteger)page;

@end


#PageViewController.m

 이 파일에서는 각 프로퍼티들을 Synthesize하는 것과 initPageViewInfo함수 밖에 구현해둔 것이 없는데, 소스는 아래와 같다. 이 소스에서 명시해둔 이미지들은 첨부해둔 예제소스 압축파일에 동봉되어있다.

-(void)initPageViewInfo:(NSInteger)page{
    NSString *imgName;

    switch (page) {
        case 0:
            imgName = @"Bicycle.jpg";
            break;
        case 1:
            imgName = @"City.jpg";
            break;
        case 2:
            imgName = @"Surf.jpg";
            break;
        case 3:
            imgName = @"Leaves.jpg";
            break;
        default:
            break;
    }
    
    // 현재 페이지의 이미지와 텍스트 적용
    [_imageView setImage:[UIImage imageNamed:imgName]];
    [_titleLabel setText:[NSString stringWithFormat:@"Page %d",page]];
}

 이렇게 길고긴 코딩이 끝나고나면 우리가 원하는 스크롤뷰를 이용한 페이징 구현이 완성되게된다. 이 방법을 사용하면서 중간중간 찾아본바로는 PageController가 현재 구현한 기능들을 모두 포함하고있는 것같던데.. 정석으로 PageController를 사용해서 구현하는 방법도 나중에 찾아봐야할 것 같다. 급하게 작업을 끝마쳐야 했어서 눈앞에 보이는 예제로 작업했는데 오히려 길을 돌아간것 같은 느낌이.. 

What is TeamFoundationServer?


(이미지 출처 http://www.visualstudioonline.com)


 개발을 진행하다보면 소스 제어의 필요성을 느끼게된다. 단순하게 이야기하면 (우연히!) 막힘없이 코딩이 진행되고 있었다고 하더라도 새로운 도구를 끌어다쓰거나 새로운 시도를해 코드의 진행방향이 확틀어지게된 후 나중에 그것을 되돌리려고 해도 방법이 없게되는 경우라던가 말이다. 그외에 코딩이 어떻게 진행되고 있는지 개괄적인 정보를 가지고 있을 필요도 있고말이다.

 이런 상황을 위해 사용하는 소스 제어 방법으로는 Subversion ,  TeamFoundationServer등 여러가지가 있겠지만 이 글에서는 MS에서 제공하는 TeamFoundationServer(VisualStudioOnline)을 소개한다. 

 


(이미지 출처 http://www.visualstudioonline.com)


 TeamFoundationServer는 기존에 MS의 Visual Studio에서 사용하던 소스 제어 방법이었는데, 최근 기존의 물리적인 서버에 구애받던 방식에서 벗어나 웹에서도 프로젝트의 관리가 가능하도록한 서비스를 새로 출범했다. 현재는 VisualStudioOnline이라는 이름으로 서비스 되고 있다. 굳이 다른것들을 미뤄두고 TeamFoudationServer를 다루는데는 몇가지 이유가 있다. 일단, 이 서비스를 이용하는데는 그저 Microsoft Live계정 하나만있으면 된다. 팀 Repository의 갯수의 제한도 없으며 프로젝트의 개발자는 최대 5명까지 무료, VisualStudio는 물론 Eclipse와 XCode까지 지원한다. 방식은 기존의 Teamfoundation과 Git방식을 지원한다. 자세히 사용해보지는 않았지만 백로그와 버그 추척 기능 등등을 지원한다고.. 게다가 클라우드를 표방하는 다른 SVN(Naver,Google)등과 다르게 프로젝트를 비공개로 유지할 수 있다. 그런데 이게 전부 무료 



How To Use ?


 이 글에서는 iOS개발에 초점을 맞추고 있으므로 XCode5에서의 설정에 무게를 두고 설명을 진행하겠다. 결과만 놓고봤을때 사용방법은 정말 간단하지만 글을 쓰고있는 나도 서투른게 많아서 헤맸던 부분들이 몇개 있어 그것들을 짚어가면서 적어보도록하겠다. 


1. Live 계정 생성 

 당연하게도 Micrsoft에서 제공하는 VisualStudioOnline(이하 VSO)서비스를 이용하려면 MS Live 계정이 필요하다. 라이브 계정을 만드려면 여기로

 

2. VisualStudioOnline 서비스 시작 준비 

 라이브 계정을 생성했다면 VSO서비스를 시작할 수 있다. (링크를 누르면 해당 페이지로 이동하게된다.) 메인 화면에서 이제 우측 상단의 무료로 시작하기 버튼을 누르면 서비스를 시작할 준비를 마치게된다. 



(무료로 시작하기 버튼을 클릭하면 서비스 시작을 위한 절차를 밟게된다)


3. VisualStudioOnline계정 생성

 위의 무료로 시작하기 버튼을 누르면 가입해둔 Live계정을 로그인하라는 창이 출력된다. 로그인을 하고나면 VSO계정 만들기라는 창이 나오는데 입력해야할 정보는 이름, 계정에 할당될 이메일 계정, 국가, 그리고 가장 중요한 계정 URL을 설정하게된다. 이름은 후에 VSO에서 작업을 할때 남겨질 작업자의 이름등으로 사용되고, 계정 URL은 자신의 VSO도메인을 의미한다. 이 URL을 기준으로 작업이 이루어지게된다. 각각 필요한 값을 입력한 후 계정만들기를 누르면 일단 준비는 끝 



(간단한 입력만 마치면 서비스를 사용할 준비가 끝나게된다.)


4. 프로젝트 생성 

 VSO계정을 만들었으면, 이제 직접 사용할 프로젝트를 생성해야한다. (보통 계정을 생성하고 나면 바로 프로젝트를 생성하라는 창이 뜬다.) 이곳에서는 프로젝트의 이름, 프로젝트에 대한 설명, Version Control의 방식, 그리고 Process templete을 설정하게된다. 

 프로젝트의 이름, 프로젝트 설명은 원하는 대로 적으면 될것이고, XCode5에서 사용하기 위해서 VersionControl방식은 Git방식으로 설정하도록하자. 그리고 Process templete은 프로젝트의 개발 방식 템플릿을 정하는 것인데 이것에 따라 TeamFoundation의 소스 제어 방식이 달라지게된다. 이것에 대한 참고는 링크에서 확인하면된다. 정보를 모두 입력했으면 Create Project버튼을 클릭!



(필요한 정보를 입력한 후 Create project 버튼을 누르면 이제 정말 프로젝트가 생성된다. 드디어)


5. Authentication 

 웹에서 프로젝트를 생성하였다면, 이제 빈 프로젝트를 XCode를 이용해 체크아웃 받아야한다. 그 전에 사소한 설정을 하나 해줄 필요가 있는데, 그 사소한 설정이라함은 계정 인증을 이야기한다. 

 방법은 어렵지 않다 우측 상단의 자신이 가입할때 적어둔 이름이 적힌 메뉴가 하나 있는데 그 메뉴를 클릭 후 My Profile항목을 클릭하면 자신의 프로필에 관련된 레이어가 하나 출력되는데, 이곳에서 인증관련 설정을 해주면 되는것이다. 


(Enable alternate credentials 버튼을 누르면 패스워드 등을 설정하는 텍스트박스가 출력된다)


 이곳에서 Enable alternate credentials라는 글자를 눌러주면 유저 이름과 비밀번호를 입력할 수 있는 텍스트박스가 생기는데 이곳에서 설정하는 값은 추후에 XCode에서 접근할 때 사용되는 계정명으로, 나는 Live의 계정 정보와 동일하게 작성하였으나 꼭 같을 필요는 없다. 모두 변경했다면 꼭 Save Change를 눌러줄것 !


6. VisualStudioOnline -> XCode5 : CheckOut 

 인증 문제까지 해결했다면 이제 프로젝트를 XCode로 불러오고 프로젝트를 생성해 체크인하는것만 남았다. 프로젝트를 VSO에서 불러오려면 먼저 XCode를 실행한 후 프로젝트를 생성하는 초기화면에서 "Check out an existing project"항목을 클릭하거나 상태 줄에 위치한 메뉴중 Source Control메뉴에서 CheckOut을 선택해준다. 

 

(Check out an existing project 항목을 선택하면 체크아웃을 위한 창이 출력된다.)


 Check Out이라는 창이 하나 뜰것이다. 그렇다면 이제 여기서 Check Out할 우리의 프로젝트의 Git Repository주소를 넣어주어야 하는데, 그것은 VSO에서 Code 메뉴에 가면 확인할 수 있다. Code메뉴에 가보면 "The Repository Empty"라는 메시지와 함께 파란 박스들이 있을텐데, 그 곳에서 주소를 긁어오면 된다. Clone the Empty repository 항목의 From the Command Line항목을 참고하면된다. 


(붉게 표시된 부분에 있는 회색 주소를 긁어오면된다)


  긁어온 주소를 'Or enter a repository location'이라는 항목 아래의 텍스트 박스에 삽입후 Next버튼을 누르면 일련의 처리가 이루어지다가 User Name과 Password를 입력하라는 창이 뜨게된다. 이때, 전단계에서 설정했던 인증 정보를 입력해주면된다 (Live계정과 다른경우 인증정보와 같은 값을 넣어주어야함)

 혹시 Fatal : .. 어쩌구하면서 access오류가 뜬다면 주소를 정상적으로 복사해서 넣었는지 확인해보는 것이 좋다. 그 외에 인증서 관련 오류(Xcode can't verifty the identify of the "..")가 뜬다면 Show Certificate항목을 선택 후 Trust항목을 클릭해보면 옵션을 선택하는 메뉴가 있는데, 옵션들을 모두 'Always Trust'로 변경해주고 저장해주면 해결할 수 있다. 

 이런 일련의 과정을 거치고나면, 체크아웃한 파일을 어디에 저장할 것인지 물어보는데, 각자 원하는 곳을 선택해 빈 프로젝트 폴더를 CheckOut받도록하자. 빈 폴더를 체크아웃받아서 뭐 어쩌자는 것인지 싶겠지만 그것은 다음단계에서 확인할 수 있다. [각주:1]


7. XCode5 -> VisualStudioOnline : Commit 

 Empty Repository를 CheckOut받았다면 이제 VSO로 각자가 만든 프로젝트를 실제로 업로드할때다. 먼저,   XCode 5를 실행한 후 프로젝트를 생성하자 이때 생성하는 위치를 조금 다르게 설정해주어야 한다. 

 생성하고 싶은 템플릿을 설정하고 프로젝트가 저장될 위치를 선택할때 프로젝트가 저장될 위치를 아까 Check Out받은 빈 폴더로 지정하도록한다. 이 폴더는 Repository 정보를 포함하는 폴더로 , 눈썰미 있는 사람들은 알아챘겠지만 해당 폴더에 저장할때는 프로젝트 생성창 하단의 Create At Repository 체크박스가 비활성화되게 된다. 여튼 이곳에 프로젝트를 생성한다. 그후 XCode5의 메뉴중 Source Control - Commit 항목을 클릭해준다. 그렇게하면 아래와 같은 화면이 뜬다. 


(커밋하는 화면 해당 커밋에 대한 설명을 넣어주어야 진행된다)


 커밋을 하기전에 해당 커밋에 대한 설명을 작성해주고 Commits ** Files 버튼을 눌러주면 자동으로 Commit이 진행된다. 이 최초의 커밋은 커밋 후 직전에 바로 웹에서는 확인되지 않고 일정 시간이 지난 후 웹에서도 확인할 수 있다. 문제가 있는 것은 아니다. 

 이렇게 최초 업로드를 한 후 프로젝트를 진행하며 중간중간 소스들을 커밋하면 프로젝트의 진행 상황 및 버전 컨트롤을 VSO웹에서 할 수 있게된다. VSO자체의 자세한 기능들은 추후에 기회가 되면 다루도록하겠다. 



참고 문헌

http://www.visualstudio.com/get-started/

  1. 참고 페이지 : http://stackoverflow.com/questions/12099727/xcode-cant-verify-the-identity-of-the-server-github-com-xcode [본문으로]

 앱을 만들때 데이터를 처리해야하는 경우가 생길 수 밖에없다. 보통은 통신을 하거나, 주소록을 긁어오거나 하는 등의 작업이 그것인데 무작정 해당 기능을 구현하다보면 화면이 프리징 되어버리는 문제에 부딪히게 된다.

 예컨데 특정 데이터를 연속적으로 읽어오는데 동시에 Progressbar를 증가시키려고 한다면 


데이터 처리 -> Progress 값 증가 -> 데이터 처리 -> Progress 값 증가  

 

 위와 같이 의도한대로 작동하는게 아니라 데이터처리가 끝날때까지 Progressbar는 멈춰있다가 처리가 끝날때 한번에 애니메이션이 발생한다. 즉, 값은 전달되지만 화면처리가 정상적으로 이뤄지지 않는것이다. 이런 경우 스레드를 분류해줄 필요가 생긴다. 개념적으로는 아래와 같다 



-(void)viewDidLoad{
    // 화면이 불러왔을때 바로 실행하는 경우 
    [self performSelectorInBackground:@selector(dataReceive) withObject:nil];
}

-(void)dataReceive{
    // 데이터 처리 

    // 값으로 넘겨줄 NSNumber객체 생성 
    NSNumber *obj = [NSNumber numberWithFloat:0.1];
    [self performSelectorOnMainThread:@selector(increaseProgress:) withObject:obj waitUntilDone:TRUE];
    NSLog(@"데이터 처리 at %@",[NSThread CurrentThread]);
}

-(void)increaseProgress:(NSNumber *)progVal{
    // NSProgressBar타입의 prog라는 객체가 있다고 가정 
    [[self prog] setProgress:[progVal floatValue] animated:TRUE];
    NSLog(@"화면처리 at %@",[NSThread CurrentThread]);
}

 위와같이 처리해주면, 화면처리와 데이터 처리를 병렬적으로 보이게 처리할 수 있다. 이때 기본적으로 performSelectorOnMainThread등의 셀렉터를 이용한 함수들은 리턴값을 가지지 않아 함수에서 반환값을 뽑아오지는 못한다. 아마 많은 사람들이 BOOL값 정도는 받아와서 해당 작업이 완료되었는지 체크하려고 할 터이다.

 그럴때는 return값을 받아올 객체의 주소값을 넘기면 변경된 데이터를 가지고 올 수 있긴하나, 그 방법은 좀 더 복잡하니 후에 다루도록 하겠다. 

iOS에서 통신을 하는 방법은 몇가지가 있는데 가장 간편하게 사용되는 방법은 NSURLConnection을 이용해 동기적으로 통신하는 것이다. 가장 심플하다 


NSURLConnection Delegate를 이용하면 다양한 방법으로 통신을 컨트롤할 수 있긴하나 , 일단 내가 최근에 사용했던 가장 가벼운 방법부터 정리해본다. 기본적으로 POST방식으로 보내는 방법을 기준으로 설명한다.


// NSURLRequest 객체 생성 
// 통신을 Request(요청)하는 객체를 만든다
NSMutableURLRequest* request = [[NSMutableURLRequest alloc]init];

// POST 내용을 작성 
NSString *post = [NSString stringWithFormat:@"Post로 보낼내용"];

NSData *postData = [post dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
NSString *postLength = [NSString stringWithFormat:@"%d", (int)[postData length]];

// Request 객체에 들어갈 내용들 설정 
[request setURL:[NSURL URLWithString:@"http://www.urURL.co.kr"]];
[request setHTTPMethod:@"POST"];
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[request setValue:@"Mozilla/4.0 (compatible;)" forHTTPHeaderField:@"User-Agent"];
[request setHTTPBody:postData];
[request setTimeoutInterval:30.0];

// 커넥션 에러를 다룰 객체를 생성
NSError *error = nil;
// NSURLConnection 객체를 이용해 동기적으로 보냄 (Response객체는 사용하지 않음)
// 돌아온 값은 NSData형으로 받는다 
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:Nil error:&error];
if(data == NULL){
    // 통신 실패 ! 
    NSLog(@"통신 실패 ! : %@",[error LocalizedDescription]);
}
else{
    // 통신 성공 
    // 받아온 정보가 스트링인 경우 
    NSString *returnStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"Return String : %@",returnStr);
}

UIAlertView는 간단한 앱을 만드는 경우 굉장히 자주 사용하는(?)요소 중 하나라고 생각하는데, 생각보다 UIAlertView를 한번 사용하려면 입력할 정보가 굉장히 많다.


게다가 UIAlertView자체를 구분해야되거나 버튼마다 다른 기능을 집어넣어줘야하는 경우, 더 복잡해지게된다. 

그럴땐 클래스를 따로 빼버리고 아래와 같은 처리를 해서 쉽게 만들어 쓰자 


   
-(void)alertView_Set:(NSString *)mesg withTaget:(id)delegate andTag:(int)tag withExtraButton:(NSArray *)buttons{
    UIAlertView *artView = [[UIAlertView alloc]
                            initWithTitle:@"타이틀" message:mesg delegate:delegate cancelButtonTitle:@"확인" otherButtonTitles:nil];
    
    // 입력받은 값을 Message , Delegate에 넣어준다. 
    // 해당 함수를 현재 AlertView를 띄울 뷰가 아닌 별도의 클래스로 분리할 경우 
    // Delegate를 넘겨주어야 한다. 

    // 추가 버튼이 필요하다면 버튼을 추가한다
    for(int idx = 0 ; idx < (int)[buttons count] ; idx++){
        [artView addButtonWithTitle:[buttons objectAtIndex:idx]];
    }
    
    // 태그를 주어 태그를 입력한다.
    [artView setTag:tag];
    [artView show];
}


위의 방법으로 별도로 분리해서 사용하면 한번에 tag, delegate 버튼추가를 해줄 수 있다. 태그 구분 및 버튼 구분은 해당 뷰컨트롤러의 헤더 파일에 UIAlertViewDelegate를 추가해준 후 아래와 같은 코드를 삽입하면된다.


-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    // Delegate 내장 함수 
    if([alertView tag] == 0){
        // AlertView의 태그가 0인 경우 
        if(buttonIndex == 0){
             // AlertView에서 첫번째 버튼을 누른경우
        }
        else{
             // 나머지 버튼을 누른경우 
        }
    }
}

개발을 하다보면 간단한 로그를 남겨두어야할 상황이 생긴다. 단순히 정보를 저장하는 방법에는 plist나 NSUserDefault같은 것을 사용할 수 있지만 오류 로그등을 남겨둘때는 파일로 저장해두는 것이 관리도 편한것같다 .


파일을 작성하고 읽어오는데는 주로 NSFileManager를 활용하게 되는데 기본은 아래와 같다. 


// 경로를 읽어온다
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentationDirectory, NSUserDomainMask, YES);

NSString *documentDir = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"LogDirectory"];

// 파일명을 포함한 경로를 설정
NSString *filePathAndName = [documentDir stringByAppendingPathComponent:@"Log.txt"];

// FileManager를 생성
NSFileManager *fileManager = [NSFileManager defaultManager];

파일을 읽고쓰기 위한 초기화는 위와 같다. 하지만 fileManager나 NSString을 이용해 파일을 읽고 쓰기 전에, Cocoa Error같은것들을 피하기 위해서는 몇가지 처리를 해두는것이 좋다. 아래처럼




// 1. 파일을 생성할 Directory가 있는지 확인
NSError *err = NULL;
if(![fileManager isWritableFileAtPath:documentDir]){
// Directory가 없는 경우 생성해준다
     if([fileManager createDirectoryAtPath:documentDir withIntermediateDirectories:YES attributes:nil error:&err]){
         // Directory가 생성된 경우 
         NSLog(@"Log Directory Create!");
     }
else{
    // Directory 생성에 실패한 경우 
    NSLog(@"Directory Create Fails ! : %@",[err LocalizedDescription]);
    }
}

// 2. 기존에 생성할 파일이 존재하는지 확인 
if([fileManager fileExistsAtPath:logPath]){
    // 파일이 존재하는 경우 

    // 기존에 있던 파일의 내용을 불러와 뒤에 덧붙임
    NSString *logStr = [NSString StringWithFormat:@"로그 내용"];
    NSMutableString *fileStr = [NSMutableString stringWithContentsOfFile:logPath encoding:NSUTF8StringEncoding error:nil];
    // 기존 파일에서 글 내용을 읽어옴
    // 읽어올때 에러처리 생략 !

    [fileStr appendFormat:@"\n%@",logStr];
        
    if([recvErrStr writeToFile:logPath atomically:FALSE encoding:NSUTF8StringEncoding error:&err]){
        NSLog(@"Log Create!1");
    }
    else{
        NSLog(@"Log Create Err1 : %@",err);
    }
}
else{
    // 파일이 존재하지 않는 경우 
    
        NSString *logStr = [NSString stringWithFormat:@"로그 내용"];
        if([errStr writeToFile:logPath atomically:false encoding:NSUTF8StringEncoding error:&err]){
            NSLog(@"Log Create!2");
        }
        else{
            NSLog(@"Log Create Err2 : %@",err);
        }
}

위와 같은 처리를 해두면 Cocoa Eror 4. 혹은 516에러를 막을 수있다

Cocoa Error 4.는 보통 접근권한을 얻지 못했거나 디렉토리 혹은 파일이 존재하지 않을때 나타나는 오류이다

처음 NSFileManager를 사용하는 사람이라면 보통 디렉토리가 생성되어있지 않아서 발생하는 경우가 대부분이다. 그런 경우엔

위 처럼 접근권한을 얻지 못했을때 디렉토리를 만들도록 처리해두면 쉽게 해결할 수 있다. Cocoa Error 516은 사실 위의 코딩과는 상관없는 부분으로, 경로가 잘못되었을때 나타난다.


516에러가 나타난다면 경로를 제대로 작성했는지 꼭 확인해볼것 ! 나는 작업중에 작성할 스트링 내용과 주소를 바꿔쓰는 실수를 했었다..


파일을 생성한 후에 위에 사용한 fileExistsAtPath를 활용하면, 파일이 있는지 확인한 후 파일이 있으면 로그를 전송하는 식으로 사용할 수 있다.

+ Recent posts