// // 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() 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.. 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() } }