Created
October 6, 2020 06:18
-
-
Save stleamist/aa9b8ad3388c23eb837ece2e0a9b5ddb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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