SwiftUI & UI Frameworks • 1:00:58
Join us online for a deep dive into WWDC26 with Apple engineers and designers to ask questions, get advice, and follow the discussion about the week’s biggest SwiftUI announcements. Conducted in English.
Unlisted on Apple Developer site
Transcript
This transcript was generated using Whisper, it may have transcription errors.
Hi, and welcome to another SwiftUI and UI frameworks Group lab. I’m Curt and I’m part of the Worldwide Developer Relations team here at Apple. I’m joined by engineers and leaders from the UI frameworks team, Aditya, Jason, Taylor, David and Sima. In addition to those on screen, there’s a team behind the scenes helping with the triage of all your inbound questions. We’re excited to answer your questions today about all things SwiftUI.
If you have code specific questions or we somehow can’t get to your question today, please bring your questions to the developer forums at developer.apple.com/forums where we’ll continue the discussion. If you have a bug or a feature request, go to Feedback assistant.apple.com. As we’ll focus on questions today that will help the broadest audience. So without further ado, let’s go to your questions.
So our first question today is from you, Jay. And Jay says, I have an architecture question. Is there an architecture like MVC or Mvvm or other that the SwiftUI team expects us to adopt? Sam, do you want to start us off on that one? Yeah. So there’s really no architecture that we expect you to adopt.
SwiftUI is really designed to be architecture agnostic and really like any question, any architecture that works for your specific app is would work, should work with SwiftUI. And if for some reason the architecture of your choice integrates poorly with SwiftUI, or like you see some ergonomic issues with like trying to like pass data around or other issues, feel free to let us know and we will see what we can do to like, make sure your experience is the best possible. So, David, in your in your talk this year, you talked about using observable with Appkit and UIKit and then how that makes it easy to transfer to, to SwiftUI. Does that have implications for choosing an architecture.
You can use observable to, to store your model and make it easier to transition to the new framework. But another case is also it’s just swift code. Essentially. It’s not integrated with any UI, like it’s integrated with UI frameworks, but it’s not dependent on them. And so you can use that and just write tests for it and use it in other components and listen to changes yourself.
One of the kind of core messages in this session is like how you can adopt SwiftUI incrementally. So one of the important things with this architecture question is you maybe have an existing app with an existing architecture, and SwiftUI needs to be able to handle whatever architecture that is including as you start to mix and match with Appkit, UIKit, or as you start to just grow more and more SwiftUI unique code.
Yeah. And the other reason is that, you know, your app apps can be very different. You can have like a very small app for which like, you probably don’t need to go for like some, you know, elaborate architecture that requires you to refactor most of your code and you just want to get something started really quickly.
And you can also be working on like a very large app where like you have maybe a team like committing like daily to the large code base. And maybe in those cases you want to reach for like some of the other like solutions that require your code to be like more standardized across the entire code base.
So it really just depends on the kind of app that you have. Yeah, I think it’s really underrated, the ability to stay nimble. So you don’t want to build your app just because you have a certain architecture, you want to be able to ship features to your users.
And that’s what should drive all decisions, I think. Yeah. And I think that’s a great point. The the other thing that I think is interesting is structuring your data model just around the UI framework is maybe not sort of the leading way to approach it. You have to think about persistence.
How are you syncing? Do you need a crdt to keep, you know, simultaneous edits? It’s like all these things drive your data model also. And, and so like, if you can keep your SwiftUI views so that just a projection of your data model to pixels and then design the data to be as robust and testable as you can, I think that’s the best move. Yeah.
So another, another question and I, I think I’ll take this to you first, Jason. So apologies if this is basic. No apologies needed. We welcome all questions. But what’s the difference between an at viewbuilder closure or computed property and a separate view. Struct are the performance pros and cons between these approaches.
Yeah. So it’s quite common that if you have like a very large view body, you’ll you’ll quickly want to break it up into smaller pieces, which it’s easier to reason about your view. And often people will create maybe a computed property that is at Viewbuilder or now at content Builder to do this.
And yeah, there, there are differences. So one of the major ones is that a view itself has its own identity. And so it can actually track dependencies. So you pass in the the data that is specific to that view. Pass that into this new view. And it can independently track dependencies.
Whereas if you’re just using the the the computed property, it really is in effect, just like as if that was still in your view body. You’ve just you’ve just moved it out into, you know, a separate method. But the view, new view is entirely itself a new, a new view. And so yeah, maybe David, you have or may have more to.
Yeah. Like from SwiftUI’s perspective, we’re just executing a function and we do not have more granular control. If the function is calling other functions, we cannot intervene there. But if it’s a separate view that is created, we are the ones that call the body computed property. And so there we we can scope the dependency tracking and invalidation to each view independently.
Yeah. So it’s really for more performance wins. You want to move some parts of your view out to a separate custom view that has its own body. And that would let us more A granularly update, only the views that have changed, so that can really help you with performance. Could I ask what’s the trade off there? Like what you don’t want?
Probably like every single component made into a separate view, right? So like what level should like someone reach for like creating like a sub view? Essentially, this is one of the surprising things when people approach SwiftUI for the first time is they think, okay, I can’t make too many views, right? There’s like overhead to each individual view and SwiftUI, it’s different where like there is this performance benefit. There’s also preview benefit. You can now preview things at a finer grained level.
So I don’t think that we have a recommendation of like, what is too small of a view to, to break apart if there’s reasonable dependency differences. Yeah. I mean, if we, if we tunnel into the detail level, it’s like each view because it’s a struct is allocated on, on the stack. And so it’s very inexpensive to allocate a view, render it and then discard it.
And so the fewer pieces of state that a particular view depends on, the less likely it is to have to be redrawn. So that’s where the performance win comes from. It’s useful to to think of your your view body like a description of your views. They’re very short lived. You could even think about it as like just a data model for your views. They’re, they’re not going to stick around very long. So. So it’s very lightweight.
I think we often like talk about how great observable is for invalidation. And if it’s like only a small little piece of your big view reads that property, it invalidates the whole view. So that’s like another great example of a place where you can extract out and minimize the dependencies, right? Yeah, that makes sense. And it also extends beyond views to modifiers, where we have the same concept, where you can extract a couple modifiers into your modifier and then get the same benefits as splitting it up into multiple views. That’s true.
Yeah. And if you’re looking for advice on like how and when to start breaking up your views. The kind of guiding principle that I would recommend is also like if you’re seeing maybe like a repeated pattern of like the same set of modifiers that you apply, like across your view hierarchy. Maybe you have like some, some sort of like a style system, like in your app where like there’s a repeated kind of pattern that you have, then that’s a great place to say, okay, maybe this can be a custom modifier that I can just apply in various parts of my view hierarchy. And that same goes for custom views.
It’s also sometimes important to have a a view so that you can do things like read read from the environment. Right? And so one example where I this, this may be trip some people up is like, we have an environment value called background prominence that is increased if when a, like a, a table or a list row is selected.
And so you can, you can use that in your view to adapt to that selection state and maybe, you know, change the color of, of something in the selection so that it so that it appears better against the darker background. And if you don’t create a new view, if you just have the stuff that’s within that within the cell, you, you can’t necessarily read from the environment. So that’s, that’s another, I think good use of for doing that.
So I think I’ll start with you on this next question, Taylor. And especially as we we see more internal adoption all the time with SwiftUI. What are some common anti-patterns we see people fall into with, with SwiftUI. And I’ll open this up to everyone, but we’ll start with Taylor. That’s a good question. And like, I think like there’s Anti-patterns and then there’s also like, what’s the right way to do things? A number of things come to mind.
I think we have things like Geometryreader, which we often talk about how it’s great to analyze where you’re using Geometryreader in your app and whether or not you can use a layout instead. Layout was introduced several years ago and is a great alternative, so you’re not similar to our previous discussion, invalidating an entire view hierarchy when the layout changes, and instead really customizing the layout at that point or similar tools.
You know, Onchange is another similar one to watch out for. If you’re using Onchange to try to trampoline data back and forth. It’s a good sign. Sometimes there’s a correct use, so it’s not like never use onchange, but you want to analyze. Are you using it in a way where there’s not a better substitute?
Yeah, I see that in workshops we do is that a lot of times people will use Onchange a lot as they’re moving from an imperative UI framework to a declarative one, because it’s sort of comfortable, imperative territory to jump to a non change. And so you need to think about whether you can make the view more declarative. So it just responds to data instead of you writing the code that reacts to the data. Exactly like and like we both said, like they’re not wrong to use. There are appropriate uses. It’s really just analyzing and making sure you’re using them in the appropriate way.
Anybody else have a favorite anti-patterns? I think so environment. Every time you kind of refer or declare an Add environment property in your view, every time that environment changes, we would reevaluate all views that kind of read that environment. So I think something to watch out for is like some value that’s like frequently changing in the environment that you’ve like you’re reading, and maybe some of you don’t even need to read that environment.
And they’re now suddenly getting invalidated because it’s because the environment was declared. But even if the body of that view doesn’t read the environment, we still have to like invalidate. So kind of analyze usage of environment properties and kind of see if you can remove some of them or reduce the the frequency.
Yeah. That’s another place where observable works really well because you can pass observable down through the observable object, down through the environment. The object pointer itself is not changing. And so the environment itself is stable and your view is just reading properties from it. And so that’s a great use there.
I have one, it’s not so much an anti-pattern I guess, but I would say in going down to like the level of like controls or leaf views or something, I’ve often see people create wrapper views around things like buttons because they have like customizations they want to make to a button that they want to use throughout their app.
And so they’ll make like a, you know, my custom button view that has effectively a button with a bunch of modifiers applied to it or styling the label within the button. And I think that’s a great opportunity to explore button styles and all the other various style protocols that we have.
If you’re doing that, you can you can often just create a custom button style. And within the button style, you can actually pass the configuration right through to another button. If you want to just apply some modifiers to, to a button, or you can just use the buttons configuration dot label and apply whatever modifiers you want there. So that’s a good way of creating like reusable custom buttons or any other control really, and share that across your app.
And that way you can use all of the button initializers that exist, which include all the convenience ones for localization and using system images. And you don’t have to recreate those initializers. You don’t have to have your custom view, you know, let you pass through a title. And then you’re just, you’re doing a lot of stuff that the framework can give you for free.
And that’s especially important too when you’re when you’re creating something custom, because, you know, if you apply a modifier to a button that may be different than applying it in the label. If you want to add padding to a button, you want to really do that in the label, or ideally do that with a custom button style.
Something that Eddie and I were talking about the other day, which is like, how much list gives you out of the box? I think another similar thing is, especially with AI tooling today, it’s easier than ever to build just like a completely custom view from scratch when sometimes like using a list or using some other standard component is the right thing to do for how much it actually gives you.
And you know, as, as the new design has evolved this year, if you’re using the built in components, you’re evolving right along with it without doing any additional work. Yeah. And often, like if this has been like a, a wrapper that you wrote maybe 5 or 6 years ago, sometimes you might not even know, you might update your app.
And we’ve we found this in like some workshops where even the developer neither like us from Apple nor the developer who’s trying to like update their app can seem to figure out why something isn’t changing and you have to spend like half an hour debugging to figure out, well, oh, is this like wrapper that everyone’s forgotten about? Right. If you can like stick to like stock components, it’s definitely easier to update. Yeah. I think a similar thing is like when it’s like, oh, well, SwiftUI, maybe today does not have this standard component that I really want to use.
I should just rewrite it from scratch in SwiftUI versus like we were talking earlier, the interoperability, like it is so easy to bring in a UIKit or Appkit view that does what you want, that it’s totally recommended that that is not an anti-pattern. That is what we would recommend doing. Yeah. One more anti-pattern is potentially using conditional modifiers. If you write your own modifiers and then you have an if condition, you apply one modifier. Otherwise like you turn for example, just the view as is. And this is like, I think acceptable if you like.
The condition never changes once the view has been initialized, but when the condition changes while the view is on screen, it completely recreates the view, which has all sorts of issues. The animations like can break state gets reinitialized, but also from a performance perspective is a concern. And that’s a good excuse for me to call out Ren’s talk on lazy stacks.
That was in this year’s Wwdc videos, where he goes into lots of things that you can do that will accidentally cause a lazy stack to update more slowly than it should. And so that’s one of the ones he talks about, and there’s some other ones in there. And I’d encourage folks to take a look at that. Thanks for that question, Ozzie. Clearly we have opinions on that one.
So let’s let’s move on to the next one. And I think I think we’ll do this just down the line and let everyone take a crack at it. So we’ll start with you, Sima. What’s one concept in SwiftUI that takes many developers the longest to understand? And how would you explain that?
Yeah, I think that’s a good question. So the kind of the hidden part of SwiftUI is the way how we use update is how we kind of have like an internal representation, which is like a graph and it effectively has like edges and nodes that represent individual views. And the edges are like the inputs to the view. And so we use that data structure internally to understand which parts of your hierarchy updated as a result of like a state change.
And I think it’s important to, on some level, kind of be aware of that and be aware of the fact that there is, there is some underlying mechanism that like diffs or like effectively figures out if your view has indeed changes and if so, like updates its body. And I think as you, as your app grows and as you kind of look into more like performance tooling and instruments.
It’s very helpful to know about that underlying update mechanism to then reason about what is wrong with my app’s performance. Why is my view updating more than I would expect it to? Updates are all about state change, not rerendering and comparing the views like we’re not diffing the Dom of an HTML hierarchy, we’re actually looking at the state.
So David, do you have a concept that you think people trip over? Yeah. Sometimes with foreach when you want to like filter the data, they’re out. A lot of people write like an if in the body of the foreach. And that like gives you the result visually, but has the problem of.
Then SwiftUI often to figure out the the total number of elements that we often need for certain operations internally, we need to iterate through the whole foreach. Right. And then this is ifs one one reason why that happens. But any views as well. Right. And in our docs we have some examples of that. And also a flag you can pass to debug this issue.
And the one of the simple solution is either filtering it upfront and having returning constant number of views or from any view, putting it in a in an stack or Z stack. So that makes it a constant number of views from the. Yeah. So the for each can count without doing more interpretation. Exactly.
Yeah. I’m going to riff off of David’s anti-pattern about conditional modifiers. I think one of the things that’s interesting, like mental model shift is your views body is a description of that view and all the states that can go across. And so, you know, coming to SwiftUI for the first time, you’re often like, well, I want to add an, I want to make the opacity be zero, but normally it’s normal, right?
Normally it’s just like the default value, but most of our modifiers have what we call an inert variance. So instead of thinking of it as like adding and removing an opacity modifier, it’s well, the opacity is either one, it’s fully opaque or you’ve changed it to zero or some other value.
And so you can use ternary. And opacity is just like the simplest example. You can kind of apply this to almost any modifier you think of it as. Here’s my description of all the states it can be in. And the values I pass to those modifiers is what’s kind of capturing that, which even helps with things like animation. It makes it easier to animate between all these different states as well.
Yeah, definitely. And I also say like, if you come across anything that doesn’t have this like inert version that lets you do this, definitely file feedback for that feedback assistant.apple.com. Good luck. I think so going also riffing off the like the idea that views are like a description, something I often see is people referring to like the view body being reevaluated, reevaluated as like rerendering or redrawing. But that’s that’s not always the case.
It’s really just like your body can be reevaluated, but it doesn’t necessarily mean that something needs to be redrawn or, or rendered again, like SwiftUI will also handle that. So that’s, that’s I something, something I’ve just seen often. I think that especially if you come from another framework, it’s easy to think about it like that. The body is going to all like reevaluating the body is always going to cause this, this. Big. Task to happen. Yeah, yeah.
I know you come with a deep UIKit understanding and background. Is there anything about SwiftUI where the impedance difference there is? I mean, for me, as someone who has been writing UIKit for more than a decade. Which sounds pretty wild to say. The biggest change for me, like it’s something I understood or know, but it’s hard to internalize, is that UIKit lays out and arranges and renders views top down.
So we’ll start at the outside the window and go down to like the smallest child and like have some like loops as needed. Whereas SwiftUI tends to do it the other way around. It’ll start at like the innermost node and like build outwards. And so sometimes, especially if you’re like working in a sandwich or a cake or.
A sandwich or a cake, it means that you’re using interop and you have layers or alternating. Yeah. It can. It’s something to like, try to keep in mind that there’s a slight difference in approach in both frameworks. And so you might want to like consider how your data flows and how you decide to lay out and invalidate state to take those, these this difference into account.
Yeah, that gets it. One of the things that was challenging for me when I approached it too, it’s like the layout system is kind of like all the Subviews are asked, well, how big are you? And the Subviews asks, well, how big do you want to be? Until we get down to the leaf nodes, like, oh, I want to be this big. And then that sort of flows back up.
There’s a really great video from Paul a few years ago where he discusses this, and I found that to be very helpful in understanding the layout system. Yeah. It’s a video on custom layout, I believe from WW23. Yeah, probably you would know. I think I was kind of busy that year, but yeah, I think that was the year I did what’s new in SwiftUI? So.
So I think we’ll go back to you on this one, Audie. This is from Tyler. And and the question in UIKit, the concept of celery juice was fundamental to optimizing scrolling content. In SwiftUI are lazy stacks and grids sufficient for infinitely scrolling grid layout? Is there guidance to drop down to hosted SwiftUI and Uicollectionview in these cases?
What’s what’s the right way to think about that sort of issue? Yeah, I mean, I think the the primary reason UIKit was using reuse was because views are allocated and stored on the heap. So there’s like this fixed cost that you have to pay upfront no matter what you’re doing with your views.
So like cell reuse was primarily to get around the fact that, well, you have to instantiate and allocate memory for each one of your rows or cells, and then all of them come with their overhead, adding them and removing them from the hierarchy. Excuse me, have their own overhead. SwiftUI doesn’t have the same issue necessarily. Sorry. Excuse me.
Well, it takes a drink. Does anyone else want to jump in on that? Yeah, like SwiftUI has also some benefits. It has some implicit prefetching. So while you’re scrolling in one direction, we look in what what cells come on screen next. And we start evaluating those view bodies.
And we have because our graph is made up of individual nodes, we have the concept of partially evaluating that. And so while we have rendered the current frame, there’s sometimes some time left. And we reuse that to continue to evaluate the graph for the next cell and then stop right before the next frame begins.
Yeah, yeah. Nerding out. That is like one of the coolest things about this graph model is that you can do this like partial evaluation and then sort of say, hey, we’re out of time for this frame. We’ll keep it up the next one. Yeah, it’s really cool. And again, Zstack has this great animation of how this, this process works. And this actually gets at a perf thing that I thought was really interesting.
Sort of. I learned while working with Rens on this, this session Where if you’ve got, you know, an on a pier that reconfigures the state of a cell, that causes it to need to be laid out again because it changes the size, you actually throw away all that pre work that was done in prior frames. And so it’s really important to try to get that work done in a nit, not in body or in on a pier so that you get it done earlier. Yeah.
It’s kind of similar to our discussion about on change. Like there’s sometimes a good use case for on a pier, but as much as you can avoid it and have your data model handle it or otherwise. Yeah. Great. The reason for both of like on change and on a pier is that both of them have to go into the view body. And so we have to recompute the entire view body in order to execute those closures. Right. So yeah.
All right. Excellent. So the next question I think is super interesting is very pertinent to this year. The change from View builder and other builders to content builder looks great. Thank you. I mean, I didn’t do it, thank you. I’m fascinated how it works now and why this improved type checking performance. And could you say a little bit more about how that change was thought about and created? Maybe you want to jump on that. I can get started on that one.
First, I wanted to call out what’s new in SwiftUI talk. It has a great visualization of kind of the type checking work that needed to be done before this change, and now how it changed with Content Builder, where essentially the compiler needs to kind of consider less overloads when it’s like trying to type check your view body code. And that was causing like that type checking work was causing the notorious expression is too complex. The type checker that probably most of you have encountered.
And like this year, we, we did so much work to kind of improve the type checking story there. And that involved us kind of moving away to this new content builder Type where essentially it let us collapse the amount of overloads we had for each kind of custom type, like foreach group and section, which used to have like each like all of their own Viewbuilder overloads. Now we only have one.
And I think it’s a great improvement and hopefully a long awaited one, because I feel like even all of us here have like dealt with like the type checking problem. So we’ve even limited our API in some cases because of this. Yeah, we had like a ceiling and we were like, oh, we can’t like introduce more because like it’s going to worsen us. forEach only worked in some types because we couldn’t add it to any more places. Yeah.
I, Stephen shared a great analogy when we were working on the what’s New session that in the past you could think of like a view with lots of groups and sections in foreach. It was sort of like a cave, and the type checker had to explore every path through it, and then it could find the path out. and we’ve made it a hallway.
It’s content builder. All the levels down until you know that it’s a view. I think that’s a great analogy. That is cool. And it backports all the way to the beginning of SwiftUI because it’s effectively a, a type alias to view builder and then view builder got a lot smarter. So excellent. That’s one of my favorite things. So thanks for that question.
Oh, this one’s great. This, this is a throwback to the SwiftUI group lab last year, I think. SwiftUI is most loved. Debugging tool is self dot underscore print changes underscore included. When your teams hit a mystery rerender storm. Maybe I’ll start with you on on this one. Jason. What’s what’s the real workflow? How do you debug why you know rerendering is happening?
Is that instrument? Is that under bar print changes? Is it some mystery technique that that we don’t share with the world? Yeah. So I’m assuming they’re asking like, what do we do internally? And it’s really yeah, all of all of those above two. Another thing I’ve often seen happen or people do is like, do have a color that just is like random.
So you can very easily see if your body is reevaluating. Maybe on race, you try this on resize, maybe something’s evaluating. You’re not expecting that to happen. It’s also a good way of visualizing. I think the, the differences that we talked about in the first question or the second question, I can’t remember now, but of, you know, having the computer property view builder versus the, the separate view, that’s a good way to, to visualize that.
Does anybody have. Any. No, because that means you can sort of like add these print changes or. There’s also underbar log changes which uses OS logging. So a good tip in case you ever stumble across that. But yeah, you can sort of add it at these different layers and see like, okay, well, it’s this small little view changing.
Sometimes you’ll see like self changed and sometimes that can feel like a mystery of what that means. It’s really just like the value of that view has changed. And that often means you just need to crawl up to the, to the next level and see why that parent view changed.
I think this is like a great reason to use actually the SwiftUI instrument, like the instrument might, it might seem as though it’s mostly for optimizing performance, but it’s also like a really good debugging tool. If you can reproduce your issue and capture trace, you can figure out exactly what inputs cause something to reevaluate.
So if you have a hunch that, oh, I think something here changed, but I don’t know what, it’s actually great because you get this like giant graph of like exactly what caused your body to reevaluate, which can be sometimes a little simpler than trying to use trying to use print changes.
And especially given the strength of, of agents. Now you can, you can throw a trace or, you know, a crash report also at an agent. And, you know, sometimes the problem is one line in a thousand or, or 10,000 lines. And it’s hard for a human to see that we get bored reading that. Right. But the agent doesn’t get bored and it. Can.
Sometimes it even notices the one thing you overlooked. You know, I’ve had this happen a few times when we’ve been investigating things. It’s like, oh my gosh, how did it find that? Like one little. I often use the SwiftUI instrument. It also has a way to represent the view hierarchy as a tree.
And then it can draw a flame graph that is weighted by how much time we spend in each view. But I also just like to look at it. If I want to understand what an app does. Like I often look in our first party apps and see where their performance problems. And recently I was looking into the TV app and I noticed that we update sliders, but if I looked at the screen like there are no sliders.
And so this was something interesting to look at. Like this is often like, if you have an intuition or of what you would expect from an app or like look at it at, at the app and see there are things updating that are not even on screen or shouldn’t update at that point in time. This is really good indication that there is something that is worth investigating, right? And either there’s a bug in the app or in your understanding of it. And either things are good things to fix. Exactly. Yeah.
So there’s a follow up to, to this one. Why the underscore still? Well, I think that’s just really a signal to not ship this. Like don’t, don’t like remove this when you’re done with it. Don’t, don’t submit to the app store with with this. Yeah. You don’t want your users console to be filled up with all these print changes. And there’s a little bit of performance overhead of printing these strangers, right? Like it’s. Not the. Optimized path. Because. The string has to be generated, it has to be written out. Yeah, exactly. Excellent. Thanks.
So I think we’ve talked about this next question already, but it’s actually I think I’m going to go past this one because we opened with it about architecture and move on to the next one. Oh, this is a great question. And maybe I’ll start with you on this one. Sema. So nice to see the compilation improvements with that content builder. Is there any reason to continue using at View Builder? Is there a reason to choose between those.
That’s a great question. So I can there’s there’s probably some confusion around those two names. So content builder and view builder actually are the same. It’s just the type alias, but something that content builder would allow you to. It’s now possible to use it outside of your view.
So like you can have custom sort of SwiftUI like, like if I can say like DSLs or like you can create your own building blocks that aren’t necessarily SwiftUI views. So we’re kind of opening up that a little bit to the outside, which. Is interesting, interesting. And thought of that. Anybody have anything to add to that? That’s a good description. That’s a I learned something, so thanks for that question, I love it.
All right. So this this question is another architecture one, but a different take on it. So person asks, I’ve used Swift and MVC for over a year, but SwiftUI property wrappers feel like opaque magic. So my worry is if I don’t fully grasp how that state manages life cycle under the hood, how will I safely adopt swift concurrency?
So I think concurrency means I’m going to start with you. Sema but what’s the the mental model for connecting like at state and and, you know, state management and concurrency and how should I think about those together? That’s a great question. So in SwiftUI, we really try to make it as simple as possible to adopt concurrency in your code.
So we use our main actor. So that means everything that’s declared in your views, all of your state property wrappers will also be a main actor isolated. And that makes it really easy to work with. The one sort of the one or problem that you can encounter, like an issue that also actually we did cover in last year’s WPT talks is some closures in SwiftUI are Sendable. And that actually means that as an optimization, we would execute them from off main thread. And we we annotate those closures with assignable to kind of signal that. And so you’ll notice sometimes when you’re trying to use your state values, you’ll get some concurrency errors.
And the reason to that is you basically want to kind of use the copy of the value in the state, because the value of the state property wrapper or macro now is actually conditionally sendable when the value is sendable. So there is a literal not trick, but like a way you can use those values is by basically declaring like an explicit capture list in the closure and kind of copying over the values. And that’s just going to tell Swift compiler that, hey, like just use the copy of the state value in this closure. But these closures are really rare in SwiftUI. Most of our closures are like main actors, so you shouldn’t encounter any of this like normally.
But yeah, that’s like the only, the only thing I can think of or like you can run into maybe like a concurrency issue with state. Yeah. Daniel did a great. Well, Daniel and Sema both did great talks on concurrency and SwiftUI last year. And Daniel’s talk in particular talked about the sendable closures and the one place where I thought it was interesting that those were used is like for animations where it’s something that has to be done at frame rate. And so it takes it out of that main rendering loop, which is nice. Yeah. All right. Thanks.
Oh, this is interesting. So, Taylor, I’m gonna start with you on this one. And then maybe you’ll you’ll throw back to me. We’ll see. But what’s the recommended way for implementing navigation in an iOS app in iOS 27? If custom transitions are required? SwiftUI, UIKit plus SwiftUI. Yeah, that’s a great question.
And it really ultimately depends on like kind of the needs of your app and the user experience you’re trying to deliver. There is. Since you asked about iOS 27, there is a new API for customizing navigation transitions in SwiftUI. This year, like with built in support for Crossfades and other custom ones. So if those fit your needs, I think iOS 27 has your back.
If you’re trying to back deploy, that’s maybe where you have a different question. If you need a support releases before this API was introduced. And you know, we mentioned interop a few times, I think it is totally expected that if the experience you need is something that UIKit can deliver in the release you’re trying to deliver for, you should feel free to pull that in mix and match both ways as our layer cake example called out.
So hopefully that answers your question. And it’s kind of a case by case basis. Anybody have anything to add to that? I think this is a good question for the forums. And I believe tomorrow there’s a SwiftUI Q&A happening on the forums providing recommendations for navigation. It’s difficult because it’s highly dependent on what your app is and what your goals are.
So I think if you can bring a more specific question to the forums, you’ll probably get a something, an answer that’s better tailored for your situation. Yeah. I think another important thing to think about there, and, you know, this was emphasized in the state of the Union on on Monday and please watch it if you haven’t. But but resize ability of, of apps is important.
You know, iPhone apps can resize on iPad in the iPhone mirroring, they can resize. And so as much as you can stick with one of the system components for your navigation, whether that’s UIKit or Appkit or, you know, SwiftUI, try to do that because those are going to support that resizing that adapting for you. You’ll have a lot more work to do if you have to support it and you’re trying to actually build your own custom thing. Yeah.
All right. The next question is about scroll views. Maybe we’ll start with you on this one, David. Sure. So what’s the recommended way to track a scroll views scrolling offset? The goal is to show certain view elements based on how far the user has scrolled. Yeah. So we have different API’s depending on what is what you exactly need. Like the on geometry change modifier, we also have scroll scroll effects that are also take away a little bit of work that you need to do, but also are more efficient in certain situations.
And generally we have like the scroll position modifier as well to see which item is currently on screen. Right. And there’s, there’s a really interesting API that I can’t remember the name of, I’m afraid, but Renz talks about it in his talk where you can track what percent of a view is visible. And this is this is super useful if you’re doing analytics and you want to see, okay, this, this ad or this impression was on screen or fully on screen or whatever, but you can apply that to like the first or last view in, in a scroll view.
And now when that transitions to, you know, 80% visible, use that to trigger the update instead of trying to track the content offset, because the content offset is actually estimated based on the estimated heights of the views that aren’t in the viewport in the view view visible range.
So I thought that was a really interesting application. Yes. Yeah. I’d really like to emphasize like the content offset is, especially when combined with lazy stacks is essentially should be treated as an implementation detail. And it doesn’t really have any semantic meaning should try to focus on something relative to the views that are on screen. Yeah. Go ahead. You were going to jump in. Oh I’m. Sorry.
The scroll position modifier. I think it takes an ID and not an offset. Right. And so this allows you to also be more relative to what what your data model is. Yeah. And that actually reminds me and it’s, I guess sort of unrelated, but maybe tied up in the question.
It often comes up when people are trying to create an experience where like more views are paged in as you go farther. And so instead of looking at it like scroll position or content offset, you can add a view at the bottom of the list that that view actually has an on appear, which then tells your networking layer to go fetch more data and then updates the data model. And that’s a way you can implement infinite scrolling lists by paging based on the last view becoming visible, which is kind of cool.
Yeah. All right. So this is a this is a I guess it’s sort of an API design question. So maybe I’ll start with you on on this one. Taylor. The question is there a way to build custom layouts that are lazy similar to lazy vstack. Oh, that’s a good question. And we were just talking about different types of lazy views.
So today, no, there is no protocol for describing a custom lazy layout specifically. So please do file feedbacks like we were talking about earlier through feedback assistant that you’d like to see that and a little bit more detail about your use case. So today this would be a great example where it may be appropriate depending on your layout to interoperate and bring in a UI collection view. If your use case needs that, or we have a suite of other, just other really lazy layouts, lazy grids, lazy stacks, etc.. But you know, behind your question was also, you know, there is a protocol for non lazy layouts.
And so if you don’t require that laziness, I do recommend checking that out. That’s a great tool for building a custom layout that is much more optimized than trying to say, like, arrange things with a geometryreader and offsets and things like that. I think the other thing to think about there is, can you compose those? Can you use one of our lazy layouts and then a custom layout within that for a sub piece of it.
And so you get the laziness from the built in component, and then the custom layout from your component. That’s not always going to be possible. But. When appropriate, it’s great. Like I’ve seen some pretty cool sort of mosaic layouts built that way that look a lot more complex than like a simple stack or grid, but are basically built using that technique. Yeah.
And I think that that lets us do a nice transition here to another performance question. We’ll start with you, David. What’s the most common mistake you see developers make in large SwiftUI apps that ends up hurting performance? Yeah. Usually it’s often it’s too many invitations that tickle down.
Maybe start from the top and then cause invitations downstream. One is, if you have something that you put up in the environment at the very top that is read something downstream or by a lot of views, if that’s updating too often, like a very bad example would be to put in the time in milliseconds.
Have we. Seen that? We have seen the scroll position, for example, being put into the environment, which also updates every frame or potentially every frame while you’re scrolling? Yeah, that’s not the time yet. Another one is if you use like a value type that you have higher up in your view hierarchy and pass down through many views and like some views, maybe just really don’t use it, but just forward it.
We still need to evaluate those views to pass it to the next view, right? And so using observable macro helps here because the reference stays the same. You just pass that down and then only at the views that are actually accessing any properties there. We track the invalidation and update those. Right. Yeah. And to to put a little more color on that, a complex value type that is getting passed down level by level.
You have to do equality comparison on it at each level. Yes. And so the difference there is that the observable we just compared that the pointer is is unchanged. And so that’s where actually maybe somewhat surprising to, to folks who have like really gotten into swift and value types that there, there are cases where a reference type is the right choice. And that’s why observables are reference type. And they can also be put into an environment. And then you can indirectly update something more frequently that is technically in the environment. But the environment itself is not really static. Yeah, exactly.
I mean I think we talked about how much users love smooth scrolling, you know, and these tips and tricks for like scroll views and lists. Another thing is resize ability, you know, on Mac, iPad, and now even iPhone, you can resize your app. And that’s another place where users really expect like a really smooth experience. And so that’s another place where we’ve kind of talked about, hey, try to avoid geometryreader because using Geometryreader means on every single frame, as you resize, we are updating that view’s body.
And so another great place where you can kind of watch out for, you want to have as few views updating as possible during resize only when it’s actually really necessary. And otherwise keep it completely scoped just to the layout. Yeah. And this is another place to bring in the thing Jason mentioned of putting, putting a border or a background or transparent overlay that’s in color.
And then if you resize your scroll and it’s suddenly a disco dance party, like you’ve got a lot of validations going on. Yes. Yeah. Yeah. Also avoiding large view bodies as our scope of invalidation is the view body and everything that the view body is calling. If you can split that up, especially if the view body or something, you’re accessing the view body updates often, that also really helps.
So the next one eventually becomes a question about threads in isolation. So I’m gonna start with you Sima. So do at observable properties. And I add observable is a type really. So maybe it’s state properties holding out observables automatically get allocated on the right thread. Or is it recommended that we explicitly make these types at main actor if the UI is going to reference them? Are there some best practices on how to decide where the state should live?
Yeah. So as I mentioned before, SwiftUI is we use our admin actor. And this means that currently at runtime, we would call your bodies and your views on the main thread. And so that means that anytime you put an observable class declared in state, then it’s going to automatically get allocated in the main thread. Now with approachable concurrency settings and like I think for new apps, we’re now setting the isolation to main actor. So main actor is going to be inferred for everything, including your custom observable classes.
And I know before that setting, it used to be the case where like if you were to adopt Swift six, you would need to like manually annotate your observable classes with admin actor for them to be usable from your UI code. So now you don’t need to do that if you set the approachable concurrency setting and also the main isolation set to main actor. Both of these settings make it really easy to integrate observable classes with SwiftUI views.
Anybody have anything to add to that? Same as our concurrency. Great. We’re not going to we’re not going to contradict Sima’s expertise, that’s for sure. I would add that because your state can also be accessed on the main thread, you also usually want atomic updates not coming them like you update part of your observable, and that’s already rendered while you’re still running on the background thread. Is that I would factor out the background computation into a separate function and then await the result.
And once it’s done, jump back to the main thread and then update the observable. That’s a great point. And that also goes back to like the updates to state in your view, should be mostly synchronous so that SwiftUI can can produce better animations. And you don’t experience any hitches or like weirdness. And like when your app is like running. So yeah, I think it’s a really good point.
Yeah. Because the user experience level, it’s instead of seeing like, we’ve probably all seen different apps that have these like glitchy things where it’s like things appear like back to back to back really quickly versus waiting a tiny bit longer and having this much more continuous smooth experience. Yeah, yeah.
Yeah. It’s amazing to me how much of, of those things is actually it’s feel based and you need to test on device with the release build at least some of the time to really feel what those things are like, because that intuitive sense of quality that comes from something updating crisply is so important.
Oh, I also wanted to mention the timeline view. Like if you need something that like updates on every frame in your app, timeline view can be a great API to adopt because it would update on every frame. And I think it’s like an optimized way to deliver that kind of experience.
And that that is a great chance for me to, to direct folks towards Hawkins session this year at Dub Dub on advanced graphics and animation, which has the best explanation of shaders that I’ve personally ever seen, and the best explanation of alignment guides. But how Chen puts a shader inside a timeline view and gets this gorgeous flowing background on a sample podcast app. I could just watch that animation all day. It’s so good. My favorite SwiftUI feature is that you can integrate your own custom shaders. Like this is really rare in any other framework. Yeah. And then those shaders, can you hit one of my favorite things?
Those shaders can be used as foreground or background styles, which means you can use a shader to render the glyphs of text in your app. And then you can animate it and have the gradient flowing through the text. And it’s so fun. Then you throw in a text render and then they can move around. And the. Sizes can. Change. You can make some pretty amazing stuff. Yeah. So that’s why. Yeah. Now you get a sense of what we all do in our spare time.
All right. So this is this is another kind of architecture question, but it’s sort of a a design system kind of architecture. And and maybe, Jason, I’ll let you take a first crack at this one. So the question is in our Multi-module app. So an app that’s made up of distinct modules, we return any views, any view from protocols to hide the concrete view types and avoid cross-module dependencies.
Is there a more idiomatic way to do this without any view? For example, should we be using app Viewbuilder closures or any view existentials? Or what’s what’s the approach that we should be looking at here? Yeah. So definitely use some view if you can for your use case, because that will that will handle that better.
If that’s not possible, any view is fine as long as the underlying type isn’t changing. So just try to avoid dynamic changes to the type, which I think also falls in line with a lot of the advice that we’ve already heard about avoiding, you know, conditionals. Yeah. I’m curious if have you seen any patterns of this being done well or poorly when you’ve dug into different apps? Yeah. So one issue is that we do not know, don’t have static information about this time. Right. And so for each relies on that, for example, to get the static count of the view that you return from the body closure.
And so for any of you, because the type can actually change, we can never be sure about any static information we gathered from that. And there like a good thing to do is put it in a stack or zstack to then communicate to SwiftUI for each that it only ever returns a single view. And then we can tap into the better performance there. Yeah. And in our our APIs. The APIs in SwiftUI, we’re almost always returning some view. And you’ll see that referred to as an opaque type.
But the the type checker actually knows what type it is without having to expose that type to the people using using that. And so that’s a way to hide the type, but still give the type checker the information it needs and give our internal systems like clear knowledge that it’s a static type.
So instead of swap instead of Jason said, it’s totally true. Like if you keep the value that’s returned and the wrapped in any view consistent, well, that that can be performant. But, but if it’s a some view, then we know that you’re doing that. Like the compiler will catch you if you try to change it.
Lots and lots of great performance questions today. So I appreciate that. So Taylor, I think I’ll start this one with with you. What are the performance trade offs? Speed and memory of using a compositing group or a drawing group. This is some of the stuff that’s been there since since early. On. The very beginning. Yeah. Yeah. And that’s a good question because yeah, there’s lots of these something something group. So compositing group is not strictly about performance. It’s really about how you can apply visual effects.
I think one of the things that we were talking earlier about what surprises people is if you create this kind of structure of views with like a Z stack and things, and you apply a shadow to the overall thing. If you’re used to other, other, other UI frameworks, you might think, okay, well, I’m just applying a single shadow, but actually what it’s doing is it’s applying a shadow to each individual visual element, which is sometimes what you want, but sometimes you do just want to say, hey, I want to take the overall visual result and apply a shadow to that. So compositing group is great for that. It’s less about performance and more about really fine tuning the visual result you want versus drawing group.
I remember this was, I think, one of the very first this is the very first year that I was introduced. We had this great example where it was like this kind of pie chart esque visual, and it was showing like how far you could push SwiftUI to build these custom graphics, and you could get pretty far.
But at a certain point, it was just like there was just too many individual views being rendered. Not not SwiftUI views, but on screen like layer representations of the views. And so it drawing group does is it takes all of the, like kind of rendered layers that would have been drawn and puts them onto a single one.
And so then in that, in that example, you saw the performance suddenly shoot back up because even though there were the same exact number of SwiftUI views, updating it was rendering to a single, a single drawing layer. So that’s a great tool when you have that kind of setup. And the cool thing is like all of the same other APIs you’d expect from SwiftUI work.
So gestures work the same way, etc. so it’s like a great tool for these very rich interactive experiences that otherwise would have a lot of layers. I think it wasn’t mentioned in the question, but canvas is kind of similar in that way. Do you want to explain what canvas is? Oh yeah. Canvas is. If you have ever used UIKit or Appkit, there is a Drawrect method. It’s very similar to that, where you get more control over each individual.
Drawing invocation and can compose them in very interesting ways if you want. Instead of this way, you describe the view. You want more direct control and want to invoke the functions. Yeah. And I’ll take this opportunity to shout out David’s dub dub talk this year, where he uses canvas to build a very unique color picker. And I will call out that if you’re watching that talk, look at the code snippets, because the code for drawing that is, is in the code snippets of the talk, even though we didn’t share it in the slides.
Yeah. That’s cool. And I mean, the one trade off with canvas is you. Those are not SwiftUI views being drawn, and so you can’t attach some gestures to each individual piece. For example. Right there you will also see in the code that I have one drag gesture that applies to the overall slider.
And there are three sliders. And so I need to do some, some math to recognize where, where I’m starting. The drag from. Accessibility is kind of in a similar vein where you would need to use a proper thing, but that’s a good plug for accessibility representation. One of my favorite API’s because it’s so cool where you can say, hey, here’s my overall canvas, but actually it’s represented by three sliders, right? Like you don’t need to think about how that, you know, fake accessibility hierarchy looks like it is three sliders.
And I’m afraid despite having lots of questions that we didn’t get to. And so thank you for those that we’ve reached the end of this group lab. We do have another SwiftUI group lab at 7 p.m. Pacific time tomorrow evening, so we’d love to have you back for that.
We’re so thankful that you joined us today. A big thanks to our panelists and to all the folks working hard behind the scenes to make this happen today. If we didn’t get to your questions, come back tomorrow evening or visit the developer forums at developer.apple.com/forums. You can try out the new AI search [email protected] to get answers about frameworks, designs, accounts, and everything related to development on our platform.
And of course, as we mentioned, please go to feedback.apple.com to file any bugs or feature requests. Speaking of feedback, you’ll receive an email with a survey link to let us know about your experience with Wwdc. We’d love to incorporate your feedback into future events. Again, thanks for joining us. I hope you have a great w w DC.