【iOS 18】MapKitの検索機能のアップデートまとめ
iOS 18, macOS 15, visionOS 2でMapKitの検索機能が強化された。そのあたりについてのWWDC24での解説や公式サンプルの実装、ドキュメント(APIリファレンス)の記載内容についてまとめる。
WWDC24のセッションでの解説
アップデート概要
今年、検索結果をフィルタリングする新しい方法を含む、検索APIに改良を加えました。
-
音楽会場、スケート場、お城など、より多くの種類の場所を検索できます。
-
川や山脈などの物理的な特徴を検索できます。
-
市や郵便番号など、住所を構成する特定の要素を検索することもできます。
-
特定の境界領域に限定した検索が可能になったので、重要なエリアだけに焦点を絞ることができます。
-
また、サーバーAPIのために、ページ分割機能を追加しました。
- これにより、大量の検索結果が得られるようになりました。実際、何ページもある!
実装解説
これらの新しい検索機能は、より効率的に店舗を見つけ、収集するのに役立つと思いますか?もちろんです!一日中ベイエリアで野生のアップルストアを探し回った後では、どんなコレクターでも強いコーヒーが必要だろう!私は、自分のウェブサイトでこれらの新しい検索機能を使い、自分が望むだけカフェインを補給できるよう、前もって計画を立てられるようにするつもりだ。そして、ネイティブアプリにも同じ機能を追加するつもりだ。
まず、クパチーノを見つける必要がある。
新しいAddressFiltersでそれができそうだ!
ここでは、ほとんどの国の都市である地域のみを検索する AddressFilter
を作成します。
そして、そのフィルタを新しい検索オブジェクトに差し込む。
最後に、Cupertino's Classy Cleanersというビジネスが見つかるかもしれないというリスクなしに、探している都市を見つけるために、Cupertinoという言葉で検索を実行する。
クパチーノに一致する都市の検索結果は、私が設定したshowMap関数に送られる。
その関数の中で、私は最初の検索結果を受け取り、それを使って新しいマップに渡す region を作る。
このコードでは、地図がクパチーノの真上にセンタリングされる、
これはまさに私が望んでいたものだ。次に、その地域内で新しい検索を作成し、コーヒーを検索する。
そして最後に、すべてのコーヒー検索結果に新しい PlaceAnnotation
を追加する。
これですべての結果が地図上にマークされた!しかし、まだちょっと問題がある。
ズームアウトすると、最初のビューポートの外側の地図にたくさんのアノテーションが追加されていることに気づきます。
こんな遠くのコーヒーの店は本当にいらない、
というわけで、RegionPriority
を使ってそれらを取り除くことができる!
この1つのプロパティだけで、検索に渡した地域内に結果があることを要求できる。
この変更後、私のページの結果は、私の元のregionにあるものだけに制限されます。
ネイティブ実装
もちろん、これらの機能はすべてネイティブアプリでも利用できる。私のアプリもまったく同じように強化する。
ここでは、各コーヒーショップのマーカーを表示するビューを作りました。
非同期に、findCityを使って都市の地域を取得し、findCoffeeを使ってその地域にあるコーヒーショップを見つける。
これが findCity
で、
JSのコードと同じように、addressFilter
で場所を検索する。
そして、これはfindCoffeeで、
クパチーノのリージョンを取り、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" という本セッションの内容に対応するサンプルアプリが公開されている:
概要としては次のように書かれている:
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:)
,MKMapItem
のidentifier
プロパティ MKMapItemRequest(mapItemIdentifier:)
-
- Place Card関連
MKMapItemDetailViewControllerDelegate
MKMapItemAnnotation
MKSelectionAccessory
- 検索関連
MKLocalSearchRegionPriority
MKAddressFilter
-
MKPointOfInterestCategory
の一部カテゴリ.distillery
.castle
.conventionCenter
- etc...
Place IDやPlace Cardは本記事では解説していない別の新機能(別記事に書いた)を使ったものだが、iOS 18のサンプルとして使うならこのあたりの実装がある機能を試せばよい。
たとえば MKAddressFilter
や MKLocalSearchRegionPriority
は、
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リファレンスはここ:
An object that filters which address options to include or exclude in search results. (検索結果にどのアドレスオプションを含めるか、または除外するかをフィルタリングするオブジェクト。)
上述のWWDCセッションの解説やサンプルにもあった通り、MKLocalSearch
の addressFilter
に指定して使う:
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "cupertino"
request.addressFilter = MKAddressFilter(
including: .locality
)
あとは「どういうフィルターが指定できるのか」がわかれば良さそう。以下に MKAddressFilter.Options
の種類と概要を列挙する。
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リファレンス:
定義:
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
の増補
追加されたのは以下:
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