I started exploring SwiftUI and I can't find a way to get a simple thing: I'd like a View to have proportional height (basically a percentage of its parent's height). Let's say I have 3 views vertically stacked. I want:
The first to be 43% (of its parent's height) high
The second to be 37% (of its parent's height) high
The last to be 20% (of its parent's height) high
I watched this interesting video from the WWDC19 about custom views in SwiftUI (https://developer.apple.com/videos/play/wwdc2019/237/) and I understood (correct me if I'm wrong) that basically a View never has a size per se, the size is the size of its children. So, the parent view asks its children how tall they are. They answer something like: "half your height!" and then... what? How does the layout system (that is different from the layout system we are used to) manage this situation?
If you write the below code:
struct ContentView : View {
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.red)
Rectangle()
.fill(Color.green)
Rectangle()
.fill(Color.yellow)
}
}
}
The SwiftUI layout system sizes each view to be 1/3 high and this is right according to the video I posted here above.
You can wrap the rectangles in a frame this way:
struct ContentView : View {
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.red)
.frame(height: 200)
Rectangle()
.fill(Color.green)
.frame(height: 400)
Rectangle()
.fill(Color.yellow)
}
}
}
This way the layout system sizes the first rectangle to be 200 high, the second one to be 400 high and the third one to fit all the left space. And again, this is fine.
What you can't do (this way) is specifying a proportional height.
UPDATE
If your deployment target at least iOS 16, macOS 13, tvOS 16, or watchOS 9, you can write a custom Layout. For example:
import SwiftUI
struct MyLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
return proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
precondition(subviews.count == 3)
var p = bounds.origin
let h0 = bounds.size.height * 0.43
subviews[0].place(
at: p,
proposal: .init(width: bounds.size.width, height: h0)
)
p.y += h0
let h1 = bounds.size.height * 0.37
subviews[1].place(
at: p,
proposal: .init(width: bounds.size.width, height: h1)
)
p.y += h1
subviews[2].place(
at: p,
proposal: .init(
width: bounds.size.width,
height: bounds.size.height - h0 - h1
)
)
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(MyLayout {
Color.pink
Color.indigo
Color.mint
}.frame(width: 50, height: 100).padding())
Result:
Although this is more code than the GeometryReader solution (below), it can be easier to debug and to extend to a more complex layout.
ORIGINAL
You can make use of GeometryReader. Wrap the reader around all other views and use its closure value metrics to calculate the heights:
let propHeight = metrics.size.height * 0.43
Use it as follows:
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { metrics in
VStack(spacing: 0) {
Color.red.frame(height: metrics.size.height * 0.43)
Color.green.frame(height: metrics.size.height * 0.37)
Color.yellow
}
}
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
Related
I want to achieve the following constraint-based layout of images in a SwiftUI List:
pin left/right edges of each Image to the list margins (adapts to screen size)
dynamic height based on aspect ratio (I'd also be ok with fixed height)
maintain image aspect ratio, and content should fill the space
What I have tried and doesn't work (based on this article):
struct MyView: View {
#ObservedObject var viewModel: MyViewModel
let aspectRatio = CGSize(width: 345, height: 120)
var body: some View {
List {
ForEach(viewModel.items) { item in
GeometryReader { geo in
Image("test_image")
.resizable()
.aspectRatio(aspectRatio, contentMode: .fill)
.frame(width: geo.size.width)
.clipped()
}
}
}
}
}
The size I get from geo is (343, 32) on iPhone 11 Pro. Width makes sense but it's not letting the cells expand beyond a height of 32 for some reason. Any tips welcome because I'm really starting to miss auto layout constraints.
No need to use GeometryReader for something like this. For the fixed height, you can just supply a frame with height only. You also don't need to create your own let aspectRatio = CGSize(width: 345, height: 120) - if you leave it nil (by default) it should be fine.
Edit: Using padding instead of VStack with spacing
struct MyView: View {
var body: some View {
List {
ForEach(0..<10, id: \.self) { item in
Image("test_image")
.resizable()
.aspectRatio(contentMode: .fill) /// no need for custom aspect ratio
.frame(height: 120) /// fixed height of image
.clipped() /// stop image from overflowing
.padding(.vertical, 12) /// extra vertical padding
}
}
}
}
Result (with "test_image"):
However, this has a fixed height of 120, so the top and bottom of the images are cropped out. To fix this, you can just avoid frame and clipped altogether.
struct MyView: View {
var body: some View {
List {
ForEach(0..<10, id: \.self) { item in
Image("test_image")
.resizable()
.aspectRatio(contentMode: .fill) /// doesn't matter if it's fit or fill
.padding(.vertical, 12) /// extra vertical padding
}
}
}
}
Result:
I am trying to detect when this text view has been swiped on. The code compiles fine, but I am not able to trigger the swipe on my actual device. When I swipe, nothing happens. Tap seems to work just fine. Can anyone let me know what I'm doing wrong in my code?
In case this matters, I'm developing a watch OS app in swift 5.3 with the latest Xcode.
var body: some View {
Text(tempstring).onTapGesture { checkStateRoll() }
.frame(maxWidth: .infinity, maxHeight: .infinity)
.gesture(DragGesture(minimumDistance: 10, coordinateSpace: .global)
.onEnded { value in
let horizontalAmount = value.translation.width as CGFloat
let verticalAmount = value.translation.height as CGFloat
if abs(horizontalAmount) > abs(verticalAmount) {
horizontalAmount < 0 ? leftswipe() : rightswipe()
} else {
verticalAmount < 0 ? upswipe() : downswipe()
}
tempstring = String(numdice) + "d" + String(typesofdice[typedice])
speaknumber()
} )
.background(progstate == 2 ? Color.blue : Color.red)
}
}
Thanks a lot for any help. This has been stumping me for weeks.
A couple of things:
With respect to layout, setting a frame will no change the text size because the text sizes itself to its content. Use a greedy view like a ZStack if you want to take up all of the space.
The sequence of the modifiers does matter. You read the order in which they are applied from bottom to top, so in this case they need to go on the stack.
Here is an example playground:
import SwiftUI
import PlaygroundSupport
struct V: View {
var body: some View {
ZStack {
Color.red
Text("Test")
}
.onTapGesture { print("tap") }
.gesture(DragGesture(minimumDistance: 10, coordinateSpace: .global).onEnded { print($0)})
}
}
let host = UIHostingController(rootView: V().frame(width: 500.0, height: 500.0))
host.preferredContentSize = CGSize(width: 300, height: 300)
PlaygroundPage.current.liveView = host
I'm need to set an absolute position for my view.
A position modifier is positioning a view in regards to it center but I'd like to align it in regards to its leading edge.
Is there any way to achieve that?
I was trying to play with alignmentGuide but had no luck with that.
Here's my sample code:
var body: some View {
ZStack {
Text("Hello, this is a long text.")
.border(Color.blue, width: 1)
.position(x: 10, y: 50)
}
.frame(width: 200, height: 100)
}
Red dashed line is showing how I want the text to be positioned.
You can apply a GeometryReader to the Text's background to read out the correct width and then add half of it to the x position.
Just add some variable to safe the width:
#State private var textWidth: CGFloat?
And then apply the rectReader to the Text's background and add the Text's width to the x position:
Text("Hello, this is a long text.")
.border(Color.blue, width: 1)
.background(rectReader())
.position(x: 10 + (textWidth ?? 0) / 2, y: 50)
That's the rectReader:
private func rectReader() -> some View {
return GeometryReader { geometry -> AnyView in
DispatchQueue.main.async {
self.textWidth = geometry.size.width
}
return AnyView(Rectangle().fill(Color.clear))
}
}
I'm playing with SwiftUI animations. I wanted to make a square bigger modifying its frame with an animation this way:
struct ContentView: View {
let squareWidth = CGFloat(100)
let squareHeight = CGFloat(100)
#State private var bigger = false
var body: some View {
VStack {
Color.green
.frame(width: bigger ? self.squareWidth * 2 : self.squareWidth, height: self.squareHeight)
.animation(.default)
Button(action: {
self.bigger.toggle()
}) {
Text("Click me!")
}
}
}
}
The animation happens from the centre of the View, that is the anchor point (0.5, 0.5). Is there a way to change this anchor point? I found out that I can use scaleEffect on the view specifying an anchor:
struct ContentView: View {
let squareWidth = CGFloat(100)
let squareHeight = CGFloat(100)
#State private var bigger = false
var body: some View {
VStack {
Color.green
.frame(width: self.squareWidth, height: self.squareHeight)
.scaleEffect(x: bigger ? 2 : 1, y: 1, anchor: .leading)
.animation(.default)
Button(action: {
self.bigger.toggle()
}) {
Text("Click me!")
}
}
}
}
But it doesn't seem exactly the same. If I needed to apply several effects I should specify the anchor point for each of them, instead of specifying the anchor point on the view just one time. On UIKit I could write:
var myView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
myView.layer.anchorPoint = CGPoint(x: 0, y: 0.5) //that is, the .leading of SwiftUI
It is possible, but your approach must change.
There is an implied, built-in centring of views in SwiftUI, which can be confusing.
The parent view is the one placing the content in the centre, recalculating the position of its subviews each time their frames change.
Embedding the VStack in a HStack and adding borders to both clearly shows this behaviour.
Also, note the Spacer(), which pushes the view to the left.
HStack {
VStack {
Color.green
.frame(width: bigger ? self.squareWidth * 2 : self.squareWidth)
.frame(height: self.squareHeight)
.animation(.default)
Button("Click me!") { self.bigger.toggle() }
}.border(Color.black)
Spacer()
}.border(Color.red)
Using the .layer.anchorPoint in UIKit should have the same effect as using scaleEffect/offset/rotationEffect in SwiftUI, as it should affect the origin of the underlying geometry/texture which is being rendered by the GPU.
If you have a complex view (composed of multiple subviews) that needs the same scale effect, you can group them using a Group { } and use .scaleEffect on it.
I want to show an image in a View, where the image source will vary in size.
Is there a way to dynamically adjust the size of the image to the width of the containing view or get the screen size even?
I have a horizontal scrollview with each element being a Stack of an image and a text. I would like to size every image depending on the screen width, since I want to be independent of the device.
How would you do that in SwiftUI?
Thanks!
struct ImageRow: View {
var items: [ImageWithDescription]
var body: some View {
ScrollView(showsHorizontalIndicator: false) {
HStack(alignment: .bottom){
ForEach(self.items.identified(by: \.name)) { item in
Spacer()
VStack {
Image(uiImage: item.image ?? UIImage())
.resizable()
.frame(minWidth: (item.image.size.width) / 3, height: (item.image.size.height) / 3)
.cornerRadius(20, antialiased: true)
.clipShape(Rectangle())
.shadow(radius: 10)
Text(item.name)
.color(.primary)
.font(.subheadline)
}
}
.padding()
}
}
.frame(height: 300)
}
}
You can get the size of the screen (or window, in case you are using an iPad with split screen).
In the example below, the top view uses GeometryReader to get the size and passes it down the hierarchy. The subview uses that data to create a blue rectangle with half the size of the screen. It will also adapt if the device is rotated.
GeometryReader has many features, and although they are barely documented, I've written an article which fully explains how to use them: https://swiftui-lab.com/geometryreader-to-the-rescue/
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
MySubview(size: geometry.size)
}
}
}
struct MySubview: View {
let size: CGSize
var body: some View {
print("size = \(size)")
return Color.blue.frame(width: size.width / 2, height: size.height / 2).fixedSize()
}
}