Skip to content

Commit

Permalink
Code cleanup. (#153)
Browse files Browse the repository at this point in the history
In addition to lots of internal refactoring and documentation cleanup, this includes the following public API changes:
* `ITMApplication.webViewLogger` can now be nil.
* `ITMApplication.createWebViewLogger` now returns optional
* `ITMApplication` no longer calls JS `window.Bentley_FinishLaunching()`
* `ITMWebViewLogger.name` is now public and var
  • Loading branch information
tcobbs-bentley authored Jan 16, 2024
1 parent 6230d05 commit 05eb6fe
Show file tree
Hide file tree
Showing 15 changed files with 611 additions and 453 deletions.
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

0 comments on commit 05eb6fe

Please sign in to comment.