Skip to content

Instantly share code, notes, and snippets.

@stleamist
Created October 6, 2020 06:18
Show Gist options
  • Save stleamist/aa9b8ad3388c23eb837ece2e0a9b5ddb to your computer and use it in GitHub Desktop.
Save stleamist/aa9b8ad3388c23eb837ece2e0a9b5ddb to your computer and use it in GitHub Desktop.
import UIKit
/*
EXAMPLE:
let hangulCharacterSet = CharacterSet(charactersIn: "가"..."힣").union(CharacterSet(charactersIn: "ㄱ"..."ㆎ"))
let nanumGothicDescriptor = UIFontDescriptor(name: "NanumGothic", size: 17).addingCharacterSet(hangulCharacterSet)
let font = UIFont.systemFont(ofSize: 17).addingFrontCascadeDescriptors([nanumGothicDescriptor])
let label = UILabel()
label.font = font
label.text = "Swift 프로그래밍 언어"
label.respectFontDescriptorSize()
*/
extension String {
// TODO: 연속되는 Range는 합쳐서 반환하기
// TODO: NSRange 대신 Range 사용하기
func rangesOfCharacter(from aSet: CharacterSet) -> [NSRange] {
var ranges: [NSRange] = []
for (index, character) in self.enumerated() {
guard let scalar = character.unicodeScalars.first else {
continue
}
if aSet.contains(scalar) {
let range = NSRange(location: index, length: 1)
ranges.append(range)
}
}
return ranges
}
}
extension UIFontDescriptor {
func addingCharacterSet(_ characterSet: CharacterSet) -> UIFontDescriptor {
return self.addingAttributes([.characterSet: characterSet])
}
var cascadeList: [UIFontDescriptor]? {
return self.fontAttributes[.cascadeList] as? [UIFontDescriptor]
}
var characterSet: CharacterSet? {
return self.fontAttributes[.characterSet] as? CharacterSet
}
var size: CGFloat? {
return self.fontAttributes[.size] as? CGFloat
}
}
extension UIFont {
convenience init(cascadeDescriptors: [UIFontDescriptor], finalDescriptor: UIFontDescriptor, size: CGFloat) {
var descriptors = cascadeDescriptors
/*
원래는 descriptors.removeFirst()로 base로서 사용할 descriptor를 제거해야 하나,
cascadeList 속성을 갖고 있는 descriptor로 UIFont를 초기화할 때 base descriptor의 characterSet 속성이 접근 불가능해지는 버그가 있어
base descriptor를 cascadeList에도 추가하여 나중에도 base descriptor에 접근 가능하게 한다.
참고 1: 이 때 base descriptor와 cascadeList[0]은 완전히 동일하므로 cascading 사용에 문제가 없다.
참고 2: base descriptor의 characterSet 속성은 접근만 불가능해지는 것일 뿐, 폰트에서는 여전히 base descriptor의 characterSet이 적용되어진 것으로 보여진다.
*/
guard let firstDescriptor = descriptors.first else { // descriptors.removeFirst()
self.init(descriptor: finalDescriptor, size: size)
return
}
descriptors.append(finalDescriptor)
let descriptor = firstDescriptor.addingAttributes([.cascadeList: descriptors])
self.init(descriptor: descriptor, size: size)
}
func addingFrontCascadeDescriptors(_ cascadeDescriptors: [UIFontDescriptor]) -> UIFont {
return UIFont(cascadeDescriptors: cascadeDescriptors, finalDescriptor: self.fontDescriptor, size: self.pointSize)
}
}
extension UILabel {
var mutatingAttributedText: NSMutableAttributedString? {
if let attributedText = self.attributedText {
if let mutableAttributedText = attributedText as? NSMutableAttributedString {
return mutableAttributedText
} else {
return NSMutableAttributedString(attributedString: attributedText)
}
} else {
return nil
}
}
}
class CascadingLabel: UILabel {
var respectsFontDescriptorSize: Bool = false
var lineHeight: CGFloat? {
didSet {
self.updateLineHeight()
}
}
var lineHeightMultipleForFontPointSize: CGFloat? {
didSet {
guard let lineHeightMultiple = self.lineHeightMultipleForFontPointSize else {
return
}
self.lineHeight = self.font.pointSize * lineHeightMultiple
self.updateLineHeight()
}
}
private var descriptorsToAlignMiddle: [(shiftingFontDescriptor: UIFontDescriptor, basisFontDescriptor: UIFontDescriptor)] = []
override var text: String? {
didSet {
if respectsFontDescriptorSize {
self.respectFontDescriptorSize()
}
if lineHeight != nil {
self.updateLineHeight()
}
for (shiftingFontDescriptor, basisFontDescriptor) in descriptorsToAlignMiddle {
self.alignMiddle(lineHeightOf: shiftingFontDescriptor, toCapHeightOf: basisFontDescriptor)
}
}
}
/* 최종 text와 font를 설정한 뒤에 호출해야 함. */
private func respectFontDescriptorSize() {
guard let text = self.text, let cascadeList = self.font.fontDescriptor.cascadeList else {
return
}
for descriptor in cascadeList.reversed() {
let size = descriptor.size ?? 0
let characterSet = descriptor.characterSet ?? CTFontCopyCharacterSet(UIFont(descriptor: descriptor, size: 0)) as CharacterSet
let rangeList = text.rangesOfCharacter(from: characterSet)
let mutableAttributedString = self.mutatingAttributedText
for range in rangeList {
let font = UIFont(descriptor: descriptor, size: size)
mutableAttributedString?.addAttribute(.font, value: font, range: range)
}
self.attributedText = mutableAttributedString
}
}
func alignMiddle(lineHeightOf shiftingFontDescriptor: UIFontDescriptor, toCapHeightOf basisFontDescriptor: UIFontDescriptor) {
self.descriptorsToAlignMiddle.append((shiftingFontDescriptor, basisFontDescriptor))
guard let text = self.text else {
return
}
let shiftingFont = UIFont(descriptor: shiftingFontDescriptor, size: shiftingFontDescriptor.pointSize)
let basisFont = UIFont(descriptor: basisFontDescriptor, size: basisFontDescriptor.pointSize)
let offset = (basisFont.capHeight / 2) - ((shiftingFont.ascender + shiftingFont.descender) / 2)
let shiftingCharacterSet = shiftingFontDescriptor.characterSet ?? CTFontCopyCharacterSet(shiftingFont) as CharacterSet
let rangeList = text.rangesOfCharacter(from: shiftingCharacterSet)
let mutableAttributedString = self.mutatingAttributedText
for range in rangeList {
// TODO: 폰트 크기 문제 때문에 .font를 재할당해야할지 결정해야 함.
mutableAttributedString?.addAttribute(.baselineOffset, value: offset, range: range)
}
self.attributedText = mutableAttributedString
}
// 러프한 코드
private func updateLineHeight() {
guard let lineHeight = self.lineHeight, let mutableAttributedString = self.mutatingAttributedText else {
return
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.maximumLineHeight = lineHeight
paragraphStyle.minimumLineHeight = lineHeight
mutableAttributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: mutableAttributedString.length))
self.attributedText = mutableAttributedString
}
}
extension CascadingLabel {
func addKoreanFrontCascadeFont(name: String, pointSizeRatio: CGFloat = 1, lineHeightMultiple: CGFloat? = nil) {
// alignMiddle 메소드에 사용할 font descriptor가 addingFrontCascadeDescriptors 호출 시 대체되므로 이전에 미리 할당해 놓는다.
let basisFontDescriptor = self.font.fontDescriptor
let koreanFontDescriptor = UIFontDescriptor(fontAttributes: [
.name : name,
.size: self.font.pointSize * pointSizeRatio,
.characterSet: CharacterSet.hangulLetters
])
self.font = self.font.addingFrontCascadeDescriptors([koreanFontDescriptor])
self.respectsFontDescriptorSize = true
self.alignMiddle(lineHeightOf: koreanFontDescriptor, toCapHeightOf: basisFontDescriptor)
if lineHeightMultiple != nil {
self.lineHeightMultipleForFontPointSize = lineHeightMultiple
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment