Ios 如何找到在UIBezierPath表示的不规则形状内绘制文本的位置?

Ios 如何找到在UIBezierPath表示的不规则形状内绘制文本的位置?,ios,swift,core-graphics,uibezierpath,Ios,Swift,Core Graphics,Uibezierpath,在我的应用程序中,我有一堆从SVG文件导入的UIBezierPaths,它们都表示不规则的形状。我想在形状内部绘制文本,以便“标记”这些形状。下面是一个例子: 我想在形状内画出文本“A”。基本上,这将在自定义UIView的draw(:)方法中完成,我希望调用具有签名的方法,例如: func drawText(_ text: String, in path: UIBezierPath, fontSize: CGFloat) { // draws text, or do nothing i

在我的应用程序中,我有一堆从SVG文件导入的
UIBezierPath
s,它们都表示不规则的形状。我想在形状内部绘制文本,以便“标记”这些形状。下面是一个例子:

我想在形状内画出文本“A”。基本上,这将在自定义
UIView
draw(:)
方法中完成,我希望调用具有签名的方法,例如:

func drawText(_ text: String, in path: UIBezierPath, fontSize: CGFloat) {
    // draws text, or do nothing if there is not enough space 
}
在网上看了一会儿后,我发现,它似乎总是在图像的中心画线。这不是我想要的。如果我将
A
放在
UIBezierPath
的边界框的中心(我知道我可以使用它),它将在形状之外。这可能是因为形状是凹的

注意,文本很短,所以我不需要担心换行之类的问题

当然,在形状中有很多地方我可以把“A”放进去,我只是想要一个解决方案,可以选择任何一个地方,以合理的大小显示文本。我想到的一种方法是找到可以在形状中内接的最大矩形,然后在该矩形的中心绘制文本。我在Matlab中发现了这一点,这似乎是一个真正的计算密集型的工作。。。我不确定这是一个好的解决方案

我应该如何实现
drawText(uuxIn:fontSize:)

为了避免成为XY问题,以下是一些背景:

我处理的形状实际上是行政区域的边界。换句话说,我在画地图。我不想使用现有的地图API,因为我希望我的地图看起来非常粗糙。它只会显示行政区域的边界和标签。所以我当然可以使用MapAPI用来在地图上绘制标签的任何算法,对吗


这个问题不是重复的,因为我知道我可以用
UIBezierPath.contains
。我想找到一点。这两者都不是复制品,因为我的问题是在路径中绘制文本,而不是在路径上绘制。

TextKit是为类似这样的任务而构建的。您可以在贝塞尔形状路径的外部创建路径数组,然后将其设置为textView的路径:

textView.textContainer.exclusionPaths = [pathsAroundYourBezier];
请记住,排除路径是容器中的文本不会显示的路径。Apple文档在此:

由于在TEXTVIEW的开始处存在排除路径的错误而更新:

我想出了一种方法,可以找到文本在路径中的位置

用法:

    let pathVisible = rectPathSharpU(CGSize(width: 100, height: 125), origin: CGPoint(x: 0, y: 0))
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = pathVisible.cgPath
    shapeLayer.strokeColor = UIColor.green.cgColor
    shapeLayer.backgroundColor = UIColor.clear.cgColor
    shapeLayer.lineWidth = 3
    self.view.layer.addSublayer(shapeLayer)

    let path = rectPathSharpU(CGSize(width: 100, height: 125), origin: CGPoint(x: 0, y: 0))
    let fittingRect = findFirstRect(path: path, thatFits: "A".size())!
    print("fittingRect: \(fittingRect)")
    let label = UILabel.init(frame: fittingRect)
    label.text = "A"
    self.view.addSubview(label)
输出:

可能需要考虑弯曲路径的情况,可能需要迭代路径边界中的每个y点,直到找到一个相当大的空间

查找第一个拟合矩形的函数:

func findFirstRect(path: UIBezierPath, thatFits: CGSize) -> CGRect? {
    let points = path.cgPath.points
    allPoints: for point in points {
        var checkpoint = point
        var size = CGSize(width: 0, height: 0)
        thisPoint: while size.width <= path.bounds.width {
            if path.contains(checkpoint) && path.contains(CGPoint.init(x: checkpoint.x + thatFits.width, y: checkpoint.y + thatFits.height)) {
                return CGRect(x: checkpoint.x, y: checkpoint.y, width: thatFits.width, height: thatFits.height)
            } else {
                checkpoint.x += 1
                size.width += 1
                continue thisPoint
            }
        }
    }
    return nil
}
创建测试路径:

func rectPathSharpU(_ size: CGSize, origin: CGPoint) -> UIBezierPath {

    // Initialize the path.
    let path = UIBezierPath()

    // Specify the point that the path should start get drawn.
    path.move(to: CGPoint(x: origin.x, y: origin.y))
    // add lines to path
    path.addLine(to: CGPoint(x: (size.width / 3) + origin.x, y: (size.height / 3 * 2) + origin.y))
    path.addLine(to: CGPoint(x: (size.width / 3 * 2) + origin.x, y: (size.height / 3 * 2) + origin.y))
    path.addLine(to: CGPoint(x: size.width + origin.x, y: origin.y))
    path.addLine(to: CGPoint(x: (size.width) + origin.x, y: size.height + origin.y))
    path.addLine(to: CGPoint(x: origin.x, y: size.height + origin.y))

    // Close the path. This will create the last line automatically.
    path.close()

    return path
}
如果这不适用于像您的图片示例那样具有大量圆弧的路径,请发布实际路径数据,以便我可以使用它进行测试

好处:我还创建了一个函数来查找对称路径的最宽部分,不过高度没有考虑在内。尽管它可能有用:

func findWidestY(path: UIBezierPath) -> CGRect {
    var widestSection = CGRect(x: 0, y: 0, width: 0, height: 0)
    let points = path.cgPath.points
    allPoints: for point in points {
        var checkpoint = point
        var size = CGSize(width: 0, height: 0)
        thisPoint: while size.width <= path.bounds.width {
            if path.contains(checkpoint) {
                checkpoint.x += 1
                size.width += 1
                continue thisPoint
            } else {
                if size.width > widestSection.width {
                    widestSection = CGRect(x: point.x, y: point.y, width: size.width, height: 1)
                }
                break thisPoint
            }
        }
    }
    return widestSection
}
func findWidestY(路径:UIBezierPath)->CGRect{
var widtsection=CGRect(x:0,y:0,宽度:0,高度:0)
设点=path.cgPath.points
所有点:用于点到点{
var检查点=点
变量大小=CGSize(宽度:0,高度:0)
此点:而size.width widtsection.width{
WidtSection=CGRect(x:point.x,y:point.y,width:size.width,height:1)
}
打破这一点
}
}
}
返回广域
}

