在上篇文章中,咱们完成了对拼图的元素拆分和基本拖拽的用户操做逻辑。如今咱们先来补充完整当用户拖拽拼图元素时的逻辑。git
在现实生活中,拼图游戏老是被「禁固」在一个肯定画布上,玩家只能在这个画布中发挥本身的想象力,恢复拼图。所以,咱们也须要在画布上给用户限定一个「区域」。github
从以前的两篇文章中,咱们知道了「黎锦拼图」中的拼图元素只能在画布的左部分进行操做,不能超出屏幕以外的范围进行操做。所以咱们须要对拼图元素作一个限定。swift
为了可以较好的看到元素的边界,咱们先给拼图元素加上「边界」。补充 Puzzle
里的markdown
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)
}
}
复制代码
加上边界的思路比较简单,咱们的目的是为了让用户在拖拽拼图元素的过程,对拼图元素可以有个比较好的边界把控。运行工程,拖拽拼图元素,拼图元素的边界已经加上啦!闭包
![拼图元素边界]](i.loli.net/2019/09/08/…)app
限定拼图元素的可移动位置,能够在 Puzzle
的拖拽手势的回调方法中进行边界确认。咱们先来「防止」拼图元素跨越画布的中间线。ide
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)
}
}
复制代码
在拼图元素的拖拽回调方法里,在手势 state
枚举值的 .change
判断里,根据当前拼图元素的「最右边」位置,也就是 self.frame.origin.x + self.frame.size.width / 2
与父视图中间位置的对比,来决定出是否该拼图元素是否越界。oop
运行工程!发现咱们不再能把拼图元素拖到右边画布里去啦~布局
通过上一个游戏「可否关个灯」的讲解,咱们已经大体了解了如何经过状态去维护游戏逻辑,对于一个拼图游戏来讲,可否把各个拼图元素按照必定的顺序给复原回去,决定游戏是否牲胜利。动画
「黎锦拼图」依然仍是个 2D 游戏,细心的你必定也会发现,这个游戏本质上与「可否关个灯」这个游戏是同样的,咱们均可以把游戏画布按照必定的划分规则切割出来,并经过一个二维列表与切割完成的拼图元素作映射,每次用户对拼图元素的拖拽行为结束后,都去触发一次状态的更新。最后,咱们根据每次更新完成后的状态去判断出玩家是否赢得了当前游戏。
咱们的 Puzzle
类表明着拼图元素自己,拼图游戏的胜利条件是咱们要把各个拼图元素按照必定顺序复原,重点在按照必定的顺序。咱们能够经过给 puzzle
对象设置 tag
来作到标识每一块拼图元素。
class ViewController: UIViewController {
override func viewDidLoad() {
// ......
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
// 添加 tag
puzzle.tag = (itemY * itemHCount) + itemX
print(puzzle.tag)
puzzles.append(puzzle)
view.addSubview(puzzle)
}
}
}
}
复制代码
在 ViewController.swift
文件中的 puzzles
是用于存放全部被切割完成后的 Puzzle
实例对象,若是咱们相对游戏的状态进行维护,还须要一个 contentPuzzles
用于管理被用户拖拽到画布上的拼图元素,只有当位于画布上的拼图元素按照必定顺序放置在画布上,才能赢得比赛。
为了完成以上所表达的逻辑,咱们先来把「元素下图」。在画布上提供一个「功能栏」,让用户从功能栏中拖拽出拼图元素到画布上,从而完成以前已经完成的元素上图过程。
功能栏的做用在于承载全部拼图,在 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
})
}
复制代码
运行工程,底部功能栏加上动画后,效果还不错~
为了可以较好的处理底部功能栏中所承载的功能,咱们须要对底部功能栏进行封装,建立一个新的类 LiBottomView
。
class LiBottomView: UIView {
}
复制代码
如今,咱们要把拼图元素都「布置」到功能栏上,采用 UICollectionView
长铺布局,也须要建立一个 LiBottomCollectionView
和 LiBottomCollectionViewCell
。
水平布局的 LiBottomCollectionView
中,咱们也没有过多的动画要求,所以实现起来较为简单。
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() {
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
}
}
复制代码
在新建的 LiBottomCollectionViewCell
补充代码。
class LiBottomCollectionViewCell: UICollectionViewCell {
var img = UIImageView()
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() {
img.contentMode = .scaleAspectFit
img.image = viewModel?.image
img.frame = CGRect(x: 0, y: 0, width: width, height: height)
if !subviews.contains(img) {
addSubview(img)
}
}
}
复制代码
运行工程~能够看到咱们的底部功能栏已经把拼图元素都布局好啦~
当咱们把一个拼图元素从功能栏中拖拽到画布上时,原先位于功能栏上的拼图元素须要被移除,咱们先来实现拼图元素在功能栏上的移除功能。
底部功能栏上的拼图元素数据源来自 LiBottomCollectionView
的 viewModels
,当该数据源被赋值时会调用 reloadDate()
方法刷新页面,所以咱们只须要经过某个「方法」移除在数据源中的拼图元素便可。
给底部功能栏的 Cell 添加上长按手势,在长按手势识别器的回调方法中,传递出当前 Cell 的数据源,经过 LiBottomCollectionView
操做主数据源进行删除,再执行 reloadData()
方法便可。
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
}
}
}
复制代码
在 Cell 的长按手势识别器回调方法中,咱们分别对手势的三个状态 .began
、.changed
和 .ended
进行了处理。在 .began
手势状态中,经过 longTapBegan()
闭包把当前 Cell 的索引传递出去给父视图,在 .changed
手势状态中,经过 longTapChange()
闭包把用户在当前视图上操做的坐标转化成与父视图一致的坐标,在 .ended
方法中一样把当前视图的索引传递出去。
在父视图 LiBottomCollectionView
中,修改 cellForRow
方法:
// ...
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
}
}
复制代码
在 cellForRow
方法中,对 cell 的闭包 longTapEnded()
执行了移除视图操做,达到当用户在底部功能栏对拼图元素执行长按手势操做后,再「释放」长按手势时,从底部功能栏中移除该拼图元素的效果。剩下的两个 cell 的闭包经过 collectionView 再传递到了对应的父视图中。
在 LiBottomView
这一层级的视图进行拼图元素的上图操做。
class LiBottomView: UIView {
// ...
private func initView() {
// ...
collectionView!.longTapBegan = {
let center = $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)
}
}
}
复制代码
longTapBegan()
方法中新建一个 Puzzle
拼图元素,注意此时不能直接使用传递出来的 puzzle
对象,不然会由于引用关系而致使后续一些奇怪的问题产生。
在 longTapChange()
方法中维护由在 LiBottomCollectionViewCell
中所触发的长按手势事件回调出的 CGPoint
。在实现从底部功能栏上图这一环节中,很容易会想到在 longTapBegan()
新建 Puzzle
对象时,给该新建对象再绑上一个 UIPanGesture
拖拽手势,但其实仔细一想,UILongPressGestureRecognizer
是继承于 UIGestureRecognizer
类的,UIGestureRecognizer
中维护了一套与用户手势相关识别流程,不论是轻扫、拖拽仍是长按,本质上也都是经过在必定时间间隔点判断用户手势的移动距离和趋势来决定出具体是哪一个手势类型,所以咱们直接使用 UILongPressGestureRecognizer
便可。
运行工程~在底部功能栏里选中一个你喜欢的拼图,长按它!激发手势,慢慢的拖拽到画笔上,好好感觉一下吧~
在这篇文章中,咱们主要关注了底部功能栏的逻辑实现,让底部功能栏具有初步「功能」的做用,并完善了上一篇文章中「元素上图」的需求,使其更加完整,目前,咱们完成的需求有:
GitHub 地址:github.com/windstormey…