Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ (llm-ios) animated launchscreen swift module #9592

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/neat-berries-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"live-mobile": minor
---

Create a swift module that will allows us to control the display of the gif launchscreen. Also use this module to display an animated IOS splashscreen.
1 change: 1 addition & 0 deletions apps/ledger-live-mobile/e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export async function launchApp() {
'\\(".*sdk.*.braze.*",".*.googleapis.com/.*",".*clients3.google.com.*",".*tron.coin.ledger.com/wallet/getBrokerage.*"\\)',
mock: getEnv("MOCK") ? getEnv("MOCK") : "0",
disable_broadcast: getEnv("DISABLE_TRANSACTION_BROADCAST") ? 1 : 0,
IS_TEST: true,
},
languageAndLocale: {
language: "en-US",
Expand Down
22 changes: 4 additions & 18 deletions apps/ledger-live-mobile/ios/LaunchScreen.storyboard
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
<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="Y6W-OH-hqX">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
Expand All @@ -15,27 +15,13 @@
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" preservesSuperviewLayoutMargins="YES" image="Logo_nocache1" id="2tu-j2-iR2">
<rect key="frame" x="132" y="362" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES" flexibleMaxY="YES"/>
<constraints>
<constraint firstAttribute="width" constant="128" id="1yt-In-1lb"/>
<constraint firstAttribute="width" secondItem="2tu-j2-iR2" secondAttribute="height" multiplier="1:1" id="Qer-VL-bzv"/>
<constraint firstAttribute="width" secondItem="2tu-j2-iR2" secondAttribute="height" multiplier="1:1" id="SyH-01-2Nr"/>
</constraints>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" red="0.039215686274509803" green="0.039215686274509803" blue="0.039215686274509803" alpha="1" colorSpace="calibratedRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-170" y="94"/>
<point key="canvasLocation" x="-170.22900763358777" y="93.661971830985919"/>
</scene>
</scenes>
<resources>
<image name="Logo_nocache1" width="512" height="512"/>
</resources>
</document>
66 changes: 66 additions & 0 deletions apps/ledger-live-mobile/ios/SplashScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// SplashScreen.swift
// ledgerlivemobile
//
// Created by Lucas WEREY on 17/03/2025.
// Copyright Β© 2025 Ledger SAS. All rights reserved.
//

import Foundation
import React
import UIKit

@objc(RNSplashScreenModule)
public class RNSplashScreenModule: NSObject {

@objc
static func requiresMainQueueSetup() -> Bool {
return true
}

@objc func showSplashScreen() {
DispatchQueue.main.async {
let appDelegate = UIApplication.shared.delegate
let viewController = SplashScreenController()
viewController.modalPresentationStyle = .fullScreen
appDelegate?.window??.rootViewController?.present(viewController, animated: false)
}
}

@objc func hideSplashScreen() {
DispatchQueue.main.async {
self.fadeOutAndDismissSplashScreen()
}
}

private func fadeOutAndDismissSplashScreen() {
guard let appDelegate = UIApplication.shared.delegate,
let presentedViewController = appDelegate.window??.rootViewController?.presentedViewController as? SplashScreenController else {
return
}

let fadeOutDuration = 0.3
let scaleDownTransform = CGAffineTransform(scaleX: 0.95, y: 0.95)

let animateFadeOutAndDismiss = {
UIView.animate(withDuration: fadeOutDuration,
delay: 0,
options: .curveEaseOut,
animations: {
presentedViewController.view.alpha = 0
presentedViewController.view.transform = scaleDownTransform
},
completion: { _ in
presentedViewController.dismiss(animated: false) {
presentedViewController.view.removeFromSuperview()
}
})
}

if presentedViewController.gifCompletedOnce {
animateFadeOutAndDismiss()
} else {
presentedViewController.onGifCompletion = animateFadeOutAndDismiss
}
}
}
128 changes: 128 additions & 0 deletions apps/ledger-live-mobile/ios/SplashScreenController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// SplashScreenController.swift
// ledgerlivemobile
//
// Created by Lucas WEREY on 17/03/2025.
// Copyright Β© 2025 Ledger SAS. All rights reserved.
//

import Foundation
import ImageIO
import UIKit

class SplashScreenController: UIViewController {
var displayLink: CADisplayLink?
var startTime: CFTimeInterval = 0
var gifImages: [UIImage] = []
var gifDuration: TimeInterval = 0
var imageView: UIImageView?
var gifCompletedOnce: Bool = false
var onGifCompletion: (() -> Void)?

override func viewDidLoad() {
super.viewDidLoad()
self.view?.backgroundColor = UIColor(
red: 12 / 255.0, green: 12 / 255.0, blue: 12 / 255.0, alpha: 1.0)

if let gifPath = Bundle.main.path(forResource: "logo", ofType: "gif"),
let gifData = NSData(contentsOfFile: gifPath),
let gifImage = UIImage.gif(data: gifData as Data)
{
imageView = UIImageView(image: gifImage)
imageView?.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(imageView!)

NSLayoutConstraint.activate([
imageView!.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
imageView!.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
imageView!.heightAnchor.constraint(equalToConstant: 42),
imageView!.widthAnchor.constraint(equalToConstant: 207),
])

if let source = CGImageSourceCreateWithData(gifData, nil) {
let count = CGImageSourceGetCount(source)
for i in 0..<count {
if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
gifImages.append(UIImage(cgImage: image))
}
let frameProperties =
CGImageSourceCopyPropertiesAtIndex(source, i, nil) as? [String: Any]
let gifProperties =
frameProperties?[kCGImagePropertyGIFDictionary as String]
as? [String: Any]
if let delayTime = gifProperties?[
kCGImagePropertyGIFUnclampedDelayTime as String] as? Double
{
gifDuration += delayTime
} else if let delayTime = gifProperties?[
kCGImagePropertyGIFDelayTime as String] as? Double
{
gifDuration += delayTime
}
}
}

displayLink = CADisplayLink(target: self, selector: #selector(updateGif))
displayLink?.add(to: .main, forMode: .default)
startTime = CACurrentMediaTime()
}
}

@objc func updateGif() {
let elapsedTime = CACurrentMediaTime() - startTime
if elapsedTime > gifDuration {
gifCompletedOnce = true
displayLink?.invalidate()
displayLink = nil
onGifCompletion?()
return
}

let frameIndex = Int((elapsedTime / gifDuration) * Double(gifImages.count))
imageView?.image = gifImages[frameIndex]
}
}

extension UIImage {
public class func gif(data: Data) -> UIImage? {
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
print("SwiftGif: Source for the image does not exist")
return nil
}

return UIImage.animatedImageWithSource(source)
}

class func animatedImageWithSource(_ source: CGImageSource) -> UIImage? {
let count = CGImageSourceGetCount(source)
var images = [CGImage]()
var duration: TimeInterval = 0

for i in 0..<count {
if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
images.append(image)
}

let frameProperties =
CGImageSourceCopyPropertiesAtIndex(source, i, nil) as? [String: Any]
let gifProperties =
frameProperties?[kCGImagePropertyGIFDictionary as String]
as? [String: Any]

if let delayTime = gifProperties?[
kCGImagePropertyGIFUnclampedDelayTime as String] as? Double
{
duration += delayTime
} else if let delayTime = gifProperties?[
kCGImagePropertyGIFDelayTime as String] as? Double
{
duration += delayTime
}
}

let animation = UIImage.animatedImage(
with: images.map { UIImage(cgImage: $0) }, duration: duration)

return animation
}
}
13 changes: 13 additions & 0 deletions apps/ledger-live-mobile/ios/SplashScreenModule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// splashScreenModule.h
// ledgerlivemobile
//
// Created by Lucas WEREY on 17/03/2025.
// Copyright Β© 2025 Ledger SAS. All rights reserved.
//


