Scan Process

The scanning process happens entirely through the TireTreadScanView and should be performed in Landscape mode.

Implementing the ScannerViewControllerHolder Interface

In order to set up the scan view and retrieve the scan view controller, your UIViewController subclass should implement the ScannerViewControllerHolder interface.

This interface gives your view controller two properties:

  1. scannerViewController: After the setup call TireTreadScanViewKt.TireTreadScanView (see below) is successfully made, this property will hold the instance of the TireTreadScannerViewController, which handles the scanning process.

  2. dismissViewController: This is a function property that will be called by the SDK when a request has been made to dismiss the scanner. Assign it a block within the lifetime of your ScannerViewControllerHolder that handles this scenario in the appropriate manner.

Here is an example of how to implement the ScannerViewControllerHolder interface:

class YourViewController: UIViewController, ScannerViewControllerHolder {

    // Implement the properties defined by the ScannerViewControllerHolder interface
    var scannerViewController: UIViewController?

    var dismissViewController: (() -> Void)?

    // Rest of your class implementation...
}

Remember to replace YourViewController with the actual name of your view controller.

Make sure that YourViewController runs in Landscape mode.

Setting up the Scan View Controller

Once the SDK has been initialized and the necessary permissions have been granted, you can set up the scan view controller.

  • With JSON config

  • With config object

private func setupTireTreadScanView() {

    // You can also use the full JSON string as an input!
    let config = "scan_config.json"
    TireTreadScanViewKt.TireTreadScanView(
        context: self,
        config: config,
        onScanAborted: onScanAborted,
        onScanProcessCompleted: onScanProcessCompleted
        callback: { event in
        // handle scan events
    }) { measurementUUID, error in
        // Handle errors during scanning process
    }
}

private func addScanViewControllerAsChild() {
    guard let scannerViewController = scannerViewController else {
        // Handle error
        return
    }
    addChild(scannerViewController)
    view.addSubview(scannerViewController.view)
    scannerViewController.didMove(toParent: self)
}

private func onScanAborted(measurementUUID: String?) {
    // handle scan aborted
}

private func onScanProcessCompleted(measurementUUID: String) {
    // handle scan process completed
}
private func setupTireTreadScanView() {

    let config = TireTreadScanViewConfig()
    config.measurementSystem = userDefaults.imperialSystem ? .imperial : .metric
    config.defaultUiConfig = customUiConfig

    // config.additionalContext = additionalContext

    // ScanSpeed is experimental, may impact scan performance and may be removed with any major SDK release.
    // You are advised to ignore this configuration on your implementation.
    // config.scanSpeed = userDefaults.scanSpeed

    // creates a TireTreadScannerViewController. You can later refer to it here
    // as self.scannerViewController.

    // Alternatively initialise scan process with a JSON config
    TireTreadScanViewKt.TireTreadScanView(
        context: self,
        config: config,
        onScanAborted: onScanAborted,
        onScanProcessCompleted: onScanProcessCompleted,
        callback: { event in
            // Handle scan events
        }) { measurementUUID, error in
        // Handle errors during scanning process
    }
}

private func addScanViewControllerAsChild() {
    guard let scannerViewController = scannerViewController else {
        // Handle error
        return
    }
    addChild(scannerViewController)
    view.addSubview(scannerViewController.view)
    scannerViewController.didMove(toParent: self)
}

Use a TireTreadScanViewConfig object to set the behavior, appearance, and metadata of the scanning process. You can use the following options:

  • measurementSystem - MeasurementSystem.metric or MeasurementSystem.imperial

  • defaultUiConfig - allows the customization of the Default UI

  • useDefaultHaptic - if set to false, the SDK will not make use of the device’s haptic during the scanning process to provide feedback to the users.

  • useDefaultUi - if set to false, the scan view will not display the default UI elements (except the Camera Preview).

  • additionalContext - provides additional context to a scan. Check below for more information.

callback is a function, which helps in tracking all the events(Handling the SDK’s events) related to Tire Tread scans. onError is a function callback that is invoked when an error occurs during scanning onScanAborted is a function callback that is invoked when the scan is aborted by the process or by the user onScanProcessCompleted is a function callback that is invoked when the scan process is finished and the result can be queried

