This codelab will teach you how to modify an iOS app to include the Blockly visual programming library.

What is Blockly?

Blockly is a library for building block programming apps. It includes everything you need for defining and rendering blocks in a drag-n-drop editor. Each block represents a chunk of code that can be easily stacked and translated into code. It's helpful for allowing apps to easily customize components and write code.

What you will build

An iOS app where you can program buttons to play different sounds, using Blockly.

What you'll learn

This codelab is focused on Blockly. Non-relevant concepts and code blocks are glossed over and are provided for you to simply copy and paste.

Install Xcode 8.3.1 (or higher)

The latest version of Xcode can be downloaded from Apple's developer site.

Download the Code

Clone the Blockly-iOS GitHub repository from the command line.

$ git clone https://github.com/google/blockly-ios.git
$ cd blockly-ios

Before we dive into code, let's run the completed version of the codelab to get an idea of what you're about to build:

  1. Navigate to Samples/BlocklyCodeLab.
  2. Open the file named BlocklyCodeLab.xcodeproj in Xcode.
  3. Select Product > Run to run the app.

Edit Mode

By default, the app launches in "Edit Mode". In this mode, you can see 9 buttons, all of which are now configurable.

Tapping a button will display a Blockly editor, which is where you can "code" how sounds should play for that button. Here's an example of this screen:

Play Mode

If you go back and tap the <Done> button, you will see the app transform into "Play Mode". In this mode, tapping a button will play the "code" that was configured for that button.

Play around with the app for a bit. Once you're ready, you can move on to the next step.

Now that you know what you'll be building, let's open the Starter app:

  1. Close all instances of Xcode.
  2. Navigate to Samples/BlocklyCodeLab-Starter.
  3. Open the file named BlocklyCodeLabStarter.xcodeproj in Xcode.

Add Blockly projects to Starter app

In Xcode, select File > Add Files to "BlocklyCodeLabStarter"... and add two Xcode projects from the Blockly-iOS repo into the starter project:

Setup projects as dependencies

Now we need to add those projects as dependencies for the starter app. In the project settings, select the BlocklyCodeLabStarter target and open its General settings tab:

Scroll down to the Embedded Binaries section and press the + button. In the window that pops up, select the following frameworks:

Press the Add button.

It should now look like this:

Take a minute to look at the structure of the Starter app.

MusicMakerViewController

This class is the master view controller for the Music Maker app. It controls the creation of the buttons which we will be programming and is responsible for toggling between editing and using the functionality of the buttons in this app.

MusicMaker

This class is in charge of playing sounds, and will be responsible for running our generated code sequentially, so that the buttons can be coded to play songs.

ButtonEditorViewController

This is the view controller that is pushed onto the screen when tapping a button in "Edit Mode". Currently, this class does very little, but we will integrate a Blockly workbench into it to program the functionality of the buttons.

What is a workbench?

WorkbenchViewController is a view controller that contains a Blockly workspace, toolbox, trash can, and undo/redo controls. A workspace is the UI component that contains the "active" blocks to be dragged around, while the toolbox contains the list of blocks that can be added to the workspace.

Add a workbench to ButtonEditorViewController

From the project navigator, open ButtonEditorViewController.swift.

First, you'll need to import Blockly at the top of the file:

import Blockly

Add a property to ButtonEditorViewController in order to create a new instance of WorkbenchViewController:

/// The main Blockly editor.
private var workbenchViewController: WorkbenchViewController = {
  let workbenchViewController = WorkbenchViewController(style: .alternate)
  workbenchViewController.toolboxDrawerStaysOpen = true

  return workbenchViewController
}()

Override viewDidLoad() so that workbenchViewController gets added to the view controller after view load.

override func viewDidLoad() {
  super.viewDidLoad()

  edgesForExtendedLayout = []

  // Add editor to this view controller
  addChildViewController(workbenchViewController)
  view.addSubview(workbenchViewController.view)
  workbenchViewController.view.frame = view.bounds
  workbenchViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  workbenchViewController.didMove(toParentViewController: self)
}

Let's test this code out. Run your app and tap one of the buttons. Now, instead of displaying a blank screen, it should now display a Blockly workbench:

You likely noticed that the workspace is empty, and there is currently nothing interesting to do with it. We need to add blocks to the toolbox, so they can be added to the workspace.

Specify blocks that can be used in the workbench

Before we add blocks to the toolbox, we need to specify the types of blocks that can be added to a workbench. This is done by loading block definitions into the workbench's "block factory".

Blockly provides default definitions for many blocks that are common in programming (eg. "if" blocks, loop blocks, etc.). Let's load all of these default blocks into the workbench by adding the following code to ButtonEditorViewController:

private var workbenchViewController: WorkbenchViewController = {
  let workbenchViewController = ...

  // Load default blocks into workbench's block factory
  let blockFactory = workbenchViewController.blockFactory
  blockFactory.load(fromDefaultFiles: .allDefault)

  return workbenchViewController
}()

Load the toolbox

Now that we've specified the blocks that the workbench can use, we can construct a toolbox for the workbench, which groups blocks under different category names.

We've defined a simple toolbox in a resource file Resources/Non-Localized/toolbox.xml. If you open it up, it will look like this:

<xml>
  <category name="Loops" colour="120">
    <block type="controls_repeat_ext">
      <value name="TIMES">
        <shadow type="math_number">
          <field name="NUM">5</field>
        </shadow>
      </value>
    </block>
  </category>
</xml>

This XML defines a toolbox with a single "repeat loop" block inside a single category named "Loops".

Let's create a toolbox from this toolbox.xml and load it into the workbench. Copy the following and add it to the workbenchViewController instantiation code. Notice the blocks must be loaded before the toolbox:

private var workbenchViewController: WorkbenchViewController = {
  let workbenchViewController = ...

  // Load default blocks into workbench's block factory
  ...

  // Load toolbox
  do {
    let toolboxPath = "toolbox.xml"
    if let bundlePath = Bundle.main.path(forResource: toolboxPath, ofType: nil) {
      let xmlString = try String(
        contentsOfFile: bundlePath, encoding: String.Encoding.utf8)
      let toolbox = try Toolbox.makeToolbox(
        xmlString: xmlString, factory: blockFactory)
      try workbenchViewController.loadToolbox(toolbox)
    } else {
      print("Could not load toolbox XML from '\(toolboxPath)'")
    }
  } catch let error {
    print("An error occurred loading the toolbox: \(error)")
  }

  return workbenchViewController
}()

Try running the app again, and editing a button. You should now see a toolbox, and be able to drag and drop blocks into the workspace!

Next, we want to add a custom "play sound" block to our workbench:

Create sound block JSON

To define a custom block in Blockly, it must be specified inside a JSON file.

Let's create a JSON file to define a new "play sound" block:

  1. In Xcode, navigate to BlocklyCodeLabStarter > Resources > Non-Localized in the project navigator.
  2. Right-click on the Non-Localized group and select New File...
  3. Select Empty and hit Next.
  4. Set the file name as sound_blocks.json and hit Save.

Add the following code to sound_blocks.json:

[{
  "type": "play_sound",
  "message0": "Play %1",
  "args0": [
    {
      "type": "field_dropdown",
      "name": "VALUE",
      "options": [
        ["C4", "Sounds/c4.m4a"],
        ["D4", "Sounds/d4.m4a"],
        ["E4", "Sounds/e4.m4a"],
        ["F4", "Sounds/f4.m4a"],
        ["G4", "Sounds/g4.m4a"],
        ["A4", "Sounds/a4.m4a"],
        ["B4", "Sounds/b4.m4a"],
        ["C5", "Sounds/c5.m4a"]
      ]
    }
  ],
  "previousStatement": null,
  "nextStatement": null,
  "colour": 20,
  "tooltip": "",
  "helpUrl": ""
}]

Load the sound block into the block factory

The next step is to load this block definition into the workbench's block factory, so it knows how to build this block.

Open ButtonEditorViewController. Edit workbenchViewController to load the sound block from the JSON file (just before loading the toolbox):

private var workbenchViewController: WorkbenchViewController = {
  let workbenchViewController = ...

  // Load default blocks into the block factory
  ...

  // Load sound blocks into the block factory
  do {
    try blockFactory.load(fromJSONPaths: ["sound_blocks.json"])
  } catch let error {
    print("An error occurred loading the sound blocks: \(error)")
  }

  // Load toolbox
  ...

  return workbenchViewController
}()

Add the sound block to the toolbox

The last step is to update the workbench's toolbox to include the new sound block.

Open Resources/Non-Localized/toolbox.xml. This file already contains a "Loops" category. After that category, add a "Sounds" category that includes our custom "play_sound" block:

<xml>
  <category name="Loops" colour="120">
    ...
  </category>
  <category name="Sounds" colour="20">
    <block type="play_sound" />
  </category>
</xml>

Run the app one more time, and play around with the new "Sounds" category and the new "Play (sound)" block. It should look like this:

Next, we need to add saving and loading to our workspaces. Code will generate from the XML on the workspace, so we will save and load the workspace when we edit the contents of each button.

Setup the save method

Open the ButtonEditorViewController. Add the following code to the saveBlocks() method:

/**
 Saves the workspace for this button ID to disk.
 */
public func saveBlocks() {
  // Save the workspace to disk
  if let workspace = workbenchViewController.workspace {
    do {
      let xml = try workspace.toXML()
      FileHelper.saveContents(xml, to: "workspace\(buttonID).xml")
    } catch let error {
      print("Couldn't save workspace to disk: \(error)")
    }
  }
}

This method simply takes the workspace, and exports and saves it to a local XML file on the device. Next, overload the viewWillDisappear() method, and add a call to saveBlocks():

override func viewWillDisappear(_ animated: Bool) {
  // Save on exit
  saveBlocks()

  super.viewWillDisappear(animated)
}

Now, whenever the ButtonEditorViewController closes, the workspace will be saved to disk.

Setup the load method

In the ButtonEditorViewController, edit the loadBlocks(forButtonID:) method:

/**
 Load a workspace for a button ID into the workbench, if it exists on disk.

 - parameter buttonID: The button ID to load.
 */
public func loadBlocks(forButtonID buttonID: String) {
  self.buttonID = buttonID

  do {
    // Create fresh workspace
    let workspace = Workspace()

    // Load blocks into this workspace from a saved file (if it exists).
    if let xml = FileHelper.loadContents(of: "workspace\(buttonID).xml") {
      try workspace.loadBlocks(
        fromXMLString: xml, factory: workbenchViewController.blockFactory)
    }

    // Load the workspace into the workbench
    try workbenchViewController.loadWorkspace(workspace)
  } catch let error {
    print("Couldn't load workspace from disk: \(error)")
  }
}

Similar to the saveBlocks() method, the loadBlocks() method loads a workspace from XML into the WorkbenchViewController.

Finally, go back to MusicMakerViewController. Fill out the editButton(buttonID:)method so it calls the loadBlocks() method:

/**
 Opens the code editor for a given button ID.

 - parameter buttonID: The button ID to edit.
 */
func editButton(buttonID: String) {
  editingButtonID = buttonID

  // Load the editor for this button number
  let buttonEditorViewController = ButtonEditorViewController()
  buttonEditorViewController.loadBlocks(forButtonID: buttonID)
  navigationController?.pushViewController(buttonEditorViewController, animated: true)
}

Now, test the code. Edit the workspace on one of the buttons, add some blocks, close it, and reopen it. The workspace should still contain the blocks you added.

Now that each button can be configured with its own Blockly workspace, the next thing we want to do is to generate JavaScript code from each workspace.

This generated code will eventually be run inside a JavaScript virtual machine, effectively executing the blocks set up in the Blockly workspace.

Add a JavaScript generator for the sound block

When Blockly generates JavaScript code for blocks in a workspace, it translates each block into code. By default, it knows how to translate all library-provided default blocks into JavaScript code. However, for any custom blocks, we need to specify our own translation functions (aka. code generators).

Navigate to Resources/Non-Localized and create a new file named sound_block_generators.js. Add the following function definition to this file:

Blockly.JavaScript['play_sound'] = function(block) {
  var value = '\'' + block.getFieldValue('VALUE') + '\'';
  return 'MusicMaker.playSound(' + value + ');\n';
};

With this translation function, the following "play_sound" block:

translates into the JavaScript code "MusicMaker.playSound('Sounds/c4.m4a');".

This file is now ready to be used inside a "code generator service", which we will set up next.

Create the code generator service

In order to generate code from a Blockly workspace, we need to create and use the CodeGeneratorService object. It is a service that can be configured to generate code in many languages such as JavaScript, Lua, Python, PHP, and Go.

Create a new file named CodeManager.swift (using File -> New... -> File -> Swift File). This file will be responsible for all code generation in our app. First, import Blockly.

import Blockly

Add the CodeManager class and define a CodeGeneratorService on it:

/**
 Manages JS code in the app. It generates JS code from workspace XML and
 saves it in-memory for future use.
 */
class CodeManager {
  /// Service used for converting workspace XML into JS code.
  private var codeGeneratorService: CodeGeneratorService = {
    let service = CodeGeneratorService(
      jsCoreDependencies: [
        // The JS file containing the Blockly engine
        "blockly_web/blockly_compressed.js",
        // The JS file containing a list of internationalized messages
        "blockly_web/msg/js/en.js"
      ])

    return service
  }()
}

The dependency files for the CodeGeneratorService are the core Blockly files, as well as the internationalized messages (these files are already located in the Starter project under Resources/Non-Localized).

Create a request builder

Next, the CodeGeneratorService needs to be associated with a "request builder". A request builder contains all the use-specific information for generating code.

In this case, we want to configure the request builder to:

Add the following to the codeGeneratorService initialization:

/// Service used for converting workspace XML into JS code.
private var codeGeneratorService: CodeGeneratorService = {
  let service = ...

  let builder = CodeGeneratorServiceRequestBuilder(
    // This is the name of the JS object that will generate JavaScript code
    jsGeneratorObject: "Blockly.JavaScript")
  // Load the block definitions for all default blocks
  builder.addJSONBlockDefinitionFiles(fromDefaultFiles: .allDefault)
  // Load the block definitions for our custom sound block
  builder.addJSONBlockDefinitionFiles(["sound_blocks.json"])
  builder.addJSBlockGeneratorFiles([
    // Use JavaScript code generators for the default blocks
    "blockly_web/javascript_compressed.js",
    // Use JavaScript code generators for our custom sound block
    "sound_block_generators.js"])

  // Assign the request builder to the service and cache it so subsequent
  // code generation runs are immediate.
  service.setRequestBuilder(builder, shouldCache: true)

  return service
}()

Generate code

Now that we've set up the code generation service, we need to create methods to generate and cache the code for each button. We'll start with a property to store the generated code:

/// Stores JS code for a unique key (ie. a button ID).
private var savedCode = [String: String]()

Next, we'll call the actual code generation:

/**
 Generates code for a given `key`.
 */
func generateCode(forKey key: String, workspaceXML: String) {
  do {
    // Clear the code for this key as we generate the new code.
    self.savedCode[key] = nil

    let _ = try codeGeneratorService.generateCode(
      forWorkspaceXML: workspaceXML,
      onCompletion: { requestUUID, code in
        // Code generated successfully. Save it for future use.
        self.savedCode[key] = code
      },
      onError: { requestUUID, error in
        print("An error occurred generating code - \(error)\n" +
          "key: \(key)\n" +
          "workspaceXML: \(workspaceXML)\n")
      })
  } catch let error {
    print("An error occurred generating code - \(error)\n" +
      "key: \(key)\n" +
      "workspaceXML: \(workspaceXML)\n")
  }
}

When we call the generateCode(forKey:workspaceXML:) method, it should (assuming nothing goes wrong!) store the resulting code in the savedCode dictionary for this button. We'll also want an accessor method to get the code for a given button:

/**
 Retrieves code for a given `key`.
 */
func code(forKey key: String) -> String? {
  return savedCode[key]
}

Finally, let's add a bit of cleanup code to the deinit method so that pending requests are cancelled if the CodeManager is deallocated:

deinit {
  codeGeneratorService.cancelAllRequests()
}

Use CodeManager

Now that we have the CodeManager class set up to generate code, we just need to make sure we call it at the right times.

Open MusicMakerViewController.swift. Add a CodeManager instance to this class:

/// Generates and stores Javascript code for each button.
private var codeManager = CodeManager()

We'll start with filling out the generateCode(forButtonID:) method.

/**
 Requests that the code manager generate code for a given button ID.

 - parameter buttonID: The button ID.
 */
func generateCode(forButtonID buttonID: String) {
  // If a saved workspace file exists for this button, generate the code for it.
  if let workspaceXML = FileHelper.loadContents(of: "workspace\(buttonID).xml") {
    codeManager.generateCode(forKey: String(buttonID), workspaceXML: workspaceXML)
  }
}

The CodeManager will store the code for each button when we call this method. So now all we need to do is call generateCode(forButtonID:) in the right places. We'll call it once in viewWillAppear(:), so the just-edited button gets generated when we finish editing. We'll also call it for each button in viewDidLoad(), so we make sure that code is generated for each button when we first enter the app.

override func viewDidLoad() {
  super.viewDidLoad()

  // Load code for each button
  for i in 1...9 {
    generateCode(forButtonID: String(i))
  }

  // Start in edit mode
  setEditing(true, animated: false)
  updateState(animated: false)
}

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)

  // If this view controller is appearing again after editing a button,
  // generate new code for it.
  if !editingButtonID.isEmpty {
    generateCode(forButtonID: editingButtonID)
    editingButtonID = ""
  }
}

Now, we should verify that our code is getting properly generated. Change the runCode(forButtonID:) method to:

/**
 Runs code associated with a given button ID.

 - parameter buttonID: The button ID.
 */
func runCode(forButtonID buttonID: String) {
  let code = codeManager.code(forKey: buttonID) ?? "Unavailable"
  print("Code for button \(buttonID):\n \(code)")
}

Run the app and try it out! Edit a workspace, finish editing, then press the button you edited. Your code should generate and print out to the Xcode console (viewable by pressing ⌘-Shift-C).

If you were to drag a sound block into the workspace and generate its JavaScript code, it would look something like this:

MusicMaker.playSound('Sounds/cheer.mp3');

The problem is that if we ran this code in a JavaScript virtual machine, it would fail because MusicMaker.playSound(...) isn't defined in any JavaScript file.

What we want to do is to define this method in JavaScript, but to implement it using native iOS code.

Go to the project navigator and open MusicMaker.swift. You can see a class named MusicMaker with a static playSound(:) method. Let's modify this class so it can be executed from a JavaScript VM (which we'll do in the next part of the code lab).

Import JavaScriptCore to MusicMaker.swift:

import JavaScriptCore

Create the following protocol and add it to the top of MusicMaker.swift:

