Thêm bản đồ vào ứng dụng iOS bằng SwiftUI (Swift)

1. Trước khi bắt đầu

Lớp học lập trình này hướng dẫn bạn cách sử dụng Maps SDK cho iOS với SwiftUI.

screenshot-iphone-12-black@2x.png

Điều kiện tiên quyết

  • Kiến thức cơ bản về Swift
  • Hiểu biết cơ bản về SwiftUI

Bạn sẽ thực hiện

  • Bật và sử dụng Maps SDK cho iOS để thêm Google Maps vào một ứng dụng iOS bằng SwiftUI.
  • Thêm điểm đánh dấu vào bản đồ.
  • Truyền trạng thái giữa một đối tượng SwiftUI và GMSMapView.

Bạn cần có

2. Bắt đầu thiết lập

Đối với bước bật sau đây, hãy bật Maps SDK cho iOS.

Thiết lập Nền tảng Google Maps

Nếu bạn chưa có tài khoản Google Cloud Platform và dự án có bật tính năng thanh toán, vui lòng xem hướng dẫn Bắt đầu sử dụng Google Maps Platform để tạo tài khoản thanh toán và dự án.

  1. Trong Cloud Console, hãy nhấp vào trình đơn thả xuống dự án rồi chọn dự án mà bạn muốn sử dụng cho lớp học lập trình này.

  1. Bật các API và SDK của Google Maps Platform cần thiết cho lớp học lập trình này trong Google Cloud Marketplace. Để làm như vậy, hãy làm theo các bước trong video này hoặc tài liệu này.
  2. Tạo khoá API trong trang Thông tin xác thực của Cloud Console. Bạn có thể làm theo các bước trong video này hoặc tài liệu này. Tất cả các yêu cầu gửi đến Nền tảng Google Maps đều cần có khoá API.

3. Tải mã bắt đầu

Để giúp bạn bắt đầu nhanh nhất có thể, sau đây là một số mã khởi đầu giúp bạn theo dõi lớp học lập trình này. Bạn có thể chuyển ngay đến giải pháp, nhưng nếu muốn làm theo tất cả các bước để tự xây dựng giải pháp, hãy tiếp tục đọc.

  1. Sao chép kho lưu trữ nếu bạn đã cài đặt git.
git clone https://github.com/googlecodelabs/maps-ios-swiftui.git

Ngoài ra, bạn có thể nhấp vào nút sau đây để tải mã nguồn xuống.

  1. Sau khi nhận được mã, trong một dòng lệnh, hãy cd vào thư mục starter/GoogleMapsSwiftUI.
  2. Chạy carthage update --platform iOS để tải Maps SDK cho iOS xuống
  3. Cuối cùng, hãy mở tệp GoogleMapsSwiftUI.xcodeproj trong Xcode

4. Tổng quan về mã

Trong dự án khởi đầu mà bạn tải xuống, các lớp sau đây đã được cung cấp và triển khai cho bạn:

  • AppDelegateUIApplicationDelegate của ứng dụng. Đây là nơi SDK Bản đồ dành cho iOS sẽ được khởi chạy.
  • City – một cấu trúc đại diện cho một thành phố (chứa tên và toạ độ của thành phố).
  • MapViewController – một UIKit có phạm vi hạn chế UIViewController chứa Google Maps (GMSMapView)
    • SceneDelegateUIWindowSceneDelegate của ứng dụng mà ContentView được tạo thực thể.

Ngoài ra, các lớp sau đây có một phần nội dung triển khai và bạn sẽ hoàn tất các lớp này vào cuối Lớp học lập trình này:

  • ContentView – khung hiển thị SwiftUI cấp cao nhất chứa ứng dụng của bạn.
  • MapViewControllerBridge – một lớp kết nối khung hiển thị UIKit với khung hiển thị SwiftUI. Cụ thể, đây là lớp sẽ giúp MapViewController có thể truy cập trong SwiftUI.

5. SwiftUI so với UIKit

