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

SwiftUI中MVVM模式下地图搜索建议的状态同步问题

SwiftUI MVVM架构下MKLocalSearchCompletion结果的状态同步方案

问题背景

在SwiftUI应用中遵循MVVM原则开发地图地点搜索界面时,遇到了MKLocalSearchCompleter的结果无法正确同步到视图的问题:

  • 定义了结构体类型的Model SearchState,包含currentLocation、currentText、suggestedLocations: [MKLocalSearchCompletion]等属性
  • ViewModel持有SearchState并通过@Published发布给视图
  • 已实现AutocompleteLocations类处理MKLocalSearchCompleter逻辑,但无法让其内部的建议列表变更触发视图更新

现有AutocompleteLocations代码

import MapKit

class AutocompleteLocations: NSObject, MKLocalSearchCompleterDelegate {

    var completer: MKLocalSearchCompleter
    var suggestions: [MKLocalSearchCompletion]
    
    init(suggestions: inout [MKLocalSearchCompletion]) {
        self.completer = MKLocalSearchCompleter()
        self.suggestions = suggestions
        super.init()
        self.completer.delegate = self
    }
    
    // MKLocalSearchCompleterDelegate method
    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        suggestions = completer.results
    }
    
    func updateQuery(query: String)
    {
        self.completer.queryFragment = query
    }
    
    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        // Handle error
    }
}

已尝试的无效方案

  • 方案1:在SearchState中实例化AutocompleteLocations作为属性,视图直接访问其suggestions。无效原因:嵌套引用类型的属性变更无法触发@Published的状态更新(SwiftUI仅监听值类型的整体替换或引用类型的引用变更)。
  • 方案2:在ViewModel中手动调用objectWillChange.send()。效果不佳:搜索结果返回时机不确定,无法精准触发更新。
  • 方案3:让AutocompleteLocations直接使用SearchState中的数组。无法实现:Swift数组是值类型,赋值时会产生拷贝,修改的是副本而非原数组。
  • 方案4:将AutocompleteLocations改为ObservableObject,单独发布suggestions再由ViewModel转发。违背MVVM原则:让服务类持有状态,破坏了Model纯数据、ViewModel管理状态的职责划分,且无法将建议列表统一存放在SearchState中。

符合MVVM的解决方案

核心思路:将AutocompleteLocations改为无状态的服务类,通过回调将搜索结果传递给ViewModel,由ViewModel负责更新SearchState,利用@Published的自动更新机制触发视图刷新。

步骤1:重构AutocompleteLocations

去掉自身的suggestions属性,改用回调传递结果:

import MapKit

class AutocompleteLocations: NSObject, MKLocalSearchCompleterDelegate {
    private let completer: MKLocalSearchCompleter
    private let onResultsUpdated: ([MKLocalSearchCompletion]) -> Void
    
    // 初始化时传入结果更新的回调
    init(onResultsUpdated: @escaping ([MKLocalSearchCompletion]) -> Void) {
        self.completer = MKLocalSearchCompleter()
        self.onResultsUpdated = onResultsUpdated
        super.init()
        completer.delegate = self
    }
    
    // 更新搜索关键词
    func updateQuery(query: String) {
        completer.queryFragment = query
    }
    
    // MARK: - MKLocalSearchCompleterDelegate
    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        // 将结果通过回调传递给外部
        onResultsUpdated(completer.results)
    }
    
    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        // 错误时可以清空结果或传递错误信息(根据需求扩展)
        onResultsUpdated([])
    }
}

步骤2:ViewModel中整合服务与状态更新

ViewModel持有AutocompleteLocations实例,在回调中更新SearchState的suggestedLocations:

import SwiftUI
import MapKit

// 自定义地点结构体(示例)
struct CustomLocation {
    var latitude: Double
    var longitude: Double
}

struct SearchState {
    var currentLocation: CustomLocation
    var currentText: String = ""
    var suggestedLocations: [MKLocalSearchCompletion] = []
    // 其他业务属性和方法
}

class SearchViewModel: ObservableObject {
    @Published var searchState: SearchState
    private let autocompleteService: AutocompleteLocations
    
    init(initialState: SearchState) {
        self.searchState = initialState
        // 初始化服务并传入结果回调
        self.autocompleteService = AutocompleteLocations { [weak self] suggestions in
            guard let self = self else { return }
            // 直接更新SearchState的建议列表,值类型的赋值会触发@Published的更新
            self.searchState.suggestedLocations = suggestions
        }
    }
    
    // 对外暴露的搜索更新方法
    func updateSearchQuery(_ query: String) {
        // 先更新搜索文本状态
        searchState.currentText = query
        // 通知服务更新关键词
        autocompleteService.updateQuery(query: query)
    }
}

步骤3:视图中绑定状态

视图只需绑定ViewModel的searchState.suggestedLocations,即可自动响应更新:

struct SearchView: View {
    @StateObject private var viewModel: SearchViewModel
    
    init(initialLocation: CustomLocation) {
        let initialState = SearchState(currentLocation: initialLocation)
        _viewModel = StateObject(wrappedValue: SearchViewModel(initialState: initialState))
    }
    
    var body: some View {
        VStack {
            TextField("搜索地点", text: $viewModel.searchState.currentText)
                .onChange(of: viewModel.searchState.currentText) { newText in
                    viewModel.updateSearchQuery(newText)
                }
            
            List(viewModel.searchState.suggestedLocations, id: \.self) { completion in
                Text(completion.title)
            }
        }
    }
}

方案优势

  • 严格遵循MVVM职责划分:
    • SearchState作为纯数据模型,仅负责存储状态,不包含任何业务逻辑
    • AutocompleteLocations作为专门的服务类,仅处理MKLocalSearchCompleter的底层逻辑,不持有状态
    • SearchViewModel作为中间层,协调服务与模型,统一管理状态并发布给视图
  • 自动触发视图更新:通过@Published的特性,只要SearchState的属性发生变更,视图会自动刷新
  • 代码解耦:服务类与ViewModel通过回调通信,便于后续测试和扩展

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 15:12:05