If the callback to TireTreadScanViewKt.TireTreadScanView(context, config, callback) does not produce an error, then the TireTreadScanView becomes accessible to your application as the main view object of scannerViewController.

Additional Context

The AdditionalContext is an optional property which allows you to provide more context to a scan. This makes sense in a workflow, where a scan is connected to other TireTread scans or other information in a larger context. Its initialization requires a TirePosition.

The TirePosition identifies the position where the tire is mounted on the vehicle, considering the primary direction of travel. E.g. the front left tire of a passenger car would be specified as axle: 1, side: TireSide.left, position_on_axle: 1 and the rear right tire of a passenger car as axle: 2, side: TireSide.right, position_on_axle: 1

The TirePosition's parameters are as follows:

  • axle: Number of the axle, whereas 1 is the front axle (in direction of travel). Its value must be at least 1.

  • side: Enum, representing the side where the Tire is located (Left, Center, Right).

  • positionOnAxle: Position on the axle, whereas 1 is the outermost tire and higher values represent the following inner tires. If the side is set to center, 1 is the leftmost tire (in direction of travel), if there are multiple tires on the center axle. Its value must be at least 1.

If the AdditionalContext is provided, it will also be returned with the results in the TreadDepthResult object, in the measurement property.

TirePosition initialization will throw an IllegalArgumentException if axle or positionOnAxle are 0.

Audio Feedback

The Tire Tread SDK can provide audio feedback to guide users through the scan process.

To make use of these audio feedbacks, your application needs to provide (and thus can customize) the audio files inside its Resources folder.

The audio feedbacks (with their respective files names) on iOS are played on:

  • Focus Point Found

    • tiretread_focuspoint_found.wav

  • Scan Started

    • tiretread_sound_start.wav

  • Scan Stopped

    • tiretread_sound_stop.wav

  • Phone too close to the Tire

    • tiretread_sound_beep_too_close.wav

  • Phone too distant from the Tire

    • tiretread_sound_beep_too_far.wav

The SDK only supports these file names, and the .wav extension.

An example implementation, and the example audio files, can be found in our GitHub Example implementation.

To disable the audio feedback, remove these audio files from the applications' Resources or rename them.

Start Scanning

In the previous example, we are not disabling the Default UI (useDefaultUi = false) in the TireTreadScanView. This means that the ScanView will draw and control all the required components for the scan process.

When using the default UI, your application only needs to implement behavior in a few callbacks from the SDK: onScanAbort, onUploadCompleted, onUploadFailed, and onUploadAborted. Jump to the Handling the SDK’s events section if you are using the default UI.

When not using the default UI, your application can define the UX of the entire scan process. For that, start by adding a button alongside of the TireTreadScanView. This button will be responsible for starting and stopping the scan process. Use the functions TireTreadScanner.companion.instance.startScanning() and TireTreadScanner.companion.instance.stopScanning() for that.

The TireTreadScanner.companion.instance also provides the captureDistanceStatus property, with which your application can check if the user is positioning the device at the correct distance from the tire. This property can be used to prevent scans from initiating at an incorrect distance, e.g.:

if (TireTreadScanner.companion.instance.captureDistanceStatus == DistanceStatus.ok)
{
    TireTreadScanner.companion.instance.startScanning()
}
else {
    // Notify user to move the phone to the correct position before starting
    print("Move the phone to the correct position before starting")
}

The scanning process can be stopped at any time. If not manually stopped, the scan process is automatically stopped after 10 seconds.

Besides the "Start/Stop" button, when not using the default UI, add also an "Abort" button (or e.g., an 'x' icon), through which the users can abort the Scan Process without waiting for the results. This button should call the TireTreadScanner.companion.instance.abortScanning() function.

Enabling volume keys to start scanning

Optionally can create an extension for the ScanViewController and set up a VolumeButtonObserver to trigger the scan via the phone’s volume buttons.

