เพิ่มแผนที่ลงในแอป iOS ด้วย SwiftUI (Swift)

1. ก่อนที่คุณจะเริ่มต้น

Codelab นี้จะสอนวิธีใช้ Maps SDK สำหรับ iOS กับ SwiftUI

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

ข้อกำหนดเบื้องต้น

  • ความรู้พื้นฐานเกี่ยวกับ Swift
  • มีความคุ้นเคยกับ SwiftUI ในระดับพื้นฐาน

สิ่งที่คุณต้องดำเนินการ

  • เปิดใช้และใช้ Maps SDK สำหรับ iOS เพื่อเพิ่ม Google Maps ลงในแอป iOS โดยใช้ SwiftUI
  • เพิ่มเครื่องหมายลงในแผนที่
  • ส่งสถานะระหว่างออบเจ็กต์ SwiftUI กับ GMSMapView

สิ่งที่คุณต้องมี

2. ตั้งค่า

สำหรับขั้นตอนการเปิดใช้ต่อไปนี้ ให้เปิดใช้ Maps SDK สำหรับ iOS

ตั้งค่า Google Maps Platform

หากยังไม่มีบัญชี Google Cloud Platform และโปรเจ็กต์ที่เปิดใช้การเรียกเก็บเงิน โปรดดูคู่มือเริ่มต้นใช้งาน Google Maps Platform เพื่อสร้างบัญชีสำหรับการเรียกเก็บเงินและโปรเจ็กต์

  1. ใน Cloud Console ให้คลิกเมนูแบบเลื่อนลงของโปรเจ็กต์ แล้วเลือกโปรเจ็กต์ที่ต้องการใช้สำหรับ Codelab นี้

  1. เปิดใช้ Google Maps Platform APIs และ SDK ที่จำเป็นสำหรับ Codelab นี้ใน Google Cloud Marketplace โดยทำตามขั้นตอนในวิดีโอนี้หรือเอกสารประกอบนี้
  2. สร้างคีย์ API ในหน้าข้อมูลเข้าสู่ระบบของ Cloud Console คุณสามารถทำตามขั้นตอนในวิดีโอนี้หรือเอกสารประกอบนี้ คำขอทั้งหมดไปยัง Google Maps Platform ต้องใช้คีย์ API

3. ดาวน์โหลดโค้ดเริ่มต้น

เรามีโค้ดเริ่มต้นที่จะช่วยให้คุณเริ่มต้นใช้งานได้อย่างรวดเร็วที่สุด และช่วยให้คุณทำตาม Codelab นี้ได้ คุณสามารถข้ามไปยังโซลูชันได้ แต่หากต้องการทำตามขั้นตอนทั้งหมดเพื่อสร้างโซลูชันด้วยตนเอง โปรดอ่านต่อ

  1. โคลนที่เก็บหากคุณติดตั้ง git ไว้
git clone https://github.com/googlecodelabs/maps-ios-swiftui.git

หรือจะคลิกปุ่มต่อไปนี้เพื่อดาวน์โหลดซอร์สโค้ดก็ได้

  1. เมื่อได้รับรหัสแล้ว ให้ไปที่ไดเรกทอรี starter/GoogleMapsSwiftUI ในเทอร์มินัล cd
  2. เรียกใช้ carthage update --platform iOS เพื่อดาวน์โหลด Maps SDK สำหรับ iOS
  3. สุดท้าย ให้เปิดไฟล์ GoogleMapsSwiftUI.xcodeproj ใน Xcode

4. ภาพรวมของโค้ด

ในโปรเจ็กต์เริ่มต้นที่คุณดาวน์โหลดมา เราได้จัดเตรียมและใช้งานคลาสต่อไปนี้ให้คุณแล้ว

  • AppDelegate - UIApplicationDelegate ของแอปพลิเคชัน ส่วนนี้คือที่ที่จะเริ่มต้น Maps SDK สำหรับ iOS
  • City - โครงสร้างที่แสดงถึงเมือง (มีชื่อและพิกัดของเมือง)
  • MapViewController - UIKit ที่มีขอบเขตจำกัดUIViewControllerซึ่งมี Google Maps (GMSMapView)
    • SceneDelegate - UIWindowSceneDelegate ของแอปพลิเคชันที่สร้างอินสแตนซ์ของ ContentView

นอกจากนี้ คลาสต่อไปนี้มีการติดตั้งใช้งานบางส่วนและคุณจะต้องติดตั้งใช้งานให้เสร็จภายในสิ้น Codelab นี้

  • ContentView - มุมมอง SwiftUI ระดับบนสุดที่มีแอปของคุณ
  • MapViewControllerBridge - คลาสที่เชื่อมมุมมอง UIKit กับมุมมอง SwiftUI กล่าวโดยละเอียดคือคลาสนี้จะทำให้ MapViewController เข้าถึงได้ใน SwiftUI

5. SwiftUI กับ UIKit

SwiftUI เปิดตัวใน iOS 13 เป็นเฟรมเวิร์ก UI ทางเลือกแทน UIKit สำหรับการพัฒนาแอปพลิเคชัน iOS SwiftUI มีข้อดีหลายประการเมื่อเทียบกับ UIKit ซึ่งเป็นรุ่นก่อนหน้า ตัวอย่างเช่น

  • มุมมองจะอัปเดตโดยอัตโนมัติเมื่อสถานะเปลี่ยนแปลง การใช้ออบเจ็กต์ที่เรียกว่า State จะทำให้ UI อัปเดตโดยอัตโนมัติเมื่อมีการเปลี่ยนแปลงค่าพื้นฐานที่ออบเจ็กต์นั้นมี
  • การแสดงตัวอย่างสดช่วยให้พัฒนาได้เร็วขึ้น การแสดงตัวอย่างแบบเรียลไทม์ช่วยลดความจำเป็นในการสร้างและติดตั้งใช้งานโค้ดกับโปรแกรมจำลองเพื่อดูการเปลี่ยนแปลงภาพ เนื่องจากคุณสามารถดูตัวอย่างมุมมอง SwiftUI ได้อย่างง่ายดายใน Xcode
  • แหล่งข้อมูลความจริงอยู่ใน Swift โดยประกาศมุมมองทั้งหมดใน SwiftUI ใน Swift จึงไม่จำเป็นต้องใช้ Interface Builder อีกต่อไป
  • ทำงานร่วมกับ UIKit การทำงานร่วมกันกับ UIKit ช่วยให้มั่นใจได้ว่าแอปที่มีอยู่จะใช้ SwiftUI กับมุมมองที่มีอยู่ได้ทีละน้อย นอกจากนี้ คุณยังใช้ไลบรารีที่ยังไม่รองรับ SwiftUI เช่น Maps SDK สำหรับ iOS ใน SwiftUI ได้

แต่ก็มีข้อเสียบางประการเช่นกัน

  • SwiftUI ใช้ได้ใน iOS 13 ขึ้นไปเท่านั้น
  • ตรวจสอบลำดับชั้นของมุมมองในตัวอย่าง Xcode ไม่ได้

สถานะและการไหลของข้อมูลใน SwiftUI

SwiftUI มีวิธีใหม่ในการสร้าง UI โดยใช้แนวทางแบบประกาศ นั่นคือคุณบอก SwiftUI ว่าต้องการให้มุมมองมีลักษณะอย่างไรพร้อมกับสถานะต่างๆ ทั้งหมดของมุมมองนั้น แล้วระบบจะจัดการส่วนที่เหลือให้ SwiftUI จะจัดการการอัปเดตมุมมองทุกครั้งที่สถานะพื้นฐานเปลี่ยนแปลงเนื่องจากเหตุการณ์หรือการกระทําของผู้ใช้ การออกแบบนี้มักเรียกว่าการไหลของข้อมูลแบบทิศทางเดียว แม้ว่ารายละเอียดของการออกแบบนี้จะอยู่นอกขอบเขตของโค้ดแล็บนี้ แต่เราขอแนะนำให้อ่านวิธีการทำงานนี้ในเอกสารประกอบของ Apple เกี่ยวกับการไหลของสถานะและข้อมูล

