mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-27 18:17:40 +00:00
Merge pull request #2460 from keymanapp/feat/ios/adhoc-install-refactor
refactor(ios): Adhoc installation code refactor/reorg
This commit is contained in:
commit
692a94104f
7 changed files with 239 additions and 126 deletions
|
|
@ -210,6 +210,8 @@
|
|||
CE2B1E4821B60E8A007D092E /* DeviceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2B1E4521B60E7C007D092E /* DeviceKit.framework */; };
|
||||
CE2B1E4A21B60FB1007D092E /* DeviceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CE2B1E4521B60E7C007D092E /* DeviceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
CE67D961228A6F190029F2B5 /* KeyboardCommandStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE67D960228A6F190029F2B5 /* KeyboardCommandStructs.swift */; };
|
||||
CE71705823A9C14D00A924A1 /* ResourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE71705723A9C14D00A924A1 /* ResourceFileManager.swift */; };
|
||||
CE71705F23A9C97F00A924A1 /* PackageInstallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE71705E23A9C97F00A924A1 /* PackageInstallViewController.swift */; };
|
||||
CE808A48236697BE00713E6B /* DeviceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2B1E4521B60E7C007D092E /* DeviceKit.framework */; };
|
||||
CE808A4B236697D400713E6B /* ObjcExceptionBridging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1687ACCD1FD8DE5300926D69 /* ObjcExceptionBridging.framework */; };
|
||||
CE808A4D236697D500713E6B /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */; };
|
||||
|
|
@ -414,6 +416,8 @@
|
|||
CE24ECEF21B763740052D291 /* KeymanResponder+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeymanResponder+Types.swift"; sourceTree = "<group>"; };
|
||||
CE2B1E4521B60E7C007D092E /* DeviceKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceKit.framework; path = ../../Carthage/Build/iOS/DeviceKit.framework; sourceTree = "<group>"; };
|
||||
CE67D960228A6F190029F2B5 /* KeyboardCommandStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardCommandStructs.swift; sourceTree = "<group>"; };
|
||||
CE71705723A9C14D00A924A1 /* ResourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceFileManager.swift; sourceTree = "<group>"; };
|
||||
CE71705E23A9C97F00A924A1 /* PackageInstallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageInstallViewController.swift; sourceTree = "<group>"; };
|
||||
CECB38931F2199BC0098882F /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Reachability.h; path = KeymanEngine/lib/Reachability/Reachability.h; sourceTree = SOURCE_ROOT; };
|
||||
CECB38941F2199BC0098882F /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Reachability.m; path = KeymanEngine/lib/Reachability/Reachability.m; sourceTree = SOURCE_ROOT; };
|
||||
F243887E14BBD43000A3E055 /* KeymanEngineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeymanEngineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
|
@ -714,8 +718,6 @@
|
|||
9A9CB0852241717C00231FB9 /* LexicalModelRepositoryDelegate.swift */,
|
||||
C07A9D8D1FD1798900828ADD /* APIKeyboardRepository.swift */,
|
||||
9A9CB0812241704800231FB9 /* APILexicalModelRepository.swift */,
|
||||
CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */,
|
||||
CE22DFB9230B94DB00A4551C /* ResourceDownloadQueue.swift */,
|
||||
);
|
||||
path = KeyboardRepository;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -738,6 +740,17 @@
|
|||
name = KeymanWebView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE71705923A9C7D300A924A1 /* Resource Management */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */,
|
||||
CE22DFB9230B94DB00A4551C /* ResourceDownloadQueue.swift */,
|
||||
CE71705723A9C14D00A924A1 /* ResourceFileManager.swift */,
|
||||
CE71705E23A9C97F00A924A1 /* PackageInstallViewController.swift */,
|
||||
);
|
||||
path = "Resource Management";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F243887314BBD43000A3E055 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -825,6 +838,7 @@
|
|||
F273AB9615641D9300A47CEE /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE71705923A9C7D300A924A1 /* Resource Management */,
|
||||
165EB39F2098992D00040A69 /* Errors */,
|
||||
C055E6E81F99EA320035C2DD /* Extension */,
|
||||
C0452BA91F9F1CAF0064431A /* Model */,
|
||||
|
|
@ -1252,6 +1266,7 @@
|
|||
C0324B931F87689B00AF3785 /* KeymanURLProtocol.swift in Sources */,
|
||||
C05F43311FBD62550058CBD4 /* JSONDecoder.DateDecodingStrategy+ISO8601Fallback.swift in Sources */,
|
||||
C0452BAB1F9F1FE10064431A /* Language.swift in Sources */,
|
||||
CE71705F23A9C97F00A924A1 /* PackageInstallViewController.swift in Sources */,
|
||||
C06D37431F81F5C400F61AE0 /* KeyboardPickerBarButtonItem.swift in Sources */,
|
||||
C082CE151F90AFD400860F02 /* Collection+SafeAccess.swift in Sources */,
|
||||
C06D37441F81F5C400F61AE0 /* KeyboardNameTableViewCell.swift in Sources */,
|
||||
|
|
@ -1260,6 +1275,7 @@
|
|||
C08E69911FDA6F6F0026056B /* FullKeyboardID.swift in Sources */,
|
||||
9A9CB0822241704800231FB9 /* APILexicalModelRepository.swift in Sources */,
|
||||
C040E5101F8606E300901EE4 /* TextField.swift in Sources */,
|
||||
CE71705823A9C14D00A924A1 /* ResourceFileManager.swift in Sources */,
|
||||
9A082559227589360051EBB0 /* Formatter+ISODateExtension.swift in Sources */,
|
||||
C0324B8D1F87480700AF3785 /* TextFieldDelegateProxy.swift in Sources */,
|
||||
C075EB061F8EFF870041F4BD /* String+Helpers.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// PackageInstallViewController.swift
|
||||
// KeymanEngine
|
||||
//
|
||||
// Created by Joshua Horton on 12/18/19.
|
||||
// Copyright © 2019 SIL International. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
public class PackageInstallViewController: UIViewController {
|
||||
public typealias CompletionHandler = (Error?) -> Void
|
||||
|
||||
let package: KeymanPackage
|
||||
var wkWebView: WKWebView?
|
||||
let completionHandler: CompletionHandler
|
||||
|
||||
public init(for package: KeymanPackage, completionHandler: @escaping CompletionHandler) {
|
||||
self.package = package
|
||||
self.completionHandler = completionHandler
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
_ = view
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func loadView() {
|
||||
wkWebView = WKWebView.init(frame: .zero)
|
||||
wkWebView!.backgroundColor = .white
|
||||
view = wkWebView!
|
||||
|
||||
// Ensure the web view fills its available space.
|
||||
wkWebView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
let cancelBtn = UIBarButtonItem(title: "Cancel", style: .plain,
|
||||
target: self,
|
||||
action: #selector(cancelBtnHandler))
|
||||
let installBtn = UIBarButtonItem(title: "Install", style: .plain,
|
||||
target: self,
|
||||
action: #selector(installBtnHandler))
|
||||
|
||||
navigationItem.leftBarButtonItem = cancelBtn
|
||||
navigationItem.rightBarButtonItem = installBtn
|
||||
}
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
wkWebView?.loadHTMLString(package.infoHtml(), baseURL: nil)
|
||||
}
|
||||
|
||||
@objc func cancelBtnHandler() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func installBtnHandler() {
|
||||
dismiss(animated: true, completion: {
|
||||
let package = self.package
|
||||
do {
|
||||
// Time to pass the package off to the final installers - the parse__KMP methods.
|
||||
// ... they should probably be moved to ResourceFileManager eventually.
|
||||
if package.isKeyboard() {
|
||||
try Manager.shared.parseKbdKMP(package.sourceFolder)
|
||||
} else {
|
||||
try Manager.parseLMKMP(package.sourceFolder)
|
||||
}
|
||||
self.completionHandler(nil)
|
||||
} catch {
|
||||
log.error(error as! KMPError)
|
||||
self.completionHandler(error)
|
||||
}
|
||||
|
||||
//this can fail gracefully and not show errors to users
|
||||
do {
|
||||
try FileManager.default.removeItem(at: package.sourceFolder)
|
||||
} catch {
|
||||
log.error("unable to delete temp files: \(error)")
|
||||
self.completionHandler(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// ResourceFileManager.swift
|
||||
// KeymanEngine
|
||||
//
|
||||
// Created by Joshua Horton on December 18, 2019.
|
||||
// Copyright © 2019 SIL International. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* This class stores common methods used for installing language resources, regardless of source.
|
||||
*
|
||||
* It also contains methods for general-purpose installation of language resources from .kmp files.
|
||||
*/
|
||||
public class ResourceFileManager {
|
||||
public static let shared = ResourceFileManager()
|
||||
|
||||
fileprivate init() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apple doesn't provide a method that performs copy-and-overwrite functionality. This function fills in that gap.
|
||||
*/
|
||||
private func copyWithOverwrite(from source: URL, to destination: URL) throws {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// For now, we'll always allow overwriting.
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
try fileManager.removeItem(at: destination)
|
||||
}
|
||||
|
||||
// Throws an error if the destination file already exists, and there's no
|
||||
// built-in override parameter. Hence, the previous if-block.
|
||||
try fileManager.copyItem(at: source, to: destination)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function to "import" a file from outside the app's designated file system area to a new location within,
|
||||
* copying the original. It will be placed within the app's Documents folder.
|
||||
*
|
||||
* Returns the app-owned destination path, usable for subsequent file operations.
|
||||
*/
|
||||
public func importFile(_ url: URL) -> URL? {
|
||||
var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
destinationUrl.appendPathComponent(url.lastPathComponent)
|
||||
|
||||
do {
|
||||
try copyWithOverwrite(from: url, to: destinationUrl)
|
||||
return destinationUrl
|
||||
} catch {
|
||||
log.error(error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function to "install" external KMP files to within the Keyman app's alloted iOS file management domain.
|
||||
* Note that we don't request permissions to support opening/modifying files "in place," so we need to copy .kmps
|
||||
* before unzipping them.
|
||||
*
|
||||
* This implementation does not change how files are managed by the app; only where the file management code
|
||||
* is located.
|
||||
*/
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
public func installFile(_ url: URL) {
|
||||
// Once selected, start the standard install process.
|
||||
log.info("Installing KMP from \(url)")
|
||||
|
||||
// Step 1: Copy it to a temporary location, making it a .zip in the process
|
||||
var destinationUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
destinationUrl.appendPathComponent("\(url.lastPathComponent).zip")
|
||||
|
||||
do {
|
||||
try copyWithOverwrite(from: url, to: destinationUrl)
|
||||
installAdhocKeyboard(url: destinationUrl)
|
||||
} catch {
|
||||
showKMPError(KMPError.copyFiles)
|
||||
log.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
private func installAdhocKeyboard(url: URL) {
|
||||
let documentsDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
var destination = documentsDirectory
|
||||
destination.appendPathComponent("temp/\(url.lastPathComponent)")
|
||||
|
||||
KeymanPackage.extract(fileUrl: url, destination: destination, complete: { kmp in
|
||||
if let kmp = kmp {
|
||||
self.promptAdHocInstall(kmp)
|
||||
} else {
|
||||
self.showKMPError(KMPError.invalidPackage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
private func promptAdHocInstall(_ kmp: KeymanPackage) {
|
||||
let vc = PackageInstallViewController(for: kmp, completionHandler: { error in
|
||||
if let err = error {
|
||||
if let kmpError = err as? KMPError {
|
||||
self.showKMPError(kmpError)
|
||||
}
|
||||
} else {
|
||||
self.showSimpleAlert(title: "Success", message: "Installed successfully.")
|
||||
}
|
||||
})
|
||||
|
||||
let nvc = UINavigationController.init(rootViewController: vc)
|
||||
UIApplication.shared.keyWindow?.rootViewController?.present(nvc, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
public func showKMPError(_ error: KMPError) {
|
||||
showSimpleAlert(title: "Error", message: error.rawValue)
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension, unavailable)
|
||||
public func showSimpleAlert(title: String, message: String) {
|
||||
let alertController = UIAlertController(title: title, message: message,
|
||||
preferredStyle: UIAlertController.Style.alert)
|
||||
alertController.addAction(UIAlertAction(title: "OK",
|
||||
style: UIAlertAction.Style.default,
|
||||
handler: nil))
|
||||
|
||||
UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,27 +22,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
func application(_ app: UIApplication, open url: URL,
|
||||
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// .kmp package install, Keyman 10 onwards
|
||||
var destinationUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
destinationUrl.appendPathComponent("\(url.lastPathComponent).zip")
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
// We really should validate that it is a .kmp first... but the app doesn't yet
|
||||
// process URL links, so it's fine for now. (Will change with QR code stuff.)
|
||||
|
||||
// For now, we'll always allow overwriting.
|
||||
if fileManager.fileExists(atPath: destinationUrl.path) {
|
||||
try fileManager.removeItem(at: destinationUrl)
|
||||
}
|
||||
|
||||
// Throws an error if the destination file already exists, and there's no
|
||||
// built-in override parameter. Hence, the previous if-block.
|
||||
try fileManager.copyItem(at: url, to: destinationUrl)
|
||||
installAdhocKeyboard(url: destinationUrl)
|
||||
return true
|
||||
} catch {
|
||||
showKMPError(KMPError.copyFiles)
|
||||
log.error(error)
|
||||
guard let destinationUrl = ResourceFileManager.shared.importFile(url) else {
|
||||
return false
|
||||
}
|
||||
|
||||
ResourceFileManager.shared.installFile(destinationUrl)
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
|
|
@ -80,13 +68,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
|
||||
NotificationCenter.default.post(name: launchedFromUrlNotification, object: self,
|
||||
userInfo: [urlKey: url]
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
_overlayWindow = nil
|
||||
FontManager.shared.unregisterCustomFonts()
|
||||
|
|
@ -123,103 +104,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
return _overlayWindow!
|
||||
}
|
||||
|
||||
public func installAdhocKeyboard(url: URL) {
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
var destination = documentsDirectory
|
||||
destination.appendPathComponent("temp/\(url.lastPathComponent)")
|
||||
|
||||
KeymanPackage.extract(fileUrl: url, destination: destination, complete: { kmp in
|
||||
if let kmp = kmp {
|
||||
self.promptAdHocInstall(kmp)
|
||||
} else {
|
||||
self.showKMPError(KMPError.invalidPackage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func showKMPError(_ error: KMPError) {
|
||||
showSimpleAlert(title: "Error", message: error.rawValue)
|
||||
}
|
||||
|
||||
public func showSimpleAlert(title: String, message: String) {
|
||||
let alertController = UIAlertController(title: title, message: message,
|
||||
preferredStyle: UIAlertController.Style.alert)
|
||||
alertController.addAction(UIAlertAction(title: "OK",
|
||||
style: UIAlertAction.Style.default,
|
||||
handler: nil))
|
||||
|
||||
self.window?.rootViewController?.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
public func promptAdHocInstall(_ kmp: KeymanPackage) {
|
||||
_adhocDirectory = kmp.sourceFolder
|
||||
let isKbd = kmp.isKeyboard()
|
||||
|
||||
let vc = UIViewController()
|
||||
vc.view.backgroundColor = .red
|
||||
let wkWebView = WKWebView.init(frame: vc.view.frame)
|
||||
wkWebView.backgroundColor = .white
|
||||
vc.view.addSubview(wkWebView)
|
||||
let cancelBtn = UIBarButtonItem(title: "Cancel", style: .plain,
|
||||
target: self,
|
||||
action: #selector(cancelAdHocBtnHandler))
|
||||
let installBtn = UIBarButtonItem(title: "Install", style: .plain,
|
||||
target: self,
|
||||
action: (isKbd ? #selector(installAdHocKeyboardBtnHandler) :
|
||||
#selector(installAdHocLexicalModelBtnHandler)) )
|
||||
vc.navigationItem.leftBarButtonItem = cancelBtn
|
||||
vc.navigationItem.rightBarButtonItem = installBtn
|
||||
let nvc = UINavigationController.init(rootViewController: vc)
|
||||
|
||||
self.window?.rootViewController?.present(nvc, animated: true, completion: {
|
||||
wkWebView.loadHTMLString(kmp.infoHtml(), baseURL: nil)
|
||||
})
|
||||
}
|
||||
|
||||
@objc func installAdHocKeyboardBtnHandler() {
|
||||
if let adhocDir = _adhocDirectory {
|
||||
self.window?.rootViewController?.dismiss(animated: true, completion: {
|
||||
do {
|
||||
try Manager.shared.parseKbdKMP(adhocDir)
|
||||
self.showSimpleAlert(title: "Success", message: "Installed successfully.")
|
||||
} catch {
|
||||
self.showKMPError(error as! KMPError)
|
||||
}
|
||||
|
||||
//this can fail gracefully and not show errors to users
|
||||
do {
|
||||
try FileManager.default.removeItem(at: adhocDir)
|
||||
} catch {
|
||||
log.error("unable to delete temp files")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@objc func installAdHocLexicalModelBtnHandler() {
|
||||
if let adhocDir = _adhocDirectory {
|
||||
self.window?.rootViewController?.dismiss(animated: true, completion: {
|
||||
do {
|
||||
try Manager.parseLMKMP(adhocDir)
|
||||
self.showSimpleAlert(title: "Success", message: "Installed successfully.")
|
||||
} catch {
|
||||
self.showKMPError(error as! KMPError)
|
||||
}
|
||||
|
||||
//this can fail gracefully and not show errors to users
|
||||
do {
|
||||
try FileManager.default.removeItem(at: adhocDir)
|
||||
} catch {
|
||||
log.error("unable to delete temp files")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@objc func cancelAdHocBtnHandler() {
|
||||
self.window?.rootViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func registerCustomFonts() {
|
||||
FontManager.shared.registerCustomFonts()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg
|
|||
if launchUrl != nil {
|
||||
perform(#selector(self.dismissActivityIndicator), with: nil, afterDelay: 1.0)
|
||||
let error = notification.error
|
||||
appDelegate.showSimpleAlert(title: "Keyboard Download Error", message: error.localizedDescription)
|
||||
ResourceFileManager.shared.showSimpleAlert(title: "Keyboard Download Error", message: error.localizedDescription)
|
||||
launchUrl = nil
|
||||
}
|
||||
}
|
||||
|
|
@ -825,7 +825,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg
|
|||
if let urlString = params["url"] {
|
||||
// Download and set custom keyboard
|
||||
guard let url = URL(string: urlString) else {
|
||||
appDelegate.showSimpleAlert(title: "Custom Keyboard",
|
||||
ResourceFileManager.shared.showSimpleAlert(title: "Custom Keyboard",
|
||||
message: "The keyboard could not be installed: Invalid Url")
|
||||
launchUrl = nil
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue