SwiftUI Sticky/Pinned Bottom Sheet View - ios

I'm working on implementing a very simple bottom sheet in SwiftUI, but I am hitting an issue where I can't figure out how to get the bottom of the sheet to "stick" or "pin" to the bottom of the screen/view.
Here is the code I'm using for the bottom sheet
import SwiftUI
struct BottomSheetView<Content: View>: View {
#Binding var isOpen: Bool
#ViewBuilder let content: Content
#State private var contentSize: CGSize = .zero
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0.0) {
ChildSizeReader(size: $contentSize) {
content
}
}
.frame(
width: geometry.size.width,
height: contentSize.height,
alignment: .top
)
.clipShape(
Path(
UIBezierPath(
roundedRect: CGRect(x: 0.0, y: 0.0, width: geometry.size.width, height: contentSize.height),
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: 16.0, height: 16.0)
)
.cgPath
)
)
.frame(
height: geometry.size.height,
alignment: .bottom
)
.offset(y: isOpen ? 0.0 : contentSize.height)
.animation(
.spring()
.speed(1.25)
)
}
.edgesIgnoringSafeArea(.bottom)
}
}
struct BottomSheetView_Previews: PreviewProvider {
static var previews: some View {
BottomSheetView(isOpen: .constant(true)) {
Text("Bottom Sheet")
.frame(maxWidth: .infinity, minHeight: 200.0)
.background(
Rectangle()
.fill(Color.Primary.primaryRed)
)
}
}
}
And here is an image of the resulting view
The view as it sits statically is fine, but when the animation happens to open the bottom sheet, it bounces up and you can see below the view for a brief second. I imagine this will also become more apparent when I add a drag gesture to allow the user to interact with the bottom sheet (It's controlled by a button at the moment).
I'm looking for almost a stretchy view where when it is expanded open, at its max content height, the view can bounce up, and the bottom will stick to the bottom of the screen. Is this possible, and if so, how can I go about getting it to do that?

Related

NavigationLink shows up in ScrollView wrong

I have encountered this problem during the Swift programming. I have this long frame that I created, here it is.
The main idea that this frame will be used in a horizontal Scroll View in different view, like this. It will be opening different view on tap.
Here's the catch. If we want to transition to different view, we need NavigationLink. In order to work NavigationLink needs NavigationView. When we add our LongFrame in NavigationView, this happens
If we tap on it, it will display View, but in small frame
And If we, for example, add our LongFrameScrollView somewhere, It won't even show up sometimes
I will provide the code here. My guess that should be connected to .frame, but without this line of code I can't create this frame(.
// FRAME ITSELF
import SwiftUI
struct LongFrameView: View {
var body: some View {
NavigationView {
NavigationLink {
PlayerView()
} label: {
ZStack {
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: [Color(red: 0.268, green: 0.376, blue: 0.587), Color(red: 0.139, green: 0.267, blue: 0.517)]),
startPoint: .leading,
endPoint: .trailing))
.frame(width: 310, height: 62)
.cornerRadius(8)
HStack {
Image("mountains")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 70, height: 62)
.cornerRadius(8, corners: [.topLeft, .bottomLeft])
VStack(alignment: .leading) {
Text("Sense of anxiety")
.font(.custom("Manrope-Bold", size: 14))
.foregroundColor(.white)
Text("11 MIN")
.font(.custom("Manrope-Medium", size: 12))
.foregroundColor(.white)
}
Spacer()
}
}
.frame(width: 310, height: 62)
}
}
}
}
struct LongFrameView_Previews: PreviewProvider {
static var previews: some View {
LongFrameView()
}
}
// MARK: - WITH THIS CODE WE CAN DEFINE WHERE CORNER RADIUS WILL BE CHANGED OR NOT. DO NOT MODIFY
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
// SCROLL VIEW WITH FRAMES
import SwiftUI
struct LongFrameScrollView: View {
let rows = Array(repeating: GridItem(.fixed(60), spacing: 10, alignment: .leading), count: 2)
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 10) {
// PLACEHOLDER UNTIL API IS READY
LongFrameView()
LongFrameView()
LongFrameView()
LongFrameView()
}
}
.padding([.horizontal, .bottom], 10)
}
}
struct LongFrameScrollView_Previews: PreviewProvider {
static var previews: some View {
LongFrameScrollView()
}
}
NavigationView should be added as the first/top view. So embed your ScrollView with NavigationView inside LongFrameScrollView and removed it from LongFrameView. Inside LongFrameView you just need NavigationLink.
NavigationView {
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 10) {
// PLACEHOLDER UNTIL API IS READY
LongFrameView()
LongFrameView()
LongFrameView()
LongFrameView()
}
}
.padding([.horizontal, .bottom], 10)
}

How to make the webview in full screen

