How to remove vertical safeAreaInsets while close gesture is active? - ios

Here's a main ContentView and DetailedCardView and when open DetailedCardView there's a ScrollView with more content. It also have closeGesture which allow to close the card when swiping from left to right and vice versa. So, at that point problems start to appear, like this ones inside DetailedCardView:
When scrolling in ScrollView then safeAreaInsets .top and .bottom start to be visible and as a result it's shrink the whole view vertically.
The closeGesture and standard ScrollView gesture while combined is not smooth and I'd like to add closing card not only when swiping horizontally but vertically too.
As a result I'd like to have the same gesture behaviour like opening and closing detailed card in App Store App Today section. Would love to get your help 🙌
ContentView DetailedCardView closeGesture in Action safeAreaInsets
struct ContentView: View {
#State private var showCardView = false
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
#State private var selectedCardNumber = 0
var body: some View {
ZStack {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(1..<101) { number in
colors[number % colors.count]
.overlay(Text("\(number)").font(.largeTitle.bold()).foregroundColor(.white))
.frame(height: 300)
.cornerRadius(30)
.padding(10)
.onTapGesture {
showCardView.toggle()
selectedCardNumber = number
}
}
}
}
.opacity(showCardView ? 0 : 1)
.animation(.spring(), value: showCardView)
if showCardView {
CardDetailView(showCardView: $showCardView, number: selectedCardNumber)
}
}
}
}
struct CardDetailView: View {
#Binding var showCardView: Bool
#State private var cardPosition: CGSize = .zero
let number: Int
var body: some View {
ScrollView {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(.green)
.overlay(Text("\(number)").font(.system(size: 100).bold()).foregroundColor(.white))
.frame(height: 500)
Color.red.opacity(0.8)
.frame(height: 200)
Color.gray.opacity(0.8)
.frame(height: 200)
Color.blue
.frame(height: 200)
Color.yellow
.frame(height: 200)
}
.scaleEffect(gestureScale())
.gesture(closeGesture)
}
private var closeGesture: some Gesture {
DragGesture()
.onChanged { progress in
withAnimation(.spring()) {
cardPosition = progress.translation
}
}
.onEnded { _ in
withAnimation(.spring()) {
let positionInTwoDirections = abs(cardPosition.width)
if positionInTwoDirections > 100 {
showCardView = false
}
cardPosition = .zero
}
}
}
private func gestureScale() -> CGFloat {
let max = UIScreen.main.bounds.width / 2
let currentAmount = abs(cardPosition.width)
let percentage = currentAmount / max
return 1 - min(percentage, 0.5)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

How can I add a more rows of buttons in my frame when my hstack is too long in Swift UI?

I would like to make the second row appear when my list is too long.
Do you have any idea how to do that?
Thank you in advance!
import SwiftUI
struct ContentView: View {
#StateObject var vm = SpeakingVM()
var speakingModel: SpeakingModel
var body: some View {
HStack(spacing:20){
ForEach(speakingModel.sentence.indices) { index in
Button(action: {
}, label: {
Text(speakingModel.sentence[index].definition)
.padding(.vertical,10)
.padding(.horizontal)
.background(Capsule().stroke(Color.blue))
.lineLimit(1)
})
}
}.frame(width: UIScreen.main.bounds.width - 30, height: UIScreen.main.bounds.height / 3)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(speakingModel: SpeakingModel(sentence: [SpeakingModel.Word(definition: "Météo"),SpeakingModel.Word(definition: "Cheval"),SpeakingModel.Word(definition: "Ascenceur")], sentenceInFrench: "Quel temps fait-il ?"))
}
}
What i would like :
Put your data in tags and customize the item view and change it appropriately.
import SwiftUI
struct HashTagView: View {
#State var tags: [String] = ["#Lorem", "#Ipsum", "#dolor", "#consectetur", "#adipiscing", "#elit", "#Nam", "#semper", "#sit", "#amet", "#ut", "#eleifend", "#Cras"]
#State private var totalHeight = CGFloat.zero
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.tags, id: \.self) { tag in
self.item(for: tag)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if tag == self.tags.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if tag == self.tags.last! {
height = 0 // last item
}
return result
})
}
}.background(viewHeightReader($totalHeight))
}
private func item(for text: String) -> some View {
Text(text)
.padding(.all, 5)
.font(.body)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}
struct HashTagView_Previews: PreviewProvider {
static var previews: some View {
HashTagView()
}
}

