前言
今天我们就使用swift
开发一个拼图小游戏,来拼个中秋满月。图片素材的话我们就使用中秋创意投稿大赛的封面(也可以换成你想要的)
先看下效果:
在开始之前我们先给UIImage
扩展一个裁剪图片的方法
extension UIImage {
func clip(_ rect: CGRect) -> UIImage {
if let cgImage = cgImage?.cropping(to: rect) {
return UIImage(cgImage: cgImage)
}
return self
}
}
复制代码
新建一个GameView
类
- 在
GameView
里面暴露三个属性
/// 行/列间距
var spacing: CGFloat = 2
/// 每一行/列有多少个格子
var numberOfItems: Int = 3
/// 图片
var image: UIImage?
复制代码
- 根据
numberOfItems
计算总共多少个格子
/// 总的格子数
private var numberOfGrids: Int {
get {
return numberOfItems * numberOfItems
}
}
复制代码
- 在
GameView
里面增加一个reloadView
方法,根据numberOfItems
和image
来更新布局
根据
numberOfItems
和image
来计算每个item
的宽高
循环创建UIImageView
调用UIImage
扩展的clip
方法来裁剪图片
代码如下
func reloadView() {
guard let image = self.image else { return }
for item in subviews {
item.removeFromSuperview()
}
/// 每个item图片的宽度
let imageWidth = image.size.width / CGFloat(numberOfItems)
/// 每个item图片的高度
let imageHeight = 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)
}
}
复制代码
现在我们来看下裁剪后的效果,初始化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)
复制代码
裁剪后的效果已经做好了,现在还差滑动以及把图片的顺序打乱。
- 在做这两个功能时候,我们需要先建立一个
GridModel
模型,用来存放每个UIImageView
以及它的标记
/// 每个格子模型
class GridModel {
/// 起始索引
var originIndex: Int = 0
/// 打乱顺序后的索引
var index: Int = 0
var imageView = UIImageView()
init(originIndex: Int, index: Int, imageView: UIImageView) {
self.originIndex = originIndex
self.index = index
self.imageView = imageView
}
}
复制代码
- 在
GameView
里面增加一个imageViews
数组
/// 格子模型数组
private var imageViews = [GridModel]()
复制代码
- 在for循环里面把每个
imageView
保存在imageViews
里
let model = GridModel(originIndex: index, index: 0, imageView: imageView)
imageViews.append(model)
复制代码
打乱图片的顺序
- 我们先给
Array
扩展一个random
方法
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
}
}
复制代码
- 然后再将
imageViews
顺序打乱,再for循环imageViews
,设置每个imageView
的frame
和index
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
}
}
复制代码
图片滑动
- 这里我们使用
UISwipeGestureRecognizer
手势,在for循环里面给每个imageView
添加上、下、左、右
的手势
/// 添加轻扫手势
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)
复制代码
- 滑动处理前
滑动的时候我们要处理边界的问题,出现这些情况的时候,就不应该让其滑动:
1、当滑动的imageView在上边界,且是向上滑动
2、当滑动的imageView在左边界,且是向左滑动
3、当滑动的imageView在下边界,且是向下滑动
4、当滑动的imageView在右边界,且是向右滑动
所以我们定义一个GameBorder
枚举,用来判断当前滑动的imageView在哪里
/// 边界方向
enum GameBorder {
case unknown
case up
case left
case down
case right
}
/// 判断轻扫的图片是不是在边界
private func 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
}
复制代码
- 滑动处理
@objc private func swipeAction(_ recognizer: UISwipeGestureRecognizer) {
/// 获取轻扫的UIImageView
guard let swipeView = recognizer.view else { return }
guard let oldModel = imageViews.filter({ $0.imageView == swipeView }).first else { return }
/// 轻扫的UIImageView的tag
let oldIndex = oldModel.index
/// 需要替换的UIImageView的tag
var newIndex: Int = 0
/// 向上轻扫,且在上边界
if gridBorder(oldIndex) == .up,
recognizer.direction == .up {
return
}
/// 向左轻扫,且在左边界
if gridBorder(oldIndex) == .left,
recognizer.direction == .left {
return
}
/// 向下轻扫,且在下边界
if gridBorder(oldIndex) == .down,
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:
break
}
/// 取出需要替换的UIImageView
guard let newModel = imageViews.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: 0.25) {
/// 交换frame于tag
oldModel.imageView.frame = newFrame
oldModel.index = newTag
newModel.imageView.frame = oldFrame
newModel.index = oldTag
} completion: { finish in
/// 交换imageViews里面的位置
let oldIndex = self.imageViews.firstIndex {
$0.imageView == oldModel.imageView
} ?? 0
let newIndex = self.imageViews.firstIndex {
$0.imageView == newModel.imageView
} ?? 0
self.imageViews.swapAt(oldIndex , newIndex)
self.completeHandler()
}
}
/// 游戏完成提醒
private func completeHandler() {
let indexs = imageViews.map { $0.originIndex }
if indexs == originIndexs {
debugPrint("游戏完成")
}
}
复制代码
到这里就结束了,demo地址