admin管理员组

文章数量:1278720

Given the following code:

import SwiftUI

struct HomeScreenCounterView: View {
    private let value: Int
    private let title: String
    private let emptyDescription: String


    init(value: Int, title: String, emptyDescription: String) {
        self.value = value
        self.title = title
        self.emptyDescription = emptyDescription
    }

    var body: some View {
        VStack {
            Text("Messages")
                .font(.fontTextLBold)
            Text("No new messages")
                .font(.fontTextSRegular)
        }
        .padding()
        .clipShape(RoundedRectangle(cornerRadius: 16))
    }
}

#Preview {
    HomeScreenCounterView(value: 0,
                          title: "Messages",
                          emptyDescription: "No new messages")
    .foregroundStyle(Color.red)
    .background(Color.blue)
}

I get the following result:

Notice, how the text color in both labels has been changed to red.

While the background is correctly displayed as blue, it is obviously not clipped, as the clipping operator has been applied earlier, not later.

What would be the best/the most elegant way to have a background color modifier, similarly to how the other SwiftUI components work?

One of the solutions I am thinking of, and it's obviously a working solution, is to add an initializer parameter:

init(value: Int, title: String, emptyDescription: String, backgroundColor: Color) {

And then using that value to set the background color just after padding:

        .padding()
        .background(backgroundColor) 
        .clipShape(RoundedRectangle(cornerRadius: 16))

But is there a better way?

This is how I'd like the end result to look like:

Given the following code:

import SwiftUI

struct HomeScreenCounterView: View {
    private let value: Int
    private let title: String
    private let emptyDescription: String


    init(value: Int, title: String, emptyDescription: String) {
        self.value = value
        self.title = title
        self.emptyDescription = emptyDescription
    }

    var body: some View {
        VStack {
            Text("Messages")
                .font(.fontTextLBold)
            Text("No new messages")
                .font(.fontTextSRegular)
        }
        .padding()
        .clipShape(RoundedRectangle(cornerRadius: 16))
    }
}

#Preview {
    HomeScreenCounterView(value: 0,
                          title: "Messages",
                          emptyDescription: "No new messages")
    .foregroundStyle(Color.red)
    .background(Color.blue)
}

I get the following result:

Notice, how the text color in both labels has been changed to red.

While the background is correctly displayed as blue, it is obviously not clipped, as the clipping operator has been applied earlier, not later.

What would be the best/the most elegant way to have a background color modifier, similarly to how the other SwiftUI components work?

One of the solutions I am thinking of, and it's obviously a working solution, is to add an initializer parameter:

init(value: Int, title: String, emptyDescription: String, backgroundColor: Color) {

And then using that value to set the background color just after padding:

        .padding()
        .background(backgroundColor) 
        .clipShape(RoundedRectangle(cornerRadius: 16))

But is there a better way?

This is how I'd like the end result to look like:

Share Improve this question asked Feb 25 at 10:10 Richard TopchiiRichard Topchii 8,2159 gold badges60 silver badges129 bronze badges
Add a comment  | 

3 Answers 3

Reset to default 2

Use backgroundStyle() to change the style that the .background shape style will resolve to. This also sets the backgroundStyle environment value.

// Preview
HomeScreenCounterView(value: 0,
                      title: "Messages",
                      emptyDescription: "No new messages")
.foregroundStyle(Color.red)
.backgroundStyle(.blue) // <-----

// HomeScreenCounterView.body
VStack {
    Text("Messages")
    Text("No new messages")
}
.padding()
.background(.background) // <-----
.clipShape(RoundedRectangle(cornerRadius: 16))

Alternatively, you can write your own environment value to store the background style, in case you don't want to affect other views that might be using the .background shape style.

extension EnvironmentValues {
    @Entry var myBackground: AnyShapeStyle = AnyShapeStyle(.background)
}

// a view modifier that sets this environment value
extension View {
    func myBackground<S: ShapeStyle>(_ style: S) -> some View {
        environment(\.myBackground, AnyShapeStyle(style))
    }
}

You can then write your own ShapeStyle that resolves to this environment value.

struct MyBackgroundStyle: ShapeStyle {
    func resolve(in environment: EnvironmentValues) -> AnyShapeStyle {
        environment.myBackground
    }
}

extension ShapeStyle where Self == MyBackgroundStyle {
    static var myBackground: MyBackgroundStyle { .init() }
}

Usage:

// HomeScreenCounterView.body

VStack {
    Text("Messages")
    Text("No new messages")
}
.padding()
.background(.myBackground) // <-----
.clipShape(RoundedRectangle(cornerRadius: 16))

// Preview
HomeScreenCounterView(value: 0,
                  title: "Messages",
                  emptyDescription: "No new messages")
    .foregroundStyle(Color.red)
    .myBackground(.blue) // <-----

Before iOS 17, you cannot create your own ShapeStyle, so you would need a ViewModifier instead of a custom ShapeStyle.

struct MyBackgroundModifier: ViewModifier {
    @Environment(\.myBackground) var myBackground
    
    func body(content: Content) -> some View {
        content.background(myBackground)
    }
}

extension View {
    func useMyBackground() -> some View {
        modifier(MyBackgroundModifier())
    }
}

You'd then replace .background(.myBackground) with .useMyBackground().

You could consider using a semantic shape style for filling the background inside the view. The obvious choice would be .background, but you could also use one of the other semantic or hierarachical styles such as .secondary. The actual color can then be set from a parent view:

// HomeScreenCounterView

VStack {
    // ...
}
.padding()
.background(.background, in: .rect(cornerRadius: 16)) // or, say, .secondary
// Parent

HomeScreenCounterView(
    // ...
)
.foregroundStyle(.red)
// .foregroundStyle(.red, .blue) // for setting .blue as secondary
.backgroundStyle(.blue)

You can use the second argument of the background modifier to put it in any shape you like:

#Preview {
    HomeScreenCounterView(value: 0, title: "Messages", emptyDescription: "No new messages")
        .foregroundStyle(Color.red)
        .background(.blue, in: .rect(cornerRadius: 16)) // 

本文标签: iosSwiftUI adjust background externallysimilar to the foregroundStyleStack Overflow