swift - NSAttributedString, cambia la fuente en general, ¿PERO conserva todos los demás atributos?
nsattributedstringkey (6)
¿Sería válido dejar que un UITextField haga el trabajo?
De esta manera,
attributedString
y
newfont
:
let textField = UITextField()
textField.attributedText = attributedString
textField.font = newFont
let resultAttributedString = textField.attributedText
Lo sentimos, estaba equivocado, mantiene los "Atributos de caracteres" como NSForegroundColorAttributeName, por ejemplo, el color, pero no los UIFontDescriptorSymbolicTraits, que describen negrita, cursiva, condensada, etc.
Esos pertenecen a la fuente y no a los "Atributos de caracteres". Entonces, si cambia la fuente, también está cambiando los rasgos. Lo siento, pero mi solución propuesta no funciona. La fuente de destino debe tener todos los rasgos disponibles como fuente original para que esto funcione.
Digamos que tengo un
NSMutableAttributedString
.
La cadena tiene una mezcla variada de formato en todo momento:
Aquí hay un ejemplo:
Esta cadena es un infierno cambiar en iOS , realmente apesta .
Sin embargo, la fuente per se no es la fuente que desea.
Quiero:
para cada personaje, cambie ese personaje a una fuente específica (por ejemplo, Avenir)
pero,
para cada carácter, mantenga cada una de las otras atribuciones (negrita, cursiva, colores, etc., etc.) que estaba previamente en ese personaje.
¿Cómo demonios haces esto?
Nota:
si agrega trivialmente un atributo "Avenir" en todo el rango: simplemente elimina todos los otros rangos de atributos , pierde todo el formato. Desafortunadamente, los atributos no son , de hecho, "aditivos".
Aquí hay una implementación mucho más simple que mantiene todos los atributos en su lugar, incluidos todos los atributos de fuente, excepto que le permite cambiar la fuente.
Tenga en cuenta que esto solo hace uso de la fuente (nombre) de la fuente pasada.
El tamaño se mantiene de la fuente existente.
Si también desea cambiar todos los tamaños de fuente existentes al nuevo tamaño, cambie
f.pointSize
a
font.pointSize
.
extension NSMutableAttributedString {
func replaceFont(with font: UIFont) {
beginEditing()
self.enumerateAttribute(.font, in: NSRange(location: 0, length: self.length)) { (value, range, stop) in
if let f = value as? UIFont {
let ufd = f.fontDescriptor.withFamily(font.familyName).withSymbolicTraits(f.fontDescriptor.symbolicTraits)!
let newFont = UIFont(descriptor: ufd, size: f.pointSize)
removeAttribute(.font, range: range)
addAttribute(.font, value: newFont, range: range)
}
}
endEditing()
}
}
Y para usarlo:
let someMutableAttributedString = ... // some attributed string with some font face you want to change
someMutableAttributedString.replaceFont(with: UIFont.systemFont(ofSize: 12))
Dado que la respuesta de rmaddy no funcionó para mí (
f.fontDescriptor.withFace(font.fontName)
no mantiene rasgos como negrita), aquí hay una versión actualizada de Swift 4 que también incluye actualización de color:
extension NSMutableAttributedString {
func setFontFace(font: UIFont, color: UIColor? = nil) {
beginEditing()
self.enumerateAttribute(
.font,
in: NSRange(location: 0, length: self.length)
) { (value, range, stop) in
if let f = value as? UIFont,
let newFontDescriptor = f.fontDescriptor
.withFamily(font.familyName)
.withSymbolicTraits(f.fontDescriptor.symbolicTraits) {
let newFont = UIFont(
descriptor: newFontDescriptor,
size: font.pointSize
)
removeAttribute(.font, range: range)
addAttribute(.font, value: newFont, range: range)
if let color = color {
removeAttribute(
.foregroundColor,
range: range
)
addAttribute(
.foregroundColor,
value: color,
range: range
)
}
}
}
endEditing()
}
}
Notas
El problema con
f.fontDescriptor.withFace(font.fontName)
es que elimina rasgos simbólicos como
italic
,
bold
o
compressed
, ya que por alguna razón anulará aquellos con rasgos predeterminados de esa fuente.
Por qué esto se me escapa tan completamente, incluso podría ser un descuido por parte de Apple;
o "no es un error, sino una característica", porque obtenemos los rasgos de la nueva fuente de forma gratuita.
Entonces, lo que tenemos que hacer es crear un descriptor de fuente que tenga los rasgos simbólicos del descriptor de fuente de la fuente original:
.withSymbolicTraits(f.fontDescriptor.symbolicTraits)
.
Props para rmaddy para el código inicial en el que iteré.
Ya envié esto en una aplicación de producción donde analizamos una cadena HTML a través de
NSAttributedString.DocumentType.html
y luego cambiamos la fuente y el color a través de la extensión anterior.
No hay problemas hasta ahora.
En Swift Use la siguiente extensión NSMutableAttributedString para cambiar la fuente con los atributos existentes. Es bastante simple ya que solo necesitamos establecer el atributo NSAttributedString.Key.font .
extension NSMutableAttributedString {
@discardableResult
func setFont(_ font: UIFont, range: NSRange? = nil)-> NSMutableAttributedString {
let range = range ?? NSMakeRange(0, self.length)
self.addAttributes([.font: font], range: range)
return self
}
}
Usos:
let attrString = NSMutableAttributedString("Hello...")
attrString.setFont(UIFont.systemFont(ofSize: 20)
Desafío: este es bastante simple de cambiar / establecer un atributo sin reflejar otros atributos. El problema es cuando el usuario desea editar el atributo de estilo de párrafo [si la propiedad de valor de matriz] sin cambiar otros valores de atributos de párrafo.
Versión Obj-C de la respuesta de @ manmal
@implementation NSMutableAttributedString (Additions)
- (void)setFontFaceWithFont:(UIFont *)font color:(UIColor *)color {
[self beginEditing];
[self enumerateAttribute:NSFontAttributeName
inRange:NSMakeRange(0, self.length)
options:0
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
UIFont *oldFont = (UIFont *)value;
UIFontDescriptor *newFontDescriptor = [[oldFont.fontDescriptor fontDescriptorWithFamily:font.familyName] fontDescriptorWithSymbolicTraits:oldFont.fontDescriptor.symbolicTraits];
UIFont *newFont = [UIFont fontWithDescriptor:newFontDescriptor size:font.pointSize];
if (newFont) {
[self removeAttribute:NSFontAttributeName range:range];
[self addAttribute:NSFontAttributeName value:newFont range:range];
}
if (color) {
[self removeAttribute:NSForegroundColorAttributeName range:range];
[self addAttribute:NSForegroundColorAttributeName value:newFont range:range];
}
}];
[self endEditing];
}
@end
Importante
rmaddy ha inventado una técnica completamente nueva para este molesto problema en iOS.
La respuesta de manmal es la versión final perfeccionada.
Solo para el registro histórico aquí es más o menos cómo lo harías en los viejos tiempos ...
// carefully convert to "our" font - "re-doing" any other formatting.
// change each section BY HAND. total PITA.
func fixFontsInAttributedStringForUseInApp() {
cachedAttributedString?.beginEditing()
let rangeAll = NSRange(location: 0, length: cachedAttributedString!.length)
var boldRanges: [NSRange] = []
var italicRanges: [NSRange] = []
var boldANDItalicRanges: [NSRange] = [] // WTF right ?!
cachedAttributedString?.enumerateAttribute(
NSFontAttributeName,
in: rangeAll,
options: .longestEffectiveRangeNotRequired)
{ value, range, stop in
if let font = value as? UIFont {
let bb: Bool = font.fontDescriptor.symbolicTraits.contains(.traitBold)
let ii: Bool = font.fontDescriptor.symbolicTraits.contains(.traitItalic)
// you have to carefully handle the "both" case.........
if bb && ii {
boldANDItalicRanges.append(range)
}
if bb && !ii {
boldRanges.append(range)
}
if ii && !bb {
italicRanges.append(range)
}
}
}
cachedAttributedString!.setAttributes([NSFontAttributeName: font_f], range: rangeAll)
for r in boldANDItalicRanges {
cachedAttributedString!.addAttribute(NSFontAttributeName, value: font_fBOTH, range: r)
}
for r in boldRanges {
cachedAttributedString!.addAttribute(NSFontAttributeName, value: font_fb, range: r)
}
for r in italicRanges {
cachedAttributedString!.addAttribute(NSFontAttributeName, value: font_fi, range: r)
}
cachedAttributedString?.endEditing()
}
.
Nota.
Solo por claridad en un punto relacionado.
Este tipo de cosas inevitablemente comienza como una cadena HTML.
Aquí hay una nota sobre cómo convertir una cadena que es html en una
NSattributedString
... terminarás con buenos rangos de atributos (cursiva, negrita, etc.) PERO las fuentes serán fuentes que no deseas.
fileprivate extension String {
func htmlAttributedString() -> NSAttributedString? {
guard let data = self.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
guard let html = try? NSMutableAttributedString(
data: data,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil) else { return nil }
return html
}
}
.
Incluso esa parte del trabajo no es trivial, lleva un tiempo procesarla. En la práctica, debes ponerlo en segundo plano para evitar el parpadeo.