Skip to content
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Fx Studio
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Written by chuotfx on December 27, 2019

Basic iOS tutorial : MapView

iOS & Swift

Contents

  • Chuẩn bị
  • 1. MapView
  • 2. Làm việc với MapView
    • 2.1. Display MapView
      • Map Kit cung cấp 3 hệ toạ độ để xác định điểm trên bản đồ:
      • Kiểu hiển thị cho map
    • 2.2. Center Map
    • 2.3. Zoom Map
    • 2.3. Show current location
  • 3. Làm việc với các Annotation View
    • 3.1. Add AnnotationView
      • 3.1.1. Thêm 1 Annotation View
      • 3.1.2. Thêm nhiều Annotation View
      • 3.1.3. Tuỳ chỉnh
      • Custom MKAnnotationView
    • 3.2. Callout
      • 3.2.1. Hiển thị Callout
      • 3.2.1. Action
    • 3.3. Tóm tắt
  • 4. Overlay MapView
    • 4.1. Nguyên tắc chung
    • 4.2. Demo code
  • 5. Hiển thị nhiều loại Annotation View
  • 6. Routing
    • 6.1. Tìm tập hợp các toạ độ
    • 6.2. Vẽ đường đi
  • Tạm kết

Chào bạn đến với seri Lập trình iOS cho mọi người.

Bài viết này sẽ trình bày về chủ đề khá là hay và được sử dụng nhiều trong các ứng dụng mobile. Đó là MapView. Và chuẩn bị vào bài, bạn cần phải nắm một số kiến thức sau:

  • Basic iOS tutorial : Delegation Pattern
  • Core Location trong 10 phút

Mọi thứ đã ổn thì …

Bắt đầu thôi!

Chuẩn bị

  • MacOS 10.14.4
  • Xcode 11.0
  • Swift 5.1

1. MapView

Khi nói tới MapView hay Map, mọi người sẽ nghĩ tới Google Map là đầu tiên.

Nhưng bạn cần biết, chúng ta đang lập trình iOS, mà iOS lại là hàng chính chủ của Apple. Nên trong phạm vi bài viết thì mình chỉ đề cập tới Map của Apple mà thôi.

Về định nghĩa thì chúng ta có các khái niệm sau:

  • MapKit
    • Là framework chính chủ của Apple
    • Được xây dựng trên các API và data của Apple Map
    • Cung cấp cho các lập trình viên một tập các công cụ để thao tác và tích hợp Map vào ứng dụng iOS của họ
  • MapView
    • Class của nó là MKMapView
    • Dùng để hiển thị một bản đồ lên trên giao diện ứng dụng.
  • AnnotationView
    • Hay có thể gọi bằng nhiều cái tên phổ biến hơn, như: pin, marker
    • Class của nó là MKAnnotationView
    • Là các đối tượng mà bạn dùng để đánh dấu vị trí trên bản đồ
  • Overlay
    • Là các đối tượng mà dùng để vẽ lên MapView, như: hình tròn, vuông, chữ nhật, đa giác, ảnh …
    • Class:
      • iOS 7 trở về sau, MKOverlayRenderer giữ nhiệm vụ hiển thị lớp overlay.
      • Trước iOS 7, MKOverlayView giữ nhiệm vụ này.

Location ở đâu trong cuộc đời này?

Về location thì nó chính là dữ liệu cho MapView và các đối tượng khác thuộc MapView.

Chúng ta có một liên tưởng thú vị như sau:

  • UIImageView là một View để hiển thị 1 ảnh. UIImage là đối tượng cho một ảnh =>UIImage là data của UIImageView

Thì Location sẽ là data cho MapView. Bạn có thể đọc qua bài viết về Core Location để biết hơn cách lấy vị trí người dùng hiện tại.

Ngoài MapView của MapKit ra thì chúng ta có vô số các framework cung cấp MAP khác, như

  • Google Map SDK
  • Open street map
  • …

Về mặt dữ liệu (location), thì may mắn hơn. Khi toàn bộ thế giới đã thống nhất sử dụng chung một kiểu và loại dữ liệu cho Location.

Với vị trí của một địa điểm nào đó trên Apple Map, thì bạn cũng có thể dùng dữ liệu đó cho Google Map … và ngược lại.

2. Làm việc với MapView

Sau đây, mình sẽ trình bày các thao tác cơ bản nhất và cần có khi sử dụng MapView. Trước tiên thì bạn cần tạo 1 project mới.

  • ViewController
  • Mapview
  • Location Manager cho Model

import MapKit

Để muốn sử dụng MapView và các đối tượng khác liên quan tới. Bạn cần phải khai báo thêm việc import MapKit.

2.1. Display MapView

Tại file ViewController, tạo một Outlet tên là mapView. Để trỏ tới control MKMapView. Xong thì bạn hãy build ứng dụng và xem thử.

Nếu bạn không có can thiệp gì thêm,  MapView sẽ tự động nhận diện vị trí của bạn thông qua internet. Sẽ hiển thị đúng đất nước của bạn. Tuy nhiên, muốn hay hơn thì mình cần hiển thị MapView đúng vùng mà mình mong muốn. Tiếp tục, thêm đoạn code sau vào:

