因为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()
}