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

如何将Swift Data与SwiftUI视图解耦并封装为Swift Package

问题

我在iOS平台上使用Swift Data(仅支持iOS 17及以上版本)和SwiftUI开发,现有一个RecordingEditorWidget视图,目前通过@Bindable宏与Swift Data的VoiceMemo、Recording模型高度耦合。现在希望将该视图封装为独立的Swift Package,使其不依赖或强制使用特定的Memo或Recording类型(在Package中包含顶层数据模型并不合理)。

以下是关键关联的简化代码示例:

App代码

import SwiftUI
import SwiftData

@main
struct RecPackage_Stack_QApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }.modelContainer(for: VoiceMemo.self)
    }
}

数据模型代码

@Model
final class VoiceMemo {
    
    var name: String
    
    @Relationship(deleteRule: .cascade)
    var recording: Recording?
    
    func replaceRecording(newRecording: Recording) {
        recording = newRecording
    }
    
    func deleteRecording() {
        recording = nil
    }

    init(name: String, recording: Recording? = nil) {
        self.name = name
        self.recording = recording
    }
    
}

@Model
final class Recording {
    
    var attribute01:Double
    var attribute02:Double
    
    init(attribute01: Double = 0, attribute02: Double = 1) {
        self.attribute01 = attribute01
        self.attribute02 = attribute02
    }
    
}

ContentView代码

import SwiftUI
import SwiftData

enum Navigation {
    case main, edit
}

struct ContentView: View {
    
    @State var showEditScreen: Bool = false
    
    @Query private var memos: [VoiceMemo]
    @Environment(\.modelContext) var modelContext
    
    @State private var memoToEdit: VoiceMemo?
    
    var body: some View {
        
        if !showEditScreen {
            VStack {
                Spacer()
                Button("ADD MEMO") {
                    let newMemo = VoiceMemo(name: "new Memo...")
                    modelContext.insert(newMemo)
                    memoToEdit = newMemo
                    showEditScreen = true
                }
                Spacer()
                
                ForEach(memos, id: \.self) { memo in
                    HStack{
                        Button(memo.name) {
                        memoToEdit = memo
                        showEditScreen = true
                    }
                        if let rec = memo.recording {
                            Text(rec.attribute01.description)
                            Text(rec.attribute02.description)
                        }
                    }
                }
                Spacer()
            }
            .padding()
        } else {
            AddEditScreen(memo: memoToEdit!, showThisScreen: $showEditScreen)
            
        }
        
        
    }
}

AddEditScreen代码

struct AddEditScreen: View {
    
    @Bindable var memo:VoiceMemo
    @Binding var showThisScreen:Bool
    
    var body: some View {
        VStack(alignment: .leading) {
            Button("<< BACK to main") {
                showThisScreen = false
            }
            Spacer()
            TextField("hello", text: $memo.name)
            Spacer()
            RecordingEditorWidget(memo: memo)
            
        }.padding(20)
    }
    
}

待封装的RecordingEditorWidget代码

// 希望提取到独立Swift Package中的视图
struct RecordingEditorWidget: View {
    
    // 应移除对`VoiceMemo`和`Recording`类型的依赖
    @Bindable var memo:VoiceMemo
    
    @State var isRecording: Bool = false
    
    enum RecordingState {
        case blank, recording, recorded
    }
    
    var getState: RecordingState {
        
        if isRecording {
            return .recording
        } else if memo.recording == nil {
            return .blank
        } else {
            return .recorded
        }
    
    }
    
    var body: some View {
        
        ZStack{
            switch getState {
            case .blank:
                Button("Record") {
                    isRecording = true
                    // 模拟录制过程
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                        memo.recording = Recording()
                        isRecording = false
                    }
                }
            case .recording:
                Text("Recording in progress...")
            case .recorded:
                // 模拟音频编辑器
                @Bindable var recording = memo.recording!
                VStack {
                    Spacer()
                    Slider(value: $recording.attribute01)
                    Spacer()
                    Slider(value: $recording.attribute02)
                    Spacer()
                    Button("Trash Recording") {
                        memo.deleteRecording()
                    }
                }
            }
        }.background(.green.opacity(0.2))
    }
}
解决方案

核心思路是通过抽象依赖,让RecordingEditorWidget只关注自身UI逻辑和必要的交互回调,完全脱离对外部数据模型的直接依赖。具体步骤如下:


