Обновление DetailView UI при изменении модели

swiftui

#1

Всем привет. Значит поступила задача сделать приложение для часов и представилась возможность более детально изучить SwiftUI.
Сразу к сути вопроса.
1-й экран простой список сессий

@EnvironmentObject var sessionManager: WatchSessionManager

var body: some View {
    NavigationView {
        List($sessionManager.sessionList) { s in
            NavigationLink(destination: SessionStartView(session: s) {
                SessionRow(session: s)
            }
        }
        .listStyle(.elliptical)
        .navigationBarTitle("Sessions")
    }
}

2-й экран детали сессии

@Binding var session: SessionModel
@State var isRunning = false

var body: some View {
    VStack {
        //вывод данных сессии
    }
    .toolbar {
        ToolbarItem(placement: .cancellationAction) {
                Button {
                    isRunning.toggle()
                    startStopSession() // тут идет изменение модели session
                } label: {
                    Text(isRunning ? "Stop" : "Start")
                }
        }
    }
    .onChange(of: session.status) { newValue in
        print("Status has changed")
    }
}

Ситуация следующая: в WatchSessionManager приходит команда с телефона на остановку сессии

if let id = response[CommandKeys.stopSession.rawValue] as? String,
   let session = sessionList.first(where: { $0.id == id && $0.status == true }) {
    session.status = false
    statusChanged(for: session) // тут идет обновление sessionList
}

Так вот, при этом 1-й экран обновляется. Как подхватить это обновление на 2-м экране, что бы там сделать обновление UI?
Я вспомнил о методе .onChange(), который прописал. Он срабатывает, но, он так же срабатывает на изменения при действии юзера, если нажать на ToolbarItem. Можно конечно добавить дополнительные флаги, что бы проверять не было ли это от юзера или от телефона, но вдруг есть способ более красивый.
Пока мне ничего не приходит в голову.

Призываю @ODiN’a :slight_smile:


#2

Покажите WatchSessionManager, а то пока непонятно, почему второе вью не обновляется

startStopSession() - это приватный метод во втором вью?

И не обновляется когда? Когда на кнопку на втором вью жмёшь?


#3

При действии юзера (нажатие на кнопку) все работает и обновляется.
Не обновляется когда с телефона приходит команда для остановки.

class WatchSessionManager: NSObject, ObservableObject {
    
    static let shared = WatchSessionManager()
    
    private var sessionManager: PhoneWatchConnection
    
    @Published var sessionList: [SessionModel] = []
    @Published var isLoading = false

    override private init() {
        ...
    }

    func statusChanged(for item: SessionModel) {
        DispatchQueue.main.async {
            if let i = self.sessionList.firstIndex(where: { $0.index == item.index }) {
                self.sessionList.remove(at: i)
                self.sessionList.insert(item, at: i)
            }
        }
        
        let message = [CommandKeys.statusChanged.rawValue: item.jsonString as Any]
        sessionManager.sendMessage(message: message, errorHandler: { (error) in
            print("Error sending message: \(error)")
        })
    }

    func loadSessions() {
        ...
        parseResponse(response)
    }

    private func parseResponse(_ response: [String: Any]) {
        if let json = response[CommandKeys.loadSessions.rawValue] as? String,
               let list = Mapper<SessionModel>().mapArray(JSONString: json) {
                DispatchQueue.main.async {
                    self.sessionList = list
                }
        }
    }
}

#4

не вижу параметра status. Я так понимаю status - это @Published. onChange же срабатывает во втором вью?


#5
class SessionModel: Object, Mappable, Identifiable {
    
    @objc dynamic var id: String = UUID().uuidString
    @objc dynamic var index: Int = 0
    @objc dynamic var name: String = ""
    @objc dynamic var status: Bool = false
    @objc dynamic var startDate: Date?
    @objc dynamic var endDate: Date?
    let workoutData = List<WorkoutData>()
}

Status не Published

.onChange во 2-м вью срабатывает, но мне кажется что это не совсем верно.

Попробую описать ситуацию для простоты.

Есть модель, у нее параметры (не Published, они и не должны быть такими).
Есть некий менеджер, где хранится список моделей. Этот менеджер соответственно обзервабл и сам список паблишед.
1-е вью показывает список моделей из менеджера (все как у меня)
2-е вью показывает данные по конкретной модели
В менеджере есть метод, который запускается после инициализации. В этом методе запускается таймер и по истечению оного, какая-то модель обновляется (меняется любой параметр). Что бы 1-е вью обновилось, этого мало, надо перезаписать модель в списке (удалить/добавить на тоже место). Вот тут вопрос как при этом обновить 2-е вью с этой моделью? 2-е вью при этом было открыто с этой моделью.


#6

Ну всё верно. Получается у вас на втором вью не используется для построения вью ни один параметр с паблишером. Самое просто решения для вью, которые не обновляются, назначить нужному вью .id. Если убрать этот id из вью (например в менеджер под паблишед), то обновляться будет только это вью и все дочерние. Если оставить id под @State внутри вью и менять его в onChange, то будет обновляться всё вью целиком.

class WatchSessionManager: NSObject, ObservableObject {
    @Published var id = UUID()
     func statusChanged(for item: SessionModel) { {
         ....
         id = UUID()
     }
}

VStack {
        //вывод данных сессии
}.id(session.id)

или так:
2.

@State private var id = UUID()
VStack {
        //вывод данных сессии
    }.id(id)
    .onChange(of: session.status) { _ in
        id = UUID()
    }

но первый вариант мне нравится больше тк не дёргаем всё вью (если сложное вью) - экономим ресурсы, вроде бы :grin:


#7

Спасибо за мысли. Буду пробовать.