Skip to content

borderWidth renders at double thickness when borderRadius is an array (iOS) #14442

Description

@macCesar

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):

  1. setBorderWidth_: runs first. borderRadius is not yet on the proxy, so borderLayer returns self.layer. self.layer.borderWidth = 4.
  2. 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.
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions