Day 23
Day 23 관련
Milestone: Projects 1-3
It’s time for another consolidation day, because we’ve covered a lot of ground in the first three topics and it’s important you review them if you want them to stick in your head.
However, this will also be the first day you’re asked to create a complete app from scratch. Don’t worry: I outline all the components required to make it work, and also provide hints to give you a head start.
As you’ll see, creating an app from scratch is a very different experience to adding modifications to an existing app: you get blank page paralysis, which is where your brain knows where you want to get to but you’re just not sure how to start.
A common reason to get stuck is that folks try to write flawless code first time. As Margaret Atwood once said, “if I waited for perfection, I would never write a word.” So, dive in and see where you get – these milestone challenges will help you learn to get comfortable starting fresh projects, and to get real functionality up and running quickly.
Today you have three topics to work through, one of which of is your challenge.
What you learned
What you learned
You’ve made your first two projects now, and completed a technique project too – this same cadence of app, game, technique is used all the way up to project 30, and you’ll start to settle into it as time goes by.
Both the app and the game were built with UIKit – something we’ll continue for two more milestones – so that you can really start to understand how the basics of view controllers work. These really are a fundamental part of any iOS app, so the more experience I can give you with them the better.
At this point you’re starting to put your Swift knowledge into practice with real context: actual, hands-on projects. Because most iOS apps are visual, that means you’ve started to use lots of things from UIKit, not least:
- Table views using
UITableView
. These are used everywhere in iOS, and are one of the most important components on the entire platform. - Image views using
UIImageView
, as well as the data inside them,UIImage
. Remember: aUIImage
contains the pixels, but aUIImageView
displays them – i.e., positions and sizes them. You also saw how iOS handles retina and retina HD screens using @2x and @3x filenames. - Buttons using
UIButton
. These don’t have any special design in iOS by default – they just look like labels, really. But they do respond when tapped, so you can take some action. - View controllers using
UIViewController
. These give you all the fundamental display technology required to show one screen, including things like rotation and multi-tasking on iPad. - System alerts using
UIAlertController
. We used this to show a score in project 2, but it’s helpful for any time you need the user to confirm something or make a choice. - Navigation bar buttons using
UIBarButtonItem
. We used the built-in action icon, but there are lots of others to choose from, and you can use your own custom text if you prefer. - Colors using
UIColor
, which we used to outline the flags with a black border. There are lots of built-in system colors to choose from, but you can also create your own. - Sharing content to Facebook and Twitter using
UIActivityViewController
. This same class also handles printing, saving images to the photo library, and more.
One thing that might be confusing for you is the relationship between CALayer
and UIView
, and CGColor
and UIColor
. I’ve tried to describe them as being “lower level” and “higher level”, which may or may not help. Put simply, you’ve seen how you can create apps by building on top of Apple’s APIs, and that’s exactly how Apple works too: their most advanced things like UIView
are built on top of lower-level things like CALayer
. Some times you need to reach down to these lower-level concept, but most of the time you’ll stay in UIKit.
If you’re concerned that you won’t know when to use UIKit or when to use one of the lower-level alternatives, don’t worry: if you try to use a UIColor
when Xcode expects a CGColor
, it will tell you!
Both projects 1 and 2 worked extensively in Interface Builder, which is a running theme in this series: although you can do things in code, it’s generally preferable not to. Not only does this mean you can see exactly how your layout will look when previewed across various device types, but you also open the opportunity for specialized designers to come in and adjust your layouts without touching your code. Trust me on this: keeping your UI and code separate is A Good Thing.
Three important Interface Builder things you’ve met so far are:
- Storyboards, edited using Interface Builder, but used in code too by setting storyboard identifiers.
- Outlets and action from Interface Builder. Outlets connect views to named properties (e.g.
imageView
), and actions connect them to methods that get run, e.g.buttonTapped()
. - Auto Layout to create rules for how elements of your interface should be positioned relative to each other.
You’ll be using Interface Builder a lot throughout this series. Sometimes we’ll make interfaces in code, but only when needed and always with good reason.
There are three other things I want to touch on briefly, because they are important.
First, you met the Bundle
class, which lets you use any assets you build into your projects, like images and text files. We used that to get the list of NSSL JPEGs in project 1, but we’ll use it again in future projects.
Second, loading those NSSL JPEGs was done by scanning the app bundle using the FileManager
class, which lets you read and write to the iOS filesystem. We used it to scan directories, but it can also check if a file exists, delete things, copy things, and more.
Finally, you learned how to generate truly random numbers using Swift’s Int.random(in:)
method. Swift has lots of other functionality for randomness that we’ll be looking at in future projects.
Key points
Key points
There are five important pieces of code that are important enough they warrant some revision. First, this line:
let items = try! fm.contentsOfDirectory(atPath: path)
The fm
was a reference to FileManager
and path
was a reference to the resource path from Bundle
, so that line pulled out an array of files at the directory where our app’s resources lived. But do you remember why the try!
was needed?
When you ask for the contents of a directory, it’s possible – although hopefully unlikely! – that the directory doesn’t actually exist. Maybe you meant to look in a directory called “files” but accidentally wrote “file”. In this situation, the contentsOfDirectory()
call will fail, and Swift will throw an exception – it will literally refuse to continue running your code until you handle the error.
This is important, because handling the error allows your app to behave well even when things go wrong. But in this case we got the path straight from iOS – we didn’t type it in by hand, so if reading from our own app’s bundle doesn’t work then clearly something is very wrong indeed.
Swift requires any calls that might fail to be called using the try
keyword, which forces you to add code to catch any errors that might result. However, because we know this code will work – it can’t possibly be mistyped – we can use the try!
keyword, which means “don’t make me catch errors, because they won’t happen.” Of course, if you’re wrong – if errors do happen – then your app will crash, so be careful!
The second piece of code I’d like to look at is this method:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pictures.count
}
That was used in project 1 to make the table view show as many rows as necessary for the pictures
array, but it packs a lot into a small amount of space.
To recap, we used the Single View App template when creating project 1, which gave us a single UIViewController
subclass called simply ViewController
. To make it use a table instead, we changed ViewController
so that it was based on UITableViewController
, which provides default answers to lots of questions: how many sections are there? How many rows? What’s in each row? What happens when a row is tapped? And so on.
Clearly we don’t want the default answers to each of those questions, because our app needs to specify how many rows it wants based on its own data. And that’s where the override
keyword comes in: it means “I know there’s a default answer to this question, but I want to provide a new one.” The “question” in this case is “numberOfRowsInSection”, which expects to receive an Int
back: how many rows should there be?
The last two pieces of code I want to look at again are these:
let cell = tableView.dequeueReusableCell(withIdentifier: "Picture", for: indexPath)
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController {
}
Both of these are responsible for linking code to storyboard information using an identifier string. In the former case, it’s a cell reuse identifier; in the latter, it’s a view controller’s storyboard identifier. You always need to use the same name in Interface Builder and your code – if you don’t, you’ll get a crash because iOS doesn’t know what to do.
The second of those pieces of code is particularly interesting, because of the if let
and as? DetailViewController
. When we dequeued the table view cell, we used the built-in “Basic” style – we didn’t need to use a custom class to work with it, so we could just crack on and set its text.
However, the detail view controller has its own custom thing we need to work with: the selectedImage
string. That doesn’t exist on a regular UIViewController
, and that’s what the instantiateViewController()
method returns – it doesn’t know (or care) what’s inside the storyboard, after all. That’s where the if let
and as?
typecast comes in: it means “I want to treat this is a DetailViewController
so please try and convert it to one.”
Only if that conversion works will the code inside the braces execute – and that’s why we can access the selectedImage
property safely.
Challenge
Challenge
You have a rudimentary understanding of table views, image views, and navigation controllers, so let’s put them together: your challenge is to create an app that lists various world flags in a table view. When one of them is tapped, slide in a detail view controller that contains an image view, showing the same flag full size. On the detail view controller, add an action button that lets the user share the flag picture and country name using UIActivityViewController
.
To solve this challenge you’ll need to draw on skills you learned in tutorials 1, 2, and 3:
- Start with a Single View App template, then change its main
ViewController
class so that builds onUITableViewController
instead. - Load the list of available flags from the app bundle. You can type them directly into the code if you want, but it’s preferable not to.
- Create a new Cocoa Touch Class responsible for the detail view controller, and give it properties for its image view and the image to load.
- You’ll also need to adjust your storyboard to include the detail view controller, including using Auto Layout to pin its image view correctly.
- You will need to use
UIActivityViewController
to share your flag.
As always, I’m going to provide some hints below, but I suggest you try to complete as much of the challenge as you can before reading them.
Hints:
- To load the images from disk you need to use three lines of code:
let fm = FileManager.default
, thenlet path = Bundle.main.resourcePath!
, then finallylet items = try! fm.contentsOfDirectory(atPath: path)
. - Those lines end up giving you an array of all items in your app’s bundle, but you only want the pictures, so you’ll need to use something like the
hasSuffix()
method. - Once you have made
ViewController
build onUITableViewController
, you’ll need to override itsnumberOfRowsInSection
andcellForRowAt
methods. - You’ll need to assign a cell prototype identifier in Interface Builder, such as “Country”. You can then dequeue cells of that type using
tableView.dequeueReusableCell(withIdentifier: "Country", for: indexPath)
. - The
didSelectItemAt
method is responsible for taking some action when the user taps a row. - Make sure your detail view controller has a property for the image name to load, as well as the
UIImageView
to load it into. The former should be modified fromViewController
insidedidSelectItemAt
; the latter should be modified in theviewDidLoad()
method of your detail view controller.
Bonus tip: try setting the imageView
property of the table view cell. Yes, they have one. And yes, it automatically places an image right there in the table view cell – it makes a great preview for every country.
Note
Don’t worry if you don’t complete challenges in the day they were assigned – in future days you’ll find you have some time to spare here and there, so challenges are something you can return back to in the future.