override func viewDidLoad() {
        super.viewDidLoad()
        
        // This is coordinate of Eiffel Tower of Pari city.
        let eiffelTowerLocation = CLLocation(latitude: 48.858042, longitude:  2.294793)
        let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
        let region = MKCoordinateRegion(center: eiffelTowerLocation.coordinate, span: span)
        
        mapView.region = region
    }

Giải thích:

  • region là khu vực nhìn thấy trên map và được chỉ định
  • span chỉ định khoảng cách chiều ngang và chiều dọc vùng lãnh thổ hiển thị trên map
    • MKCoordinateSpan có hai thuộc tính latitudeDelta, longitudeDelta, đều được tính bằng đơn vị độ, phút, giây. Một độ của vĩ độ tương đương với 111 kilomet. Tuy nhiên một độ của kinh độ thì khác nhau.

Build và xem kết quả

Map Kit cung cấp 3 hệ toạ độ để xác định điểm trên bản đồ:

  • Map coordinate: là cách cơ bản để xác định địa điểm trên trái đất, biểu diễn kinh độ và vĩ độ trên trái đất. Sử dụng:
    • CLLocationCoordinate2D xác định toạ độ.
    • MKCoordinateSpan, MKCoordinateRegion xác định khu vực.
  • Map point: là giá trị của x và y trên bản đồ được chiếu theo phép chiếu Mecartor.
    • Trong app, nên sử dụng hệ toạ độ khi xác định vị trí và kích cỡ của lớp overlay.
    • MKMapPoint xác định toạ độ
    • MKMapSize, MKMapRect xác định khu vực.
  • Point: là đơn vị đồ hoạ liên quan đến hệ toạ độ của một đối tượng view.
    • Map point và Map coordinate phải được chuyển đổi sang
    • Point trước khi vẽ lên view.
    • CGPoint xác định toạ độ.
    • CGSize, CGRect xác định khu vực.

Kiểu hiển thị cho map

Bạn có thể tuỳ chỉ các kiểu hiển thị trên map, với các chế độ tiêu chuẩn và vệ tinh. Sử dụng thuộc tính .mapType.

Trong đó:

  • standard
    • Kiểu tiêu chuẩn, hay thấy trên các bản đồ.
    • Hiển thị tất cả các con đường và tên của nó
  • satellite
    • Hiển thị theo ảnh vệ tinh cho khu vực
  • hybrid
    • Loại trung gian hỗn hợp
    • Ảnh vệ tinh và các con đường/tên của nó lên trên ảnh vệ tinh
  • satelliteFlyover
    • Ảnh vệ tính nếu có
    • Khó phân biệt với cái satellite trên
  • hybridFlyover
    • Loại hỗn hợp nếu có
  • mutedStandard
    • Nỗi bật các chi tiết trên map

Ví dụ 1 kiểu hybridFlyover

2.2. Center Map

Công việc tiếp theo, cần thao tác là di chuyển vị trí chính giữa của MapView tới 1 vị trí khác. Thêm function này vào

func center(location: CLLocation) {
        mapView.setCenter(location.coordinate, animated: true)
    }

Đối số truyền vào chính là toạ độ của bạn muốn zoom tới. Trong bài mình sử dụng vị trí hiện tại của người dùng. Khi nhấn vào button thì MapView sẽ di chuyền về đúng vị trí đó.

  • Việc lấy current Location: thì đã trình bày ở bài viết Core Location
  • Sử dụng class LocationManager để lấy.
@IBAction func movetoCurrentLocaltion(_ sender: Any) {
        LocationManager.shared().getCurrentLocation { (location) in
            self.center(location: location)
        }
    }

Kết quả như sau:

  • Trong hình đã tăng span từ 0.01  lên 0.5

Đà Nẵng thân yêu!

2.3. Zoom Map

Tuy nhiên, bạn muốn xem rõ chi tiết, thì phải zoom map thêm. Tiếp tục bạn edit đoạn code sau:

func center(location: CLLocation) {
        //center
        mapView.setCenter(location.coordinate, animated: true)
        
        //zoom
        let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
        let region = MKCoordinateRegion(center: location.coordinate, span: span)
        mapView.setRegion(region, animated: true)
    }

Việc zoom map thì sử dụng function setRegion(animated:) . Với:

  • region với chính toạ độ của center
  • span với giá trị nhỏ cho latitude delta và longitude delta
  • Giá trị span càng nhỏ, thì bản đồ zoom càng chi tiết nhất
  • animated để thêm hiệu ứng cho bản đồ khi zoom

Build và cảm nhận

So với hình ở trên thì đã thấy được cầu sông Hàn của Đà Nẵng thân yêu!

2.3. Show current location

Thêm một thuộc tính vui của MapView

mapView.showsUserLocation = true

Cái chấm tròn tròn đó chính là biểu thị vị trí hiện tại của người dùng.

3. Làm việc với các Annotation View

Đánh dấu toạ độ của một địa điểm nào đó trên bản đồ, là một trong những công việc bạn phải làm được khi sử dụng MapView. Ví dụ: hiển thị các địa điểm như ATM, nhà hàng, khách sạn, quán nhậu, bệnh viện … hoặc chú thích trên bản đồ …

Với mỗi loại map thì có một cái tên khác nhau. Apple Map được gọi là Annotation View.

3.1. Add AnnotationView

Các đối tượng ta cần sử dụng:

  • MKAnnotation : cung cấp thông tin, toạ độ của vị trí cần hiển thị trên map. (là phần data)
  • MKAnnotationView : là View được add lên MapView (là phần view của Annotaion)

Trình tự thao tác như sau:

  • Bước 1: Khởi tạo đối tượng annotation thích hợp.
    • Sử dụng MKPointAnnotation để tạo một annotation đơn giản.
    • Giá trị của thuộc tính title và subtitle sẽ được hiển thị trong callout bubble của annotation.
    • Sử dụng đối tượng của một class nhận protocol MKAnnotation
  • Bước 2: Khởi tạo đối tượng annotation view để hiển thị dữ liệu của annotation trên map:
    • Sử dụng MKPinAnnotationView.
    • Sử dụng MKAnnotationView hoặc subclass của nó.
  • Bước 3: Implement phương thức mapView:viewForAnnotation trong MKMapViewDelegate.
  • Bước 4: Thêm đối tượng annotation vào map.
    • Bằng cách gọi phương thức addAnnotation: hoặc addAnnotations:

3.1.1. Thêm 1 Annotation View

Viết thêm 1 function để add thêm 1 Annotation View cho MapView.

  • MKPointAnnotation : để cung cấp thông tin và toạ độ
  • mapView.addAnnotation(annotation) để thêm vào MapView
func addAnnotation() {
        let annotation = MKPointAnnotation()
        annotation.coordinate = CLLocationCoordinate2D(latitude: 16.071763, longitude: 108.223963)
        annotation.title = "Point 0001"
        annotation.subtitle = "subtitle 0001"
        
        mapView.addAnnotation(annotation)
    }

Thực thi ứng dụng thì ta thấy kết quả như sau:

Vì annotation thuộc kiểu MKPointAnnotation, được hiển thị mặc định theo kiểu MKMarkerAnnotationView như hình trên. Tuy nhiên, nếu bạn muốn custom lại, thì phải thêm phần delegate của MapView vào.

Tiếp tục thêm extension, để implement các function của MKMapViewDelegate.

  • viewFor annotation : là function sẽ duyệt qua các đối tượng annotation được thêm vào MapView. Chúng lại yêu cầu bạn cung cấp cho nó 1 View của Annotation
  • Tạo một view với kiểu MKPinAnnotationView
  • .animatesDrop tạo hiệu ứng rơi xuống cho pin
  • .pinTintColor đổi màu pin
extension ViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        let pin = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin")
        pin.animatesDrop = true
        pin.pinTintColor = .red
        
        return pin
    }
}

Và xét thêm mapView.delegate = self. Build và cảm nhận kết quả

Bạn cần phải phân biết được Annotation View thì cũng có nhiều loại. Với 2 hình trên là kiểu khác nhau:

  • marker là MKMarkerAnnotationView
  • pin là MKPinAnnotationView
  • Còn nhiều loại khác nữa hoặc custom lại chúng nó.

3.1.2. Thêm nhiều Annotation View

Với cách thêm 1 annotation như trên, thì chúng ta sẽ khó khăn trong trường hợp muốn thêm danh sách các địa điểm vào bản đồ. Như vậy thì chúng ta phải tiến hành custom lại chúng nó.

Đầu tiên tạo một file mới, đặt tên là MyPin.swift

import Foundation
import MapKit

class MyPin: NSObject, MKAnnotation {
    let title: String?
    let locationName: String
    let coordinate: CLLocationCoordinate2D
    
    init(title: String, locationName: String, coordinate: CLLocationCoordinate2D) {
        self.title = title
        self.locationName = locationName
        self.coordinate = coordinate
        
        super.init()
    }
    
    var subtitle: String? {
        return locationName
    }
}

Trong đó:

  • Phải kế thừa lại protocol MKAnnotation
  • Implement các thuộc tính cần thiết
    • title
    • subtitle
    • coordinate

Quay về ViewController, thêm dữ liệu cho nhiều pin.

let pins: [MyPin] = [
MyPin(title: "Point 0001", locationName: "Point 0001", coordinate: CLLocationCoordinate2D(latitude: 16.071763, longitude: 108.223963)),                        
MyPin(title: "Point 0002", locationName: "Point 0002", coordinate: CLLocationCoordinate2D(latitude: 16.074443, longitude: 108.224443)),                             
MyPin(title: "Point 0003", locationName: "Point 0003", coordinate: CLLocationCoordinate2D(latitude: 16.073969, longitude: 108.228798)),                             
MyPin(title: "Point 0004", locationName: "Point 0004", coordinate: CLLocationCoordinate2D(latitude: 16.069783, longitude: 108.225086)),                             
MyPin(title: "Point 0005", locationName: "Point 0005", coordinate: CLLocationCoordinate2D(latitude: 16.070629, longitude: 108.228563))
]

Và add vào MapView với function mapView.addAnnotations(pins)

