Skip to main content

iOS - UI Module

note

To present native user interfaces, a proper implementation of the UIModule protocol is required. SwiftUI code is supported if it's wrapped in a UIViewController before it is returned to the Q2MobileCore.

The UIModule interface is used when creating a module that requires presenting native user interfaces. The implementation should be responsible for providing a UIViewController, which is presented using a specific modal presentation style by Q2MobileCore.

UIModule Methods

The module provides the following capabilities:

UI Presentation

  • viewControllerToPresent(with:) - Asks the module to provide UI that should be presented by the host application for a UI module (iOS only)

Lifecycle Events

  • didOpen(_:) - Informs the module when host application decided to present this module for the provided identifier
  • shouldLeavePage(decisionHandler:) - Prompts the user to either navigate away from the page or to cancel navigation

Protocol Definition

UIModule Protocol
/// A type that can provide native UI.
public protocol UIModule: Module {

#if !os(watchOS)

/// Asks the module to provide UI that should be presented by the host application for an UI module.
/// - Parameters:
/// - context: contains the Identifier for the view and the module invocation data.
/// - Returns: Instance of `UIViewController` to be be presented by the host application.
func viewControllerToPresent(with context: UIModuleContext) async -> UIViewController?
#endif

/// Informs the module when host application decided to present this module for the provided identifier.
/// - Parameter identifier: Module identifier.
func didOpen(_ identifier: String)

/// Prompts the user to either navigate away from the page or to cancel navigation.
/// - Parameter decisionHandler: The users selection.
func shouldLeavePage(decisionHandler: @escaping (LeaveOrStayOnPage) -> Void)
}

Implementation

There are two ways to implement a UIModule depending on your needs:

If your module inherits from Q2ModuleBase, you only need to override the methods required for your specific functionality. This approach provides default implementations for all protocol methods, making your code cleaner and more focused.

UIModule Implementation with Q2ModuleBase
import Q2ModuleInterfaces

class CustomUIModule: Q2ModuleBase {

override func viewControllerToPresent(with context: UIModuleContext) async -> UIViewController? {
// Create and return your custom view controller based on context
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "CustomViewController")

// Configure the view controller with context data if needed
if let customVC = viewController as? CustomViewController {
customVC.moduleData = context.moduleInvocationData
}

return viewController
}

override func didOpen(_ identifier: String) {
print("CustomUIModule: didOpen(\(identifier))")
// Add your custom logic when module opens
}

override func shouldLeavePage(decisionHandler: @escaping (LeaveOrStayOnPage) -> Void) {
// Add your custom logic to determine if user can leave
// For example, check if there are unsaved changes
decisionHandler(.leave)
}
}

Option 2: Raw Implementation

For full control over all protocol methods, you can implement the UIModule protocol directly by creating an NSObject class that conforms to all protocol requirements.

Raw UIModule Implementation
import Q2ModuleInterfaces

class CustomUIModule: NSObject, UIModule {

var moduleDelegate: ModuleDelegate?

var moduleDataSource: ModuleDataSource?

/// Provide a view controller which needs to be presented by Q2 Mobile Core.
func viewControllerToPresent(with context: UIModuleContext) async -> UIViewController? {
// Create and return your custom view controller based on context
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "CustomViewController")

// Configure the view controller with context data if needed
if let customVC = viewController as? CustomViewController {
customVC.moduleData = context.moduleInvocationData
}

return viewController
}

/// Notifies the module by Q2 Mobile Core that it has finished presenting module.
func didOpen(_ identifier: String) {
print("CustomUIModule: didOpen(\(identifier))")
}

/// Informs Q2 Mobile Core that whether user is able to dismiss the module or not.
func shouldLeavePage(decisionHandler: @escaping (LeaveOrStayOnPage) -> Void) {
decisionHandler(.leave)
}

// MARK: - Module Protocol Requirements

func log(_ message: String, level: LogLevel) {
print("CustomUIModule: \(message)")
}

func logEvent(_ name: String, attributes: [String: Any]?) {
print("CustomUIModule: Event - \(name), attributes: \(attributes ?? [:])")
}

func logError(error: Error, attributes: [String: Any]?) {
print("CustomUIModule: Error - \(error), attributes: \(attributes ?? [:])")
}
}

Presentation

To present UI provided by your module by Q2MobileCore, use Tecton - openModule capability in your online extension.

tecton.actions
.openModule('moduleIdentifier', '{"key": "value"}')
.then(response => {
// was successful at opening module
})
.catch(error => {
// could not open module
});
  • moduleIdentifier is configured by Q2 on module creation.
  • data is in JSON form to pass to the type implementing UI Module.

Dismiss Response

If you want to provide dismiss response back to Tecton layer, inherit your view controller from UIModuleViewController.

Assume you have a dismiss button tied with the following dismiss method in a view controller which is inherited from UIModuleViewController:

UIModuleViewController Implementation with Dismiss Response
final class CustomUIViewController: UIModuleViewController {

self.navigationItem.leftBarButtonItem = UIBarButtonItem(
title: "Data",
style: .plain,
target: self,
action: #selector(dismissViewWithData)
)

@objc func dismissViewWithData() {
let date = Date().description
let dismissResponse = UIModuleDismissResponse(data: date)
dismissWithResponse(dismissResponse)
}
}

On dismissal, the current date would be provided as Data back to the Tecton layer which has invoked this UIModule.