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

Prevent complete animators from causing drawing #505

Merged
merged 5 commits into from
Jun 30, 2021
Merged
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
LastUpgradeVersion = "1250"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
2 changes: 1 addition & 1 deletion Apps/DebugApp/DebugApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1100;
LastUpgradeCheck = 1240;
LastUpgradeCheck = 1250;
ORGANIZATIONNAME = "Mapbox Inc";
TargetAttributes = {
35F08DD72347987700768B9F = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
2 changes: 1 addition & 1 deletion Apps/Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1200;
LastUpgradeCheck = 1240;
LastUpgradeCheck = 1250;
TargetAttributes = {
077C4EE9252F7E88007636F1 = {
CreatedOnToolsVersion = 12.0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Mapbox welcomes participation and contributions from everyone.
### Bug fixes 🐞

* Fixed a crash at runtime, when an application is built with the xcframework (direct download) rather than from source (i.e. Swift Package Manager). ([#497](https://github.com/mapbox/mapbox-maps-ios/pull/497))
* Fixed an issue where animators created by fly to and ease to were not released until the next fly to or ease to began. ([#505](https://github.com/mapbox/mapbox-maps-ios/pull/505))
* Fixed an issue where a complete animator would trigger redrawing unnecessarily. ([#505](https://github.com/mapbox/mapbox-maps-ios/pull/505))

## 10.0.0-rc.2 - June 23, 2021

Expand Down
376 changes: 182 additions & 194 deletions Mapbox/MapboxMaps.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ let package = Package(
name: "MapboxMaps",
dependencies: ["MapboxCoreMaps", "Turf", "MapboxMobileEvents", "MapboxCommon"],
exclude: [
"MapView/Info.plist"
"Info.plist"
]
),
.testTarget(
name: "MapboxMapsTests",
dependencies: ["MapboxMaps"],
exclude: [
"MapView/Info.plist",
"MapView/Integration Tests/HTTP/HTTPIntegrationTests.swift",
"Info.plist",
"Foundation/Integration Tests/HTTP/HTTPIntegrationTests.swift",
],
resources: [
.copy("Foundation/GeoJSON/Fixtures/point.geojson"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,10 @@ extension CameraAnimationsManager: CameraAnimatorDelegate {

// MARK: CameraAnimatorDelegate functions
func schedulePendingCompletion(forAnimator animator: CameraAnimator, completion: @escaping AnimationCompletion, animatingPosition: UIViewAnimatingPosition) {
guard let mapView = mapView else { return }
mapView.pendingAnimatorCompletionBlocks.append((completion, animatingPosition))
mapView?.pendingAnimatorCompletionBlocks.append(
PendingAnimationCompletion(
completion: completion,
animatingPosition: animatingPosition))
}

var camera: CameraState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ public class CameraAnimationsManager {

mapView.addCameraAnimator(flyToAnimator)

flyToAnimator.addCompletion { (position) in
flyToAnimator.addCompletion { [weak self, weak flyToAnimator] (position) in
if let internalAnimator = self?.internalAnimator,
let animator = flyToAnimator,
internalAnimator === animator {
self?.internalAnimator = nil
}
// Call the developer-provided completion (if present)
completion?(position)
}
Expand Down Expand Up @@ -128,7 +133,12 @@ public class CameraAnimationsManager {
}

// Nil out the `internalAnimator` once the "ease to" finishes
animator.addCompletion { (position) in
animator.addCompletion { [weak self, weak animator] (position) in
if let internalAnimator = self?.internalAnimator,
let animator = animator,
internalAnimator === animator {
self?.internalAnimator = nil
}
completion?(position)
}

Expand Down
9 changes: 9 additions & 0 deletions Sources/MapboxMaps/Foundation/DisplayLinkProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import QuartzCore

internal protocol DisplayLinkProtocol: AnyObject {
var preferredFramesPerSecond: Int { get set }
func add(to runloop: RunLoop, forMode mode: RunLoop.Mode)
func invalidate()
}

extension CADisplayLink: DisplayLinkProtocol {}
14 changes: 14 additions & 0 deletions Sources/MapboxMaps/Foundation/ForwardingDispalyLinkTarget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import QuartzCore

internal class ForwardingDisplayLinkTarget: NSObject {
private let handler: (CADisplayLink) -> Void

internal init(handler: @escaping (CADisplayLink) -> Void) {
self.handler = handler
super.init()
}

@objc internal func update(with displayLink: CADisplayLink) {
handler(displayLink)
}
}
111 changes: 52 additions & 59 deletions Sources/MapboxMaps/Foundation/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import UIKit
import Turf

internal typealias PendingAnimationCompletion = (completion: AnimationCompletion, animatingPosition: UIViewAnimatingPosition)

open class MapView: UIView {

// mapbox map depends on MapInitOptions, which is not available until
Expand Down Expand Up @@ -90,7 +88,7 @@ open class MapView: UIView {
private var needsDisplayRefresh: Bool = false
private var dormant: Bool = false
private var displayCallback: (() -> Void)?
@objc dynamic internal var displayLink: CADisplayLink?
private var displayLink: DisplayLinkProtocol?

/// Holding onto this value that comes from `MapOptions` since there is a race condition between
/// getting a `MetalView`, and intializing a `MapView`
Expand All @@ -102,6 +100,8 @@ open class MapView: UIView {
/// a nib.
@IBOutlet internal private(set) weak var mapInitOptionsProvider: MapInitOptionsProvider?

private let dependencyProvider: MapViewDependencyProviderProtocol

/// The preferred frames per second used for map rendering
public var preferredFramesPerSecond: PreferredFPS = .maximum {
didSet {
Expand Down Expand Up @@ -132,6 +132,20 @@ open class MapView: UIView {
/// - mapInitOptions: `MapInitOptions`; default uses
/// `ResourceOptionsManager.default` to retrieve a shared default resource option, including the access token.
public init(frame: CGRect, mapInitOptions: MapInitOptions = MapInitOptions()) {
dependencyProvider = MapViewDependencyProvider()
super.init(frame: frame)
commonInit(mapInitOptions: mapInitOptions, overridingStyleURI: nil)
}

required public init?(coder: NSCoder) {
dependencyProvider = MapViewDependencyProvider()
super.init(coder: coder)
}

internal init(frame: CGRect,
mapInitOptions: MapInitOptions,
dependencyProvider: MapViewDependencyProviderProtocol) {
self.dependencyProvider = dependencyProvider
super.init(frame: frame)
commonInit(mapInitOptions: mapInitOptions, overridingStyleURI: nil)
}
Expand Down Expand Up @@ -265,44 +279,41 @@ open class MapView: UIView {
mapboxMap.size = bounds.size
}

func validateDisplayLink() {
if superview != nil
&& window != nil
&& displayLink == nil {
let target = BaseMapViewProxy(mapView: self)
displayLink = window?.screen.displayLink(withTarget: target, selector: #selector(target.updateFromDisplayLink))

private func validateDisplayLink() {
if let window = window, displayLink == nil {
displayLink = dependencyProvider.makeDisplayLink(
window: window,
target: ForwardingDisplayLinkTarget { [weak self] in
self?.updateFromDisplayLink(displayLink: $0)
},
selector: #selector(ForwardingDisplayLinkTarget.update(with:)))
updateDisplayLinkPreferredFramesPerSecond()
displayLink?.add(to: .current, forMode: .common)

}
}

@objc func updateFromDisplayLink(displayLink: CADisplayLink) {
private func updateFromDisplayLink(displayLink: CADisplayLink) {
if window == nil {
return
}

if needsDisplayRefresh
|| cameraAnimatorsSet.allObjects.count > 0
|| !pendingAnimatorCompletionBlocks.isEmpty {
needsDisplayRefresh = false

for animator in cameraAnimatorsSet.allObjects {
if let cameraOptions = animator.currentCameraOptions {
mapboxMap.setCamera(to: cameraOptions)
}
for animator in cameraAnimatorsSet.allObjects {
if let cameraOptions = animator.currentCameraOptions {
mapboxMap.setCamera(to: cameraOptions)
}
}

/// This executes the series of scheduled animation completion blocks and also removes them from the list
while !pendingAnimatorCompletionBlocks.isEmpty {
let pendingCompletion = pendingAnimatorCompletionBlocks.removeFirst()
let completion = pendingCompletion.completion
let animatingPosition = pendingCompletion.animatingPosition
completion(animatingPosition)
}
/// This executes the series of scheduled animation completion blocks and also removes them from the list
while !pendingAnimatorCompletionBlocks.isEmpty {
let pendingCompletion = pendingAnimatorCompletionBlocks.removeFirst()
let completion = pendingCompletion.completion
let animatingPosition = pendingCompletion.animatingPosition
completion(animatingPosition)
}

self.displayCallback?()
if needsDisplayRefresh {
needsDisplayRefresh = false
displayCallback?()
}
}

Expand Down Expand Up @@ -353,8 +364,17 @@ open class MapView: UIView {
eventsListener.push(event: .memoryWarning)
}

required public init?(coder: NSCoder) {
super.init(coder: coder)
// MARK: Telemetry

private func setUpTelemetryLogging() {
guard let validResourceOptions = resourceOptions else { return }
let eventsListener = EventsManager(accessToken: validResourceOptions.accessToken)

DispatchQueue.main.async {
eventsListener.push(event: .map(event: .loaded))
}

self.eventsListener = eventsListener
}
}

Expand All @@ -368,7 +388,7 @@ extension MapView: DelegatingMapClientDelegate {
}

internal func getMetalView(for metalDevice: MTLDevice?) -> MTKView? {
let metalView = MTKView(frame: frame, device: metalDevice)
let metalView = dependencyProvider.makeMetalView(frame: frame, device: metalDevice)
displayCallback = {
metalView.setNeedsDisplay()
}
Expand All @@ -389,30 +409,3 @@ extension MapView: DelegatingMapClientDelegate {
return metalView
}
}

private class BaseMapViewProxy: NSObject {
weak var mapView: MapView?

init(mapView: MapView) {
self.mapView = mapView
super.init()
}

@objc func updateFromDisplayLink(displayLink: CADisplayLink) {
mapView?.updateFromDisplayLink(displayLink: displayLink)
}
}

// MARK: Telemetry
extension MapView {
internal func setUpTelemetryLogging() {
guard let validResourceOptions = resourceOptions else { return }
let eventsListener = EventsManager(accessToken: validResourceOptions.accessToken)

DispatchQueue.main.async {
eventsListener.push(event: .map(event: .loaded))
}

self.eventsListener = eventsListener
}
}
16 changes: 16 additions & 0 deletions Sources/MapboxMaps/Foundation/MapViewDependencyProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import MetalKit

internal protocol MapViewDependencyProviderProtocol {
func makeMetalView(frame: CGRect, device: MTLDevice?) -> MTKView
func makeDisplayLink(window: UIWindow, target: Any, selector: Selector) -> DisplayLinkProtocol?
}

internal final class MapViewDependencyProvider: MapViewDependencyProviderProtocol {
func makeMetalView(frame: CGRect, device: MTLDevice?) -> MTKView {
MTKView(frame: frame, device: device)
}

func makeDisplayLink(window: UIWindow, target: Any, selector: Selector) -> DisplayLinkProtocol? {
window.screen.displayLink(withTarget: target, selector: selector)
}
}
Comment on lines +3 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ™‡πŸΎ this is awesome.. We've needed to do this for a long time!

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import UIKit

internal struct PendingAnimationCompletion {
var completion: AnimationCompletion
var animatingPosition: UIViewAnimatingPosition
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import XCTest
@testable import MapboxMaps
import MapboxCoreMaps

class MapViewIntegrationTests: IntegrationTestCase {
final class MapViewIntegrationTests: IntegrationTestCase {
var rootView: UIView!
var mapView: MapView!
var dataPathURL: URL!
Expand Down Expand Up @@ -77,46 +77,6 @@ class MapViewIntegrationTests: IntegrationTestCase {
XCTAssertNil(weakMapView)
}

func testUpdatePreferredFPS() {
let originalFPS = mapView.preferredFramesPerSecond
XCTAssertNotNil(originalFPS)
XCTAssertEqual(originalFPS.rawValue, 0)

let newFPS = 12
mapView.preferredFramesPerSecond = PreferredFPS(rawValue: newFPS)
XCTAssertNotEqual(originalFPS, mapView.preferredFramesPerSecond)
XCTAssertEqual(mapView.preferredFramesPerSecond.rawValue, newFPS)
}

func testUpdateFromDisplayLink() {
let originalFPS = mapView.preferredFramesPerSecond
XCTAssertNotNil(mapView.displayLink)
mapView.preferredFramesPerSecond = .lowPower
XCTAssertNotEqual(originalFPS, mapView.preferredFramesPerSecond)
XCTAssertEqual(mapView.preferredFramesPerSecond.rawValue, mapView.displayLink?.preferredFramesPerSecond)
}

func testAnimatorCompletionBlocksAreRemoved() {
let firstCompletion = PendingAnimationCompletion(completion: {_ in}, animatingPosition: .end)
let secondCompletion = PendingAnimationCompletion(completion: {_ in}, animatingPosition: .current)

mapView.pendingAnimatorCompletionBlocks.append(firstCompletion)
mapView.pendingAnimatorCompletionBlocks.append(secondCompletion)
mapView.scheduleRepaint()
XCTAssertEqual(mapView.pendingAnimatorCompletionBlocks.count, 2)

mapView.updateFromDisplayLink(displayLink: CADisplayLink())
XCTAssertEqual(mapView.pendingAnimatorCompletionBlocks.count, 0)
}

func testUpdateFromDisplayLinkWhenNil() {
mapView.displayLink = nil
mapView.preferredFramesPerSecond = .maximum

XCTAssertNil(mapView.displayLink?.preferredFramesPerSecond)
XCTAssertNotEqual(mapView.preferredFramesPerSecond.rawValue, mapView.displayLink?.preferredFramesPerSecond)
}

func testDataClearing() {
defer {
mapView?.removeFromSuperview()
Expand Down
Loading