Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code cleanup. #153

Merged
merged 9 commits into from
Jan 16, 2024
35 changes: 22 additions & 13 deletions Sources/ITwinMobile/ITMActionSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import UIKit
import WebKit

/// ``ITMNativeUIComponent`` that presents a `UIAlertController` with a style of `.actionSheet`.
/// This class is used by the `ActionSheet` TypeScript class in @itwin/mobile-core.
/// This class is used by the `presentActionSheet` TypeScript function in @itwin/mobile-core as well as
/// the `ActionSheetButton` TypeScript React Component in @itwin/mobile-ui-react.
final public class ITMActionSheet: ITMNativeUIComponent {
var activeContinuation: CheckedContinuation<String?, Never>? = nil
/// - itmNativeUI: The ``ITMNativeUI`` used to present the action sheet.
private var activeContinuation: CheckedContinuation<String?, Never>? = nil
/// Creates an ``ITMActionSheet``.
/// - Parameter itmNativeUI: The ``ITMNativeUI`` used to present the action sheet.
override init(itmNativeUI: ITMNativeUI) {
super.init(itmNativeUI: itmNativeUI)
queryHandler = itmMessenger.registerQueryHandler("Bentley_ITM_presentActionSheet", handleQuery)
Expand All @@ -20,13 +22,27 @@ final public class ITMActionSheet: ITMNativeUIComponent {
activeContinuation?.resume(returning: value)
activeContinuation = nil
}

/// Try to convert the `sourceRect` property of `params` into an ``ITMRect``.
/// - Parameter params: JSON data from the web app.
/// - Throws: If `params` does not contain a `sourceRect` property that can be converted to an ``ITMRect``,
/// an exception is thrown.
/// - Returns: The contents of the `sourceRect` property in `params` converted to an ``ITMRect``.
public static func getSourceRect(from params: JSON) throws -> ITMRect {
guard let sourceRectDict = params["sourceRect"] as? JSON,
let sourceRect: ITMRect = try? ITMDictionaryDecoder.decode(sourceRectDict) else {
throw ITMError(json: ["message": "ITMActionSheet: no source rect"])
}
return sourceRect
}

@MainActor
private func handleQuery(params: [String: Any]) async throws -> String? {
private func handleQuery(params: JSON) async throws -> String? {
guard let viewController = viewController else {
throw ITMError(json: ["message": "ITMActionSheet: no view controller"])
}
let alertActions = try ITMAlertAction.createArray(from: params, errorPrefix: "ITMActionSheet")
let sourceRect = try Self.getSourceRect(from: params)
// If a previous query hasn't fully resolved yet, resolve it now with nil.
resume(returning: nil)
return await withCheckedContinuation { (continuation: CheckedContinuation<String?, Never>) in
Expand All @@ -49,15 +65,8 @@ final public class ITMActionSheet: ITMNativeUIComponent {
}
alert.onClose = alert.onDeinit
alert.popoverPresentationController?.sourceView = itmMessenger.webView
if let sourceRectDict = params["sourceRect"] as? [String: Any],
let sourceRect: ITMRect = try? ITMDictionaryDecoder.decode(sourceRectDict) {
alert.popoverPresentationController?.sourceRect = CGRect(sourceRect)
} else {
// We shouldn't ever get here, but a 0,0 popover is better than an unhandled exception.
assert(false)
alert.popoverPresentationController?.sourceRect = CGRect()
}
ITMAlertAction.addActions(alertActions, to: alert) { [self] _, action in
alert.popoverPresentationController?.sourceRect = CGRect(sourceRect)
ITMAlertAction.add(actions: alertActions, to: alert) { [self] action in
resume(returning: action.name)
}
viewController.present(alert, animated: true)
Expand Down
33 changes: 17 additions & 16 deletions Sources/ITwinMobile/ITMAlert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,29 @@ extension UIAlertAction.Style {

/// Swift class that holds data passed from JavaScript for each action in an ``ITMAlert`` and ``ITMActionSheet``.
public struct ITMAlertAction: Codable, Equatable {
/// The name of the alert, passed back to JavaScript when the user selects it.
/// The name of the action, passed back to JavaScript when the user selects it.
let name: String
/// The title of the alert.
/// The title of the action.
/// - Note: UIAlertAction's title is in theory optional. However, if the title is missing when used in an action sheet
/// on an iPad, it will ALWAYS throw an exception. So title here is not optional.
let title: String
/// The style of the alert.
/// The style of the action.
let style: ITMAlertActionStyle

/// Create an array ``ITMAlertAction`` from the given JSON data passed from JavaScript.
/// Create an array of ``ITMAlertAction`` values from the given JSON data passed from JavaScript.
/// - Parameters:
/// - params: The JSON data for the ``ITMAlertAction`` passed from JavaScript.
/// - params: The JSON data passed from JavaScript. This must contain an `actions` array property
/// containing the individual alert actions.
/// - errorPrefix: The prefix to use in any error thrown if `params` contains invalid data.
/// - Returns: An array of ``ITMAlertAction`` values based on the data in `params`.
static func createArray(from params: [String: Any], errorPrefix: String) throws -> [ITMAlertAction] {
guard let actions = params["actions"] as? [[String: Any]], !actions.isEmpty else {
static func createArray(from params: JSON, errorPrefix: String) throws -> [ITMAlertAction] {
guard let actions = params["actions"] as? [JSON], !actions.isEmpty else {
throw ITMError(json: ["message": "\(errorPrefix): actions must be present and not empty"])
}
do {
return try actions.map { try ITMDictionaryDecoder.decode($0) }
} catch {
throw ITMError(json: ["message": "\(errorPrefix): invalid action"])
throw ITMError(json: ["message": "\(errorPrefix): invalid actions"])
}
}

Expand All @@ -60,10 +61,10 @@ public struct ITMAlertAction: Codable, Equatable {
/// - actions: The actions to add to the alert controller.
/// - alertController: The `UIAlertController` to add the actions to.
/// - handler: The handler that is called when an action is selected by the `UIAlertController`.
static func addActions(_ actions: [ITMAlertAction], to alertController: UIAlertController, handler: ((UIAlertAction, ITMAlertAction) -> Void)? = nil) {
static func add(actions: [ITMAlertAction], to alertController: UIAlertController, handler: ((ITMAlertAction) -> Void)? = nil) {
for action in actions {
alertController.addAction(UIAlertAction(title: action.title, style: UIAlertAction.Style(action.style)) { alertAction in
handler?(alertAction, action)
alertController.addAction(UIAlertAction(title: action.title, style: UIAlertAction.Style(action.style)) { _ in
handler?(action)
})
}
}
Expand All @@ -74,9 +75,9 @@ public struct ITMAlertAction: Codable, Equatable {
/// ``ITMNativeUIComponent`` that presents a `UIAlertController` with a style of `.alert`.
/// This class is used by the `presentAlert` TypeScript function in @itwin/mobile-core.
final public class ITMAlert: ITMNativeUIComponent {
var activeContinuation: CheckedContinuation<String?, Never>? = nil
/// - Parameters:
/// - itmNativeUI: The ``ITMNativeUI`` used to present the alert.
private var activeContinuation: CheckedContinuation<String?, Never>? = nil
/// Creates an ``ITMAlert``.
/// - Parameter itmNativeUI: The ``ITMNativeUI`` used to present the alert.
override init(itmNativeUI: ITMNativeUI) {
super.init(itmNativeUI: itmNativeUI)
queryHandler = itmMessenger.registerQueryHandler("Bentley_ITM_presentAlert", handleQuery)
Expand All @@ -88,7 +89,7 @@ final public class ITMAlert: ITMNativeUIComponent {
}

@MainActor
private func handleQuery(params: [String: Any]) async throws -> String? {
private func handleQuery(params: JSON) async throws -> String? {
guard let viewController = viewController else {
throw ITMError(json: ["message": "ITMAlert: no view controller"])
}
Expand All @@ -99,7 +100,7 @@ final public class ITMAlert: ITMNativeUIComponent {
activeContinuation = continuation
let alert = ITMAlertController(title: params["title"] as? String, message: params["message"] as? String, preferredStyle: .alert)
alert.showStatusBar = params["showStatusBar"] as? Bool ?? false
ITMAlertAction.addActions(alertActions, to: alert) { _, action in
ITMAlertAction.add(actions: alertActions, to: alert) { action in
self.resume(returning: action.name)
}
alert.onDeinit = {
Expand Down
10 changes: 5 additions & 5 deletions Sources/ITwinMobile/ITMAlertController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,18 @@ open class ITMAlertController: UIAlertController {
// This avoids cases where topmost view controller is dismissed while presenting alert
// Create temporary window to show alert anywhere and anytime and avoid view hiearchy issues.
if alertWindow == nil {
alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow!.rootViewController = ITMErrorViewController()
alertWindow!.windowLevel = UIWindow.Level.alert + 1
var alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = ITMErrorViewController()
alertWindow.windowLevel = UIWindow.Level.alert + 1
ITMAlertController.alertWindow = alertWindow
}
alertWindow?.windowScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene
alertWindow!.windowScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene
alertWindow!.makeKeyAndVisible()
// Even though we initialized the UIWindow with the proper frame, makeKeyAndVisible sometimes
// corrupts the frame, changing the orientation and moving it completely off-screen. I think
// this is a bug in iOS, and I am not sure why it happens sometimes and not other times.
// However, resetting the frame after the makeKeyAndVisible call fixes the problem.
alertWindow!.frame = UIScreen.main.bounds
alertWindow!.frame = alertWindow?.windowScene?.screen.bounds ?? UIScreen.main.bounds
return alertWindow!.rootViewController!
}

Expand Down
Loading