I have a LocationService
public class LocationService: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published public var authorizationStatus: CLAuthorizationStatus
private let locationManager = CLLocationManager()
private let geoCoder = CLGeocoder()
public var locationNotRestricted: Bool {
NSLocale.current.regionCode == "SA"
}
public override init() {
authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
public func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
switch authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
locationManager.startUpdatingLocation()
default:
break
}
}
public func openAppSettings() {
if let url = URL(string: UIApplication.openSettingsURLString) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}
}
And when requestPermission is called it shows a view that was behind the EnableLocationScene in the stack on now above EnableLocationScene.
This is the RegisterCoordinator where LocationService is used and shows the EnableLocationScene which triggers requestPermission:
public struct RegisterCoordinator: View {
#ObservedObject var viewModel: RegisterCoordinatorViewModel
var onFinish: () -> Void
public init(onFinish: #escaping () -> Void) {
self.viewModel = RegisterCoordinatorViewModel()
self.onFinish = onFinish
}
public var body: some View {
Router($viewModel.routes) { screen, value in
switch screen {
case .setupPasscode:
locationCheck
}
}
}
private var locationCheck: AnyView {
switch viewModel.locationService.authorizationStatus {
case .notDetermined:
let text = "These are important so we can tell you where transactions are done and also track what shops are near you and what country you are in.\nYou can also opt out anytime."
return AnyView(EnableLocationScene(text: text, buttonAction: viewModel.locationService.requestPermission))
case .restricted:
let text = "Location use is restricted. Please enable location services in settings."
return AnyView(EnableLocationScene(text: text, buttonText: "Open Settings", buttonAction: viewModel.locationService.openAppSettings))
case .denied:
let text = "The app does not have location permissions. Please enable location services in settings."
return AnyView(EnableLocationScene(text: text, buttonText: "Open Settings", buttonAction: viewModel.locationService.openAppSettings))
case .authorizedAlways, .authorizedWhenInUse:
if LocalFeatureFlag.bypassLocation || viewModel.locationService.locationNotRestricted {
return AnyView(SetupPasscodeScene(onSubmit: { _ in viewModel.routeToSetupPasscode() }))
} else {
let text = "You are in a restricted location"
return AnyView(EnableLocationScene(text: text, showButton: false, buttonAction: { }))
}
default:
let text = "Unexpected Location status, Please enable location services in settings."
return AnyView(EnableLocationScene(text: text, buttonText: "Open Settings", buttonAction: viewModel.locationService.openAppSettings))
}
}
}
and the RegisterCoordinatorViewModel:
class RegisterCoordinatorViewModel: ObservableObject {
#Published var routes: Routes<RegisterScreen>
#StateObject var locationService = LocationService()
var state = RegisterCoordinatorViewModelState()
init() {
self.routes = [.root(.phoneNumber, embedInNavigationView: true)]
}
func routeToSetupPasscode() {
routes.push(.setupPasscode)
}
Ive tried initialising the locationService in the view to stop this, but it didnt help
This doesnt happen when i initialise the LocationService but when i call requestPermission
If i look at the view stack in the view hierachy inspector it shows everything as i would expect it in the correct order
can anyone see why this is happening?
Related
I have a CurrentValueSubject to hold data received from Firebase fetch request.
final class CardRepository: ObservableObject {
private let store = Firestore.firestore()
var resultSubject = CurrentValueSubject<[Card], Error>([])
init() {
}
func get() {
store.collection(StorageCollection.EnglishCard.getPath)
.addSnapshotListener { [unowned self] snapshot, err in
if let err = err {
resultSubject.send(completion: .failure(err))
}
if let snapshot = snapshot {
let cards = snapshot.documents.compactMap {
try? $0.data(as: Card.self)
}
resultSubject.send(cards)
}
}
}
}
In my ViewModel, I want whenever resultSubject sends or emits a value. It will change the state and has that value attached to the succes state.
class CardViewModel: CardViewModelProtocol, ObservableObject {
#Published var repository: CardRepository
#Published private(set) var state: CardViewModelState = .loading
private var cancellables: Set<AnyCancellable> = []
required init (_ repository: CardRepository) {
self.repository = repository
bindingCards()
}
private func bindingCards() {
let _ = repository.resultSubject
.sink { [unowned self] comp in
switch comp {
case .failure(let err):
self.state = .failed(err: err)
case .finished:
print("finised")
}
} receiveValue: { [unowned self] res in
self.state = .success(cards: res)
}
}
func add(_ card: Card) {
repository.add(card)
}
func get() {
repository.get()
}
}
On my ContentView, it will display a button that print the result.
struct ContentView: View {
#StateObject var viewModel = CardViewModel(CardRepository())
var body: some View {
Group {
switch viewModel.state {
case .loading:
ProgressView()
Text("Loading")
case .success(cards: let cards):
let data = cards
Button {
print(data)
} label: {
Text("Tap to show cards")
}
case .failed(err: let err):
Button {
print(err)
} label: {
Text("Retry")
}
}
Button {
viewModel.get()
} label: {
Text("Retry")
}
}.onAppear {viewModel.get() }
}
}
My problem is the block below only trigger once when I first bind it to the resultSubject.
} receiveValue: { [unowned self] res in
self.state = .success(cards: res)
}
I did add a debug and resultSubject.send(cards) works every time.
You need to store the Cancellable returned from the .sink in the class so it doesn't get deallocated:
Either in a set var cancellables = Set<AnyCancellable>() if you want to use multiple Publishers, or in var cancellable: AnyCancellable?.
Add .store(in &cancellables) like so:
} receiveValue: { [unowned self] res in
self.state = .success(cards: res)
}.store(in: &cancellables)
Edit:
In ObservableObject classes we don't use sink, we assign to an #Published:
let _ = repository.resultSubject
.assign(to: &$self.state)
I have 2 tabs and the associated views are tabAView and tabBView.
On tabAView, 1 API call is there and got user object which is Published object in its ViewModel. ViewModel name is UserViewModel. UserViewModel is being observed by tabAView.
On tabBView, I have to use that user object. Because on some actions, user object value is changed, those changes should be reflected on subsequent views.
I am confused about the environment object usage here. Please suggest what will be the best approach.
Here is the code to understand better my problem.
struct ContentView: View {
enum AppPage: Int {
case TabA=0, TabB=1
}
#StateObject var settings = Settings()
var viewModel: UserViewModel
var body: some View {
NavigationView {
TabView(selection: $settings.tabItem) {
TabAView(viewModel: viewModel)
.tabItem {
Text("TabA")
}
.tag(AppPage.TabA)
AppsView()
.tabItem {
Text("Apps")
}
.tag(AppPage.TabB)
}
.accentColor(.white)
.edgesIgnoringSafeArea(.top)
.onAppear(perform: {
settings.tabItem = .TabA
})
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(settings)
}
}
This is TabAView:
struct TabAView: View {
#ObservedObject var viewModel: UserViewModel
#EnvironmentObject var settings: Settings
init(viewModel: UserViewModel) {
self.viewModel = viewModel
}
var body: some View {
Vstack {
/// code
}
.onAppear(perform: {
/// code
})
.environmentObject(settings)
}
}
This is the UserViewModel where API is hit and user object comes:
class UserViewModel: ObservableObject {
private var apiService = APIService.shared
#Published var user: EndUserData?
init () {
getUserProfile()
}
func getUserProfile() {
apiService.getUserAccount() { user in
DispatchQueue.main.async {
self.user = user
}
}
}
}
Below is the APIService function, where the user object is saved into UserDefaults for use. Which I know is incorrect.(That is why I am looking for another solution). Hiding the URL, because of its confidential.
func getUserAccount(completion: #escaping (EndUserData?) -> Void) {
self.apiManager.makeRequest(toURL: url, withHttpMethod: .get) { results in
guard let response = results.response else { return completion(nil) }
if response.httpStatusCode == 200 {
guard let data = results.data else { return completion(nil) }
do {
let str = String(decoding: data, as: UTF8.self)
print(str)
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
UserDefaults.standard.set(data, forKey: "Account")
completion(responseData.data)
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
return completion(nil)
}
}
}
}
This is another TabBView:
struct TabBView: View {
var user: EndUserData?
init() {
do {
guard let data = UserDefaults.standard.data(forKey: "Account") else {
return
}
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
user = responseData.data
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
}
}
var body: some View {
VStack (spacing: 10) {
UserSearch()
}
}
}
This is another view in TabBView, where the User object is used. Changes are not reflecting here.
struct UserSearch: View {
private var user: EndUserData?
init(comingFromAppsSection: Bool) {
do {
guard let data = UserDefaults.standard.data(forKey: "Account") else {
return
}
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
user = responseData.data
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
}
}
var body: some View {
Vstack {
Text(user.status)
}
}
}
I have removed most of the code from a confidential point of view but this code will explain the reason and error. Please look into the code and help me.
I am trying to wrap NSComboBox with NSViewRepresentable for use in SwiftUI. I would like to pass in both the list of dropdown options and the text value of the combo box as bindings. I would like the text value binding to update on every keystroke, and on selection of one of the dropdown options. I would also like the text value/selection of the combo box to change if binding is changed externally.
Right now, I am not seeing my binding update on option selection, let alone every keystroke, as demonstrated by the SwiftUI preview at the bottom of the code.
My latest lead from reading old documentation is that maybe in an NSComboBox the selection value and the text value are two different properties and I've written this wrapping as if they are one and the same? Trying to run that down. For my purposes, they will be one and the same, or at least only the text value will matter: it is a form field for arbitrary user string input, that also has some preset strings.
Here is the code. I think this should be paste-able into a Mac-platform playground file:
import AppKit
import SwiftUI
public struct ComboBoxRepresentable: NSViewRepresentable {
private var options: Binding<[String]>
private var text: Binding<String>
public init(options: Binding<[String]>, text: Binding<String>) {
self.options = options
self.text = text
}
public func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.delegate = context.coordinator
comboBox.usesDataSource = true
comboBox.dataSource = context.coordinator
return comboBox
}
public func updateNSView(_ comboBox: NSComboBox, context: Context) {
comboBox.stringValue = text.wrappedValue
comboBox.reloadData()
}
}
public extension ComboBoxRepresentable {
final class Coordinator: NSObject {
var options: Binding<[String]>
var text: Binding<String>
init(options: Binding<[String]>, text: Binding<String>) {
self.options = options
self.text = text
}
}
func makeCoordinator() -> Coordinator {
Coordinator(options: options, text: text)
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDelegate {
public func comboBoxSelectionDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox else { return }
text.wrappedValue = comboBox.stringValue
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDataSource {
public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
guard options.wrappedValue.indices.contains(index) else { return nil }
return options.wrappedValue[index]
}
public func numberOfItems(in comboBox: NSComboBox) -> Int {
options.wrappedValue.count
}
}
#if DEBUG
struct ComboBoxRepresentablePreviewWrapper: View {
#State private var text = "four"
var body: some View {
VStack {
Text("selection: \(text)")
ComboBoxRepresentable(
options: .constant(["one", "two", "three"]),
text: $text
)
}
}
}
struct ComboBoxRepresentable_Previews: PreviewProvider {
#State private var text = ""
static var previews: some View {
ComboBoxRepresentablePreviewWrapper()
.frame(width: 200, height: 100)
}
}
#endif
Thank you in advance if you have any suggestions!
public struct ComboBoxRepresentable: NSViewRepresentable {
//If the options change the parent should be an #State or another source of truth if they don't change just remove the #Binding
#Binding private var options: [String]
#Binding private var text: String
public init(options: Binding<[String]>, text: Binding<String>) {
self._options = options
self._text = text
}
public func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.delegate = context.coordinator
comboBox.usesDataSource = true
comboBox.dataSource = context.coordinator
comboBox.stringValue = text
comboBox.reloadData()
return comboBox
}
public func updateNSView(_ comboBox: NSComboBox, context: Context) {
//You don't need anything here the delegate updates text and the combobox is already updated
}
}
public extension ComboBoxRepresentable {
final class Coordinator: NSObject {
//This is a much simpler init and injects the new values directly int he View vs losing properties in a class updates can be unreliable
var parent: ComboBoxRepresentable
init(_ parent: ComboBoxRepresentable) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDelegate {
public func comboBoxSelectionDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox else { return }
//It is a known issue that this has to be ran async for it to have the current value
//https://stackoverflow.com/questions/5265260/comboboxselectiondidchange-gives-me-previously-selected-value
DispatchQueue.main.async {
self.parent.text = comboBox.stringValue
}
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDataSource {
public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
guard parent.options.indices.contains(index) else { return nil }
return parent.options[index]
}
public func numberOfItems(in comboBox: NSComboBox) -> Int {
parent.options.count
}
}
#if DEBUG
struct ComboBoxRepresentablePreviewWrapper: View {
#State private var text = "four"
//If they dont update remove the #Binding
#State private var options = ["one", "two", "three"]
var body: some View {
VStack {
Text("selection: \(text)")
ComboBoxRepresentable(
options: $options,
text: $text
)
}
}
}
struct ComboBoxRepresentable_Previews: PreviewProvider {
#State private var text = ""
static var previews: some View {
ComboBoxRepresentablePreviewWrapper()
.frame(width: 200, height: 100)
}
}
#endif
Okay, I think I have come to a solution that satisfies the requirements I laid out in the question:
public struct ComboBoxRepresentable: NSViewRepresentable {
private let title: String
private var text: Binding<String>
private var options: Binding<[String]>
private var onEditingChanged: (Bool) -> Void
public init(
_ title: String,
text: Binding<String>,
options: Binding<[String]>,
onEditingChanged: #escaping (Bool) -> Void = { _ in }
) {
self.title = title
self.text = text
self.options = options
self.onEditingChanged = onEditingChanged
}
public func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.delegate = context.coordinator
comboBox.usesDataSource = true
comboBox.dataSource = context.coordinator
comboBox.placeholderString = title
comboBox.completes = true
return comboBox
}
public func updateNSView(_ comboBox: NSComboBox, context: Context) {
comboBox.stringValue = text.wrappedValue
comboBox.reloadData()
}
}
public extension ComboBoxRepresentable {
final class Coordinator: NSObject {
private var parent: ComboBoxRepresentable
init(parent: ComboBoxRepresentable) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDelegate {
public func comboBoxSelectionDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox,
parent.options.wrappedValue.indices.contains(comboBox.indexOfSelectedItem) else { return }
parent.text.wrappedValue = parent.options.wrappedValue[comboBox.indexOfSelectedItem]
}
public func controlTextDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox else { return }
parent.text.wrappedValue = comboBox.stringValue
}
public func controlTextDidBeginEditing(_ notification: Notification) {
parent.onEditingChanged(true)
}
public func controlTextDidEndEditing(_ notification: Notification) {
parent.onEditingChanged(false)
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDataSource {
public func comboBox(_ comboBox: NSComboBox, completedString string: String) -> String? {
parent.options.wrappedValue.first { $0.hasPrefix(string) }
}
public func comboBox(_ comboBox: NSComboBox, indexOfItemWithStringValue string: String) -> Int {
guard let index = parent.options.wrappedValue.firstIndex(of: string) else { return NSNotFound }
return index
}
public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
guard parent.options.wrappedValue.indices.contains(index) else { return nil }
return parent.options.wrappedValue[index]
}
public func numberOfItems(in comboBox: NSComboBox) -> Int {
parent.options.wrappedValue.count
}
}
On the point about updating the bound value as the user types, to get that you have implement the parent NSTextField delegate method controlTextDidChange.
And then in comboBoxSelectionDidChange, you need to update the bound value from the bound options using the combo box's newly-selected index.
I am a beginner so sorry for my question.
I have a map and annotations in a Datas file.
Now I would like the user to be able to register an address with a form.
How do I go from a normal address to a
location: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522)
Thanks for your help
Core Location has a "geocoder", which is what would take a street address and convert it to a CLLocationCoordinate2D.
This is from the official Apple documentation (https://developer.apple.com/documentation/corelocation/converting_between_coordinates_and_user-friendly_place_names):
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?)
}
}
Note that the preceding function uses callback functions and is an asynchronous function. That means that it does not return immediately -- instead, it finishes at an indeterminate time in the future and calls the callback function when it is finished.
Since you tagged the question SwiftUI, the following is a trivial example with SwiftUI:
import SwiftUI
import MapKit
class LocationManager : ObservableObject {
#Published var location : CLLocationCoordinate2D?
func convertAddress(address: String) {
getCoordinate(addressString: address) { (location, error) in
if error != nil {
//handle error
return
}
DispatchQueue.main.async {
self.location = location
}
}
}
private 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?)
}
}
}
struct ContentView: View {
#State var addressString = "1600 Pennsylvania Ave, Washington D.C."
#StateObject private var locationManager = LocationManager()
var body: some View {
VStack {
Text("Address: \(addressString)")
if let location = locationManager.location {
Text("Lat: \(location.latitude) Long: \(location.longitude)")
}
}.onAppear {
locationManager.convertAddress(address: addressString)
}
}
}
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()
}
}