Caps-iOS/CapCollector/Presentation/GridViewController.swift
2020-05-16 11:21:55 +02:00

343 lines
9.5 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 caps horizontally.
private let columns = 40
/// The number of hroizontal 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 = [Cap]()
private var installedTiles = [Int : RoundedImageView]()
private var changedTiles = Set<Int>()
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
private var capCount = 0
@IBAction func toggleAverageColor(_ sender: Any) {
isShowingColors = !isShowingColors
for (tile, view) in installedTiles {
if isShowingColors {
view.image = nil
} else {
if let image = tiles[tile].thumbnail {
view.image = image
continue
}
tiles[tile].downloadMainImage() { image in
view.image = image
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
app.database.add(listener: self)
capCount = app.database.capCount
tiles = app.database.caps.sorted { $0.tile < $1.tile }
let width = CGFloat(columns) * GridViewController.len + GridViewController.len / 2
let height = (CGFloat(capCount) / 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)
saveChangedTiles()
}
// MARK: Tiles
private func tileColor(tile: Int) -> UIColor {
return tiles[tile].color
}
private func saveChangedTiles() {
for tile in changedTiles {
let cap = tiles[tile]
app.database.update(tile: tile, for: cap.id)
}
}
/**
Switch two tiles.
*/
private func switchTiles(_ lhs: Int, _ rhs: Int) -> Bool {
let temp = tiles[rhs]
tiles[rhs] = tiles[lhs]
tiles[lhs] = temp
changedTiles.insert(lhs)
changedTiles.insert(rhs)
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)
view.backgroundColor = tileColor(tile: tile)
defer {
installedTiles[tile] = view
}
// Only set image if images are shown
guard !isShowingColors else {
return
}
if let image = tiles[tile].thumbnail {
view.image = image
return
}
tiles[tile].downloadMainImage() { image in
view.image = image
}
}
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..<capCount {
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 { }
extension GridViewController: DatabaseDelegate {
func database(didChangeCap id: Int) {
guard let view = installedTiles[id] else {
return
}
guard let cap = app.database.cap(for: id) else {
return
}
tiles[cap.tile] = cap
view.backgroundColor = cap.color
// Only set image if images are shown
if !isShowingColors {
view.image = cap.image
}
}
func database(didAddCap cap: Cap) {
tiles.append(cap)
refresh(tile: cap.tile, inVisibleRect: visibleRect)
}
func databaseRequiresFullRefresh() {
updateTiles()
}
}