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)
}
}
Related
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()
}
}
I am trying to create dragable view from top which is kind of overlay .. I am attaching something same but having bottom sheet but i am looking for Top sheet. When overlay come background should be blur.
I am not sure about how to implement hence dont have code..I tried parent child for List in SwiftUI but that is having diff. Behaviour.
Any example or suggestion is appropriated.
something like this?
struct SheetView: View {
let title: String
var body: some View {
NavigationView {
List {
ForEach(0..<30) { item in
Text("Item \(item)")
}
}
.listStyle(.plain)
.navigationTitle(title)
}
}
}
struct ContentView: View {
#State private var currentDrag: CGFloat = 0
#State private var stateDrag: CGFloat = -300
let minDrag: CGFloat = -UIScreen.main.bounds.height + 120
var body: some View {
ZStack {
VStack(spacing: 0) {
Spacer(minLength: 36)
SheetView(title: "Bottom Sheet")
.background(.primary)
.opacity(stateDrag + currentDrag > minDrag ? 0.5 : 1)
.blur(radius: stateDrag + currentDrag > minDrag ? 5 : 0)
}
VStack(spacing: 0) {
SheetView(title: "Top Sheet")
.background(.background)
.offset(x: 0, y: stateDrag + currentDrag)
Image(systemName: "line.3.horizontal").foregroundColor(.secondary)
.padding()
.frame(maxWidth: .infinity)
.background(.background)
.contentShape(Rectangle())
.offset(x: 0, y: stateDrag + currentDrag)
.gesture(DragGesture()
.onChanged { value in
withAnimation {
currentDrag = value.translation.height
}
}
.onEnded { value in
stateDrag += value.translation.height
stateDrag = max(stateDrag, minDrag)
currentDrag = .zero
})
}
}
}
}
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)
}
I have spent that past few days looking for a collapsing top bar in SwiftUI. I currently have a solution that I made from this article. I have adjusted the original code slightly, but I still encounter a bug when I scroll to the bottom of the ScrollView:
My goal is to have a top bar like the one from Twitter below:
While I may be able to find a solution to the bug, I was hoping to find a more stable and customizable top bar in SwiftUI. I have looked for libraries and other related solutions to making a custom top bar, but I have not found anything that good.
Here is my code:
ContentView.swift
struct ContentView: View {
#State var initialOffset: CGFloat?
#State var offset: CGFloat?
#State var previousOffset: CGFloat?
var isOffset : Bool {
// previousOffset! < offset
if TopBar.heightAndRadiusForBackground(initialOffset: self.initialOffset, offset: self.offset, previousOffset: self.previousOffset) != 160.0 {
return true
}
else {
return false
}
}
var body: some View {
VStack {
TopBar(offset: self.$offset,
initialOffset: self.$initialOffset,
previousOffset: self.$previousOffset)
ScrollView {
GeometryReader { geometry in
Color.clear
.preference(key: OffsetKey.self,
value: geometry.frame(in: .global).minY
)
.frame(height: 0)
}
LazyVStack {
ForEach((1...10), id: \.self) {
Text("\($0)")
.font(.title)
.bold()
.frame(width: UIScreen.main.bounds.width, height: 200, alignment: .center)
}
}
}
.padding(.top, self.isOffset ? -16 : 40)
.animation(.easeOut)
}
.onPreferenceChange(OffsetKey.self) {
if self.initialOffset == nil || self.initialOffset == 0 {
self.initialOffset = $0
}
if self.offset != nil {
self.previousOffset = self.offset
}
self.offset = $0
}
}
}
TopBar.swift
struct TopBar: View {
#Binding var offset: CGFloat?
#Binding var initialOffset: CGFloat?
#Binding var previousOffset: CGFloat?
var isOffset : Bool {
if TopBar.heightAndRadiusForBackground(initialOffset: self.initialOffset, offset: self.offset, previousOffset: self.previousOffset) != 160.0 {
return true
}
else {
return false
}
}
func viewForBackground() -> some View {
let newHeight = TopBar.heightAndRadiusForBackground(initialOffset: self.initialOffset, offset: self.offset, previousOffset: self.previousOffset)
return Rectangle()
.foregroundColor(.green)
.frame(width: UIScreen.main.bounds.width, height: newHeight)
.offset(y: self.isOffset ? -32 : -12)
}
var body: some View {
HStack {
Text("Top Bar")
.font(.system(size: 34, weight: .bold, design: .default))
Spacer()
}
.offset(y: self.isOffset ? -16 : UIScreen.main.bounds.height/28)
.padding(.leading)
.animation(.easeInOut)
.background(viewForBackground().animation(.easeOut))
}
}
extension TopBar {
static func heightAndRadiusForBackground(initialOffset: CGFloat?, offset: CGFloat?, previousOffset: CGFloat?) -> CGFloat {
let maxHeight: CGFloat = 160.0
let minHeight: CGFloat = 90
guard let initialOffset = initialOffset,
let offset = offset else { return maxHeight }
if previousOffset == nil {
return maxHeight
}
// Previous < offset: Scrolling down
// Previous > offset: Scrolliing up
if initialOffset > offset {
if previousOffset! < offset {
return maxHeight
}
else {
return minHeight
}
}
return maxHeight
}
}
struct OffsetKey: PreferenceKey {
static let defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?,
nextValue: () -> CGFloat?) {
value = value ?? nextValue()
}
}
Project
I want to create a custom scroll that snaps views to middle
But I can't figure out how to set the offset correctly.
This is the sample of the scroll:
struct contentView: View {
#State var startOffset: CGFloat = 0
#State var translationWidth: CGFloat = 0
#State var offset: CGFloat = 0
#State var index: Int = 0
var body: some View {
GeometryReader { geo in
HStack {
ForEach(0..<5, id: \.self) { num in
Button(action: {
// some action
}) {
Circle()
}
.frame(width: geo.size.width)
.offset(x: -self.offset*CGFloat(self.index) + self.translationWidth)
}
}.frame(width: geo.size.width, height: 200)
.offset(x: self.startOffset )
.highPriorityGesture (
DragGesture(minimumDistance: 0.1, coordinateSpace: .global)
.onChanged({ (value) in
self.translationWidth = value.translation.width
})
.onEnded({ (value) in
if self.translationWidth > 40 {
self.index -= 1
} else if self.translationWidth < -40 {
self.index += 1
}
self.translationWidth = 0
})
)
.onAppear {
withAnimation(.none) {
let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
self.offset = geo.size.width
self.startOffset = itemsWidth/2
}
}
}
}
}
It works but it is slightly off the middle, it doesn't scroll exactly in the middle and I don't understand why.
The problem occurs with the onAppear closure:
.onAppear {
withAnimation(.none) {
let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
self.offset = geo.size.width
self.startOffset = itemsWidth/2
}
}
I might be missing some small pixels in my calculations.
UPDATE
So Apparently the HStack has a default value for spacing between views.
so to fix it you should remove it like so:
HStack(spacing: 0) {
....
}
So after a bit of experimenting, I realized the HStack is using a default spacing which is not equal to zero.
It means that in my calculations I didn't include the spacing between items
so to fix this all you need to add is:
HStack(spacing: 0) {
...
}