From 39064e19354430f0754be1366f99c5d916852694 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Mar 2021 10:17:15 +0700 Subject: [PATCH 1/3] feat(ios): adds a progress bar to search during page loads --- .../KeyboardSearchViewController.swift | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift index a46a61e59d..1f9e5119e5 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift @@ -69,6 +69,9 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat private let languageCode: String? private let session: URLSession + private var progressView: UIProgressView? + private var observation: NSKeyValueObservation? = nil + private static var ENDPOINT_ROOT: URL { var baseURL = KeymanHosts.KEYMAN_COM baseURL.appendPathComponent("go") @@ -93,6 +96,10 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat fatalError("init(coder:) has not been implemented") } + deinit { + observation = nil + } + public override func loadView() { let webView = WKWebView() webView.navigationDelegate = self @@ -104,6 +111,18 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat webView.load(URLRequest(url: KeyboardSearchViewController.ENDPOINT_ROOT)) } + progressView = UIProgressView(progressViewStyle: .bar) + progressView!.translatesAutoresizingMaskIntoConstraints = false + observation = webView.observe(\.estimatedProgress) { _, _ in + if let progressView = self.progressView { + progressView.setProgress(Float(webView.estimatedProgress), animated: true) + progressView.isHidden = progressView.progress > 0.99 + } + } + progressView!.setProgress(1, animated: false) + progressView!.isHidden = true + + webView.addSubview(progressView!) view = webView } @@ -131,6 +150,25 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat } decisionHandler(.allow) + // Makes it clear that there IS a progress bar, in case of super-slow response. + progressView?.setProgress(0, animated: false) + // This way, if the load is instant, the 0.01 doesn't really stand out. + progressView?.setProgress(0.01, animated: true) + progressView?.isHidden = false + } + + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let navVC = self.navigationController { + progressView?.topAnchor.constraint(equalTo: navVC.navigationBar.bottomAnchor).isActive = true + } else { + progressView?.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true + } + + progressView?.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true + progressView?.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true } override public func viewWillDisappear(_ animated: Bool) { From 0f7f2739f580d587ebd0c96248115bbf75168d39 Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Mar 2021 13:01:49 +0700 Subject: [PATCH 2/3] fix(ios): better ux for net-delayed package installations, kbd display mgmt --- .../AssociatingPackageInstaller.swift | 26 +++++-- .../PackageInstallViewController.swift | 76 ++++++++++++++++--- .../ResourceFileManager.swift | 16 ++-- .../Keyman/PackageBrowserViewController.swift | 4 +- 4 files changed, 90 insertions(+), 32 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/AssociatingPackageInstaller.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/AssociatingPackageInstaller.swift index 9eb8791b9a..2ac9436d30 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/AssociatingPackageInstaller.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/AssociatingPackageInstaller.swift @@ -121,7 +121,9 @@ public class AssociatingPackageInstaller Void)) { + public func promptForLanguages(inNavigationVC navVC: UINavigationController) { guard self.associationQueriers == nil, !closureShared.pickingCompleted else { fatalError("Invalid state - language picking has already been triggered.") } @@ -400,7 +408,6 @@ public class AssociatingPackageInstaller: UIViewCon private var navMapping: [NavigationMode : UIBarButtonItem] = [:] private var dismissalBlock: (() -> Void)? = nil + private weak var welcomeView: UIView? public init(for package: Resource.Package, defaultLanguageCode: String? = nil, @@ -349,20 +350,27 @@ public class PackageInstallViewController: UIViewCon Manager.shared.shouldReloadKeyboard = true self.pickingCompletionHandler(selectedResources.map { $0.typedFullID }) - let dismissalBlock = { - if let nvc = self.navigationController { - self.dismiss(animated: true) - nvc.popToRootViewController(animated: false) - } else { // Otherwise, if the root view of a navigation controller, dismiss it outright. (pop not available) - self.dismiss(animated: true) - } + // Prevent swipe dismissal. + if #available(iOSApplicationExtension 13.0, *) { + self.isModalInPresentation = true + } + // No more selection-manipulation allowed. + // This matters when there's no welcome page available. + languageTable.isUserInteractionEnabled = false + // Prevent extra 'install' commands. + self.rightNavigationMode = .none + + let dismissalBlock = { self.associators.forEach { $0.pickerFinalized() } } // First, show the package's welcome - if it exists. if let welcomeVC = PackageWebViewController(for: package, page: .welcome) { - self.dismissalBlock = dismissalBlock + // Prevent swipe dismissal. + if #available(iOSApplicationExtension 13.0, *) { + welcomeVC.isModalInPresentation = true + } let subNavVC = UINavigationController(rootViewController: welcomeVC) _ = subNavVC.view @@ -383,6 +391,13 @@ public class PackageInstallViewController: UIViewCon subNavVC.presentationController?.delegate = self self.present(subNavVC, animated: true, completion: nil) + self.welcomeView = welcomeVC.view + + self.dismissalBlock = { + // Tells the user that we've received the 'done' command. + doneItem.isEnabled = false + dismissalBlock() + } } else { dismissalBlock() } @@ -393,10 +408,33 @@ public class PackageInstallViewController: UIViewCon } @objc private func onWelcomeDismissed() { - self.dismissalBlock?() - self.dismissalBlock = nil + if let dismissalBlock = self.dismissalBlock { + dismissalBlock() + self.dismissalBlock = nil - self.uiCompletionHandler() + // Tell our owner (the AssociatingPackageInstaller) that all UI interactions are done. + // Triggers synchronization code, so make sure it only runs once! + self.uiCompletionHandler() + } + + // Show a spinner forever. + // When installation is complete, this controller's view will be dismissed, + // removing said spinner. + let activitySpinner = Alerts.constructActivitySpinner() + + // Determine the top-most view. If we're presenting the welcome page, THAT. + // If we aren't, our directly-owned view should be the top-most. + let activeView: UIView = self.welcomeView ?? view + + activitySpinner.center = activeView.center + activitySpinner.startAnimating() + activeView.addSubview(activitySpinner) + + activitySpinner.centerXAnchor.constraint(equalTo: activeView.centerXAnchor).isActive = true + activitySpinner.centerYAnchor.constraint(equalTo: activeView.centerYAnchor).isActive = true + + // Note: we do NOT block user interaction; we merely bind them to the active view. + // Why prevent them from reading more on the welcome page while they wait? } public func tableView(_ tableView: UITableView, titleForHeaderInSection: Int) -> String? { @@ -515,4 +553,20 @@ public class PackageInstallViewController: UIViewCon associators.forEach { $0.deselectLanguages( Set([languages[indexPath.row].id]) ) } } + + internal func progressUpdate>(_ status: AssociatingPackageInstaller.Progress) where Resource.Package == Package { + switch(status) { + case .starting, .inProgress: + // nothing worth note + break + // All UI interactions have been completed AND installation is fully complete. + case .complete, .cancelled: + if let nvc = self.navigationController { + self.dismiss(animated: true) + nvc.popToRootViewController(animated: false) + } else { // Otherwise, if the root view of a navigation controller, dismiss it outright. (pop not available) + self.dismiss(animated: true) + } + } + } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index 2039f78480..68ab97b707 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -228,27 +228,23 @@ public class ResourceFileManager { activitySpinner.centerXAnchor.constraint(equalTo: rootVC.view.centerXAnchor).isActive = true activitySpinner.centerYAnchor.constraint(equalTo: rootVC.view.centerYAnchor).isActive = true rootVC.view.isUserInteractionEnabled = false - } else if status == .complete { + } else if status == .complete || status == .cancelled { // Report completion! activitySpinner.stopAnimating() activitySpinner.removeFromSuperview() rootVC.view.isUserInteractionEnabled = true - rootVC.dismiss(animated: true, completion: nil) + rootVC.dismiss(animated: true) { + Manager.shared.showKeyboard() + } successHandler?(package) } } if let navVC = rootVC as? UINavigationController { - packageInstaller.promptForLanguages(inNavigationVC: navVC) { - // The user will be on the main screen after this, so we should resummon the keyboard. - Manager.shared.showKeyboard() - } + packageInstaller.promptForLanguages(inNavigationVC: navVC) } else { let nvc = UINavigationController.init() - packageInstaller.promptForLanguages(inNavigationVC: nvc) { - // The user will be on the main screen after this, so we should resummon the keyboard. - Manager.shared.showKeyboard() - } + packageInstaller.promptForLanguages(inNavigationVC: nvc) rootVC.present(nvc, animated: true, completion: nil) } } diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift index ad27110129..7388147997 100644 --- a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -116,9 +116,7 @@ class PackageBrowserViewController: UIDocumentPickerViewController, UIDocumentPi } if let navVC = self.navVC { - packageInstaller.promptForLanguages(inNavigationVC: navVC) { - // do nothing; the Settings menu dismissal will take care of displaying the keyboard - } + packageInstaller.promptForLanguages(inNavigationVC: navVC) } } } From d2a70127169ec880c51b7d0fcdb4aa7f00a4845e Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 3 Mar 2021 13:21:49 +0700 Subject: [PATCH 3/3] fix(ios): handling of cases with no welcome.htm --- .../PackageInstallViewController.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift index ac2d20d316..748edfacab 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift @@ -60,6 +60,7 @@ public class PackageInstallViewController: UIViewCon private var dismissalBlock: (() -> Void)? = nil private weak var welcomeView: UIView? + private var mayPick: Bool = true public init(for package: Resource.Package, defaultLanguageCode: String? = nil, @@ -268,9 +269,11 @@ public class PackageInstallViewController: UIViewCon } set(mode) { - leftNavMode = mode + if mayPick { + leftNavMode = mode - navigationItem.leftBarButtonItem = navMapping[mode] + navigationItem.leftBarButtonItem = navMapping[mode] + } } } @@ -280,9 +283,11 @@ public class PackageInstallViewController: UIViewCon } set(mode) { - rightNavMode = mode + if mayPick { + rightNavMode = mode - navigationItem.rightBarButtonItem = navMapping[mode] + navigationItem.rightBarButtonItem = navMapping[mode] + } } } @@ -358,8 +363,10 @@ public class PackageInstallViewController: UIViewCon // No more selection-manipulation allowed. // This matters when there's no welcome page available. languageTable.isUserInteractionEnabled = false - // Prevent extra 'install' commands. + // Prevent extra 'install' commands and nav-bar related manipulation. + self.navigationItem.leftBarButtonItem?.isEnabled = false self.rightNavigationMode = .none + self.mayPick = false let dismissalBlock = { self.associators.forEach { $0.pickerFinalized() } @@ -399,7 +406,8 @@ public class PackageInstallViewController: UIViewCon dismissalBlock() } } else { - dismissalBlock() + self.dismissalBlock = dismissalBlock + onWelcomeDismissed() } }