I am participating in the Mid-Autumn Festival Creative Submission contest, please see: Mid-Autumn Festival Creative Submission Contest for details

preface

Today we will use Swift to develop a jigsaw puzzle game to put together a Mid-Autumn Full moon. For the picture material, we will use the cover of the Mid-Autumn Festival Creative Submission Contest (you can also change it to what you want).

Take a look at the effect first:

Before we start, let’s extend the UIImage method to crop an image

extension UIImage { func clip(_ rect: CGRect) -> UIImage { if let cgImage = cgImage? .cropping(to: rect) { return UIImage(cgImage: cgImage) } return self } }Copy the code

Create a newGameViewclass

  • inGameViewThree attributes are exposed inside
Var numberOfItems: Int = 3 var image: UIImage?Copy the code
  • According to thenumberOfItemsCalculate the total number of cells
Private var numberOfGrids: Int {get {return numberOfItems * numberOfItems}} private var numberOfGrids: Int {get {return numberOfItems * numberOfItems}}Copy the code
  • inGameViewAdd one to itreloadViewMethod, according tonumberOfItemsandimageTo update the layout

The loop that calculates the width and height of each item based on numberOfItems and Image creates a UIImageView that calls the CLIP method of the UIImage extension to crop the image

The following code

func reloadView() { guard let image = self.image else { return } for item in subviews { item.removeFromSuperview() } /// Let imageWidth = image.size. Width/CGFloat(numberOfItems) /// Let imageHeight = image.size image.size.height / CGFloat(numberOfItems) for index in 0.. <numberOfGrids { let x = index % numberOfItems let y = index / numberOfItems let width = (bounds.size.width - CGFloat(numberOfItems - 1) * spacing) / CGFloat(numberOfItems) let height = (bounds.size.height - CGFloat(numberOfItems - 1) * spacing) / CGFloat(numberOfItems) let imageView = UIImageView() imageView.frame = CGRect(x: CGFloat(x)*width + (spacing*CGFloat(x)), y: CGFloat(y)*height + (spacing*CGFloat(y)), width: width, height: height) imageView.image = image.clip(CGRect(x: CGFloat(x)*imageWidth, y: CGFloat(y)*imageHeight, width: imageWidth, height: imageHeight)) imageView.isUserInteractionEnabled = true addSubview(imageView) } }Copy the code

Now let’s take a look at the clipping effect, initialize the GameView

let image = UIImage(named: "02")! Let imageHeight = (image.size. Height * (view.bounds.size. Width -20))/image.size. Width // Let imageView1 = UIImageView(frame: CGRect(x: 10, y: 80, width: view.bounds.size. Width-20, height: imageHeight)) imageView1.image = image let imageView2 = GameView(frame: CGRect(x: 10, y: 400, width: view.bounds.size.width - 20, height: imageHeight)) imageView2.image = image imageView2.reloadView() view.addSubview(imageView1) view.addSubview(imageView2)Copy the code

The clipping effect is done, now we need to slide and disarrange the image order.

  • To do these two functions, we need to build oneGridModelThe model is used to store eachUIImageViewAnd its markings
Var originIndex: Int = 0 var originIndex: Int = 0 Int = 0 var imageView = UIImageView() init(originIndex: Int, index: Int, imageView: UIImageView) { self.originIndex = originIndex self.index = index self.imageView = imageView } }Copy the code
  • inGameViewAdd one to itimageViewsAn array of
Private var imageViews = [GridModel]()Copy the code
  • Inside the for loop, put eachimageViewStored in theimageViewsIn the
let model = GridModel(originIndex: index, index: 0, imageView: imageView)
imageViews.append(model)
Copy the code

Shuffle the pictures

  • We first giveArrayExtending arandommethods
extension Array { @discardableResult mutating func random() -> Array { for index in 0.. <count { let newIndex = Int(arc4random_uniform(UInt32(count))) if index ! = newIndex { swapAt(index, newIndex) } } return self } }Copy the code
  • thenimageViewsShuffle the order and do the for loopimageViews, set eachimageViewtheframeandindex
