Problem getting lat and lon from LocationManager SwiftUI - ios

I'm a beginner, have been working on a weather project, trying to get the lat and long from LocationManager in WeatherViewModel class and pass it the API so i can get the user location based data. but can't get the lattitude and longitude in the WeatherViewModel. the simulator shows a random temperature and city as globe.(SwiftUI and Xcode 11.6)
here's the LocationManager class
import Foundation
import CoreLocation
import Combine
class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {
private let manager: CLLocationManager
#Published var lastKnownLocation: CLLocation?
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .denied{
print("denied")
}
else{
print("athorized")
manager.requestLocation()
}
}
func start() {
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
}
init(manager: CLLocationManager = CLLocationManager()) {
self.manager = manager
super.init()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func startUpdating() {
self.manager.delegate = self
self.manager.requestWhenInUseAuthorization()
self.manager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
lastKnownLocation = locations.last
print("\((lastKnownLocation?.coordinate.latitude)!)")
print("\((lastKnownLocation?.coordinate.longitude)!)")
}
}
Here's the WeatherViewModel class
import SwiftUI
import Combine
import CoreLocation
class WeatherViewModel: ObservableObject {
#ObservedObject var location = LocationManager()
let client = OpenWeatherAPI()
var stateView: StateView = StateView.loading{
willSet{
objectWillChange.send() }
}
var currentWeather = CurrentWeather.emptyInit(){
willSet{
objectWillChange.send()
}
}
var todayWeather = ForecastWeather.emptyInit(){
willSet{
objectWillChange.send()
}
}
var hourlyWeathers: [ForecastWeather] = [] {
willSet{
objectWillChange.send()
}
}
var dailyWeathers: [ForecastWeather] = [] {
willSet{
objectWillChange.send()
}
}
var currentDescription = ""{
willSet{
objectWillChange.send()
}
}
private var stateCurrentWeather = StateView.loading
private var stateForecastWeather = StateView.loading
var latitude: String{
return "\(location.lastKnownLocation?.coordinate.latitude ?? 0.0)"
}
var longitude: String{
return "\(location.lastKnownLocation?.coordinate.longitude ?? 0.0)"
}
func printLatlon(){
print("WeatherViewModel")
print(latitude )
print(longitude )
}
init (){
getData()
printLatlon()
}
func retry(){
stateView = .loading
stateCurrentWeather = .loading
stateForecastWeather = .loading
}
private func getData() {
client.getCurrentWeather(at: latitude, at: longitude) { [weak self] currentWeather, error in
guard let ws = self else { return }
if let currentWeather = currentWeather {
ws.currentWeather = currentWeather
ws.todayWeather = currentWeather.getForecastWeather()
ws.currentDescription = currentWeather.description()
ws.stateCurrentWeather = .success
} else {
ws.stateCurrentWeather = .failed
}
ws.updateStateView()
}
client.getForecastWeather(at: latitude, at: longitude) { [weak self] forecastWeatherResponse, error in
guard let ws = self else { return }
if let forecastWeatherResponse = forecastWeatherResponse {
ws.hourlyWeathers = forecastWeatherResponse.list
ws.dailyWeathers = forecastWeatherResponse.dailyList
ws.stateForecastWeather = .success
} else {
ws.stateForecastWeather = .failed
}
ws.updateStateView()
}
}
private func updateStateView() {
if stateCurrentWeather == .success, stateForecastWeather == .success {
stateView = .success
}
if stateCurrentWeather == .failed, stateForecastWeather == .failed {
stateView = .failed
}
}
}
I'm passing the data in this
private func getData() {
client.getCurrentWeather(at: latitude, at: longitude) { }
client.getForecastWeather(at: latitude, at: longitude) { }
}
if i use this
var latitude: String{
return "\(location.lastKnownLocation?.coordinate.latitude ?? 0.0)"
}
var longitude: String{
return "\(location.lastKnownLocation?.coordinate.longitude ?? 0.0)"
}
Then i get this output in the simulator
the console shows this
the console shows that it didnt get the lat and lon
but if i give a string of lat and lon like this
var latitude = "36.778259"
var longitude = "-119.417931"
Then i get expected output in the simulator
console output
because I pass the string location and thats why showing my string values

Related

Implementing WeatherKit to widget - SwiftUI

So I have a widget for a weather app (the app currently doesn't use WeatherKit but I'd eventually like to move it over), but I want to implement a widget using WeatherKit beforehand. I made a practice app using WeatherKit and got all the components I'd want to use in the widget to work correctly.
Here's the entirety of the main app that works perfectly:
import SwiftUI
import CoreLocation
import WeatherKit
var userCity: String = ""
class LocationManager: NSObject, ObservableObject {
#Published var currentLocation: CLLocation?
#Published var userCity: String = ""
private let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last, currentLocation == nil else { return }
DispatchQueue.main.async {
self.currentLocation = location
}
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print(location)
guard error == nil else {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
guard placemarks!.count > 0 else {
print("Problem with the data received from geocoder")
return
}
let pm = placemarks![0].locality
print(pm!)
let userLocation = (pm!)
print(userLocation)
DispatchQueue.main.async {
self.userCity = userLocation
}
})
}
}
struct ContentView: View {
let weatherService = WeatherService.shared
#StateObject private var locationManager = LocationManager()
#State private var weather: Weather?
var body: some View {
VStack {
if let weather{
let celsiusWeather = weather.currentWeather.temperature.converted(to: .celsius)
let celsiusFormatted = String(format: "%.0f", celsiusWeather.value)
let celsiusFinal = "\(celsiusFormatted)°C"
let fahrenheitWeather = weather.currentWeather.temperature.converted(to: .fahrenheit)
let fahrenheitFormatted = String(format: "%.0f", fahrenheitWeather.value)
let fahrenheitFinal = "\(fahrenheitFormatted)°F"
VStack{
Image(systemName: weather.currentWeather.symbolName)
.resizable()
.symbolRenderingMode(.palette)
.scaledToFit()
.frame(width: 80, height: 80)
Text(locationManager.userCity)
.font(.largeTitle)
Text(fahrenheitFinal)
Text(celsiusFinal)
}
}
}
.padding()
.task(id: locationManager.currentLocation) {
do{
if let location = locationManager.currentLocation{
self.weather = try await weatherService.weather(for: location)
print(weather!)
}
}catch{
print(error)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Basically, I've just been trying to move this code over to the widget, unsure if that would even work properly (clearly something doesn't).
And here is what I've been trying to do with the widget, but it comes up blank, so something is coming up nil and can't retrieve the weather data. But Xcode doesn't give me any errors.
import WidgetKit
import SwiftUI
import WeatherKit
import CoreLocation
var userCity: String?
class LocationManager: NSObject, ObservableObject {
#Published var currentLocation: CLLocation?
#Published var userCity: String = ""
private let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last, currentLocation == nil else { return }
DispatchQueue.main.async {
self.currentLocation = location
}
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print(location)
guard error == nil else {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
guard placemarks!.count > 0 else {
print("Problem with the data received from geocoder")
return
}
let pm = placemarks![0].locality
print(pm!)
let userLocation = (pm!)
print(userLocation)
DispatchQueue.main.async {
self.userCity = userLocation
}
})
}
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), city: "", temp: "0") // placeholder humidity
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), city: "", temp: "0") // placeholder humidity
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let locationManager = LocationManager()
#State var currentWeather: Weather?
let nextUpdate = Date().addingTimeInterval(1800)
Task {
do{
if let location = locationManager.currentLocation{
let currentWeather = try await WeatherService.shared.weather(for: location)
print(currentWeather)
}
}catch{
print(error)
}
}
let entry = SimpleEntry(date: .now, city: userCity!, temp: (currentWeather?.currentWeather.temperature.description)!)
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let city: String
let temp: String
}
struct WidgetWeatherTestWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack{
Text(entry.city)
Text(entry.temp)
}
}
}
struct ShitsdagWidget: Widget {
let kind: String = "WidgetWeatherTest"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
WidgetWeatherTestWidgetEntryView(entry: entry)
}
.configurationDisplayName("Widget Weather Test")
.description("Testing weatherkit in a Widget")
.supportedFamilies([.systemSmall])
}
}
Because widgets are set up slightly differently than just SwiftUI with the timeline and everything, I might just have things in the wrong spot to be triggered in the right order. Any help would be great.

How to run .task() at least once every hour

In my main view MainView() I load weather data from WeatherKit asynchronously via .task() which runs whenever the user's location changes:
.task(id: locationManager.currentLocation){
if let location = locationManager.currentLocation{
weather = try await weatherService.weather(for: location)
}
}
However, the user's location may not change for a long period of time or even never and I would like for the weather data to be at least updated once every hour.
How would I update WeatherKit data every hour AND update every single time their location changes.
Obviously I can’t rely on the WeatherKit weather object within the ‘.task(id:)’ to update as that is the object that I’d need to update.
for fetching location after every few mins what approach I have used is
class BackgroundLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
static let shared = BackgroundLocationManager()
private var locationManager: CLLocationManager!
private var timer: DispatchSourceTimer?
private var counter: Int = 0
#Published var location: LocationModel? = nil
private override init() {
super.init()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.allowsBackgroundLocationUpdates = true
locationManager.requestAlwaysAuthorization()
}
private func startTimer(delay: Int = 15) {
let queue = DispatchQueue(label: "com.example.timer", qos: .background)
timer = DispatchSource.makeTimerSource(queue: queue)
timer?.schedule(deadline: .now(), repeating: .seconds(delay))
timer?.setEventHandler { [weak self] in
self?.locationManager.startUpdatingLocation()
self?.counter = 0
}
timer?.resume()
}
func fetchLocation(interval: Int? = nil) {
if let interval = interval {
startTimer(delay: interval)
} else {
self.locationManager.startUpdatingLocation()
}
}
func stopFetchingLocation() {
timer?.cancel()
timer = nil
}
}
extension BackgroundLocationManager {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
break
case .restricted, .denied:
// do something when permission is denied
case .authorizedAlways, .authorizedWhenInUse, .authorized:
// do something when permission is given
break
#unknown default:
return
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
if let error = error as? CLError, error.code == .denied {
// do something when unable to fetch location
return
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.counter += 1
locationManager.stopUpdatingLocation()
let currentLocation = locations.last?.coordinate
let long = String(currentLocation?.longitude ?? 0)
let lat = String(currentLocation?.latitude ?? 0)
let verticalAcc = String(locations.last?.verticalAccuracy ?? 0)
let horizontalAcc = String(locations.last?.horizontalAccuracy ?? 0)
let altitude = String(locations.last?.altitude ?? 0)
let location = LocationModel(longitude: long, latitude: lat,
verticalAccuracy: verticalAcc,
horizontalAccuracy: horizontalAcc, alitude: altitude)
if counter <= 1 {
self.location = location
}
}
}
To use it according to my need
struct MainView: View {
#StateObject var vm = MainViewModel()
init() {
BackgroundLocationManager.shared.fetchLocation() // when needed once
BackgroundLocationManager.shared.fetchLocation(interval: 15) // needed after how many seconds
}
var body: some View {
ZStack {
MyCustomeView()
}
.onReceive(BackgroundLocationManager.shared.$location) { location in
vm.doSomething(location: location)
}
}
}
Use a timer inside your view.
struct WeatherView: View {
let weatherService: WeatherService
let location: Location
#State var weather: Weather?
let timer = Timer.publish(every: 3600, on: .main, in: .common).autoconnect()
var body: some View {
Text(weather.description)
.onReceive(timer) { _ in
fetch()
}
}
func fetch() {
Task {
weather = try await weatherService.weather(for: location)
}
}
}

How would I convert a city name to CLLocation

