Proportional height (or width) in SwiftUI
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())
SwiftUI Adjusting frame size of VStacks by percent height
Here is calculable relative layout solution based on GeometryReader
(Note: using NSScreen is inappropriate in such case as might introduce expected layout on different models)
Tested with Xcode 12b
struct CardView: View {
var body: some View {
GeometryReader { gp in
VStack {
VStack {
Text("Blue")
}
.frame(width: gp.size.width, height: gp.size.height * 0.7)
.background(Color.blue)
VStack {
Text("Red")
}
.frame(width: gp.size.width, height: gp.size.height * 0.3)
.background(Color.red)
}
}
.frame(height: 280).frame(maxWidth: .infinity)
.cornerRadius(24).padding(.horizontal, 30)
}
}
How to set height and width in proportion with superview in SwiftUI?
You can use GeometryReader
to get that information from the parent:
struct MyView: View {
var body: some View {
GeometryReader { geometry in
/*
Implement your view content here
and you can use the geometry variable
which contains geometry.size of the parent
You also have function to get the bounds
of the parent: geometry.frame(in: .global)
*/
}
}
}
You can have a custom struct for you View
and bind it's geometry to a variable to make it's geometry accessible from out of the View
itself.
- Example:
First define a view called GeometryGetter (giving credit to @kontiki):
struct GeometryGetter: View {
@Binding var rect: CGRect
var body: some View {
return GeometryReader { geometry in
self.makeView(geometry: geometry)
}
}
func makeView(geometry: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.rect = geometry.frame(in: .global)
}
return Rectangle().fill(Color.clear)
}
}
Then, to get the bounds of a Text view (or any other view):
struct MyView: View {
@State private var rect: CGRect = CGRect()
var body: some View {
Text("some text").background(GeometryGetter($rect))
// You can then use rect in other places of your view:
Rectangle().frame(width: 100, height: rect.height)
}
}
How can I scale proportionally in view? Swiftui
You can pass along a scale
factor based on the GeometryReader
that you already have in place.
struct ContentView: View {
var body: some View {
TestView()
}
}
struct Hand: Shape {
let inset: CGFloat
let angle: Angle
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: inset, dy: inset)
var p = Path()
p.move(to: CGPoint(x: rect.midX, y: rect.midY))
p.addLine(to: position(for: CGFloat(angle.radians), in: rect))
return p
}
private func position(for angle: CGFloat, in rect: CGRect) -> CGPoint {
let angle = angle - (.pi/2)
let radius = min(rect.width, rect.height)/2
let xPos = rect.midX + (radius * cos(angle))
let yPos = rect.midY + (radius * sin(angle))
return CGPoint(x: xPos, y: yPos)
}
}
struct TickHands: View {
var scale: Double
@State private var dateTime = Date()
private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Hand(inset: 105 * scale, angle: dateTime.hourAngle)
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
Hand(inset: 70 * scale, angle: dateTime.minuteAngle)
.stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
Hand(inset: 40 * scale, angle: dateTime.secondAngle)
.stroke(Color.orange, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
Circle().fill(Color.orange).frame(width: 10)
}
.onReceive(timer) { (input) in
self.dateTime = input
}
}
}
extension Date {
var hourAngle: Angle {
return Angle (degrees: (360 / 12) * (self.hour + self.minutes / 60))
}
var minuteAngle: Angle {
return Angle(degrees: (self.minutes * 360 / 60))
}
var secondAngle: Angle {
return Angle (degrees: (self.seconds * 360 / 60))
}
}
extension Date {
var hour: Double {
return Double(Calendar.current.component(.hour, from: self))
}
var minutes: Double {
return Double(Calendar.current.component(.minute, from: self))
}
var seconds: Double {
return Double(Calendar.current.component(.second, from: self))
}
}
struct TestView: View {
var clockSize: CGFloat = 500
var body: some View {
GeometryReader { geometry in
ZStack {
let scale = geometry.size.height / 200
TickHands(scale: scale)
ForEach(0..<60*4) { tick in
Ticks.tick(at: tick, scale: scale)
}
}
}.frame(width: clockSize, height: clockSize)
}
}
struct Ticks{
static func tick(at tick: Int, scale: CGFloat) -> some View {
VStack {
Rectangle()
.fill(Color.primary)
.opacity(tick % 20 == 0 ? 1 : 0.4)
.frame(width: 2 * scale, height: (tick % 5 == 0 ? 15 : 7) * scale)
Spacer()
}
.rotationEffect(Angle.degrees(Double(tick)/(60) * 360))
}
}
Note that you may want to change how the scale
effect changes the width
vs the height
-- for example, maybe you want the width to always be 2
. Or, perhaps you want to use something like min(1, 2 * scale)
to prevent the ticks from going above or below a certain size. But, the principal will be the same (ie using the scale
factor). You can also adjust the 200
that I have to something that fits your ideal scaling algorithm.
How to set UIImage's height to always be the same as the width?
If your image is already of a square aspect ratio you can just make it resizable, and select aspectRatio to fit.
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
You can delete .frame(maxWidth: .infinity) because it is intrinsic when you use resizable.
This is the effect obtained :
Obviously if your source Images are not squared, you have to specify the aspect ratio that you want.
UPDATE :
If you want a square colored background with a smaller image in the middle you can't do it directly from an Image object like you did in the Example, but you need a more complex object like this :
struct WidgetView : View
{
var backgroundColor : Color
var imageName : String
var body : some View
{
ZStack {
Rectangle()
.fill(backgroundColor)
.cornerRadius(10)
.aspectRatio(contentMode: .fit)
Image(systemName: imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 50)
.font(.system(size: 30))
}
.padding(5)
}
}
The usage is very simple :
ForEach(0 ... 6, id: \.self) {
WidgetView(backgroundColor: colors[$0 % colors.count], imageName: "plus")
}
And the obtained effect is the following :
SwiftUI change view size by ratio
You can do it with scaleEffect, using single value or a size!
struct mainView: View {
var body: some View {
VStack {
CoolView().scaleEffect(0.6) // <-- 0.6 from original size
CoolView().scaleEffect(0.5) // <-- 0.5 from original size
CoolView().scaleEffect(0.2) // <-- 0.2 from original size
}
}
}
struct mainView: View {
var body: some View {
VStack {
CoolView().scaleEffect(CGSize(width: 0.6, height: 0.6)) // <-- 0.6 from original size
CoolView().scaleEffect(CGSize(width: 0.5, height: 0.5)) // <-- 0.5 from original size
CoolView().scaleEffect(CGSize(width: 0.2, height: 0.2)) // <-- 0.2 from original size
}
}
}
How to set relative width in a HStack embedded in a ForEach in SwiftUI?
Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4
Note: ViewHeightKey
is taken from this another my solution
struct ChildView: View {
let attribute: Attribute
@State private var fitHeight = CGFloat.zero
var body: some View {
GeometryReader { geometry in
HStack(alignment: .top, spacing: 0) {
Text(self.attribute.name)
.bold()
.frame(width: 0.3 * geometry.size.width, alignment: .leading)
.background(Color.yellow)
Text(self.attribute.value)
.fixedSize(horizontal: false, vertical: true)
.frame(width: 0.7 * geometry.size.width, alignment: .leading)
}
.background(Color.red)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height) })
}
.onPreferenceChange(ViewHeightKey.self) { self.fitHeight = $0 }
.frame(height: fitHeight)
}
}
Make SwiftUI Rectangle same height or width as another Rectangle
Here is a working approach, based on view preferences. Tested with Xcode 11.4 / macOS 10.15.6
struct ViewWidthKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
struct ContentView: View {
@State private var boxWidth = CGFloat.zero
var body: some View {
VStack {
HStack {
ZStack {
Rectangle()
.fill(Color.purple)
.frame(width: 20)
Text("1")
.font(.subheadline)
.foregroundColor(.white)
}
ZStack {
Rectangle()
.fill(Color.orange)
Text("2")
.font(.subheadline)
.foregroundColor(.white)
}
.background(GeometryReader {
Color.clear.preference(key: ViewWidthKey.self,
value: $0.frame(in: .local).size.width) })
}
HStack {
ZStack {
Rectangle()
.fill(Color.red)
.frame(height: 20)
Text("3")
.font(.subheadline)
.foregroundColor(.white)
}.frame(width: boxWidth)
}.frame(maxWidth: .infinity, alignment: .bottomTrailing)
}
.onPreferenceChange(ViewWidthKey.self) { self.boxWidth = $0 }
.frame(minWidth: 400, minHeight: 250)
}
}
Related Topics
Setting Contentoffset Programmatically Triggers Scrollviewdidscroll
iOS Autolayout Vertically Equal Space to Fill Parent View
iOS Autolayout to Center My View Between Two Views
This Certificate Was Signed by an Unknown Authority
How to Add Buttons to Navigation Controller Visible After Segueing
Uicollectionview Scrolling Choppy When Loading Cells
Uiview Atop the Keyboard Similar to Imessage App
Show Search Bar in Navigation Bar Without Scrolling on iOS 11
--Resource-Rules Has Been Deprecated in MAC Os X >= 10.10
How to Use Cocoapods When Creating a Cocoa Touch Framework
How to Keep an Iphone/iPad Web App in Full Screen Mode
Can't Set Headers on My Wkwebview Post Request
Connect to Vpn Programmatically in iOS 8
How to Stop Firebase from Logging Status Updates When App Is Launched
Swift:Missing Argument Label 'Xxx' in Call