{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-321", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/321", "wordCount": 3326, "segments": [{"start": 7.0, "text": "Hi, my name is Rens, and I'm a UI Frameworks Engineer."}, {"start": 12.0, "text": "Lazy stacks are an essential component"}, {"start": 14.0, "text": "for any SwiftUI app showing long and custom scrolling content."}, {"start": 18.0, "text": "And they have long been a part of SwiftUI."}, {"start": 20.0, "text": "Like many other SwiftUI components,"}, {"start": 23.0, "text": "the power of lazy stacks comes from their simplicity."}, {"start": 26.0, "text": "Different SwiftUI components can be mixed"}, {"start": 28.0, "text": "with many other SwiftUI components to build complex apps."}, {"start": 32.0, "text": "For example, from the 2027 releases"}, {"start": 35.0, "text": "you can use reorderable to drag and reorder views."}, {"start": 38.0, "text": "You can learn more about that"}, {"start": 40.0, "text": "in \"Code-along: Build powerful drag and drop in SwiftUI\"."}, {"start": 44.0, "text": "And SwiftUI allows swipe actions to be added on views outside list."}, {"start": 49.0, "text": "Of course, both work great when used with lazy stacks."}, {"start": 53.0, "text": "I think it's a good time for a refresh and dive into lazy stacks and scrolling."}, {"start": 58.0, "text": "I'll explain how they work, what you can do with them,"}, {"start": 61.0, "text": "and what you may want to avoid."}, {"start": 63.0, "text": "Afterwards, you will have a better understanding"}, {"start": 65.0, "text": "of the internals of lazy stacks stacks,"}, {"start": 67.0, "text": "that you'll be able to apply to lazy stacks in your own apps."}, {"start": 71.0, "text": "This video will assume basic familiarity with SwiftUI layout using stacks."}, {"start": 76.0, "text": "If you're new to SwiftUI, I recommend \"Stacks, Grids, and Outlines in SwiftUI\"."}, {"start": 84.0, "text": "I've been working on an Origami app"}, {"start": 86.0, "text": "that shows the instructions to make some popular origami pieces."}, {"start": 90.0, "text": "In this early version, it's only showing the steps to make a swan."}, {"start": 94.0, "text": "Here is the set-up for the main view."}, {"start": 97.0, "text": "I have a ScrollView, with a LazyVStack inside,"}, {"start": 100.0, "text": "that in turn contains a StepView for each step."}, {"start": 103.0, "text": "The lazy stack allows scrolling through a potentially large number of steps,"}, {"start": 107.0, "text": "without loading all views at once immediately."}, {"start": 111.0, "text": "I'll now focus on the LazyVStack."}, {"start": 114.0, "text": "Three of the steps to create an origami swan are fully visible."}, {"start": 119.0, "text": "A small part of the StepView for step 4 is also visible."}, {"start": 123.0, "text": "Now, the full LazyVStack is a lot larger than the visible views."}, {"start": 128.0, "text": "But, unlike a VStack,"}, {"start": 129.0, "text": "a LazyVStack does not evaluate or render views that aren't visible."}, {"start": 134.0, "text": "A LazyVStack simply lays out its views top to bottom,"}, {"start": 137.0, "text": "and stops once the visible rect is filled."}, {"start": 140.0, "text": "If you scroll down the LazyVStack adds views as appropriate"}, {"start": 144.0, "text": "to make sure the visible rect remains filled,"}, {"start": 146.0, "text": "and as views are scrolled out of screen, they are removed from the lazy stack."}, {"start": 154.0, "text": "By not loading all views at once,"}, {"start": 156.0, "text": "a LazyVStack can be more efficient than a VStack."}, {"start": 159.0, "text": "But there is a correctness cost."}, {"start": 162.0, "text": "Since a LazyVStack doesn't load all of its views,"}, {"start": 165.0, "text": "the height of the subviews that are off-screen are estimated."}, {"start": 169.0, "text": "This estimated height is based on the average size of views"}, {"start": 172.0, "text": "that have been placed before,"}, {"start": 174.0, "text": "and the estimated number of remaining subviews."}, {"start": 177.0, "text": "The lazy stack is also unaware of changes in off-screen views,"}, {"start": 181.0, "text": "since they aren't loaded."}, {"start": 183.0, "text": "Similarly, since not all views are loaded,"}, {"start": 186.0, "text": "it wouldn't be able to find the maximum width of all views."}, {"start": 189.0, "text": "So the ideal width of a LazyVStack is that of its first subview."}, {"start": 194.0, "text": "In the case of my origami app, the first view is infinitely flexible,"}, {"start": 198.0, "text": "so the width of the LazyVStack equals the screen width."}, {"start": 203.0, "text": "Since the height of the LazyVStack is estimated and not precise,"}, {"start": 206.0, "text": "it can change during scrolling as the lazy stack learns more about the layout"}, {"start": 210.0, "text": "of new views scrolled onto screen."}, {"start": 214.0, "text": "For example, if you scroll down all the way"}, {"start": 217.0, "text": "and the last views are a bit smaller than the other views"}, {"start": 220.0, "text": "the lazy stack has to adjust"}, {"start": 222.0, "text": "its originally estimated size to account for this."}, {"start": 227.0, "text": "The space above the visible rect isn't precise either."}, {"start": 230.0, "text": "The scroll position, or content offset of the scroll view,"}, {"start": 233.0, "text": "therefore depends on an estimated position of the visible items."}, {"start": 238.0, "text": "One example where the space above the visible region is not precise,"}, {"start": 242.0, "text": "is after an orientation change on an iPhone."}, {"start": 247.0, "text": "StepViews are less tall in landscape than they are in portrait."}, {"start": 251.0, "text": "The subtitle text generally fits on fewer lines in landscape."}, {"start": 254.0, "text": "During the orientation change,"}, {"start": 256.0, "text": "the lazy stack will keep the StepView for step 4,"}, {"start": 259.0, "text": "the topmost visible view, anchored."}, {"start": 262.0, "text": "The LazyVStack isn't yet aware of the exact layout changes"}, {"start": 265.0, "text": "in the first few StepViews, since they aren't loaded."}, {"start": 268.0, "text": "But when scrolling all the way back up,"}, {"start": 270.0, "text": "the lazy stack must align to the top of the scroll view."}, {"start": 274.0, "text": "This means it must correct the estimated space"}, {"start": 276.0, "text": "above the visible region along the way."}, {"start": 279.0, "text": "It will update the content offset of the ScrollView with the same amount,"}, {"start": 282.0, "text": "such that the content offset at the top is zero as well."}, {"start": 287.0, "text": "The lazy stack and the embedding scroll view"}, {"start": 289.0, "text": "coordinate the position and content offset."}, {"start": 292.0, "text": "That way, when the estimations are updated,"}, {"start": 295.0, "text": "the relative position of the visible subviews"}, {"start": 297.0, "text": "in the scroll view doesn't change."}, {"start": 300.0, "text": "It's common to compose different types of views or content in the same lazy stack."}, {"start": 305.0, "text": "For my origami app, I think it could be cool"}, {"start": 308.0, "text": "if people could share a photo of their creation with others when they are done."}, {"start": 312.0, "text": "I'd like to display these photos at the bottom, in a horizontal scroll view."}, {"start": 316.0, "text": "I've added a Showcase view for these photos."}, {"start": 320.0, "text": "The Showcase view has a horizontally scrolling ScrollView,"}, {"start": 324.0, "text": "with a LazyHStack within."}, {"start": 326.0, "text": "This means that my app now has a LazyHStack nested"}, {"start": 330.0, "text": "inside the outer LazyVStack."}, {"start": 334.0, "text": "Nesting a LazyHStack in a LazyVStack like this"}, {"start": 337.0, "text": "can also be good for performance,"}, {"start": 339.0, "text": "since not everyone will scroll this nested scroll view to see the extra views."}, {"start": 345.0, "text": "For a LazyHStack, the ideal height, and therefore,"}, {"start": 348.0, "text": "its height in a vertical ScrollView, is that of the first subview."}, {"start": 351.0, "text": "For my origami app, all photos use the same height."}, {"start": 355.0, "text": "But if every photo had a user description label with a variable number of lines,"}, {"start": 360.0, "text": "longer subtitles would be cut off."}, {"start": 363.0, "text": "The LazyHStack cannot know in advance what the largest subtitle of all views is."}, {"start": 367.0, "text": "It hasn't loaded all of them."}, {"start": 370.0, "text": "The best solution is to fix the view heights."}, {"start": 373.0, "text": "For example, for text, you can set a line limit,"}, {"start": 375.0, "text": "and reserve space for shorter text."}, {"start": 378.0, "text": "But I'm actually thinking the photos in my origami app, should be a bit larger."}, {"start": 382.0, "text": "Maybe I should just add them vertically below the steps."}, {"start": 386.0, "text": "If I add them in a section, I could even pin the section header."}, {"start": 390.0, "text": "To pin section headers, use the pinnedViews parameter"}, {"start": 394.0, "text": "on the LazyVStack."}, {"start": 396.0, "text": "I've added the new Section inside the Showcase view, with a header view."}, {"start": 401.0, "text": "If I scroll down,"}, {"start": 403.0, "text": "the Showcase section header sticks to the top."}, {"start": 409.0, "text": "I'll now discuss some patterns to avoid so the lazy stacks perform at their best."}, {"start": 414.0, "text": "I'll do that using the photos showcase I've just added."}, {"start": 418.0, "text": "It could be nice to add a scroll transition to the photos"}, {"start": 421.0, "text": "as they scroll into and out of screen."}, {"start": 424.0, "text": "Here, I'm using the .scrollTransition modifier"}, {"start": 427.0, "text": "to give my steps an effect as they scroll on or off-screen."}, {"start": 432.0, "text": "However, lazy stacks only load views that are on screen,"}, {"start": 436.0, "text": "based on their original position."}, {"start": 438.0, "text": "And the transform here is pushing them out of their original frame."}, {"start": 443.0, "text": "That causes them to disappear when they should be visible,"}, {"start": 446.0, "text": "since the lazy stack believes they are off-screen."}, {"start": 450.0, "text": "Here, as you scroll down,"}, {"start": 452.0, "text": "the pink swan disappears too soon."}, {"start": 456.0, "text": "If you apply a scroll transition to views in a lazy stack, make sure that views that"}, {"start": 460.0, "text": "wouldn't be normally visible aren't pushed into the visible rect."}, {"start": 465.0, "text": "Here, I'm using a different scale effect."}, {"start": 468.0, "text": "This works fine."}, {"start": 470.0, "text": "In general, make sure that views that wouldn't normally be visible"}, {"start": 474.0, "text": "aren't pushed into the visible rect in a transform."}, {"start": 477.0, "text": "The lazy stack won't be aware of that."}, {"start": 480.0, "text": "Since you need to scroll down a little to get to the Showcase,"}, {"start": 483.0, "text": "I'll also add a button to quickly scroll there."}, {"start": 488.0, "text": "But the button shouldn't always be visible."}, {"start": 491.0, "text": "I'd like it to only be visible when near the top of the scroll view."}, {"start": 495.0, "text": "When someone scrolls down, it should disappear."}, {"start": 501.0, "text": "Here, I'm using an .onScrollGeometryChange on my scroll view"}, {"start": 505.0, "text": "to get the absolute content offset."}, {"start": 508.0, "text": "When I'm scrolled down more than a 100 points, the button disappears."}, {"start": 513.0, "text": "This works, but since the content offset of a lazy stack is estimated,"}, {"start": 518.0, "text": "the exact position where the buttons disappears,"}, {"start": 521.0, "text": "can change when the estimations change."}, {"start": 524.0, "text": "Instead, it's better to use the relative positions of subviews"}, {"start": 527.0, "text": "in the visible region of the scroll view."}, {"start": 531.0, "text": "One way to do that, is to use"}, {"start": 533.0, "text": "the .onScrollTargetVisibilityChange modifier."}, {"start": 537.0, "text": "The closure in that modifier is called when the visibility of the subviews"}, {"start": 540.0, "text": "in the visible region of the scroll view changes."}, {"start": 543.0, "text": "Here, the visibility of the \"Scroll to Showcase\" button"}, {"start": 547.0, "text": "depends only on which subviews are visible, with a threshold of 80%."}, {"start": 552.0, "text": "I've now covered the layout of lazy stacks in detail."}, {"start": 556.0, "text": "I've said that lazy stacks add subviews,"}, {"start": 558.0, "text": "when they are about to enter the visible part of the scroll view."}, {"start": 561.0, "text": "However, the subviews a lazy stack loads individually,"}, {"start": 565.0, "text": "do not always correspond directly to the view structs you define in code."}, {"start": 568.0, "text": "Let's go back to my original code and take another look at the ContentView."}, {"start": 573.0, "text": "In this simple case, there is a 1-to-1 correspondence of StepView instances"}, {"start": 578.0, "text": "and the subviews that the LazyVStack sees."}, {"start": 582.0, "text": "There's a ScrollView, and the ScrollView has a LazyVStack as a subview,"}, {"start": 586.0, "text": "and the LazyVStack has the ForEach as a subview."}, {"start": 589.0, "text": "But of course"}, {"start": 591.0, "text": "the ForEach isn't just a single view."}, {"start": 594.0, "text": "It's resolved to one StepView for every step."}, {"start": 598.0, "text": "And in most cases, these are the subviews that the LazyVStack loads."}, {"start": 603.0, "text": "But here, the StepView is slightly more complicated."}, {"start": 607.0, "text": "And that will be important."}, {"start": 609.0, "text": "The body contains two views, StepDiagram and StepInstructions,"}, {"start": 614.0, "text": "at the top level of the body."}, {"start": 616.0, "text": "They're also not embedded in another layout, like a VStack."}, {"start": 622.0, "text": "In this case, LazyVStack still has the ForEach"}, {"start": 625.0, "text": "which is resolved to a StepView for every step."}, {"start": 627.0, "text": "But just like the ForEach resolves to multiple StepViews,"}, {"start": 631.0, "text": "each StepView now resolves to two views as well."}, {"start": 634.0, "text": "The LazyVStack evaluates and loads StepDiagram"}, {"start": 638.0, "text": "and StepInstructions seperately."}, {"start": 640.0, "text": "Of course, StepView still needs to be evaluated for the lazy stack"}, {"start": 644.0, "text": "to create either of those."}, {"start": 646.0, "text": "Views can also resolve to a dynamic number of views."}, {"start": 648.0, "text": "But that is something you have to watch out for."}, {"start": 652.0, "text": "In this case, StepView is using a detailLevel environment value,"}, {"start": 656.0, "text": "to check whether it should be visible."}, {"start": 659.0, "text": "The ForEach again resolves to a StepView for each step."}, {"start": 662.0, "text": "But each StepView now resolves to either one subview or zero subviews."}, {"start": 667.0, "text": "In this case, step 2 isn't visible given the current detail level,"}, {"start": 671.0, "text": "but the first and third steps are."}, {"start": 675.0, "text": "That works, and the contents of the StepView are loaded lazily,"}, {"start": 679.0, "text": "but the StepView itself can be kept alive longer than you may expect."}, {"start": 683.0, "text": "That is because a LazyVStack addresses the visible subviews using their index."}, {"start": 690.0, "text": "It now has to keep earlier StepViews around,"}, {"start": 692.0, "text": "just in case the detailLevel environment value changes,"}, {"start": 695.0, "text": "because that would affect the indices."}, {"start": 699.0, "text": "In leaf subviews that are created many times in a ForEach, like StepView,"}, {"start": 703.0, "text": "avoid creating a dynamic number of subviews."}, {"start": 707.0, "text": "The example where a detailLevel environment value"}, {"start": 710.0, "text": "is used to filter out steps,"}, {"start": 711.0, "text": "is therefore not a good idea."}, {"start": 715.0, "text": "Say that unrelated environment value, like writingStyle,"}, {"start": 719.0, "text": "is used in the contents of the StepView body."}, {"start": 722.0, "text": "A change in this environment value can now cause body evaluations for views"}, {"start": 726.0, "text": "that are scrolled out of screen, causing unnecessary view updates."}, {"start": 730.0, "text": "The lazy stack also won't release state allocated for the StepView."}, {"start": 735.0, "text": "Instead, filter at the data level."}, {"start": 738.0, "text": "If you're using SwiftData, use a Predicate to filter your Query."}, {"start": 742.0, "text": "Here, I'm using the detailLevel in the Predicate."}, {"start": 746.0, "text": "This makes the number of subviews immediately clear to the LazyVStack."}, {"start": 750.0, "text": "It doesn't have to construct views to compute view counts or indices."}, {"start": 755.0, "text": "Note that unwrapping an optional in a view body has the same effect."}, {"start": 759.0, "text": "Here, I'm optionally unwrapping an apiToken Environment variable."}, {"start": 764.0, "text": "The body only returns something if that token isn't nil."}, {"start": 769.0, "text": "The token is something that could be handled by a NetworkClient model object."}, {"start": 774.0, "text": "If someone is not authenticated,"}, {"start": 775.0, "text": "a view higher up in the hierarchy could show a ContentUnavailableView,"}, {"start": 779.0, "text": "instead of showing the lazy stack in the first place."}, {"start": 783.0, "text": "Since lazy stacks only keep a small part of their data in memory,"}, {"start": 787.0, "text": "they do not need to perform a full diff of their contents."}, {"start": 790.0, "text": "They only perform a minimal check for changes in the visible views."}, {"start": 795.0, "text": "Lazy stacks don't always load a subview all at once."}, {"start": 799.0, "text": "I'll now discuss prefetching,"}, {"start": 801.0, "text": "an internal mechanism with which lazy stacks"}, {"start": 803.0, "text": "improve the scrolling performance of your apps."}, {"start": 807.0, "text": "When you scroll in a specific direction,"}, {"start": 809.0, "text": "and the visible part of the lazy stack reaches the end of the placed content,"}, {"start": 813.0, "text": "the lazy stack already prefetches views before adding them on screen."}, {"start": 817.0, "text": "Prefetching means that lazy stacks will perform part of the work"}, {"start": 821.0, "text": "of displaying a view before it's visible."}, {"start": 824.0, "text": "While scrolling, a ScrollView needs to draw frames at constant rates."}, {"start": 829.0, "text": "That means there is only a limited time available"}, {"start": 831.0, "text": "to perform computations, up to a frame deadline."}, {"start": 835.0, "text": "This work includes the ScrollView updating the content offset,"}, {"start": 839.0, "text": "your views rendering at a new position,"}, {"start": 841.0, "text": "and work your app may do in response to the content offset change."}, {"start": 845.0, "text": "And when the ScrollView contains a lazy stack,"}, {"start": 847.0, "text": "it includes the work for evaluating views scrolled on screen,"}, {"start": 850.0, "text": "performing the layout, and rendering them."}, {"start": 853.0, "text": "But, the work for placing new views on screen can be expensive."}, {"start": 859.0, "text": "If the work would take too long, passing the deadline,"}, {"start": 862.0, "text": "that would result in a dropped frame."}, {"start": 864.0, "text": "That is visible as a hitch while scrolling so that should be avoided."}, {"start": 870.0, "text": "Prefetching is used to prevent such dropped frames."}, {"start": 874.0, "text": "While scrolling, the lazy stack already checks if there is enough time available"}, {"start": 878.0, "text": "to perform part of the work of rendering a new subview,"}, {"start": 881.0, "text": "before it is scrolled on screen."}, {"start": 883.0, "text": "For example, a lazy stack may be able to evaluate the body"}, {"start": 887.0, "text": "and layout of a view about to appear on screen, before it appears."}, {"start": 892.0, "text": "When the view finally does appear,"}, {"start": 894.0, "text": "most of the work has already been performed,"}, {"start": 897.0, "text": "broken up across multiple frames."}, {"start": 901.0, "text": "The work to show a nested LazyHStack in a LazyVStack"}, {"start": 904.0, "text": "can be broken up across multiple frames as well."}, {"start": 907.0, "text": "When the view appears, onAppear is called."}, {"start": 909.0, "text": "So generally your view's body is called at one point,"}, {"start": 912.0, "text": "and onAppear only a little later, when the view is placed on the screen."}, {"start": 916.0, "text": "If the scroll direction is reversed,"}, {"start": 917.0, "text": "it's even possible that the view's body is called as part of prefetching,"}, {"start": 921.0, "text": "and onAppear is never called."}, {"start": 923.0, "text": "Using onAppear in a lazy stack"}, {"start": 925.0, "text": "is useful for a number of things, even for data loading."}, {"start": 928.0, "text": "One such use case is infinite scrolling."}, {"start": 931.0, "text": "Here, the origami app fetches more photos from the web, when you scroll to the end."}, {"start": 937.0, "text": "The last view, a ProgressView, has an .onAppear modifier."}, {"start": 941.0, "text": "When that view appears, a new page is fetched."}, {"start": 946.0, "text": "But, loading everything in onAppear for each view is not a good idea."}, {"start": 952.0, "text": "In this example, onAppear is used to set-up every view."}, {"start": 956.0, "text": "The size and large parts of the view's contents"}, {"start": 959.0, "text": "completely change after it's placed."}, {"start": 962.0, "text": "The work that prefetching has done earlier will be thrown away,"}, {"start": 965.0, "text": "and has to be re-done when the view appears."}, {"start": 968.0, "text": "The lazy stack may also load more views than needed,"}, {"start": 971.0, "text": "and scrolling can be affected, as I'll show later."}, {"start": 975.0, "text": "Instead, set-up the view in the initializer"}, {"start": 977.0, "text": "such that it is in a reasonable state before it appears on screen."}, {"start": 983.0, "text": "Even when it's not essential,"}, {"start": 984.0, "text": "it can be useful to load content before views appear."}, {"start": 988.0, "text": "Here, I'm using the task modifier"}, {"start": 990.0, "text": "to remotely load a diagram from the internet when the view appears."}, {"start": 994.0, "text": "But I can actually make use of prefetching to load it slightly earlier,"}, {"start": 998.0, "text": "so the chance is higher it's loaded by the time it appears."}, {"start": 1001.0, "text": "For example, I could use a DiagramLoader observable object, connected to a cache."}, {"start": 1007.0, "text": "When the cache doesn't contain the data for a specific ID,"}, {"start": 1010.0, "text": "it could load the data immediately when it's initialized."}, {"start": 1013.0, "text": "Since it starts loading the diagram in the initializer,"}, {"start": 1016.0, "text": "the diagram will be fetched slightly earlier."}, {"start": 1019.0, "text": "Views that are scrolled out of screen aren't rendered or updated anymore."}, {"start": 1022.0, "text": "But they aren't removed from memory immediately."}, {"start": 1025.0, "text": "Lazy stacks keep these around for a number of updates,"}, {"start": 1028.0, "text": "in case they are scrolled back on screen."}, {"start": 1031.0, "text": "When views finally are deleted from memory,"}, {"start": 1033.0, "text": "state variables are deleted alongside."}, {"start": 1037.0, "text": "Since the data associated with views scrolled out of screen will be deleted,"}, {"start": 1040.0, "text": "don't depend on view state for data that needs to be kept alive after scrolling."}, {"start": 1045.0, "text": "Here, StepView uses an isHighlighted state variable."}, {"start": 1049.0, "text": "But if the view is scrolled away, that highlight state will be lost."}, {"start": 1053.0, "text": "Instead, move important state to model objects,"}, {"start": 1056.0, "text": "or outer views using a binding, as here."}, {"start": 1060.0, "text": "You typically use lazy stacks inside a scroll view."}, {"start": 1063.0, "text": "I'd now like to give you some tips to make scrolling work well in lazy stacks."}, {"start": 1068.0, "text": "Earlier, I added a button to my origami app,"}, {"start": 1070.0, "text": "to scroll to the showcase with user photos."}, {"start": 1074.0, "text": "The code to programmatically scroll to the section would look like this."}, {"start": 1078.0, "text": "I'm using a ScrollPosition binding to scroll to the showcase's section header."}, {"start": 1084.0, "text": "Programmatic scrolling works in lazy stacks,"}, {"start": 1086.0, "text": "even if the target view isn't on screen."}, {"start": 1090.0, "text": "Scrolling to an off-screen view requires the lazy stack to estimate its position."}, {"start": 1095.0, "text": "In an animated scroll,"}, {"start": 1096.0, "text": "the lazy stack updates this estimated position on every frame."}, {"start": 1100.0, "text": "Still, there are some things"}, {"start": 1102.0, "text": "that can prevent scrolling from being smooth and fast."}, {"start": 1105.0, "text": "For example, also here,"}, {"start": 1107.0, "text": "having a dynamic number of views in StepView has a performance impact."}, {"start": 1111.0, "text": "Programmatic scrolling to a view with an ID"}, {"start": 1114.0, "text": "is most performant if each view in your ForEach"}, {"start": 1116.0, "text": "always resolves to one single subview."}, {"start": 1119.0, "text": "In that case, the lazy stack can query the ForEach to find the ID to scroll to,"}, {"start": 1124.0, "text": "without constructing any of the views."}, {"start": 1126.0, "text": "Scrolling to a subview near the end is also more performant,"}, {"start": 1130.0, "text": "if the lazy stack can quickly count its subviews."}, {"start": 1133.0, "text": "As before, instead of filtering out views with a conditional in the view body,"}, {"start": 1137.0, "text": "you should filter on the data level,"}, {"start": 1138.0, "text": "for example, with a predicate on a Query."}, {"start": 1142.0, "text": "Programmatic scrolling also becomes less smooth,"}, {"start": 1144.0, "text": "if too many views change their layout after they appear on screen."}, {"start": 1148.0, "text": "A common pattern that does this, is using onGeometryChange,"}, {"start": 1151.0, "text": "to set a state value that is then used in another layout pass."}, {"start": 1156.0, "text": "Here, StepView has a state variable subtitleHeight,"}, {"start": 1159.0, "text": "updated in an onGeometryChange on the subtitle."}, {"start": 1164.0, "text": "The view is then evaluated again,"}, {"start": 1166.0, "text": "and subtitleHeight is used to compute the frame of the diagram."}, {"start": 1170.0, "text": "This makes scrolling less reliable."}, {"start": 1172.0, "text": "The lazy stack measures the view's original height,"}, {"start": 1175.0, "text": "but the height changes after the view appears,"}, {"start": 1177.0, "text": "pushing down other content."}, {"start": 1181.0, "text": "In cases like this, if you cannot use SwiftUI's layout primitives,"}, {"start": 1184.0, "text": "use a custom layout instead."}, {"start": 1187.0, "text": "Here, that custom layout is StepLayout."}, {"start": 1189.0, "text": "To learn more about using custom layouts"}, {"start": 1191.0, "text": "check out \"Compose custom layouts with SwiftUI\"."}, {"start": 1196.0, "text": "Alright, I've shown you many aspects of lazy stacks."}, {"start": 1199.0, "text": "I've talked about their layout,"}, {"start": 1201.0, "text": "how views structs don't always resolve to a single subview"}, {"start": 1204.0, "text": "and how that affects lazy stacks,"}, {"start": 1206.0, "text": "how lazy stacks prefetch views for better scrolling performance,"}, {"start": 1209.0, "text": "and how they allow programmatic scrolling to views off-screen."}, {"start": 1213.0, "text": "Along the way, I've given some tips"}, {"start": 1215.0, "text": "and best practices that you can use for lazy stacks in your apps."}, {"start": 1219.0, "text": "For example, avoid using the absolute content size"}, {"start": 1223.0, "text": "or content offset with lazy stacks, since these are estimated and unstable."}, {"start": 1228.0, "text": "Avoid using conditional view content in leaf views to filter out data,"}, {"start": 1232.0, "text": "as that can cause SwiftUI views to stay alive longer than expected."}, {"start": 1237.0, "text": "Set up your lazy stack's subviews before onAppear is called where possible"}, {"start": 1240.0, "text": "to ensure prefetching works best."}, {"start": 1243.0, "text": "And don't change the layout of subviews of lazy stacks after they appear,"}, {"start": 1247.0, "text": "as that can push the lazy stack out of the targeted scroll position."}, {"start": 1252.0, "text": "Understanding some of the mechanisms with which SwiftUI components work"}, {"start": 1256.0, "text": "helps you excel at using them."}, {"start": 1257.0, "text": "And I think, after preparing this video, I now excel at creating swans."}]}