Converter HTML em NSAttributedString no iOS

151

Eu estou usando uma instância de UIWebViewpara processar algum texto e colori-lo corretamente, ele fornece o resultado como HTML, mas em vez de exibi-lo no UIWebVieweu quero exibi-lo usando Core Textcom a NSAttributedString.

Sou capaz de criar e desenhar o, NSAttributedStringmas não tenho certeza de como posso converter e mapear o HTML na string atribuída.

Entendo que, no Mac OS X, NSAttributedStringexiste um initWithHTML:método, mas isso foi apenas uma adição ao Mac e não está disponível para iOS.

Sei também que existe uma pergunta semelhante a essa, mas ela não tinha respostas. Embora tentasse novamente, verificaria se alguém criou uma maneira de fazer isso e, em caso afirmativo, se poderia compartilhá-lo.

Joshua
fonte
2
A biblioteca NSAttributedString-Additions-for-HTML foi renomeada e rolada em uma estrutura pelo mesmo autor. Agora, ele se chama DTCoreText e inclui várias classes de layout de texto principal. Você pode encontrá-lo aqui
Brian Douglas Moakley

Respostas:

290

No iOS 7, o UIKit adicionou um initWithData:options:documentAttributes:error:método que pode inicializar um NSAttributedStringuso de HTML, por exemplo:

[[NSAttributedString alloc] initWithData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] 
                                 options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
                                           NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)} 
                      documentAttributes:nil error:nil];

Em Swift:

let htmlData = NSString(string: details).data(using: String.Encoding.unicode.rawValue)
let options = [NSAttributedString.DocumentReadingOptionKey.documentType:
        NSAttributedString.DocumentType.html]
let attributedString = try? NSMutableAttributedString(data: htmlData ?? Data(),
                                                          options: options,
                                                          documentAttributes: nil)
pix
fonte
28
Por alguma razão, a opção NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType está fazendo com que a codificação demore muito, muito tempo :(
Arie Litovsky
14
Pena que o NSHTMLTextDocumentType é (literalmente) ~ 1000x mais lento que a configuração de atributos com o NSRange. (Perfilada uma pequena etiqueta com um tag em negrito.)
Jason Moore
6
Esteja ciente de que se você não puder NSHTMLTextDocumentType com esse método, se desejar usá-lo em um encadeamento em segundo plano. Mesmo com o ios 7, ele não usa o TextKit para renderização em HTML. Dê uma olhada na biblioteca DTCoreText recomendada pela Ingve.
TJez
2
Impressionante. Apenas um pensamento, você provavelmente poderia fazer [NSNumber numberWithInt: NSUTF8StringEncoding] como @ (NSUTF8StringEncoding), não?
Jarsen
15
Eu estava fazendo isso, mas tenha cuidado no iOS 8. É dolorosamente lento, perto de um segundo para algumas centenas de caracteres. (Em iOS 7 foi quase instantânea.)
Norman
43

Há uma adição de código aberto de trabalho em andamento ao NSAttributedString de Oliver Drobnik no Github. Ele usa o NSScanner para análise de HTML.

Ingve
fonte
Requer uma implantação mínima do iOS 4.3 :( Não obstante, muito impressionante.
Oh Danny Boy
3
@Lirik Overkill para você, talvez, mas perfeito para outra pessoa, ou seja, seu comentário não é nem um pouco útil.
Wuf810 4/11
3
Observe que este projeto requer é de código aberto e coberto por uma licença BSD de 2 cláusulas padrão. Isso significa que você deve mencionar Cocoanetics como o autor original desse código e reproduzir o texto da LICENÇA dentro do seu aplicativo.
dulgan
28

A criação de um NSAttributedString a partir do HTML deve ser feita no thread principal!

Atualização: Acontece que a renderização HTML NSAttributedString depende do WebKit, e deve ser executada no thread principal ou ocasionalmente trava o aplicativo com um SIGTRAP .

Novo log de falha da Relic:

insira a descrição da imagem aqui

Abaixo está uma extensão atualizada do Swift 2 String segura para threads :

extension String {
    func attributedStringFromHTML(completionBlock:NSAttributedString? ->()) {
        guard let data = dataUsingEncoding(NSUTF8StringEncoding) else {
            print("Unable to decode data from html string: \(self)")
            return completionBlock(nil)
        }

        let options = [NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType,
                   NSCharacterEncodingDocumentAttribute: NSNumber(unsignedInteger:NSUTF8StringEncoding)]

        dispatch_async(dispatch_get_main_queue()) {
            if let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) {
                completionBlock(attributedString)
            } else {
                print("Unable to create attributed string from html string: \(self)")
                completionBlock(nil)
            }
        }
    }
}

Uso:

let html = "<center>Here is some <b>HTML</b></center>"
html.attributedStringFromHTML { attString in
    self.bodyLabel.attributedText = attString
}

Resultado:

insira a descrição da imagem aqui

Andrew Schreiber
fonte
Andrew. Isso está funcionando bem. Eu queria saber o que todos os eventos curtos que eu tenho que lidar no meu UITextView, se eu seguir essa abordagem. Ele pode lidar com eventos do calendário, chamadas, e-mails, links de sites etc. disponíveis em HTML? Espero que o UITextView seja capaz de lidar com eventos comparados ao UILabel.
harshit2811
A abordagem acima é boa apenas para formatação. Eu recomendaria usar TTTAttributedLabel se você precisar de manipulação de eventos.
Andrew Schreiber
A codificação padrão que NSAttributedString usa é NSUTF16StringEncoding (não UTF8!). É por isso que isso não vai funcionar. Ao menos em meu caso!
Umit Kaya
Essa deve ser a solução aceita. A conversa em cadeia de caracteres HTML em um encadeamento em segundo plano acabará por falhar e com bastante frequência durante a execução de testes.
Ratsimihah 20/0318
21

Extensão do inicializador Swift em NSAttributedString

Minha inclinação era adicionar isso como uma extensão ao NSAttributedStringinvés de String. Eu tentei como uma extensão estática e um inicializador. Eu prefiro o inicializador, que é o que eu incluí abaixo.

Swift 4

internal convenience init?(html: String) {
    guard let data = html.data(using: String.Encoding.utf16, allowLossyConversion: false) else {
        return nil
    }

    guard let attributedString = try?  NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) else {
        return nil
    }

    self.init(attributedString: attributedString)
}

Swift 3

extension NSAttributedString {

internal convenience init?(html: String) {
    guard let data = html.data(using: String.Encoding.utf16, allowLossyConversion: false) else {
        return nil
    }

    guard let attributedString = try? NSMutableAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else {
        return nil
    }

    self.init(attributedString: attributedString)
}
}

Exemplo

