Out of Bedlam Swiftish

BonMot 라이브러리

macOS나 iOS에서 텍스트 속성을 세밀하게 다루려면 NSAttributedString을 사용해야합니다. 하지만 사용법이 복잡하고 능숙하게 다루기까지 많은 시간이 걸리는 것이 사실입니다. NSAttributedString을 다룰 수 있도록 편의성을 제공하는 BonMot을 소개해 드립니다. BonMot은 스위프트와 Objective-C 모두를 지원하는 NSAttributedString 생성 라이브러리라고 할 수 있습니다. 최근 4.0 버전으로 진행하면서 스위프트를 우선하도록 스위프트 친화적인 문법으로 변경되고 있습니다. 이 글에서는 스위프트를 기준으로 설명하도록 하겠습니다. (예제 코드는 https://github.com/OutOfBedlam/Sample-BonMot 에서 내려받을 수 있습니다.)

Github Raizlab/BonMot

지원되는 기능

  • 폰트
  • 텍스트 색상
  • 트랙킹 (Tracking points 또는 em 단위로)
  • 첫 행 글머리 들여쓰기
  • 글머리 들여쓰기
  • 행 높이 (line height multiple)
  • 최대 행 높이
  • 최소 행 높이
  • 행 간격 (line spacing)
  • Line break mode
  • 문단 앞 간격
  • 문단 뒤 간격
  • Baseline offset
  • Hyphenation factor (_the threshold for hyphenating across line breaks)
  • 텍스트 정렬
  • 밑줄과 취소선
  • Figure case (대문자 vs. 소문자 숫자)
  • Figure spacing (mono space vs. proportional numbers)
  • URLs
  • 다중 행간 정렬이 가능한 인라인 이미지
  • 태그 파싱 (중첩된 태그는 제외)

사용법

BonMot을 사용하기 원하는 스위프트 소스 파일에서 import BonMot만 하면 됩니다.

SimpleExample

가장 간단한 형태의 예를 만들어 보겠습니다.

Xcode를 열고 새로운 macOS Cocoa Application을 만듭니다.

만들어진 프로젝트 폴더 아래에서 BonMot의 GitHub 레포지토리를 서브 모듈로 추가합니다.

git submodule add https://github.com/Raizlabs/BonMot.git

BonMot 그룹을 생성하고 위에서 서브 모듈로 추가된BonMot.xcodeproj를 Xcode 프로젝트에 추가합니다.

프로젝트를 선택하고 BonMot.framework가 Link 되도록 추가되었는지 확인하고 빌드시 프레임워크가 실행파일에 같이 포함되도록 아래 그림과 같이 설정합니다.

MainMenu.xib를 선택하고 Label을 추가합니다.

AppDelegate.swift에 추가한 Label에 대한 @IBOutlet weak var simpleExample: NSTextField!을 만들어 연결합니다. 최종적으로 AppDelegate.swift의 코드는 다음과 같습니다.

import Cocoa
import BonMot

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
  @IBOutlet weak var window: NSWindow!
  @IBOutlet weak var simpleExample: NSTextField!

  func applicationDidFinishLaunching(_ aNotification: Notification) {
		 // SimpleExample
		 simpleExample.attributedStringValue = "BonMot Example".styled(with: StringStyle(
		.tracking(.point(6)),
		.font(BONFont(name: "AvenirNextCondensed-Bold", size: 20)!),
		.alignment(.center)),
		.color(.red))
  }

  func applicationWillTerminate(_ aNotification: Notification) {
  }
}

빌드해서 실행하면 다음과 같이 속성 텍스트가 출력되는 것을 확인할 수 있습니다.

예제 코드에서 확인할 수 있듯이 보통의 문자열(String)에 .styled() 확장 함수를 호출하면서 다양한 속성 값들을 인자로 넘김으로써 NSAttributedString을 생성합니다. "BonMot Example".styled(with: ...)

BonMot을 사용하여 NSAttributedString을 만들고 직관적으로 속성을 변경하는 기본 흐름을 확인하였습니다. 아래로 설명할 내용들도 기본적으로 동일한 흐름을 따릅니다.

XML Example

