mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-28 11:17:45 +00:00
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:
commit
4ab75cef6d
5 changed files with 141 additions and 37 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue