Swift Map用Picker切换地图样式
Swift Map用Picker切换地图样式

Swift Map用Picker切换地图样式

因为Map是系统定义的结构体,在解决问题的过程中,尝试让Map遵循Hashable协议以满足Picker的调用要求,但结果却导致代码不兼容,虽然代码层面没有报错,但是预览异常,并且模拟器报错:

Thread 1: EXC_BAD_ACCESS (code=2, address=0x16eeefed0)

因此,需要创建一个enum类型,并将MapStyle的静态变量封装起来,如想要解决这一问题,可直接滑动到文章的底部。

下面是Map使用Picker切换地图样式的构建过程,涵盖了遇到的报错情况,最后是实现代码。

构建过程

在Swift中,使用Picker切换地图样式,代码层面需要将Map.mapStyle()绑定一个变量:

@State private var selectedStyle: MapStyle = .hybrid // 默认选择混合模式

该变量类型为:MapStyle类型,内容为地图的默认类型,如:hybrid(混合地图)。

接着通过Picker修改selectedStyle变量的值,以完成地图的切换效果,在Picker中用ForEach循环显示选项。

在视图中创建Picker选择器:

Picker("选择地图类型",selection: $selectedStyle) {
    Text("混合模式").tag(MapStyle.hybrid)
    Text("影像模式").tag(MapStyle.imagery)
    Text("标准模式").tag(MapStyle.standard)
}
.pickerStyle(.segmented) // 可切换为 .menu 或 .wheel

创建Picker选择器,绑定selectedStyle变量,在Picker中使用ForEach显示可选的地图样式,但是实际运行时,会提示如下报错:

Generic struct 'Picker' requires that 'MapStyle' conform to 'Hashable'
Instance method 'tag' requires that 'MapStyle' conform to 'Hashable'

报错的描述为:Picker要求MapStyle遵循Hashable协议。

因此,应该考虑给MapStyle添加遵循Hashable的扩展,在hash方法中,使用ObjectIdentifier进行区分:

extension MapStyle: Hashable {
    public static func == (lhs: MapStyle, rhs: MapStyle) -> Bool {
        // 假设 MapStyle 内部是通过引用唯一标识的
        return lhs === rhs
    }
    
    public func hash(into hasher: inout Hasher) {
        // 使用其内存地址作为唯一标识
        hasher.combine(ObjectIdentifier(self))
    }
}

上述代码这会提示:

Argument type 'MapStyle' expected to be an instance of a class or class-constrained type

表示ObjectIdentifier 只能用于引用类型(class 或 @objc 类型),而 MapStyle 是一个值类型(struct)。因此,直接使用 ObjectIdentifier(self) 会报错。

接着尝试使用id字段做标识,在扩展中创建一个id属性,通过判断自身的内容赋值相应的id:

private var id: Int {
    switch self {
    case .standard:
        return 0
    case .imagery:
        return 1
    case .hybrid:
        return 2
    default:
        return -1
    }
}

当MapStyle为.standards(标准模式) ,id为0,以此判断,因此,扩展代码为:

extension MapStyle: Hashable {
    public static func == (lhs: MapStyle, rhs: MapStyle) -> Bool {
        // 通过 id 或其他属性进行值语义比较
        return lhs.id == rhs.id
    }
    
    public func hash(into hasher: inout Hasher) {
        // 根据 id 或其他属性生成哈希值
        hasher.combine(id)
    }
    
    private var id: Int {
        switch self {
        case .standard:
            return 0
        case .imagery:
            return 1
        case .hybrid:
            return 2
        default:
            fatalError("Unknown MapStyle encountered.")
        }
    }
}

运行上述代码时,预览报错,模拟器报错,并将问题定位到self一栏:

Thread 1: EXC_BAD_ACCESS (code=2, address=0x16eeefed0)

经过一些修改和尝试,无法从扩展的方面,完成对MapStyle对Hashable协议的支持,因此无法满足在Picker的要求,只能放弃扩展这一方式。

解决方案

不直接扩展 MapStyle,而是创建一个安全的中间层类型,避免对系统类型进行过多修改。

1、使用枚举封装 MapStyle

创建一个 enum 类型,将 MapStyle 的静态变量封装起来,同时确保类型安全和 Hashable 性能:

enum CustomMapStyle: CaseIterable, Hashable {
    case standard 
    case imagery
    case hybrid

