Attributed String in Swift: The right way

Attributed String in Swift: The right way

Tôi thường gặp phải khá nhiều trường hợp phải sử dụng Attributed String vì những yêu cầu quái dở của khách hàng =)) hoặc đội Design hơi phóng túng và phong lưu. Hồi mới code objective-C thì mỗi lần động đến Attributed String tôi luôn thấy rất khó chịu và mệt mỏi vì code rất mất thời gian với việc create từng đoạn string một, combine lại rồi apply style cho nó,... Bây giờ thì code swift và tôi tìm đc 1 bài viết rất hay về vấn đề này. Tác giả Daniele Margutti đã viết 1 library SwiftRichString để dành riêng cho việc xử lý attributed string trong swift: github mà chúng ta sẽ cùng xem xét như trong bài giới thiệu của tác giả ở link cuối bài.

Type Safety:

Swift implements type safety: Có nghĩa là bạn không thể sử dụng 1 variable vào những việc mà kiểu của nó không cho phép. Nói cách khác, theo quy tắc về "type safety", bạn có thể chia cho 0, đó không phải là type error nhưng bạn không thể thực hiện phép toán số học với 1 string trừ khi bạn convert nó trước.

Type safety có thể được mô tả trong một đặc điểm của ngôn ngữ và được thi hành trong 1 một trường phát triển như Xcode; nó cũng có thể được thi hành với việc lập trình cẩn trọng. Nếu ko có Type safety rất dễ để tạo thành những code không an toàn.

NSAttributedStrings sử dụng NSDictionary để set các thuộc tính cho text, các id của thuộc tính (kiểu strings) được truyền vào cùng với value của nó như sau:

shadow.shadowOffset = CGSizeMake(-2.0, -2.0)
let attributes = [
        NSFontAttributeName : titleFont,
        NSUnderlineStyleAttributeName : 1,
        NSForegroundColorAttributeName : UIColor.whiteColor(),
        NSTextEffectAttributeName : NSTextEffectLetterpressStyle, 
        NSStrokeWidthAttributeName : 3.0,
        NSShadowAttributeName : shadow
]
 
var title = NSAttributedString(string: "NSAttributedString", attributes: attributes)

Và để làm cho đoạn code trên an toàn hơn, tác giả đã tạo ra 1 khái niệm là Style: Bạn sẽ tạo ra 1 Style với tên của nó và gán các thuộc tính vào bằng cách sử dụng creational pattern truyền thống:

let myStyle = Style("super", {
  $0.font = FontAttribute(.TimesNewRomanPS_BoldItalicMT, size: 40) // font + size
  $0.underline = UnderlineAttribute(color: UIColor.blue, style: NSUnderlineStyle.styleSingle) // underline attributes
  $0.color = UIColor(hex: "#FF4555") // text color
  $0.align = .center // text alignment
})

Ngay cả thuộc tính của font cũng là type safe, bạn có thể tạo 1 đối tượng UIFont theo kiểu type-safe bằng cách chỉ định 1 trong nhũng font mà iOS cung cấp sẵn có. FontAttribute cũng có thể được mở rộng và thêm tên font chữ của riêng bạn. Khi nhìn vào thư viện SwiftRichString bạn có thể thấy 1 số cấu trúc khác như StrikeAttribute, ShadowAttribute hoặc UnderlineAttribute... tất cả đều được tạo ra để làm mọi thứ rõ ràng và nhanh chóng hơn trong việc sử dụng. Điều lý tưởng của Style đó là bạn có thể tạo ra được các bộ styles mà bạn có thể sử dụng trong code của các app tiếp theo. Có 1 kiểu Style đặc biệt duy nhất là .default: Nó được apply mặc định ở vị trí đầu tiên ko phụ thuộc vào thứ tự mà nó đc truyền cho các tham số trong 1 functions của styles. Điểu này đảm bảo bạn có thể có 1 base common style cho string của bạn và thêm vào 1 hoặc nhiều thuộc tính cho string đó 1 cách dễ dàng. Nếu bạn không cần sử dụng tagged string trong code, bạn có thể tạo 1 object Stylemà không có tên.

Painless Attributed String Creation and Management

Create 1 NSAttributedString, combine lại rồi apply style cho nó là cực kỳ tẻ nhạt và rườm rà và rất nhiều code dài dòng:

let yourAttributes = [
  NSForegroundColorAttributeName: UIColor.blackColor(),
  NSFontAttributeName: UIFont.systemFontOfSize(15)]

let yourOtherAttributes = [
  NSForegroundColorAttributeName: UIColor.redColor(),
  NSFontAttributeName: UIFont.systemFontOfSize(25)]