// Create a protocol that inherits JSExport, marking methods/variables
// that should be exposed to a JavaScript VM.
// NOTE: This protocol must be attributed with @objc.
@objc protocol MusicMakerJSExports: JSExport {
  static func playSound(_ file: String)
}

Then change MusicMaker to implement this protocol, NSObject, and add the @objc attribute:

// Change MusicMaker to implement MusicMakerJSExports.
// NOTE: This class must extend NSObject and be attributed with @objc.
@objc class MusicMaker: NSObject, MusicMakerJSExports {
  ...
}

With the above code, MusicMaker can now be registered inside a JavaScript VM. Once that occurs, any call to MusicMaker.playSound(...)inside that VM will actually route the call to our native iOS method.

With code generation set up and the ability run native iOS code from JavaScript, we just need to create a class that can run the JavaScript code.

Set up CodeRunner

Create a file named CodeRunner.swift and import JavaScriptCore:

import JavaScriptCore

Then, fill it in with the following code:

/**
 Runs JavaScript code.
 */
class CodeRunner {
  /// Use a JSContext object, which contains a JavaScript virtual machine.
  private var context: JSContext?

  /// Create a background thread, so the main thread isn't blocked when executing
  /// JS code.
  private let jsThread = DispatchQueue(label: "jsContext")

  init() {
    // Initialize the JSContext object on the background thread since that's where
    // code execution will occur.
    jsThread.async {
      self.context = JSContext()
      self.context?.exceptionHandler = { context, exception in
        let error = exception?.description ?? "unknown error"
        print("JS Error: \(error)")
      }

      // Register MusicMaker class with the JSContext object. This tells JSContext to
      // route any JavaScript calls to `MusicMaker.playSound(:)` back to iOS code.
      self.context?.setObject(
        MusicMaker.self, forKeyedSubscript: "MusicMaker" as NSString)
    }
  }
}

Execute JavaScript code

After setting up the JSContext object, running JavaScript code becomes a simple task. Add this method to CodeRunner:

/**
 Runs Javascript code on a background thread.

 - parameter code: The Javascript code.
 - parameter completion: Closure that is called on the main thread when
 the code has finished executing.
 */
func runJavascriptCode(_ code: String, completion: @escaping () -> ()) {
  jsThread.async {
    // Evaluate the JavaScript code asynchronously on the background thread.
    _ = self.context?.evaluateScript(code)

    // When it finishes, call the completion closure on the main thread.
    DispatchQueue.main.async {
      completion()
    }
  }
}

Run from MusicMakerViewController

Next, go back to MusicMakerViewController and add a property to store all of the currently running JS code.

// Store a list of all CodeRunner instances currently running JS code.
private var codeRunners = [CodeRunner]()

Finally, fill out the rest of the runCode(forButtonID:) method to use CodeRunner:

/**
 Runs code associated with a given button ID.

 - parameter buttonID: The button ID.
 */
func runCode(forButtonID buttonID: String) {
  if let code = codeManager.code(forKey: buttonID),
    code != "" {

    // Create and store a new CodeRunner, so it doesn't go out of memory.
    let codeRunner = CodeRunner()
    codeRunners.append(codeRunner)

    // Run the JS code, and remove the CodeRunner when finished.
    codeRunner.runJavascriptCode(code, completion: {
      self.codeRunners = self.codeRunners.filter { $0 !== codeRunner }
    })
  } else {
    print("No code has been set up for button \(buttonID).")
  }
}

That's it! Build your project and test it out. Edit a button, add a sound block to its workspace, and save it. Tapping that button should play the sound.

There's a slight problem with code execution. If you set up more than one sound block in your workspace, all sounds will play simultaneously instead of separately (one after the other), like in this example:

Both C4 and D4 will play at the same time here. While this could be a desired behavior, it isn't intuitive. Most users think of a "play sound" block as "play this sound to completion before running the next block."