SwiftUI được ra mắt trong iOS 13 dưới dạng một khung giao diện người dùng thay thế cho UIKit để phát triển các ứng dụng iOS. So với UIKit, SwiftUI có một số ưu điểm. Ví dụ:

  • Khung hiển thị sẽ tự động cập nhật khi trạng thái thay đổi. Khi dùng các đối tượng có tên là State, mọi thay đổi đối với giá trị cơ bản mà đối tượng này chứa sẽ khiến giao diện người dùng tự động cập nhật.
  • Tính năng xem trước trực tiếp giúp phát triển nhanh hơn. Tính năng xem trước trực tiếp giảm thiểu nhu cầu tạo và triển khai mã cho trình mô phỏng để xem các thay đổi về hình ảnh vì bạn có thể dễ dàng xem trước khung hiển thị SwiftUI trên Xcode.
  • Nguồn đáng tin cậy nằm trong Swift. Tất cả các thành phần hiển thị trong SwiftUI đều được khai báo bằng Swift nên bạn không cần dùng Interface Builder nữa.
  • Tương tác với UIKit. Khả năng tương tác với UIKit đảm bảo rằng các ứng dụng hiện có có thể tăng dần việc sử dụng SwiftUI với các khung hiển thị hiện có. Ngoài ra, những thư viện chưa hỗ trợ SwiftUI (chẳng hạn như Maps SDK dành cho iOS) vẫn có thể được dùng trong SwiftUI.

Tuy nhiên, cũng có một số nhược điểm:

  • SwiftUI chỉ có trên iOS 13 trở lên.
  • Bạn không thể kiểm tra hệ thống phân cấp khung hiển thị trong bản xem trước Xcode.

Trạng thái và luồng dữ liệu SwiftUI

SwiftUI cung cấp một cách thức mới để tạo giao diện người dùng bằng cách sử dụng phương pháp khai báo – bạn cho SwiftUI biết hình thức bạn muốn cho khung hiển thị cùng với tất cả các trạng thái khác nhau của khung hiển thị đó, và hệ thống sẽ làm phần còn lại. SwiftUI xử lý việc cập nhật khung hiển thị bất cứ khi nào trạng thái cơ bản thay đổi do một sự kiện hoặc hành động của người dùng. Thiết kế này thường được gọi là luồng dữ liệu một chiều. Mặc dù thông tin cụ thể về thiết kế này nằm ngoài phạm vi của lớp học lập trình này, nhưng bạn nên đọc về cách hoạt động của thiết kế này trong tài liệu State and Data Flow (Trạng thái và luồng dữ liệu) của Apple.

Kết nối UIKit và SwiftUI bằng UIViewRepresentable hoặc UIViewControllerRepresentable

Maps SDK cho iOS được xây dựng trên UIKit và không cung cấp khung hiển thị tương thích với SwiftUI, nên việc sử dụng SDK này trong SwiftUI yêu cầu tuân thủ UIViewRepresentable hoặc UIViewControllerRepresentable. Các giao thức này cho phép SwiftUI lần lượt đưa các UIViewUIViewController được tạo bằng UIKit vào. Mặc dù bạn có thể sử dụng một trong hai giao thức để thêm Google Maps vào khung hiển thị SwiftUI, nhưng ở bước tiếp theo, chúng ta sẽ xem xét việc sử dụng UIViewControllerRepresentable để thêm UIViewController có chứa bản đồ.

6. Thêm bản đồ

Trong phần này, bạn sẽ thêm Google Maps vào một khung hiển thị SwiftUI.

add-a-map-screenshot@2x.png

Thêm khoá API

Bạn cần cung cấp khoá API mà bạn đã tạo ở bước trước cho Maps SDK cho iOS để liên kết tài khoản của bạn với bản đồ sẽ xuất hiện trên ứng dụng.

