Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Annotation efficiency #14

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions HowManyCities.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
6C0EB7A3282C4AFB00E95F6B /* UIDismissableAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0EB7A2282C4AFB00E95F6B /* UIDismissableAlertController.swift */; };
6C0EB7A5282C6B3B00E95F6B /* CityinfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0EB7A4282C6B3B00E95F6B /* CityinfoViewModel.swift */; };
6C0EB7A7282C84EB00E95F6B /* WarningBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0EB7A6282C84EB00E95F6B /* WarningBannerView.swift */; };
6C0EB7A9282C8A2000E95F6B /* CityLimitWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0EB7A8282C8A2000E95F6B /* CityLimitWarning.swift */; };
6C7C8D9828161AA60091E09F /* NormalizedCountryNames.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6C7C8D9728161AA60091E09F /* NormalizedCountryNames.plist */; };
6C7C8D9B2817BFE60091E09F /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7C8D9A2817BFE60091E09F /* OrderedCollections */; };
6C7C8D9E2817C1330091E09F /* MapGuessModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C7C8D9D2817C1330091E09F /* MapGuessModelTests.swift */; };
Expand Down Expand Up @@ -115,6 +117,8 @@
5E08EA20EB451FE21A3BAC71 /* Pods_HowManyCities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HowManyCities.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6C0EB7A2282C4AFB00E95F6B /* UIDismissableAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDismissableAlertController.swift; sourceTree = "<group>"; };
6C0EB7A4282C6B3B00E95F6B /* CityinfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityinfoViewModel.swift; sourceTree = "<group>"; };
6C0EB7A6282C84EB00E95F6B /* WarningBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WarningBannerView.swift; sourceTree = "<group>"; };
6C0EB7A8282C8A2000E95F6B /* CityLimitWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityLimitWarning.swift; sourceTree = "<group>"; };
6C7C8D9728161AA60091E09F /* NormalizedCountryNames.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = NormalizedCountryNames.plist; sourceTree = "<group>"; };
6C7C8D9D2817C1330091E09F /* MapGuessModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapGuessModelTests.swift; sourceTree = "<group>"; };
6C7F4D9B2814F52000F90FDE /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -467,6 +471,7 @@
6C9703D32829CAF9001F7591 /* Ratio.swift */,
6CBBD5E42823BAE7002D9FC2 /* Vibration.swift */,
6C9703D52829CB09001F7591 /* GenericError.swift */,
6C0EB7A8282C8A2000E95F6B /* CityLimitWarning.swift */,
);
path = Models;
sourceTree = "<group>";
Expand All @@ -477,6 +482,7 @@
6CAD802827F3C1140057A41E /* CityAnnotationView.swift */,
6CAD802E27F3D2C20057A41E /* MapGuessStatsBar.swift */,
6C9C42AB281219D0006529E9 /* MapToast.swift */,
6C0EB7A6282C84EB00E95F6B /* WarningBannerView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -805,6 +811,7 @@
6CC3C5802804FB6C00AE5C50 /* NSLayoutConstraint+Extension.swift in Sources */,
6CAD7FE827F381DF0057A41E /* AppDelegate.swift in Sources */,
6C9F60662825AAB90035B0FA /* StateTotalPopulationRenderer.swift in Sources */,
6C0EB7A7282C84EB00E95F6B /* WarningBannerView.swift in Sources */,
6C9C42B528122999006529E9 /* MKOverlayRenderer+Extension.swift in Sources */,
6CBBD5DA2821F7DB002D9FC2 /* CLLocation+Extension.swift in Sources */,
6C82DBE127F569F80084159F /* MKCoordinateRegion+Extension.swift in Sources */,
Expand All @@ -815,6 +822,7 @@
6C9703E0282B3870001F7591 /* CitySortMode.swift in Sources */,
6CBBD5D62821DA8F002D9FC2 /* CityInfoViewController.swift in Sources */,
6C9C42A9281219AE006529E9 /* MKZoomScale+Extension.swift in Sources */,
6C0EB7A9282C8A2000E95F6B /* CityLimitWarning.swift in Sources */,
6CAD7FEA27F381DF0057A41E /* SceneDelegate.swift in Sources */,
6CAD802D27F3CC810057A41E /* CityAnnotation.swift in Sources */,
);
Expand Down
75 changes: 71 additions & 4 deletions HowManyCities/MapGuessViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import SwifterSwift
import MapCache
import OrderedCollections

protocol CityEditDelegate {
func removeCity(_ city: City) -> City?
}

final class MapGuessViewController: UIViewController {

private var viewModel: MapGuessViewModel
Expand All @@ -27,7 +31,7 @@ final class MapGuessViewController: UIViewController {

map.delegate = self

map.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: "MKAnnotationView")
map.register(CityAnnotationView.self, forAnnotationViewWithReuseIdentifier: "CityAnnotationView")

return map
}()
Expand Down Expand Up @@ -62,6 +66,12 @@ final class MapGuessViewController: UIViewController {
return button
}()

private lazy var warningBanner: WarningBannerView = {
let view = WarningBannerView().autolayoutEnabled

return view
}()

private lazy var guessStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [cityInputTextField, countryDropdownButton]).autolayoutEnabled
stackView.axis = .horizontal
Expand Down Expand Up @@ -145,6 +155,8 @@ final class MapGuessViewController: UIViewController {

view.backgroundColor = .systemBackground

mapView.addSubview(warningBanner)

view.addSubview(mapView)
view.addSubview(resetButton)
view.addSubview(finishButton)
Expand All @@ -161,6 +173,10 @@ final class MapGuessViewController: UIViewController {
view.addSubview(moreStatsButton)

NSLayoutConstraint.activate([
warningBanner.topAnchor.constraint(equalTo: mapView.topAnchor),
warningBanner.leadingAnchor.constraint(equalTo: mapView.leadingAnchor),
warningBanner.trailingAnchor.constraint(equalTo: mapView.trailingAnchor),

mapView.topAnchor.constraint(equalTo: view.topAnchor),
mapView.bottomAnchor.constraint(equalTo: view.centerYAnchor, constant: -64),
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
Expand Down Expand Up @@ -216,11 +232,23 @@ final class MapGuessViewController: UIViewController {
mapView.addOverlays(cities.map(by: \.asShape), level: .aboveLabels)
mapView.addAnnotations(annotations)

updateGameState()

return annotations
}

private func updateGameState() {
guessStats.updatePopulationGuessed(viewModel.populationGuessed)
guessStats.updateNumCitiesGuessed(viewModel.numCitiesGuessed)
guessStats.updatePercentageTotalPopulation(viewModel.percentageTotalPopulationGuessed)

return annotations
let warning = viewModel.cityLimitWarning
warningBanner.setState(warning)
if case .unableToSave(_) = warning {
finishButton.isEnabled = false
} else {
finishButton.isEnabled = true
}
}

private func addCustomTileOverlay() {
Expand Down Expand Up @@ -297,10 +325,32 @@ final class MapGuessViewController: UIViewController {

@objc private func didTapMoreStats() {
let vc = GameStatsViewController(statsProvider: viewModel.gameStatsProvider)
vc.cityEditDelegate = self
present(UINavigationController(rootViewController: vc), animated: true)
}
}

extension MapGuessViewController: CityEditDelegate {
func removeCity(_ city: City) -> City? {
if let city = viewModel.removeCity(city) {

if let annotation = mapView.annotations.first(where: { $0.coordinate == city.coordinates /* TODO: GOTTA BE A BETTER WAY... man */ }) {
mapView.removeAnnotation(annotation)
}

if let overlay = mapView.overlays.first(where: { $0.coordinate == city.coordinates }) {
mapView.removeOverlay(overlay)
}

updateGameState()
return city
} else {
// TODO: Error messaging
return nil
}
}
}

// TODO: Move this into separate file or vc???
extension MapGuessViewController {
func showToast(_ message: String, toastType: ToastType) {
Expand Down Expand Up @@ -460,9 +510,26 @@ extension MapGuessViewController: MKMapViewDelegate {
return .init(overlay: overlay)
}

func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
for annotation in mapView.annotations(in: mapView.visibleMapRect) {
if let annotation = annotation as? MKAnnotation,
let annotationView = mapView.view(for: annotation) as? CityAnnotationView {
// annotationView.transform = .init(scaleX: scaleFactor, y: scaleFactor) // WARNING!!!!! THIS WILL SCALE THE CALLOUT TOO
annotationView.setZoom(mapView.zoomLevel)
}
}
}

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "MKAnnotationView", for: annotation)
annotationView.canShowCallout = true
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "CityAnnotationView", for: annotation) as? CityAnnotationView
annotationView?.canShowCallout = true
if let annotation = annotation as? CityAnnotation {
annotationView?.frame = .init(x: 0, y: 0, width: annotation.annotationSize, height: annotation.annotationSize)
}
annotationView?.setZoom(mapView.zoomLevel)

// annotationView?.backgroundColor = .black.withAlphaComponent(0.5)

return annotationView
}
}
Expand Down
4 changes: 4 additions & 0 deletions HowManyCities/Models/CityAnnotation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ final class CityAnnotation: NSObject, MKAnnotation {
var title: String?
var subtitle: String?

let annotationSize: CGFloat

init(_ city: City) {
self.coordinate = city.coordinates
self.title = city.fullTitle
self.subtitle = "pop: \(city.population.commaSeparated)" // TODO: Localize

self.annotationSize = log10(Double(city.population))
}
}
14 changes: 14 additions & 0 deletions HowManyCities/Models/CityLimitWarning.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// CityLimitWarning.swift
// HowManyCities
//
// Created by Geoffrey Liu on 5/11/22.
//

import Foundation

enum CityLimitWarning: Equatable {
case none
case warning(_ remaining: Int)
case unableToSave(_ surplus: Int)
}
4 changes: 4 additions & 0 deletions HowManyCities/Models/MapGuessModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ final class MapGuessModel: Codable {
var usedMultiCityInput: Bool = false
var lastRegion: MKCoordinateRegion = .init(center: .zero, span: .full)

func removeCity(_ city: City) -> City? {
guessedCities.remove(city)
}

func resetState() {
guessedCities = .init()
}
Expand Down
19 changes: 19 additions & 0 deletions HowManyCities/Models/MapGuessViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ final class MapGuessViewModel: NSObject {
var populationGuessed: Int { model.populationGuessed }
var percentageTotalPopulationGuessed: Double { model.percentageTotalPopulationGuessed }

var cityLimitWarning: CityLimitWarning {
// TODO: Thresholds depend on game mode
guard let maxCities = model.gameConfiguration?.maxCities else {
return .none
}
let warningThreshold = Int(0.93*Double(maxCities))
if numCitiesGuessed > maxCities {
return .unableToSave(numCitiesGuessed - maxCities)
} else if numCitiesGuessed >= warningThreshold {
return .warning(maxCities - numCitiesGuessed)
} else {
return .none
}
}

init(cities: Cities? = nil) {
super.init()
let decoder = JSONDecoder()
Expand Down Expand Up @@ -110,6 +125,10 @@ final class MapGuessViewModel: NSObject {
model.guessedCities
}

func removeCity(_ city: City) -> City? {
model.removeCity(city)
}

func resetState() {
model.resetState()
}
Expand Down
37 changes: 35 additions & 2 deletions HowManyCities/Stats/GameStatsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ final class GameStatsViewController: UIViewController {
typealias Item = GameStatsViewModel.Item
typealias ElementKind = GameStatsViewModel.ElementKind

var cityEditDelegate: CityEditDelegate?

private lazy var collectionView: UICollectionView = {
let sectionProvider: UICollectionViewCompositionalLayoutSectionProvider = { (sectionIndex, environment) -> NSCollectionLayoutSection? in
guard let section = Section(rawValue: sectionIndex) else { return nil }
Expand Down Expand Up @@ -128,13 +130,44 @@ final class GameStatsViewController: UIViewController {
cell.contentView.layoutMargins = .zero
}

let cityCellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, City> { cell, indexPath, itemIdentifier in
var configuration = UIListContentConfiguration.cell()
let cityCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, City> { cell, indexPath, itemIdentifier in
var configuration = UIListContentConfiguration.valueCell()
configuration.attributedText = self.viewModel.string(for: itemIdentifier)
configuration.directionalLayoutMargins.leading = 0
configuration.directionalLayoutMargins.trailing = 0
// configuration.image = .remove

// let removeImage = UIImageView(image: .remove)

let removeButton = UIButton(primaryAction: .init(title: "", image: .remove) { action in
// var snapshot = self.viewModel.dataSource.snapshot()
// if let index = snapshot.indexOfItem(.city(itemIdentifier)) {
// let ordinalItem = snapshot.indexOfItem(.ordinal(0, (index - 1) / 2, 0))
//
// snapshot.deleteItems([.city(itemIdentifier)]) // TODO: Will have to remove the city from viewmodel too

if let _ = self.cityEditDelegate?.removeCity(itemIdentifier) {
var snapshot = self.viewModel.dataSource.snapshot()
self.viewModel.refreshCityList(&snapshot) // TODO: More efficiently, couldn't we just remove the city item and its associated ordinal?
// A: I guess but then we still have to update the sections that depend on the city
// TODO: FUCK ORDINALS JUST GET RID OF THEM AND RENDER EVERYTHING IN THE SAME CVC!!!!
self.viewModel.refreshStateList(&snapshot)
self.viewModel.refreshTerritoryList(&snapshot)
self.viewModel.refreshOtherStats(&snapshot)

self.viewModel.dataSource.apply(snapshot)
}
})
let customAccessory = UICellAccessory.CustomViewConfiguration(customView: removeButton,
placement: .trailing(displayed: .always),
tintColor: .systemGray)

cell.contentConfiguration = configuration
// cell.accessories = [.delete(displayed: .always, options: .init(isHidden: false, reservedLayoutWidth: .actual, tintColor: .systemGray, backgroundColor: nil), actionHandler: {
// // No fucking clue
// print("???")
// })]
cell.accessories = [.customView(configuration: customAccessory)]
}

let multiCityCellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, [City]> { cell, indexPath, itemIdentifier in
Expand Down
21 changes: 21 additions & 0 deletions HowManyCities/Views/CityAnnotationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,26 @@ import Foundation
import MapKit

final class CityAnnotationView: MKAnnotationView {
var originalFrame: CGRect?

override func prepareForReuse() {
super.prepareForReuse()

originalFrame = nil
}

func setZoom(_ level: Int) {
if originalFrame == nil {
originalFrame = frame
}

guard let originalFrame = originalFrame else {
return
}

// TODO: FINE TUNE THIS
let scaleFactor = pow(1.5, level-3.0)
frame.size.width = originalFrame.size.width * scaleFactor
frame.size.height = originalFrame.size.height * scaleFactor
}
}
Loading