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

SwiftUI滚动视图加载Mock数据严重卡顿,求排查与优化方案

问题描述

我正在开发一款带有滚动视图的Feed流应用,使用SwiftUI构建界面,ViewModel提供数据。目前遵循Clean架构,尚未对接后端接口,在仓库层创建了Mocked文件来解码并返回本地存储的JSON文件。

相关代码

SwiftUI视图

struct FeedView: View {
    
    @StateObject private var viewModel = FeedViewModel()
    @State private var isFeedSelected: Bool = true

    var body: some View {
        NavigationView {
            ScrollView {
                LazyVStack {
                    if(isFeedSelected) {
                        ForEach(viewModel.feed) { post in
                            PostItemView(item: post)
                        }
                    } else {
                        ForEach(viewModel.classFeed) { classItem in
                            Text(classItem.userName)
                        }
                    }
                }
            }
            .padding([.top, .bottom], 20)
            .onAppear {
                if(isFeedSelected) {
                    viewModel.getFeed()
                } else {
                    viewModel.getClasses()
                }
            }
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    HStack {
                        Button(action: {
                            isFeedSelected = true
                        }) {
                            SelectButton(text: "Feed", isSelected: isFeedSelected == true)
                        }
                        Button(action: {
                            isFeedSelected = false
                        }) {
                            SelectButton(text: "Classes", isSelected: isFeedSelected == false)
                        }
                    }
                }
        }
    }
}

ViewModel代码

func getFeed() {
        feedUseCase
            .getFeed()
            .sink { res in
                switch res {
                case .failure(let err):
                    self.state = .error(error: err)
                default:
                    break
                }
            } receiveValue: { [weak self] feedsValue in
                self?.feed = feedsValue
                self?.state = .success
            }
            .store(in: &cancellables)
    }
    
    func getClasses() {
        feedUseCase
            .getClasses()
            .sink { res in
                switch res {
                case .failure(let err):
                    self.state = .error(error: err)
                default:
                    break
                }
            } receiveValue: { [weak self] value in
                self?.classFeed = value
                self?.state = .success
            }
            .store(in: &cancellables)
    }

UseCase文件

final class FeedUseCase {
    
    static let shared = FeedUseCase()
    
    let repository: FeedRepositoryProtocol
    
    init() {
        self.repository = FeedRepository.shared
    }
    
    func getFeed() -> AnyPublisher<[FeedItemEntity], Error> {
        return repository.getFeed()
            .eraseToAnyPublisher()
    }
    
    func getClasses() -> AnyPublisher<[ClassItemEntity], Error> {
        return repository.getClasses()
            .eraseToAnyPublisher()
    }
    
}

Repository协议及实现

protocol FeedRepositoryProtocol {
    func getFeed() -> AnyPublisher<[FeedItemEntity], Error>
    func getClasses() -> AnyPublisher<[ClassItemEntity], Error>
}

final class FeedRepository: FeedRepositoryProtocol {
        
    static let shared = FeedRepository()
    
    #if MOCKED
        let service: MockedFeedService
    #else
        let service: FeedService
    #endif
    
    init() {
        
        #if MOCKED
            self.service =  MockedFeedService.shared
        #else
            self.service =  FeedService.shared
        #endif 

    }
    
    func getFeed() -> AnyPublisher<[FeedItemEntity], Error> {
        return service.getFeed().eraseToAnyPublisher()
    }
    
    func getClasses() -> AnyPublisher<[ClassItemEntity], Error> {
        return service.getClasses().eraseToAnyPublisher()
    }
    
}

MockedFeedService代码

class MockedFeedService: FeedServiceProtocol {
    
    static let shared = MockedFeedService()

    
    func getFeed() -> Future<[ios_foodloose_app.FeedItemEntity], Error> {
        return Future<[FeedItemEntity], Error> { promise in
            
            guard let url = Bundle(for: MockedFeedService.self).url(forResource: "basic-test-feed-response",withExtension: "json"),
                  let data = try? Data(contentsOf: url) else {
                return promise(.failure(NSError(domain: "", code: 401, userInfo: [ NSLocalizedDescriptionKey: "Error"])))
            }
            
            do {
                let response = try JSONDecoder().decode( [FeedItemEntity].self, from: data)
                promise(.success(response))
            } catch {
                print("JSON Decode error: \(error)")
                promise(.failure(error))
            }
        }
    }
}