Để cung cấp khoá API, hãy mở tệp AppDelegate.swift rồi chuyển đến phương thức application(_, didFinishLaunchingWithOptions). SDK được khởi chạy bằng GMSServices.provideAPIKey() với chuỗi "YOUR_API_KEY". Thay thế chuỗi đó bằng khoá API của bạn. Khi hoàn tất bước này, Maps SDK cho iOS sẽ được khởi chạy khi ứng dụng khởi động.

Thêm Google Map bằng MapViewControllerBridge

Giờ đây, khi khoá API của bạn được cung cấp cho SDK, bước tiếp theo là hiển thị bản đồ trên ứng dụng.

Bộ điều khiển khung hiển thị được cung cấp trong mã khởi đầu, MapViewController chứa một GMSMapView trong khung hiển thị của bộ điều khiển đó. Tuy nhiên, vì trình kiểm soát khung hiển thị này được tạo trong UIKit, nên bạn sẽ cần phải bắc cầu lớp này sang SwiftUI để có thể sử dụng bên trong ContentView. Cách làm như sau:

  1. Mở tệp MapViewControllerBridge trong Xcode.

Lớp này tuân thủ UIViewControllerRepresentable, đây là giao thức cần thiết để bao bọc một UIViewController UIKit để có thể dùng làm khung hiển thị SwiftUI. Nói cách khác, việc tuân thủ giao thức này sẽ giúp bạn dễ dàng chuyển đổi một khung hiển thị UIKit sang một khung hiển thị SwiftUI. Để tuân thủ giao thức này, bạn cần triển khai 2 phương thức:

  • makeUIViewController(context) – phương thức này được SwiftUI gọi để tạo UIViewController cơ bản. Đây là nơi bạn sẽ khởi tạo UIViewController và truyền trạng thái ban đầu cho thành phần này.
  • updateUIViewController(_, context) – phương thức này được SwiftUI gọi bất cứ khi nào trạng thái thay đổi. Đây là nơi bạn sẽ thực hiện mọi sửa đổi đối với UIViewController cơ bản để phản ứng với thay đổi về trạng thái.
  1. Tạo một MapViewController

Bên trong hàm makeUIViewController(context), hãy khởi tạo một MapViewController mới và trả về hàm này dưới dạng kết quả. Sau khi thực hiện, MapViewControllerBridge của bạn sẽ có dạng như sau:

MapViewControllerBridge

import GoogleMaps
import SwiftUI

struct MapViewControllerBridge: UIViewControllerRepresentable {

  func makeUIViewController(context: Context) -> MapViewController {
    return MapViewController()
  }

  func updateUIViewController(_ uiViewController: MapViewController, context: Context) {
  }
}

Sử dụng MapViewControllerBridge trong ContentView

Giờ đây, MapViewControllerBridge đang tạo một thực thể của MapViewController, bước tiếp theo là sử dụng cấu trúc này trong ContentView để hiển thị bản đồ.

  1. Mở tệp ContentView trong Xcode.

ContentView được khởi tạo trong SceneDelegate và chứa khung hiển thị ứng dụng cấp cao nhất. Bản đồ sẽ được thêm từ bên trong tệp này.

  1. Tạo một MapViewControllerBridge trong thuộc tính body.

Trong thuộc tính body của tệp này, một ZStack đã được cung cấp và triển khai cho bạn. ZStack chứa một danh sách các thành phố có thể tương tác và kéo được mà bạn sẽ sử dụng ở bước sau. Hiện tại, trong ZStack, hãy tạo một MapViewControllerBridge làm khung hiển thị con đầu tiên của ZStack để bản đồ sẽ xuất hiện trong ứng dụng phía sau khung hiển thị danh sách thành phố. Sau khi thực hiện, nội dung của thuộc tính body trong ContentView sẽ có dạng như sau:

ContentView

var body: some View {

  let scrollViewHeight: CGFloat = 80

  GeometryReader { geometry in
    ZStack(alignment: .top) {
      // Map
      MapViewControllerBridge()

      // Cities List
      CitiesList(markers: $markers) { (marker) in
        guard self.selectedMarker != marker else { return }
        self.selectedMarker = marker
        self.zoomInCenter = false
        self.expandList = false
      }  handleAction: {
        self.expandList.toggle()
      } // ...
    }
  }
}
  1. Bây giờ, hãy chạy ứng dụng. Bạn sẽ thấy bản đồ tải trên màn hình thiết bị cùng với danh sách các thành phố có thể kéo ở cuối màn hình.

7. Thêm điểm đánh dấu vào bản đồ

Ở bước trước, bạn đã thêm một bản đồ cùng với một danh sách có thể tương tác, hiển thị danh sách các thành phố. Trong phần này, bạn sẽ thêm điểm đánh dấu cho từng thành phố trong danh sách đó.

map-with-markers@2x.png

Điểm đánh dấu dưới dạng trạng thái

ContentView khai báo một thuộc tính có tên là markers, đây là danh sách GMSMarker đại diện cho từng thành phố được khai báo trong thuộc tính tĩnh cities. Xin lưu ý rằng thuộc tính này được chú thích bằng trình bao bọc thuộc tính SwiftUI State để cho biết rằng thuộc tính này phải do SwiftUI quản lý. Vì vậy, nếu phát hiện thấy bất kỳ thay đổi nào đối với thuộc tính này, chẳng hạn như thêm hoặc xoá một điểm đánh dấu, thì các khung hiển thị sử dụng trạng thái này sẽ được cập nhật.

ContentView

  static let cities = [
    City(name: "San Francisco", coordinate: CLLocationCoordinate2D(latitude: 37.7576, longitude: -122.4194)),
    City(name: "Seattle", coordinate: CLLocationCoordinate2D(latitude: 47.6131742, longitude: -122.4824903)),
    City(name: "Singapore", coordinate: CLLocationCoordinate2D(latitude: 1.3440852, longitude: 103.6836164)),
    City(name: "Sydney", coordinate: CLLocationCoordinate2D(latitude: -33.8473552, longitude: 150.6511076)),
    City(name: "Tokyo", coordinate: CLLocationCoordinate2D(latitude: 35.6684411, longitude: 139.6004407))
  ]

  /// State for markers displayed on the map for each city in `cities`
  @State var markers: [GMSMarker] = cities.map {
    let marker = GMSMarker(position: $0.coordinate)
    marker.title = $0.name
    return marker
  }

Xin lưu ý rằng ContentView sử dụng thuộc tính markers để hiển thị danh sách các thành phố bằng cách truyền thuộc tính đó vào lớp CitiesList.

CitiesList

struct CitiesList: View {

  @Binding var markers: [GMSMarker]

  var body: some View {
    GeometryReader { geometry in
      VStack(spacing: 0) {
        // ...
        // List of Cities
        List {
          ForEach(0..<self.markers.count) { id in
            let marker = self.markers[id]
            Button(action: {
              buttonAction(marker)
            }) {
              Text(marker.title ?? "")
            }
          }
        }.frame(maxWidth: .infinity)
      }
    }
  }
}

Truyền trạng thái đến MapViewControllerBridge bằng @Binding

Ngoài danh sách các thành phố hiển thị dữ liệu từ thuộc tính markers, hãy truyền thuộc tính này đến cấu trúc MapViewControllerBridge để có thể dùng thuộc tính này hiển thị các điểm đánh dấu đó trên bản đồ. Cách thực hiện như sau:

  1. Khai báo một thuộc tính markers mới trong MapViewControllerBridge được chú thích bằng @Binding

MapViewControllerBridge

struct MapViewControllerBridge: : UIViewControllerRepresentable {
  @Binding var markers: [GMSMarker]
  // ...
}
  1. Trong MapViewControllerBridge, hãy cập nhật phương thức updateUIViewController(_, context) để sử dụng thuộc tính markers

