preface

In the last article, we completed the UI and logic for the bottom feature bar of the “Li Jin Jigsaw” game, and could also “drag and drop” puzzle elements from the bottom feature bar onto the canvas. Now, we need to fill in the boundaries of the complete puzzle elements.

Add complete puzzle elements to define boundaries

Through the interpretation of previous articles than we have been very clear on the rules of the game, also know that puzzle elements can only move in the canvas, but in the last article, we only in the canvas on the left side of the jigsaw puzzle elements do not let the “across the” median line limit, and loaded into the game only when jigsaw puzzle elements successfully executed judgment when the canvas.

What we want to achieve is that the jigsaw elements need to be qualified for other positions on the canvas when they are dragged out of the bottom function bar, rather than “staying” on the canvas and performing boundary judgments when the user drags them.

Let’s start by completing the boundary limits of puzzle elements that remain on the game canvas while the user continues to drag them.

extension Puzzle {
    @objc
    fileprivate func pan(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: superview)
        
        switch panGesture.state {
        case .began:
            layer.borderColor = UIColor.white.cgColor
            layer.borderWidth = 1
        case .changed:
            if right > rightPoint {
                right = rightPoint
            }
            if left < leftaPoint {
                left = leftaPoint
            }
            if top < topPoint {
                top = topPoint
            }
            if bottom > bottomPoint {
                bottom = bottomPoint
            }
            
        case .ended:
            layer.borderWidth = 0
        default: break
        }
        
        center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
        panGesture.setTranslation(.zero, in: superview)
    }
}
Copy the code

Several boundary variable values are dynamically modified according to the value of the isCopy variable of the puzzle element.

class Puzzle: UIImageView {

    /// whether to "copy" puzzle elements
    private var isCopy = false
    private var rightPoint: CGFloat = 0
    private var leftaPoint: CGFloat = 0
    private var topPoint: CGFloat = 0
    private var bottomPoint: CGFloat = 0
    
    / /...
    
    func updateEdge(a) {
        if superview ! = nil {
            if !isCopy {
                topPoint = topSafeAreaHeight
                bottomPoint = superview!.bottom - bottomSafeAreaHeight
                rightPoint = superview!.width / 2
                leftaPoint = 0}}else {
            if superview ! = nil {
                topPoint = superview!.top
                bottomPoint = superview!.bottom
                rightPoint = superview!.width
                leftaPoint = superview!.width / 2}}}}Copy the code

When the Puzzle object instantiation is added subview to another parent view, we can call updateEdge to update the boundary value of the Puzzle element strongly associated with the parent view. When the user drags an element from the bottom function bar onto the canvas, we know from the code in the previous article that it actually adds a pushdown gesture to the CollectionViewCell, which passes the three states of the gesture to the superview for processing.

The superview processing logic associated with CollectionViewCell is changed to:

class LiBottomView: UIView {
    / /...
    
    private var rightPoint: CGFloat = 0
    private var leftaPoint: CGFloat = 0
    private var topPoint: CGFloat = 0
    private var bottomPoint: CGFloat = 0
   
    / /...
    
    private func initView(a) {
        / /...
       
        collectionView!.longTapChange = {
            guard let tempPuzzle = self.tempPuzzle else { return }
            tempPuzzle.center = CGPoint(x: $0.x, y: $0.y + self.top)

            if tempPuzzle.right > self.rightPoint {
                tempPuzzle.right = self.rightPoint
            }
            if tempPuzzle.left < self.leftaPoint {
                tempPuzzle.left = self.leftaPoint
            }
            if tempPuzzle.top < self.topPoint {
                tempPuzzle.top = self.topPoint
            }
            if tempPuzzle.bottom > self.bottomPoint {
                tempPuzzle.bottom = self.bottomPoint
            }
        }
        collectionView!.longTapEnded = {
            self.moveEnd?($0)}}}Copy the code

In moving the puzzle element that the long press gesture adds to the screen view, we also qualify the value passed by the current callback in the gesture changed state callback handling method. Running the project, I found that the jigsaw element dragged from the function bar already has a boundary

