Una extension Swift per String e HTML

Convertire stringhe HTML in NSAttributedString con una extension nativa di String, impostare font, dimensione e colore via CSS e leggere i document attributes.

Read in English

Tradotto dall’inglese con l’aiuto dell’IA e rivisto dall’autore.Leggi l’originale

Uno sfondo sfocato di codice sorgente colorato con il logo arancione di Swift a sinistra e un badge html e css a destra.

Una delle richieste più comuni che ricevo ogni giorno è gestire stringhe HTML provenienti da un backend, visualizzandole correttamente in una UILabel. Nella maggior parte dei casi un'app iOS deve convivere con una controparte web che interpreta l'HTML molto meglio di una UILabel.

Tutto questo era vero prima dell'arrivo di NSAttributedString, che ha cambiato le regole del gioco. Gli oggetti UILabel accettano anche un valore particolare in una proprietà chiamata attributedText:

Swift

let aLabel = UILabel()
aLabel.attributedText = NSAttributedString(string: “hello, world!”)

Ovviamente non basta incollare del testo HTML come valore di questa proprietà... altrimenti non ci sarebbe motivo di scrivere questo post.


All'inizio c'erano le extension fai da te, che permettevano di interpretare i tag HTML trasformandoli in attributi per la nostra NSAttributedString. In particolare mi sono innamorato di una di queste extension. L'ho usata molte volte in passato. Poi è arrivato Swift e quell'extension non è stata aggiornata. All'inizio, quando i progetti non erano così puri e contenevano sia Objective-C sia Swift, ho continuato a usarla. Negli ultimi due anni, però, le cose si sono complicate, perché i miei progetti ora sono completamente basati su Swift e non voglio più avere a che fare con codice "ibrido".

Naturalmente esistono molte altre extension simili scritte in Swift, ma nessuna mi ha dato l'"effetto wow" di quella, per molti motivi: complessità, gestione delle dipendenze e altro. Insomma, dovevo trovare un'altra strada.

Per caso mi sono imbattuto nel post di Dejan Atanasov. Si scopre che è possibile gestire l'HTML direttamente con componenti nativi di Swift. Vediamo l'operazione fondamentale, poi vi mostrerò come ho migliorato il suggerimento di Dejan.

NSAttributedString offre un costruttore che accetta in input la versione RAW della stringa in formato dati binari e alcune altre opzioni.

Swift

NSAttributedString(data:options:documentAttributes:)

Inoltre, e qui avviene la magia, se istruiamo NSAttributedString a considerare l'input grezzo come un documento HTML, il risultato sarà un oggetto correttamente inizializzato con tutti gli attributi possibili estratti direttamente dai tag HTML. Fantastico. Proprio quello che cercavamo.

Per i più curiosi c'è un'ulteriore opzione: è possibile ottenere anche un oggetto con un dizionario di tutti i parametri usati dal convertitore HTML, da riutilizzare in seguito (per esempio può servire ad allineare altri contenuti nella UIView che non fanno parte della pagina HTML).

Vediamo come costruire questa chiamata:

Swift

