iOS Implementation 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.

  4. 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 WEB TICKET SUBMISSION URL WITH PARQEX>") {
      webView.load(URLRequest(url: url))
    }
  }
}

This is the basic code for opening your ParqEx main home/dashboard page (which should auto-launch ParqEx) in a webview. If ParqEx is not installed on your web ticket submission page or does not auto-launch, contact your ParqEx tech support (support@parqex.com). 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 ParqEx running in the webview, e.g. to specify user credentials, the language the app is using so ParqEx can be properly localized, or helpful metadata that needs to be included (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.parqexConfig object. If you want the data to be passed directly as certain custom fields, please use the custom field ID as the variable name, as below:

let userContentController = WKUserContentController()
let scriptSource = "window.parqexConfig = window.parqexConfig || { " + 
                     "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.parqex = window.parqex || {};"
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

Your native app can get certain information/data from ParqEx:

  1. Define a callback function:

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 ParqEx 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.parqexConfig = window.parqexConfig || { " + 
                         "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.parqex = window.parqex || {};"
    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 WEB TICKET SUBMISSION URL WITH PARQEX>") {
        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 to EXIT the parking (white-labeled ParqEx app)

When a user is done using the parking functionality in the app, they can click on the menu, back or home icon to navigate back to the native app. To end the parking session, the webview needs to pass control back to the native app. This happens through another callback function which the ParqEx modal calls. 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)
}

Last updated