You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将UserDefaults笔记数据同步到Apple Watch?同步失败求助

问题描述

我正在开发一款跨iPhone和Apple Watch的笔记应用,支持创建笔记和子笔记,两端CRUD操作已完成,但Watch端无法查看数据。采用UserDefaults共享组存储数据,尝试在NoteViewModel实现NSObject和WCSessionDelegate协议但未解决问题,启动应用时输出会话状态WCSessionActivationState(rawValue: 2)(即会话已激活)。

开发环境:Xcode 16 beta、iOS 18 beta、WatchOS 11 beta

相关代码

NoteViewModel.swift

import Combine
import Foundation
import WatchConnectivity

class NoteViewModel: NSObject, WCSessionDelegate, ObservableObject {
    @Published var notes: [Note] = []
    
    func addNote(title: String) {
        let newNote = Note(title: title, subNotes: [])
        notes.append(newNote)
        saveNotes()
    }

    func addSubNote(to note: Note, title: String, content: String) {
        if let index = notes.firstIndex(where: { $0.id == note.id }) {
            let newSubNote = SubNote(title: title, content: content)
            notes[index].subNotes.append(newSubNote)
            saveNotes()
        }
    }

    func removeNote(note: Note) {
        notes.removeAll { $0.id == note.id }
        saveNotes()
    }

    func removeSubNote(from note: Note, subNote: SubNote) {
        if let index = notes.firstIndex(where: { $0.id == note.id }) {
            notes[index].subNotes.removeAll { $0.id == subNote.id }
            saveNotes()
        }
    }

    func updateNote(note: Note, newTitle: String) {
        if let index = notes.firstIndex(where: { $0.id == note.id }) {
            notes[index].title = newTitle
            saveNotes()
        }
    }

    override init() {
        super.init()
        if WCSession.isSupported() {
            WCSession.default.delegate = self
            WCSession.default.activate()
            self.notes = loadNotes()
        }
    }

    func session(
        _ session: WCSession,
        activationDidCompleteWith activationState: WCSessionActivationState,
        error: Error?
    ) {
        if let error = error {
            print("Error al activar la sesión: \(error.localizedDescription)")
        } else {
            print("Sesión activada con estado: \(activationState)")
        }
    }

    #if os(iOS)
    func sessionDidBecomeInactive(_ session: WCSession) { }

    func sessionDidDeactivate(_ session: WCSession) {
        session.activate()
    }

    func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
        if let notesArray = message["notes"] as? [[String: Any]] {
            let decoder = JSONDecoder()
            if let data = try? JSONSerialization.data(withJSONObject: notesArray, options: []),
               let notes = try? decoder.decode([Note].self, from: data) {
                self.notes = notes
                saveNotes()
            }
        }
    }
    #endif

    private func saveNotes() {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(notes) {
            if let defaults = UserDefaults(suiteName: "group.lucasleone.WatchNotesApp") {
                defaults.set(encoded, forKey: "notes")
            }
            sendNotesToWatch(notes: notes)
        }
    }

    private func loadNotes() -> [Note] {
        if let defaults = UserDefaults(suiteName: "group.lucasleone.WatchNotesApp"),
           let savedNotes = defaults.data(forKey: "notes") {
            let decoder = JSONDecoder()
            if let loadedNotes = try? decoder.decode([Note].self, from: savedNotes) {
                return loadedNotes
            }
        }
        return []
    }

    private func sendNotesToWatch(notes: [Note]) {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(notes) {
            let notesDict = try? JSONSerialization.jsonObject(with: encoded, options: []) as? [[String: Any]]
            if WCSession.default.isReachable {
                WCSession.default.sendMessage(["notes": notesDict ?? []], replyHandler: nil, errorHandler: { error in
                    print("Error enviando mensaje: \(error.localizedDescription)")
                })
            }
        }
    }
}

Watch端HomeView.swift

struct HomeView: View {
    @StateObject var viewModel = NoteViewModel()

    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.notes) { note in
                    NavigationLink(
                        destination: NoteDetailView(
                            note: note, viewModel: viewModel)
                    ) {
                        Text(note.title)
                    }
                }
            }
            .navigationTitle("Notas")
        }
    }
}

解决方案

1. 补全Watch端WCSession消息处理逻辑

