gpt4 book ai didi

nstextview - 在Swift中使用NSLayoutManager隐藏Markdown字符

转载 作者:行者123 更新时间:2023-12-03 09:49:39 28 4
gpt4 key购买 nike

我正在使用Markdown语法的Mac应用程序中的富文本编辑器上工作。我使用NSTextStorage监视Markdown语法中的匹配项,然后将样式实时应用于NSAttributedString,如下所示:

enter image description here

在这一点上,我对此事已经不知所措,但我为取得进展感到兴奋。 :) This tutorial was very helpful

下一步,当呈现NSTextView的字符串时,我想隐藏Markdown字符。因此,在上面的示例中,键入了最后一个星号后,我希望隐藏* *字符,并以粗体显示sample

我正在使用NSLayoutManager委托(delegate),并且可以看到匹配的字符串,但是我不清楚如何使用shouldGenerateGlyphs方法生成修改后的字形/属性。这是我到目前为止所拥有的:

func layoutManager(_: NSLayoutManager, shouldGenerateGlyphs _: UnsafePointer<CGGlyph>, properties _: UnsafePointer<NSLayoutManager.GlyphProperty>, characterIndexes _: UnsafePointer<Int>, font _: NSFont, forGlyphRange glyphRange: NSRange) -> Int {
let pattern = "(\\*\\w+(\\s\\w+)*\\*)" // Look for stuff like *this*
do {
let regex = try NSRegularExpression(pattern: pattern)
regex.enumerateMatches(in: textView.string, range: glyphRange) {
match, _, _ in
// apply the style
if let matchRange = match?.range(at: 1) {
print(matchRange) <!-- This is the range of *sample*

// I am confused on how to provide the updated properties below...
// let newProps = NSLayoutManager.GlyphProperty.null
// layoutManager.setGlyphs(glyphs, properties: newProps, characterIndexes: charIndexes, font: aFont, forGlyphRange: glyphRange)
// return glyphRange.length
}
}
} catch {
print("\(error.localizedDescription)")
}

return 0
}

我如何根据发现的隐藏星号的文本范围来修改这些内容以传递到 setGlyphs中?

最佳答案

前言

我实现了此方法,以在我的应用程序中实现类似的目的。请记住,此API的文档非常少,因此我的解决方案基于反复试验,而不是深入了解此处的所有 Activity 部件。

简而言之:它应该可以工作,但使用后果自负 :)

还要注意,我在此答案中涉及了许多细节,以期使它可以被任何Swift开发人员访问,甚至包括那些没有使用Objective-C或C语言的开发人员。您可能已经知道以下详细内容。

在TextKit和字形上

要理解的一件事很重要,那就是字形是一个或多个字符的可视表示形式,如WWDC 2018 Session 221“TextKit Best Practices”中所述:

slide of session 221 explaining the difference between characters and glyphs

我建议收看整个谈话。在了解layoutManager(_:shouldGenerateGlyphs:properties:characterIndexes:font:forGlyphRange:)的工作原理的特定情况下,它并不是 super 有帮助,但是它提供了大量有关TextKit的工作原理的一般信息。

了解shouldGenerateGlyphs
所以。据我了解,每次NSLayoutManager即将在渲染它们之前生成一个新的字形时,它将使您有机会通过调用layoutManager(_:shouldGenerateGlyphs:properties:characterIndexes:font:forGlyphRange:)来修改此字形。

修改字形

根据文档,如果要修改字形,则应在此方法中通过调用setGlyphs(_:properties:characterIndexes:font:forGlyphRange:)进行修改。

对我们来说很幸运,setGlyphs期望使用与shouldGenerateGlyphs中传递给我们的参数完全相同的参数。这意味着从理论上讲,您可以仅通过调用shouldGenerateGlyphs来实现setGlyphs,一切都会好起来的(但这并不是 super 有用)。

返回值

该文档还说shouldGenerateGlyphs的返回值应该是“此方法中存储的实际字形范围”。这没有多大意义,因为期望的返回类型是Int,而不是人们期望的NSRange。从反复试验中,我认为框架希望我们在这里返回从索引0开始的传递的glyphRange中修改的字形的数量(稍后再进行介绍)。

另外,“存储在此方法中的字形范围”是对setGlyphs的调用,该调用将在内部存储新生成的字形(此字词措辞很差)。

一个不太有用的实现

因此,这是shouldGenerateGlyphs的正确实现(该操作什么都不做):

func layoutManager(_ layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>, properties: UnsafePointer<NSLayoutManager.GlyphProperty>, characterIndexes: UnsafePointer<Int>, font: UIFont, forGlyphRange glyphRange: NSRange) -> Int {
layoutManager.setGlyphs(glyphs, properties: fixedPropertiesPointer, characterIndexes: characterIndexes, font: font, forGlyphRange: glyphRange)

return glyphRange.length
}

它也应该等效于仅从方法返回 0:

