361 lines
10 KiB
Swift
361 lines
10 KiB
Swift
//
|
|
// GridViewController.swift
|
|
// CapCollector
|
|
//
|
|
// Created by Christoph on 07.01.19.
|
|
// Copyright © 2019 CH. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
class GridViewController: UIViewController {
|
|
|
|
/// The number of horizontal pixels for each cap.
|
|
static let len: CGFloat = 60
|
|
|
|
private lazy var rowHeight = GridViewController.len * 0.866
|
|
|
|
private lazy var margin = GridViewController.len - rowHeight
|
|
|
|
private var myView: UIView!
|
|
|
|
private var canvasSize: CGSize = .zero
|
|
|
|
@IBOutlet weak var scrollView: UIScrollView!
|
|
|
|
/// A dictionary of the caps for the tiles
|
|
private var tiles = [Int]()
|
|
|
|
/// The name of the tile image
|
|
private var name: String = "default"
|
|
|
|
/// The number of caps horizontally.
|
|
private var columns = 40
|
|
|
|
/// A dictionary for the colors of the caps
|
|
private var colors = [Int : UIColor]()
|
|
|
|
/// The currently displaxed image views indexed by their tile ids
|
|
private var installedTiles = [Int : RoundedImageView]()
|
|
|
|
private var selectedTile: Int? = nil
|
|
|
|
private weak var selectionView: RoundedButton!
|
|
|
|
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
|
return .portrait
|
|
}
|
|
|
|
override var shouldAutorotate: Bool {
|
|
return true
|
|
}
|
|
|
|
private var isShowingColors = false
|
|
|
|
@IBAction func toggleAverageColor(_ sender: Any) {
|
|
isShowingColors = !isShowingColors
|
|
for (tile, view) in installedTiles {
|
|
if isShowingColors {
|
|
view.image = nil
|
|
view.backgroundColor = tileColor(tile: tile)
|
|
} else {
|
|
let id = tiles[tile]
|
|
if let image = app.database.storage.thumbnail(for: id) {
|
|
view.image = image
|
|
continue
|
|
}
|
|
self.downloadImage(cap: id, tile: tile)
|
|
}
|
|
}
|
|
}
|
|
|
|
func load(tileImage: Database.TileImage) {
|
|
let totalCount = app.database.capCount
|
|
let firstNewId = tileImage.caps.count + 1
|
|
if totalCount >= firstNewId {
|
|
self.tiles = tileImage.caps + (firstNewId...totalCount)
|
|
} else {
|
|
self.tiles = tileImage.caps
|
|
}
|
|
self.columns = tileImage.width
|
|
self.name = tileImage.name
|
|
}
|
|
|
|
private func saveTileImage() {
|
|
let tileImage = Database.TileImage(name: name, width: columns, caps: tiles)
|
|
guard app.database.save(tileImage: tileImage) else {
|
|
log("Failed to save tile image")
|
|
return
|
|
}
|
|
log("Tile image saved")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
colors = app.database.colors
|
|
|
|
let width = CGFloat(columns) * GridViewController.len + GridViewController.len / 2
|
|
let height = (CGFloat(tiles.count) / CGFloat(columns)).rounded(.up) * rowHeight + margin
|
|
canvasSize = CGSize(width: width, height: height)
|
|
myView = UIView(frame: CGRect(origin: .zero, size: canvasSize))
|
|
|
|
scrollView.addSubview(myView)
|
|
scrollView.contentSize = canvasSize
|
|
scrollView.delegate = self
|
|
scrollView.zoomScale = 0.5
|
|
scrollView.maximumZoomScale = 1
|
|
setZoomRange()
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
updateTiles()
|
|
|
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
|
myView.addGestureRecognizer(tapRecognizer)
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
setZoomRange()
|
|
updateTiles()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
saveTileImage()
|
|
}
|
|
|
|
// MARK: Tiles
|
|
|
|
private func tileColor(tile: Int) -> UIColor? {
|
|
let id = tiles[tile]
|
|
return colors[id]
|
|
}
|
|
|
|
/**
|
|
Switch two tiles.
|
|
*/
|
|
private func switchTiles(_ lhs: Int, _ rhs: Int) -> Bool {
|
|
let temp = tiles[rhs]
|
|
tiles[rhs] = tiles[lhs]
|
|
tiles[lhs] = temp
|
|
return true
|
|
}
|
|
|
|
|
|
private func setZoomRange() {
|
|
let size = scrollView.frame.size
|
|
let a = size.width / canvasSize.width
|
|
let b = size.height / canvasSize.height
|
|
let scale = min(a,b)
|
|
scrollView.minimumZoomScale = min(a,b)
|
|
if scrollView.zoomScale < scale {
|
|
scrollView.setZoomScale(scale, animated: true)
|
|
}
|
|
}
|
|
|
|
@objc func handleTap(_ sender: UITapGestureRecognizer) {
|
|
|
|
let loc = sender.location(in: myView)
|
|
let y = loc.y
|
|
let s = y.truncatingRemainder(dividingBy: rowHeight)
|
|
let row = Int(y / rowHeight)
|
|
guard s > margin else {
|
|
return
|
|
}
|
|
let column: Int
|
|
if row.isEven {
|
|
column = Int(loc.x / GridViewController.len)
|
|
// Abort, if user tapped outside of the grid
|
|
if column >= columns {
|
|
clearTileSelection()
|
|
return
|
|
}
|
|
} else {
|
|
column = Int((loc.x - GridViewController.len / 2) / GridViewController.len)
|
|
}
|
|
handleTileTapped(tile: row * columns + Int(column))
|
|
}
|
|
|
|
private func handleTileTapped(tile: Int) {
|
|
if let selected = selectedTile {
|
|
switchTiles(oldTile: selected, newTile: tile)
|
|
} else {
|
|
showSelection(tile: tile)
|
|
}
|
|
}
|
|
|
|
private func showSelection(tile: Int) {
|
|
clearTileSelection()
|
|
|
|
if let view = installedTiles[tile] {
|
|
view.borderWidth = 3
|
|
view.borderColor = AppDelegate.tintColor
|
|
selectedTile = tile
|
|
} else {
|
|
selectedTile = nil
|
|
}
|
|
}
|
|
|
|
private func tileIsVisible(tile: Int, in rect: CGRect) -> Bool {
|
|
return rect.intersects(frame(for: tile))
|
|
}
|
|
|
|
private func makeTile(_ tile: Int) {
|
|
let view = RoundedImageView(frame: frame(for: tile))
|
|
myView.addSubview(view)
|
|
defer {
|
|
installedTiles[tile] = view
|
|
}
|
|
// Only set image if images are shown
|
|
guard !isShowingColors else {
|
|
view.backgroundColor = tileColor(tile: tile)
|
|
return
|
|
|
|
}
|
|
if let image = app.database.storage.thumbnail(for: tiles[tile]) {
|
|
view.image = image
|
|
return
|
|
}
|
|
|
|
downloadImage(tile: tile)
|
|
}
|
|
|
|
private func downloadImage(tile: Int) {
|
|
let id = tiles[tile]
|
|
downloadImage(cap: id, tile: tile)
|
|
}
|
|
|
|
private func downloadImage(cap id: Int, tile: Int) {
|
|
app.database.downloadImage(for: id) { img in
|
|
guard img != nil else {
|
|
return
|
|
}
|
|
guard let view = self.installedTiles[tile] else {
|
|
self.log("No installed tile for downloaded image \(id)")
|
|
return
|
|
}
|
|
guard let image = app.database.storage.thumbnail(for: id) else {
|
|
self.log("Failed to load image for cap \(id) after successful download")
|
|
return
|
|
}
|
|
DispatchQueue.main.async {
|
|
guard self.isShowingColors else {
|
|
view.image = image
|
|
return
|
|
}
|
|
guard let color = image.averageColor else {
|
|
self.log("Failed to get average color from image for cap \(id)")
|
|
return
|
|
}
|
|
view.backgroundColor = color
|
|
self.colors[id] = color
|
|
}
|
|
}
|
|
}
|
|
|
|
private func frame(for tile: Int) -> CGRect {
|
|
let row = tile / columns
|
|
let column = tile - row * columns
|
|
let x = CGFloat(column) * GridViewController.len + (row.isEven ? 0 : GridViewController.len / 2)
|
|
let y = CGFloat(row) * rowHeight
|
|
return CGRect(x: x, y: y, width: GridViewController.len, height: GridViewController.len)
|
|
}
|
|
|
|
private func switchTiles(oldTile: Int, newTile: Int) {
|
|
guard oldTile != newTile else {
|
|
clearTileSelection()
|
|
return
|
|
}
|
|
guard switchTiles(oldTile, newTile) else {
|
|
clearTileSelection()
|
|
return
|
|
}
|
|
// Switch cap colors
|
|
let temp = installedTiles[oldTile]?.backgroundColor
|
|
installedTiles[oldTile]?.backgroundColor = installedTiles[newTile]?.backgroundColor
|
|
installedTiles[newTile]?.backgroundColor = temp
|
|
if !isShowingColors {
|
|
let temp = installedTiles[oldTile]?.image
|
|
installedTiles[oldTile]?.image = installedTiles[newTile]?.image
|
|
installedTiles[newTile]?.image = temp
|
|
}
|
|
clearTileSelection()
|
|
|
|
}
|
|
|
|
private func clearTileSelection() {
|
|
guard let tile = selectedTile else {
|
|
return
|
|
}
|
|
installedTiles[tile]?.borderWidth = 0
|
|
selectedTile = nil
|
|
}
|
|
|
|
private func showTiles(in rect: CGRect) {
|
|
for tile in 0..<tiles.count {
|
|
refresh(tile: tile, inVisibleRect: rect)
|
|
}
|
|
}
|
|
|
|
private func refresh(tile: Int, inVisibleRect rect: CGRect) {
|
|
if tileIsVisible(tile: tile, in: rect) {
|
|
show(tile: tile)
|
|
} else if let installed = installedTiles[tile] {
|
|
installed.removeFromSuperview()
|
|
installedTiles[tile] = nil
|
|
}
|
|
}
|
|
|
|
private func show(tile: Int) {
|
|
guard installedTiles[tile] == nil else {
|
|
return
|
|
}
|
|
makeTile(tile)
|
|
}
|
|
|
|
private func remove(tile: Int) {
|
|
installedTiles[tile]?.removeFromSuperview()
|
|
installedTiles[tile] = nil
|
|
}
|
|
|
|
private var visibleRect: CGRect {
|
|
let scale = scrollView.zoomScale
|
|
let offset = scrollView.contentOffset
|
|
let size = scrollView.visibleSize
|
|
|
|
let scaledOrigin = CGPoint(x: offset.x / scale, y: offset.y / scale)
|
|
let scaledSize = CGSize(width: size.width / scale, height: size.height / scale)
|
|
return CGRect(origin: scaledOrigin, size: scaledSize)
|
|
}
|
|
|
|
private func updateTiles() {
|
|
DispatchQueue.main.async {
|
|
self.showTiles(in: self.visibleRect)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension GridViewController: UIScrollViewDelegate {
|
|
|
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
return myView
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
updateTiles()
|
|
}
|
|
}
|
|
|
|
private extension Int {
|
|
|
|
var isEven: Bool {
|
|
return self % 2 == 0
|
|
}
|
|
}
|
|
|
|
extension GridViewController: Logger { }
|