1

Beginner trying to create an app that gives the user a random "word of the day".

Through testing I realised that the app has to be opened every day by the user in order to manually trigger the function isRefreshRequired() (which retrieves the new word and updates user interface and notification content). But I need this to be done automatically, regardless of whether the user opens the app or not.

Is this even possible? I am stuck between which methods to use:

  1. NSCalendarDayChanged. I found this useful answer but I am not sure where it would be implemented? If I use it applicationDidFinishLaunching, where would the function calendarDayDidChange() be called? In viewDidLoad()?

  2. significantTimeChangeNotification() - Possibly a better method? Again, not sure how to actually implement this and where. The document also says:

If the the device is asleep when the day changes, this notification will be posted on wakeup.

Does wakeup mean when the app is reopened? Because I want the isRefreshRequired() function to execute even if the user hasn't touched the app.

  1. Background App Refresh: this doesn't seem useful since it needs a URLSession and the user may have disabled background refresh. Seems a bit excessive for an app that just wants to give the user a new word!

The code in question:

    override func viewDidLoad() {
            super.viewDidLoad()
            
    isRefreshRequired()
        }

    func isRefreshRequired() {
                // if it is first time opening app
                if userDefaults.bool(forKey: "First Launch") == false {
                    //run code during first launch
                    _ = updateWordOfTheDay()
                    NotificationManager.askNotificationPermission()
                    print("First time opening app")
                    userDefaults.set(true, forKey: "First Launch")
                }
                else {
                    //run code after first launch
                    print("Not first time opening app")
                    userDefaults.set(true, forKey: "First Launch")
    
                    let lastAccessDate = userDefaults.object(forKey: "lastAccessDate") as? Date ?? Date()
                    userDefaults.set(Date(), forKey: "lastAccessDate")
    
                    // if it is the same day give the vocab picked for that day
                    if calendar.isDateInToday(lastAccessDate) {
                        readFromUserDefaults()
                        print("No refresh required as it is the same day as lastAccessDate which was \(lastAccessDate)")
                    }
                    else {
                        print("new day since lastAccessDate")
                        _ = updateWordOfTheDay()
                    }
    
                }
            }

    func sendNotification(using: ChineseVocab) {
        
        let center = UNUserNotificationCenter.current()
        center.removeAllDeliveredNotifications()
        center.removeAllPendingNotificationRequests()
        
let content = UNMutableNotificationContent()
        content.title = using.Chinese + " " + using.Pinyin
        content.body = using.Definition

}

    func saveToUserDefaults(with data: ChineseVocab) {
            let encoder = JSONEncoder()
            if let encoded = try? encoder.encode(data) {
                userDefaults.set(encoded, forKey: "selectedVocab")
            }
        }
    
         func readFromUserDefaults() {
            if let savedVocab = userDefaults.object(forKey: "selectedVocab") as? Data {
                let decoder = JSONDecoder()
                if let wordOfTheDay = try? decoder.decode(ChineseVocab.self, from: savedVocab) {
                updateLabels(using: wordOfTheDay)
                 NotificationManager.sendNotification(using: wordOfTheDay)
                    print("readFromUserDefaults() is working: \(wordOfTheDay)")
                } else {
                    print("unable to load readFromUserDefaults()")
                }
            }
        }
Sushi
  • 29
  • 6

1 Answers1

1

What you are trying to do is call a function when the app is closed or in the background. Taking a step back, the user isn't actually using your app at this time. So because of that, your app shouldn't really be running functions either. I believe this is done on purpose so that the phone can allocate resources to apps/functions that the user is actively using.

So there is no way to just call a function when the app is closed. But going off your points above:

  1. I think using applicationDidBecomeActive or applicationDidFinishLaunching is probably your best bet. These are functions within the AppDelegate and you can call the functions within the methods directly. The applicationDidBecomeActive will execute every time the app appears on the screen. If you're only need it on one screen, you may be able to just call in the viewDidAppear / viewWillAppear of the ViewController instead.

  2. Idk anything about significantTimeChangeNotification so I don't want to tell you it won't work, but I would guess the notification would not execute if the app is closed.

  3. Background App Refresh is basically what you are trying to do, however, even if you implement it.. it will only execute when the app is the background. If the app is completely closed, the background fetch won't be called. So I would assume that most of the time it wouldn't be called anyway.

nicksarno
  • 3,850
  • 1
  • 13
  • 33
  • Thanks for your feedback! If it is basically impossible to refresh data when the app is closed, how do most apps function? There are many "word of the day" apps that I downloaded to compare, and they all manage to send the user a new word each day even if the app wasn't opened. How do they do it? – Sushi Oct 16 '20 at 17:48
  • I'm assuming when you say they "send the user a new word", it's coming in a push notification? You can connect external push notifications by connecting your app to a 3rd party backend, such as Google Firebase. And from there, you can run 'cloud functions' that execute independent of your app and can send push notifications to devices. But you mentioned you were a beginner and I would shy away from this for now. It's not very easy to implement and the cloud functions are not written in Swift (usually JavaScript I think). – nicksarno Oct 16 '20 at 18:08
  • For your purposes, if you run the functions within the applicationDidBecomeActive, anytime the user opens the app, it would get updated immediately. – nicksarno Oct 16 '20 at 18:10
  • I am using local notifications, but I guess the other apps are using push notifications? The problem is that the notification content is meant to be the new word. If the app is not opened every day, the notification content is just the old word from the previous day/whenever the app was last opened :( – Sushi Oct 16 '20 at 18:12
  • I believe the local notifications (unless scheduled) only execute when the app is in the background, and not closed. So once it closes, they would not execute. If you know ahead of time what the words will be for like the next week or month, you may be able to schedule a bunch of local notifications when the user opens the app (for the next week or month). Of course, you will need to set all of the the notification times and contents ahead time. https://developer.apple.com/documentation/usernotifications/scheduling_a_notification_locally_from_your_app – nicksarno Oct 16 '20 at 18:20
  • Ok I understand. Currently I have scheduled local notifications and their content is supposed to be based on the new word. If the app is opened every day it works, and if it isn't it just sends the old word from the last access time T_T – Sushi Oct 16 '20 at 18:42
  • It sounds like you need to change your notifications. Instead of scheduling the same notification to repeat every day with different content, try to schedule a totally different / new notification each day. So schedule 7 different notifications for the next 7 days.. – nicksarno Oct 16 '20 at 19:16
  • If you add your code for scheduling notifications above, I can try to help. – nicksarno Oct 16 '20 at 19:16
  • Thank you for your time and patience! I think you are right - is there a way to change the content of the notification to read from userdefaults instead? I have added the code above – Sushi Oct 17 '20 at 17:18
  • You can update your read function to "func readFromUserDefaults() -> ChineseVocab? {" and then return wordOfTheDay where your first print statement is, and return nil for your second. When you want the content. – nicksarno Oct 17 '20 at 19:12
  • I'l note though, that you're code is not what I was mentioning here. If you click the link I posted above, there are date-based notifications you can set, which means you literally put the time and day that each notification would send. So if u have the data in advance, I would propose creating a function that loops through x days and creates x different schedules date-based notifications. – nicksarno Oct 17 '20 at 19:14
  • UserNotifications are presented by the system. So even if your app has been killed, the system will present a pending notification. You can use `UNCalendarNotificationTrigger(dateMatching:repeats)` to schedule a notification that will repeat. You can become a `UNUserNotificationCenterDelegate` and create a callback that gets called when the notification is presented with `userNotificationCenter(center:notification;completionHander:)` – DPrice Oct 17 '20 at 22:52