Download classifier, database
This commit is contained in:
@ -18,29 +18,31 @@ class CapCell: UITableViewCell {
|
||||
@IBOutlet private weak var nameLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var countLabel: UILabel!
|
||||
|
||||
var id = 0
|
||||
|
||||
var cap: Cap! {
|
||||
didSet {
|
||||
updateCell()
|
||||
}
|
||||
}
|
||||
|
||||
func updateCell() {
|
||||
capImage.image = cap.image
|
||||
var id: Int = 0
|
||||
|
||||
func set(image: UIImage?) {
|
||||
capImage.image = image ?? UIImage(named: "launch")
|
||||
}
|
||||
|
||||
func set(cap: Cap, match: Float?) {
|
||||
id = cap.id
|
||||
if let image = cap.image {
|
||||
set(image: image)
|
||||
|
||||
} else {
|
||||
capImage.image = UIImage(named: "launch")
|
||||
cap.downloadMainImage() { image in
|
||||
self.set(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
//capImage.borderColor = AppDelegate.tintColor
|
||||
|
||||
matchLabel.text = text(for: cap.match)
|
||||
matchLabel.text = cap.matchDescription(match: match)
|
||||
nameLabel.text = cap.name
|
||||
countLabel.text = "\(cap.id) (\(cap.count) image" + (cap.count > 1 ? "s)" : ")")
|
||||
countLabel.text = cap.subtitle
|
||||
}
|
||||
|
||||
private func text(for value: Float?) -> String? {
|
||||
guard let nr = value else {
|
||||
return nil
|
||||
}
|
||||
let percent = Int((nr * 100).rounded())
|
||||
return String(format: "%d %%", arguments: [percent])
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,8 +10,10 @@ 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
|
||||
@ -24,6 +26,13 @@ class GridViewController: UIViewController {
|
||||
|
||||
@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!
|
||||
@ -38,13 +47,21 @@ class GridViewController: UIViewController {
|
||||
|
||||
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 {
|
||||
view.image = Cap.tileImage(tile: tile)
|
||||
if let image = tiles[tile].thumbnail {
|
||||
view.image = image
|
||||
continue
|
||||
}
|
||||
tiles[tile].downloadMainImage() { image in
|
||||
view.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,12 +69,15 @@ class GridViewController: UIViewController {
|
||||
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(Cap.totalCapCount) / CGFloat(columns)).rounded(.up) * rowHeight + margin
|
||||
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
|
||||
@ -82,10 +102,36 @@ class GridViewController: UIViewController {
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
Cap.save()
|
||||
|
||||
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
|
||||
@ -106,11 +152,16 @@ class GridViewController: UIViewController {
|
||||
guard s > margin else {
|
||||
return
|
||||
}
|
||||
let column: CGFloat
|
||||
let column: Int
|
||||
if row.isEven {
|
||||
column = loc.x / GridViewController.len
|
||||
column = Int(loc.x / GridViewController.len)
|
||||
// Abort, if user tapped outside of the grid
|
||||
if column >= columns {
|
||||
clearTileSelection()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
column = (loc.x - GridViewController.len / 2) / GridViewController.len
|
||||
column = Int((loc.x - GridViewController.len / 2) / GridViewController.len)
|
||||
}
|
||||
handleTileTapped(tile: row * columns + Int(column))
|
||||
}
|
||||
@ -123,8 +174,6 @@ class GridViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var installedTiles = [Int : RoundedImageView]()
|
||||
|
||||
private func showSelection(tile: Int) {
|
||||
clearTileSelection()
|
||||
|
||||
@ -144,13 +193,23 @@ class GridViewController: UIViewController {
|
||||
private func makeTile(_ tile: Int) {
|
||||
let view = RoundedImageView(frame: frame(for: tile))
|
||||
myView.addSubview(view)
|
||||
view.backgroundColor = Cap.tileColor(tile: tile)
|
||||
view.backgroundColor = tileColor(tile: tile)
|
||||
defer {
|
||||
installedTiles[tile] = view
|
||||
}
|
||||
// Only set image if images are shown
|
||||
if !isShowingColors {
|
||||
view.image = Cap.tileImage(tile: tile)
|
||||
guard !isShowingColors else {
|
||||
return
|
||||
|
||||
}
|
||||
if let image = tiles[tile].thumbnail {
|
||||
view.image = image
|
||||
return
|
||||
}
|
||||
|
||||
installedTiles[tile] = view
|
||||
tiles[tile].downloadMainImage() { image in
|
||||
view.image = image
|
||||
}
|
||||
}
|
||||
|
||||
private func frame(for tile: Int) -> CGRect {
|
||||
@ -166,13 +225,18 @@ class GridViewController: UIViewController {
|
||||
clearTileSelection()
|
||||
return
|
||||
}
|
||||
Cap.switchTiles(oldTile, newTile)
|
||||
guard switchTiles(oldTile, newTile) else {
|
||||
clearTileSelection()
|
||||
return
|
||||
}
|
||||
// Switch cap colors
|
||||
installedTiles[oldTile]?.backgroundColor = Cap.tileColor(tile: oldTile)
|
||||
installedTiles[newTile]?.backgroundColor = Cap.tileColor(tile: newTile)
|
||||
let temp = installedTiles[oldTile]?.backgroundColor
|
||||
installedTiles[oldTile]?.backgroundColor = installedTiles[newTile]?.backgroundColor
|
||||
installedTiles[newTile]?.backgroundColor = temp
|
||||
if !isShowingColors {
|
||||
installedTiles[oldTile]?.image = Cap.tileImage(tile: oldTile)
|
||||
installedTiles[newTile]?.image = Cap.tileImage(tile: newTile)
|
||||
let temp = installedTiles[oldTile]?.image
|
||||
installedTiles[oldTile]?.image = installedTiles[newTile]?.image
|
||||
installedTiles[newTile]?.image = temp
|
||||
}
|
||||
clearTileSelection()
|
||||
|
||||
@ -187,32 +251,46 @@ class GridViewController: UIViewController {
|
||||
}
|
||||
|
||||
private func showTiles(in rect: CGRect) {
|
||||
for i in 0..<Cap.totalCapCount {
|
||||
if tileIsVisible(tile: i, in: rect) {
|
||||
if installedTiles[i] != nil {
|
||||
continue
|
||||
}
|
||||
makeTile(i)
|
||||
} else if let tile = installedTiles[i] {
|
||||
tile.removeFromSuperview()
|
||||
installedTiles[i] = nil
|
||||
}
|
||||
for tile in 0..<capCount {
|
||||
refresh(tile: tile, inVisibleRect: rect)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTiles() {
|
||||
guard #available(iOS 12.0, *) else {
|
||||
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)
|
||||
let rect = CGRect(origin: scaledOrigin, size: scaledSize)
|
||||
|
||||
showTiles(in: rect)
|
||||
return CGRect(origin: scaledOrigin, size: scaledSize)
|
||||
}
|
||||
|
||||
private func updateTiles() {
|
||||
DispatchQueue.main.async {
|
||||
self.showTiles(in: self.visibleRect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,8 +312,31 @@ private extension Int {
|
||||
}
|
||||
}
|
||||
|
||||
extension GridViewController: Logger {
|
||||
extension GridViewController: Logger { }
|
||||
|
||||
extension GridViewController: DatabaseDelegate {
|
||||
|
||||
static let logToken: String = "[Grid]"
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftyDropbox
|
||||
|
||||
class ImageSelector: UIViewController {
|
||||
|
||||
@ -28,13 +27,25 @@ class ImageSelector: UIViewController {
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - CollectionView
|
||||
// MARK: - Variables
|
||||
|
||||
private var titleLabel: UILabel!
|
||||
|
||||
private var subtitleLabel: UILabel!
|
||||
|
||||
private var images = [UIImage?]()
|
||||
|
||||
var cap: Cap!
|
||||
|
||||
@IBOutlet weak var collection: UICollectionView!
|
||||
|
||||
private var titleText: String {
|
||||
"Cap \(cap.id) (\(cap.count) images)"
|
||||
}
|
||||
|
||||
private var subtitleText: String {
|
||||
cap.name
|
||||
}
|
||||
|
||||
// MARK: - Life cycle
|
||||
|
||||
@ -43,6 +54,7 @@ class ImageSelector: UIViewController {
|
||||
|
||||
collection.dataSource = self
|
||||
collection.delegate = self
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@ -50,18 +62,46 @@ class ImageSelector: UIViewController {
|
||||
|
||||
downloadImages()
|
||||
}
|
||||
|
||||
override func didMove(toParent parent: UIViewController?) {
|
||||
super.didMove(toParent: parent)
|
||||
|
||||
guard parent != nil && self.navigationItem.titleView == nil else {
|
||||
return
|
||||
}
|
||||
initNavigationItemTitleView()
|
||||
}
|
||||
|
||||
private func initNavigationItemTitleView() {
|
||||
self.titleLabel = UILabel()
|
||||
titleLabel.text = titleText
|
||||
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
titleLabel.textColor = .label
|
||||
|
||||
self.subtitleLabel = UILabel()
|
||||
subtitleLabel.text = subtitleText
|
||||
subtitleLabel.font = .preferredFont(forTextStyle: .footnote)
|
||||
subtitleLabel.textColor = .secondaryLabel
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||
stackView.distribution = .equalCentering
|
||||
stackView.alignment = .center
|
||||
stackView.axis = .vertical
|
||||
|
||||
self.navigationItem.titleView = stackView
|
||||
}
|
||||
|
||||
// MARK: Image download
|
||||
|
||||
private func downloadImages() {
|
||||
images = [UIImage?](repeating: nil, count: cap.count)
|
||||
event("\(cap.count) images for cap \(cap.id)")
|
||||
log("\(cap.count) images for cap \(cap.id)")
|
||||
if let image = cap.image {
|
||||
self.images[0] = image
|
||||
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
|
||||
} else {
|
||||
cap.downloadImage { mainImage in
|
||||
self.images[0] = mainImage
|
||||
cap.downloadMainImage { image in
|
||||
self.images[0] = image
|
||||
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
|
||||
}
|
||||
}
|
||||
@ -69,7 +109,10 @@ class ImageSelector: UIViewController {
|
||||
return
|
||||
}
|
||||
for number in 1..<cap.count {
|
||||
cap.downloadImage(number) { image in
|
||||
_ = cap.downloadImage(number) { image in
|
||||
guard let image = image else {
|
||||
return
|
||||
}
|
||||
self.images[number] = image
|
||||
self.collection.reloadItems(at: [IndexPath(row: number, section: 0)])
|
||||
}
|
||||
@ -79,11 +122,7 @@ class ImageSelector: UIViewController {
|
||||
// MARK: Select
|
||||
|
||||
private func selectedImage(nr: Int) {
|
||||
guard let image = images[nr] else {
|
||||
return
|
||||
}
|
||||
|
||||
cap.setMainImage(to: nr, image: image)
|
||||
app.database.setMainImage(of: cap.id, to: nr)
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +143,7 @@ extension ImageSelector: UICollectionViewDataSource {
|
||||
withReuseIdentifier: "Image",
|
||||
for: indexPath) as! ImageCell
|
||||
|
||||
cell.capView.image = images[indexPath.row]
|
||||
cell.capView.image = images[indexPath.row] ?? UIImage(named: "launch")
|
||||
return cell
|
||||
}
|
||||
|
||||
@ -138,9 +177,6 @@ extension ImageSelector : UICollectionViewDelegateFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageSelector: Logger {
|
||||
|
||||
static let logToken = "[ImageSelector]"
|
||||
}
|
||||
extension ImageSelector: Logger { }
|
||||
|
||||
|
||||
|
185
CapCollector/Presentation/SearchAndDisplayAccessory.swift
Normal file
185
CapCollector/Presentation/SearchAndDisplayAccessory.swift
Normal file
@ -0,0 +1,185 @@
|
||||
//
|
||||
// SearchAndDisplayAccessory.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 09.10.19.
|
||||
// Copyright © 2019 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PassthroughView: UIView {
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let view = super.hitTest(point, with: event)
|
||||
return view == self ? nil : view
|
||||
}
|
||||
}
|
||||
|
||||
protocol CapAccessoryDelegate: class {
|
||||
|
||||
func capSearchWasDismissed()
|
||||
|
||||
func capSearch(didChange text: String)
|
||||
|
||||
func capAccessoryDidDiscardImage()
|
||||
|
||||
func capAccessory(shouldSave image: UIImage)
|
||||
|
||||
func capAccessoryCameraButtonPressed()
|
||||
}
|
||||
|
||||
class SearchAndDisplayAccessory: PassthroughView {
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
@IBOutlet weak var newImageView: PassthroughView!
|
||||
|
||||
@IBOutlet weak var capImage: RoundedImageView!
|
||||
|
||||
@IBOutlet weak var saveButton: UIButton!
|
||||
|
||||
@IBOutlet weak var deleteButton: UIButton!
|
||||
|
||||
@IBOutlet weak var cameraButton: UIButton!
|
||||
|
||||
@IBOutlet weak var searchBar: UISearchBar!
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func cameraButtonPressed() {
|
||||
delegate?.capAccessoryCameraButtonPressed()
|
||||
}
|
||||
|
||||
@IBAction func saveButtonPressed() {
|
||||
if let image = capImage.image {
|
||||
delegate?.capAccessory(shouldSave: image)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func cancelButtonPressed() {
|
||||
discardImage()
|
||||
}
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
var view: UIView?
|
||||
|
||||
weak var blurView: UIVisualEffectView?
|
||||
|
||||
weak var currentBlurContraint: NSLayoutConstraint?
|
||||
|
||||
weak var delegate: CapAccessoryDelegate?
|
||||
|
||||
var currentImage: UIImage? {
|
||||
capImage.image
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
convenience init(width: CGFloat) {
|
||||
let frame = CGRect(origin: .zero, size: CGSize(width: width, height: 145))
|
||||
self.init(frame: frame)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setup()
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
view = fromNib()
|
||||
view!.frame = bounds
|
||||
//view!.autoresizingMask = .flexibleHeight
|
||||
addSubview(view!)
|
||||
|
||||
let blur = UIBlurEffect(style: .systemThinMaterial)
|
||||
let blurView = UIVisualEffectView(effect: blur)
|
||||
self.blurView = blurView
|
||||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||
blurView.isUserInteractionEnabled = false
|
||||
insertSubview(blurView, at: 0)
|
||||
|
||||
let t = searchBar.topAnchor.constraint(equalTo: blurView.topAnchor)
|
||||
let b = searchBar.bottomAnchor.constraint(equalTo: blurView.bottomAnchor)
|
||||
let l = leadingAnchor.constraint(equalTo: blurView.leadingAnchor)
|
||||
let r = trailingAnchor.constraint(equalTo: blurView.trailingAnchor)
|
||||
addConstraints([t, b, l, r])
|
||||
|
||||
currentBlurContraint = t
|
||||
|
||||
self.newImageView.alpha = 0
|
||||
self.newImageView.isHidden = true
|
||||
|
||||
searchBar.text = nil
|
||||
searchBar.setShowsCancelButton(false, animated: false)
|
||||
searchBar.delegate = self
|
||||
|
||||
cameraButton.setImage(UIImage.templateImage(named: "camera_square"), for: .normal)
|
||||
}
|
||||
|
||||
// MARK: Search bar
|
||||
|
||||
func dismissAndClearSearchBar() {
|
||||
searchBar.resignFirstResponder()
|
||||
searchBar.text = nil
|
||||
}
|
||||
|
||||
// MARK: Cap image
|
||||
|
||||
func showImageView(with image: UIImage) {
|
||||
capImage.image = image
|
||||
showImageView()
|
||||
}
|
||||
|
||||
func discardImage() {
|
||||
dismissAndClearSearchBar()
|
||||
hideImageView()
|
||||
delegate?.capAccessoryDidDiscardImage()
|
||||
}
|
||||
|
||||
func hideImageView() {
|
||||
currentBlurContraint?.isActive = false
|
||||
let t = searchBar.topAnchor.constraint(equalTo: blurView!.topAnchor)
|
||||
addConstraint(t)
|
||||
currentBlurContraint = t
|
||||
|
||||
self.newImageView.alpha = 0
|
||||
self.newImageView.isHidden = true
|
||||
self.capImage.image = nil
|
||||
}
|
||||
|
||||
private func showImageView() {
|
||||
currentBlurContraint?.isActive = false
|
||||
let t = blurView!.topAnchor.constraint(equalTo: saveButton.topAnchor, constant: -8)
|
||||
addConstraint(t)
|
||||
currentBlurContraint = t
|
||||
|
||||
self.newImageView.isHidden = false
|
||||
self.newImageView.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UISearchBarDelegate
|
||||
|
||||
extension SearchAndDisplayAccessory: UISearchBarDelegate {
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
searchBar.text = nil
|
||||
delegate?.capSearchWasDismissed()
|
||||
}
|
||||
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
delegate?.capSearch(didChange: searchText)
|
||||
}
|
||||
}
|
117
CapCollector/Presentation/SearchAndDisplayAccessory.xib
Normal file
117
CapCollector/Presentation/SearchAndDisplayAccessory.xib
Normal file
@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16086"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SearchAndDisplayAccessory" customModule="CapCollector" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="cameraButton" destination="bIA-eq-Tn5" id="r0F-0j-Ve9"/>
|
||||
<outlet property="capImage" destination="vQm-nH-J8o" id="bQK-Vu-Z1U"/>
|
||||
<outlet property="deleteButton" destination="qhB-Sd-K8H" id="oym-9o-1m3"/>
|
||||
<outlet property="newImageView" destination="0wK-yR-rO9" id="Gi3-I6-Xv9"/>
|
||||
<outlet property="saveButton" destination="dt5-LD-28a" id="IYT-eN-3lb"/>
|
||||
<outlet property="searchBar" destination="bCh-7y-t0w" id="8Tt-4h-Fkg"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="PassthroughView" customModule="CapCollector" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="145"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<searchBar contentMode="redraw" searchBarStyle="minimal" placeholder="Search caps" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bCh-7y-t0w">
|
||||
<rect key="frame" x="0.0" y="89" width="358" height="56"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardAppearance="alert" returnKeyType="search" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
|
||||
<scopeButtonTitles>
|
||||
<string>Title</string>
|
||||
<string>Title</string>
|
||||
</scopeButtonTitles>
|
||||
</searchBar>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bIA-eq-Tn5">
|
||||
<rect key="frame" x="358" y="89" width="56" height="56"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="bIA-eq-Tn5" secondAttribute="height" multiplier="1:1" id="O09-ww-bHE"/>
|
||||
<constraint firstAttribute="height" constant="56" id="Y3N-l5-d94"/>
|
||||
</constraints>
|
||||
<inset key="imageEdgeInsets" minX="5" minY="5" maxX="5" maxY="5"/>
|
||||
<state key="normal" image="camera_square"/>
|
||||
<connections>
|
||||
<action selector="cameraButtonPressed" destination="-1" eventType="touchUpInside" id="ooo-b8-Atj"/>
|
||||
</connections>
|
||||
</button>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0wK-yR-rO9">
|
||||
<rect key="frame" x="0.0" y="-1" width="414" height="90"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="vQm-nH-J8o" customClass="RoundedImageView" customModule="CapCollector" customModuleProvider="target">
|
||||
<rect key="frame" x="2" y="2" width="86" height="86"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="vQm-nH-J8o" secondAttribute="height" multiplier="1:1" id="WHb-tV-k4S"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
|
||||
<color key="value" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
|
||||
<real key="value" value="1"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dt5-LD-28a">
|
||||
<rect key="frame" x="96" y="60" width="151" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="Boy-dJ-2BJ"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Save"/>
|
||||
<connections>
|
||||
<action selector="saveButtonPressed" destination="-1" eventType="touchUpInside" id="O49-6L-mNY"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qhB-Sd-K8H">
|
||||
<rect key="frame" x="255" y="58.5" width="151" height="33"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Delete"/>
|
||||
<connections>
|
||||
<action selector="cancelButtonPressed" destination="-1" eventType="touchUpInside" id="4Sm-nV-aOd"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="vQm-nH-J8o" firstAttribute="leading" secondItem="0wK-yR-rO9" secondAttribute="leading" constant="2" id="6q2-Lc-eIF"/>
|
||||
<constraint firstItem="dt5-LD-28a" firstAttribute="leading" secondItem="vQm-nH-J8o" secondAttribute="trailing" constant="8" id="OUT-X8-cxn"/>
|
||||
<constraint firstAttribute="bottom" secondItem="vQm-nH-J8o" secondAttribute="bottom" constant="2" id="P8h-BA-8Yc"/>
|
||||
<constraint firstAttribute="height" constant="90" id="Rsk-qm-0WZ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="qhB-Sd-K8H" secondAttribute="trailing" constant="8" id="fzn-7b-WrO"/>
|
||||
<constraint firstItem="vQm-nH-J8o" firstAttribute="top" secondItem="0wK-yR-rO9" secondAttribute="top" constant="2" id="h1D-Ut-lnp"/>
|
||||
<constraint firstAttribute="bottom" secondItem="dt5-LD-28a" secondAttribute="bottom" id="wFi-0u-ian"/>
|
||||
<constraint firstItem="qhB-Sd-K8H" firstAttribute="leading" secondItem="dt5-LD-28a" secondAttribute="trailing" constant="8" id="y1c-g7-5zg"/>
|
||||
<constraint firstItem="qhB-Sd-K8H" firstAttribute="centerY" secondItem="dt5-LD-28a" secondAttribute="centerY" id="yhy-DU-ReF"/>
|
||||
<constraint firstItem="qhB-Sd-K8H" firstAttribute="width" secondItem="dt5-LD-28a" secondAttribute="width" id="zpv-6V-gXr"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="bIA-eq-Tn5" secondAttribute="bottom" id="Geo-F0-pdI"/>
|
||||
<constraint firstItem="bCh-7y-t0w" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="M9M-He-vxM"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="top" secondItem="0wK-yR-rO9" secondAttribute="top" constant="1" id="Uyp-J7-bKr"/>
|
||||
<constraint firstItem="bIA-eq-Tn5" firstAttribute="leading" secondItem="bCh-7y-t0w" secondAttribute="trailing" id="i7w-LO-9lv"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="0wK-yR-rO9" secondAttribute="trailing" id="iWS-vI-RAT"/>
|
||||
<constraint firstItem="bCh-7y-t0w" firstAttribute="top" secondItem="0wK-yR-rO9" secondAttribute="bottom" id="iab-RT-0nN"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="bIA-eq-Tn5" secondAttribute="trailing" id="kQV-mX-EoD"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="bCh-7y-t0w" secondAttribute="bottom" id="r8c-kO-KT5"/>
|
||||
<constraint firstItem="0wK-yR-rO9" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="rd3-iN-jtD"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<point key="canvasLocation" x="97.5" y="105.625"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="camera_square" width="220" height="220"/>
|
||||
</resources>
|
||||
</document>
|
@ -1,272 +0,0 @@
|
||||
//
|
||||
// SettingsController.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 15.10.18.
|
||||
// Copyright © 2018 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SettingsController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var totalCapsLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var totalImagesLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var recognizedCapsLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var imagesStatsLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var databaseUpdatesLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var dropboxAccountLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var countsLabel: UILabel!
|
||||
|
||||
private var isUpdatingCounts = false
|
||||
|
||||
private var isUploadingNameFile = false
|
||||
|
||||
private var isUpdatingThumbnails = false
|
||||
|
||||
private var isUpdatingColors = false
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
updateDropboxStatus()
|
||||
updateNameFileStats()
|
||||
updateDatabaseStats()
|
||||
|
||||
(navigationController as! NavigationController).allowLandscape = false
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
setClassifierChoice(Persistence.useMobileNet)
|
||||
}
|
||||
|
||||
private func updateThumbnails() {
|
||||
isUpdatingThumbnails = true
|
||||
for cap in Cap.all.values {
|
||||
cap.makeThumbnail()
|
||||
}
|
||||
isUpdatingThumbnails = false
|
||||
}
|
||||
|
||||
private func updateColors() {
|
||||
isUpdatingColors = true
|
||||
Cap.shouldSave = false
|
||||
for cap in Cap.all.values {
|
||||
cap.makeAverageColor()
|
||||
}
|
||||
Cap.shouldSave = true
|
||||
isUpdatingColors = false
|
||||
}
|
||||
|
||||
private func updateDatabaseStats() {
|
||||
let totalCaps = Cap.totalCapCount
|
||||
totalCapsLabel.text = "\(totalCaps) caps"
|
||||
totalImagesLabel.text = "\(Cap.imageCount) images"
|
||||
let recognizedCaps = Persistence.recognizedCapCount
|
||||
let newCapCount = totalCaps - recognizedCaps
|
||||
recognizedCapsLabel.text = "\(newCapCount) new caps"
|
||||
let ratio = Float(Cap.imageCount)/Float(Cap.totalCapCount)
|
||||
let (images, count) = Cap.getCapStatistics().enumerated().first(where: { $0.element != 0 })!
|
||||
imagesStatsLabel.text = String(format: "%.2f images per cap, lowest count: %d (%dx)", ratio, images, count)
|
||||
}
|
||||
|
||||
private func updateNameFileStats() {
|
||||
let capCount = Cap.totalCapCount - Persistence.lastUploadedCapCount
|
||||
let imageCount = Cap.imageCount - Persistence.lastUploadedImageCount
|
||||
databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images"
|
||||
}
|
||||
|
||||
private func updateDropboxStatus() {
|
||||
dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in"
|
||||
}
|
||||
|
||||
private func setClassifierChoice(_ useMobileNet: Bool) {
|
||||
tableView.cellForRow(at: IndexPath(row: 0, section: 0))?.accessoryType = useMobileNet ? .checkmark : .none
|
||||
}
|
||||
|
||||
private func toggleClassifier() {
|
||||
let newValue = !Persistence.useMobileNet
|
||||
Persistence.useMobileNet = newValue
|
||||
setClassifierChoice(newValue)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||
switch indexPath.section {
|
||||
case 0: // Choose models
|
||||
return true
|
||||
case 1: // Mosaic
|
||||
return true
|
||||
case 2: // Database
|
||||
return indexPath.row == 2 && !isUploadingNameFile
|
||||
case 3: // Refresh
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
return !isUpdatingCounts
|
||||
case 1:
|
||||
return !isUpdatingThumbnails
|
||||
case 2:
|
||||
return !isUpdatingColors
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case 4: // Dropbox account
|
||||
return true
|
||||
case 5: // Log file
|
||||
return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
switch indexPath.section {
|
||||
case 0: // Choose models
|
||||
return indexPath
|
||||
case 1: // Mosaic
|
||||
return indexPath
|
||||
case 2: // Database
|
||||
return (indexPath.row == 2 && !isUploadingNameFile) ? indexPath : nil
|
||||
case 3: // Refresh count
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
return isUpdatingCounts ? nil : indexPath
|
||||
case 1:
|
||||
return isUpdatingThumbnails ? nil : indexPath
|
||||
case 2:
|
||||
return isUpdatingColors ? nil : indexPath
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
case 4: // Dropbox account
|
||||
return indexPath
|
||||
case 5: // Log file
|
||||
return indexPath
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
switch indexPath.section {
|
||||
case 0: // Choose models
|
||||
toggleClassifier()
|
||||
case 2: // Upload
|
||||
if indexPath.row == 2 && !isUploadingNameFile {
|
||||
uploadNameFile()
|
||||
}
|
||||
case 3: // Refresh count
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
updateCounts()
|
||||
case 1:
|
||||
updateThumbnails()
|
||||
case 2:
|
||||
updateColors()
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCounts() {
|
||||
isUpdatingCounts = true
|
||||
Cap.shouldSave = false
|
||||
// TODO: Don't make all requests at the same time
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let list = Cap.all.keys.sorted()
|
||||
let total = list.count
|
||||
var finished = 0
|
||||
let chunks = list.chunked(into: 10)
|
||||
for chunk in chunks {
|
||||
self.updateCounts(ids: chunk)
|
||||
finished += 10
|
||||
DispatchQueue.main.async {
|
||||
self.countsLabel.text = "\(finished) / \(total) finished"
|
||||
}
|
||||
}
|
||||
self.isUpdatingCounts = false
|
||||
Cap.shouldSave = true
|
||||
DispatchQueue.main.async {
|
||||
self.countsLabel.text = "Refresh image counts"
|
||||
self.updateDatabaseStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCounts(ids: [Int]) {
|
||||
var count = ids.count
|
||||
let s = DispatchSemaphore(value: 0)
|
||||
for cap in ids {
|
||||
Cap.all[cap]!.updateCount { _ in
|
||||
count -= 1
|
||||
if count == 0 {
|
||||
s.signal()
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = s.wait(timeout: .now() + .seconds(30))
|
||||
event("Finished updating ids \(ids.first!) to \(ids.last!)")
|
||||
}
|
||||
|
||||
private func uploadNameFile() {
|
||||
event("Uploading name file")
|
||||
isUploadingNameFile = true
|
||||
Cap.saveAndUpload() { _ in
|
||||
self.isUploadingNameFile = false
|
||||
self.updateNameFileStats()
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleDropbox() {
|
||||
guard !DropboxController.shared.isEnabled else {
|
||||
DropboxController.shared.signOut()
|
||||
updateDropboxStatus()
|
||||
return
|
||||
}
|
||||
|
||||
DropboxController.shared.setup(in: self)
|
||||
updateDropboxStatus()
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
guard let id = segue.identifier else {
|
||||
return
|
||||
}
|
||||
switch id {
|
||||
case "showMosaic":
|
||||
(navigationController as! NavigationController).allowLandscape = true
|
||||
case "showLog":
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsController: Logger {
|
||||
|
||||
static let logToken = "[Settings]"
|
||||
}
|
||||
|
||||
private extension Array {
|
||||
func chunked(into size: Int) -> [[Element]] {
|
||||
return stride(from: 0, to: count, by: size).map {
|
||||
Array(self[$0 ..< Swift.min($0 + size, count)])
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,9 @@ enum SortCriteria: Int {
|
||||
case match = 3
|
||||
}
|
||||
|
||||
protocol SortControllerDelegate {
|
||||
protocol SortControllerDelegate: class {
|
||||
|
||||
var sortControllerShouldIncludeMatchOption: Bool { get }
|
||||
|
||||
func sortController(didSelect sortType: SortCriteria, ascending: Bool)
|
||||
}
|
||||
@ -26,32 +28,19 @@ class SortController: UITableViewController {
|
||||
|
||||
var ascending: Bool = true
|
||||
|
||||
var delegate: SortControllerDelegate?
|
||||
private var includeMatches: Bool {
|
||||
delegate?.sortControllerShouldIncludeMatchOption ?? false
|
||||
}
|
||||
|
||||
weak var delegate: SortControllerDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let height = Cap.hasMatches ? 310 : 270
|
||||
let height = includeMatches ? 298 : 258
|
||||
preferredContentSize = CGSize(width: 200, height: height)
|
||||
}
|
||||
|
||||
private func setCell() {
|
||||
for i in 0..<4 {
|
||||
let index = IndexPath(row: i, section: 1)
|
||||
let cell = tableView.cellForRow(at: index)
|
||||
cell?.accessoryType = i == selected.rawValue ? .checkmark : .none
|
||||
}
|
||||
let index = IndexPath(row: 0, section: 0)
|
||||
let cell = tableView.cellForRow(at: index)
|
||||
cell?.accessoryType = ascending ? .checkmark : .none
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
setCell()
|
||||
}
|
||||
|
||||
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||
let generator = UIImpactFeedbackGenerator(style: style)
|
||||
generator.impactOccurred()
|
||||
@ -63,7 +52,7 @@ class SortController: UITableViewController {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard indexPath.section == 1 else {
|
||||
ascending = !ascending
|
||||
setCell()
|
||||
tableView.reloadData()
|
||||
delegate?.sortController(didSelect: selected, ascending: ascending)
|
||||
giveFeedback(.light)
|
||||
return
|
||||
@ -76,17 +65,16 @@ class SortController: UITableViewController {
|
||||
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
guard indexPath.row == 3 else { return indexPath }
|
||||
return Cap.hasMatches ? indexPath : nil
|
||||
return includeMatches ? indexPath : nil
|
||||
}
|
||||
|
||||
/*
|
||||
// MARK: - Navigation
|
||||
|
||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
// Get the new view controller using segue.destination.
|
||||
// Pass the selected object to the new view controller.
|
||||
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
cell.accessoryType = ascending ? .checkmark : .none
|
||||
default:
|
||||
cell.accessoryType = indexPath.row == selected.rawValue ? .checkmark : .none
|
||||
break
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user