You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS实现UIImageView类似WhatsApp故事的分段状态边框效果

实现类WhatsApp故事的头像分段环形边框

问题背景

  • 开发类WhatsApp状态(故事)视图功能,用户上传的故事数据存储在数组中
  • 需要为用户头像UIImageView添加分段边框,边框分段数与故事数组元素总数一致,效果参考:
    WhatsApp风格分段头像边框参考效果

需求说明

UIImageView的边框按故事总数量等分,用户查看某条故事后,对应位置的边框段颜色变更:

示例:用户共上传5条故事,边框分为5段,若用户已查看其中2条,则对应2段边框显示为灰色,剩余3段未查看的边框显示为绿色。

原有实现代码未达到预期效果,原有代码如下:

func setImageViewWithBorder() {
        
        self.img_userProfile.backgroundColor = .clear
        self.img_userProfile.clipsToBounds = true
        
        let maskLayer = CAShapeLayer()
        maskLayer.frame = img_userProfile.frame
        maskLayer.path = UIBezierPath(rect: self.img_userProfile.frame).cgPath
        self.img_userProfile.layer.mask = maskLayer
        
        
        let line = NSNumber(value: Float(self.img_userProfile.frame.width / 2))
        
        let borderLayer = CAShapeLayer()
        borderLayer.path = maskLayer.path
        borderLayer.fillColor = UIColor.clear.cgColor
        borderLayer.strokeColor = UIColor.green.cgColor
        borderLayer.lineDashPattern = [line]
        borderLayer.lineDashPhase = self.img_userProfile.frame.width / 4
        borderLayer.lineWidth = 10
        borderLayer.frame = self.img_userProfile.frame
        self.img_userProfile.layer.addSublayer(borderLayer)
    }

实现方案

原有代码的核心问题有两个:一是用矩形路径绘制边框,和圆形头像的环形边框需求不匹配;二是单CAShapeLayer只能设置一种描边颜色,无法实现分段异色的效果。
正确实现思路是用圆弧路径拆分每段边框,逐段创建独立的ShapeLayer绘制,可直接复用以下代码:

import UIKit

// 边框配置可根据UI需求自行调整
private let storyBorderWidth: CGFloat = 3
private let borderSegmentGap: CGFloat = 0.15 // 段与段之间的间隙,单位弧度
private let viewedSegmentColor = UIColor.gray.cgColor
private let unviewedSegmentColor = UIColor.systemGreen.cgColor

func updateStoryAvatarBorder(storyTotalCount: Int, viewedStoryCount: Int) {
    // 移除之前添加的旧边框图层,避免重复叠加
    img_userProfile.superview?.layer.sublayers?
        .filter { $0.name == "storyBorderSegment" }
        .forEach { $0.removeFromSuperlayer() }
    
    // 头像先裁切为圆形
    img_userProfile.layer.cornerRadius = img_userProfile.bounds.width / 2
    img_userProfile.clipsToBounds = true
    
    // 计算环形边框的绘制参数
    let borderRadius = (img_userProfile.bounds.width + storyBorderWidth) / 2
    let borderCenter = CGPoint(x: img_userProfile.frame.midX, y: img_userProfile.frame.midY)
    // 单段故事对应的圆弧长度,扣除间隙占比
    let singleSegmentRadian = (2 * CGFloat.pi - CGFloat(storyTotalCount) * borderSegmentGap) / CGFloat(storyTotalCount)
    // 从顶部12点钟方向开始绘制(默认圆弧起点为3点钟方向,因此初始角度偏移-π/2)
    var currentStartAngle: CGFloat = -CGFloat.pi / 2
    
    for index in 0..<storyTotalCount {
        let currentEndAngle = currentStartAngle + singleSegmentRadian
        let segmentPath = UIBezierPath(
            arcCenter: borderCenter,
            radius: borderRadius,
            startAngle: currentStartAngle,
            endAngle: currentEndAngle,
            clockwise: true
        )
        
        let segmentLayer = CAShapeLayer()
        segmentLayer.name = "storyBorderSegment"
        segmentLayer.path = segmentPath.cgPath
        segmentLayer.fillColor = UIColor.clear.cgColor
        segmentLayer.strokeColor = index < viewedStoryCount ? viewedSegmentColor : unviewedSegmentColor
        segmentLayer.lineWidth = storyBorderWidth
        segmentLayer.lineCap = .round // 段端点做圆角处理,效果更贴近原生WhatsApp
        img_userProfile.superview?.layer.addSublayer(segmentLayer)
        
        // 更新下一段的起点,预留间隙
        currentStartAngle = currentEndAngle + borderSegmentGap
    }
}

使用说明

  • 传入故事总数、已查看故事数即可完成绘制,例:5条故事已看2条,调用updateStoryAvatarBorder(storyTotalCount: 5, viewedStoryCount: 2)
  • 必须在img_userProfile完成布局、获取到正确的frame之后调用该方法,建议放在viewDidLayoutSubviews生命周期中调用,避免尺寸、位置计算偏移
  • 后续用户浏览新故事后,只要传入最新的已查看数量重新调用该方法,即可自动刷新边框状态
  • 如果需要实现故事播放时的边框进度动画,只需对对应分段图层的strokeEnd属性添加CABasicAnimation即可实现

内容的提问来源于stack exchange,提问作者Sagar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 00:15:38