let html = "<b>Hello World!</b>"
let attributedString = NSAttributedString(html: html)
Mobile Dan
fonte
eu quero Olá mundo para ser assim <p> <b> <i> Olá </ i> </ b> <i> mundo </ i> </ p>
Uma Madhavi
Salvar alguns LOC e substituir guard ... NSMutableAttributedString(data:...por try self.init(data:...(e adicionar throwsà inicialização)
nyg
e, finalmente, ele não funciona - texto ganha o tamanho da fonte aleatória
Vyachaslav Gerchicov
2
Você está decodificar os dados com UTF-8, mas você codificado com UTF-16
Shyam Bhat
11

Esta é uma Stringextensão escrita em Swift para retornar uma string HTML como NSAttributedString.

extension String {
    func htmlAttributedString() -> NSAttributedString? {
        guard let data = self.dataUsingEncoding(NSUTF16StringEncoding, allowLossyConversion: false) else { return nil }
        guard let html = try? NSMutableAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil) else { return nil }
        return html
    }
}

Usar,

label.attributedText = "<b>Hello</b> \u{2022} babe".htmlAttributedString()

Acima, propositadamente, adicionei um unicode \ u2022 para mostrar que ele é renderizado corretamente.

Um trivial: a codificação padrão que NSAttributedStringusa é NSUTF16StringEncoding(não UTF8!).

samwize
fonte
UTF16 salvou meu dia, obrigado samwize!
Yueyu 26/03/19
UTF16 salvou meu dia, obrigado samwize!
Yueyu 26/03/19
6

Fiz algumas modificações na solução de Andrew e atualize o código para Swift 3:

Este código agora usa o UITextView como selfcapaz de herdar sua fonte original, tamanho da fonte e cor do texto

Nota: toHexString()é extensão daqui

extension UITextView {
    func setAttributedStringFromHTML(_ htmlCode: String, completionBlock: @escaping (NSAttributedString?) ->()) {
        let inputText = "\(htmlCode)<style>body { font-family: '\((self.font?.fontName)!)'; font-size:\((self.font?.pointSize)!)px; color: \((self.textColor)!.toHexString()); }</style>"

        guard let data = inputText.data(using: String.Encoding.utf16) else {
            print("Unable to decode data from html string: \(self)")
            return completionBlock(nil)
        }

        DispatchQueue.main.async {
            if let attributedString = try? NSAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil) {
                self.attributedText = attributedString
                completionBlock(attributedString)
            } else {
                print("Unable to create attributed string from html string: \(self)")
                completionBlock(nil)
            }
        }
    }
}

Exemplo de uso:

mainTextView.setAttributedStringFromHTML("<i>Hello world!</i>") { _ in }
He Yifei 何 一 非
fonte
5

Versão Swift 3.0 Xcode 8

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
}
fssilva
fonte
5

Swift 4


  • Inicializador de conveniência NSAttributedString
  • Sem guardas extras
  • lança erro

extension NSAttributedString {

    convenience init(htmlString html: String) throws {
        try self.init(data: Data(html.utf8), options: [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ], documentAttributes: nil)
    }

}

Uso

UILabel.attributedText = try? NSAttributedString(htmlString: "<strong>Hello</strong> World!")
AamirR
fonte
Você salva meu dia. Obrigado.
Pkc456
@ pkc456 meta.stackexchange.com/questions/5234/… , faça upvote :) obrigado!
AamirR
Como posso definir o tamanho e a família de fontes?
Kirqe 01/10/19
Isso é muito melhor do que o sugerido por Mobile Dan, já que não envolve uma cópia redundante com self.init (attributeString: attributeString)
cianeto
4

A única solução que você tem agora é analisar o HTML, criar alguns nós com os atributos point / font / etc, e combiná-los em um NSAttributedString. É muito trabalho, mas, se feito corretamente, pode ser reutilizável no futuro.

jer
fonte
1
Se o HTML for XHTML-Strict, você poderá usar o NSXMLDOcument e os amigos para ajudar na análise.
Dylan Lukes
Como você sugeriria que eu desenvolvesse os nós com determinados atributos?
Joshua
2
Esse é um detalhe de implementação. No entanto, ao analisar o HTML, você tem acesso a cada atributo para cada tag, o que especifica itens como nome da fonte, tamanho, etc. Você pode usar essas informações para armazenar os detalhes relevantes que você precisa adicionar ao texto atribuído como atributos. . Geralmente, você precisa se familiarizar com a análise antes de executar essa tarefa.
Jer
2

A solução acima está correta.

[[NSAttributedString alloc] initWithData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] 
                                 options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
                                           NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)} 
                      documentAttributes:nil error:nil];

Mas o aplicativo wioll falhará se você estiver executando o ios 8.1,2 ou 3.

Para evitar a falha, o que você pode fazer é: execute isso em uma fila. Para que ele esteja sempre no thread principal.

