Best way to set a dynamic frame for all devices - swiftui

What is the best way to avoid hardcoding a .frame
.frame(width: 200, height: 200)
and make the view dynamic and responsive to all screen sizes. My question is specific for iOS devices.
I'm sure there you guys do not need any code to answer this question but just to make things clear, here is an example. I'd like to make this Rectangle() dynamic to all screen sizes.
import SwiftUI
struct VideoPlayerView: View {
var body: some View {
Rectangle()
}
}
struct VideoPlayerView_Previews: PreviewProvider {
static var previews: some View {
VideoPlayerView()
}
}

You can use a GeometryReader to get view dimensions. Here is an example:
struct VideoPlayerView: View {
var body: some View {
GeometryReader { g in
Rectangle()
.frame(width: g.size.width / 2, height: g.size. height / 2)
}
}
}

Related

Swiftui ScrollView is affected unexpectedly by scaleEffect modifier

I have some views with many details with fixed sizes, and am trying to use scaleEffect() to reduce them proportionally to fit better smaller devices. However, when using scaleEffect() on a ScrollView, I noticed that it has a larger effect than expected on the axis of the ScrollView. Small example below:
import SwiftUI
struct FancyItemView: View {
var body: some View {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
}
}
struct ItemDisplayView: View {
var sizeAdjustment: Double
var body: some View {
ScrollView(.horizontal){
FancyItemView()
}
.background(.blue)
.scaleEffect(sizeAdjustment)
.frame(width: 150 * sizeAdjustment, height: 100 * sizeAdjustment)
.border(.black)
}
}
struct ContentView: View {
var body: some View {
VStack{
ItemDisplayView(sizeAdjustment: 1)
ItemDisplayView(sizeAdjustment: 0.8)
ItemDisplayView(sizeAdjustment: 1.2)
}
.background(.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Screenshot of the resulting view: https://i.stack.imgur.com/POvjw.png
In this example I am using only one item view, but in my real code the ScrollView contains titles and grids of items. I may be able to work around this issue by applying scaleEffect to the other views around ScrollView and not applying to it, but that would make the code much more confusing. So I am wondering if there is anything I am missing to make scaleEffect work properly with ScrollView.
Thanks
I don´t think .scaleEffect is the propper tool here. It is more for visual presentation/animation than for laying out views. Get rid of the .scaleEffect modifier and pass your scale var through to your Controll and style it appropriatly.
struct FancyItemView: View {
var sizeAdjustment: Double
var body: some View {
Rectangle()
.fill(.red)
.frame(width: 100 * sizeAdjustment, height: 100 * sizeAdjustment)
}
}
struct ItemDisplayView: View {
var sizeAdjustment: Double
var body: some View {
ScrollView(.horizontal){
FancyItemView(sizeAdjustment: sizeAdjustment) // pass the multiplier to the ChildView
}
.background(.blue)
// .scaleEffect(sizeAdjustment) // remove this
// .frame(width: 150 * sizeAdjustment, height: 100 * sizeAdjustment) // you probably don´t want this either
// or at least get rid of the multiplier
.border(.black)
}
}

SwiftUI - How can I use ObservedObject or EnvironmentObject to store GeometryReader data?

I am trying to follow the design created for an app which has some objects placed in the middle of the screen.
The objects should have a size and padding proportional to the device's screen size, meaning they should appear bigger if the screen is bigger than the screen we take as a base in the design (the base is an iPhone 11 screen in this case). In addition, these objects have more objects inside, which should also be proportional to the screen size. For example: a Text view placed whithin the borders of a RoundedRectangle for which the font should grow if the screen is bigger than the screen used as a base; or an image inside another image. In these examples, the object and the objects inside of it should all be proportional to the screen size.
So far, we are using GeometryReader to accomplish this. The way we are doing it needs us to use GeometryReader in each file we have defined for a screen and its views. Once we have GeometryReader data, we use the Scale struct to get the correct proportions for the objects.
Here is the sample code:
GeometryReaderSampleView.swift
import SwiftUI
struct GeometryReaderSampleView: View {
var body: some View {
NavigationView {
GeometryReader { metrics in
ZStack {
VStack {
LoginMainDecorationView(Scale(geometry: metrics))
Spacer()
}
VStack {
HStack {
GreenSquareView(Scale(geometry: metrics))
Spacer()
}
.offset(x: 29, y: Scale(geometry: metrics).vertical(300.0))
Spacer()
}
}
}
}
}
}
struct GreenSquareView: View {
let scale:Scale
init (_ scale:Scale) {
self.scale = scale
}
var body: some View {
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: scale.horizontal(30))
.fill(Color.green)
.frame(width: scale.horizontal(157), height: scale.horizontal(146))
Text("Here goes\nsome text")
.font(.custom("TimesNewRomanPS-ItalicMT", size: scale.horizontal(20)))
.padding(.top, scale.horizontal(29))
.padding(.leading, scale.horizontal(19))
VStack {
Spacer()
HStack {
Spacer()
Image(systemName: "heart.circle")
.resizable()
.frame(width: scale.horizontal(20), height: scale.horizontal(20))
.offset(x: scale.horizontal(-20), y: scale.vertical(-17.0))
}
}.frame(width: scale.horizontal(157), height: scale.horizontal(146))
}
}
}
struct LoginMainDecorationView: View {
let scale:Scale
init (_ scale:Scale) {
self.scale = scale
}
var body: some View {
HStack {
Image(systemName: "cloud.rain")
.resizable()
.frame(width: scale.horizontal(84), height: scale.horizontal(68), alignment: .leading)
.offset(x: 0, y: scale.vertical(200.0))
Spacer()
Image(systemName: "cloud.snow")
.resizable()
.frame(width: scale.horizontal(119), height: scale.horizontal(91), alignment: .trailing)
.offset(x: scale.horizontal(-20.0), y: scale.vertical(330.0))
}
}
}
struct GeometryReaderSampleView_Previews: PreviewProvider {
static var previews: some View {
GeometryReaderSampleView()
}
}
Scale.swift
import SwiftUI
struct Scale {
// Size of iPhone 11 Pro
let originalWidth:CGFloat = 375.0
let originalHeight:CGFloat = 734.0
let horizontalProportion:CGFloat
let verticalProportion:CGFloat
init(screenWidth:CGFloat, screenHeight:CGFloat) {
horizontalProportion = screenWidth / originalWidth
verticalProportion = screenHeight / originalHeight
}
init(geometry: GeometryProxy) {
self.init(screenWidth: geometry.size.width, screenHeight: geometry.size.height)
}
func horizontal(_ value:CGFloat) -> CGFloat {
return value * horizontalProportion
}
func vertical(_ value:CGFloat) -> CGFloat {
return value * verticalProportion
}
}
The question / request
I would like to simplify this code and store the GeometryReader data (the Scale struct with its info) in an ObservedObject or an EnvironmentObject so that we can use it in different views and files all over the project. The problem with this is that we cannot get GeometryReader data until the view is loaded, and once the view is loaded I believe we cannot declare ObservedObject or EnvironmentObject anymore (is that correct?).
I know there could be a way to get the screen size without using GeometryReader as shown here: How to get the iPhone's screen width in SwiftUI?. But if I used GeometryReader to get the size of a view that is inside another view, I would like to have its information stored as well.
The goal would be not to use this code inside each view that needs to use scale:
let scale:Scale
init (_ scale:Scale) {
self.scale = scale
}
and instead use ObservedObject or EnvironmentObject to get the scale data from the views that need it. Therefore, how can I use ObservedObject or EnvironmentObject to store GeometryReader data?
I tend to think that you're fighting the general principals of SwiftUI a little by doing this (ie basing things on screen sizes rather than using the built-in SwiftUI layout principals that are screen size independent like padding). Assuming you want to go forward with the plan, though, I'd recommend using an #Envrionment value. I don't think it needs to be an #EnvironmentObject, since Scale is a struct and there's no compelling reason to have a reference-type to box the value.
Here's a simple example:
private struct ScaleKey: EnvironmentKey {
static let defaultValue = Scale(screenWidth: -1, screenHeight: -1)
}
extension EnvironmentValues {
var scale: Scale {
get { self[ScaleKey.self] }
set { self[ScaleKey.self] = newValue }
}
}
struct ContentView: View {
var body: some View {
GeometryReader { metrics in
SubView()
.environment(\.scale, Scale(geometry: metrics))
}
}
}
struct SubView : View {
#Environment(\.scale) private var scale : Scale
var body: some View {
Text("Scale: \(scale.horizontal(1)) x \(scale.vertical(1))")
}
}

Determine padding for correctly placing a Button in the top-trailing corner?

In an application that hides the status bar, is there something in SwiftUI that helps with correctly placing a button in the top-trailing corner?
The padding necessary for it to look correct seems dependent on the device type (with or without notch / "corner radius" of the display varying with the device size). There will be no safe area insets on the top with the status bar hidden.
How can the correct padding for such a button be determined? Is there a better way than to check for specific device types / screen sizes?
Example code:
// SwiftUIPlayground
// https://github.com/ralfebert/SwiftUIPlayground/
import Foundation
import SwiftUI
struct CloseButtonExampleView: View {
var body: some View {
Color.yellow
.ignoresSafeArea()
.overlay(self.closeButton, alignment: .topTrailing)
}
#ViewBuilder var closeButton: some View {
Button(
action: {},
label: {
ZStack {
Circle()
.foregroundColor(.green)
.frame(width: 30, height: 30)
Image(systemName: "xmark")
.foregroundColor(.white)
}
}
)
.padding(16) // <-- what's the correct value?
}
}
struct CloseButtonExampleView_Previews: PreviewProvider {
static var previewView: some View {
CloseButtonExampleView()
}
static var previews: some View {
previewView
.previewDevice(PreviewDevice(rawValue: "iPhone SE (2nd generation)"))
previewView
.previewDevice(PreviewDevice(rawValue: "iPhone 12"))
previewView
.previewDevice(PreviewDevice(rawValue: "iPhone 12 Pro Max"))
}
}
I assume the correct is not hard code any value there at all, so just using
#ViewBuilder var closeButton: some View {
Button(
// .. other code here
)
.padding() // << default !!
.ignoresSafeArea() // << !!
}
gives (tested with Xcode 12.5 / iOS 14.5)

