iosswiftiphoneswiftuiios16

How to change the language without leaving the application on SwiftUI?


I want to make a modern language change, but I don’t know how to restart the application, or rather I know, but for me it works with an error and not the way I want. In other applications I saw that the language is updated without leaving the application, the slider is just spinning and that’s it, the language replaced. How to achieve this in SwiftUI?

This is how I run the application:

import SwiftUI

@main
 struct MyApp_UkraineApp: App {
    @StateObject var appSettings = AppSettings()

    var body: some Scene {
        WindowGroup {
            MainMenuView()
                .environmentObject(appSettings)
        }
    }
}

This is how I change languages:

import SwiftUI

struct MainMenuView: View {
    
    @EnvironmentObject private var appSettings: AppSettings
        
    var body: some View {
                HStack {
                    Button("Ukraine") {
                        Language.selected = .ukraine
                        resetApp()
                    }
                    Button("English") {
                        Language.selected = .english
                        resetApp()
                    }
                }
    
    func resetApp() {
        if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
            if let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate {
                windowSceneDelegate.window??.rootViewController = UIHostingController(rootView: MainMenuView())
                }
            }
        }
    }
}

enum Language: String, CaseIterable {
    case english, ukraine

    var code: String {
        switch self {
        case .english: return "en"
        case .ukraine: return "uk"
        }
    }

    static var selected: Language {
        set {
            UserDefaults.standard.set([newValue.code], forKey: "AppleLanguages")
            UserDefaults.standard.set(newValue.rawValue, forKey: "language")
        }
        get {
            return Language(rawValue: UserDefaults.standard.string(forKey: "language") ?? "") ?? .english
        }
    }

    static func switchLanguageBetweenEnglishAndGerman() {
        selected = selected == .english ? .ukraine : .english
    }
}

I get this error, I know why it appears, but I don’t know how to fix it:

Thread 1: Fatal error: No ObservableObject of type AppSettings found. A View.environmentObject(_:) for AppSettings may be missing as an ancestor of this view.

But the main question is how to make sure that there is a modern language update without restarting the application?


Solution

  • You can put environment(\.locale, ...) at the root of your app to set language.

    The environment is not directly settable though, so you will need to store the setting somewhere (in an @Observable or ObservableObject).

    @Observable
    class LanguageSetting {
        // initialise this from UserDefaults if you like
        var locale = Locale(identifier: "en")
    }
    
    // in your App conformance
    @State var languageSettings = LanguageSetting()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(languageSettings)
                .environment(\.locale, languageSettings.locale)
        }
    }
    

    When you want to change the language:

    @Environment(LanguageSetting.self) var languageSettings
    
    var body: some View {
        Button("Chinese Simplified") {
            // code to update user defaults omitted...
    
            languageSettings.locale = Locale(identifier: "zh-Hans")
        }
    }
    

    See the Observation migration guide for how to do this with ObservableObject.