private func randomImageView() {
    imageViews.random()

    let width = (bounds.size.width - CGFloat(numberOfItems - 1) * spacing) / CGFloat(numberOfItems)
    let height = (bounds.size.height - CGFloat(numberOfItems - 1) * spacing) / CGFloat(numberOfItems)

    for (index, item) in imageViews.enumerated() {
        let x = index % numberOfItems
        let y = index / numberOfItems
        item.imageView.frame = CGRect(x: CGFloat(x)*width + (spacing*CGFloat(x)), y: CGFloat(y)*height + (spacing*CGFloat(y)), width: width, height: height)
        item.index = index
    }
}
Copy the code

Photo slide

  • Here we useUISwipeGestureRecognizerGestures are given to each in the for loopimageViewaddUp, down, left, rightThe gesture
Let upSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction(_:))) upSwipe.direction = .up imageView.addGestureRecognizer(upSwipe) let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction(_:))) leftSwipe.direction = .left imageView.addGestureRecognizer(leftSwipe) let downSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction(_:))) downSwipe.direction = .down imageView.addGestureRecognizer(downSwipe) let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeAction(_:))) rightSwipe.direction = .right imageView.addGestureRecognizer(rightSwipe)Copy the code
  • Before sliding

When sliding, we have to deal with the boundary problem. When this happens, we should not let it slide: 1. When the sliding imageView is on the upper boundary and slides up 2. When the sliding imageView is on the left boundary and slides to the left 3

So let’s define a GameBorder enumeration to determine where the current sliding imageView is

/// Boundary direction enum GameBorder {case unknown case up case left case Down case right gridBorder(_ index: Int) -> GameBorder { if index < numberOfItems { return .up } if index % numberOfItems == 0 { return .left } if index >= numberOfGrids - numberOfItems { return .down } if (index + 1) % numberOfItems == 0 { return .right } return .unknown }Copy the code
  • Sliding handle
@objc private func swipeAction(_ recognizer: UISwipeGestureRecognizer) {/// Get lightly scanned UIImageView guard let swipeView = recognizer.view else {return} Guard let OldModel = imageviews. filter({$0. ImageView == swipeView}). First else {return} /// Scan UIImageView tag let oldIndex = Oldmodel.index UIImageView tag var newIndex: Recognizer.direction ==. Up {return} // Recognizer.direction ==. Up {return} // Recognizer.direction ==. Recogndborder (oldIndex) ==. Left, recognizer.direction ==. Left {return} /// recognizer.direction ==. Left {return} Recogndborder (oldIndex) ==.down, recognizer.direction ==.down {return} /// Recognizer.direction ==.down {return} If gridBorder(oldIndex) ==. Right, recognizer.direction == .right { return } switch recognizer.direction { case .up: newIndex = oldIndex - numberOfItems case .left: newIndex = oldIndex - 1 case .down: newIndex = oldIndex + numberOfItems case .right: newIndex = oldIndex + 1 default: Uiimageviews.filter ({$0.index == newIndex}).first else {return} let oldFrame = oldModel.imageView.frame let oldTag = oldModel.index let newFrame = newModel.imageView.frame let newTag =  newModel.index UIView.animate(withDuration: Frame = newFrame oldModel.index = newTag newModel.imageView.frame = oldFrame newModel.index = oldTag } completion: {finish in exchanging the positions of the imageViews inside / / / let oldIndex = self. The imageViews. FirstIndex {$0. ImageView = = oldModel. ImageView}?? 0 let newIndex = self.imageViews.firstIndex { $0.imageView == newModel.imageView } ?? 0 self.imageViews.swapAt(oldIndex , NewIndex) self.com selfhandler ()}} private func completeHandler() {let indexs = imageviews.map { $0. OriginIndex} if indexs == originIndexs {debugPrint(" game finished ")}}Copy the code

So I’m done here, the demo address