// // 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.. UIView? { return myView } func scrollViewDidScroll(_ scrollView: UIScrollView) { updateTiles() } } private extension Int { var isEven: Bool { return self % 2 == 0 } } extension GridViewController: Logger { }