Kết quả như sau:

3.1.3. Tuỳ chỉnh

Chúng ta sẽ bắt đầu tuỳ chỉnh ở function delegate của MKMapViewDelegate. Edit đoạn code của function viewFor Annotation

extension ViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        guard let annotation = annotation as? MyPin else { return nil }
        
        let identifier = "mypin"
        var view: MKPinAnnotationView
        
        if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView {
            dequeuedView.annotation = annotation
            view = dequeuedView
            
        } else {
            view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
            view.pinTintColor = .green
            view.animatesDrop = true
            view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
        }
        
        return view
    }
}

Bạn thấy nó quen thuộc không?

Đó chính là các datasoucre của TableView. Nên khi bạn đã học xong UITableView thì các control khác của iOS sẽ giống về cách xử lý datasource.

Nếu bạn muốn tạo ra cá tính riêng cho mình thì tiếp tục với việc thay đổi image của Annotation View. Vì dù là MKMarkerAnntationView hay MKPinAnnotationView hay bất kì class khác … thì cũng là của hệ thông cung cấp.

Và muốn thay đổi image cho Annotation View thì bạn lại phải edit một chút của delegate MKMapViewDelegate.

extension ViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        guard let annotation = annotation as? MyPin else { return nil }
        
        let identifier = "mypin"
        var view: MKAnnotationView
        
        if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) {
            dequeuedView.annotation = annotation
            view = dequeuedView
            
        } else {
            view = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
            view.image = UIImage(named: "pin")
            view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
        }
        
        return view
    }
}

Trong này, ta sử dụng về class gốc là MKAnnotationView. Và thay đổi thuộc tính image của nó bằng một UIImage riêng của mình.

Build và xem kết quả

Custom MKAnnotationView

Để ngầu hơn thì bạn có thể custom lại class MKAnnotationView. Khi đó bạn tuỳ ý trong việc quản lý giao diện của Annotation View.

Tạo một file mới với tên là MyPinView.swift. Edit theo đoạn code sau:

import Foundation
import MapKit

class MyPinView: MKPinAnnotationView {
    private var imageView: UIImageView!
    
    override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

        self.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
        self.imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
        self.imageView.image = UIImage(named: "no_image")
        self.addSubview(self.imageView)

        self.imageView.layer.cornerRadius = 5.0
        self.imageView.layer.masksToBounds = true
    }

    override var image: UIImage? {
        get {
            return self.imageView.image
        }

        set {
            if let _ = imageView {
                self.imageView.image = newValue
            }
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Giải thích:

  • Kế thừa lại MKAnnotationView
  • Thêm 1 UIImageView với kích thước là 50 x 50 pixel
  • Mỗi lần thay đổi dữ liệu của biến image, thì cập nhật lại image của Image View

Tiếp tục bạn edit lại function của delegate MKMapViewDelegate, để load đúng MyPinView

extension ViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        guard let annotation = annotation as? MyPin else { return nil }
        
        let identifier = "mypin"
        var view: MyPinView
        
        if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MyPinView {
            dequeuedView.annotation = annotation
            view = dequeuedView
            
        } else {
            view = MyPinView(annotation: annotation, reuseIdentifier: identifier)
            view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
        }
        
        return view
    }
}

Kết quả như sau:

Mục đích sau này nếu bạn gọi API để lấy các địa điểm. Khi đó bạn sẽ phải download image của các địa điểm và hiển thị chúng lên bản đồ. Vì việc download là bất đồng bộ, nên lúc chưa có ảnh thì cài đặt một image mặc định cho Annotation View.

3.2. Callout

Nếu bạn để ý tại function view for annotation, trong delegate của MKMapViewDelegate dòng code này

view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

Nhưng không thấy nó hiển thị gì trong ứng dụng. Bạn có thể thấy chữ callout trong tên function trên. Ta tìm hiểu tiếp.

3.2.1. Hiển thị Callout

Thêm dòng code này vào trong function view for annotation.

view.canShowCallout = true

Build và cảm nhận kết quả. Tất nhiên bạn phải chạm vào cái pin đó.

Callout của một Annotation View là một view thuộc MapView. Nó hiển thị các thông tin cần thiết để mô tả cho Annotation View đó là gì.

Bạn có thể thêm nhiều thứ vào cho callout chứ không chỉ đơn giản là 2 label và 1 cái nút trên. Cái đó được gọi là các accessory view. Ta có:

  • title
  • sub-title
  • left
  • right

Giờ thì bạn hiểu tại sao lúc sub-class MKAnnotation, thì bắt buộc phải có title & sub-title rồi chứ.

Giờ tiếp tục thêm ảnh cho callout, bằng cách xét thuộc tính leftCalloutAccessoryView.

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        guard let annotation = annotation as? MyPin else { return nil }
        
        let identifier = "mypin"
        var view: MyPinView
        
        if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MyPinView {
            dequeuedView.annotation = annotation
            view = dequeuedView
            
        } else {
            view = MyPinView(annotation: annotation, reuseIdentifier: identifier)
            view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
            view.leftCalloutAccessoryView = UIImageView(image: UIImage(named: "pin"))
            view.canShowCallout = true
        }
        
        return view
    }

Edit lại cho đầy đủ phụ kiện của callout. Build và cảm nhận kết quả tiếp

3.2.1. Action

Sang phần cuối của việc tương tác với Annotation View. Đó là việc bắt các sự kiện do người dùng tác động lên. Ta cần bắt 2 loại sự kiện sau đây:

  • Người dùng tác động vào Annotation View
  • Người dùng tác động vào View trong callout của Annotation View

Để thực hiện, ta thêm các function tiếp theo vào phần extension của delegate MKMapViewDelegate.

    func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
        print("selected callout")
    }
    
    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
        print("selected pin")
    }

Tuy nhiên, 2 function này cũng không hưu ích, khi bạn muốn bắt riêng sự kiện cho việc tap vào button bên phải. Giải quyết vấn đề này thì ta tiếp tục thêm target cho button đó.

extension ViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        guard let annotation = annotation as? MyPin else { return nil }
        
        let identifier = "mypin"
        var view: MyPinView
        
        if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MyPinView {
            dequeuedView.annotation = annotation
            view = dequeuedView
            
        } else {
            view = MyPinView(annotation: annotation, reuseIdentifier: identifier)
            let button = UIButton(type: .detailDisclosure)
            button.addTarget(self, action: #selector(selectPinView(_:)), for: .touchDown)
            view.rightCalloutAccessoryView = button
            view.leftCalloutAccessoryView = UIImageView(image: UIImage(named: "pin"))
            view.canShowCallout = true
        }
        
        return view
    }
    
    @objc func selectPinView(_ sender: UIButton?) {
        print("select button detail")
    }
    
    func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
        print("selected callout")
    }
    
    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
        print("selected pin")
    }
}

3.3. Tóm tắt

Tới đây thì cũng tạm ổn cho phần Annotation View rồi. Còn giờ tóm tắt lại một chút trước khi tới phần tiếp theo:

  • MapView có một hoặc nhiều Annotation View
    • Class đại diện là MKAnnotationView
  • Mỗi Annotation View thì có phần dữ liệu cho nó là Annotation
    • Class đại diện là MKAnnotation
  • Mỗi Annotation View thì có thêm một phần view chú thích mô tả thêm là callout
  • Bạn có thể custom được
    • Annotation
    • Annotation View

4. Overlay MapView

4.1. Nguyên tắc chung

Định nghĩa về Overlay cho Map

Sử dụng overlay để thêm một lớp nội dung lên trên map. Một đối tượng overlay cần nhận protocol MKOverlay,

  • Chứa thông tin của các điểm
  • Xác định hình dạng
  • Kích thước và vị trí của ovelay trên map.

Overlay có thể hiển thị các hình dạng như: hình tròn, hình chữ nhật, đa giác,… hoặc overlay tự custom.

  • iOS 7 trở về sau, MKOverlayRenderer giữ nhiệm vụ hiển thị lớp overlay.
  • Trước iOS 7, MKOverlayView giữ nhiệm vụ này.

Nguyên tắc thêm các overlay vào Map View

  • Bước 1: Khởi tạo đối tượng overlay data thích hợp.
    • Sử dụng MKCircle, MKPolygon, MKPolyline.
    • Sử dụng MKTileOverlay.
    • Subclass của MKShape hoặc MKMultiPoint.
    • Class nhận MKOverlay protocol.
  • Bước 2: Khởi tạo đối tượng overlay render để hiển thị lớp overlay lên màn hình:
    • Sử dụng MKCircleRenderer, MKPolygonRenderer, MKPolylineRenderer.
    • Sử dụng MKTileOverlayRenderer.
    • Sử dụng subclass của MKOverlayPathRenderer.
    • Sử dụng subclass của MKOverlayRenderer.
  • Bước 3: Implement phương thức mapView:rendererForOverlay:.
  • Bước 4: Thêm đối tượng overlay data vào map.
    • Bằng cách gọi phương thức addOverlay:

4.2. Demo code

Giờ bắt đầu demo đơn giản bằng việc vẽ các hình tròn lên MapView. Mở file ViewController, tiến hành thêm function sau:

func addOverlayData() {
        let coordinates = [
            CLLocationCoordinate2D(latitude: 16.071763, longitude: 108.223963),
            CLLocationCoordinate2D(latitude: 16.074443, longitude: 108.224443),
            CLLocationCoordinate2D(latitude: 16.073969, longitude: 108.228798),
            CLLocationCoordinate2D(latitude: 16.069783, longitude: 108.225086),
            CLLocationCoordinate2D(latitude: 16.070629, longitude: 108.228563)
        ]
        
        for center in coordinates {
            let radius = 100.0 //Distance unit: meters
            let overlay = MKCircle(center: center, radius: radius)
            
            //add circle
            mapView.addOverlay(overlay)
        }
    }

Giải thích:

  • Vị trí để thêm các overlay thì được khai báo trong array coordinates.
  • Duyệt qua từng phần tử của array
  • Tạo 1 đối tượng overlay với kiểu MKCircle
    • center: toạ độ
    • radius: bán kính với hệ đơn vị là meter
  • Dùng function addOverlay của MapView để thêm các đối tượng overlay vào Map

