This tutorial works through the process of creating a revision app with virtual cue cards. Data storage is implemented using adherence to the NSCoding protocol and uses NSKeyedArchiver and NSKeyedUnarchiver
A starter application is available for cloning from GitHub, however if you wish to develop the whole project for yourself, then create an iOS application with a storyboard similar to that shown below: The outlet and action names in the below code should be semantic enough for you to identify which element on your own storyboard the below to. You will need to add the delete button if you use the project from GitHub.
class CueCard : NSObject {
init(question: String, answer: String){
self.question = question
self.answer = answer
}
var question : String
var answer: String
}
Note that it inherits from NSObject. This is required to enable archiving to disk later onclass CueCard : NSObject, NSCoding {
Note that an error will appear warning that the CueCard class does not adhere to the NSCoding Protocol. Use the 'Fix' option to add method stubs to implement the required methods in NSCodingfunc encode(with aCoder: NSCoder) {
aCoder.encode(question, forKey: "q")
aCoder.encode(answer, forKey: "a")
}
required init?(coder aDecoder: NSCoder) {
question = aDecoder.decodeObject(forKey: "q") as! String
answer = aDecoder.decodeObject(forKey: "a") as! String
}
iOS applications should adhere to the MVC design pattern, so the process of retrieving, adding, removing and storing tweets should not be handled by the View Controller. Instead the view controller should simply call appropriate methods within the card manager based on user interaction with the view, and update the view as a result of changes in the manager.
The Card manager will be used by both the create cue card and read cue card view controllers, but it should be the same manager and we only ever will want one of them within our app. This creates a good argument for making it a Singleton.
In Swift making a class a Singleton requires having a static public property that refers to the class, and making the init method private
class CardManager{
static let shared = CardManager()
private init(){
}
}
private var cards : [CueCard]!
private var currentIndex = 0
var currentCard: CueCard? {
return cardAtIndex(currentIndex)
}
func cardAtIndex(_ index: Int) -> CueCard?{
guard cards != nil else {
return nil
}
guard cards.count > index else {
return nil
}
return cards[index]
}
func nextCard() -> CueCard? {
guard cards != nil else {
return nil
}
if cards.count > currentIndex + 1 {
currentIndex += 1
}
return currentCard
}
func previousCard() -> CueCard? {
guard cards != nil else {
return nil
}
if currentIndex > 0 {
currentIndex -= 1
}
return currentCard
}
func firstCard() -> CueCard? {
currentIndex = 0
return currentCard
}
func lastCard() -> CueCard? {
guard cards != nil else {
return nil
}
currentIndex = cards.count - 1
return currentCard
}
func randomCard() -> CueCard? {
guard cards != nil else {
return nil
}
guard cards.count > 0 else{
return nil
}
currentIndex = Int(arc4random_uniform(UInt32(cards.count)))
return currentCard
}
Looking carefully at the above methods, you will see a number of statements with the keyword guard
. guard
allows us to check that, for example, an optional is not nil, and if it is, exit. Once the guard statement is completed, optionals are treated as having values, which can simplify the logic. This is an example of 'Early Exit' programming, and is designed to avoid nesting conditional statements, which can be hard to read.
func addCard(_ card: CueCard){
cards.append(card)
}
func removeCurrentCard(){
guard cards != nil else {
return
}
guard cards.count > currentIndex else {
return
}
cards.remove(at: currentIndex)
if currentIndex > 0 {
currentIndex -= 1
}
}
As the CardManager class is a singleton, we can create a reference to it in both the 'create card' and 'read cue cards' view controllers. This can be achieved with the following code:
let cardManager = CardManager.shared
Add this code to both view controllers
let card = CueCard(question: questionTextView.text, answer: answerTextView.text)
cardManager.addCard(card)
func display(_ card: CueCard){
questionTextView.text = card.question
answerTextView.text = card.answer
}
@IBAction func firstQuestionPressed(_ sender: AnyObject) {
if let card = cardManager.firstCard(){
display(card)
}
}
@IBAction func revealAnswerPressed(_ sender: AnyObject) {
answerTextView.alpha = 1
}
display(card:)
method to call the hideAnswer
methodWhen the view first loads, it needs to display the current card, or a message to tell the user to add some cards. Add the following methods to do that:
func displayCurrentCard(){
if let card = cardManager.currentCard {
display(card)
}
else {
questionTextView.text = "No questions added yet, why not
create one?"
}
}
viewDidLoad()
func deleteCurrentCard(){
cardManager.removeCurrentCard()
displayCurrentCard()
}
@IBAction func deletePressed(_ sender: Any) {
let alert = UIAlertController(title: "Delete Card",
message:"Are you sure you want to delete this card?",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes",
style: .destructive,
handler: { (action) in
self.deleteCurrentCard()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true)
}
private var filePath: String {
let docFolderUrl =
FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).last!
return docFolderUrl.appendingPathComponent("cards.data").path
}
private func load() {
if let cards = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) as? [CueCard] {
self.cards = cards
if !cards.isEmpty {
currentIndex = 0
}
}
else {
self.cards = [CueCard]()
}
}
private func save() {
NSKeyedArchiver.archiveRootObject(cards, toFile: filePath)
}
load()
method, and modify the AddCard()
and removeCurrentCard()
methods so they call the save()
method before returningNSCoding is just one way of persisting data. It could also be achieved by implementing the Codable
protocol. Create a version of the project that persists data using Codable instead of NSCoding.