Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: daixieit

FIT3178: iOS Application Development
Lab 4 – Using Core Data
Overview:
The pu
rpose of the lab for this week is twofold. First, this lab demonstrates how to
implement Core Data within our projects. Secondly the lab shows a way to separate
the Database and View Controller layers (we will replace Core Data with Firebase for
the database in Lab 06).
This lab is a continuation of last week’s base solution. Extension tasks are not required.
To work on this week’s lab, you will need to pull the main branch from your Git Repo of
the week 3 lab. This main branch should contain the solution WITHOUT the extension tasks,
as you should have created a branch for the extension tasks last week. See the following
page for instructions.
Assessment note: This lab exercise forms part of your assessment for FIT3178. You should
complete the entire exercise, revise the contained material, and attempt the extension
activity prior to the following week’s lab class. In your Week 4 lab, your demonstrator will
conduct a short interview with you to assess your understanding of this lab’s material and
review your solution to the extension task, as well as giving you feedback. This will be worth
5% of your unit mark. You must attend your allocated Week 4 lab class to receive this mark.
Note: Some of the Macs in the lab may be running Windows and need to be rebooted into
the OS X environment. Restart the Mac and hold down the Option key while it is booting.
You will need to select ‘EFI Boot’ and select OS X to launch the correct operating system.
The Task:
For this lab you will add a Core Data persistent database layer to the app from last
week’s lab. This will require no changes to the UI, only changes in the code. The
summary of changes to be made are as follows:
● Create a Core Data Model
● Create a Database Listener
● Create a Database Protocol
● Create a Core Data Controller Class
● Change the Add Hero Controller to use these
● Change the All Heroes Controller to use these
● Change the Current Party Controller to use these
If you branched your Lab 03 project and did the extension exercises, you should follow
the steps on this page. Start by opening your Lab 03 project.
1) Go to the Source Control Navigator. It will show your extension branch is active.
2) Right-click on the "main" branch and select "Check Out..." from the menu.
3) Choose "Check Out" in the pop-up dialog.
You can now begin the lab exercise.
The steps are broken down to enable testing at multiple points. This represents our
suggestion for how to implement such features—break them into small manageable
chunks that can be written and tested independently.
Creating the Data Model:
To begin this week, open your solution to Week 3 (If you haven’t finished week 3, we
recommend completing that first, as we will not be providing a solution to be used for
this week).
Begin by right-clicking on the project
folder in the Navigation Explorer and
selecting “New Group”.
Name this folder DataModel
Next, right-click on this folder in the
Navigator and select “New File...”
Scroll down until you see Core Data
> Data Model. Select this and click
Next.
Name this "Week04-DataModel”.
Make sure the group is set as the
"DataModel folder we created just
before.
Click Create and this will create our
Core Data Model
After it has been created, open the
data model if it has not already
opened automatically.
Currently our model is empty. Let's
create some entities that we will
need.
Click the Add Entity button to create
a new entity
Name the new entity "Superhero".
Under attributes click the "+" button
to add some attributes.
Create a "name" Attribute of type
String.
Create an "abilities" Attribute of type
String.
Create a “universe” Attribute of type
Integer32
Create a second entity “Team”.
Create a new attribute called "name"
of type String
Create a new Relationship called
"heroes". Set its Destination to be
Superhero
Select the "heroes" Relationship and
go to the Data Model inspector.
Change the Type to be "To Many".
A team can hold many heroes after
all!
Select the Superhero entity again.
Create a new relationship called
"teams". Set its destination to
"Team" and its inverse to "heroes".
Change the Type to be "To Many".
These two relationships are now
“linked”. Changes made on one
affect the other
For both entities, go to the "Data
Model Inspector" on the right and
change Codegen to "Manual/None".
By default, the Core Data Model will
try and automatically create our
classes. This is not what we want.
Our Data Model is almost finished.
Before we get to the final step,
delete our old "Superhero.swift"
class file (select move to Trash).
We will be generating a managed
object Superhero class as a
replacement.
Open the Data Model and select
both "Superhero" and "Team"
entities.
From the menu, select Editor ->
Create NSManagedObject Subclass.
In the dialog that opens, ensure that
both Superhero and Team are
selected and click Next.
Ensure the group is "DataModel"
and click finish. This will create our
class files for both Superhero and
Team.
Next, we add two sections of code to our Superhero+CoreDateProperties class:
Add the following enum to the top of
the class under the import functions.
This is like what we had in the old
superhero class, except Int32.
Next, add the following code to the
end of the class. Here we create an
extension of our class, that adds
getter and setter methods to allow
for input of an enum to Core Data.
We need to convert our enums to
the raw Int32 values to allow them to
be entered into the core data
database.
With this our Data Model is complete and ready to be used.
Adding the MulticastDelegate class:
Create a new Group in our project called "Database".
Storing weak links to a list of delegates (used for the listeners below) is not trivial. We
wrote the MulticastDelegate class for this purpose. This file, MulticastDelegate.swift, is
available from Moodle.
This class has methods to add and remove a particular listener, and a method that
invokes a closure (some code passed to the method) on all the listeners. It stores weak
references to avoid reference cycles. You do not need to understand this class.
The file is licenced under the Apache free software licence. To quote the linked
WIkipedia page, this means “It allows users to use the software for any purpose, to
distribute it, to modify it, and to distribute modified versions of the software under the
terms of the license, without concern for royalties.” You can include this file in your lab
project, your second assignment, and for anything else you want.
Download the MulticastDelegate.swift and drag it into the Database folder. Xcode will
show a dialog with some options for adding the file to the project. Make sure the
“Copy items if needed” option is checked and click FInish. The file will be added to the
project.
Creating the Database Protocol:
For this step we will be creating the Database Protocol and a few associated classes
and enumerators. These will be used to control what functionality a database will have,
define the behaviour of its listeners, and define the types of listeners that a database
can have.
At the core will be the DatabaseProtocol. This protocol defines all the core behaviour of
a database. It is crucial to note that this will be flexible enough to work for both an
offline database such as Core Data AND online databases such as Firebase! The
example shown in this lab is rudimentary. It should provide a good starting point for
more complex applications.
Create a Swift file, name it "DatabaseProtocol" and save it in the Database folder.
Open the file and we can begin making the required enumerators and classes. Starting
with the DatabaseChange enumerator
enum DatabaseChange {
case add
case remove
case update
}
The DatabaseChange enumerator is used to define what type of change has been
done to the database. There are several possible cases here that are very useful, these
being add, remove, and update. For this week we will only be using update. Other
cases can also be added for more complex implementations and functionality.
Underneath the DatabaseChange enum we need to create a second one called
ListenerType
enum ListenerType {
case team
case heroes
case all
}
The database we are building has multiple different sets of data that each require their
own specific behaviour to handle. It can prove useful to specify the type of data each
of our listeners will be dealing with. In the case of this app, we can have listeners that
listen for team, hero or both. These will be used when the database has any changes
(the changes from our previous enum!)
Now that we have the listener types defined, we need to define the listener itself.
protocol DatabaseListener: AnyObject {
var listenerType: ListenerType {get set}
func onTeamChange(change: DatabaseChange, teamHeroes: [Superhero])
func onAllHeroesChange(change: DatabaseChange, heroes: [Superhero])
}
This protocol defines the delegate we will be using for receiving messages from the
database. It has three things that any implementation must take care of.
● The implementation must always specify the listener’s type
● An onTeamChange method for when a change to heroes in a team has
occurred.
● An onAllHeroesChange method for when a change to any of the heroes has
occurred.
Each of the onChange methods also returns a change type. Whilst not utilised for this
week it enables us to slightly change the behaviour based on what kind of change has
occurred to the database.
Another important note here is that the DatabaseListener is kept database agnostic.
There are no specific calls or mentions of Core Data or any other technology here. This
enables us to easily reuse this code if we do another implementation of a database
(such as in week 6).
With these done, the last thing to define is the DatabaseProtocol itself. This protocol
defines all the behaviour that a database must have. And these will be the public facing
methods that can be accessed by other parts of the application. As before, the goal
here is abstraction and re-usability as much as possible. Each specific database
controller implementation may have additional functionality to support these methods,
but these are the required ones.
protocol DatabaseProtocol: AnyObject {
func cleanup()
func addListener(listener: DatabaseListener)
func removeListener(listener: DatabaseListener)
func addSuperhero(name: String, abilities: String, universe: Universe)
-> Superhero
func deleteSuperhero(hero: Superhero)
}
To make it easier to debug and build up the app progressively, we will start with only a
couple methods required for adding/removing listeners and heroes.
Creating the Core Data Controller:
Create a new Cocoa Touch Class file, name it "CoreDataController", ensure it inherits
from NSObject, and implements the DatabaseProtocol and save it within the Database
folder.
Before coding any functionality for the class, we need to import the CoreData
framework. Previously we have been using components from the Foundation and UIKit
frameworks, which are automatically added to the top of our class files. When using
Core Data we need to add our own input statement.
import CoreData
Note: This should be above the class inside of the swift file. Not inside it
As with previous weeks the first step is to include the class properties.
var listeners = MulticastDelegate()
var persistentContainer: NSPersistentContainer
The “listeners” property holds all listeners added to the database inside of the
MulticastDelegate class that was added above. This creates a nice wrapper that we
can safely hold multiple listeners in without having to worry about memory issues.
The persistentContainer property holds a reference to our persistent container and
within it, our managed object context. Any time we need to create, delete, retrieve, or
to save our database we need to do so via the managed object context. This makes
the Core Data Controller the ideal place to keep a reference to the persistent controller.
As the persistentContainer property is not optional and has not been assigned a value,
we must create an initialiser to handle this. Unlike previous weeks where we create an
initialiser with parameters, here we will override the default initialiser.
override init() {
super.init()
}
Inside the initialiser add the following code to instantiate the Core Data stack before
the call to super.init (all variables must have values before this call).
persistentContainer = NSPersistentContainer(name: "Week04-DataModel")
persistentContainer.loadPersistentStores() { (description, error ) in
if let error = error {
fatalError("Failed to load Core Data Stack with error: \(error)")
}
}
The first line of code initializes the Persistent Container property using the data model
named "Week04-Datamodel".
The second line loads the Core Data stack, and we provide a closure for error handling.
In this case we are triggering a fatal error if the stack fails to load. Generally if this
occurs it is because the name in the line above does not match what the xcdatamodel
object is named in XCode.
cleanup method
This method will check to see if there are changes to be saved inside of the view
context and then save, as necessary.
func cleanup() {
if persistentContainer.viewContext.hasChanges {
do {
try persistentContainer.viewContext.save()
} catch {
fatalError("Failed to save changes to Core Data with error: \(error)")
}
}
}
Changes made to the managed object context must be explicitly saved by calling the
save method on the managed object context. This method can throw an error, so must
be done within a do-catch statement.
addSuperhero method
The addSuperhero method is responsible for adding new superheroes to Core Data. It
takes in a name and abilities, generates a new Superhero object then returns it. The
Superhero is a Core Data managed object stored within a specific managed object
context.
func addSuperhero(name: String, abilities: String, universe: Universe) -> Superhero
{
let hero = NSEntityDescription.insertNewObject(forEntityName:
"Superhero", into: persistentContainer.viewContext) as! Superhero
hero.name = name
hero.abilities = abilities
hero.herouniverse = universe
return hero
}
Once a managed object has been created, all changes made to it are tracked. Note
that any new object will not be saved to persistent memory until the save method has
been called on its associated managed object context.
deleteSuperhero method
The deleteSuperhero method is a straightforward one. It takes in a Superhero to be
deleted and removes it from the main managed object context. As with other changes,
the deletion will not be made permanent until the managed context is saved.
func deleteSuperhero(hero: Superhero) {
persistentContainer.viewContext.delete(hero)
}
fetchAllHeroes methods
The fetchAllHeroes method is used to query Core Data to retrieve all hero entities
stored within persistent memory. It requires no input parameters and will return an
array of Superhero objects.
To query Core Data an NSFetchRequest is created. This is mostly handled for us within
the pre-generated entity classes that were created for Superhero and Team. Once a
fetch request is created it must be passed to the managed object context to execute
func fetchAllHeroes() -> [Superhero] {
var heroes = [Superhero]()
let request: NSFetchRequest = Superhero.fetchRequest()
do {
try heroes = persistentContainer.viewContext.fetch(request)
} catch {
print("Fetch Request failed with error: \(error)")
}
return heroes
}
A fetch request can throw an error so it must be done within a do-catch statement. If it
succeeds, we use it to populate our hero array and return it.
addListener method
The addListener method does two things. Firstly it adds the new database listener to
the list of listeners. And secondly, it will provide the listener with initial immediate
results depending on what type of listener it is.
func addListener(listener: DatabaseListener) {
listeners.addDelegate(listener)
if listener.listenerType == .heroes || listener.listenerType == .all {
listener.onAllHeroesChange(change: .update, heroes:
fetchAllHeroes())
}
}
Checking what information to provide and then providing it requires us
to check the listener type. In this case if the type is either heroes or all then the method
will call the delegate method onAllHeroesChange and pass through all the heroes
fetched from the database.