From 319c81e4469235b046d58aa26f3526fd77d551bb Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Wed, 25 Mar 2026 02:44:24 +0530
Subject: [PATCH 1/2] feat: add download data empty state button to keyboard
(#627)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When a Scribe keyboard is installed but language data has not been
downloaded yet, a blue 'Please download language data' button now
appears at the top of the keyboard. Tapping it opens the Scribe app
and navigates directly to the Download Data screen.
Changes:
- KeyboardViewController.swift
- Added downloadDataBtn (UIButton?) property
- Added hasLanguageData() to check if the language SQLite file
exists in the shared app group container
- Added showDownloadDataBtn() to create and layout the blue CTA
button at the top of the keyboard view
- Added conditionallyShowDownloadDataBtn() to show/hide the button
based on data availability and current command state (only shown
in .idle state, hidden during translate/conjugate/plural/info)
- Added openScribeApp() using the UIResponder chain to open the
scribe:// URL scheme and direct the user to the download screen
- Called conditionallyShowDownloadDataBtn() at the end of loadKeys()
- Button is also hidden during displayInformation state
- CommandVariables.swift
- Added downloadDataMsg variable with default English fallback text
'Please download language data'
- Language interface variables (all 11 keyboards)
- ENInterfaceVariables: 'Please download language data'
- DEInterfaceVariables: 'Bitte Sprachdaten herunterladen'
- FRInterfaceVariables: 'Veuillez télécharger les données linguistiques'
- ESInterfaceVariables: 'Por favor descarga los datos del idioma'
- ITInterfaceVariables: 'Scarica i dati della lingua'
- PTInterfaceVariables: 'Por favor baixe os dados do idioma'
- RUInterfaceVariables: 'Загрузите языковые данные'
- SVInterfaceVariables: 'Ladda ner språkdata'
- NBInterfaceVariables: 'Last ned språkdata'
- HEInterfaceVariables: 'אנא הורד נתוני שפה'
- IDInterfaceVariables: 'Unduh data bahasa'
- Scribe/Info.plist
- Registered scribe:// URL scheme (CFBundleURLSchemes) so the app
can be opened from the keyboard extension
- Scribe/AppDelegate.swift
- Added application(_:open:options:) handler for the scribe:// URL
- Posts NavigateToDownloadScreen notification which InstallationVC
already handles to push the DownloadDataScreen
Closes #627
---
.../KeyboardViewController.swift | 96 +++++++++++++++++++
.../CommandVariables.swift | 5 +
.../English/ENInterfaceVariables.swift | 1 +
.../French/FR-AZERTYInterfaceVariables.swift | 1 +
.../German/DEInterfaceVariables.swift | 1 +
.../Hebrew/HEInterfaceVariables.swift | 1 +
.../Indonesian/IDInterfaceVariables.swift | 1 +
.../Italian/ITInterfaceVariables.swift | 1 +
.../Norwegian/NBInterfaceVariables.swift | 1 +
.../Portuguese/PTInterfaceVariables.swift | 1 +
.../Russian/RUInterfaceVariables.swift | 1 +
.../Spanish/ESInterfaceVariables.swift | 1 +
.../Swedish/SVInterfaceVariables.swift | 1 +
Scribe/AppDelegate.swift | 9 ++
Scribe/Info.plist | 11 +++
15 files changed, 132 insertions(+)
diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift
index d74847f9..61dfee62 100644
--- a/Keyboards/KeyboardsBase/KeyboardViewController.swift
+++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift
@@ -290,6 +290,100 @@ class KeyboardViewController: UIInputViewController {
pluralKey.isHidden = state
}
+ // MARK: Download Data Button
+
+ /// A button shown above the keyboard when no language data has been downloaded.
+ var downloadDataBtn: UIButton?
+
+ /// Checks whether language data has been downloaded for the current keyboard language.
+ func hasLanguageData() -> Bool {
+ let langAbbr = getControllerLanguageAbbr()
+ guard !langAbbr.isEmpty,
+ let containerURL = FileManager.default.containerURL(
+ forSecurityApplicationGroupIdentifier: "group.be.scri.userDefaultsContainer"
+ ) else {
+ return true // default to true to avoid showing the button unnecessarily
+ }
+ let dbPath = containerURL.appendingPathComponent("\(langAbbr.uppercased())LanguageData.sqlite").path
+ return FileManager.default.fileExists(atPath: dbPath)
+ }
+
+ /// Opens the Scribe app via the responder chain so the user can download language data.
+ @objc func openScribeApp() {
+ guard let url = URL(string: "scribe://") else { return }
+ var responder: UIResponder? = self
+ while responder != nil {
+ if let application = responder as? UIApplication {
+ application.open(url)
+ return
+ }
+ responder = responder?.next
+ }
+ }
+
+ /// Shows or hides the download data button based on whether language data is available.
+ func conditionallyShowDownloadDataBtn() {
+ // Only show the download button in idle state (not during commands).
+ guard commandState == .idle else {
+ downloadDataBtn?.isHidden = true
+ return
+ }
+ if hasLanguageData() {
+ downloadDataBtn?.isHidden = true
+ } else {
+ showDownloadDataBtn()
+ }
+ }
+
+ /// Creates and displays the download data button above the keyboard.
+ func showDownloadDataBtn() {
+ // Remove any existing button first.
+ downloadDataBtn?.removeFromSuperview()
+
+ let btn = UIButton(type: .system)
+ btn.translatesAutoresizingMaskIntoConstraints = false
+ btn.backgroundColor = scribeCTAColor
+ btn.setTitleColor(.white, for: .normal)
+ btn.setTitle(downloadDataMsg, for: .normal)
+ btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
+ btn.layer.cornerRadius = commandKeyCornerRadius
+ btn.layer.masksToBounds = true
+
+ // Add a download icon on the left side.
+ let iconConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .medium)
+ btn.setImage(UIImage(systemName: "arrow.down.circle.fill", withConfiguration: iconConfig), for: .normal)
+ btn.tintColor = .white
+ if #available(iOS 15.0, *) {
+ var config = UIButton.Configuration.plain()
+ config.baseForegroundColor = .white
+ config.image = UIImage(systemName: "arrow.down.circle.fill", withConfiguration: iconConfig)
+ config.imagePadding = 8
+ config.imagePlacement = .leading
+ config.title = downloadDataMsg
+ config.attributedTitle = AttributedString(
+ downloadDataMsg,
+ attributes: AttributeContainer([.font: UIFont.systemFont(ofSize: 16, weight: .medium)])
+ )
+ btn.configuration = config
+ btn.backgroundColor = scribeCTAColor
+ } else {
+ btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)
+ btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12)
+ }
+
+ btn.addTarget(self, action: #selector(openScribeApp), for: .touchUpInside)
+
+ view.addSubview(btn)
+ downloadDataBtn = btn
+
+ NSLayoutConstraint.activate([
+ btn.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
+ btn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8),
+ btn.topAnchor.constraint(equalTo: view.topAnchor, constant: 4),
+ btn.heightAnchor.constraint(equalToConstant: scribeKey.frame.height > 0 ? scribeKey.frame.height : 36)
+ ])
+ }
+
/// Logic to create notification tooltip.
func createInformationStateDatasource(text: NSMutableAttributedString, backgroundColor: UIColor)
-> ToolTipViewDatasource {
@@ -1714,6 +1808,7 @@ class KeyboardViewController: UIInputViewController {
deactivateBtn(btn: padEmojiKey2)
setInformationState()
+ downloadDataBtn?.isHidden = true
return // return to skip normal keyboard setup
}
@@ -1835,6 +1930,7 @@ class KeyboardViewController: UIInputViewController {
}
setKeyPadding()
+ conditionallyShowDownloadDataBtn()
}
func setCommaAndPeriodKeysConditionally() {
diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift
index 236fd9e5..43778e4f 100644
--- a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift
+++ b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift
@@ -177,3 +177,8 @@ var pluralPromptAndCursor = ""
var pluralPromptAndPlaceholder = ""
var pluralPromptAndColorPlaceholder = NSMutableAttributedString()
var alreadyPluralMsg = ""
+
+// MARK: Download Data Variables
+
+/// The message shown on the download data button when no language data has been downloaded.
+var downloadDataMsg = "Please download language data"
diff --git a/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift b/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift
index 28f5f4fc..4812c060 100644
--- a/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift
@@ -294,4 +294,5 @@ func setENKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "Already plural"
+ downloadDataMsg = "Please download language data"
}
diff --git a/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift b/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift
index 8be89a71..8882feca 100644
--- a/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift
@@ -293,4 +293,5 @@ func setFRKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "Déjà pluriel"
+ downloadDataMsg = "Veuillez télécharger les données linguistiques"
}
diff --git a/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift b/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift
index de063eb9..4da5a232 100644
--- a/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift
@@ -356,4 +356,5 @@ func setDEKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "Schon Plural"
+ downloadDataMsg = "Bitte Sprachdaten herunterladen"
}
diff --git a/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift
index 3e3f6319..5cc7cee4 100644
--- a/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift
@@ -254,4 +254,5 @@ func setHEKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "כבר בצורת רבים"
+ downloadDataMsg = "אנא הורד נתוני שפה"
}
diff --git a/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift
index 8acfbe95..57b04d32 100644
--- a/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift
@@ -244,4 +244,5 @@ func setIDKeyboardLayout() {
textForAttribute: translatePlaceholder,
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
+ downloadDataMsg = "Unduh data bahasa"
}
diff --git a/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift
index 8337b9f7..256ee789 100644
--- a/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift
@@ -281,4 +281,5 @@ func setITKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "Già plurale"
+ downloadDataMsg = "Scarica i dati della lingua"
}
diff --git a/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift
index 2d7e8a74..9e08ac1e 100644
--- a/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift
@@ -300,4 +300,5 @@ func setNBKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "Allerede flertall"
+ downloadDataMsg = "Last ned språkdata"
}
diff --git a/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift
index eeee3642..4d5f19d3 100644
--- a/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift
@@ -279,4 +279,5 @@ func setPTKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "Já plural"
+ downloadDataMsg = "Por favor baixe os dados do idioma"
}
diff --git a/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift
index 40e17c4c..282238e8 100644
--- a/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift
@@ -269,4 +269,5 @@ func setRUKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "Уже во множ-ом"
+ downloadDataMsg = "Загрузите языковые данные"
}
diff --git a/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift
index 200d9b7b..b33cdb99 100644
--- a/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift
@@ -353,4 +353,5 @@ func setESKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "Ya en plural"
+ downloadDataMsg = "Por favor descarga los datos del idioma"
}
diff --git a/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift
index 3943138d..f326ba38 100644
--- a/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift
+++ b/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift
@@ -349,4 +349,5 @@ func setSVKeyboardLayout() {
withColor: UIColor(cgColor: commandBarPlaceholderColorCG)
)
alreadyPluralMsg = "Redan plural"
+ downloadDataMsg = "Ladda ner språkdata"
}
diff --git a/Scribe/AppDelegate.swift b/Scribe/AppDelegate.swift
index b00aa906..e86a60b8 100644
--- a/Scribe/AppDelegate.swift
+++ b/Scribe/AppDelegate.swift
@@ -82,6 +82,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
*/
}
+ func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
+ // Handle scribe:// URL scheme to navigate to the download data screen.
+ if url.scheme == "scribe" {
+ NotificationCenter.default.post(name: NSNotification.Name("NavigateToDownloadScreen"), object: nil)
+ return true
+ }
+ return false
+ }
+
func applicationDidBecomeActive(_: UIApplication) {
/*
Restart any tasks that were paused (or not yet started) while the application was inactive.
diff --git a/Scribe/Info.plist b/Scribe/Info.plist
index 633b5171..5c2f37fe 100644
--- a/Scribe/Info.plist
+++ b/Scribe/Info.plist
@@ -46,5 +46,16 @@
UIViewControllerBasedStatusBarAppearance
+ CFBundleURLTypes
+
+
+ CFBundleURLSchemes
+
+ scribe
+
+ CFBundleURLName
+ be.scri.scribe
+
+
From 6b4e15f10fc4519fa8fe567c399b995a7dd77915 Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Tue, 31 Mar 2026 15:15:32 +0530
Subject: [PATCH 2/2] Fix circular reference compilation errors by using
classes
---
Keyboards/DataManager/DataContract.swift | 2 +-
Keyboards/KeyboardsBase/NavigationStructure.swift | 9 +++++++--
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/Keyboards/DataManager/DataContract.swift b/Keyboards/DataManager/DataContract.swift
index 0eb96a77..4ca89639 100644
--- a/Keyboards/DataManager/DataContract.swift
+++ b/Keyboards/DataManager/DataContract.swift
@@ -38,7 +38,7 @@ struct DeclensionSection: Codable {
let declensionForms: [Int: DeclensionNode]?
}
-struct DeclensionNode: Codable {
+class DeclensionNode: Codable {
let label: String?
let value: String?
let displayValue: String?
diff --git a/Keyboards/KeyboardsBase/NavigationStructure.swift b/Keyboards/KeyboardsBase/NavigationStructure.swift
index 54f5d558..64ef35f3 100644
--- a/Keyboards/KeyboardsBase/NavigationStructure.swift
+++ b/Keyboards/KeyboardsBase/NavigationStructure.swift
@@ -6,15 +6,20 @@
import Foundation
/// Represents a single option in the navigation.
-enum NavigationNode {
+indirect enum NavigationNode {
case nextLevel(NavigationLevel, displayValue: String?) // navigate deeper, with optional display value
case finalValue(String) // terminal node, insert this text
}
/// Represents a level in the navigation hierarchy.
-struct NavigationLevel {
+class NavigationLevel {
let title: String // title for command bar
let options: [(label: String, node: NavigationNode)] // buttons to display
+
+ init(title: String, options: [(label: String, node: NavigationNode)]) {
+ self.title = title
+ self.options = options
+ }
}
/// Builds navigation trees for conjugations and declensions.