extension String {
    var html2Attributed: NSAttributedString? {
        do {
            guard let data = data(using: String.Encoding.utf8) else {
                return nil
            }
            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
}

Che cosa succede? Abbiamo esteso il comportamento dell'oggetto String per gestire la conversione che ci interessa. Prima di tutto verifichiamo di poter convertire la stringa in dati; se l'operazione fallisce per qualche motivo, restituiamo nil. Altrimenti procediamo con un costrutto try che a sua volta restituisce il valore che ci interessa. Il costrutto try catch ci permette di gestire eventuali eccezioni e di restituire nil in caso di errore.

Il metodo init NSAttributedString (data: options: documentAttributes:) accetta il valore binario della stringa come oggetto Data e un dizionario opzionale. Questo dizionario specifica che il .documentType dei dati in input è HTML, NSAttributedString.DocumentType.html, e ne indica anche la codifica, .characterEncoding: String.Encoding.utf8.rawValue.

Fin qui tutto bene. A questo punto possiamo convertire HTML in NSAttributedString in un solo passaggio:

Swift

aLabel.attributedText = “<b>Hello</b>,<i>world</i>”.html2Attributed

Fantastico! Restano però due problemi importanti da risolvere:

  • Noterete che il testo viene mostrato con un font predefinito molto diverso da quello a cui ci ha abituato iOS 11.

  • C'è un parametro che non abbiamo preso in considerazione e che abbiamo impostato a nil (documentAttributed).


Cominciamo dal primo problema. Per impostazione predefinita il testo HTML viene mostrato con il font Times New Roman. Per risolverlo ci sono molte strade, e dipende anche da ciò che vogliamo ottenere e dal contesto. Dato che abbiamo a che fare con HTML, una soluzione abbastanza flessibile usa il CSS e lo stile globale dell'html. Se avessimo una semplice pagina HTML con solo il nostro testo, potremmo usare un CSS simile al seguente per forzare lo stile del testo in tutta la pagina:

JavaScript

<style>
html * {font-size: 12.0pt !important;color: #383838 !important;font-family: Helvetica !important;}
</style>
<b>hello</b>, <i>world</i>

Portando questo concetto in una extension Swift:

Swift

import UIKit
import Foundation

extension String {
    func htmlAttributed(family: String?, size: CGFloat, color: UIColor) -> NSAttributedString? {
        do {
            let htmlCSSString = "<style>" +
                "html *" +
                "{" +
                "font-size: \(size)pt !important;" +
                "color: #\(color.hexString!) !important;" +
                "font-family: \(family ?? "Helvetica"), Helvetica !important;" +
            "}</style> \(self)"

            guard let data = htmlCSSString.data(using: String.Encoding.utf8) else {
                return nil
            }

            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
}

Molto, molto meglio! Ora il nostro testo HTML è inserito in un contesto HTML più complesso che usa un CSS. Notate come font-size, color e font-family siano parametrizzati tramite i parametri in input, il che rende il metodo abbastanza generale da usare in situazioni diverse.

Vale la pena notare come gestiamo UIColor. Quest'ultimo non ha un'extension integrata per la conversione in esadecimale del colore specificato:

Swift

import UIKit
import Foundation

extension UIColor {
    var hexString:String? {
        if let components = self.cgColor.components {
            let r = components[0]
            let g = components[1]
            let b = components[2]
            return  String(format: "%02X%02X%02X", (Int)(r * 255), (Int)(g * 255), (Int)(b * 255))
        }
        return nil
    }
}

Ultimo ma non meno importante, c'è il parametro documentAttributes che non abbiamo ancora usato. A cosa serve? Questo parametro funziona per riferimento (non per valore) e al termine della funzione viene popolato con alcune informazioni utili sul testo attribuito appena creato. Questi dati possono servire a conoscere l'area di rendering effettiva sullo schermo e altre informazioni di servizio che altrimenti sarebbero difficili da recuperare.

Swift

extension String {
    var htmlAttributed: (NSAttributedString?, NSDictionary?) {
        do {
            guard let data = data(using: String.Encoding.utf8) else {
                return (nil, nil)
            }

            var dict:NSDictionary?
            dict = NSMutableDictionary()

            return try (NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: &dict), dict)
        } catch {
            print("error: ", error)
            return (nil, nil)
        }
    }
}

Stavolta l'extension restituisce una tupla e non un singolo valore. In questo modo possiamo gestire in un solo passaggio i due valori importanti: il dizionario degli attributi e la relativa NSAttributedString.

È importante notare come il dizionario venga prima creato e poi passato per riferimento con & al metodo di inizializzazione. Così, quando usciamo dal costruttore, l'oggetto sarà correttamente valorizzato.


Mettiamo tutto insieme. L'extension completa, che offre vari metodi per affrontare situazioni diverse con le stringhe, si trova qui:

Swift

//
//  String+HTML.swift
//  AttributedString
//
//  Created by Costantino Pistagna on 08/11/2017.
//  Copyright © 2017 sofapps.it All rights reserved.
//
import UIKit
import Foundation

extension UIColor {
    var hexString:String? {
        if let components = self.cgColor.components {
            let r = components[0]
            let g = components[1]
            let b = components[2]
            return  String(format: "%02X%02X%02X", (Int)(r * 255), (Int)(g * 255), (Int)(b * 255))
        }
        return nil
    }
}

extension String {
    var html2Attributed: NSAttributedString? {
        do {
            guard let data = data(using: String.Encoding.utf8) else {
                return nil
            }
            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
    
    var htmlAttributed: (NSAttributedString?, NSDictionary?) {
        do {
            guard let data = data(using: String.Encoding.utf8) else {
                return (nil, nil)
            }

            var dict:NSDictionary?
            dict = NSMutableDictionary()

            return try (NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: &dict), dict)
        } catch {
            print("error: ", error)
            return (nil, nil)
        }
    }
    
    func htmlAttributed(using font: UIFont, color: UIColor) -> NSAttributedString? {
        do {
            let htmlCSSString = "<style>" +
                "html *" +
                "{" +
                "font-size: \(font.pointSize)pt !important;" +
                "color: #\(color.hexString!) !important;" +
                "font-family: \(font.familyName), Helvetica !important;" +
                "}</style> \(self)"

            guard let data = htmlCSSString.data(using: String.Encoding.utf8) else {
                return nil
            }

            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }

    func htmlAttributed(family: String?, size: CGFloat, color: UIColor) -> NSAttributedString? {
        do {
            let htmlCSSString = "<style>" +
                "html *" +
                "{" +
                "font-size: \(size)pt !important;" +
                "color: #\(color.hexString!) !important;" +
                "font-family: \(family ?? "Helvetica"), Helvetica !important;" +
            "}</style> \(self)"

            guard let data = htmlCSSString.data(using: String.Encoding.utf8) else {
                return nil
            }

            return try NSAttributedString(data: data,
                                          options: [.documentType: NSAttributedString.DocumentType.html,
                                                    .characterEncoding: String.Encoding.utf8.rawValue],
                                          documentAttributes: nil)
        } catch {
            print("error: ", error)
            return nil
        }
    }
}

Riferimenti

github.comNSAttributedString+DDHTMLSemplifica il lavoro con le stringhe attribuite permettendo di descrivere la formattazione tramite HTML.

medium.comCome convertire HTML con NSAttributedStringQuesto tutorial mostra come convertire HTML con NSAttributedString.

Continua a leggere

Tutti gli articoli
  1. WWDC18: ARKit 2, confronto delle prestazioni del tracking 2D
  2. Un workflow di sharing personalizzato con CloudKit
  3. Stored properties ed extension: un approccio Swift puro
↑ ↓ naviga↵ apriesc chiudi