## Introduction
This guide assumes a high level of familiarity with XCode and Swift/SwiftUI and guides you through creating your own iOS application that displays an avatar. You can skip ahead to the [example app](#example-app) section to see all of the code in one place.

Begin by creating an iOS project with XCode. We recommend choosing SwiftUI and using `WKWebView` (rather than UIKit / `UIWebView`).

### Creating a WebView
Lets set up our webview:
```swift
// Webview.swift
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    lazy private var coordinator: Coordinator = Coordinator(self)
    private var webView: WKWebView

    init() {
        self.webView = WebView.createWebView(url: URL(string: "https://trulience.com/avatar/<your-avatar-id>")!)

        // register any SDK events you want to listen for
        webView.configuration.userContentController.add(coordinator, name: "trlChat")
        // ...
    }

    static func createWebView(url: URL) -> WKWebView {
        let configuration = WKWebViewConfiguration()
               
        // 
        configuration.allowsInlineMediaPlayback = true
        configuration.suppressesIncrementalRendering = true
        configuration.defaultWebpagePreferences.allowsContentJavaScript = true
        
        let pref = WKWebpagePreferences.init()
        pref.allowsContentJavaScript = true
        pref.preferredContentMode = .mobile
        configuration.defaultWebpagePreferences = pref
        let webView = WKWebView(frame: .zero, configuration: configuration)
        webView.allowsBackForwardNavigationGestures = true
        webView.load(URLRequest(url: url))
        webView.isOpaque = false // prevent an 'unstyled flash' of white
        
        // optional: AVAudioSession settings
        do {
            try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: [.defaultToSpeaker, .allowBluetooth])
            try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
        } catch {
            print("Failed to set audio session category.")
        }
        
        // optional: script to prevent zooming on the webview page
        let disableZoomScriptSrc: String = "var meta = document.createElement('meta');" +
            "meta.name = 'viewport';" +
            "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" +
            "var head = document.getElementsByTagName('head')[0];" +
            "head.appendChild(meta);"
        let disableZoomScript: WKUserScript = WKUserScript(source: disableZoomScriptSrc, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        webView.configuration.userContentController.addUserScript(disableZoomScript)

        
        return webView
    }

    func makeUIView(context: Context) -> WKWebView {
        webView.scrollView.contentInsetAdjustmentBehavior = .never
        webView.uiDelegate = context.coordinator
        webView.navigationDelegate = context.coordinator
        return webView
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
}
```

You'll also want to create the `Coordinator` class mentioned throughout the code above:
```swift
// Webview.swift
class Coordinator: NSObject, WKScriptMessageHandler, WKUIDelegate, WKNavigationDelegate {
    // ...
}
```
We'll use this class [later](#communicating-between-native-and-web) to implement native\<=\>web communication.

You should now be able to use your `WebView` in your main `ContentView`.

```swift
// ContentView.swift
struct ContentView: View {
    var body: some View {
        WebView()
        .edgesIgnoringSafeArea([.bottom])
        .background(Color.black) 
    }
}
```

### Microphone Permissions

Ensure your `Info.plist` contains the `Privacy - Microphone Usage Description` key with some string value that describes the reason for the permission request, e.g. "Grant microphone access to speak with the avatar.".

Once the user has granted microphone permissions to your app once, you can prevent the need for repetitive permission popups by implementing this method on your coordinator class:
```swift
// Webview.swift
class Coordinator: NSObject, WKScriptMessageHandler, WKUIDelegate, WKNavigationDelegate {
    // ...
    func webView(_ webView: WKWebView, decideMediaCapturePermissionsFor origin: WKSecurityOrigin,
                 initiatedBy frame: WKFrameInfo, type: WKMediaCaptureType) async -> WKPermissionDecision {
        return .grant
    }
// ...
```

### Debugging


You can use Safari on macOS as a remote debugger to inspect the webview's console. To enable this, you have to set the `isInspectable` flag on your webview.
```swift
if #available(iOS 16.4, *) {
    webView.isInspectable = true
}
```

Additionally, it's possible to intercept `console.log`, `console.debug` and `console.error` calls happening inside of the webview through script injection to print them in the XCode console. Begin by defining a `WebviewLogger` class that reacts to a custom `consoleLog` message:
```swift
class WebViewLogger: NSObject, WKScriptMessageHandler {
    var logs: [String] = []
    
    func log(_ message: String, prefix: String? = "") {
        let today = Date.now
        let formatter3 = DateFormatter()
        formatter3.dateFormat = "HH:mm:ss"
        let timestamp: String = formatter3.string(from: today)
        if let prefix = prefix {
            let loggedMessage: String = timestamp + " :: " + prefix + " :: " + message
            logs.append(loggedMessage)
            print(loggedMessage)
        } else {
            let loggedMessage: String = timestamp + " :: " + message
            logs.append(loggedMessage)
            print(loggedMessage)
        }
    }
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "consoleLog", let messageBody = message.body as? String {
            log(messageBody, prefix: "[WEB]")
        }
    }
}
```

Then instantiate your logger class in your webview class:
```swift
let logger = WebViewLogger()
```

Finally, inject a script into the webview to replace functions like `console.log` with a custom function that dispatches our `consoleLog` message:

```swift
let configuration = WKWebViewConfiguration()

let source = """
var originalConsoleLog = console.log;
console.log = function() {
    var args = Array.from(arguments).map(function(arg) {
        if (typeof arg === 'undefined') {
            return 'undefined';
        } else if (arg === null) {
            return 'null';
        } else if (typeof arg === 'object') {
            try {
                return JSON.stringify(arg);
            } catch (error) {
                return '[Object]';
            }
        } else {
            return arg.toString();
        }
    });
    window.webkit.messageHandlers.consoleLog.postMessage(args.join(' '));
    originalConsoleLog.apply(console, arguments);
};

window.webkit.messageHandlers.consoleLog.postMessage(`ConsoleError: ${args.join(' ')}`);
originalError.apply(console, arguments);
};
"""
let userScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)

configuration.userContentController.addUserScript(userScript)
configuration.userContentController.add(logger, name: "consoleLog")

// create a WKWebView that uses your configuration
let webView = WKWebView(frame: .zero, configuration: configuration)
```

### Communicating between Native and Web
Your application is now split into two halves, the native Swift code that gets executed on your iOS device, and the javascript web code executed inside of the webview. You can send events across the boundary to communicate between the two.

#### Swift to Web
Define a `callJavaScriptFunction` that accepts a function name and optional parameters.
```swift
struct WebView: UIViewRepresentable {
    // ...
    func callJavaScriptFunction(functionName: String, parameter: Any? = nil) {
        var script: String
        
        if let param = parameter {
            if let jsonData = try? JSONSerialization.data(withJSONObject: param, options: .fragmentsAllowed),
               let jsonString = String(data: jsonData, encoding: .utf8) {
                script = "window?.NativeBridge?.\(functionName)(\(jsonString));"
            } else {
                print("Error serializing JSON")
                return
            }
        } else {
            script = "window?.NativeBridge?.\(functionName)();"
        }
        webView.evaluateJavaScript(script) { (result, error) in
            if let error = error {
                print("Error calling JavaScript function: \(error)")
            }
            print("Result \(result ?? "None")")
        }
    }
}
```

Create a function that your native Swift code can invoke:
```swift
func switchFlipped(state: Bool) {
    let arg: [String: Any] = [
        "state": state,
    ]
    callJavaScriptFunction(functionName: "switchFlipped", parameter: arg)
}
```

Finally create a `NativeBridge` class in your web app to handle the `switchFlipped` event:
```javascript
class NativeBridge {
    static INSTANCE = new NativeBridge();

    constructor() {
        return NativeBridge.INSTANCE;
    }

    static getInstance() {
        return NativeBridge.INSTANCE;
    }

    switchFlipped({ state }) {
        // handle switch flip
    }
}

window.NativeBridge = NativeBridge.getInstance();
```

#### Web to Swift
Define a utility function for serialising parameters and passing them to a given function. You can think of this as the web counterpart to the `callJavaScriptFunction` function we defined in Swift earlier.
```javascript
export const IOSNativeHandler = window.webkit?.messageHandlers;

const callNativeIOSFunction = (func, message) => {
  console.log(`Calling IOSNativeHandler.[${func}].`);
  if (!IOSNativeHandler?.[func]) {
    console.log(`IOSNativeHandler.[${func}] does not exist.`);
    return false;
  }

  try {
    // Check if the message is an object and stringify it
    const stringifiedMessage =
      typeof message === "object" ? JSON.stringify(message) : message;

    // Call the corresponding function with the message parameter
    IOSNativeHandler[func].postMessage(stringifiedMessage);
    console.log(`IOSNativeHandler.[${func}] posted.`);
  } catch (err) {
    console.error("Error while calling iOS native function", err);
    return false;
  }
  return true;
};
```

You can use it in your code like this:
```javascript
callNativeIOSFunction("switchFlipped", {state: true})
```

Finally you can react to the function call in your Swift application with some code like this:
```swift
class Coordinator: NSObject, WKScriptMessageHandler, WKUIDelegate, WKNavigationDelegate {
    // ...
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "switchFlipped" {
            if let messageString = message.body as? String {
                // Convert the JSON string to Data
                if let data = messageString.data(using: .utf8) {
                    do {
                        // Deserialize the JSON data into a dictionary
                        if let messageBody = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
                            // Extract variables
                            if let state = messageBody["state"] as? Bool {
                                // handle switch flip
                            }
                        }
                    } catch {
                        print("Failed to deserialize JSON: \(error)")
                    }
                }
            }
        }
    }
}

```

Check out the example app for a more in-depth look at communicating between native and web. The `NativeBridge` class in [src/utils/NativeBridge.js](https://github.com/BenWeekes/agora-trulience-sdk/blob/main/react/src/utils/nativeBridge.js) showcases an event-driven approach on the javascript side. We also demonstrate how `callNativeIOSFunction` and `callNativeAndroidFunction` can be unified by a single `callNativeAppFunction` function that resolves between the two.

## Example App

Take a look at our [integration examples repository](https://github.com/trulience/integration-examples/tree/main/ios) for a minimal iOS app that loads an HTML page with a Trulience avatar embedded in an iframe. The example demonstrates two-way communication between Swift and JavaScript, hooking up a native iOS button to call a Javascript function that sends a message to the iframe using the `trl-chat` event. To find out more about iframe events, see the [iframe documentation](https://docs.trulience.com/docs/developer/iframe#sending-events).