# iOS (Compat) Liveness SDK

[Get your Public Merchant Key](https://doc.youverify.co/getting-started/getting-your-public-merchant-key)

**Youverify Liveness SDK Compat** is the compatibility version of the iOS SDK, designed for integrating active liveness detection into UIKit-based applications. This SDK is tailored for apps that need to support iOS 12 and later, providing a reliable solution for older iOS versions.

### Features

* **Active Liveness Detection**: Requires users to perform specific actions (e.g., head movements, answering questions) to confirm liveness. Available on iOS 12+.
* Built with **UIKit**, ensuring compatibility with legacy iOS applications.

**Note**: Passive liveness detection is not supported in this version.

### Installation

#### Step 1: Add to Podfile

Add the following to your `Podfile`:

```ruby
pod 'YouverifyLivenessSDKCompat'
pod 'MediaPipeTaskVision'
```

#### Step 2: Install Dependencies

Run this command in your terminal:

```sh
pod install
```

#### Step 3: Open Workspace

Open the generated `.xcworkspace` file in Xcode.

#### Project Configuration

To prevent linker errors, add the following post-install script to your application's Podfile, making sure to replace `"YourAppTarget"` with the correct target name for your app.

post\_install do |installer|\
target\_name = "YourAppTarget"

```
# Get the paths to the xcconfig files
debug_xcconfig_path = "#{installer.pods_project.project_dir}/Target Support Files/Pods-#{target_name}/Pods-#{target_name}.debug.xcconfig"
release_xcconfig_path = "#{installer.pods_project.project_dir}/Target Support Files/Pods-#{target_name}/Pods-#{target_name}.release.xcconfig"

# Modify the debug .xcconfig file if it exists
if File.exist?(debug_xcconfig_path)
  puts "Modifying debug .xcconfig file: #{debug_xcconfig_path}"

  # Read the debug .xcconfig file
  debug_xcconfig = File.read(debug_xcconfig_path)

  # Remove framework flags from OTHER_LDFLAGS
  debug_xcconfig.gsub!(/-framework\s+"MediaPipeTasksCommon"/, '')
  debug_xcconfig.gsub!(/-framework\s+"MediaPipeTasksVision"/, '')

  # Remove -force_load from OTHER_LDFLAGS for iphoneos and iphonesimulator
  debug_xcconfig.gsub!(/-force_load\s+"?[^"]*libMediaPipeTasksCommon.*\.a"?/, '')

  # Write the modified debug .xcconfig file
  File.write(debug_xcconfig_path, debug_xcconfig)
  puts "Modified debug .xcconfig file."
else
  puts "Debug .xcconfig file not found at #{debug_xcconfig_path}"
end

# Modify the release .xcconfig file if it exists
if File.exist?(release_xcconfig_path)
  puts "Modifying release .xcconfig file: #{release_xcconfig_path}"

  # Read the release .xcconfig file
  release_xcconfig = File.read(release_xcconfig_path)

  # Remove framework flags from OTHER_LDFLAGS
  release_xcconfig.gsub!(/-framework\s+"MediaPipeTasksCommon"/, '')
  release_xcconfig.gsub!(/-framework\s+"MediaPipeTasksVision"/, '')

  # Remove -force_load from OTHER_LDFLAGS for iphoneos and iphonesimulator
  release_xcconfig.gsub!(/-force_load\s+"?[^"]*libMediaPipeTasksCommon.*\.a"?/, '')

  # Write the modified release .xcconfig file
  File.write(release_xcconfig_path, release_xcconfig)
  puts "Modified release .xcconfig file."
else
  puts "Release .xcconfig file not found at #{release_xcconfig_path}"
end
```

end

***

### Usage

#### 1. Import the SDK

In your Swift file:

```swift
import YouverifyLivenessSDK
```

#### 2. Initialize the SDK

Create an instance of `YVLiveness` with your configuration:

```swift
private var yvLiveness = YVLiveness(
    publicKey: Environment.shared.value(forKey: "API_KEY") ?? "",
    user: YVLivenessUser(firstName: "Ugochukwu"),
    onSuccess: { data in
        print("The success data is \(data)")
    },
    onFailure: { errorData in
        print("The error data is \(errorData)")
    },
    sandboxEnvironment: false
)
```

* The `publicKey` is dynamically retrieved from an environment variable (`Environment.shared.value(forKey: "API_KEY") ?? ""`) in the working code. Replace this with your actual API key or retrieval logic.
* The `user` parameter requires a `firstName`, while optional fields like `lastName` and `email` can be added if needed (see Configuration Options).

#### 3. Start Liveness Detection

Launch the SDK with a list of tasks:

```swift
yvLiveness.startSDK(tasks: tasks)
DispatchQueue.main.async {
    self.addLivenessViewController()
}
```

#### 4. Manage the Liveness View Controller

Add these methods to handle the liveness UI:

```swift
private var livenessViewController: YVLivenessViewController?

private func addLivenessViewController() {
    guard livenessViewController == nil else { return }
    let vc = YVLivenessViewController(sdk: yvLiveness)
    vc.delegate = self
    addChild(vc)
    view.addSubview(vc.view)
    vc.didMove(toParent: self)
    livenessViewController = vc
}

private func removeLivenessViewController() {
    guard let vc = livenessViewController else { return }
    vc.willMove(toParent: nil)
    vc.view.removeFromSuperview()
    vc.removeFromParent()
    livenessViewController = nil
}
```

* The `guard` statement in `addLivenessViewController()` prevents duplicate additions of the view controller.

#### 5. Handle Modal Closing

Implement the delegate to close the liveness UI:

```swift
extension MainViewController: YVLivenessViewDelegate {
    func closeModal() {
        DispatchQueue.main.async {
            self.removeLivenessViewController()
        }
    }
}
```

***

### Configuration Options

| Option               | Type     | Required | Description                            | Default | Possible Values |
| -------------------- | -------- | -------- | -------------------------------------- | ------- | --------------- |
| `publicKey`          | String   | Yes      | Your API key for authentication        | -       | Any string      |
| `user`               | Class    | Yes      | User details (requires `firstName`)    | -       | See below       |
| `user.firstName`     | String   | Yes      | User’s first name                      | -       | Any string      |
| `user.lastName`      | String   | No       | User’s last name                       | `nil`   | Any string      |
| `user.email`         | String   | No       | User’s email                           | `nil`   | Any string      |
| `onSuccess`          | Function | No       | Callback on success with liveness data | `nil`   | Any function    |
| `onFailure`          | Function | No       | Callback on failure with error data    | `nil`   | Any function    |
| `sandboxEnvironment` | Bool     | No       | Toggle sandbox mode for testing        | `true`  | `true`, `false` |

***

### Available Tasks

Tasks verify liveness through user interactions. Below are the supported options:

#### Complete The Circle

Users trace a circle with head movements.

| Option       | Type           | Required | Description                 | Default   | Values                      |
| ------------ | -------------- | -------- | --------------------------- | --------- | --------------------------- |
| `difficulty` | TaskDifficulty | No       | Task difficulty             | `.medium` | `.easy`, `.medium`, `.hard` |
| `timeout`    | Number         | No       | Time (ms) before task fails | `nil`     | Any milliseconds            |

Example:

```swift
TaskProperties(task: .completeTheCircle(CompleteTheCircleTask(difficulty: .medium)))
```

#### Yes or No

Users answer questions by tilting their head (right = yes, left = no).

| Option                   | Type           | Required | Description                 | Default   | Values                      |
| ------------------------ | -------------- | -------- | --------------------------- | --------- | --------------------------- |
| `difficulty`             | TaskDifficulty | No       | Task difficulty             | `.medium` | `.easy`, `.medium`, `.hard` |
| `timeout`                | Number         | No       | Time (ms) before task fails | `nil`     | Any milliseconds            |
| `questions`              | Array          | Yes      | List of yes/no questions    | -         | See below                   |
| `questions.question`     | String         | Yes      | Yes/no question             | -         | Any string                  |
| `questions.answer`       | Bool           | Yes      | Correct answer              | -         | `true`, `false`             |
| `questions.errorMessage` | String         | No       | Message on wrong answer     | `nil`     | Any string                  |

Example:

```swift
TaskProperties(task: .yesOrNo(YesOrNoTask(questions: [YesOrNoQuestion(question: "Is 2 + 2 = 7?", answer: false)])))
```

#### Motions

Users perform nods, blinks, or mouth movements.

| Option       | Type           | Required | Description                 | Default   | Values                      |
| ------------ | -------------- | -------- | --------------------------- | --------- | --------------------------- |
| `difficulty` | TaskDifficulty | No       | Task difficulty             | `.medium` | `.easy`, `.medium`, `.hard` |
| `timeout`    | Number         | No       | Time (ms) before task fails | `nil`     | Any milliseconds            |
| `maxNods`    | Int            | No       | Max nods to perform         | `5`       | Any number                  |
| `maxBlinks`  | Int            | No       | Max blinks to perform       | `5`       | Any number                  |

Example:

```swift
TaskProperties(task: .motions(MotionsTaskClass(difficulty: .medium)))
```

***

### Liveness Data

The `onSuccess` callback returns:

| Field          | Type   | Description                     |
| -------------- | ------ | ------------------------------- |
| `faceImage`    | String | User’s face image               |
| `livenessClip` | String | Video of liveness check         |
| `passed`       | Bool   | True if passed, false if failed |
| `metadata`     | Any    | Metadata from initialization    |

The `onFailure` callback returns error details as a string or object, depending on the issue.

***

### Full Example

Here’s the complete implementation from the working code:

```swift
import UIKit
import YouverifyLivenessSDK

class MainViewController: UIViewController {
    private var yvLiveness = YVLiveness(
        publicKey: Environment.shared.value(forKey: "API_KEY") ?? "",
        user: YVLivenessUser(firstName: "Ugochukwu"),
        onSuccess: { data in
            print("The success data is \(data)")
        },
        onFailure: { errorData in
            print("The error data is \(errorData)")
        },
        sandboxEnvironment: false
    )
    
    private var livenessViewController: YVLivenessViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Create a vertical stack view
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 10
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // Create buttons
        let completeCircleButton = createButton(title: "Complete the Circle", action: #selector(completeTheCircleTapped))
        let yesOrNoButton = createButton(title: "Yes or No", action: #selector(yesOrNoTapped))
        let motionsButton = createButton(title: "Motions", action: #selector(motionsTapped))
        let passiveLivenessButton = createButton(title: "Passive Liveness", action: #selector(passiveLivenessTapped))
        
        // Add buttons to stack view
        stackView.addArrangedSubview(completeCircleButton)
        stackView.addArrangedSubview(yesOrNoButton)
        stackView.addArrangedSubview(motionsButton)
        stackView.addArrangedSubview(passiveLivenessButton)
        
        // Add stack view to the main view
        view.addSubview(stackView)
        
        // Set Auto Layout constraints
        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
        ])
    }
    
    private func addLivenessViewController() {
        guard livenessViewController == nil else { return } // Prevent duplicate adds
        let vc = YVLivenessViewController(sdk: yvLiveness)
        vc.delegate = self
        
        addChild(vc)
        
        view.addSubview(vc.view)
        vc.didMove(toParent: self)
        livenessViewController = vc
    }

    private func removeLivenessViewController() {
        guard let vc = livenessViewController else { return }
        vc.willMove(toParent: nil)
        vc.view.removeFromSuperview()
        vc.removeFromParent()
        livenessViewController = nil
    }
    
    // Helper function to create buttons with actions
    private func createButton(title: String, action: Selector) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.addTarget(self, action: action, for: .touchUpInside)
        return button
    }
    
    // MARK: - Button Actions
    
    private func startLiveness(with tasks: [TaskProperties]) {
        yvLiveness.startSDK(tasks: tasks)
        DispatchQueue.main.async {
            self.addLivenessViewController()
        }
    }
    
    @objc private func completeTheCircleTapped() {
        startLiveness(with: [
            TaskProperties(task: .completeTheCircle(CompleteTheCircleTask(difficulty: .medium)))
        ])
    }
    
    @objc private func yesOrNoTapped() {
        startLiveness(with: [TaskProperties(
            task: .yesOrNo(YesOrNoTask(questions: [YesOrNoQuestion(question: "Is 2 + 2 = 7?", answer: false)]))
        )])
    }
    
    @objc private func motionsTapped() {
        startLiveness(with: [TaskProperties(
            task: .motions(MotionsTaskClass(difficulty: .medium))
        )])
    }
    
}

extension MainViewController: YVLivenessViewDelegate {
    func closeModal() {
        DispatchQueue.main.async {
            self.removeLivenessViewController()
        }
    }
}
```

### Credits

This SDK is developed and maintained solely by [Youverify](https://youverify.co)
