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 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) { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift index 26cefffefa..748edfacab 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift @@ -59,6 +59,8 @@ public class PackageInstallViewController: UIViewCon private var navMapping: [NavigationMode : UIBarButtonItem] = [:] private var dismissalBlock: (() -> Void)? = nil + private weak var welcomeView: UIView? + private var mayPick: Bool = true public init(for package: Resource.Package, defaultLanguageCode: String? = nil, @@ -267,9 +269,11 @@ public class PackageInstallViewController: UIViewCon } set(mode) { - leftNavMode = mode + if mayPick { + leftNavMode = mode - navigationItem.leftBarButtonItem = navMapping[mode] + navigationItem.leftBarButtonItem = navMapping[mode] + } } } @@ -279,9 +283,11 @@ public class PackageInstallViewController: UIViewCon } set(mode) { - rightNavMode = mode + if mayPick { + rightNavMode = mode - navigationItem.rightBarButtonItem = navMapping[mode] + navigationItem.rightBarButtonItem = navMapping[mode] + } } } @@ -349,20 +355,29 @@ 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 and nav-bar related manipulation. + self.navigationItem.leftBarButtonItem?.isEnabled = false + self.rightNavigationMode = .none + self.mayPick = false + + 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,8 +398,16 @@ 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() + self.dismissalBlock = dismissalBlock + onWelcomeDismissed() } } @@ -393,10 +416,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 +561,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) } } }