    var mapStyle: MapStyle {
        switch self {
        case .standard: return .standard
        case .imagery: return .imagery
        case .hybrid: return .hybrid
        }
    }

    var displayName: String {
        switch self {
        case .standard: return "标准模式"
        case .imagery: return "影像模式"
        case .hybrid: return "混合模式"
        }
    }
}

创建的CustomMapStyle遵循CaseIterable(生成枚举集合)和Hashable协议。

在枚举中定义了三个值,对应的是三种地图样式:

1、hybrid
表示混合视图,包括卫星图像和道路名称的叠加。
2、imagery
表示纯卫星视图(没有任何叠加信息)。
3、standard
表示标准地图视图(包括道路、地标和其他兴趣点信息)。

然后定义了两个计算属性:

var mapStyle: MapStyle {
	switch self {
	case .standard: return .standard
	case .imagery: return .imagery
	case .hybrid: return .hybrid
	}
}

当调用mapStyle时,根据self判断当前的枚举值,然后返回MapStyle类型的值,如self.standard返回的是MapStyle.standard。

var displayName: String {
	switch self {
	case .standard: return "标准模式"
	case .imagery: return "影像模式"
	case .hybrid: return "混合模式"
	}
}

调用displayName,则会根据self返回对应的中文名称。

在视图部分仍然使用一个变量来绑定地图类型,但是现在这个变量需要符合自定义的CustomMapStyle枚举:

@State private var selectedStyle:CustomMapStyle = .hybrid // 默认选择混合模式

视图中添加Picker选择器,绑定selectedStyle变量:

Picker("选择地图类型", selection: $selectedStyle) {
    ForEach(CustomMapStyle.allCases, id: \.self) { style in
        Text(style.displayName).tag(style)
    }
}
.pickerStyle(.segmented) // 可切换为 .menu 或 .wheel

这样,当我们每次尝试切换视图中的Picker选择器,都会完成地图样式的切换。

其中,ForEach使用的是allCases,它表示罗列枚举的集合,其功能是由CaseIterable协议实现的。

tag(style)则给每个Text视图绑定了对应的标识符,用于将tag值更新到Picker绑定的selectedStyle变量上。

最后,将mapStyle绑定到selectedStyle.mapStyle变量上:

Map(initialPosition: startPosition)
	.mapStyle(selectedStyle.mapStyle)

完成对地图样式的切换效果:

2、直接绑定静态值

如果不希望创建额外的封装层,可以使用唯一标识符存储选中状态。

在视图中添加mapStyles元组:

let mapStyles = [
    (id: 0, style: MapStyle.standard, name: "标准模式"),
    (id: 1, style: MapStyle.imagery, name: "影像模式"),
    (id: 2, style: MapStyle.hybrid, name: "混合模式")
]

同时,使用Int来标记选择的地图样式:

@State private var selectedStyleID: Int = 2 // 默认值为混合模式(ID 2)

在Picker的ForEach中绑定mapStyles元组:

Picker("选择地图类型", selection: $selectedStyleID) {
    ForEach(mapStyles, id: \.id) { style in
        Text(style.name).tag(style.id)
    }
}
.pickerStyle(.segmented)

Picker绑定selectedStyleID变量,ForEach则遍历mapStyles元组,在ForEach中显示元组的name字段,tag则绑定id字段。

当选择对应选择项时,将选择项的id更新到selectedStyleID变量。

最后,mapStyle对selectedStyleID进行匹配,完成对地图样式的切换效果:

Map(initialPosition: startPosition)
	.mapStyle(mapStyles.first { $0.id == selectedStyleID }?.style ?? .hybrid)

在mapStyle中,使用数组的first(where:)方法查找第一个与selectedStyleID相等的元素,找到满足的元素并返回其style属性,例如:

style: MapStyle.standard

如果没有找到,则利用nil合并运算符(??),设置默认值为.hybrid。

相关文章

Apple地图框架MapKit:https://fangjunyu.com/2024/11/22/apple%e5%9c%b0%e5%9b%be%e6%a1%86%e6%9e%b6mapkit/

附录

使用枚举实现地图切换的代码

import SwiftUI
import MapKit
import CoreLocation

let startPosition = MapCameraPosition.region(
    MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 35.561591, longitude: 119.619717),
        span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
    )
)