#import <React/RCTBridgeModule.h>

@interface RNSplashScreenModule : NSObject <RCTBridgeModule>
@end
18 changes: 18 additions & 0 deletions apps/ledger-live-mobile/ios/SplashScreenModule.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// SplashScreenModule.m
// ledgerlivemobile
//
// Created by Lucas WEREY on 17/03/2025.
// Copyright Β© 2025 Ledger SAS. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "React/RCTBridge.h"


@interface RCT_EXTERN_MODULE(RNSplashScreenModule, NSObject)

RCT_EXTERN_METHOD(showSplashScreen)
RCT_EXTERN_METHOD(hideSplashScreen)

@end
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#import <Expo/Expo.h>
#import "React/RCTBridgeModule.h"
#import "React/RCTViewManager.h"
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
BAE7949027D7C46A00C465F5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = BAE7948F27D7C46A00C465F5 /* GoogleService-Info.plist */; };
BC25CCBE6BEA4D62BE203C17 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F63C9E4AC284203A95B417E /* libz.tbd */; };
BC26C23ED1C649E7849FF99A /* HMAlphaMono-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 1EA470840B7C44A3BF38FA4A /* HMAlphaMono-Medium.otf */; };
CA226CA32D8B194600235086 /* logo.gif in Resources */ = {isa = PBXBuildFile; fileRef = CA226CA22D8B194600235086 /* logo.gif */; };
CA226CAA2D8B198200235086 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA226CA82D8B198200235086 /* SplashScreen.swift */; };
CA226CAB2D8B198200235086 /* SplashScreenModule.m in Sources */ = {isa = PBXBuildFile; fileRef = CA226CA92D8B198200235086 /* SplashScreenModule.m */; };
CA226CAE2D8B199800235086 /* SplashScreenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA226CAC2D8B199800235086 /* SplashScreenController.swift */; };
D93C63AD261B4E7183B66B1C /* Inter-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 4C2397DC873B4F8E91B750A8 /* Inter-Regular.otf */; };
E466275BA70845F3AFE1D695 /* Inter-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = D91EB0620BDE4709B6F44620 /* Inter-Bold.otf */; };
E55EFC09114D43B3AB041882 /* FontAwesome5_Pro_Solid.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 991AA9919E4840DBB799E117 /* FontAwesome5_Pro_Solid.ttf */; };
Expand Down Expand Up @@ -91,6 +95,11 @@
C44A2A704D1E428CB7694186 /* FontAwesome5_Solid.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Solid.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf"; sourceTree = "<group>"; };
C630F76F0E1C857DA405027E /* Pods-ledgerlivemobile-ledgerlivemobileTests.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ledgerlivemobile-ledgerlivemobileTests.staging.xcconfig"; path = "Target Support Files/Pods-ledgerlivemobile-ledgerlivemobileTests/Pods-ledgerlivemobile-ledgerlivemobileTests.staging.xcconfig"; sourceTree = "<group>"; };
C7C5BB743ED04E53A2323979 /* OpenSans-SemiBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-SemiBold.ttf"; path = "../assets/fonts/OpenSans-SemiBold.ttf"; sourceTree = "<group>"; };
CA226CA22D8B194600235086 /* logo.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = logo.gif; sourceTree = "<group>"; };
CA226CA82D8B198200235086 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = "<group>"; };
CA226CA92D8B198200235086 /* SplashScreenModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SplashScreenModule.m; sourceTree = "<group>"; };
CA226CAC2D8B199800235086 /* SplashScreenController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenController.swift; sourceTree = "<group>"; };
CA226CAD2D8B199800235086 /* SplashScreenModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SplashScreenModule.h; sourceTree = "<group>"; };
CA4B7558A3274D76ACA6E26C /* MaterialIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf"; sourceTree = "<group>"; };
CDD38AE8681742F38FB51EFC /* MaterialCommunityIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialCommunityIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf"; sourceTree = "<group>"; };
CE7E1D553FBD49E0A85347D3 /* MuseoSans-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "MuseoSans-Bold.otf"; path = "../assets/fonts/MuseoSans-Bold.otf"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -173,6 +182,11 @@
13B07FAE1A68108700A75B9A /* ledgerlivemobile */ = {
isa = PBXGroup;
children = (
CA226CAC2D8B199800235086 /* SplashScreenController.swift */,
CA226CAD2D8B199800235086 /* SplashScreenModule.h */,
CA226CA82D8B198200235086 /* SplashScreen.swift */,
CA226CA92D8B198200235086 /* SplashScreenModule.m */,
CA226CA22D8B194600235086 /* logo.gif */,
17F58473269C64870070C475 /* RCTBluetoothHelperModule.h */,
17F58471269C64670070C475 /* RCTBluetoothHelperModule.m */,
2402D074219C2E6600276138 /* ledgerlivemobile.entitlements */,
Expand Down Expand Up @@ -399,6 +413,7 @@
D93C63AD261B4E7183B66B1C /* Inter-Regular.otf in Resources */,
B116B48438CB416C980DD0E7 /* Inter-SemiBold.otf in Resources */,
AD6EE26071FA4673B5C89936 /* FontAwesome5_Pro_Brands.ttf in Resources */,
CA226CA32D8B194600235086 /* logo.gif in Resources */,
F99BEAC629E8441C83104922 /* FontAwesome5_Pro_Light.ttf in Resources */,
2EA091AF2C3FBD5900B6181E /* GoogleService-Info-Testing.plist in Resources */,
E85A22C62B17E835005A8E6D /* LaunchScreen.storyboard in Resources */,
Expand Down Expand Up @@ -626,6 +641,9 @@
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
17F58472269C64670070C475 /* RCTBluetoothHelperModule.m in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
CA226CAA2D8B198200235086 /* SplashScreen.swift in Sources */,
CA226CAB2D8B198200235086 /* SplashScreenModule.m in Sources */,
CA226CAE2D8B199800235086 /* SplashScreenController.swift in Sources */,
F45A09B01925C0C58A9D40CE /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Loading
Loading