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

Fixed Annotations HitTesting & Added annotation anchor v2 #38

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 67 additions & 14 deletions Sources/Annotations/MKMapAnnotationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,57 @@ class MKMapAnnotationView<Content: View>: MKAnnotationView {

private var controller: NativeHostingController<Content>?

// MARK: Computed Properties

override var intrinsicContentSize: CGSize {
controller?.view.intrinsicContentSize
?? .init(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}

private var intrinsicContentFrame: CGRect {
let size = intrinsicContentSize
return CGRect(origin: .init(x: -size.width / 2, y: -size.height / 2), size: size)
}

// MARK: Methods

func setup(for mapAnnotation: ViewMapAnnotation<Content>) {
annotation = mapAnnotation.annotation
iakov-kaiumov marked this conversation as resolved.
Show resolved Hide resolved
clusteringIdentifier = mapAnnotation.clusteringIdentifier

#if canImport(UIKit)
backgroundColor = .clear
#endif

let controller = NativeHostingController(rootView: mapAnnotation.content)
addSubview(controller.view)
bounds.size = controller.preferredContentSize
self.controller = controller
}

// MARK: Overrides

#if canImport(UIKit)
let controller = NativeHostingController(rootView: mapAnnotation.content, ignoreSafeArea: true)

override func layoutSubviews() {
super.layoutSubviews()
#if canImport(UIKit)
controller.view.backgroundColor = .clear
#endif

if let controller = controller {
bounds.size = controller.preferredContentSize
addSubview(controller.view)

controller.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
topAnchor.constraint(equalTo: controller.view.topAnchor),
leftAnchor.constraint(equalTo: controller.view.leftAnchor),
rightAnchor.constraint(equalTo: controller.view.rightAnchor),
bottomAnchor.constraint(equalTo: controller.view.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)

self.controller = controller
self.invalidateIntrinsicContentSize()

if let anchor = mapAnnotation.anchorPoint {
centerOffset = CGPoint(
x: (anchor.x - 0.5) * intrinsicContentFrame.width,
y: (anchor.y - 0.5) * intrinsicContentFrame.height
)
}
}

#endif
// MARK: Overrides

override func prepareForReuse() {
super.prepareForReuse()
Expand All @@ -53,6 +79,33 @@ class MKMapAnnotationView<Content: View>: MKAnnotationView {
controller = nil
}

#if canImport(UIKit)

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return intrinsicContentFrame.contains(point)
}

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
controller?.view.frame = intrinsicContentFrame
return controller?.view.hitTest(point, with: event) ?? super.hitTest(point, with: event)
}

#elseif canImport(AppKit)

override func isMousePoint(_ point: NSPoint, in rect: NSRect) -> Bool {
rect.contains(point)
}

override func hitTest(_ point: NSPoint) -> NSView? {
controller?.view.frame = intrinsicContentFrame
guard let view = controller?.view.hitTest(point) ?? super.hitTest(point) else {
return nil
}
return view
}

#endif

}

#endif
5 changes: 5 additions & 0 deletions Sources/Annotations/ViewMapAnnotation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,33 @@ public struct ViewMapAnnotation<Content: View>: MapAnnotation {

public let annotation: MKAnnotation
let clusteringIdentifier: String?
let anchorPoint: CGPoint?
let content: Content

// MARK: Initialization

public init(
coordinate: CLLocationCoordinate2D,
anchorPoint: CGPoint = .init(x: 0.5, y: 0.5),
title: String? = nil,
subtitle: String? = nil,
clusteringIdentifier: String? = nil,
@ViewBuilder content: () -> Content
) {
self.annotation = Annotation(coordinate: coordinate, title: title, subtitle: subtitle)
self.anchorPoint = anchorPoint
self.clusteringIdentifier = clusteringIdentifier
self.content = content()
}

public init(
annotation: MKAnnotation,
anchorPoint: CGPoint = .init(x: 0.5, y: 0.5),
clusteringIdentifier: String? = nil,
@ViewBuilder content: () -> Content
) {
self.annotation = annotation
self.anchorPoint = anchorPoint
self.clusteringIdentifier = clusteringIdentifier
self.content = content()
}
Expand Down
51 changes: 51 additions & 0 deletions Sources/Extensions/UIHostingController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// NativeHostingController.swift
// Map
//
// Created by Paul Kraft on 25.04.22.
//

import SwiftUI

#if !os(watchOS)
extension UIHostingController {
/// This convenience init uses dynamic subclassing to disable safe area behaviour for a UIHostingController
/// This solves bugs with embedded SwiftUI views having redundant insets
/// More on this here: https://defagos.github.io/swiftui_collection_part3/
/// - Parameters:
/// - rootView: The content View
/// - ignoreSafeArea: Disables the safe area insets if true
convenience public init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)

if ignoreSafeArea {
disableSafeArea()
}
}

func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }

let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }

if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets),
imp_implementationWithBlock(safeAreaInsets),
method_getTypeEncoding(method))
}

objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
#endif
45 changes: 25 additions & 20 deletions Sources/Map/Map+Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,16 @@ extension Map {
func update(_ mapView: MKMapView, from newView: Map, context: Context) {
defer { view = newView }
let animation = context.transaction.animation
let animated = animation != nil
updateAnnotations(on: mapView, from: view, to: newView)
updateCamera(on: mapView, context: context, animated: animation != nil)
updateCamera(on: mapView, context: context, animated: animated)
updateInformationVisibility(on: mapView, from: view, to: newView)
updateInteractionModes(on: mapView, from: view, to: newView)
updateOverlays(on: mapView, from: view, to: newView)
updatePointOfInterestFilter(on: mapView, from: view, to: newView)
updateRegion(on: mapView, from: view, to: newView, animated: animation != nil)
updateRegion(on: mapView, from: view, to: newView, animated: animated)
updateType(on: mapView, from: view, to: newView)
updateUserTracking(on: mapView, from: view, to: newView)
updateUserTracking(on: mapView, from: view, to: newView, animated: animated)

if let key = context.environment.mapKey {
MapRegistry[key] = mapView
Expand Down Expand Up @@ -238,11 +239,11 @@ extension Map {
}
}

private func updateUserTracking(on mapView: MKMapView, from previousView: Map?, to newView: Map) {
private func updateUserTracking(on mapView: MKMapView, from previousView: Map?, to newView: Map, animated: Bool) {
if #available(macOS 11, *) {
let newTrackingMode = newView.userTrackingMode.actualValue
if newView.usesUserTrackingMode, mapView.userTrackingMode != newTrackingMode {
mapView.userTrackingMode = newTrackingMode
mapView.setUserTrackingMode(newTrackingMode, animated: animated)
}
}
}
Expand All @@ -253,28 +254,32 @@ extension Map {
guard !regionIsChanging else {
return
}
view?.coordinateRegion = mapView.region
view?.mapRect = mapView.visibleMapRect
DispatchQueue.main.async { [weak self] in
self?.view?.coordinateRegion = mapView.region
self?.view?.mapRect = mapView.visibleMapRect
}
}

@available(macOS 11, *)
public func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
guard let view = view, view.usesUserTrackingMode else {
return
}
switch mode {
case .none:
view.userTrackingMode = .none
case .follow:
view.userTrackingMode = .follow
case .followWithHeading:
#if os(macOS) || os(tvOS)
view.userTrackingMode = .follow
#else
view.userTrackingMode = .followWithHeading
#endif
@unknown default:
assertionFailure("Encountered unknown user tracking mode")
DispatchQueue.main.async {
switch mode {
case .none:
view.userTrackingMode = .none
case .follow:
view.userTrackingMode = .follow
case .followWithHeading:
#if os(macOS) || os(tvOS)
view.userTrackingMode = .follow
#else
view.userTrackingMode = .followWithHeading
#endif
@unknown default:
assertionFailure("Encountered unknown user tracking mode")
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Map/Map+Watch+Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ extension Map: WKInterfaceObjectRepresentable {

private func updateAnnotations(on mapView: WKInterfaceMap, from previousView: Map?, to newView: Map) {
let changes: CollectionDifference<AnnotationItems.Element>
if let previousView = previousView {
if let previousView {
changes = newView.annotationItems.difference(from: previousView.annotationItems) { $0.id == $1.id }
} else {
changes = newView.annotationItems.difference(from: []) { $0.id == $1.id }
Expand Down