Như đã đề cập ở bước trước, updateUIViewController(_, context) sẽ được SwiftUI gọi bất cứ khi nào trạng thái thay đổi. Trong phương thức này, chúng ta muốn cập nhật bản đồ để hiển thị các điểm đánh dấu trong markers. Để làm việc này, bạn cần cập nhật thuộc tính map của mỗi điểm đánh dấu. Sau khi hoàn tất bước này, MapViewControllerBridge của bạn sẽ có dạng như sau:

import GoogleMaps
import SwiftUI

struct MapViewControllerBridge: UIViewControllerRepresentable {

  @Binding var markers: [GMSMarker]

  func makeUIViewController(context: Context) -> MapViewController {
    return MapViewController()
  }

  func updateUIViewController(_ uiViewController: MapViewController, context: Context) {
    // Update the map for each marker
    markers.forEach { $0.map = uiViewController.map }
  }
}
  1. Truyền thuộc tính markers từ ContentView sang MapViewControllerBridge

Vì bạn đã thêm một thuộc tính mới trong MapViewControllerBridge, nên giờ đây, bạn phải truyền giá trị cho thuộc tính này trong trình khởi tạo cho MapViewControllerBridge. Vì vậy, nếu cố gắng tạo ứng dụng, bạn sẽ nhận thấy ứng dụng không biên dịch được. Để khắc phục vấn đề này, hãy cập nhật ContentView nơi MapViewControllerBridge được tạo và truyền thuộc tính markers như sau:

struct ContentView: View {
  // ...
  var body: some View {
    // ...
    GeometryReader { geometry in
      ZStack(alignment: .top) {
        // Map
        MapViewControllerBridge(markers: $markers)
        // ...
      }
    }
  }
}

Lưu ý rằng tiền tố $ được dùng để truyền markers vào MapViewControllerBridge vì nó mong đợi một thuộc tính được liên kết. $ là tiền tố dành riêng để sử dụng với trình bao bọc thuộc tính Swift. Khi áp dụng cho một Trạng thái, nó sẽ trả về một Binding.

  1. Hãy chạy ứng dụng để xem các điểm đánh dấu xuất hiện trên bản đồ.

8. Tạo hiệu ứng chuyển động đến một thành phố đã chọn

Ở bước trước, bạn đã thêm các điểm đánh dấu vào bản đồ bằng cách truyền Trạng thái từ một khung hiển thị SwiftUI sang một khung hiển thị khác. Ở bước này, bạn sẽ tạo hiệu ứng chuyển động đến một thành phố hoặc điểm đánh dấu sau khi thành phố hoặc điểm đánh dấu đó được nhấn vào trong danh sách có thể tương tác. Để thực hiện ảnh động, bạn sẽ phản ứng với các thay đổi đối với một Trạng thái bằng cách sửa đổi vị trí camera của bản đồ khi thay đổi xảy ra. Để tìm hiểu thêm về khái niệm camera của bản đồ, hãy xem phần Camera và khung hiển thị.

animate-city@2x.png

Tạo hiệu ứng ảnh động cho bản đồ đến thành phố đã chọn

Cách tạo hiệu ứng chuyển động cho bản đồ đến một thành phố đã chọn:

  1. Xác định một Binding mới trong MapViewControllerBridge

ContentView có một thuộc tính State (Trạng thái) tên là selectedMarker. Thuộc tính này được khởi tạo thành giá trị rỗng và được cập nhật bất cứ khi nào một thành phố được chọn trong danh sách. Việc này do khung hiển thị CitiesList buttonAction trong ContentView xử lý.

ContentView

CitiesList(markers: $markers) { (marker) in
  guard self.selectedMarker != marker else { return }
  self.selectedMarker = marker
  // ...
}

Bất cứ khi nào selectedMarker thay đổi, MapViewControllerBridge phải nhận biết được sự thay đổi trạng thái này để có thể tạo hiệu ứng động cho bản đồ đến điểm đánh dấu đã chọn. Vì vậy, hãy xác định một Binding mới trong MapViewControllerBridge thuộc loại GMSMarker và đặt tên cho thuộc tính này là selectedMarker.

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  @Binding var selectedMarker: GMSMarker?
}
  1. Cập nhật MapViewControllerBridge để tạo hiệu ứng cho bản đồ bất cứ khi nào selectedMarker thay đổi

