Bronze Challenge Question

I’m rather confused by what this challenge actually wants, as it can’t be simply changing the method signature, can it?

What I did was add a nested Enum to represent the errors that this function can throw, and then proceeded to add a couple of guard statements. Once this function is marked as a throwing function, from my understanding, it’s no longer needed to handle the errors that calling the encode and write methods in the encoder object may throw. Here’s what I did:

Nested enum with error cases

enum Error: Swift.Error {
        case encodingError
        case writingError
}

The actual saveChanges(_:) method:

@objc func saveChanges() throws {
        print("Saving items to: \(itemArchiveURL)")
        let encoder = PropertyListEncoder()

        guard let data = try? encoder.encode(allItems) else {
            throw ItemStore.Error.encodingError
        }
        guard let _ = try? data.write(to: itemArchiveURL, options: [.atomic]) else {
            throw ItemStore.Error.writingError
        }
        print("Saved all of the items")
    }

The problem that I found was that now, in the initializer, the code that registers the ItemStore as an observer for the didEnterBackgroundNotification, which calls the “saveChanges” method in selector: #selector(saveChanges), does not really throw anything? or at least that’s what the compiler says so when I add try to it . Is this because it’s been exposed to Objective-C and it’s now handled differently? I’d appreciate if someone could shed some light on this.

Thank you!

Did you try this:

enum Error: Swift.Error {
        case encodingError
        case writingError
        case mysteriousError
}

@objc func saveChanges() throws {
      throw ItemStore.Error.mysteriousError
}

to see if the exception is being propagated?

Thank you for your help! I tried what you suggested and that causes the app to trap, even if the notificationCenter.addObserver method is called within the do block in the do/catch block that was added in the initializer of ItemStore.

Adding try anywhere in the aforementioned method just causes the compiler to show the warning “No calls to throwing functions occur within ‘try’ expression” so I guess any errors thrown in a method called in the selector parameter of notificationCenter.addObserver need to be handled differently? in an Objective-C kind of manner?

Here’s what my ItemStore initializer looks like, here’s where I attempt to catch the errors:

init() {
        do {
            let data = try Data(contentsOf: itemArchiveURL)
            let unarchiver = PropertyListDecoder()
            let items = try unarchiver.decode([Item].self, from: data)
            allItems = items
            
            let notificationCenter = NotificationCenter.default
            notificationCenter.addObserver(self,
                                           selector: #selector(saveChanges),
                                           name: UIScene.didEnterBackgroundNotification,
                                           object: nil)
        } catch Error.encodingError {
            print("Couldn't encode items.")
        } catch Error.writingError {
            print("Couldn't write to file.")
        } catch {
            print("Error reading in saved items: \(error)")
        }
    }

That says: when the saveChanges method is called by the notification center, the application will crash if the method throws an exception.

So, instead of throwing an exception, the method should post a notification to signal that the save operation has failed.

@objc func saveChanges() /* throws */ {
        print ("Saving items to: \(itemArchiveURL)")
        let encoder = PropertyListEncoder()

        guard let data = try? encoder.encode (allItems) else {
            // throw ItemStore.Error.encodingError
            postErrorNotification (ItemStore.Error.encodingError)
        }
        guard let _ = try? data.write (to: itemArchiveURL, options: [.atomic]) else {
            // throw ItemStore.Error.writingError
            postErrorNotification (ItemStore.Error.writingError)
        }
        print ("Saved all of the items")
    }
//  Test.swift
import Foundation

// -------------------------------------------------
//
class Bar {
    static let event = NSNotification.Name ("Bar.event")
    
    func bar () {
        NotificationCenter.default.post (name: Bar.event, object: nil)
    }
}

// -------------------------------------------------
//
class Foo {
    static let event = NSNotification.Name ("Foo.event")

    init () {
        // Interested in Bar events
        NotificationCenter.default.addObserver (self,
                                        selector: #selector (handleBarEvents),
                                        name: Bar.event,
                                        object: nil)
    }
    
    @objc func handleBarEvents()  {
        print ("\(type (of:self)): \(#function): operation failed - post a notification to signal instead of throwing...")
        NotificationCenter.default.post (name: Foo.event, object: nil)
    }
}

// -------------------------------------------------
//
class Test {
    let foo = Foo ()
    let bar = Bar ()

    func main () {
        // Interested in Foo events
        NotificationCenter.default.addObserver (self,
                                        selector: #selector (handleFooEvents),
                                        name: Foo.event,
                                        object: nil)
        bar.bar ()
    }
    
    @objc func handleFooEvents() {
        print ("\(type (of:self)): \(#function)...")
    }
}

Thank you for your help, I really appreciate it. I would admittedly need to re-read your last response and even type it all out before it makes sense to me, though!

I’m having the same problem, and I’m not understanding the last post by ibex10.

If I replace the code in saveChanges with throw myError.myTestError, I get Thread 1: EXC_BAD_ACCESS (code=1, address=0x8184201) in the first line of AppDelegate.

ibex10’s last reply suggests that saveChanges not be a method that throws an error nor have the signature func saveChanges() throws which is what the Bronze Challenge suggests.

I understand enough of this to make a method that throws in a playground work properly, but I’m missing something here if saveChanges is going to throw an error and cause the app to crash instead of handle the error elegantly.