The reason for this is that in MusicMaker.playSound(:), the call to player.play() executes asynchronously, causing the method to return immediately. If you open MusicMaker.swift,here's what you should see:

/**
 Play a specific sound. It blocks synchronously until playback of the sound
 has finished.

 This method is exposed to a JS context as `MusicMaker.playSound(_)`.

 - parameter file: The sound file to play.
 */
static func playSound(_ file: String) {
  guard let player = AudioPlayer(file: file) else {
    return
  }
  player.completion = { player, successfully in
    self.audioPlayers.remove(player)
  }

  // The following call returns immediately (not until the end of file playback).
  if player.play() {
    self.audioPlayers.insert(player)
  }
}

While we could fix the problem by rewriting the play(:) method to run synchronously, it wouldn't be good programming practice or a scalable solution . There are many blocks where it's very natural to make an asynchronous API method call (eg. fetching network data, waiting for user input) that blocks further down the chain may rely upon. Changing every call to be synchronous would be a lot of work and may be difficult to do.

Set up locks

A solution to this problem is to set up a lock. The idea is to:

1) Pause execution of the playSound(:) method immediately after player.play() is called.

2) Resume execution once the player has finished playback.

Add a member variable in MusicMaker.swift to store the NSConditionLock instances:

// Store all locks to ensure they stay in memory while the player is playing.
private static var conditionLocks = [String: NSConditionLock]()

Let's start with the first step, by implementing the "pause execution" using a NSConditionLock:

static func playSound(_ file: String) {
  guard let player = AudioPlayer(file: file) else {
    return
  }

  // Create a new lock, and give it a unique ID.
  // Set its condition to `0` to signify "playback has not completed yet".
  let uuid = NSUUID().uuidString
  self.conditionLocks[uuid] = NSConditionLock(condition: 0)

  player.completion = { player, successfully in
    self.audioPlayers.remove(player)
  }

  if player.play() {
    self.audioPlayers.insert(player)

    // Here is where the "pause" magic happens.
    // Tell this thread to block execution only until it can acquire the lock
    // when its condition is set to `1` (signifying "playback is complete").
    self.conditionLocks[uuid]?.lock(whenCondition: 1)
  }
}

With the code above, the thread pauses after calling player.play(), so that code execution doesn't immediately jump back to JSContext. Now, we just need to add code to properly resume thread execution when playback has completed:

static func playSound(_ file: String) {
  ...

  player.completion = { player, successfully in
    self.audioPlayers.remove(player)
    
    // Here is where the "resume" magic happens.
    // Playback has finished -- immediately acquire the lock, to unlock it with
    // its condition set to `1` (signifying "playback is complete"). This
    // effectively unblocks the other thread.
    self.conditionLocks[uuid]?.lock()
    self.conditionLocks[uuid]?.unlock(withCondition: 1)
  }

  if player.play() {
    self.audioPlayers.insert(player)

    self.conditionLocks[uuid]?.lock(whenCondition: 1)

    // Once execution has made it here, playback has completed. 
    // Unlock and dispose of the lock, in order to resume JavaScript execution.
    self.conditionLocks[uuid]?.unlock()
    self.conditionLocks[uuid] = nil
  }
}

By adding locks to MusicMaker to match the code above, sound blocks in your workspace should now play separately instead of simultaneously.

And with that, you're done with the Blockly codelab! If you'd like to continue playing with the app, we suggest adding or changing the available blocks. There are more sound files in the Resources/Non-Localized/Sounds/ folder - try hooking them up to a new block!

For more documentation, visit the Blockly developer site.

Additionally, Blockly has an active developer forum. Please drop by and say hello. We're happy to answer any questions or give advice on best practices for building an app with Blockly. Feel free to show us your prototypes early; collectively we have a lot of experience and can offer hints which will save you time.

And if you are an active Blockly developer, help us focus our development efforts by telling us what you are doing with Blockly. The questionnaire only takes a few minutes and will help us better support the Blockly community.