How to change SwiftUI view if location services are not enabled - swiftui

How do you change the current view of a project when location services are not allowed by the user.
Currently I have it set up like this:
{
#Published var location: CLLocation? = nil
#Published var locationAllowed:Bool = 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()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthoization status: CLAuthorizationStatus, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
switch status {
case .restricted, .denied:
// Disable your app's location features
locationAllowed = false
break;
case .authorizedWhenInUse:
// Enable your app's location features.
locationAllowed = true
break;
case .authorizedAlways:
// Enable or prepare your app's location features that can run any time.
locationAllowed = true
break;
case .notDetermined:
locationAllowed = true
break;
}
self.location = location
}
}
Where the variable locationAllowed is a published variable. So how would I go about changing the view depending on the value of locationAllowed. I attempted to do it from the SceneDelagate:
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var contentView = ContentView().environment(\.managedObjectContext, managedObjectContext)
if (LocationManager().$locationAllowed == false){
contentView = LocationNotAllowedView().environment(\.managedObjectContext, managedObjectContext) }
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
This just threw a compiler error. My other idea was to have a state variable in the ContentView() and just change the view to my other view when it read false, but that didn't seem to work either.

you can solve it using optional view inside contentView since you had publisher
import SwiftUI
struct test: View {
#ObservedObject var worker = locationManager()
var body: some View {
HStack {
optionalLocation.init(allowed: worker.isAllowed)
}
}
}
struct optionalLocation: View {
var allowed: Bool?
var body: some View {
if let allowed = self.allowed, allowed {
return AnyView(
Text("Its Allowed!")
)
} else {
return AnyView(
Text("Not Allowed")
)
}
}
}

Related

Sunkit on SwiftUi: help needed to use CoreLocation to determine the sunrise, golden hour etc