Xong phần data cho overlay. Cũng tương tự như Annotation & Annotation View, thì chúng ta cần phải tạo các View của overlay, đó chính là các renderer. Tiếp tục trong phần extension của delegate MKMapViewDelegate, thêm function sau:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        guard let circle = overlay as? MKCircle else {
            return MKOverlayRenderer()
        }
        
        let circleRenderer = MKCircleRenderer(circle: circle)
        circleRenderer.fillColor = UIColor(red: 0, green: 0, blue: 1, alpha: 0.5)
        circleRenderer.strokeColor = .blue
        circleRenderer.lineWidth = 1
        circleRenderer.lineDashPhase = 10
        return circleRenderer
    }

Bạn lại bắt gặp hình bóng của delegate & datasouce của UITableView ở đây. Nó cũng tương tự như TableView hay Annotation View. Trong đó:

  • Bạn cần lấy được phần data, đó là biến circle
  • Xác định đúng kiểu của nó là MKCircle
  • Tạo 1 đối tượng MKCircleRenderer
  • Xét các giá trị của các thuộc tính đối tượng renderer

Build và cảm nhận kết quả:

Bạn tự tìm hiểu thêm các kiểu overlay khác (như hình chữ nhật, đa giác, vẽ đường, ảnh …) hoặc tự tay custom một overlay của riêng mình. Trong phạm vi bài này, mình sẽ không trình bày phần custom overlay & renderer.

5. Hiển thị nhiều loại Annotation View

Đôi khi bạn nhận được yêu cầu, cần phải hiểu thị trên MapView nhiều loại Annotation View khác nhau. Nhằm phân biệt các loại địa điểm (như: atm với nhà hàng …) Khi đó, bạn cần phải hiển thị chúng nó trên MapView. Và đây cũng là một thao tác cần phải thực hiện được.

Đầy tiên, mình giả định yêu cầu cho bài tập này bao gồm:

  • Vẽ các overlay hình tròn
  • Pin trung tâm của các hình tròn đó lên MapView
    • Sử dụng MKPinAnnotationView
  • Thêm Annotation View cho vị trí hiện tại của người dùng.
    • Phải custom Annotation View

Đầu tiên, cần viết thêm 1 function để add 1 pin đơn giản với kiểu MKPointAnnotation.

func addPin(coordinate: CLLocationCoordinate2D) {
        let annotation = MKPointAnnotation()
        annotation.coordinate = coordinate
        mapView.addAnnotation(annotation)
    }

Cập nhật lại function addOverlayData, để mỗi lần thêm overlay thì mình thêm tiếp 1 pin

func addOverlayData() {
        let coordinates = [
            CLLocationCoordinate2D(latitude: 16.071763, longitude: 108.223963),
            CLLocationCoordinate2D(latitude: 16.074443, longitude: 108.224443),
            CLLocationCoordinate2D(latitude: 16.073969, longitude: 108.228798),
            CLLocationCoordinate2D(latitude: 16.069783, longitude: 108.225086),
            CLLocationCoordinate2D(latitude: 16.070629, longitude: 108.228563)
        ]
        
        for center in coordinates {
            let radius = 100.0 //Distance unit: meters
            let overlay = MKCircle(center: center, radius: radius)
            
            //add circle
            mapView.addOverlay(overlay)
            
            //add pin
            addPin(coordinate: center)
        }
    }

Thêm Annotation View cho vị trí hiện tại của người dùng. Sử dụng class model LocationManager.

LocationManager.shared().getCurrentLocation { (location) in
            let myPin = MyPin(title: "Me", locationName: "I am here.", coordinate: location.coordinate)
            self.mapView.addAnnotation(myPin)
        }

Cập nhật lại function viewFor Annotation trong delegate của MKMapViewDelegate.

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        
        if let pin = annotation as? MKPointAnnotation {
            let identifier = "pin"
            var view: MKPinAnnotationView
            
            if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView {
                dequeuedView.annotation = annotation
                view = dequeuedView
                
            } else {
                view = MKPinAnnotationView(annotation: pin, reuseIdentifier: identifier)
                view.animatesDrop = true
                view.pinTintColor = .green
            }
            
            return view
            
        } else if let annotation = annotation as? MyPin {
            let identifier = "mypin"
            var view: MyPinView
            
            if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MyPinView {
                dequeuedView.annotation = annotation
                view = dequeuedView
                
            } else {
                view = MyPinView(annotation: annotation, reuseIdentifier: identifier)
                let button = UIButton(type: .detailDisclosure)
                button.addTarget(self, action: #selector(selectPinView(_:)), for: .touchDown)
                view.rightCalloutAccessoryView = button
                view.leftCalloutAccessoryView = UIImageView(image: UIImage(named: "pin"))
                view.canShowCallout = true
            }
            
            return view
        } else {
            return nil
        }
    }

Giải thích:

  • Để phân biệt các loại Annotation View khác nhau. Thì chúng ta phải phân biệt được các class Annotation khác nhau
  • Sử dụng việc ép kiểu if let với as?. Nhằm xác định class kiểu dữ liệu của mỗi annotation
  • Sau đó tuy thuộc vào từng kiểu Annotation thì sẽ tạo các Annotation View tương ứng.
    • MKPointAnnotation thì tạo MKPinAnnotationView
    • MyPin thì tạo MyPinView

