中秋快乐啊,使用swift写一个拼图小游戏

2,293 阅读2分钟

前言

今天我们就使用swift开发一个拼图小游戏,来拼个中秋满月。图片素材的话我们就使用中秋创意投稿大赛的封面(也可以换成你想要的)

02.png

先看下效果:

0.gif

在开始之前我们先给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方法,根据numberOfItemsimage来更新布局

根据numberOfItemsimage来计算每个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)

Simulator Screen Shot - iPhone 12 Pro Max - 2021-09-07 at 14.21.12.png

裁剪后的效果已经做好了,现在还差滑动以及把图片的顺序打乱。

  • 在做这两个功能时候,我们需要先建立一个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,设置每个imageViewframeindex
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地址