Day 99
Day 99 êŽë š
Milestone: Projects 28-30
Today is the last consolidation day of this 100 Days challenge, and weâll be looking over what youâve learned before diving straight into your toughest challenge yet.
I know the thought of a tough challenge might not be welcome at this point, because youâre almost certainly tired. But as the Greek philosopher Epictetus once said, âthe greater the difficulty the more glory in surmounting itâ â this is a biggie, but youâll feel great when youâre done because youâll have had to exercise so many of the skills youâve learned over these past few months.
Although biometric authentication is an interesting topic â and certainly an important one if you happen to be on a team building apps for a bank! â the real highlights of these last days have been more Core Graphics practice and dipping a toe in Instruments. Both of these are skills that will last you years. Sure, you might not remember the exact code required to make clipping masks in Core Graphics, or the precise button to press in Instruments to find the problem youâre hunting for, but you know where to look, and, more importantly, you know what youâre looking for.
As you might have learned yourself, when youâre facing a problem half the battle is figuring out what youâre looking for â what words to use, or even vaguely what kind of UIKit class might contain your answer. Since this course started youâve gained so much experience across so many of Appleâs frameworks, but you will already have forgotten half of it.
And you know what? Thatâs OK. In fact, thatâs normal. What you havenât lost is all the code you wrote, which you can use as a reference for all your projects going forward. You also havenât lost the concepts â youâre used to the idea of a table view delegate now, just like youâre used to storyboards, or view controllers, or Codable
, and more. Those are the things that last, and, helpfully, those are the things that matter.
Today you have three topics to work through, one of which of is your challenge.
What you learned
What you learned
Project 29 was a serious game with a lot going on, not least the dynamically rendered buildings with destructible terrain, the scene transitions and the UIKit/SpriteKit integration.
And in project 30 we took our first steps outside of Xcode and into Instruments. I could write a whole book on Instruments, partly because itâs extremely powerful, but also because itâs extremely complicated. As per usual, I tried to cherrypick things so you can see useful, practical benefits from what I was teaching, and certainly you have the skills now to be able to diagnose and result a variety of performance problems on iOS.
Here are some of the things you learned in this milestone:
- How to access the keychain using SwiftKeychainWrapper.
- How to force the keyboard to disappear by calling
resignFirstResponder()
on a text view. (And remember: it also works on text fields.) - How to detect when your app is moving to the background by registering for the
UIApplication.willResignActiveNotification
notification. - How to use
LAContext
from the LocalAuthentication framework to require Touch ID authentication. - Using the
stride()
function to loop across a range of numbers using a specific increment, e.g. from 0 to 100 in tens. - Creating colors using the hue, saturation, and brightness. As I said, keeping the saturation and brightness constant while choosing different hues helps you create similar color palettes easily.
- SpriteKit texture atlases. These are automatically made by Xcode if you place images into a folder with the .atlas extension, and are drawn significantly quicker than regular graphics.
- Using
usesPreciseCollisionDetection
to make collisions work better with small, fast-moving physics bodies. - Transitioning between scenes with the
presentScene()
method and passing in a transition effect. Weâll be using this again in project 36, so youâll have ample time to practice transitions. - Using the blend mode
.clear
to erase parts of an image. Once that was done, we just recalculated the pixel-perfect physics to get destructible terrain. - Adding dynamic shadows to views using
layer.shadowRadius
and other properties â and particularly how to use thelayer.shadowPath
property to save shadow calculation. - The importance of using
dequeueReusableCell(withIdentifier:)
so that iOS can re-use cells rather than continually creating new ones. - How the
UIImage(named:)
initializer has an automatic cache to help load common images. When you donât need that, use theUIImage(contentsOfFile:)
initializer instead.
Key points
Key points
There are two things Iâd like to review for this milestone.
First, the weak keyword. We used it in project 29 to add a property to our game scene:
weak var viewController: GameViewController!
We also added the opposite property to the game view controller:
var currentGame: GameScene!
This approach allowed the game scene to call methods on the view controller, and vice versa. At the time I explained why one was weak
and the other was not â do you remember? I hope so, because itâs important!
There are four possibilities:
- Game scene holds strong view controller and view controller holds strong game scene.
- Game scene holds strong view controller and view controller holds weak game scene.
- Game scene holds weak view controller and view controller holds weak game scene.
- Game scene holds weak view controller and view controller holds strong game scene.
Remember, âstrongâ means âI want to own this; donât let this memory be destroyed until Iâm done with it,â and weak means âI want to use this but I donât want to own it; Iâm OK if it gets destroyed, so donât keep it around on my account.â
Now, the view controller has an SKView
, which is what renders SpriteKit content. Thatâs owned strongly, because obviously the view controller canât really work without something to draw to. And that SKView
has a scene
property, which is the current SKScene
visible on the screen. Thatâs also strongly owned. As a result, the view controller already - albeit indirectly â has strong ownership of the game scene.
As a result, both options 1 and 2 will cause a strong reference cycle, because they would cause the game scene to have a strong reference to something that has a strong reference back to the game scene. This isnât necessarily bad as long as you remember to break the strong reference cycle, but letâs face it: why take the risk?
That leaves options 3 and 4: both have the game scene using a weak
reference to the view controller, but one has a weak reference going back the other way and the other has a strong one. Which is better? Honestly, Iâm not sure it matters: using a strong reference wouldnât result in anything new because thereâs already the indirect strong reference in place. So, use whichever you prefer!
The second thing Iâd like to cover is much easier: itâs the UIImage(contentsOfFile:)
initializer for UIImage
. Like I said in project 30, the UIImage(named:)
initializer has a built-in cache system to help load and store commonly used images â anything you load with that initializer will automatically be cached, and so load instantly if you request it again.
Of course, if you donât want something to be cached, thatâs the wrong solution, which is where UIImage(contentsOfFile:)
comes in: give it a path and it will load the image, with no magic caching ever happening.
The downside is that UIImage(named:)
automatically finds images inside your app bundle, whereas UIImage(contentsOfFile:)
does not. So, you need to write code like this:
let path = Bundle.main.path(forResource: someImage, ofType: nil)!
imageView.image = UIImage(contentsOfFile: path)
Thatâs hardly a lot of code, but itâs never nice writing even simple repetitive code â and look at that force unwrap after path()
! Wouldnât it be great to get rid of it? Iâm going to show you how to do just that, and, as a bonus, Iâm also going to teach you something new: convenience initializers.
Youâve already seen initializers â weâve used dozens of them. They are special methods that create things, like UILabel()
or UIImage(named:)
. Swift has complex rules about its initializers, all designed to stop you trying to access something that hasnât been created yet.
Fortunately, thereâs one easy part, which is convenience initializers, which are effectively wrappers around basic initializers that are designed to make coding a bit more pleasant. A convenience initializer is able to do some work before calling a regular initializer, which in our case means we can add a wrapper around UIImage(contentsOfFile:)
so that itâs nicer to call.
To make things even better, weâre also going to get rid of the force unwrap. Remember, path(forResource:)
can return nil because the file you requested might not exist. Force unwrapping it works on occasion if you know something definitely exists, but itâs usually a better idea to use an alternative such as failable initializers.
Youâve already used failable initializers several times â both UIImage(named:)
and UIImage(contentsOfFile:)
are failable, for example. A failable initializer is one that might return a valid created object, or it might fail and return nil. Weâre going to use this so that we can return nil
if the image name canât be found in the app bundle.
So, leveraging the power of Swift extensions that you learned in project 24, hereâs a UIImage
extension that creates a new, failable, convenience initializer called UIImage(uncached:)
. It works like UIImage(named:)
in that you donât need to provide the full bundle path, but it doesnât have the downside of caching images you donât intend to use more than once.
Hereâs the code:
extension UIImage {
convenience init?(uncached name: String) {
if let path = Bundle.main.path(forResource: name, ofType: nil) {
self.init(contentsOfFile: path)
} else {
return nil
}
}
}
Note the init?
syntax that marks this as an initializer that returns an optional.
Challenge
Challenge
Your challenge is to create a memory pairs game that has players find pairs of cards â itâs sometimes called Concentration, Pelmanism, or Pairs. At the very least you should:
Come up with a list of pairs. Traditionally this is two pictures, but you could also use capital cities (e.g. one card says France and its match says Paris), languages (e.g one card says âhelloâ and the other says âbonjourâ), and so on. Show a grid of face-down cards. How many is down to you, but if youâre targeting iPad I would have thought 4x4 or more. Let the player select any two cards, and show them face up as they are tapped. If they match remove them; if they donât match, wait a second then turn them face down again. Show a You Win message once all are matched. You can use either SpriteKit or UIKit depending on which skill you want to practice the most, but I think youâll find UIKit much easier.
Donât under-estimate this challenge! To make it work youâre going to need to draw on a wide variety of skills, and it will push you. Thatâs the point, though, so take your time and give yourself space to think.
If youâre looking for a more advanced challenge, go for a variant of the game that uses word pairs and add a parental option that lets them create new cards. This would mean:
- Authenticating users using Touch ID or Face ID.
- Showing a new view controller that lists all existing cards and lets them enter a new card.
- You can use a
UIAlertController
with one or two text fields for your card entry, depending on what kind of game youâve made.
Please go ahead and try to solve the challenge now. My hints are below, but please try to avoid reading them unless youâre really struggling.
- Start small. Seriously! Find something really simple that works, and only try something bigger or better once your simplest possible solution actually works.
- If youâre using UIKit, you could try to solve this using a
UICollectionView
. This gives you a natural grid, as well as touch handling for selecting cells, but make sure you think carefully about cells being re-used â this might prove more difficult than you thought. - An easier approach is to lay out your cards much like we did with the word letters in project 8, 7 Swifty Words. You could show your card backs as a button image, then when the button is tapped show the other side of the card â which might be as simple as changing the picture and making the buttonâs text label have a non-clear color, or perhaps using Core Graphics to render the text directly onto the card front image.
- If you made the buttons work and want to try something fancier, you can actually create a flip animation to toggle between views â see my article How to flip a UIView with a 3D effect:
transition(with:)
for more information. - In terms of tracking the game state it really only has three states: player has chosen zero cards, player has chosen one card (flip it over), and player has chosen two cards (flip the second one over). When theyâve chosen two cards you need to decide whether you have a match, then either remove the cards or flip them back down and go back to the first state.
- For the content to show, you can just type in a list of words/images into your code if you want, but youâre welcome to use
Codable
if you want to push yourself.
Again, this is not an easy challenge so please take your time and donât feel bad when you find yourself having to look back at previous projects.
Note
Donât worry if you donât complete challenges in the day they were assigned. Todayâs challenge is particularly tough, so it will probably be something you solve over a few days. Take your time, look back on previous projects to see how you can re-use ideas, donât be afraid to experiment, and remember to ask for help if you need it.