Sau khi khai báo một Binding mới, bạn cần cập nhật hàm updateUIViewController_, context) của MapViewControllerBridge để bản đồ chuyển động đến điểm đánh dấu đã chọn. Hãy tiếp tục và thực hiện việc này bằng cách sao chép mã sau:

struct MapViewControllerBridge: UIViewControllerRepresentable {
  @Binding var selectedMarker: GMSMarker?

  func updateUIViewController(_ uiViewController: MapViewController, context: Context) {
    markers.forEach { $0.map = uiViewController.map }
    selectedMarker?.map = uiViewController.map
    animateToSelectedMarker(viewController: uiViewController)
  }

  private func animateToSelectedMarker(viewController: MapViewController) {
    guard let selectedMarker = selectedMarker else {
      return
    }

    let map = viewController.map
    if map.selectedMarker != selectedMarker {
      map.selectedMarker = selectedMarker
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        map.animate(toZoom: kGMSMinZoomLevel)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          map.animate(with: GMSCameraUpdate.setTarget(selectedMarker.position))
          DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
            map.animate(toZoom: 12)
          })
        }
      }
    }
  }
}

Hàm animateToSelectedMarker(viewController) sẽ thực hiện một chuỗi ảnh động trên bản đồ bằng hàm animate(with) của GMSMapView.

  1. Truyền selectedMarker của ContentView đến MapViewControllerBridge

Sau khi MapViewControllerBridge khai báo Binding mới, hãy tiếp tục cập nhật ContentView để truyền vào selectedMarker nơi MapViewControllerBridge được khởi tạo.

ContentView

struct ContentView: View {
  // ...
  var body: some View {
    // ...
    GeometryReader { geometry in
      ZStack(alignment: .top) {
        // Map
        MapViewControllerBridge(markers: $markers, selectedMarker: $selectedMarker)
        // ...
      }
    }
  }
}

Khi bạn hoàn tất bước này, bản đồ sẽ chuyển động mỗi khi một thành phố mới được chọn trong danh sách.

Tạo ảnh động cho khung hiển thị SwiftUI để làm nổi bật thành phố

SwiftUI đơn giản hoá quy trình tạo ảnh động cho các khung hiển thị, vì nó sẽ xử lý việc thực hiện ảnh động cho các quá trình chuyển đổi State. Để minh hoạ điều này, bạn sẽ thêm nhiều ảnh động hơn bằng cách lấy tiêu điểm của khung hiển thị vào thành phố đã chọn sau khi ảnh động trên bản đồ hoàn tất. Để thực hiện việc này, hãy hoàn tất các bước sau:

  1. Thêm onAnimationEnded vào MapViewControllerBridge

Vì ảnh động SwiftUI sẽ được thực hiện sau chuỗi ảnh động trên bản đồ mà bạn đã thêm trước đó, hãy khai báo một bao đóng mới có tên là onAnimationEnded trong MapViewControllerBridge và gọi bao đóng này sau khi trễ 0,5 giây sau ảnh động cuối cùng trên bản đồ trong phương thức animateToSelectedMarker(viewController).

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
    var onAnimationEnded: () -> ()

    private func animateToSelectedMarker(viewController: MapViewController) {
    guard let selectedMarker = selectedMarker else {
      return
    }

    let map = viewController.map
    if map.selectedMarker != selectedMarker {
      map.selectedMarker = selectedMarker
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        map.animate(toZoom: kGMSMinZoomLevel)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          map.animate(with: GMSCameraUpdate.setTarget(selectedMarker.position))
          DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
            map.animate(toZoom: 12)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
              // Invoke onAnimationEnded() once the animation sequence completes
              onAnimationEnded()
            })
          })
        }
      }
    }
  }
}
  1. Triển khai onAnimationEnded trong MapViewControllerBridge

