RailsCards
There is a Flashcard App for Ruby on Rails developers that we will replicate in a similar fashion for this lab.
Ours is very similar, but there is plenty of room for creative freedom. Here is a look at what the final app might look like:
There are two screens: one to show the command, and the other to show the definition.
This lab can be done in Xcode or using the Playgrounds app on the iPad. I will be presuming Xcode here, but both would work.
Models
-
Create a new Single View App
called RailsCards
. We will be using SwiftUI
for this lab, so make sure you set your "User Interface" option accordingly.
-
If you want an AppIcon for this app, you can get one here or create one here.
-
Create three folders -- Models, Controllers, Views -- like we did last week to organize our work.
-
Within the project create a new, blank Swift file called Flashcard.swift
and create a simple model named Flashcard
within this file to represent the essence of a flashcard. In this case, that means the model has the following:
- A variable called
command
of type String
(this is the front of the flashcard with the rails command).
- A variable called
definition
also of type String
(this is the back of the flashcard with that the rails command does).
- A constructor method that takes a command String and a definition String as arguments and sets the appropriate variables.
In this case, use a struct
and not a class
for this model since this model has only state and not behavior. (As discussed in class, this is not the primary distinction between structs and classes in Swift, but that is true in other languages and we will observe this practice here.)
-
Create another new file called Deck.swift
that will help represent a series of flashcards. Within this file, we fill define a new class
called Deck
. This will be a class
since it will have both state and behavior.
-
Create a variable called cards
in Deck.swift
, which will be an array of Flashcard
objects (using the struct we created earlier).
-
In the init()
method, set the body of the method to the following code:
init() {
let cardData = [
"rails generate model ModelName": "Creates a model with the specified model_name",
"rails generate migration MigrationName": "Creates a migration with the specified migration_name",
"rails generate controller ControllerName": "Creates a controller with the specified controller_name",
"rails generate scaffold ModelName": "Provides shortcut for creating your controller, model and view files in one step",
"rails destroy scaffold ModelName": "Destroys the created controller, model and view files that were generated for the given Model",
"rails server": "Starts ruby server at http://localhost:3000",
"rails console": "Opens the rails console for the current RAILS_ENV",
"rake test:units": "Runs all unit tests for the application",
"rake -T": "Lists all available rake tasks",
"rake db:create": "Creates the database defined in config/database.yml for the current RAILS_ENV",
"rake db:migrate": "Migrates the database through scripts in the db/migrate directory",
"rake db:drop": "Drops the database for the current RAILS_ENV",
"rake db:reset": "Drops and recreates the database from db/schema.rb for the current environment",
"rake db:rollback": "Runs the down method from the latest migration",
"rake doc:app": "Builds the RDoc HTML files",
"gem list": "lists the gems that this rails application depends on",
"gem server": "Presents a web page at http://localhost:8808/ with info about installed gems",
"bundle install": "Installs all required gems for this application",
"rake log:clear": "Truncates all *.log files in log/ to zero bytes",
"rake routes": "Prints out all the defined routes in match order with names",
"rake tmp:clear": "Clears session, cache and socket files from tmp/",
"rake test:benchmark": "Benchmarks your application"
]
// Write a simple way to loop through the dictionary of cards, create a `Flashcard` object
// for each card, and add that object to the `cards` array we created earlier
}
-
Replace the comment above with the appropriate code. After that, add a method that draws a random card (see below):
func drawRandomCard() -> Flashcard {
return cards[Int(arc4random_uniform(UInt32(cards.count)))]
}
Controller
-
Now create a new Swift file called ViewController.swift
. Create a new class
called ViewController
and make sure it inherits from ObservableObject
like we did in lab 2.
-
Create two variables in our ViewController
:
let deck = Deck() // create an instance of `Deck`
@Published var flashcard: Flashcard // holds a single flashcard object from the deck
Remember, because this class is an ObservableObject
it can 'publish' information to any other object that might be observing it and that object will update as soon as the published data has changed. Last week, our published data were just strings, but this time we are publishing an object of type Flashcard
. That might seem different, but it's really not.
-
Write two methods for our controller:
- An
init()
method that sets self.flashcard
to a random Flashcard
from the deck
.
- A method to update the
self.flashcard
variable to a different random card.
Both of these methods can utilize the drawRandomCard()
method in the Deck
class.
Views
-
First thing we are going to do is move ContentView.swift
and RailsCardsApp.swift
into the Views folder. Second, we are going change the name of ContentView
to a more meaningful name like CardView
. After we do that, we also need to go into the RailsCardsApp
file and change ContentView
to CardView
.
-
Let's start creating the view in the CardView.swift
. Create an instance of your ViewController
in the CardView
. Make sure this is an @ObservedObject
like we did last week.
-
Change Text("Hello, World!")
to display the rails command from the ViewController
.
This is what your canvas should look like:
-
Let's create the definition page now. Create a new SwiftUI File
called DefinitionView.swift
.
-
Put the following code under struct DefinitionView: View
:
let viewController: ViewController
-
Replace the code following struct DefinitionView_Previews: PreviewProvider
with:
struct DefinitionView_Previews: PreviewProvider {
static var previews: some View {
DefinitionView(viewController: ViewController())
}
}
-
Now replace, Text("Hello, World!")
with the flashcard definition from the ViewController
.
-
Now let's link the two views. In CardView.swift
, wrap your Text
element in a NavigationView
.
-
Then, wrap your Text
element again in a NavigationLink
with DefinitionView
as your destination
. Pass in the instance of your ViewController
to your DefinitionView
.
-
Now try out your app! It should be mostly functional. However, you might notice that you are getting the same flashcard every time. This is because we never update our flashcard with a new, random card. Let's do that in our CardView.swift
.
-
Add .onAppear() { }
outside your NavigationLink
(but inside your NavigationView
) to call your ViewController
method that updates the flashcard with a new, random card everytime the command screen comes into the foreground. (To do this, call the method within in the curly braces of the .onAppear()
block.)
-
Now the app should be fully functional, but it is not as appealing as it could be. Let's refactor the views to improve the appearance of our user interface.
Making Cards
-
Since our app is called RailsCards
let's clean up our views using cards.
-
Do this by wrapping your NavigationLink
in a ZStack
, setting the width
to 350.0
and the height
to 200.0
.
-
Add the following code right before the .onAppear
block to create a card outline. Make sure you understand what each statement is doing.
.overlay(
RoundedRectangle(cornerRadius: 10.0)
.stroke(Color.gray)
)
-
Create a similar card in the DefinitionView
. Make sure to add some padding
inside the card and to center the text.