@Developer Tools Engineer it will cause a crash even when Book is in the SwiftUI environment. Because the @Environment value got called multiple times, and some of that calls with a default value.
Here I tried your example in a simplified version and got a crash:
Then I added a default Book and it started to work:
This became especially noticeable when, in Xcode 16.2, the @Entry macro was changed, and it started generating code with a computed property for defaultValue instead of a stored property.
Used code:
import SwiftUI
struct Book {
// Interesting properties here...
let title: String
static func createBook(title: String) async -> Book {
// Await the content here...
// Initialize the Book with actual content...
return Book(title: title)
}
}
extension EnvironmentValues {
@Entry var previewBook: Book = {
// let bookTitle = "default book to avoid crashes in preview"
// print(bookTitle)
// return Book(title: bookTitle)
// This is the default value and will only be used if you forget to set your preview modifier
// trait.
fatalError("No value set for previewBook in the environment, did you forget your preview modifier trait?")
}()
}
extension View {
func previewBook(_ book: Book) -> some View {
environment(\.previewBook, book)
}
}
struct BookContent: PreviewModifier {
static func makeSharedContext() async throws -> Book {
// This async throws function is called to create the context for the preview modifer
// that can then be applied to preview content.
return await Book.createBook(title: "Example")
}
// This is called to modify your preview content with the context created above whenever
// you apply your preview modifier to a #Preview.
func body(content: Content, context: Book) -> some View {
content.previewBook(context)
}
}
extension PreviewTrait where T == Preview.ViewTraits {
// Create a convenience for applying the above preview modifier to a preview
static var previewBook: Self {
.modifier(BookContent())
}
}
struct BookView: View {
let book: Book
var body: some View {
VStack {
Text(book.title)
.padding()
}
}
}
#Preview(traits: .previewBook) {
// @Previewable allows us to use normal SwiftUI attributes like @Environment, @State,
// and @Binding in a preview body
@Previewable @Environment(\.previewBook) var book
BookView(book: book)
}
Post
Replies
Boosts
Views
Activity
@Developer Tools Engineer , you are giving a bad example because this will lead to a crash:
extension EnvironmentValues {
@Entry var previewBook: Book = {
// This is the default value and will only be used if you forget to set your preview modifier
// trait.
fatalError("No value set for previewBook in the environment, did you forget your preview modifier trait?")
}()
}
@testinstadev , this is due to the poor design of SwiftUI and constant unnecessary reinitializations of Views. And also Observation.
Try to add @ObservationIgnored to playerItem and cancellable:
@ObservationIgnored
var playerItem:AVPlayerItem? { ...
@ObservationIgnored
private var cancellable = Set<AnyCancellable>()
@DTS Engineer , but the question was:
Does LazyVStack and LazyVGrid release views from memory inside a ScrollView?
in iOS 18
And not about loading views incrementally.
With such a big change, why not make it a separate component? Or at least add a parameter to existing? For example, so it looks like this:
ReusableVStack { ... }
// or
LazyVStack(reuseViews: true) { ... }
This is a long-standing bug.
When the cornerRadius is greater than ~65.5% of min(height, width) it seems to be drawn as half of height or width.
You can write your own function to create a path with rounded corners using addArc for circular or addQuadCurve for continuous rounding.
Well, if Xcode doesn't show it, there are other ways. And indeed, this @__EntryDefaultValue macro makes the variable computed. So in Xcode 16.2, the behavior of @Entry is different from Xcode 16.1.
Considering the number of bugs in SwiftUI, changing the existing behavior and adding such ambiguity is not a good solution.
FB16430342
You can use let _ syntax. Example:
var body: some View {
let _ = assert(foo.valid)
......
}
You can use let _ = to make this work, example:
var body: some View {
VStack {
Text("Hello")
let _ = Self.logger.trace("hello")
}
}
For ObservableObject you can use
objectWillChange.send()
Try RawRepresentable. Example:
struct TypedID: Codable, RawRepresentable {
let rawValue: String
init(rawValue: String) {
self.rawValue = rawValue
}
init(_ string: String) {
self.init(rawValue: string)
}
}
Result:
iOS 18 adds a new variable for this: https://developer.apple.com/documentation/uikit/uitabbarcontroller/4434591-istabbarhidden
You can override the horizontal size class to .compact. The solution is so-so, but I haven't found a better one yet.
Example:
TabView {
...
}
.environment(\.horizontalSizeClass, .compact)
Is that the case? I've never heard that before 🤔Do you have a link where this is stated/explained? That would make me rethink some of the architectural decisions in my app. I assumed the entire purpose of that modifier is to place it anywhere in a NavigationStack, even on sub-pages.
I often encountered similar statements when searching for solutions to problems with NavigationStack. For example, here is the first link that I found now with the answer from Apple Engineer: https://forums.developer.apple.com/forums/thread/727307?answerId=749141022#749141022
Quote from the answer:
move navigationDestination modifiers as high up in the view hierarchy. This is more efficient for the Navigation system to read up front than with potentially every view update.
The problem with moving everything to ContentView is that you lose all modularity. ContentView would be tightly coupled with MiddleView, which is what I really want to avoid.
After workarounding a number of problems with NavigationStack, I came to the following solution for myself, maybe it may be useful:
do not use NavigationPath, only an array (depending on the OS version, there were different problems).
place navigationDestination on the root view of NavigationStack.
use a synchronized @State variable for the path (yes, as you said, would not like to use this, but the absence of unnecessary re-initializations / body calls calms me down)
To solve the problem with modularization, you can use ViewBuilder or View for different parts of the path.
Simplified example:
import SwiftUI
enum Destination: Hashable {
case flow1(Flow1Destination)
case flow2(Flow2Destination)
}
struct ContentView: View {
@State var path: [Destination] = []
var body: some View {
NavigationStack(path: $path) {
RootView(path: $path)
.navigationDestination(for: Destination.self) { destination in
switch destination {
case let .flow1(destination):
Flow1FactoryView(destination: destination, path: $path, getDestination: { .flow1($0) })
case let .flow2(destination):
Flow2FactoryView(destination: destination, path: $path, getDestination: { .flow2($0) })
}
}
}
}
}
struct RootView: View {
@Binding var path: [Destination]
var body: some View {
VStack {
Button("Flow1") {
path.append(.flow1(.details))
}
Button("Flow2") {
path.append(.flow2(.login))
}
}
.navigationTitle("Root")
}
}
enum Flow1Destination: Hashable {
case details
case more
}
struct Flow1FactoryView<Destination: Hashable>: View {
let destination: Flow1Destination
@Binding var path: [Destination]
let getDestination: (Flow1Destination) -> Destination
var body: some View {
switch destination {
case .details: DetailsView(onShowMore: { path.append(getDestination(.more)) })
case .more: MoreView()
}
}
}
struct DetailsView: View {
let onShowMore: () -> Void
var body: some View {
Button("Show more", action: onShowMore)
}
}
struct MoreView: View {
var body: some View {
Text("No more")
}
}
enum Flow2Destination: Hashable {
case login
case forgot
}
struct Flow2FactoryView<Destination: Hashable>: View {
let destination: Flow2Destination
@Binding var path: [Destination]
let getDestination: (Flow2Destination) -> Destination
var body: some View {
switch destination {
case .login: LoginView(onForgotPassword: { path.append(getDestination(.forgot)) })
case .forgot: ForgotView(onClose: { path.removeLast() })
}
}
}
struct LoginView: View {
let onForgotPassword: () -> Void
var body: some View {
VStack {
Text("Login")
Button("Forgot?", action: onForgotPassword)
}
}
}
struct ForgotView: View {
let onClose: () -> Void
var body: some View {
Button("Forgot", action: onClose)
}
}
#Preview {
ContentView()
}
When using ObservableObject for such purposes, when passing a navigation path from it to NavigationStack, there is such a problem that there are many unnecessary calls to navigationDestination and unnecessary reinitialization of View from the entire path
https://feedbackassistant.apple.com/feedback/14536210
Additionally, due to the presence of EnvironmentObject, most likely some operations occur that lead to a loop of this process.
Also, it is better to move navigationDestination as close to NavigationStack as possible, i.e. it is better to move it from MiddleView to ContentView. An example according to your code:
// This is literally empty
@MainActor final class SomeEnvironmentObject: ObservableObject {}
@MainActor
final class Router: ObservableObject {
@Published var path: NavigationPath = .init()
}
struct ContentView: View {
@StateObject var router = Router()
@State private var someEnvironmentObject = SomeEnvironmentObject()
var body: some View {
NavigationStack(path: $router.path) {
Button("Show Middle View") {
router.path.append(0)
}
.navigationDestination(for: Int.self) { destination in
MiddleView(path: $router.path)
}
.navigationDestination(for: String.self) { destination in
InnerView(path: $router.path)
}
}
.environmentObject(someEnvironmentObject)
}
}
struct MiddleView: View {
@EnvironmentObject var someEnvironmentObject: SomeEnvironmentObject
@Binding var path: NavigationPath
var body: some View {
Button("Show Inner View \(someEnvironmentObject.self)") {
path.append("0")
}
}
}
struct InnerView: View {
@Binding var path: NavigationPath
var body: some View {
Text("Inner View")
}
}
But if you want to leave it the same, you can use the @State variable for the navigation path, which is synchronized with the path from the Navigator, this will also fix the problem for this particular case, and will reduce the number of unnecessary navigationDestination calls and reinitializations to zero (not on all OS versions), example:
@main
struct ExampleApp: App {
@State var router = Router()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(router)
}
}
}
// This is literally empty
@MainActor final class SomeEnvironmentObject: ObservableObject {}
@MainActor
final class Router: ObservableObject {
@Published var path: NavigationPath = .init()
}
struct ContentView: View {
@State private var someEnvironmentObject = SomeEnvironmentObject()
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button("Show Middle View") {
path.append(0)
}
.navigationDestination(for: Int.self) { destination in
MiddleView(path: $path)
}
}
.environmentObject(someEnvironmentObject)
.synchronize(path: $path)
}
}
extension View {
// Or pass Binding navigation path from Navigator in a parameter
func synchronize(path: Binding<NavigationPath>) -> some View {
modifier(NavigationPathSynchronizationModifier(path: path))
}
}
struct NavigationPathSynchronizationModifier: ViewModifier {
@EnvironmentObject var router: Router // Or pass navigator path directly as a @Binding
@Binding var path: NavigationPath
func body(content: Content) -> some View {
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) {
content
.onChange(of: path) { _, newValue in
guard router.path != newValue else { return }
router.path = newValue
}
.onChange(of: router.path) { _, newValue in
guard path != newValue else { return }
path = newValue
}
} else {
content
.onChange(of: path) { newValue in
guard router.path != newValue else { return }
router.path = newValue
}
.onChange(of: router.path) { newValue in
guard path != newValue else { return }
path = newValue
}
}
}
}
struct MiddleView: View {
@EnvironmentObject var someEnvironmentObject: SomeEnvironmentObject
@Binding var path: NavigationPath
var body: some View {
Button("Show Inner View \(someEnvironmentObject.self)") {
path.append("0")
}
.navigationDestination(for: String.self) { destination in
InnerView(path: $path)
}
}
}
struct InnerView: View {
@Binding var path: NavigationPath
var body: some View {
Text("Inner View")
}
}