본문 바로가기
기타 개발지식

go-mobile로 go로 작성한 코드 iOS 라이브러리로 빌드 후 웹뷰 구성

by 찐코딩 2024. 8. 1.

f-did library go ver. → go mobile iOS 빌드

📌 사전설치(Mac OS)

 

 

 
Xcode
 
 
Xcode command line tool
 
 
go 1.21.0
 
 
 
 

 

gomobile 설치하기

 
go.mod가 포함된 디렉토리에서 아래 명령어 입력
 
Bash
복사
캡션
 
$ go install golang.org/x/mobile/cmd/gomobile@latest $ gomobile init # 설치 확인 $ gomobile version
 
 
💡
(24.07.02) 현재 gomobile version 명령어를 실행하면
gomobile version unknown: binary is out of date, re-install it
 
라는 에러 메시지가 나타나는데 현재 무시하고 진행하여도 빌드하는 데에 문제 없는 것으로 보임
 
 

ios로 빌드

 
💡
현재 git clone 시 디렉토리명이 fcrypto 로 설정되어 있으므로 didLibrary로 변경하여 진행하여야 함
 
 
Bash
복사
캡션
 
gomobile bind -target=ios -o didLibrary.xcframework didLibrary/didmanager didLibrary/keymanager didLibrary/storagemanager didLibrary/vcmanager
 
 

webview 구현을 위한 라이브러리 추가

https://velog.io/@dbqls200/iOS-SwiftUI-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-WebView-%EB%9D%84%EC%9A%B0%EA%B8%B0

 

[iOS] SwiftUI 사용하여 WebView 띄우기

SwiftUI로 WebView 어떻게 띄우지?

velog.io

 

 

SwiftUI에서 WebView를 사용해보자

현재 많은 앱 서비스들은 웹 뷰를 사용하고 있다. 그 이유에 대해서는 상당히 다양할 것이다. 업데이트가 너무 빈번해서 앱 스토어를 통해서 업데이트하는 것보다 웹을 업데이트하면 간편하게

velog.io

https://phillip5094.tistory.com/133

 

SwiftUI에서 WKWebView <-> JavaScript 상호작용

안녕하세요. 이번에는 SwiftUI 환경에서 WKWebView가 JavaScript의 함수를 호출하고, JavaScript가 WKWebView의 함수를 호출하는 방법에 대해 알아볼게요. iOS 전체 코드: https://github.com/phillip5094/SwiftUI-WebView-JS J

phillip5094.tistory.com

 

💡
웹뷰를 구현하는 방법으로는 3가지가 있는데 각 방법은 위 블로그에서 확인
 

 

 

빌드된 라이브러리를 추가하고, webview 구현을 위해 Webkit.framework 라이브러리를 추가한다.

 

 

웹뷰 구성

 

// Global.swift
	
class Global {
    static let shared = Global()
    let dbPath: String

    private init() {
        let fileManager = FileManager.default
        let urls = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
        dbPath = urls[0].appendingPathComponent("secureData").path

        // 폴더가 존재하지 않으면 생성
        if !fileManager.fileExists(atPath: dbPath) {
            do {
                try fileManager.createDirectory(atPath: dbPath, withIntermediateDirectories: true, attributes: nil)
            } catch {
                print("Error creating directory: \(error)")
            }
        }
    }
}
 
 
💡
key, did, vc 등을 저장하기 위해 데이터베이스 경로 설정
 

 

// WebView.swift

import SwiftUI
import WebKit
import DidLibrary

// WebViewMananger 싱글톤 클래스 정의
class WebViewManager {
    static let shared = WebViewManager()
    var webView: WKWebView
    
    private init() {
        let config = WKWebViewConfiguration()
        let contentController = WKUserContentController()
        
        // 자바스크립트 -> swift 넘겨오는 함수명과 일치해야 함
        contentController.add(ContentController(), name: "createKey")
        
        config.userContentController = contentController
        
        // webView 객체 초기화
        self.webView = WKWebView(frame: .zero, configuration: config)
        // 웹뷰에서 자바스크립트 설정 허용 
        self.webView.configuration.preferences.javaScriptEnabled = true
        
        // WKWebView에서 발생하는 JavaScript 오류와 로그 메시지를 iOS 애플리케이션으로 전달하기 위한 설정
        let scriptSource = """
        window.onerror = function(message, source, lineno, colno, error) {
            window.webkit.messageHandlers.logHandler.postMessage({
                message: message,
                source: source,
                lineno: lineno,
                colno: colno,
                error: error
            });
        };
        console.log = function(message) {
            window.webkit.messageHandlers.logHandler.postMessage({
                message: message
            });
        };
        console.error = console.log;
        console.debug = console.log;
        console.warn = console.log;
        """
        let script = WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
        contentController.addUserScript(script)
        contentController.add(LogHandler(), name: "logHandler")
    }
}

// 로그 핸들러: 웹에서 에러 메시지를 로그로 찍기 위한 핸들러
class LogHandler: NSObject, WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if let log = message.body as? [String: Any] {
            if let message = log["message"] as? String {
                print("JavaScript log: \(message)")
            }
            if let source = log["source"] as? String,
               let lineno = log["lineno"] as? Int,
               let colno = log["colno"] as? Int,
               let error = log["error"] as? String {
                print("JavaScript error: \(message) at \(source):\(lineno):\(colno), error: \(error)")
            }
        }
    }
}

class ContentController: NSObject, WKScriptMessageHandler {
		// 데이터베이스 경로
    private var dbPath = Global.shared.dbPath
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let body = message.body as? [String: Any] else {
            print("Invalid message format")
            return
        }
        