SwiftUI. Subview animation is not working if subview's View #State was changed while parent View is animating. iOS 16

I have a structure, where the parent view is a hidden pop-up with content. On some user action parent View starts to slide up. I need all its content to slide up with the parent View. It was working perfectly before iOS 16, but now it's broken. If the child's View subview #State is changed during the animation, then this View appears instantly on a screen, i.e. not sliding. As I understand, because View's #State was changed SwiftUI redraws this particular View and disables its animation, so it appears in its final position without animation. I was trying to force the animation of this particular View using .animation or withAnimation. Nothing of it helped. How to fix this bug in iOS 16?
Minimal reproducible example:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
#State var shouldShow: SlideCardPosition = .bottom
#State var text1: String = ""
var body: some View {
VStack {
Button {
shouldShow = .top
} label: {
Text("Show PopUp")
.padding(.top, 100)
}
PopUpUIView(shouldShow: $shouldShow)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct SlideOverCard<Content: View>: View {
#GestureState private var dragState = SlideDragState.inactive
#Binding private var position: SlideCardPosition
#State private var highlightedBackground = false
private var contentHeight: CGFloat
private var backgroundColor: Color?
private var withCorners: Bool
private var isHandleHidden: Bool
private var overlayOpacity: CGFloat
init(position: Binding<SlideCardPosition>,
contentHeight: CGFloat,
backgroundColor: Color? = nil,
withCorners: Bool = true,
isHandleHidden: Bool = false,
overlayOpacity: CGFloat = 0.75,
content: #escaping () -> Content) {
_position = position
self.content = content
self.contentHeight = contentHeight
self.backgroundColor = backgroundColor
self.withCorners = withCorners
self.isHandleHidden = isHandleHidden
self.overlayOpacity = overlayOpacity
}
var content: () -> Content
var body: some View {
return Rectangle()
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.foregroundColor(Color.black.opacity(highlightedBackground ? overlayOpacity : 0))
.position(x: UIScreen.screenWidth / 2, y: (UIScreen.screenHeight) / 2)
.edgesIgnoringSafeArea([.top, .bottom])
.overlay(
Group {
VStack(spacing: 0) {
if !isHandleHidden {
Handle()
}
self.content()
Spacer()
}
}
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.background(backgroundColor != nil ? backgroundColor! : Color.black)
.cornerRadius(withCorners ? 40.0 : 0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: position(from: position) + dragState.translation.height)
.animation(dragState.isDragging ? nil : .interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0), value: UUID())
.edgesIgnoringSafeArea([.top, .bottom])
.onTapGesture {}
.onChange(of: position) { _ in
withAnimation(.easeInOut) {
highlightedBackground.toggle()
}
}
)
.onTapGesture {
position = position == .bottom ? .top : .bottom
}
}
private func position(from cardPosition: SlideCardPosition) -> CGFloat {
switch cardPosition {
case .top: return UIScreen.screenHeight - contentHeight - UIScreen.topSafeAreaHeight
case .bottom: return 1000
}
}
}
enum SlideCardPosition {
case top
case bottom
}
private enum SlideDragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
private struct Handle: View {
private let handleThickness: CGFloat = 5
var body: some View {
RoundedRectangle(cornerRadius: handleThickness / 2.0)
.frame(width: 34, height: handleThickness)
.foregroundColor(.white)
.padding(.top, 8)
}
}
import UIKit
extension UIScreen {
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
private static let window = UIApplication.shared.windows[0]
private static let safeFrame = window.safeAreaLayoutGuide.layoutFrame
static var topSafeAreaHeight: CGFloat {
safeFrame.minY
}
static var bottomSafeAreaHeight: CGFloat {
window.frame.maxY - safeFrame.maxY
}
}
import SwiftUI
struct PopUpUIView: View {
#Binding var shouldShow: SlideCardPosition
#State var text1 = "some random text"
var body: some View {
SlideOverCard(position: $shouldShow,
contentHeight: 300) {
VStack(spacing: 10) {
Text(text1)
.foregroundColor(.white)
.padding(.top, 80)
}
}.onChange(of: shouldShow) { _ in
if shouldShow == .top {
text1 = UUID().uuidString
}
}
}
}
struct PopUpUIView_Previews: PreviewProvider {
static var previews: some View {
PopUpUIView(shouldShow: .constant(.bottom))
}
}
Example of incorrect animation with a dynamic text.
Example of what I want to achieve. It is working fine if text is static.

When move view by offset with animation, subview animating individually

Im trying to move cell view by offset with animation.
I thought Test Text will move with superview but when middle of the moving down, Test Text deviate from position.
I think Text animating individually but i cant understand why.
I might not understand well about relation between animating and subviews.
Can someone tell how animating effect to subview and how to fix Test Text Location when moving superview?
https://i.stack.imgur.com/1bA5H.gif
let SAFEAREA_BOTTOM = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.flatMap({ $0.windows })
.first(where: { $0.isKeyWindow })?
.safeAreaInsets.bottom ?? 0
struct TestView: View {
#State var extend: Bool = false
#State var cellFrame: CGRect = .zero
#State var offsetY: CGFloat = 0
var body: some View {
VStack {
cell
Spacer()
}
}
var cell: some View {
VStack(spacing: 0) {
HStack {
Text("Test")
Spacer()
}
VStack {
Text("Extension1")
Text("Extension2")
}
.frame(height: extend ? nil : 0)
.clipped()
}.padding()
.background(Color.green)
.padding()
.offset(x: 0, y: offsetY)
.overlay( GeometryReader { receiveViewFrame($0) })
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) {
if extend {
extend = false
offsetY = 0
} else {
extend = true
}
}
}
.onChange(of: cellFrame) { frame in
print("onChange")
guard extend else { return }
withAnimation(.easeInOut(duration: 1)) {
offsetY = UIScreen.main.bounds.height - frame.origin.y - frame.height - SAFEAREA_BOTTOM
}
}
}
private func receiveViewFrame(_ g: GeometryProxy) -> some View {
DispatchQueue.main.async {
cellFrame = g.frame(in: .global)
}
return Color.clear.frame(width: g.size.width, height: g.size.height)
}
}

SwiftUI: Drawing a line between two views in a list / how to determine the center location of a view?

I want to draw a line between a selected source-View and a selected target-View when I press the "connect" button.
As far as I understand I need the (center?) CGPoint of these views...
How can I determine the center CGPoint of a view inside a forEach-HStack-list?
Elements from both lists will be removed/added.
struct ContentView: View {
#State private var selectedTarget: Int = 0
#State private var selectedSource: Int = 3
#State private var isConnected = false
var body: some View {
ZStack {
VStack {
HStack {
ForEach(0..<6) { index in
Text("Target \(index)")
.padding()
.foregroundColor(.white)
.background(selectedTarget == index ? .red : .black)
.onTapGesture {
selectedTarget = index
}
}
}
List {
EmptyView()
}
.frame(width: 400, height: 400, alignment: .center)
.border(.black, width: 2)
HStack {
ForEach(0..<6) { index in
Text("Source \(index)")
.padding()
.foregroundColor(.white)
.background(selectedSource == index ? .orange : .black)
.onTapGesture {
selectedSource = index
}
}
}
Button(isConnected ? "CUT" : "CONNECT") {
isConnected.toggle()
}
.padding()
}
}
}
}
Here is the rough demo code and take an idea from this code.
You can achieve this by first finding the position of the Text by GeometryReader and draw the path between those points.
struct ContentView: View {
#State private var selectedTarget: Int = 0
#State private var selectedSource: Int = 3
#State private var isConnected = false
#State private var targetCGPoint: CGPoint = .zero
#State private var sourceCGPoint: CGPoint = .zero
var body: some View {
ZStack {
if isConnected {
getPath()
}
VStack {
HStack {
ForEach(0..<6) { index in
GeometryReader { geo in
Text("Target \(index)")
.padding()
.foregroundColor(.white)
.background(selectedTarget == index ? Color.red : Color.black)
.onTapGesture {
let size = geo.size
targetCGPoint = CGPoint(x: geo.frame(in: .global).origin.x + (size.width / 2), y: geo.frame(in: .global).origin.y)
print("Target Global center: \(geo.frame(in: .global).midX) x \(geo.frame(in: .global).midY)")
selectedTarget = index
}
}
}
}
// Removed list for demo
Spacer()
HStack {
ForEach(0..<6) { index in
GeometryReader { geo in
Text("Source \(index)")
.padding()
.foregroundColor(.white)
.background(selectedSource == index ? Color.orange : Color.black)
.onTapGesture {
let size = geo.size
sourceCGPoint = CGPoint(x: geo.frame(in: .global).origin.x + (size.width / 2), y: geo.frame(in: .global).origin.y)
print("Source Global center: \(geo.frame(in: .global).midX) x \(geo.frame(in: .global).midY)")
selectedSource = index
}
}
}
}
Button(isConnected ? "CUT" : "CONNECT") {
isConnected.toggle()
}
.padding()
}
}
}
func getPath() -> some View {
Path { path in
path.move(to: sourceCGPoint)
path.addLine(to: targetCGPoint)
}
.stroke(Color.blue, lineWidth: 10)
}
}

