iOS - UI Module
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
/// 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:
Option 1: Inherit from Q2ModuleBase (Recommended)
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.
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.
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
});
moduleIdentifieris configured by Q2 on module creation.datais 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:
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.