Proportional Height (Or Width) in Swiftui

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:

a pink block 43 points tall atop an indigo block 37 points tall atop a mint block 20 points tall

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

demo

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 :

Sample Image

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 :

Sample Image

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

demo

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

demo

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



Leave a reply



Submit