김경록의 앱 개발 여정

[Swift] 행간, 첫 줄 들여쓰기 등 적용하기 Paragraph Style 본문

TIL

[Swift] 행간, 첫 줄 들여쓰기 등 적용하기 Paragraph Style

Kim Roks 2025. 1. 13. 16:58

 

이전 작성한 포스팅 공백이 포함된 커스텀 뷰 구현기 를 작성 후 아쉬움이 남아 더 나아 보이는 좋은 방법을 찾았고 이를 기록하고자 합니다.

Paragraph Style이란?

NSParagraphStyle과 NSMutableParagraphStyle은 iOS에서 텍스트의 문단 스타일을 제어하는 데 사용되는 클래스입니다.

이를 통해 텍스트의 줄 간격, 들여쓰기, 정렬, 줄 바꿈 모드 등 다양한 문단 관련 속성을 설정할 수 있습니다.
주로 NSAttributedString과 함께 사용되며, 텍스트에 대한 세부적인 스타일링을 가능하게 합니다.

 

 

주요 속성들

  1. lineSpacing
    • 문단 내 줄 간격을 설정합니다.
    • 기본값은 0이고, 양수 값으로 설정하면 줄 간 간격이 늘어납니다.
  2. paragraphSpacing
    • 문단 간 간격을 설정합니다.
    • 문단의 마지막 줄과 다음 문단의 첫 줄 사이의 간격을 조정합니다.
  3. alignment
    • 텍스트 정렬 방식을 설정합니다.
    • 예: .left, .center, .right, .justified
  4. firstLineHeadIndent
    • 문단의 첫 줄 들여 쓰기 간격을 설정합니다.
    • 예: 목록이나 강조 문단 작성 시 사용.
  5. headIndent
    • 문단의 모든 줄(첫 줄 제외)의 들여쓰기 간격을 설정합니다.
  6. tailIndent
    • 텍스트의 오른쪽 경계(끝점)를 설정합니다.
  7. lineBreakMode
    • 텍스트 줄 바꿈 방식을 설정합니다.
    • 예:. byTruncatingTail,. byWordWrapping
  8. minimumLineHeight / maximumLineHeight
    • 줄 높이의 최소 및 최댓값을 설정합니다.
  9. baseWritingDirection
    • 텍스트의 쓰기 방향을 설정합니다.
    • 예:. natural,. leftToRight,. rightToLeft

 

 

사용 예시

지난번과 비슷하지만 간소화된 ui를 작성해 보았습니다.

그냥 아무렇게나 썻어요..!

분홍색으로 칠해진 '스포츠' 부분을 카테고리, 나머지 제목 부분을 타이틀이라 칭하겠습니다.

 

 

해당 ui에서 의도된 부분은 다음과 같습니다.

  • 카테고리는 배경색을 가지며, CornerRadius 값을 가집니다.
  • 타이틀 부분은 카테고리와 이어지듯 보이며 최대 두줄로 표현됩니다. 단, 줄 바꿈 시 카테고리 바로 아래쪽으로 이어나갑니다.
  • 카테고리와 타이틀은 폰트가 서로 다릅니다.

 

이전에 어떻게 구현했었는지는 따로 다루지 않겠습니다.

타이틀에 firstLineHeadIndent  속성을 사용하여 문단 첫 시작에 공백을 포함시킬 수 있었습니다.

단, 해당 넓이는 동적으로 계산되어야 하므로 카테고리의 넓이가 완전히 정해진 후 계산되어야 합니다.

전체 코드는 아래와 같습니다. 주석이 쓰인 부분만 보셔도 무방합니다.

더보기
import UIKit
import SnapKit

class ViewController: UIViewController {
    let categoryView: UIView = {
        let view = UIView()
        view.backgroundColor = .magenta
        view.layer.cornerRadius = 4
        view.layer.masksToBounds = true
        
        return view
    }()
    
    let categoryLabel: UILabel = {
        let label = UILabel()
        label.textColor = .white
        label.font = .boldSystemFont(
            ofSize: 12
        )
        label.textAlignment = .center
        label.text = "스포츠"
        
        return label
    }()
    
    let titleView: UIView = {
        let view = UIView()
        
        return view
    }()
    
    let titleLabel: UILabel = {
        let label = UILabel()
        label.textColor = .black
        label.font = .boldSystemFont(ofSize: 16)
        label.numberOfLines = 2
        return label
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setlayout()
        setConstraints()
    }
    
    // category의 넓이가 구해지고 사용되어야하므로
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        configureHeadInset()
    }
    
    // 주요 사용 예시 
    func configureHeadInset() {
        // categoryView의 넓이를 기반으로 firstLineHeadIndent 설정
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.firstLineHeadIndent = categoryView.frame.width + 8 // 여백 추가 (옵션)
        let attributedText = NSMutableAttributedString(string: "요키치 - 웨스트브룩 NBA 역사상 최초의 동반 트리플 더블 2회 달성")
        attributedText.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedText.length))
        titleLabel.attributedText = attributedText
    }
    
    func setlayout() {
        view.addSubview(categoryView)
        categoryView.addSubview(categoryLabel)
        view.addSubview(titleView)
        titleView.addSubview(titleLabel)
    }
    
    func setConstraints() {
        categoryView.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide).offset(100)
            $0.leading.equalTo(view.safeAreaLayoutGuide).offset(20)
            $0.width.equalTo(41)
            $0.height.equalTo(18)
        }
        
        categoryLabel.snp.makeConstraints {
            $0.top.bottom.trailing.leading.equalTo(categoryView)
        }
        
        titleView.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide).offset(100)
            $0.leading.equalTo(view.safeAreaLayoutGuide).offset(20)
            $0.trailing.equalTo(view.safeAreaLayoutGuide).offset(-20)
        }
        titleLabel.snp.makeConstraints {
            $0.top.bottom.trailing.leading.equalTo(titleView)
        }
        
    }
}

 

또한 해당 방식을 사용하면 이전에 미처 신경 쓰지 못한 부분까지 구현할 수 있었습니다.

LineSpace 속성을 추가해 보겠습니다.

좌: 적용전, 우: 적용 후

 

configureHeadInset() 내에서 paragraphStyle.lineSpacing = 5로 값을 지정해줌

더욱 자연스러운 문단 내 줄 간격을 적용할 수 있었습니다.

사용법도 간단하고 강력한 기능을 제공해 주어서 앞으로도 애용하게 될 것 같습니다.