Build và cảm nhận kết quả

Trong phần này, chỉ cần bạn phân biệt được kiểu hay tên class của mỗi Annotation thì mọi thứ sẽ được giải quyết.

6. Routing

Đây là tính năng ưa dùng nhất dành cho các thanh niên trong thời đại 4.0.

Thật là khó chịu khi ứng dụng của bạn có làm việc mới MapView mà lại không thể tìm đường đi giữa 2 điểm trên bản đồ. Nếu bắt gặp vấn đề này thì 100% người dùng sẽ lên Google Map để search và tìm đường đi. Nhưng đó là dành cho người dùng, còn người lập trình muốn sử dụng được tính năng đó thì phải có sự trả giá. Khi …

Giờ đây Google Map hay Google API đã tăng cường chính sách hút máu triệt để các lập trình viên.

Lập trình viên phải trả 1 ít tiền để sử dụng được các API của Google phục vụ cho việc tìm đường đi. Nếu bạn tìm đến các cứu cánh khác, các dịch vụ location hay map khác thì cũng bị chặt chém không kém. Nhưng …

Chúng ta là iOS developer, nền tảng chúng ta sử dụng đã rất tốt và đầy đủ rồi.

Và việc tìm đường đi giữa 2 điểm cũng được Apple hỗ trợ. Nó được tích hợp vào các framework MapKit và CoreLocation. Công việc chúng ta bây giờ là bóc tách và sử dụng chúng.

Quay về chuyện chính khi bạn muốn tìm đường đi ngắn nhất giữa hai điểm trên bản đồ, thì công việc của bạn chia ra làm 2 phần:

  • Tìm tập hợp dữ liệu các toạ độ giữa 2 điểm đó
  • Vẽ đường đi

6.1. Tìm tập hợp các toạ độ

Thêm function này vào code của bạn

func routing(source: CLLocationCoordinate2D, destination: CLLocationCoordinate2D) {
        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: source, addressDictionary: nil))
        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destination, addressDictionary: nil))
        request.requestsAlternateRoutes = true
        request.transportType = .automobile

        let directions = MKDirections(request: request)

        directions.calculate { [unowned self] response, error in
            guard let unwrappedResponse = response else { return }

            for route in unwrappedResponse.routes {
                self.mapView.addOverlay(route.polyline)
                self.mapView.setVisibleMapRect(route.polyline.boundingMapRect, animated: true)
            }
        }
    }

Để tìm dữ liệu cho đường đi, thì bạn sử dụng đối tượng MKDirections. Vì việc tìm đường đi cũng cần phải request, ở đây mình cũng không rõ là nó sẽ request tới đâu, nhưng chắc là service của Apple bảo kê.

Để request được dữ liệu thì bạn lại cần tạo đối tượng MKDirections.Request. Đối tượng này cần các dữ liệu sau:

  • source : điểm bắt đầu
  • destination : điểm kết thúc
  • requestsAlternateRoutes : cho biết là tìm một hay nhiều con đường
  • transportType : kiểu di chuyển, có thể là đi bộ, xe máy, ô tô và tàu.

Sau khi gọi MKDirections(request: request), thì sẽ có dữ liệu trả về cho đối tượng directions. Sau đó, sử dụng function directions.calculate để phân tích completion trả về.

Dữ liệu trả về thông quan routes, đó là từng đoạn đường đi giữa 2 địa điểm. Nó là array các MKRoute, mỗi route sẽ chưa thông tin của một con đường đi giữa 2 điểm đó.

Gọi hàm thực thi và truyền dữ liệu cho 2 điểm cần tìm đường đi.

  • Để xác định vị trí 2 điểm thì mình pin chúng nó lên bản đồ
  • Thông tin 2 điểm thì bạn xem trong code dưới
//routing
        let source = CLLocationCoordinate2D(latitude: 16.071668, longitude: 108.230178)
        addPin(coordinate: source, title: "Vincom", subTitle: "Da Nang, Viet Nam")
        
        let destination = CLLocationCoordinate2D(latitude: 16.080838, longitude: 108.238573)
        addPin(coordinate: destination, title: "Asian Tech", subTitle: "Da Nang, Viet Nam")
        
        routing(source: source, destination: destination)

Ta có thể xem dữ liệu của việc tìm đường như sau:

Có 1 con đường tìm được, với 6 step di chuyển.

6.2. Vẽ đường đi

Tiếp theo là vẽ đường đi. Bạn cũng đã thấy thì tại response dữ liệu tìm đường đi trả về. Ta thêm vào MapView các overlay là MKPolyline. Giờ chúng ta lại edit lại function renderer các overlay của delegate MKMapViewDelegate.

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        
        if let polyline = overlay as? MKPolyline {
            let renderer = MKPolylineRenderer(polyline: polyline)
            renderer.strokeColor = UIColor.blue
            renderer.lineWidth = 3
            return renderer
            
        } else if let circle = overlay as? MKCircle {
            let circleRenderer = MKCircleRenderer(circle: circle)
            circleRenderer.fillColor = UIColor(red: 0, green: 0, blue: 1, alpha: 0.5)
            circleRenderer.strokeColor = .blue
            circleRenderer.lineWidth = 1
            circleRenderer.lineDashPhase = 10
            return circleRenderer
            
        } else {
            return MKOverlayRenderer()
        }
        
    }

