Swift(iOS)/Information

iOS ↔ Typescript 함수 공유 방법 (1)

y0ngha 2021. 10. 27. 00:27

Web에서 Native Application의 Function을 호출하고 싶을 때 사용하는 방법이다.

(예를 들어 Front에서 아이디저장, 자동로그인을 Checked했을 때 Native Application에 사용자 계정 정보를 Preferences에 담고 있어야 이 후에 앱이 다시 실행되면 아이디를 불러온다던지, 자동로그인을 할 수 있기 때문에.

사용자 계정 정보는 Web의 Session이나 Local Storage 등에 저장을 하게 되면 노출의 위험이 있을 뿐더러, 사용자가 캐시, 세션에 대한 데이터를 초기화를 진행했을 때 정보가 다 사라지기 때문에... 그리고 기간이 유효하니 Native Application에 저장하는 방식을 채택했다.)

 

먼저 Typescript에서는 아래와 같은 class를 하나 생성해주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare global {
  interface Window {
    webkit: any;
  }
}
 
declare global {
  interface Native {
  }
}
 
 
export class NativeService implements Native {
 
}
cs

global 안에 interface가 2개 따져있는데, 하나는 Window, 하나는 Native에 대한 Interface다.

Window는 뭐냐면, Android나 iOS에서 Webview를 통해 함수를 주입하게 될 경우 'window' Object 안에 들어가게 된다.

(Android : window.(Android Native에서 주입할 때 정의한 Key).(함수명))

(iOS : window.webkit.messageHandlers.(iOS Native에서 사용하는 Key).(함수명))

 

iOS에 대해서 작성하고 있으니, Window라는 interface 안에다가 webkit을 넣어주자.

그래야지 window.webkit으로 사용이 가능하다.

 

그 다음 Native Interface는 Native Application에서 정의한 함수의 원형과 동일하게 작성해준다.

(이 Interface를 NativeService에서 사용할 것이니 자신이 생각하는 함수의 원형을 적고, 서비스에다가는 그 함수를 구현해주면 된다.)

아래는 위에 기술한 바와 같이 아이디 비밀번호 저장을 위해 iOS Preference에 저장하는 로직이다.

 

NativeService.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
declare global {
  interface Window {
    webkit: any;
  }
}
 
declare global {
  interface Native {
    setPref: (key: string, data: string=> Promise<void>;
  }
}
 
 
export class NativeService implements Native {
    async setPref(key: string, data: string, isArray: boolean = false): Promise<void> {
         await window.webkit.messageHandlers.native.postMessage(
          {
            'funcName''setPref',
            'key': key,
            'data': data
          });   
    }
}
cs

ViewController.swift

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
@available(iOS 8.0*)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){
    if message.name == "native" {
        if let dictionary: [StringString= message.body as? Dictionary {
            if let funcName = dictionary["funcName"] {
                switch funcName {
                    case "setPref":
                        if let _key = dictionary["key"] {
                            if _key == "" {
                                let alert = UIAlertController(title: "알림", message: "[오류] 웹 오류 발생\nKEY가 빈값입니다..", preferredStyle: UIAlertController.Style.alert)
                                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction!in
                                }))
                                self.present(alert, animated: true, completion: nil)
                            } else {
                                if let _data = dictionary["data"] {
                                    self.preferences.setValue(_data, forKey: _key)
                                } else {
                                    let alert = UIAlertController(title: "알림", message: "[오류] 웹 오류 발생\n설정값을 찾을 수 없습니다.", preferredStyle: UIAlertController.Style.alert)
                                    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction!in
                                    }))
                                    self.present(alert, animated: true, completion: nil)
                                }
                            }
                        } else {
                            let alert = UIAlertController(title: "알림", message: "[오류] 웹 오류 발생\nKEY를 찾을 수 없습니다.", preferredStyle: UIAlertController.Style.alert)
                            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction!in
                            }))
                            self.present(alert, animated: true, completion: nil)
                        }
                        break;
                    default:
                        let alert = UIAlertController(title: "알림", message: "[오류] 웹 오류 발생\n정의되지 않은 funcName 입니다.\n[\(funcName)]", preferredStyle: UIAlertController.Style.alert)
                        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction!in
                        }))
                        self.present(alert, animated: true, completion: nil)
                        print("undefined functionName")
                }
            } else {
                let alert = UIAlertController(title: "알림", message: "[오류] 웹 오류 발생\nfuncName을 찾을 수 없습니다.", preferredStyle: UIAlertController.Style.alert)
                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction!in
                }))
                self.present(alert, animated: true, completion: nil)
                print("funcName is Not Found!")
            }
        }
    }
}
cs

먼저, TypeScript부터 해석을 해보자면 16번째 줄부터 해석을 진행하면 된다.

window.webkit.messageHandlers.native.postMessage를 사용하게 될 경우에 Swift의 userContentController 딴으로 넘길 수 있다.

 

여기서 native 란, Swift 코드상의 message.name에 속하는 부분이다.

messageHandlers method를 사용하게 될 경우 뒤에 붙은 메소드가 'message.name' 이 되는거고, postMessage를 이용하게 되면 postMessage 함수 내부에 있는 데이터가 'message.body' 가 되는 것 이다.

 

현재 위에 기술되어있는 소스를 보면 window.webkit.messageHandlers.native.postMessage를 통해 JSON 형식으로 Body를 작성하여 Request 하였다.

 

Swift에서는 이를 받고 name이 사전에 정의한 'native' 가 맞을 때 처리를 진행하며, *Optional Bidning을 사용하여 message의 body를 *Dictionary 로 형변환을 진행했을 때 'Key: Value' 형식이 'String: String' 이 정상적으로 처리되었을 때 진행한다는 내용이다.

(Swift 소스 Line No 3, 4)

(*Optional Binding ? → If let 구문은 어떻게 동작할까?)

(*Dictionary ? → [Key: Value] 형식의 Collection)

 

정상적으로 Dictionary로 형변환을 진행하였으면, 이제 Key를 줘서 Value를 가져올 수 있는데 Key는 사전에 Web Application에서 정의한 것을 그대로 사용하면 된다.

 

위 함수가 작동되면, self.preferences(iOS Application의 저장공간)에 data가 저장되는 코드다.

 

지금 위 예제는 리턴 값이 없을때의 예제이고, 리턴값을 있게 하고 싶을 경우에는 후술하도록 하겠다.