Maintaining state

Randomize the bottom function bar

In order to maintain the current state of “Lijin Jigsaw” game, we need to associate and manage the content on the current game canvas with a certain data source. Before carrying out this part of the work, let’s put in to disrupt function puzzle of bar element position, otherwise we are not necessary for state maintenance, directly from the function bar at the bottom of the first drag and drop to the element to the canvas until the final bar is located in the function of the last puzzle elements, the game is finished, this is a problem.

To disrupt the layout of elements in the function bar at the bottom, we need to start with the function bar’s data source.

class ViewController: UIViewController {
    
    override func viewDidLoad(a) {
        / /...
        
        for itemY in 0..<itemVCount {
            for itemX in 0..<itemHCount {
                let x = itemW * itemX
                let y = itemW * itemY
                
                let img = contentImageView.image!.image(with: CGRect(x: x, y: y, width: itemW, height: itemW))
                let puzzle = Puzzle(size: CGSize(width: itemW, height: itemW), isCopy: false)
                puzzle.image = img
                puzzle.tag = (itemY * itemHCount) + itemX
                puzzles.append(puzzle)
            }
        }
        
        / / randomization
        for i in 1..<puzzles.count {
            let index = Int(arc4random()) % i
            if index ! = i {
                puzzles.swapAt(i, index)
            }
        }
    }
}
Copy the code

When the puzzle elements are generated, a simple exchange is performed on the data sources of the puzzle elements. It’s a little redundant to randomize in this way, so you can optimize this code.

Fixed two bugs

If you are careful, you should be able to get a hint from the giFs in the previous post. When we “long press” and “drag” the jigsaw puzzle element from the bottom bar, we will find that the jigsaw puzzle element from the bottom bar does not match the deleted jigsaw puzzle element.

The puzzle element in the figure above is incorrect because we directly treated the index index representing the “position” of the puzzle element as the position index of the puzzle element Cell in the CollectionView, which is used for remove operation. Therefore, we also need to add a gameIndex gameIndex to the puzzle element Cell, which represents its position index in the game, and use cellIndex to represent its position index in the function bar CollectionView. The modified LiBottomCollectionViewCell code is as follows:

class LiBottomCollectionViewCell: UICollectionViewCell {
    // ...

    var cellIndex: Int?
    var gameIndex: Int?

    // ...
}

// ...

extension LiBottomCollectionViewCell {
    @objc
    fileprivate func longTap(_ longTapGesture: UILongPressGestureRecognizer) {
        guard let cellIndex = cellIndex else { return }
        
        switch longTapGesture.state {
        case .began:
            longTapBegan?(cellIndex)
        case .changed:
            var translation = longTapGesture.location(in: superview)
            
            let itemCount = 5
            if cellIndex > itemCount {
                translation.x = translation.x - CGFloat(cellIndex / itemCount * Int(screenWidth))
            }
            
            let point = CGPoint(x: translation.x, y: translation.y)
            longTapChange?(point)
        case .ended:
            longTapEnded?(cellIndex)
        default: break}}}// ...
Copy the code

While fixing this bug, I also found that when the user slides the function bar to the next page, the puzzle elements in the picture above cannot move. After repeated confirmation, in fact, this problem will occur as long as the puzzle elements in the function bar are not the first page.

In LiBottomCollectionViewCell long press callback event to print out the change of the x coordinates, found that the first page of the elements above x coordinate transformation after the contrast is and function bar pages for contrast, sliding to the first page, will add sliding across the width of each page, as a result, The idea is to figure out how many pages the current user has slid across, multiply this by the width of each page, and subtract it from the current converted X coordinate of the puzzle element.

Fix the second bug. Jigsaw elements The element removed from the function bar after the image above is inconsistent with the element in the image above. After checking for a while, I found that the problem was actually caused by the fact that the previous comment did not bring the corresponding logic, which resulted in more than one reloadData. The code of LiBottomCollectionView was modified as follows:

extension LiBottomCollectionView: UICollectionViewDataSource {
    // ...
    
    func collectionView(_ collectionView: UICollectionView.cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // ...

        cell.cellIndex = indexPath.row
        cell.gameIndex = viewModels[indexPath.row].tag
        cell.longTapBegan ={[weak self] index in
            guard let self = self else { return }
            guard self.viewModels.count ! = 0 else { return }
            self.longTapBegan?(self.viewModels[index], cell.center)
            // --------
            // There was a 'self.reloadData()'
        }

        // ...}}Copy the code

Running the project, we found that we have solved all the current bugs!

Tag element

For a puzzle game, at this time our basic logic function at the bottom of the column has been completed, but from a user perspective, this game is too difficult really, because I don’t know which puzzle elements should be placed where, we also need to provide users with a “tip”, used for placing order inform each puzzle elements.

class LiBottomCollectionViewCell: UICollectionViewCell {
    // ...

    override init(frame: CGRect) {
        super.init(frame: frame)

        // ...
        
        img.contentMode = .scaleAspectFit
        img.frame = CGRect(x: 0, y: 0, width: width, height: height)
        addSubview(img)

        
        tipLabel = UILabel(frame: CGRect(x: width - 10, y: top - 10, width: 17, height: 17))
        tipLabel.font = UIFont.systemFont(ofSize: 11)
        tipLabel.backgroundColor = UIColor.rgb(80.80.80)
        tipLabel.textColor = .white
        tipLabel.textAlignment = .center
        tipLabel.layer.cornerRadius = tipLabel.width / 2
        tipLabel.layer.masksToBounds = true
        addSubview(tipLabel)

        // ...
    }

    // ...
    
    private func setViewModel(a) {
        img.image = viewModel?.image
        tipLabel.text = "\(gameIndex!)"}}// ...
Copy the code

Running project, jigsaw element markers added!

About the image

Now that we’ve completed most of the logic on the left side of the game canvas, it’s time to supplement the logic on the right side of the game canvas. We need to create a puzzle element that is symmetrical to the image on the left side of the game canvas.

The puzzle elements on the left and the puzzle elements on the right should be in exact position and mirror – symmetric. In my WWDC19 scholarship application project, I adopted a lazy practice where the user had to place the puzzle element on the left side of the game canvas, trigger the end state event with the long press gesture, and then move the puzzle element to see the copy puzzle element on the right side of the game canvas. This approach can only be said to work, far from “elegant” things.

What we want to achieve is that when the user presses and selects a puzzle element in the bottom feature bar, the movement of the puzzle element in the area of the bottom feature bar will not trigger the generation of a copy of the puzzle element on the right side of the game canvas. Once the user moves up the area of the bottom feature bar, the copy of the puzzle element will appear. We need to generate a copy of the puzzle elements when the player selects them from the bottom feature bar.

To prevent copy Puzzle elements from being generated in awkward places on the game canvas, we made some changes to the initialization method of our Puzzle class.

class Puzzle: UIImageView {
    
    convenience init(size: CGSize.isCopy: Bool) {
        // Get out first
        self.init(frame: CGRect(x: -1000, y: -1000, width: size.width, height: size.height))
        self.isCopy = isCopy
        
        initView()
    }

    private func initView(a) {
        contentMode = .scaleAspectFit
        
        if !isCopy {
            // ...
        } else {
            // If it is a copy puzzle element, the mirror is flipped
            transform = CGAffineTransform(scaleX: -1, y: 1)}}}Copy the code

In the viewController.swift file, we need to declare a copy jigsaw element that temporarily matches the jigsaw element from the bottom function bar and moves with it. Wait until the user determines the position of the puzzle element in the picture above, trigger the event of long press gesture to end the state, then remove the copy puzzle element used to cooperate with the movement, and re-create a “confirmed” puzzle element on the right side of the game canvas.

class ViewController: UIViewController {
    // ...

    private var copyPuzzles = [Puzzle] ()// Copy puzzle elements for matching moves
    private var tempCopyPuzzle: Puzzle?

    // ...

    override func viewDidLoad(a) {
        // ...

        bottomView.moveBegin = {
            self.tempCopyPuzzle = Puzzle(size: $0.frame.size, isCopy: true)
            self.tempCopyPuzzle?.image = $0.image
            self.tempCopyPuzzle?.tag = $0.tag
            // Create a copy puzzle element when receiving the long press event called back from the bottom function bar
            self.view.addSubview(self.tempCopyPuzzle!)
        }
        
        bottomView.moveChanged = {
            guard let tempPuzzle = self.tempCopyPuzzle else { return }
            
            // Display it beyond the position of the bottom function bar
            if $0.y < self.bottomView.top {
                // Focus of computation
                tempPuzzle.center = CGPoint(x: self.view.width - $0.x, y: $0.y)
            }
        }
        
        bottomView.moveEnd = {
            guard let tempPuzzle = self.tempCopyPuzzle else { return }
            // Long press to complete, remove first
            tempPuzzle.removeFromSuperview()
            
            let copyPuzzle = Puzzle(size: $0.frame.size, isCopy: true)
            copyPuzzle.center = tempPuzzle.center
            copyPuzzle.image = tempPuzzle.image
            // Add the 'copy' puzzle element of 'confirm'
            self.view.addSubview(copyPuzzle)
            self.copyPuzzles.append(copyPuzzle)
        }
    }

    // ...
}
Copy the code

Running the project at this point, you’ll notice that the copy element can only be triggered when you pull the puzzle element above from the bottom function bar, but no longer when the long press is over. (From now on, the puzzle element on the left side of the game canvas is called leftPuzzle, The puzzle element on the right side of the game canvas is rightPuzzle. This is because leftPuzzle’s movement gesture is not passed to rightPuzzle, and we need to make a little change to the Puzzle class.

class Puzzle: UIImageView {
    var longTapChange: ((CGPoint) - > ())?
    
    // ...

    / / / mobile ` rightPuzzle `
    func copyPuzzleCenterChange(centerPoint: CGPoint) {
        if !isCopy { return }
        
        center = CGPoint(x: screenWidth - centerPoint.x, y: centerPoint.y)
    }
}

extension Puzzle {
    @objc
    fileprivate func pan(_ panGesture: UIPanGestureRecognizer) {
        // ...
        
        center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
        panGesture.setTranslation(.zero, in: superview)
        
        // Pass the position of the long press gesture
        longTapChange?(center)
    }
}
Copy the code

Change it in viewController.swift.

class ViewController: UIViewController {
    // ...

    override func viewDidLoad(a) {
         bottomView.moveBegin = { puzzle in
            // Migrate the time when 'leftPuzzle' is added to the game canvas to 'ViewController'
            self.view.addSubview(puzzle)
            self.leftPuzzles.append(puzzle)
            puzzle.updateEdge()
            
            // Search for 'rightPuzzle' equal to 'leftPuzzle' and pass in the moving distance
            puzzle.longTapChange = {
                for copyPuzzle in self.rightPuzzles {
                    if copyPuzzle.tag = = puzzle.tag {
                        copyPuzzle.copyPuzzleCenterChange(centerPoint: $0)}}}// ...
        }
        
        bottomView.moveChanged = {
            guard let tempPuzzle = self.tempCopyPuzzle else { return }
            
            // Display it beyond the position of the bottom function bar
            if $0.y < self.bottomView.top {
                // encapsulates the 'rightPuzzle' movement method
                tempPuzzle.copyPuzzleCenterChange(centerPoint: $0)
            }
            
        }
        
        bottomView.moveEnd = {
            // ...

            // Pass in the tag
            copyPuzzle.tag = tempPuzzle.tag

            // ...}}// ...
}
Copy the code

Running the project, we found that we can already mirror left and right!

Afterword.

In this article, we have perfected all the logic of the jigsaw elements from the function bar above at the bottom. In the next article, we will focus on the core gameplay logic of “Li Jin Jigsaw”. At present, we have completed the following requirements:

  • Jigsaw material preparation;
  • Element above;
  • State maintenance;
  • Elemental adsorption;
  • The UI perfect;
  • Winning logic;
  • Victory dynamic effect.

GitHub address: github.com/windstormey…