Update SwiftUI View size to match image - ios

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.

Related

SwiftUI .contextMenu has visible padding around my View with a clipShape

I have an image in a chat bubble. This effect is achieved using a clipShape to a custom shape for the bubble.
When I attach a .contextMenu and present the contextMenu, it shows a padding around the chat bubble. Also the default cornerRadius of the contextMenu clips a small part of the chat bubble tip.
What I am trying to achieve is what I can see in the Apple Messages app. presenting contextMenu on a message with text preserves the clipShape of the text without adding padding. And on an image it removes the clipShape and resizes the image to a proper aspect Ratio. Neither of which I'm able to achieve.
struct ContentView: View {
var body: some View {
VStack {
Image("leaf")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 264, height: 361)
.clipShape(Bubble(location: .rightBottom))
.contentShape(Bubble(location: .rightBottom))
.contextMenu(ContextMenu(menuItems: {
Button {
} label: {
HStack {
Text("Save")
Image(systemName: "arrow.down.to.line")
}
}
}))
}
}
}
I have tried to use .contentShape to the same shape of the bubble but that had no effect at all. Is there anyway to show the contextMenu without the rounded corners and extra padding on the left side in screenshot below? Or to make the contextMenu containers background clear ideally?
Image prior to pressing context menu:
The solution was very simple. Just use content shape's first argument kind:
func contentShape<S>(_ kind: ContentShapeKinds, _ shape: S)
and set kind to .contextMenuPreview:
Image("leaf")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 264, height: 361)
.clipShape(Bubble(location: .rightBottom))
.contentShape(.contextMenuPreview, Bubble(location: .rightBottom))
.contextMenu(ContextMenu(menuItems: {
Button {
} label: {
HStack {
Text("Save")
Image(systemName: "arrow.down.to.line")
}
}
}))

SwiftUI - corner radius changes the height of an image

If the .cornerRadius modifier comes after the .frame modifier, the image becomes much slower. What is the reason behind this ?
struct ContentView: View {
var body: some View {
VStack(spacing: 100) {
Image("image1")
.resizable()
.scaledToFill()
.frame(width: 343, height: 184)
.cornerRadius(8)
Image("image1")
.resizable()
.scaledToFill()
.cornerRadius(8)
.frame(width: 343, height: 184)
}
}
}
In SwiftUI the order of modifiers is important and can lead to a different output.
A rule of thumb is:
read them from bottom to top
understand that every modifier produce a new view
the modifier "modifies" only what's below:
-> .frame(width: 343, height: 184)
-> .cornerRadius(8)
-> .scaledToFill()
-> .resizable()
Since you used the cornerRadius the documentation states that:
Clips this view to its bounding frame, with the specified corner
radius.
Behind the scene, you can imagine the engine applying two modifiers, the clipped and the corner radius.
Because every modifier produces a new view when the clipping is calculated it's like it doesn't know what are the bounds in which constrain the new view and that explains why the image goes out.
If we go a step further where we try to apply two frame and cornerRadius hopefully should be clearer how SwiftUI interprets the modifiers:
If you want to know more, this discussion goes deeper into the details of how SwiftUI sees the modifier: Order of modifiers in SwiftUI view impacts view appearance

Remove SwiftUI Form padding

When you use a SwiftUI Form it creates padding on the leading edge, which is fine for Form input but I would like to add additional content to the Form e.g. an image that takes the entire width of the screen, but because the image is in the Form, padding gets applied and the image gets pushed off screen slightly. How can I remove all Form padding?
struct MyForm: View {
var body: some View {
Form {
Image(uiImage: someImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: UIScreen.main.bounds.width)
TextField("Name", text: $name)
// Other fields
}
}
}
I know that the underlying view for Form is a UITableView where I can do things like UITableView.appearance().backgroundColor = .clear to change the Form appearance, but I can't figure out how to remove the leading padding.
I also know I can move the Image view outside the Form and put everything in a stack, but that creates other issues with scrolling that I'd like to avoid.
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
Image(uiImage: someImage)
.resizable()
.aspectRatio(contentMode: .fill)
.listRowInsets(EdgeInsets()) // << this one !!
Note: hardcode to UIScreen.main.bounds.width is not needed

