0

I'm have a hard time creating a user setting options. I would like the user to customize the frequency of the timer to receive the local notifications. I'm using a switch on the SystemSettingsVC to for the user to select and set the user default and I'm using the user default setting in my MainVC for the TimerInterval. My app runs but the time doesnt change. I know that the switch is working because I'm also testing the background color change.

Here is my code for my SystemSettingsVC: ...

import UIKit
import SwiftUI
import CoreData

class SettingsViewController: UIViewController {


@IBOutlet weak var timeSelection: UISegmentedControl!


let userDefaults = UserDefaults.standard

let TIME_KEY = "TIME_KEY"
let ONE_HOUR_KEY = 60.0
let THREE_HOUR_KEY = 120.0
let SIX_HOUR_KEY = 300.0


override func viewDidLoad() {
    super.viewDidLoad()

    
    updateTime()
    
}


func updateTime() {
    let time = userDefaults.object(forKey: "TIME_KEY")
    if(time as? Double == ONE_HOUR_KEY) {
        timeSelection.selectedSegmentIndex = 0
        let userDefaults = UserDefaults.standard
        userDefaults.set(60.0, forKey: "TIME_KEY")
        view.backgroundColor = UIColor.white
       save()
    }
    else if(time as? Double == THREE_HOUR_KEY) {
        timeSelection.selectedSegmentIndex = 1
        let userDefaults = UserDefaults.standard
        userDefaults.set(120.0, forKey: "TIME_KEY")
        view.backgroundColor = UIColor.gray
       save()
    }
    else if(time as? Double == SIX_HOUR_KEY) {
        timeSelection.selectedSegmentIndex = 2
        let userDefaults = UserDefaults.standard
        userDefaults.set(300.0, forKey: "TIME_KEY")
        view.backgroundColor = UIColor.darkGray
        
        save()
    }
}

func save() {
    if let savedData = try? NSKeyedArchiver.archivedData(withRootObject: clock, requiringSecureCoding: false){
let defaults = UserDefaults.standard
        defaults.set(savedData, forKey: "TIME_KEY")
     }
}




@IBAction func selectTimeOfQuotes(_ sender: Any) {
    
    
    switch timeSelection.selectedSegmentIndex
    {
    case 0:
        userDefaults.set(60.0, forKey: "TIME_KEY")
        save()
    case 1:
        userDefaults.set(120.0, forKey: "TIME_KEY")
        save()
    case 2:
        userDefaults.set(300.0, forKey: "TIME_KEY")
     save()
    default:
        userDefaults.set(60.0, forKey: "TIME_KEY")
        save()
    }
    updateTime()
}
}

...