1. 创建Swift Package并实现通用视图

在Package中编写解耦后的RecordingEditorWidget,通过绑定和闭包回调实现与外部的交互:

// Swift Package中的RecordingEditorWidget
import SwiftUI

struct RecordingEditorWidget: View {
    // 外部传入:是否已有录制内容
    let hasRecording: Bool
    // 外部传入:两个可编辑属性的双向绑定
    @Binding var attribute01: Double
    @Binding var attribute02: Double
    // 交互回调:触发录制开始逻辑
    let onStartRecording: () -> Void
    // 交互回调:录制完成后通知外部处理数据
    let onFinishRecording: () -> Void
    // 交互回调:触发删除录制内容逻辑
    let onDeleteRecording: () -> Void
    
    @State private var isRecording: Bool = false
    
    private enum RecordingState {
        case blank, recording, recorded
    }
    
    private var currentState: RecordingState {
        if isRecording {
            return .recording
        } else if !hasRecording {
            return .blank
        } else {
            return .recorded
        }
    }
    
    var body: some View {
        ZStack {
            switch currentState {
            case .blank:
                Button("Record") {
                    isRecording = true
                    onStartRecording()
                    // 模拟录制时长
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                        isRecording = false
                        onFinishRecording()
                    }
                }
            case .recording:
                Text("Recording in progress...")
            case .recorded:
                VStack {
                    Spacer()
                    Slider(value: $attribute01)
                    Spacer()
                    Slider(value: $attribute02)
                    Spacer()
                    Button("Trash Recording") {
                        onDeleteRecording()
                    }
                }
            }
        }
        .background(.green.opacity(0.2))
    }
}

// Package内预览(可选)
struct RecordingEditorWidget_Previews: PreviewProvider {
    static var previews: some View {
        RecordingEditorWidget(
            hasRecording: false,
            attribute01: .constant(0),
            attribute02: .constant(1),
            onStartRecording: {},
            onFinishRecording: {},
            onDeleteRecording: {}
        )
    }
}

2. 主项目中适配使用

修改AddEditScreen,将VoiceMemo的属性和操作映射到Package视图的参数上:

struct AddEditScreen: View {
    @Bindable var memo: VoiceMemo
    @Binding var showThisScreen: Bool
    
    // 临时存储录制前的属性值,避免未创建Recording时的绑定空值问题
    @State private var tempAttribute01: Double = 0
    @State private var tempAttribute02: Double = 1
    
    var body: some View {
        VStack(alignment: .leading) {
            Button("<< BACK to main") {
                showThisScreen = false
            }
            Spacer()
            TextField("hello", text: $memo.name)
            Spacer()
            
            RecordingEditorWidget(
                hasRecording: memo.recording != nil,
                attribute01: bindingForAttribute01,
                attribute02: bindingForAttribute02,
                onStartRecording: {
                    // 录制开始前的自定义逻辑(可选)
                },
                onFinishRecording: {
                    // 录制完成后创建Recording实例并关联到memo
                    let newRecording = Recording(
                        attribute01: tempAttribute01,
                        attribute02: tempAttribute02
                    )
                    memo.replaceRecording(newRecording: newRecording)
                },
                onDeleteRecording: {
                    memo.deleteRecording()
                    // 重置临时属性值
                    tempAttribute01 = 0
                    tempAttribute02 = 1
                }
            )
        }
        .padding(20)
        .onAppear {
            // 页面加载时同步已有Recording的属性到临时变量
            if let recording = memo.recording {
                tempAttribute01 = recording.attribute01
                tempAttribute02 = recording.attribute02
            }
        }
    }
    
    // 自定义绑定:根据是否存在Recording切换绑定目标
    private var bindingForAttribute01: Binding<Double> {
        memo.recording != nil ? $memo.recording!.attribute01 : $tempAttribute01
    }
    
    private var bindingForAttribute02: Binding<Double> {
        memo.recording != nil ? $memo.recording!.attribute02 : $tempAttribute02
    }
}

3. 功能验证

修改完成后,RecordingEditorWidget完全独立于主项目的Swift Data模型:

  • 不依赖任何外部自定义数据类型
  • 所有数据交互通过绑定和闭包完成
  • 完整保留原有的录制、编辑、删除功能
  • 主项目只需负责将自身数据模型的逻辑映射到Package视图的参数中

内容的提问来源于stack exchange,提问作者Nerdy Bunz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 19:34:51