How do I keep clipped content of one subview from blocking gestures in underlying subviews with SwiftUI?

I have a main view consisting of a zStack with a background image at the bottom, a user-loaded image above that, and two toolbars at the top. The toolbars are at the vertical top and bottom of the view. I want those toolbars to appear semi-transparent with background images matching the main view's background image in size and position. If the user drags or scales their image to overlap with the toolbars, it should be obscured by them. To accomplish that, I've built those toolbar views as zStacks with the same background image aligned to the top or bottom, respectively, and clipped the content to match the height of the toolbars.
My problem is that the clipped content of those toolbar views blocks gestures on the underlying, user-loaded image. To help visualize the problem, I've linked a screenshot of the debug view hierarchy for the main view (MockScoringView).
Here's the code for the main view:
GeometryReader { geometry in
ZStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.baseOffset.width + self.newOffset.width, y: self.baseOffset.height + self.newOffset.height)
.scaleEffect(self.scale)
.gesture(DragGesture()
.onChanged { value in
self.newOffset.width = value.translation.width / self.scale
self.newOffset.height = value.translation.height / self.scale
}
.onEnded { value in
self.baseOffset.width += self.newOffset.width
self.baseOffset.height += self.newOffset.height
self.newOffset = CGSize.zero
}
)
VStack(spacing: 0) {
MockTopToolbarView()
Spacer()
MockBottomToolbarView()
}
}
}
And the code for the top toolbar view, which is very similar to the one on bottom:
ZStack {
Image("Background")
.resizable()
.scaledToFill()
.frame(height: 56, alignment: .top)
.clipped()
Rectangle()
.foregroundColor(.toolbarGray)
.frame(height: 56)
}
I'd like to modify the content of the toolbar views' backgrounds so they do not block gestures from reaching the user-loaded image.
Debug View Hierarchy
You could set allowsHitTesting(false) on the background images.
Alternatively, since you do not want the user image to be visible behind the toolbars you could also just add the Rectangle and toolbars to a single VStack.
ZStack {
Image("Background")
.resizable()
.scaledToFill()
VStack {
TopToolBar()
Rectangle()
.foregroundColor(.blue)
...
BottomToolBar()
}
}

SwiftUI - Vertical Centering Content inside Scrollview

i'm trying to code a simple login page on my app. I started using SwiftUI on my newlly updated Mac OS Catalina. The Apple documentation is still lacking a lot.
I need to center a VStack vertically on a Scrollview ocupying the whole page with a "limit" on it's width of 400.
Something like this:
ScrollView(.vertical) {
VStack {
Text("Hello World")
}
.frame(maxWidth: 400, alignment: .center)
}
It was easy with UIScrollView, just needed to set the ContentView to fill height and width and then centering a Vertical StackLayout inside the Content View but now with SwiftUI i just wonder..
The goal is something like this (Credit to the author)
If someone is wondering why i want everything inside a scrollview, it's beacause my form is quite big and i expect the user to use both landscape and portrait view so i really need the content to be scrollable, bear in mind also that in a Ipad the form doens't fill the whole screen that's why i want it centered vertically.
You can vertically center content in a scroll view by using GeometryReader to get the parent view's dimensions and setting the scroll view's content's minHeight to the parent's height.
When the content is too big to fit vertically it'll just scroll like normal.
For example:
var body: some View {
GeometryReader { geometry in // Get the geometry
ScrollView(.vertical) {
VStack {
Text("Form goes here")
.frame(maxWidth: 400) // Set your max width
}
.padding()
.background(Color.yellow)
.frame(width: geometry.size.width) // Make the scroll view full-width
.frame(minHeight: geometry.size.height) // Set the content’s min height to the parent
}
}
}
I've build a more generic view based on #Alex answer
/// Custom vertical scroll view with centered content vertically
///
struct VScrollView<Content>: View where Content: View {
#ViewBuilder let content: Content
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
content
.frame(width: geometry.size.width)
.frame(minHeight: geometry.size.height)
}
}
}
}
You can use it anywhere in your app like this
var body: some View {
VScrollView {
VStack {
Text("YOUR TEXT HERE")
}
}
}

Resources