Hello
I want to make an app to catch runrise and goldenhour in SwiftUI.
I don't have errors, but it doesn't work neither.
The result is saying the following:
Current Location Unknown
Time Zone Unknown
I use the SunKit package Dependency.
I don't get an error.
Dependency used is SunKit:
https://github.com/Sunlitt/SunKit
Help is appreciated. As it doesn't work
Normally I should have the core location to fill in the location and specify the hour.
But it aint happening.
I don't know what is happening.
import SwiftUI
import SunKit
import CoreLocation
class LocationManagerDelegate: NSObject, CLLocationManagerDelegate {
var contentView: ContentView?
var locationManager = CLLocationManager()
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
if let contentView = contentView {
do {
try contentView.sun.setLocation(location)
contentView.location = location
} catch let error {
print(error)
}
}
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if let error = error {
print(error)
return
}
if let placemarks = placemarks, let placemark = placemarks.first, let timeZone = placemark.timeZone {
if let contentView = self.contentView {
contentView.timeZone = timeZone
print("TimeZone: \(timeZone.identifier) offset : \(timeZone.secondsFromGMT())")
do { try contentView.sun.setTimeZone(Double(timeZone.secondsFromGMT()) / 3600.0)
} catch let error {
print(error)
}
}
}
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
}
}
}
struct ContentView: View {
let sun: Sun = Sun.common
let timeFormatter: DateFormatter = {
let tf = DateFormatter()
tf.dateFormat = "HH:mm"
return tf
}()
#State var locationManager = CLLocationManager()
#State var locationManagerDelegate = LocationManagerDelegate()
#State public var location: CLLocation?
#State public var timeZone: TimeZone?
init() {
locationManagerDelegate.contentView = self
locationManager.delegate = locationManagerDelegate
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
}
var body: some View {
VStack {
Text("Azimuth: \(sun.azimuth)°")
Text("Elevation: \(sun.elevation.degrees)°")
Text("Sunrise time: \(timeFormatter.string(from: sun.sunrise))")
if let location = location {
Text("Current Location: \(location.coordinate.latitude), \(location.coordinate.longitude)")
} else {
Text("Current Location: Unknown")
}
if let timeZone = timeZone {
Text("Time Zone: \(timeZone.abbreviation() ?? "Unknown")")
} else {
Text("Time Zone: Unknown")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Stil more details needed ?

Calling CLLocation manager hides view it was called from swiftui

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?

How to force re-create view in SwiftUI?

I made a view which fetches and shows a list of data. There is a context menu in toolbar where user can change data categories. This context menu lives outside of the list.
What I want to do is when user selects a category, the list should refetch data from backend and redraw entire of the view.
I made a BaseListView which can be reused in various screens in my app, and since the loadData is inside the BaseListView, I don't know how to invoke it to reload data.
Did I do this with good approaching? Is there any way to force SwiftUI recreates entire of view so that the BaseListView loads data & renders subviews as first time it's created?
struct ProductListView: View {
var body: some View {
BaseListView(
rowView: { ProductRowView(product: $0, searchText: $1)},
destView: { ProductDetailsView(product: $0) },
dataProvider: {(pageIndex, searchText, complete) in
return fetchProducts(pageIndex, searchText, complete)
})
.hideKeyboardOnDrag()
.toolbar {
ProductCategories()
}
.onReceive(self.userSettings.$selectedCategory) { category in
//TODO: Here I need to reload data & recreate entire of view.
}
.navigationTitle("Products")
}
}
extension ProductListView{
private func fetchProducts(_ pageIndex: Int,_ searchText: String, _ complete: #escaping ([Product], Bool) -> Void) -> AnyCancellable {
let accountId = Defaults.selectedAccountId ?? ""
let pageSize = 20
let query = AllProductsQuery(id: accountId,
pageIndex: pageIndex,
pageSize: pageSize,
search: searchText)
return Network.shared.client.fetchPublisher(query: query)
.sink{ completion in
switch completion {
case .failure(let error):
print(error)
case .finished:
print("Success")
}
} receiveValue: { response in
if let data = response.data?.getAllProducts{
let canLoadMore = (data.count ?? 0) > pageSize * pageIndex
let rows = data.rows
complete(rows, canLoadMore)
}
}
}
}
ProductCategory is a separated view:
struct ProductCategories: View {
#EnvironmentObject var userSettings: UserSettings
var categories = ["F&B", "Beauty", "Auto"]
var body: some View{
Menu {
ForEach(categories,id: \.self){ item in
Button(item, action: {
userSettings.selectedCategory = item
Defaults.selectedCategory = item
})
}
}
label: {
Text(self.userSettings.selectedCategory ?? "All")
.regularText()
.autocapitalization(.words)
.frame(maxWidth: .infinity)
}.onAppear {
userSettings.selectedCategory = Defaults.selectedCategory
}
}
}
Since my app has various list-view with same behaviours (Pagination, search, ...), I make a BaseListView like this:
struct BaseListView<RowData: StringComparable & Identifiable, RowView: View, Target: View>: View {
enum ListState {
case loading
case loadingMore
case loaded
case error(Error)
}
typealias DataCallback = ([RowData],_ canLoadMore: Bool) -> Void
#State var rows: [RowData] = Array()
#State var state: ListState = .loading
#State var searchText: String = ""
#State var pageIndex = 1
#State var canLoadMore = true
#State var cancellableSet = Set<AnyCancellable>()
#ObservedObject var searchBar = SearchBar()
#State var isLoading = false
let rowView: (RowData, String) -> RowView
let destView: (RowData) -> Target
let dataProvider: (_ page: Int,_ search: String, _ complete: #escaping DataCallback) -> AnyCancellable
var searchable: Bool?
var body: some View {
HStack{
content
}
.if(searchable != false){view in
view.add(searchBar)
}
.hideKeyboardOnDrag()
.onAppear(){
print("On appear")
searchBar.$text
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.sink { text in
print("Search bar updated")
self.state = .loading
self.pageIndex = 1
self.searchText = text
self.rows.removeAll()
self.loadData()
}.store(in: &cancellableSet)
}
}
private var content: some View{
switch state {
case .loading:
return Spinner(isAnimating: true, style: .large).eraseToAnyView()
case .error(let error):
print(error)
return Text("Unable to load data").eraseToAnyView()
case .loaded, .loadingMore:
return
ScrollView{
list(of: rows)
}
.eraseToAnyView()
}
}
private func list(of data: [RowData])-> some View{
LazyVStack{
let filteredData = rows.filter({
searchText.isEmpty || $0.contains(string: searchText)
})
ForEach(filteredData){ dataItem in
VStack{
//Row content:
if let target = destView(dataItem), !(target is EmptyView){
NavigationLink(destination: target){
row(dataItem)
}
}else{
row(dataItem)
}
//LoadingMore indicator
if case ListState.loadingMore = self.state{
if self.rows.isLastItem(dataItem){
Seperator(color: .gray)
LoadingView(withText: "Loading...")
}
}
}
}
}
}
private func row(_ dataItem: RowData) -> some View{
rowView(dataItem, searchText).onAppear(){
//Check if need to load next page of data
if rows.isLastItem(dataItem) && canLoadMore && !isLoading{
isLoading = true
state = .loadingMore
pageIndex += 1
print("Load page \(pageIndex)")
loadData()
}
}.padding(.horizontal)
}
private func loadData(){
dataProvider(pageIndex, searchText){ newData, canLoadMore in
self.state = .loaded
rows.append(contentsOf: newData)
self.canLoadMore = canLoadMore
isLoading = false
}
.store(in: &cancellableSet)
}
}
In your BaseListView you should have an onChange modifier that catches changes to userSettings.$selectedCategory and calls loadData there.
If you don't have access to userSettings in BaseListView, pass it in as a Binding or #EnvironmentObject.

Passing Lat/Long to function in SwiftUI

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

SwiftUI - Location in API call with CLLocationManager and CLGeocoder

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