UITextView Representable in SwiftUI not respecting bounds for width in ScrollView - swift

I want to present text in a UITextView using SwiftUI. I'm using UIViewRepresentable and having issues with the width when it exceeds the bounds of the scrollView. I would like if to wrap the text but instead it just continues on one long line and the left and right are not visible:
This is my code:
struct ContentView: View {
var messages = ["here is some long text to show the textView is not wrapping it's content"]
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
ScrollView(.vertical) {
ScrollViewReader { scrollView in
ZStack {
LazyVStack {
ForEach(messages, id: \.self) { message in
TextView(text: message)
}
}
}
}
}
}
}
}
}
struct TextView: UIViewRepresentable {
var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
textView.isScrollEnabled = false
textView.backgroundColor = .clear
textView.isEditable = false
textView.clipsToBounds = true
textView.isEditable = false
textView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
textView.backgroundColor = #colorLiteral(red: 0.03137254902, green: 0.4980392157, blue: 0.9960784314, alpha: 1)
textView.textColor = UIColor.white
textView.text = text
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
I'm wondering if this has something to do with the scrollView. And either way I would like to know if there is a fix to this? I tried setting the frame width to geometry.size.width but that did nothing.

This happens because you're using a UIRepresentable view. There are a few solutions, simplest of them is to use explicit .frame() on top of the UIRepresentable view that is in a SwiftUI view.
e.g. .frame(width: UIScreen.main.bounds.width) so it only extends as much as screen's width.

Related

how to align top the text in this UILabel

Why the justified text won't align top? there's additional padding on top and bottom. I want the text to be aligned top and should only take the required space for the length of the text. How to fit the frame? Deleting height will show only one line.
import SwiftUI
struct TextWidget: View {
private let view = UILabel()
let justified: Bool
var body: some View {
VStack (alignment: .leading) {
HStack (alignment: .top) {
if justified {
JustifiedText("sampletext looonggg texttttsss dddddDDDDD dddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdd ")
.padding(.bottom, 5.0)
}
if !justified {
Text("sampletext looonggg texttttsss dddddDDDDD dddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdddddDDDDDdddddDD dddddDDDDD")
.font(Font.system(size: 25, weight: .regular ))
.padding(.bottom, 5.0)
}
}.background(Color.purple)
} .frame(width: 200, height: 400, alignment: .top)
}
}
struct JustifiedText: UIViewRepresentable {
private let value: String
init(_ string: String) {
value = string
}
func makeUIView(context: Context) -> UILabel {
let view = UILabel()
view.textAlignment = .justified
view.numberOfLines = 0
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.text = value
view.adjustsFontSizeToFitWidth = false
view.allowsDefaultTighteningForTruncation = false
view.font = UIFont.systemFont(ofSize: 20)
return view
}
func updateUIView(_ uiView: UILabel, context: Context) {
}
}
Edit: someone suggested a similar post, but it does not justify the text. justifying text and maintaining font size is a main requirement.

SwiftUI: Showing HTML text inside a ScrollView

I'm trying to show some HTML text in an app I'm making, but I can't get it to work properly.
The problem is that the view that contains the text is always shown with the height of only one line, even tho it is wrapped in a ScrollView. In the screenshot, you guys can see that the ScrollView works, but the original size is very small.
The code is:
ScrollView {
GeometryReader { proxy in
AttributedText(htmlContent: job.description)
.frame(height: proxy.size.height, alignment: .center)
}
}
struct AttributedText: UIViewRepresentable {
let htmlContent: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}
}
struct AttributedText_Previews: PreviewProvider {
static var previews: some View {
AttributedText(htmlContent: "<h1>This is HTML String</h1>")
}
}
I have also tried to use the GeometryReader to establish the size of the ScrollView, but no luck either.
Is there any way to make this look normal and nice, scrollable, and with a proper text size?
Thanks a lot in advance!
Edit: After #RajaKishan's answer, this is how it looks like (where you can see that the content is cut-off):
As WKWebView has already scroll and you are also wrapped inside the scroll view, so parent scroll view not get the proper size.
You have to disable WKWebView scrolling. Also, bind the size with webview and update the frame of webview.
Here is the possible demo.
struct AttributedText: UIViewRepresentable {
let htmlContent: String
#Binding var size: CGSize
private let webView = WKWebView()
var sizeObserver: NSKeyValueObservation?
func makeUIView(context: Context) -> WKWebView {
webView.scrollView.isScrollEnabled = false //<-- Here
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: AttributedText
var sizeObserver: NSKeyValueObservation?
init(parent: AttributedText) {
self.parent = parent
sizeObserver = parent.webView.scrollView.observe(\.contentSize, options: [.new], changeHandler: { (object, change) in
parent.size = change.newValue ?? .zero
})
}
}
}
For view
#State private var size: CGSize = .zero
var body: some View{
ScrollView {
AttributedText(htmlContent: "<h1>This is HTML String</h1>", size: $size)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: size.height, maxHeight: .infinity)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}

SwiftUI Is it possible to disable the gesture on Text of UITextView implemented by UIViewRepresentable?

I made the UITextView by UIViewRepresentable and added TapGesture like below.
But I want to disable the gesture only on text. Just want to activate tap() function on out of text. Is it possible to do that ? I don't know how to detect the position of texts on UIViewRepresentable and disable the gesture...
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<TextView>) -> UITextView {
let textView = UITextView()
textView.backgroundColor = UIColor.clear
textView.isScrollEnabled = false
textView.text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
textView.font = UIFont(name: "ArialMT", size: 20)
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
struct TmpView9: View {
#State var text: String
func tap() -> some Gesture {
TapGesture()
.onEnded{
print("taped!!!!")
}
}
var body: some View {
VStack {
TextView(text: $text)
.frame(width:300, height: 300, alignment: .topLeading)
.border(Color.red, width: 1)
.gesture(tap())
}
}
}

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?
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
You can find my simple trick for growing TextEditor here in this answer
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)
}
}
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.
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())
}
}
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.
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
To achieve this visual design here is the code I used.
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
}
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
}
}
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
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)
)
}
}
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)
}

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.
Adding .padding emphasises this issue:
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.
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.
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)
}
}
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
Try:
button.setBackgroundImage(UIImage.imageWithColor(.lightGray), for: .normal)
You can st it for .focused and .highlighted