เชื่อมต่อ UIKit และ SwiftUI โดยใช้ UIViewRepresentable หรือ UIViewControllerRepresentable

เนื่องจาก Maps SDK สำหรับ iOS สร้างขึ้นบน UIKit และไม่ได้มีมุมมองที่เข้ากันได้กับ SwiftUI การใช้ใน SwiftUI จึงต้องเป็นไปตาม UIViewRepresentable หรือ UIViewControllerRepresentable โปรโตคอลเหล่านี้ช่วยให้ SwiftUI สามารถรวม UIView และ UIViewController ที่สร้างด้วย UIKit ได้ตามลำดับ แม้ว่าคุณจะใช้โปรโตคอลใดก็ได้เพื่อเพิ่ม Google Maps ลงในมุมมอง SwiftUI แต่ในขั้นตอนถัดไป เราจะมาดูการใช้ UIViewControllerRepresentable เพื่อรวม UIViewController ที่มีแผนที่

6. เพิ่มแผนที่

ในส่วนนี้ คุณจะเพิ่ม Google Maps ลงในมุมมอง SwiftUI

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

เพิ่มคีย์ API

คุณต้องระบุคีย์ API ที่สร้างไว้ในขั้นตอนก่อนหน้าให้กับ Maps SDK สำหรับ iOS เพื่อเชื่อมโยงบัญชีกับแผนที่ที่จะแสดงในแอป

หากต้องการระบุคีย์ API ให้เปิดไฟล์ AppDelegate.swift แล้วไปที่เมธอด application(_, didFinishLaunchingWithOptions) ระบบจะเริ่มต้น SDK โดยใช้ GMSServices.provideAPIKey() ที่มีสตริง "YOUR_API_KEY" แทนที่สตริงดังกล่าวด้วยคีย์ API ของคุณ การทำขั้นตอนนี้ให้เสร็จจะเริ่มต้น Maps SDK สำหรับ iOS เมื่อแอปพลิเคชันเปิดตัว

เพิ่มแผนที่ของ Google โดยใช้ MapViewControllerBridge

ตอนนี้ SDK ได้รับคีย์ API แล้ว ขั้นตอนถัดไปคือการแสดงแผนที่ในแอป

ตัวควบคุมมุมมองที่ระบุไว้ในโค้ดเริ่มต้น MapViewController มี GMSMapView ในมุมมอง อย่างไรก็ตาม เนื่องจากตัวควบคุมมุมมองนี้สร้างขึ้นใน UIKit คุณจึงต้องเชื่อมต่อคลาสนี้กับ SwiftUI เพื่อให้ใช้ภายใน ContentView ได้ โดยทำดังนี้

  1. เปิดไฟล์ MapViewControllerBridge ใน Xcode

คลาสนี้เป็นไปตาม UIViewControllerRepresentable ซึ่งเป็นโปรโตคอลที่จำเป็นในการห่อหุ้ม UIKit UIViewController เพื่อให้ใช้เป็นมุมมอง SwiftUI ได้ กล่าวคือ การปฏิบัติตามโปรโตคอลนี้จะช่วยให้การเชื่อมโยงมุมมอง UIKit กับมุมมอง SwiftUI เป็นไปได้ง่ายขึ้น การปฏิบัติตามโปรโตคอลนี้ต้องใช้ 2 วิธีต่อไปนี้

  • makeUIViewController(context) - SwiftUI เรียกใช้เมธอดนี้เพื่อสร้าง UIViewController ที่อยู่เบื้องหลัง คุณจะสร้างอินสแตนซ์ของ UIViewController และส่งสถานะเริ่มต้นได้ที่นี่
  • updateUIViewController(_, context) - SwiftUI จะเรียกใช้เมธอดนี้ทุกครั้งที่สถานะเปลี่ยนแปลง คุณจะทำการแก้ไขใดๆ กับ UIViewController พื้นฐานเพื่อตอบสนองต่อการเปลี่ยนแปลงสถานะได้ที่นี่
  1. สร้าง MapViewController