문자열의 부분별로 스타일을 다르게 줄 수 있도록 XML형태의 태그를 임의로 정의하고 정의한 태그에 해당되는 스타일을 각각 다르게 적용할 수 있습니다. 아래의 예에서 localizedXMLString은 임의로 정의한 태그와 BonMot에서 정의하고 있는 태그 (BON:으로 시작되는 태그)를 포함하고 있습니다. 최종적으로 각 태그와 속성을 baseStyle을 정의하는 과정에서 .xmlRules을 사용하여 맵핑합니다.

// 보통은 NSLocalizedString로 부터 가져옮니다
let localizedXMLString = "I want to be different. If everyone is wearing <black><BON:noBreakSpace/>black,<BON:noBreakSpace/></black> I want to be wearing <red><BON:noBreakSpace/>red.<BON:noBreakSpace/></red>\n<signed><BON:emDash/>Maria Sharapova</signed> <racket/>"

// 색상을 입힌 이미지를 가져와서 행 높이에 맞추기위해 베이스라인을 조정합니다.
let racket = NSImage(named: "Tennis Racket")!.styled(with:
  .color(.raizlabsRed),
  .baselineOffset(-4.0))

// 스타일을 정의합니다.
let accent = StringStyle(.font(BONFont(name: "Chalkboard-Bold", size: 18)!))
let black = accent.byAdding(.color(.white), .backgroundColor(.black))
let red = accent.byAdding(.color(.white), .backgroundColor(.raizlabsRed))
let signed = accent.byAdding(.color(.raizlabsRed))

// 모든 xml 태그에 적용될 스타일들을 포함하여 하나의 스타일로 정의합니다.
let baseStyle = StringStyle(
  .font(BONFont(name: "GillSans-Light", size: 18)!),
  .lineHeightMultiple(1.8),
  .color(.darkGray),
  .xmlRules([
    .style("black", black),
    .style("red", red),
    .style("signed", signed),
    .enter(element: "racket", insert: racket)
    ]))

// NSAttributedString을 만들어서 적용합니다.
xmlExample.attributedStringValue = localizedXMLString.styled(with: baseStyle)

상수 racket의 초기화과정과 baseStyle에서 .enter(element: "racket", insert: racket)은 텍스트 사이에 이미지를 삽입하는 방법에 대한 예시입니다. (주의: 이 글을 작성하는 시점에 macOS상에서는 이미지 삽입이 정상적으로 동작하지 않고 있습니다. iOS상에서는 정상적으로 동작합니다.)

Composition Example

이 경우는 텍스트 조각들에 각각 스타일을 적용하여 하나의 텍스트로 조합하는 방법입니다. 앞에서 설명한 XML방식에 대비하여 이 방법의 단점은 문자열의 지역화가 필요한 경우에는 적용하기 어렵다는 점입니다. BonMot의 예제코드와 문서에도 XML방식을 권장하고 있습니다.

///////////////////////////
// Composition Example
let boat = NSImage(named: "boat")!.styled(with:
  .color(.raizlabsRed))

let preamble = baseStyle.byAdding(
  .font(BONFont(name: "AvenirNext-Bold", size: 14)!))

let bigger = baseStyle.byAdding(
  .font(BONFont(name: "AvenirNext-Heavy", size: 64)!))

compositeExample.attributedStringValue = NSAttributedString.composed(of: [
  "You’re going to need a\n".styled(with: preamble),
  "Bigger\n".localizedUppercase.styled(with: bigger),
  boat,
  ], baseStyle: StringStyle(.alignment(.center), .color(.black)))

이 코드에서 먼저 스타일을 정의한 후에 NSAttributedString.composed(of: []) 메서드를 통해서 속성이 다른 문자열들을 하나로 조합합니다.

이 글을 작성하고 있는 현 시점에서 macOS 버전에서는 그림파일을 이모티콘으로 텍스트 중에 인라인으로 넣는 기능에 문제가 있어서 위의 예제 코드에서처럼 그림파일을 로딩하여 삽이하는 것이 제대로 동작하지 않는 것으로 보입니다. 이 문제는 iOS에서는 문제가 없는 것으로 차차 해결 될 것으로 기대합니다.