Lorenzo Fiamingo
Lorenzo Fiamingo

Reputation: 4109

Change background color of TextEditor in SwiftUI

TextEditor seems to have a default white background. So the following is not working and it displayed as white instead of defined red:

var body: some View {
    TextEditor(text: .constant("Placeholder"))
        .background(Color.red)
}

Is it possible to change the color to a custom one?

Upvotes: 121

Views: 42921

Answers (16)

黄玉顺
黄玉顺

Reputation: 1

I was looking for a solution as well and here is what I have figured it out.

I think a more proper way is that we can just create a custom wrapper for UITextView to remove the default background color, and then we apply the style in View instead of interacting with the UIKit stuff inside the View.

import Foundation
import SwiftUI

struct TextViewWrapper: UIViewRepresentable {
    @Binding var text: String
    
    func makeUIView(context: Context) -> UITextView {
        let uiTextView = UITextView()
        uiTextView.font = .preferredFont(forTextStyle: .body)
        uiTextView.backgroundColor = .clear
        return uiTextView
    }
    
    func updateUIView(_ uiTextView: UITextView, context: Context) {
        uiTextView.text = text
    }
}
@State private var description = ""

TextViewWrapper(text: $description)
    .frame(height: 72)
    .padding(
        EdgeInsets(
            top: 8,
            leading: 8,
            bottom: 8,
            trailing: 8
        )
    )
    .background(Color.secondary.opacity(0.2))
    .cornerRadius(12)

Upvotes: 0

Victor Sebastian
Victor Sebastian

Reputation: 207

To change TextEditor background color, this modifier works well for me. Just we need to consider both the cases of iOS 16 and below.

 extension View {
        func textEditorBackground(_ content: Color) -> some View {
            if #available(iOS 16.0, *) {
                return self.scrollContentBackground(.hidden)
                    .background(content)
            } else {
                UITextView.appearance().backgroundColor = .clear
                return self.background(content)
            }
        }
    }

TextEditor(text: $message)
.modifier(TextEditorBackground(color: Color("PrimaryGray")))

Upvotes: 3

Ahmed M. Hassan
Ahmed M. Hassan

Reputation: 1286

iOS 13+

Well, this soultion seems to work with me in iOS 16 and some earlier versions as well.

TextEditor(text: $text)
    .scrollContentBackgroundHidden()
    .background(Style.backgroundColor) // Color works with iOS 16
    .textEditorBackground {
        Color(Style.backgroundColor) // Color works with earlier versions
    }

With the help of both extensions to support earlier versions as well

extension View {
    /// Hides the standard system background of the view.
    /// 
    func scrollContentBackgroundHidden() -> some View {
        if #available(iOS 16.0, *) {
            return self.scrollContentBackground(.hidden)
        } else {
            return self
        }
    }

    /// Layers the given views behind this ``TextEditor``.
    ///
    func textEditorBackground<V>(@ViewBuilder _ content: () -> V) -> some View where V : View {
        self
            .onAppear {
                UITextView.appearance().backgroundColor = .clear
            }
            .background(content())
    }
}

Upvotes: 1

Kavindu Dissanayake
Kavindu Dissanayake

Reputation: 1

Ios 16
Make sure to add .scrollContentBackground(.hidden) this lines then background will apply

TextEditor(text:.constant(hint))
.scrollContentBackground(.hidden)
.background(.red) // To see this

This work for me

Upvotes: 12

Niklas
Niklas

Reputation: 25443

As many have stated, with iOS 16 you need to use scrollContentBackground. I created an extension method to handle both cases:

struct ContentView: View {
    @State private var editingText: String = ""
    
    var body: some View {
        TextEditor(text: $editingText)
            .transparentScrolling()
            .background(Color.red)
    }
}

public extension View {
    func transparentScrolling() -> some View {
        if #available(iOS 16.0, *) {
            return scrollContentBackground(.hidden)
        } else {
            return onAppear {
                UITextView.appearance().backgroundColor = .clear
            }
        }
    }
}

Upvotes: 33

Qiquan Lu
Qiquan Lu

Reputation: 675

It appears the UITextView.appearance().backgroundColor = .clear trick in IOS 16,

only works for the first time you open the view and the effect disappear when the second time it loads.