        var error: NSError?
        var result: Any?

        switch message.name {
        case "createKey":
            if let algoType = body["algoType"] as? String,
               let keyName = body["keyName"] as? String,
               let keyPass = body["keyPass"] as? String,
               let keyType = body["keyType"] as? String {
                result = KeymanagerCreateKey(self.dbPath, algoType, keyPass, keyName, keyType, &error)
                print(result as Any)
                let sanitizedResult = (result as? String)?.replacingOccurrences(of: "'", with: "\\'") ?? ""
                WebViewManager.shared.webView.evaluateJavaScript("document.getElementById('createKeyResult').innerText = `Result: \(sanitizedResult)`;") { result, error in
                    if let error = error {
                        print("A JavaScript exception occurred: \(error.localizedDescription)")
                    }
                }
            }
            
         default:
            print("Unhandled message: \(message.name)")
        }
    }
}

// SwiftUI WebView 구조체 정의
struct WebView: UIViewRepresentable {
    let request: URLRequest
    
    init(request: URLRequest) {
        self.request = request
    }
    
    func makeUIView(context: Context) -> WKWebView {
        let webView = WebViewManager.shared.webView
        webView.load(request)
        return webView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.load(request)
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject {
        let parent: WebView
        
        init(parent: WebView) {
            self.parent = parent
        }
    }
}
 
 
 
Coordinator는 SwiftUI와 UIKit 간의 상호작용을 관리하는 객체
 
 
 
makeCoordinator 함수는 Coordinator 객체를 생성
 
 
 
Coordinator 클래스는 부모 WebView 구조체에 대한 참조를 유지
 
 
 
ContentController 클래스는  WKScriptMessageHandler 프로토콜을 준수하여 WKWebView에서 실행되는 JavaScript로부터 메시지를 수신
 
 
 
userContentController 메서드는 자바스크립트에서 메시지를 수신했을 때 호출되는 메서드
 
 
 
WebView 구조체는 UIViewRepresentable 프로토콜을 준수하여 SwiftUI에서 WKWebView를 사용할 수 있게 함
 
 

외부 라이브러리 사용

 
여기서는 didmanager, keymanager, storagemanager, vcmanager 를 사용하고 있는데
 
스위프트 코드 내에서 사용할 시에는 명명 규칙 ⇒ (라이브러리명 대문자)+(함수명 카멜케이스)로 명명하여 사용하면 된다.
 

예시

let result = VcmanagerVPdefinitionVerifiy(verifyPublicKey, signature, vpDefinition, &error)
 
 
 

SPA으로 구현한 웹에서는 네이티브 → 웹뷰 통신이 어려워요🥲 그렇다면 어떻게?

 

 

 

네이티브 →

WebViewManager.shared.webView.evaluateJavaScript("globalFunction('getCreateKeyResult', `\(sanitizedResult)`)")

 

네이티브에서는 WebViewManager.shared.webView.evaluateJavaScript(자바스크립트에서 호출할 함수명)을 사용하여 자바스크립트 함수를 호출하는데

 
SPA 기반의 웹페이지에서는 해당 페이지 내에서 정의한 함수 호출은 불가능하다
 
바로 전역 함수가 아니기 때문.
 

이를 위해 전역 변수를 등록하고 해당 페이지가 실행될 때 (onMounted) 전역 함수 이벤트리스너를 등록하고 컴포넌트 인스턴스가 해제되기 전에 이벤트리스너를 제거하면 된다.

 

// app.vue

<script setup lang="ts>
// ... 생략 ...

// 전역 함수 정의
const globalFunction = (method: string, arg?: any) => {
  const event = new CustomEvent('global-function-called', { detail: { method: method, arg: arg } });
  window.dispatchEvent(event);
};

// 전역 변수로 제공
nuxtApp.provide('globalFunction', globalFunction);

// 전역 함수로 등록
if (typeof window !== 'undefined') {
  window.globalFunction = globalFunction;
}
</script>
 
 
 
 
//ios-test.vue

<script setup lang="ts>

// ...생략...

const callbackFunctions: Record<string, Function> = {
  getCreateKeyResult: (result: string) => {
    createKeyResult.value = JSON.parse(result) as IResult<CreateKeyResult>
  },
  getDeleteKeyResult: (result: string) => {
    deleteKeyResult.value = JSON.parse(result) as IResult<boolean>
  },
  getChangeVcStatResult: (result: string) => {
    getChangeVcStatResult.value = JSON.parse(result) as IResult<null>
  } ...
}

/**
 * ios -> javascript 호출을 감지하는 이벤트 핸들러
 *
 *
 * 컴포넌트 및 페이지 mounted될 때 window.addEventLister('global-function-called', (event: Event) => handleGlobalFuncionCalled(event as CustomEvent))
 * 컴포넌트 인스턴스가 마운트 해제되기 직전에 해당 이벤트 리스너 해제
 *
 */
function handleGlobalFuncionCalled(event: CustomEvent) {
  callbackFunctions[event.detail.method](event.detail.arg)
}

onMounted(() => {
  console.log("agent:::" + didLibrary.agent)
  // 전역 이벤트 리스너 등록
  window.addEventListener('global-function-called', (event: Event) => handleGlobalFuncionCalled(event as CustomEvent))
  onBeforeUnmount(() => {
    // 전역 이벤트 리스너 해제
    window.removeEventListener('global-function-called', (event: Event) => handleGlobalFuncionCalled(event as CustomEvent))
  });
})

</script>
 

댓글