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

SwiftUI iOS 17:如何实现长按地图加新标记及标记点击显详情?

问题与解决方案

问题概述

需要实现类似Google Maps的交互功能:

  • 点击地图上的现有标记,弹出展示标记详情的sheet
  • 长按地图空白区域,添加新标记并弹出录入详情的sheet

当前存在两个核心问题:

  • 使用onTapGesture会拦截标记的点击事件,导致难以选中现有标记
  • 添加新标记时无法正确显示AddNewMarkerView

解决方案

1. 用长按手势替代点击手势实现添加新标记

将地图的onTapGesture改为onLongPressGesture,这样不会抢占标记的点击事件,仅在长按空白区域时触发添加新标记的逻辑。

2. 正确绑定Map的selection

将Map(selection: self.selectedMarker)改为Map(selection: $selectedMarker),确保选中标记时能正确更新selectedMarker变量。

3. 调整viewState与sheet的触发逻辑

  • 点击现有标记时,自动设置viewState = .details并弹出详情sheet
  • 长按添加新标记时,设置viewState = .newMarker并弹出录入sheet
  • 新增临时标记状态变量,避免取消添加时误删已有标记

4. 简化Marker渲染逻辑

不需要根据viewState区分渲染Marker,所有标记统一添加.tag(marker),确保点击能正确选中。

修改后的完整代码

import SwiftUI
import MapKit

struct CustomMarker: Identifiable, Hashable {
    var id: String
    var title: String
    var location: CLLocationCoordinate2D
    var category: String
    var notes: String

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(title)
        hasher.combine(location.latitude)
        hasher.combine(location.longitude)
        hasher.combine(category)
        hasher.combine(notes)
    }

    static func ==(lhs: CustomMarker, rhs: CustomMarker) -> Bool {
        return lhs.id == rhs.id &&
            lhs.title == rhs.title &&
            lhs.location.latitude == rhs.location.latitude &&
            lhs.location.longitude == rhs.location.longitude &&
            lhs.category == rhs.category &&
            lhs.notes == rhs.notes
    }
}

enum ViewState {
    case details
    case newMarker
}

struct ContentView: View {
    @State private var markers: [CustomMarker] = [CustomMarker(id: "1", title: "First Marker", location: CLLocationCoordinate2D(latitude: 35.6764, longitude: 139.6500), category: "My Category", notes: "My Notes")]
    @State private var viewState: ViewState = .details
    @State private var selectedMarker: CustomMarker?
    @State private var showingSheet: Bool = false
    @State private var isTemporaryMarker = false

    var body: some View {
        MapReader { proxy in
            Map(selection: $selectedMarker) {
                UserAnnotation()
                
                ForEach(markers) { marker in
                    Marker(marker.title, coordinate: marker.location)
                        .tag(marker)
                }
            }
            .onLongPressGesture { position in
                guard let coordinate = proxy.convert(position, from: .local) else { return }
                
                isTemporaryMarker = true
                let newMarker = CustomMarker(
                    id: UUID().uuidString,
                    title: "",
                    location: coordinate,
                    category: "",
                    notes: ""
                )
                markers.append(newMarker)
                selectedMarker = newMarker
                viewState = .newMarker
                showingSheet = true
            }
        }
        .sheet(isPresented: $showingSheet, onDismiss: {
            if isTemporaryMarker {
                markers.removeLast()
                isTemporaryMarker = false
            }
            selectedMarker = nil
        }) {
            switch viewState {
            case .details:
                MarkerDetailsView(marker: $selectedMarker!)
            case .newMarker:
                AddNewMarkerView(marker: $markers.last!, onSave: {
                    isTemporaryMarker = false
                    showingSheet = false
                })
            }
        }
        .onChange(of: selectedMarker) { newValue in
            if let newValue = newValue, !isTemporaryMarker {
                viewState = .details
                showingSheet = true
            }
        }
    }
}

// 示例详情视图
struct MarkerDetailsView: View {
    @Binding var marker: CustomMarker
    var body: some View {
        VStack(spacing: 16) {
            Text(marker.title)
                .font(.title)
            Text(marker.category)
                .foregroundColor(.secondary)
            Text(marker.notes)
            Spacer()
            Button("关闭") {
                // 关闭逻辑
            }
        }
        .padding()
    }
}

// 示例添加新标记视图
struct AddNewMarkerView: View {
    @Binding var marker: CustomMarker
    var onSave: () -> Void
    var body: some View {
        VStack(spacing: 16) {
            TextField("输入标题", text: $marker.title)
                .textFieldStyle(.roundedBorder)
            TextField("输入分类", text: $marker.category)
                .textFieldStyle(.roundedBorder)
            TextField("输入备注", text: $marker.notes)
                .textFieldStyle(.roundedBorder)
            Spacer()
            HStack {
                Button("取消") {
                    showingSheet = false
                }
                .buttonStyle(.bordered)
                Button("保存") {
                    onSave()
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .padding()
    }
}

关键改动说明

  • 手势隔离:通过onLongPressGesture实现添加新标记,彻底避免干扰标记的点击事件
  • 双向绑定优化:使用$selectedMarker确保标记选中状态能实时同步
  • 临时标记管理:新增isTemporaryMarker变量,精准控制取消添加时的标记移除逻辑
  • 视图逻辑解耦:sheet根据viewState自动切换显示内容,无需额外判断

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 11:05:21