So we need to provide both ways in the app. Answer from StuFF mc works.

 var body: some View {
    if #available(iOS 16.0, *) {
        mainView.scrollContentBackground(.hidden)
    } else {
        mainView.onAppear {
            UITextView.appearance().backgroundColor = .clear
        }
    }
}

// rename body to mainView
var mainView: some View {
    TextEditor(text: $notes).background(Color.red)              
}

Upvotes: 0

Mohammad Razipour
Mohammad Razipour

Reputation: 3711

import SwiftUI

struct AddCommentView: View {
    
    init() {
        UITextView.appearance().backgroundColor = .clear
    }
    
    var body: some View {
           VStack {
                    if #available(iOS 16.0, *) {
                        TextEditor(text: $viewModel.commentText)
                            .scrollContentBackground(.hidden)
                    } else {
                        TextEditor(text: $viewModel.commentText)
                    }
                }
                .background(Color.blue)
                .frame(height: UIScreen.main.bounds.width / 2)
                .overlay(
                    RoundedRectangle(cornerRadius: 5)
                        .stroke(Color.red, lineWidth: 1)
                )
    }
}

Upvotes: 0

Braxton Ward
Braxton Ward

Reputation: 81

To achieve this visual design here is the code I used.

enter image description here

iOS 16

TextField(
    "free_form",
    text: $comment,
    prompt: Text("Type your feedback..."),
    axis: .vertical
)
.lineSpacing(10.0)
.lineLimit(10...)
.padding(16)
.background(Color.themeSeashell)
.cornerRadius(16)

iOS 15

ZStack(alignment: .topLeading) {
    RoundedRectangle(cornerRadius: 16)
        .foregroundColor(.gray)
    
    TextEditor(text: $comment)
        .padding()
        .focused($isFocused)
    
    if !isFocused {
        Text("Type your feedback...")
            .padding()
    }
}
.frame(height: 132)
.onAppear() {
    UITextView.appearance().backgroundColor = .clear
}

Upvotes: 7

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119917

iOS 16

You should hide the default background to see your desired one:

TextEditor(text: .constant("Placeholder"))
    .scrollContentBackground(.hidden) // <- Hide it
    .background(.red) // To see this

iOS 15 and below

TextEditor is backed by UITextView. So you need to get rid of the UITextView's backgroundColor first and then you can set any View to the background.

struct ContentView: View {
    init() {
        UITextView.appearance().backgroundColor = .clear
    }

    var body: some View {
        List {
            TextEditor(text: .constant("Placeholder"))
                .background(.red)
        }
    }
}

Demo

enter image description here

You can find my simple trick for growing TextEditor here in this answer

Upvotes: 232

StuFF mc
StuFF mc

Reputation: 4169

Update iOS 16 / SwiftUI 4.0

You need to use .scrollContentBackground(.hidden) instead of UITextView.appearance().backgroundColor = .clear

https://twitter.com/StuFFmc/status/1556561422431174656

Warning: This is an iOS 16 only so you'll probably need some if #available and potentially two different TextEditor component.

Upvotes: 12

Using the Introspect library, you can use .introspectTextView for changing the background color.

TextEditor(text: .constant("Placeholder"))
.cornerRadius(8)
.frame(height: 100)
.introspectTextView { textView in
    textView.backgroundColor = UIColor(Color.red)
}

Result

Upvotes: 1

voorjaar
voorjaar

Reputation: 181

This works for me on macOS

extension NSTextView {
  open override var frame: CGRect {
    didSet {
      backgroundColor = .clear
      drawsBackground = true
    }
  }
}
struct ContentView: View {
    @State var text = ""
    var body: some View {        
        TextEditor(text: $text)           
            .background(Color.red)    
    }

Reference this answer

Upvotes: 10

Michael Alfano
Michael Alfano

Reputation: 46

You can use Mojtaba's answer (the approved answer). It works in most cases. However, if you run into this error:

"Return from initializer without initializing all stored properties"

when trying to use the init{ ... } method, try adding UITextView.appearance().backgroundColor = .clear to .onAppear{ ... } instead.

Example:

var body: some View {
    VStack(alignment: .leading) {

        ...

    }
    .onAppear {
        UITextView.appearance().backgroundColor = .clear
    }
}

Upvotes: 1

Rebeloper
Rebeloper

Reputation: 788

extension View {
/// Layers the given views behind this ``TextEditor``.
    func textEditorBackground<V>(@ViewBuilder _ content: () -> V) -> some View where V : View {
        self
            .onAppear {
                UITextView.appearance().backgroundColor = .clear
            }
            .background(content())
    }
}

Upvotes: 6

Marc T.
Marc T.

Reputation: 5340

Pure SwiftUI solution on iOS and macOS

colorMultiply is your friend.

struct ContentView: View {
    
