🗺️

【iOS 18】MapKitの検索機能のアップデートまとめ

2024/07/20に公開

iOS 18, macOS 15, visionOS 2でMapKitの検索機能が強化された。そのあたりについてのWWDC24での解説や公式サンプルの実装、ドキュメント(APIリファレンス)の記載内容についてまとめる。

WWDC24のセッションでの解説

アップデート概要

今年、検索結果をフィルタリングする新しい方法を含む、検索APIに改良を加えました。

140

  • 音楽会場、スケート場、お城など、より多くの種類の場所を検索できます。

  • 川や山脈などの物理的な特徴を検索できます。

  • 市や郵便番号など、住所を構成する特定の要素を検索することもできます。

  • 特定の境界領域に限定した検索が可能になったので、重要なエリアだけに焦点を絞ることができます。

  • また、サーバーAPIのために、ページ分割機能を追加しました。

    • これにより、大量の検索結果が得られるようになりました。実際、何ページもある!

実装解説

これらの新しい検索機能は、より効率的に店舗を見つけ、収集するのに役立つと思いますか?もちろんです!一日中ベイエリアで野生のアップルストアを探し回った後では、どんなコレクターでも強いコーヒーが必要だろう!私は、自分のウェブサイトでこれらの新しい検索機能を使い、自分が望むだけカフェインを補給できるよう、前もって計画を立てられるようにするつもりだ。そして、ネイティブアプリにも同じ機能を追加するつもりだ。

143

まず、クパチーノを見つける必要がある。

新しいAddressFiltersでそれができそうだ!

145

ここでは、ほとんどの国の都市である地域のみを検索する AddressFilter を作成します。

そして、そのフィルタを新しい検索オブジェクトに差し込む。

最後に、Cupertino's Classy Cleanersというビジネスが見つかるかもしれないというリスクなしに、探している都市を見つけるために、Cupertinoという言葉で検索を実行する。

149

クパチーノに一致する都市の検索結果は、私が設定したshowMap関数に送られる。

152

その関数の中で、私は最初の検索結果を受け取り、それを使って新しいマップに渡す region を作る。

153

このコードでは、地図がクパチーノの真上にセンタリングされる、

これはまさに私が望んでいたものだ。次に、その地域内で新しい検索を作成し、コーヒーを検索する。

そして最後に、すべてのコーヒー検索結果に新しい PlaceAnnotation を追加する。

157

これですべての結果が地図上にマークされた!しかし、まだちょっと問題がある。

ズームアウトすると、最初のビューポートの外側の地図にたくさんのアノテーションが追加されていることに気づきます。

161

こんな遠くのコーヒーの店は本当にいらない、

というわけで、RegionPriority を使ってそれらを取り除くことができる!

164

この1つのプロパティだけで、検索に渡した地域内に結果があることを要求できる。

この変更後、私のページの結果は、私の元のregionにあるものだけに制限されます。

166

ネイティブ実装

もちろん、これらの機能はすべてネイティブアプリでも利用できる。私のアプリもまったく同じように強化する。

169

ここでは、各コーヒーショップのマーカーを表示するビューを作りました。

非同期に、findCityを使って都市の地域を取得し、findCoffeeを使ってその地域にあるコーヒーショップを見つける。

これが findCity で、

173

JSのコードと同じように、addressFilter で場所を検索する。

そして、これはfindCoffeeで、

177

クパチーノのリージョンを取り、regionPriority を使って、そのリージョン内にあるコーヒーショップを検索している。

Swiftコード全体

セッションページに記載されているコード)

// Finding coffee in Cupertino

struct CoffeeMap: View {
    @State private var position: MapCameraPosition = .automatic
    @State private var coffeeShops: [MKMapItem] = []
    
    var body: some View {
        Map(position: $position) {
            ForEach(coffeeShops, id: \.self) { café in
                Marker(item: cafe)
            }
        }
        .task {
            guard let cupertino = await findCity() else {
                return
            }
            coffeeShops = await findCoffee(in: cupertino)
        }
    }
    
    private func findCity() async -> MKMapItem? {
        let request = MKLocalSearch.Request()
        request.naturalLanguageQuery = "cupertino"
        
        request.addressFilter = MKAddressFilter(
            including: .locality
        )
        
        let search = MKLocalSearch(request: request)
        let response = try? await search.start()
        return response?.mapItems.first
    }
    
    private func findCoffee(in city: MKMapItem ) async -> [MKMapItem] {
        let request = MKLocalSearch.Request()
        request.naturalLanguageQuery = "coffee"
        let downtown = MKCoordinateRegion(
            center: city.placemark.coordinate,
            span: .init(
                latitudeDelta: 0.01,
                longitudeDelta: 0.01
            )
        )
        request.region = downtown
        request.regionPriority = .required
        let search = MKLocalSearch(request: request)
        let response = try? await search.start()
        return response?.mapItems ?? []
    }
}

サンプルアプリ

"Interacting with nearby points of interest" という本セッションの内容に対応するサンプルアプリが公開されている:

https://developer.apple.com/documentation/mapkit/mapkit_for_appkit_and_uikit/interacting_with_nearby_points_of_interest

概要としては次のように書かれている:

This sample code project demonstrates how to programmatically search for map-based addresses and points of interest using a natural language string, and get more information for points of interest that a person selects on the map. The search results center around the locations visible in the map view.
(このサンプル・コード・プロジェクトは、自然言語文字列を使用してマップ・ベースの住所とPOIをプログラムで検索し、マップ上で人が選択したPOIの詳細情報を取得する方法を示します。検索結果は、マップビューに表示されている場所を中心に表示されます。)

セッションで出てきたクパチーノのコーヒーショップを検索する例よりも高機能なサンプルで、かなり多くの新要素が盛り込まれている。

が、実はかなり前から存在するアプリで、iOS 18で追加された新機能だけを解説するものではない。サンプルページ(=サンプルのREADME)では MKLocalSearchCompleter を用いた検索クエリのサジェストやSwiftDataへの保存について解説があるが、それらの多くはiOS 18の話ではない

じゃあどこが新要素なんだ、というのがわかりにくかったので、Xcode 15.4でプロジェクトを開き、iOS Deployment Targetを 17.0 に、Swift Language Versionを Swift 5 にしてどこがビルドエラーになるか見てみた。

以下に列挙する(MapKitに関係のないSwiftUI関連の新APIは除外):

  • Place ID関連
    • MKMapItem.Identifier(rawValue:), MKMapItemidentifier プロパティ
    • MKMapItemRequest(mapItemIdentifier:)
  • Place Card関連
    • MKMapItemDetailViewControllerDelegate
    • MKMapItemAnnotation
    • MKSelectionAccessory
  • 検索関連
    • MKLocalSearchRegionPriority
    • MKAddressFilter
    • MKPointOfInterestCategory の一部カテゴリ
      • .distillery
      • .castle
      • .conventionCenter
      • etc...

Place IDやPlace Cardは本記事では解説していない別の新機能(別記事に書いた)を使ったものだが、iOS 18のサンプルとして使うならこのあたりの実装がある機能を試せばよい。

たとえば MKAddressFilterMKLocalSearchRegionPriority は、

request.regionPriority = mapConfiguration.regionPriority.localSearchRegionPriority
if mapConfiguration.resultType == .pointsOfInterest {
    request.pointOfInterestFilter = mapConfiguration.pointOfInterestOptions.filter
} else if mapConfiguration.resultType == .addresses {
    request.addressFilter = mapConfiguration.addressOptions.filter
}

とか

searchCompleter?.regionPriority = mapConfiguration.regionPriority.localSearchRegionPriority
if mapConfiguration.resultType == .pointsOfInterest {
    searchCompleter?.pointOfInterestFilter = mapConfiguration.pointOfInterestOptions.filter
} else if mapConfiguration.resultType == .addresses {
    searchCompleter?.addressFilter = mapConfiguration.addressOptions.filter
}

という感じで使われているので、サンプル内のSettings画面にある "Serach For" という項目で "Addresses" を選択した上で検索を行うことでiOS 18の新機能を利用できるとわかる。(なお MKPointOfInterestFilter はiOS 13から存在するAPI)

MKAddressFilter で何を実際にフィルターするのか、の実装はこのようになっている:

enum AddressOptions: CaseIterable {
    case anyField
    case includeCityAndPostalCode

    var filter: MKAddressFilter {
        switch self {
        case .anyField:
            return MKAddressFilter.includingAll
        case .includeCityAndPostalCode:
            return MKAddressFilter(including: [.locality, .postalCode])
        }
    }
}

.anyField.includeCityAndPostalCode かは、アプリのSettings画面からはここで選択できるようになっている:

また MKPointOfInterestCategory の新しいカテゴリ群は以下のように travelPointsOfInterest として定義されていて、

static let travelPointsOfInterest: [MKPointOfInterestCategory] = [
    // Places to eat, drink, and be merry.
    .bakery,
    .brewery,
    .cafe,
    .distillery,
    .restaurant,
    .winery,

    // Places to stay.
    .campground,
    .hotel,
    .rvPark,

    // Places to go.
    .beach,
    .castle,
    .conventionCenter,
    .fairground,
    .fortress,
    .nationalMonument,
    .nationalPark,
    .planetarium,
    .spa,
    .zoo
]

