Home > Software engineering >  Adding a view modifier inside .onChange
Adding a view modifier inside .onChange

Time:06-29

Is it possible to add a view modifier inside .onChange?

Simplified example:

content
  .onChange(of: publishedValue) {
    content.foregroundColor(.red)
  }

I have a theme that when changed needs to change the status bar color. I have a view modifier created for that ( https://barstool.engineering/set-the-ios-status-bar-style-in-swiftui-using-a-custom-view-modifier/ ). The modifier works fine, but I need to update it as the publishedValue changes.

Actual minimal example:

import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel: TestViewModel

    var body: some View {
        ZStack {
            Rectangle().foregroundColor(.mint)
            VStack(alignment: .center, spacing: 25) {
                Text("Test text \(viewModel.publishedValue)")
                    .onChange(of: viewModel.publishedValue) { newValue in
                        // Change status bar color
                        if viewModel.publishedValue % 2 == 0 {
                            self.body.statusBarStyle(.lightContent)
                        } else {
                            self.body.statusBarStyle(.darkContent)
                        }
                    }
                Button("Increment") {
                    viewModel.publishedValue  = 1
                }
            }
        }
        .ignoresSafeArea()
        .statusBarStyle(.lightContent)
    }
}

class TestViewModel: ObservableObject {
    @Published var publishedValue: Int

    init(publishedValue: Int) {
        self.publishedValue = publishedValue
    }
}

extension View {
    /// Overrides the default status bar style with the given `UIStatusBarStyle`.
    ///
    /// - Parameters:
    ///   - style: The `UIStatusBarStyle` to be used.
    func statusBarStyle(_ style: UIStatusBarStyle) -> some View {
        return self.background(HostingWindowFinder(callback: { window in
            guard let rootViewController = window?.rootViewController else { return }
            let hostingController = HostingViewController(rootViewController: rootViewController, style: style)
            window?.rootViewController = hostingController
        }))
    }

}

fileprivate class HostingViewController: UIViewController {
    private var rootViewController: UIViewController?
    private var style: UIStatusBarStyle = .default

    init(rootViewController: UIViewController, style: UIStatusBarStyle) {
        self.rootViewController = rootViewController
        self.style = style
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        guard let child = rootViewController else { return }
        addChild(child)
        view.addSubview(child.view)
        child.didMove(toParent: self)
    }

    override var preferredStatusBarStyle: UIStatusBarStyle {
        return style
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        setNeedsStatusBarAppearanceUpdate()
    }
}

fileprivate struct HostingWindowFinder: UIViewRepresentable {
    var callback: (UIWindow?) -> ()

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        DispatchQueue.main.async { [weak view] in
            self.callback(view?.window)
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        // ...
    }
}

GitHub repo for the example project: https://github.com/Iikeli/view-modifier-test

CodePudding user response:

You are way overcomplicating this. Since your viewModel is @ObservedObject and the publishedValue is Published, the body of your View will be recalculated automatically every time publishedValue is updated. There's no need for a manual onChange.

You can simply move the logic into the input argument of statusBarStyle.

var body: some View {
    ZStack {
        Rectangle().foregroundColor(.mint)
        VStack(alignment: .center, spacing: 25) {
            Text("Test text \(viewModel.publishedValue)")
            Button("Increment") {
                viewModel.publishedValue  = 1
            }
        }
    }
    .ignoresSafeArea()
    .statusBarStyle(viewModel.publishedValue % 2 == 0 ? .lightContent : .darkContent)
}

Or even better, move the logic into a separate computed property:

var body: some View {
    ....
    .statusBarStyle(statusBarStyle)
}

private var statusBarStyle: UIStatusBarStyle {
    viewModel.publishedValue % 2 == 0 ? .lightContent : .darkContent
}

CodePudding user response:

The short answer is no, but that doesn't mean you can't use it to have views change based on some .onChange(..) action. For example.

@State var somethingChanged = false 
Text(somethingChanged ? "First Value" : "Second Value")

// Your code/view
    .onChange(..) {
         //Some Condition or whatever you want.
         somethingChanged = true
    }

Your usage might look something like this.

content
    .foregroundColor(somethingChanged ? .red : .blue)
    .onChange(ofPublishedValue) {
        somethingChanged = true
    }
  • Related