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)) } } } } } }
额外优化建议
- 替换try?为do-catch:原代码中
try? Data(contentsOf: url)会隐藏具体错误信息,改用do-catch能捕获更详细的异常,方便调试。 - 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) }
- 检查测试JSON大小:若JSON包含海量测试数据,即使在后台解析也会耗时,可考虑减少数据量或实现分批次加载逻辑。
内容的提问来源于stack exchange,提问作者Seb
相关产品推荐
相关产品推荐

