Contents
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:
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.
- Class của nó là
- 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 đồ
- Hay có thể gọi bằng nhiều cái tên phổ biến hơn, như:
- 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.
- iOS 7 trở về sau,
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ỉ địnhspan
chỉ định khoảng cách chiều ngang và chiều dọc vùng lãnh thổ hiển thị trên mapMKCoordinateSpan
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ên0.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 centerspan
với giá trị nhỏ cholatitude 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ặcaddAnnotations:
- Bằng cách gọi phương thức
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ó 1View 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
- Class đại diện là
- Mỗi Annotation View thì có phần dữ liệu cho nó là Annotation
- Class đại diện là
MKAnnotation
- Class đại diện là
- 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:
- Bằng cách gọi phương thức
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ếncircle
- 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ớias?
. 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 đầudestination
: điểm kết thúcrequestsAlternateRoutes
: cho biết là tìm một hay nhiều con đườngtransportType
: 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!
Related Posts:
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
Fan page
Tags
Recent Posts
- CO-STAR – Công thức vàng để viết Prompt hiệu quả cho LLM
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
You may also like:
Archives
- 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)