Im using a LazyVGrid to show an article gallery.
Everything is working fine, and I have no memory warnings, because every time a view leaves screen, memory usage is reduced.
My problem is when I use matchedGeometryEffect with the image, in order to sync animation with a new View.
Animation works perfect, but the memory is increasing while the scrollview is scrolling.
Is like if matchedGeometryEffect was maintaining the memory reference to the object, and don't allow releases.
The container
LazyVGrid(
columns: [
GridItem(.flexible())
],spacing: 16
){
ForEach(viewModel.articles){ article in
LazyVStack{
ArticleCardView(article: article, animation: animation, show: $show)
.onTapGesture {
withAnimation(.spring()){
selectedArticle = article
show.toggle()
}
}
}
}
}
The cardView
VStack{
if !show {
Image(uiImage: readImage(name: "\(article.id)00"))
.resizable()
.aspectRatio(contentMode: .fit)
//.matchedGeometryEffect(id: "img\(article.id)00", in: animation)
...
The new view
VStack{
GeometryReader { geo in
TabView {
Image(uiImage: readImage(name: "\(article!.id)00"))
.resizable()
.aspectRatio(contentMode: .fit)
//.matchedGeometryEffect(id: "img\(article!.id)00", in: animation, isSource: false)
.tag(1)
...
Everything works perfect, but if I uncomment commented lines, memory usage increases when scroll.
Any idea?
Than you
Solved: I just embed the view into VStack and memories is released:
...
ForEach(viewModel.articles){ article in
VStack{
ArticleCardView(article: article, animation: animation, show: $show)...
Related
I'm pretty new to SwiftUI, learning it for the first time, and couldn’t understand why the below snippet doesn’t work. Ideally, the VStack should stretch in all directions and the Image should have a width of 200px without losing its aspect ratio.
Code
struct ContentView: View {
var body: some View {
VStack() {
Image("Image Name")
.resizable()
.frame(width: 200)
.aspectRatio(contentMode: .fit)
}
.background(Color.red)
.frame(maxWidth: .infinity,maxHeight: .infinity)
}
}
After I accidentally reordered the modifiers, it worked. So, how am I supposed to know the correct order of modifiers without a hit and trial method each time?
// new VStack modifier order
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
// new Image modifier order
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
The best way to think about it for now is to imagine that SwiftUI renders your view after every single modifier. So, as soon as you say .background(Color.red) it colors the background in red, regardless of what frame you give it. If you then later expand the frame, it won’t magically redraw the background – that was already applied.
Of course, this isn’t actually how SwiftUI works, because if it did it would be a performance nightmare, but it’s a neat mental shortcut to use while you’re learning.
Please refer to this link for more details https://www.hackingwithswift.com/books/ios-swiftui/why-modifier-order-matters#:~:text=Every%20time%20we%20modify%20a,up%3A%20ModifiedContent%3CModifiedContent%3C%E2%80%A6
I have got a modal sheet, here is the code:
SettingsDashboardView:
#State private var notificationsSettingsSheet = false
var body: some View {
Button(action: {
self.notificationsSettingsSheet.toggle()
}) {
VStack(alignment: .leading) {
HStack(alignment: .top, spacing: 4) {
Label("Set Daily Reminders", systemImage: "alarm").foregroundColor(Color("TextColor"))
.font(.system(.headline, design: .rounded))
Spacer()
}
}
}
.sheet(isPresented: $notificationsSettingsSheet) {
NotificationSettingsModal()
}
}
NotificationSettingsModal:
var body: some View {
ZStack(alignment: .bottom) {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Text("Daily Reminders")
.font(.system(.title, design: .rounded))
.fontWeight(.bold)
.padding(.top, headingTopPadding)
.padding(.horizontal, headingHorizontalPadding).foregroundColor(Color("TextColor"))
Spacer().frame(height: 164)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
Spacer().frame(height: 64)
}
}.background(Color("BackgroundColor").edgesIgnoringSafeArea(.all))
}
When I launch the app and open my sheet, in about 50% of cases sheet dismisses itself after about half a second. If I open sheet after that everything works fine. What can cause this problem?
This will probably not solve the mentioned issue but can be useful for others.
In most cases, this issue happens when the view gets redrawn due to a change in some variables. Be careful that it might be the parent view that have some variables changes.
The best way to debug this kind of behaviour is to use the technique describe here, on Hacking with Swift. The idea is to identify what change caused a view to reload itself by printing print(Self._printChanges()) inside the body property. Note that by doing it, you will temporarily need to add an explicit return.
Then, observer the console and it most cases you will be able to identify the issue and refactor your code.
In my experience (does not seem to be the case here) this often happens when using #Environment(\.editMode) var editMode in both the view and parent view. For some reasons this value changes in both views when presenting a sheet, causing the view to be redrawn and the sheet closed.
I solved this problem by removing the codes below while setting to NavigationView on my homeView this week, which caused my subView's sheet automatically dismissed the first time showing.
NavigationView {...}
// .navigationViewStyle(StackNavigationViewStyle())
I am following https://www.youtube.com/watch?v=Xetrbmnszjc tutorial to learn SwiftUI. In a view, we have a vertical stack, which has some text and a button at the bottom. When I click the button, the whole of the vertical stack view is highlighted. I cannot understand why this happens.
VStack {
Text(drink.description)
.foregroundColor(.primary)
.font(.body)
.lineLimit(nil)
.lineSpacing(12)
HStack{
Spacer()
OrderButton()
Spacer()
}.padding(.top,25)
}.padding(.top)
.padding(.bottom)
}
struct OrderButton: View{
var body: some View{
Button(action:{}){
Text("Order Now")
}.frame(width:200,height:50)
.font(.headline)
.foregroundColor(.white)
.background(Color.black)
.cornerRadius(10)
}
}
Please help on how to fix it. Since SwiftUI is relatively new, it becomes difficult to find answers to problems.
The attached image shows the scenario when I click on the Order Now button.
Try this:
Button(action:{}){
Text("Order Now")
.frame(width:200,height:50)
.font(.headline)
.foregroundColor(.white)
.background(Color.black)
}.buttonStyle(PlainButtonStyle())
.cornerRadius(10)
You need to change view inside button, not frame itself. View which you put in there sets frame to all button automatically, and clickable plane also be the same frame as view.
I'm trying to make a view which holds an image loaded asynchronously from a network request. Before the image loads, I want to show a placeholder view which has a fixed size. When the image loads, I want to replace this placeholder view with the image, scaled to fit inside the frame of the placeholder view, but then I want the parent view to shrink to match the size of this image. I can't figure out how to do this last part.
Right now, it looks like this:
struct ItemCell: View {
var body: some View {
Group {
CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
.aspectRatio(contentMode: .fit)
.frame(minWidth: 0, maxWidth: 150, minHeight: 0, maxHeight: 190, alignment: .bottomLeading)
}.background(Color.red) // To show that the view isn't resizing properly
}
}
struct PlaceholderView: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 3, style: .continuous)
.frame(width: 150, height: 190)
.foregroundColor(Color(.secondarySystemBackground))
Image(systemName: "globe")
.resizable()
.scaledToFit()
.frame(width: 50)
.foregroundColor(.secondary)
}
}
}
The CustomImageView is adapted from this article on loading images asynchronously. The ItemCells are placed in a horizontal ScrollView. When I test this, it:
correctly displays the placeholder view before the image is loaded;
resizes the image so it maintains its aspect ratio and fits inside the 150x190 frame, but has a weird animation where some of the images shrink and then expand back; also, some of the images seem to shrink too much;
does not resize the parent view to match the size of the image properly, but instead retains the full original height and some (?) extra width on some cells.
These two problems are shown in the gif below, with blue images and a red background. Notice the extra height on the first and third cells, and the extra width on the second. Also, note that the first image ends up smaller than when it first loads, even though it fit inside the original 150x190 frame at first.
How can I fix these problems?
Figured out how to do it. There were several problems with my original code. First, the ItemCells used in the ScrollView should be modified with the .fixedSize() view modifier, like so:
ScrollView(...) {
HStack(...) {
ForEach(...) { ...
ItemCell()
.fixedSize()
}
}
}
Then, changing the frame of the CustomImageCell to be use idealHeight instead of maxHeight and making the Group a VStack with a Spacer() to push everything to the bottom, as #Paulw11 had suggested in comments:
struct ItemCell: View {
var body: some View {
VStack {
Spacer()
CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 150, idealHeight: 190)
}
}
}
These changes fix both the image resizing animation issue and the extra space issue.
I can't figure out how to align Image view on top of ZStack, by default views in SwiftUI are placed at the center of their parent, and we then use stacks to align them, I have the following piece of code:
struct ContentView: View {
var body: some View {
ZStack {
Image("bgImage")
Text("Hello, World!")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red) //this is for debugging purposes, to show the area of the ZStack
}
}
How can I position the image to the top ?
To tell the ZStack to align things a particular way within it, configure it with the alignment parameter:
ZStack(alignment: .top) {
Color.clear
Image(...)
Text("Hello, World!")
}
(Color.clear expands to fill all available space, so this forces your ZStack to be as large as the enclosing view without needing to add a .frame().)
That will align everything at the top of course, which might not be what you want. You could fix that by making a nesting your ZStacks to align as you want them to:
ZStack{
ZStack(alignment: .top) {
Color.clear
Image(...) // This will be at the top
}
Text("Hello, World!") // This will be centered
}
That said, I'd probably use a .background for this example.
ZStack {
Color.clear
Text("Hello, World!")
}
.background(Image(...), alignment: .top)
And if you only have one view, you can get rid of the ZStack and use a frame instead:
Text("Hello, World!")
.frame(maxHeight: .infinity)
.background(Image(uiImage:#imageLiteral(resourceName: "image.jpg")),
alignment: .top)
Keep in mind that in this case the image will draw outside its frame. In many cases that's fine (and it's completely legal), but it can matter sometimes (for example, if you put this inside a stack). You can add .border(Color.green) to the end to see how that works.
This example really gets to the heart of SwiftUI layout, so it's worth understanding what's going on. This isn't a workaround or a trick, so you should get to the place where this feels very normal.
The top-level content view (the one that contains the ZStack) offers its entire space to the ZStack. A ZStack is always exactly the size that contains its contents, so first the ZStack needs to layout its children. It lays them out according to its alignment, and then sizes itself exactly to fit around them. So with top-alignment (but without Color.clear), the Image is at the top of the ZStack. The ZStack is just exactly the same size as the Image.
The top-level content view then places the ZStack in its center.
The way the ZStack lays out its children is similar to how the content view did. It offers all the space it was offered, and then the child-views decide their sizes. Views always decide their own sizes. The Image and Text are fixed-sized views, so they are just the size of their contents. But Color is a flexible-sized view. It accepts the entire space that the ZStack offered (which is the same space that the top-level content view offered) and returns that as its size. Since a ZStack must exactly contain its children, the ZStack is now the size of the top-level content view, and things behave as you expect, aligning at the top of the screen.
Let's compare to using .frame() as you originally did:
ZStack {
Image("bgImage")
Text("Hello, World!")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red) //this is for debugging purposes, to show the area of the ZStack
First, I want to focus on your comment, because it's not correct. This is not the area of the ZStack. The ZStack is exactly the size of its contents (the Image and the Text). You put the background on the frame view, and the frame view is larger.
A key confusion people have is that they think .frame(...) changes the size of the View it's attached to. That's not correct at all. As before, a ZStack is always the size of its contents. .frame() creates a completely new view of the requested size. It then positions the wrapped view inside itself according to the frame's alignment. So in this example it works like this:
Top-level - Background - Frame - ZStack - { Image Text }
The top-level view offers all its space to the Background. Backgrounds are the size of what they contain, so it offers all of that space to the Frame. The Frame is flexible in both directions (due to the max fields), and so it ignores its child's size and chooses to be the size it was offered.
The Frame then offers all that space to the ZStack. The ZStack lays out its children, and returns its size as exactly the size that contains them.
The Frame then places the ZStack according to the Frame's alignment (.center, since that's the default). If you'd set the Frame's alignment to .top, then the ZStack would have been placed at the top of the frame (but the text would be centered in the ZStack not in the Frame).
It then reports to the Background that it is as large as the top-level view (since its flexible).
The Background then claims that same size to the top-level content view.
And finally, the top-level content view places the Background in its center.
You could always just put the things you want to be at the top in a VStack and use a Spacer.
ZStack(){
Image(...)
Spacer()
}
The complete code should look something like this:
struct ContentView: View {
var body: some View {
ZStack(){
Text("Hello, World!")
VStack {
Image(...)
Spacer()
}
}
}
}
You could do this with HStacks as well. Important to notice that if the image has no limits to its size, it will always take up as much space as possible. That would remove the purpose of the Spacer. Hope this helps :-)
So one thing working against you is the infinity maxHeight modifier, assuming that you do not want some space between the image and the bottom of the view.
.edgesIgnoringSafeArea(.all)
You may just need to tell your ZStack to ignore safe area insets.
struct ContactsView: View {
var body: some View {
ZStack {
Image("bgImage")
Text("Hello, World!")
}.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.red).edgesIgnoringSafeArea(.all)
}
}
If you need space between the bottom and the image, wrap the ZStack in a VStack and throw a Spacer in the bottom of the VStack.
struct ContactsView: View {
var body: some View {
VStack {
ZStack {
Image(systemName: "bgImage")
Text("Hello, World!")
}.frame(maxWidth: .infinity, maxHeight: 300).background(Color.red)
Spacer()
}.edgesIgnoringSafeArea(.all)
}
}
ZStack {
Image("background")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
}
Add .edgesIgnoringSafeArea(.all)