使用SwiftData的 OutlineGroup

编程语言 2026-07-12

我的SwiftData模型由一个名为 Entry 的模型实体组成。目前我把它们显示在一个 List 中:

@Model
final class Entry: Decodable, Identifiable {
   var fullName: String = ""
   // ...
}

struct EntriesView: View {
//...
    @Query(sort: \Entry.fullName) private var entries: [Entry]

    var body: some View {
        List {
            ForEach(entries, id: \.fullName) { entry in
                NavigationLink {
                    EntryView(entry: entry) // EntryView shows the Entry details
                } label: {
                    Text(entry.fullName)
                }
            }
        }
    }
}

我想做的是在列表中添加一个文件夹结构,类似于Xcode。所以会有带有子文件夹的文件夹结构,每个文件夹包含一个或多个 Entry

我找到了Apple的这个示例:

struct FileItem: Hashable, Identifiable, CustomStringConvertible {
    var id: Self { self }
    var name: String
    var children: [FileItem]? = nil
    var description: String {
        switch children {
        case nil:
            return "📄 \(name)"
        case .some(let children):
            return children.isEmpty ? "📂 \(name)" : "📁 \(name)"
        }
    }
}


let data =
  FileItem(name: "users", children:
    [FileItem(name: "user1234", children:
      [FileItem(name: "Photos", children:
        [FileItem(name: "photo001.jpg"),
         FileItem(name: "photo002.jpg")]),
       FileItem(name: "Movies", children:
         [FileItem(name: "movie001.mp4")]),
          FileItem(name: "Documents", children: [])
      ]),
     FileItem(name: "newuser", children:
       [FileItem(name: "Documents", children: [])
       ])
    ])


OutlineGroup(data, children: \.children) { item in
    Text("\(item.description)")
}

但是我不明白如何将其翻译成我的情况。我应该为文件夹创建一个 SwiftData 实体吗?

编辑:只有文件夹应该有子项,条目是最终叶子。

解决方案

有两个主要修改你需要对 FileItem 在文档示例中配合SwiftData工作。

  • 为反向关系添加一个额外的 parent 属性。你可以使用 parent == nil 作为谓词来仅获取顶层项。
  • 添加一个 isFolder 属性以区分文件夹和非文件夹。OutlineGroup 指向 [T]? 的键路径,以区分“not expandable at all”(nil)和“expandable but no children”([])。然而,在SwiftData中并没有这样的区分。
@Model
class FileItem: CustomStringConvertible {
    var name: String
    @Relationship(inverse: \FileItem.parent)
    var children: [FileItem]
    var parent: FileItem?
    var isFolder: Bool

    // this is the key path we will pass to OutlineGroup
    var outlineGroupChildren: [FileItem]? {
        if isFolder {
            children
        } else {
            nil
        }
    }

    init(name: String, children: [FileItem]? = nil) {
        self.name = name
        self.children = children ?? []
        isFolder = children != nil
    }

    var description: String {
        if isFolder {
            return "📄 \(name)"
        } else {
            return children.isEmpty ? "📂 \(name)" : "📁 \(name)"
        }
    }
}

struct ContentView: View {
    @Query(filter: #Predicate<FileItem> { $0.parent == nil }) private var fileItems: [FileItem]
    @Environment(\.modelContext) var ctx
    var body: some View {
        Button("Add data") {
            let data =
            FileItem(name: "users", children:
                [FileItem(name: "user1234", children:
                  [FileItem(name: "Photos", children:
                    [FileItem(name: "photo001.jpg"),
                     FileItem(name: "photo002.jpg")]),
                   FileItem(name: "Movies", children:
                     [FileItem(name: "movie001.mp4")]),
                      FileItem(name: "Documents", children: [])
                  ]),
                 FileItem(name: "newuser", children:
                   [FileItem(name: "Documents", children: [])
                   ])
                ])
            ctx.insert(data)
        }

        List {
            OutlineGroup(fileItems, children: \.outlineGroupChildren) { item in
                Text("\(item.description)")
            }
        }
        .padding()
    }
}

回到你的用例,文件夹和条目都将由这个 FileItem 类型表示。文件夹是 FileItems,其中 isFolder 为true,条目是 FileItems,其中 isFolder 为false。

如果你想使用不同的 @Model 类来建模文件夹和条目(例如 FolderEntry),那么你不能直接使用 OutlineGroup。要使用 OutlineGroup,叶子必须由与父项相同的类型表示。你需要把 @Model 对象映射成如下的枚举:

enum FileItem {
    case entry(Entry)
    case folder(Folder)

    var outlineGroupChildren: [FileItem]? { ... }
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章