Merge pull request #4573 from keymanapp/chore/ios/4339-progress-feedback

chore(ios): better visual feedback for keyboard search during poor internet connectivity
This commit is contained in:
Joshua Horton 2021-03-03 14:35:25 +07:00 committed by GitHub
commit 4ab75cef6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 37 deletions

View file

@ -121,7 +121,9 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
var associationQueryProgress: [Int: LanguagePickAssociator.Progress] = [:]
var installProgressMap: [KeymanPackage.Key: PackageInstallResult?] = [:]
let progressCallback: ProgressReceiver
let externalProgressCallback: ProgressReceiver
var promptProgressCallback: ProgressReceiver?
let downloadManager: ResourceDownloadManager
var isCancelled = false
@ -129,7 +131,7 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
init(with associationSpecs: [Associator], downloadManager: ResourceDownloadManager, receiver: @escaping ProgressReceiver) {
self.associationSpecs = associationSpecs
self.downloadManager = downloadManager
self.progressCallback = receiver
self.externalProgressCallback = receiver
}
// Since progress info is stored here, it makes the most sense to track progress-related
@ -139,12 +141,19 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
return !isCancelled && pickingCompleted
}
internal func notifyProgress(_ status: Progress) {
// Prompt gets first dibs - that way, if a prompt exists, its UI code
// executes before control transfers back to other modules.
self.promptProgressCallback?(status)
self.externalProgressCallback(status)
}
/**
* Computes the initial level of progress made toward the overall installation at the time that language selections were
* finalized.
*/
internal func initializeProgress() {
self.progressCallback(.starting)
notifyProgress(.starting)
}
/**
@ -153,7 +162,7 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
internal func reportProgress(complete: Bool = false) {
if reportsProgress {
if complete {
progressCallback(.complete)
notifyProgress(.complete)
} else {
//progressCallback(.inProgress)
}
@ -161,7 +170,7 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
}
internal func reportCancelled() {
progressCallback(.cancelled)
notifyProgress(.cancelled)
}
}
@ -388,8 +397,7 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
* May only be called once during the lifetime of its instance and is mutually exclusive with `pickLanguages`,
* the programmatic alternative.
*/
public func promptForLanguages(inNavigationVC navVC: UINavigationController,
uiCompletionHandler: @escaping (() -> 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<Resource: LanguageResource, Package: Ty
closureShared.installGroup.enter()
let wrappedCompletionHandler = {
uiCompletionHandler()
self.closureShared.installGroup.leave()
}
@ -410,6 +417,9 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
pickingCompletionHandler: coreInstallationClosure(),
uiCompletionHandler: wrappedCompletionHandler)
closureShared.promptProgressCallback = { progress in
pickerPrompt.progressUpdate(progress)
}
navVC.pushViewController(pickerPrompt, animated: true)
}

View file

@ -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) {

View file

@ -59,6 +59,8 @@ public class PackageInstallViewController<Resource: LanguageResource>: 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<Resource: LanguageResource>: UIViewCon
}
set(mode) {
leftNavMode = mode
if mayPick {
leftNavMode = mode
navigationItem.leftBarButtonItem = navMapping[mode]
navigationItem.leftBarButtonItem = navMapping[mode]
}
}
}
@ -279,9 +283,11 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
}
set(mode) {
rightNavMode = mode
if mayPick {
rightNavMode = mode
navigationItem.rightBarButtonItem = navMapping[mode]
navigationItem.rightBarButtonItem = navMapping[mode]
}
}
}
@ -349,20 +355,29 @@ public class PackageInstallViewController<Resource: LanguageResource>: 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<Resource: LanguageResource>: 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<Resource: LanguageResource>: 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<Resource: LanguageResource>: UIViewCon
associators.forEach { $0.deselectLanguages( Set([languages[indexPath.row].id]) ) }
}
internal func progressUpdate<Package: TypedKeymanPackage<Resource>>(_ status: AssociatingPackageInstaller<Resource, Package>.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)
}
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}