Resize, or "sink" into navigation bar effect in SwiftUI - ios

I have the following code below:
ZStack {
VStack {
NavigationView {
VStack() {
Spacer()
image?
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fill)
.frame(width: 160, height: 160)
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.overlay(
CameraButtonView(showActionSheet: $showActionSheet)
.offset(x: 50, y: 65)
)
.shadow(radius: 10)
Form {
....
}
}
}
.environment(\.horizontalSizeClass, .compact)
}
}
in essence it pretty simplistic were the above portion is an image with the camera button so that users can either select from camera roll or take a picture, which will be displayed by the image, and the rest is a form when the user will be able to fill in the form accrodingly.
What I have been trying to do is make the top part of the view (the image portion) scroll up or if possible scroll/sink into the navigation bar (almost like in the iphone contacts app), when the user starts to scroll the form portion of the view.
Currently the form portion scrolls under the image portion, which as you can imagine take a good portion of the view, so hiding that top image portion would make it easier to fill in the rest of the form.
Has anyone been able to do so in SwiftUI?

Related

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

Update SwiftUI View size to match image

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.

How to have back button of style being displayed in Apple App Store, on banner images?

I need to have back button which looks like the one displayed on any App's page in Apple App Store, having banner image. See image below:
I have tried finding any possible system icon image name as guided here:
https://stackoverflow.com/a/58900114/1152014
But couldn't find any appropriate one. Please guide.
The system icon image name is chevron.left.circle.fill (the one with fill color) and chevron.left.circle (the one without fill color)
By default the fill color is black, which you can change using the foreground color option.
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Image(systemName: "chevron.left.circle.fill")
.resizable()
.frame(width: 50, height: 50, alignment: .center)
.listRowBackground(Color.green)
Text("")
Image(systemName: "chevron.left.circle")
.resizable()
.frame(width: 50, height: 50, alignment: .center)
.listRowBackground(Color.green)
}
.foregroundColor(.white)
}
}

Resources