the making of interactive storyline app “Distopio“

In a dystopian world, what’s your destiny gonna be…

This is a sister project of the quiz app from the previous post, using MVC design pattern, the structure of building this one is very similar to the quiz app, so I will keep this one brief.

A fun design is the round corner button using Figma:

I also wanted the background image to change to a new visual world so that when user chooses a different storyline, it will visually take them there, this was done by adding a secondary image, give it an IBOutlet, toggle between the original background image and the added one through hiding and un-hiding, see below:

override func viewDidLoad() {
        super.viewDidLoad()
       
        BGimage.isHidden = true
        BGimage1.isHidden = false
        updateUI()
        
    }

Important note:

If you keep having issues such as not being able to change the Button’s text once the attributed text format is applied, here’s the solution:

To maintain your attributes, you actually can no long call currentTitle like you used to anymore, because attributed title string cannot be passed through a normal title string without making the attributes nil. So you have to do this in two steps:

  1. Create your fancy decoration

  2. Call the fancy decoration to apply to your normal boring title (but pretend you are just gonna be basic for now just so that the string can pass through)

Step 1.

Taking this app’s code for example:

@IBAction func choiceMade(_ sender: UIButton) {
       
       BGimage.isHidden = true
       BGimage1.isHidden = false
       
       let userChoice = sender.attributedTitle(for: .normal)?.string ?? ""
       // old version code replaced by the line above to preserve attrituted text format:
       // old version code: storyBrain.nextStory(userChoice: sender.currentTitle!)

       storyBrain.nextStory(userChoice: userChoice)
       
       updateUI()
   }

In this code block, the first (?) is optional chaining. It means, if the attributed title is not nil (if it exists), then access its string property (the plain text of the title). If the attributed title is nil, the entire expression short-circuits and returns nil.

The double question marks (??) are the nil-coalescing operator. This is like a safety net saying if the result of the previous expression is nil (the button had no title), then use an empty string ("") as the default value.

This pattern is commonly used to avoid potential crashes caused by force-unwrapping optional values. But since you are providing a default value with ?? "", you ensure that “userChoice” will always have a string value, even if the button's title is missing.

Step 2.

func updateUI() {
        
        storyLable.text = storyBrain.getStoryTitle()
        
        let choice1Text = NSAttributedString(string: storyBrain.getChoice1(), attributes: nil)
        choice1Button.setAttributedTitle(choice1Text, for: .normal)
        // old version code: choice1Button.setTitle(storyBrain.getChoice1(), for: .normal)
        
        let choice2Text = NSAttributedString(string: storyBrain.getChoice2(), attributes: nil)
        choice2Button.setAttributedTitle(choice2Text, for: .normal)
        // old version code: choice2Button.setTitle(storyBrain.getChoice2(), for: .normal)
        
        // old version code replaced by the code above them to preserve attrituted text format
    
        if storyBrain.storyNumber == 2 {
            BGimage.isHidden = false
            BGimage1.isHidden = true
        } else {
            BGimage.isHidden = true
            BGimage1.isHidden = false
        }
    }

Imagine you're playing a game where you can choose your own adventure. You’ve got this magic book “storyBrain” that tells you the story and gives you choices.

let choice1Text = NSAttributedString(string: storyBrain.getChoice1(), attributes: nil)

This is like asking the magic book for the first choice in the story. You take that choice and write it down on a special piece of paper (NSAttributedString) called “choice1Text”. This special paper can make the text look fancy, but right now, you're just writing it down normally (that's what “attributes: nil” means).

choice1Button.setAttributedTitle(choice1Text, for: .normal)

Now, you have a magic button (choice1Button) that can show the choice to the player. You use your magic to put the text from your special paper (choice1Text) onto the button.

“for: .normal” means you're putting the text on the button for when it's in its normal state, not when it's being pressed or anything like that.

So these two lines of code are getting the first choice from the story, writing it down, and then putting that text on a button for the player to choose.

It’s a lot…but here is a snippet of the code and UI:

And my assets design:

Clean code:

*** similar code with detailed breakdown notes see quiz app post ***

( 1 ) Story.swift — Model

import Foundation

struct Story {
    let title: String
    let choice1: String
    let choice1Destination: Int
    let choice2: String
    let choice2Destination: Int
}

( 2 ) StoryBrain.swift — Model

import Foundation

struct StoryBrain {
    
    var storyNumber = 0
    
    let stories = [
        
        Story(
            title: "Your car has blown a tire on a winding road in the middle of nowhere with no cell phone reception. You decide to hitchhike. A rusty pickup truck rumbles to a stop next to you. A man with a wide brimmed hat with soulless eyes opens the passenger door for you and asks: 'Need a ride?'.",
            choice1: "I'll hop in. Thanks for the help!", choice1Destination: 2,
            choice2: "Better ask him if he's a murderer first.", choice2Destination: 1),
        Story(
            title: "He nods slowly, unfazed by the question.",
            choice1: "At least he's honest. I'll climb in.", choice1Destination: 2,
            choice2: "Wait, I know how to change a tire.", choice2Destination: 3),
        Story(
            title: "As you begin to drive, the stranger starts talking about his relationship with his mother. He gets angrier and angrier by the minute. He asks you to open the glovebox. Inside you find a bloody knife, two severed fingers, and a cassette tape of Elton John. He reaches for the glove box.",
            choice1: "I love Elton John! Hand him the cassette tape.", choice1Destination: 5,
            choice2: "It's him or me! You take the knife and stab him.", choice2Destination: 4),
        Story(
            title: "What? Such a cop out! Did you know traffic accidents are the second leading cause of accidental death for most adult age groups?",
            choice1: "The", choice1Destination: 0,
            choice2: "End", choice2Destination: 0),
        Story(
            title: "As you smash through the guardrail and careen towards the jagged rocks below you reflect on the dubious wisdom of stabbing someone while they are driving a car you are in.",
            choice1: "The", choice1Destination: 0,
            choice2: "End", choice2Destination: 0),
        Story(
            title: "You bond with the murderer while crooning verses of 'Can you feel the love tonight'. He drops you off at the next town. Before you go he asks you if you know any good places to dump bodies. You reply: 'Try the pier.'",
            choice1: "The", choice1Destination: 0,
            choice2: "End", choice2Destination: 0)
    ]
    
    func getStoryTitle() -> String {
        return stories[storyNumber].title
    }
    
    func getChoice1() -> String {
        return stories[storyNumber].choice1
    }
    
    func getChoice2() -> String {
        return stories[storyNumber].choice2
    }
        
        
    mutating func nextStory(userChoice: String) {
        
        let currentStory = stories[storyNumber]
        if userChoice == currentStory.choice1 {
            storyNumber = currentStory.choice1Destination
        } else if userChoice == currentStory.choice2 {
            storyNumber = currentStory.choice2Destination
        }
    }
}

( 3 ) ViewController.swift — Control

import UIKit

class ViewController: UIViewController {


    @IBOutlet weak var BGimage1: UIImageView!
    @IBOutlet weak var BGimage: UIImageView!
    @IBOutlet weak var storyLable: UILabel!
    @IBOutlet weak var choice1Button: UIButton!
    @IBOutlet weak var choice2Button: UIButton!
    
    var storyBrain = StoryBrain()
    
    override func viewDidLoad() {
        super.viewDidLoad()
       
        BGimage.isHidden = true
        BGimage1.isHidden = false
        updateUI()
        
    }

    @IBAction func choiceMade(_ sender: UIButton) {
        
        BGimage.isHidden = true
        BGimage1.isHidden = false
        
        let userChoice = sender.attributedTitle(for: .normal)?.string ?? ""
        // old version code replaced by the line above to preserve attrituted text format:
        // old version code: storyBrain.nextStory(userChoice: sender.currentTitle!)

        storyBrain.nextStory(userChoice: userChoice)
        

        updateUI()
    }
    
    func updateUI() {
        
        storyLable.text = storyBrain.getStoryTitle()
        
        let choice1Text = NSAttributedString(string: storyBrain.getChoice1(), attributes: nil)
        choice1Button.setAttributedTitle(choice1Text, for: .normal)
        // old version code: choice1Button.setTitle(storyBrain.getChoice1(), for: .normal)
        
        let choice2Text = NSAttributedString(string: storyBrain.getChoice2(), attributes: nil)
        choice2Button.setAttributedTitle(choice2Text, for: .normal)
        // old version code: choice2Button.setTitle(storyBrain.getChoice2(), for: .normal)
        
        // old version code replaced by the code above them to preserve attrituted text format
    
        if storyBrain.storyNumber == 2 {
            BGimage.isHidden = false
            BGimage1.isHidden = true
        } else {
            BGimage.isHidden = true
            BGimage1.isHidden = false
        }
    }

}
Previous
Previous

the making of BMI Calculator app

Next
Next

the making of quiz app