Какие проблемы возникают при помещении синглтона в ObservedObject в SwiftUI?

«Какие проблемы возникают при помещении синглтона в ObservedObject в SwiftUI?» — вопрос из категории SwiftUI, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Размещение синглтона внутри ObservedObject создает архитектурные проблемы и может нарушить реактивность SwiftUI.

Основные проблемы:

  1. Нарушение реактивности – SwiftUI отслеживает изменения только самого @Published свойства ObservedObject. Если синглтон изменяется внутри, но свойство-обертка не меняется, View не обновится.

  2. Скрытая зависимость – Наличие синглтона внутри ViewModel скрывает реальную зависимость, усложняя понимание кода.

  3. Нарушение принципа единой ответственности – ViewModel становится посредником без добавленной ценности.

Пример проблемного кода:

class AppSettings {
    static let shared = AppSettings()
    var themeColor: Color = .blue
}

class ContentViewModel: ObservableObject {
    // Проблема: изменения в shared.themeColor не вызовут обновление View
    let settings = AppSettings.shared
}

struct ContentView: View {
    @ObservedObject var viewModel = ContentViewModel()

    var body: some View {
        Text("Hello")
            .foregroundColor(viewModel.settings.themeColor) // Не обновится при изменении themeColor
    }
}

Правильные подходы:

1. Прямое использование как ObservableObject:

// Синглтон должен быть ObservableObject
class AppSettings: ObservableObject {
    static let shared = AppSettings()
    @Published var themeColor: Color = .blue
}

struct ContentView: View {
    @ObservedObject var settings = AppSettings.shared

    var body: some View {
        Text("Hello")
            .foregroundColor(settings.themeColor) // Теперь обновится корректно
    }
}

2. Использование EnvironmentObject:

// В корне приложения
ContentView()
    .environmentObject(AppSettings.shared)

// В дочерней View
struct ChildView: View {
    @EnvironmentObject var settings: AppSettings

    var body: some View {
        Text("Hello")
            .foregroundColor(settings.themeColor)
    }
}

3. Внедрение зависимости через протокол:

protocol SettingsProvider {
    var themeColor: Color { get set }
}

class ContentViewModel: ObservableObject {
    @Published var settings: SettingsProvider

    init(settings: SettingsProvider = AppSettings.shared) {
        self.settings = settings
    }
}

Вывод: Избегайте помещения синглтона в ObservedObject как простого свойства. Вместо этого используйте синглтон напрямую как ObservableObject или через EnvironmentObject.