Paulw11
Paulw11

Reputation: 115041

Changing the color of a Button in SwiftUI on tvOS

I am trying to change the color of a SwiftUI Button on tvOS.

Modifying the background almost works, except that you can see that the underlying UIButton is actually using a rounded, translucent image over the top of the background. This results in a different colour at the corners where the rectangular background lies outside the rounded image.

enter image description here

Adding .padding emphasises this issue:

enter image description here

struct ContentView: View {

    @State
    var selected = false

    var body: some View {

        Button(action: {
            self.selected.toggle()
        }) {
               Text($selected.wrappedValue ? "On":"Off")
                .foregroundColor(.white)
            }.padding(.all)
             .background(self.selected ? Color.green : Color.blue)
        }
    }
}

A related issue is the changing the color of the "focus" view, as I suspect that this is the same view as the button itself, transformed win size and color.

The typical technique in tvOS with a UIButton is to change the button image, but there doesn't seem to be any way to access the image of the underlying UIButton.

Upvotes: 3

Views: 5502

Answers (4)

PiterPan
PiterPan

Reputation: 1802

Here is a working solution for tvOS on Swift 5.5

struct TVButton: View {

// MARK: - Variables
@FocusState private var isFocused: Bool
@State private var buttonScale: CGFloat = 1.0
var title: String?
var bgColor: Color = .white
var action: () -> Void

// MARK: - Body
var body: some View {
    TVRepresentableButton(title: title, color: UIColor(bgColor)) {
        action()
    }
    .focused($isFocused)
    .onChange(of: isFocused) { newValue in
        withAnimation(Animation.linear(duration: 0.2)) {
            buttonScale = newValue ? 1.1 : 1.0
        }
    }
    .scaleEffect(buttonScale)
}
}

struct TVRepresentableButton: UIViewRepresentable {

// MARK: - Typealias
typealias UIViewType = UIButton

// MARK: - Variables
var title: String?
var color: UIColor
var action: () -> Void

func makeUIView(context: Context) -> UIButton {
    let button = UIButton(type: .system)
    button.setBackgroundImage(UIImage(color: color), for: .normal)
    button.setBackgroundImage(UIImage(color: color.withAlphaComponent(0.85)), for: .focused)
    button.setBackgroundImage(UIImage(color: color), for: .highlighted)
    button.setTitle(title, for: .normal)
    button.addAction(.init(handler: { _ in action() }), for: .allEvents)
    
    return button
}

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

extension UIImage {
    public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
    let rect = CGRect(origin: .zero, size: size)
    UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
    color.setFill()
    UIRectFill(rect)
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    guard let cgImage = image?.cgImage else { return nil }
    self.init(cgImage: cgImage)
}
}

now you are able to change Title, background color, scale and handle tap action on tvOS

Upvotes: 1

Trygve
Trygve

Reputation: 1387

Try:

button.setBackgroundImage(UIImage.imageWithColor(.lightGray), for: .normal)

You can st it for .focused and .highlighted

Upvotes: -2

LuLuGaGa
LuLuGaGa

Reputation: 14418

Again, I have only checked it on iOS, but this should help:

struct ContentView: View {

    @State var selected = false

    var body: some View {

        Button(action: { self.selected.toggle() }) {
            Text($selected.wrappedValue ? "On":"Off")
                .foregroundColor(.white)
        }   .padding(.all)
            .background(RoundedRectangle(cornerRadius: 5.0)
                            .fill(self.selected ? Color.green : Color.blue))

    }
}

You can pass any view into .background() modifier, so you might also want to try placing an Image() in there.

Upvotes: 4

salo.dm
salo.dm

Reputation: 2315

This code seems to work fine on iOS but, as you've shown, in tvOS the underlying UIButton is visible. I'm not sure how to get at the underlying button or its image, but it seems you can hack it for now (until Apple either fixes the issue or provides a way to get at that button.)

First, move the padding up to modify the Text so that it will properly affect the underlying button.

Second, (and this is the hack) clip the view after the background modifier with a cornerRadius. As long as the radius is equal to or greater than that of the underlying button, it will clip off the excess background. (Of course, what you see is not the green Color, but the color resulting from the green color superimposed on the gray of the translucent button image. At least that's how it's shown in Xcode.)

struct ContentView: View {

    @State var selected = false

    var body: some View {

        Button(action: {
            self.selected.toggle()
        }) {
            Text($selected.wrappedValue ? "On":"Off")
                .foregroundColor(.white)
                .padding(.all)
        }
        .background(self.selected ? Color.green : Color.blue)
        .cornerRadius(5)
    }
}

Upvotes: 3

Related Questions