By returning 0, it can indicate for the layout manager to do the default processing.



做有用的事

因此,现在,我们如何编辑字形属性以使此方法做有用的事情(例如隐藏字形)?

访问参数值
shouldGenerateGlyphs的大多数参数是 UnsafePointer。这是Swift层中泄漏的TextKit C API,并且首先使实现此方法变得很麻烦。

关键是 这里所有UnsafePointer类型的参数都是数组(在C中, SomeType *或它的Swift等效 UnsafePointer<SomeType>-是我们表示数组的方式), 和这些数组的长度均为glyphRange.length 。间接记录在 setGlyphs方法中:

Each array has glyphRange.length items



这意味着使用Apple给我们的漂亮的 UnsafePointer API,我们可以使用如下循环来迭代这些数组的元素:

for i in 0 ..< glyphRange.length {
print(properties[i])
}

在幕后, UnsafePointer将执行指针算术,以在给定下标传递给任何索引的情况下访问正确地址的内存。我建议阅读 UnsafePointer文档,这确实是很酷的东西。

传递对setGlyphs有用的东西

现在,我们可以打印参数的内容,并检查框架为每个字形赋予我们的属性。现在,我们如何修改它们并将结果传递给 setGlyphs

首先,需要注意的重要一点是,虽然我们可以直接修改 properties参数,但这可能是一个坏主意,因为该内存块不是我们所有的,而且我们不知道一旦退出该内存框架将如何处理该内存。方法。

因此,正确的方法是创建我们自己的字形属性数组,然后将其传递给 setGlyphs:

var modifiedGlyphProperties = [NSLayoutManager.GlyphProperty]()
for i in 0 ..< glyphRange.length {
// This contains the default properties for the glyph at index i set by the framework.
var glyphProperties = properties[i]
// We add the property we want to the mix. GlyphProperty is an OptionSet, we can use `.insert()` to do that.
glyphProperties.insert(.null)
// Append this glyph properties to our properties array.
modifiedGlyphProperties.append(glyphProperties)
}

// Convert our Swift array to the UnsafePointer `setGlyphs` expects.
modifiedGlyphProperties.withUnsafeBufferPointer { modifiedGlyphPropertiesBufferPointer in
guard let modifiedGlyphPropertiesPointer = modifiedGlyphPropertiesBufferPointer.baseAddress else {
fatalError("Could not get base address of modifiedGlyphProperties")
}

// Call setGlyphs with the modified array.
layoutManager.setGlyphs(glyphs, properties: modifiedGlyphPropertiesPointer, characterIndexes: characterIndexes, font: font, forGlyphRange: glyphRange)
}

return glyphRange.length

重要的是从 properties数组中读取原始字形属性,并将自定义属性添加到该基本值中(使用 .insert()方法)。否则,您将覆盖字形的默认属性,并且会发生奇怪的事情(例如,我已经看到 \n字符不再插入可视换行符了)。

确定要隐藏的字形

先前的实现应该可以正常工作,但是现在我们无条件地隐藏所有生成的字形,并且如果我们仅隐藏其中的一些字形(在您的情况下,当字形为 *时)将更加有用。

基于字符值的隐藏

为此,您可能需要访问用于生成最终字形的字符。但是,该框架不会为您提供字符,而是为每个生成的字形在字符串中提供它们的索引。您将需要遍历这些索引,并查看NSTextStorage以找到相应的字符。

不幸的是,这并不是一件容易的事:Foundation使用UTF-16代码单元在内部表示字符串(这就是NSString和NSAttributedString在后台使用的功能)。因此,该框架为我们提供的 characterIndexes并不是通常意义上的“字符”索引,而是UTF-16代码单元的索引†。

在大多数情况下,每个UTF-16代码单元都会用于生成唯一的字形,但是在某些情况下,会使用多个代码单元来生成唯一的字形(这称为UTF-16代理对,在以下情况下很常见使用表情符号处理字符串)。我建议您使用更多“异国情调”字符串来测试您的代码,例如:

textView.text = "Officiellement nous (👨‍👩‍👧‍👧) vivons dans un cha\u{0302}teau 🏰 海"

因此,为了能够比较我们的字符,我们首先需要将它们转换为我们通常所说的“字符”的简单表示形式:

/// Returns the extended grapheme cluster at `index` in an UTF16View, merging a UTF-16 surrogate pair if needed.
private func characterFromUTF16CodeUnits(_ utf16CodeUnits: String.UTF16View, at index: Int) -> Character {
let codeUnitIndex = utf16CodeUnits.index(utf16CodeUnits.startIndex, offsetBy: index)
let codeUnit = utf16CodeUnits[codeUnitIndex]

if UTF16.isLeadSurrogate(codeUnit) {
let nextCodeUnit = utf16CodeUnits[utf16CodeUnits.index(after: codeUnitIndex)]
let codeUnits = [codeUnit, nextCodeUnit]
let str = String(utf16CodeUnits: codeUnits, count: 2)
return Character(str)
} else if UTF16.isTrailSurrogate(codeUnit) {
let previousCodeUnit = utf16CodeUnits[utf16CodeUnits.index(before: codeUnitIndex)]
let codeUnits = [previousCodeUnit, codeUnit]
let str = String(utf16CodeUnits: codeUnits, count: 2)
return Character(str)
} else {
let unicodeScalar = UnicodeScalar(codeUnit)!
return Character(unicodeScalar)
}
}

