-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add category convenience methods to NSNumber for style values (was: Runtime styling properties are too loosely typed) #5970
Comments
Solution 1: More protocolsWe’d need type-specific protocols that each conform to MGLStyleAttributeValue:
This isn’t a huge departure from what we’re already doing. It’s just a matter of defining some additional protocols and having the existing categories and properties adopt them. One downside is that people may see |
Solution 2: Lightweight genericsAll properties would be of type MGLStyleAttributeValue, which should be a class with a single type parameter that determines the primitive underlying type of the value. Perhaps something like this: @interface MGLStyleAttributeValue<T> : NSObject
+ (instancetype)valueWithRawValue:(T)rawValue;
+ (instancetype)valueWithFunctionType:(MGLStyleAttributeFunctionType)functionType property:(NSString *)property base:(NSNumber *)base stops:(NSDictionary<NSNumber *, MGLStyleAttributeValue<T> *> *)stops;
// MGLStyleAttributeFunctionTypeNone if created using +valueWithRawValue:
@property (nonatomic) MGLStyleAttributeFunctionType functionType;
- (T)rawValueAtZoomLevel:(double)zoomLevel;
@end The conversion from Objective-C types to C++ types would occur in each individual property, but we’d factor out almost everything into macros similar to the current In Swift 2.2, type parameters in lightweight generics are ignored, making the properties just as loosely typed as we have now. So we’d need to clarify the expected type in documentation and enforce it at runtime via assertions. In Swift 3, type parameters are fully bridged from Objective-C. I’m not sure about Swift 2.3. This approach greatly simplifies the runtime styling API implementation. It allows us to centralize the logic for converting between Objective-C and C++ types and remove the disparate public and private categories. It strengthens the type system, shifting most type checking to compile time. Dealing with attributes would get a bit more verbose. Instead of setting a paint attribute property to |
Solution 3: More setters and gettersWe’d need to generate at least one additional setter and getter per property. // Idiomatic primitive types, structs and objects:
waterLayer.fillColor = [UIColor redColor];
waterLayer.fillTranslate = CGVectorMake(10, 10);
stateLayer.iconPadding = UIEdgeInsetsMake(0, 0, 0, 0);
stateLayer.iconHaloWidth = 1.0f;
// Primitive types and structs still needs to be wrapped one way or
// another in containers when using functions:
MGLStyleAttributeFunction *paddingFunction = [[MGLStyleAttributeFunction alloc] init];
paddingFunction.stops = @{@6.0: NSStringFromUIEdgeInsets(UIEdgeInsetsMake(0, 0, 0, 0))};
stateLayer.iconPaddingFunction = paddingFunction; What about undefined? |
I’ve never been a huge fan of having properties that can be set or unset depending on whether some sibling property is set. Solution 3 is a cleaner solution than what we have now, but I’m not convinced that it’s better than solution 2 on balance. What is the type of Additionally, solution 3 won’t scale well beyond zoom functions. For #5948, we’ll already need to add
An explicit “unset” method for a one-to-one relationship would be un-Cocoa and make it more difficult to be KVO compliant in the future. Unsetting an object-typed property should always entail setting the property to nil. |
They're actually independent in that sense, setting a function will override the constant and vice versa as excepted.
Function stops would still be weakly typed, unfortunately. No improvements there. I agree about the “unset” method, it's not something I’d prefer. |
Yes, that’s what we’d have to do, but it still isn’t as straightforward as having one property for each attribute. It will be possible to make this approach KVO compliant, but it means more work on the part of the developer: if the icon padding is undefined, you need to check two different properties to determine that.
That’s exactly what the lightweight generics in solution 2 are meant to address. One class can handle every type, be it an NSNumber, NSString, or NSValue. |
Another problem with the current architecture is that it crashes hard if you try to use the wrong type: the |
@boundsj pointed out that solution 2 would break KVO compliance (radar://problem/28053584) if we rely on typedefs. |
Working on solution 2 on the 1ec5-style-types-generics-5970 branch. Stuck on some linker issues at the moment. |
Took a bit of a detour cleaning up the containing classes too: #6588. |
Solution 2 – #5970 (comment) – has been implemented in #6601. In the course of landing #6601, we realized how verbose this approach would become in Swift but especially in Objective-C. Before: // MGLLineStyleLayer.swift
open var lineGapWidth: MGLStyleValue!
// MGLMapViewDelegate.mapViewDidFinishLoadingMap(_:)
let layer = mapView.style().layer(identifier: "motorway") as? MGLLineStyleLayer
layer?.lineGapWidth = 5 as MGLStyleAttributeValue
layer?.lineJoin = MGLLineJoin.bevel
layer?.lineColor = UIColor.blue
let lineColor = layer?.lineColor as? UIColor
let fn = MGLStyleAttributeFunction()
fn.stops = [1: 5, 18: 3]
layer?.lineWidth = fn // MGLLineStyleLayer.h
@property (nonatomic, null_resettable) id <MGLStyleValue> lineGapWidth;
// -[MGLMapViewDelegate mapViewDidFinishLoadingMap:]
MGLLineStyleLayer *layer = (MGLLineStyleLayer *)[mapView.style layerWithIdentifier:@"motorway"];
layer.lineGapWidth = @5;
layer.lineJoin = @(MGLLineJoinBevel);
layer.lineColor = UIColor.blueColor;
UIColor *lineColor = [layer.lineColor isKindOfClass:[UIColor class]] ? layer.lineColor : nil;
MGLStyleAttributeFunction *fn = [[MGLStyleAttributeFunction alloc] init];
fn.stops = @{@1: @5, @18: @3};
layer.lineWidth = fn; After: // MGLLineStyleLayer.swift
open var lineGapWidth: MGLStyleValue<NSNumber>!
// MGLMapViewDelegate.mapViewDidFinishLoadingMap(_:)
let layer = mapView.style().layer(identifier: "motorway") as? MGLLineStyleLayer
layer?.lineGapWidth = MGLStyleValue(rawValue: 5)
layer?.lineJoin = MGLStyleValue(rawValue: MGLLineJoin.bevel as NSValue)
layer?.lineColor = MGLStyleValue(rawValue: .blue)
let lineColor = (lineLayer.lineGapWidth as? MGLStyleConstantValue<NSNumber>)?.rawValue
layer?.lineWidth = MGLStyleValue(stops: [
1: MGLStyleValue(rawValue: 5),
18: MGLStyleValue(rawValue: 3),
]) // MGLLineStyleLayer.h
@property (nonatomic, null_resettable) MGLStyleValue<NSNumber *> *lineGapWidth;
// -[MGLMapViewDelegate mapViewDidFinishLoadingMap:]
MGLLineStyleLayer *layer = (MGLLineStyleLayer *)[mapView.style layerWithIdentifier:@"motorway"];
layer.lineGapWidth = [MGLStyleValue<NSNumber *> valueWithRawValue:@5];
layer.lineJoin = [MGLStyleValue<NSValue *> valueWithRawValue:@(MGLLineJoinBevel)];
layer.lineColor = [MGLStyleValue<UIColor> valueWithRawValue:UIColor.blueColor];
UIColor *lineColor = [layer.lineColor isKindOfClass:[MGLStyleConstantValue class]] ? [(MGLStyleConstantValue<NSNumber *> *)layer.lineColor rawValue] : nil;
MGLStyleAttributeFunction *fn = [[MGLStyleAttributeFunction alloc] init];
NSDictionary *stops = @{
@1: [MGLStyleValue<NSNumber *> valueWithRawValue:@5],
@18: [MGLStyleValue<NSNumber *> valueWithRawValue:@3],
};
layer.lineWidth = [MGLStyleValue<NSNumber *> valueWithStops:stops]; Some additional approaches have been proposed, all of which can be layered atop the work in that PR. The following comments detail those proposals, which we’ll revisit following the first beta. |
Reopening in case folks want to continue discussing alternative approaches before the release. |
Added tests of MGLStyleValue written in Swift, along with bridging headers just in case they become needed in the future.
My vote is for simplicity. So option 4? If there is something unrecognized skip it and throw a warning. The current implementation is "OK" but I can't figure out LineJoin for the life of me: Tried every combo I can think of... |
Believe it or not, this is what it takes to put an enumeration value into an NSValue in Swift 3: var bevel = MGLLineJoin.bevel
// @encode(MGLLineJoin) in Objective-C produces "Q"
lineLayer.lineJoin = MGLStyleValue(rawValue: NSValue(&bevel, withObjCType: "Q")) The only proposal above that skirts the issue is #5970 (comment), because it allows the developer to work with primitives instead of objects. But that proposal has other drawbacks, such as needing to consult multiple methods to “get” a value. What we can do instead is to provide NSValue initializers and getters, as we do for enumerations like CLLocationCoordinate2D and structs like MGLOfflinePackProgress. That’ll let you say: lineLayer.lineJoin = MGLStyleValue(rawValue: NSValue(mglLineJoin: .round)) I’m marking this issue as a release blocker specifically for the NSValue initializers and getters. |
Just chiming in that I hit this trying to set |
Closing again since #7061 should take care of the issues noted above in #5970 (comment) and #5970 (comment) |
Moving out of the 3.4.0 milestone and reopening as a placeholder for future work (i.e. #5970 (comment)) |
We’re working towards a generalized expression syntax in the style specification in mapbox/mapbox-gl-js#4715. I’ve added lots of notes in mapbox/mapbox-gl-js#4715 (review) about how we’d bridge expressions to NSExpression. On the plus side, it doesn’t sound like style functions (aka ramps) would be part of that proposal. Unfortunately, it sounds like we’ll end up needing to support style functions and expressions simultaneously for backwards compatibility with existing v8 styles. For the runtime styling API, that would mean MGLStyleValue would gain another initializer, Another casualty with the upcoming expression work is compile-time type checking: NSExpression is usually created from a format string, and its evaluated type isn’t known until runtime. But given the awkwardness expressed in #6601 (comment), coupled with the huge gains in expressiveness that come with NSExpression, I think the scales tip in favor of NSExpression despite being loosely typed. /cc @anandthakker |
Note that the proposal now does include stops for zoom (or zoom-and-property) functions. |
It seems likely that we’ll move away from compile-time type checking in favor of runtime type checking, contrary to the original intent of this issue. Expressions are extremely cumbersome to implement using structured data types. This is the same consideration that led us to adopt NSPredicate (with its format strings) for filtering. Closing this issue to focus on #8074, which tracks the work to reimplement MGLStyleValue atop NSExpression (or replace MGLStyleValue with NSExpression). |
We currently use the MGLStyleAttributeValue protocol to allow any style attribute property to be set to the appropriate value type or an instance of MGLStyleAttributeFunction. While this approach is slightly more strongly typed than
id
, preventing the developer from setting a property to e.g. a UIButton, it doesn’t keep the developer from setting the property to an NSString where an NSNumber is expected or an NSArray where an NSValue is expected. The attribute properties’ types should be more specific, allowing the compiler to catch common developer errors./cc @frederoni @incanus @jfirebaugh
The text was updated successfully, but these errors were encountered: