preface

In the previous article, we completed the user action logic for the jigsaw element splitting and basic drag and drop. Now let’s complete the logic when the user drags and drops puzzle elements.

In real life, jigsaw puzzles are always “locked” on a certain canvas, where players can only use their imagination to restore the puzzle. Therefore, we also need to define a “region” on the canvas for the user.

From the previous two articles, we know that the jigsaw elements in the “Li Jin Jigsaw” can only be manipulated on the left side of the canvas, not beyond the scope of the screen. So we need to make a constraint on the puzzle elements.

Limit the puzzle

In order to better see the boundary of the elements, we first add “boundaries” to the puzzle elements. Supplement Puzzle

extension Puzzle {
    @objc
    fileprivate func pan(_ panGesture: UIPanGestureRecognizer) {
        switch panGesture.state {
        case .began:
            layer.borderColor = UIColor.white.cgColor
            layer.borderWidth = 1
        case .changed:
        case .ended:
            layer.borderWidth = 0
        default: break
        }

        let translation = panGesture.translation(in: superview)
        center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
        panGesture.setTranslation(.zero, in: superview)
    }
}
Copy the code

The idea of adding boundaries is relatively simple. Our purpose is to allow users to have a better boundary control over puzzle elements during the process of dragging and dropping them. Run the project, drag and drop the puzzle element, the boundary of the puzzle element has been added!

! [Puzzle element boundary]](i.loli.net/2019/09/08/…)

Limiting the movable positions of Puzzle elements can be done in Puzzle’s drag-and-drop gesture callback method for boundary confirmation. Let’s start by “preventing” puzzle elements from crossing the middle line of the canvas.

extension Puzzle {
    @objc
    fileprivate func pan(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: superview)
        
        let newRightPoint = centerX + width / 2
        
        switch panGesture.state {
        case .began:
            layer.borderColor = UIColor.white.cgColor
            layer.borderWidth = 1
        case .changed:
            if newRightPoint > superview!.width / 2 {
                right = superview!.width / 2
            }
        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

In the jigsaw element drag-and-drop callback method, in the.change judgment of the gesture state enumeration value, based on the “rightmost” position of the current jigsaw element, X + self.frame.size. Width / 2 against the middle position of the superview to determine whether the jigsaw element is out of bounds.

Run the project! We can no longer drag puzzle elements onto the canvas on the right

Maintaining state

After the explanation of “Can YOU turn off the lights” in the last game, we have roughly understood how to maintain the game logic through state. For a puzzle game, whether the puzzle elements can be restored in a certain order determines whether the game will win or not.

“Li Jin puzzles” it’s still a 2 d game, careful, you must also find the game and essentially “can pass a light” this game is the same, we can divide the game canvas according to certain rules of cutting out, and through a two-dimensional mapping list and cutting completed puzzle elements do, every time a user after the drag-and-drop behavior of jigsaw puzzle elements, To trigger a status update. Finally, we determine whether the player has won the current game based on the state after each update.

State to create

Our Puzzle class represents the Puzzle elements themselves, and the winning condition of a Puzzle game is that we put the Puzzle elements back together in a certain order, especially in a certain order. We can do this by attaching tags to puzzle objects to identify each piece of the puzzle.

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
                / / add the tag
                puzzle.tag = (itemY * itemHCount) + itemX
                print(puzzle.tag)
                
                puzzles.append(puzzle)
                view.addSubview(puzzle)
            }
        }
    }
}
Copy the code

The viewController.swift file housed all the Puzzle instance objects that had been cut, and if we were maintaining the state of the game, required a contentPuzzles that managed the Puzzle elements that the user dragged onto the canvas. A match can only be won if the puzzle elements located on the canvas are placed on the canvas in a certain order.

To complete the logic expressed above, let’s start with the element diagram. Provide a “function bar” on the canvas that allows the user to drag and drop puzzle elements onto the canvas to complete the previously completed element drawing process.

The function bar

The function bar is used to host all the jigsaw pieces, adding relevant code in viewController.swift:

class ViewController: UIViewController {

    // ...        
    let bottomView = UIView(frame: CGRect(x: 0, y: view.height, width: view.width, height: 64 + bottomSafeAreaHeight))
    bottomView.backgroundColor = .white
    view.addSubview(bottomView)
    
    UIView.animate(withDuration: 0.25, delay: 0.5, options: .curveEaseIn, animations: {
        bottomView.bottom = self.view.height
    })
}
Copy the code

Run the project, the bottom function bar with animation, the effect is good ~

In order to better handle the functions carried in the bottom function bar, we need to wrap the bottom function bar and create a new class LiBottomView.

class LiBottomView: UIView {}Copy the code

Now we are going to put the puzzle elements in column “layout” to function, using UICollectionView long shop layout, also need to create a LiBottomCollectionView and LiBottomCollectionViewCell.

In the horizontal layout LiBottomCollectionView, we don’t have too many animation requirements, so it is relatively simple to implement.

class LiBottomCollectionView: UICollectionView {

    let cellIdentifier = "PJLineCollectionViewCell"
    var viewModels = [Puzzle] ()override init(frame: CGRect.collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        initView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    private func initView(a) {
        backgroundColor = .clear
        showsHorizontalScrollIndicator = false
        isPagingEnabled = true
        dataSource = self
        
        register(LiBottomCollectionViewCell.self, forCellWithReuseIdentifier: "LiBottomCollectionViewCell")}}extension LiBottomCollectionView: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView.numberOfItemsInSection section: Int) -> Int {
        return viewModels.count
    }
    
    func collectionView(_ collectionView: UICollectionView.cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LiBottomCollectionViewCell", for: indexPath) as! LiBottomCollectionViewCell
        cell.viewModel = viewModels[indexPath.row]
        return cell
    }
}
Copy the code

In the new LiBottomCollectionViewCell added code.

class LiBottomCollectionViewCell: UICollectionViewCell {
    var img = UIImageView(a)var viewModel: Puzzle? {
        didSet { setViewModel() }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        layer.borderWidth = 1
        layer.borderColor = UIColor.darkGray.cgColor
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowRadius = 10
        layer.shadowOffset = CGSize.zero
        layer.shadowOpacity = 1
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")}private func setViewModel(a) {
        img.contentMode = .scaleAspectFit
        img.image = viewModel?.image
        img.frame = CGRect(x: 0, y: 0, width: width, height: height)
        if !subviews.contains(img) {
            addSubview(img)
        }
    }
}
Copy the code

Running the project, you can see that we have all the puzzle elements laid out in the function bar at the bottom

Function bar Figure above

When we drag a jigsaw element from the function bar onto the canvas, the jigsaw element originally located on the function bar needs to be removed. Let’s remove the jigsaw element from the function bar first.

The puzzle element data source on the function bar at the bottom comes from the viewModels of LiBottomCollectionView. When the data source is assigned, the reloadDate() method is called to refresh the page, so we only need to remove the puzzle element in the data source through a “method”.

Add the long-press gesture to the Cell in the function bar at the bottom. In the callback method of the long-press gesture recognizer, transfer the data source of the current Cell, delete the main data source through LiBottomCollectionView operation, and then execute the reloadData() method.

class LiBottomCollectionViewCell: UICollectionViewCell {
    var longTapBegan: ((Int) - > ())?
    var longTapChange: ((CGPoint) - > ())?
    var longTapEnded: ((Int) - > ())?
    var index: Int?

    // ...

    override init(frame: CGRect) {
        // ...        
        
        let longTapGesture = UILongPressGestureRecognizer(target: self, action: .longTap)
        addGestureRecognizer(longTapGesture)
    }

    // ...
}

extension LiBottomCollectionViewCell {
    @objc
    fileprivate func longTap(_ longTapGesture: UILongPressGestureRecognizer) {
        guard let index = index else { return }
        
        switch longTapGesture.state {
        case .began:
            longTapBegan?(index)
        case .changed:
            let translation = longTapGesture.location(in: superview)
            let point = CGPoint(x: translation.x, y: translation.y)
            longTapChange?(point)
        case .ended:
            longTapEnded?(index)
        default: break}}}Copy the code

In the Cell’s long press gesture recognizer callback, we processed the gesture states.began,.changed, and.ended. In the. Began gesture state, pass the index of the current Cell to the superview through the longTapBegan() closure, and in the. Changed gesture state, The longTapChange() closure converts the coordinates of user operations on the current view to the same coordinates as the parent view, and the index of the current view is also passed in.ended methods.

In the superview LiBottomCollectionView, modify the cellForRow method:

// ...


extension LiBottomCollectionView: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView.cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LiBottomCollectionViewCell", for: indexPath) as! LiBottomCollectionViewCell
        cell.viewModel = viewModels[indexPath.row]
        cell.index = 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)
        }
        cell.longTapChange = {
            self.longTapChange?($0)
        }
        cell.longTapEnded = {
            self.longTapEnded?(self.viewModels[$0])
            self.viewModels.remove(at: $0)
            self.reloadData()
        }
        
        return cell
    }
}
Copy the code

In the cellForRow method, a view removal operation is performed on the cell’s closure, longTapEnded(), to remove the puzzle element from the bottom function bar when the user makes a gesture on the puzzle element in the bottom function bar and then “releases” the long-press gesture. The closure of the remaining two cells is passed to the corresponding superview through the collectionView.

Perform the image above operations on the puzzle elements at the LiBottomView level.

class LiBottomView: UIView {
    // ...
    
    private func initView(a) {
        // ...        
    
        collectionView!.longTapBegan = {
            let center = The $1
            let tempPuzzle = Puzzle(size: $0.frame.size, isCopy: false)
            tempPuzzle.image = $0.image
            tempPuzzle.center = center
            tempPuzzle.y + = self.top
            self.tempPuzzle = tempPuzzle
            
            self.superview!.addSubview(tempPuzzle)
        }
        collectionView!.longTapChange = {
            guard let tempPuzzle = self.tempPuzzle else { return }
            tempPuzzle.center = CGPoint(x: $0.x, y: $0.y + self.top)
        }
    }
}
Copy the code

Create a new Puzzle element in the longTapBegan() method. Note that you can’t just use the Puzzle object that was passed, or you’ll end up with some weird questions later because of references.

In longTapChange () method of maintenance triggered by in LiBottomCollectionViewCell long press to bring up a CGPoint gesture events. In the section above from the bottom bar, it’s easy to think of attaching a UIPanGesture drag gesture to a longTapBegan() new Puzzle object, but if you think about it, UILongPressGestureRecognizer is inherited from UIGestureRecognizer class, UIGestureRecognizer maintain a set of associated with the user gestures recognition process, whether it’s light sweep, drag and drop or long press, Essence is through at a certain time interval judgment user gestures of moving distance and trend which is to determine the specific gestures type, so we use UILongPressGestureRecognizer directly.

Run the project ~ select a puzzle you like in the bottom function bar, and long press it! Activate the gesture and slowly drag it onto the brush to feel it

Afterword.

In this article, we mainly focus on the logical realization of the bottom function bar, making the bottom function bar have the function of preliminary “function”, and improving the requirement of “element diagram” in the last article to make it more complete. 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…