在.onchange中添加视图修饰符

发布于 2025-02-11 07:13:36 字数 3845 浏览 2 评论 0原文

是否可以在内部添加视图修饰符.onchange

简化的示例:

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

我有一个主题,当更改需要更改状态栏颜色时。我有一个为此创建的视图修饰符( https://barstool.engineering/set-the--ios-status-bar-style-in-swiftui-in-swiftui-is-a-custom-view-modifier/ )。修饰符正常工作,但是我需要将其更新为已发布的Value更改。

实际最小示例:

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: https://github.com/iikeli/iikeli/iikeli/ikeli/view -Modifier-Test

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

挖鼻大婶 2025-02-18 07:13:37

简短的答案是否定的,但这并不意味着您不能使用它来根据某些.onchange(..)操作更改视图。例如。

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

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

您的用法可能看起来像这样。

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

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
    }
静谧 2025-02-18 07:13:37

首先,感谢您的帮助。在我的情况下,这两个答案都没有帮助,因为我无法通过可变更改进行更新。但是,通过一些谷歌搜索并尝试了不同的解决方案,我发现了一种工作解决方案,以更新状态栏颜色。

我需要在hostingViewController中更新样式变量,然后相应地进行更新。因此,我将hostingViewController添加为@stateObject,并在.onchange()中更新了样式变量。我要开始的解决方案并不是一开始,但确实有效。

代码:

import SwiftUI
import Introspect

struct ContentView: View {
    @ObservedObject var viewModel: TestViewModel
    @StateObject var hostingViewController: HostingViewController = .init(rootViewController: nil, style: .default)

    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 {
                            hostingViewController.style = .lightContent
                        } else {
                            hostingViewController.style = .darkContent
                        }

                    }
                Button("Increment") {
                    viewModel.publishedValue += 1
                }
            }
        }
        .ignoresSafeArea()
        .introspectViewController { viewController in
            let window = viewController.view.window
            guard let rootViewController = window?.rootViewController else { return }
            hostingViewController.rootViewController = rootViewController
            window?.rootViewController = hostingViewController
        }
    }
}

class TestViewModel: ObservableObject {
    @Published var publishedValue: Int

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

class HostingViewController: UIViewController, ObservableObject {
    var rootViewController: UIViewController?

    var style: UIStatusBarStyle = .default {
        didSet {
            self.rootViewController?.setNeedsStatusBarAppearanceUpdate()
        }
    }

    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()
    }
}

这个答案给我所有实现解决方案所需的一切。注意:您可以覆盖rootviewController,但是您喜欢,我使用无论如何我们已经在项目中使用了它。

github branch

First of all, thanks for the help. Neither of the answers helped in my situation, since I couldn't get the modifier to update with the variable change. But with some Googling and trying out different solutions I figured out a working solution for updating the status bar colors.

I needed to update the style variable in the HostingViewController, and then update accordingly. So I added the HostingViewController as a @StateObject and updated the style variable inside the .onChange(). Not quite the solution I was going with to start out, but it does work.

The code:

import SwiftUI
import Introspect

struct ContentView: View {
    @ObservedObject var viewModel: TestViewModel
    @StateObject var hostingViewController: HostingViewController = .init(rootViewController: nil, style: .default)

    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 {
                            hostingViewController.style = .lightContent
                        } else {
                            hostingViewController.style = .darkContent
                        }

                    }
                Button("Increment") {
                    viewModel.publishedValue += 1
                }
            }
        }
        .ignoresSafeArea()
        .introspectViewController { viewController in
            let window = viewController.view.window
            guard let rootViewController = window?.rootViewController else { return }
            hostingViewController.rootViewController = rootViewController
            window?.rootViewController = hostingViewController
        }
    }
}

class TestViewModel: ObservableObject {
    @Published var publishedValue: Int

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

class HostingViewController: UIViewController, ObservableObject {
    var rootViewController: UIViewController?

    var style: UIStatusBarStyle = .default {
        didSet {
            self.rootViewController?.setNeedsStatusBarAppearanceUpdate()
        }
    }

    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()
    }
}

Big shoutout to this answer for giving me all that I needed to implement the solution. Note: You can override the rootViewController however you like, I used SwiftUI-Introspect as we are already using it in our project anyway.

GitHub branch

冷弦 2025-02-18 07:13:36

您正在解决这个问题。由于您的viewModel@observedObject,而已发布Value IS 已发布,您的view> view 代码>每次更新都会自动重新计算。无需手动onChange

您只需将逻辑移动到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)
}

甚至更好,将逻辑移至单独的计算属性:

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

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

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
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文