This lab is designed to take you through many of the key features of XCode which is the default Integrated Development Environment (IDE) for writing iOS apps. Don’t worry if you don’t understand many of the terms. Spend time finding your way around the XCode interface and ensure you understand the purpose behind the steps you carry out.
To complete this lab you will need to be running XCode 8 since we will be writing our code in the Swift programming language.
During this lab we will be building a shopping app called To Do which allows you to add and remove items from a shopping list at check items off as you buy them. All the graphical assets are available for you to use.
In order to complete this lab you will need access to a Mac computer running a recent version of OS X and XCode 8. These are both free downloads from the Mac App Store.
The lab is broken down in a series of tasks. At the end of each task you should run your project in the simulator to ensure there are no build errors. The steps it takes you through are typical for all iOS projects.
So during this lab, make sure you remember to test everything after each step.
Once you have successfully completed the main lab tasks you are encouraged to have a go at the XCode Extension Exercises. These are slightly more challenging and will help your understanding of writing iOS apps. Presentation Slides During the lab you will have several key concepts explained to you. A copy of the slides is available online for you to refer back to.
After you power on the Mac you will be able to log in using your standard University credentials by selecting the Other option.
Press the cmd + space keys to bring up the Spotlight window. Type ‘xcode’ and press the enter key to launch the3 IDE..
You will be presented with a screen similar to that shown. Make sure you choose the second option (create a new XCode project).
From the project screen you should pick the Single View App template.
On the project options screen you need to assign a suitable title to your app and enter your organisation name (use your name for this. You also need to provide a unique identifier in a * ‘reverse uri’ format (I base mine on my email address). Make sure you have chosen ‘Swift’ as you programming language and iPhone as your device. The Core Data, Unit and UI Test checkboxes should be unchecked. When prompted, save your project to a suitable location.
In the next screen you will need to choose where to save your project. In the labs you can save to the desktop (OSX shortcut is cmd+D).
The next step is to build our interface (or view) as a storyboard. This is stored in a file called Main.storyboard
and contains any views we want to use in the app. Open this file.
When you first open the storyboard it contains a single view which represents one screen in the app. The shape and size of this view is based on the iPhone 6S.
For this exercise we will be targeting the iPhone SE wo we need to perform two steps.
Select the File Inspector tab (see screenshot below) and uncheck the option Use Trait Variations.
You will be prompted to choose the size class data you wish to retain. Make sure the iPhone option is selected and click on Disable Trait Variations.
The views in your storyboard can be sized to represent any iOS device.
Click on the View as button in the bottom gutter as shown in the screenshot. This opens a drawer that allows you to switch between the different device screens and their orientation.
Choose the iPhone SE and keep the orientation as portrait.
Click on the View as button again to close the drawer.
We want to display a list of items on the the screen using a special view called a UITableViewController.
The first step is to delete the current view controller in the storyboard. Click on the top of the view and then click the backspace key on your keyboard to remove it.
Locate the libraries pane which is located at the bottom of the utilities panel at the right-hand side of the screen. Select the Object Library (the third tab as shown in the screenshot below).
Next we will add a Navigation Controller which includes a Table View Controller. Drag a Navigation Controller into the storyboard.
You can see that the table view controller is labelled as the root view controller. The root view controller in the view that gets loaded when the application first runs. It sits inside the navigation controller which provides a mechanism for additional views to be stacked and unstacked.
You need to tell your app which view needs to be loaded first. Open the Attributes Inspector, this is the fourth tab in the Inspector Pane which is at the top of the utilities panel.
Select the Navigation Controller in the storyboard and then check the Is Initial View Controller checkbox in the Attributes Inspector. You should see an arrow pointing to the Navigation Controller.
In our table view you can see a single table cell labelled a prototype cell. As we add rows to our table, this cell gets duplicated as many times as required to hold the information. At the moment it is blank however we need it to contain a label to hold the names of the items in our shopping list.
Open the Document Outline (by clicking on the indicated button in the grey status bar shown above) and select the Table View Cell (as shown).
Open the Attribute Inspector in the right pane and use the Style dropdown to select the Basic style. This will add a single label to the prototype cell.
We also need to give this prototype cell an identifier which we can use to refer to it in our code. Let's assign the identifier ShoppingItem.
We will need a Navigation Bar across the top of the Table View Controller. Select it by clicking on the top edge of it. We need to add a Navigation Bar. This will allow us to change the page title and, later, add a button to it.
Open the Attributes Inspector and, in the Simulated Metrics section set the Top Bar to Opaque Navigation Bar as shown.
Double click to change the label on the nav bar to show the name of our app.
Each view requires a matching controller which contains the code that is needed to handle user interactions. Since we have deleted a view and added a different view we need to delete the existing view controller and add a new one. In the Project Navigator locate the file called ViewController.swift right click on it and delete it. In the dialog box choose Move to Trash.
We now need to add a new controller file. This will be a subclass of UITableViewController. Start by right-clicking on the ToDo folder in the Project Navigator and choosing New File from the context menu.
In the iOS tab, locate the Cocoa Touch Class option and choose Next.
First change the Subclass to UITableViewController.
Then change the name to ListController.
The final task is to connect it to the Table View in the storyboard. Select the Table View in the Storyboard then open the Identity Inspector which is the third tab in the Inspectors Pane at the top of the Utilities Panel. You can now assign your custom class ListController to this view.
Test your app now. Set the iPhone SE simulator as the active scheme then Build and Run the Active Scheme.
In this task we will learn how the list view displays data and use this knowledge to display some placeholder text. An iOS app uses three data source methods to determine what to display in a Table View. These are already defined in the ListController.swift file. Open this and locate the three methods (shown below). The third one is commented out using the /* */ characters and so appears in green. Remove these block comment characters and make sure you understand how they work.
override func numberOfSections(in tableView: UITableView) -> Int {
return 0 ①
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0 ②
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) ③
return cell ④
}
- The first tells the controller how many table sections there will be , it returns an integer ①.
- The second calculates how many cells should be in a given section, returning an integer ②.
- The third takes an indexpath struct (which contains a section and a row value), it returns a UITableViewCell object ③④.
We want to have a single section with 5 rows in it. In each cell we want to display the string “Hello World”. To achieve this we modify our methods as shown. The Autocomplete feature should help you.
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ShoppingItem", for: indexPath)
if let label = cell.textLabel {
label.text = "Hello World"
}
return cell
}
There is a lot going on here so let's break it down...
- We return 1 from the first method to define one section,
- We return 5 from the second method which indicates that all sections should contain 5 rows
- In the third method we use the identifier to specify what cell should be used. We then try to get a reference to its textLabel object and, if this succeeds we assign a string to it, this is termed unwrapping an optional variable.
Test you app to make sure the changes work correctly.
Whilst it was a good test to make sure the three uitableview datasource methods were working correctly, displaying the same text in each cell doesn’t make a particularly good to do list! Computers use a data structure called an array to store lists of items so we will modify our code to create an array, add items to it and use this data in our table view.
class ListController: UITableViewController {
var items:[String] = ["Bread", "Butter"]
override func viewDidLoad() {
super.viewDidLoad()
}
At the top of your ListController class, declare an array object. This is placed just under the class declaration and before the viewDidLoad method. Because it has been declared outside any methods it has global scope (it can be accessed by any method in this class.
Let’s analyse the syntax we used. The var keyword tells us we are declaring a variable. The name of the variable is items and it will hold an array of strings (the square brackets denote an array). It will be assigned two values, Bread and Butter.
We now need to modify two of our data source methods. The number of rows will depend on the number of strings in the array (retrieved using the .count property) and the string in each cell is stored in the appropriate array index.
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ShoppingItem", for: indexPath)
if let label = cell.textLabel {
label.text = self.items[indexPath.row]
}
return cell
}
Hint: Use tab to finish off syntax (Auto Complete), saves time and a lot of typing!
The indexPath variable contains both a section and a row property. We are using the row property.
If we run our app now we should see the two items appearing in our table view.
Hint: Remember you need to change the override func tableView as we don’t need 5 cells, we need enough cells to display the items in the array.
An important feature is to allow users to add new items to the list. We will achieve this by adding a button to the navigation bar. When the user clicks this it will display a dialog box on the screen. During this task we will learn about how we use IBActions to detect user interaction in the view and run a block of code in the view controller.
Start by displaying a navigation bar in the storyboard. Select the Navigation Controller and open the Attributes Inspector. For the Top Bar choose Opaque Navigation Bar from the dropdown.
Next find the Bar Button Item in the Object Library. Drag this to the right hand side of the navigation bar and select it.
In the Attributes Inspector use the dropdown list change the System Item attribute to Add.
In the toolbar click on the Assistant Editor button to display both the storyboard and the view controller.
Hold down the ctrl key and drag from the Bar Button Item into the View Controller as shown.
A dialog box will appear. Choose the Action connection, give the action a suitable name and specify the UIBarButtonItem object Type will trigger it. Click on Connect.
This will add a new method that has a special @IBAction keyword in front of it. Any code you add here will run when the Bar Button Item is clicked.
@IBAction func showDialog(_ sender: UIBarButtonItem) {
print("showDialog")
}
Call the print function which will display a message in the system log (in the Debug Area at the bottom of the XCode window) when it is run.
Run your app and try pressing the button a few time. What happens? Look at the Output window in Xcode.
Now we have a button to click with an NSAction to run we can add code to display our dialog box. Dialog boxes are instances of the UIAlertController class so the first step is to create one of these and display it on-screen.
@IBAction func showDialog(_ sender: UIBarButtonItem) {
print("showDialog")
let alert = UIAlertController(title: "New Item", message: "Type item below", preferredStyle: UIAlertControllerStyle.alert)
present(alert, animated: true, completion: nil)
}
Start by creating a UIAlertController and instantiating it. We can then call the presentViewController, passing our controller which will display it on the screen. Unfortunately since there are no buttons we can’t dismiss it!
Lets add an action to the alert which will add a button. We specify a title and a method that will be called when the button has been clicked. We will also need to add this method.
@IBAction func showDialog(_ sender: UIBarButtonItem) {
print("showDialog")
let alert = UIAlertController(title: "New Item", message: "Type item below", preferredStyle: UIAlertControllerStyle.alert)
alert.addTextField(configurationHandler: nil)
alert.addAction(UIAlertAction(title: "Add", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
If we run our app now we'll see a button. When this is clicked the dialog will close the the log message ‘itemAdded’ will be displayed in the output window in XCode. We can also click on the cancel button. At this stage this is the only functionality.
To trigger an action we need to write a handler. At the moment both buttons have no handler assigned. We only need to write a handler for the Add button. The handler takes the form of a closure (an anonymous function passed as a parameter). This will be familiar to anyone who as programmed in a functional language such as JavaScript. Modify the addAction.
@IBAction func showDialog(_ sender: UIBarButtonItem) {
print("showDialog")
let alert = UIAlertController(title: "New Item", message: "Type item below", preferredStyle: UIAlertControllerStyle.alert)
alert.addTextField(configurationHandler: nil)
alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { (action) in
if let textFields = alert.textFields {
if let item = textFields[0].text {
print(item)
}
}
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
Take a close look at the handler. This closure takes an action as a parameter and returns Void (nothing). We unwrap the array of textfields because they may not exist. We then need to unwrap the text value from the first array index. Try running the app and watch the output window.
Our final step is to add our new string value it to the array. Let’s append the string to the array and print it out to make sure this has been successful.
if let textFields = alert.textFields {
if let item = textFields[0].text {
print(item)
self.items.append(item)
print(self.items)
}
}
If you test your app you will see that the array now contains three strings but the UITableView only displays the first two.
You have probably noticed that although the items are being added to the array, this is not updating the information in the TableView, this has to be explicitly requested. The most logical place to update this is in the alert.addAction() handler however there is a problem…
Modern software can split its execution into multiple threads that each run separately. The user interface (UI) thread handles user input and updates the screen however if we run all our code in this thread the interface would become unresponsive while the other code is being run. To avoid locking up the user interface, we can create additional threads and run code in them, this is what happens when we run a completion handler.
Can you see where the problem is? We need to reload the data in the TableView from inside a completion handler yet the code in the completion handler is not running in the UI thread so can't see the TableView! How do we resolve this.
Because this is quite a common issue, Apple developed a powerful set of tools to handle multithreading, called Grand Central Dispatch (GCD). It allows us to push code to any named thread using a completion handler, passing in the name of the thread queue and the code to execute (as a completion handler).
if let textFields = alert.textFields {
if let item = textFields[0].text {
print(item)
self.items.append(item)
print(self.items)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
In this example we push the reload command into the main UI thread queue.
By the time you arrive at this page you will have a simple app that allows you to add items to a list however you have probably noticed that there is a lot of missing functionality that we might want to add.
- There is no customisation of the icon or load screen
- If you tap on a cell the background changes to grey and remains grey
- It would be good to be able to tap on a row to check it off
- We can’t delete or reorder the items
- If we shut down the app, when we open it again our data has gone!
- The app is only available in English, how can we add support for other languages?
The rest of this chapter will cover these additional features. By the end we will have a complete, useful app and you will have learned a range of additional skills.
With the simulator running, from the menu choose Hardware > Home. Notice the app uses the default icon. In this activity we will substitute our own custom icon. Later in the module you will be shown how to create your own but for now you can access a complete set of icons. These are available in a zip file called Icons.zip which you should download and uncompress.
Rather than uploading images directly into the project directory you store these in an Assets Catalog called Assets.xcassets which should already exist in your project. Open this file and select the App Icon entry in the left bar. You need to drag to correct size image into each space. For example the first space is labelled 2x 29px so you need to locate an icon 58px square (this is saved as 29x2.png so you should be able to complete the exercise quickly.
If you run your app then return to the home screen you should see your new icon.
When the application launches you should display a loading screen. There are two approaches, either build a custom launch view or use static PNG images. In this worksheet you will use a set of static png images.
Start by deleting the LaunchScreen.storyboard file in the project navigator then access the project settings screen by selecting the blue icon in the Project Navigator as shown.
Click on the Use Asset Catalog button and opt to migrate.
To see the changes you will need to navigate away from the project settings screen then return to it. Delete the Launch Screen File value.
If you open the Assets catalog (Assets.xcassets) you will see that there is a new entry called LaunchImage. Delete this then add a new Launch Image File. You can download the images in the LaunchImage.zip file to populate the different placeholders.
To see the launch screen display, you will need to delete the app from the simulator home screen. Press shift+cmd+H to access the home screen then click and hold the app icon. Finally click the delete button on the app icon.
When you run the app you should see your launch screen appear when the application loads. If this happens too quickly you can introduce a delay (in seconds) by editing the didFinishLaunchingWithOptions method in the AppDelegate.swift file.
Whilst there are a lot of methods already in the View Controller file there are also others you may need to add. In this activity we will be adding a method that is triggered when a table view cell has been selected. We will add code to deselect the row which will remove the grey background colour. How do I know the names of these methods? You have probably noticed the features called intellisense and autocomplete. XCode recognises what you are typing and suggests alternatives for you to choose from. Let's give it a go. Find a blank line between two of the methods and type in the letter t.
Straight away we can see a method that looks hopeful. TableView:DidSelectRowAtIndexPath
Use the arrow keys to select it and press the enter key. Add opening and closing braces and you are ready to add code. Before we do this let's see if it is getting triggered and if we can get the current table cell row. Note the use of \()
to insert a variable into a string, this is called string interpolation.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("row \(indexPath.row) selected")
}
Try running your app and tapping on different rows. What appears in the system log?
The Table View has a method called deselectRowAtIndexPath so we should be able to call this and pass it the indexPath we were sent when the method got called.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("row \(indexPath.row) selected")
tableView.deselectRow(at: indexPath, animated: true)
}
If you run your app and tap on a table view cell you should see the grey colour fades away (animated).
Now we know which cell has been selected by the user we can select it and change its properties. The first step is to get a reference to the correct cell based on its indexPath. We get a reference to the current Table View then use this to get a reference to the selected cell based on the indexPath passed to the method. To prevent us referencing a null pointer we need to unwrap both the tableview and the cell.
TODO: needs updating
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("row \(indexPath.row) selected")
if let table = self.tableView {
if let cell = table.cellForRow(at: indexPath) {
print("we found the selected cell: \(cell)")
}
}
tableView.deselectRow(at: indexPath, animated: true)
}
Once we have a reference to the cell we can simply change its accessoryType property like so.
if let cell = table.cellForRow(at: indexPath) {
print("we found the selected cell: \(cell)")
cell.accessoryType = UITableViewCellAccessoryType.checkmark
}
Run your app. What would you expect to happen if you tap a cell that already has a checkMark? At the moment the checkMark remains but it would be useful if it could be toggled on or off each time the cell was tapped. For this to work we need to see what the accessory type property is currently set to and then change it based on its current value, in other words we need to use a conditional (if … else). Notice that to compare for equality we use the == symbol (comparison) rather than the = symbol which means assign.
if let cell = table.cellForRow(at: indexPath) {
print("we found the selected cell: \(cell)")
if cell.accessoryType == UITableViewCellAccessoryType.checkmark {
// we already have a checkmark, remove it...
} else {
// there is no checkmark, add one...
}
}
Since we already know how to change the accessory type we can easily complete the code.
if cell.accessoryType == UITableViewCellAccessoryType.checkmark {
cell.accessoryType = UITableViewCellAccessoryType.none
} else {
cell.accessoryType = UITableViewCellAccessoryType.checkmark
}
whilst the code we have written works well, there are some changes we can make to declutter.
- Option chaining allows us to unwrap an object deep in the heirarchy without having to unwrap each step. The
?
character indicates the object is an optional. - If a the code requires a class property of a specific the class itself can be omitted. So instead of
UITableViewCellAccessoryType.checkmark
we can use.checkmark
. - a Ternary Conditional Operator allows us to specifiy the
.accessoryType
property on a single line.
Here is a version of the function that uses all 3 of the improvements listed above. Compare this to the version you currently have. Which version is more readable?
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = self.tableView?.cellForRow(at: indexPath) {
cell.accessoryType = (cell.accessoryType == .checkmark ? .none : .checkmark)
}
tableView.deselectRow(at: indexPath, animated: true)
}
Note that single-line if blocks are considered bad programming practice and so are not supported in swift.
You have probably seen apps which allow you to change the order of the table cells. This is done by entering an edit mode. Start by adding a second bar button to the left-side of the navigation bar as shown. Change its System Item property to Custom and its Title to Edit.
Create an IBAction called editMode.
@IBAction func editMode(_ sender: UIBarButtonItem) {
// implement code to toggle edit mode
}
Add code to toggle the view's edit mode as shown.
@IBAction func editMode(_ sender: UIBarButtonItem) {
self.isEditing = !self.isEditing
print("editMode: \(self.isEditing)")
}
The !
inverts the current mode and assigns it as the new editing mode. Try running the app and see what happens to the UI when you click the button a few times. The XCode console should indicate the current status but the button text does not change to reflect this.
@IBAction func editMode(_ sender: UIBarButtonItem) {
self.isEditing = !self.isEditing
print("editMode: \(self.isEditing)")
sender.title = (self.isEditing ? "Done" : "Edit")
}
Notice that we have again used the Ternary Conditional Operator to check the isEditing
property and assign the correct text to the button. The function passes a parameter sender
which points to the button that triggered it.
Next you need to identify the following two delegate methods and uncomment them. The second one indicates that all rows are editable. You have the option here of defining non-editable rows if needed.
override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
}
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}
Finally we need to add code to rearrange the data in the array based on the row we dragged. The delegate method passed two important parameters, the indexPath of the item's current location and that of the location it was dragged to. The rest of the code should make sense.
override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
let item:String = items[fromIndexPath.row]
self.items.remove(at: fromIndexPath.row)
self.items.insert(item, at: to.row)
self.tableView.reloadData()
}
The function passes a reference to the UITableView
plus two IndexPath
parameters. The first one fromIndexPath
references the place the moved row came from whilst the second, to
is where it has been dropped. We use this information to re-arrange the position of the item in the array. Finally we call the reloadData()
method of the table which loads the data from the array back into the table (by calling the table data source methods we defined earlier).
Run the application and try grabbing rows at their right-hand edge and dragging them to re-order. Click on the Done button when finished.
To enable the deleting of rows you need to uncomment a third delegate method.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
The else … if branch can be deleted entirely. We now need to describe the steps to take when the editing style is set to delete. There are two steps
- the UITableViewCell needs to be removed from the UITableView
- The record needs to be removed from the underlying data (our array)
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
self.items.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
There are two ways to delete rows:
- Swipe left and click on the Delete button
- Enter edit mode and click the red circle then click on the delete button.
Have a go at adding new items and then deleting them.
You now have a functioning app but there is a serious flaw. Try adding items then closing it fully (Home screen then double-tap the home button and delete.
When you open the app, all your items have gone. In the next section you will learn a simple technique to persist data, even when the app has been closed or the phone restarted.
The data is only in the running app.If you press the home button the app is running in the background and the data is safe however if the app ever closes properly (for example if is force-quit (double tap home button) or it crashes, your data will be lost. In this task you will be using a persistence class called UserDefaults to save the list every time an item is added. When the app loads up it will load the persisted data into the array so the list will appear. Data is stored against a key. To save data we specify the key we will be using and to retrieve it we specify the key and the NSUserDefault object will give us the data.
When the application loads we need to check for any persisted data. Because the key in the NSUserDefaults may not exist (optional) we should unwrap it before assigning it to the list array. Of course if it doesn't exist the array will contain the default values.
override func viewDidLoad() {
super.viewDidLoad()
let savedItems = UserDefaults.standard
if let loadedItems:[String] = savedItems.object(forKey: "items") as! [String]? {
print("data loaded")
print(loadedItems)
self.items = loadedItems
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Each time the items array gets changed we should store it in UserDefaults. Any data is stored against a KEY, in this case we use the string "items".
Unlike a desktop app there is no Save button. Because of this, the contents of the array should be persisted every time it changes, this happens:
- When a new item is added
- When an item is deleted
- When the order of the items changes.
Rather than repeating the same code in three different places we will write a function which we can call wherever needed.
func saveList() {
let savedItems = UserDefaults.standard
savedItems.set(items, forKey: "items")
savedItems.synchronize()
print("list saved")
}
To call our function:
self.saveList()
Modify your app so that the function is called whenever the contents of the array change. Make sure you test all permutations (the log messages should help).
You now have a fully functional todo app and have learned a lot about both the IDE (XCode) and the programming language we will be using (Swift).
①②③④⑤⑥⑦⑧⑨⑩