当前NoteViewModel仅在iOS端实现了消息接收,Watch端既没处理iOS发来的消息,也没有主动请求数据的逻辑。修改如下:

步骤1:统一消息接收逻辑

移除#if os(iOS)对didReceiveMessage的包裹,让两端都能处理消息,同时添加数据请求的响应逻辑:

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    // 接收iOS推送的笔记数据
    if let notesData = message["notesData"] as? Data {
        do {
            let notes = try JSONDecoder().decode([Note].self, from: notesData)
            DispatchQueue.main.async { // 必须在主线程更新@Published属性
                self.notes = notes
                self.saveNotes()
            }
            replyHandler(["status": "success"])
        } catch {
            print("解码数据失败: \(error.localizedDescription)")
            replyHandler(["status": "error", "message": error.localizedDescription])
        }
    } 
    // 响应Watch端的数据请求
    else if message["requestNotes"] as? Bool == true {
        do {
            let data = try JSONEncoder().encode(notes)
            replyHandler(["notesData": data])
        } catch {
            replyHandler(["status": "error", "message": error.localizedDescription])
        }
    }
}

步骤2:Watch端主动请求数据

在NoteViewModel初始化方法中添加主动请求逻辑:

override init() {
    super.init()
    if WCSession.isSupported() {
        WCSession.default.delegate = self
        WCSession.default.activate()
        self.notes = loadNotes()
        // Watch端启动后主动向iPhone请求数据
        requestNotesFromiPhone()
    }
}

// 新增方法:向iPhone请求笔记数据
private func requestNotesFromiPhone() {
    guard WCSession.default.isReachable else {
        print("iPhone不可达,无法请求数据")
        return
    }
    WCSession.default.sendMessage(["requestNotes": true], replyHandler: { response in
        if let notesData = response["notesData"] as? Data {
            do {
                let notes = try JSONDecoder().decode([Note].self, from: notesData)
                DispatchQueue.main.async {
                    self.notes = notes
                    self.saveNotes()
                }
            } catch {
                print("解码请求数据失败: \(error.localizedDescription)")
            }
        }
    }, errorHandler: { error in
        print("请求数据失败: \(error.localizedDescription)")
    })
}

2. 修复UserDefaults共享组同步问题

  • 确认iOS和WatchOS target均已启用App Groups功能,并加入同一个组group.lucasleone.WatchNotesApp
  • 在saveNotes中添加同步操作,避免写入延迟:
private func saveNotes() {
    let encoder = JSONEncoder()
    if let encoded = try? encoder.encode(notes),
       let defaults = UserDefaults(suiteName: "group.lucasleone.WatchNotesApp") {
        defaults.set(encoded, forKey: "notes")
        defaults.synchronize() // 强制同步共享组数据
        sendNotesToWatch(notes: notes)
    }
}

3. 简化数据序列化流程

当前代码存在冗余的序列化步骤,直接传输Data更高效且避免错误:

private func sendNotesToWatch(notes: [Note]) {
    guard WCSession.default.isReachable else { return }
    do {
        let data = try JSONEncoder().encode(notes)
        WCSession.default.sendMessage(["notesData": data], replyHandler: nil, errorHandler: { error in
            print("发送数据失败: \(error.localizedDescription)")
        })
    } catch {
        print("编码笔记失败: \(error.localizedDescription)")
    }
}

4. 确保模型正确实现Codable & Identifiable

Note和SubNote必须严格实现协议才能正常序列化和在ForEach中使用:

import Foundation

struct Note: Identifiable, Codable {
    let id = UUID()
    var title: String
    var subNotes: [SubNote]
}

struct SubNote: Identifiable, Codable {
    let id = UUID()
    var title: String
    var content: String
}

5. 会话激活后触发同步

在activationDidCompleteWith中添加激活后的同步逻辑:

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
    if let error = error {
        print("会话激活失败: \(error.localizedDescription)")
    } else {
        print("会话激活状态: \(activationState)")
        #if os(watchOS)
        // Watch端激活后再次尝试请求数据
        requestNotesFromiPhone()
        #else
        // iOS端激活后主动推送最新数据
        sendNotesToWatch(notes: notes)
        #endif
    }
}

内容的提问来源于stack exchange,提问作者Lucas Leone

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 18:59:49