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) } }