Here is the code for my view controller to where I call the user defaults, I placed let userDefaults = UserDefaults.standard in my ViewDidLoad : '''Code''' ```

func configureAlerts() {
    let center = UNUserNotificationCenter.current()
    
     center.removeAllDeliveredNotifications()
     center.removeAllPendingNotificationRequests()


    let listQuotes = quotes
    
    
    let i = 1
                  
       let content = UNMutableNotificationContent()
        content.title = “Inspire”
        content.body = listQuotes[i].shareMessage
       content.sound = UNNotificationSound.default
  
        let alertDate = Date().byAdding(days: i)
  
        var alertComponents = Calendar.current.dateComponents([.day, .month, .year], from:     alertDate)
        alertComponents.hour = 8
        

    let userDefaults = UserDefaults.standard
   
   typealias NSTimeInterval = Double

   let thisTime:TimeInterval = userDefaults.double(forKey: "TIME_KEY")
      

        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: thisTime, repeats: true)
        let uuidString = UUID().uuidString
        let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)

        center.add(request) { error in
           if let error = error {
           print(error.localizedDescription)
            }
        }
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
Sharlene
  • 5
  • 4

1 Answers1

0

You are not showing how your Models are connected so we can't tell where the miscommunication is happening or maybe that is the issue. They are not connected.

But at a simple glance you are not rescheduling the notifications. selectTimeOfQuotes, save or updateTime do not call configureAlerts.

Something to note you have a lot of repeating code and hardcoded values that could be the source of the confusion.

BTW 120 is 2 hours not 3 idk if that is on purpose but it highlights my next point.

When you change a value you only want to do it in 1 place; if possible; so centralizing the models will help you avoid having to change things in multiple places.

For the options for your picker an enum can hold everything.

enum NotificationInterval: Double, CaseIterable, Codable{
    case ONE_HOUR_KEY = 3660 //TimeInterval == seconds
    case THREE_HOUR_KEY = 10800 //TimeInterval == seconds
    case SIX_HOUR_KEY = 21600 //TimeInterval == seconds
    
    func label() -> String{
        var result = ""
        switch self {
        case .ONE_HOUR_KEY:
            result = "1 hour"
        case .THREE_HOUR_KEY:
            result = "3 hours"
        case .SIX_HOUR_KEY:
            result = "6 hours"
        }
        return result
    }
    func color() -> UIColor{
        var result = UIColor.label
        switch self {
        case .ONE_HOUR_KEY:
            result = UIColor.white
        case .THREE_HOUR_KEY:
            result = UIColor.gray
        case .SIX_HOUR_KEY:
            result = UIColor.darkGray
        }
        return result
    }
    ///Key for storage of user selected interval
    static var userDefaultKey: String{
        "TIME_KEY"
    }
    ///Saves value to store using the `userDefaultKey`
    func saveToStore(){
        var mgr = UserDefaultManager()
        mgr.intervalTime = self
    }
    ///Gets value from store using the `userDefaultKey`
    static func getFromStore() -> NotificationInterval{
        let raw = UserDefaultManager().intervalTime
        return raw
    }
    ///Gets the index for the object in the `allCases` array
    func getAllCasesIndex() -> Int?{
        NotificationInterval.allCases.firstIndex(where: {
            self == $0
        })
    }
    ///Gets the index for the `userDefaultKey` stored object in the `allCases` array
    static func getStoredIndex() -> Int?{
        NotificationInterval.getFromStore().getAllCasesIndex()
    }
}

Then since you have at least 2 unrelated classes that use the value store in user defaults you can centralize that work too

///This stores and retreives userdefaults to a predetermined store
struct UserDefaultManager{
    //Having a single location for this will simplify  UserDefault storage
    //A use case would be switching to an App Group store when you decide to support watch in the future or if you want to add Widgets
    private let store = UserDefaults.standard
    ///User selected interval for the notifications
    var intervalTime: NotificationInterval{
        get{
            getObject(forKey: NotificationInterval.userDefaultKey, type: NotificationInterval.self) ?? NotificationInterval.ONE_HOUR_KEY
        }
        set{
            save(newValue, forKey: NotificationInterval.userDefaultKey)
        }
    }
    ///Saves any Codable to UserDefaults
    func save<T: Codable>(_ object: T, forKey: String){
        
        let encoder = JSONEncoder()
        do{
            let encoded = try encoder.encode(object)
            store.set(encoded, forKey: forKey)
        }catch{
            print(error)
        }
    }

    //Gets any Codable from UserDefaults
    func getObject<T: Codable>(forKey: String, type: T.Type) -> T?{
        guard let saved = store.object(forKey: forKey) as? Data else {
            return nil
        }
        let decoder = JSONDecoder()
        do{
            let loaded = try decoder.decode(T.self, from: saved)
            return loaded
        }catch{
            print(error)
            return nil
        }
        
    }
    
}

Then your SettingsViewController will look like this

class SettingsViewController: UIViewController {
    ///Programatic use of IBOutlet
    var timeSelection: UISegmentedControl!
    
    private let quoteManager = QuoteManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //Create control PS I dont have a storyboard setup but you can replace this with your IBOutlet and IBAction
        timeSelection = UISegmentedControl(items: NotificationInterval.allCases.map({
            $0.label()
        }))
        timeSelection.addTarget(self, action: #selector(selectTimeOfQuotes), for: .allEvents)
        //Set the initial value from storage
        timeSelection.selectedSegmentIndex =  NotificationInterval.getStoredIndex() ?? 0
        self.view.addSubview(timeSelection)
        timeSelection.translatesAutoresizingMaskIntoConstraints = false
        timeSelection.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        timeSelection.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        //End of programatic setup
        
        //Set the color from storage
        view.backgroundColor = NotificationInterval.getFromStore().color()
        
    }
    ///Programatic use of IBAction
    @objc
    func selectTimeOfQuotes() {
        //Identify the selected interval
        let interval = NotificationInterval.allCases[timeSelection.selectedSegmentIndex]
        //Save it
        interval.saveToStore()
        //Change the color
        view.backgroundColor = interval.color()
        //Change the notification
        quoteManager.rescheduleQuotes()
    }
}

As the last line of code shows once all the work is done you should reschedule the quotes.

I created a mini-QuoteManager since you do not show this connection. This manager can be used by any View Controller to get the quotes and maybe even reschedule when the quotes change by calling the provided method.

//Adapt this to your use case this is just a sample
///Liason for quote Storege
struct QuoteManager{
    var listQuotes = ["one", "two", "three"]
    private let notificationManager = NotificationManager.shared
    private let userDefaultsManager = UserDefaultManager()
    ///Reschedules quotes
    func rescheduleQuotes(count: Int = 10){
        let title = "Inspire"
        notificationManager.deleteNotifications()
        
        print(#function)
        for n in 1..<count+1{
            print(n)
            let newDate = userDefaultsManager.intervalTime.rawValue*Double(n)
            //Idenfier must be unique so I added the n
            notificationManager.scheduleUNTimeIntervalNotificationTrigger(title: title, body: listQuotes.randomElement()!, timeInterval: newDate, identifier: "com.yourCompany.AppName.\(title)_\(n.description)")
        }
    }
}

The QuoteManager calls the NotificationManager. I created a small version below.

class NotificationManager: NSObject, UNUserNotificationCenterDelegate{
    //Singleton is requierd because of delegate
    static let shared: NotificationManager = NotificationManager()
    let notificationCenter = UNUserNotificationCenter.current()
    
    private override init(){
        super.init()
        //This assigns the delegate
        notificationCenter.delegate = self
        requestAuthorization()
    }
    func scheduleUNTimeIntervalNotificationTrigger(title: String, body: String, timeInterval: TimeInterval, identifier: String, repeats: Bool = false){
        print(#function)
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = .default
        
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: repeats)
        let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
        notificationCenter.add(request) { (error) in
            if error != nil {
                print(error!)
            }
            self.printNotifications()
        }
    }
    func requestAuthorization() {
        print(#function)
        notificationCenter.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in
            if granted {
                print("Access Granted!")
            } else {
                print("Access Not Granted")
            }
        }
    }
    func deleteNotifications(){
        print(#function)
        notificationCenter.removeAllPendingNotificationRequests()
        notificationCenter.removeAllDeliveredNotifications()
    }
    
    ///Prints to console schduled notifications
    func printNotifications(){
        print(#function)
        notificationCenter.getPendingNotificationRequests { request in
            print("UNTimeIntervalNotificationTrigger Pending Notification")
            for req in request{
                if req.trigger is UNTimeIntervalNotificationTrigger{
                    print((req.trigger as! UNTimeIntervalNotificationTrigger).nextTriggerDate()?.description ?? "invalid next trigger date")
                    print(req.content.body)
                }
            }
            print("UNCalendarNotificationTrigger Pending Notification")
            for req in request{
                if req.trigger is UNCalendarNotificationTrigger{
                    print((req.trigger as! UNCalendarNotificationTrigger).nextTriggerDate()?.description ?? "invalid next trigger date")
                }
            }
        }
    }
    
    //MARK: UNUserNotificationCenterDelegate
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        
        completionHandler(.banner)
    }
}

It might seem like a lot but if you focus on the SettingsViewController you will see how much simpler the whole thing becomes.

All this is working code. Just copy and paste into a .swift file.

You might have to change the UISegmentedControl since I created it programmatically but if you put the SettingsViewController in a blank storyboard it should work as is.

lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • Thank you for your comments and your help I really appreciate it..... as you can see I'm very new at this. I'm going to take you advice and organize this code better. – Sharlene Dec 22 '21 at 02:29
  • @Sharlene I’m glad it helped if I answered your question please accept the answer by selecting the green check mark above. – lorem ipsum Dec 22 '21 at 02:37
  • For:enum NotificationInterval: Double, CaseIterable{ case ONE_HOUR_KEY = 60 case THREE_HOUR_KEY = 180 case SIX_HOUR_KEY = 300 How would provide a label for the case instead of having 60, 180, 300 appear on the main storyboard? – Sharlene Dec 22 '21 at 18:53
  • Just add a `label()` like I did for `color()` and change `$0.rawValue.description` to `$0.label()` to the `items` of the `UISegmentedControl` in `viewDidLoad`. See above. – lorem ipsum Dec 22 '21 at 19:14
  • @Sharlene Also, if you want to have access to the whole `NotificationInterval` so you can use the `label`, `color` and `rawValue` somewhere else you can change the `UserDefaultManager` to save the object vs just the seconds – lorem ipsum Dec 22 '21 at 19:43
  • The only problem I'm discovering with this format of the solution for the time interval it doesnt seem to cycle through the array of Quotes in the quote manager "listedQuotes". I've even added a shuffle or random to the array and it only returns the same value in the quote. Is there a step I'm missing? – Sharlene Dec 25 '21 at 15:59
  • @Sharlene this code does not have any shuffling built-in. If you want a random quote you can change `listQuotes[1]` in the `QuoteManager` to `listQuotes.randomElement()!` But that would give you a random element for the notification if you set `repeats = true` it would be the same random quote repeating. To truly get a random quote every time you would have to schedule multiple notifications, each with their own random element. – lorem ipsum Dec 25 '21 at 16:27
  • [This question](https://stackoverflow.com/questions/67675639/schedule-local-notification-every-n-days-timezone-safe/68039675#68039675) schedules notifications every `n` days but you could easily mimic `recurringNotification` to an `n` `timeInterval`, and in every loop of the `for` get a new `randomElement()` – lorem ipsum Dec 25 '21 at 16:28