{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-370", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/370", "wordCount": 3189, "segments": [{"start": 7.0, "text": "Hello, and welcome to \"Elevate your app's text experience with TextKit.\""}, {"start": 12.0, "text": "I'm Tarun Uday, an engineer on the TextKit team."}, {"start": 17.0, "text": "TextKit is Apple's next generation text engine,"}, {"start": 20.0, "text": "and the foundation of text layout"}, {"start": 22.0, "text": "and rendering across all of Apple's platforms."}, {"start": 26.0, "text": "Text controls in SwiftUI, UIKit and AppKit"}, {"start": 30.0, "text": "all use TextKit to lay out and render their text content."}, {"start": 35.0, "text": "In this video, I want to talk about something"}, {"start": 38.0, "text": "we've been hearing from developers for a while,"}, {"start": 40.0, "text": "a tension between convenience and control,"}, {"start": 44.0, "text": "and the new APIs we've built to resolve it."}, {"start": 47.0, "text": "If you're building a text editing experience on Apple platforms,"}, {"start": 50.0, "text": "you have two paths."}, {"start": 52.0, "text": "The first path is to use the framework text view."}, {"start": 56.0, "text": "That's NSTextView in AppKit, UITextView in UIKit and TextEditor in SwiftUI."}, {"start": 64.0, "text": "With these, you get an incredible amount for free."}, {"start": 67.0, "text": "Text input, selection, accessibility, undo and redo,"}, {"start": 72.0, "text": "dictation, inline predictions, and more."}, {"start": 76.0, "text": "These text views use TextKit internally,"}, {"start": 79.0, "text": "but that internal implementation is mostly hidden."}, {"start": 83.0, "text": "You have limited ability to customize how the text is drawn"}, {"start": 86.0, "text": "or how the viewport manages its visual elements."}, {"start": 90.0, "text": "The second path is to use TextKit as the text engine,"}, {"start": 94.0, "text": "and render the text in a view or a layer directly."}, {"start": 98.0, "text": "We call this a custom text view to differentiate them"}, {"start": 102.0, "text": "from the prepackaged framework text views."}, {"start": 105.0, "text": "You set up an NSTextLayoutManager,"}, {"start": 108.0, "text": "implement viewport layout on your own view or layer,"}, {"start": 112.0, "text": "and handle all the rendering yourself."}, {"start": 115.0, "text": "When you build custom text views, you get total control over the storage, layout,"}, {"start": 121.0, "text": "and the viewport layout process,"}, {"start": 123.0, "text": "but you give up everything that the framework text views provide."}, {"start": 126.0, "text": "And building a production-quality text editing experience from scratch"}, {"start": 130.0, "text": "is a lot of work."}, {"start": 132.0, "text": "For some scenarios though,"}, {"start": 134.0, "text": "choosing between the convenience of a framework text view,"}, {"start": 136.0, "text": "and the control of a custom text view has been difficult."}, {"start": 141.0, "text": "Today we'll look at how we can get the best of both worlds."}, {"start": 145.0, "text": "For an in-depth introduction to the TextKit architecture"}, {"start": 148.0, "text": "and custom text views, watch, \"Meet TextKit 2,\" from WWDC21."}, {"start": 154.0, "text": "And for details on how framework text views adopted TextKit,"}, {"start": 158.0, "text": "watch, \"What's new in TextKit and text views,\" from WWDC22."}, {"start": 164.0, "text": "While this talk is self-contained,"}, {"start": 166.0, "text": "these two sessions will give you a deeper foundation for everything we cover today."}, {"start": 172.0, "text": "I'm going to start by giving you a recap on TextKit's architecture."}, {"start": 177.0, "text": "Later on, I'll talk about some new API we've introduced in TextKit."}, {"start": 182.0, "text": "At the very end, I'll show you new ways of extending text views, using some examples."}, {"start": 189.0, "text": "Understanding the TextKit architecture is pivotal"}, {"start": 192.0, "text": "to making a great custom text experience."}, {"start": 195.0, "text": "Let's start there."}, {"start": 197.0, "text": "TextKit uses a four-layer architecture for text rendering."}, {"start": 202.0, "text": "At the base is the text storage layer."}, {"start": 205.0, "text": "This encapsulates all the text data to be rendered."}, {"start": 209.0, "text": "The layout layer sits on top of the text storage."}, {"start": 212.0, "text": "It's responsible for breaking the text into chunks for rendering."}, {"start": 217.0, "text": "Next, is the viewport layer."}, {"start": 219.0, "text": "This keeps track of which of the chunks from the layout are visible."}, {"start": 224.0, "text": "At the top is the view layer."}, {"start": 226.0, "text": "This is where the text appears in your app."}, {"start": 230.0, "text": "The storage, layout, and viewport layers"}, {"start": 233.0, "text": "are shared across all of Apple's UI Frameworks."}, {"start": 237.0, "text": "You can use these shared layers to render text on any view"}, {"start": 241.0, "text": "or view-like drawable visual element provided by a UI Framework."}, {"start": 246.0, "text": "Next, I'll cover how each layer works."}, {"start": 249.0, "text": "By understanding the pieces that make up each layer,"}, {"start": 252.0, "text": "you can customize TextKit to create unique experiences in your apps."}, {"start": 258.0, "text": "To do that, I'll use the example of rendering a long NSAttributedString"}, {"start": 263.0, "text": "in a custom text view."}, {"start": 265.0, "text": "Text content storage is responsible"}, {"start": 268.0, "text": "for breaking this attributed string into paragraphs."}, {"start": 271.0, "text": "For this example,"}, {"start": 272.0, "text": "the text content storage creates NSTextParagraph objects"}, {"start": 276.0, "text": "for each paragraph of the underlying attributed string."}, {"start": 280.0, "text": "NSTextContentStorage and NSTextParagraph"}, {"start": 283.0, "text": "are concrete types that work with NSAttributedStrings."}, {"start": 288.0, "text": "If you have a different backing storage type,"}, {"start": 290.0, "text": "you can write your own subclasses of the corresponding abstract classes:"}, {"start": 295.0, "text": "NSTextContentManager and NSTextElement."}, {"start": 300.0, "text": "Ok, that was the text storage layer."}, {"start": 303.0, "text": "Continuing the example, I'll look at layout next."}, {"start": 307.0, "text": "After the text content storage breaks the attributed string into paragraphs,"}, {"start": 311.0, "text": "the NSTextLayoutManager does the work to prepare the paragraphs for rendering."}, {"start": 316.0, "text": "The text layout manager performantly measures the metrics of the glyphs"}, {"start": 321.0, "text": "that make up the represented text,"}, {"start": 323.0, "text": "and dynamically creates an NSTextLayoutFragment"}, {"start": 326.0, "text": "that stores the calculated layout information of the paragraph."}, {"start": 331.0, "text": "These objects are immutable."}, {"start": 333.0, "text": "Which means, if a paragraph is edited,"}, {"start": 336.0, "text": "the NSTextParagraph and NSTextLayoutFragment are recreated."}, {"start": 342.0, "text": "For example, if I replace the word sandwich with slider"}, {"start": 348.0, "text": "a new NSTextParagraph is created for that paragraph,"}, {"start": 352.0, "text": "and a corresponding new NSTextLayoutFragment"}, {"start": 355.0, "text": "is created with new layout information."}, {"start": 358.0, "text": "Next I'll show you how the top two layers,"}, {"start": 361.0, "text": "the viewport and the view,"}, {"start": 363.0, "text": "work together to efficiently render huge amounts of text."}, {"start": 368.0, "text": "The text view is a dynamically sized view"}, {"start": 371.0, "text": "that can grow as text is laid out and drawn into it,"}, {"start": 375.0, "text": "and shrink as text is removed."}, {"start": 377.0, "text": "The viewport is the part of the text view that is visible to the user."}, {"start": 381.0, "text": "TextKit organizes all of its work around the viewport"}, {"start": 385.0, "text": "only rendering text that the user can see."}, {"start": 388.0, "text": "This means that one of your core tasks when working with TextKit"}, {"start": 392.0, "text": "is enhancing the user's interaction"}, {"start": 394.0, "text": "based on the layout information that the viewport provides."}, {"start": 398.0, "text": "To facilitate the rendering of layout fragments onto the text view,"}, {"start": 402.0, "text": "TextKit provides a dedicated class: NSTextViewportLayoutController."}, {"start": 408.0, "text": "The NSTextViewportLayoutController,"}, {"start": 413.0, "text": "I'll just call it the viewport controller."}, {"start": 416.0, "text": "The viewport controller coordinates with the text layout manager,"}, {"start": 420.0, "text": "and the text view to efficiently layout and render paragraphs of text."}, {"start": 425.0, "text": "Let me show you how."}, {"start": 428.0, "text": "The text view knows the scroll position"}, {"start": 430.0, "text": "and size of the viewport with respect to the whole document,"}, {"start": 434.0, "text": "and provides this to the viewport controller."}, {"start": 437.0, "text": "The viewport controller then requests the text layout manager"}, {"start": 440.0, "text": "to provide all of the layout fragments that intersect with the viewport,"}, {"start": 445.0, "text": "and sends them to the text view for rendering."}, {"start": 448.0, "text": "This coordination, facilitated by the viewport controller,"}, {"start": 451.0, "text": "repeats on any change of viewport state."}, {"start": 454.0, "text": "That is, any scroll, edit, or selection event,"}, {"start": 457.0, "text": "and is called the viewport layout process."}, {"start": 461.0, "text": "The viewport layout process is central to TextKit's performant layout and rendering."}, {"start": 467.0, "text": "And that's it!"}, {"start": 469.0, "text": "To build your own custom text view, instantiate an NSTextContentStorage,"}, {"start": 474.0, "text": "NSTextLayoutManager,"}, {"start": 476.0, "text": "and render the text using an NSTextViewportLayoutController"}, {"start": 481.0, "text": "into it's delegate,"}, {"start": 482.0, "text": "a view provided by the UI Framework."}, {"start": 485.0, "text": "Even though I refer to the text view as a view,"}, {"start": 488.0, "text": "this could be any drawable visual element"}, {"start": 491.0, "text": "that the UI Framework provides."}, {"start": 493.0, "text": "For example,"}, {"start": 495.0, "text": "in UIKit, you can choose a UIView"}, {"start": 498.0, "text": "or a CALayer to render text into a custom text view."}, {"start": 502.0, "text": "UI Frameworks also package its own type of text view"}, {"start": 506.0, "text": "for your convenience with these TextKit layers."}, {"start": 509.0, "text": "In UIKit, you can use UITextView to implement"}, {"start": 513.0, "text": "an off-the-shelf text editing experience."}, {"start": 516.0, "text": "AppKit and SwiftUI have similar views."}, {"start": 520.0, "text": "Occasionally, a framework text view might not meet the needs of your app."}, {"start": 525.0, "text": "Perhaps you're building an app"}, {"start": 527.0, "text": "that has multiple presentations"}, {"start": 529.0, "text": "of the same text."}, {"start": 531.0, "text": "Connect multiple text layout managers"}, {"start": 533.0, "text": "to the same text content storage,"}, {"start": 535.0, "text": "and edits in one view"}, {"start": 537.0, "text": "will propagate through the shared content storage to the other."}, {"start": 541.0, "text": "This means you can present the same document"}, {"start": 544.0, "text": "in two different views"}, {"start": 545.0, "text": "and they stay in sync automatically."}, {"start": 548.0, "text": "With the flexibility that TextKit provides,"}, {"start": 550.0, "text": "you can build custom text views"}, {"start": 552.0, "text": "with a layering configuration that's right for your scenario."}, {"start": 557.0, "text": "Now, let's take a look at some of the new APIs"}, {"start": 559.0, "text": "that we are introducing in TextKit."}, {"start": 562.0, "text": "In the previous section, we talked about rendering text from a layout fragment"}, {"start": 566.0, "text": "onto the viewport."}, {"start": 568.0, "text": "That's the viewport layout process."}, {"start": 570.0, "text": "And before the 2027 releases,"}, {"start": 573.0, "text": "we did not have a way of referring to the destination views"}, {"start": 576.0, "text": "where the text is rendered across TextKit."}, {"start": 579.0, "text": "This meant that while TextKit helped you keep track of layout fragments,"}, {"start": 583.0, "text": "it did not help you keep track of the views that they were drawn in."}, {"start": 587.0, "text": "First, meet NSTextViewportRenderingSurface."}, {"start": 591.0, "text": "This is a new protocol that represents a visual element inside the viewport"}, {"start": 595.0, "text": "that you can draw into."}, {"start": 598.0, "text": "The view that actually renders a layout fragment's text"}, {"start": 602.0, "text": "and provides a common abstraction to work with."}, {"start": 605.0, "text": "You can conform your UIView, NSView or CALayer to this protocol,"}, {"start": 610.0, "text": "and use it in the viewport controller's delegate methods"}, {"start": 613.0, "text": "to keep track of what views are visible in the viewport."}, {"start": 618.0, "text": "The rendering surface comes with a companion key protocol"}, {"start": 622.0, "text": "NSTextViewportRenderingSurfaceKey."}, {"start": 625.0, "text": "A rendering surface key is any class that can uniquely identify a rendering surface"}, {"start": 631.0, "text": "across viewport layout process cycles,"}, {"start": 634.0, "text": "like NSTextLayoutFragment."}, {"start": 636.0, "text": "This means you can use NSTextLayoutFragment"}, {"start": 640.0, "text": "as a key to cache rendering surfaces in map tables or dictionaries."}, {"start": 646.0, "text": "The viewport layout process extensively uses the rendering surface key"}, {"start": 650.0, "text": "to rendering surface mapping internally."}, {"start": 654.0, "text": "You can assign a rendering surface to a key"}, {"start": 657.0, "text": "during the viewport layout process"}, {"start": 658.0, "text": "by using the renderingSurfaceFor delegate method."}, {"start": 663.0, "text": "These are cleared at the beginning of the viewport layout process."}, {"start": 667.0, "text": "You can query the rendering surface for a particular key"}, {"start": 670.0, "text": "within the didLayout process"}, {"start": 672.0, "text": "using the viewport controller's renderingSurfaceFor method."}, {"start": 677.0, "text": "These new APIs empower you to use"}, {"start": 680.0, "text": "and customize your own rendering surfaces"}, {"start": 683.0, "text": "when building custom text views using TextKit."}, {"start": 686.0, "text": "Now that we've seen how TextKit works in our 2027 releases,"}, {"start": 690.0, "text": "let's look at how the text views"}, {"start": 692.0, "text": "that power apple's default text experiences work."}, {"start": 695.0, "text": "UIKit's UITextView and AppKit's NSTextView"}, {"start": 699.0, "text": "power thousands of long-form text experiences on Apple's platforms,"}, {"start": 703.0, "text": "including Messages, TextEdit, Notes, and Journal."}, {"start": 707.0, "text": "If you have a SwiftUI app,"}, {"start": 709.0, "text": "the most convenient way to implement"}, {"start": 711.0, "text": "a long-form text experience is using TextEditor."}, {"start": 715.0, "text": "But you could also include a UITextView"}, {"start": 718.0, "text": "or NSTextView in your app by using a ViewRepresentable."}, {"start": 723.0, "text": "Let me show you."}, {"start": 726.0, "text": "To start, I'll create a view called MyTextView."}, {"start": 731.0, "text": "I will populate MyTextView's body"}, {"start": 734.0, "text": "with a ViewRepresentable, that I'll call, TextViewRepresentable."}, {"start": 739.0, "text": "TextViewRepresentable will conditionally be"}, {"start": 742.0, "text": "an NSViewRepresentable on macOS"}, {"start": 745.0, "text": "and a UIViewRepresentable otherwise."}, {"start": 748.0, "text": "Inside the NSViewRepresentable,"}, {"start": 750.0, "text": "you simply call the initializer for your NSTextView,"}, {"start": 754.0, "text": "or your NSTextView subclass in the makeNSView method."}, {"start": 759.0, "text": "And do the same for UITextView inside the UIViewRepresentable."}, {"start": 764.0, "text": "You can see specific examples of this in the accompanying sample app."}, {"start": 769.0, "text": "In order to show you how you can extend UITextView using its Textkit hooks,"}, {"start": 774.0, "text": "I'll be creating a few different example apps."}, {"start": 778.0, "text": "In my first example, I want to build a code editor for my iPad"}, {"start": 782.0, "text": "so that I can write some quick code while I'm away from my Mac."}, {"start": 786.0, "text": "I'll start with a UITextView subclass,"}, {"start": 789.0, "text": "initialize it and set it's font to the monospaced system font."}, {"start": 794.0, "text": "Ok, that's a start!"}, {"start": 796.0, "text": "But, this isn't really a great code editor experience if I can't see line numbers."}, {"start": 802.0, "text": "Let's start building that."}, {"start": 804.0, "text": "First, let's create a view that can hold a TextView and a lineNumberView."}, {"start": 810.0, "text": "We'll call this ContainerView."}, {"start": 813.0, "text": "The ContainerView will hold on to our UITextView subclass,"}, {"start": 816.0, "text": "and a UIView to display the line numbers."}, {"start": 820.0, "text": "I have a basic setup,"}, {"start": 822.0, "text": "so what I want now is to recompute"}, {"start": 824.0, "text": "and show the NSTextParagraph index for the layout fragments in the viewport,"}, {"start": 830.0, "text": "whenever there is a change in the viewport."}, {"start": 832.0, "text": "And in order to do that,"}, {"start": 834.0, "text": "I need the text view to be notified whenever its viewport controller"}, {"start": 838.0, "text": "has gone through a viewport layout process."}, {"start": 841.0, "text": "And that's possible now!"}, {"start": 843.0, "text": "Starting with our 2027 releases,"}, {"start": 846.0, "text": "UITextView and NSTextView now conform to NSTextViewportLayoutControllerDelegate."}, {"start": 853.0, "text": "This means you can subclass UITextView or NSTextView"}, {"start": 857.0, "text": "and override the delegate methods to add your own behavior."}, {"start": 862.0, "text": "I'll do that next!"}, {"start": 864.0, "text": "In my TextView subclass, I'll override the delegate methods."}, {"start": 868.0, "text": "First, I'll override the WillLayout method to do some setup work."}, {"start": 873.0, "text": "I'll show the details in a bit."}, {"start": 875.0, "text": "I'll override the configureRenderingSurface method"}, {"start": 879.0, "text": "to capture the bounds of the paragraphs to be rendered."}, {"start": 882.0, "text": "Finally, I'll override the DidLayout method"}, {"start": 886.0, "text": "to share the accumulated info back to the ContainerView"}, {"start": 889.0, "text": "so it can render the line numbers."}, {"start": 892.0, "text": "Before showing these methods, I'll add some state to my subclass."}, {"start": 896.0, "text": "I'll start with an array to accumulate the bounds of each paragraph"}, {"start": 900.0, "text": "the text view lays out,"}, {"start": 902.0, "text": "an integer to track the starting line number,"}, {"start": 906.0, "text": "and a closure"}, {"start": 907.0, "text": "that I'll use to send the accumulated info up to my ContainerView,"}, {"start": 911.0, "text": "so it can render the line numbers."}, {"start": 913.0, "text": "The viewport controller delegate methods"}, {"start": 915.0, "text": "help the text view know when scrolling"}, {"start": 918.0, "text": "or editing has happened,"}, {"start": 920.0, "text": "so that we can redraw the line numbers."}, {"start": 923.0, "text": "I'll implement the methods next, starting with WillLayout."}, {"start": 927.0, "text": "I'll start by calling super."}, {"start": 929.0, "text": "Remember to do that in all of these delegate methods."}, {"start": 933.0, "text": "I'll clear out the lines variable so that we can get ready"}, {"start": 936.0, "text": "to store the bounds of the layout fragments."}, {"start": 938.0, "text": "We also need the starting LineNumber,"}, {"start": 941.0, "text": "that's basically a count of all the paragraphs"}, {"start": 943.0, "text": "before the viewport starts."}, {"start": 947.0, "text": "Let me do that in its own function"}, {"start": 949.0, "text": "and call it from within the WillLayout method."}, {"start": 955.0, "text": "I'll start with some simple nil checks and variable naming."}, {"start": 959.0, "text": "I'll use the enumerateTextElements from text location method"}, {"start": 962.0, "text": "to enumerate the elements"}, {"start": 964.0, "text": "and increment my count until we reach the viewportRange."}, {"start": 968.0, "text": "And that's it!"}, {"start": 969.0, "text": "The sample code improves this with caching,"}, {"start": 971.0, "text": "so you don't pay this cost on every layout pass."}, {"start": 975.0, "text": "Let's go back to our delegate methods."}, {"start": 977.0, "text": "and see how we can get the bounds for each paragraph."}, {"start": 982.0, "text": "We'll do that using the next delegate method,"}, {"start": 984.0, "text": "configureRenderingSurfaceFor: textLayoutFragment."}, {"start": 988.0, "text": "I'll start again by calling super,"}, {"start": 990.0, "text": "so that I get the default text view behavior"}, {"start": 994.0, "text": "and then append the lines array"}, {"start": 996.0, "text": "with the layout fragment's layoutFragmentFrame variable."}, {"start": 1001.0, "text": "That's it for the configureRenderingSurfaceFor"}, {"start": 1003.0, "text": "textLayoutFragment method."}, {"start": 1005.0, "text": "This method will be triggered for every paragraph in the viewport."}, {"start": 1010.0, "text": "Let's look at the DidLayout method."}, {"start": 1012.0, "text": "At this point,"}, {"start": 1013.0, "text": "I have the bounds information of every paragraph in the viewport,"}, {"start": 1017.0, "text": "and I want to pass it to the ContainerView."}, {"start": 1020.0, "text": "Before firing the closure,"}, {"start": 1022.0, "text": "I need to convert the fragment frames from text container coordinates"}, {"start": 1026.0, "text": "to viewport coordinates."}, {"start": 1028.0, "text": "I do that by subtracting the viewport origin."}, {"start": 1031.0, "text": "Then, I pass the starting line number"}, {"start": 1033.0, "text": "and the adjusted frames to the ContainerView."}, {"start": 1036.0, "text": "Back in the ContainerView, I set the closure."}, {"start": 1039.0, "text": "For each frame, I calculate the actual line number"}, {"start": 1042.0, "text": "by adding the index to the starting line number,"}, {"start": 1045.0, "text": "and draw it at the right position in the LineNumber view."}, {"start": 1049.0, "text": "And that's it."}, {"start": 1050.0, "text": "We set up the variables,"}, {"start": 1052.0, "text": "collect the bounds for each paragraph in the text view,"}, {"start": 1055.0, "text": "and pass it to the ContainerView to display it."}, {"start": 1058.0, "text": "Let's run the app and see how we did."}, {"start": 1061.0, "text": "Perfect,"}, {"start": 1062.0, "text": "we added line numbers to a UITextView with just a few lines of code."}, {"start": 1067.0, "text": "I have more work to do but this is a great first step to building a code editor."}, {"start": 1072.0, "text": "Using the framework text view's viewport layout process"}, {"start": 1075.0, "text": "is a powerful way to access"}, {"start": 1078.0, "text": "and display individual paragraph information."}, {"start": 1081.0, "text": "Let me show you one more example."}, {"start": 1084.0, "text": "This time involving modifying layout for multiple paragraphs."}, {"start": 1088.0, "text": "Here I've set up a UITextView to show some of my favorite recipes."}, {"start": 1093.0, "text": "But I really want to see them one recipe at a time."}, {"start": 1097.0, "text": "That is, I want to collapse each multi-paragraph recipe"}, {"start": 1100.0, "text": "into just its heading."}, {"start": 1103.0, "text": "To do this, I'll start with the same three viewport delegate methods"}, {"start": 1106.0, "text": "from the last example."}, {"start": 1109.0, "text": "But on top of this, if a paragraph is collapsed,"}, {"start": 1112.0, "text": "I want to avoid doing layout on it."}, {"start": 1115.0, "text": "To do that, I'll conform the TextView to NSTextContentStorageDelegate."}, {"start": 1121.0, "text": "Through this conformance,"}, {"start": 1123.0, "text": "I'll get access to textContentManager: shouldEnumerate,"}, {"start": 1127.0, "text": "which will help me mark textElements as collapsed or not."}, {"start": 1131.0, "text": "Remember, NSTextContentManager"}, {"start": 1133.0, "text": "is just the abstract version of NSTextContentStorage,"}, {"start": 1137.0, "text": "and NSTextElement is the abstract version of NSTextParagraph."}, {"start": 1143.0, "text": "We want some state to hold on to which sections are collapsed."}, {"start": 1147.0, "text": "We'll use a set of ints to keep track of the paragraph offset"}, {"start": 1151.0, "text": "to uniquely identify each paragraph."}, {"start": 1155.0, "text": "Additionally, we add a method to handle when the user taps on a toggle button."}, {"start": 1162.0, "text": "These are all the pieces you need!"}, {"start": 1164.0, "text": "Skip layout using the text content storage delegate method,"}, {"start": 1169.0, "text": "process every paragraph that does layout in the viewport"}, {"start": 1172.0, "text": "using the viewport controller delegate methods,"}, {"start": 1175.0, "text": "and handle the user interaction"}, {"start": 1177.0, "text": "for when the user taps on a section's disclosure button."}, {"start": 1181.0, "text": "You can take a look at the sample code for the details."}, {"start": 1184.0, "text": "Let's look at what that accomplished."}, {"start": 1187.0, "text": "I can collapse any recipe into just the heading"}, {"start": 1190.0, "text": "by tapping on the triangle next to it,"}, {"start": 1192.0, "text": "and I did it right in UITextView."}, {"start": 1195.0, "text": "Ok, let's take a step back."}, {"start": 1198.0, "text": "So far, our examples have been about text,"}, {"start": 1201.0, "text": "paragraphs, line numbers, and section headings."}, {"start": 1204.0, "text": "But text views display much more than just text."}, {"start": 1208.0, "text": "Think about Messages with inline photos and stickers."}, {"start": 1212.0, "text": "Or Notes, with drawings and document scans."}, {"start": 1217.0, "text": "All of that non-text content lives inside the text view,"}, {"start": 1221.0, "text": "managed by TextKit."}, {"start": 1223.0, "text": "These are called text attachments."}, {"start": 1225.0, "text": "Text attachments follow the same architecture as regular text."}, {"start": 1229.0, "text": "Let me focus on one paragraph,"}, {"start": 1232.0, "text": "and represent an attachment using the paperclip symbol"}, {"start": 1235.0, "text": "to make things simple."}, {"start": 1237.0, "text": "A text attachment is stored in the text storage"}, {"start": 1239.0, "text": "just like any other character,"}, {"start": 1242.0, "text": "and is done using a NSTextAttachment object."}, {"start": 1246.0, "text": "When the layout manager encounters a text attachment,"}, {"start": 1250.0, "text": "it asks for an NSTextAttachmentViewProvider,"}, {"start": 1253.0, "text": "that's the corresponding object in the layout layer."}, {"start": 1257.0, "text": "The view provider provides the necessary information"}, {"start": 1259.0, "text": "to render the attachment onto the text view."}, {"start": 1263.0, "text": "This brings us to a challenge."}, {"start": 1265.0, "text": "Since these objects are immutable,"}, {"start": 1267.0, "text": "if we were to edit the text in the paragraph"}, {"start": 1270.0, "text": "all instances would have to be discarded and recreated."}, {"start": 1274.0, "text": "Let me show you a concrete example."}, {"start": 1277.0, "text": "Say I'm building a messaging app with inline animations."}, {"start": 1281.0, "text": "Watch carefully as I edit."}, {"start": 1283.0, "text": "The animation restarts on every edit for the corresponding paragraph."}, {"start": 1288.0, "text": "My view provider is recreated on every edit"}, {"start": 1291.0, "text": "and that restarts the animation."}, {"start": 1294.0, "text": "To solve this, we've added a new API on UITextView."}, {"start": 1299.0, "text": "Once I initialize my text view,"}, {"start": 1301.0, "text": "I use the register forTextAttachmentViewProviderType method"}, {"start": 1305.0, "text": "to register a view provider reuse policy"}, {"start": 1309.0, "text": "for a particular subclass of NSTextAttachmentViewProvider."}, {"start": 1313.0, "text": "For the first argument, I add the onEditingInlineParagraphs"}, {"start": 1318.0, "text": "reuse policy."}, {"start": 1319.0, "text": "This preserves the view provider across paragraph edits,"}, {"start": 1322.0, "text": "so keystrokes don't tear down my view provider."}, {"start": 1326.0, "text": "For the second argument, I provide the view provider subclass type,"}, {"start": 1330.0, "text": "and the text view will take care of all objects of that particular class."}, {"start": 1334.0, "text": "In the sample code, you can see a second type of reuse policy:"}, {"start": 1338.0, "text": "onScrollingOutOfViewport."}, {"start": 1341.0, "text": "This caches the attachment's rendering surface"}, {"start": 1344.0, "text": "when it scrolls off screen"}, {"start": 1345.0, "text": "and restores it when it comes back."}, {"start": 1348.0, "text": "You can combine both reuse policies depending on your scenario."}, {"start": 1353.0, "text": "Now, on editing, UITextView reuses the view provider,"}, {"start": 1358.0, "text": "maintaining state, and avoiding any animation glitches."}, {"start": 1363.0, "text": "So there you go!"}, {"start": 1364.0, "text": "Three examples of using TextKit in UITextView,"}, {"start": 1368.0, "text": "line numbers for a text editor,"}, {"start": 1371.0, "text": "collapsible sections in a recipe app,"}, {"start": 1373.0, "text": "and inline text attachment reuse in a simple text view."}, {"start": 1377.0, "text": "You can download the sample app to look at the details."}, {"start": 1380.0, "text": "To recap, to create a convenient but powerful rich text editor experience,"}, {"start": 1386.0, "text": "kickstart your app with UITextView on UIKit and NSTextVIew on AppKit."}, {"start": 1392.0, "text": "If you have a SwiftUI app,"}, {"start": 1394.0, "text": "use a ViewRepresentable to include these text views in your app."}, {"start": 1399.0, "text": "For those of you who want much more control over your text rendering,"}, {"start": 1402.0, "text": "create custom text views using TextKit"}, {"start": 1405.0, "text": "and use the new Rendering Surface APIs."}, {"start": 1409.0, "text": "Check out the sample code to see collapsible sections,"}, {"start": 1412.0, "text": "line numbers, and inline attachment reuse in action."}, {"start": 1416.0, "text": "Thanks for watching!"}]}