ObservableObjectをサポートするための既存の型の拡張
Extending existing types to support ObservableObject
SwiftUI doesn’t let us bind optionals to text fields.
SwiftUIではオプションをテキストフィールドにバインドできないため、これを修正するには少し考える必要があります。
ObservableObject
ObservedObject
による解決!!
EditViewとMKPointAnnotation-ObservableObject.swiftにて利用。
MapView.swift
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
//ContentViewのcenterCoordinateにバインディング
@Binding var centerCoordinate: CLLocationCoordinate2D
@Binding var selectedPlace: MKPointAnnotation?
@Binding var showingPlaceDetails: Bool
var annotations: [MKPointAnnotation]
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
if annotations.count != view.annotations.count {
view.removeAnnotations(view.annotations)
view.addAnnotations(annotations)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
//-------------------------------------------
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
parent.centerCoordinate = mapView.centerCoordinate
}
//---------------------------------
//viewFor annotation
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// this is our unique identifier for view reuse
let identifier = "Placemark"
// attempt to find a cell we can recycle
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
// we didn't find one; make a new one
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
// allow this to show pop up information
annotationView?.canShowCallout = true
// attach an information button to the view
annotationView?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
} else {
// we have a view to reuse, so give it the new annotation
annotationView?.annotation = annotation
}
// whether it's a new view or a recycled one, send it back
return annotationView
}
//calloutAccessoryControlTapped
//ボタン(calloutAccessoryControlTapped)がタップされたとき、アラートをトリガー
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
guard let placemark = view.annotation as? MKPointAnnotation else { return }
parent.selectedPlace = placemark
parent.showingPlaceDetails = true
}
}
}
extension MKPointAnnotation {
static var example: MKPointAnnotation {
let annotation = MKPointAnnotation()
annotation.title = "London"
annotation.subtitle = "Home to the 2012 Summer Olympics."
annotation.coordinate = CLLocationCoordinate2D(latitude: 51.5, longitude: -0.13)
return annotation
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView(centerCoordinate: .constant(MKPointAnnotation.example.coordinate), selectedPlace: .constant(MKPointAnnotation.example), showingPlaceDetails: .constant(false), annotations: [MKPointAnnotation.example])
}
}
MKPointAnnotation-ObservableObject.swift
import MapKit
extension MKPointAnnotation: ObservableObject {
public var wrappedTitle: String {
get {
self.title ?? "Unknown value"
}
set {
title = newValue
}
}
public var wrappedSubtitle: String {
get {
self.subtitle ?? "Unknown value"
}
set {
subtitle = newValue
}
}
}
EditView.swift
import SwiftUI
import MapKit
struct EditView: View {
// for dismiss
@Environment(\.presentationMode) var presentationMode
// 引数
@ObservedObject var placemark: MKPointAnnotation
var body: some View {
NavigationView {
Form {
Section {
TextField("Place name", text: $placemark.wrappedTitle)
TextField("Description", text: $placemark.wrappedSubtitle)
}
}
.navigationBarTitle("Edit place")
.navigationBarItems(trailing: Button("Done") {
self.presentationMode.wrappedValue.dismiss()
})
}
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(placemark: MKPointAnnotation.example)
}
}
import SwiftUI
import MapKit
struct ContentView: View {
//中心座標
@State private var centerCoordinate = CLLocationCoordinate2D()
//Annotation配列
@State private var locations = [MKPointAnnotation]()
//plus(+)button押下 -->> sheet表示用
//mapviewのボタン(calloutAccessoryControlTapped)押下のAnnotationを特定
@State private var selectedPlace: MKPointAnnotation?
//show Alert
@State private var showingPlaceDetails = false
//show sheet
@State private var showingEditScreen = false
var body: some View {
ZStack {
//引数として、中心座標、
MapView(centerCoordinate: $centerCoordinate, selectedPlace: $selectedPlace, showingPlaceDetails: $showingPlaceDetails, annotations: locations)
.edgesIgnoringSafeArea(.all)
Circle()
.fill(Color.blue)
.opacity(0.3)
.frame(width: 32, height: 32)
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
// create a new location
let newLocation = MKPointAnnotation()
newLocation.title = "Example location"
newLocation.coordinate = self.centerCoordinate
self.locations.append(newLocation)
self.selectedPlace = newLocation
self.showingEditScreen = true
}) {
Image(systemName: "plus")
}
.padding()
.background(Color.black.opacity(0.75))
.foregroundColor(.white)
.font(.title)
.clipShape(Circle())
.padding(.trailing)
}
}
}
.alert(isPresented: $showingPlaceDetails) {
Alert(title: Text(selectedPlace?.title ?? "Unknown"), message: Text(selectedPlace?.subtitle ?? "Missing place information."), primaryButton: .default(Text("OK")), secondaryButton: .default(Text("Edit")) {
// edit this place
self.showingEditScreen = true
})
}
.sheet(isPresented: $showingEditScreen) {
if self.selectedPlace != nil {
EditView(placemark: self.selectedPlace!)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ウィキペディアからデータをダウンロードする
Downloading data from Wikipedia
このアプリ全体をより便利にするEditViewために、興味深い場所が表示されるように画面を変更します。実際にはGPS座標を使用してウィキペディアにクエリを実行でき、近くにある場所のリストが返されます。
ウィキペディアのAPIは正確な形式でJSONデータを送り返すので、Codableすべてを格納できる構造体を定義するために少し作業を行う必要があります。
<?xml version="1.0" encoding="UTF-8"?>
<api>
<query>
<pages>
<page pageid="128948" ns="0" title="エマ・ワトソン" contentmodel="wikitext" pagelanguage="ja" touched="2014-04-22T16:31:02Z" lastrevid="50754976" counter="" length="35154" />
</pages>
</query>
</api>
Result.swift
import Foundation
struct Result: Codable {
let query: Query
}
struct Query: Codable {
let pages: [Int: Page]
}
struct Page: Codable {
let pageid: Int
let title: String
let terms: [String: [String]]?
}
import SwiftUI
import MapKit
struct EditView: View {
enum LoadingState {
case loading, loaded, failed
}
@State private var loadingState = LoadingState.loading
@State private var pages = [Page]()
// for dismiss
@Environment(\.presentationMode) var presentationMode
// 引数
@ObservedObject var placemark: MKPointAnnotation
//-----------------------
func fetchNearbyPlaces() {
let urlString = "https://en.wikipedia.org/w/api.php?ggscoord=\(placemark.coordinate.latitude)%7C\(placemark.coordinate.longitude)&action=query&prop=coordinates%7Cpageimages%7Cpageterms&colimit=50&piprop=thumbnail&pithumbsize=500&pilimit=50&wbptterms=description&generator=geosearch&ggsradius=10000&ggslimit=50&format=json"
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
// we got some data back!
let decoder = JSONDecoder()
if let items = try? decoder.decode(Result.self, from: data) {
// success – convert the array values to our pages array
self.pages = Array(items.query.pages.values)
self.loadingState = .loaded
return
}
}
// if we're still here it means the request failed somehow
self.loadingState = .failed
}.resume()
}
//-----------------------
var body: some View {
NavigationView {
Form {
Section {
TextField("Place name", text: $placemark.wrappedTitle)
TextField("Description", text: $placemark.wrappedSubtitle)
}
Section(header: Text("Nearby…")) {
if loadingState == .loaded {
List(pages, id: \.pageid) { page in
Text(page.title)
.font(.headline)
+ Text(": ") +
Text("Page description here")
.italic()
}
} else if loadingState == .loading {
Text("Loading…")
} else {
Text("Please try again later.")
}
}
}
.navigationBarTitle("Edit place")
.navigationBarItems(trailing: Button("Done") {
self.presentationMode.wrappedValue.dismiss()
})
.onAppear(perform: fetchNearbyPlaces)
}
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(placemark: MKPointAnnotation.example)
}
}
ウィキペディアの結果を並べ替える
Sorting Wikipedia results
これを修正するために、結果をソートし、インラインクロージャーを提供するだけでなくsorted()、Page構造体をに準拠させComparableます。title並べ替えの候補として最適な文字列が既にあるため、これは実際には非常に簡単です。
Result.swift
import Foundation
struct Result: Codable {
let query: Query
}
struct Query: Codable {
let pages: [Int: Page]
}
struct Page: Codable, Comparable {
let pageid: Int
let title: String
let terms: [String: [String]]?
var description: String {
terms?["description"]?.first ?? "No further information"
}
static func < (lhs: Page, rhs: Page) -> Bool {
lhs.title < rhs.title
}
}
import SwiftUI
import MapKit
struct EditView: View {
enum LoadingState {
case loading, loaded, failed
}
@State private var loadingState = LoadingState.loading
@State private var pages = [Page]()
// for dismiss
@Environment(\.presentationMode) var presentationMode
// 引数
@ObservedObject var placemark: MKPointAnnotation
//-----------------------
func fetchNearbyPlaces() {
let urlString = "https://en.wikipedia.org/w/api.php?ggscoord=\(placemark.coordinate.latitude)%7C\(placemark.coordinate.longitude)&action=query&prop=coordinates%7Cpageimages%7Cpageterms&colimit=50&piprop=thumbnail&pithumbsize=500&pilimit=50&wbptterms=description&generator=geosearch&ggsradius=10000&ggslimit=50&format=json"
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
// we got some data back!
let decoder = JSONDecoder()
if let items = try? decoder.decode(Result.self, from: data) {
// success – convert the array values to our pages array
self.pages = Array(items.query.pages.values).sorted()
self.loadingState = .loaded
return
}
}
// if we're still here it means the request failed somehow
self.loadingState = .failed
}.resume()
}
//-----------------------
var body: some View {
NavigationView {
Form {
Section {
TextField("Place name", text: $placemark.wrappedTitle)
TextField("Description", text: $placemark.wrappedSubtitle)
}
Section(header: Text("Nearby…")) {
if loadingState == .loaded {
List(pages, id: \.pageid) { page in
Text(page.title)
.font(.headline)
+ Text(": ") +
Text(page.description)
}
} else if loadingState == .loading {
Text("Loading…")
} else {
Text("Please try again later.")
}
}
}
.navigationBarTitle("Edit place")
.navigationBarItems(trailing: Button("Done") {
self.presentationMode.wrappedValue.dismiss()
})
.onAppear(perform: fetchNearbyPlaces)
}
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(placemark: MKPointAnnotation.example)
}
}