• MapApp 地图应用


    1. 简述

      1.1 重点

        1)更好地理解 MVVM 架构

        2)更轻松地使用 SwiftUI 框架、对齐、动画和转换

      1.2 资源下载地址:

    Swiftful-Thinking:icon-default.png?t=N7T8https://www.swiftful-thinking.com/downloads

      1.3 项目结构图:

      1.4 图片、颜色资源文件图:

      1.5 启动图片配置图:

    2. Model 层

      2.1 创建模拟位置文件 Location.swift

    1. import Foundation
    2. import MapKit
    3. struct Location: Identifiable, Equatable{
    4. let name: String
    5. let cityName: String
    6. let coordinates: CLLocationCoordinate2D
    7. let description: String
    8. let imageNames: [String]
    9. let link: String
    10. // UUID().uuidString,生产的每个ID都不一样,为了保证有相同可识别的相同模型,使用名称加城市名称
    11. var id: String {
    12. name + cityName
    13. }
    14. // Equatable 判断 id 是否一样
    15. static func == (lhs: Location, rhs: Location) -> Bool {
    16. return lhs.id == rhs.id
    17. }
    18. }

    3. 数据服务层

      3.1 创建模拟位置数据信息服务 LocationsDataSerVice.swift

    1. import Foundation
    2. import MapKit
    3. class LocationsDataService {
    4. static let locations: [Location] = [
    5. Location(
    6. name: "Colosseum",
    7. cityName: "Rome",
    8. coordinates: CLLocationCoordinate2D(latitude: 41.8902, longitude: 12.4922),
    9. description: "The Colosseum is an oval amphitheatre in the centre of the city of Rome, Italy, just east of the Roman Forum. It is the largest ancient amphitheatre ever built, and is still the largest standing amphitheatre in the world today, despite its age.",
    10. imageNames: [
    11. "rome-colosseum-1",
    12. "rome-colosseum-2",
    13. "rome-colosseum-3",
    14. ],
    15. link: "https://en.wikipedia.org/wiki/Colosseum"),
    16. Location(
    17. name: "Pantheon",
    18. cityName: "Rome",
    19. coordinates: CLLocationCoordinate2D(latitude: 41.8986, longitude: 12.4769),
    20. description: "The Pantheon is a former Roman temple and since the year 609 a Catholic church, in Rome, Italy, on the site of an earlier temple commissioned by Marcus Agrippa during the reign of Augustus.",
    21. imageNames: [
    22. "rome-pantheon-1",
    23. "rome-pantheon-2",
    24. "rome-pantheon-3",
    25. ],
    26. link: "https://en.wikipedia.org/wiki/Pantheon,_Rome"),
    27. Location(
    28. name: "Trevi Fountain",
    29. cityName: "Rome",
    30. coordinates: CLLocationCoordinate2D(latitude: 41.9009, longitude: 12.4833),
    31. description: "The Trevi Fountain is a fountain in the Trevi district in Rome, Italy, designed by Italian architect Nicola Salvi and completed by Giuseppe Pannini and several others. Standing 26.3 metres high and 49.15 metres wide, it is the largest Baroque fountain in the city and one of the most famous fountains in the world.",
    32. imageNames: [
    33. "rome-trevifountain-1",
    34. "rome-trevifountain-2",
    35. "rome-trevifountain-3",
    36. ],
    37. link: "https://en.wikipedia.org/wiki/Trevi_Fountain"),
    38. Location(
    39. name: "Eiffel Tower",
    40. cityName: "Paris",
    41. coordinates: CLLocationCoordinate2D(latitude: 48.8584, longitude: 2.2945),
    42. description: "The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris, France. It is named after the engineer Gustave Eiffel, whose company designed and built the tower. Locally nicknamed 'La dame de fer', it was constructed from 1887 to 1889 as the centerpiece of the 1889 World's Fair and was initially criticized by some of France's leading artists and intellectuals for its design, but it has become a global cultural icon of France and one of the most recognizable structures in the world.",
    43. imageNames: [
    44. "paris-eiffeltower-1",
    45. "paris-eiffeltower-2",
    46. ],
    47. link: "https://en.wikipedia.org/wiki/Eiffel_Tower"),
    48. Location(
    49. name: "Louvre Museum",
    50. cityName: "Paris",
    51. coordinates: CLLocationCoordinate2D(latitude: 48.8606, longitude: 2.3376),
    52. description: "The Louvre, or the Louvre Museum, is the world's most-visited museum and a historic monument in Paris, France. It is the home of some of the best-known works of art, including the Mona Lisa and the Venus de Milo. A central landmark of the city, it is located on the Right Bank of the Seine in the city's 1st arrondissement.",
    53. imageNames: [
    54. "paris-louvre-1",
    55. "paris-louvre-2",
    56. "paris-louvre-3",
    57. ],
    58. link: "https://en.wikipedia.org/wiki/Louvre"),
    59. ]
    60. }

    4. ViewModel 层

      4.1 创建位置信息的 ViewModel LocationsViewModel.swift

    1. import Foundation
    2. import MapKit
    3. import SwiftUI
    4. class LocationsViewModel: ObservableObject{
    5. /// All loaded locations Published
    6. @Published var locationes: [Location] = []
    7. /// Current location on map
    8. @Published var mapLocation: Location {
    9. didSet {
    10. // 设置地图位置,然后更新地图区域
    11. updateMapRegion(location: mapLocation)
    12. }
    13. }
    14. /// Current region on map : 这是地图上的当前区域
    15. @Published var mapRegion: MKCoordinateRegion = MKCoordinateRegion()
    16. /// 坐标跨度
    17. let mapSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
    18. /// Show list of locations : 显示位置列表
    19. @Published var showLocationsList: Bool = false
    20. /// Show location detail via sheet : 显示位置详情信息页面
    21. @Published var sheetLocation: Location? = nil
    22. init() {
    23. let locations = LocationsDataService.locations
    24. self.locationes = locations
    25. self.mapLocation = locations.first!
    26. self.updateMapRegion(location: locations.first!)
    27. }
    28. /// 更新地图区域
    29. private func updateMapRegion(location: Location){
    30. withAnimation(.easeInOut) {
    31. mapRegion = MKCoordinateRegion(
    32. // 中心点: 经纬度 latitude: 纬度 longitude: 经度
    33. center: location.coordinates,
    34. // 坐标跨度:
    35. span: mapSpan)
    36. }
    37. }
    38. /// 位置列表开关
    39. func toggleLocationsList(){
    40. withAnimation(.easeInOut) {
    41. // showLocationsList = !showLocationsList
    42. showLocationsList.toggle()
    43. }
    44. }
    45. /// 显示下一个位置
    46. func showNextLocation(location: Location){
    47. withAnimation(.easeInOut) {
    48. mapLocation = location
    49. showLocationsList = false
    50. }
    51. }
    52. /// 下一个按钮处理事件
    53. func nextButtonPressed(){
    54. // Get the current index
    55. guard let currentIndex = locationes.firstIndex(where: { $0 == mapLocation }) else {
    56. print("Could not find current index in locations array! Should naver happen.")
    57. return
    58. }
    59. // check if the nextIndex is valid: 检查下一个索引是否有校
    60. let nextIndex = currentIndex + 1
    61. guard locationes.indices.contains(nextIndex) else {
    62. // Next index is NOT avlid
    63. // Restart from 0
    64. guard let firstLocation = locationes.first else { return }
    65. showNextLocation(location: firstLocation)
    66. return
    67. }
    68. // Next index IS valid
    69. let nextLocation = locationes[nextIndex]
    70. showNextLocation(location: nextLocation)
    71. }
    72. }

    5. 创建 View 层

      5.1 位置列表 View

        1) 创建实现文件 LocationsListView.swift
    1. import SwiftUI
    2. /// 位置列表
    3. struct LocationsListView: View {
    4. /// 环境变量中的 ViewModel
    5. @EnvironmentObject private var viewMode: LocationsViewModel
    6. var body: some View {
    7. List {
    8. ForEach(viewMode.locationes) { location in
    9. Button {
    10. viewMode.showNextLocation(location: location)
    11. } label: {
    12. listRowView(location: location)
    13. }
    14. .padding(.vertical, 4)
    15. .listRowBackground(Color.clear)
    16. }
    17. }
    18. .listStyle(.plain)
    19. }
    20. }
    21. extension LocationsListView {
    22. /// 列表行
    23. private func listRowView(location: Location) -> some View{
    24. HStack {
    25. if let imageName = location.imageNames.first {
    26. Image(imageName)
    27. .resizable()
    28. .scaledToFill()
    29. .frame(width: 45, height: 45)
    30. .cornerRadius(10)
    31. }
    32. VStack(alignment: .leading) {
    33. Text(location.name)
    34. .font(.headline)
    35. Text(location.cityName)
    36. .font(.headline)
    37. }
    38. .frame(maxWidth: .infinity, alignment: .leading)
    39. }
    40. }
    41. }
    42. struct LocationsListView_Previews: PreviewProvider {
    43. static var previews: some View {
    44. LocationsListView()
    45. .environmentObject(LocationsViewModel())
    46. }
    47. }
        2) 效果图:

      5.2 位置预览 View

        1) 创建实现文件 LocationPreviewView.swift
    1. import SwiftUI
    2. /// 位置预览视图
    3. struct LocationPreviewView: View {
    4. /// 环境变量中配置的 viewModel
    5. @EnvironmentObject private var viewModel: LocationsViewModel
    6. let location: Location
    7. var body: some View {
    8. HStack(alignment:.bottom, spacing: 0) {
    9. VStack(alignment: .leading, spacing: 16) {
    10. imageSection
    11. titleSection
    12. }
    13. VStack(spacing: 8) {
    14. learnMoreButton
    15. nextButton
    16. }
    17. }
    18. .padding(20)
    19. .background(
    20. RoundedRectangle(cornerRadius: 10)
    21. .fill(.ultraThinMaterial.opacity(0.7))
    22. .offset(y: 65)
    23. )
    24. .cornerRadius(10)
    25. }
    26. }
    27. extension LocationPreviewView {
    28. /// 图片部分
    29. private var imageSection: some View{
    30. ZStack {
    31. if let imageImage = location.imageNames.first{
    32. Image(imageImage)
    33. .resizable()
    34. .scaledToFill()
    35. .frame(width: 100, height: 100)
    36. .cornerRadius(10)
    37. }
    38. }
    39. .padding(6)
    40. .background(Color.white)
    41. .cornerRadius(10)
    42. }
    43. /// 标题部分
    44. private var titleSection: some View{
    45. VStack(alignment: .leading, spacing: 4) {
    46. Text(location.name)
    47. .font(.title2)
    48. .fontWeight(.bold)
    49. Text(location.cityName)
    50. .font(.subheadline)
    51. }
    52. .frame(maxWidth: .infinity, alignment: .leading)
    53. }
    54. /// 了解更多按钮
    55. private var learnMoreButton: some View{
    56. Button {
    57. viewModel.sheetLocation = location
    58. } label: {
    59. Text("Learn more")
    60. .font(.headline)
    61. .frame(width: 125, height: 35)
    62. }
    63. .buttonStyle(.borderedProminent)
    64. }
    65. /// 下一个按钮
    66. private var nextButton: some View{
    67. Button {
    68. viewModel.nextButtonPressed()
    69. } label: {
    70. Text("Next")
    71. .font(.headline)
    72. .frame(width: 125, height: 35)
    73. }
    74. .buttonStyle(.bordered)
    75. }
    76. }
    77. struct LocationPreviewView_Previews: PreviewProvider {
    78. static var previews: some View {
    79. ZStack {
    80. Color.black.ignoresSafeArea()
    81. LocationPreviewView(location: LocationsDataService.locations.first!)
    82. .padding()
    83. }
    84. .environmentObject(LocationsViewModel())
    85. }
    86. }
        2) 效果图:

      5.3 位置注释 View

        1) 创建实现文件 LocationMapAnnotationView.swift
    1. import SwiftUI
    2. /// 位置注释视图
    3. struct LocationMapAnnotationView: View {
    4. let accentColor = Color("AccentColor")
    5. var body: some View {
    6. VStack(spacing: 0) {
    7. Image(systemName: "map.circle.fill")
    8. .resizable()
    9. .scaledToFill()
    10. .frame(width: 30, height: 30)
    11. .font(.headline)
    12. .foregroundColor(.white)
    13. .padding(6)
    14. .background(accentColor)
    15. //.cornerRadius(36)
    16. .clipShape(Circle())
    17. Image(systemName: "triangle.fill")
    18. .resizable()
    19. .scaledToFill()
    20. .foregroundColor(accentColor)
    21. .frame(width: 10, height: 10)
    22. .rotationEffect(Angle(degrees: 180))
    23. .offset(y: -3)
    24. .padding(.bottom, 35)
    25. }
    26. }
    27. }
    28. #Preview {
    29. ZStack{
    30. Color.black.ignoresSafeArea()
    31. LocationMapAnnotationView()
    32. }
    33. }
        2) 效果图:

      5.4 主页 View

        1) 创建实现文件 LocationsView.swift
    1. import SwiftUI
    2. import MapKit
    3. /// 主页 View
    4. struct LocationsView: View {
    5. @EnvironmentObject private var viewModel: LocationsViewModel
    6. let maxWidthForIpad: CGFloat = 700
    7. var body: some View {
    8. ZStack {
    9. mapLayer
    10. .ignoresSafeArea()
    11. VStack(spacing: 0) {
    12. header
    13. .padding()
    14. .frame(maxWidth: maxWidthForIpad)
    15. Spacer()
    16. locationsPreviewStack
    17. }
    18. }
    19. // .fullScreenCover 全屏显示
    20. .sheet(item: $viewModel.sheetLocation) { location in
    21. LocationDetailView(location: location)
    22. }
    23. }
    24. }
    25. extension LocationsView {
    26. /// 头View
    27. private var header: some View{
    28. VStack {
    29. Button {
    30. viewModel.toggleLocationsList()
    31. } label: {
    32. Text(viewModel.mapLocation.name + ", " + viewModel.mapLocation.cityName)
    33. .font(.title2)
    34. .fontWeight(.black)
    35. .foregroundColor(.primary)
    36. .frame(height: 55)
    37. .frame(maxWidth: .infinity)
    38. .animation(.none, value: viewModel.mapLocation)
    39. .overlay(alignment: .leading) {
    40. Image(systemName: "arrow.down")
    41. .font(.headline)
    42. .foregroundColor(.primary)
    43. .padding()
    44. .rotationEffect(Angle(degrees: viewModel.showLocationsList ? 180 : 0))
    45. }
    46. }
    47. // 列表
    48. if viewModel.showLocationsList{
    49. LocationsListView()
    50. }
    51. }
    52. .background(.thickMaterial.opacity(0.7))
    53. .cornerRadius(10)
    54. .shadow(color: Color.black.opacity(0.3), radius: 20, x: 0, y: 15)
    55. }
    56. /// 地图 View
    57. private var mapLayer: some View{
    58. Map(coordinateRegion: $viewModel.mapRegion,
    59. annotationItems: viewModel.locationes,
    60. annotationContent: { location in
    61. // 地图标识颜色
    62. // MapMarker(coordinate: location.coordinates, tint: .blue)
    63. // 自定义标识
    64. MapAnnotation(coordinate: location.coordinates) {
    65. LocationMapAnnotationView()
    66. .scaleEffect(viewModel.mapLocation == location ? 1 : 0.7)
    67. .shadow(radius: 10)
    68. .onTapGesture {
    69. viewModel.showNextLocation(location: location)
    70. }
    71. }
    72. })
    73. }
    74. /// 地址预览堆栈
    75. private var locationsPreviewStack: some View{
    76. ZStack {
    77. ForEach(viewModel.locationes) { location in
    78. // 显示当前地址
    79. if viewModel.mapLocation == location {
    80. LocationPreviewView(location: location)
    81. .shadow(color: Color.black.opacity(0.3), radius: 20)
    82. .padding()
    83. .frame(maxWidth: maxWidthForIpad)
    84. .frame(maxWidth: .infinity)
    85. // .opacity
    86. // .transition(AnyTransition.scale.animation(.easeInOut))
    87. // 添加动画
    88. .transition(.asymmetric(
    89. insertion: .move(edge: .trailing),
    90. removal: .move(edge: .leading)))
    91. }
    92. }
    93. }
    94. }
    95. }
    96. struct LocationsView_Previews: PreviewProvider {
    97. static var previews: some View {
    98. LocationsView()
    99. .environmentObject(LocationsViewModel())
    100. }
    101. }
        2) 效果图:

      5.5 位置详情页 View

        1) 创建实现文件 LocationDetailView.swift
    1. import SwiftUI
    2. import MapKit
    3. /// 位置详情页视图
    4. struct LocationDetailView: View {
    5. // @Environment(\.presentationMode) var presentationMode
    6. // @Environment(\.dismiss) var dismiss
    7. @EnvironmentObject private var viewModel: LocationsViewModel
    8. let location: Location
    9. var body: some View {
    10. ScrollView {
    11. VStack {
    12. imageSection
    13. VStack(alignment: .leading, spacing: 16){
    14. titleSection
    15. Divider()
    16. descriptionSection
    17. Divider()
    18. mapLayer
    19. }
    20. .frame(maxWidth: .infinity, alignment: .leading)
    21. .padding()
    22. }
    23. }
    24. // 安全区
    25. .ignoresSafeArea()
    26. // 超薄材质,灰白色
    27. .background(.ultraThinMaterial)
    28. // 添加返回按钮
    29. .overlay(alignment: .topLeading) {
    30. backButton
    31. }
    32. }
    33. }
    34. extension LocationDetailView{
    35. /// 滑动切换图
    36. private var imageSection: some View{
    37. TabView {
    38. ForEach(location.imageNames, id: \.self) {
    39. Image($0)
    40. .resizable()
    41. .scaledToFill()
    42. .frame(width: UIDevice.current.userInterfaceIdiom == .pad ? nil : UIScreen.main.bounds.width)
    43. .clipped()
    44. }
    45. }
    46. .frame(height: 500)
    47. .tabViewStyle(.page)
    48. .shadow(color: .black.opacity(0.3), radius: 20, y: 10)
    49. }
    50. /// 标题视图
    51. private var titleSection: some View{
    52. VStack(alignment: .leading, spacing: 8){
    53. Text(location.name)
    54. .font(.largeTitle)
    55. .fontWeight(.semibold)
    56. .foregroundStyle(.primary)
    57. Text(location.cityName)
    58. .font(.title3)
    59. .foregroundStyle(.secondary)
    60. }
    61. }
    62. /// 描述视图
    63. private var descriptionSection: some View{
    64. VStack(alignment: .leading, spacing: 16){
    65. Text(location.description)
    66. .font(.subheadline)
    67. .foregroundStyle(.secondary)
    68. if let url = URL(string: location.link) {
    69. Link("Read more on Wikipedia", destination: url)
    70. .font(.headline)
    71. .tint(.blue)
    72. }
    73. }
    74. }
    75. /// 地图 View
    76. private var mapLayer: some View{
    77. Map(coordinateRegion: .constant(MKCoordinateRegion(
    78. center: location.coordinates,
    79. span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))),
    80. annotationItems: [location]) { location in
    81. MapAnnotation(coordinate: location.coordinates){
    82. // 自定义标识
    83. LocationMapAnnotationView()
    84. .shadow(radius: 10)
    85. }
    86. }
    87. .allowsHitTesting(false) // 禁止点击
    88. .aspectRatio(1, contentMode: .fit) // 纵横比
    89. .cornerRadius(30)
    90. }
    91. /// 返回按钮
    92. private var backButton: some View{
    93. Button{
    94. // dismiss.callAsFunction()
    95. viewModel.sheetLocation = nil
    96. } label: {
    97. Image(systemName: "xmark")
    98. .font(.headline)
    99. .padding(16)
    100. .foregroundColor(.primary)
    101. .background(.thickMaterial)
    102. .cornerRadius(10)
    103. .shadow(radius: 4)
    104. .padding()
    105. }
    106. }
    107. }
    108. #Preview {
    109. LocationDetailView(location: LocationsDataService.locations.first!)
    110. .environmentObject(LocationsViewModel())
    111. }
        2) 效果图:

      5.6 启动结构体文件 SwiftfulMapAppApp.swift

    1. import SwiftUI
    2. @main
    3. struct SwiftfulMapAppApp: App {
    4. @StateObject private var viewModel = LocationsViewModel()
    5. var body: some Scene {
    6. WindowGroup {
    7. LocationsView()
    8. .environmentObject(viewModel)
    9. }
    10. }
    11. }

    6. 整体效果:

  • 相关阅读:
    职场经验分享--接口中按时间戳查数据容易被忽略的细节
    ModuleNotFoundError: No module named ‘torchvision.models.utils‘
    关于我对axios的源码理解
    黑豹程序员-架构师学习路线图-百科:JSON替代XML
    triton 客戶端用https协议访问服务
    【Java】详解SimpleDateFormat的format方法和parse方法
    如何使用Python脚本
    [附源码]计算机毕业设计网文论坛管理系统Springboot程序
    RP-母版 流程图 发布和预览 团队项目
    python 绘制3D图
  • 原文地址:https://blog.csdn.net/u011193452/article/details/134438619