7

Update: This was a red herring

So my original question asked how to make a navigation link disabled and only enabled if two Toggle affecting two @State var isXYZToggleOn Bool properties are both true. This has always been working, my first attempt using .disabled(!(hasAgreedToTermsAndConditions && hasAgreedToPrivacyPolicy)) was the correct one (also suggested by @superpuccio, but using two negatations and one boolean or (||)).

Turns I did not get my NavigationLink to be enabled because the toggling did not work, not because of incorrect usage of the booleans and disabled view modifier.

Running on device, instead of simulator made everything work! But I still see the warning message as soon as I press any Toggle (but only once):

invalid mode 'kCFRunLoopCommonModes' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution.

I get this error message running on simulator too, but then the NavigationLink does not get enabled ever.

I'm running Xcode 5 beta on Catalina 5 beta on a 2016 Macbook Pro. Deleting derived data, restarting Xcode, restarting my computer, resetting the simulator, changing simulator, nothing helps. I still see invalid mode 'kCFRunLoopCommonModes' when I press the first Toggle, and the NavigationLink never gets enabled.

So the new question is:

Running on simulator: how to solve invalid mode 'kCFRunLoopCommonModes' problem causing my @State bound to Toggle to never become true??

Original question

Using XCode 11 beta 5 and SwiftUI. In a WelcomeScene I have two Toggle views, one toggle for accepting Terms & Conditions and one toggle for Privacy Policy. These toggles updates two separate @State properties respectively.

In the bottom of the scene I have a NagivationLink (button) which will take to the next scene, which I would like to be disabled by default and only be enabled when both hasAgreedToTermsAndConditions and hasAgreedToPrivacyPolicy states are true.

When initiating a NavigationLink there is a isActive argument, which takes a Binding<Bool>, which sounds like the correct thing. However, @Binding properties cannot be marked lazy, thus I cannot make it a computed property being dependent on hasAgreedToTermsAndConditions and hasAgreedToPrivacyPolicy.

There is also a disabled view modifier which takes a Bool, which is also incorrect since it does not get updated...

struct WelcomeScene: View {

    @State var hasAgreedToTermsAndConditions: Bool = false
    @State var hasAgreedToPrivacyPolicy: Bool = false

    var body: some View {
        VStack {
            Image(named: "MyImage")

            Spacer()

            Text("Welcome friend!".uppercased())
                .font(.system(size: 55))

            Toggle(isOn: $hasAgreedToTermsAndConditions) {
                Text("I agree to the Terms and Conditions")
            }.toggleStyle(DefaultToggleStyle())

            Toggle(isOn: $hasAgreedToPrivacyPolicy) {
                Text("I agree to the Privacy Policy")
            }.toggleStyle(DefaultToggleStyle())

            // This does not compile 'Binding<Bool> is not convertible to Bool', but I cannot figure out how to create a compupted property binding using those 2 states...
            NavigationLink("Proceed", destination: SignInScene(), isActive: ($hasAgreedToTermsAndConditions && $hasAgreedToPrivacyPolicy))
        }.padding(30)
    }
}
How can I make a NavigationLink disabled by default and only enabled when both toggles are on?
Sajjon
  • 8,938
  • 5
  • 60
  • 94
  • 2
    Also, when running in iOS simulator, when pressing any toggle I get: `invalid mode 'kCFRunLoopCommonModes' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution.` – Sajjon Aug 19 '19 at 14:47
  • https://stackoverflow.com/q/56980875/1311272 – Sajjon Aug 19 '19 at 14:53

2 Answers2

0

I did not find any way to get a publisher from a state so it was necessary to move things into an ObservableObject. From @Published items one can access it's publisher via $hasAgreedToPrivacyPolicy, which one can use in things like Publishers.CombineLatest to combine two publisher values and based on that set other variables.

Two Navigation links are necessary because once one was active, making it inactive does not hide what is displayed in the details view. The second is basically there to show content while the terms are not accepted yet.

Note: What you show here is a split view (using NavigationLink embedded in NavigationView). You can also use .sheet (which gets controlled by isActive) or a conditional to switch between the views.

import SwiftUI
import Combine

class Settings: ObservableObject {
    @Published var hasAgreedToTermsAndConditions: Bool = false
    @Published var hasAgreedToPrivacyPolicy: Bool = false

    @Published var isActive = false
    @Published var isNotActive = true

    private var cancellable: AnyCancellable? = nil

    init() {
        self.cancellable = Publishers.CombineLatest($hasAgreedToPrivacyPolicy, $hasAgreedToTermsAndConditions).map{
            return $0.0 && $0.1
        }.sink{
            self.isActive = $0
            self.isNotActive = !$0
        }
    }
}

struct WelcomeSceneView: View {
    @ObservedObject var settings = Settings()

    var body: some View {
        NavigationView {
            VStack {
                Image(uiImage: welcomeLogo)

                Spacer()

                Text("Welcome friend!".uppercased())
                //.font(.system(size: 55))

                Toggle(isOn: $settings.hasAgreedToTermsAndConditions) {
                    Text("I agree to the Terms and Conditions")
                }.toggleStyle(DefaultToggleStyle())

                Toggle(isOn: $settings.hasAgreedToPrivacyPolicy) {
                    Text("I agree to the Privacy Policy")
                }.toggleStyle(DefaultToggleStyle())


                // This does not compile 'Binding<Bool> is not convertible to Bool', but I cannot figure out how to create a compupted property binding using those 2 states...
                Text("is active: \(settings.isActive.description)")
                NavigationLink("Can proceed", destination: SignInScene(), isActive: $settings.isActive)
                .hidden()
                NavigationLink("Can not proceed", destination: PleaseConfirmView(), isActive: $settings.isNotActive)
                .hidden()
            }.padding(30)
        }
    }

