SwiftUI views in VStack are overlapping each other - swiftui

I have two views in a VStack. All looks fine until I try to enlarge the font in accessibility setting. Then for some reason the stack is not expanding to accommodate both views. Instead it is pushing one view on top of another. See below.
How can I align these correctly? Below is my code.
var body: some View {
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.")
.fixedSize(horizontal: false, vertical: true)
.padding()
GeometryReader { geometry in
VStack(spacing: 0) {
Image("tmp")
.resizable()
.scaledToFill()
.frame(width: geometry.size.width * 0.88)
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
.frame(width: geometry.size.width * 0.8, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
.frame(width: geometry.size.width * 0.8, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
.frame(width: geometry.size.width * 0.8, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
}
.padding()
.background(
Rectangle()
.fill(Color.white)
)
}
.cornerRadius(10)
.edgesIgnoringSafeArea(.all)
}
.scaledToFit()
.shadow(color: .gray, radius: 10, x: 5, y: 5)
.scaledToFill()
Spacer()
}
.background(Rectangle()
.fill(Color.gray)
.scaledToFill())
}

The Issue:
The issue of overlapping is with this section:
.scaledToFit() // Not needed
.shadow(color: .gray, radius: 10, x: 5, y: 5)
.scaledToFill() // Not needed
The Answer:
You don't need neither scaledToFit nor scaledToFill there.
Visualizing the Issue:
using scaledToFill:
using scaledToFit:
See blue borders? Thats the issue.
Some Tips:
There is no need to create a dummy Rectangle for background color. .background modifier can accept a color directly like:
.background(Color.gray)
You can ignore safe area only for background modifier like:
.background(Color.gray.edgesIgnoringSafeArea(.all))
Don't repeat modifiers! group them together and apply once like:
Group {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
}
.frame(width: geometry.size.width * 0.8, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
Multiplying width in a float number can cause misaligned images. So always round the result like:
(geometry.size.width * 0.88).rounded(.down)
Result:
Image:
Refactored Code:
var body: some View {
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.")
.padding()
GeometryReader { geometry in
VStack(spacing: 0) {
Image("tmp")
.resizable()
.scaledToFill()
.frame(width: (geometry.size.width * 0.88).rounded(.down))
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
}
.frame(width: (geometry.size.width * 0.8).rounded(.down), alignment: .leading)
.padding()
.background(Color.white)
}
.cornerRadius(10)
}
.shadow(color: .gray, radius: 10, x: 5, y: 5)
Spacer()
}
.background(Color.gray.edgesIgnoringSafeArea(.all))
}

Try changing the modifiers on the image to the following.
Image("tmp")
.resizable()
.scaledToFit()
.frame(maxWidth: geometry.size.width * 0.88)

Related

SwiftUI: set custom underline to text?

I have this piece of code:
var body: some View {
ZStack {
Color.black
.opacity(0.8)
.edgesIgnoringSafeArea(.all)
(Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do ")
+
Text("eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, ")
.underline(true, color: Color.yellow)
+
Text("quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."))
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.lineSpacing(4.0)
.padding(.horizontal, 12)
}
}
I would like to customize the underline, like increasing the offset and the line height. How can I do so?
Thank you for your help

How to create two text views that are the same size

I am working on a game and I have text views representing cards that can be flipped. There is text on each side of the card. I would like the card to be the same size on each side. i.e. I would like the card on the side with less text to be the same size as the card on the size with more text. I don't want to hard code the card size as sometimes there will be only one line of text and other times there will be multiple lines of text. Here is an excerpt of the code I currently have:
import SwiftUI
struct FlipTestView: View {
#State var flipped = false
var body: some View {
let flipDegrees = flipped ? 180.0 : 0
VStack {
ZStack() {
Text("Card One First Side Of Card")
.placedOnCard(Color.yellow)
.flipRotate(flipDegrees).opacity(flipped ? 0.0 : 1.0)
Text("""
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
""")
.placedOnCard(Color.green)
.flipRotate(-180 + flipDegrees).opacity(flipped ? 1.0 : 0.0)
}
.animation(.easeInOut(duration: 0.8))
.onTapGesture { flipped.toggle()}
}
}
}
struct FlipTestView_Previews: PreviewProvider {
static var previews: some View {
FlipTestView()
}
}
extension View {
func flipRotate(_ degrees : Double) -> some View {
return rotation3DEffect(Angle(degrees: degrees), axis: (x: 1.0, y: 0.0, z: 0.0))
}
func placedOnCard(_ color: Color) -> some View {
return
padding(15)
// .frame(width: 350, height: 65, alignment: .center).background(color)
.background(color)
.cornerRadius(16)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.blue, lineWidth: 4)
)
}
}
Move the .placedOnCard to the ZStack that contains both sides, then it will be sized to the larger of the two sides. Here is a solution that refactors your code:
ZStack() {
Text("Card One First Side Of Card")
.opacity(flipped ? 0.0 : 1.0)
Text("""
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
""")
.flipRotate(180.0).opacity(flipped ? 1.0 : 0.0)
}
.placedOnCard(flipped ? Color.green : Color.yellow)
.flipRotate(flipDegrees)
.animation(.easeInOut(duration: 0.8))
.onTapGesture { flipped.toggle()}

