SwiftUI - setting the frame of view - swift

When I learn new stuff like SwiftUI (beta 6)
I want to start from the basic.
I just want to set frame to subview like in UIKit.
What I'm missing here ? (this is from Simulator)
1. the subview is not in 0,0 position.
2. why at least the start of the word is not inside the border ?
UPDATE :
how to set the text view in 0,0 position ? (just like on UIKit)
I thought my question is very clear, but for some reason, it's not.

I think it's important to understand why your solution doesn't work because at a first glance it seems correct and it seems that SwiftUI works in some weird ways (because, of course, we are all used to UIKit).
You tried:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
.position(CGPoint(x: 0, y: 0))
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
}
}
}
And you got:
First of all, the position modifier says:
Fixes the center of the view at the specified point in its parent’s
coordinate space.
Two things are important here:
The view is moved based on its centre, not based on its top-left corner
The view is moved in the parent's coordinate space
But who is the Text's parent? A view modifier in SwiftUI is something that applies to a View and returns a View. Modifiers are applied from the last one to the first one (in reverse order respect to how you see them). In your case:
So: The centre of the Text is positioned at (0,0) respect to a Frame 50x100 with a red Border. The resulting View is placed in the centre of the screen because of the VStack (it's the VStack default behaviour). In other words: the position's parent (position returns a View, every modifier returns a View) is the Frame 50x100 placed in the centre of the screen.
If you want to position the top-left corner of the Text at (0,0) in the Frame coordinate space you should use the Spacer modifier this way:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
Spacer()
}
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
}
}
And you'll get:
If you want, instead, the top-left corner of the Frame to be at (0,0) respect to the whole View I think the simplest way is:
struct ContentView: View {
var body: some View {
HStack {
VStack {
Text("Hello World")
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
Spacer()
}
Spacer()
}
.edgesIgnoringSafeArea(.all)
}
}
And you'll get:

Do Like this way
struct ContentView: View {
var body: some View {
VStack{
Text("Hello World")
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
.padding()
Spacer()
}
}
}
Below is output
if you want to remove space on top add .edgesIgnoringSafeArea(.top) for your view like below
struct ContentView: View {
var body: some View {
VStack{
Text("Hello World")
.frame(width: 50, height: 100)
.border(Color.red, width: 4)
.padding()
Spacer()
}
.edgesIgnoringSafeArea(.top)
}
}

Related

Resize cursors are missing in `NSCursor`

I'm writing a resizable view in SwiftUI on macOS. Drag its edge can change its size. I'd like to show according cursor when dragging different edges. So I looked up Apple NSCursor documentation, however, I cannot find resize cursors like those generally used in system as shown below.
How can I get those cursors? I attached my code below in which drag the Divider() to resize the cyan rectangle. Hovering on that Divider() will change the cursor.
struct ContentView: View {
#AppStorage("InspectorHeight") var inspectorHeight = 200.0
var body: some View {
ZStack(alignment: .top) {
Color.cyan
.frame(width: 100)
.frame(height: CGFloat(inspectorHeight))
Divider()
.padding(.vertical, 2)
.onHover { inside in
if inside {
NSCursor.resizeUpDown.push()
} else {
NSCursor.pop()
}
}
.gesture(
DragGesture()
.onChanged { drag in
inspectorHeight = max(100, inspectorHeight - Double(drag.translation.height))
}
)
}
.frame(height: 600, alignment: .bottom)
.padding()
}
}
The system cursors you're looking for don't exist, so you'll have to create custom ones, which you should be able to approximate using SFSymbols, e.g.:
NSCursor(image: NSImage(systemSymbolName: "arrow.up.and.down", accessibilityDescription: nil)!, hotSpot: NSPoint(x: 8, y: 8)).push()

SwiftUI: disable interactions on transparent parts of ScrollView

Assuming I have a horizontal ScrollView, where the first item has a view a lot taller than the rest. Is there any way to limit the interaction to the visible elements (black rectangles in the image below)?
ScrollView(.horizontal) {
HStack(alignment: .bottom) {
VStack(alignment: .center, spacing: 1) {
Rectangle()
.frame(width: 20, height: 70)
Rectangle()
.frame(width: 40, height: 40)
}
ForEach((1...10), id: \.self) { _ in
Rectangle()
.frame(width: 40, height: 40)
}
}
}
.background(.yellow)
I don't want to have the invisible area here blocking everything beneath that view, so everything in the red area should not scroll on the scrollview.
In my case, there’s a zoomable ScrollView in horizontal and vertical direction behind it and text all over it that can be tapped on (live text image view).
Here are my ideas, I was either not smart enough to properly implement them or it's simply not possible:
allowsHitTesting(false) on ScrollView and enable on the child views: child views can't be enabled if parent is off
.contentShape(...) on ScrollView: doesn't disable scrolling, also I found no way to manually draw the contentshape around the content
move the first item down and use .offset(y: -300) on the first item to move it back up: item is clipped outside the ScrollView
use UIViewRepresentable with UIScrollView and gestureRecognizerShouldBegin(...) to let all gestures on that area through: I don't know how to implement that (would this be possible?)
move the first item outside the ScrollView: in theory works, but not ideal since the item no longer scrolls and I couldn't get the animations of reordering right
detach the "upper" view, track the position of the item inside the ScrollView and move the upper view manually: probably would lag behind, also not a very elegant solution
Is one of them actually possible? Any other ideas? Thanks :)
How about something like this:
ZStack(alignment: .top) {
ScrollView(.horizontal) {
HStack(alignment: .bottom) {
Rectangle()
.frame(width: 20, height: 90)
ForEach((1...15), id: \.self) { _ in
Rectangle()
.frame(width: 20, height: 20)
}
}
}
.background(.yellow)
Text("test")
.frame(width: 100, height: 50)
.background(.red)
}
Text is in front of you ScrollView and can't be hit, thus not triggering your ScrollView. From your question, it's hard to guess, what content lies behind your ScrollView and if any further culprits may apply.

SwiftUI layout in VStack where one child is offered a maxHeight but can use less than that height

I'm trying to build a layout inside a VStack that contains two children. The first child should take up all available space unused by the second child. The second child has a preferred size based on its own contents. I'd like to limit the height of the second child to a maximum height, but it should be able to take less than the maximum (when its own contents cannot make use of all the height). This should all be responsive to the root view size, which is the parent of the VStack (because the device can rotate).
My attempt uses the .frame(maxHeight: n) modifier, which seems to unconditionally takes up the entire n points of height, even when the view being modified doesn't use it. This results in whitespace rendered above and below the VStack's second child. This problem is shown in the Portrait preview below - the hasIdealSizeView only has a height of 57.6pts, but the frame that wraps that view has a height of 75pts.
import SwiftUI
struct StackWithOneLimitedHeightChild: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
fullyExpandingView
hasIdealSizeView
.frame(maxHeight: geometry.size.height / 4)
}
}
}
var fullyExpandingView: some View {
Rectangle()
.fill(Color.blue)
}
var hasIdealSizeView: some View {
HStack {
Rectangle()
.aspectRatio(5/3, contentMode: .fit)
Rectangle()
.aspectRatio(5/3, contentMode: .fit)
}
// the following modifier just prints out the resulting height of this view in the layout
.overlay(alignment: .center) {
GeometryReader { geometry in
Text("Height: \(geometry.size.height)")
.font(.system(size: 12.0))
.foregroundColor(.red)
}
}
}
}
struct StackWithOneLimitedHeightChild_Previews: PreviewProvider {
static var previews: some View {
Group {
StackWithOneLimitedHeightChild()
.previewDisplayName("Portrait")
.previewLayout(PreviewLayout.fixed(width: 200, height: 300))
StackWithOneLimitedHeightChild()
.previewDisplayName("Landscape")
.previewLayout(PreviewLayout.fixed(width: 300, height: 180))
}
}
}
This observed result is consistent with how the .frame(maxHeight: n) modifier is described in the docs and online blog posts (the flow chart here is extremely helpful). Nonetheless, I can't seem to find another way to build this type of layout.
Related question: what are the expected use cases for .frame(maxHeight: n)? It seems to do the opposite of what I'd expect by unconditionally wrapping the view in a frame that is at least n points in height. It seems no different than .frame(height: n), using an explicit value for the offered height.
The behavior of .minHeight in this example is strange and far from intuitive. But I found a solution using a slightly different route:
This defines the minHeight for the expanding view (to get the desired layout in portrait mode), but adds a .layoutPriority to the second, making it define itself first and then give the remaining space to the upper view.
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
fullyExpandingView
.frame(minHeight: geometry.size.height / 4 * 3)
hasIdealSizeView
.layoutPriority(1)
}
}
}
There's probably a really short way to go about this but in the meantime here is what I did.
Firstly I created a struct for your hasIdealSizeView and I made it return a GeometryProxy, and with that i could return the height of the HStack, in this case, the same height you were printing on to the Text View. then with that I used the return proxy to check if the height is greater than the maximum, and if it is, i set it to the maximum, otherwise, set the height to nil, which basically allows the native SwiftUI flexible height:
//
// ContentView.swift
// Test
//
// Created by Denzel Anderson on 3/16/22.
//
import SwiftUI
struct StackWithOneLimitedHeightChild: View {
#State var viewHeight: CGFloat = 0
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
fullyExpandingView
.overlay(Text("\(viewHeight)"))
// GeometryReader { geo in
hasIdealSizeView { proxy in
viewHeight = proxy.size.height
}
.frame(height: viewHeight > geometry.size.height / 4 ? geometry.size.height / 4:nil)
}
.background(Color.green)
}
}
var fullyExpandingView: some View {
Rectangle()
.fill(Color.blue)
}
}
struct hasIdealSizeView: View {
var height: (GeometryProxy)->()
var body: some View {
HStack {
Rectangle()
.fill(.white)
.aspectRatio(5/3, contentMode: .fit)
Rectangle()
.fill(.white)
.aspectRatio(5/3, contentMode: .fit)
}
// the following modifier just prints out the resulting height of this view in the layout
.overlay(alignment: .center) {
GeometryReader { geometry in
Text("Height: \(geometry.size.height)")
.font(.system(size: 12.0))
.foregroundColor(.red)
.onAppear {
height(geometry)
}
}
}
}
}

Is there a way to eliminate outside edge of padding in SwiftUI?

So I've noticed when using padding() it creates an outside space/edge outside a view frame. Is there a way to eliminate that outside edge?
Here is .padding(.top, 0)
Here is .padding(.top, 1) the extra outside edge has appeared
Here is .padding(.top, 10) the outside edge remains on any padding above 1px from what I see.
import SwiftUI
struct TestView: View {
var body: some View {
VStack {
Text("")
.frame(width: 300, height: 20)
.background(Color(.black))
.cornerRadius(10)
Text("")
.frame(width: 300, height: 20)
.background(Color(.black))
.cornerRadius(10)
.padding(.top, 0)//here you can change the 0 to 1
}
}
}
The reason this happens is because the VStack automatically has its own spacing. Replace:
VStack {
/* ... */
}
with:
VStack(spacing: 0) {
/* ... */
}
This removes padding between each view within the VStack. I am assuming SwiftUI makes an assumption that if you want 0 padding, you want them touching, otherwise you likely want padding from within its own space within the VStack.

How to use GeometryReader within a LazyVGrid

I'm building a grid with cards which have an image view at the top and some text at the bottom. Here is the swift UI code for the component:
struct Main: View {
var body: some View {
ScrollView {
LazyVGrid(columns: .init(repeating: .init(.flexible()), count: 2)) {
ForEach(0..<6) { _ in
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.random))
VStack {
Rectangle()
.frame(minHeight: 72)
Text(ipsum)
.fixedSize(horizontal: false, vertical: true)
.padding()
}
}.clipShape(RoundedRectangle(cornerRadius: 10))
}
}.padding()
}.frame(width: 400, height: 600)
}
}
This component outputs the following layout:
This Looks great, but I want to add a Geometry reader into the Card component in order to scale the top image view according to the width of the enclosing grid column. As far as I know, that code should look like the following:
struct Main: View {
var body: some View {
ScrollView {
LazyVGrid(columns: .init(repeating: .init(.flexible()), count: 2)) {
ForEach(0..<6) { _ in
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.random))
VStack {
GeometryReader { geometry in
Rectangle()
.frame(minHeight: 72)
Text(ipsum)
.fixedSize(horizontal: false, vertical: true)
.padding()
}
}
}.clipShape(RoundedRectangle(cornerRadius: 10))
}
}.padding()
}.frame(width: 400, height: 600)
}
}
The trouble is that this renders as the following:
As you can see, I'm not even trying to use the GeometryReader, I've just added it. If I add the geometry reader at the top level, It will render the grid correctly, however this is not of great use to me because I plan to abstract the components into other View conforming structs. Additionally, GeometryReader seems to be contextually useful, and it wouldn't make sense to do a bunch of math to cut the width value in half and then make my calculations from there considering the geometry would be from the top level (full width).
Am I using geometry reader incorrectly? My understanding is that it can be used anywhere in the component tree, not just at the top level.
Thanks for taking a look!
I had the same problem as you, but I've worked it out. Here's some key point.
If you set GeometryReader inside LazyVGrid and Foreach, according to SwiftUI layout rule, GeometryReader will get the suggested size (may be just 10 point). More importantly, No matter what subview inside GeometryReader, it wouldn't affect the size of GeometryReader and GeometryReader's parent view.
For this reason, your view appears as a long strip of black. You can control height by setting GeometryReader { subView }.frame(some size),
Generally, we need two GeometryReader to implement this. The first one can get size and do some Computing operations, then pass to second one.
(Since my original code contains Chinese, it may be hard for you to read, so I can only give a simple structure for you.)
GeometryReader { firstGeo in
LazyVGrid(columns: rows) {
ForEach(dataList) { data in
GeometryReader { secondGeo in
// subview
}
.frame(width: widthYouWantSubViewGet)
}
}
}
I just started to learn swift for a week. There may be some mistakes in my understanding. You are welcome to help correct it.