ภายในฟังก์ชัน makeUIViewController(context) ให้สร้างอินสแตนซ์ MapViewController ใหม่และส่งคืนเป็นผลลัพธ์ หลังจากดำเนินการดังกล่าวแล้ว MapViewControllerBridge ของคุณควรมีลักษณะดังนี้

MapViewControllerBridge

import GoogleMaps
import SwiftUI

struct MapViewControllerBridge: UIViewControllerRepresentable {

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

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

ใช้ MapViewControllerBridge ใน ContentView

ตอนนี้ MapViewControllerBridge กำลังสร้างอินสแตนซ์ของ MapViewController ขั้นตอนถัดไปคือการใช้โครงสร้างนี้ภายใน ContentView เพื่อแสดงแผนที่

  1. เปิดไฟล์ ContentView ใน Xcode

ContentView จะได้รับการเริ่มต้นใน SceneDelegate และมีมุมมองแอปพลิเคชันระดับบนสุด ระบบจะเพิ่มแผนที่จากภายในไฟล์นี้

  1. สร้าง MapViewControllerBridge ภายในพร็อพเพอร์ตี้ body

ในพร็อพเพอร์ตี้ body ของไฟล์นี้ เราได้ระบุและใช้ ZStack ให้คุณแล้ว ZStack มีรายการเมืองที่โต้ตอบและลากได้ ซึ่งคุณจะใช้ในขั้นตอนถัดไป ตอนนี้ ให้สร้าง ZStack เป็นมุมมองย่อยแรกของ ZStack เพื่อให้แผนที่แสดงในแอปที่อยู่ด้านหลังมุมมองรายการเมืองMapViewControllerBridge เมื่อดำเนินการดังกล่าว เนื้อหาของพร็อพเพอร์ตี้ body ภายใน ContentView ควรมีลักษณะดังนี้

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. ตอนนี้ให้เรียกใช้แอป คุณควรเห็นแผนที่โหลดบนหน้าจอของอุปกรณ์พร้อมกับรายการเมืองที่ลากได้ที่ด้านล่างของหน้าจอ

7. เพิ่มเครื่องหมายลงในแผนที่

ในขั้นตอนก่อนหน้า คุณได้เพิ่มแผนที่ข้างรายการที่โต้ตอบได้ซึ่งแสดงรายชื่อเมือง ในส่วนนี้ คุณจะเพิ่มเครื่องหมายสำหรับแต่ละเมืองในรายการนั้น

map-with-markers@2x.png

เครื่องหมายเป็นสถานะ

ContentView ประกาศพร็อพเพอร์ตี้ชื่อ markers ซึ่งเป็นรายการของ GMSMarker ที่แสดงถึงแต่ละเมืองที่ประกาศในพร็อพเพอร์ตี้แบบคงที่ cities โปรดสังเกตว่าพร็อพเพอร์ตี้นี้มีคำอธิบายประกอบด้วยตัวห่อหุ้มพร็อพเพอร์ตี้ SwiftUI State เพื่อระบุว่าควรให้ SwiftUI จัดการ ดังนั้น หากตรวจพบการเปลี่ยนแปลงในพร็อพเพอร์ตี้นี้ เช่น การเพิ่มหรือนำเครื่องหมายออก ระบบจะอัปเดตมุมมองที่ใช้สถานะนี้

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
  }

โปรดทราบว่า ContentView ใช้พร็อพเพอร์ตี้ markers เพื่อแสดงรายการเมืองโดยส่งไปยังคลาส 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)
      }
    }
  }
}

ส่งสถานะไปยัง MapViewControllerBridge โดยใช้ @Binding

