Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- uitableview
- RxCocoa
- rxswift
- swift documentation
- swiftUI
- Human interface guide
- ribs
- Refactoring
- MVVM
- Clean Code
- UITextView
- Protocol
- HIG
- uiscrollview
- 리팩토링
- clean architecture
- SWIFT
- Xcode
- Observable
- ios
- tableView
- 스위프트
- 리펙터링
- combine
- collectionview
- UICollectionView
- map
- 클린 코드
- 애니메이션
- 리펙토링
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] [오픈소스 까보기] apple foundation - AttributedString (#Guts, #Run) 본문
오픈소스 까보기
[iOS - swift] [오픈소스 까보기] apple foundation - AttributedString (#Guts, #Run)
jake-kim 2023. 8. 23. 16:11* foundation의 AttributedString.swift Github 주소 참고
AttributedString.swift 구현부
* NSAttributedString 개념은 이전 포스팅 글 참고
- AttributedString은 Sendable을 따르고 있으므로 동시성 처리에 안전
- 내부적으로 _guts를 가지고 있는데 guts는 core라는 의미
// AttributedString.swift
@dynamicMemberLookup
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public struct AttributedString : Sendable {
internal var _guts: Guts
internal init(_ guts: Guts) {
_guts = guts
}
}
- AttributedString+Guts.swift에서 Guts를 정의
extension AttributedString {
internal final class Guts : @unchecked Sendable {
typealias Index = AttributedString.Index
typealias Runs = AttributedString.Runs
typealias AttributeMergePolicy = AttributedString.AttributeMergePolicy
typealias AttributeRunBoundaries = AttributedString.AttributeRunBoundaries
typealias _InternalRun = AttributedString._InternalRun
typealias _InternalRuns = AttributedString._InternalRuns
typealias _AttributeValue = AttributedString._AttributeValue
typealias _AttributeStorage = AttributedString._AttributeStorage
var string: BigString
var runs: _InternalRuns
// Note: the caller is responsible for performing attribute fix-ups if needed based on the source of the runs
init(string: BigString, runs: _InternalRuns) {
precondition(string.isEmpty == runs.isEmpty, "An empty attributed string should not contain any runs")
self.string = string
self.runs = runs
}
// Note: the caller is responsible for performing attribute fix-ups if needed based on the source of the runs
convenience init(string: String, runs: _InternalRuns) {
self.init(string: BigString(string), runs: runs)
}
convenience init() {
self.init(string: BigString(), runs: _InternalRuns())
}
}
}
- Guts를 잠깐 살펴보면 내부적으로 startIndex, endIndex 이런 값들을 가져오는 core 역할을 수행
public var startIndex : Index {
Index(_guts.string.startIndex)
}
public var endIndex : Index {
Index(_guts.string.endIndex)
}
@preconcurrency
public subscript<K: AttributedStringKey>(_: K.Type) -> K.Value? where K.Value : Sendable {
get {
_guts.getUniformValue(in: _stringBounds, key: K.self)?.rawValue(as: K.self)
}
set {
ensureUniqueReference()
if let v = newValue {
_guts.setAttributeValue(v, forKey: K.self, in: _stringBounds)
} else {
_guts.removeAttributeValue(forKey: K.self, in: _stringBounds)
}
}
}
- 아래쪽 코드를 보면 public init()으로 생성자를 정의하고 여기서 내부적으로 구현한 Guts()값을 넣어주어서 사용
- Guts()는 내부적으로 가지고 있고 여기서 문자열 처리에 관한 코어 기능을 담당
- AttributedString 자체는 인터페이스만 있고, 이 안에 코어 기능을 담당하는 Guts을 두어서 기능과 인터페이스를 분리하여 개발된 형태
// MARK: Initialization
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString {
public init() {
self._guts = Guts()
}
internal init(_ s: some AttributedStringProtocol) {
if let s = _specializingCast(s, to: AttributedString.self) {
self = s
} else if let s = _specializingCast(s, to: AttributedSubstring.self) {
self = AttributedString(s)
} else {
// !!!: We don't expect or want this to happen.
let substring = AttributedSubstring(s.__guts, in: s._stringBounds)
self = AttributedString(substring)
}
}
...
}
코어 기능을 담당하는 Guts 살펴보기
- AttributedString+Guts.swift 파일에 정의되어 있는 Guts
extension AttributedString {
internal final class Guts : @unchecked Sendable {
typealias Index = AttributedString.Index
typealias Runs = AttributedString.Runs
typealias AttributeMergePolicy = AttributedString.AttributeMergePolicy
...
var string: BigString
var runs: _InternalRuns
// Note: the caller is responsible for performing attribute fix-ups if needed based on the source of the runs
init(string: BigString, runs: _InternalRuns) {
precondition(string.isEmpty == runs.isEmpty, "An empty attributed string should not contain any runs")
self.string = string
self.runs = runs
}
// Note: the caller is responsible for performing attribute fix-ups if needed based on the source of the runs
convenience init(string: String, runs: _InternalRuns) {
self.init(string: BigString(string), runs: runs)
}
convenience init() {
self.init(string: BigString(), runs: _InternalRuns())
}
}
}
- 애플의 코딩 스타일을 보면 extension으로 기능별로 분리를 한 상태
(첫번째 extension - 타입 정의, 초기화 코드)
extension AttributedString {
internal final class Guts : @unchecked Sendable {
typealias Index = AttributedString.Index
typealias Runs = AttributedString.Runs
...
// Note: the caller is responsible for performing attribute fix-ups if needed based on the source of the runs
init(string: BigString, runs: _InternalRuns) {
precondition(string.isEmpty == runs.isEmpty, "An empty attributed string should not contain any runs")
self.string = string
self.runs = runs
}
...
}
}
(두번째 extension - copy와 관련된 함수)
- __consuming 키워드는 성능 최적화를 위해 사용한 것 - 구체적인 개념은 이전 포스팅 글 참고
extension AttributedString.Guts {
__consuming func copy() -> AttributedString.Guts {
AttributedString.Guts(string: self.string, runs: self.runs)
}
__consuming func copy(in range: Range<BigString.Index>) -> AttributedString.Guts {
let string = BigString(self.string.unicodeScalars[range])
let runs = self.runs.extract(utf8Offsets: range._utf8OffsetRange)
let copy = AttributedString.Guts(string: string, runs: runs)
// FIXME: Extracting a slice should not invalidate anything but .textChanged attribute runs on the edges
if range.lowerBound != string.startIndex || range.upperBound != string.endIndex {
var utf8Range = copy.stringBounds._utf8OffsetRange
utf8Range = copy.enforceAttributeConstraintsBeforeMutation(to: utf8Range)
copy.enforceAttributeConstraintsAfterMutation(in: utf8Range, type: .attributesAndCharacters)
}
return copy
}
}
runs 인스턴스 분석
- 첫번째 copy() 메소드는 Guts인스턴스를 리턴하는데, 여기서 string, runs 프로퍼티를 받아서 리턴
extension AttributedString.Guts {
__consuming func copy() -> AttributedString.Guts {
AttributedString.Guts(string: self.string, runs: self.runs)
}
}
- 여기서 Guts가 가지고 있는 runs 값이란?
- InternalRuns라는 구조체
- 안에 Rope를 가지고 있는 상태
- 이 rope를 단순히 wrapping하고 있는 구조체이며, extension으로 각 필요한 기능들을 확장해나가는 구현
- (예측할 수 있는 것은 Rope라는 곳은 여러곳에서 사용되기 때문에 InternalRuns안에서는 wrapping해서 사용하여 Rope의 변화를 안주고 사용할 수 있도록 구현된 것)
typealias _InternalRuns = AttributedString._InternalRuns
var runs: _InternalRuns
...
/// An internal convenience wrapper around `Rope<_InternalRun>`, giving it functionality
/// that's specific to attributed strings.
struct _InternalRuns: Sendable {
typealias _InternalRun = AttributedString._InternalRun
typealias Storage = Rope<_InternalRun>
var _rope: Rope<_InternalRun>
init() {
self._rope = Rope()
}
init(_ rope: Storage) {
self._rope = rope
}
init(_ runs: some Sequence<_InternalRun>) {
self._rope = Rope(runs)
}
}
extension AttributedString._InternalRuns {
...
}
- Rope란?
- 데이터 조회를 빠르게하기 위해서 정의된 ordered - tree 자료구조
- String에 관한 데이터를 처리할 때 사용하며, 특정 데이터를 조회할 때 O(logN)으로 빠르게 탐색할 수 있도록 사용한 것
/// An ordered data structure of `Element` values that organizes itself into a tree.
/// The rope is augmented by the commutative group specified by `Element.Summary`, enabling
/// quick lookup operations.
public struct Rope<Element: RopeElement> {
@usableFromInline
internal var _root: _Node?
@usableFromInline
internal var _version: _RopeVersion
@inlinable
public init() {
self._root = nil
self._version = _RopeVersion()
}
@inlinable
internal init(root: _Node?) {
self._root = root
self._version = _RopeVersion()
}
@inlinable
internal var root: _Node {
@inline(__always) get { _root.unsafelyUnwrapped }
@inline(__always) _modify { yield &_root! }
}
@inlinable
public init(_ value: Element) {
self._root = .createLeaf(_Item(value))
self._version = _RopeVersion()
}
}
- 즉, Guts안에서 가지고 있는 인스턴스인 runs는 데이터들을 tree 자료구조로 정의하고 있으며 어떤 데이터를 가지고 있는지 O(logN)으로 빠르게 조회 가능하고, 이 run을 가지고 속성값들을 가져와서 처리하는 형태
- run 단어의 의미: 연속된 데이터 블록을 의미
// Guts에서 runs를 가지고 있는 형태
var runs: _InternalRuns
- run을 가져와서 처리
- run 쓰임1: run에서 attributes, range값을 가져와서 hash function에 사용
- run 쓰임2: run에서 가지고 있는 utf8OffsetRange를 가져와서 uniformValue로 반환할 때 사용
- run 쓰임3: run이 가지고 있는 Attributes들을 가져와서 AttributedStorage로 반환
// run 쓰임1
internal func characterwiseHash(
in range: Range<BigString.Index>,
into hasher: inout Hasher
) {
let runs = AttributedString.Runs(self, in: range) // <- run 사용
hasher.combine(runs.count) // Hash discriminator
for run in runs {
hasher.combine(run._attributes)
hasher.combine(string[run._range])
}
}
// run 쓰임2
func getUniformValue<K: AttributedStringKey>(
in range: Range<BigString.Index>, key: K.Type
) -> _AttributeValue? {
var result: _AttributeValue? = nil
for run in self.runs(in: range._utf8OffsetRange) {
guard let value = run.attributes[K.name] else {
return nil
}
if let previous = result, value != previous {
return nil
}
result = value
}
return result
}
// run 쓰임3
func getUniformValues(in range: Range<BigString.Index>) -> _AttributeStorage {
var attributes = _AttributeStorage()
var first = true
for run in self.runs(in: range._utf8OffsetRange) {
guard !first else {
attributes = run.attributes
first = false
continue
}
attributes = attributes.filterWithoutInvalidatingDependents {
guard let value = run.attributes[$0.key] else { return false }
return value == $0.value
}
if attributes.isEmpty {
break
}
}
return attributes
}
정리
- 애플이 구현한 foundation의 attributedString 구조는 안에서 Guts라는 인스턴스를 따로 두어서 기능을 담당하는 인스턴스를 별도로 분리 (캡슐화)
- 정의, 초기화, 주요 기능 단위 별로 extension으로 나누어서 구현
- Guts안에서는 연속된 데이터 블록을 의미하는 Run이라는 것을 사용하고 있는데, ordered 트리 구조이며 이것을 통해 문자열의 Attributes, range 값등을 저장하여 필요할때 가져와서 처리하는 형태
- 보완하면 좋을점: Guts를 인스턴스 그대로 사용하고 있는데, 만약 Guts를 프로토콜로 설계했으면 testable한 코드를 가질 수 있기 때문에 더욱 AttributedString을 테스트하기 좋고 유지보수하기 쉬울 것
* 참고
'오픈소스 까보기' 카테고리의 다른 글
Comments