skip to Main Content

Problem

I’m trying to implement a language change feature in my SwiftUI app where when a user selects a new language, the app should restart and appear in the selected language. Currently, the language changes, but the UI doesn’t fully update until the app is manually restarted.

Current Implementation


Language Selection View

struct LanguageSelectionView: View {
    @Environment(.dismiss) private var dismiss
    @Environment(.tabBarVisibility) var tabBarVisibility
    @StateObject private var localizationManager = LocalizationManager.shared
    @State private var selectedLanguage: Language
    @State private var showConfirmation = false
    @State private var languageToChange: Language?
    @AppStorage("restartKey") var restartKey: Bool = false

    var body: some View {
        NavigationView {
            // ... UI implementation ...
            .alert(isPresented: $showConfirmation) {
                Alert(
                    title: Text("Change Language"),
                    message: Text("The app needs to restart to change the language. Do you want to continue?"),
                    primaryButton: .default(Text("Yes")) {
                        if let language = languageToChange {
                            LocalizationManager.shared.setLanguage(language)
                            restartKey.toggle()  // This should trigger app restart
                        }
                    },
                    secondaryButton: .cancel()
                )
            }
        }
    }
}

Localization Manager


class LocalizationManager: ObservableObject {
    static let shared = LocalizationManager()
    
    @Published var currentLanguage: Language {
        didSet {
            UserDefaults.standard.set(currentLanguage.rawValue, forKey: "selectedLanguage")
            Bundle.setLanguageBundle(currentLanguage)
            NotificationCenter.default.post(name: Notification.Name("LanguageChanged"), object: nil)
        }
    }
    
    @AppStorage("restartKey") var restartKey: Bool = false
    
    func setLanguage(_ language: Language) {
        guard language != currentLanguage else { return }
        
        UserDefaults.standard.set([language.rawValue], forKey: "AppleLanguages")
        UserDefaults.standard.set(language.rawValue, forKey: "selectedLanguage")
        UserDefaults.standard.synchronize()
        
        currentLanguage = language
        
        // Attempt to trigger restart
        DispatchQueue.main.async {
            self.restartKey.toggle()
        }
    }
}

Main App


@main
struct FaceAIEditApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @StateObject private var authManager = AuthenticationManager.shared
    @StateObject private var localizationManager = LocalizationManager.shared
    @AppStorage("restartKey") var restartKey: Bool = false

    init() {
        // Set initial language bundle on app launch

        // TabBar appearance configuration
        let tabBarAppearance = UITabBarAppearance()
        tabBarAppearance.configureWithOpaqueBackground()
        tabBarAppearance.shadowColor = .clear
        tabBarAppearance.backgroundColor = .clear
        UITabBar.appearance().standardAppearance = tabBarAppearance
        
        if #available(iOS 15.0, *) {
            UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
        }
    }

    var body: some Scene {
        WindowGroup {
            ZStack {
                if authManager.isLoading {
                    // Show loading screen while checking auth state
                    ProgressView()
                        .scaleEffect(1.5)
                        .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                } else {
                    MainTabView()
                        .id(restartKey) // This triggers view rebuild on language change
                        .environment(.locale, .init(identifier: localizationManager.currentLanguage.rawValue))
                }
            }
            .environmentObject(authManager)
            .environmentObject(localizationManager) // Add localizationManager as environment object
            .environment(.language, localizationManager.currentLanguage.rawValue)
            .onAppear {
                authManager.checkAndSignInAnonymously()
            }
        }
    }
}

Issue

When changing the language:

  1. The language is saved correctly,
  2. Some UI elements don’t update,
  3. Restarting the app manually updates it,

Question

How can I programmatically restart the entire app when the user selects a new language so that:

  1. The app fully restarts,
  2. All views are updated to the new language,
  3. The app returns to its initial state (root view),
  4. The change is smooth and user-friendly

I’ve Tried:

  1. Using @AppStorage("restartKey") to trigger updates,
  2. Posting notifications for language changes,
  3. Force-updating views with the .id modifier,
  4. Various UIApplication window manipulations

Environment

  • iOS 15+
  • Swift 5.5
  • SwiftUI
  • Xcode 14

2

Answers


  1. I’m using the following extension:

    import SwiftUI
    
    public extension View {
        @ViewBuilder
        func forceLocale(_ locale: String? ) -> some View {
            if let locale {
                self.environment(.locale, Locale(identifier: locale))
            } else {
                self
            }
        }
    }
    

    if you need to use OS’s locale just call .forceLocale(nil)

    usage:

    import SwiftUI
    
    @main
    struct TestSwiftUIApp: App {
        var body: some Scene {
            WindowGroup {
                LanguageView()
                    .frame(width: 400, height: 400)
            }
        }
    }
    
    struct LanguageView: View {
        @State var identifier: String? = "en"
        
        var body: some View {
            VStack {
                Button("Ukrainian", action: {
                    self.identifier = "uk"
                })
                
                Button("English", action: {
                    self.identifier = "en"
                })
                
                Button("OS configured", action: {
                    self.identifier = nil
                })
                
                Text("hello")
            }
            .forceLocale(identifier)
        }
    }
    

    no app restart needed to apply this setting.

    Works well as you see:

    enter image description here

    Login or Signup to reply.
  2. On apps ive worked on, RootView would listen to notification center after user has selected a new language.

    Then paired with id modifier, the rootview get rebuilt with the new language.

    here is a sample code.

    @State private var current: Language = .en
    
    var body: some Scene {
        WindowGroup {
            RootView()
            .id(currentLanguage)
            .onAppear {
                current = //your saved language here
            }
            .onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: YOUR_NOTIFICATION_KEY))) { _ in
                current = LanguageUtil.current()
                // do any customize on views to retain customization on nav, etc..
    
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search