FeedItemEntity结构

struct FeedItemEntity: Codable {
    var ts: Int
    var userName: String
    var userLocation: String
    var userImageUrl: String
    var title: String
    var postImageUrl: String
    var postType: PostType
    var rating: Int
    var comments: CommentsEntity
    var likes: Int
    var hashTag: [String]
}

当前问题

App加载数据时会严重卡顿,长达5分钟无法使用,恢复后切换到Classes标签加载数据时,卡顿问题再次出现。请问哪里出错了?如果问题是代码在MainThread执行导致的,能否提供后台线程执行的示例代码?


问题分析与解决方案

核心问题原因

卡顿的直接原因是JSON文件读取和解析操作在主线程执行。Future的闭包默认会在调用它的线程(此处为主线程)运行,若你的测试JSON文件体积大,或FeedItemEntity嵌套层级复杂,这些耗时操作会完全阻塞主线程,导致界面失去响应。

解决方案:将耗时操作移至后台线程

修改MockedFeedService中的方法,把文件读取、JSON解析逻辑放到后台队列执行,完成后切回主线程返回结果。

修改后的MockedFeedService代码

class MockedFeedService: FeedServiceProtocol {
    
    static let shared = MockedFeedService()
    // 创建后台串行队列,专门处理耗时IO和解析操作
    private let backgroundQueue = DispatchQueue(label: "com.yourapp.feed.mockqueue", qos: .background)

    func getFeed() -> Future<[FeedItemEntity], Error> {
        return Future<[FeedItemEntity], Error> { [weak self] promise in
            // 切换到后台队列执行耗时逻辑
            self?.backgroundQueue.async {
                do {
                    guard let url = Bundle(for: MockedFeedService.self).url(forResource: "basic-test-feed-response", withExtension: "json") else {
                        throw NSError(domain: "", code: 401, userInfo: [NSLocalizedDescriptionKey: "JSON文件未找到"])
                    }
                    let data = try Data(contentsOf: url)
                    let response = try JSONDecoder().decode([FeedItemEntity].self, from: data)
                    // 切回主线程返回结果,避免后续操作在后台线程执行
                    DispatchQueue.main.async {
                        promise(.success(response))
                    }
                } catch {
                    print("JSON解析错误: \(error)")
                    DispatchQueue.main.async {
                        promise(.failure(error))
                    }
                }
            }
        }
    }
    
    // Classes逻辑同理修改
    func getClasses() -> Future<[ClassItemEntity], Error> {
        return Future<[ClassItemEntity], Error> { [weak self] promise in
            self?.backgroundQueue.async {
                do {
                    guard let url = Bundle(for: MockedFeedService.self).url(forResource: "basic-test-class-response", withExtension: "json") else {
                        throw NSError(domain: "", code: 401, userInfo: [NSLocalizedDescriptionKey: "JSON文件未找到"])
                    }
                    let data = try Data(contentsOf: url)
                    let response = try JSONDecoder().decode([ClassItemEntity].self, from: data)
                    DispatchQueue.main.async {
                        promise(.success(response))
                    }
                } catch {
                    print("JSON解析错误: \(error)")
                    DispatchQueue.main.async {
                        promise(.failure(error))
                    }
                }
            }
        }
    }
}

额外优化建议

  1. 替换try?为do-catch:原代码中try? Data(contentsOf: url)会隐藏具体错误信息,改用do-catch能捕获更详细的异常,方便调试。
  2. ViewModel显式指定主线程更新UI:虽然Combine的sink默认在主线程执行,但显式添加receive(on: DispatchQueue.main)能避免后续流程中意外在后台线程更新UI:
func getFeed() {
    feedUseCase
        .getFeed()
        .receive(on: DispatchQueue.main) // 确保UI更新在主线程
        .sink { res in
            switch res {
            case .failure(let err):
                self.state = .error(error: err)
            default:
                break
            }
        } receiveValue: { [weak self] feedsValue in
            self?.feed = feedsValue
            self?.state = .success
        }
        .store(in: &cancellables)
}
  1. 检查测试JSON大小:若JSON包含海量测试数据,即使在后台解析也会耗时,可考虑减少数据量或实现分批次加载逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 00:35:02