I have this code and I am trying to make the webview height to be a full screen
struct ContentView: View {
#State private var showWebView = false
private let urlString: String = "https://example"
var body: some View {
VStack(spacing: 40) {
// Normal WebView
WebView(url: URL(string: urlString)!).frame(height: 900)
.cornerRadius(10)
.shadow(color: .black.opacity(0.3), radius: 20.0, x: 5, y: 5)
}
}
}
I have tried to add to the height :
height: self.view.frame.height
but it didn't work
You should just add .ignoresSafeArea() modifier to your WebView:
struct ContentView: View {
#State private var showWebView = false
private let urlString: String = "https://example"
var body: some View {
VStack(spacing: 40) {
WebView(url: URL(string: urlString)!)
.cornerRadius(10)
.shadow(color: .black.opacity(0.3), radius: 20.0, x: 5, y: 5)
.ignoresSafeArea()
}
}
}
Also you can remove corenerRadius() modifier, because in this case it's not necessary.
To make a web view full screen with SwiftUI, you can use the .edgesIgnoringSafeArea(.all) modifier on the WebView object, like this:
WebView(url: url).edgesIgnoringSafeArea(.all)
This will make the web view occupy the entire screen, ignoring the safe area. The safe area is the part of the screen that is not obscured by the device's bezel or other device-specific elements. Ignoring the safe area will ensure that the web view takes up the maximum amount of space on the screen.
You can also use the .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) modifier to achieve the same effect, like this:
WebView(url: URL(string: urlString)!)
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity
)
This will make the web view occupy the entire screen, regardless of the device's safe area.

Spacer does not work when I draw custom tab bar with SwiftUI

I was doing some practice in swift ui I made a tab bar like in the picture and I want to move it to the bottom of the screen, but I could not spacer did not work, maybe it is related to the path also I want to give corner radius I am new yet switui thank you in advance for your answers
struct ContentView: View {
var body: some View {
VStack {
Spacer()
CustomTabBar()
}
}}
struct CustomTabBar: View {
var body: some View {
ZStack {
Path { path in
path.addRect(CGRect(x: 10, y: 200, width: UIScreen.main.bounds.size.width - 20, height: 80))
}
.cornerRadius(40)
.foregroundColor(.blue)
Path { path in
path.addArc(center: CGPoint(x: UIScreen.main.bounds.size.width / 2, y: 200), radius: 50, startAngle: .degrees(0), endAngle: .degrees(180), clockwise: false)
}.foregroundColor(.white)
}
}}
Yes, you are right. It's related to the Path elements you have. I would do the same with pure SwiftUI Shapes, Rectangle and Circle. This also eases your work with corner radius.
Here's my code:
struct ContentView: View {
var body: some View {
VStack {
Spacer()
CustomTabBar()
}
}
}
struct CustomTabBar: View {
var body: some View {
ZStack {
Rectangle()
.frame(height: 80)
.foregroundColor(Color.blue)
.cornerRadius(40)
.padding(.horizontal, 10)
Circle()
.frame(width: 100, height: 100)
.foregroundColor(Color.white)
.offset(x: 0, y: -50)
}
}
}
This produces the following result:

swiftUI transitions: Scale from some frame - like iOS Homescreen is doing when opening an App