UIBezierPath
仪器外观不适合此任务。我至少会使用
CGPath.boundingBoxOfPath
CGPath.contains
加上一些交叉光线启发式算法(从中心开始)来找到标签矩形的四个角在路径中的位置。考虑到标签很小,从定义上讲,这就足够了。否则计算算法将非常繁重。“交叉射线启发式算法”就是这个词!我以前不知道该找什么。谢谢@Asperi知道一定有一个API是为这个做的!现在我只需要弄清楚如何创建一个文本容器。。。正如我在问题中所说的,我实际上是在做一个地图视图,这些区域中有几十个彼此相邻。那么,我应该为每个地区使用一个文本容器吗?是的,我建议为每个地区使用一个文本容器。本教程中有一个createTextView方法,但是整个代码很长,因为有很多部分:-)这似乎不适合某些路径,显然是因为。如果路径的形状使文本视图的第一行没有足够的空间显示文本(意味着文本从第二行或第三行开始),则排除路径将被完全忽略…@Sweeper,哇,5年前的那个bug今天仍然是个问题?试着把它复制粘贴到操场上,快速查看最后一行:
let textView1=UITextView(frame:CGRect(x:0,y:0,width:100,height:250));textView1.textAlignment=.center;textView1.text=“你好世界你好世界你好世界”;textView1.textContainer.ExclutionPaths=[UIBezierPath(rect:CGRect(x:0,y:0,宽度:100,高度:125));textView1
。文本在文本视图的顶部呈现,此时文本应从中间开始。这是由于旧的错误,而不是一些新的错误,对吗?
func findWidestY(path: UIBezierPath) -> CGRect {
    var widestSection = CGRect(x: 0, y: 0, width: 0, height: 0)
    let points = path.cgPath.points
    allPoints: for point in points {
        var checkpoint = point
        var size = CGSize(width: 0, height: 0)
        thisPoint: while size.width <= path.bounds.width {
            if path.contains(checkpoint) {
                checkpoint.x += 1
                size.width += 1
                continue thisPoint
            } else {
                if size.width > widestSection.width {
                    widestSection = CGRect(x: point.x, y: point.y, width: size.width, height: 1)
                }
                break thisPoint
            }
        }
    }
    return widestSection
}