I am building a SwiftUI app that shows data based on user lat/long. I have based my code off of this sample provided by the framework dev.
With SwiftUI I have my LocationManager set as:
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate{
#Published var userLatitude: Double = 0.0
#Published var userLongitude: Double = 0.0
private let locationManager = CLLocationManager()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.distanceFilter = 100.0
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
userLatitude = location.coordinate.latitude
userLongitude = location.coordinate.longitude
print("Hello I'm here! \(location)")
}
}
Whenever I go back to my ContentView and try to read the Lat/Long it just shows up as 0.0. but if I output them within the body the values show up correctly.
struct ContentView: View {
#State private var times = prayerTimes()
#ObservedObject var locationViewModel = LocationViewModel()
var body: some View {
NavigationView {
PrayerTimeView(times: $times)
.navigationBarTitle(Text("Prayer Times"))
}
}
static func prayerTimes() -> PrayerTimes? {
let cal = Calendar(identifier: Calendar.Identifier.gregorian)
let date = cal.dateComponents([.year, .month, .day], from: Date())
let coordinates = Coordinates(latitude: locationViewMode.userLatitude, longitude: locationViewMode.userLongitude)
var params = CalculationMethod.moonsightingCommittee.params
params.madhab = .hanafi
return PrayerTimes(coordinates: coordinates, date: date, calculationParameters: params)
}
}
prayerTimes() only call once when you init the view. Why don't you make times as a #Published of your ViewModel. When location changes, just update that value.
PrayerTimeView(times: $viewmodel.times)
.navigationBarTitle(Text("Prayer Times"))
Related
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 want to print the date, however, it states the sam until I refresh the app. this is my code
struct Dates: View {
func todayDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-dd'T'HH:mm:ssZ"
let today = Date() /// Date() is the current date
let todayAsString = dateFormatter.string(from: today)
return todayAsString
}
var body: some View {
Text(todayDate())
Would anyone know why this is happening?
Display Live Time in SwiftUI
You need to create a Timer with the time interval you want to update the Date label. In this case we will update the label every 1 second. First, the ViewModel (the Timer cannot be declared inside the SwiftUI View because it is an struct):
class ViewModel: ObservableObject {
#Published var currentTime = ""
var timer = Timer()
init() {
let repeatEveryXSeconds: TimeInterval = 1
timer = Timer.scheduledTimer(withTimeInterval: repeatEveryXSeconds, repeats: true, block: { [weak self] timer in
self?.currentTime = DateFormatter.myFormatter.string(from: Date())
})
}
}
extension DateFormatter {
static let myFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm:ss"
return formatter
}()
}
And the view itself:
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
Text("Date is: \(viewModel.currentTime)")
.frame(width: 200)
.padding()
}
}
You can use also CalendarUserNotifications triggers: https://developer.apple.com/documentation/usernotifications/scheduling_a_notification_locally_from_your_app
I am using Xcode 12.
I am able to show a map with a region, but with hard-coded values.
Instead, I want to set the region of the map based on the user's current location.
I have a LocationManager class, which gets the user's location and publish it.
I have a ShowMapView SwiftUI View that observes an object based on the LocationManager class to get the user's location.
But, I don't know how to use the data from the locationManager object to set the region used by the map.
Here is the LocationManager class, which gets the user's location and publishes it.
import Foundation
import MapKit
final class LocationManager: NSObject, ObservableObject {
#Published var location: CLLocation?
private let locationManager = CLLocationManager()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.distanceFilter = kCLDistanceFilterNone
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
self.location = location
}
}
}
Here is the ShowMapView SwiftUI View, which needs to get the user's location that's published and set the region used by the map. As you can see, the values are hard-coded for now.
import Combine
import MapKit
import SwiftUI
struct AnnotationItem: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
struct ShowMapView: View {
#ObservedObject private var locationManager = LocationManager()
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 38.898150, longitude: -77.034340),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
)
var body: some View {
Map(coordinateRegion: $region, annotationItems: [AnnotationItem(name: "Home", coordinate: CLLocationCoordinate2D(latitude: self.locationManager.location!.coordinate.latitude, longitude: self.locationManager.location!.coordinate.longitude))]) {
MapPin(coordinate: $0.coordinate)
}
.frame(height: 300)
}
}
Here's one possible solution to this:
final class LocationManager: NSObject, ObservableObject {
#Published var location: CLLocation?
#Published var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 38.898150, longitude: -77.034340),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
)
private var hasSetRegion = false
private let locationManager = CLLocationManager()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.distanceFilter = kCLDistanceFilterNone
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
self.location = location
if !hasSetRegion {
self.region = MKCoordinateRegion(center: location.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
hasSetRegion = true
}
}
}
}
struct ShowMapView: View {
#ObservedObject private var locationManager = LocationManager()
var homeLocation : [AnnotationItem] {
guard let location = locationManager.location?.coordinate else {
return []
}
return [.init(name: "Home", coordinate: location)]
}
var body: some View {
Map(coordinateRegion: $locationManager.region, annotationItems: homeLocation) {
MapPin(coordinate: $0.coordinate)
}
.frame(height: 300)
}
}
In this solution, the region is published by the location manager. As soon as a location is received, the region is centered on that spot (in didUpdateLocations). Then, a boolean flag is set saying the region has been centered initially. After that boolean is set, it no longer updates the region. This will let the user still drag/zoom, etc.
I also changed your code for putting down the pin a little bit. You were force-unwrapping location, which is nil until the first location is set by the location manager, causing a crash. In my edit, it just returns an empty array of annotation items if there isn't a location yet.
I have a method in my class that opens a map when given an address string. Trying to show an alert in a view by toggling a boolean in a method in the class. I can't figure out how to toggle the boolean in the class method. This is what I tried. The Published bool in class method updates but does not update in the View. I did put up a repo of just this feature if anybody wants to play around with it.
https://github.com/Ongomobile/LocationTest/tree/main/LocationTest
import SwiftUI
#main
struct LocationTestApp: App {
var body: some Scene {
WindowGroup {
ContentView(location: LocationManager())
}
}
}
Here is my Class:
import UIKit
import MapKit
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject {
var locationManager = CLLocationManager()
lazy var geocoder = CLGeocoder()
#Published var locationString = "1140"
// #Published var locationString = "1 apple park way cupertino"
#Published var currentAddress = ""
#Published var isValid: Bool = true
func openMapWithAddress () {
geocoder.geocodeAddressString(locationString) { placemarks, error in
if let error = error {
self.isValid = false
// prints false but does not update
print("isValid")
print(error.localizedDescription)
}
guard let placemark = placemarks?.first else {
return
}
guard let lat = placemark.location?.coordinate.latitude else{return}
guard let lon = placemark.location?.coordinate.longitude else{return}
let coords = CLLocationCoordinate2DMake(lat, lon)
let place = MKPlacemark(coordinate: coords)
let mapItem = MKMapItem(placemark: place)
mapItem.name = self.locationString
mapItem.openInMaps(launchOptions: nil)
}
}
}
Here is the view:
import SwiftUI
struct ContentView: View {
#ObservedObject var locationManager = LocationManager()
#State private var showingAlert = false
var body: some View {
Button {
locationManager.openMapWithAddress()
} label: {
Text("Get Map")
}
.alert(isPresented: $showingAlert) {
Alert(title: Text("Important message"), message:
Text("Enter a valid address"), dismissButton:
.default(Text("OK")))
}
}
}
I updated this answer to reflect some refactor help that I got from #rlong405 I put up a repository with this solution maybe it could help others.
OpenMapsInSwiftUI
import SwiftUI
struct ContentView: View {
#ObservedObject var locationManager = LocationManager()
var body: some View {
VStack{
Form{
Section {
Text("Enter Address")
TextField("", text: $locationManager.locationString)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal)
}
Button {
locationManager.openMapWithAddress()
} label: {
Text("Get Map")
}
.alert(isPresented: $locationManager.invalid) {
Alert(title: Text("Important message"), message:
Text("Enter a valid address"), dismissButton:
.default(Text("OK"), action:{
locationManager.invalid = false
locationManager.locationString = ""
}))
}
}
}
}
}
import UIKit
import MapKit
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject {
lazy var geocoder = CLGeocoder()
#Published var locationString = ""
#Published var invalid: Bool = false
func openMapWithAddress () {
geocoder.geocodeAddressString(locationString) { placemarks, error in
if let error = error {
DispatchQueue.main.async {
self.invalid = true
}
print(error.localizedDescription)
}
guard let placemark = placemarks?.first else {
return
}
guard let lat = placemark.location?.coordinate.latitude else{return}
guard let lon = placemark.location?.coordinate.longitude else{return}
let coords = CLLocationCoordinate2DMake(lat, lon)
let place = MKPlacemark(coordinate: coords)
let mapItem = MKMapItem(placemark: place)
mapItem.name = self.locationString
mapItem.openInMaps(launchOptions: nil)
}
}
}
I'm struggling with this for a long time without finding where I'm wrong (I know I'm wrong).
I have one API call with the location of the phone (this one is working), but I want the same API call with a manual location entered by a textfield (using Geocoding for retrieving Lat/Long). The geocoding part is ok and updated but not passed in the API call.
I also want this API call to be triggered when the TextField is cleared by the dedicated button back with the phone location.
Please, what am I missing? Thanks for your help.
UPDATE: This works on Xcode 12.2 beta 2 and should work on Xcode 12.0.1
This is the code:
My Model
import Foundation
struct MyModel: Codable {
let value: Double
}
My ViewModel
import Foundation
import SwiftUI
import Combine
final class MyViewModel: ObservableObject {
#Published var state = State.ready
#Published var value: MyModel = MyModel(value: 0.0)
#Published var manualLocation: String {
didSet {
UserDefaults.standard.set(manualLocation, forKey: "manualLocation")
}
}
#EnvironmentObject var coordinates: Coordinates
init() {
manualLocation = UserDefaults.standard.string(forKey: "manualLocation") ?? ""
}
enum State {
case ready
case loading(Cancellable)
case loaded
case error(Error)
}
private var url: URL {
get {
return URL(string: "https://myapi.com&lat=\(coordinates.latitude)&lon=\(coordinates.longitude)")!
}
}
let urlSession = URLSession.shared
var dataTask: AnyPublisher<MyModel, Error> {
self.urlSession
.dataTaskPublisher(for: self.url)
.map { $0.data }
.decode(type: MyModel.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
func load(){
assert(Thread.isMainThread)
self.state = .loading(self.dataTask.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("⚠️ API Call finished")
break
case let .failure(error):
print("❌ API Call failure")
self.state = .error(error)
}
},
receiveValue: { value in
self.state = .loaded
self.value = value
print("👍 API Call loaded")
}
))
}
}
The Location Manager
import Foundation
import SwiftUI
import Combine
import CoreLocation
import MapKit
final class Coordinates: NSObject, ObservableObject {
#EnvironmentObject var myViewModel: MyViewModel
#Published var latitude: Double = 0.0
#Published var longitude: Double = 0.0
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
deinit {
locationManager.stopUpdatingLocation()
}
}
extension Coordinates: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
self.placemark = places?[0]
})
self.locationManager.stopUpdatingLocation()
}
}
extension Coordinates {
func getLocation(from address: String, completion: #escaping (_ location: CLLocationCoordinate2D?)-> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location?.coordinate else {
completion(nil)
return
}
completion(location)
}
}
}
The View
import Foundation
import SwiftUI
struct MyView: View {
#EnvironmentObject var myViewModel: MyViewModel
#EnvironmentObject var coordinates: Coordinates
private var icon: Image { return Image(systemName: "location.fill") }
var body: some View {
VStack{
VStack{
Text("\(icon) \(coordinates.placemark?.locality ?? "Unknown location")")
Text("Latitude: \(coordinates.latitude)")
Text("Longitude: \(coordinates.longitude)")
}
VStack{
Text("UV Index: \(myViewModel.value.value)")
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
HStack{
TextField("Manual location", text: $myViewModel.manualLocation)
if !myViewModel.manualLocation.isEmpty{
Button(action: { clear() }) { Image(systemName: "xmark.circle.fill").foregroundColor(.gray) }
}
}
}.padding()
}
func commit() {
coordinates.getLocation(from: self.myViewModel.manualLocation) { places in
coordinates.latitude = places?.latitude ?? 0.0
coordinates.longitude = places?.longitude ?? 0.0
}
myViewModel.load()
}
func clear() {
myViewModel.manualLocation = ""
myViewModel.load()
}
}