SwiftUI ScrollView VStack GeometryReader height ignored

I want to use a ScrollView outside of a VStack, so that my content is scrollable if the VStack expands beyond screen size.
Now I want to use GeometryReader within the VStack and it causes problems, which I can only solve by setting the GeometryReader frame, which does not really help me given that I use the reader to define the view size.
Here is the code without a ScrollView and it works nicely:
struct MyExampleView: View {
var body: some View {
VStack {
Text("Top Label")
.background(Color.red)
GeometryReader { reader in
Text("Custom Sized Label")
.frame(width: reader.size.width, height: reader.size.width * 0.5)
.background(Color.green)
}
Text("Bottom Label")
.background(Color.blue)
}
.background(Color.yellow)
}
}
This results in the following image:
The custom sized label should be full width, but half the width for height.
Now if I wrap the same code in a ScrollView, this happens:
Not just did everything get smaller, but the height of the Custom Sized Label is somehow ignored.
If I set the height of the GeometryReader, I can adjust that behaviour, but I want to GeometryReader to grow as large as its content. How can I achieve this?
Thanks
It should be understood that GeometryReader is not a magic tool, it just reads available space in current context parent, but... ScrollView does not have own available space, it is zero, because it determines needed space from internal content... so using GeometryReader here you have got cycle - child asks parent for size, but parent expects size from child... SwiftUI renderer somehow resolves this (finding minimal known sizes), just to not crash.
Here is possible solution for your scenario - the appropriate instrument here is view preferences. Prepared & tested with Xcode 12 / iOS 14.
struct DemoLayout_Previews: PreviewProvider {
static var previews: some View {
Group {
MyExampleView()
ScrollView { MyExampleView() }
}
}
}
struct MyExampleView: View {
#State private var height = CGFloat.zero
var body: some View {
VStack {
Text("Top Label")
.background(Color.red)
Text("Custom Sized Label")
.frame(maxWidth: .infinity)
.background(GeometryReader {
// store half of current width (which is screen-wide)
// in preference
Color.clear
.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.width / 2.0)
})
.onPreferenceChange(ViewHeightKey.self) {
// read value from preference in state
self.height = $0
}
.frame(height: height) // apply from stored state
.background(Color.green)
Text("Bottom Label")
.background(Color.blue)
}
.background(Color.yellow)
}
}
struct ViewHeightKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
Note: ... and don't use GeometryReader if you are not sure about context in which your view is.