Triển khai phương thức đóng onAnimationEnded trong đó MapViewControllerBridge được tạo thực thể trong ContentView. Sao chép và dán mã sau đây để thêm một Trạng thái mới có tên là zoomInCenter, đồng thời sửa đổi khung hiển thị bằng cách sử dụng clipShape và thay đổi đường kính của hình dạng bị cắt tuỳ thuộc vào giá trị của zoomInCenter

ContentView

struct ContentView: View {
  @State var zoomInCenter: Bool = false
  // ...
  var body: some View {
    // ...
    GeometryReader { geometry in
      ZStack(alignment: .top) {
        // Map
        let diameter = zoomInCenter ? geometry.size.width : (geometry.size.height * 2)
        MapViewControllerBridge(markers: $markers, selectedMarker: $selectedMarker, onAnimationEnded: {
          self.zoomInCenter = true
        })
        .clipShape(
           Circle()
             .size(
               width: diameter,
               height: diameter
             )
             .offset(
               CGPoint(
                 x: (geometry.size.width - diameter) / 2,
                 y: (geometry.size.height - diameter) / 2
               )
             )
        )
        .animation(.easeIn)
        .background(Color(red: 254.0/255.0, green: 1, blue: 220.0/255.0))
      }
    }
  }
}
  1. Hãy chạy ứng dụng để xem ảnh động!

9. Gửi một sự kiện đến SwiftUI

Trong bước này, bạn sẽ theo dõi các sự kiện phát ra từ GMSMapView và gửi sự kiện đó đến SwiftUI. Cụ thể, bạn sẽ đặt một uỷ quyền cho khung hiển thị bản đồ và lắng nghe các sự kiện di chuyển camera để khi một thành phố được lấy làm tiêu điểm và camera bản đồ di chuyển do một cử chỉ, khung hiển thị bản đồ sẽ huỷ lấy tiêu điểm để bạn có thể xem thêm nội dung trên bản đồ.

Sử dụng các đối tượng điều phối SwiftUI

GMSMapView phát ra các sự kiện như thay đổi vị trí camera hoặc khi người dùng nhấn vào một điểm đánh dấu. Cơ chế để theo dõi các sự kiện này là thông qua giao thức GMSMapViewDelegate. SwiftUI giới thiệu khái niệm về Trình điều phối, được dùng riêng để đóng vai trò là đại biểu cho các trình điều khiển chế độ xem UIKit. Vì vậy, trong thế giới SwiftUI, một Trình điều phối sẽ chịu trách nhiệm tuân thủ giao thức GMSMapViewDelegate. Để làm việc này, vui lòng hoàn thành các bước sau:

  1. Tạo một đối tượng Điều phối có tên là MapViewCoordinator trong MapViewControllerBridge

Tạo một lớp lồng nhau bên trong lớp MapViewControllerBridge và gọi lớp đó là MapViewCoordinator. Lớp này phải tuân thủ GMSMapViewDelegate và phải khai báo MapViewControllerBridge dưới dạng một thuộc tính.

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  // ...
  final class MapViewCoordinator: NSObject, GMSMapViewDelegate {
    var mapViewControllerBridge: MapViewControllerBridge

    init(_ mapViewControllerBridge: MapViewControllerBridge) {
      self.mapViewControllerBridge = mapViewControllerBridge
    }
  }
}
  1. Triển khai makeCoordinator() trong MapViewControllerBridge

Tiếp theo, hãy triển khai phương thức makeCoordinator() trong MapViewControllerBridge và trả về một phiên bản của MapViewCoodinator mà bạn đã tạo ở bước trước.

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  // ...
  func makeCoordinator() -> MapViewCoordinator {
    return MapViewCoordinator(self)
  }
}
  1. Đặt MapViewCoordinator làm uỷ quyền của chế độ xem bản đồ