    @State private var editingText: String = ""
    
    var body: some View {
        
        TextEditor(text: $editingText)
            .frame(width: 400, height: 100, alignment: .center)
            .cornerRadius(3.0)
            .colorMultiply(.gray)
    }
}

Upvotes: 20

tillhain
tillhain

Reputation: 51

Custom Background color with SwiftUI on macOS

On macOS, unfortunately, you have to fallback to AppKit and wrap NSTextView.

You need to declare a view that conforms to NSViewRepresentable

This should give you pretty much the same behaviour as SwiftUI's TextEditor-View and since the wrapped NSTextView does not draw its background, you can use the .background-ViewModifier to change the background

struct CustomizableTextEditor: View {
    @Binding var text: String
    
    var body: some View {
        GeometryReader { geometry in
            NSScrollableTextViewRepresentable(text: $text, size: geometry.size)
        }
    }
    
}

struct NSScrollableTextViewRepresentable: NSViewRepresentable {
    typealias Representable = Self
    
    // Hook this binding up with the parent View
    @Binding var text: String
    var size: CGSize
    
    // Get the UndoManager
    @Environment(\.undoManager) var undoManger
    
    // create an NSTextView
    func makeNSView(context: Context) -> NSScrollView {
        
        // create NSTextView inside NSScrollView
        let scrollView = NSTextView.scrollableTextView()
        let nsTextView = scrollView.documentView as! NSTextView
        
        // use SwiftUI Coordinator as the delegate
        nsTextView.delegate = context.coordinator
        
        // set drawsBackground to false (=> clear Background)
        // use .background-modifier later with SwiftUI-View
        nsTextView.drawsBackground = false
        
        // allow undo/redo
        nsTextView.allowsUndo = true
        
        return scrollView
    }
    
    func updateNSView(_ scrollView: NSScrollView, context: Context) {
        // get wrapped nsTextView
        guard let nsTextView = scrollView.documentView as? NSTextView else {
            return
        }
        
        // fill entire given size
        nsTextView.minSize = size
        
        // set NSTextView string from SwiftUI-Binding
        nsTextView.string = text
    }
    
    // Create Coordinator for this View
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    // Declare nested Coordinator class which conforms to NSTextViewDelegate
    class Coordinator: NSObject, NSTextViewDelegate {
        var parent: Representable // store reference to parent
        
        init(_ textEditor: Representable) {
            self.parent = textEditor
        }
        
        // delegate method to retrieve changed text
        func textDidChange(_ notification: Notification) {
            // check that Notification.name is of expected notification
            // cast Notification.object as NSTextView

            guard notification.name == NSText.didChangeNotification,
                let nsTextView = notification.object as? NSTextView else {
                return
            }
            // set SwiftUI-Binding
            parent.text = nsTextView.string
        }
        
        // Pass SwiftUI UndoManager to NSTextView
        func undoManager(for view: NSTextView) -> UndoManager? {
            parent.undoManger
        }

        // feel free to implement more delegate methods...
        
    }
    
}

Usage

ContenView: View {
    @State private var text: String

    var body: some View {
        VStack {
            Text("Enter your text here:")
            CustomizableTextEditor(text: $text)
                .background(Color.red)
        }
            .frame(minWidth: 600, minHeight: 400)

    }
}

Edit:

  • Pass reference to SwiftUI UndoManager so that default undo/redo actions are available.
  • Wrap NSTextView in NSScrollView so that it is scrollable. Set minSize property of NSTextView to enclosing SwiftUIView-Size so that it fills the entire allowed space.

Caveat: Only first line of this custom TextEditor is clickable to enable text editing.

Upvotes: 5

Related Questions