How do I get the effect in a translation, that iOS home screen does when opening an App: It scales the App to fullscreen, starting from the App-icon on the home screen?
In my code, I have the icon-frame (CGRect) with position, width and height and I have the final frame. Is there a way to (probably combine some) transitions to get a scaling from the icon-frame to the final frame?
I get somewhat similar with:
view.transition(AnyTransition.scale(scale: 0, anchor: UnitPoint.trailing))
Which scales from zero to the original size, starting from trailing center position.
It's only close as:
it scales from zero instead of the size of the original icon
it starts from a fixed point (trailing, center) but I'd like to let it start from where the icon is.
Just to be sure: It must be a transition as the view is newly created and dropped. I tried with keeping the view and just change its opacity to show/hide it. With many other problems like not getting the reverse animation, when the view disappears.
Here is a demo of idea how such effect could be done with combined transitions... (positions and sizes are hardcoded for demo simplicity - they can be read with geometry reader, alignment guides or anchor preferences, and actually do not affect the transition usage idea, also animation can be configured)
Demo:
struct TestRisingView: View {
let screen = UIScreen.main.bounds
#State var showingView = false
#State var btFrame: CGRect = .zero
var body: some View {
GeometryReader { g in
ZStack {
Rectangle().fill(Color.clear)
self.activatingButton(frame: CGRect(x: 80, y: 30, width: 60, height: 40))
self.activatingButton(frame: CGRect(x: self.screen.maxX - 80, y: 30, width: 60, height: 40))
self.activatingButton(frame: CGRect(x: self.screen.maxX - 80, y: self.screen.maxY - 60, width: 60, height: 40))
self.activatingButton(frame: CGRect(x: 80, y: self.screen.maxY - 60, width: 60, height: 40))
if self.showingView {
self.topView
.zIndex(1)
.transition(
AnyTransition.scale(scale: 0.12).combined(with:
AnyTransition.offset(x: self.btFrame.origin.x - g.size.width/2.0,
y: self.btFrame.origin.y - g.size.height/2.0))
)
}
}
}
}
func activatingButton(frame: CGRect) -> some View {
Button(action: {
withAnimation {
self.btFrame = frame
self.showingView.toggle()
}
}) {
Text("Tap")
.padding()
.background(Color.yellow)
}
.position(frame.origin)
}
var topView: some View {
Rectangle()
.fill(Color.green)
.frame(width: 300, height: 400)
}
}
The above sample code should give you an idea on scaling an image
struct ImageCustomScaling: View {
// image to be scaled
var scaleImage: Image
// scale ratio
var scaleTo: Double
#State private var start = false
var body: some View {
VStack {
scaleImage
.font(.title)
.scaleEffect(self.start ? CGFloat(scaleTo) : 1)
.opacity(self.start ? 1 : 0)
.animation(Animation.interpolatingSpring(stiffness: 25, damping: 5, initialVelocity: 10).delay(0.9))
}
.foregroundColor(.blue)
.onAppear {
self.start = true
}
}
}
Above can be called by something like below
ImageCustomScaling(scaleImage: Image(systemName: "cloud.fill"), scaleTo: 5 )
Added animation modifier to give visual cue. You can change it to meet your needs.
Now in 2021 we finally got an answer from Apple. matchedGeometryEffect solves this problem elegantly and easily.
Here is a little screenshot of what it does:
And here is the code for this demo:
struct Stackoverflow: View {
#Namespace var namespace
#State private var shown: Bool = false
var body: some View {
VStack {
Button {
shown.toggle()
} label: {
Text("Tap Me!\nI'm a red square.")
.multilineTextAlignment(.center)
}
.background(
Color.red
.matchedGeometryEffect(id: "1", in: namespace, properties: .frame))
Spacer()
if shown {
Color.clear
.aspectRatio(contentMode: .fit)
.matchedGeometryEffect(id: "1", in: namespace, properties: .size)
.border(Color.gray)
}
Spacer()
}
.animation(.easeInOut(duration: 1))
}
}
struct Stackoverflow_Previews: PreviewProvider {
static var previews: some View {
Stackoverflow()
}
}
Check here on swift-lab.com, for more information and explanation on how to use matchedGeometryEffect.

Anchor point in SwiftUI

I'm playing with SwiftUI animations. I wanted to make a square bigger modifying its frame with an animation this way:
struct ContentView: View {
let squareWidth = CGFloat(100)
let squareHeight = CGFloat(100)
#State private var bigger = false
var body: some View {
VStack {
Color.green
.frame(width: bigger ? self.squareWidth * 2 : self.squareWidth, height: self.squareHeight)
.animation(.default)
Button(action: {
self.bigger.toggle()
}) {
Text("Click me!")
}
}
}
}
The animation happens from the centre of the View, that is the anchor point (0.5, 0.5). Is there a way to change this anchor point? I found out that I can use scaleEffect on the view specifying an anchor:
struct ContentView: View {
let squareWidth = CGFloat(100)
let squareHeight = CGFloat(100)
#State private var bigger = false
var body: some View {
VStack {
Color.green
.frame(width: self.squareWidth, height: self.squareHeight)
.scaleEffect(x: bigger ? 2 : 1, y: 1, anchor: .leading)
.animation(.default)
Button(action: {
self.bigger.toggle()
}) {
Text("Click me!")
}
}
}
}
But it doesn't seem exactly the same. If I needed to apply several effects I should specify the anchor point for each of them, instead of specifying the anchor point on the view just one time. On UIKit I could write:
var myView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
myView.layer.anchorPoint = CGPoint(x: 0, y: 0.5) //that is, the .leading of SwiftUI
It is possible, but your approach must change.
There is an implied, built-in centring of views in SwiftUI, which can be confusing.
The parent view is the one placing the content in the centre, recalculating the position of its subviews each time their frames change.
Embedding the VStack in a HStack and adding borders to both clearly shows this behaviour.
Also, note the Spacer(), which pushes the view to the left.
HStack {
VStack {
Color.green
.frame(width: bigger ? self.squareWidth * 2 : self.squareWidth)
.frame(height: self.squareHeight)
.animation(.default)
Button("Click me!") { self.bigger.toggle() }
}.border(Color.black)
Spacer()
}.border(Color.red)
Using the .layer.anchorPoint in UIKit should have the same effect as using scaleEffect/offset/rotationEffect in SwiftUI, as it should affect the origin of the underlying geometry/texture which is being rendered by the GPU.
If you have a complex view (composed of multiple subviews) that needs the same scale effect, you can group them using a Group { } and use .scaleEffect on it.

Resources