Swiftui ScrollView is affected unexpectedly by scaleEffect modifier - swiftui

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)
}
}

Related

Why is Text onTapGesture not working for widened frame in SwiftUI?

I have this testing file that is not working how I expect it to.
import SwiftUI
struct SwiftUIView: View {
#State var boolTest = false
var nums = ["1","2","3","4","5","6","7"]
var body: some View {
VStack {
ForEach(nums, id: \.self) { num in
Text("\(num)")
.frame(width: 400)
.font(.system(size: 70))
.foregroundColor(boolTest ? .red : .green)
.onTapGesture {
boolTest.toggle()
}
}
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
When I tap on a number, the foreground color changes as expected. However, I want to be able to tap on the areas left and right of the Text("\(num)") so I expanded the frame modifier to test. However the color only changes when I tap directly on the number or text.
How do I tap on the space left or right of the number and have it change colors instead of it doing nothing?
The system treats the "invisible" (ie doesn't have visible drawn content) part of the view as unresponsive unless you set a contentShape on it.
//...
.frame(width: 400)
.contentShape(Rectangle())
//...

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))")
}
}

Best way to set a dynamic frame for all devices

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)
}
}
}

Scrollview doesn't scroll past views with an offset

To solve a much more complicated problem, I created the following simple test project:
struct ContentView: View {
var body: some View {
ScrollView {
ZStack {
ForEach(0..<50) { index in
Text("Test \(index)")
.offset(x: 0, y: CGFloat(index * 20))
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This draws 50 Text views inside a ZStack, each one with a larger y offset, so that they are drawn down past the visible part of the screen:
The whole thing is wrapped inside of a ScrollView, so I expect that I should be able to scroll down to the last view.
However, it doesn't scroll past Test 26.
How can I arrange views inside of a ZStack by assigning offsets and update the ScrollView's contentSize?
The content size is calculated by the size of the view inside the ScrollView. So that only thing we can do is to change that view size.
By default VStack size is the total size (actual view sizes) of views inside it.
As well as we can use frame() to change the size in this case. check apple document
Since VStack arrange view in the middle we can align it to the .top.
struct ContentView: View {
var body: some View {
ScrollView() {
VStack {
ForEach(0..<50) { index in
Text("Test \(index)")
.offset(x: 0, y: CGFloat(index * 20))
}
}
.frame( height: 2000, alignment: .top) //<=here
.border(Color.black)
}
}
}
If I understood your intention correctly then you don't need offset in this scenario at all (SwiftUI works differently)
ScrollView {
VStack {
ForEach(0..<50) { index in
Text("Test \(index)") // << use .padding if needed
}
}
}
Note: The offset does not change view layout, view remains in the same place where it was created, but rendered in place of offset; frames of other views also not affected. Just be aware.

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)
}
}