spiegel-keyman/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift
2020-08-05 12:19:43 +07:00

356 lines
13 KiB
Swift

//
// PackageInstallViewController.swift
// KeymanEngine
//
// Created by Joshua Horton on 12/18/19.
// Copyright © 2019 SIL International. All rights reserved.
//
import Foundation
import WebKit
import DeviceKit
public class PackageInstallViewController<Resource: LanguageResource>: UIViewController, UITableViewDelegate, UITableViewDataSource, UITabBarControllerDelegate {
private enum NavigationMode: Int { // b/c hashable
// left nav
case cancel
case back
// right nav
case none
case next
case install
}
public typealias CompletionHandler = ([Resource.FullID]?) -> Void
// Needed to support iOS 9 + 10.
@IBOutlet weak var webViewContainer: UIView!
// Common fields between both layout formats.
@IBOutlet weak var lblVersion: UILabel!
@IBOutlet weak var lblCopyright: UILabel!
@IBOutlet weak var languageTable: UITableView!
// iPad layout only - May be altered programatically.
@IBOutlet weak var ipadTagWidthConstraint: NSLayoutConstraint?
// iPhone layout only
// Do _NOT_ use `weak` here - things break otherwise.
@IBOutlet var iphoneTabViewController: UITabBarController!
let package: Resource.Package
var wkWebView: WKWebView?
let completionHandler: CompletionHandler
let defaultLanguageCode: String
let associators: [LanguagePickAssociator]
let languages: [Language]
private var leftNavMode: NavigationMode = .cancel
private var rightNavMode: NavigationMode = .none
private var navMapping: [NavigationMode : UIBarButtonItem] = [:]
public init(for package: Resource.Package,
defaultLanguageCode: String? = nil,
languageAssociators: [LanguagePickAssociator] = [],
completionHandler: @escaping CompletionHandler) {
self.package = package
self.completionHandler = completionHandler
self.languages = package.languages
let packageFirstLangCode = package.installableResourceSets[0][0].languageID
self.defaultLanguageCode = package.languages.contains(where: { $0.id == defaultLanguageCode }) ? defaultLanguageCode! : packageFirstLangCode
self.associators = languageAssociators
var xib: String
if Device.current.isPad {
xib = "PackageInstallView_iPad"
} else {
xib = "PackageInstallView_iPhone"
}
super.init(nibName: xib, bundle: Bundle.init(for: PackageInstallViewController.self))
_ = view
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidLoad() {
wkWebView = WKWebView.init(frame: webViewContainer.frame)
wkWebView!.backgroundColor = .white
wkWebView!.translatesAutoresizingMaskIntoConstraints = false
webViewContainer.addSubview(wkWebView!)
// Ensure the web view fills its available space. Required b/c iOS 9 & 10 cannot load
// these correctly from XIBs.
wkWebView!.topAnchor.constraint(equalTo: webViewContainer.topAnchor).isActive = true
wkWebView!.leadingAnchor.constraint(equalTo: webViewContainer.leadingAnchor).isActive = true
wkWebView!.bottomAnchor.constraint(equalTo: webViewContainer.bottomAnchor).isActive = true
wkWebView!.trailingAnchor.constraint(equalTo: webViewContainer.trailingAnchor).isActive = true
loadNavigationItems()
leftNavigationMode = .cancel
rightNavigationMode = .install
navigationItem.title = package.name
// Initialize the package info labels and the language-picker table.
let versionFormat = NSLocalizedString("installer-label-version", bundle: engineBundle, comment: "")
lblVersion.text = String.localizedStringWithFormat(versionFormat, package.version.description)
if let copyright = package.metadata.info?.copyright?.description {
lblCopyright.text = copyright
} else {
lblCopyright.isHidden = true
}
languageTable.delegate = self
languageTable.dataSource = self
// Set the default selection.
let defaultRow = languages.firstIndex(where: { $0.id == self.defaultLanguageCode })!
let defaultIndexPath = IndexPath(row: defaultRow, section: 0)
languageTable.selectRow(at: defaultIndexPath, animated: false, scrollPosition: .top)
languageTable.cellForRow(at: defaultIndexPath)?.accessoryType = .checkmark
associators.forEach {
$0.pickerInitialized()
$0.selectLanguages(Set([self.defaultLanguageCode]))
}
// iPhone-only layout setup
if let tabVC = iphoneTabViewController {
tabVC.delegate = self
let tabView = tabVC.view!
self.view.addSubview(tabVC.view)
tabView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
tabView.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
tabView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tabView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tabVC.tabBar.items![0].title = NSLocalizedString("installer-label-package-info", bundle: engineBundle, comment: "")
tabVC.tabBar.items![1].title = NSLocalizedString("installer-label-select-languages", bundle: engineBundle, comment: "")
// Prevents the tab view from being obscured by the navigation bar.
// It's weird, yeah.
edgesForExtendedLayout = []
rightNavigationMode = .next
if #available(*, iOS 13.4) {
// No icon issues here.
} else {
tabVC.tabBar.items![0].titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -16)
tabVC.tabBar.items![1].titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -16)
}
}
// If there's only one language in the package, hide the language picker.
if languages.count <= 1 {
if let sourceTagConstraint = ipadTagWidthConstraint {
// Rebuild the width constraint, setting it to zero width.
// The 'proper' iOS approach is to disable the old one and replace it with a new one.
sourceTagConstraint.isActive = false
let hideTagConstraint = NSLayoutConstraint(item: sourceTagConstraint.firstItem as Any,
attribute: sourceTagConstraint.firstAttribute,
relatedBy: sourceTagConstraint.relation,
toItem: sourceTagConstraint.secondItem,
attribute: sourceTagConstraint.secondAttribute,
multiplier: 0,
constant: 0)
hideTagConstraint.isActive = true
} else if let tabVC = iphoneTabViewController {
// Hide the tab bar, making it appear as if only the "info view" portion exists.
tabVC.tabBar.isHidden = true
// Since we won't be showing the user a language list, allow them to install from the info view.
rightNavigationMode = .install
}
}
}
override public func viewWillAppear(_ animated: Bool) {
if let readmeURL = package.readmePageURL {
wkWebView?.loadFileURL(readmeURL, allowingReadAccessTo: package.sourceFolder)
} else {
wkWebView?.loadHTMLString(package.infoHtml(), baseURL: nil)
}
}
private func loadNavigationItems() {
navMapping[.cancel] = UIBarButtonItem(title: NSLocalizedString("command-cancel", bundle: engineBundle, comment: ""), style: .plain,
target: self,
action: #selector(cancelBtnHandler))
navMapping[.back] = UIBarButtonItem(title: NSLocalizedString("command-back", bundle: engineBundle, comment: ""), style: .plain,
target: self,
action: #selector(backBtnHandler))
navMapping[.none] = UIBarButtonItem(title: NSLocalizedString("command-install", bundle: engineBundle, comment: ""), style: .plain,
target: self,
action: nil)
navMapping[.none]!.isEnabled = false
navMapping[.next] = UIBarButtonItem(title: NSLocalizedString("command-next", bundle: engineBundle, comment: ""), style: .plain,
target: self,
action: #selector(nextBtnHandler))
navMapping[.install] = UIBarButtonItem(title: NSLocalizedString("command-install", bundle: engineBundle, comment: ""), style: .plain,
target: self,
action: #selector(installBtnHandler))
}
private var leftNavigationMode: NavigationMode {
get {
return leftNavMode
}
set(mode) {
leftNavMode = mode
navigationItem.leftBarButtonItem = navMapping[mode]
}
}
private var rightNavigationMode: NavigationMode {
get {
return rightNavMode
}
set(mode) {
rightNavMode = mode
navigationItem.rightBarButtonItem = navMapping[mode]
}
}
public func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController.view.tag == 1, languages.count <= 1 {
return false
} else {
return true
}
}
public func tabBarController(_ tabBarController: UITabBarController, didSelect item: UIViewController) {
if item.view.tag == 0 {
leftNavigationMode = .cancel
rightNavigationMode = .next
} else if item.view.tag == 1 {
leftNavigationMode = .back
if languageTable.indexPathsForSelectedRows?.count ?? 0 == 0 {
rightNavigationMode = .none
} else {
rightNavigationMode = .install
}
}
}
@objc func cancelBtnHandler() {
dismiss(animated: true, completion: {
self.completionHandler(nil)
self.associators.forEach { $0.pickerDismissed() }
})
}
@objc func backBtnHandler() {
guard let tabVC = iphoneTabViewController else {
return
}
// Should only occur for iPhone layouts
tabVC.selectedIndex -= 1
// Is not automatically called!
tabBarController(tabVC, didSelect: tabVC.viewControllers![tabVC.selectedIndex])
}
@objc func nextBtnHandler() {
guard let tabVC = iphoneTabViewController else {
return
}
// Should only occur for iPhone layouts
tabVC.selectedIndex += 1
// Is not automatically called!
tabBarController(tabVC, didSelect: tabVC.viewControllers![tabVC.selectedIndex])
}
@objc func installBtnHandler() {
dismiss(animated: true, completion: {
let selectedItems = self.languageTable.indexPathsForSelectedRows ?? []
let selectedLanguageCodes = selectedItems.map { self.languages[$0.row].id }
let selectedResources = self.package.installableResourceSets.flatMap { $0.filter { selectedLanguageCodes.contains($0.languageID) }} as! [Resource]
self.completionHandler(selectedResources.map { $0.typedFullID })
self.associators.forEach { $0.pickerFinalized() }
})
}
public func tableView(_ tableView: UITableView, titleForHeaderInSection: Int) -> String? {
return NSLocalizedString("installer-section-available-languages", bundle: engineBundle, comment: "")
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return languages.count
default:
return 0
}
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "any"
var cell: UITableViewCell
if let reusedCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) {
cell = reusedCell
// The checkmark is not properly managed by default.
let shouldCheck = languageTable.indexPathsForSelectedRows?.contains(indexPath) ?? false
// Note for later: also ensure that it wasn't already installed. (exception to rule above)
cell.accessoryType = shouldCheck ? .checkmark : .none
} else {
let selectionColor = UIView()
if #available(iOSApplicationExtension 11.0, *) {
selectionColor.backgroundColor = UIColor(named: "SelectionPrimary")
} else {
selectionColor.backgroundColor = Colors.selectionPrimary
}
cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
cell.selectedBackgroundView = selectionColor
}
switch indexPath.section {
case 0:
let index = indexPath.row
cell.detailTextLabel?.text = languages[index].name
return cell
default:
return cell
}
}
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
navigationItem.rightBarButtonItem?.isEnabled = true
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
associators.forEach { $0.selectLanguages( Set([languages[indexPath.row].id]) ) }
}
public func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if languageTable.indexPathsForSelectedRows?.count ?? 0 == 0 {
rightNavigationMode = .none
} else {
rightNavigationMode = .install
}
tableView.cellForRow(at: indexPath)?.accessoryType = .none
associators.forEach { $0.deselectLanguages( Set([languages[indexPath.row].id]) ) }
}
}