struct ContentView: View {
    @State private var viewModel = ViewModel()
    @State private var selectedStyle:CustomMapStyle = .hybrid // 默认选择混合模式
    var body: some View {
        if viewModel.isUnlocked {
            ZStack {
                MapReader { proxy in
                    Map(initialPosition: startPosition) {
                        ForEach(viewModel.locations) { location in
                            Annotation(location.name, coordinate: location.coordinate) {
                                Image(systemName: "star.circle")
                                    .resizable()
                                    .foregroundStyle(.red)
                                    .frame(width: 44, height: 44)
                                    .background(.white)
                                    .clipShape(.circle)
                                    .onLongPressGesture {
                                        viewModel.selectedPlace = location
                                    }
                            }
                        }
                    }
                    .mapStyle(selectedStyle.mapStyle)
                    .sheet(item: $viewModel.selectedPlace) { place in
                        EditView(location: place) {
                            viewModel.update(location: $0)
                        }
                    }
                    .onTapGesture { position in
                        if let coordinate = proxy.convert(position, from: .local) {
                            viewModel.addLocation(at: coordinate)
                        }
                    }
                }
                
                VStack {
                    Spacer()
                    VStack {
                        Picker("选择地图类型", selection: $selectedStyle) {
                            ForEach(CustomMapStyle.allCases, id: \.self) { style in
                                Text(style.displayName).tag(style)
                            }
                        }
                        .pickerStyle(.segmented) // 可切换为 .menu 或 .wheel
                    }
                    .background(Color.white)
                }
            }
            .onAppear {
                print("Current selectedStyle: \(selectedStyle)")
            }
        } else {
            Button("Unlock Places", action: viewModel.authenticate)
                .padding()
                .background(.blue)
                .foregroundStyle(.white)
                .clipShape(.capsule)
        }
    }
}

#Preview {
    ContentView()
}
enum CustomMapStyle: CaseIterable, Hashable {
    case standard
    case imagery
    case hybrid
    
    var mapStyle: MapStyle {
        switch self {
        case .standard: return .standard
        case .imagery: return .imagery
        case .hybrid: return .hybrid
        }
    }
    
    var displayName: String {
        switch self {
        case .standard: return "标准模式"
        case .imagery: return "影像模式"
        case .hybrid: return "混合模式"
        }
    }
}

直接绑定静态值的代码

import SwiftUI
import MapKit
import CoreLocation

let startPosition = MapCameraPosition.region(
    MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 35.561591, longitude: 119.619717),
        span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
    )
)

struct ContentView: View {
    @State private var viewModel = ViewModel()
    @State private var selectedStyleID:Int = 2 // 默认选择混合模式
    
    let mapStyles = [
        (id: 0, style: MapStyle.standard, name: "标准模式"),
        (id: 1, style: MapStyle.imagery, name: "影像模式"),
        (id: 2, style: MapStyle.hybrid, name: "混合模式")
    ]

    var body: some View {
        if viewModel.isUnlocked {
            ZStack {
                MapReader { proxy in
                    Map(initialPosition: startPosition) {
                        ForEach(viewModel.locations) { location in
                            Annotation(location.name, coordinate: location.coordinate) {
                                Image(systemName: "star.circle")
                                    .resizable()
                                    .foregroundStyle(.red)
                                    .frame(width: 44, height: 44)
                                    .background(.white)
                                    .clipShape(.circle)
                                    .onLongPressGesture {
                                        viewModel.selectedPlace = location
                                    }
                            }
                        }
                    }
                    .mapStyle(mapStyles[selectedStyleID].style)
                    .sheet(item: $viewModel.selectedPlace) { place in
                        EditView(location: place) {
                            viewModel.update(location: $0)
                        }
                    }
                    .onTapGesture { position in
                        if let coordinate = proxy.convert(position, from: .local) {
                            viewModel.addLocation(at: coordinate)
                        }
                    }
                }
                
                VStack {
                    Spacer()
                    VStack {
                        Picker("选择地图类型", selection: $selectedStyleID) {
                            ForEach(mapStyles, id: \.id) { style in
                                Text(style.name).tag(style.id)
                            }
                        }
                        .pickerStyle(.segmented)

                    }
                    .background(Color.white)
                }
            }
        } else {
            Button("Unlock Places", action: viewModel.authenticate)
                .padding()
                .background(.blue)
                .foregroundStyle(.white)
                .clipShape(.capsule)
        }
    }
    
}

#Preview {
    ContentView()
}

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注