Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SwiftUI Performance for Demanding Apps

SwiftUI Performance for Demanding Apps

Talk given at SwiftLeeds, UK, 2023

SwiftUI is powerful and flexible, but sometimes confusing. Things like modifiers order, inline views, `body` complexity, and POD views, can all seriously affect our performance. In this talk, we will learn the best ways to use SwiftUI for resource-heavy and dynamic UIs, while maintaining the golden 60fps.

In 2022, we (Adobe Bēhance) rebuilt our navigation infra, and our main Feed, in SwiftUI. We also insisted the app must run great on the worst phone we support - iPhone 6S Plus. Getting there was a journey.

We will start by comparing SwiftUI to UIKit: We know there’s no more View Controller, and views are mere “function of their state”, but what does it mean?

Next, we will dive into specific scenarios and see how this new way of thinking is critical for achieving great performance. We will learn things like:
- Avoiding redundant view diffing.
- Controlling view update lifecycle.
- How to “hide” complex state to improve performance.
- Avoiding SwiftUI’s pitfalls, like nested publishers and environment memory leaks.
- And more…

Aviel Gross

October 10, 2023
Tweet

More Decks by Aviel Gross

Other Decks in Programming

Transcript

  1. Luc Bern rdi - D t Flow Through SwiftUI, WWDC19

    “Views, as a function of state” 3
  2. 4

  3. 8

  4. 10

  5. UIView Controller View " Delegate Delegate Delegate State State State

    View UIView " Delegate Delegate Delegate State State State Long lived Passing events 12
  6. UIKit UIKit UIViewController Coordinates everything Long lived References are kept

    (for performance, navigation, etc.) Views & VCs own their state Know their layout size Do event passing 13
  7. State State State View View View View View View View

    View View " No delegates No View Controller 15
  8. SwiftUI UIKit SwiftUI UIViewController Coordinates everything Doesn’t exist! Long lived

    Short lived views (recreated by the OS many times) References are kept (for performance, navigation, etc.) References make no sense (even if it wasn’t a struct!) Views & VCs own their state View does not own state (not even @State!) Know their size Control their size Do event passing No events, just state mutations (usually) 16
  9. 18

  10. 19

  11. 20

  12. 21

  13. The Declarative Approach • We change state, not views •

    ❌ someLabel.text = “Sent” • ✅ sent = true • Views subscribe to, or depend on, state • ✅ @Binding var sent: Bool 22
  14. 24

  15. Current View What’s On Screen equal? body body equal? Step

    A: Step B: *simpli ed, ignores some things (more on this later…) New State & Deps 26
  16. Current View What’s On Screen *simpli ed, and ignores some

    things (more a er the break…) equal? Step A: Yes: 27 %
  17. New State & Deps Current View What’s On Screen equal?

    body body equal? Step A: Step B: *simpli ed, and ignores some things (more a er the break…) No: 28
  18. Current View What’s On Screen *simpli ed, and ignores some

    things (more a er the break…) Yes: body body equal? Step B: 29 %
  19. New State & Deps *simpli ed, and ignores some things

    (more a er the break…) No: body body equal? Step B: Curr View Rendered On Screen 30 %
  20. /// Step A: Structure of State & Dependencies func stateAndDependencies()

    -> Any... /// Step B: Create Body func body(_ state: ...) -> some Thing 31 Imagine View is a function:
  21. • View Controllers usually have 2 kinds of logic: 1.

    Pass events / changes between views. 2. Handle / serve data (CRUD - Create, Read, Update, Delete) 34
  22. • View Controllers usually have 2 kinds of logic: 1.

    Pass events / changes between views. 2. Handle / serve data (CRUD - Create, Read, Update, Delete) SwiftUI #1 is redundant #2 shouldn’t ever lived in the VC, now forced to live somewhere else 35
  23. 1. Less Di ng 2. Fast Di ng 3. Expected

    Behavior Fast Di ng Less Di ng Behavior 39
  24. SomeView body Keep View body Simple Content Content Fast Di

    ng Avoid Prefer data SomeView body 40
  25. Keep View body Simple struct CupcakeView: View { @State var

    cupcakes: [Item] var body: some View { let sortedItems = complexSort(cupcakes) List(sortedItems) { ... } } Fast Di ng Avoid 41
  26. Keep View body Simple class CupcakesProvider: ObservableObject { @Published var

    cupcakes: [Item] func provideItemsSomehow() async { let items = await fetchItems() let sortedItems = complexSort(items) self.cupcakes = sortedItems } } Fast Di ng Prefer 42
  27. Keep View body Simple class CupcakesProvider: ObservableObject { @Published var

    cupcakes: [Item] func provideItemsSomehow() async { … } } struct CupcakeView: View { @StateObject var provider = CupcakesProvider() var body: some View { List(provider.cupcakes) { ... .task { provider.provideItemsSomehow() } } } Fast Di ng Prefer 43
  28. Keep View body Simple @Observable class CupcakesProvider { var cupcakes:

    [Item] func provideItemsSomehow() async { … } } struct CupcakeView: View { @State var provider = CupcakesProvider() var body: some View { List(provider.cupcakes) { ... .task { provider.provideItemsSomehow() } } } Fast Di ng Prefer 44
  29. HStack { if gotCupcake { CupcakeView() } // ... }

    HStack Optional // … Prefer “No Effect” Modi iers over Conditional Views CupcakeView Behavior Fast Di ng Avoid 45
  30. HStack { if gotCupcake { CupcakeView() } else { CupcakeView()

    .opacity(0.5) } // ... } HStack ConditionalContent // … CupcakeView Opacity Modi ier CupcakeView Prefer “No Effect” Modi iers over Conditional Views Behavior Fast Di ng Avoid 46
  31. HStack { CupcakeView() .opacity(gotCupcake ? 1 : 0) // ...

    } HStack Opacity Modi ier // … CupcakeView Prefer “No Effect” Modi iers over Conditional Views Behavior Fast Di ng Prefer 47
  32. HStack { CupcakeView() .opacity(gotCupcake ? 1 : 0) // ...

    } HStack Opacity Modi ier // … CupcakeView Prefer “No Effect” Modi iers over Conditional Views Behavior Fast Di ng WWDC21 Demystify Swi!Ul 48
  33. Behavior Fast Di ng “When you introduce a branch, pause

    for a second and consider whether you're representing multiple views or two states of the same view.” - Raj Ramamurthy / WWDC21 49
  34. Prefer “No Effect” Modi iers over Conditional Views Behavior Fast

    Di ng 50 List { ForEach(items) { item in if !item.isEmpty { Text(item) } } } Optional Text View Size Avoid Size 0-1
  35. Prefer “No Effect” Modi iers over Conditional Views Behavior Fast

    Di ng 51 List { ForEach(items) { item in Text(item) .opacity(item.isEmpty ? 0 : 1) } } View Size Opacity Modi ier Text Size 1 Note: Empty items will get a row too!
  36. Prefer “No Effect” Modi iers over Conditional Views Behavior Fast

    Di ng 52 List { ForEach(items.filter(\.isNotEmpty)) { item in Text(item) } } Text View Size Prefer Size 1
  37. Prefer “No Effect” Modi iers over Conditional Views Behavior Fast

    Di ng 53 List { ForEach(filteredItems) { item in Text(item) } } Text View Size Prefer Size 1
  38. Prefer “No Effect” Modi iers over Conditional Views Behavior Fast

    Di ng 54 List { ForEach(filteredItems) { item in Text(item) } } Text View Size WWDC23 Demystify Swi!Ul performance Rens Breur rensbr.eu/blog/swi!ui-diffing
  39. Fast Di ng Split State Into Custom View Types Less

    Di ng var body: some View { } struct BigView: View { @State private var @StateObject private var let Avoid 55
  40. Fast Di ng Split State Into Custom View Types Less

    Di ng Avoid 56 (lldb) po print(type(of: body)) TupleView<(ModifiedContent<HStack<TupleView<(_ConditionalContent<Text, Button<Text>>, Image)>>, _BackgroundStyleModifier<Color>>, VStack<TupleView<(Text, ModifiedContent<Text, _PaddingLayout>)>>, ModifiedContent<List<Never, Section<EmptyView, ForEach<ClosedRange<Int>, Int, ModifiedContent<Text, _OverlayModifier<_ShapeView<Rectangle, Color>>>>, EmptyView>>, _PaddingLayout>)>
  41. Fast Di ng Split State Into Custom View Types Less

    Di ng var body: some View { foo bar biz } var foo: some View { } var bar: some View { } var biz: some View { } Avoid 57
  42. struct BarView: View { } struct BizView: View { }

    struct FooView: View { } Fast Di ng Split State Into Custom View Types Less Di ng var body: some View { FooView( ) BarView( ) BizView( ) } Prefer 58
  43. Fast Di ng Split State Into Custom View Types Less

    Di ng 59 (lldb) po print(type(of: body)) TupleView<(FooView, BarView, BizView)> Prefer
  44. Fast Di ng Utilize POD* or Equality Less Di ng

    *Pl in Ol’ D t & Spicy section warning! Disclaimers: 1. None of this is part of the o cial SwiftUI API 2. Underscore functions should not be used in production 3. Any of this might change in the future 4. I am not a tax advisor and this is not tax advice 60
  45. Fast Di ng Utilize POD* or Equality Less Di ng

    Speed memcmp ' Equality (“==“) (if implemented…) ( Re lection ) *Pl in Ol’ D t 61
  46. Fast Di ng Utilize POD* or Equality Less Di ng

    POD Non-POD struct CupcakeCard: View { let imagePath: String let price: Int let id: String // ... struct CupcakesList: View { @State var data = CakeProvider() @Binding var user: UserInfo @Environment(\.network) var api // ... Check: _isPOD(CupcakesView.self) *Pl in Ol’ D t 62
  47. Non-POD View always uses re ection unless it’s Equatable )

    ( Fast Di ng Utilize POD* or Equality Less Di ng *Pl in Ol’ D t 63 POD View always uses memcmp unless wrapped in EquatableView ' (
  48. Fast Di ng Utilize POD* or Equality Less Di ng

    *Pl in Ol’ D t Is POD? Equatable? == Re lection Wrapped In EquatableView? memcmp Equatable? No No No No Yes Yes Yes Yes ( ) ' 64
  49. Fast Di ng Utilize POD or Equality Less Di ng

    As-is (not in EquatableView) inside EquatableView memcmp ' ✔ Equality (“==“) ( Ignored ✔ Re lection ) Never POD View POD View always uses memcmp unless wrapped in EquatableView 65
  50. Fast Di ng Utilize POD or Equality Less Di ng

    As-is (not in EquatableView) inside EquatableView memcmp ' Never Equality (“==“) ( ✔ ✔ (redundant) Re lection ) ✔ Non-POD View Non-POD View always uses re ection unless it’s Equatable 66
  51. Fast Di ng Utilize POD* or Equality Less Di ng

    Confused? So are we! Here’s another perspective: Equatable lets you avoid re ection EquatableView lets you force == *Pl in Ol’ D t 67
  52. Fast Di ng Utilize POD* or Equality Less Di ng

    *Pl in Ol’ D t struct ExpensiveView: View { let value: Int /// <- dependencies @State private var model = Model() /// <- state var body: some View { ... } } Non-POD 71
  53. Fast Di ng Utilize POD* or Equality Less Di ng

    *Pl in Ol’ D t struct ExpensiveView: View { let value: Int /// <- dependencies var body: some View { ExpensiveViewInternal(value: value) } } private struct ExpensiveViewInternal: View { let value: Int @State private var model = Model() /// <- state var body: some View { ... } } Non-POD POD 72
  54. 1. Keep View body Simple 2. “No Effect” Modi iers

    > Conditional Views 3. Split State Into Custom View Types 4. Utilize POD or Equality In Summary ✍ 73
  55. , Behavior • Code correctness: • Make the code do

    what you want it to do… • …or what you think it should be doing. 76
  56. - Structure • Code layout: • Where logic goes… •

    How to build the body… • Make your code: • Flexible to change • Easy to reason about • Easy to delete 77