これは "Category" という項目で "Travel Categories" を含めたり除外したりの選択ができるようになっている。

ドキュメント(APIリファレンス)

ここまで見てきて、iOS 18の検索機能の強化はおおまかに

  • MKAddressFilter による検索結果のフィルタ
  • MKLocalSearchRegionPriority によるプライオリティ指定
  • MKPointOfInterestCategory の増補

あたりだとわかる。ひとつひとつドキュメントで見ていく。

MKAddressFilter による検索結果のフィルタ

MKAddressFilter のAPIリファレンスはここ:

https://developer.apple.com/documentation/mapkit/mkaddressfilter?changes=latest_minor

An object that filters which address options to include or exclude in search results. (検索結果にどのアドレスオプションを含めるか、または除外するかをフィルタリングするオブジェクト。)

上述のWWDCセッションの解説やサンプルにもあった通り、MKLocalSearchaddressFilter に指定して使う:

let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "cupertino"

request.addressFilter = MKAddressFilter(
    including: .locality
)

あとは「どういうフィルターが指定できるのか」がわかれば良さそう。以下に MKAddressFilter.Options の種類と概要を列挙する。

https://developer.apple.com/documentation/mapkit/mkaddressfilter/options?changes=latest_minor

static var administrativeArea: MKAddressFilter.Options

The primary administrative divisions of countries or regions. (国または地域の主要な行政区分。)

static var country: MKAddressFilter.Options

Countries and regions.

static var locality: MKAddressFilter.Options

Local administrative divisions, postal cities, and populated places. (地方行政区画、郵便都市、人口密集地。)

static var postalCode: MKAddressFilter.Options

An address code for mail sorting and delivery.

static var subAdministrativeArea: MKAddressFilter.Options

The secondary administrative divisions of countries or regions.

static var subLocality: MKAddressFilter.Options

Local administrative subdivisions, postal city subdistricts, and neighborhoods. (地方行政区画、郵便小地区、近隣地区。)

MKLocalSearchRegionPriority によるプライオリティ指定

APIリファレンス:
https://developer.apple.com/documentation/mapkit/mklocalsearchregionpriority

定義:

enum MKLocalSearchRegionPriority : Int, @unchecked Sendable

caseは以下2通りのみ:

case `default`

A value indicating that the results can originate from outside the specified region. (指定された地域外からの結果を示す値。)

case required

A value indicating that no results can originate from outside the specified region. (指定された地域外からの結果が得られないことを示す値。)

MKPointOfInterestCategory の増補

https://developer.apple.com/documentation/mapkit/mkpointofinterestcategory?changes=latest_minor

追加されたのは以下:

public static let animalService: MKPointOfInterestCategory

public static let automotiveRepair: MKPointOfInterestCategory

public static let baseball: MKPointOfInterestCategory

public static let basketball: MKPointOfInterestCategory

public static let beauty: MKPointOfInterestCategory

public static let bowling: MKPointOfInterestCategory

public static let castle: MKPointOfInterestCategory

public static let conventionCenter: MKPointOfInterestCategory

public static let distillery: MKPointOfInterestCategory

public static let fairground: MKPointOfInterestCategory

public static let fishing: MKPointOfInterestCategory

public static let fortress: MKPointOfInterestCategory

public static let golf: MKPointOfInterestCategory

public static let goKart: MKPointOfInterestCategory

public static let hiking: MKPointOfInterestCategory

public static let kayaking: MKPointOfInterestCategory

public static let landmark: MKPointOfInterestCategory

public static let mailbox: MKPointOfInterestCategory

public static let miniGolf: MKPointOfInterestCategory

public static let musicVenue: MKPointOfInterestCategory

public static let nationalMonument: MKPointOfInterestCategory

public static let planetarium: MKPointOfInterestCategory

public static let rockClimbing: MKPointOfInterestCategory

public static let rvPark: MKPointOfInterestCategory

public static let skatePark: MKPointOfInterestCategory

public static let skating: MKPointOfInterestCategory

public static let skiing: MKPointOfInterestCategory

public static let soccer: MKPointOfInterestCategory

public static let spa: MKPointOfInterestCategory

public static let surfing: MKPointOfInterestCategory

public static let swimming: MKPointOfInterestCategory

public static let tennis: MKPointOfInterestCategory

public static let volleyball: MKPointOfInterestCategory

Discussion