let partOne = NSMutableAttributedString(string: "This is an example ", attributes: yourAttributes)
let partTwo = NSMutableAttributedString(string: "for the combination of Attributed String!", attributes: yourOtherAttributes)

let combination = NSMutableAttributedString()

combination.appendAttributedString(partOne)
combination.appendAttributedString(partTwo)

Chúng ta có thể làm tốt hơn như sau:

et titleStyle,highglited = // just define your own style
// Let's combine simple String, String with applied attributes, as like you can imagine
let attributedText = "Hello" + username.set(style: titleStyle) + "," + " welcome here".set(style: highglited)

function set() cho phép bạn add thuộc tính vào 1 String đã có hoặc append thuộc tính trực tiếp cho 1 instance của NSAttributedString . Có vài kiểu set() mà cho phép bạn thiết lập các thuộc tính cho toàn bộ string hoặc 1 phần và cũng có thể hỗ trợ cho việc kết hợp pattern matching thông qua regular expression. Ví dụ như sau:

// Apply Style to String
var test_1 = "Hello".set(style: bold) + "\n" + "World!".set(style: highlighted)

// Apply Multiple Styles to a String (.default is set in first place)
var test_2 = "Hello".set(styles: mixStyle1,mixStyle2,defaultStyle)

// Apply Style with Pattern Matching
let test_3 = "prefix12 aaa3 prefix45".set(styles: bold, pattern: "fix([0-9])([0-9])", options: .caseInsensitive)
let test_4 = "👿🏅the winner".set(styles: bold, pattern: "the winner", options: .caseInsensitive)

// Apply Style to a substring
let test_5 = "Hello Man! Welcome".set(style: bold, range: 6..<10)

Rõ ràng nếu bạn đã đang làm việc với Attributed String instances, bạn ko muốn việc phải re-create chúng lặp đi lặp lại khi bạn muốn thêm vào nội dung mới. Ở ví dụ dưới, chúng ta sẽ append một renderd string vào một existing instance:

// Create an NSAttributedString with two styles
var test_a = "Hello".set(styles: mixStyle1,mixStyle2)
// Then remove mixStyle1 collection from characters 0..2 of the attributed string
test_a.remove(style: mixStyle1, range: 0..<3)
 
var test_a = "Hello World".set(styles: mixStyle1,mixStyle2)
// Remove attributes from mixStyle1 collection at specified substring
test_a.remove(style: mixStyle1, range: 0..<5)
// Add (incrementally / not replacing) attributes in mixStyle3 at specified substring
test_a.add(style: mixStyle3, range: 0..<4)

Render content from tag based source

Đôi khi chúng ta cần phải load formatted text từ 1 file, url hoặc một datasource nào đó. Về cơ bản, điểu này khá là giống với việc khi mà 1 browser render 1 HTML file. Rõ ràng NSAttributedStrings supports HTML nhưng không hề vui vẻ gì khi chơi với CSS 1 cách trực tiếp trong iOS =)). Ý tưởng ở đây là để giữ cho concept cảu các content được gắn tag bằn cách tạo ra 1 scanner hỗ trợ đầy đủ unicode để có thể phân tích cú pháp và hiển thị 1 string với các thuộc tính nhất định. Đó là những gì MarkupString làm, chủ cần load và parse 1 source string rồi sau đó cho phép bạn apply styles bằng function .render()

// Define your own rendering styles
let tag_center = Style("center" ...
let tag_italic = Style("italic" ...
let tag_extreme = Style("extreme" ...
let tag_underline = Style("underline" ...
let tag_default = Style.default { ...
                                 
// Parse content of the source string
let sourceString = "<center>The quick brown fox</center>\njumps over the lazy dog is an <italic>English-language</italic> pangram—a phrase that contains <italic>all</italic> of the letters of the alphabet. It is <extreme><underline>commonly</underline></extreme> used for touch-typing practice."
let parser = try! MarkupString(source: sourceString)
// Render with styles and produce an attributed string
let test_8 = test8_parser.render(withStyles: [tag_center,tag_italic,tag_extreme,tag_underline,tag_default])

Tagged String có thể được tạo ra bằng cách sử dụng functions .tagged() mà nhận 1 Style name (1 String) như là 1 object:

// Create a tagged string from code
let taggedString = "Hello".tagged("test") + " world ".tagged(tag_default)
// Then parse and render it
let parser = try! MarkupString(source: taggedString, styles: [tag_default,test_style])
let attributedText = parser.render()

Nguồn: Attributed String in Swift: The right way