Listen for Environment Changes

Viewed 2192

How do I detect environment changes in SwiftUI? For example, I store the current colour scheme as

@Environment(\.colorScheme) var colorScheme: ColorScheme

And I display an ASAuthorizationAppleIDButton based on the environment value:

fileprivate struct AppleSignInView : UIViewRepresentable {

    var colorScheme: ColorScheme

    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        switch colorScheme {
        case .light:
            return ASAuthorizationAppleIDButton(type: .continue, style: .black)

        case .dark:
            return ASAuthorizationAppleIDButton(type: .continue, style: .white)

        @unknown default:
            return ASAuthorizationAppleIDButton(type: .continue, style: .black)
        }
    }

    func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) { }
}

And In my body property I instantiate the struct:

var body: some View {
    AppleSignInView(colorScheme: colorScheme)
}

This works fine, but the new colour scheme isn't propagated when I change the colour scheme from the Xcode.

How do I listen to this environment variable change?

2 Answers

Here is how I do it: the trick is to add the .id(self.colorScheme) line. This forces SwiftUI to redraw this button every time the colorScheme changes.

SignInWithAppleButton(style: self.colorScheme == .light ? .black : .white)
  .frame(width: 280, height: 60)
  .onTapGesture(perform: self.showAppleLogin)
  .id(self.colorScheme)

This saves you from returning 2 versions of the button in an if/else statement, like in kontiki's answer.

And my button, for good measure:

struct SignInWithAppleButton: UIViewRepresentable {
  var style: ASAuthorizationAppleIDButton.Style

  func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
    return ASAuthorizationAppleIDButton(type: .default, style: style)
  }

  func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {}
}

I recently answer a similar question, the OP was looking for a way of having custom fonts reacting to environment changes of the .sizeCategory.

Have a look at: https://stackoverflow.com/a/56730649/7786555


UPDATE

Here's an implementation that works. Notice that AppleSignInView is put inside an if statement. This is to force SwiftUI recreate your UIViewRepresantable. Ideally you would just pass the colorScheme, but in that case you need to change the ASAuthorizationAppleIDButton.style in updateUIView(). However, .style is not modifiable. To work around it, you use the if statement to force a recreation of the SignIn button.

import SwiftUI
import AuthenticationServices

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme: ColorScheme

    var body: some View {

        Group {
            if colorScheme == .light {
                AppleSignInView(colorScheme: .light).frame(width: 200, height: 50)
            } else {
                AppleSignInView(colorScheme: .dark).frame(width: 200, height: 50)
            }
        }
    }
}

fileprivate struct AppleSignInView : UIViewRepresentable {
    var colorScheme: ColorScheme

    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
        switch colorScheme {
        case .light:
            return ASAuthorizationAppleIDButton(type: .continue, style: .black)

        case .dark:
            return ASAuthorizationAppleIDButton(type: .continue, style: .white)

        @unknown default:
            return ASAuthorizationAppleIDButton(type: .continue, style: .black)
        }
    }

    func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
    }
}
Related