Skip to main content

iOS Guide

  1. Open Xcode and create a new Single View App.
  2. Go to the Storyboard and select the View Controller. 3.Go to the Editor menu and select Embed in -> Navigation Controller.
  3. In your ViewController.swift file add the following:
import UIKit
import WebKit
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let webView = WKWebView(frame: .zero)

view.addSubview(webView)

let layoutGuide = view.safeAreaLayoutGuide
webView.translatesAutoresizingMaskIntoConstraints = false
webView.leadingAnchor.constraint(
equalTo: layoutGuide.leadingAnchor).isActive = true
webView.trailingAnchor.constraint(
equalTo: layoutGuide.trailingAnchor).isActive = true
webView.topAnchor.constraint(
equalTo: layoutGuide.topAnchor).isActive = true
webView.bottomAnchor.constraint(
equalTo: layoutGuide.bottomAnchor).isActive = true

if let url = URL(string: "<YOUR WEBVIEW URL WITH SOLVVY>") {
webView.load(URLRequest(url: url))
}
}
}

This is the basic code for opening your ticket submission page (which should auto-launch Solvvy) in a webview. If Solvvy is not installed on your web ticket submission page or does not auto-launch, contact your Solvvy Sales Engineer or Solutions Engineer. Note: to customize the behavior of the webview in various ways, please consult the documentation.

Passing data to the webview

In certain situations, it is necessary to pass data to Solvvy running in the webview, e.g. to specify which language the app is using so Solvvy can be properly localized, or helpful metadata that needs to be included on the ticket if the user does not self-serve (like mobile platform, app version, user ID, etc.). This can easily be accomplished by setting JS variables on the webpage when launching the webview. Put all your variables in the window.solvvyConfig object. If you want the data to be passed directly into tickets as certain custom fields, please use the custom field ID as the variable name, as below:

let userContentController = WKUserContentController()
let scriptSource = "window.solvvyConfig = window.solvvyConfig || { " + 
"language : 'de'," +
"email : 'test@example.com'," +
"custom_23793987 : 'test123'," + // Support ID
"custom_23873898 : 'iPad'," + // Device Type (Name)
"darkMode : true," + // Dark mode (boolean)
"some_array : [ 'item1', 'item2' ]};" + // Some array of strings
"window.solvvy = window.solvvy || {};"
let script = WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(script)
let config = WKWebViewConfiguration()
config.userContentController = userContentController
let webView = WKWebView(frame: .zero, configuration: config)

Getting Data From the Webview

When a user is not able to self-serve, Solvvy presents a list of options (or channels) for contacting support (or automatically defaults to one if only one is configured). Most of these support options can be handled, or executed, within the Solvvy flow, such as email ticket submission. However, for some support options (e.g. live chat), it may be preferable to execute the support contact flow directly from the native app (e.g. using a 3rd party native SDK). To facilitate this, your native app needs to find out from the webview whether this native support flow needs to launch after the webview dismisses itself (i.e. if the user was not able to self-serve). Your native app also needs to get the question that the user typed in at the beginning of the Solvvy flow, so they don't have to re-type their issue. Both of these things can be accomplished with the following code.

  1. Define a callback function to receive the user's original question:
override func viewDidLoad() {
super.viewDidLoad()

let config = WKWebViewConfiguration()
let userContentController = WKUserContentController()
...

userContentController.add(self, name: "supportOptionHandler")

config.userContentController = userContentController

let webView = WKWebView(frame: .zero, configuration: config)
view.addSubview(webView)
...

}

extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
if let messageBody = message.body as? [String: Any],
let userQuestion = messageBody["userQuestion"] as? String,
let supportOption = messageBody["supportOption"] as? String {
print("userQuestion: \(userQuestion), supportOption: \(supportOption)")
}
}
}

Make sure you use the exact name: name: "supportOptionHandler" because that is what Solvvy will call when a support option is clicked by the user.

  1. Now your ViewController.swift should look like this:
import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

override func viewDidLoad() {
super.viewDidLoad()

let config = WKWebViewConfiguration()
let userContentController = WKUserContentController()
let scriptSource = "window.solvvyConfig = window.solvvyConfig || { " +
"language : 'de'," +
"email : 'test@example.com'," +
"custom_23793987 : 'test123'," + // Support ID
"custom_23873898 : 'iPad'," + // Device Type (Name)
"darkMode : true," + // Dark mode (boolean)
"some_array : [ 'item1', 'item2' ]};" + // Some array of strings
"window.solvvy = window.solvvy || {};"
let script = WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(script)

userContentController.add(self, name: "supportOptionHandler")

config.userContentController = userContentController

let webView = WKWebView(frame: .zero, configuration: config)

view.addSubview(webView)

let layoutGuide = view.safeAreaLayoutGuide
webView.translatesAutoresizingMaskIntoConstraints = false
webView.leadingAnchor.constraint(
equalTo: layoutGuide.leadingAnchor).isActive = true
webView.trailingAnchor.constraint(
equalTo: layoutGuide.trailingAnchor).isActive = true
webView.topAnchor.constraint(
equalTo: layoutGuide.topAnchor).isActive = true
webView.bottomAnchor.constraint(
equalTo: layoutGuide.bottomAnchor).isActive = true

if let url = URL(string: "<YOUR WEBVIEW URL WITH SOLVVY>") {
webView.load(URLRequest(url: url))
}
}
}

extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
if let messageBody = message.body as? [String: Any],
let userQuestion = messageBody["userQuestion"] as? String,
let supportOption = messageBody["supportOption"] as? String {
print("userQuestion: \(userQuestion), supportOption: \(supportOption)")
}
}
}

Closing the webview when the user self-served

When a user is satisfied with one of the answers returned, they can click "Yes" to indicate they got their answer. On the next screen, if they click "Close" to end the Solvvy experience, the webview needs to pass control back to the native app. This happens through another callback function which the Solvvy modal calls when that "Close" button is clicked. Use the following code to handle that callback:

...
userContentController.add(self, name: "exitHandler")
...

extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// do something like close the webview
print("EXIT HANDLER CALLED!")
}
}

Please note that the userContentController method is called with only an empty string argument.

Allowing attachments on tickets

If you want to allow Email/Ticket as one of the options for contacting support, and you want to allow the users to add attachments from their local device to the ticket form, then you will need to add the following code:

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

var lastTapPosition: CGPoint = CGPoint(x: 0, y: 0)

override func viewDidLoad() {
super.viewDidLoad()

...

let webView = WKWebView(frame: .zero, configuration: config)

// Intercept the user tap and store its location so we can use it to position our menu on screen.
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(webViewTapped(_:)))
tapGesture.delegate = self
webView.addGestureRecognizer(tapGesture)

view.addSubview(webView)

...
}

@objc
func webViewTapped(_ sender: UITapGestureRecognizer) {
// Store the last tap location.
self.lastTapPosition = sender.location(in: self.view)
}

// One problem here is that we are not the ones declaring a variable for our UIPopoverPresentationController but
// it is coming from the web view itself, so we need to find a way to tell the controller that our View Controller
// will be its delegate for this situation. We can achieve that by overriding the present method from our
// View Controller to be able to set it as the delegate of the popover.
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if #available(iOS 13, *) {
viewControllerToPresent.popoverPresentationController?.delegate = self
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}

...

extension ViewController: UIPopoverPresentationControllerDelegate {
func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) {
popoverPresentationController.sourceView = self.view // with this code the crash doesn’t happen anymore
popoverPresentationController.sourceRect = CGRect(origin: self.lastTapPosition, size: CGSize(width: 0, height: 0)) // Display on last tap position on webview
}
}

// No matter how much we tap the web view, our webViewTapped method will never be called. That’s because our web view
// is already intercepting our taps to perform the necessary web view actions and, if we want to have our
// View Controller to recognize them simultaneously, we have to make our View Controller conform to the
// UIGestureRecognizerDelegate protocol
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

Intercept URL requests from within a webview

First, make something conform to WKNavigationDelegate. For example:

class ViewController: UIViewController, WKNavigationDelegate {

Second, make that object the navigation delegate of your web view. If you were using your view controller, you’d write this:

webView.navigationDelegate = self

Finally, implement the decidePolicyFor method with whatever logic should decide whether the page is loaded normally or execute deep links to app screens. For deep links make sure you call the decisionHandler() closure with .cancel so the load halts.

As an example, this implementation will load all links inside the web view as long as they don’t go to the Apple homepage:

func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let host = navigationAction.request.url?.host, host == "www.apple.com" {
// do stuff or call deep links
decisionHandler(.cancel)
return
}

decisionHandler(.allow)
}

Load error handling

Possible scenarios where you might want to handle load errors include:

  • Network issues
  • Incompatible browser or device*
  • Solvvy load failure

Although these scenarios are rare, the following load error handler can be implemented to provide a cleaner experience for your users. The possible load errors are:

  • "loading_timeout" - Solvvy did not load within 10 seconds
  • "loading_failed" - Solvvy failed to load
  • "incompatible_browser" - Incompatible browser or device*

If this handler is not implemented, where possible, the Solvvy team will implement a generic fallback to redirect to your web contact form.

...
userContentController.add(self, name: "loadErrorHandler")
...

extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
if let messageBody = message.body as? [String: Any],
let error = messageBody["error"] as? String{
print("error: \(error)")
}
}
}