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

如何使用SwiftData高效获取并排序多模型类型数据?

在SwiftData中高效实现混合类型数据的排序列表展示

需求描述

需要创建SwiftUI视图,将Photo和Video这类不同类型的对象展示在同一列表中,并基于共同属性creationDate逆序排序(类似iOS照片应用效果)。但SwiftData仅支持同类型记录的获取与排序,需寻找高效实现方式。

已排除的方案

  • 实体继承:Core Data不推荐使用该特性,且SwiftData完全不支持。
  • 内存全量合并排序:需提前加载所有记录,无法利用懒加载,数据量大时性能极差。

示例代码

Model

import SwiftData

protocol Media {
    var creationDate: Date { get set }
}

@Model
final class Photo: Media {
    var creationDate: Date
    var isLivePhoto: Bool
    
    init() {
        self.creationDate = Date()
        self.isLivePhoto = Bool.random()
    }
}

@Model
final class Video: Media {
    var creationDate: Date
    var duration: Int
    
    init() {
        self.creationDate = Date()
        self.duration = Int.random(in: 0..<10)
    }
}

View

import SwiftUI

struct MediaListView: View {
    @Query(sort: \Photo.creationDate, order: .reverse) var photos: [Photo]
    @Query(sort: \Video.creationDate, order: .reverse) var videos: [Video]
    @Environment(\.modelContext) var modelContext
    
    var body: some View {
        ScrollView {
            LazyVStack {
                // 如何在不加载所有记录到内存的情况下,展示按creationDate排序的Photo和Video列表?
            }
        }
        .onAppear() {               
            // 添加随机测试数据
            for _ in 0...10 {
                if Bool.random() {
                    modelContext.insert(Photo())
                }
                else {
                    modelContext.insert(Video())
                }
            }
        }
    }
}

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            MediaListView()
        }
        .modelContainer (for: [Photo.self, Video.self])
    }
}

解决方案:基于有序查询的懒加载归并合并

由于Photo和Video的查询已经按creationDate逆序排列,我们可以通过归并排序思路,在保留懒加载特性的前提下合并两个序列,无需一次性加载所有数据。

1. 封装统一媒体类型与懒加载合并集合

首先定义枚举统一处理不同媒体类型,再实现一个遵循RandomAccessCollection的自定义集合,完成有序序列的归并:

import SwiftData

// 统一媒体项枚举,便于视图处理
enum MediaItem: Identifiable {
    case photo(Photo)
    case video(Video)
    
    var id: ObjectIdentifier {
        switch self {
        case .photo(let photo): return photo.id
        case .video(let video): return video.id
        }
    }
    
    var creationDate: Date {
        switch self {
        case .photo(let photo): return photo.creationDate
        case .video(let video): return video.creationDate
        }
    }
}

// 懒加载的归并集合
struct MergedMediaCollection: RandomAccessCollection {
    typealias Element = MediaItem
    typealias Index = Int
    
    private let photos: [Photo]
    private let videos: [Video]
    private var mergedIndices: [(type: MediaType, index: Int)]
    
    enum MediaType { case photo, video }
    
    init(photos: [Photo], videos: [Video]) {
        self.photos = photos
        self.videos = videos
        
        // 归并排序生成索引映射
        var photoIdx = 0
        var videoIdx = 0
        var merged = [(MediaType, Int)]()
        
        while photoIdx < photos.count && videoIdx < videos.count {
            let photoDate = photos[photoIdx].creationDate
            let videoDate = videos[videoIdx].creationDate
            
            if photoDate > videoDate {
                merged.append((.photo, photoIdx))
                photoIdx += 1
            } else {
                merged.append((.video, videoIdx))
                videoIdx += 1
            }
        }
        
        // 追加剩余元素
        while photoIdx < photos.count {
            merged.append((.photo, photoIdx))
            photoIdx += 1
        }
        while videoIdx < videos.count {
            merged.append((.video, videoIdx))
            videoIdx += 1
        }
        
        self.mergedIndices = merged
    }
    
    // 集合核心属性
    var startIndex: Index { mergedIndices.startIndex }
    var endIndex: Index { mergedIndices.endIndex }
    
    subscript(position: Index) -> Element {
        let (type, idx) = mergedIndices[position]
        switch type {
        case .photo: return .photo(photos[idx])
        case .video: return .video(videos[idx])
        }
    }
    
    func index(after i: Index) -> Index { mergedIndices.index(after: i) }
    func index(before i: Index) -> Index { mergedIndices.index(before: i) }
}

2. 在视图中集成合并集合

修改MediaListView,将两个查询结果合并为懒加载序列,并通过ForEach渲染:

struct MediaListView: View {
    @Query(sort: \Photo.creationDate, order: .reverse) var photos: [Photo]
    @Query(sort: \Video.creationDate, order: .reverse) var videos: [Video]
    @Environment(\.modelContext) var modelContext
    
    // 计算属性:懒加载合并后的媒体序列
    private var mergedMedia: MergedMediaCollection {
        MergedMediaCollection(photos: photos, videos: videos)
    }
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(mergedMedia) { item in
                    MediaRow(item: item)
                        .padding(.vertical, 4)
                }
            }
            .padding()
        }
        .onAppear() {               
            // 添加随机测试数据
            for _ in 0...10 {
                Bool.random() ? modelContext.insert(Photo()) : modelContext.insert(Video())
            }
        }
    }
}

// 单个媒体项的行视图
struct MediaRow: View {
    let item: MediaItem
    
    var body: some View {
        HStack {
            Text(item.creationDate.formatted(date: .numeric, time: .standard))
                .font(.caption)
            
            Spacer()
            
            switch item {
            case .photo(let photo):
                Text("Photo \(photo.isLivePhoto ? "(Live)" : "")")
                    .font(.body)
            case .video(let video):
                Text("Video (\(video.duration)s)")
                    .font(.body)
            }
        }
        .padding(.horizontal, 8)
        .padding(.vertical, 6)
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

3. 大场景优化:分页加载(可选)

如果数据量极大,可给@Query添加分页限制,结合滚动触发加载更多:

// 修改查询为分页加载
@Query(sort: \Photo.creationDate, order: .reverse, limit: 20) var photos: [Photo]
@Query(sort: \Video.creationDate, order: .reverse, limit: 20) var videos: [Video]

// 加载更多数据的逻辑
func loadMore() {
    guard let lastPhotoDate = photos.last?.creationDate,
          let lastVideoDate = videos.last?.creationDate else { return }
    
    let earliestLoadedDate = min(lastPhotoDate, lastVideoDate)
    // 更新查询谓词,加载更早的数据
    photos = photos.filter { $0.creationDate < earliestLoadedDate }
    videos = videos.filter { $0.creationDate < earliestLoadedDate }
}

方案优势

  • 保留懒加载:@Query的懒加载特性完全保留,合并过程仅处理索引映射,不会提前加载全量数据。
  • 性能高效:归并排序时间复杂度为O(n+m),仅需一次遍历即可完成合并。
  • 类型安全:通过MediaItem枚举统一处理不同类型,避免类型转换风险。

内容的提问来源于stack exchange,提问作者Khắc Hảo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 16:05:40