How to make SwiftUI responsive to different devices? (Swift) - ios

I'm doing this tutorial and it requires me to place numbers(CGFloats) in for offset and padding, the problem is, this looks different on different devices. For example, on iPod touch, it goes off the screen.
My question is, how do I make these values change with the size of the screen? I know how to write a function to do this, so I guess I'm really asking: How do I retrieve the screen size data in order to use it?
CircleImage()
.offset(y: -130)
.padding(.bottom, -100)

You can use GeometryReader to get the size of the view that the item is in and then make calculations based on the size.
struct ContentView : View {
var body: some View {
GeometryReader { geometry in
Circle().offset(y: geometry.size.height / 4)
}
}
}
Note that this just retrieves the size of the current View and by default expands to fill available space.
Additional reading on GeometryReader: https://www.hackingwithswift.com/quick-start/swiftui/how-to-provide-relative-sizes-using-geometryreader

Related

How to make Swift UI adaptive all Screen Size and Text Size in swift ui

can anyone tell how to make Responsive UI in Swift UI, which is compatible on all Device , and using figma Text like 24 then it shows iphone 8 and iphone 11 same view
You'll want to minimize the amount of hardcoded sizes or frames in your app as that will make your app less responsive across different phone models and orientations. Here are some links you can check out to learn responsive UI:
https://www.youtube.com/watch?v=67ZCQ5ihj_I&t=186s
https://www.youtube.com/watch?v=ALzrixd_hd8
Consider trying out geometry reader as well. Here's an article that goes into depth on what it is and how to use it: https://www.hackingwithswift.com/quick-start/swiftui/how-to-provide-relative-sizes-using-geometryreader
For images you can do something like this. You can use GeometryReader class to make your images responsive. Do not use hardcoded size for images. In this snippet I used 50% width of the screen and for height it will also take 50% of the height.
Also Be careful using GeometryReader class. The GeometryProxy returns the current view width and height.
var body: some View {
GeometryReader { reader in
VStack {
Image(contact.imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: reader.size.width * 0.5, height: reader.size.width * 0.5)
.clipped()
.cornerRadius(reader.size.width)
.shadow(radius: 10)
}
}
}
A better way to do it without wrapping all your views in a geometry reader is to make an extension to get the screen with and height, which is also callable on all pages without having to do the extension on every page like a geometryreader.
extension View{
func getScreenBounds() -> CGRect{
return UIScreen.main.bounds
}
}
and to call it is the same as geometry reader:
.frame(width: getScreenBounds().width * 0.5, height: getScreenBounds().width * 0.5

Matched Geometry Effect with AsyncImage iOS 15

Consider the following example:
struct ContentView: View {
#State var showSplash: Bool = true
#Namespace var animationNamespace
var body: some View {
ZStack {
if showSplash {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
.frame(width: geometry.size.width)
.transition(.move(edge: .bottom))
.edgesIgnoringSafeArea(.all)
.clipped()
} placeholder: {
Color.gray
}
}
.onTapGesture {
toggleSplashScreen(false)
}
} else {
ScrollView {
GeometryReader { geometry in
AsyncImage(url: URL(string: "https://picsum.photos/seed/864a5875-6d8b-43d6-8d65-04c5cfb13f3b/1920/1440")) { image in
image
image
.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "SplashImage", in: animationNamespace)
.transition(.move(edge: .bottom))
} placeholder: {
Color.gray
}
.frame(width: geometry.size.width, height: 400)
.clipped()
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
toggleSplashScreen(true)
}
}
}
}
}
}
With a helper method here:
private extension ContentView {
func toggleSplashScreen(_ toggle: Bool) {
withAnimation(.spring(response: 0.85, dampingFraction: 0.95)) {
showSplash = toggle
}
}
}
This produces:
I noticed two things here that I would like to fix
The flashing white effect when transitioning between the two states.
I noticed since we are using AsyncImage, when showSplash changes the AsyncImages only sometimes hits the placeholder block. As a result, the transition becomes really choppy. I tested this with a static image from the assets file and the transition then became smooth. I also tried creating a Caching mechanism on the AsyncImage but still had issues with it hitting placeholder block sometimes.
Would love to hear any ideas :) Thanks!
There are a couple of things that I think you could do to improve this.
First, You are fighting a little bit against the way SwiftUI maintains a view's identity. One of the ways that SwiftUI determines when it can reuse an existing structure as opposed to recreating a structure, is by it's location in the view hierarchy. So when you toggle your structure you go from:
GeometryReader
AsyncImage
to
ScrollView
GeometryReader
AsyncImage
As a result, the system thinks these are two AsyncImage views and so it's rebuilding the view (and reloading the image) every time. I think that's where your white flashes come from since you're seeing your gray placeholder in the middle of your animation. If you could leave the scroll view in place, possibly disabling scrolling when it's not needed (if that's possible) then the OS could maintain the identity of the AsyncImage. (see https://developer.apple.com/videos/play/wwdc2021/10022/)
That leads to the second area of investigation for you. AsyncImage is wonderful in the convenience it gives you in loading content from the network. Unfortunately it doesn't make that communication faster. Your goal should be to have AsyncImage go to the network as few times as possible.
Right now, your resizing strategy focuses on resizing the image. That means that for every transition you're "hitting the network" (read putting your code on the slow, dusty, dirt road path). Instead of resizing the image, you should just load the image once (the slow part) and resize the view that is displaying it. The general idea would be to let AsyncImage load the image, then control how the image is animated by animating the frame of the view.
This is where I get less helpful. I don't know enough about AsyncImage to know if it's capable of implementing that strategy. It seems that it should be... but I don't know that it is. You might have to resort to downloading and storing the image as state separately from the view that presents it.
So my advice is to limit the number of times AsyncImage has to reload the network data. That involves helping SwiftUI maintain the identity of the AsyncImage so it doesn't have to reload each time the view is created. And, try to implement your animations and scaling on the view, not the image, because rescaling the image also requires a network reload.

Determine if a Shape / View in SwiftUI will cover an existing Shape / View

I have a simple app that will put some shapes on the screen, but it is currently possible for the user to mask an existing one, which I want to avoid.
First, here is some simple code to provide an MVP for my project:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Circle()
.fill(Color.red)
.frame(width: 44,
height: 44)
.zIndex(1.0)
Circle()
.fill(Color.blue)
.frame(width: 55,
height: 55)
.zIndex(2.0)
}
}
}
This image shows the above code in action:
However, when the zIndex is flipped, as shown here, the blue completely obscures the red.
I'd rather not have the user be able to do this, but I have yet to find something within SwiftUI that can determine if the shape is completely covered or not.
You might try to represent each shape as a struct with it's own size and coordinate (x and y). Existing shapes are to be put in array. Whenever user drags the shape, you update the values and check wether it's size and coordinates are identical to some shape from array (you can do it when the gesture ends or while it is happening. it's up to you). If condition is met you either move static shape away by some k or show user in any way that he is unable to drop shape at that place

How to stop GeometryReader changing value when keyboard opens

I'm struggling to find documentation to support this but it seems as though the values of
GeometryReader.size.width & height change when the keyboard opens. This can be proven through something like:
var body: some View {
TabView {
GeometryReader { g in
Rectangle()
.frame(width:g.size.width/2,height:g.size.height/20)
TextField(...)
}
}
}
which shows the rectangle resizing when the keyboard opens by clicking on the textfield.
How would I prevent this from happening? I want to specify the frame size relative to screen size to support many screen sizes...
You don't need a geometry reader to know the screen's size. you can get screen's dimensions using UIScreen.main.bounds.width and UIScreen.main.bounds.height.
Note width always shows the horizontal-dimension's size, and height always shows the vertical one (incase of screen rotation)
Add this to the GeometryReader:
.ignoresSafeArea(.keyboard)

How to create a ScrollView that can adapt to its children's changing size

[Beta 4] I have a list of cards, each of which can be expanded when the user taps on them to display more information.
However, the containing ScrollView does not expand or contract when the size of the cards changes.
The only way I have found to make this work is to use List — which seems to adjust automatically to the content, but this is not what I want as it introduces other features of the list (most notably the dividers).
Not using a ForEach (i.e. just stacking several Card() yields the same result.
In Beta 3 I found a workaround by making expanded a #Binding, and then have an array of #State for each card, but since Beta 4, this doesn't work anymore as the changing value does not propagate up anymore unless I also have an element in the parent view that binds to this array directly (i.e. also a toggle). It seems that the state of #State only changes now if there are actually elements directly bound to it.
struct ExpandableChildViewTest: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ForEach(1...10) { _ in
Card()
}
}
}
.padding()
.border(Color.green, width: 1)
}
}
struct Card: View {
#State private var expanded = true
var body: some View {
ZStack {
Rectangle()
.frame(height: expanded ? 350: 100)
.foregroundColor(Color.blue)
.cornerRadius(20)
.padding(.vertical)
.tapAction() {
self.expanded.toggle()
}
}
}
}
I would expect (hope) the containing Scrollview to adjust its size when its children change size, but that doesn't happen. This leads to either content falling out of the Scrollview, or a lot of extra space inside the Scrollview.
The picture shows the extra white space at the top because the second card is collapsed. The outer Scrollview does not recompute itself.
(my reputation does not yet allow to comment, so I'm using this way...)
I have a similar situation: a timeline with different granulations (decade, year, month etc.), and the user shall be able to zoom smoothly, while the scale adopts itself accordingly. The only way I found up to now is to calculate the overall width of the scale for the current magnification, and to hand it over to the view through the model. The embedded view then gets a .frame with that width, and the ScrollView then handles the scrolling correctly.
However, this is far away from being elegant. Because the embedded view actually knows its width, it should be possible somehow (but how?) to make the ScrollView aware of that width. In my environment, the ScrollView (without the explicit .frame) just shows an empty screen, with the explicit .frame everything's fine (but not performant enough).
Btw: Since Beta 5, there are changes to the way how binding is performed. You now set the model in the view with #ObservedObject var model: Model, and you propagate the model's changes using #Published var something: SomeType, this is much easier than before.

Resources