Create sliding effect by offsetting button position with image

I have created a slider view which consists of 2 images, a chevron pointer and a slider line. My ultimate goal is to offset the button in the opposite direction to the chevron image so that when the user clicks the empty space the button will slide to its location. Below is a gif of me attempting to achieve this. My approach is quite manual and I suspect there is a sophisticated way of achieving my goal. I have it working when I click the button once, I suspect that on the other side I have my numbers wrong so it doesn't work when the image is on the left side of the screen.
import SwiftUI
extension Animation {
static func smooth() -> Animation {
Animation.spring()
.speed(0.7)
}
}
struct SliderView: View {
//this works as a toggle you can implement other booleans with this
#State var partyType: Bool = false
#State private var isOn = false
#State var width: CGFloat = 0
#State var height: CGFloat = 0
let screen = UIScreen.main.bounds
var body: some View {
ZStack {
Toggle("",isOn: $isOn).toggleStyle(SliderToggle())
}
}
func imageWidthX(name: String) -> CGFloat {
let image = UIImage(named: name)!
let width = image.size.width
return width * 2.5
}
func hostParty() -> Bool {
if partyType {
return true
} else {
return false
}
}
}
struct SlideEffect: GeometryEffect {
var offset: CGSize
var animatableData: CGSize.AnimatableData {
get { CGSize.AnimatableData(offset.width, offset.height) }
set { offset = CGSize(width: newValue.first, height: newValue.second) }
}
public func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(
CGAffineTransform(
translationX: offset.width,
y: offset.height
)
)
}
}
struct SliderView_Previews: PreviewProvider {
static var previews: some View {
SliderView()
}
}
struct SliderToggle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
GeometryReader{ geo in
VStack (alignment: .leading,spacing: 0){
Button(action: {
withAnimation() {
configuration.isOn.toggle()
}
}) {
Image("chevronPointer")
.clipped()
.animation(.smooth())
.padding(.horizontal, 30)
.offset(x: geo.size.width - 100, y: 0)
}
.modifier(
SlideEffect(
offset: CGSize(
width: configuration.isOn ? -geo.size.width + imageWidthX(name: "chevronPointer") : 0,
height: 0.0
)
)
)
Image("sliderLine")
.resizable()
.scaledToFit()
.frame(width: geo.size.width, alignment: .center)
}
}
.frame(height: 40)
}
}
func imageWidthX(name: String) -> CGFloat {
let image = UIImage(named: name)!
let width = image.size.width
return width * 2.5
}
I worded the question pretty poorly but ultimately the goal I wanted to achieve was to have 2 buttons on a stack that controlled the view under it. When the opposite side is clicked the slider icon will travel to that side and vice versa. The offset made things a lot more complicated than necessary so I removed this code and basically created invisible buttons. IF there is a more elegant way top achieve this I would be truly greatful
HStack {
ZStack {
Button (" ") {
withAnimation(){
configuration.isOn.toggle()
}}
.padding(.trailing).frame(width: 70, height: 25)
Image("chevronPointer")
.clipped()
.animation(.smooth())
.padding(.horizontal, 30)
//.offset(x: geo.size.width - 100, y: 0)
.modifier(SlideEffect(offset: CGSize(width: configuration.isOn ? geo.size.width - imageWidthX(name: "chevronPointer"): 0, height: 0.0)))
}
Spacer()
Button(" "){
withAnimation(){
configuration.isOn.toggle()
}
}.padding(.trailing).frame(width: 70, height: 25)
}

Resources