Zork – Game Screen

In this post we will begin the first real step of the project as we create the game screen – this will consist of a simple text field for user input and a scrollable text view showing the history of messages throughout the game. I will go in-depth on the description of code for the view controller as well as the manager object that records the message history. When we complete this lesson, you should be able to type messages into the text field and see them get appended to the log of messages in the text view.

Project Setup

If you haven’t already done it, go ahead and create a new Xcode project. I used the iOS – Application – Single View Application template and named my project “BlogECSTextRPG”. I made sure to select Swift as the language and did not use Core Data, Unit Tests or UI Tests.

Resources

You will need to import the sample database I created for this project. You can learn about how I created it and what kinds of content it holds by reading the intro lesson, but for now just download a copy HERE.

Important:

If you add files to your project by drag-and-drop on the Project Navigator pane, you will see an options dialog that allows you to copy the item and specify target membership. Files with common file extensions such as .swift, or .png, have a default setting to be added to the target. Uncommon file extensions such as the .db used by my sqlite database are not automatically included by default. You can enable it here, but if you imported the Zork.db file via any other means, verify that the “Target Membership” for your build target is enabled. You can see this from the File Inspector in the Utilities pane.

SQLite

To save me from learning SQL, I found a Swift wrapper library on github called SQLite.swift. This library installed easily with CocoaPods though there are a variety of options you can pick from. It also has pretty good documentation to help teach you the basics. You must add this library to the project in one form or another.

If you want to directly use the project from my repository, you will need to make sure you have installed and use CocoaPods or the project wont compile. Cocoapods is another free tool you can install on your mac, and here is an installation guide and usage guide just in case.

If you used Cocoapods to install this library, dont forget to close the Xcode project and reopen it with the “{ProjectName}.xcworkspace” file instead of the normal project file.

User Interface

The game screen for this app is very simple. It consists of a ‘UITextView’ and ‘UITextField’ contained in a vertical ‘UIStackView’ which is constrained to the edges of the window. See the sample screen shot below for an idea of what to create. Of course you can always copy the ‘Main.storyboard’ file from my demo project if you wish.

ECS_Zork_02_01_GameScreen_zpsslpju7pi

The text view, text field, and bottom auto-layout constraint all have outlets to the ‘GameViewController’ (I will show this next) and the text field also has a referencing outlet for its delegate (also to the ‘GameViewController’).

Game View Controller

Copy the code below as a starting point for the Game View Controller. I will discuss the code in small snippets like this so that the explanation is as close to the code as possible.

import UIKit

class GameViewController: UIViewController {

    // MARK: - IBOutlets
    @IBOutlet private var historyTextView: UITextView!
    @IBOutlet private var commandTextField: UITextField!
    @IBOutlet private var stackBottomConstraint: NSLayoutConstraint!
    
    // TODO: Add Next Code Snippets Here...
}

extension GameViewController: UITextFieldDelegate {
    func textFieldShouldReturn(textField: UITextField) -> Bool {
        guard let input = textField.text, message = String(UTF8String: "\n> \(input)") where input.characters.count > 0 else { return true }
        LoggingSystem.instance.addLog(message)
        textField.text = "";
        historyTextView.text = LoggingSystem.instance.print()
        scrollToCurrent()
        return true
    }
}

This code defines ‘GameViewController’ as a subclass of ‘UIViewController’ which allows it to be used as the custom class of the screen in our storyboard file. In the body of the class, I have added three ‘IBOutlets’ which allow Xcode to connect the views in the storyboard to our code. The ‘historyTextView’ will be used to show the history of messages presented by our game. Currently that will only include messages that you type yourself, but in the future it will also include messages that happen as a result of your commands. The ‘commandTextField’ will be used by the player to input commands to the game such as “read the leaflet”. The ‘stackBottomConstraint’ will be used in order to help layout the screen so that the keyboard can show or hide without blocking visibility to our text field.

I implemented the ‘UITextFieldDelegate’ as an extension of the ‘GameViewController’. Currently, the implementation via an extension doesn’t do much more than help organize the code into logical sections. You can also accomplish that using the // MARK: - code comments which provide titles and separators in the selection pull down menu, but another benefit of the extension is that it could easily be moved to another file, which might be a good idea if any of your classes start to get a bit too long to read easily.

I used the textFieldShouldReturn method to capture a user’s input. If you are running the game in a simulator you can use the ‘return’ key on your keyboard to trigger this, and on a device (or on a simulator where you show the software keyboard) you can also trigger this by tapping the ‘Done’ button. The first line inside the body of the method uses a ‘guard’ statement to verify that a few things appear as I expect. First, I need to verify that I can get some text from the textField. Because the ‘text’ property is an optional it could be nil, and this guard statement will verify that it is not. Next I create a custom formatted message. I begin with a newline character \n followed by the > character, both of which serve as a visual separator so that we can differentiate the messages that a user has typed and messages which will be posted later by game content. Finally the ‘guard’ statement includes a ‘where’ clause which makes sure the text isn’t an empty string, because there would be no action necessary to take in this use case.

The next statements pass the custom message to the Logging System, reset the text field (so the user can easily input another command), and then populate the text view with the newly updated collection of messages contained by the Logging System. Finally we cause the text view to scroll so that the most current messages are always visible.

Copy this next code snippet into the body of the ‘GameViewController’ where I left a “TODO” comment placeholder:

// MARK: - Fields
private let bottomPadding: CGFloat = 16

// MARK: - UIViewController
override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWillShow (_:)), name: UIKeyboardWillShowNotification, object: .None)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWillHide (_:)), name: UIKeyboardWillHideNotification, object: .None)
    commandTextField.becomeFirstResponder()
}

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: .None)
    NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: .None)
}

// MARK: - Notification Handlers
func keyboardWillShow (notification: NSNotification) {
    guard let userInfo = notification.userInfo, keyboardSize = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() else { return }
    stackBottomConstraint.constant = keyboardSize.height + bottomPadding
}

func keyboardWillHide (notification: NSNotification) {
    stackBottomConstraint.constant = bottomPadding
}

This code responds to two notifications: Showing the Keyboard, and Hiding the Keyboard. I used the ‘UIViewControllers’ methods: ‘viewWillAppear’ to add my listeners and the ‘viewWillDisappear’ to remove them. It is always important to remove any notification listener which you have added or you will end up with memory leaks and potentially some nasty bugs.

The notification for showing the keyboard includes some handy information such as the size of the keyboard. In the “keyboardWillShow” notification handler I use a guard statement to verify that there is indeed a ‘userInfo’ object as I expected, and then to verify that it contains the information I am looking for. If so, I update the ‘stackBottomConstraint.constant’ value to be the height of the keyboard plus the amount of padding that I think looks nice.

When the keyboard hides, I don’t care whether or not the notification included any user info, because all I need to do is move the ‘stackBottomConstraint.constant’ value back to the bottom of the screen (at the padding value) which I used when I set up the view in the storyboard.

Copy this next code snippet just after the ‘keyboardWillHide’ method:

// MARK: - Private
private func scrollToCurrent() {
    let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
    dispatch_after(time, dispatch_get_main_queue()) { [weak self] in
        guard let textView = self?.historyTextView else { return }
        let offset = CGPointMake(0, max(textView.contentSize.height - textView.bounds.height, 0))
        textView.setContentOffset(offset, animated: true)
    }
}

The last method for this class allows the textView to scroll to the bottom after we have updated its contents. I tried a variety of order of operations for this but the only one which consistently worked for me during testing was to allow a brief pause between the updated text and the scrolling. To handle that I used the ‘dispatch_after’ method to perform a block after a small amount of ‘time’. This block captures a weak reference to self (so that it wont keep the view alive in case you wanted to exit the screen for some reason), then assuming the textView is still there will cause it to scroll to the bottom with animation.

Logging System

This class was so short that I included it as a single snippet:

import Foundation

class LoggingSystem {
    
    // MARK: - Singleton
    static let instance = LoggingSystem ()
    private init() {}
    
    // MARK: - Fields
    private var logs: [String] = []
    private var maxLogCount = 100
    
    // MARK: - Public Methods
    func addLog (message: String) {
        logs.append(message)
        if (logs.count > maxLogCount) {
            logs.removeFirst()
        }
    }
    
    func print () -> String {
        return logs.joinWithSeparator("\n")
    }
}

Functionally all the class does is to store an array of strings – you can add a message, or print the collection of messages to a new string where each message appears on a new line. I added some extra optional logic which constrains the collection to 100 entries although that wasn’t strictly necessary especially given the amount of memory that will likely be available.

I implemented this using the Singleton pattern. The singleton pattern verifies that there is always exactly one instance of a class. In Swift this is easily accomplished by using a ‘static’ instance property which makes it available as soon as anything has knowledge of the class itself. By making the ‘init’ method private, nothing else can create an instance of this class so therefore I will only ever have one.

The singleton is somewhat of a controversial pattern. Technically I could have made the entire system work using only class methods and static properties, but occasionally an instance will offer greater flexibility. It is my personal preference that any time fields are needed that I use an instance, thus the singleton. Other patterns often used include ‘dependency injection’ and ‘inversion of control’ in case you want to study them for yourself and come to your own conclusion on the matter.

Demo

Now would be a good time to test drive our project. Save your work, then build and run. Type some input into the text field and when you are ‘Done’ it should clear the text field and your message should appear in the text view just above it. When you add enough messages you should be able to scroll around in the text view to read older messages, and adding additional messages should cause the scroll view to scroll back to the most current message.

Summary

This was a quick lesson where we created and configured the Xcode project which we will be using for the rest of this tutorial series. We imported the sample Zork database I discussed from the first lesson and also learned a bit about Cocoapods and how to install a SQLite Swift Library so that working with SQL would be much simpler. After creating the interface, we also implemented two new classes, one to control the game screen and one to manage a history of messages. The two classes worked together to allow input typed by the user to be saved in history and presented back in the scrollable text view area.

Don’t forget that this project is accompanied by a repository HERE. There will be one commit per lesson so you can see exactly how the project would have been cofigured and how the code would look at each step.

Leave a Reply

Your email address will not be published. Required fields are marked *