I am trying to implement location-triggered-notification in an iOS app, which I recently started using SwiftUI.
As a first experimenting step, I have a button. And when tapped it should create a notification intented to fire once I get close to a certain location. As a starting point I read this page.
But it is not all crystal clear when putting it into practice.
The location part is working but not the notification one.
Though I have used CoreLocation and UserNotifications before, I have no previous experience with location triggered notifications.
Hereafter is the relevant (not working) code that I have at this point.
Any tip or advice, in the right direction to make it work will be welcome.
import SwiftUI
import MapKit
struct ContentView: View {
......
func handleShareLocation() {
print(#function)
//TRIAL-CODE
guard CLLocationManager.locationServicesEnabled() else {
return
}
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .sound, .badge],
completionHandler: { _,_ in //[weak self] granted, _ in
/*guard granted else {
self?.delegate?.notificationPermissionDenied()
return
}*/
let notificationInfo = LocationNotificationInfo (notificationId: "nyc_promenade_notification_id",
locationId: "nyc_promenade_location_id",
radius: 500.0,
latitude: 40.696503,
longitude: -73.997809,
title: "Welcome to the Brooklyn Promenade!",
body: "Tap to see more information",
data: ["location": "NYC Brooklyn Promenade"])
self?.requestNotification(notificationInfo: notificationInfo)
})
//TRIAL-CODE
}
var body: some View {
VStack {
Spacer()
Button(action: {
self.handleShareLocation()
}) {
Text("Create location notification")
.padding()
}
Spacer()
}
}
}
Beside, I am also using the class:
class LocationNotificationScheduler: NSObject {
........
}
coming from the document I mentioned.
In there two unclear points are:
Where is LocationNotificationSchedulerDelegate coming from?
What is notificationScheduled?
Those two points are causing error messages like:
Use of undeclared type 'LocationNotificationSchedulerDelegate'
Let me know if more information is needed.
Related
I have a function that checks whether the user has enabled their push notification/ notification alert in the iphone. If the user has turned on their notification, it will print true in the swift view, and if the user has turned off their notification, it will print false.
I could achieve this using the stated below function and gives relevant output. But, this is not real-time. I have to close and re-open the app to reflect current changes. Im looking way to give true/false real-time and async using publisher and subscriber which show status realtime in my app
My code :-
var isRegister : Bool = false
func isEnabled() -> Bool {
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { (settings) in
if(settings.authorizationStatus == .authorized) {
isRegister = true
print("Push notification is enabled")
} else {
isRegister = false
print("Push notification is not enabled")
}
}
return isRegister
}
there are a delay in getting authorization status of notification
so you should have to use completion
func authorizeStatus(completion: #escaping (_ isAuthorized: Bool) -> Void) {
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { (settings) in
if(settings.authorizationStatus == .authorized) {
completion(true)
print("Push notification is enabled")
} else {
completion(false)
print("Push notification is not enabled")
}
}
}
and how to check
override func viewDidLoad() {
super.viewDidLoad()
authorizeStatus { isAuthorized in
print(isAuthorized)
}
}
and if you want check status when click on Allow or Cancel of Notification Alert you can check it as follow
UNUserNotificationCenter.current().requestAuthorization(options: [ .sound, .badge]) { (success, error) in
if let error = error {
print("Request Authorization Failed (\(error), \(error.localizedDescription))")
}
else{
print(success) // check here
if success {
print("you enable it")
} else {
print("you cancel it")
}
}
}
how to check in swift ui
var body: some View {
VStack {
.onAppear { self. authorizeStatus { isAuthorized in
print(isAuthorized)
}
}
}
how to check in swift ui
var body: some View {
VStack {
.onAppear { self. authorizeStatus { isAuthorized in
print(isAuthorized)
}
}
}
Im trying to add text componenet to display text in my app. These solution does display in the log of xcode but no in the app. There is no way to use Text() as its bool value.
Any way to display text message on the screen directly and in the realtime
I have coded an application that needs the permissions of the photo library. The worry is that it needs it as soon as the application is launched so it must check at the start of the application the permission granted and if it is not determined, ask for it, wait for the result and change the view again! I've been breaking my head for three days, but I can't do it! Can you help me?
Here is my code:
Content view :
struct ContentView: View {
var auth = false
#State var limitedAlert = false
#State var statusPhoto = 0
#State var AuthPictStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
var body: some View {
VStack{}
.task{
while true {
AuthPictStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
}
}
if AuthPictStatus == .authorized {
CardView(canExecute: true)
}
if AuthPictStatus == .denied {
PhotoLybrairyDenied()
}
if AuthPictStatus == .limited {
CardView(canExecute: true)
.onAppear{limitedAlert = true}
.alert(isPresented: $limitedAlert) {
Alert(
title: Text("L'accès au photos est limitées !"),
message: Text("Votre autorisations ne nous permets d'accèder a seulement certaines photos ! De se fait, nous ne pouvons pas trier l'intégralité de vos photos !"),
primaryButton: .destructive(Text("Continuer malgré tout"), action: {
}),
secondaryButton: .default(Text("Modifier l'autorisation"), action: { // 1
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
return
}
if UIApplication.shared.canOpenURL(settingsUrl) {
UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
print("Settings opened: \(success)") // Prints true
})
}
})
)
}
}
if AuthPictStatus == .notDetermined {
CardView(canExecute: false)
.blur(radius: 5)
.disabled(true)
}
}
}
PhotoDeleteApp :
//
// PhotoDeleteApp.swift
// PhotoDelete
//
// Created by Rémy on 09/04/2022.
//
import SwiftUI
import Photos
#main
struct PhotoDeleteApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear{
PHPhotoLibrary.requestAuthorization({status in })
}
}
}
}
//
// PhotoDeleteApp.swift
// PhotoDelete
//
// Created by Rémy on 09/04/2022.
//
import SwiftUI
import Photos
#main
struct PhotoDeleteApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear{
PHPhotoLibrary.requestAuthorization({status in })
}
}
}
}
I dealt with similar headaches related to photo permissions (I'm on SwiftUI and iOS 14). I did some troubleshooting and found that using a SwiftUIViewDelegate to implement custom authorization status wasnt working as expected. I did a test and selected "only selected photos" (which should make the authorization status .limited). Except the authorization status wasn't limited, but .authorized, and .denied only when I denied access entirely.
So in my SwiftUI app, I gave up trying to be fancy and used the package Permissions SwiftUI. It provides some documentation on how to customize the message, but it handles the permissions using a sheet making the implementation carefree (It does not address .limited case either or the bug I described with the user selecting limited being actually full access).
My implementation looks like this, and is in my entry point #main, under the first View HomeTabView(homeViewModel: homeViewModel) inside the WindowGroup { }
.JMModal(showModal: $homeViewModel.showingPermissionsSelector, for: [.photo], autoDismiss: true, autoCheckAuthorization: false, restrictDismissal: false)
.changeHeaderTo("App Permissions")
.changeHeaderDescriptionTo("Export and Import of images requires photos access.")
.changeBottomDescriptionTo("Allowing the app to import photos provides the app access to airdropped and saved user photos.")
I suggest you try it out and see if it's good enough for your purposes.
To include the Permissions SwiftUI package in your project, go to your Project in the left panel, select "Project", go to "Package Dependencies" on the top bar, press the + button, and search for Permissions SwiftUI, there will be many options but only add PermissionsSwiftUIPhoto. If you need other permissions, there are plenty to choose from.
I have the permissions bound to a "import photos" button (in a subview) hence HomeTabViewModel belongs to parent
Button(action: {
let authorization = PHPhotoLibrary.authorizationStatus()
print("switching on image authorization status: \(authorization)")
switch authorization {
case .notDetermined:
parent.homeVM.showingPermissionsSelector = true
case .restricted:
parent.homeVM.showingPermissionsSelector = true
case .denied:
parent.homeVM.showingPermissionsSelector = true
case .authorized:
parent.homeVM.showingImagePicker = true
case .limited:
parent.homeVM.showingImagePicker = true // I've never reached this case (bug?)
#unknown default:
print("unhandled authorization status")
break
}
my homeViewModel (simplified for example)
import SwiftUI
final class HomeTabViewModel: ObservableObject {
#Published var showingPermissionsSelector = false
#Published var showingImagePicker = false
// #Published var showingLimitedSelector = false // Thought I would need this but I dont because there is no differentiation between .authorized and .denied from my testing
}
but you could have your app do an .onAppear { // check auth status and change $homeViewModel.showingPermissionsSelector based on your code's logic}
I dealt with the same problem you are having Rémy, and on one hand I'm glad I dont have to differentiate between .limited and .authorized since it makes it easier for us, but also it's a bit spooky because it means photo authorization is not quite working as expected on iOS...
In iOS 14, It could display ATT (App Tracking Transparency) dialog when app starts in SwiftUI as follows.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
if #available(iOS 14, *) {
ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
// loadAd()
})
} else {
// loadAd()
}
return true
}
But, in iOS 15.0, it does not work. Apple document describes as follows.
Calls to the API only prompt when the application state is: UIApplicationStateActive. Calls to the API through an app extension do not prompt.
https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/3547037-requesttrackingauthorization
How to display ATT dialog when the app starts in iOS 15 ?
2021/9/28 update
I solved it as follows.
struct HomeView: View {
var body: some View {
VStack {
Text("Hello!")
}.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in })
}
}
}
Instead of calling app tracking transparency permission in didFinishLaunchingWithOptions call in applicationDidBecomeActive it will solve your issue
In AppDelegate
func applicationDidBecomeActive(_ application: UIApplication) {
requestDataPermission()
}
func requestDataPermission() {
if #available(iOS 14, *) {
ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
switch status {
case .authorized:
// Tracking authorization dialog was shown
// and we are authorized
print("Authorized")
case .denied:
// Tracking authorization dialog was
// shown and permission is denied
print("Denied")
case .notDetermined:
// Tracking authorization dialog has not been shown
print("Not Determined")
case .restricted:
print("Restricted")
#unknown default:
print("Unknown")
}
})
} else {
//you got permission to track, iOS 14 is not yet installed
}
}
in info.plist
<key>NSUserTrackingUsageDescription</key>
<string>Reason_for_data_tracking</string>
As #donchan already mentioned use the following code:
struct HomeView: View {
var body: some View {
VStack {
Text("Hello!")
}.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in })
}
}
}
For iOS 15, I faced the same problems and I fixed them by delaying code execution for a second.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if #available(iOS 14, *) {
ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
DispatchQueue.main.async {
self.bannerView.load(GADRequest())
self.interstitial.load(request)
}
})
} else {
// Fallback on earlier versions
self.bannerView.load(GADRequest())
self.interstitial.load(request)
}
}
A very important addition to all the answers above: the ATT dialogue must be invoked once!
If, for example, inside the advertising manager you have repeated calls to the ATT dialog before requesting an advertisement (as it was for previous OS versions), then the dialog WILL NOT be shown! Therefore, the ATT dialogue request must be inserted directly into the view and with a delay of at least 1 second for its unconditional triggering.
If you are writing a SwiftUI app, you can trigger it on your start screen.
struct HomeView: View {
var body: some View {
VStack {
Text("Hello!")
}.onAppear {
ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in })
}
}
}
Do not forget to add the necessary additions to the .plist.
<key>NSUserTrackingUsageDescription</key>
<string>...</string>
Thus, it will run on the simulator or real device.
I'm very much new to ios && using cocoapods.
I scanned over SO to find easiest way to detect network status and lots of answers directed me to Reachability git by AshleyMills.
I'm writing my webView app in swiftui & I want to pop up an alert/notifier when user's internet connection is lost. (So they don't sit idle while my webview tries to load)
It would be best if listner to network changes keeps running in the background while the app is on.
Most of the answers in SO seem to be Swift-based (appdelegate, ViewDidload, etc), which I don't know how to use because I started off with SwiftUI
Thanks in advance.
Edit(With attempts for Workaround that Lukas provided)
I tried the below. It compiles and runs BUT the alert wouldn't show up. Nor does it respond to connection change.
I have number of subViews in my ContentView. So I called timer && reachability on app level.
#State var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let reachability = try! Reachability()
#State private var showAlert: Bool = false
It seems to be not working:
WindowGroup {
ContentView()
.onAppear() {
if !isConnected() {
self.showAlert = true
}
}
.onReceive(timer) { _ in
if !isConnected() {
self.showAlert = true
}else{
self.showAlert = false
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Error"), message: Text("Your internet connection is too slow."), dismissButton: .default(Text("ok")))
}
}
NWPathMonitor was introduced in iOS 12 as a replacement for Reachability. A naive implementation would be as follows.
We could expose the status that comes back from pathUpdateHandler, however that would require you to import Network everywhere you wanted to use the status - not ideal. It would be better to create your own enum that maps to each of the cases provided by NWPath.Status, you can see the values here. I’ve created one that just handles the connected or unconnected states.
So we create a very simple ObservedObject that publishes our status. Note that as the monitor operates on its own queue and we may want to update things in the view we will need to make sure that we publish on the main queue.
import Network
import SwiftUI
// An enum to handle the network status
enum NetworkStatus: String {
case connected
case disconnected
}
class Monitor: ObservableObject {
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "Monitor")
#Published var status: NetworkStatus = .connected
init() {
monitor.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
// Monitor runs on a background thread so we need to publish
// on the main thread
DispatchQueue.main.async {
if path.status == .satisfied {
print("We're connected!")
self.status = .connected
} else {
print("No connection.")
self.status = .disconnected
}
}
}
monitor.start(queue: queue)
}
}
Then we can use our Monitor class how we like, you could use it as a StateObject as I have done below, or you could use it as an EnvironmentObject. The choice is yours. Ideally you should have only a single instance of this class in your app.
struct ContentView: View {
#StateObject var monitor = Monitor()
// #EnvironmentObject var monitor: Monitor
var body: some View {
Text(monitor.status.rawValue)
}
}
Tested in Playgrounds on an iPad Pro running iOS 14.2, and on Xcode 12.2 using iPhone X (real device) on iOS 14.3.
I worked one time with rechabiltiy and had the same problem. My solution is a work-around but in my case it was fine.
When the user get to the view where the connection should be constantly checked you can start a time(https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-a-timer-with-swiftui). The timer calls a function in an interval of your choice where you can check the connection with the following code:
let reachability = try! Reachability()
...
func isConnected() -> Bool{
if reachability.connection == .none{
return "false" //no Connection
}
return true
}
}
You can also call this function .onAppear
I am redeveloping an android app for iOS with SwiftUI that contains a countdown feature. When the countdown finishes the user should be noticed about the end of the countdown. The Notification should be somewhat intrusive and work in different scenarios e.g. when the user is not actively using the phone, when the user is using my app and when the user is using another app. I decided to realize this using Local Notifications, which is the working approach for android. (If this approach is totally wrong, please tell me and what would be best practice)
However I am stuck receiving the notification when the user IS CURRENTLY using my app. The Notification is only being shown in message center (where all notifications queue) , but not actively popping up.
Heres my code so far:
The User is being asked for permission to use notifications in my CountdownOrTimerSheet struct (that is being called from a different View as actionSheet):
/**
asks for permission to show notifications, (only once) if user denied there is no information about this , it is just not grantedand the user then has to go to settings to allow notifications
if permission is granted it returns true
*/
func askForNotificationPermission(userGrantedPremission: #escaping (Bool)->())
{
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
userGrantedPremission(true)
} else if let error = error {
userGrantedPremission(false)
}
}
}
Only if the user allows permission for notification my TimerView struct is being called
askForNotificationPermission() { (success) -> () in
if success
{
// permission granted
...
// passing information about the countdown duration and others..
...
userConfirmedSelection = true // indicates to calling view onDismiss that user wishes to start a countdown
showSheetView = false // closes this actionSheet
}
else
{
// permission denied
showNotificationPermissionIsNeededButton = true
}
}
from the previous View
.sheet(isPresented: $showCountDownOrTimerSheet, onDismiss: {
// what to do when sheet was dismissed
if userConfirmedChange
{
// go to timer activity and pass startTimerInformation to activity
programmaticNavigationDestination = .timer
}
}) {
CountdownOrTimerSheet(startTimerInformation: Binding($startTimerInformation)!, showSheetView: $showCountDownOrTimerSheet, userConfirmedSelection: $userConfirmedChange)
}
...
NavigationLink("timer", destination:
TimerView(...),
tag: .timer, selection: $programmaticNavigationDestination)
.frame(width: 0, height: 0)
In my TimerView's init the notification is finally registered
self.endDate = Date().fromTimeMillis(timeMillis: timerServiceRelevantVars.endOfCountDownInMilliseconds_date)
// set a countdown Finished notification to the end of countdown
let calendar = Calendar.current
let notificationComponents = calendar.dateComponents([.hour, .minute, .second], from: endDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: notificationComponents, repeats: false)
let content = UNMutableNotificationContent()
content.title = "Countdown Finished"
content.subtitle = "the countdown finished"
content.sound = UNNotificationSound.defaultCritical
// choose a random identifier
let request2 = UNNotificationRequest(identifier: "endCountdown", content: content, trigger: trigger)
// add the notification request
UNUserNotificationCenter.current().add(request2)
{
(error) in
if let error = error
{
print("Uh oh! We had an error: \(error)")
}
}
As mentioned above the notification gets shown as expected when the user is everyWhere but my own app. TimerView however displays information about the countdown and is preferably the active view on the users device. Therefore I need to be able to receive the notification here, but also everywhere else in my app, because the user could also navigate somewhere else within my app. How can this be accomplished?
In this example a similar thing has been accomplished, unfortunately not written in swiftUI but in the previous common language. I do not understand how this was accomplished, or how to accomplish this.. I did not find anything on this on the internet.. I hope you can help me out.
With reference to the documentation:
Scheduling and Handling Local Notifications
On the section about Handling Notifications When Your App Is in the Foreground:
If a notification arrives while your app is in the foreground, you can
silence that notification or tell the system to continue to display
the notification interface. The system silences notifications for
foreground apps by default, delivering the notification’s data
directly to your app...
Acording to that, you must implement a delegate for UNUserNotificationCenter and call the completionHandler telling how you want the notification to be handled.
I suggest you something like this, where on AppDelegate you assign the delegate for UNUserNotificationCenter since documentation says it must be done before application finishes launching (please note documentation says the delegate should be set before the app finishes launching):
// AppDelegate.swift
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
// Here we actually handle the notification
print("Notification received with identifier \(notification.request.identifier)")
// So we call the completionHandler telling that the notification should display a banner and play the notification sound - this will happen while the app is in foreground
completionHandler([.banner, .sound])
}
}
And you can tell SwiftUI to use this AppDelegate by using the UIApplicationDelegateAdaptor on your App scene:
#main
struct YourApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This approach is similar to Apple's Fruta: Building a Feature-Rich App with SwiftUI
https://developer.apple.com/documentation/swiftui/fruta_building_a_feature-rich_app_with_swiftui
Apple have used In-app purchases this way
This class holds all your code related to Notification.
class LocalNotificaitonCenter: NSObject, ObservableObject {
// .....
}
In your #main App struct, define LocalNotificaitonCenter as a #StateObject and pass it as an environmentObject to sub-views
#main
struct YourApp: App {
#Environment(\.scenePhase) private var scenePhase
#StateObject var localNotificaitonCenter = LocalNotificaitonCenter()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(localNotificaitonCenter)
}
}
}
It is just that!