Overlay with relative Width

Xcode 11 Beta 4 deprecated .relativeSize as well as .relativeWidth and .relativeHeight (see this related post).
So what is the alternative?
I want to create an overlay that has a width relative to it's parent.
Let's say I have the following main view
struct MainView: View {
var body: some View {
ZStack(alignment: .topLeading) {
BackgroundView()
SideBarView()
.frame(idealWidth: 200)
.fixedSize(horizontal: true, vertical: false)
}
}
}
With a simple BackgroundView and SideBarView as well those work as expected.
struct SideBarView: View {
var body: some View {
Rectangle()
.foregroundColor(.green)
}
}
struct BackgroundView: View {
var body: some View {
Rectangle()
.foregroundColor(.red)
}
}
This was suggested in the release notes and this answer.
How can I avoid to hardcode those values as I could before by using .relativeWidth(0.3) instead of .frame(idealWidth:)?1
1Note: Using .relativeWidth never actually worked, e.g. using 0.3 as a relative value never resulted in a view that was 30 % of the width of the parent, but you could get close to your desired result through trial-and-error.
There are multiple ways to achieve it, one way is using .overlay instead of ZStack. The view you use in the overlay, will get the size of the BackgroundView offered by the parent. Then you simply use GeometryReader to get the width and multiply it by 0.7:
struct ContentView: View {
var body: some View {
BackgroundView().overlay(SideBarView())
}
}
struct SideBarView: View {
var body: some View {
GeometryReader { proxy in
HStack {
Spacer()
Rectangle()
.frame(width: proxy.size.width * 0.7)
.fixedSize(horizontal: true, vertical: false)
.foregroundColor(.green)
}
}
}
}
struct BackgroundView: View {
var body: some View {
Rectangle()
.foregroundColor(.red)
}
}