Do phải phân biệt giữa các overlay nên cần sử dụng if let ép kiểu các đội tượng overlay đúng với các kiểu dữ liệu mình thêm vào MapView.

Sau khi đã xác định MKPolyline, thì tới việc tạo renderer cho nó. Công với việc tiếp theo là xét các thuộc tính như màu và độ dày đường đi.

Cuối cùng là return nó về cho MapView thân thương. Bạn build và cảm nhận kết quả.

Cung đường đi làm thân thương!

OKAY, bây giờ thì bạn đã quá đủ skill để tiêu diệt MapView và làm việc với MapKit của Apple. Bạn có thể checkout mã nguồn tại đây. Chúc bạn thành công!

Tạm kết

  • Hiển thị MapView và tuỳ chỉnh hiển thị
  • Move, Center, Zoom map
  • Annotation & Annotation View
  • Show Callout
  • Handle actions trên MapView
  • Overlay Map
  • Routing
  • Custom Annotation & Annotation View

Cảm ơn bạn đã đọc bài viết này!

 

FacebookTweetPinYummlyLinkedInPrintEmailShares11

Related Posts:

  • feature_bg_3
    Clean Architecture trong iOS
Tags: basic ios tutorial, iOS, mapview
Written by chuotfx

Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Donate – Buy me a coffee!

Fan page

Fx Studio

Tags

Actor Advanced Swift AI api AppDistribution autolayout basic ios tutorial blog ci/cd closure collectionview combine concurrency crashlytics dart dart basic dart tour Declarative delegate deploy design pattern fabric fastlane firebase flavor flutter GCD gradients iOS MVVM optional Prompt engineering protocol Python rxswift safearea Swift Swift 5.5 SwiftData SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • Role-playing vs. Persona-based Prompting
  • [Swift 6.2] Raw Identifiers – Đặt tên hàm có dấu cách, tại sao không?
  • Vibe Coding là gì?
  • Cách Đọc Sách Lập Trình Nhanh và Hiệu Quả Bằng GEN AI
  • Nỗ Lực – Hành Trình Kiến Tạo Ý Nghĩa Cuộc Sống
  • Ai Sẽ Là Người Fix Bug Khi AI Thống Trị Lập Trình?
  • Thời Đại Của “Dev Tay To” Đã Qua Chưa?
  • Prompt Engineering – Con Đường Để Trở Thành Một Nghề Nghiệp
  • Vấn đề Ảo Giác (hallucination) khi tương tác với Gen AI và cách khắc phục nó qua Prompt
  • Điều Gì Xảy Ra Nếu… Những Người Dệt Mã Trở Thành Những Người Bảo Vệ Cuối Cùng Của Sự Sáng Tạo?

You may also like:

  • Clean Architecture trong iOS
    feature_bg_3

Archives

  • May 2025 (2)
  • April 2025 (1)
  • March 2025 (8)
  • January 2025 (7)
  • December 2024 (4)
  • September 2024 (1)
  • July 2024 (1)
  • June 2024 (1)
  • May 2024 (4)
  • April 2024 (2)
  • March 2024 (5)
  • January 2024 (4)
  • February 2023 (1)
  • January 2023 (2)
  • November 2022 (2)
  • October 2022 (1)
  • September 2022 (5)
  • August 2022 (6)
  • July 2022 (7)
  • June 2022 (8)
  • May 2022 (5)
  • April 2022 (1)
  • March 2022 (3)
  • February 2022 (5)
  • January 2022 (4)
  • December 2021 (6)
  • November 2021 (8)
  • October 2021 (8)
  • September 2021 (8)
  • August 2021 (8)
  • July 2021 (9)
  • June 2021 (8)
  • May 2021 (7)
  • April 2021 (11)
  • March 2021 (12)
  • February 2021 (3)
  • January 2021 (3)
  • December 2020 (3)
  • November 2020 (9)
  • October 2020 (7)
  • September 2020 (17)
  • August 2020 (1)
  • July 2020 (3)
  • June 2020 (1)
  • May 2020 (2)
  • April 2020 (3)
  • March 2020 (20)
  • February 2020 (5)
  • January 2020 (2)
  • December 2019 (12)
  • November 2019 (12)
  • October 2019 (19)
  • September 2019 (17)
  • August 2019 (10)

About me

Education, Mini Game, Digital Art & Life of coders
Contacts:
contacts@fxstudio.dev

Fx Studio

  • Home
  • About me
  • Contact us
  • Mail
  • Privacy Policy
  • Donate
  • Sitemap

Categories

  • Art (1)
  • Blog (44)
  • Code (11)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (102)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (87)

Newsletter

Stay up to date with our latest news and posts.
Loading

    Copyright © 2025 Fx Studio - All rights reserved.