Skip to content

Commit 6efdd2d

Browse files
graycreateclaude
andcommitted
feat: implement SwiftUI splash screen with typewriter animation
- Replace LaunchScreen.storyboard with SwiftUI SplashView - Add vector PDF logo from Android project for better scaling - Support light/dark mode with adaptive colors - Add TypewriterView with ease-out animation for slogan - Slogan: "Way to explore" with monospaced font - Logo size: 200x200, centered with slogan below 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 44c830c commit 6efdd2d

File tree

10 files changed

+186
-102
lines changed

10 files changed

+186
-102
lines changed

V2er.xcodeproj/project.pbxproj

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10-
0B1A2B3C4D5E6F7081920A01 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0B1A2B3C4D5E6F7081920A02 /* LaunchScreen.storyboard */; };
10+
0B1A2B3C4D5E6F7081920A03 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1A2B3C4D5E6F7081920A04 /* SplashView.swift */; };
11+
0B1A2B3C4D5E6F7081920A07 /* TypewriterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1A2B3C4D5E6F7081920A08 /* TypewriterView.swift */; };
1112
1AEBC3AC5DAA63523F5448F5 /* RichContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E205F350A3537A3E41B1AFC3 /* RichContentView.swift */; };
1213
28B24CA92EA3460D00F82B2A /* BalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B24CA82EA3460D00F82B2A /* BalanceView.swift */; };
1314
28B24CAB2EA3561400F82B2A /* OnlineStatsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B24CAA2EA3561400F82B2A /* OnlineStatsInfo.swift */; };
@@ -185,7 +186,8 @@
185186
/* End PBXContainerItemProxy section */
186187

187188
/* Begin PBXFileReference section */
188-
0B1A2B3C4D5E6F7081920A02 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
189+
0B1A2B3C4D5E6F7081920A04 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
190+
0B1A2B3C4D5E6F7081920A08 /* TypewriterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypewriterView.swift; sourceTree = "<group>"; };
189191
28B24CA82EA3460D00F82B2A /* BalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceView.swift; sourceTree = "<group>"; };
190192
28B24CAA2EA3561400F82B2A /* OnlineStatsInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineStatsInfo.swift; sourceTree = "<group>"; };
191193
31C4B81E79369CDE4880B773 /* RichContentView+Preview.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "RichContentView+Preview.swift"; path = "V2er/Sources/RichView/Views/RichContentView+Preview.swift"; sourceTree = "<group>"; };
@@ -532,7 +534,6 @@
532534
5D436FEE24791C2D00FFA37E /* Assets.xcassets */,
533535
5DEC5D662730F25800B34BC5 /* www */,
534536
5D436FF624791C2D00FFA37E /* Info.plist */,
535-
0B1A2B3C4D5E6F7081920A02 /* LaunchScreen.storyboard */,
536537
5D436FF024791C2D00FFA37E /* Preview Content */,
537538
);
538539
path = V2er;
@@ -721,6 +722,15 @@
721722
path = FeedDetail;
722723
sourceTree = "<group>";
723724
};
725+
0B1A2B3C4D5E6F7081920A06 /* Splash */ = {
726+
isa = PBXGroup;
727+
children = (
728+
0B1A2B3C4D5E6F7081920A04 /* SplashView.swift */,
729+
0B1A2B3C4D5E6F7081920A08 /* TypewriterView.swift */,
730+
);
731+
path = Splash;
732+
sourceTree = "<group>";
733+
};
724734
5DE5B4C826845F4F00569684 /* View */ = {
725735
isa = PBXGroup;
726736
children = (
@@ -737,6 +747,7 @@
737747
5D1D7B8526FC9AF6008E0C08 /* Login */,
738748
5D843E9626A46CB800C47D95 /* Message */,
739749
5D179BFD2496F6EC00E40E90 /* Widget */,
750+
0B1A2B3C4D5E6F7081920A06 /* Splash */,
740751
4EC32AF129D818FC003A3BD4 /* WebBrowserView.swift */,
741752
);
742753
path = View;
@@ -940,7 +951,6 @@
940951
isa = PBXResourcesBuildPhase;
941952
buildActionMask = 2147483647;
942953
files = (
943-
0B1A2B3C4D5E6F7081920A01 /* LaunchScreen.storyboard in Resources */,
944954
5D436FF224791C2D00FFA37E /* Preview Assets.xcassets in Resources */,
945955
5DEC5D7E2730F29000B34BC5 /* image_holder_loading.gif in Resources */,
946956
5DEC5D732730F28F00B34BC5 /* bootstrap.min.css in Resources */,
@@ -979,6 +989,8 @@
979989
isa = PBXSourcesBuildPhase;
980990
buildActionMask = 2147483647;
981991
files = (
992+
0B1A2B3C4D5E6F7081920A03 /* SplashView.swift in Sources */,
993+
0B1A2B3C4D5E6F7081920A07 /* TypewriterView.swift in Sources */,
982994
5D73FBDA27284ADB004558E9 /* RichText.swift in Sources */,
983995
5D2DD00A26FB443D0001C85A /* GlobalActions.swift in Sources */,
984996
5D71DF57247C153C00B53ED4 /* ExplorePage.swift in Sources */,

V2er/Assets.xcassets/Colors/SplashLogoColor.colorset/Contents.json

Lines changed: 0 additions & 38 deletions
This file was deleted.

V2er/Assets.xcassets/SplashLogo.imageset/Contents.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"images" : [
33
{
4-
"filename" : "splash_logo.png",
4+
"filename" : "splash_logo.pdf",
55
"idiom" : "universal"
66
}
77
],
1.4 KB
Binary file not shown.
-32.5 KB
Binary file not shown.

V2er/General/RootView.swift

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,20 +98,30 @@ struct RootHostView: View {
9898
$store.appState.loginState
9999
}
100100

101+
var launchFinished: Bool {
102+
store.appState.globalState.launchFinished
103+
}
104+
101105
var body: some View {
102-
MainPage()
103-
.buttonStyle(.plain)
104-
.toast(isPresented: toast.isPresented) {
105-
DefaultToastView(title: toast.title.raw, icon: toast.icon.raw)
106-
}
107-
.sheet(isPresented: loginState.showLoginView) {
108-
LoginPage()
109-
}
110-
.overlay {
111-
if loginState.raw.showTwoStepDialog {
112-
TwoStepLoginPage()
106+
ZStack {
107+
MainPage()
108+
.buttonStyle(.plain)
109+
.toast(isPresented: toast.isPresented) {
110+
DefaultToastView(title: toast.title.raw, icon: toast.icon.raw)
111+
}
112+
.sheet(isPresented: loginState.showLoginView) {
113+
LoginPage()
114+
}
115+
.overlay {
116+
if loginState.raw.showTwoStepDialog {
117+
TwoStepLoginPage()
118+
}
113119
}
114-
}
115120

121+
if !launchFinished {
122+
SplashView()
123+
.transition(.opacity)
124+
}
125+
}
116126
}
117127
}

V2er/LaunchScreen.storyboard

Lines changed: 0 additions & 47 deletions
This file was deleted.

V2er/State/DataFlow/State/GlobalState.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct GlobalState: FluxState {
1919
var lastSelectedTab: TabId = .none
2020
var scrollTopTab: TabId = .none
2121
var toast = Toast()
22+
var launchFinished: Bool = false
2223

2324
static var account: AccountInfo? {
2425
AccountState.getAccount()

V2er/View/Splash/SplashView.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//
2+
// SplashView.swift
3+
// V2er
4+
//
5+
// Created by Claude on 2024/12/1.
6+
//
7+
8+
import SwiftUI
9+
10+
struct SplashView: View {
11+
@EnvironmentObject private var store: Store
12+
@Environment(\.colorScheme) private var colorScheme
13+
14+
@State private var showSlogan = false
15+
16+
private let slogan = "Way to explore"
17+
18+
// Logo color adapts to color scheme (matches Android)
19+
private var logoColor: Color {
20+
colorScheme == .dark ? .white : Color(red: 0.067, green: 0.071, blue: 0.078)
21+
}
22+
23+
var body: some View {
24+
ZStack {
25+
// Background color - matches Android implementation
26+
Color("SplashBackground")
27+
.ignoresSafeArea()
28+
29+
// Logo - vector PDF with template rendering (fixed position)
30+
Image("SplashLogo")
31+
.renderingMode(.template)
32+
.resizable()
33+
.aspectRatio(contentMode: .fit)
34+
.frame(width: 200, height: 200)
35+
.foregroundColor(logoColor)
36+
37+
// Slogan with typewriter effect (fixed position below logo)
38+
if showSlogan {
39+
TypewriterView(text: slogan, typingDelay: .milliseconds(35))
40+
.font(.system(size: 17, weight: .semibold, design: .default))
41+
.foregroundColor(logoColor.opacity(0.85))
42+
.offset(y: 74)
43+
}
44+
}
45+
.onAppear {
46+
// Show slogan after a short delay
47+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
48+
showSlogan = true
49+
}
50+
51+
// Hide splash after animation completes
52+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
53+
withAnimation(.easeOut(duration: 0.3)) {
54+
store.appState.globalState.launchFinished = true
55+
}
56+
}
57+
}
58+
}
59+
}
60+
61+
#Preview {
62+
SplashView()
63+
.environmentObject(Store.shared)
64+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
// TypewriterView.swift
3+
// V2er
4+
//
5+
// Created by Claude on 2024/12/1.
6+
//
7+
8+
import SwiftUI
9+
10+
struct TypewriterView: View {
11+
var text: String
12+
var typingDelay: Duration = .milliseconds(50)
13+
var easeIn: Bool = true
14+
15+
@State private var animatedText: AttributedString = ""
16+
@State private var typingTask: Task<Void, Error>?
17+
@State private var hasAppeared = false
18+
19+
var body: some View {
20+
Text(animatedText)
21+
.onChange(of: text) { _ in
22+
if hasAppeared {
23+
animateText()
24+
}
25+
}
26+
.onAppear() {
27+
animateText()
28+
hasAppeared = true
29+
}
30+
}
31+
32+
private func animateText() {
33+
typingTask?.cancel()
34+
35+
typingTask = Task {
36+
let defaultAttributes = AttributeContainer()
37+
animatedText = AttributedString(text,
38+
attributes: defaultAttributes.foregroundColor(.clear)
39+
)
40+
41+
let totalChars = text.count
42+
var charIndex = 0
43+
var index = animatedText.startIndex
44+
45+
while index < animatedText.endIndex {
46+
try Task.checkCancellation()
47+
48+
// Update the style
49+
animatedText[animatedText.startIndex...index]
50+
.setAttributes(defaultAttributes)
51+
52+
// Calculate delay with ease-out effect (starts fast, slows down)
53+
let delay: Duration
54+
if easeIn && totalChars > 1 {
55+
// Ease-out: start fast, end slow - more natural typing feel
56+
let progress = Double(charIndex) / Double(totalChars - 1)
57+
let easeOutProgress = 1 - pow(1 - progress, 2) // quadratic ease-out
58+
let baseDelay = Double(typingDelay.components.attoseconds) / 1_000_000_000_000_000_000
59+
let minDelay = baseDelay * 0.6
60+
let maxDelay = baseDelay * 1.5
61+
let currentDelay = minDelay + (maxDelay - minDelay) * easeOutProgress
62+
delay = .milliseconds(Int(currentDelay * 1000))
63+
} else {
64+
delay = typingDelay
65+
}
66+
67+
// Wait
68+
try await Task.sleep(for: delay)
69+
70+
// Advance the index, character by character
71+
index = animatedText.index(afterCharacter: index)
72+
charIndex += 1
73+
}
74+
}
75+
}
76+
}
77+
78+
#Preview {
79+
TypewriterView(text: "Way to explore")
80+
.font(.title)
81+
.padding()
82+
}

0 commit comments

Comments
 (0)