forked from vospennikov/ClusterMap
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request vospennikov#1 from vospennikov/feature/swiftui_sup…
…port Add SwiftUI support
- Loading branch information
Showing
115 changed files
with
3,203 additions
and
1,832 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
retain_public: true | ||
targets: | ||
- ClusterMap |
2 changes: 1 addition & 1 deletion
2
...ource/CLLocationCoordinate2D.Random.swift → ...ource/CLLocationCoordinate2D+Random.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// | ||
// CoordinateRandomizer.swift | ||
// DataSource | ||
// | ||
// Created by Mikhail Vospennikov on 08.02.2023. | ||
// | ||
|
||
import Foundation | ||
import MapKit | ||
|
||
public struct CoordinateRandomizer { | ||
public init() { } | ||
|
||
public func generateRandomCoordinates(count: Int, within region: MKCoordinateRegion) -> [CLLocationCoordinate2D] { | ||
(0..<count).map { _ in | ||
region.randomCoordinate() | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
2 changes: 1 addition & 1 deletion
2
...ataSource/MKCoordinateRegion.Cities.swift → ...ataSource/MKCoordinateRegion+Cities.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
31 changes: 31 additions & 0 deletions
31
Example/Example-AppKit/App/Map/Annotations/ClusterAnnotation.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// | ||
// ClusterAnnotation.swift | ||
// Example-AppKit | ||
// | ||
// Created by Mikhail Vospennikov on 03.09.2023. | ||
// | ||
|
||
import ClusterMap | ||
import MapKit | ||
|
||
final class ClusterAnnotation: PointAnnotation { | ||
var memberAnnotations = [MKAnnotation]() | ||
|
||
override func isEqual(_ object: Any?) -> Bool { | ||
guard let object = object as? ClusterAnnotation else { return false } | ||
|
||
if self === object { | ||
return true | ||
} | ||
|
||
if coordinate != object.coordinate { | ||
return false | ||
} | ||
|
||
if memberAnnotations.count != object.memberAnnotations.count { | ||
return false | ||
} | ||
|
||
return memberAnnotations.map(\.coordinate) == object.memberAnnotations.map(\.coordinate) | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
Example/Example-AppKit/App/Map/Annotations/ClusterAnnotationView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// | ||
// ClusterAnnotationView.swift | ||
// Example-AppKit | ||
// | ||
// Created by Mikhail Vospennikov on 03.09.2023. | ||
// | ||
|
||
import MapKit | ||
|
||
final class ClusterAnnotationView: MKAnnotationView { | ||
lazy var countLabel: NSTextField = { | ||
let label = NSTextField() | ||
label.isBezeled = false | ||
label.isEditable = false | ||
label.isSelectable = false | ||
label.maximumNumberOfLines = 1 | ||
label.backgroundColor = .clear | ||
label.font = .boldSystemFont(ofSize: 13) | ||
label.textColor = .white | ||
label.alignment = .center | ||
addSubview(label) | ||
return label | ||
}() | ||
|
||
override func prepareForDisplay() { | ||
super.prepareForDisplay() | ||
|
||
if let annotation = annotation as? ClusterAnnotation { | ||
configure(annotation) | ||
} | ||
} | ||
|
||
func configure(_ annotation: ClusterAnnotation) { | ||
countLabel.stringValue = "\(annotation.memberAnnotations.count)" | ||
updateLayout() | ||
} | ||
|
||
var backgroundColor: NSColor? { | ||
get { | ||
guard let layer, let backgroundColor = layer.backgroundColor else { return nil } | ||
return NSColor(cgColor: backgroundColor) | ||
} | ||
set { | ||
layer?.backgroundColor = newValue?.cgColor | ||
} | ||
} | ||
|
||
private func updateLayout() { | ||
updateFrameSize() | ||
placeLabelToCenter() | ||
drawBackground() | ||
} | ||
|
||
private func updateFrameSize() { | ||
countLabel.sizeToFit() | ||
let maxLength = max(countLabel.bounds.width, countLabel.bounds.height) | ||
frame.size = CGSize(width: maxLength, height: maxLength) | ||
} | ||
|
||
private func placeLabelToCenter() { | ||
countLabel.frame.origin.x = bounds.width / 2 - countLabel.bounds.width / 2 | ||
countLabel.frame.origin.y = bounds.height / 2 - countLabel.bounds.height / 2 | ||
} | ||
|
||
private func drawBackground() { | ||
layer?.cornerRadius = frame.width / 2 | ||
layer?.masksToBounds = true | ||
layer?.borderColor = NSColor.white.cgColor | ||
layer?.borderWidth = 1.5 | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
Example/Example-AppKit/App/Map/Annotations/PointAnnotation.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// | ||
// PointAnnotation.swift | ||
// Example-AppKit | ||
// | ||
// Created by Mikhail Vospennikov on 03.09.2023. | ||
// | ||
|
||
import ClusterMap | ||
import Foundation | ||
import MapKit | ||
|
||
class PointAnnotation: MKPointAnnotation, CoordinateIdentifiable, Identifiable { | ||
var id: UUID = .init() | ||
|
||
override func isEqual(_ object: Any?) -> Bool { | ||
guard let object = object as? PointAnnotation else { return false } | ||
return coordinate == object.coordinate && self === object | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
// | ||
// MapViewController.swift | ||
// Example-AppKit | ||
// | ||
// Created by Mikhail Vospennikov on 10.02.2023. | ||
// | ||
|
||
import ClusterMap | ||
import Cocoa | ||
import DataSource | ||
import MapKit | ||
|
||
final class MapViewController: NSViewController { | ||
private lazy var mapView = MKMapView(frame: .zero) | ||
private lazy var clusterManager = ClusterManager<PointAnnotation>( | ||
configuration: .init(maxZoomLevel: 17, minCountForClustering: 3, clusterPosition: .nearCenter) | ||
) | ||
private var mapViewDelegate: MapViewDelegate? | ||
private lazy var dataSource = CoordinateRandomizer() | ||
private var annotations: [PointAnnotation] = [] | ||
|
||
override func loadView() { | ||
view = NSView(frame: NSRect(x: 0, y: 0, width: 640, height: 480)) | ||
} | ||
|
||
override func viewDidLoad() { | ||
super.viewDidLoad() | ||
configureHierarchy() | ||
} | ||
|
||
private func configureHierarchy() { | ||
configureMap() | ||
configureMapControls() | ||
} | ||
} | ||
|
||
// MARK: - Layout | ||
private extension MapViewController { | ||
func configureMap() { | ||
let mapView = MKMapView(frame: view.bounds) | ||
mapView.isZoomEnabled = true | ||
mapView.isPitchEnabled = false | ||
mapView.isRotateEnabled = false | ||
mapView.isScrollEnabled = true | ||
mapView.showsZoomControls = true | ||
mapView.setRegion(.sanFrancisco, animated: false) | ||
self.mapView = mapView | ||
view.addSubview(mapView) | ||
mapView.autoresizingMask = [.height, .width] | ||
|
||
let delegate = MapViewDelegate( | ||
regionDidChange: { newRegion in | ||
Task { await self.reloadMap() } | ||
} | ||
) | ||
mapView.delegate = delegate | ||
mapViewDelegate = delegate | ||
} | ||
|
||
func configureMapControls() { | ||
let controlsContainerView = NSStackView() | ||
controlsContainerView.spacing = 16.0 | ||
controlsContainerView.orientation = .vertical | ||
controlsContainerView.addArrangedSubview(buildAnnotationActionsControl()) | ||
|
||
view.addSubview(controlsContainerView) | ||
controlsContainerView.translatesAutoresizingMaskIntoConstraints = false | ||
NSLayoutConstraint.activate([ | ||
controlsContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | ||
controlsContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | ||
controlsContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8.0), | ||
]) | ||
} | ||
|
||
func buildAnnotationActionsControl() -> NSView { | ||
let containerView = NSStackView() | ||
containerView.spacing = 8.0 | ||
containerView.orientation = .horizontal | ||
containerView.distribution = .fillProportionally | ||
containerView.addArrangedSubview( | ||
NSButton(title: "Add annotations", target: self, action: #selector(addActionHandler)) | ||
) | ||
containerView.addArrangedSubview( | ||
NSButton(title: "Remove annotations", target: self, action: #selector(removeActionHandler)) | ||
) | ||
return containerView | ||
} | ||
} | ||
|
||
// MARK: - Target handlers | ||
@objc private extension MapViewController { | ||
func addActionHandler(_ sender: NSButton) { | ||
Task { | ||
let annotations = dataSource.generateRandomCoordinates(count: 10000, within: mapView.region) | ||
await addAnnotations(annotations) | ||
} | ||
} | ||
|
||
func removeActionHandler(_ sender: NSButton) { | ||
clusterManager.removeAll() | ||
Task { await reloadMap() } | ||
} | ||
} | ||
|
||
extension MapViewController { | ||
func addAnnotations(_ coordinates: [CLLocationCoordinate2D]) async { | ||
clusterManager.add(coordinates.map { | ||
let point = PointAnnotation() | ||
point.coordinate = $0 | ||
return point | ||
}) | ||
async let changes = clusterManager.reload(mapViewSize: mapView.bounds.size, coordinateRegion: mapView.region) | ||
await applyChanges(changes) | ||
} | ||
|
||
func reloadMap() async { | ||
async let changes = clusterManager.reload(mapViewSize: mapView.bounds.size, coordinateRegion: mapView.region) | ||
await applyChanges(changes) | ||
} | ||
|
||
@MainActor | ||
private func applyChanges(_ difference: ClusterManager<PointAnnotation>.Difference) { | ||
for annotationType in difference.removals { | ||
switch annotationType { | ||
case .annotation(let annotation): | ||
annotations.removeAll(where: { $0 == annotation }) | ||
mapView.removeAnnotation(annotation) | ||
case .cluster(let clusterAnnotation): | ||
if let result = annotations.enumerated().first(where: { $0.element.id == clusterAnnotation.id }) { | ||
annotations.remove(at: result.offset) | ||
mapView.removeAnnotation(result.element) | ||
} | ||
} | ||
} | ||
for annotationType in difference.insertions { | ||
switch annotationType { | ||
case .annotation(let annotation): | ||
annotations.append(annotation) | ||
mapView.addAnnotation(annotation) | ||
case .cluster(let clusterAnnotation): | ||
let cluster = ClusterAnnotation() | ||
cluster.id = clusterAnnotation.id | ||
cluster.coordinate = clusterAnnotation.coordinate | ||
cluster.memberAnnotations = clusterAnnotation.memberAnnotations | ||
annotations.append(cluster) | ||
mapView.addAnnotation(cluster) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
Oops, something went wrong.