Skip to content

Commit 4d188c7

Browse files
committed
ios: fix app flickers on start
fixing ix it by adding a check for when react app is ready, which runs recursively in WebView.swift. Also added launch screen and transition to make app start feels smoother
1 parent 38e251e commit 4d188c7

File tree

9 files changed

+166
-8
lines changed

9 files changed

+166
-8
lines changed

frontends/ios/BitBoxApp/BitBoxApp.xcodeproj/project.pbxproj

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
/* Begin PBXBuildFile section */
1010
8E1846EB2E0AFF6000802D8B /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E1846EA2E0AFF5B00802D8B /* NetworkMonitor.swift */; };
1111
8E1846EC2E0AFF6000802D8B /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E1846EA2E0AFF5B00802D8B /* NetworkMonitor.swift */; };
12+
B536195B2E4B2D1B00B3E10D /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B536195A2E4B2D1B00B3E10D /* Launch Screen.storyboard */; };
13+
B536195C2E4B2D1B00B3E10D /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B536195A2E4B2D1B00B3E10D /* Launch Screen.storyboard */; };
1214
D700B5F62C884C34000496D4 /* Mobileserver.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D76518BB2B1F8F7400DC03A9 /* Mobileserver.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1315
D700B5FD2CA2FFAF000496D4 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76516322B1F3D1A00DC03A9 /* WebView.swift */; };
1416
D700B5FE2CA2FFAF000496D4 /* BitBoxAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76516082B1F3B1300DC03A9 /* BitBoxAppApp.swift */; };
@@ -74,6 +76,7 @@
7476

7577
/* Begin PBXFileReference section */
7678
8E1846EA2E0AFF5B00802D8B /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
79+
B536195A2E4B2D1B00B3E10D /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
7780
D700B5F82C888CB9000496D4 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
7881
D700B5F92C986A3C000496D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
7982
D700B60A2CA2FFAF000496D4 /* BitBoxApp Testnet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BitBoxApp Testnet.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -127,9 +130,18 @@
127130
/* End PBXFrameworksBuildPhase section */
128131

