1

The following code works perfectly fine on iOS, but not on iPadOS. When I tap on one of the items in the list, the corresponding detail view is shown, but it will not change if I tap on another item. When I change the model in the LanguageDetail view to @ObservedObject, it works. To be clear, this is only an example to illustrate the problem. In my actual project, I'm not able to make this change though. The code below demonstrates this problem.

struct ContentView: View {
    let languages: [String] = ["Objective-C", "Java", "Python", "Swift", "Rust"]
    
    @State var selectedLanguage: String?
    
    var body: some View {
        NavigationView {
            List(languages, id: \.self) { language in
                Button(action: {selectedLanguage = language}) {
                    Text(language)
                        .bold()
                        .padding()
                }
            }
            .background {
                NavigationLink(isActive: $selectedLanguage.isPresent()) {
                    if let lang = selectedLanguage {
                        LanguageDetail(model: LanguageDetailModel(languageName: lang))
                    } else {
                        EmptyView()
                    }
                } label: {
                    EmptyView()
                }
            }
        }
    }
}
struct LanguageDetail: View {
    @StateObject var model: LanguageDetailModel
    var body: some View {
        VStack {
            Text(model.languageName)
                .font(.headline)
            Text("to rule them all...")
        }
    }
}

class LanguageDetailModel: ObservableObject {
    @Published var languageName: String
    
    init(languageName: String) {
        self.languageName = languageName
    }
}

This extension is needed:

/// This extension is from the [SwiftUI Navigation Project on Github](https://github.com/pointfreeco/swiftui-navigation)
extension Binding {
    /// Creates a binding by projecting the current optional value to a boolean describing if it's
    /// non-`nil`.
    ///
    /// Writing `false` to the binding will `nil` out the base value. Writing `true` does nothing.
    ///
    /// - Returns: A binding to a boolean. Returns `true` if non-`nil`, otherwise `false`.
    public func isPresent<Wrapped>() -> Binding<Bool>
    where Value == Wrapped? {
        .init(
            get: { self.wrappedValue != nil },
            set: { isPresent, transaction in
                if !isPresent {
                    self.transaction(transaction).wrappedValue = nil
                }
            }
        )
    }
}
stsandro
  • 343
  • 1
  • 2
  • 10
  • you could consider using `NavigationStack` instead of the deprecated `NavigationView`, it seems to be easier if you plan one day to use ios-16 – workingdog support Ukraine Nov 16 '22 at 02:36
  • That is a good point, but right now that is not possible. – stsandro Nov 16 '22 at 11:42
  • I've dug a little bit deeper into `@StateObject` and `@ObservedObject`. The root problem seems to be there. This only works on iOS, because the `LanguageDatail` view and its corresponding model are destroyed when navigating back. On iPadOS, the view is not destroyed and the `@StateObject` is not changed. – stsandro Nov 16 '22 at 12:52

1 Answers1

0
import SwiftUI

enum Language: Int, Identifiable, CaseIterable {

    case objc
    case java
    case python
    case swift
    case rust
    
    var id: Self { self }
    
    var localizedDescription: LocalizedStringKey {
        switch self {
            case .objc: return "Objective-C"
            case .java: return "Java"
            case .python: return "Python"
            case .swift: return "Swift"
            case .rust: return "Rust"
        }
    }
}

struct LanguagesTest: View {

    @State private var selection: Language?
    
    var body: some View {
        NavigationSplitView {
            List(Language.allCases, selection: $selection) { language in
                NavigationLink(value: language) {
                    Text(language.localizedDescription)
                }
            }
        } detail: {
            if let language = selection {
                LanguageDetail(language: language)
            }
            else {
                Text("Select Language")
            }
        }
    }
}


struct LanguageDetail: View {

    let language: Language

    var body: some View {
        VStack {
            Text(language.localizedDescription)
                .font(.headline)
            Text("to rule them all...")
        }
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133
  • There are two problems here. First, I can't use `NavigationSplitView` as it is iOS 16+. Second, this only works, because there is no `@StateObject var model: LanguageDetailModel` in the `LanguageDetail` view. I've tested this right now. – stsandro Nov 16 '22 at 12:47
  • `@StateObject` is for when you want `@State` but need a reference type so you can persist/sync data, also the object init must be on the same line as the property wrapper. I think you should add more info on what you want to achieve in the detail view because `@StateObject` isn't right. – malhal Nov 16 '22 at 12:54
  • Yes, I'm investigating this right now. I think I will have to use `@ObservedObject`, but that would mean that I need to change the model quite a bit. My code definitely works, when I use an `@ObservedObject` instead of a `@StateObject` – stsandro Nov 16 '22 at 13:00
  • Try using `@Binding` with a model struct instead of an object, that works better cause its how SwiftUI was designed to work. – malhal Nov 16 '22 at 14:07
  • I'll consider this. Thanks for the tip – stsandro Nov 16 '22 at 14:21