I have three views inside an HStack. The first two are HStack's and the third is text. I want to use .offset to make the Text go off screen. I then have a DragGesture() which allows me to pull the view over.
Although when I pull, the text is not there. I played around with the .offset value and when I lowered it so only part of the text is off screen, the rest of the text is not there.
How can I get the text to render/ actually be viewable once I drag the screen?
import SwiftUI
struct ContentView: View {
#State private var draggedOffset = CGSize.zero
var body: some View {
HStack {
Rectangle()
.frame(width: 100, height: 100, alignment: .center)
Spacer()
Rectangle()
.frame(width: 100, height: 100, alignment: .center)
Spacer()
Text("Hello, world! Testing, Testing")
.padding()
.lineLimit(1)
}
.padding()
.animation(.spring())
.offset(x: draggedOffset.width)
.gesture(DragGesture()
.onChanged { value in
self.draggedOffset = value.translation
}
.onEnded { value in
self.draggedOffset = CGSize.zero
}
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It's a bit tricky. The main thing here is .fixedSize(horizontal: true, vertical: false) - that lets the Text expand horizontally. There's no need for .lineLimit(1).
You should also remove the Spacer()s. Because those also expand horizontally, SwiftUI will get stuck trying to figure out which ones to expand and which to not.
Then, there's the Color.clear. This has a couple purposes:
Give expected resizing behavior: Color can be freely resized, so if you ever want to use ContentView inside another View, you won't run into trouble.
Align the HStack: Because it's wider than the screen width, SwiftUI will center it by default. You want it aligned left, so you can pass in .leading to the alignment parameter.
struct ContentView: View {
#State private var draggedOffset = CGSize.zero
var body: some View {
Color.clear /// placeholder view that fills the screen width
.background( /// use this to constrain everything to the bounds of the screen
HStack {
Rectangle()
.frame(width: 100, height: 100) /// `alignment` is unnecessary here
Rectangle()
.frame(width: 100, height: 100)
Text("Hello, world! Testing, Testing")
.padding()
.fixedSize(horizontal: true, vertical: false) /// make the Text extend as much as possible horizontally
}, alignment: .leading /// align everything left
)
.padding()
.animation(.spring())
.offset(x: draggedOffset.width)
.gesture(
DragGesture()
.onChanged { value in
self.draggedOffset = value.translation
}
.onEnded { value in
self.draggedOffset = CGSize.zero
}
)
}
}
Result:
Related
I'm a bit stumped building a draggable slide containing a list. Inspired by this post, a MRE of what I have is below.
import SwiftUI
struct ContentView: View {
static let kMinHeight: CGFloat = 100.0
#State var currentHeight: CGFloat = kMinHeight // << any initial
var body: some View {
GeometryReader { g in // << for top container height limit
ZStack(alignment: .bottom) {
Rectangle().fill(Color.yellow) // << just for demo
self.card()
.gesture(DragGesture()
.onChanged { value in
// as card is at bottom the offset is reversed
let newHeight = self.currentHeight - (value.location.y - value.startLocation.y)
if newHeight > Self.kMinHeight && newHeight < g.size.height {
self.currentHeight = newHeight
}
})
}
}
}
func card() -> some View {
ZStack(alignment: .top){
RoundedRectangle(cornerRadius: 16.0)
.frame(height:currentHeight)
VStack{
RoundedRectangle(cornerRadius: Constants.radius)
.fill(Color.white)
.frame(
width: Constants.indicatorWidth,
height: Constants.indicatorHeight
)
Text("Card")
.font(.title)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.top)
Text("LINE 2")
.foregroundColor(Color.white)
// Uncomment the following lines, and the slides becomes unusable
// List {
// Text("The Fellowship of the Ring")
// Text("The Two Towers")
// Text("The Return of the King")
//}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The slides built using this example works, but note that I've commented out a few lines containing a list. Once these lines are uncommented, the slider becomes unusable.
Two obvious questions: (1) Why is that, and (2) any suggestions on how to fix it? :)
In your code you are only specifying the height of the rectangle within the card. This isn't an issue if only the rectangle is present. But:
The trouble here is that List wraps its own scrolling behavior, so it expands to take up all the space it can. Rather than capping only the rectangle inside card, cap the height of your card to currentHeight:
func card() -> some View {
ZStack(alignment: .top){
RoundedRectangle(cornerRadius: 16.0)
VStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.frame(
width: 20,
height: 20
)
Text("Card")
.font(.title)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.top)
Text("LINE 2")
.foregroundColor(Color.white)
// Uncomment the following lines, and the slides becomes unusable
List {
Text("The Fellowship of the Ring")
Text("The Two Towers")
Text("The Return of the King")
}
}
}
.frame(maxHeight: currentHeight) // This line
}
The problem you are having is the List is a greedy view. It is taking up ALL of the space it possibly can, which throws your height out of whack. The simple solution is to put it in a frame, that is zero when the card is all the way down. Since we have those values, all we need to do is this:
List {
Text("The Fellowship of the Ring")
Text("The Two Towers")
Text("The Return of the King")
}
.frame(height: currentHeight - DraggableSlides.kMinHeight)
I'm following a SwiftUI tutorial and have made this view that updates with data when a user drags or zooms into an image. After I detoured from the tutorial to round these strings to the nearest hundredth, I noticed this behavior where the text is moving back and forth (and kind of clipping for a second) when it's updating the values. I've tried various combinations of the .frame, lineLimit, minimumScaleFactor modifiers to no avail. The behavior I want is for the system icons to not move and the text to be left aligned against them and then the Text frames should take up all the available space left (and not clip into the text when the text goes from 4 characters long to 5 characters long)
Current Behavior:
InfoPanelView.swift:
...
struct InfoPanelView: View {
var scale: CGFloat
var offset: CGSize
#State private var isInfoPanelVisible: Bool = false
var body: some View {
HStack {
Image(systemName: "circle.circle")
.symbolRenderingMode(.hierarchical)
.resizable()
.frame(width: 30, height: 30)
.onLongPressGesture(minimumDuration: 1) {
withAnimation(.easeOut) {
isInfoPanelVisible.toggle()
}
}
Spacer()
HStack(spacing: 2) {
Image(systemName: "arrow.up.left.and.arrow.down.right")
Text(String(format: "%.2f", scale))
Spacer()
Image(systemName: "arrow.left.and.right")
Text(String(format: "%.2f", offset.width))
Spacer()
Image(systemName: "arrow.up.and.down")
Text(String(format: "%.2f", offset.height))
Spacer()
}
.font(.footnote)
.padding(8)
.background(.ultraThinMaterial)
.cornerRadius(8)
.frame(maxWidth: 420)
.opacity(isInfoPanelVisible ? 1 : 0)
Spacer()
}
}
}
...
I ended up creating a new subview:
struct ExpandingText: View {
var value: CGFloat
var body: some View {
ZStack {
Text(String(describing: (0..<500).map{letter in letter}))
.foregroundColor(Color.clear)
.lineLimit(1)
Text(String(format: "%.2f", value))
.id(value)
.transition(AnyTransition.opacity.animation(.easeInOut(duration:0)))
.multilineTextAlignment(.leading)
.lineLimit(1)
.frame(width: 75, alignment: .leading)
}
}
}
The 500 character invisible string that's limited to 1 line ensures that the ZStack takes up as much width as that text ever could (so the icons no longer move) and the id modifier combined with making the transition override duration being 0 fixes the text box clipping issue.
Edit:
Adding a larger width frame with alignment set to .leading as per xTwisteDx's suggestion makes it left aligned as well
How to make a full-width header using List with InsetGroupedListStyle on iOS?
One way is to use negative padding (as an example in my code), but this doesn't seem like the best solution using fixed value as it may change in the future.
Is there a better way?
Example code:
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Section (header:
VStack {
Text("Header")
.foregroundColor(.white)
}
.frame( // This doesn't remove list's paddings
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.background(Color.red)
// .padding(.horizontal, -16) // This works, but fixed value is not the best solution.
.textCase(nil)
.font(.body)
) {
Text("Hello, world!")
.padding()
}
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You could set the width of the header to the width of the screen, and the padding (x2) you need could be reduced from the width. To can achieve this using a GeometryReader. For example,
// Geometry reader to get the width
GeometryReader { reader in
List {
Section (header:
VStack {
Text("Header")
.foregroundColor(.white)
}
// Setting the frame of the Header to the size of the screen
// Reducing 20 from the width, giving a padding of 10 on each side
.frame(width: reader.size.width - 20, height: 20, alignment: .leading)
.background(Color.red)
.textCase(nil)
.font(.body)
) {
Text("Hello, world!")
.padding()
}
}
.listStyle(InsetGroupedListStyle())
}
You can see the result below
you could try this:
...
{
Text("Hello, world!").padding()
}.headerProminence(.increased) // <--- here
I'm trying to build a component which receives a name and displays it inside of a frame. I want this frame to have the smallest width as possible respecting the paddings and avoiding breaking the line. The limit for the width is 90: after that, the line of text should be broken. I've tried to use the maxWidth attribute of the frame modifier, but my frame gets a fixed 90 value as width. Here is a code snippet of the component:
import SwiftUI
struct ContentView: View {
var name: String
var body: some View {
Text(name)
.font(.system(size: 12))
.background(Color.red)
.padding(.horizontal, 7)
.padding(.vertical, 5)
.background(Color.yellow)
.frame(minWidth: 0, maxWidth: 90)
.background(Color.green)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView(name: "This is a very long name")
.previewDisplayName("Long name")
ContentView(name: "Hi")
.previewDisplayName("Short name")
}
.previewLayout(.sizeThatFits)
}
}
The preview looks like this:
I've also tried to use the minWidth and idealWidth attributes, but the result is always the same. How can I make the width take 90 as a limit instead of a fixed value?
I'm not entirely sure if this is what you want, but if you move the frame(minWidth: 0, maxWidth: 90) modifier to make it the last one than all views early in the modifier chain will have a size that is less than or equal to the maxWidth. Any modifier later in the chain than the frame modifier will not vary in size.
Actually fixedSize by one axis should work, but for some reason it is not, so proposed below only run-time solution (not for static Preview), ie. calculated limit manually and apply on next refresh (to avoid cycling).
struct DemoTightingText: View {
var name: String
#State private var maxWidth: CGFloat = 90
var body: some View {
VStack {
Text(name)
.font(.system(size: 12))
.background(Color.red)
.padding(.horizontal, 7)
.padding(.vertical, 5)
.background(Color.yellow)
.background(GeometryReader {gp -> Color in
DispatchQueue.main.async {
// update on next cycle with calculated width !!!
self.maxWidth = min(gp.size.width, 90)
}
return Color.clear
})
}
.frame(maxWidth: maxWidth)
.background(Color.green)
// .fixedSize(horizontal: true, vertical: false) // this must be work, but don't
}
}
I had a layout that essentially looked like this:
ZStack(alignment: .bottom) {
GeometryReader { geometry in
ZStack {
Text("Centered")
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
.background(Color.red)
}
Group {
GeometryReader { geometry in // This GeometryReader is causing issues.
VStack {
Text("I want this at the bottom")
}
.frame(width: geometry.size.width, height: nil, alignment: .topLeading)
}
}
}
When this is rendered, both Text elements are rendered in the center of the screen. The second text element's container takes up the entire width of the screen, which is intended. If I remove the problematic GeometryReader, then the text is properly rendered at the bottom of the screen, but obviously the frame is not set to the entire width of the screen. Why is this happening?
By default SwiftUI containers tight to content, but GeometryReader consumes maximum of available space. So if to remove second GeometryReader the VStack just wraps internal Text.
If it is still needed to keep second GeometryReader (to read width) and put text to the bottom, the simplest approach would be to add Spacer as below
Group {
GeometryReader { geometry in
VStack {
Spacer()
Text("I want this at the bottom")
}
.frame(width: geometry.size.width, height: nil, alignment: .topLeading)
}
}
Alternate approach of how to stick view at bottom you can find in my answer in this post Position view bottom without using a spacer
How about this?
struct ContentView: View {
var body: some View {
ZStack(alignment: .bottom) {
GeometryReader {geometry in
Text("Centered")
.frame(width: geometry.size.width, height: geometry.size.height)
.background(Color.red)
}
WidthReader {w in
Text("I want this at the bottom").frame(width: w)
}
}
}
}
struct WidthReader<Content: View>: View {
let widthContent: (CGFloat) -> Content
#State private var width: CGFloat = 0
#State private var height: CGFloat = 0
var body: some View {
GeometryReader {g in
widthContent(width).background(
GeometryReader {g1 in
Spacer().onAppear {height = g1.size.height}.onChange(of: g1.size.height, perform: {height = $0})
}
).onAppear {width = g.size.width}.onChange(of: g.size.width, perform: {width = $0})
}.frame(height: height)
}
}
The easiest way is to add the .fixedSize() modifier to your Stack.