129132
/* Begin PBXGroup section */
133+
B536195D2E4B2D9E00B3E10D /* Assets */ = {
134+
isa = PBXGroup;
135+
children = (
136+
);
137+
path = Assets;
138+
sourceTree = "<group>";
139+
};
130140
D76515FC2B1F3B1300DC03A9 = {
131141
isa = PBXGroup;
132142
children = (
143+
B536195D2E4B2D9E00B3E10D /* Assets */,
144+
B536195A2E4B2D1B00B3E10D /* Launch Screen.storyboard */,
133145
D700B5F82C888CB9000496D4 /* Config.xcconfig */,
134146
D76518BB2B1F8F7400DC03A9 /* Mobileserver.xcframework */,
135147
D76516072B1F3B1300DC03A9 /* BitBoxApp */,
@@ -327,6 +339,7 @@
327339
files = (
328340
D700B6022CA2FFAF000496D4 /* assets in Resources */,
329341
D700B6032CA2FFAF000496D4 /* Preview Assets.xcassets in Resources */,
342+
B536195C2E4B2D1B00B3E10D /* Launch Screen.storyboard in Resources */,
330343
D700B6042CA2FFAF000496D4 /* Assets.xcassets in Resources */,
331344
);
332345
runOnlyForDeploymentPostprocessing = 0;
@@ -337,6 +350,7 @@
337350
files = (
338351
D76518B62B1F45B300DC03A9 /* assets in Resources */,
339352
D76516102B1F3B1500DC03A9 /* Preview Assets.xcassets in Resources */,
353+
B536195B2E4B2D1B00B3E10D /* Launch Screen.storyboard in Resources */,
340354
D765160D2B1F3B1500DC03A9 /* Assets.xcassets in Resources */,
341355
);
342356
runOnlyForDeploymentPostprocessing = 0;
@@ -468,9 +482,10 @@
468482
INFOPLIST_KEY_CFBundleDisplayName = "BitBoxApp Testnet";
469483
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
470484
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
471-
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
485+
INFOPLIST_KEY_UILaunchScreen_Generation = NO;
486+
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
472487
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
473-
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait";
488+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
474489
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
475490
LD_RUNPATH_SEARCH_PATHS = (
476491
"$(inherited)",
@@ -500,9 +515,10 @@
500515
INFOPLIST_KEY_CFBundleDisplayName = "BitBoxApp Testnet";
501516
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
502517
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
503-
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
518+
INFOPLIST_KEY_UILaunchScreen_Generation = NO;
519+
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
504520
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
505-
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait";
521+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
506522
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
507523
LD_RUNPATH_SEARCH_PATHS = (
508524
"$(inherited)",
@@ -648,9 +664,10 @@
648664
INFOPLIST_KEY_CFBundleDisplayName = BitBoxApp;
649665
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
650666
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
651-
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
667+
INFOPLIST_KEY_UILaunchScreen_Generation = NO;
668+
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
652669
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
653-
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait";
670+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
654671
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
655672
LD_RUNPATH_SEARCH_PATHS = (
656673
"$(inherited)",
@@ -679,9 +696,10 @@
679696
INFOPLIST_KEY_CFBundleDisplayName = BitBoxApp;
680697
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
681698
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
682-
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
699+
INFOPLIST_KEY_UILaunchScreen_Generation = NO;
700+
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
683701
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
684-
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait";
702+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
685703
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
686704
LD_RUNPATH_SEARCH_PATHS = (
687705
"$(inherited)",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"images": [
3+
{
4+
"filename": "dbb_logo_dark.svg",
5+
"idiom": "universal",
6+
"scale": "1x"
7+
},
8+
{
9+
"appearances": [
10+
{
11+
"appearance": "luminosity",
12+
"value": "dark"
13+
}
14+
],
15+
"filename": "dbb_logo_light.svg",
16+
"idiom": "universal",
17+
"scale": "1x"
18+
}
19+
],
20+
"info": {
21+
"author": "xcode",
22+
"version": 1
23+
},
24+
"properties": {
25+
"preserves-vector-representation": true
26+
}
27+
}
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

frontends/ios/BitBoxApp/BitBoxApp/WebView.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,20 @@ import Mobileserver
3535
let scheme = "qrc"
3636

3737
class JavascriptBridge: NSObject, WKScriptMessageHandler {
38+
weak var webView: WKWebView?
39+
3840
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
3941
if message.name == "goCall", let body = message.body as? [String: AnyObject] {
4042
let queryID = body["queryID"] as! Int
4143
let query = body["query"] as! String
4244
MobileserverBackendCall(queryID, query)
45+
} else if message.name == "appReady" {
46+
// React app is ready, show the webview
47+
DispatchQueue.main.async {
48+
UIView.animate(withDuration: 0.3) {
49+
self.webView?.alpha = 1.0
50+
}
51+
}
4352
}
4453
}
4554
}
@@ -107,6 +116,7 @@ struct WebView: UIViewRepresentable {
107116
let contentController = WKUserContentController()
108117
let bridge = JavascriptBridge()
109118
contentController.add(bridge, name: "goCall")
119+
contentController.add(bridge, name: "appReady")
110120
let config = WKWebViewConfiguration()
111121
config.userContentController = contentController
112122

@@ -128,6 +138,11 @@ struct WebView: UIViewRepresentable {
128138
let webView = WKWebView(frame: .zero, configuration: config)
129139
webView.navigationDelegate = context.coordinator
130140
webView.uiDelegate = context.coordinator
141+
// hide the WebView initially to prevent white flash (flicker) on initial load
142+
webView.alpha = 0.0
143+
144+
// set webView reference in bridge for appReady handling
145+
bridge.webView = webView
131146

132147
// Disables automatic content inset adjustment to prevent safe area issues
133148
// https://developer.apple.com/documentation/uikit/uiscrollview/contentinsetadjustmentbehavior-swift.property
@@ -156,6 +171,11 @@ struct WebView: UIViewRepresentable {
156171
}
157172

158173
class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
174+
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
175+
// React app will notify Swift when ready via appReady message handler
176+
// This prevents the white flicker on app open
177+
}
178+
159179
// Intercept all URLs
160180
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
161181
guard let url = navigationAction.request.url else {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
3+
<device id="retina6_12" orientation="portrait" appearance="light"/>
4+
<dependencies>
5+
<deployment identifier="iOS"/>
6+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
7+
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
8+
<capability name="System colors in document resources" minToolsVersion="11.0"/>
9+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
10+
</dependencies>
11+
<scenes>
12+
<!--View Controller-->
13+
<scene sceneID="EHf-IW-A2E">
14+
<objects>
15+
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
16+
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
17+
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
18+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
19+
<subviews>
20+
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="dbb_logo_light" translatesAutoresizingMaskIntoConstraints="NO" id="BitBox-Logo">
21+
<rect key="frame" x="162.5" y="386" width="68" height="81"/>
22+
<constraints>
23+
<constraint firstAttribute="width" constant="68" id="logo-width"/>
24+
<constraint firstAttribute="height" constant="81" id="logo-height"/>
25+
</constraints>
26+
</imageView>
27+
</subviews>
28+
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
29+
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
30+
<constraints>
31+
<constraint firstItem="BitBox-Logo" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="logo-centerX"/>
32+
<constraint firstItem="BitBox-Logo" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="logo-centerY"/>
33+
</constraints>
34+
</view>
35+
</viewController>
36+
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
37+
</objects>
38+
<point key="canvasLocation" x="53" y="375"/>
39+
</scene>
40+
</scenes>
41+
<resources>
42+
<image name="dbb_logo_light" width="68" height="81"/>
43+
<systemColor name="systemBackgroundColor">
44+
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
45+
</systemColor>
46+
</resources>
47+
</document>

frontends/web/src/app.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useDefault } from './hooks/default';
2323
import { usePrevious } from './hooks/previous';
2424
import { useIgnoreDrop } from './hooks/drop';
2525
import { usePlatformClass } from './hooks/platform';
26+
import { useAppReady } from './hooks/appReady';
2627
import { AppRouter } from './routes/router';
2728
import { Wizard as BitBox02Wizard } from './routes/device/bitbox02/wizard';
2829
import { getAccounts } from './api/account';
@@ -174,6 +175,8 @@ export const App = () => {
174175
const isBitboxBootloader = devices[deviceIDs[0]] === 'bitbox02-bootloader';
175176
const showBottomNavigation = (deviceIDs.length > 0 || activeAccounts.length > 0) && !isBitboxBootloader;
176177

178+
useAppReady(accounts, devices);
179+
177180
return (
178181
<ConnectedApp>
179182
<Providers>

frontends/web/src/globals.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export declare global {
2424
}
2525
onMobileCallResponse?: (queryID: number, response: unknown) => void;
2626
onMobilePushNotification?: (msg: TPayload) => void;
27+
reactAppReady?: boolean;
2728
runningOnIOS?: boolean;
2829
// Called by Android when the back button is pressed.
2930
onBackButtonPressed?: () => boolean;
@@ -32,6 +33,9 @@ export declare global {
3233
goCall: {
3334
postMessage: (msg: { queryID: number; query: string; }) => void;
3435
}
36+
appReady: {
37+
postMessage: (msg?: any) => void;
38+
}
3539
}
3640
}
3741
}

frontends/web/src/hooks/appReady.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Copyright 2024 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useEffect } from 'react';
18+
19+
/**
20+
* Hook to signal to native iOS WebView that React app is ready to be shown.
21+
* This prevents the white flicker on app launch by keeping the WebView hidden
22+
* until the React app has finished loading its initial data.
23+
*/
24+
export const useAppReady = (accounts: any, devices: any) => {
25+
useEffect(() => {
26+
if (accounts !== undefined && devices !== undefined) {
27+
window.reactAppReady = true;
28+
if (window.runningOnIOS && window.webkit?.messageHandlers.appReady) {
29+
window.webkit.messageHandlers.appReady.postMessage({});
30+
}
31+
}
32+
}, [accounts, devices]);
33+
};

0 commit comments

Comments
 (0)