Hi Swift community, I'd like to propose another addition to AttributedString
building off of some of the previously pitched APIs. I'd like to pitch adding new APIs that will allow developers to easily track important indices across mutations to an AttributedString
. Please let me know if you have any thoughts, suggestions, or concerns and I'd love to discuss adding in these new capabilities to support advanced AttributedString
uses!
AttributedString Tracking Indices
- Proposal: SF-NNNN
- Authors: Jeremy Schonfeld
- Review Manager: TBD
- Status: Pitch
Introduction
Similar to many other Collection
types in Swift, AttributedString
uses an opaque index type (AttributedString.Index
) to represent locations within the storage of the text. AttributedString
uses an opaque type instead of a trivial type like an integer in order to store a more complex representation of the location within the text. Specifically, AttributedString
uses its index to store not only a raw UTF-8 offset into the text storage, but also a "path" through the rope structure that backs the AttributedString
. This allows AttributedString
to quickly find a location within the text storage given an index without performing linear scans of the underlying text on each access or mutation. However, because this opaque index type stores more complex information, it must be handled very carefully and kept in-sync with the AttributedString
itself. Unlike an integer offset (which can often still be a valid index into a collection like Array
after a mutation even if it points to a different semantic location), AttributedString.Index
currently makes no guarantees about its validity after any mutation to the AttributedString
and in many cases will (intentionally) crash when used improperly. As AttributedString
is adopted in more advanced use cases throughout our platforms, we'd like to improve upon this developer experience for some common use cases of stored AttributedString.Index
s.
Motivation
In many cases, developers may wish to use these index types to store "pointers" to locations in the AttributedString
separately from the text itself. For example, a text editor that uses an AttributedString
as its underlying storage would likely want to store a RangeSet<AttributedString.Index>
at the view or view model layer to represent a user's selection in the text while still allowing mutations of the text. Alternatively, complex, in-place, mutating operations that process an AttributedString
in chunks may wish to temporarily store an AttributedString.Index
representing the current processing location while it performs mutations on the text. In these scenarios, it is currently challenging (or in some cases not possible) to keep an opaque AttributedString.Index
in-sync with a separate AttributedString
while mutations are occuring since every mutation invalidates every previously produced index value. With more complex AttributedString
-based APIs, it's important that we provide a mechanism for developers to keep these indices not only valid to prevent unexpected applications crashes but also correctly positioned to ensure they achieve expected end user behavior.
Proposed solution
To accomplish this goal, we will provide a few new APIs to make AttributedString
index management and synchronization easy to use. First, we will propose a new API that allows AttributedString
to update an index, range, or list of ranges while a mutation is being performed to ensure the indices remain valid and correct post-mutation. Developers will use a new proposed transform(updating:_:)
API to do so like the following:
var attrStr = AttributedString("The quick brown fox jumped over the lazy dog")
guard let rangeOfJumped = attrStr.range(of: "jumped") else { ... }
let updatedRangeOfJumped = attrStr.transform(updating: rangeOfJumped) {
$0.insert("Wow!", at: $0.startIndex)
}
if let updatedRangeOfJumped {
print(attrStr[updatedRangeOfJumped]) // "jumped"
}
Note that in the above sample code, the returned Range
references the range of "jumped" which is valid for use with the mutated attrStr
(it will not crash) and it locates the same text - it does not represent the range of "fox ju" (a range offset by the 4 characters that were inserted at the beginning of the string).
Additionally, we will provide a set of APIs and guarantees to reduce the frequency of crashes caused by invalid indices and allow for dynamically determining whether an index has become out-of-sync with a given AttributedString
. For example:
var attrStr = AttributedString("The quick brown fox jumped over the lazy dog")
guard var rangeOfJumped = attrStr.range(of: "jumped") else { ... }
// ... some additional processing ...
guard rangeOfJumped.isValid(within: attrStr) else {
// A mutation has ocurred without correctly updating `rangeOfJumped` - we should not use this range as it may crash or represent the wrong location
}
// `rangeOfJumped` has been correctly kept in-sync with `attrStr` as it has changed, we can use it freely
For the detailed design of these APIs and a wide variety of considerations and rejected alternatives, please check out the full proposal on the swift-foundation repo PR.