These last two days have been particularly heavy. My iPhone XS fell and the camera broke and, sincerely, I am not trusting the Serbian Apple Premium Resellers since I will be back to Italy on Wednesday.
Then, yesterday morning, my external LaCie P9227, formatted with half a TB dedicated to TimeMachine and 1.5TB dedicated to external data storage, decided to fail. It first could not complete backup, then I could not eject it, then the TM partition unmounted itself, then I started to get kernel panics whenever this drive was connected.
I tried everything I knew, plus everything the Apple Support told me, plus everything a wonderful user on the discussions.apple.com forum is telling me to do.
It seems that my MacBook Pro is fine but I am getting very stressed about my data and I have 900GB of them which I would like to save from the external drive. But how can I use known tools such as DiskWarrior if the disk will cause a kernel panic as soon as I connect it?
Anyway, today I will start learning project 8 and hopefully finish it before I have to go out. Just before I forget, this should be my #Day38 for the #100DaysOfSwift initiative. I will start from Paul’s Day 36 where we get our grasps on creating our user interface totally in code. I will trust iCloud Drive to back up everything I am doing until I can get a new Time Machine drive.
We create a new Single View App and we can call it either Project8 or, as I did, Seven Swifty Words. I know, using spaces is not advised in project names, and also the repository will become Seven%20Swifty%20Words but well, I am ready to take the uncomfortableness!
Next, in the Targets tab, make this app run only on iPad. A lot of room will be needed so we ask for a big device!
Here are the goals for this project:
- how to use Auto Layout to create user interfaces entirely in code — no storyboard needed.
- text alignment
- layout margins
- … much more!
Building a UIKit user interface programmatically
This topic’s video is 36 minutes long, the longest so far in the series and I just have to say it: this guy is a genius! I will try to paraphrase what I learned in this video.
UI Shopping list
We will start by creating two large labels, one for the clues and one for the answers (they will not be “working” just yet, they will just be there!). Then we will have a
UITextField in the center—to store the user’s current guess, two buttons to submit and clear/try again and 20 (!!!) buttons containing different parts of the clues!
We create five properties, one each for the
scoreLabel (all these three are of type
UILabel!) , a
currentAnswer and an array of buttons called
letterButtons, which we will use at the end of this part.
This will let create our view in code. In past tutorials I had seen this created directly in
viewDidLoad() but this looks cleaner. As I said before, I do not like a
viewDidLoad method filled with things that should not be there.
So, first we create the view (of type
UIView()) and set its background color to be white. Imagine this as a blank canvas on which to draw (sounds nice, right?).
Placing three labels at the top
As we used an implicitly unwrapped optional in the properties’ declarations, we now need to instantiate them with the
UILabel() initialiser. Then we set the label’s tamic (!), that is
.translatesAutoresizingMaskIntoContrains, to be
false as we want to create our constraints in code, its text alignment to be right aligned, its text to be “Score: 0” and, finally, we add it to our view using the
To create constraints we will be using anchors like in Project6 but we will be asking the help of the
NSLayoutConstraint.activate() method which accepts an array of constraints. This is avoid always writing
isActive == true for every single view.
UIKit has several guides that we can anchor our views to and the most common is the
safeAreaLayoutGuide (that is, the space available once you subtract rounded corners and notches) which, inside itself, has a
layoutMarginGuide, which helps the views not run from the left to the right edges of the screen.
You seen, our devices seem to have endless screens but actually that is only the background. Our view are pinned inside said anchors to look nice and well placed!
So, after creating the first label we will call
NSLayoutConstraint.activate() and start writing the array. For the
scoreLabel we will want:
topAnchor’s constraint equal to the view’s layout margins guide’s top anchor.
trailingAnchor’s constraint equal to view’s layout margins guide’s trailing anchor.
Then we need to repeat the job for the
cluesLabel and the
answersLabel. The difference here is that we will set for both of them a
.font property equal to
UIFont.systemFont(ofSize: 24) and a text of, respectively, “CLUES” and “ANSWERS”.
Very important now: set the
.numberOfLines equal to
0 so that the label will resize using “as many lines as needed”.
For constraint here is what is needed:
topAnchor’s constraints of the
cluesLabeland of the
answersLabelneed to be pinned to the
bottomAnchorof the score label (imagine a straight line drawing very thinly and prolonging the bottom side of the score label—don’t you feel the architect in you rising to prominence?!)
cluesLabelwill be pinned to the leading edge of the screen (so that it will rotate for right-to-left languages), be indented by 100 points and take 60% of the screen (minus 100 for the indent).
answersLabelwill instead be pinned to the trailing edge of the screen, indented by 100 and take 40% of the screen (minus 100 points).
- these two labels should be matched in height.
Next is the text field which will show the word being formed by the user. We will instantiate it (
UITextField()), set its
.placeholder property to “Tap letters to guess”, set its text alignment to center, its font size to 44 and, important, its
.isUserInteractionEnabled property to
false. In this way the user can tap his life out of it without every doing anything.
Now constraints time:
- this needs to be entered in our view…
- 50% its width…
- 20 points below the clues label.
Now we create two new buttons, this time making both declaration and instantiation in the same line inside the
loadView method instead of at the top of the class. They need to be
UIButton(type: .system), with a custom
.setTitle(\_:for:) property for the normal state.
Constraint-wise we need three of them for each button:
- vertical position: for the submit button the
topAnchorwill be equal to the
bottomAnchorwhile the clear button will just be aligned to the submit one (using the
- horizontal position: both will be centred but with a 100 point offset to the left and to the right.
- height: 44 points for both of them.
We now need to place 20 buttons in the bottom part of the screen. As soon as layouts get more complex it is a smart move to embed all needed subviews in a container, in this case a blank
UIView. So, we create a
UIView(), we set its tamic to
false and we add it to the main view. As this is the last view of our layout (excluded the buttons inside it) we need to give it more constraints.
At this very point my Xcode started to not auto-complete the code anymore. Cleaning the build-folder, restarting Xcode, rebooting the Mac… nothing… it was just not going beyond the first variable name. Very strange… If anyone knows what may have caused it, please share your thoughts in the comments below!
For this view we want it to have a width of 750 points, a width of 320, to be centred in the superview, to be 20 points below the nearest view above it and 20 points above the nearest view below it. This produces a quite amazing result:
This isn’t a mistake, but Auto Layout didn’t have a precise size for any of our views before so it was simply filling the screen from top to bottom. Now that we have views pinned to the top, pinned to the bottom and with the bottom one having a precise size, Auto Layout will have to stretch what it can, in this case, the score label.
Before doing this it was using a feature called intrinsic content size, that is “how big should a view be to show its content?”. But now we have two other important players coming in the field: content hugging priority and content compression resistance priority, which, I am sincere, I have really had a hard time understanding no matter who was explaining this. Let’s try again!
The first one, the hugging thing, seems to refer to how much the edges of the view would like to stay hugged to their content inside them. If the priority is high (250 by default so pretty low, being out of 1000) the view will resist stretching, otherwise it will not (by default it will not too much).
The second one, whose name is already quite scary by itself, refers to how much the view should resist from being shrunk so much that its inner content becomes unreadable or invisible.
Higher priorities means that Auto Layout will work harder to satisfy them, in general it will more likely stretch them than shrink them, understood?!
Right now we have a problem: all our views share the same priorities so Auto Layout is forced to make its choice at random—well, not really, it will start from the top!—.
What we want in the end is for the clues and answers labels to be larger so we will set their content hugging priority to
1(I guess 249 would have worked just as well but better to be sure, right?). This is the amazing result:
The last task
We now need to actually create a grid of button and we want to have 4 rows and 5 columns, that is: it is time for nested loops! For this time we will not ask for “tamic” to be false so that we can give some fixed positions and have Auto Layout figure out the rest for us.
First thing we set two constants to represent the width and height of the buttons (this makes reference easier). Then, we loop once through row 0 to 3 and once through columns 0 to 4 and, for each of them, we create a new button with a large font and with a large three letter text so that it keeps the spacing in check.
But we cannot stop here: we want to control the size of our buttons and we do this using the Core Graphic Rectangle initialiser. The
x position of each button will be equal to the column times the width, the
y position will be equal to the row times the height and each of the dimension will be set to inherit from the two constants we created before.
Loading a level and adding button targets
This new part where we are going to program the first part of the game is a summa of genius thinking. I may very well be exaggerating but this is all very new to me and I have always been impressed by the translation of logic into practical action. Let’s recap how this game works:
- it asks the player to spell seven words out of given letter groups
- each word is provided with a clue that should help the guessing
We have some constraints:
- the total number of letter groups should add up to 20 (so the best way is to have six three-syllable words and a bisyllabic one).
We get to download the files for the project from the Hacking with Swift GitHub repository and we copy both files in the Xcode project (Paul says to copy both in the video while in the written explanation he asks to copy only one).
Task 1: configure the button taps
We need two new arrays here:
- To store the buttons currently being used to spell a word for a potential answer and
- To store all possible solutions.
Two integers are needed as well, one for the
score and one for the
We create three empty
@objc methods below
viewDidLoad to manage what should happen when a user taps either the letter, the submit or the clear button. These look suspiciously similar to
@IBAction methods but I will keep this for myself!
UIButtons behave slightly differently from
UIBarButtonItems (where we could specify target and selector in the initialiser) so we need to use the dedicated
addTarget() method to connect them to our three methods. This is done right where we created the buttons using the
.addTarget(self, action: #selector(_methodNameHere), for: .touchUpInside) caller. This last parameter specifies that the user pressed down on the button and lifted their touch while still in the button’s area! Just look with awe at how much care Apple has put in wording this! You can of course expect that a
.touchUpOutside will be waiting for you just behind the corner!
The call to this method for the letter buttons must be added inside the nested loop, just telling so that you do not start looking for the buttons in the
We leave these three methods by now and will fill them later.
Task 2: load the level
This method needs to load and parse our level text file and randomly assign letter groups to buttons. This contains a whole lot of new things so, brace yourself!
First: we create three variables, one to store all the level’s clues, one to store how many letters each answer has and an array for all letter groups.
Second: we check if our
Bundle.main contains a
txt file named
level\(level) then, if we find it (and we should find it), we optionally
try? to extrapolate a string from it using the
contentsOf: initialiser for the String data type. Supposing all this went right…
Third: we initialise a variable of type
[String] to contain the
componentsof that string separated by the
\n character (the special character that indicates a newline). We then simply
.shuffle() this array. Still here? Very well, let’s move on…
Fourth: we create a for loop using a tuple instead of a constant so that we can call the
.enumerated() method on our array and store the index of the element in the
index member of the tuple and the string in the
Fifth: we then separate our lines into two parts (called components) separated by a colon and a space calling the
(separatedBy: ": ") initialiser and then store them in two separate constants—
Sixth: we add a string to the clue string created before which will write the (index + 1) quantity followed by the clue so that we bypass the fact that arrays start from 0.
Seventh: we create the correct word for our solution using the
.replacingOccurences(of:with:) method on each first component of the line, modify the solution string with the count of the letter it contains to give an even clearer clue to the user and we append the word now stripped of the
| character to the
Eight: last but not least we need still to use those words separated by the pipe character as bits for the user to use so we append every bit to the
letterBits array using the
.components(separatedBy: "|") and then the
Task 3: configure the buttons!
We need to set up the text for the clues label and the answers label using our two strings
solutionString plus the
.trimmingCharacter(in: .whitespacesAndNewLines) method. This allows us to trim (that is, cut out) any blank space and/or new line character from the beginning and/or the end of our string (our strings will ended come with an extra new line at the end because of the loop).
We then shuffle our letter bits array and if its count is equal to the letter buttons count (which should always be but we are never too safe) we call another loop between zero and the amount of buttons minus one, setting the title for each button to the corresponding letter bit.
Finally we call our hard earned method in
viewDidLoad and enjoy this view:
It’s play time:
firstIndex(of:) and joined()
This new part looks easy when you read the code but it is conceptually quite complex. Also, I needed to rely on the text because Paul’s cadence was so fast I could not possibly understand the logic behind the three methods we have to write here.
As usual we want to be safe so we check if the
sender’s title label’s text (that is, the text inside of the button tapped) contains something. If so great, we store it into a constant, if not we exit from the method (because something went awfully wrong).
Second, we append said found text to the
.text property of the current answer’s text field.
Now, important because it seems trivial when you see it on screen but trivial is not: we append the button we tapped on to the list of activated buttons and then we hide it so that it is not possible to interact with it anymore (theoretically it is still there and it is tappable but…)
This method does a few simple things: first it substitutes the text in the current answer text field with an empty string so that it looks empty to the user, then it loops over all the button on which the user has tapped to show them back and finally it removes them from the activated buttons’ array so that we can start over.
First of all we check if there is some text in the current answer’s text field. In this way if the user presses the submit button before tapping any letter it will just exit the method (that is, it will do nothing at all). Assuming some text was found we store it in a constant and conditional bind its position in the solution array if found! This is done using the
.firstIndex(of:) method on the solutions array. If we succeeded here as well our adrenaline should be pumping quite high so better profit from that: we remove all the buttons from the activated buttons’ array and we create an optional array of strings with the components of the the answers label’s text separated by the newline special character.
We then assign the answer text given by the user to the
splitAnswers array in the position found before. We finally join the elements of the array together once more using the
.joined(separator:) method, but at this point I have really not understood what is happening. Maybe this is because I am struggling with keeping track of all these variable names… I tend to lose myself on what is what and what belongs to who… Let me try to rephrase all this, maybe it will come out better …
- we check if there is an answer
- we browse the
solutionsarray for a valid answer and, if we find it, we store its position in the
solutionsarray in the
solutionPositionconstant. That is, if the answer is wrong nothing should happen, right? In any case we either find the first answer in the array or we find nothing, I guess…
- we split the answers in an array of answers when we find the
\nseparator. I have the feeling we had already done this but can’t find it now. Also, couldn’t we use the solutions array for this? That should already contain those answers…
- we insert the answer given by the user in this new array at that position so that it replaces the number of letters needed (yes, this makes more sense) and then we rejoin it.
Okay, I feel better now!
We then empty the current answer’s text field and increase the score by 1 (please notice that the score label will not update at this stage and it is intentional!).
If the score is evenly divisible by 7 we can move to the next level by creating an alert.
We need to do something when the user guesses everything right. We add
1 to the level, we remove everything from the solutions array, we call once more the
loadLevel method (which should therefore elaborate on the new file) and then we show all the buttons!
The last thing to do is what allows us to have the score label updated. We could write the code everywhere we need but that is repetitive (remember the DRY rule?) and, also, not safe at all. We may change something in the future and then forget to update it somewhere.
The solution to this is property observers which let us execute some code every time a property is changed. There are two of them:
willSet, the first which will run the code inside the braces as soon as the property gets a change in value and the other which will do it just the instant before that is done!
We then add the necessary code as a computed property after the default value of
score and that is done.
This was a brilliant project, I really enjoyed going through it.
The only thing that I see not working is that, when we finish the second level we get the same alert as before and we are thrown into an empty level but with the buttons still showing some letters. Something dark and evil seems at work here and I fear this will be the topic of our next challenge!
As always, feel free to comment down below and let me know if this new narrative style is pleasing to you or if you would prefer it as it was before.
The code for this project can be found here.
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!