Summary
A View with borderWidth: N and borderRadius: [top, right, bottom, left] on iOS draws the border at 2N points instead of N. The doubling is consistent and reproduces with or without viewShadowColor. Replacing the array with a scalar borderRadius: N renders the border at the correct N.
Reproduction
const win = Ti.UI.createWindow({ backgroundColor: 'gray' });
win.add(Ti.UI.createView({
width: 300,
height: 200,
backgroundColor: 'white',
borderColor: 'magenta',
borderWidth: 4,
borderRadius: [24, 24, 0, 0] // top corners rounded, bottom square
}));
win.open();
Expected: a 4dp magenta border.
Actual: an 8dp magenta border, growing inward.
Swap borderRadius: [24, 24, 0, 0] for borderRadius: 24 and the border renders at 4dp as declared.
Root cause
iphone/TitaniumKit/TitaniumKit/Sources/API/TiUIView.m, the -borderLayer method (lines 626-645) is asymmetric.
The array → scalar transition (lines 630-636) tears down _borderLayer and restores self.layer.borderWidth from the shape layer's lineWidth:
self.layer.mask = nil;
self.layer.borderColor = _borderLayer.strokeColor;
self.layer.borderWidth = _borderLayer.lineWidth / 2;
[_borderLayer removeFromSuperlayer];
RELEASE_TO_NIL(_borderLayer);
_borderLayer = (CAShapeLayer *)self.layer;
The scalar → array transition (lines 637-642) creates a new CAShapeLayer with lineWidth = self.layer.borderWidth * 2 (centered-stroke compensation), but does not clear self.layer.borderWidth or self.layer.borderColor:
_borderLayer = [[CAShapeLayer alloc] init];
_borderLayer.fillColor = UIColor.clearColor.CGColor;
_borderLayer.strokeColor = self.layer.borderColor;
_borderLayer.lineWidth = self.layer.borderWidth * 2;
[self.layer addSublayer:_borderLayer];
For the typical TSS application order (borderWidth applied before borderRadius):
setBorderWidth_: runs first. borderRadius is not yet on the proxy, so borderLayer returns self.layer. self.layer.borderWidth = 4.
setBorderRadius_: runs. array.count = 4, so borderLayer enters the else if branch and creates a separate CAShapeLayer with lineWidth = 8. self.layer.borderWidth is left at 4.
updateBorderRadius: applies the per-corner mask via addCornerRadius:toLayer:.
Two borders render at the same time:
self.layer.borderWidth = 4 (native CALayer border, clipped by the per-corner mask) → 4dp visible.
_borderLayer stroke with lineWidth = 8, inner half visible after the mask → 4dp visible.
Total: 8dp = twice the declared borderWidth: 4.
Proposed fix
Reset the native border on self.layer when promoting to a separate CAShapeLayer:
} else if ((array.count > 1) && (!_borderLayer || _borderLayer == self.layer)) {
_borderLayer = [[CAShapeLayer alloc] init];
_borderLayer.fillColor = UIColor.clearColor.CGColor;
_borderLayer.strokeColor = self.layer.borderColor;
_borderLayer.lineWidth = self.layer.borderWidth * 2;
+ self.layer.borderColor = nil;
+ self.layer.borderWidth = 0;
[self.layer addSublayer:_borderLayer];
}
This makes the scalar-to-array transition symmetric to the existing array-to-scalar handling.
Environment
- Platform: iOS simulator (iPhone 17e), also reproduces on device
- Titanium SDK: 13.2.0.GA
- Code analyzed against
main at commit 48d5761 (borderLayer unchanged in recent history)
Summary
A
ViewwithborderWidth: NandborderRadius: [top, right, bottom, left]on iOS draws the border at 2N points instead of N. The doubling is consistent and reproduces with or withoutviewShadowColor. Replacing the array with a scalarborderRadius: Nrenders the border at the correct N.Reproduction
Expected: a 4dp magenta border.
Actual: an 8dp magenta border, growing inward.
Swap
borderRadius: [24, 24, 0, 0]forborderRadius: 24and the border renders at 4dp as declared.Root cause
iphone/TitaniumKit/TitaniumKit/Sources/API/TiUIView.m, the-borderLayermethod (lines 626-645) is asymmetric.The array → scalar transition (lines 630-636) tears down
_borderLayerand restoresself.layer.borderWidthfrom the shape layer'slineWidth:The scalar → array transition (lines 637-642) creates a new
CAShapeLayerwithlineWidth = self.layer.borderWidth * 2(centered-stroke compensation), but does not clearself.layer.borderWidthorself.layer.borderColor:For the typical TSS application order (
borderWidthapplied beforeborderRadius):setBorderWidth_:runs first.borderRadiusis not yet on the proxy, soborderLayerreturnsself.layer.self.layer.borderWidth = 4.setBorderRadius_:runs.array.count = 4, soborderLayerenters theelse ifbranch and creates a separateCAShapeLayerwithlineWidth = 8.self.layer.borderWidthis left at 4.updateBorderRadius:applies the per-corner mask viaaddCornerRadius:toLayer:.Two borders render at the same time:
self.layer.borderWidth = 4(native CALayer border, clipped by the per-corner mask) → 4dp visible._borderLayerstroke withlineWidth = 8, inner half visible after the mask → 4dp visible.Total: 8dp = twice the declared
borderWidth: 4.Proposed fix
Reset the native border on
self.layerwhen promoting to a separateCAShapeLayer:} else if ((array.count > 1) && (!_borderLayer || _borderLayer == self.layer)) { _borderLayer = [[CAShapeLayer alloc] init]; _borderLayer.fillColor = UIColor.clearColor.CGColor; _borderLayer.strokeColor = self.layer.borderColor; _borderLayer.lineWidth = self.layer.borderWidth * 2; + self.layer.borderColor = nil; + self.layer.borderWidth = 0; [self.layer addSublayer:_borderLayer]; }This makes the scalar-to-array transition symmetric to the existing array-to-scalar handling.
Environment
mainat commit 48d5761 (borderLayerunchanged in recent history)