I am using WeatherKit and am trying to convert a city name to a CLLocation so I can pass it to the weatherService but I am not sure how to do this. I know there is a geocodeAddressString but I wasn't able to get it to work and I don't think it would return a CLLocation. Along with that I am also having trouble with getting location data from multiple locations. I am confused on what type of array I would make meaning would I make an array of Weather objects of an array of just city names and then convert them to coordinates when I need to get the weather data. My code that I have so far is below. Any help with these would be great.
WeatherKitTestApp
'''
import SwiftUI
import WeatherKit
#main
struct WeatherKitTestApp: App {
let weatherService = WeatherService.shared
#StateObject private var locationManager = LocationManager()
var body: some Scene {
WindowGroup {
ContentView(weatherService: weatherService, locationManager: locationManager)
}
}
}
'''
ContentView
'''
import SwiftUI
import WeatherKit
struct ContentView: View {
let weatherService: WeatherService
#ObservedObject var locationManager: LocationManager
#State private var weather: Weather?
#State var searchLocation: String = ""
var body: some View {
NavigationView {
if locationManager.weatherLocations.isEmpty {
Text("Add a location")
} else {
TabView {
ForEach(locationManager.weatherLocations, id: \.self) { location in
VStack {
HStack {
TextField("Weather Location", text: $searchLocation)
.padding(.horizontal)
.frame(width: 200)
.background {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color(UIColor.lightGray))
}
Button(action: {
locationManager.weatherLocations.append(searchLocation)
searchLocation = ""
print(locationManager.weatherLocations)
}) {
Image(systemName: "plus")
}
}
.padding(.horizontal)
if let weather {
VStack {
Text("\(location)")
Text("\(weather.currentWeather.date.formatted())")
Text(weather.currentWeather.temperature.formatted())
}
}
}
.task(id: locationManager.currentLocation) {
do {
if let location = locationManager.currentLocation {
self.weather = try await weatherService.weather(for: location)
}
} catch {
print(error)
}
}
}
}
.tabViewStyle(.page)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(weatherService: WeatherService(), locationManager: LocationManager())
}
}
'''
LocationManager
'''
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject {
#Published var currentLocation: CLLocation? {
didSet {
if let currentLocation {
resolveLocationName(with: currentLocation) { locationName in
self.weatherLocations.append(locationName!)
}
}
}
}
#Published var city: String = ""
private let locationManager = CLLocationManager()
#Published var weatherLocations: [String] = []
override init() {
super.init()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
public func resolveLocationName(with location: CLLocation, completion: #escaping ((String?) -> Void)) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
guard let place = placemarks?.first, error == nil else {
completion(nil)
return
}
var name = ""
if let locality = place.locality {
name += locality
}
if let adminRegion = place.administrativeArea {
name += ", \(adminRegion)"
}
completion(name)
}
}
func getCoordinate(addressString: String, completionHandler: #escaping(CLLocationCoordinate2D, NSError?) -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?[0] {
let location = placemark.location!
completionHandler(location.coordinate, nil)
return
}
}
completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
}
}
// Another attempt at trying to convert city name to CLLocation
/*
private func getLocation() {
CLGeocoder().geocodeAddressString(city, completionHandler: {(placemarks, error) in
if error != nil {
print("Error: ", error!)
}
if let placemark = placemarks?.first {
let location = placemark.location!.coordinate
return
}
})
}
*/
func appendLocation(location: String) {
weatherLocations.append(location)
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last, currentLocation == nil else { return }
DispatchQueue.main.async {
self.currentLocation = location
}
manager.stopUpdatingLocation()
}
}
'''
to convert an address, eg a city name (Tokyo) to a CLLocation try this example code, works for me:
import SwiftUI
import CoreLocation
struct ContentView: View {
#State var location: CLLocation?
var body: some View {
Text("Tokyo \(location?.coordinate.latitude ?? 0) , \(location?.coordinate.longitude ?? 0)")
.onAppear {
getLocation("Tokyo") { loc in
location = loc
}
}
}
func getLocation(_ adres: String, completion: #escaping (CLLocation?) -> Void) {
CLGeocoder().geocodeAddressString(adres) { placemarks, error in
if error != nil {
print("error: \(error as Optional)")
} else {
if let placemark = placemarks?.first,
let coord = placemark.location?.coordinate {
return completion(CLLocation(latitude: coord.latitude, longitude: coord.longitude))
}
}
return completion(nil)
}
}

Get API Coordinates and Show city in SwiftUI

I call an API and get a response with a longitude and latitude. Now, I'm trying to show and update the city name. I made a function ("place") to convert the longitude and latitude to a city name. Unfortunately is doesn't work. Please, who could give me a help on this one? Thanks in advance.
Below I will show the code. I filled the struct "APIcoord" with lat and lon example coordinates.
public final class WeatherService: NSObject {
private let locationManager = CLLocationManager()
private var completionHandler: ((Weather) -> Void)?
public override init() {
super.init()
locationManager.delegate = self
}
public func loadWeatherData(_ completionhandler: #escaping((Weather) -> Void)) {
self.completionHandler = completionhandler
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
private func makeDataRequest(forCoordinates coorodinates: CLLocationCoordinate2D) {
}
}
extension WeatherService: CLLocationManagerDelegate {
public func locationManager(
_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]
) {
guard let location = locations.first else {return}
makeDataRequest(forCoordinates: location.coordinate)
}
public func locationManager(
_ manager: CLLocationManager, didFailWithError error: Error
) {
print("Something went wrong: \(error.localizedDescription)")
}
}
struct APIResponse: Decodable {
let coord: APIcoord
}
struct APIcoord: Decodable{
let lon: Double = -122.031
let lat: Double = 37.33
}
public struct Weather {
let longtitude: Double
let latitude: Double
init(response: APIResponse) {
longtitude = response.coord.lon
latitude = response.coord.lat
}
}
public class WeaterViewModel: ObservableObject {
#Published var longtitude: Double = 0.0
#Published var latitude: Double = 0.0
public let weatherService: WeatherService
public init(weatherService: WeatherService) {
self.weatherService = weatherService
}
public func refresh() {
weatherService.loadWeatherData { weather in
DispatchQueue.main.sync {
self.longtitude = weather.longtitude
self.latitude = weather.latitude
}
}
}
}
struct ContentView: View {
#ObservedObject var viewModel: WeaterViewModel
#State var placeFound: String?
var body: some View {
NavigationView{
ZStack{
VStack {
Text(placeFound ?? "No Place Found")
.onAppear(){
place(APIlatitude: viewModel.latitude, APIlongitude: viewModel.longtitude){(city) in
placeFound = city
print("Place: \(String(describing: city))" )
}
}
}
.onAppear{
viewModel.refresh()
}
}
.navigationBarHidden(true)
}
}
func place(APIlatitude : Double, APIlongitude : Double, completion: #escaping (String?) -> Void) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: APIlatitude, longitude: APIlongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, _) -> Void in
placemarks?.forEach { (placemark) in
if let city = placemark.locality {
print(city)
completion(city)
}
else{
print("Can't find place")
}
}
})
}
}
You could pair GLGeocoder's async API with SwiftUI's task like this:
struct ContentView: View {
#StateObject var locator = Locator()
var body: some View {
if let location = locator.location {
ContentView2(location: location)
}
else {
Text("Hello, world!")
.padding()
}
}
}
struct ContentView2Config {
let geocoder = CLGeocoder()
var text = Self.noCityText
static let noCityText = "No City"
mutating func reverseGeocode(location: Location) async {
text = Self.noCityText
let location = CLLocation(latitude: location.lat, longitude: location.lon)
if let city = try? await geocoder.reverseGeocodeLocation(location)
.first
.flatMap({ placemark in
placemark.locality
})
{
text = city
}
else {
text = Self.noCityText
}
}
}
struct ContentView2: View {
let location: Location
#State var config = ContentView2Config()
var body: some View {
Text(config.text)
.task(id: location.id) {
await config.reverseGeocode(location: location)
}
}
}
And in case it helps, here is the location manager:
struct Location: Identifiable {
let id = UUID()
let lat: Double
let lon: Double
}
class Locator: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var location: Location?
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
locationManager.startUpdatingLocation()
locationManager.requestWhenInUseAuthorization()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let coordinate = locations.last?.coordinate else {
location = nil
return
}
location = Location(lat: coordinate.latitude, lon: coordinate.longitude)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
location = nil
}
}

How to wait for another value to be assigned to run .onAppear?

I am learning to develop apps for iOS from scratch. And I chose SwiftUI to make an app that gets the location of the user, get with geocode the city where he is and with that information obtained, show a list of items that belong to that city from a API.
So, I learned on one hand how to get the location and on the other hand how to display a list. My problem now is that when you run .onAppear(perform: loadData) to display my list, the "city" result is still empty. Evidently the value of city is obtained after I try to display the list of the city.
Both the algorithm I have to get the location and the one I have to show the list work separately.
So my code is:
import SwiftUI
struct Response: Codable {
var cinemas: [Cinema]
}
struct Cinema: Codable {
var _id: String
var cinemaName: String
var cinemaCategory: String
}
struct HomeScreenView: View {
#State var results = [Cinema]()
#ObservedObject var lm = LocationManager()
var latitude: String {
return("\(lm.location?.latitude ?? 0)") }
var longitude: String { return("\(lm.location?.longitude ?? 0)") }
var placemark: String { return("\(lm.placemark?.description ?? "XXX")") }
var status: String { return("\(lm.status)") }
var city: String {
return("\(lm.placemark?.locality ?? "empty")")
}
var body: some View {
VStack {
List(results, id: \._id) { item in
VStack(alignment: .leading) {
Text(item.cinemaName)
.font(.headline)
Text(item.cinemaCategory)
}
}.onAppear(perform: loadData)
}
}
func loadData() {
guard let url = URL(string: "https://mycinemasapi.com/cinemasbycity/\(self.city)") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.cinemas
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
UPDATE:
LocationManager class
import Foundation
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var status: CLAuthorizationStatus? {
willSet { objectWillChange.send() }
}
#Published var location: CLLocation? {
willSet { objectWillChange.send() }
}
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private func geocode() {
guard let location = self.location else { return }
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
if error == nil {
self.placemark = places?[0]
} else {
self.placemark = nil
}
})
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.status = status
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.location = location
self.geocode()
}
}
As by your code just do load data on placemark received, like
List(results, id: \._id) { item in
VStack(alignment: .leading) {
Text(item.cinemaName)
.font(.headline)
Text(item.cinemaCategory)
}
}.onReceive(lm.$placemark) {
if $0 != nil {
self.loadData()
}
}

Resources