private extension ScanViewController {
    func setupVolumeButtonObserver() {
        volumeButtonObserver = VolumeButtonObserver()
        volumeButtonObserver?.onVolumeButtonPressed = { [weak self] in
            self?.handleVolumeButtonPressed()
        }
    }

    func resetVolumeButtonObserver() {
        self.volumeButtonObserver = nil
    }

    private func handleVolumeButtonPressed() {
        if TireTreadScanner.companion.isInitialized {
            if TireTreadScanner.companion.instance.isScanning {
                TireTreadScanner.companion.instance.stopScanning()
            } else {
                if (TireTreadScanner.companion.instance.captureDistanceStatus == DistanceStatus.ok)
                {
                    TireTreadScanner.companion.instance.startScanning()
                }
                else {
                    // Notify user to move the phone to the correct position before starting
                    print("Move the phone to the correct position before starting")
                }
            }
        }
    }
}

Handling the SDK’s events

The TireTreadScanView communicates back to your application via callback function that is invoked on every scan event. With this callback function you can react on changes and implement your own workflow around the scan process. The events available are:

  • OnScanStarted: Invoked when the scanning process begins

  • OnScanStopped: Invoked when the scanning process ends

  • OnScanAborted: Invoked when the scanning process is aborted

    • Should be implemented also when using the default UI

  • OnScanProcessCompleted: Invoked when the scanning process is aborted

    • Must be implemented also when using the default UI

  • OnError: Invoked when the scanning process ran into an error

    • Must be implemented also when using the default UI

  • OnImageUploaded: Invoked after each frame is uploaded

  • OnDistanceChanged: Invoked whenever the distance between the device and the tire changes

    • This information serves as a guide to assist your app’s users in scanning correctly

extension YourViewController {

    private func onError(measurementUUID: String?, exception: Exception) {
        print("onUploadFailed")
        self.displayError()
    }

    private func onScanAborted(measurementUUID: String?) {
        TireTreadScanner.companion.abortScanning()
        finish()
    }

    private func onScanProcessCompleted(measurementUUID: String) {
        print("onUploadCompleted")
        if let safeUuid = event.measurementUUID {
            // redirect to the result loading screen
            self.displayLoading(uuid: safeUuid)
        } else {
            self.displayError()
        }
    }

    private func handleScanEvent(event: ScanEvent) {
        switch(event) {

        case let event as OnImageUploaded:
            print("onImageUploaded: \(event.total) images uploaded in total")
            break

        case event as OnDistanceChanged:
            self.onDistanceChanged(event.measurementUUID, event.previousStatus, event.newStatus, event.previousDistance, event.newDistance)
            break
        default:
            print("ScanEvent: \(event.description)")
            break
        }
    }

    func startScanning() {
        TireTreadScanner.companion.startScanning()
    }

    func stopScanning() {
        TireTreadScanner.companion.stopScanning()
    }

    /// When not using the "default UI", use this callback to provide guidance to the users
    /// Called when the distance has changed.
    ///
    /// - Parameters:
    ///   - uuid: The UUID associated with the distance change.
    ///   - previousStatus: The previous distance status.
    ///   - newStatus: The new distance status.
    ///   - previousDistance: The previous distance value.
    ///   - newDistance: The new distance value.
    ///
    /// Note: The distance values are provided in millimeters if the metric system is selected (`UserDefaultsManager.shared.imperialSystem = false`), and in inches if the imperial system is selected (`UserDefaultsManager.shared.imperialSystem = true`).
    private func onDistanceChanged(uuid: String?, previousStatus: DistanceStatus, newStatus: DistanceStatus, previousDistance: Float, newDistance: Float) {
        if Int(newDistance) != Int(previousDistance) {
            let distanceInCentimeters = UserDefaultsManager.shared.imperialSystem ? (newDistance * 2.54) : (newDistance / 10.0)
            DispatchQueue.main.async { [weak self] in
                self?.updateUI(status: newStatus, distance: Int(UserDefaultsManager.shared.imperialSystem ? newDistance : distanceInCentimeters))
            }
        }
    }
}
You can check out our GitHub example repository for a full implementation of the ScanViewController.

Check out the next section to learn how to handle the measurement results after the scan process is finished.