Learn why SwiftUI views repeatedly refresh and cause infinite loops when using environment size classes with observable players, plus best practices to resolve this issue.
---
This video is based on the question https://stackoverflow.com/q/79469769/ asked by the user 'Deepak Sharma' ( https://stackoverflow.com/u/917521/ ) and on the answer https://stackoverflow.com/a/79471291/ provided by the user 'Sweeper' ( https://stackoverflow.com/u/5133585/ ) at 'Stack Overflow' website. Thanks to these great users and Stackexchange community for their contributions.
Visit these links for original content and any more details, such as alternate solutions, latest updates/developments on topic, comments, revision history etc. For example, the original title of the Question was: SwiftUI infinite loop issue with @ Environment size vars
Also, Content (except music) licensed under CC BY-SA https://meta.stackexchange.com/help/l...
The original Question post is licensed under the 'CC BY-SA 4.0' ( https://creativecommons.org/licenses/... ) license, and the original Answer post is licensed under the 'CC BY-SA 4.0' ( https://creativecommons.org/licenses/... ) license.
If anything seems off to you, please feel free to drop me a comment under this video.
---
Understanding the Problem: Infinite Loop in SwiftUI with Environment Size Classes
Developers working with SwiftUI often observe their view's body being called repeatedly in an infinite loop. This occurs particularly when using environment variables like horizontalSizeClass or verticalSizeClass combined with an observable ViewModel that manages an AVPlayer.
This issue typically appears after device rotations (portrait to landscape and back) and involves unexpected repeated initialization and deinitialization of the player manager.
Root Causes
Several interrelated factors cause this infinite loop:
@ State with @ Observable ViewModel Instantiation: Every time the parent view’s initializer runs (triggered by environment changes on rotation), a new instance of the ViewModel (e.g., TestPlayerVM) is created.
SwiftUI Instance Leak: Occasionally, SwiftUI erroneously retains one of these ViewModel instances instead of deinitializing it normally.
@ Observable Tracking of Combine Cancellables: The cancellable set used to store Combine subscriptions is observed by SwiftUI because the ViewModel is marked @ Observable.
Modification of cancellable in init and deinit: Adding and removing subscriptions triggers SwiftUI state updates during ViewModel initialization and deinitialization.
Together, these cause SwiftUI to detect dependency changes on its environment and state, triggering repeated view updates and creating a loop.
How the Loop Unfolds
Initial View Setup: PlayerView creates the first TestPlayerVM instance.
Device Rotation: Changes to @ Environment size classes prompt SwiftUI to rebuild views, calling PlayerView.init again.
Instance Leak: SwiftUI retains this new ViewModel instance unexpectedly (a known bug), failing to deinitialize it.
Further Rotations: Subsequent rotations lead to correct deinitialization of new instances, which modify the cancellable set during deinit.
Observed cancellable Changes: Because cancellable is part of the observable state, SwiftUI reacts to these changes by re-invoking view body calculations.
Cycle Repeats: Each update triggers new initialization, modifications to cancellable, and so on, causing a continuous loop.
Practical Solutions to Break the Cycle
To avoid this infinite loop, consider these proven strategies:
Delay ViewModel Initialization: Make the ViewModel @ State optional and initialize it inside a .task modifier rather than during view init. This prevents premature creation tied to environment changes.
Ignore Observation on Internal Properties: Annotate cancellable with @ ObservationIgnored to exclude it from SwiftUI's dependency tracking.
Move Observer Setup Out of Init: Instead of setting up Combine subscriptions inside the ViewModel initializer, use onAppear in the SwiftUI view to control this lifecycle event.
Avoid Explicitly Clearing Cancellables: Rather than calling cancellable.removeAll() in deinit, rely on ARC to automatically clean up, preventing redundant state mutations during deinitialization.
Summary
SwiftUI's reactive framework can unintentionally detect and overreact to changes in observable properties managed inside your ViewModels, especially combined with environment size classes and view lifecycle during rotations. Understanding the interaction between @ State, @ Observable, and Combine subscriptions is essential.
By shifting initialization timing and controlling what SwiftUI observes, you can stop unexpected infinite view update loops and ensure smooth user experiences across device orientations.
References
SwiftUI @ Environment and size classes behavior
Managing Combine subscriptions in ViewModels
@ ObservationIgnored usage