นอกเหนือจากรายชื่อเมืองที่แสดงข้อมูลจากพร็อพเพอร์ตี้ markers แล้ว ให้ส่งพร็อพเพอร์ตี้นี้ไปยังโครงสร้าง MapViewControllerBridge เพื่อให้ใช้แสดงเครื่องหมายเหล่านั้นในแผนที่ได้ โดยสิ่งที่คุณต้องทำมีดังนี้

  1. ประกาศพร็อพเพอร์ตี้ markers ใหม่ภายใน MapViewControllerBridge ที่มีคำอธิบายประกอบด้วย @Binding

MapViewControllerBridge

struct MapViewControllerBridge: : UIViewControllerRepresentable {
  @Binding var markers: [GMSMarker]
  // ...
}
  1. ใน MapViewControllerBridge ให้อัปเดตวิธีการ updateUIViewController(_, context) เพื่อใช้พร็อพเพอร์ตี้ markers

ดังที่กล่าวไว้ในขั้นตอนก่อนหน้า SwiftUI จะเรียกใช้ updateUIViewController(_, context) ทุกครั้งที่สถานะมีการเปลี่ยนแปลง เราต้องการอัปเดตแผนที่ด้วยวิธีนี้ จึงแสดงเครื่องหมายใน markers โดยคุณจะต้องอัปเดตmapพร็อพเพอร์ตี้ของเครื่องหมายแต่ละรายการ หลังจากทำขั้นตอนนี้เสร็จแล้ว MapViewControllerBridge ควรมีลักษณะดังนี้

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. ส่งพร็อพเพอร์ตี้ markers จาก ContentView ไปยัง MapViewControllerBridge

เนื่องจากคุณได้เพิ่มพร็อพเพอร์ตี้ใหม่ใน MapViewControllerBridge ตอนนี้คุณจึงต้องส่งค่าสำหรับพร็อพเพอร์ตี้นี้ในตัวเริ่มต้นสำหรับ MapViewControllerBridge ดังนั้น หากคุณพยายามสร้างแอป คุณจะเห็นว่าแอปจะไม่คอมไพล์ หากต้องการแก้ไขปัญหานี้ ให้อัปเดต ContentView ที่สร้าง MapViewControllerBridge และส่งพร็อพเพอร์ตี้ markers ดังนี้

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

โปรดสังเกตว่ามีการใช้คำนำหน้า $ เพื่อส่ง markers ไปยัง MapViewControllerBridge เนื่องจากคาดว่าจะเป็นพร็อพเพอร์ตี้ที่เชื่อมโยง $ เป็นคำนำหน้าที่สงวนไว้สำหรับใช้กับตัวห่อหุ้มพร็อพเพอร์ตี้ Swift เมื่อใช้กับ State จะแสดงผล Binding

  1. เรียกใช้แอปเพื่อดูเครื่องหมายที่แสดงบนแผนที่

8. เคลื่อนไหวไปยังเมืองที่เลือก

ในขั้นตอนก่อนหน้า คุณได้เพิ่มเครื่องหมายลงในแผนที่โดยส่งต่อสถานะจากมุมมอง SwiftUI หนึ่งไปยังอีกมุมมองหนึ่ง ในขั้นตอนนี้ คุณจะเคลื่อนไหวไปยังเมืองหรือเครื่องหมายหลังจากที่แตะในรายการที่โต้ตอบได้ หากต้องการสร้างภาพเคลื่อนไหว คุณจะต้องตอบสนองต่อการเปลี่ยนแปลงสถานะโดยการแก้ไขตำแหน่งกล้องของแผนที่เมื่อเกิดการเปลี่ยนแปลง ดูข้อมูลเพิ่มเติมเกี่ยวกับแนวคิดของกล้องในแผนที่ได้ที่กล้องและมุมมอง

animate-city@2x.png

เคลื่อนไหวแผนที่ไปยังเมืองที่เลือก

วิธีเคลื่อนไหวแผนที่ไปยังเมืองที่เลือก

  1. กำหนด Binding ใหม่ใน MapViewControllerBridge