Nitesh Kumar Singh
fonte
@alecex Eu encontrei o mesmo problema! o aplicativo falhará no iOS 8.1, 2, 3. Mas ficará bem no iOS 8.4 ou posterior. Você pode explicar em detalhes como evitá-lo? ou existe alguma solução alternativa ou métodos podem ser usados?
Forte
Criei uma categoria rápida para lidar com isso, copiando os métodos do AppKit, que tem uma maneira muito fácil e intuitiva de fazer isso. Por que a Apple não adicioná-lo está além de mim .: github.com/cguess/NSMutableAttributedString-HTML
CGuess
2

O uso do NSHTMLTextDocumentType é lento e difícil de controlar estilos. Eu sugiro que você tente minha biblioteca chamada Atributika. Ele possui seu próprio analisador HTML muito rápido. Além disso, você pode ter qualquer nome de tag e definir qualquer estilo para eles.

Exemplo:

let str = "<strong>Hello</strong> World!".style(tags:
    Style("strong").font(.boldSystemFont(ofSize: 15))).attributedString

label.attributedText = str

Você pode encontrá-lo aqui https://github.com/psharanda/Atributika

Pavel Sharanda
fonte
2

Swift 3 :
Experimente o seguinte :

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
    }
}  

E para usar:

let str = "<h1>Hello bro</h1><h2>Come On</h2><h3>Go sis</h3><ul><li>ME 1</li><li>ME 2</li></ul> <p>It is me bro , remember please</p>"

self.contentLabel.attributedText = str.htmlAttributedString()
reza_khalafi
fonte
0

Extensões úteis

Inspirado por esta discussão, um pod, e exemplo ObjC de Erica Sadun no iOS Gourmet Cookbook p.80, eu escrevi uma extensão Stringe em NSAttributedStringir e voltar entre HTML simples cordas e NSAttributedStrings e vice-versa - no GitHub aqui , que Eu achei útil.

As assinaturas são (novamente, código completo em um Gist, link acima):

extension NSAttributedString {
    func encodedString(ext: DocEXT) -> String?
    static func fromEncodedString(_ eString: String, ext: DocEXT) -> NSAttributedString? 
    static func fromHTML(_ html: String) -> NSAttributedString? // same as above, where ext = .html
}

extension String {
    func attributedString(ext: DocEXT) -> NSAttributedString?
}

enum DocEXT: String { case rtfd, rtf, htm, html, txt }
AmitaiB
fonte
0

com fonte

extension NSAttributedString
{
internal convenience init?(html: String, font: UIFont? = nil) {
    guard let data = html.data(using: String.Encoding.utf16, allowLossyConversion: false) else {
        return nil
    }
    assert(Thread.isMainThread)
    guard let attributedString = try?  NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) else {
        return nil
    }
    let mutable = NSMutableAttributedString(attributedString: attributedString)
    if let font = font {
        mutable.addAttribute(.font, value: font, range: NSRange(location: 0, length: mutable.length))
    }
    self.init(attributedString: mutable)
}
}

Como alternativa, você pode usar as versões derivadas e definir a fonte no UILabel após definir o atributoString

Anton Tropashko
fonte
0

A conversão incorporada sempre define a cor do texto como UIColor.black, mesmo se você passar um dicionário de atributos com .forgroundColor definido para outra coisa. Para oferecer suporte ao modo DARK no iOS 13, tente esta versão da extensão em NSAttributedString.

extension NSAttributedString {
    internal convenience init?(html: String)                    {
        guard 
            let data = html.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }

        let options : [DocumentReadingOptionKey : Any] = [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ]

        guard
            let string = try? NSMutableAttributedString(data: data, options: options,
                                                 documentAttributes: nil) else { return nil }

        if #available(iOS 13, *) {
            let colour = [NSAttributedString.Key.foregroundColor: UIColor.label]
            string.addAttributes(colour, range: NSRange(location: 0, length: string.length))
        }

        self.init(attributedString: string)
    }
}
Stephen Orr
fonte