How to implement a "read more" style button at the end of a text in SwiftUI

I have a very long text and I want to show just 3 lines with more button just like the picture and also a less button when the text is expand. Any idea of how to do it with SwiftUI?
var body: some View{
VStack{
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
}
}
This answer is a bit of a hack, because it does not truncate the actual string and apply the "..." suffix, which in my humble opinion would be the better engineered solution. That would require the programmer to determine the length of the string that fits within three lines, remove the last two words (to allow for the More/Less button) and apply the "..." suffix.
This solution limits the number of lines shown and literally covers the trailing end of the third line with a white background and the button. But it may be suitable for your case...
#State private var isExpanded: Bool = false
var body: some View{
VStack{
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
.lineLimit(isExpanded ? nil : 3)
.overlay(
GeometryReader { proxy in
Button(action: {
isExpanded.toggle()
}) {
Text(isExpanded ? "Less" : "More")
.font(.caption).bold()
.padding(.leading, 8.0)
.padding(.top, 4.0)
.background(Color.white)
}
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .bottomTrailing)
}
)
}
}
You can learn how to do this by following Apple's "Introducing SwiftUI" tutorials. Specifically the "Creating a macOS app" tutorial, "Section 9 Build the Detail View".
I was also trying to achieve the similar Results as you. Also spent hours finding the solution when it was in front of me all along...!
The solution is to use ZStack along with ScrollView and GeometaryReader all at once...
struct CollapsableTextView: View {
let lineLimit: Int
#State private var expanded: Bool = false
#State private var showViewButton: Bool = false
private var text: String
init(_ text: String, lineLimit: Int) {
self.text = text
self.lineLimit = lineLimit
}
private var moreLessText: String {
if showViewButton {
return expanded ? "View Less" : "View More"
} else {
return ""
}
}
var body: some View {
VStack(alignment: .leading) {
ZStack {
Text(text)
.font(.body)
.lineLimit(expanded ? nil : lineLimit)
ScrollView(.vertical) {
Text(text)
.font(.body)
.background(
GeometryReader { proxy in
Color.clear
.onAppear {
showViewButton = proxy.size.height > CGFloat(22 * lineLimit)
}
.onChange(of: text) { _ in
showViewButton = proxy.size.height > CGFloat(22 * lineLimit)
}
}
)
}
.opacity(0.0)
.disabled(true)
.frame(height: 0.0)
}
Button(action: {
withAnimation {
expanded.toggle()
}
}, label: {
Text(moreLessText)
.font(.body)
.foregroundColor(.orange)
})
.opacity(showViewButton ? 1.0 : 0.0)
.disabled(!showViewButton)
.frame(height: showViewButton ? nil : 0.0)
}
}
}
struct CollapsableTextView_Previews: PreviewProvider {
static var previews: some View {
CollapsableTextView("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", lineLimit: 3)
}
}
The Most important part of this code is CGFloat(22 * lineLimit) here 22 is the height of single Line with the specified font used. You might have to change the height (i.e. 22 in this case) based upon your font...
Other than that every thing is pretty straight forward. I hope it might help...!
You can read this Medium article
struct ExpandableText: View {
#State private var expanded: Bool = false
#State private var truncated: Bool = false
#State private var shrinkText: String
private var text: String
let font: UIFont
let lineLimit: Int
init(_ text: String, lineLimit: Int, font: UIFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)) {
self.text = text
_shrinkText = State(wrappedValue: text)
self.lineLimit = lineLimit
self.font = font
}
var body: some View {
ZStack(alignment: .bottomLeading) {
Group {
Text(self.expanded ? text : shrinkText) + Text(moreLessText)
.bold()
.foregroundColor(.black)
}
.animation(.easeInOut(duration: 1.0), value: false)
.lineLimit(expanded ? nil : lineLimit)
.background(
// Render the limited text and measure its size
Text(text)
.lineLimit(lineLimit)
.background(GeometryReader { visibleTextGeometry in
Color.clear.onAppear() {
let size = CGSize(width: visibleTextGeometry.size.width, height: .greatestFiniteMagnitude)
let attributes:[NSAttributedString.Key:Any] = [NSAttributedString.Key.font: font]
///Binary search until mid == low && mid == high
var low = 0
var heigh = shrinkText.count
var mid = heigh ///start from top so that if text contain we does not need to loop
///
while ((heigh - low) > 1) {
let attributedText = NSAttributedString(string: shrinkText + moreLessText, attributes: attributes)
let boundingRect = attributedText.boundingRect(with: size, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
if boundingRect.size.height > visibleTextGeometry.size.height {
truncated = true
heigh = mid
mid = (heigh + low)/2
} else {
if mid == text.count {
break
} else {
low = mid
mid = (low + heigh)/2
}
}
shrinkText = String(text.prefix(mid))
}
if truncated {
shrinkText = String(shrinkText.prefix(shrinkText.count - 2)) //-2 extra as highlighted text is bold
}
}
})
.hidden() // Hide the background
)
.font(Font(font)) ///set default font
///
if truncated {
Button(action: {
expanded.toggle()
}, label: {
HStack { //taking tap on only last line, As it is not possible to get 'see more' location
Spacer()
Text("")
}.opacity(0)
})
}
}
}
private var moreLessText: String {
if !truncated {
return ""
} else {
return self.expanded ? " read less" : " ... read more"
}
}
}
And use this ExpandableText in your view like bellow
ExpandableText("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut laborum", lineLimit: 6)

