RxSwiftを使ってMapKitで現在地から場所をサジェストするauto completeを実装するサンプル


現在地付近のの施設を検索できるサンプルになります

MapKitを使用して現在地を取得しながら、UISearchBarに入力した結果をtableViewに返すサンプルになります。
実際に使用する際は、現在地の取得の許可を得るために
info.plistに

Privacy - Location When In Use Usage Description
Privacy - Location Always and When In Use Usage Description

をかいてください。

現在地の取得が、

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])

常にアップデートされるため、

**メートル以内の変更は通知しないよう以下の関数を定義してあります。

func distinctUntilChangeGreaterThan(meters: CLLocationDistance) -> Observable<CLLocation>

渡ってきたCLLocationを
.distinctUntilChangeGreaterThan(meters: CLLocationDistance(1000))
1000メートル以内の変化は通知しないように。


    var myLocationRelay = BehaviorRelay<CLLocation?>(value: nil)

        myLocationRelay
            .compactMap { $0 }
            .distinctUntilChangeGreaterThan(meters: CLLocationDistance(1000))
            .subscribe(onNext: { [weak self] coordinate in
                let span = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001)
                let region = MKCoordinateRegion(center: coordinate.coordinate, span: span)
                self?.searchCompleter.region = region
            })
            .disposed(by: bag)

すべてのコード

import CoreLocation
import MapKit
import RxCocoa
import RxSwift
import UIKit

class FindLocationViewController: UIViewController {
    var viewModel: FindLocationViewModel!
    var bag = DisposeBag()

    var searchCompleter = MKLocalSearchCompleter()
    var locationManager: CLLocationManager?
    var searchResults = [MKLocalSearchCompletion]()
    var myLocationRelay = BehaviorRelay<CLLocation?>(value: nil)

    @IBOutlet private weak var searchResultsTable: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!

    override func viewDidLoad() {
        super.viewDidLoad()
        initialSettings()
        viewModel.setUp()
        setBind()
        whereIsMyLocation()
    }

    private func initialSettings() {
        searchBar.delegate = self
        searchResultsTable.delegate = self
        searchResultsTable.dataSource = self
        searchCompleter.delegate = self
        let categories: [MKPointOfInterestCategory] = [.fitnessCenter]
        let filters = MKPointOfInterestFilter(including: categories)
        searchCompleter.pointOfInterestFilter = .some(filters)
    }

    private func setBind() {
        myLocationRelay
            .compactMap { $0 }
            .distinctUntilChangeGreaterThan(meters: CLLocationDistance(1000))
            .subscribe(onNext: { [weak self] coordinate in
                let span = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001)
                let region = MKCoordinateRegion(center: coordinate.coordinate, span: span)
                self?.searchCompleter.region = region
            })
            .disposed(by: bag)
    }

    private func whereIsMyLocation() {
        locationManager = CLLocationManager()
        locationManager?.delegate = self
        locationManager?.requestWhenInUseAuthorization()

        if CLLocationManager.locationServicesEnabled() {
            locationManager!.startUpdatingLocation()
        }
    }
}

extension FindLocationViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let newLocation = locations.last else {
            return
        }

        let location: CLLocationCoordinate2D
            = CLLocationCoordinate2DMake(newLocation.coordinate.latitude, newLocation.coordinate.longitude)
        let formatter = DateFormatter()
        formatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
        let date = formatter.string(from: newLocation.timestamp)

        print("緯度:", location.latitude, "経度:", location.longitude, "時間:", date)
        let ccLoctaion = CLLocation(latitude: location.latitude, longitude: location.longitude)

        myLocationRelay.accept(ccLoctaion)
    }
}

extension FindLocationViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)

        let result = searchResults[indexPath.row]
        let searchRequest = MKLocalSearch.Request(completion: result)

        let search = MKLocalSearch(request: searchRequest)
        search.start { response, _ in
            guard let coordinate = response?.mapItems[0].placemark.coordinate else {
                return
            }
            guard let name = response?.mapItems[0].name else {
                return
            }

            let lat = coordinate.latitude
            let lon = coordinate.longitude

            print(lat)
            print(lon)
            print(name)
            let preVC = self.presentingViewController as! WhichGymViewController
            let gymInfo = GymInfo(coodinate: coordinate, name: name)
            preVC.viewModel.selectedGymRelay.accept(gymInfo)
            self.dismiss(animated: true, completion: nil)
        }
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return searchResults.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let searchResult = searchResults[indexPath.row]
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        cell.textLabel?.text = searchResult.title
        cell.detailTextLabel?.text = searchResult.subtitle

        return cell
    }
}

extension FindLocationViewController: MKLocalSearchCompleterDelegate & UISearchBarDelegate {
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        searchCompleter.queryFragment = searchText
    }

    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        searchResults = completer.results
        searchResultsTable.reloadData()
    }

    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        // Error
    }

    func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
        return true
    }
}

import CoreLocation
import Foundation
import RxSwift

extension CLLocationCoordinate2D: Equatable {}

public func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
    return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}

extension ObservableType where Element == CLLocation {
    func distinctUntilChangeGreaterThan(meters: CLLocationDistance) -> Observable<CLLocation> {
        return scan(CLLocation(), accumulator: { lastLocation, location in
            if lastLocation.distance(from: location) > meters {
                return location
            } else {
                return lastLocation
            }
        }).distinctUntilChanged { lhs, rhs in
            lhs.coordinate == rhs.coordinate
        }
    }
}