diff --git a/ios/Cartfile b/ios/Cartfile index 9d2cc3e3b6..464dacd815 100644 --- a/ios/Cartfile +++ b/ios/Cartfile @@ -1,3 +1,3 @@ github "marmelroy/Zip" github "DaveWoodCom/XCGLogger" ~> 6.1.0 -github "dennisweissmann/DeviceKit" ~> 1.3 \ No newline at end of file +github "dennisweissmann/DeviceKit" ~> 1.11 diff --git a/ios/Cartfile.resolved b/ios/Cartfile.resolved index 54fa74f159..f112b39e56 100644 --- a/ios/Cartfile.resolved +++ b/ios/Cartfile.resolved @@ -1,3 +1,3 @@ github "DaveWoodCom/XCGLogger" "6.1.0" -github "dennisweissmann/DeviceKit" "1.11.0" +github "dennisweissmann/DeviceKit" "1.13.0" github "marmelroy/Zip" "1.1.0" diff --git a/ios/README.md b/ios/README.md index 992cdc1816..b02930225c 100644 --- a/ios/README.md +++ b/ios/README.md @@ -58,6 +58,14 @@ To build in Xcode, The framework will be built to **engine/KMEI/build/(Debug|Release)-universal/KeymanEngine.framework**. +If it doesn't build, and you have upgraded from Xcode 10.0 (or earlier) to 10.1 (or later), it may not +build due to "Could not find any available simulators for iOS" error from Carthage, probably while +building DeviceKit. Xcode 10.1 changed the output format which confuses Carthage. Upgrade Carthage: +``` +brew upgrade carthage +brew link --overwrite carthage +``` + ### Linking with KeymanEngine 1. Add KeymanEngine.framework to 'Linked Frameworks and Libraries' and 'Embedded Binaries' in your project targets. diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/APILexicalModelRepository.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/APILexicalModelRepository.swift index 6b1cdd2fd5..2c3e7a55bb 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/APILexicalModelRepository.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/APILexicalModelRepository.swift @@ -16,22 +16,18 @@ public enum APILexicalModelFetchError: Error { } public class APILexicalModelRepository: LexicalModelRepository { - private let languagesAPIURL = URLComponents(string: "https://api.keyman.com/cloud/4.0/languages")! + private let modelsAPIURL = URLComponents(string: "https://api.keyman.com/model")! //?q=bcp47:en public weak var delegate: LexicalModelRepositoryDelegate? public private(set) var languages: [String: Language]? public private(set) var lexicalModels: [String: LexicalModel]? - private(set) var options: Options? public func fetch(completionHandler: CompletionHandler?) { let deviceType = UIDevice.current.userInterfaceIdiom == .phone ? "iphone" : "ipad" let keymanVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - var urlComponents = languagesAPIURL + var urlComponents = modelsAPIURL urlComponents.queryItems = [ - URLQueryItem(name: "dateformat", value: "seconds"), - URLQueryItem(name: "device", value: deviceType), - URLQueryItem(name: "version", value: keymanVersion), - URLQueryItem(name: "languageidtype", value: "bcp47") + URLQueryItem(name: "q", value: "bcp47:en") //+languages?[0] ] log.info("Connecting to Keyman cloud: \(urlComponents.url!).") let task = URLSession.shared.dataTask(with: urlComponents.url!) { (data, response, error) in @@ -74,23 +70,22 @@ public class APILexicalModelRepository: LexicalModelRepository { return } - options = result.options languages = Dictionary(uniqueKeysWithValues: result.languages.map { ($0.id, $0) }) let lexicalModelsWithID = result.languages.flatMap { language in - language.lexicalModels?.map { kb in (kb.id, kb) } ?? [] + language.lexicalModels?.map { lm in (lm.id, lm) } ?? [] } lexicalModels = Dictionary(lexicalModelsWithID) { old, new in - var kb = old + var lm = old if old.languages == nil { - kb.languages = new.languages - return kb + lm.languages = new.languages + return lm } if let newLanguages = new.languages { - let oldLanguageIDs = Set(old.languages!.map { $0.id }) - kb.languages!.append(contentsOf: newLanguages.filter { !oldLanguageIDs.contains($0.id) }) + let oldLanguageIDs = Set(old.languages!.map { $0 }) + lm.languages!.append(contentsOf: newLanguages.filter { !oldLanguageIDs.contains($0) }) } - return kb + return lm } log.info("Request completed -- \(result.languages.count) languages.") diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/LexicalModelRepository.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/LexicalModelRepository.swift index d6d8a5ecab..86fb5d5338 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/LexicalModelRepository.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/LexicalModelRepository.swift @@ -26,7 +26,7 @@ public extension LexicalModelRepository { } // If the lexicalModel (still) supports the requested language, use that one. - guard let language = lexicalModel.languages?.first(where: {$0.id == languageID}) ?? + guard let language = lexicalModel.languages?.first(where: {$0 == languageID}) ?? // Otherwise, just use the first language listed for the lexicalModel. lexicalModel.languages?.first ?? // In cases where the lexicalModel fails to specify any language, use the requested one if it's in the collection. @@ -35,7 +35,7 @@ public extension LexicalModelRepository { return nil } - return InstallableLexicalModel(lexicalModel: lexicalModel, language: language, isCustom: false) + return InstallableLexicalModel(lexicalModel: lexicalModel, languageID: language as! String, isCustom: false) } public func fetch(completionHandler: CompletionHandler? = nil) { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift index 52356ea918..749e6e69ef 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift @@ -160,7 +160,7 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega cell.selectionStyle = .none let lm = userLexicalModels[indexPath.row] - cell.textLabel?.text = lm.languageName + cell.textLabel?.text = lm.languageID // maybe do a lookup for lm.languageName cell.detailTextLabel?.text = lm.name cell.tag = indexPath.row @@ -326,7 +326,7 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega } private func checkUpdates() -> Bool { - if Manager.shared.apiLexicalModelRepository.languages == nil { + if Manager.shared.apiLexicalModelRepository.lexicalModels == nil { return false } @@ -389,7 +389,7 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega func showAddLexicalModel() { let button: UIButton? = (navigationController?.toolbar?.viewWithTag(toolbarButtonTag) as? UIButton) button?.isEnabled = false - let vc = LanguageViewController(Manager.shared.apiLexicalModelRepository) + let vc = LanguageViewController(Manager.shared.apiLexicalModelRepository) //may need to be different for models navigationController?.pushViewController(vc, animated: true) setIsDoneButtonEnabled(true) } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 2e2a3da9ab..1d857a90aa 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -651,7 +651,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat id: lexicalModelID, name: name, languageID: languageId, - languageName: languageName, +// languageName: languageName, version: version, isCustom: false)) } @@ -958,78 +958,77 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat languageID: String, isUpdate: Bool, fetchRepositoryIfNeeded: Bool = true) { - guard let lexicalModels = apiLexicalModelRepository.lexicalModels, - let options = apiLexicalModelRepository.options - else { - if fetchRepositoryIfNeeded { - log.info("Fetching repository from API for lexical model download") - apiLexicalModelRepository.fetch { error in - if let error = error { - self.downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels - } else { - log.info("Fetched repository. Continuing with lexical model download.") - self.downloadLexicalModel(withID: lexicalModelID, - languageID: languageID, - isUpdate: isUpdate, - fetchRepositoryIfNeeded: false) - } - } - return - } - let message = "Lexical model repository not yet fetched" - let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) - downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels - return - } +// guard let lexicalModels = apiLexicalModelRepository.lexicalModels, +// else { +// if fetchRepositoryIfNeeded { +// log.info("Fetching repository from API for lexical model download") +// apiLexicalModelRepository.fetch { error in +// if let error = error { +// self.downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels +// } else { +// log.info("Fetched repository. Continuing with lexical model download.") +// self.downloadLexicalModel(withID: lexicalModelID, +// languageID: languageID, +// isUpdate: isUpdate, +// fetchRepositoryIfNeeded: false) +// } +// } +// return +// } +// let message = "Lexical model repository not yet fetched" +// let error = NSError(domain: "Keyman", code: 0, userInfo: [NSLocalizedDescriptionKey: message]) +// downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels +// return +// } - guard let lexicalModel = apiLexicalModelRepository.installableLexicalModel(withID: lexicalModelID, languageID: languageID), - let filename = lexicalModels[lexicalModelID]?.filename - else { - let message = "Lexical model not found with id: \(lexicalModelID), languageID: \(languageID)" - let error = NSError(domain: "Keyman", code: 0, - userInfo: [NSLocalizedDescriptionKey: message]) - downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels - return - } - - guard downloadQueue == nil else { - let error = NSError(domain: "Keyman", code: 0, - userInfo: [NSLocalizedDescriptionKey: "Download queue is busy"]) - downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels : [lexicalModel] - return - } - - guard reachability.currentReachabilityStatus() != NotReachable else { - let error = NSError(domain: "Keyman", code: 0, - userInfo: [NSLocalizedDescriptionKey: "No internet connection"]) - downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels : [lexicalModel] - return - } - - do { - try FileManager.default.createDirectory(at: Storage.active.lexicalModelDir(forID: lexicalModelID), - withIntermediateDirectories: true) - } catch { - log.error("Could not create dir for download: \(error)") - return - } - - let lexicalModelURL = options.lexicalModelBaseURL?.appendingPathComponent(filename) - - // TODO: Better typing - downloadQueue = HTTPDownloader(self) - let commonUserData: [String: Any] = [ - Key.lexicalModelInfo: [lexicalModel], - Key.update: isUpdate - ] - downloadQueue!.userInfo = commonUserData - - let request = HTTPDownloadRequest(url: lexicalModelURL!, userInfo: commonUserData) - request.destinationFile = Storage.active.lexicalModelURL(for: lexicalModel).path - request.tag = 0 - downloadQueue!.addRequest(request) - - downloadQueue!.run() +// guard let lexicalModel = apiLexicalModelRepository.installableLexicalModel(withID: lexicalModelID, languageID: languageID), +// let filename = lexicalModels[lexicalModelID]?.filename +// else { +// let message = "Lexical model not found with id: \(lexicalModelID), languageID: \(languageID)" +// let error = NSError(domain: "Keyman", code: 0, +// userInfo: [NSLocalizedDescriptionKey: message]) +// downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels +// return +// } +// +// guard downloadQueue == nil else { +// let error = NSError(domain: "Keyman", code: 0, +// userInfo: [NSLocalizedDescriptionKey: "Download queue is busy"]) +// downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels : [lexicalModel] +// return +// } +// +// guard reachability.currentReachabilityStatus() != NotReachable else { +// let error = NSError(domain: "Keyman", code: 0, +// userInfo: [NSLocalizedDescriptionKey: "No internet connection"]) +// downloadFailed(forKeyboards: [], error: error) //??? forLexicalModels : [lexicalModel] +// return +// } +// +// do { +// try FileManager.default.createDirectory(at: Storage.active.lexicalModelDir(forID: lexicalModelID), +// withIntermediateDirectories: true) +// } catch { +// log.error("Could not create dir for download: \(error)") +// return +// } +// +// let lexicalModelURL = lexicalModelBaseURL?.appendingPathComponent(filename) +// +// // TODO: Better typing +// downloadQueue = HTTPDownloader(self) +// let commonUserData: [String: Any] = [ +// Key.lexicalModelInfo: [lexicalModel], +// Key.update: isUpdate +// ] +// downloadQueue!.userInfo = commonUserData +// +// let request = HTTPDownloadRequest(url: lexicalModelURL!, userInfo: commonUserData) +// request.destinationFile = Storage.active.lexicalModelURL(for: lexicalModel).path +// request.tag = 0 +// downloadQueue!.addRequest(request) +// +// downloadQueue!.run() } private func lexicalModelFontURLs(forFont font: Font?, options: Options) -> [URL] { @@ -1083,15 +1082,14 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat } } - /// Assumes that lexical model has font and oskFont set and ignores fonts contained in Language. private func downloadLexicalModel(_ lexicalModelAPI: LexicalModelAPICall) { - let lexicalModel = lexicalModelAPI.lexicalModel + let lexicalModel = lexicalModelAPI.lexicalModels[0] let installableLexicalModels = lexicalModel.languages!.map { language in - InstallableLexicalModel(lexicalModel: lexicalModel, language: language, isCustom: true) + InstallableLexicalModel(lexicalModel: lexicalModel, languageID: language, isCustom: true) } - let filename = lexicalModel.filename - let lexicalModelURL = lexicalModelAPI.options.lexicalModelBaseURL!.appendingPathComponent(filename) + let packageFilename = lexicalModel.packageFilename + let lexicalModelURL = URL(string: "https://api.keyman.com/model")!.appendingPathComponent(packageFilename) if downloadQueue != nil { // Download queue is active. diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift index e0d8d3f02a..e18c2538f0 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift @@ -13,7 +13,6 @@ public struct InstallableLexicalModel: Codable { public var id: String public var name: String public var languageID: String - public var languageName: String public var version: String public var isCustom: Bool @@ -24,22 +23,21 @@ public struct InstallableLexicalModel: Codable { public init(id: String, name: String, languageID: String, - languageName: String, version: String, isCustom: Bool) { self.id = id self.name = name self.languageID = languageID - self.languageName = languageName +// self.languageName = languageName self.version = version self.isCustom = isCustom } - public init(lexicalModel: LexicalModel, language: Language, isCustom: Bool) { + public init(lexicalModel: LexicalModel, languageID: String, isCustom: Bool) { self.id = lexicalModel.id self.name = lexicalModel.name - self.languageID = language.id - self.languageName = language.name + self.languageID = languageID +// self.languageName = language.name self.version = lexicalModel.version self.isCustom = isCustom } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/KMPLexicalModel.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/KMPLexicalModel.swift index 1443b15b42..8717ce9559 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/KMPLexicalModel.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/KMPLexicalModel.swift @@ -59,7 +59,7 @@ public class KMPLexicalModel for language in self.languages { let lexicalModel = InstallableLexicalModel(id: lexicalModelId!, name: name!, languageID: language.languageId, - languageName: language.name, +// languageName: language.name, version: version, isCustom: true) //update this based on adhoc vs api diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModel.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModel.swift index 6704ac9217..769a57f647 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModel.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModel.swift @@ -17,7 +17,8 @@ public struct LexicalModel: Codable { public var id: String /// Name of the lexicalModel. `.js` file which should be appended to `Options.lexicalModel.BaseURI`. - public var filename: String + /// Name of the lexicalModel KMP file. + public var packageFilename: String /// The lexicalModel. is the recommended default for the language. public var isDefault: Bool @@ -31,13 +32,13 @@ public struct LexicalModel: Codable { /// Dot-decimal version number of the lexicalModel.. public var version: String - /// Language objects linked to the lexicalModel.. - public var languages: [Language]? + /// Tags of languages supported by this lexicalModel.. + public var languages: [String]? enum CodingKeys: String, CodingKey { case name case id - case filename + case packageFilename case isDefault = "default" case lastModified case fileSize @@ -47,15 +48,15 @@ public struct LexicalModel: Codable { public init(name: String, id: String, - filename: String, + packageFilename: String, isDefault: Bool?, lastModified: Date, fileSize: Int?, version: String, - languages: [Language]?) { + languages: [String]?) { self.name = name self.id = id - self.filename = filename + self.packageFilename = packageFilename self.isDefault = isDefault ?? false self.lastModified = lastModified self.fileSize = fileSize @@ -68,17 +69,17 @@ public struct LexicalModel: Codable { let name = try container.decode(String.self, forKey: .name) let id = try container.decode(String.self, forKey: .id) - let filename = try container.decode(String.self, forKey: .filename) + let packageFilename = try container.decode(String.self, forKey: .packageFilename) let isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault) // TODO: Handle both seconds and ISO 8601 let lastModified = try container.decode(Date.self, forKey: .lastModified) let fileSize = try container.decodeIfPresent(Int.self, forKey: .fileSize) let version = try container.decode(String.self, forKey: .version) - let languages = try container.decodeIfPresent([Language].self, forKey: .languages) + let languages = try container.decodeIfPresent([String].self, forKey: .languages) self.init(name: name, id: id, - filename: filename, + packageFilename: packageFilename, isDefault: isDefault, lastModified: lastModified, fileSize: fileSize, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelAPICall.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelAPICall.swift index 3fbb1c8648..6920119bc3 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelAPICall.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelAPICall.swift @@ -9,6 +9,18 @@ import Foundation struct LexicalModelAPICall: Codable { - let options: Options - let lexicalModel: LexicalModel + let lexicalModels: [LexicalModel] + + enum CodingKeys: String, CodingKey { + case lexicalModels + } + + enum LexicalModelCodingKeys: String, CodingKey { + case lexicalModel + } + + init(from decoder: Decoder) throws { + let lexicalModelContainer = try decoder.container(keyedBy: CodingKeys.self) + self.lexicalModels = try lexicalModelContainer.decode([LexicalModel].self, forKey: .lexicalModels) + } }