Files
ios-client/NetBird/Source/App/Views/Components/CustomLottieView.swift
T
evgeniyChepelev f5b1de0cc7 Redesign app (#60)
* fix ui state for airplaine mode

* fix slide bar

* Keep VPN tunnel alive during network unavailability

- Add isNetworkUnavailable flag to NetBirdAdapter to track network state
- Modify ConnectionListener to stay in 'connecting' state when network
  is unavailable instead of transitioning to 'disconnected'
- Update PacketTunnelProvider to set network unavailable flag and
  trigger automatic reconnect when network returns
- Fix CustomLottieView to show grey 'Disconnected' state immediately
  when network is lost, without closing the VPN tunnel
- Ensure UI shows correct state after app foreground/background cycle

This allows the VPN tunnel to survive temporary network outages
(e.g. airplane mode) and automatically reconnect when network returns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix UI state after app foreground/background cycle

Show correct connected/disconnected state immediately when app returns
from background, without replaying animations. Use extensionStatus
(iOS VPN state) as the source of truth for UI state.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* netbird credential

* Update MainView.swift

* Separate button by state

* Add gogoleServiceInfo plist reference

* Remove dead code in animation state machine and fix copy-to-clipboard UX  - Remove unreachable shouldForceReset block in CustomLottieView (already handled by earlier return) - Guard against copying empty fqdn/ip strings when disconnected - Use consistent .smooth animation for both fqdn and ip copy feedback

- Remove unreachable shouldForceReset block in CustomLottieView (already handled by earlier return)
- Guard against copying empty fqdn/ip strings when disconnected
- Use consistent .smooth animation for both fqdn and ip copy feedback

* Tab bar

* Update peer view

* Add offline state handling and network warning banner

- Show "Offline" instead of "Connected" when VPN tunnel is active but device has no internet
- Add NetworkWarningBanner with "Network Issues" warning when connected without internet
- Remove InternetStatusView (online/offline indicator) from connection screen
- Add FirstLaunchView onboarding screen
- Remove unused components (CustomBackButton, Extensions, JustifiedText, SolidButton, TransparentGradientButton)
- Update AboutView, AdvancedView, ServerView, PeerTabView, RouteTabView, iOSNetworksView styling
- Add EmptyTabPlaceholder component

* Add AppButton component with liquid glass support for iOS 26+

* Update FirstLaunchView.swift

* old version

* Update project.pbxproj

* Code refactoring

* Update project.pbxproj

* Update MainViewModel.swift

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-02 15:23:51 +01:00

174 lines
7.0 KiB
Swift

import Foundation
import SwiftUI
import Lottie
import NetworkExtension
struct CustomLottieView: UIViewRepresentable {
@Environment(\.colorScheme) var colorScheme
@Binding var vpnState: VPNDisplayState
func makeUIView(context: Context) -> LottieAnimationView {
let animationView = LottieAnimationView()
animationView.animation = LottieAnimation.named(colorScheme == .dark ? "button-full2-dark" : "button-full2")
animationView.contentMode = .scaleAspectFit
// Set initial frame based on current state
if vpnState == .connected {
animationView.currentFrame = context.coordinator.connectedFrame
} else {
animationView.currentFrame = context.coordinator.disconnectedFrame
}
context.coordinator.currentState = vpnState
return animationView
}
func updateUIView(_ uiView: LottieAnimationView, context: Context) {
let previousState = context.coordinator.currentState
let newState = vpnState
guard previousState != newState else { return }
context.coordinator.currentState = newState
// Stop any running animation before starting a new one
if context.coordinator.isPlaying {
uiView.stop()
context.coordinator.isPlaying = false
}
switch (previousState, newState) {
// Normal flow: disconnected -> connecting
case (.disconnected, .connecting):
context.coordinator.playConnectingFadeIn(uiView: uiView)
// Normal flow: connecting -> connected
case (.connecting, .connected):
context.coordinator.playConnectingFadeOut(uiView: uiView)
// Normal flow: connected -> disconnecting
case (.connected, .disconnecting):
context.coordinator.playDisconnectingFadeIn(uiView: uiView)
// Normal flow: disconnecting -> disconnected
case (.disconnecting, .disconnected):
context.coordinator.playDisconnectingFadeOut(uiView: uiView)
// Edge case: connecting -> disconnecting (user cancelled)
case (.connecting, .disconnecting):
context.coordinator.playDisconnectingFadeIn(uiView: uiView)
// Edge case: connecting -> disconnected (failed or cancelled)
case (.connecting, .disconnected):
context.coordinator.playDisconnectingFadeOut(uiView: uiView)
// Edge case: disconnecting -> connecting (reconnect)
case (.disconnecting, .connecting):
context.coordinator.playConnectingFadeIn(uiView: uiView)
// Direct jump to connected (e.g. app foreground)
case (_, .connected):
uiView.currentFrame = context.coordinator.connectedFrame
// Direct jump to disconnected (e.g. network unavailable)
case (_, .disconnected):
uiView.currentFrame = context.coordinator.disconnectedFrame
default:
break
}
}
func makeCoordinator() -> Coordinator {
Coordinator(initialState: vpnState)
}
class Coordinator: NSObject {
var isPlaying = false
var currentState: VPNDisplayState
init(initialState: VPNDisplayState) {
self.currentState = initialState
super.init()
}
let connectedFrame: CGFloat = 142
let disconnectedFrame: CGFloat = 339
let connectingFadeIn: (startFrame: CGFloat, endFrame: CGFloat) = (0, 78)
let connectingLoopRange: (startFrame: CGFloat, endFrame: CGFloat) = (78, 120)
let connectingFadeOut: (startFrame: CGFloat, endFrame: CGFloat) = (121, 142)
let disconnectingFadeIn: (startFrame: CGFloat, endFrame: CGFloat) = (152, 214)
let disconnectingLoopRange: (startFrame: CGFloat, endFrame: CGFloat) = (215, 258)
let disconnectingFadeOut: (startFrame: CGFloat, endFrame: CGFloat) = (259, 339)
func playConnectingFadeIn(uiView: LottieAnimationView) {
self.isPlaying = true
uiView.play(fromFrame: connectingFadeIn.startFrame, toFrame: connectingFadeIn.endFrame, loopMode: .playOnce) { [weak self] _ in
guard let self = self else { return }
if self.currentState == .connected {
self.playConnectingFadeOut(uiView: uiView)
} else if self.currentState == .connecting {
self.playConnectingLoop(uiView: uiView)
} else {
self.isPlaying = false
}
}
}
func playConnectingLoop(uiView: LottieAnimationView) {
self.isPlaying = true
uiView.play(fromFrame: connectingLoopRange.startFrame, toFrame: connectingLoopRange.endFrame, loopMode: .playOnce) { [weak self] _ in
guard let self = self else { return }
if self.currentState == .connected {
self.playConnectingFadeOut(uiView: uiView)
} else if self.currentState == .connecting {
self.playConnectingLoop(uiView: uiView)
} else {
self.isPlaying = false
}
}
}
func playConnectingFadeOut(uiView: LottieAnimationView) {
self.isPlaying = true
uiView.play(fromFrame: connectingFadeOut.startFrame, toFrame: connectingFadeOut.endFrame, loopMode: .playOnce) { [weak self] _ in
self?.isPlaying = false
}
}
func playDisconnectingFadeIn(uiView: LottieAnimationView) {
self.isPlaying = true
uiView.play(fromFrame: disconnectingFadeIn.startFrame, toFrame: disconnectingFadeIn.endFrame, loopMode: .playOnce) { [weak self] _ in
guard let self = self else { return }
if self.currentState == .disconnected {
self.playDisconnectingFadeOut(uiView: uiView)
} else if self.currentState == .disconnecting {
self.playDisconnectingLoop(uiView: uiView)
} else {
self.isPlaying = false
}
}
}
func playDisconnectingLoop(uiView: LottieAnimationView) {
self.isPlaying = true
uiView.play(fromFrame: disconnectingLoopRange.startFrame, toFrame: disconnectingLoopRange.endFrame, loopMode: .playOnce) { [weak self] _ in
guard let self = self else { return }
if self.currentState == .disconnected {
self.playDisconnectingFadeOut(uiView: uiView)
} else if self.currentState == .disconnecting {
self.playDisconnectingLoop(uiView: uiView)
} else {
self.isPlaying = false
}
}
}
func playDisconnectingFadeOut(uiView: LottieAnimationView) {
self.isPlaying = true
uiView.play(fromFrame: disconnectingFadeOut.startFrame, toFrame: disconnectingFadeOut.endFrame, loopMode: .playOnce) { [weak self] _ in
self?.isPlaying = false
}
}
}
}