← Назад к вопросам

Что будет, если свойство не Published, а просто ссылка на Router, и мы хотим вызывать обновления для Router?

1.8 Middle🔥 161 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный вопрос, который затрагивает самую суть работы SwiftUI с моделью данных и механизмом ререндера. Давайте разберем детально.

Краткий ответ

Если свойство — простая ссылка на объект (например, Router), без обертки в @Published или @ObservedObject, то SwiftUI НЕ БУДЕТ автоматически обновлять View при изменении внутреннего состояния этого объекта. View "увидит" только изменение самой ссылки (если вы присвоите в нее совершенно новый экземпляр Router), но не изменения внутри этого экземпляра.

Детальный разбор механизма

SwiftUI обновляет интерфейс только тогда, когда обнаруживает изменение в источнике истины (Source of Truth), за которым он наблюдает. К таким источникам относятся:

  • @State
  • @StateObject
  • @ObservedObject (наблюдает за ObservableObject)
  • @EnvironmentObject
  • @Binding

Ключевой момент: Для класса, соответствующего протоколу ObservableObject (как, вероятно, ваш Router), триггером обновления является изменение свойства, помеченного как @Published. SwiftUI подписывается на objectWillChange издателя объекта.

Пример: Сравнение Published vs обычной ссылки

Допустим, у нас есть класс Router:

import Combine

// ObservableObject автоматически генерирует объект objectWillChange
class Router: ObservableObject {
    // Свойство, за которым SwiftUI может наблюдать
    @Published var currentPath: [String] = []

    // Свойство, изменение которого SwiftUI проигнорирует
    var internalState: String = "default"
}

Сценарий 1: Обычная ссылка (НЕ РАБОТАЕТ для обновлений)

struct ContentView1: View {
    // Простая ссылка - НЕ источник истины для SwiftUI
    let router: Router

    var body: some View {
        VStack {
            Text("Текущий путь: \(router.currentPath.joined(separator: " -> "))")
            Button("Добавить экран") {
                // Это изменит @Published свойство внутри router
                router.currentPath.append("Экран \(router.currentPath.count + 1)")
                // НО: ContentView1 НЕ ПЕРЕРИСОВЫВАЕТСЯ!
                // В консоли значение изменится, но интерфейс останется прежним.
            }
        }
    }
}

Сценарий 2: Наблюдаемая ссылка (РАБОТАЕТ)

struct ContentView2: View {
    // @ObservedObject сообщает SwiftUI: "Наблюдай за objectWillChange этого объекта"
    @ObservedObject var router: Router

    var body: some View {
        VStack {
            Text("Текущий путь: \(router.currentPath.joined(separator: " -> "))")
            Button("Добавить экран") {
                router.currentPath.append("Экран \(router.currentPath.count + 1)")
                // ContentView2 КОРРЕКТНО ПЕРЕРИСОВЫВАЕТСЯ,
                // так как реагирует на изменение @Published свойства router.currentPath.
            }
        }
    }
}

Как заставить View обновляться без @Published?

Если вы хотите, чтобы View реагировало на изменения объекта, доступного по простой ссылке, вам нужны обходные пути. Они зачастую менее эффективны и элегантны, чем использование @ObservedObject.

1. Принудительный ререндер через @State

Можно создать локальное @State свойство и менять его, чтобы триггерить обновление тела View.

struct ContentView3: View {
    let router: Router
    @State private var refreshID = UUID() // "Костыль" для принудительного обновления

    var body: some View {
        VStack {
            Text("Путь: \(router.currentPath.joined())")
            Button("Обновить") {
                router.internalState = "новое значение"
                // Меняем @State свойство, чтобы форсировать пересчет body
                refreshID = UUID()
            }
            .id(refreshID) // Изменение id полностью пересоздает кнопку
        }
    }
}

2. Явная подписка и обновление через @State

Вручную подписаться на изменения и обновлять локальное состояние.

import Combine

struct ContentView4: View {
    let router: Router
    @State private var currentPathForView: [String] = []
    @State private var cancellable: AnyCancellable?

    var body: some View {
        VStack {
            Text("Путь: \(currentPathForView.joined())")
        }
        .onAppear {
            // Вручную подписываемся на Publisher
            cancellable = router.$currentPath
                .receive(on: RunLoop.main)
                .assign(to: \.currentPathForView, on: self)
        }
        .onDisappear {
            cancellable?.cancel()
        }
    }
}

3. Использование @Binding к локальному @State

Если изменение должно инициироваться извне, можно использовать Binding.

struct ParentView: View {
    @StateObject private var router = Router() // Источник истины здесь

    var body: some View {
        ChildView(path: $router.currentPath) // Передаем Binding
    }
}

struct ChildView: View {
    @Binding var path: [String] // Связь с Published свойством родителя

    var body: some View {
        Button("Добавить") {
            path.append("Из ChildView")
        }
    }
}

Выводы и рекомендации

  • Используйте @ObservedObject / @StateObject: Это идиоматичный и эффективный способ заставить SwiftUI реагировать на изменения внутри ObservableObject-классов (как Router). @Published свойства внутри такого класса — правильные триггеры обновлений.
  • Простая ссылка — это просто значение: Для SwiftUI это все равно что let number: Int. View обновится, только если вы присвоите в эту ссылку совершенно новый экземпляр объекта (чего обычно не происходит с роутером).
  • Обходные пути — это крайняя мера: Применяйте методы с @State-триггерами или ручными подписками только в исключительных ситуациях, например, при интеграции с не-SwiftUI кодом. Они усложняют код и могут привести к проблемам с производительностью.

Итог: Для Router, который управляет навигацией и должен изменять интерфейс, оборачивание в @ObservedObject (в дочерних View) или @StateObject (в корневом View, создающем роутер) является стандартным и необходимым подходом. Простая ссылка не даст вам реактивного обновления интерфейса.