ContentView มีพร็อพเพอร์ตี้ State ชื่อ selectedMarker ซึ่งเริ่มต้นเป็น nil และจะได้รับการอัปเดตทุกครั้งที่มีการเลือกเมืองในรายการ CitiesList จะจัดการมุมมอง buttonAction นี้ภายใน ContentView

ContentView

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

เมื่อใดก็ตามที่ selectedMarker เปลี่ยนแปลง MapViewControllerBridge ควรทราบการเปลี่ยนแปลงสถานะนี้เพื่อให้สามารถเคลื่อนไหวแผนที่ไปยังเครื่องหมายที่เลือกได้ ดังนั้น ให้กำหนด Binding ใหม่ภายใน MapViewControllerBridge ของประเภท GMSMarker และตั้งชื่อพร็อพเพอร์ตี้ว่า selectedMarker

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  @Binding var selectedMarker: GMSMarker?
}
  1. อัปเดต MapViewControllerBridge เพื่อเคลื่อนไหวแผนที่ทุกครั้งที่ selectedMarker เปลี่ยนแปลง

เมื่อประกาศการเชื่อมโยงใหม่แล้ว คุณต้องอัปเดตฟังก์ชัน MapViewControllerBridgeupdateUIViewController_, context) เพื่อให้แผนที่เคลื่อนไหวไปยังเครื่องหมายที่เลือก โดยคัดลอกโค้ดต่อไปนี้

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)
          })
        }
      }
    }
  }
}

ฟังก์ชัน animateToSelectedMarker(viewController) จะแสดงภาพเคลื่อนไหวของแผนที่ตามลำดับโดยใช้ฟังก์ชัน animate(with) ของ GMSMapView

  1. ส่ง selectedMarker ของ ContentView ไปยัง MapViewControllerBridge

เมื่อ MapViewControllerBridge ประกาศ Binding ใหม่แล้ว ให้อัปเดต ContentView เพื่อส่งใน selectedMarker ที่มีการสร้างอินสแตนซ์ MapViewControllerBridge

ContentView

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

ตอนนี้การทำขั้นตอนนี้ให้เสร็จสมบูรณ์จะทำให้แผนที่เคลื่อนไหวทุกครั้งที่มีการเลือกเมืองใหม่ในรายการ

สร้างภาพเคลื่อนไหวของมุมมอง SwiftUI เพื่อเน้นเมือง

SwiftUI ช่วยลดความซับซ้อนของกระบวนการเคลื่อนไหวมุมมอง เนื่องจากจะจัดการการเคลื่อนไหวสำหรับการเปลี่ยนสถานะ คุณจะเพิ่มภาพเคลื่อนไหวมากขึ้นโดยโฟกัสมุมมองไปยังเมืองที่เลือกหลังจากภาพเคลื่อนไหวของแผนที่เสร็จสมบูรณ์ หากต้องการดำเนินการนี้ ให้ทำตามขั้นตอนต่อไปนี้

  1. เพิ่มonAnimationEndedปิดท้ายใน MapViewControllerBridge

เนื่องจากภาพเคลื่อนไหว SwiftUI จะทำงานหลังจากลำดับภาพเคลื่อนไหวของแผนที่ที่คุณเพิ่มไว้ก่อนหน้านี้ ให้ประกาศการปิดใหม่ที่ชื่อ onAnimationEnded ภายใน MapViewControllerBridge และเรียกใช้การปิดนี้หลังจากหน่วงเวลา 0.5 วินาทีหลังภาพเคลื่อนไหวของแผนที่สุดท้ายภายในเมธอด 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. ใช้ onAnimationEnded ใน MapViewControllerBridge

