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



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에 해당 스크립트를

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



+ Recent posts