然后,我们可以使用此函数从textStorage中提取字符,并对其进行测试:

// First, make sure we'll be able to access the NSTextStorage.
guard let textStorage = layoutManager.textStorage else {
fatalError("No textStorage was associated to this layoutManager")
}


// Access the characters.
let utf16CodeUnits = textStorage.string.utf16
var modifiedGlyphProperties = [NSLayoutManager.GlyphProperty]()
for i in 0 ..< glyphRange.length {
var glyphProperties = properties[i]
let character = characterFromUTF16CodeUnits(utf16CodeUnits, at: characterIndex)

// Do something with `character`, e.g.:
if character == "*" {
glyphProperties.insert(.null)
}

modifiedGlyphProperties.append(glyphProperties)
}

// Convert our Swift array to the UnsafePointer `setGlyphs` expects.
modifiedGlyphProperties.withUnsafeBufferPointer { modifiedGlyphPropertiesBufferPointer in
guard let modifiedGlyphPropertiesPointer = modifiedGlyphPropertiesBufferPointer.baseAddress else {
fatalError("Could not get base address of modifiedGlyphProperties")
}

// Call setGlyphs with the modified array.
layoutManager.setGlyphs(glyphs, properties: modifiedGlyphPropertiesPointer, characterIndexes: characterIndexes, font: font, forGlyphRange: glyphRange)
}

return glyphRange.length

请注意,在代理对的情况下,循环将执行两次(一次在主代理上,一次在尾部代理上),最终您将比较相同的结果字符两次。这很好,但是因为您需要在生成的字形的两个“部分”上应用所需的相同修改。

基于TextStorage字符串属性的隐藏

这不是您在问题中所要的,而是为了完成(因为这是我在我的应用程序中所做的事情),在这里您如何访问textStorage字符串属性以隐藏一些字形(在本示例中,我将隐藏所有字形)具有超文本链接的文本部分):

// First, make sure we'll be able to access the NSTextStorage.
guard let textStorage = layoutManager.textStorage else {
fatalError("No textStorage was associated to this layoutManager")
}

// Get the first and last characters indexes for this glyph range,
// and from that create the characters indexes range.
let firstCharIndex = characterIndexes[0]
let lastCharIndex = characterIndexes[glyphRange.length - 1]
let charactersRange = NSRange(location: firstCharIndex, length: lastCharIndex - firstCharIndex + 1)

var hiddenRanges = [NSRange]()
textStorage.enumerateAttributes(in: charactersRange, options: []) { attributes, range, _ in
for attribute in attributes where attribute.key == .link {
hiddenRanges.append(range)
}
}

var modifiedGlyphProperties = [NSLayoutManager.GlyphProperty]()
for i in 0 ..< glyphRange.length {
let characterIndex = characterIndexes[i]
var glyphProperties = properties[i]

let matchingHiddenRanges = hiddenRanges.filter { NSLocationInRange(characterIndex, $0) }
if !matchingHiddenRanges.isEmpty {
glyphProperties.insert(.null)
}

modifiedGlyphProperties.append(glyphProperties)
}

// Convert our Swift array to the UnsafePointer `setGlyphs` expects.
modifiedGlyphProperties.withUnsafeBufferPointer { modifiedGlyphPropertiesBufferPointer in
guard let modifiedGlyphPropertiesPointer = modifiedGlyphPropertiesBufferPointer.baseAddress else {
fatalError("Could not get base address of modifiedGlyphProperties")
}

// Call setGlyphs with the modified array.
layoutManager.setGlyphs(glyphs, properties: modifiedGlyphPropertiesPointer, characterIndexes: characterIndexes, font: font, forGlyphRange: glyphRange)
}

return glyphRange.length

†要了解两者之间的区别,建议阅读 the Swift Documentation on "Strings and Characters"。还要注意,框架在此处称为“字符”的是 而不是,与Swift称为 Character的(或“扩展字素簇”)相同。同样,TextKit框架的“字符”是UTF-16代码单元(在Swift中由 Unicode.UTF16.CodeUnit表示)。

更新2020-04-16 :利用 .withUnsafeBufferPointermodifiedGlyphProperties数组转换为UnsafePointer。它消除了拥有数组实例变量以使其在内存中保持 Activity 状态的需要。

关于nstextview - 在Swift中使用NSLayoutManager隐藏Markdown字符,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57467951/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com