← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


commit ffae96dd58cf720164c4468a8c98225675cb782c
parent 9710be2af08ea4fe161a4d71be872922e118b76c
Author: Michael Camilleri <[email protected]>
Date:   Wed, 22 Apr 2026 00:40:43 +0900

Add keyboard priming

The iOS version occasionally exhibits animation hitches in the initial
pull-to-create gesture. Prior measurements suggest that this is due to
the keyboard start-up time. This commit attempts to prime the keyboard
during launch so that these costs are not visible to the user.

Co-Authored-By: Claude Opus 4.7 <[email protected]>

Diffstat

MListless.xcodeproj/project.pbxproj | 4++++
AListlessiOS/Helpers/KeyboardWarmup.swift | 41+++++++++++++++++++++++++++++++++++++++++
MListlessiOS/ListlessiOSApp.swift | 3+++

3 files changed, 48 insertions(+), 0 deletions(-)

diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj

@@ -92,6 +92,7 @@
 		BA6953E0EFE6F8255F05A3FD /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */; };
 		BB16A28C4DA1695B722B45B2 /* ItemEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138DCA35ED82A745E4745175 /* ItemEntity.swift */; };
 		BDA8D53342F745B27B72B242 /* ItemRowDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A80C91DBDA44C879958098 /* ItemRowDragGesture.swift */; };
+		C58C877396361901A3E4489E /* KeyboardWarmup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86029FF31D90CFBA78BC8F60 /* KeyboardWarmup.swift */; };
 		C7C69D883B45F1B4CE979AF7 /* ItemListViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31EDB2C34C8B255000A2525 /* ItemListViewProtocol.swift */; };
 		C89D4C17F91AF91F18B6EF4E /* ItemStoreCompletionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114A89FD89C8EFB5771B7242 /* ItemStoreCompletionTests.swift */; };
 		CA439FD953EA59A9664E0D74 /* CloudKitErrorClassifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C3B9FF63645B35E09CF1B1 /* CloudKitErrorClassifierTests.swift */; };
@@ -234,6 +235,7 @@
 		7C73E9D4C42CCABBF0F33543 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; };
 		82F2C024B6C1F2F1FD7A25B0 /* ItemListView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ItemListView+Logic.swift"; sourceTree = "<group>"; };
 		848DBC251E2D2EB7BD089768 /* ItemListTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListTypes.swift; sourceTree = "<group>"; };
+		86029FF31D90CFBA78BC8F60 /* KeyboardWarmup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardWarmup.swift; sourceTree = "<group>"; };
 		869254E16B52F36616416DB4 /* ItemListView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ItemListView+Toolbar.swift"; sourceTree = "<group>"; };
 		88C0D6F2667BD14F29CB84E5 /* ListlessiOSUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListlessiOSUITests.swift; sourceTree = "<group>"; };
 		9262207DAC21619BD9EDEE15 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; };
@@ -296,6 +298,7 @@
 				05AB46824DCDD04903EA4C82 /* ItemRowDragGesture.swift */,
 				E71FBC3E19D32F527B5FE9E6 /* ItemRowMetrics.swift */,
 				DCCE85438FE23E43095B2C25 /* ItemRowSwipeGesture.swift */,
+				86029FF31D90CFBA78BC8F60 /* KeyboardWarmup.swift */,
 				E8B5E429B253183F887C5FD6 /* KeyCommandBridge.swift */,
 				361665491B2313B7A06A94B9 /* PerfSampler.swift */,
 				FA5D58EC1FBAA96E83A79445 /* PlatformScrollIndicatorsModifier.swift */,
@@ -917,6 +920,7 @@
 				12E43D0CD9124037022E3C38 /* KeyCommandBridge.swift in Sources */,
 				19699EC4FF57EF0D636B65E3 /* KeyValueSyncBridge.swift in Sources */,
 				F6587B84ECC6BFE92A5FB493 /* KeyboardNavigationModifier.swift in Sources */,
+				C58C877396361901A3E4489E /* KeyboardWarmup.swift in Sources */,
 				5B60B409CE4BA668DB30A65D /* Listless.xcdatamodeld in Sources */,
 				F0B2B806BD84A4F2FDF8E038 /* ListlessiOSApp.swift in Sources */,
 				7770D06CBDE3E87B7FDE7C21 /* PerfDebugView.swift in Sources */,

diff --git a/ListlessiOS/Helpers/KeyboardWarmup.swift b/ListlessiOS/Helpers/KeyboardWarmup.swift

@@ -0,0 +1,41 @@
+import UIKit
+
+/// Pre-pays the ~400ms `RemoteTextInput` / autocorrect subsystem spin-up that
+/// otherwise happens on the first real text field focus after a cold launch.
+/// Creates an offscreen `UITextField`, makes it first responder, then resigns
+/// immediately — enough to trigger the keyboard process to initialize without
+/// visibly animating anything.
+@MainActor
+enum KeyboardWarmup {
+    private static var didWarm = false
+
+    static func prime() {
+        guard !didWarm else { return }
+        guard let window = keyWindow() else { return }
+        didWarm = true
+
+        PerfSampler.shared.measure("Keyboard.warmup") {
+            let field = UITextField(frame: CGRect(x: -100, y: -100, width: 1, height: 1))
+            field.autocorrectionType = .default
+            field.autocapitalizationType = .sentences
+            window.addSubview(field)
+            field.becomeFirstResponder()
+            field.resignFirstResponder()
+            field.removeFromSuperview()
+        }
+    }
+
+    private static func keyWindow() -> UIWindow? {
+        for case let scene as UIWindowScene in UIApplication.shared.connectedScenes {
+            if let key = scene.windows.first(where: \.isKeyWindow) {
+                return key
+            }
+        }
+        for case let scene as UIWindowScene in UIApplication.shared.connectedScenes {
+            if let first = scene.windows.first {
+                return first
+            }
+        }
+        return nil
+    }
+}

diff --git a/ListlessiOS/ListlessiOSApp.swift b/ListlessiOS/ListlessiOSApp.swift

@@ -152,6 +152,9 @@ struct ListlessiOSApp: App {
         .onChange(of: appearanceMode, initial: true) { _, newValue in
             applyAppearanceMode(newValue)
         }
+        .task {
+            KeyboardWarmup.prime()
+        }
         .overlay(alignment: .top) {
             Color.outerBackground
                 .opacity(0.9)