I'm trying to use the new Map component of SwiftUI. So far, so good but now, I'm struggling to update the MapPins on a map after an API response. (in order to update the pins with a map move).
I've used TinyNetworking for the network call, and the call is working fine, but then MapPins are not updated and I don't understand why.
Here is some code:
The network call:
#ObservedObject var places = Resource(endpoint: getPlaces())
The Map declaration:
var locations: [Location]
Map(coordinateRegion: $viewModel.region,
interactionModes: .all,
showsUserLocation: true,
userTrackingMode: $trackingMode,
annotationItems: locations) { location -> MapPin in
MapPin(coordinate: location.coordinate, tint: .red)
}
Place struct:
struct Place: Codable {
let title: String
let coordinates: Coordinates
}
Location struct:
struct Location: Identifiable {
let id = UUID()
let title: String
let coordinate: CLLocationCoordinate2D
}
And here is where I try to make the connection:
MapView(followUser: $followUser, locations: convertPlacesToLocation(newPlaces: places))
private func convertPlacesToLocation(newPlaces: Resource<[Place]>) -> [Location] {
var locations: [Location] = []
if let values = newPlaces.value {
for place in values {
locations.append(Location(title: place.title, coordinate: .init(latitude: place.coordinates.lat, longitude: place.coordinates.lon)))
}
}
return locations
}
The convertPlacesToLocation is indeed called twice: when first loading the map, before the return of the network call, and after the return of the call, where the values are correct.
But the MapPin are not updated ...
Any ideas?
Here is the code to make the network call and reload places:
final class Resource<A>: ObservableObject {
let endpoint: Endpoint<A>
#Published var value: A?
init(endpoint: Endpoint<A>) {
self.endpoint = endpoint
reload()
}
func reload() {
URLSession.shared.load(endpoint) { result in
DispatchQueue.main.async {
self.value = try? result.get()
}
}
}
}
Related
I am trying to build a small Map app where location for user changes all the time. In general I get latitude and longitude updates all the time. And I need to display them and show the change with sliding animation, simular to Apple FindMyFriend, when it slides over map when they are moving in live.
This is my view:
struct ContentView: View {
#StateObject var request = Calls()
#State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 56.946285, longitude: 24.105078), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
var body: some View {
Map(coordinateRegion: $mapRegion, annotationItems: $request.users){ $user in
withAnimation(.linear(duration: 2.0)) {
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: user.latitude, longitude: user.longitude)){
Circle()
}
}
}
}
}
And function call in view model, whitch changes user location, the response is just incoming string from API:
func collectUsers(_ response: String){
if users.count != 0{
var data = response.components(separatedBy: "\n")
data.removeLast()
let updates = self.users.map{ user -> User in
let newData = updateUserLocation(user: user, input: data)
return User(id: user.id, name: user.name, image: user.image, latitude: Double(newData[1])!, longitude: Double(newData[2])!)
}
DispatchQueue.main.async {
self.users = updates
}
}else{
var userData = response.components(separatedBy: ";")
userData.removeLast()
let users = userData.compactMap { userString -> User? in
let userProperties = userString.components(separatedBy: ",")
var idPart = userProperties[0].components(separatedBy: " ")
if idPart.count == 2{
idPart.removeFirst()
}
guard userProperties.count == 5 else { return nil }
guard let id = Int(idPart[0]),
let latitude = Double(userProperties[3]),
let longitude = Double(userProperties[4]) else { return nil }
return User(id: id, name: userProperties[1], image: userProperties[2], latitude: latitude, longitude: longitude)
}
DispatchQueue.main.async {
self.users = users
}
}
}
And ofcourse my #Published:
class Calls: ObservableObject{
#Published var users = [User]()
When I use the MapMarker instead of MapAnnotation the error does not appier. I would use marker, but I need each user view in map to be different.
If any one stumbles with the same issue. I spent entire day to solve this, but the awnser is that in Xcode 14 it is a bug. After I installer Xcode 13.4.1 error messages disappiered.
I'm new to Swift/SwiftUI, and so please forgive me if this is trivial.
In an app I am attempting to retrieve the user's location, and fetch nearby websites from Wikipedia to display. A simple example is below:
//
// ContentView.swift
// MRE
//
// Created by Philipp Maier on 2/25/22.
//
import SwiftUI
import CoreLocationUI
import MapKit
import Foundation
struct ContentView: View {
enum ViewState {
case waiting, fetching
}
#State private var viewState = ViewState.waiting
#StateObject private var viewModel = LocationViewModel()
// some values to initialize
#State var listEntries = [
Geosearch(pageid: 45348219,
title: "Addison Apartments",
lat: 35.21388888888889,
lon: -80.84472222222222,
dist: 363.7 ),
Geosearch(pageid: 35914731,
title: "Midtown Park (Charlotte, North Carolina)",
lat: 35.2108,
lon: -80.8363,
dist: 1034.5 )
]
var body: some View {
NavigationView{
VStack{
ZStack(alignment: .leading){
Map(coordinateRegion: $viewModel.mapRegion, showsUserLocation: true)
.frame(height: 300)
LocationButton(.currentLocation, action: {
viewModel.requestAllowLocationPermission()
viewState = .fetching
print("Button Press: Latitude: \(viewModel.mapRegion.center.latitude), Longitude: \(viewModel.mapRegion.center.longitude)")
})
}
HStack{
Text("Latitude: \(viewModel.mapRegion.center.latitude), Longitude: \(viewModel.mapRegion.center.longitude)")
}
switch viewState {
case .waiting:
Text("Waiting for your location")
Spacer()
case .fetching:
List{
ForEach(listEntries) {location in
NavigationLink(destination: Text("Target") ) {
HStack(alignment: .top){
Text(location.title)
}
}
.task{
await fetchNearbyLandmarks(lat: viewModel.mapRegion.center.latitude, lon: viewModel.mapRegion.center.longitude)
}
}
}
}
}
}
}
func fetchNearbyLandmarks(lat: Double, lon: Double) async {
let urlString = "https://en.wikipedia.org/w/api.php?action=query&list=geosearch&gscoord=\(lat)%7C\(lon)&gsradius=5000&gslimit=25&format=json"
print(urlString)
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let items = try JSONDecoder().decode(wikipediaResult.self, from: data)
listEntries = items.query.geosearch
print ("Loadingstate: Loaded")
} catch {
print ("Loadingstate: Failed")
}
}
}
final class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40, longitude: -80.5), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
}
func requestAllowLocationPermission() {
locationManager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let latestLocation = locations.first else {
print("Location Error #1")
return
}
DispatchQueue.main.async {
self.mapRegion = MKCoordinateRegion(center: latestLocation.coordinate, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location Error #2:")
print(error.localizedDescription)
}
}
struct wikipediaResult: Codable {
let batchcomplete: String
let query: Query
}
struct Query: Codable {
let geosearch: [ Geosearch ]
}
struct Geosearch: Codable, Identifiable {
var id: Int { pageid }
let pageid: Int
let title: String
let lat: Double
let lon: Double
let dist: Double
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The core function of the app works, in that once the user pushes the "Location" button, the list of Wikipedia pages underneath updates. I've also added to Text fields to track the center of the map as the user pans around.
Here's my issue: If I pan the map, the list of locations underneath does not update. However, once I click on a link to display the page, and hit the "back" button, the updated list is built "correctly", i.e. with the new coordinates.
I'm sure that I'm overlooking something simple, but how can I track panning of the user and dynamically adjust the list underneath "in real time"?
Thanks, Philipp
For being new to Swift/SwiftUI this is quite cool!
You want to refetch when the coordinates of your Map change. So you could use .onChange, e.g.on the ZStack of the Map:
.onChange(of: viewModel.mapRegion.center.latitude) { _ in
Task {
await fetchNearbyLandmarks(lat: viewModel.mapRegion.center.latitude, lon: viewModel.mapRegion.center.longitude)
}
}
Generally this works, but it might be fetching too often (on every change of latitude). So you might want to add some kind of delay, or try to figure out when the drag ended.
I am working on a SwiftUI project and want to place a map in a view that uses coordinates stored in Firestore. Apple's example for MapKit in SwiftUI uses static latitude and longitude parameters in the #State property and then binds the property to the Map() view.
struct BusinessMapView: View {
#State private var region: MKCoordinateRegion = {
var mapCoordinates = CLLocationCoordinate2D(latitude: 44.621754, longitude: -66.475873)
var mapZoomLevel = MKCoordinateSpan(latitudeDelta: 5.00, longitudeDelta: 5.00)
var mapRegion = MKCoordinateRegion(center: mapCoordinates, span: mapZoomLevel)
return mapRegion
}()
var body: some View {
Map(coordinateRegion: $region)
}
}
What I want to do is the following but clearly this is not allowed since you cannot access other properties in another property.
struct BusinessMapView: View {
#ObservedObject var businessAddressRowViewModel: BusinessAddressRowViewModel
#State private var region: MKCoordinateRegion = {
var mapCoordinates = CLLocationCoordinate2D(latitude: businessAddressRowViewModel.businessAddress.latitude, longitude: businessAddressRowViewModel.businessAddress.longitude)
var mapZoomLevel = MKCoordinateSpan(latitudeDelta: 5.00, longitudeDelta: 5.00)
var mapRegion = MKCoordinateRegion(center: mapCoordinates, span: mapZoomLevel)
return mapRegion
}()
var body: some View {
Map(coordinateRegion: $region)
}
}
So my question is, is there a way to set the coordinates from a database for a Map() in SwiftUI or is the only option to use static values for latitude and longitude?
EDIT ADDED FOR MORE INFORMATION
class BusinessAddressRowViewModel: ObservableObject, Identifiable {
// Properties
var id: String = ""
public static let shared = BusinessAddressRowViewModel()
// Published Properties
#Published var businessAddress: BusinessAddress
// Combine Cancellable
private var cancellables = Set<AnyCancellable>()
// Initializer
init(businessAddress: BusinessAddress) {
self.businessAddress = businessAddress
self.startCombine()
}
// Starting Combine
func startCombine() {
// Get Bank Account
$businessAddress
.receive(on: RunLoop.main)
.compactMap { businessAddress in
businessAddress.id
}
.assign(to: \.id, on: self)
.store(in: &cancellables)
}
}
The shared property gives an error stating the parameter businessAddress is missing.
The data is coming from Firebase Firestore here.
class BusinessAddressRepository: ObservableObject {
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistration?
#Published var businessAddresses = [BusinessAddress]()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
// Get the currentUserUid
guard let currentUserId = Auth.auth().currentUser else {
return
}
if snapshotListener == nil {
// Add a SnapshotListener to the BusinessAddress Collection.
self.snapshotListener = db.collection(FirestoreCollection.users).document(currentUserId.uid).collection(FirestoreCollection.businessAddresses).addSnapshotListener { (querySnapshot, error) in
// Check to see if an error occured and print it. IMPLEMENT ERROR HANDLING LATER
if let error = error {
print("Error getting documents: \(error)")
} else {
print("BusinessAddressRepository - snapshotListener called")
// Check to make sure the Collection contains Documents
guard let documents = querySnapshot?.documents else {
print("No Business Addresses.")
return
}
// Documents exist.
self.businessAddresses = documents.compactMap { businessAddress in
do {
return try businessAddress.data(as: BusinessAddress.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
func stopSnapshotListener() {
if snapshotListener != nil {
snapshotListener?.remove()
snapshotListener = nil
}
}
}
Data is being passed to BusinessAddressRowViewModel from the BusinessAddressViewModel. BusinessAddressView holds the list that creates all the rows.
class BusinessAddressViewModel: ObservableObject {
var businessAddressRepository: BusinessAddressRepository
// Published Properties
#Published var businessAddressRowViewModels = [BusinessAddressRowViewModel]()
// Combine Cancellable
private var cancellables = Set<AnyCancellable>()
// Intitalizer
init(businessAddressRepository: BusinessAddressRepository) {
self.businessAddressRepository = businessAddressRepository
self.startCombine()
}
// Starting Combine - Filter results for business addresses created by the current user only.
func startCombine() {
businessAddressRepository
.$businessAddresses
.receive(on: RunLoop.main)
.map { businessAddress in
businessAddress
.map { businessAddress in
BusinessAddressRowViewModel(businessAddress: businessAddress)
}
}
.assign(to: \.businessAddressRowViewModels, on: self)
.store(in: &cancellables)
}
}
You have an initialization problem here, having nothing to do with the Map(). You are trying to use businessCoordinates the instantiated ObservedObject variable in the initializer, and, I am sure, are getting a Cannot use instance member 'businessCoordinates' within property initializer; property initializers run before 'self' is available error.
If you don't need 'businessCoordinates' anywhere in the view, other than the data, I would recommend this:
class BusinessCoordinates: ObservableObject {
public static let shared = BusinessCoordinates()
...
}
This will give you a Singleton you can use at will. Then you use it like this:
struct BusinessMapView: View {
#State private var region: MKCoordinateRegion
init() {
let mapCoordinates = CLLocationCoordinate2D(latitude: BusinessCoordinates.shared.latitude, longitude: BusinessCoordinates.shared.longitude)
var mapZoomLevel = MKCoordinateSpan(latitudeDelta: 5.00, longitudeDelta: 5.00)
_region = State(initialValue: MKCoordinateRegion(center: mapCoordinates, span: mapZoomLevel))
}
var body: some View {
Map(coordinateRegion: $region)
}
}
I am currently using an api to grab the definitions for a specific word that the user has entered, and the api returns multiple definitions. I want the user to be able to choose what exact definition they want to pair a word with. Since I am interacting with an api, it is in a function and I cannot return anything out of it. I want to grab all the definitions and then show a new view where the user can pick the appropriate definition. How can I go about doing this? I've thought of making an ObservableObject that just has an array as a work around, but that seems a bit excessive. I am new to SwiftUI, so I am unsure whether or not this would be possible. However, I think it would not be because I am not trying to return a view anywhere or using any of the built in things that accepts views.
EDIT: I made SaveArray an ObservableObject and now my problem is that the object is not being updated by my getDef function call. Within the function it is but it is not editing the actual class or at least that is what it is looking like, because on my next view I have a foreach going through the array and nothing is displayed because it is empty. I am not sure whether that is because the sheet is being brought up before the getDef function is done executing.
struct AddWord: View {
#ObservedObject var book: Book
#ObservedObject var currentArray = SaveArray()
#State var addingDefinition = false
#State var word = ""
#State var definition = ""
#State var allDefinitions: [String] = []
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form {
TextField("Word: ", text: $word)
}
.navigationBarTitle("Add word")
.navigationBarItems(trailing: Button("Add") {
if self.word != "" {
book.words.append(self.word)
getDef(self.word, book, currentArray)
addingDefinition = true
self.presentationMode.wrappedValue.dismiss()
}
}).sheet(isPresented: $addingDefinition) {
PickDefinition(definitions: currentArray, book: book, word: self.word)
}
}
}
func getDef(_ word: String, _ book: Book, _ definitionsArray: SaveArray) {
let request = NSMutableURLRequest(url: NSURL(string: "https://wordsapiv1.p.rapidapi.com/words/\(word)")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
let dict = dictionary?["results"] as? [Any]
definitionsArray.currentArray = createArray((dict!))
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
func createArray(_ array: [Any]) -> [String] {
//Get all the definitions given from the api and put it into a string array so you can display it for user to select the correct definiton for their context
var definitions = [String]()
for object in array {
let dictDef = object as? [String: Any]
definitions.append(dictDef?["definition"] as! String)
}
return definitions
}
}
struct AddWord_Previews: PreviewProvider {
static var previews: some View {
AddWord(book: Book())
}
}
struct PickDefinition: View {
#ObservedObject var definitions: SaveArray
var book: Book
var word: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
ForEach(0 ..< definitions.currentArray.count) { index in
Button("\(self.definitions.currentArray[index])", action: {
print("hello")
DB_Manager().addWords(name: self.book.name, word: self.word, definition: self.definitions.currentArray[index])
book.definitions.append(self.definitions.currentArray[index])
self.presentationMode.wrappedValue.dismiss()
})
}
}
.navigationTitle("Choose")
}
}
}
struct PickDefinition_Previews: PreviewProvider {
static var previews: some View {
PickDefinition(definitions: SaveArray(), book: Book(), word: "")
}
}
If you can post more of your code, I can provide a fully working example (e.g. the sample JSON and the views/classes you have built). But for now, I am working with what you provided. I hope the below will help you see just how ObservableObject works.
#Published var dict = [String]() //If the api returns a list of strings, you can make this of type string - I do not have a sample of the JSON so I cannot be sure. If you can provide a sample of the JSON I can better define the way this should work.
var body: some View {
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
dict = dictionary?["results"] as? [Any] //note that here we are assigning the results of the api call to the Published variable, which our StateObject variable in ContentView is listening to!
var allDef = createArray((dict!))
//No longer need to pass this data forward (as you have below) since we are publishing the information!
//Pass the array to the new view where the user will select the one they want
PickDefinition(definitions: allDef, book: self.book, word: self.word)
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
}
struct ContentView: View {
//StateObject receives updates sent by the Published variable
#StateObject var dictArray = Api()
var body: some View {
NavigationView {
List {
ForEach(dictArray.dict.indices, id: \.self) { index in
Text(dictArray.dict[index])
}
}
}
}
}
Scenario:
I'm using an Observable Class to acquire data from the network.
In this case some elementary weather data.
Problem:
I don't know how to display this data in the calling View.
For the time-being, I merely am trying to populate a Textfield (and worry about more-eleborate layout later).
I get the following:
.../StandardWeatherView.swift:22:13: Cannot invoke initializer for
type 'TextField<_>' with an argument list of type '(Text, text:
Sample?)'
Here's is my calling View which is the receiver of #ObservedObject data:
import SwiftUI
struct StandardWeatherView: View {
#EnvironmentObject var settings: Settings
#ObservedObject var standardWeatherReportLoader = StandardWeatherReportLoader()
init() {
self.standardWeatherReportLoader.doStandard()
}
var body: some View {
ZStack {
Color("FernGreen").edgesIgnoringSafeArea(.all)
TextField(Text("Weather Data"), text: standardWeatherReportLoader.weatherReport)
}
}
}
struct StandardWeatherView_Previews: PreviewProvider {
static var previews: some View {
StandardWeatherView()
}
}
Here's the publisher, acquiring data:
import Foundation
class StandardWeatherReportLoader: ObservableObject {
#Published var networkMessage: String?
#Published var hasAlert = false
#Published var weatherReport: Sample?
#Published var hasReport = false
func doStandard() {
let url = EndPoint.weather.path()
var request = URLRequest(url: EndPoint.weather.path()!)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: url!) { (data: Data?, _: URLResponse?, error: Error?) -> Void in
DispatchQueue.main.async {
guard error == nil else {
self.networkMessage = error?.localizedDescription
self.hasAlert = true
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Sample.self, from: data!)
self.weatherReport = result
self.hasReport = true
print("\n Standard Weather ----------------")
print(#function, "line: ", #line, "Result: ",result)
print("\n")
} catch let error as NSError {
print(error)
}
}
}
task.resume()
}
}
What's the simplest way of passing a string of data to the View via #Published var?
Log:
Standard Weather ---------------- doStandard() line: 38 Result:
Sample(coord: DataTaskPubTab.Coord(lon: -0.13, lat: 51.51), weather:
[DataTaskPubTab.Weather(id: 300, main: "Drizzle", description: "light
intensity drizzle")], base: "stations", main:
DataTaskPubTab.Main(temp: 280.32, pressure: 1012, humidity: 81,
tempMin: 279.15, tempMax: 281.15), visibility: 10000, wind:
DataTaskPubTab.Wind(speed: 4.1, deg: 80), clouds:
DataTaskPubTab.Clouds(all: 90), dt: 1485789600.0, id: 2643743, name:
"London")
But I'm getting nil at the TextField:
(lldb) po standardWeatherReportLoader.weatherReport nil
One option is to set a binding within your body to track whenever the TextField has updated. From within this binding, you can then edit your Published variable as you wish:
#ObservedObject var reportLoader = StandardWeatherReportLoader()
var body: some View {
// Binding to detect when TextField changes
let textBinding = Binding<String>(get: {
self.reportLoader.networkMessage
}, set: {
self.reportLoader.networkMessage = $0
})
// Return view containing the text field
return VStack {
TextField("Enter the Network Message", text: textBinding)
}
}
Edit: Also in your original post, you were passing an object of optional type Sample into the TextField which was expecting a binding String type which could cause some issues.