Learning Swift — Day 145 to 147

Hacking with Swift: facing Project 33’s challenges

Recap

This was truly a huge project and even if some things do not work as intended (and, sincerely, the teaching material was not thorough enough on the CloudKit dashboard…) I still learned a lot of things.

Would I be able to reproduce this project alone blindfolded? Absolutely not! The good thing is that, now that I have written my last article, I have a place on my system where I can go and look for without having to open again the instruction book and spend countless hours looking for what I need.

Now, let’s see what I can do with the challenges.

Challenges

Challenge 1: if the iCloud fetch fails, we tell the user. How about adding a “Retry” button to the user interface?

I am now browsing the code to look for the place where we perform the iCloud fetching and I noticed one thing: ViewController.swift is our first screen and, in viewDidLoad() there is a back bar button item called “Home” which we do not use anymore… what’s the issue? Also, what is that isDirty property there for? Still I have no answers on that.

Actually … if we push the + button we then get a new screen that seems based on the same file and where the “Home” back button is present…

I’m getting very confused … the back bar button item in code seems to refer to how the button will look like when we transition to the next screen. Does this mean that a back bar button item is there only to show you how to handle IF you want to come back where you are now? This is completely illogical… It doesn’t make sense.

I am now starting to write comments such as // SCREEN 1, or // SCREEN 2 where needed and reordering the files in a “Controllers” folder to be able to wrap my head around that.

I still cannot understand why back bar button items should be configured in screen 1 to appear in screen 2. I would find more logical for them to say “we are in screen 1 and, if you tap the back button, you are brought to screen X”… much more logical… Like this it makes no sense to me. Of course I can train my mind to understand that but it won’t be easy.

Anyway, the answer is here: https://developer.apple.com/documentation/uikit/uinavigationitem.


So, let’s look now for where we perform a fetching of the iCloud data. It is in ViewController.swift inside the loadWhistles() method.

Inside the operation.queryCompletionBlock I changed the action of the failure alert controller so that also a closure may be added, like this:

fetchFailedAC.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] (_) in                  self?.navigationItem.setRightBarButton(UIBarButtonItem(title: "Retry", style: .plain, target: self, action: #selector(self?.loadWhistles)), animated: true)
})

So this adds a touch of recursiveness to the method, which is never bad. Then, at the top of the method, I added this to restore the original right bar button item in case it would have been changed. Setting animated to false should do the thing:

navigationItem.setRightBarButton(UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addWhistle)), animated: false)

The only issue with this is that I do not know how to test it. By now everything worked. I tried to turn off the network connection so that the fetch would for sure fail and … no alert was shown… why? I don’t know… no error was thrown in the console…


Challenge 2: we made the Whistle class inherit from NSObject. Can you make it conform to the NScoding protocol? You might find project 12’s guide to NSCoding and UserDefaults in Swift useful.

This is a summary of the NSCoding protocol:

Summary

A protocol that enables an object to be encoded and decoded for archiving and distribution.

Declaration

Discussion

The NSCoding protocol declares the two methods that a class must implement so that instances of that class can be encoded and decoded. This capability provides the basis for archiving (where objects and other structures are stored on disk) and distribution (where objects are copied to different address spaces).

In keeping with object-oriented design principles, an object being encoded or decoded is responsible for encoding and decoding its instance variables. A coder instructs the object to do so by invoking encode(with:) or init(coder:). encode(with:) instructs the object to encode its instance variables to the coder provided; an object can receive this method any number of times. init(coder:) instructs the object to initialize itself from data in the coder provided; as such, it replaces any other initialization method and is sent only once per object. Any object class that should be codeable must adopt the NSCoding protocol and implement its methods.

It is important to consider the possible types of archiving that a coder supports. In macOS 10.2 and later, keyed archiving is preferred. You may, however, need to support classic archiving. For details, see Archives and Serializations Programming Guide.

Adding protocol conformance immediately throws two exceptions and asks us if we want to add protocol stubs (which we absolutely want!). An encode(with:) method and a required init?(coder:) failable initialiser will be added.

Following Project 12 I started to compile the encode(with:) method like this:

func encode(with aCoder: NSCoder) {
    aCoder.encode(recordID, forKey: "recordID")
    aCoder.encode(genre, forKey: "genre")
    aCoder.encode(comments, forKey: "comments")
    aCoder.encode(audio, forKey: "audio")
}

Then I tried to write the required init?(coder:) like this:

required init?(coder aDecoder: NSCoder) {
    recordID = aDecoder.decodeObject(forKey: "recordID") as? CKRecord.ID
    genre = aDecoder.decodeObject(forKey: "genre") as? String
    comments = aDecoder.decodeObject(forKey: "comments") as? String
    audio = aDecoder.decodeObject(forKey: "audio") as? URL 
}

I’m not sure about the first and last entry but I hope it will be fine like this. Whatever the case we now have properly conformed to the NSCoding protocol and we can start writing code to load and save the data.

The code for saving is pretty straightforward:

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

func load() {
    let defaults = UserDefaults.standard
    
    if let savedWhistles = defaults.object(forKey: "savedWhistles") as? Data {
        if let decodedWhistles = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(savedWhistles) as? [Whistle] {
            whistles = decodedWhistles
        }
    }
}

But now we have a problem:

Why the heck do we have this error? Why was such an error not there in Project 12 where we used a very similar procedure? How can I understand what is going on? Why such things are not explained in tutorials? Why no debugging is explained? Why?

In project 12 we have an extra init() at the beginning of the class but if I do that now it brings down a hell of issues… It just seems I’m stuck here, as usual with these unexplained things… Stuck and with no help at all, no one who could help solving this and nowhere to look for a solution… Usual… back to this wonderful feeling I was really NOT missing … Thank you, Sir…


I tried this, after a good amount of cursing …

I created an override init() { super.init() } just after the properties declaration so that we can have an empty initialiser. Then in viewDidLoad() I called the load() method and in the loadWhistles(), after the only time where the whistles array gets interacted with, I call save().

I am not totally sold by this approach because the whistle could become quite big with an audio file and saving that to UserDefaults is a very bad idea… I’m sure I am missing something but, again, if no one explains this to me in the tutorial, how am I supposed to know it?

I loaded the app and launched it … nothing seems different from before and, again … I have no way to know if it should or if it should not be different … Lastly, why the heck do we have to use NSCoding when we are saving data to iCloud? For sure there is a more than wonderful reason but … could someone explain this to me? No need to lose his time on this, even just point me at a resource I can read … I love reading…


Challenge 3: Fix the AddCommentsViewController class so that it correctly adjusts the text view when the keyboard appears. Look into project 16 for guidance.

First of all … the suggestion for it to look into project but, if I recall it correctly, it should be project 19. Let’s start by adding this to viewDidLoad():

let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)

And here is the method for the adjusting of the keyboard:

@objc func adjustForKeyboard(notification: Notification) {
    guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
    
    let keyboardScreenEndFrame = keyboardValue.cgRectValue
    let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, to: view.window)
    
    if notification.name == UIResponder.keyboardWillHideNotification {
        comments.contentInset = .zero
    } else {
        comments.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0)
    }
    
    comments.scrollIndicatorInsets = comments.contentInset
    
    let selectedRange = comments.selectedRange
    comments.scrollRangeToVisible(selectedRange)
}

This all works but I see two big flaws with this app and I really wish some tutorials could bring an app from true beginning to true end and publishable result! I know this may be a commercial reason but … come on … If I put my iPhone in landscape orientation and I start writing comments the cursor and the text go behind the notch … come on …

Also … I have no way to refresh the list and see if my song is properly memorised in the Cloud. This is a big flaw for me but I have no idea now how to fix it.

I will now move to the next challenge.

Challenge 4: stop people from posting too many line breaks in their comments, or at least trim the comments when shown in the main table view.

This looks to be tough!

Reading the UITextView Documentation didn’t help but was refreshing about what was done here. I wonder how people — Paul included — got to their solutions without the Documentation telling them what ought to be done.

I tried something once I saw in one of Paul’s articles that the method to be called should be the textView(_:shouldChangeTextIn:replacementText:) one. What I did inside, though logical to me, caused the text I tried to input to actually not be inputted at all… I have now asked for help and will take a 1h break to do some of my real work, hoping that the pause will be helpful.


After trying something on my own I got to the point where I could prevent an extra newline being entered but I could not go back to delete the text without showing again the alert. Thanks to some folks at StackOverflow, credit to them, I managed to find a solution. I finally understood how this method works: whenever a user 1) taps a letter on the keyboard or 2) taps the delete key or 3) pastes some text inside the text field or 4) selects some text and replaces/deletes it, this method is called, executing whatever logic you pass it and, now pay attention, substituting the existing text in the selected range (which may be nothing if you just insert at the cursor’s place) with what you entered/pasted if you return true or not doing so if you return false. Why would one return false, you may ask? Well … because you may want to execute some very specific logic such as what you will see below that doesn’t involve adding text.

First of all I declared two global properties: let characterToCheck: Character = "\n" and var characterCount = 0 (they need to be global otherwise each call of the method would reset the counted to 0).

Here is the code, with explanations in comments.

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
	// if we are trying to add a newline character, add 1 to the counter.
    if text == String(characterToCheck) { 
        characterCounter += 1 
    }

	// this, according also to the Documentation, is what happens when the user taps Delete. It substitutes the last character (range == 1) with an empty string. 
	// So, if we pressed the Delete key, we create an Array of our Characters (I know we should not do that but provide me with a better solution) then, if we are deleting the last character and that is a \n, we remove 1 from the counter.
    if text == "" {
        let characters = Array(textView.text)
        if characters.count >= range.location {
            let deletedCharacter = characters[range.location]
            if  deletedCharacter == characterToCheck {
                characterCounter -= 1
            }
        }
    }

	// If we reach the 5th newline then all this happens.
    if characterCounter > 4 {
        let newlineAC = UIAlertController(title: "Too many linebreaks", message: "Please go back and make your comment fit into a maximum of 5 lines", preferredStyle: .alert)
        newlineAC.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] (_) in
            let currentText = textView.text ?? ""
            guard let currentTextRange = Range(range, in: currentText) else { return }

			// ... but this never gets executed... I wonder why... 
            self?.comments.text = currentText.replacingOccurrences(of: "\n", with: "@ ", range: currentTextRange)
        })
        present(newlineAC, animated: true, completion: { [weak self] in
			// this allows us to get back into business
            self?.characterCounter -= 1
        })

        return false
    } else {
        return true
    }
}

So … the code is pretty awful, but this is what I was suggested to do. Creating an Array from a String is the last thing we should do but, really, I do not have another working solution at the moment. Paul is not answering my questions and the Hacking with Swift Slack channel, though Mr. John Auger helped me to his best, is pretty silent after the 100 Days of Swift challenge has completed.


Anyway … enough with these challenges.

I am pretty happy with what the project could have been and pretty disappointed with what has come to the end. The Genre saving is not working, the Text View handling is a consommé of cow crap, the errors in the Simulator are not explainable and, all this, with everything as the tutorial says.

I start wondering what is the point of following tutorials if then you do not get to the bottom of anything.


If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create your website with WordPress.com
Get started
%d bloggers like this: