【WWDC23】Meet MapKit for SwiftUI コード&追加の解説
2023年のWWDCにて発表されたMeet MapKit for SwiftUI(SwiftUI向けのMapKitについて)という動画はMapKitの新しい機能を知るための良い動画である。
しかし、この動画はソースコードが配布されていない。画面上にはソースの一部しか表示されないので、全貌をつかむにはこまめに一時停止してソースを写経するという行為が必要になる。また、足りない部分があるときは推測することも必要である。以下に私が行った結果を残す。すでに動画は日本語化されているので内容をすべて解説することはしないが、足した方がよいと思った部分があれば書く。この記事は動画の補足という位置付けなので単独で見てもあまり意味がない。
地理的な前提知識
地理感が無いとつらいので出てくるものをまとめる。
-
マサチューセッツ州
-
ボストン 都市。東側に海がある
- Boston Common ボストンにある公園の名前
- Paul Revere Mall ボストンにある公園の名前
- ノースショア 州の北部海岸
- Cape Cod Bay 州の南東にある湾
-
ボストン 都市。東側に海がある
-
ロードアイランド州
- Providence 都市。南側に海がある。
同じ場所なのに地図の縮尺が切り替わって別の場所に見えるところがある。どんな縮尺でもボストン側かロードアイランド側か判断できるとよい。
1:42 最も簡単なMapアプリ
コード
import SwiftUI
import MapKit
struct ContentView: View {
var body: some View {
Map()
}
}
2:21 Marker
コード
//⭐️【B】
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
struct ContentView: View {
var body: some View {
Map {
//⭐️【B】
Marker("Parking", coordinate: .parking)
}
}
}
地図の表示範囲は Marker
を含むように調整される。
3:00 コンテントビルダーに入れることができるいろいろなもの
コンテントビルダー(Mapのイニシャライザに渡すクロージャ)に入れて地図の上に表示できるものの紹介
単独情報なのでソースは省く
3:44 Annotation
Viewを指定できるAnnotationの紹介
自動翻訳では「right」を「右」と訳すが、実際は「まさに」のright
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
struct ContentView: View {
var body: some View {
Map {
//⭐️【C】
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
}
}
}
4:20 mapStyle
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
struct ContentView: View {
var body: some View {
Map {
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
}
.mapStyle(.standard(elevation: .realistic)) //⭐️【D】
}
}
elevation: .realistic
にすると橋が水面から浮くのを確認する。
.standard
のほかに .imagery
.hybrid
がある。
6:04 Search for places
- 「遊び場」を検索
- 「ビーチ」を検索
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
struct ContentView: View {
@State private var searchResults: [MKMapItem] = [] //⭐️【E】
var body: some View {
Map {
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
//⭐️【E】
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
}
.mapStyle(.standard(elevation: .realistic))
//⭐️【E】
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
BeantownButtons(searchResults: $searchResults)
.padding(.top)
Spacer()
}
.background(.thinMaterial)
}
}
}
//⭐️【E】
struct BeantownButtons: View {
@Binding var searchResults: [MKMapItem]
var body: some View {
HStack {
Button {
search(for: "playground")
} label: {
Label("Playground", systemImage: "figure.and.child.holdinghands")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "beach")
} label: {
Label("Beaches", systemImage: "beach.umbrella")
}
.buttonStyle(.borderedProminent)
}
.labelStyle(.iconOnly)
}
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = MKCoordinateRegion(
center: .parking,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
結果の分布にあわせて表示範囲が変わる。
Markerの色とアイコンを自動で選ぶ
8:35 Markerのカスタマイズ
単独な情報なのでソースは省く
monogramは monogram: Text("FB")
とする必要があった。
9:05 Display a place or region
話の中の「フレーム」とは表示範囲を自動で調整すること。
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
struct ContentView: View {
@State private var position: MapCameraPosition = .automatic //⭐️【F】
@State private var searchResults: [MKMapItem] = []
var body: some View {
Map(position: $position) { //⭐️【F】
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
}
.mapStyle(.standard(elevation: .realistic))
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
BeantownButtons(searchResults: $searchResults)
.padding(.top)
Spacer()
}
.background(.thinMaterial)
}
//⭐️【F】
.onChange(of: searchResults) {
position = .automatic
}
}
}
struct BeantownButtons: View {
@Binding var searchResults: [MKMapItem]
var body: some View {
HStack {
Button {
search(for: "playground")
} label: {
Label("Playground", systemImage: "figure.and.child.holdinghands")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "beach")
} label: {
Label("Beaches", systemImage: "beach.umbrella")
}
.buttonStyle(.borderedProminent)
}
.labelStyle(.iconOnly)
}
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = MKCoordinateRegion(
center: .parking,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
11:15 表示範囲をソースから指定する。
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
//⭐️【G】
extension MKCoordinateRegion {
static let boston = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.360256,
longitude: -71.057279),
span: MKCoordinateSpan(
latitudeDelta: 0.1,
longitudeDelta: 0.1))
static let northShore = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.547408,
longitude: -70.870085),
span: MKCoordinateSpan(
latitudeDelta: 0.5,
longitudeDelta: 0.5))
}
struct ContentView: View {
@State private var position: MapCameraPosition = .automatic
@State private var searchResults: [MKMapItem] = []
var body: some View {
Map(position: $position) {
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
}
.mapStyle(.standard(elevation: .realistic))
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
BeantownButtons(position: $position/* ⭐️ */, searchResults: $searchResults)
.padding(.top)
Spacer()
}
.background(.thinMaterial)
}
.onChange(of: searchResults) {
position = .automatic
}
}
}
struct BeantownButtons: View {
@Binding var position: MapCameraPosition //⭐️【G】
@Binding var searchResults: [MKMapItem]
var body: some View {
HStack {
Button {
search(for: "playground")
} label: {
Label("Playground", systemImage: "figure.and.child.holdinghands")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "beach")
} label: {
Label("Beaches", systemImage: "beach.umbrella")
}
.buttonStyle(.borderedProminent)
//⭐️【G】
Button {
position = .region(.boston)
} label: {
Label("Boston", systemImage: "building.2")
}
.buttonStyle(.bordered)
//⭐️【G】
Button {
position = .region(.northShore)
} label: {
Label("North Shore", systemImage: "water.waves")
}
.buttonStyle(.bordered)
}
.labelStyle(.iconOnly)
}
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = MKCoordinateRegion(
center: .parking,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
12:38 Control what the map displays
MapCamera
カメラの位置、高さ、方向、角度
MapCameraPosition
もうちょっと高レベルでカメラの設定を行う。
また、その周辺の情報も含む
position = .item(.capeCodBay)
で範囲を自動で調整すると言っているがMKMapItemに範囲の情報を持たせるやり方がわからず。
15:15 Search in the visible region
表示エリア内を検索する
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
extension MKCoordinateRegion {
static let boston = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.360256,
longitude: -71.057279),
span: MKCoordinateSpan(
latitudeDelta: 0.1,
longitudeDelta: 0.1))
static let northShore = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.547408,
longitude: -70.870085),
span: MKCoordinateSpan(
latitudeDelta: 0.5,
longitudeDelta: 0.5))
}
struct ContentView: View {
@State private var position: MapCameraPosition = .automatic
@State private var visibleRegion: MKCoordinateRegion? //⭐️【H】
@State private var searchResults: [MKMapItem] = []
var body: some View {
Map(position: $position) {
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
}
.mapStyle(.standard(elevation: .realistic))
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
BeantownButtons(position: $position, searchResults: $searchResults, visibleRegion: visibleRegion)
.padding(.top)
Spacer()
}
.background(.thinMaterial)
}
.onChange(of: searchResults) {
position = .automatic
}
//⭐️【H】
.onMapCameraChange { context in
visibleRegion = context.region
}
}
}
struct BeantownButtons: View {
@Binding var position: MapCameraPosition
@Binding var searchResults: [MKMapItem]
var visibleRegion: MKCoordinateRegion? //⭐️【H】
var body: some View {
HStack {
Button {
search(for: "playground")
} label: {
Label("Playground", systemImage: "figure.and.child.holdinghands")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "beach")
} label: {
Label("Beaches", systemImage: "beach.umbrella")
}
.buttonStyle(.borderedProminent)
Button {
position = .region(.boston)
} label: {
Label("Boston", systemImage: "building.2")
}
.buttonStyle(.bordered)
Button {
position = .region(.northShore)
} label: {
Label("North Shore", systemImage: "water.waves")
}
.buttonStyle(.bordered)
}
.labelStyle(.iconOnly)
}
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = visibleRegion ?? /* ⭐️【H】 */ MKCoordinateRegion(
center: .parking,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
周波数パラメータとは onMapCameraChange
の省略可の引数。
onMapCameraChange
では、領域 context.rect
と、カメラ自体 context.camera
も使える。
BeantownButtons
の var visibleRegion: MKCoordinateRegion?
は情報が渡されるだけなので @Binding
は無い。
最後、ロードアイランド州で実行するとき、ボストンの駐車場のAnnotationも含んで表示される。
17:03 Interact with search results
マーカーをタップすると少し大きくなるようにする。
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
extension MKCoordinateRegion {
static let boston = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.360256,
longitude: -71.057279),
span: MKCoordinateSpan(
latitudeDelta: 0.1,
longitudeDelta: 0.1))
static let northShore = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.547408,
longitude: -70.870085),
span: MKCoordinateSpan(
latitudeDelta: 0.5,
longitudeDelta: 0.5))
}
struct ContentView: View {
@State private var position: MapCameraPosition = .automatic
@State private var visibleRegion: MKCoordinateRegion?
@State private var searchResults: [MKMapItem] = []
@State private var selectedResult: MKMapItem? //⭐️【I】
var body: some View {
Map(position: $position, selection: $selectedResult /* ⭐️【I】 */ ) {
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
}
.mapStyle(.standard(elevation: .realistic))
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
BeantownButtons(position: $position, searchResults: $searchResults, visibleRegion: visibleRegion)
.padding(.top)
Spacer()
}
.background(.thinMaterial)
}
.onChange(of: searchResults) {
position = .automatic
}
.onMapCameraChange { context in
visibleRegion = context.region
}
}
}
struct BeantownButtons: View {
@Binding var position: MapCameraPosition
@Binding var searchResults: [MKMapItem]
var visibleRegion: MKCoordinateRegion?
var body: some View {
HStack {
Button {
search(for: "playground")
} label: {
Label("Playground", systemImage: "figure.and.child.holdinghands")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "beach")
} label: {
Label("Beaches", systemImage: "beach.umbrella")
}
.buttonStyle(.borderedProminent)
Button {
position = .region(.boston)
} label: {
Label("Boston", systemImage: "building.2")
}
.buttonStyle(.bordered)
Button {
position = .region(.northShore)
} label: {
Label("North Shore", systemImage: "water.waves")
}
.buttonStyle(.bordered)
}
.labelStyle(.iconOnly)
}
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = visibleRegion ?? MKCoordinateRegion(
center: .parking,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
「選択」をどう処理するかでややこしい話がある。
同じ型なら selectedResult: MKMapItem?
のようにすればよい。
違う型なら、.tag()
で番号をつけて var selectedTag: Int?
とすればよい。
18:44 Display useful search-result infomation
検索結果のどれかを選択するとその情報を表示する。
その場所の写真を取得して表示する。
ボストンコモンからその場所まで何分かかるかを取得することができる(そんな機能がiOSにあるのを知らなかった)。
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
extension MKCoordinateRegion {
static let boston = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.360256,
longitude: -71.057279),
span: MKCoordinateSpan(
latitudeDelta: 0.1,
longitudeDelta: 0.1))
static let northShore = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.547408,
longitude: -70.870085),
span: MKCoordinateSpan(
latitudeDelta: 0.5,
longitudeDelta: 0.5))
}
struct ContentView: View {
@State private var position: MapCameraPosition = .automatic
@State private var visibleRegion: MKCoordinateRegion?
@State private var searchResults: [MKMapItem] = []
@State private var selectedResult: MKMapItem?
@State private var route: MKRoute? //⭐️【J】
var body: some View {
Map(position: $position, selection: $selectedResult) {
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
//⭐️【J】
.annotationTitles(.hidden)
}
.mapStyle(.standard(elevation: .realistic))
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
//⭐️【J】
VStack(spacing: 0) {
if let selectedResult {
ItemInfoView(selectedResult: selectedResult, route: route)
.frame(height: 128)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding([.top, .horizontal])
}
BeantownButtons(position: $position, searchResults: $searchResults, visibleRegion: visibleRegion)
.padding(.top)
}
Spacer()
}
.background(.thinMaterial)
}
.onChange(of: searchResults) {
position = .automatic
}
//⭐️【J】
.onChange(of: selectedResult) {
getDirections()
}
.onMapCameraChange { context in
visibleRegion = context.region
}
}
//⭐️【J】
func getDirections() {
route = nil
guard let selectedResult else { return }
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: .parking))
request.destination = selectedResult
Task {
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
route = response?.routes.first
}
}
}
struct BeantownButtons: View {
@Binding var position: MapCameraPosition
@Binding var searchResults: [MKMapItem]
var visibleRegion: MKCoordinateRegion?
var body: some View {
HStack {
Button {
search(for: "playground")
} label: {
Label("Playground", systemImage: "figure.and.child.holdinghands")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "beach")
} label: {
Label("Beaches", systemImage: "beach.umbrella")
}
.buttonStyle(.borderedProminent)
Button {
position = .region(.boston)
} label: {
Label("Boston", systemImage: "building.2")
}
.buttonStyle(.bordered)
Button {
position = .region(.northShore)
} label: {
Label("North Shore", systemImage: "water.waves")
}
.buttonStyle(.bordered)
}
.labelStyle(.iconOnly)
}
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = visibleRegion ?? MKCoordinateRegion(
center: .parking,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
//⭐️【J】
struct ItemInfoView: View {
@State private var lookAroundScene: MKLookAroundScene?
var selectedResult: MKMapItem
var route: MKRoute?
private var travelTime: String? {
guard let route else {return nil}
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.hour, .minute]
return formatter.string(from: route.expectedTravelTime)
}
func getLookAroundScene() {
lookAroundScene = nil
Task {
let request = MKLookAroundSceneRequest(mapItem: selectedResult)
lookAroundScene = try? await request.scene
}
}
var body: some View {
LookAroundPreview(initialScene: lookAroundScene)
.overlay(alignment: .bottomTrailing) {
HStack {
Text("\(selectedResult.name ?? "")")
if let travelTime {
Text(travelTime)
}
}
.font(.caption)
.foregroundStyle(.white)
.padding(10)
}
.onAppear {
getLookAroundScene()
}
.onChange(of: selectedResult) {
getLookAroundScene()
}
}
}
21:46 Show overlay content
地図上に何かを描く。
ある場所へのルートを取得したら、そのルート情報から地図上で描画するための線情報を取り出せる。
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
extension MKCoordinateRegion {
static let boston = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.360256,
longitude: -71.057279),
span: MKCoordinateSpan(
latitudeDelta: 0.1,
longitudeDelta: 0.1))
static let northShore = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.547408,
longitude: -70.870085),
span: MKCoordinateSpan(
latitudeDelta: 0.5,
longitudeDelta: 0.5))
}
struct ContentView: View {
@State private var position: MapCameraPosition = .automatic
@State private var visibleRegion: MKCoordinateRegion?
@State private var searchResults: [MKMapItem] = []
@State private var selectedResult: MKMapItem?
@State private var route: MKRoute?
var body: some View {
Map(position: $position, selection: $selectedResult) {
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
.annotationTitles(.hidden)
//⭐️【K】
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
}
.mapStyle(.standard(elevation: .realistic))
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
VStack(spacing: 0) {
if let selectedResult {
ItemInfoView(selectedResult: selectedResult, route: route)
.frame(height: 128)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding([.top, .horizontal])
}
BeantownButtons(position: $position, searchResults: $searchResults, visibleRegion: visibleRegion)
.padding(.top)
}
Spacer()
}
.background(.thinMaterial)
}
.onChange(of: searchResults) {
position = .automatic
}
.onChange(of: selectedResult) {
getDirections()
}
.onMapCameraChange { context in
visibleRegion = context.region
}
}
func getDirections() {
route = nil
guard let selectedResult else { return }
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: .parking))
request.destination = selectedResult
Task {
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
route = response?.routes.first
}
}
}
struct BeantownButtons: View {
@Binding var position: MapCameraPosition
@Binding var searchResults: [MKMapItem]
var visibleRegion: MKCoordinateRegion?
var body: some View {
HStack {
Button {
search(for: "playground")
} label: {
Label("Playground", systemImage: "figure.and.child.holdinghands")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "beach")
} label: {
Label("Beaches", systemImage: "beach.umbrella")
}
.buttonStyle(.borderedProminent)
Button {
position = .region(.boston)
} label: {
Label("Boston", systemImage: "building.2")
}
.buttonStyle(.bordered)
Button {
position = .region(.northShore)
} label: {
Label("North Shore", systemImage: "water.waves")
}
.buttonStyle(.bordered)
}
.labelStyle(.iconOnly)
}
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = visibleRegion ?? MKCoordinateRegion(
center: .parking,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
struct ItemInfoView: View {
@State private var lookAroundScene: MKLookAroundScene?
var selectedResult: MKMapItem
var route: MKRoute?
private var travelTime: String? {
guard let route else {return nil}
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.hour, .minute]
return formatter.string(from: route.expectedTravelTime)
}
func getLookAroundScene() {
lookAroundScene = nil
Task {
let request = MKLookAroundSceneRequest(mapItem: selectedResult)
lookAroundScene = try? await request.scene
}
}
var body: some View {
LookAroundPreview(initialScene: lookAroundScene)
.overlay(alignment: .bottomTrailing) {
HStack {
Text("\(selectedResult.name ?? "")")
if let travelTime {
Text(travelTime)
}
}
.font(.caption)
.foregroundStyle(.white)
.padding(10)
}
.onAppear {
getLookAroundScene()
}
.onChange(of: selectedResult) {
getLookAroundScene()
}
}
}
MapPolyline
の使い方をいくつか紹介する。点情報の集まりを与えることもできる。グラデーションを付けた点線もできる。
MapPolygon
は四角形(多角形?)
MapCircle
は円
地図上で道路やラベルの上に表示するかどうかを指定することが出来る。
23:24 User Location and Map Controls
地図の上にユーザーの場所を示す
UserAnnotation
地図上のユーザー場所の印
MapUserLocationButton
地図上のユーザー場所の印
実機で試すときは
- XcodeでターゲットのInfoを開き、Privacy - Location When In Use Usage Descriptionに許可を求める文章を入れる
- 実機でダイアログが出てきたら許可する。出なければ設定で、アプリに許可を与える
をすると現在地を表示する。
コード
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 42.354528, longitude: -71.068369)
}
extension MKCoordinateRegion {
static let boston = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.360256,
longitude: -71.057279),
span: MKCoordinateSpan(
latitudeDelta: 0.1,
longitudeDelta: 0.1))
static let northShore = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 42.547408,
longitude: -70.870085),
span: MKCoordinateSpan(
latitudeDelta: 0.5,
longitudeDelta: 0.5))
}
struct ContentView: View {
@State private var position: MapCameraPosition = .automatic
@State private var visibleRegion: MKCoordinateRegion?
@State private var searchResults: [MKMapItem] = []
@State private var selectedResult: MKMapItem?
@State private var route: MKRoute?
var body: some View {
Map(position: $position, selection: $selectedResult) {
Annotation("Parking", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
.annotationTitles(.hidden)
//⭐️【L】
UserAnnotation()
//⭐️【L】削除する
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
}
.mapStyle(.standard(elevation: .realistic))
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
VStack(spacing: 0) {
if let selectedResult {
ItemInfoView(selectedResult: selectedResult, route: route)
.frame(height: 128)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding([.top, .horizontal])
}
BeantownButtons(position: $position, searchResults: $searchResults, visibleRegion: visibleRegion)
.padding(.top)
}
Spacer()
}
.background(.thinMaterial)
}
.onChange(of: searchResults) {
position = .automatic
}
.onChange(of: selectedResult) {
getDirections()
}
.onMapCameraChange { context in
visibleRegion = context.region
}
//⭐️【L】
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
}
func getDirections() {
route = nil
guard let selectedResult else { return }
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: .parking))
request.destination = selectedResult
Task {
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
route = response?.routes.first
}
}
}
struct BeantownButtons: View {
@Binding var position: MapCameraPosition
@Binding var searchResults: [MKMapItem]
var visibleRegion: MKCoordinateRegion?
var body: some View {
HStack {
Button {
search(for: "playground")
} label: {
Label("Playground", systemImage: "figure.and.child.holdinghands")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "beach")
} label: {
Label("Beaches", systemImage: "beach.umbrella")
}
.buttonStyle(.borderedProminent)
Button {
position = .region(.boston)
} label: {
Label("Boston", systemImage: "building.2")
}
.buttonStyle(.bordered)
Button {
position = .region(.northShore)
} label: {
Label("North Shore", systemImage: "water.waves")
}
.buttonStyle(.bordered)
}
.labelStyle(.iconOnly)
}
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = visibleRegion ?? MKCoordinateRegion(
center: .parking,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
struct ItemInfoView: View {
@State private var lookAroundScene: MKLookAroundScene?
var selectedResult: MKMapItem
var route: MKRoute?
private var travelTime: String? {
guard let route else {return nil}
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.hour, .minute]
return formatter.string(from: route.expectedTravelTime)
}
func getLookAroundScene() {
lookAroundScene = nil
Task {
let request = MKLookAroundSceneRequest(mapItem: selectedResult)
lookAroundScene = try? await request.scene
}
}
var body: some View {
LookAroundPreview(initialScene: lookAroundScene)
.overlay(alignment: .bottomTrailing) {
HStack {
Text("\(selectedResult.name ?? "")")
if let travelTime {
Text(travelTime)
}
}
.font(.caption)
.foregroundStyle(.white)
.padding(10)
}
.onAppear {
getLookAroundScene()
}
.onChange(of: selectedResult) {
getLookAroundScene()
}
}
}
Discussion