Sau khi tạo bộ điều phối tuỳ chỉnh, bước tiếp theo là đặt bộ điều phối làm uỷ quyền cho chế độ xem bản đồ của trình điều khiển khung hiển thị. Để thực hiện việc này, hãy cập nhật quá trình khởi tạo trình điều khiển khung hiển thị trong makeUIViewController(context). Bạn có thể truy cập vào trình điều phối đã tạo ở bước trước thông qua đối tượng Context.

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  // ...
  func makeUIViewController(context: Context) -> MapViewController {
    let uiViewController = MapViewController()
    uiViewController.map.delegate = context.coordinator
    return uiViewController
  }
}
  1. Thêm một lệnh đóng vào MapViewControllerBridge để sự kiện di chuyển camera có thể được truyền lên

Vì mục tiêu là cập nhật khung hiển thị khi camera di chuyển, hãy khai báo một thuộc tính đóng mới chấp nhận giá trị boolean trong MapViewControllerBridge có tên là mapViewWillMove và gọi đóng này trong phương thức uỷ quyền mapView(_, willMove) trong MapViewCoordinator. Truyền giá trị của gesture đến khối đóng để khung hiển thị SwiftUI chỉ có thể phản ứng với các sự kiện di chuyển camera liên quan đến cử chỉ.

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  var mapViewWillMove: (Bool) -> ()
  //...

  final class MapViewCoordinator: NSObject, GMSMapViewDelegate {
    // ...
    func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
      self.mapViewControllerBridge.mapViewWillMove(gesture)
    }
  }
}
  1. Cập nhật ContentView để truyền giá trị cho mapWillMove

Với thông báo đóng cửa mới vào ngày MapViewControllerBridge, hãy cập nhật ContentView để truyền vào một giá trị cho thông báo đóng cửa mới này. Trong khối đóng đó, hãy chuyển đổi Trạng thái zoomInCenter thành false nếu sự kiện di chuyển có liên quan đến một cử chỉ. Thao tác này sẽ cho thấy bản đồ ở chế độ xem đầy đủ một lần nữa khi bản đồ được di chuyển bằng một cử chỉ.

ContentView

struct ContentView: View {
  @State var zoomInCenter: Bool = false
  // ...
  var body: some View {
    // ...
    GeometryReader { geometry in
      ZStack(alignment: .top) {
        // Map
        let diameter = zoomInCenter ? geometry.size.width : (geometry.size.height * 2)
        MapViewControllerBridge(markers: $markers, selectedMarker: $selectedMarker, onAnimationEnded: {
          self.zoomInCenter = true
        }, mapViewWillMove: { (isGesture) in
          guard isGesture else { return }
          self.zoomInCenter = false
        })
        // ...
      }
    }
  }
}
  1. Hãy chạy ứng dụng để xem các thay đổi mới!

10. Xin chúc mừng

Chúc mừng bạn đã đi được đến đây! Bạn đã học được rất nhiều kiến thức và hy vọng rằng những bài học bạn đã học được sẽ giúp bạn xây dựng ứng dụng SwiftUI của riêng mình bằng Maps SDK cho iOS.

Kiến thức bạn học được

Tiếp theo là gì?

  • Maps SDK cho iOS
    • tài liệu chính thức cho Maps SDK dành cho iOS
  • Places SDK for iOS – tìm các doanh nghiệp địa phương và địa điểm yêu thích xung quanh bạn
  • maps-sdk-for-ios-samples
    • mã mẫu trên GitHub minh hoạ tất cả các tính năng trong Maps SDK cho iOS.
  • SwiftUI – Tài liệu chính thức của Apple về SwiftUI
  • Hãy giúp chúng tôi tạo ra nội dung hữu ích nhất cho bạn bằng cách trả lời khảo sát sau:

Bạn muốn xem những lớp học lập trình nào khác?

Trực quan hoá dữ liệu trên bản đồ Tìm hiểu thêm về cách tuỳ chỉnh kiểu bản đồ Tạo các tương tác 3D trên bản đồ

Bạn không tìm thấy lớp học lập trình mà mình quan tâm nhất? Yêu cầu cấp lại bằng cách báo lỗi mới tại đây.