iOS开发基础77-一像素线(One Pixel Line)的几种实现方式
iOS 一像素线(One Pixel Line)完全指南:从点像素原理到封装实践与常见坑
一像素线是 iOS UI 开发中最常见的元素之一——分隔线、边框、下划线都离不开它。但很多人对"一像素"的理解并不准确,直接写 height = 1 画出来的其实是 2 像素甚至 3 像素的粗线;用 Core Graphics 画的线经常模糊发虚。本文从最基础的"点 vs 像素"讲起,系统梳理一像素线的所有实现方式、封装技巧和常见坑。
一、核心概念:点(Point)vs 像素(Pixel)
在讲怎么画一像素线之前,必须先搞懂两个最基本的概念,否则一切都是空谈。
1. 点和像素的区别
| 概念 | 英文 | 说明 | 举例 |
|---|---|---|---|
| 点 | Point(pt) | 逻辑单位,iOS 布局用的单位,和屏幕分辨率无关 | iPhone 屏幕宽度 375pt |
| 像素 | Pixel(px) | 物理单位,屏幕实际的发光点,和屏幕分辨率相关 | iPhone 屏幕宽度 750px(@2x)或 1125px(@3x) |
iOS 开发中,所有布局(frame、constraint、center)用的都是点(pt),不是像素。你写 view.frame = CGRectMake(0, 0, 100, 1),这里的 1 是 1 点,不是 1 像素。
2. 屏幕缩放因子(scale)
Retina 屏幕用多个物理像素来显示一个逻辑点,这个比例就是 scale:
scale = 物理像素数 / 逻辑点数
1 像素 = 1.0 / scale 点
| 设备类型 | scale | 1 像素等于多少点 | 代表设备 |
|---|---|---|---|
| 非 Retina(@1x) | 1.0 | 1.0 pt | iPhone 3GS 及以前(已淘汰) |
| Retina(@2x) | 2.0 | 0.5 pt | iPhone 4~8、iPhone XR、iPhone SE、iPad Air/Pro |
| Retina HD(@3x) | 3.0 | 0.333 pt | iPhone 6 Plus~8 Plus、iPhone X~15 Pro Max |
获取 scale:
CGFloat scale = [UIScreen mainScreen].scale; // 2.0 或 3.0
3. 为什么一像素线要写 1.0 / scale?
如果你写 height = 1(1 点):
- 在 @2x 屏幕上,实际渲染为 2 像素(1pt × 2 = 2px)。
- 在 @3x 屏幕上,实际渲染为 3 像素(1pt × 3 = 3px)。
这不是一像素线,是两像素或三像素的粗线!
正确的一像素线高度:
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
// @2x: 0.5pt → 实际 1px
// @3x: 0.333pt → 实际 1px
一句话总结:iOS 布局用点,一像素 = 1.0 / scale 点。写 height = 1 画出来的是 2~3 像素的粗线。
二、方法一:UIView + Auto Layout / Frame(最常用)
这是实际开发中最常用的方式,简单直接,支持自动布局。
1. Auto Layout 实现
UIView *line = [[UIView alloc] init];
line.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0]; // 分隔线常用浅灰色
line.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:line];
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
[NSLayoutConstraint activateConstraints:@[
[line.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16],
[line.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],
[line.heightAnchor constraintEqualToConstant:onePixel],
[line.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:100]
]];
2. Frame 实现
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(16, 100, self.view.bounds.size.width - 32, onePixel)];
line.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0];
[self.view addSubview:line];
3. 垂直线(竖线)
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
UIView *verticalLine = [[UIView alloc] initWithFrame:CGRectMake(100, 0, onePixel, 200)];
verticalLine.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0];
[self.view addSubview:verticalLine];
4. 优缺点
| 优点 | 缺点 |
|---|---|
| 简单直接,代码量少 | 多一个 UIView 层级(但影响可忽略) |
| 支持 Auto Layout,自动适配旋转/布局变化 | 大量使用时视图层级较深 |
| 可以设置 backgroundColor、alpha 等属性 | - |
| 可以加点击事件(虽然一般不需要) | - |
实际开发中 90% 的一像素线都用这种方式,简单可靠,支持自动布局。
三、方法二:CALayer(高性能)
直接用 CALayer 画线,比 UIView 更轻量(没有事件响应、没有布局系统),性能更好。
1. 基本实现
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
CALayer *lineLayer = [CALayer layer];
lineLayer.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0].CGColor;
lineLayer.frame = CGRectMake(16, 100, self.view.bounds.size.width - 32, onePixel);
[self.view.layer addSublayer:lineLayer];
2. 重要注意事项:CALayer 不支持 Auto Layout
CALayer 的 frame 不会随父视图的自动布局自动更新。如果父视图尺寸变化(如屏幕旋转、约束更新),layer 的 frame 不会自动调整,需要手动更新:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// 手动更新 layer 的 frame
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
self.lineLayer.frame = CGRectMake(16, 100, self.view.bounds.size.width - 32, onePixel);
}
或者封装一个自定义 UIView,在 layoutSubviews 中管理内部的 layer:
@interface OnePixelLineView : UIView
@property (nonatomic, strong) CALayer *lineLayer;
@property (nonatomic, strong) UIColor *lineColor;
@end
@implementation OnePixelLineView
- (instancetype)init {
self = [super init];
if (self) {
self.lineLayer = [CALayer layer];
[self.layer addSublayer:self.lineLayer];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
self.lineLayer.frame = CGRectMake(0, 0, self.bounds.size.width, onePixel);
self.lineLayer.backgroundColor = self.lineColor.CGColor;
}
@end
3. 优缺点
| 优点 | 缺点 |
|---|---|
| 比 UIView 轻量,性能略好 | 不支持 Auto Layout,需要手动更新 frame |
| 不增加视图层级(只增加 layer) | 不能加点击事件 |
| 适合大量线条的场景(如复杂表格) | 代码稍多 |
适用场景:需要大量线条、对性能敏感的场景;或者已经在操作 layer 的自定义视图中。一般场景用 UIView 更方便。
四、方法三:Core Graphics 绘制(自定义视图)
在自定义视图的 drawRect: 中用 Core Graphics 直接绘制,适合完全自定义的绘图场景。
1. 填充矩形方式(推荐,最清晰)
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
// 在顶部绘制一像素线
CGRect lineRect = CGRectMake(0, 0, self.bounds.size.width, onePixel);
CGContextSetFillColorWithColor(context, [UIColor colorWithWhite:0.85 alpha:1.0].CGColor);
CGContextFillRect(context, lineRect);
}
2. 描边路径方式
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
CGContextSetStrokeColorWithColor(context, [UIColor colorWithWhite:0.85 alpha:1.0].CGColor);
CGContextSetLineWidth(context, onePixel);
// 注意:描边时坐标要偏移 0.5 像素,否则线会模糊(见后文"常见坑")
CGFloat offset = onePixel / 2.0;
CGContextMoveToPoint(context, 0, offset);
CGContextAddLineToPoint(context, self.bounds.size.width, offset);
CGContextStrokePath(context);
}
3. 优缺点
| 优点 | 缺点 |
|---|---|
| 完全控制绘制过程,最灵活 | 需要自定义视图,代码量最大 |
| 不增加视图层级 | drawRect: 有性能开销(不适合频繁刷新) |
| 适合复杂的自定义绘图 | 描边方式容易模糊(需要坐标偏移) |
| 可以绘制任意形状的线(虚线、斜线等) | - |
适用场景:自定义视图中已经在 drawRect: 里绘制其他内容,顺便画一条线;或者需要绘制虚线、斜线等特殊线条。
五、方法四:系统自带的分割线
很多场景不需要自己画线,系统已经提供了分割线 API。
1. UITableView 的 separator
// 显示分割线
self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
self.tableView.separatorColor = [UIColor colorWithWhite:0.85 alpha:1.0];
// 让分割线满宽(默认有左边缩进)
self.tableView.separatorInset = UIEdgeInsetsZero;
// iOS 8+ 还需要设置 layoutMargins
self.tableView.layoutMargins = UIEdgeInsetsZero;
// 在 cell 中
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
cell.layoutMargins = UIEdgeInsetsZero;
}
注意:UITableView 的 separator 本身就是一像素线(系统自动处理了 scale),不需要自己再画。
2. 隐藏 UITableView 多余的分割线
// 底部多余的分割线用空的 tableFooterView 隐藏
self.tableView.tableFooterView = [[UIView alloc] init];
3. UIStackView 的 spacing
用 UIStackView 布局时,可以用 spacing 做分隔,但 spacing 是空白不是线。如果需要线,可以在 stackView 中插入一个一像素高的 UIView。
4. UINavigationBar / UITabBar 的分隔线
导航栏底部和标签栏顶部的系统分隔线,可以通过设置 shadowImage 自定义或隐藏:
// 隐藏导航栏底部分隔线
[self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
[self.navigationController.navigationBar setShadowImage:[UIImage new]];
// 隐藏标签栏顶部分隔线
[self.tabBarController.tabBar setBackgroundImage:[UIImage new]];
[self.tabBarController.tabBar setShadowImage:[UIImage new]];
六、实际开发中的封装(重点)
实际项目中不可能每次都写 1.0 / [UIScreen mainScreen].scale,通常会封装成宏、常量或工具方法。
1. 宏定义 / 常量
// PrefixHeader.pch 或 Constants.h 中定义
#define kOnePixel (1.0 / [UIScreen mainScreen].scale)
// 或者用常量(更推荐,宏在某些场景有问题)
static inline CGFloat OnePixel() {
return 1.0 / [UIScreen mainScreen].scale;
}
使用:
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, kOnePixel)];
line.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0];
2. UIView 分类(最常用)
给 UIView 加一个分类,快速添加顶/底/左/右的一像素线:
// UIView+OnePixelLine.h
@interface UIView (OnePixelLine)
/// 添加顶部分割线
- (UIView *)addTopLineWithColor:(UIColor *)color;
- (UIView *)addTopLineWithColor:(UIColor *)color leftInset:(CGFloat)leftInset rightInset:(CGFloat)rightInset;
/// 添加底部分割线
- (UIView *)addBottomLineWithColor:(UIColor *)color;
- (UIView *)addBottomLineWithColor:(UIColor *)color leftInset:(CGFloat)leftInset rightInset:(CGFloat)rightInset;
/// 添加左侧分割线
- (UIView *)addLeftLineWithColor:(UIColor *)color;
/// 添加右侧分割线
- (UIView *)addRightLineWithColor:(UIColor *)color;
@end
// UIView+OnePixelLine.m
#import "UIView+OnePixelLine.h"
@implementation UIView (OnePixelLine)
- (UIView *)addTopLineWithColor:(UIColor *)color {
return [self addTopLineWithColor:color leftInset:0 rightInset:0];
}
- (UIView *)addTopLineWithColor:(UIColor *)color leftInset:(CGFloat)leftInset rightInset:(CGFloat)rightInset {
UIView *line = [[UIView alloc] init];
line.backgroundColor = color;
line.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:line];
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
[NSLayoutConstraint activateConstraints:@[
[line.topAnchor constraintEqualToAnchor:self.topAnchor],
[line.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:leftInset],
[line.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-rightInset],
[line.heightAnchor constraintEqualToConstant:onePixel]
]];
return line;
}
- (UIView *)addBottomLineWithColor:(UIColor *)color {
return [self addBottomLineWithColor:color leftInset:0 rightInset:0];
}
- (UIView *)addBottomLineWithColor:(UIColor *)color leftInset:(CGFloat)leftInset rightInset:(CGFloat)rightInset {
UIView *line = [[UIView alloc] init];
line.backgroundColor = color;
line.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:line];
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
[NSLayoutConstraint activateConstraints:@[
[line.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[line.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:leftInset],
[line.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-rightInset],
[line.heightAnchor constraintEqualToConstant:onePixel]
]];
return line;
}
- (UIView *)addLeftLineWithColor:(UIColor *)color {
UIView *line = [[UIView alloc] init];
line.backgroundColor = color;
line.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:line];
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
[NSLayoutConstraint activateConstraints:@[
[line.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[line.topAnchor constraintEqualToAnchor:self.topAnchor],
[line.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[line.widthAnchor constraintEqualToConstant:onePixel]
]];
return line;
}
- (UIView *)addRightLineWithColor:(UIColor *)color {
UIView *line = [[UIView alloc] init];
line.backgroundColor = color;
line.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:line];
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
[NSLayoutConstraint activateConstraints:@[
[line.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[line.topAnchor constraintEqualToAnchor:self.topAnchor],
[line.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[line.widthAnchor constraintEqualToConstant:onePixel]
]];
return line;
}
@end
使用:
// 给 cell 添加底部分割线,左右缩进 16
[cell.contentView addBottomLineWithColor:[UIColor colorWithWhite:0.85 alpha:1.0] leftInset:16 rightInset:16];
// 给按钮添加顶部分割线
[self.button addTopLineWithColor:[UIColor colorWithWhite:0.85 alpha:1.0]];
这是实际项目中最推荐的方式,封装一次,到处使用,代码简洁且统一。
3. 自定义 OnePixelLine 视图
如果需要一个可复用的一像素线视图(支持 xib/storyboard),可以自定义:
@interface OnePixelLineView : UIView
@property (nonatomic, strong) IBInspectable UIColor *lineColor;
@property (nonatomic, assign) IBInspectable BOOL isHorizontal; // YES=横线,NO=竖线
@end
@implementation OnePixelLineView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
_lineColor = [UIColor colorWithWhite:0.85 alpha:1.0];
_isHorizontal = YES;
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
CGContextSetFillColorWithColor(context, self.lineColor.CGColor);
if (self.isHorizontal) {
CGContextFillRect(context, CGRectMake(0, 0, rect.size.width, onePixel));
} else {
CGContextFillRect(context, CGRectMake(0, 0, onePixel, rect.size.height));
}
}
@end
七、常见问题与坑(重点)
坑 1:线模糊、发虚、颜色比预期浅
这是最常见的问题,尤其是用 Core Graphics 描边(stroke)画线时。
原因:Core Graphics 默认开启抗锯齿(Anti-Aliasing)。如果线的位置正好在两个像素的中间,GPU 会用半透明的颜色填充两侧的像素,导致线看起来模糊、变粗、颜色变浅。
解决方法:
方法一:关闭抗锯齿(适合水平线/垂直线,不适合斜线/曲线)
CGContextSetShouldAntialias(context, NO); // 关闭抗锯齿
// 画线...
CGContextSetShouldAntialias(context, YES); // 画完后恢复
方法二:坐标偏移 0.5 像素(描边时推荐)
描边时,线的中心在指定坐标上。如果坐标是整数,线的中心正好在像素边界上,会横跨两个像素,导致模糊。将坐标偏移 0.5 像素,让线的中心正好在像素中心:
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
CGFloat offset = onePixel / 2.0; // 0.25pt(@2x)或 0.1667pt(@3x)
CGContextMoveToPoint(context, 0, offset);
CGContextAddLineToPoint(context, self.bounds.size.width, offset);
CGContextStrokePath(context);
方法三:用填充(fill)代替描边(stroke)
填充矩形不会有模糊问题,因为矩形的边界正好对齐像素:
// 推荐用填充,不要用描边
CGRect lineRect = CGRectMake(0, 0, width, onePixel);
CGContextFillRect(context, lineRect);
总结:用 UIView/CALayer 设置 backgroundColor 一般不会模糊;用 Core Graphics 时优先用 fill 而不是 stroke;如果必须用 stroke,坐标偏移 0.5 像素或关闭抗锯齿。
坑 2:线不显示
可能原因:
- 高度写成了 0:
1.0 / scale计算错误,或写成了1 / scale(整数除法,结果为 0)。CGFloat onePixel = 1 / [UIScreen mainScreen].scale; // ❌ 整数除法,1/2=0 CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale; // ✅ 浮点除法 - 颜色和背景色相同:线的颜色和父视图背景色一样,看不出来。
- 被其他视图遮挡:线的层级在其他视图下面。
- CALayer 的 frame 为 0:添加时父视图还没布局完成,frame 计算错误。
- hidden = YES 或 alpha = 0:不小心设置了隐藏或透明。
坑 3:CALayer 的线在旋转/布局变化后位置不对
原因:CALayer 不支持 Auto Layout,frame 不会自动更新。
解决:在 layoutSubviews 或 viewDidLayoutSubviews 中手动更新 layer 的 frame,或用自定义 UIView 封装(见方法二)。
坑 4:UITableView 分割线左边有缩进
原因:iOS 7 以后 UITableView 的 separator 默认有左边缩进(15pt 或 16pt)。
解决:
// iOS 7+
self.tableView.separatorInset = UIEdgeInsetsZero;
// iOS 8+ 还需要设置 layoutMargins
self.tableView.layoutMargins = UIEdgeInsetsZero;
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
cell.layoutMargins = UIEdgeInsetsZero;
}
// iOS 15+ 可能还需要
if (@available(iOS 15.0, *)) {
self.tableView.sectionHeaderTopPadding = 0;
}
坑 5:在 xib/storyboard 中设置一像素高度
xib/storyboard 中约束的高度只能输入整数,不能直接输入 0.5。
解决方法:
- 用代码添加一像素线(推荐)。
- 在 xib 中给线一个普通高度约束,然后在代码中修改约束的 constant:
self.lineHeightConstraint.constant = 1.0 / [UIScreen mainScreen].scale; - 用
IBDesignable/IBInspectable自定义视图(见封装部分)。
坑 6:一像素线在不同设备上粗细不一致
原因:没有用 1.0 / scale,而是硬编码了 0.5 或 1。
解决:永远用 1.0 / [UIScreen mainScreen].scale 动态计算,不要硬编码。
// ❌ 硬编码,@3x 上是 1.5 像素(模糊)
line.frame = CGRectMake(0, 0, width, 0.5);
// ✅ 动态计算,所有设备都是精确 1 像素
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
line.frame = CGRectMake(0, 0, width, onePixel);
坑 7:画虚线
用 Core Graphics 画虚线时,注意线宽和偏移:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat onePixel = 1.0 / [UIScreen mainScreen].scale;
CGContextSetStrokeColorWithColor(context, [UIColor colorWithWhite:0.85 alpha:1.0].CGColor);
CGContextSetLineWidth(context, onePixel);
// 虚线样式:4pt 实线,2pt 空白
CGFloat lengths[] = {4, 2};
CGContextSetLineDash(context, 0, lengths, 2);
CGFloat offset = onePixel / 2.0;
CGContextMoveToPoint(context, 0, offset);
CGContextAddLineToPoint(context, self.bounds.size.width, offset);
CGContextStrokePath(context);
}
八、性能对比与最佳实践
1. 各种方法的性能对比
| 方法 | 性能 | 易用性 | Auto Layout | 适用场景 |
|---|---|---|---|---|
| UIView + backgroundColor | 好 | 最好 | 支持 | 绝大多数场景(推荐) |
| CALayer | 最好 | 一般 | 不支持,需手动更新 | 大量线条、性能敏感场景 |
| Core Graphics fill | 好 | 较差 | 不支持,需自定义视图 | 自定义视图中绘制 |
| Core Graphics stroke | 一般(易模糊) | 差 | 不支持 | 斜线、虚线等特殊线条 |
| 系统 separator | 最好 | 最好 | 自动 | UITableView 列表 |
2. 最佳实践
- 优先用系统 API:UITableView 用 separator,不要自己画。
- 一般场景用 UIView:
UIView + backgroundColor + 1.0/scale 高度,简单可靠,支持 Auto Layout。 - 封装成工具:项目中封装
UIView+OnePixelLine分类或宏定义,统一使用。 - Core Graphics 用 fill 不用 stroke:fill 不会模糊,stroke 容易模糊(需要坐标偏移)。
- 永远动态计算:
1.0 / [UIScreen mainScreen].scale,不要硬编码 0.5。 - CALayer 注意布局更新:在
layoutSubviews中手动更新 frame。 - 大量线条用 CALayer:如复杂表格、日历等大量线条场景,CALayer 性能更好。
九、Swift 版本对照
1. 一像素计算
let onePixel = 1.0 / UIScreen.main.scale
2. UIView + Auto Layout
let line = UIView()
line.backgroundColor = UIColor(white: 0.85, alpha: 1.0)
line.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(line)
let onePixel = 1.0 / UIScreen.main.scale
NSLayoutConstraint.activate([
line.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
line.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
line.heightAnchor.constraint(equalToConstant: onePixel),
line.topAnchor.constraint(equalTo: view.topAnchor, constant: 100)
])
3. CALayer
let onePixel = 1.0 / UIScreen.main.scale
let lineLayer = CALayer()
lineLayer.backgroundColor = UIColor(white: 0.85, alpha: 1.0).cgColor
lineLayer.frame = CGRect(x: 16, y: 100, width: view.bounds.width - 32, height: onePixel)
view.layer.addSublayer(lineLayer)
4. Core Graphics
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let onePixel = 1.0 / UIScreen.main.scale
context.setFillColor(UIColor(white: 0.85, alpha: 1.0).cgColor)
context.fill(CGRect(x: 0, y: 0, width: rect.width, height: onePixel))
}
5. UIView 分类(Swift)
extension UIView {
@discardableResult
func addBottomLine(color: UIColor, leftInset: CGFloat = 0, rightInset: CGFloat = 0) -> UIView {
let line = UIView()
line.backgroundColor = color
line.translatesAutoresizingMaskIntoConstraints = false
addSubview(line)
let onePixel = 1.0 / UIScreen.main.scale
NSLayoutConstraint.activate([
line.bottomAnchor.constraint(equalTo: bottomAnchor),
line.leadingAnchor.constraint(equalTo: leadingAnchor, constant: leftInset),
line.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -rightInset),
line.heightAnchor.constraint(equalToConstant: onePixel)
])
return line
}
}
// 使用
cell.contentView.addBottomLine(color: UIColor(white: 0.85, alpha: 1.0), leftInset: 16, rightInset: 16)
十、总结
- 核心概念:iOS 布局用点(pt),屏幕物理单位是像素(px),Retina 屏幕 scale=2 或 3。一像素 = 1.0 / scale 点(@2x 是 0.5pt,@3x 是 0.333pt)。写 height=1 画出来的是 2~3 像素的粗线。
- 实现方式:
- UIView + backgroundColor:最常用,简单可靠,支持 Auto Layout,90% 场景用这个。
- CALayer:性能最好,但不支持 Auto Layout,需手动更新 frame,适合大量线条场景。
- Core Graphics:在 drawRect: 中绘制,用 fill(填充)不要用 stroke(描边),适合自定义视图和特殊线条(虚线、斜线)。
- 系统 API:UITableView 的 separator 等,优先使用。
- 封装:项目中封装
UIView+OnePixelLine分类(添加顶/底/左/右线)或宏定义kOnePixel,统一使用。 - 常见坑:
- 线模糊:Core Graphics 用 stroke 时坐标偏移 0.5 像素,或关闭抗锯齿,或改用 fill。
- 线不显示:整数除法
1/scale=0,用1.0/scale。 - CALayer 不跟随布局:在 layoutSubviews 中手动更新 frame。
- UITableView 分割线缩进:设置 separatorInset 和 layoutMargins 为 zero。
- 不同设备粗细不一致:永远动态计算
1.0/scale,不要硬编码 0.5。
- 最佳实践:优先系统 API → 一般场景 UIView → 大量线条 CALayer → 自定义绘图 Core Graphics;封装统一工具;动态计算一像素高度。

浙公网安备 33010602011771号