ใช้onAnimationEnded Closure ที่มีการสร้างอินสแตนซ์ MapViewControllerBridge ภายใน ContentView คัดลอกและวางโค้ดต่อไปนี้ซึ่งจะเพิ่มสถานะใหม่ที่ชื่อ zoomInCenter และยังแก้ไขมุมมองโดยใช้ clipShape และเปลี่ยนเส้นผ่านศูนย์กลางของรูปร่างที่ตัดตามค่าของ 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. เรียกใช้แอปเพื่อดูภาพเคลื่อนไหวได้เลย

9. ส่งเหตุการณ์ไปยัง SwiftUI

ในขั้นตอนนี้ คุณจะได้ฟังเหตุการณ์ที่ปล่อยออกมาจาก GMSMapView และส่งเหตุการณ์นั้นไปยัง SwiftUI โดยคุณจะตั้งค่าผู้มอบสิทธิ์ให้กับมุมมองแผนที่และรับฟังเหตุการณ์การย้ายกล้องเพื่อให้เมื่อโฟกัสเมืองและกล้องแผนที่ย้ายจากการสัมผัส มุมมองแผนที่จะเลิกโฟกัสเพื่อให้คุณเห็นแผนที่มากขึ้น

ใช้ตัวประสาน SwiftUI

GMSMapView จะปล่อยเหตุการณ์ต่างๆ เช่น การเปลี่ยนแปลงตำแหน่งกล้องหรือเมื่อมีการแตะเครื่องหมาย กลไกการฟังเหตุการณ์เหล่านี้คือผ่านโปรโตคอล GMSMapViewDelegate SwiftUI นำเสนอแนวคิดของ Coordinator ซึ่งใช้เพื่อทำหน้าที่เป็นตัวแทนสำหรับตัวควบคุมมุมมอง UIKit โดยเฉพาะ ดังนั้นในโลกของ SwiftUI ตัวประสานควรมีหน้าที่รับผิดชอบในการปฏิบัติตามโปรโตคอล GMSMapViewDelegate โดยทำตามขั้นตอนต่อไปนี้

  1. สร้างผู้ประสานงานชื่อ MapViewCoordinator ใน MapViewControllerBridge

สร้างคลาสที่ซ้อนกันภายในคลาส MapViewControllerBridge แล้วตั้งชื่อว่า MapViewCoordinator คลาสนี้ควรเป็นไปตาม GMSMapViewDelegate และควรประกาศ MapViewControllerBridge เป็นพร็อพเพอร์ตี้

MapViewControllerBridge

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

    init(_ mapViewControllerBridge: MapViewControllerBridge) {
      self.mapViewControllerBridge = mapViewControllerBridge
    }
  }
}
  1. ใช้ makeCoordinator() ใน MapViewControllerBridge

จากนั้นใช้เมธอด makeCoordinator() ภายใน MapViewControllerBridge และส่งคืนอินสแตนซ์ของ MapViewCoodinator ที่คุณสร้างในขั้นตอนก่อนหน้า

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  // ...
  func makeCoordinator() -> MapViewCoordinator {
    return MapViewCoordinator(self)
  }
}
  1. ตั้งค่า MapViewCoordinator เป็นผู้มอบสิทธิ์ของมุมมองแผนที่

เมื่อสร้างโคออร์ดิเนเตอร์ที่กำหนดเองแล้ว ขั้นตอนถัดไปคือการตั้งค่าโคออร์ดิเนเตอร์เป็นผู้มอบสิทธิ์สำหรับมุมมองแผนที่ของตัวควบคุมมุมมอง โดยอัปเดตการเริ่มต้นตัวควบคุมมุมมองใน makeUIViewController(context) คุณจะเข้าถึงโคออร์ดิเนเตอร์ที่สร้างขึ้นจากขั้นตอนก่อนหน้าได้จากออบเจ็กต์ Context

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  // ...
  func makeUIViewController(context: Context) -> MapViewController {
    let uiViewController = MapViewController()
    uiViewController.map.delegate = context.coordinator
    return uiViewController
  }
}
  1. เพิ่มการปิดไปยัง MapViewControllerBridge เพื่อให้กล้องจะย้ายเหตุการณ์สามารถเผยแพร่ขึ้นไปได้

เนื่องจากเป้าหมายคือการอัปเดตมุมมองเมื่อกล้องเคลื่อนที่ ให้ประกาศพร็อพเพอร์ตี้การปิดใหม่ที่ยอมรับบูลีนภายใน MapViewControllerBridge ที่เรียกว่า mapViewWillMove และเรียกใช้การปิดนี้ในเมธอดตัวแทน mapView(_, willMove) ภายใน MapViewCoordinator ส่งค่าของ gesture ไปยัง Closure เพื่อให้มุมมอง SwiftUI ตอบสนองต่อเหตุการณ์การเคลื่อนไหวของกล้องที่เกี่ยวข้องกับท่าทางสัมผัสเท่านั้น

MapViewControllerBridge

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

  final class MapViewCoordinator: NSObject, GMSMapViewDelegate {
    // ...
    func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
      self.mapViewControllerBridge.mapViewWillMove(gesture)
    }
  }
}
  1. อัปเดต ContentView เพื่อส่งค่าสำหรับ mapWillMove

เนื่องจากมีการประกาศการปิดใหม่ในวันที่ MapViewControllerBridge ให้อัปเดต ContentView เพื่อส่งค่าสำหรับการปิดใหม่นี้ ในการปิดนั้น ให้สลับสถานะ zoomInCenter เป็น false หากเหตุการณ์การเคลื่อนไหวเกี่ยวข้องกับท่าทางสัมผัส ซึ่งจะแสดงแผนที่ในมุมมองแบบเต็มอีกครั้งเมื่อมีการย้ายแผนที่ด้วยท่าทางสัมผัส

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. เรียกใช้แอปเพื่อดูการเปลี่ยนแปลงใหม่กันเลย

10. ขอแสดงความยินดี

ขอแสดงความยินดีที่มาไกลถึงจุดนี้ คุณได้เรียนรู้เนื้อหามากมาย และเราหวังว่าบทเรียนที่คุณได้เรียนรู้จะช่วยให้คุณสร้างแอป SwiftUI ของตัวเองโดยใช้ Maps SDK สำหรับ iOS ได้

สิ่งที่คุณได้เรียนรู้

  • ความแตกต่างระหว่าง SwiftUI กับ UIKit
  • วิธีเชื่อมต่อระหว่าง SwiftUI กับ UIKit โดยใช้ UIViewControllerRepresentable
  • วิธีเปลี่ยนแปลงมุมมองแผนที่ด้วย State และ Binding
  • วิธีส่งเหตุการณ์จากมุมมองแผนที่ไปยัง SwiftUI โดยใช้ Coordinator

ขั้นตอนถัดไปคือ

  • Maps SDK สำหรับ iOS
    • เอกสารอย่างเป็นทางการสำหรับ Maps SDK สำหรับ iOS
  • Places SDK สำหรับ iOS - ค้นหาธุรกิจในพื้นที่และจุดที่น่าสนใจรอบตัวคุณ
  • maps-sdk-for-ios-samples
    • ตัวอย่างโค้ดใน GitHub ที่สาธิตฟีเจอร์ทั้งหมดภายใน Maps SDK สำหรับ iOS
  • SwiftUI - เอกสารประกอบอย่างเป็นทางการของ Apple เกี่ยวกับ SwiftUI
  • ช่วยเราสร้างเนื้อหาที่เป็นประโยชน์ต่อคุณมากที่สุดโดยตอบแบบสำรวจต่อไปนี้

คุณอยากเห็น Codelab อื่นๆ แบบไหน

การแสดงข้อมูลเป็นภาพบนแผนที่ ข้อมูลเพิ่มเติมเกี่ยวกับการปรับแต่งรูปแบบของแผนที่ การสร้างการโต้ตอบ 3 มิติในแผนที่

หากไม่พบโค้ดแล็บที่คุณสนใจมากที่สุด ขอได้โดยแจ้งปัญหาใหม่ที่นี่