    let welcomeLogo = UIImage(systemName: "headphones")!
}

struct SignInScene: View {
    var body: some View {
        Text("Some sign in scene")
    }
}

struct PleaseConfirmView: View {
    var body: some View {
        Text("Please confirm")
    }
}
Fabian
  • 5,040
  • 2
  • 23
  • 35
  • Thanks! But hmm sounds crazy that we HAVE to use two navigation links..? I just want the equivalence of ONE single `UIButton`, being disabled, until BOTH Toggles are `on`. – Sajjon Aug 19 '19 at 15:45
  • Problem is that turning isActive to off does not hide a priorly-shown details view, so a replacement must go active to hide it. You can use a conditional to show a different view once both got enabled. – Fabian Aug 19 '19 at 15:54
0

The main issue here is that you are misunderstanding the meaning of the NavigationLink init you are using.

/// Creates an instance that presents `destination` when active, with a
/// `Text` label generated from a title string.
public init(_ titleKey: LocalizedStringKey, destination: Destination, isActive: Binding<Bool>)

That isActive doesn't mean that the link is clickable only when that bool is true. That means that you are creating a NavigationLink that triggers the navigation both on click and when the isActive binding becomes true.

You can achieve what you want with the disabled modifier this way:

struct WelcomeScene: View {
    @State var hasAgreedToTermsAndConditions: Bool = false
    @State var hasAgreedToPrivacyPolicy: Bool = false

    var body: some View {
        NavigationView {
            VStack {

                Spacer()

                Toggle(isOn: $hasAgreedToTermsAndConditions) {
                    Text("I agree to the Terms and Conditions")
                }.toggleStyle(DefaultToggleStyle())

                Toggle(isOn: $hasAgreedToPrivacyPolicy) {
                    Text("I agree to the Privacy Policy")
                }.toggleStyle(DefaultToggleStyle())

                NavigationLink("Proceed", destination: SignInScene())
                    .disabled(!hasAgreedToTermsAndConditions || !hasAgreedToPrivacyPolicy)
            }.padding(30)
        }
    }
}
superpuccio
  • 11,674
  • 8
  • 65
  • 93
  • Will the `enabled`/`disabled` state of the `NavigationLink` get updated when any of the Toggles `isOn` changes? – Sajjon Aug 19 '19 at 15:46
  • @Sajjon Yes, sure. You can try yourself copy-pasting the code in my answer. When one of the toggles changes the view gets recreated since one of its State property has changed. And then the disabled bool is recomputed. – superpuccio Aug 19 '19 at 15:48
  • Unfortunatly I'm unable to try the solution out :(, every time I toggle the switch I see this following text message in the console: `invalid mode 'kCFRunLoopCommonModes' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution.` – Sajjon Aug 19 '19 at 16:05
  • @Sajjon If you copy-paste my code and you get that error it's really strange (I can run it without any issue). It must be a bug related to xCode Beta. Sometimes I can get rid of its silly bugs closing xCode, deleting the DerivedData folder (xCode->Preferences->Locations) and open xCode again. – superpuccio Aug 19 '19 at 16:11
  • @Sajjon You can even try the solution without running on the simulator, but using the Live Preview (this way you shouldn't see the error you see on the simulator). – superpuccio Aug 19 '19 at 16:15
  • deleting derived data was the first thing I tried of course :). I have restarted Xcode, my computer, I have deleted the simulator... same thing every time. In a comment above I referenced to this SO Q, might be the same issue: https://stackoverflow.com/q/56980875/1311272 – Sajjon Aug 19 '19 at 16:15
  • @Sajjon did you try with the Live Preview? (without actually using the simulator) – superpuccio Aug 19 '19 at 16:17
  • Live preview does not update the NavigationLink either. Not even if using a Single Toggle (reducing complexity of the view, seems like `State` is broken. I've tried with `EnvironmentObject` (user data) and `Publisher`, does not work either, same error being logged – Sajjon Aug 19 '19 at 16:19
  • The message appears here too, but it doesn't crash anything and it doesn't stop the Toggle from working. So go try out time :) – Fabian Aug 19 '19 at 16:19
  • I also see error `[WindowServer] display_timer_callback: unexpected state (now:17cce5d70d7 < expected:17ccf558677)` every time I press a toggle. But hmm seems like my `@State` properties does not work at all. Something is completely broken. bah! I might reinstall Xcode 11 (beta 5). – Sajjon Aug 19 '19 at 16:43
  • @Sajjon are you on Catalina Beta 5? Cause other versions of Catalina don’t support SwiftUI Beta 5. – superpuccio Aug 19 '19 at 16:48
  • @superpuccio yes Catalina beta 5 (19A526h) – Sajjon Aug 19 '19 at 16:49
  • that second message show up for everyone too. But that should not influence any @State variables. It is just about a timer callback in the background somewhere.. – Fabian Aug 19 '19 at 17:38
  • @superpuccio it was a red herring, I updated the question, I actually used `disabled` prior to posting my question, so my code was correct all the time, just because of `invalid mode 'kCFRunLoopCommonModes'` error, the `Toggle`s never worked so I could not try it. Just tried on device, works fine! – Sajjon Aug 20 '19 at 06:45