Puzzle Game using UI Drag & Drop APIs in Swift

Shankar Madeshvaran
Developer in Making
9 min readJun 20, 2019
Puzzle Game using Drag and Drop

In this sample project we are going to how implement Drag & Drop in iOS application using Swift language. I’m going to show you how it works and how to develop a simple puzzle app in your application. In this tutorial, we are going to see:

- Introduction

  • What is Drag & Drop?
  • How to use Drag and Drop APIs to develop simple Puzzle Game.

In this puzzle game, we are going to implement:

  • Show Hint image only once for each puzzle
  • Implemented Timer to show Hint image for a short period of time.
  • Make sure puzzle image are arranged in random order every time puzzle is shown to solve.
  • Make sure each puzzle is more difficult than the previous puzzle.

Drag & Drop

With drag and drop in iOS, users can drag items from one onscreen location to another using continuous gestures. A drag-and-drop activity can take place in a single app, or it can start in one app and end in another.

The app from which an item is dragged is called the source app. The app on which an item is dropped is called the destination app. For drag and drop in a single app, that app plays both roles simultaneously. The complete user action from start to finish — using system-mediated gestures — is called a drag activity.

A drag session, by contrast, is an object that’s managed by the system and that manages the items the user is dragging.

Text views and text fields automatically support drag and drop.Collection views and table views offer dedicated, view-specific methods and properties, and text views offer APIs for customizing the views’ drag-and-drop behavior.

Presenting ‘Puzzle Game’ Project

Puzzle is simple Game App which will show puzzles to solve.Each puzzle is going to be different and more difficult than the previous one that user had solved.Puzzle game also deal with lots of things during the development of a project.

How to use Drag and Drop to develop Puzzle Game

Let’s get started by creating a new project in Xcode.

1) Storyboard Designing:

Storyboard Design for Puzzle Game
  • Firsty, I have used UICollectionViewController instead of default ViewController and give background Color as red color.
  • I have embed an NavigationController by going to Edit -> Embed -> NavigationController and then I have named navigationBar as Puzzle using storyboard.
  • I implemented a custom cell for Collection View in storyboard itself which only contains a UIImageView.
  • I have set imageView’s contentMode as AspectFill and also give backgroundColor as Clear color.

2) Add necessary images to Assets.xcassets

Adding necessary images to develop puzzle Game in Assets.xcassets
  • You can find the necessary images which is used in this project in this link.

3) Creating Model class for Puzzle Game

Model Class Puzzle.Swift
  • I have implemented a simple Model Class for better understanding and also use them easily in the project.
  • title denotes the imagename of full puzzle image which is used to show hint to user.
  • solvedImages denotes array of imagenames which is in solved order.This will help us show Alert when puzzle is solved.
  • unsolvedImages denotes array of imagenames which is in unsolved order. We have implemented by using this below code
self.unsolvedImages = self.solvedImages.shuffled()
  • We have used shuffled() function to the solvedImages array to make sure we get random order each time puzzle is loaded.

4) Implement Custom Cell to load puzzle images to Collection View

Custom Cell Implementation
  • I have created an IBOutlet for imageView and make sure images is filled with full frame.

5) Implementation of Drag and Drop and other functionalities:

  • Make sure you have a swift file that is mapped to UICollectionViewController. I have named it PuzzleCollectionViewController.Swift in this project.
  • Let’s create a array of puzzle which contains title and solved image name of three different puzzle using Puzzle model class.
  • Add necessary variables to use them in this Puzzle game.
var puzzle = [Puzzle(title: “Pikachu”, solvedImages: [“pikachuLeftFace”,”pikachuRightFace”,”pikachuLeftBody”,”pikachuRightBody”]),Puzzle(title: “Charuzard”, solvedImages: [“charuzardLeftWing”,”charuzardNeck”,”charuzardRightWing”,”charuzardFace”,”charuzardBody”,”charuzardRightHand”,”charuzardTailEnd”,”charuzardTail”,”charuzardRightLeg”]), Puzzle(title: “Soldier”, solvedImages:[“soldier1”,”soldier2",”soldier3",”soldier4",”soldier5",”soldier6",”soldier7",”soldier8",”soldier9",”soldier10",”soldier11",”soldier12",”soldier13",”soldier14",”soldier15",”soldier16"])]var index: Int = 0
var gameTimer: Timer?
var hintImage = UIImageView()

Add Drag and Drop Delegate to collectionView in viewDidLoad()

override func viewDidLoad() {
super.viewDidLoad()
addLeftandRightBarButton()
collectionView.dragInteractionEnabled = true
collectionView.dragDelegate = self
collectionView.dropDelegate = self
}
  • dragInteractionEnabled — this property should set to true to enable drag interaction to collectionView.

Add Left and Right bar button to navigation Item:

func addLeftandRightBarButton() {
let hintBarbutton = UIBarButtonItem(title: “Hint”, style: .plain, target: self, action: #selector(showHintImage))
navigationItem.leftBarButtonItem = hintBarbutton

let nextButtonItem = UIBarButtonItem(title: “Next”, style: .plain, target: self, action: #selector(moveToNextPuzzle))
navigationItem.rightBarButtonItem = nextButtonItem
navigationItem.rightBarButtonItem?.isEnabled = false
}
  • LeftBarButton(Hint) — Hint button helps user to view full image of the puzzle.
  • RightBarButton(Next) — Next button helps user navigate to next puzzle if current puzzle is solved.Next button will be only enabled if user solves the current puzzle.

To implement Show Hint image functionality:

@objc func showHintImage() {
hintImage.image = UIImage(named: self.puzzle[index].title)
hintImage.backgroundColor = .white
hintImage.contentMode = .scaleAspectFit
hintImage.frame = self.view.frame
self.view.addSubview(hintImage)
self.collectionView.isHidden = true
self.view.bringSubviewToFront(hintImage)
gameTimer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(removeHintImage), userInfo: nil, repeats: false)
}
@objc func removeHintImage() {
self.view.sendSubviewToBack(hintImage)
self.collectionView.isHidden = false
navigationItem.leftBarButtonItem?.isEnabled = false
}
  • When user click leftbarButton(Hint) button it will call showHintImage()
  • In this function, I have created a UIImageView in code and assigned a full image of the puzzle which is to be solved.
  • I used the frame of view to make sure imageView fill the whole view and I also used Timer to make Hint go away in TimeInterval of 2 seconds.
  • After 2 seconds , Timer will call removeHintImage() which will make imageView send to back and make leftbarbutton(Hint) hidden to make sure user can’t use them second time.

To navigate to Next puzzle:

@objc func moveToNextPuzzle() {
index += 1
self.collectionView.reloadData()
self.collectionView.dragInteractionEnabled = true
navigationItem.rightBarButtonItem?.isEnabled = false
navigationItem.leftBarButtonItem?.isEnabled = true
}
  • When moving to next puzzle, I increment index to get Hint image as next puzzle image.
  • I have set dragInteractionEnabled to true to make sure puzzles get dragged in order to solve them.
  • Make rightBarButtonItem enabled functionality to false to make sure user can’t navigate to next puzzle without solving them.
  • Make leftBarButtonItem enabled functionality to true to make sure user can use Hint button to see the full image of the puzzle to solve.

Implementation of Collection View Delegate methods:

override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}

override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if index < puzzle.count {
return puzzle[index].unsolvedImages.count
} else {
return 0
}
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: “cell”, for: indexPath) as! ImageCollectionViewCell
cell.puzzleImage.image = UIImage(named: puzzle[index].unsolvedImages[indexPath.item])
return cell
}
  • numberOfSections — this method shows how many section need to implement in collectionView which is 1.
  • numberOfItemsInSection — this method tells how many items is needed in a section which can be easily determined based on how many elements in unsolvedImages arrray
  • cellForItemAt this methods tells what to implement in each cell of the collection view which is image in unsolvedImages array.

Impementation of UICollectionViewDelegateFlowLayout Methods:

extension PuzzleCollectionViewController : UICollectionViewDelegateFlowLayout {

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if puzzle[index].title == “Soldier” {
return UIEdgeInsets(top: 40, left: 16, bottom: 40, right: 16)
} else if puzzle[index].title == “Charuzard” {
return UIEdgeInsets(top: 40, left: 15, bottom: 40, right: 15)
} else {
return UIEdgeInsets(top: 40, left: 10, bottom: 40, right: 10)
}
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let collectionViewWidth = collectionView.bounds.width
var customCollectionWidth: CGFloat!
if puzzle[index].title == “Soldier” {
customCollectionWidth = collectionViewWidth/4–8
} else if puzzle[index].title == “Charuzard” {
customCollectionWidth = collectionViewWidth/3–10
} else {
customCollectionWidth = collectionViewWidth/2–10
}
return CGSize(width: customCollectionWidth, height: customCollectionWidth)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
  • insetForSectionAt — this method implements UIEdgeInsets for different size of puzzle
  • sizeForItemAt — this method make collection view give diffrent type and size for each puzzle.For example , first puzzle only have 4 unsolved images , second image has 9 unsolved images and third have 16 unsolved images.It will make sure every type is aligned properly in collectionView.
  • minimumInteritemSpacingForSectionAt — this method will provide zero space between every items in collection view.
  • minimumLineSpacingForSectionAt — this method will provide zero space between every line space for section in collection view.

Implementation of UICollectionViewDragDelegate Methods:

    extension PuzzleCollectionViewController:  UICollectionViewDragDelegate {

func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let item = self.puzzle[index].unsolvedImages[indexPath.item]
let itemProvider = NSItemProvider(object: item as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = dragItem
return [dragItem]
}
}
  • itemsForBeginning As you can see, in the ‘itemsForBeginning’ method we are calling our ‘dragItem’ that will deal with the dragItem to be returned. The helper method ‘dragItem’ helps us create the itemProvider with the correct string or image content, depending on the cell we are dragging.

Implementation of UICollectionViewDropDelegate Methods:

     extension PuzzleCollectionViewController:  UICollectionViewDropDelegate {     func collectionView(_ collectionView: UICollectionView,   dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
if collectionView.hasActiveDrag {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
return UICollectionViewDropProposal(operation: .forbidden)
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {

var destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
let row = collectionView.numberOfItems(inSection: 0)
destinationIndexPath = IndexPath(item: row — 1, section: 0)
}

if coordinator.proposal.operation == .move {
self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
self.collectionView.reloadData()
}
}

fileprivate func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath:IndexPath, collectionView: UICollectionView) {
if let item = coordinator.items.first,
let sourceIndexPath = item.sourceIndexPath {

collectionView.performBatchUpdates({
puzzle[index].unsolvedImages.swapAt(sourceIndexPath.item, destinationIndexPath.item)
collectionView.reloadItems(at: [sourceIndexPath,destinationIndexPath])
}, completion: nil)
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
}
}

func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession) {
if puzzle[index].unsolvedImages == puzzle[index].solvedImages {
Alert.showSolvedPuzzleAlert(on: self)
collectionView.dragInteractionEnabled = false
if index == puzzle.count — 1 {
navigationItem.rightBarButtonItem?.isEnabled = false
} else {
navigationItem.rightBarButtonItem?.isEnabled = true
}
}
}
}
  • dropSessionDidUpdate This method tell the DropDelegate that something is happening when the user drags a cell and drops it at a new location.UICollectionViewDropProposal has four types of operation which is copy,forbidden,cancel,move.In this project I have used move operation which is to arrange the puzzles.
  • performDropWith Use this method to accept the dropped content and integrate it into your collection view. In your implementation, iterate over the items property of the coordinator object and fetch the data from each UIDragItem.
  • Incorporate the data into your collection view's data source and update the collection view itself by inserting any needed items. When incorporating items, use the methods of the coordinator object to animate the transition from the drag item's preview to the corresponding item in your collection view.We have calculating whether or not the cell will be dropped out of bounds.

We need to know these parameters:

  1. coordinator of type UICollectionViewDropCoordinator — tells what drop Operation to do.
  2. destinationIndexPath of type IndexPath — Where drop operation is done
  3. collectionView of type UICollectionView — Our collection view, we need this to perform batch updates, delete the dragged item from its original location, and insert it into the new location.

To implement the reorder items method. I’ve placed this method right below my performDropWith method, and marked it fileprivate.

  • dropSessionDidEnd this method is called every time a item is dropped and checks everytime if puzzle is solved or not.If unsolvedImage array equals to solved image array. Change dragInteractionEnabled to false to tell user that puzzle has solved and show Alert .Enable rightBarbutton to true to navigate to next puzzle.

Display Alert after Puzzle is Solved:

Alert.Swift File to Display alert after puzzle has solved.
  • After implementing the above code in Alert.Swift File , to display alert, you just need to use the below code:
Alert.showSolvedPuzzleAlert(on: self)
  • I have already written a brief article about how to use UIAlertController in Swift.Click this link if want to check that out.

6) Conclusion:

Now build and run the project where you will implemented drag and drop , Timer and Hint images will work perfectly.If you have any doubts this project you can refer this project in my github link.

This project is updated for Xcode 10.2 and Swift 5.0

I hope you found this article helpful. If you did, please don’t hesitate to clap or share this post on Twitter or your social media of choice, every share helps me to write more. If you have any queries, feel free to comment below and I’ll see what I can do.Thanks.

Let’s connect!

You can find me on Twitter | LinkedIn | GitHub

--

--

Shankar Madeshvaran
Developer in Making

iOS and Web/React Js Developer. I write articles regarding Web, iOS ,React.Js concepts. Subscribe for more: https://shankarmadeshvaran.hashnode.dev