From 885f6e3c02138398d094e49243817a83349b4d50 Mon Sep 17 00:00:00 2001 From: Jason Wray Date: Tue, 31 Jul 2018 15:20:05 -0400 Subject: [ios] Add preferred FPS setting; vary maximum FPS by device capability - Add `MGLMapView.preferredFramesPerSecond`, which can be set with the provided `MGLMapViewPreferredFramesPerSecond` enum values or directly with an integer. - Adaptively set the preferred FPS based on the capabilities of the device: the oldest and least powerful devices are now capped at 30 FPS, which results in a more consistent/smoother experience. --- platform/ios/CHANGELOG.md | 4 +++ platform/ios/ios.xcodeproj/project.pbxproj | 12 +++++++ platform/ios/src/MGLMapView.h | 30 ++++++++++++++++ platform/ios/src/MGLMapView.mm | 55 ++++++++++++++++++++++++++++-- platform/ios/src/UIDevice+MGLAdditions.h | 7 ++++ platform/ios/src/UIDevice+MGLAdditions.m | 51 +++++++++++++++++++++++++++ 6 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 platform/ios/src/UIDevice+MGLAdditions.h create mode 100644 platform/ios/src/UIDevice+MGLAdditions.m diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index d7ac9500d1..1e606702c3 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -4,6 +4,10 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT ## 4.3.0 +### Styles and rendering + +* Added an `MGLMapView.preferredFramesPerSecond` property that controls the rate at which the map view is rendered. The default rate now adapts to device capabilities to provide a smoother experience. ([#12501](https://github.com/mapbox/mapbox-gl-native/issues/12501)) + ### Other changes * Fixed a crash that occurred when the user started a gesture before the drift animation for a previous gesture was complete. ([#12148](https://github.com/mapbox/mapbox-gl-native/pull/12148)) diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 2a9c9b5761..6b883a3def 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -322,6 +322,10 @@ 966FCF531F3C322400F2B6DE /* MGLUserLocationHeadingArrowLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 966FCF501F3C321000F2B6DE /* MGLUserLocationHeadingArrowLayer.h */; }; 966FCF541F3C323300F2B6DE /* MGLUserLocationHeadingArrowLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 966FCF511F3C321000F2B6DE /* MGLUserLocationHeadingArrowLayer.m */; }; 966FCF551F3C323500F2B6DE /* MGLUserLocationHeadingArrowLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 966FCF511F3C321000F2B6DE /* MGLUserLocationHeadingArrowLayer.m */; }; + 967C864B210A9D3C004DF794 /* UIDevice+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 967C8649210A9D3C004DF794 /* UIDevice+MGLAdditions.h */; }; + 967C864C210A9D3C004DF794 /* UIDevice+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 967C8649210A9D3C004DF794 /* UIDevice+MGLAdditions.h */; }; + 967C864D210A9D3C004DF794 /* UIDevice+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 967C864A210A9D3C004DF794 /* UIDevice+MGLAdditions.m */; }; + 967C864E210A9D3C004DF794 /* UIDevice+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 967C864A210A9D3C004DF794 /* UIDevice+MGLAdditions.m */; }; 968F36B51E4D128D003A5522 /* MGLDistanceFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 3557F7AE1E1D27D300CCA5E6 /* MGLDistanceFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96E027231E57C76E004B8E66 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 96E027251E57C76E004B8E66 /* Localizable.strings */; }; 96E516DC2000547000A02306 /* MGLPolyline_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 9654C1251FFC1AB900DB6A19 /* MGLPolyline_Private.h */; }; @@ -992,6 +996,8 @@ 966FCF4B1F3A5C9200F2B6DE /* MGLUserLocationHeadingBeamLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLUserLocationHeadingBeamLayer.m; sourceTree = ""; }; 966FCF501F3C321000F2B6DE /* MGLUserLocationHeadingArrowLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLUserLocationHeadingArrowLayer.h; sourceTree = ""; }; 966FCF511F3C321000F2B6DE /* MGLUserLocationHeadingArrowLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLUserLocationHeadingArrowLayer.m; sourceTree = ""; }; + 967C8649210A9D3C004DF794 /* UIDevice+MGLAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIDevice+MGLAdditions.h"; sourceTree = ""; }; + 967C864A210A9D3C004DF794 /* UIDevice+MGLAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIDevice+MGLAdditions.m"; sourceTree = ""; }; 968F36B41E4D0FC6003A5522 /* ja */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; 96E027241E57C76E004B8E66 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; 96E027271E57C77A004B8E66 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; @@ -1526,6 +1532,8 @@ 96036A00200565C700510F3D /* NSOrthography+MGLAdditions.m */, 35CE61801D4165D9004F2359 /* UIColor+MGLAdditions.h */, 35CE61811D4165D9004F2359 /* UIColor+MGLAdditions.mm */, + 967C8649210A9D3C004DF794 /* UIDevice+MGLAdditions.h */, + 967C864A210A9D3C004DF794 /* UIDevice+MGLAdditions.m */, 30E578111DAA7D690050F07E /* UIImage+MGLAdditions.h */, 30E578121DAA7D690050F07E /* UIImage+MGLAdditions.mm */, DD9BE4F51EB263C50079A3AF /* UIViewController+MGLAdditions.h */, @@ -2258,6 +2266,7 @@ 35D3A1E61E9BE7EB002B38EE /* MGLScaleBar.h in Headers */, 0778DD431F67556700A73B34 /* MGLComputedShapeSource.h in Headers */, DA8848311CBAFA6200AB86E3 /* NSString+MGLAdditions.h in Headers */, + 967C864B210A9D3C004DF794 /* UIDevice+MGLAdditions.h in Headers */, 1FCAE2A220B872A400C577DD /* MGLLocationManager.h in Headers */, DACA86262019218600E9693A /* MGLRasterDEMSource.h in Headers */, 353933F81D3FB79F003F57D7 /* MGLLineStyleLayer.h in Headers */, @@ -2356,6 +2365,7 @@ 4049C29E1DB6CD6C00B3F799 /* MGLPointCollection.h in Headers */, 3566C7671D4A77BA008152BC /* MGLShapeSource.h in Headers */, DA35A29F1CC9E94C00E826B2 /* MGLCoordinateFormatter.h in Headers */, + 967C864C210A9D3C004DF794 /* UIDevice+MGLAdditions.h in Headers */, 404C26E31D89B877000AA13D /* MGLTileSource.h in Headers */, 96E516F6200059EC00A02306 /* MGLRendererFrontend.h in Headers */, 071BBB041EE76147001FB02A /* MGLImageSource.h in Headers */, @@ -2949,6 +2959,7 @@ 40834BF71FE05E1800C1BD0D /* MMEUniqueIdentifier.m in Sources */, 3566C7681D4A77BA008152BC /* MGLShapeSource.mm in Sources */, 40834C4A1FE05F7500C1BD0D /* TSKPinningValidator.m in Sources */, + 967C864D210A9D3C004DF794 /* UIDevice+MGLAdditions.m in Sources */, 400533021DB0862B0069F638 /* NSArray+MGLAdditions.mm in Sources */, 96036A03200565C700510F3D /* NSOrthography+MGLAdditions.m in Sources */, 40834BF31FE05E1800C1BD0D /* MMETimerManager.m in Sources */, @@ -3076,6 +3087,7 @@ 400533031DB086490069F638 /* NSArray+MGLAdditions.mm in Sources */, 40834C571FE05F7600C1BD0D /* TSKPinningValidator.m in Sources */, 35136D431D42274500C20EFD /* MGLRasterStyleLayer.mm in Sources */, + 967C864E210A9D3C004DF794 /* UIDevice+MGLAdditions.m in Sources */, 96036A04200565C700510F3D /* NSOrthography+MGLAdditions.m in Sources */, 40834C071FE05E1800C1BD0D /* MMETimerManager.m in Sources */, 3538AA201D542239008EC33D /* MGLForegroundStyleLayer.mm in Sources */, diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index bfd0946cdc..e1520401a7 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -79,6 +79,21 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingMode) { MGLUserTrackingModeFollowWithCourse, }; +/** Options for `MGLMapView.preferredFramesPerSecond`. */ +typedef NSInteger MGLMapViewPreferredFramesPerSecond NS_TYPED_EXTENSIBLE_ENUM; + +/** + The default frame rate. This can be either 30 FPS or 60 FPS, depending on + device capabilities. + */ +FOUNDATION_EXTERN MGL_EXPORT const MGLMapViewPreferredFramesPerSecond MGLMapViewPreferredFramesPerSecondDefault; + +/** A conservative frame rate; typically 30 FPS. */ +FOUNDATION_EXTERN MGL_EXPORT const MGLMapViewPreferredFramesPerSecond MGLMapViewPreferredFramesPerSecondLowPower; + +/** The maximum supported frame rate; typically 60 FPS. */ +FOUNDATION_EXTERN MGL_EXPORT const MGLMapViewPreferredFramesPerSecond MGLMapViewPreferredFramesPerSecondMaximum; + /** An interactive, customizable map view with an interface similar to the one provided by Apple’s MapKit. @@ -286,6 +301,21 @@ MGL_EXPORT IB_DESIGNABLE */ - (IBAction)showAttribution:(id)sender; +/** + The preferred frame rate at which the map view is rendered. + + The default value for this property is + `MGLMapViewPreferredFramesPerSecondDefault`, which will adaptively set the + preferred frame rate based on the capability of the user’s device to maintain + a smooth experience. + + In addition to the provided `MGLMapViewPreferredFramesPerSecond` options, this + property can be set to arbitrary integer values. + + @see `CADisplayLink.preferredFramesPerSecond` + */ +@property (nonatomic, assign) MGLMapViewPreferredFramesPerSecond preferredFramesPerSecond; + @property (nonatomic) NSArray *styleClasses __attribute__((unavailable("Support for style classes has been removed."))); - (BOOL)hasStyleClass:(NSString *)styleClass __attribute__((unavailable("Support for style classes has been removed."))); diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index cdacfb462b..918506067c 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -53,6 +53,7 @@ #import "NSProcessInfo+MGLAdditions.h" #import "NSString+MGLAdditions.h" #import "NSURL+MGLAdditions.h" +#import "UIDevice+MGLAdditions.h" #import "UIImage+MGLAdditions.h" #import "UIViewController+MGLAdditions.h" @@ -87,6 +88,10 @@ const CGFloat MGLMapViewDecelerationRateNormal = UIScrollViewDecelerationRateNor const CGFloat MGLMapViewDecelerationRateFast = UIScrollViewDecelerationRateFast; const CGFloat MGLMapViewDecelerationRateImmediate = 0.0; +const MGLMapViewPreferredFramesPerSecond MGLMapViewPreferredFramesPerSecondDefault = -1; +const MGLMapViewPreferredFramesPerSecond MGLMapViewPreferredFramesPerSecondLowPower = 30; +const MGLMapViewPreferredFramesPerSecond MGLMapViewPreferredFramesPerSecondMaximum = 60; + /// Indicates the manner in which the map view is tracking the user location. typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { /// The map view is not yet tracking the user location. @@ -118,8 +123,6 @@ const double MGLMinimumZoomLevelForUserTracking = 10.5; /// Initial zoom level when entering user tracking mode from a low zoom level. const double MGLDefaultZoomLevelForUserTracking = 14.0; -const NSUInteger MGLTargetFrameInterval = 1; // Target FPS will be 60 divided by this value - /// Tolerance for snapping to true north, measured in degrees in either direction. const CLLocationDirection MGLToleranceForSnappingToNorth = 7; @@ -403,6 +406,9 @@ public: self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = YES; if (@available(iOS 11.0, *)) { self.accessibilityIgnoresInvertColors = YES; } + + self.preferredFramesPerSecond = MGLMapViewPreferredFramesPerSecondDefault; + // setup mbgl view _mbglView = new MBGLView(self); @@ -1125,7 +1131,7 @@ public: } _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateFromDisplayLink)]; - _displayLink.frameInterval = MGLTargetFrameInterval; + [self updateDisplayLinkPreferredFramesPerSecond]; [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; _needsDisplayRefresh = YES; [self updateFromDisplayLink]; @@ -1137,6 +1143,49 @@ public: } } +- (void)updateDisplayLinkPreferredFramesPerSecond +{ + if (!_displayLink) + { + return; + } + + NSInteger newFrameRate; + if (_preferredFramesPerSecond == MGLMapViewPreferredFramesPerSecondDefault) + { + // On legacy devices that cannot maintain a reasonable frame rate, set + // a lower limit to avoid jank. + newFrameRate = UIDevice.currentDevice.mgl_isLegacyDevice ? MGLMapViewPreferredFramesPerSecondLowPower : MGLMapViewPreferredFramesPerSecondMaximum; + } + else + { + newFrameRate = _preferredFramesPerSecond; + } + + if (@available(iOS 10.0, *)) + { + _displayLink.preferredFramesPerSecond = newFrameRate; + } + else + { + // CADisplayLink.frameInterval does not support more than 60 FPS (and + // no device that supports >60 FPS ever supported iOS 9). + NSInteger maximumFrameRate = 60; + _displayLink.frameInterval = maximumFrameRate / MIN(newFrameRate, maximumFrameRate); + } +} + +- (void)setPreferredFramesPerSecond:(MGLMapViewPreferredFramesPerSecond)preferredFramesPerSecond +{ + if (_preferredFramesPerSecond == preferredFramesPerSecond) + { + return; + } + + _preferredFramesPerSecond = preferredFramesPerSecond; + [self updateDisplayLinkPreferredFramesPerSecond]; +} + - (void)didMoveToWindow { [self validateDisplayLink]; diff --git a/platform/ios/src/UIDevice+MGLAdditions.h b/platform/ios/src/UIDevice+MGLAdditions.h new file mode 100644 index 0000000000..a61aedf2db --- /dev/null +++ b/platform/ios/src/UIDevice+MGLAdditions.h @@ -0,0 +1,7 @@ +#import + +@interface UIDevice (MGLAdditions) + +@property (nonatomic, readonly) BOOL mgl_isLegacyDevice; + +@end diff --git a/platform/ios/src/UIDevice+MGLAdditions.m b/platform/ios/src/UIDevice+MGLAdditions.m new file mode 100644 index 0000000000..e9da77adda --- /dev/null +++ b/platform/ios/src/UIDevice+MGLAdditions.m @@ -0,0 +1,51 @@ +#import "UIDevice+MGLAdditions.h" +#include + +@implementation UIDevice (MGLAdditions) + +- (NSString *)modelString { + char *typeSpecifier = "hw.machine"; + + size_t size; + sysctlbyname(typeSpecifier, NULL, &size, NULL, 0); + + char *answer = malloc(size); + sysctlbyname(typeSpecifier, answer, &size, NULL, 0); + + NSString *results = [NSString stringWithCString:answer encoding:NSUTF8StringEncoding]; + + free(answer); + return results; +} + +- (BOOL)mgl_isLegacyDevice { + // This is a list of supported devices that cannot maintain a reasonable frame + // rate under typical load. For brevity, unsupported devices are not included. + NSSet *blacklist = [NSSet setWithObjects: + @"iPhone4", // iPhone 4s + @"iPhone5", // iPhone 5, 5c + @"iPhone6", // iPhone 5s + + @"iPad2", // iPad 2, Mini + @"iPad3", // iPad 3 + @"iPad4", // iPad Air, Mini 2, Mini 3 + + @"iPod5", // iPod Touch 5 + + nil + ]; + + NSString *model = [self modelString]; + + for (NSString *blacklistedModel in blacklist) { + if ([model hasPrefix:[blacklistedModel stringByAppendingString:@","]]) { + return YES; + } + } + + // TODO: Also handle simulator using something like `ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]`. + + return NO; +} + +@end -- cgit v1.2.1