I hope the following is clearer to understand.

Inside the saveChanges method, if an exception is thrown to signal a failure the application will crash because the notification center seems to be unequipped to handle exceptions thrown by the saveChanges method.

So instead of throwing an exception, the method should post a notification to signal that an error has occurred while saving the data. This, of course, implies that someone should observe the notification which may be sent out by the saveChanges method.

One more thing. Upon further inspection of the following code, I found something interesting which I had totally missed.

Although the init method is equipped to handle the Error.writingError and Error.encodingErrorexception exceptions, they are not going to be thrown until the saveChanges method runs, which occurs only when the application enters the background, which in turn means there is no point in getting prepared to handle those exceptions inside the init method.

This can be remedied in two ways.

Simplify the init method and rely on the scene delegate to call the saveChanges function.

init() {
   do {
      let data = try Data(contentsOf: itemArchiveURL)
       let unarchiver = PropertyListDecoder()
       let items = try unarchiver.decode([Item].self, from: data)
       allItems = items
   } catch {
       print("Error reading in saved items: \(error)")
   }
}

// Scene Delegate method
func sceneDidEnterBackground (UIScene) {
    do {
           saveChanges ()
    } catch Error.encodingError {
            print("Couldn't encode items.")
    } catch Error.writingError {
            print("Couldn't write to file.")
    } catch (let error){
            print("Error reading in saved items: \(error)")
    }
}

// Archive changes
func saveChanges() throws {
    print("Saving items to: \(itemArchiveURL)")
    let encoder = PropertyListEncoder()

    guard let data = try? encoder.encode(allItems) else {
            throw ItemStore.Error.encodingError
    }
    guard let _ = try? data.write(to: itemArchiveURL, options: [.atomic]) else {
            throw ItemStore.Error.writingError
    }
    print ("Saved all of the items")
}

Or if you really wanted to use the notification center, you would pass it an adapter, which is a function that doesn’t throw but calls the throwing function.

init() {
   do {
      let data = try Data(contentsOf: itemArchiveURL)
      let unarchiver = PropertyListDecoder()
      let items = try unarchiver.decode([Item].self, from: data)
      allItems = items
            
      let notificationCenter = NotificationCenter.default
      notificationCenter.addObserver(self,
                         selector: #selector(saveChanges),
                         name: UIScene.didEnterBackgroundNotification,
                         object: nil)
   } catch (let error) {
      print("Error reading in saved items: \(error)")
   }
}

// Adapter
@objc func saveChanges () {
    do {
           archiveChanges ()
    } catch Error.encodingError {
            print("Couldn't encode items.")
    } catch Error.writingError {
            print("Couldn't write to file.")
    } catch (let error){
            print("Error: \(error)")
    }
}

// Archive changes
func archiveChanges () throws {
    print("Saving items to: \(itemArchiveURL)")
    let encoder = PropertyListEncoder()

    guard let data = try? encoder.encode(allItems) else {
            throw ItemStore.Error.encodingError
    }
    guard let _ = try? data.write(to: itemArchiveURL, options: [.atomic]) else {
            throw ItemStore.Error.writingError
    }
    print ("Saved all of the items")
}

Thanks! That’s what I was thinking you were getting at. Like I said, the description of the challenge in the book is that throws is part of the method signature for saveChanges. Therefore, your scene delegate version appears to be closer to the intended solution to the challenge as opposed to trying to deal with notifications and all that.

I’m not complaining, but I am thinking that this was more of a Silver Challenge than a Bronze one. Bronze is supposed to be similar to what was in the chapter, whereas Silver is supposed to use things we haven’t seen before. Making a method throw exceptions is not something that was in the chapter. It’s not hard, but it does require going beyond the book. Pulling out the call to saveChanges from the init() method also seems a bit beyond what is typically done in a Bronze Challenge. Again, not difficult, but maybe beyond simply reinforcing similar techniques that are in the chapter which is what they said Bronze Challenges do.

In any case, I am learning so that’s what matters. :slight_smile:

I’m trying to use the sceneDidEnterBackground method but it requires UISceneDelegate. When I add that protocol to the class, I get an error. I see that the documentation says it’s Objective C and not Swift, which may be the problem.

What am I missing in order to be able to use this?

If you create an iOS project for a single-view app that uses storyboards (Xcode 11.4), you will get the following files:

  • AppDelegate.swift
  • SceneDelegate.swift
  • ViewController.swift

And SceneDelegate.swift looks like this:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    ...
    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
    }
}

[annoyed grunt] Thanks! I moved saveChanges() and the related code to SceneDelegate.swift and much of it is working as expected including throwing the test exception when I turn that on, though it seems since ItemStore does the loading and SceneDelegate does the saving, sometimes the changes get lost when the app is stopped by Xcode instead of left by the user. I need to do more digging and see if they’re saving and loading to the same file URL.

Again, it seems what we’re doing here is more than the intended depth of this Bronze Challenge. But that’s fine.

fabfe31 almost had it. The only point he missed is that one must call a local function (say @objc func checkSaving()) from the notificationCenter. That checkSaving must call do try saveChanges (with the exact signature of the book that is no @objc necessary) and catch errors thrown by it in a switch to treat the various cases. Bronze chalenge indeed.