地図つきのアプリ画面は、Rork Max に頼むと驚くほどあっさり生成されます。私自身、開発の合間に「壁紙アプリ用の撮影スポットを地図にメモしておく」小さなツールを Rork Max で試作したとき、現在地に追従する地図とピン表示までは最初のプロンプト一往復で動きました。ところが、そこから先が長いのです。キーワードで周辺を検索する、ピンが数百件に増えても固まらない、位置情報の許可を自然なタイミングで求める——この3つは AI の一発生成では通らず、それぞれ手を入れる必要がありました。
個人開発では地図画面が「看板機能」になるアプリが少なくありません。店舗検索、旅の記録、スポット共有。どれも骨格は同じで、詰まる場所も同じです。ここでは Rork Max のネイティブ Swift 出力を前提に、場所検索マップを実用ラインまで持っていく手順を、試作時の実測と失敗を交えて整理します。
作るものと全体の構成
対象は「現在地周辺のスポットを検索して、ピンをタップすると詳細が開き、純正マップに経路を渡せる」画面です。構成要素は4つに分かれます。
位置情報の取得と許可管理(CoreLocation)
地図表示とピン描画(SwiftUI の Map、必要に応じて MKMapView)
周辺検索(MKLocalSearch)
詳細シートと経路連携(MKMapItem)
Rork Max への最初のプロンプトは、この分解をそのまま伝えるのが効率的です。私は次のような粒度で渡すことを好みます。
現在地周辺の地図を表示する画面を作ってください。
- iOS 17 の SwiftUI Map と MapCameraPosition を使う
- 上部に検索バー、MKLocalSearch でキーワード検索し結果をピン表示
- ピンのタップで下からシートが開き、名前・住所・「経路」ボタンを表示
- 位置情報の許可は地図画面を開いたタイミングで When In Use を要求
- CLLocationManager まわりは ObservableObject に分離
「iOS 17 の API を使う」と明示するのが要点です。指定しないと、古い MKCoordinateRegion ベースの書き方と新しい MapCameraPosition ベースの書き方が混在したコードが返ってくることがあり、後から片方に寄せる修正でクレジットを余分に使いました。
位置情報許可は「求めるタイミング」まで設計する
最初の壁は地図そのものではなく許可ダイアログです。起動直後にいきなり許可を求める実装が生成されがちですが、ユーザーから見ると「なぜ今?」となり、一度拒否されると設定アプリへ誘導する導線が必要になります。地図画面を開いた瞬間に求めるのが、文脈が伝わる最小の設計です。
Info.plist には利用目的の文言が必須です。ここは審査(App Store Review Guideline 5.1.1)で最も差し戻されやすい箇所で、「アプリの機能向上のため」のような汎用文言は通らないことがあります。「周辺のスポットを検索し、現在地からの距離を表示するために位置情報を使用します」のように、機能と結びつけて具体的に書きます。
// LocationManager.swift — 許可状態を UI に流す最小構成
import CoreLocation
import Observation
@Observable
final class LocationManager : NSObject , CLLocationManagerDelegate {
private let manager = CLLocationManager ()
var authorization: CLAuthorizationStatus = .notDetermined
var lastLocation: CLLocation ?
override init () {
super . init ()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
}
func requestWhenInUse () {
// 地図画面の onAppear から呼ぶ。起動直後には呼ばない
manager. requestWhenInUseAuthorization ()
}
func locationManagerDidChangeAuthorization ( _ manager: CLLocationManager) {
authorization = manager.authorizationStatus
if authorization == .authorizedWhenInUse {
manager. startUpdatingLocation ()
}
}
func locationManager ( _ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
lastLocation = locations. last
}
}
拒否された場合の分岐も忘れずに用意します。authorization == .denied のときは地図の代わりに「設定を開く」ボタンつきの案内ビューを出す——この分岐を Rork Max に追加依頼するときは「拒否時のフォールバック UI を足してください」と明示すると、UIApplication.openSettingsURLString を使った導線まで一度で生成されました。
なお、バックグラウンド位置情報(Always 許可)は、この種のアプリでは原則不要です。有効化すると審査で利用理由の説明を求められ、正当化できないとリジェクト対象になります。生成コードに allowsBackgroundLocationUpdates が紛れ込んでいないか、申請前に一度検索しておくと安心です。位置情報が取れないときの切り分けは Rorkで位置情報が取れない時に最初に確認したい5つのポイント にまとまっているので、React Native 側の視点も含めて併読をおすすめします。
SwiftUI の Map を iOS 17 API で組む
iOS 17 以降の SwiftUI Map は、MapCameraPosition でカメラを宣言的に制御します。現在地追従は .userLocation(fallback:) を初期値にするだけで済み、ユーザーが地図をドラッグすると自動的に追従が外れる挙動まで含めて標準で面倒を見てくれます。
// SpotMapView.swift — 検索結果をピン表示する地図本体
import SwiftUI
import MapKit
struct SpotMapView : View {
@State private var camera: MapCameraPosition =
. userLocation ( fallback : .automatic)
@State private var spots: [Spot] = [] // 検索結果
@State private var selected: Spot ?
var body: some View {
Map ( position : $camera, selection : $selected) {
UserAnnotation () // 現在地の青い点
ForEach (spots) { spot in
Marker (spot.name, coordinate : spot.coordinate)
. tag (spot)
}
}
. mapControls {
MapUserLocationButton ()
MapCompass ()
}
. sheet ( item : $selected) { spot in
SpotDetailSheet ( spot : spot)
. presentationDetents ([. height ( 220 ), .medium])
}
}
}
Marker はシステム標準の見た目、Annotation は任意の SwiftUI ビューを置けるカスタムピンです。試作では最初から Annotation で凝ったピンにしたくなりますが、後述するパフォーマンスの理由で、まず Marker で組んで必要になってから置き換える順序を私は取っています。
MKLocalSearch で「この辺りのカフェ」を検索する
周辺検索の本体は MKLocalSearch です。要点は検索リクエストに現在の表示領域(region)を渡すことで、これを省くと東京で「カフェ」と検索したのに別の都市の結果が混ざる、という体験の悪い挙動になります。
// SpotSearchService.swift — 表示領域を優先した周辺検索
import MapKit
struct SpotSearchService {
func search ( for query: String ,
in region: MKCoordinateRegion) async throws -> [Spot] {
let request = MKLocalSearch. Request ()
request.naturalLanguageQuery = query
request.region = region // 表示中の領域を優先
request.resultTypes = .pointOfInterest // 住所ヒットを除外
let response = try await MKLocalSearch ( request : request). start ()
return response.mapItems. map { item in
Spot ( name : item.name ?? "名称不明" ,
coordinate : item.placemark.coordinate,
mapItem : item)
}
}
}
// 例: 吉祥寺駅周辺の region で "カフェ" を検索すると
// 半径約1km内の店舗が20件前後返る(上限は API 側で制御)
表示領域は Map の onMapCameraChange で取得できます。検索のたびに最新のカメラ領域を渡す、という接続を Rork Max に依頼するときは「onMapCameraChange で region を State に保持し、検索時にそれを使う」とデータの流れごと指定すると、意図どおりの配線になりました。逆に「検索バーをつけて」だけだと、初期領域を固定で使い回す実装が返ってきて、地図を動かしても検索範囲が変わらないバグに気づくのが遅れました。
カテゴリを絞りたい場合は pointOfInterestFilter が使えます。カフェ・レストランだけに限定するなら MKPointOfInterestFilter(including: [.cafe, .restaurant]) を設定します。フィルタなしの自然言語検索より結果が安定するので、検索対象が決まっているアプリでは指定しておく価値があります。
ピンが増えたら SwiftUI Map の限界が来る
ここが今回いちばん伝えたい判断ポイントです。SwiftUI の Map には、2026年7月時点でもピンの自動クラスタリング(近接ピンの束ね表示)がありません。撮影スポットのメモが増えて300ピンほどを Annotation で描画したとき、私の iPhone 15 Pro でも地図のドラッグが目に見えてカクつきました。Marker に戻すと多少改善しますが、それでも束ね表示がないので見た目が「ピンの絨毯」になり、実用性が先に破綻します。
選択肢は2つです。
方式 クラスタリング 実装コスト 向いている規模 SwiftUI Map(Marker/Annotation) なし(自前で間引き) 低い 〜100ピン程度・検索結果表示 MKMapView を UIViewRepresentable で包む MKClusterAnnotation が標準対応 中〜高 数百ピン以上・保存スポット一覧
検索結果の表示(多くて数十件)なら SwiftUI Map のままで問題ありません。一方、ユーザーが貯めたスポットを全件描画する画面は、素直に MKMapView に切り替えるのが結局近道でした。クラスタリングは MKMarkerAnnotationView に clusteringIdentifier を設定するだけで有効になります。
// ClusteredMapView.swift — MKMapView をラップして標準クラスタリングを使う
import SwiftUI
import MapKit
struct ClusteredMapView : UIViewRepresentable {
let annotations: [MKPointAnnotation]
func makeUIView ( context : Context) -> MKMapView {
let mapView = MKMapView ()
mapView.delegate = context.coordinator
mapView. register (
MKMarkerAnnotationView. self ,
forAnnotationViewWithReuseIdentifier : "spot" )
return mapView
}
func updateUIView ( _ mapView: MKMapView, context : Context) {
mapView. removeAnnotations (mapView.annotations)
mapView. addAnnotations (annotations)
}
func makeCoordinator () -> Coordinator { Coordinator () }
final class Coordinator : NSObject , MKMapViewDelegate {
func mapView ( _ mapView: MKMapView,
viewFor annotation: MKAnnotation) -> MKAnnotationView ? {
guard ! (annotation is MKUserLocation) else { return nil }
let view = mapView. dequeueReusableAnnotationView (
withIdentifier : "spot" , for : annotation)
as! MKMarkerAnnotationView
view.clusteringIdentifier = "spotCluster" // これで束ね表示が有効化
return view
}
}
}
Rork Max にこの切り替えを頼む場合は「SwiftUI Map を UIViewRepresentable の MKMapView に置き換え、clusteringIdentifier でクラスタリングを有効化」と書けば一度で通ります。曖昧に「ピンが多くて重いので直して」と頼むと、表示件数を間引くだけの対処が返ってくることがあり、根本の解決になりませんでした。生成の得意・不得意の感触は Rork Max で SwiftUI ネイティブアプリを開発する手順 で書いた内容と一貫していて、「何を使って解決するか」まで指定できるかどうかが分かれ目になります。
詳細シートから純正マップへ経路を渡す
経路案内を自前実装する必要はありません。検索結果の MKMapItem をそのまま純正マップに渡せば、徒歩・車・電車の経路はすべて向こうが引き受けてくれます。
// SpotDetailSheet 内の「経路」ボタン
Button ( "経路を表示" ) {
spot.mapItem. openInMaps ( launchOptions : [
MKLaunchOptionsDirectionsModeKey :
MKLaunchOptionsDirectionsModeWalking
])
}
アプリ内で完結する経路描画(MKDirections)も可能ですが、個人開発の初期リリースでは、この「純正マップへ委譲する」割り切りが保守コストを大きく下げます。私は最初のバージョンでは必ずこちらを選び、ユーザーから要望が来てから内製化を検討する順序にしています。
審査と実運用でつまずいた点
最後に、試作から申請までで実際に手が止まった箇所を残しておきます。
許可文言の具体性 : 前述のとおり Guideline 5.1.1 の頻出ポイントです。機能名を含めた文言に書き直してから審査に出しました。
シミュレータでの位置テスト : Rork Max のブラウザ内シミュレータでも Features > Location 相当のカスタム座標指定ができますが、移動シミュレーション(Freeway Drive など)を伴う検証は実機のほうが確実でした。Companion アプリ経由の実機テストと組み合わせるのが現実的です。
WeatherKit との組み合わせ : スポット詳細に天気を出す拡張は相性がよい一方、認証まわりに固有の手続きがあります。Rork Max で WeatherKit を使う天気アプリ — 認証とアトリビューションの落とし穴 で書いた注意点がそのまま当てはまります。
クレジットの節約 : 地図画面は修正の往復が増えがちです。「LocationManager だけ」「検索サービスだけ」と修正範囲をファイル単位で指定すると、無関係な画面まで再生成されるのを防げて、消費が体感で3〜4割減りました。
まとめ
まずは検索結果を Marker で表示するところまでを Rork Max に生成させ、動いたら onMapCameraChange と検索の接続を確認してみてください。クラスタリングへの切り替えは、保存ピンが100件を超えてからで間に合います。地図画面は一度に全部を作ろうとせず、許可 → 表示 → 検索 → 集約の順に一段ずつ確かめるのが、結局いちばん速い道だと感じています。