Как в SwiftUI и Combine передаешь данные из одного модуля в другой?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Передача данных между модулями в SwiftUI и Combine
В современной iOS-разработке передача данных между модулями строится на принципах однонаправленного потока данных и реактивного программирования. Вот ключевые подходы, которые я использую в production-проектах.
1. Через @StateObject / @ObservedObject / @EnvironmentObject
Наиболее распространенный способ при работе с SwiftUI — использование Combine Publishers внутри ObservableObject, которые затем инжектируются в иерархию представлений.
// Модуль А: Создание и управление данными
class DataService: ObservableObject {
@Published var userData: UserData?
private var cancellables = Set<AnyCancellable>()
func fetchData() {
// Логика загрузки данных
$userData
.sink { newValue in
print("Data updated: \(newValue)")
}
.store(in: &cancellables)
}
}
// Модуль Б: Получение данных через @StateObject
struct ModuleAView: View {
@StateObject private var service = DataService()
var body: some View {
VStack {
Text(service.userData?.name ?? "No data")
ModuleBView()
.environmentObject(service) // Передача в дочерние модули
}
}
}
// Модуль В: Использование через @EnvironmentObject
struct ModuleBView: View {
@EnvironmentObject var service: DataService
var body: some View {
Button("Update") {
service.userData = UserData(name: "Updated")
}
}
}
2. Dependency Injection с протоколами
Для тестируемости и соблюдения Dependency Inversion Principle я использую протоколы:
// Общий протокол в отдельном модуле
protocol DataProviderProtocol {
var currentData: AnyPublisher<DataModel, Never> { get }
func updateData(_ newData: DataModel)
}
// Реализация в модуле А
class DataProvider: DataProviderProtocol {
private let dataSubject = CurrentValueSubject<DataModel, Never>(DataModel())
var currentData: AnyPublisher<DataModel, Never> {
dataSubject.eraseToAnyPublisher()
}
func updateData(_ newData: DataModel) {
dataSubject.send(newData)
}
}
// Использование в модуле Б через инъекцию
class ModuleBViewModel {
private let dataProvider: DataProviderProtocol
private var cancellables = Set<AnyCancellable>()
init(dataProvider: DataProviderProtocol) {
self.dataProvider = dataProvider
setupBindings()
}
private func setupBindings() {
dataProvider.currentData
.sink { [weak self] data in
self?.processData(data)
}
.store(in: &cancellables)
}
}
3. Router/Coordinator с передачей данных
Для навигации между модулями с передачей параметров:
class AppCoordinator: ObservableObject {
@Published var currentRoute: Route?
enum Route: Hashable {
case detail(itemId: UUID)
case settings(config: AppConfig)
}
func navigateToDetail(with item: DataItem) {
currentRoute = .detail(itemId: item.id)
}
}
// В SwiftUI используем NavigationStack с привязкой
struct RootView: View {
@StateObject private var coordinator = AppCoordinator()
var body: some View {
NavigationStack(path: $coordinator.routes) {
HomeView()
.navigationDestination(for: Route.self) { route in
switch route {
case .detail(let itemId):
DetailView(itemId: itemId)
case .settings(let config):
SettingsView(config: config)
}
}
}
.environmentObject(coordinator)
}
}
4. Shared Data Containers (например, для модульного приложения)
В случае модульной архитектуры с отдельными Swift Packages:
// Shared модуль с общими моделями и интерфейсами
public struct AppState {
public var user: User?
public var settings: Settings
}
public class AppStore {
public static let shared = AppStore()
private let stateSubject = CurrentValueSubject<AppState, Never>(AppState())
public var state: AnyPublisher<AppState, Never> {
stateSubject.eraseToAnyPublisher()
}
public func updateUser(_ user: User) {
var newState = stateSubject.value
newState.user = user
stateSubject.send(newState)
}
}
// В любом модуле
import SharedModule
class FeatureViewModel {
init() {
AppStore.shared.state
.map { $0.user }
.removeDuplicates()
.sink { user in
// Реакция на изменения пользователя
}
.store(in: &cancellables)
}
}
5. NotificationCenter с Combine
Для широковещательных событий между несвязанными модулями:
extension Notification.Name {
static let dataUpdated = Notification.Name("dataUpdated")
}
class DataEmitter {
func emitUpdate() {
NotificationCenter.default.post(
name: .dataUpdated,
object: nil,
userInfo: ["data": newData]
)
}
}
class DataReceiver {
init() {
NotificationCenter.default.publisher(for: .dataUpdated)
.compactMap { $0.userInfo?["data"] as? DataModel }
.sink { data in
self.handleUpdate(data)
}
.store(in: &cancellables)
}
}
Критические рекомендации
- Избегайте синглтонов там, где возможна dependency injection
- Используйте протоколы для межмодульного взаимодействия
- Управляйте жизненным циклом подписок Combine через
Set<AnyCancellable> - Тестируйте потоки данных с помощью
TestSchedulerиз Combine - Логируйте ключевые события в цепочках Publisher для отладки
Главный принцип: данные должны течь сверху вниз, события — снизу вверх. Родительские модули владеют источниками данных, дочерние — только читают и отправляют интенты на изменение. Это обеспечивает предсказуемость состояния приложения и упрощает отладку сложных взаимодействий.