TextEditor sticking to minHeight inSwiftUI

I am trying to build a view with the newly introduced TextEditor. The idea is that I have some content at the top (blue frame), then a ScrollView with a TextEditor and a variable number of Text below it (red frame).
The TextEditor(yellow frame) view is supposed to have a minimum height, but should take up all the available space if there aren't to many Text views following – which it currently does not do...
import SwiftUI
struct ScrollViewWithTextEditor: View {
var comments = ["Foo", "Bar", "Buzz"]
var loremIpsum = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
"""
var body: some View {
VStack {
Group {
Text("Some Content above")
}
.frame(maxWidth: .infinity)
.border(Color.blue, width: 3.0)
.padding(.all, 10)
ScrollView {
ScrollView {
TextEditor(text: .constant(loremIpsum))
.frame(minHeight: 200.0)
}
.frame(minHeight: 200.0)
.border(Color.yellow, width: 3.0)
.cornerRadius(3.0)
.padding(.all, 10.0)
VStack {
ForEach(comments, id: \.self) { comment in
Text(comment)
}
.padding(.all, 10)
.frame(maxWidth: .infinity, alignment: .leading)
.border(Color.gray, width: 1)
.cornerRadius(3.0)
.padding(.all, 10)
}
}
.frame(minHeight: 200.0)
.border(Color.red, width: 3)
.padding(.all, 3)
}
}
}
struct ScrollViewWithTextEditor_Previews: PreviewProvider {
static var previews: some View {
ScrollViewWithTextEditor()
}
}
Any suggestions on how to solve this?
Here is possible solution. Tested with Xcode 12 / iOS 14.
ScrollView {
// make clear static text in background to define size and
// have TextEditor in front with same text fit
Text(loremIpsum).foregroundColor(.clear).padding(8)
.frame(maxWidth: .infinity)
.overlay(
TextEditor(text: .constant(loremIpsum))
)
}
.frame(minHeight: 200.0)
.border(Color.yellow, width: 3.0)

Fixing overlapping views in SwiftUI

I am trying to build a screen similar to the following.
But there are few things that are wrong with this screen:
The image and the text below it are not exactly the same width. The image is slightly wider.
The image and its associated text overlap the text above it.
The text underneath the image is truncated.
I've been on this for two days with no luck! Can someone provide feedback on how I can achieve this? Below is what I have in terms of code.
struct ImageTitleTileView: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
Image("image")
.resizable()
.frame(width: geometry.size.width)
.aspectRatio(1, contentMode: .fill)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.")
.padding()
.background(Color.white)
}
.edgesIgnoringSafeArea(.all)
}.aspectRatio(contentMode: .fit)
}
}
struct MainItemView: View {
var body: some View {
ZStack {
Color(.whiteSmoke)
VStack(spacing: 10) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.")
ImageTitleTileView()
}.padding(16)
}
}
}
What its happening is that your image is showing the image part outside your frame.
You need to set clipped() property to the image to hide the image parts outside your frame.
Image("image")
.resizable() // allow image to be resizable
.scaledToFill() // scale your image as you need it to fill or to fit
.frame(height: 200, alignment: .center) // set image frame size
.aspectRatio(contentMode: .fill) // image aspect ratio
.clipped() // hide image outside the frame parts (CSS: overflow: hidden)
Just change some parameters . You can get this work.
One is the image ratio from 1.0 to 0.9;
The other is moving the frame from image to Stack;
The last is removing the padding from the 'text'
struct ImageTitleTileView: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
Image("image")
.resizable()
.aspectRatio(0.90, contentMode: .fill)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.")
.background(Color.white)
}.frame(width: geometry.size.width / 1.5)
.edgesIgnoringSafeArea(.all)
}.aspectRatio(contentMode: .fit)
}
}