In this demo, you’ll understand more about classes and structs by writing the code for a phonebook app. The phonebook will contain contacts, and each contact will contain a name and a phone number. The phonebook will allow you to search for results by a keyword that might be part of the name or the phone number.
Open Xcode on your Mac and create a new playground file.
Before writing any code, you first should have a good idea of what the functionality you want to write. Here are the key parts:
- Your goal is to build a phonebook that stores an array of contacts.
- Each contact consists of Name and Phone.
- Phonebook allows you to search for contact names or phone numbers that contain a keyword.
An excellent starting point for writing your code is to think about the app’s high-level parts:
You’ll create a new type named Contact; assume it’s a struct for now. It contains two variables: name and phone. Each variable should be a String type. Write this down:
struct Contact {
var name: String
var phone: String
}
You might think: “Shouldn’t the phone number be an Int?” The answer is “No”. A phone number is never used in any math operation. Also, it can start with zeros or a plus sign, which doesn’t make sense for an Int, and it won’t store them. That’s why it’s best to save phone numbers as String.
The other part of your app is PhoneBook. It stores an array of Contact, which stores multiple contacts and has a function to search in this list using a string, and returns an array of contacts that match the search. Write it down and assume this is a struct, too:
struct PhoneBook {
var storedContacts: [Contact] = []
func save(contact: Contact) {
}
func search(keyword: String) -> [Contact] {
return []
}
}
The return line in the function returns an empty array. You only wrote it so Xcode doesn’t complain that the function isn’t returning anything.
Now that you have your app’s high-level design, it feels a lot simpler, right?
As for deciding if either of the two types should be updated to a class, it’s obvious that Contact is just data and there is no meaning to have one centralized contact. But it makes sense to have the object taking care of all the contacts to be a centralized object. Update PhoneBook to a class type:
class PhoneBook {
You defined two functions inside PhoneBook, but they don’t do anything yet. It’s time to add their implementation. save is straightforward, adding the input contact to storedContacts. Update its implementation to the following:
func save(contact: Contact) {
storedContacts.append(contact)
}
You might ask, “Why do I need a specific function when I can just append the contact directly to storedContacts without the save function?”
To fully explain would require a few lessons in Object Oriented Programming and understanding how to design classes and objects for an app. But to give you a simple answer: For value types like Contact, it might be OK to change its inner properties directly because each copy is unique. For reference types like PhoneBook, you should create functions that handle changing the data because the change will affect multiple places in the app. You want to have control over how the data changes and don’t want any outside code to make changes.
Ensure no outside code can change storedContacts by marking it private like this:
private var storedContacts: [Contact] = []
When you add private before a func or var, only other code inside the same object — in this case, inside PhoneBook — can access it. If any outside code tries to access it, Swift displays an error.
The next function to implement is search. Update its implementation to the following:
func search(keyword: String) -> [Contact] {
var results: [Contact] = [] //1
for contact in storedContacts { //2
if contact.name.contains(keyword) ||
contact.phone.contains(keyword) {
results.append(contact)
}
}
return results //3
}
This code:
- Creates an array inside the function that will contain the search function results.
-
Loops over the stored contacts in the phonebook and checks if
nameorphonestrings contain the keyword string. If a contact does, the results should include it. - Returns the array that will either have some results or be empty.
Now, to try out the phonebook code, at the end of the playground, create a new instance of PhoneBook:
let phoneBookInstance = PhoneBook()
Then, add a contact to it:
let ehabContact = Contact(name: "Ehab Amer", phone: "0123456789")
That looks new for a constructor, right? Contact has two variables, and neither has an initial value. For Swift to allocate memory for your contact, it needs to give them starting values. Swift knows the constructor for Contact needs to set the values for name and phone, so it makes some constructor code for you. If you don’t want to rely on the constructor Swift generates, you can always create your own. Go back to the declaration and add this new function:
init(name: String, phone: String) {
self.name = name
self.phone = phone
}
This looks very different from the other functions you’ve seen before. Constructors for classes or structs are always named init short for initializer. They don’t need the func keyword before them, and Swift verifies that any constructor function properly gives initial values to all the member variables for the type. If you remove either line from the function, Xcode complains.
Because the constructor input parameters have identical names to the variables in Contact, you can differentiate between them using self. self.name refers to the variables defined in Contact, whereas name on its own refers to the parameter name.
Now, save the contact you just created to the phonebook:
phoneBookInstance.save(contact: ehabContact)
Because you marked storedContacts private, this is the only way to add the contact. You could try to add it directly, but the playground will display an error:
phoneBookInstance.storedContacts.append(ehabContact)
Comment or erase this line so you can continue. You also might wonder about the fact that you declared phoneBookInstance with a let but then changed it by adding ehabContact. This can get a little confusing, but phoneBookInstance didn’t change; it’s still the same object holding a var of storedContacts. The var inside phoneBookInstance changed.
Next, create a new variable and initialize it with the value of phoneBookInstance:
let samePhoneBook = phoneBookInstance
Create another contact and save it to samePhoneBook:
let kodecoContact = Contact(name: "Kodeco", phone: "0112233445")
samePhoneBook.save(contact: kodecoContact)
Try the search function with the keyword "01":
dump(samePhoneBook.search(keyword: "01"))
Run the playground and look at the printed results in the lower pane.
You used a new function — dump — here instead of print. When you start creating more complicated objects, dump often will give you nicer formatting. However, it works best when it is printing an object. If you try to use String interpolation, you’ll lose the pretty formatting.
Notice that when you searched for the text "01", both contacts were part of the result although you stored kodecoContact in samePhoneBook only and not ehabContact.
But samePhoneBook was created with the value of phoneBookInstance, which had the first contact stored inside it. What happened is samePhoneBook and phoneBookInstance refer to the PhoneBook stored at the same memory address. The two variables will always have the same value.
Now, search for the letter "o" in phoneBookInstance:
dump(phoneBookInstance.search(keyword: "o"))
You can try doing the same with kodecoContact and assign it to a different variable, change the name of one of the contacts and see if both change or just one. Add the following code to try it:
var kodecoContactCopy = kodecoContact
kodecoContactCopy.name = "Kodeco Copy"
print(kodecoContact.name)
print(kodecoContactCopy.name)
With simple output, print is better than dump. Run the playground and notice that only one of the two variables has a different name. This shows the difference between value types and reference types.