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

SwiftUI MVVM架构下笔记选择与更新的规范实现(含服务器交互场景)

SwiftUI MVVM架构下笔记选择与更新的规范实现(含服务器交互场景)

你遇到的列表+详情页更新同步的问题,确实是SwiftUI MVVM模式下非常典型的场景——既要处理服务器端的PUT请求,还要在更新成功后同步列表和详情页的数据,同时避免返回列表时数据被重置的尴尬。下面我结合你的代码,一步步给出规范的改造方案:

一、明确Model的核心职责

首先要让NoteModel成为唯一的数据源,封装所有数据逻辑(列表加载、单条更新、网络请求),视图只负责展示和触发动作,完全不关心数据的来源和处理细节。这里我们用SwiftUI 5+的@Observable特性,让Model的变化自动驱动视图刷新:

import Foundation

@Observable class NoteModel {
    var notes: [Note] = []
    var selectedNote: Note?
    private var isNotesFetched = false // 标记列表是否已加载,避免重复触发请求
    
    // 加载笔记列表(模拟网络请求)
    func fetchNotes() async {
        guard !isNotesFetched else { return }
        
        // 模拟网络延迟
        try? await Task.sleep(nanoseconds: 500_000_000)
        
        // 真实场景替换为GET请求获取列表
        self.notes = [
            Note(title: "First note", content: "Test note"),
            Note(title: "Reminder", content: "Don't forget to water the plants!"),
            Note(title: "Shopping list", content: "Eggs, milk, hat, bread")
        ]
        isNotesFetched = true
    }
    
    // 更新笔记(封装PUT请求+同步最新数据)
    func updateNote(_ targetNote: Note, newTitle: String, newContent: String) async throws {
        // 1. 模拟向服务器发送PUT请求
        try? await Task.sleep(nanoseconds: 300_000_000)
        // 真实场景替换为:
        // let updatedNote = try await APIClient.updateNote(targetNote.id, title: newTitle, content: newContent)
        
        // 2. 模拟服务器返回处理后的最新笔记(比如服务器自动添加标记)
        let updatedNote = Note(id: targetNote.id, title: "\(newTitle) [Server Updated]", content: newContent)
        
        // 3. 同步更新列表和选中的笔记
        if let index = notes.firstIndex(where: { $0.id == targetNote.id }) {
            notes[index] = updatedNote
        }
        if selectedNote?.id == targetNote.id {
            selectedNote = updatedNote
        }
    }
    
    // 可选:单独拉取单条最新笔记(比如更新后需要强制刷新)
    func fetchLatestNote(forId noteId: UUID) async {
        try? await Task.sleep(nanoseconds: 300_000_000)
        guard let index = notes.firstIndex(where: { $0.id == noteId }) else { return }
        let latestNote = Note(id: noteId, title: notes[index].title, content: notes[index].content + " (Synced from server)")
        notes[index] = latestNote
        selectedNote = latestNote
    }
}

// 数据模型(保持Identifiable、Hashable方便列表和导航使用)
struct Note: Identifiable, Hashable {
    let id = UUID()
    var title: String
    var content: String
    
    // 可选:添加初始化方法方便测试
    init(title: String, content: String) {
        self.title = title
        self.content = content
    }
}

二、列表页:简化视图逻辑,只做展示和导航

列表页不需要直接处理数据绑定,只需要展示model.notes,并在点击时标记selectedNote,同时解决之前返回列表数据重置的问题(通过isNotesFetched只加载一次,下拉刷新可强制重新加载):

import SwiftUI

struct NoteListView: View {
    @State private var model = NoteModel()

    var body: some View {
        NavigationStack {
            List(model.notes) { note in
                NavigationLink {
                    NoteDetailView(model: model, note: note)
                } label: {
                    Text(note.title)
                }
                .onTapGesture {
                    model.selectedNote = note // 点击时标记选中的笔记
                }
            }
            .navigationTitle("My Notes")
            .task {
                await model.fetchNotes() // 视图出现时加载列表(仅一次)
            }
            .refreshable {
                await model.fetchNotes() // 下拉刷新时强制重新加载列表
            }
        }
    }
}

三、详情页:专注用户输入,调用Model完成更新

详情页负责收集用户的编辑内容,然后调用Model的updateNote方法,所有数据更新逻辑交给Model处理,视图只需要处理加载状态和错误提示:

struct NoteDetailView: View {
    @ObservedObject var model: NoteModel
    let note: Note
    @State private var editedTitle: String
    @State private var editedContent: String
    @State private var isUpdating = false // 标记是否正在更新

    // 初始化时填充当前笔记内容
    init(model: NoteModel, note: Note) {
        self.model = model
        self.note = note
        self._editedTitle = State(initialValue: note.title)
        self._editedContent = State(initialValue: note.content)
    }

    var body: some View {
        Form {
            TextField("Note Title", text: $editedTitle)
                .disabled(isUpdating)
            
            TextEditor(text: $editedContent)
                .frame(height: 200)
                .disabled(isUpdating)
            
            Button("Save Changes") {
                Task {
                    isUpdating = true
                    do {
                        try await model.updateNote(note, newTitle: editedTitle, newContent: editedContent)
                        // 可选:更新成功后提示用户
                        print("Note updated successfully!")
                    } catch {
                        // 处理更新错误,比如弹出Alert
                        print("Failed to update note: \(error.localizedDescription)")
                    }
                    isUpdating = false
                }
            }
            .disabled(isUpdating || editedTitle.isEmpty)
        }
        .navigationTitle("Edit Note")
        .navigationBarTitleDisplayMode(.inline)
    }
}

四、核心设计思路说明

  1. 职责分离:Model管数据(网络、存储、更新),视图管展示和交互,完全符合MVVM的设计原则,后续修改网络逻辑只需要改Model,不需要动视图。
  2. 自动刷新:因为NoteModel是@Observable,当notes或selectedNote变化时,所有依赖这些数据的视图(列表、详情)会自动刷新,不需要手动发布通知。
  3. 避免数据重置:通过isNotesFetched标记,fetchNotes只会在首次加载时调用,返回列表时不会重复请求导致数据被覆盖;下拉刷新则可以强制重新加载。
  4. 服务器同步优先:更新操作不会直接修改本地数据,必须等服务器返回最新版本后再更新Model,确保本地数据和服务器完全一致,避免本地修改和服务器冲突。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 07:38:12