SwiftUI - Custom Snap Scroll With Offset - ios

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) {
...
}

Related

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

How to create dragable view from top in SwiftUI?

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

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

How to tease adjacent pages in PageTabView?

I am working with SwiftUI 2 and using a TabView with PageTabViewStyle.
Now, I am searching for a way to "tease" the pages adjacent to the current page like so:
Is it possible to achieve this effect with TabView and PageTabViewStyle?
I already tried to reduce the width of my TabView to be windowWidth-50. However, this did not lead to the adjacent pages being visible at the sides. Instead, this change introduced a hard vertical edge 50px left of the right window border, where new pages would slide in.
Here is a simple implementation. You can use the struct with the AnyView array or use the logic directly in your own implementation.
struct ContentView: View {
#State private var selected = 4
var body: some View {
// the trailing closure takes an Array of AnyView type erased views
TeasingTabView(selectedTab: $selected, spacing: 20) {
[
AnyView(TabContentView(title: "First", color: .yellow)),
AnyView(TabContentView(title: "Second", color: .orange)),
AnyView(TabContentView(title: "Fourth", color: .green)),
AnyView(TabContentView(title: "Fifth", color: .blue)),
AnyView(
Image(systemName: "lizard")
.resizable().scaledToFit()
.padding()
.frame(maxHeight: .infinity)
.border(.red)
)
]
}
}
}
struct TeasingTabView: View {
#Binding var selectedTab: Int
let spacing: CGFloat
let views: () -> [AnyView]
#State private var offset = CGFloat.zero
var viewCount: Int { views().count }
var body: some View {
VStack(spacing: spacing) {
GeometryReader { geo in
let width = geo.size.width * 0.7
LazyHStack(spacing: spacing) {
Color.clear
.frame(width: geo.size.width * 0.15 - spacing)
ForEach(0..<viewCount, id: \.self) { idx in
views()[idx]
.frame(width: width)
.padding(.vertical)
}
}
.offset(x: CGFloat(-selectedTab) * (width + spacing) + offset)
.animation(.easeOut, value: selectedTab)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation.width
}
.onEnded { value in
withAnimation(.easeOut) {
offset = value.predictedEndTranslation.width
selectedTab -= Int((offset / width).rounded())
selectedTab = max(0, min(selectedTab, viewCount-1))
offset = 0
}
}
)
}
//
HStack {
ForEach(0..<viewCount, id: \.self) { idx in
Circle().frame(width: 8)
.foregroundColor(idx == selectedTab ? .primary : .secondary.opacity(0.5))
.onTapGesture {
selectedTab = idx
}
}
}
}
}
}
struct TabContentView: View {
let title: String
let color: Color
var body: some View {
Text(title).font(.title)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(color.opacity(0.4), ignoresSafeAreaEdges: .all)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}

How to make a view's height animate from 0 to height in SwiftUI?

I'd like to make this bar's height (part of a larger bar graph) animate on appear from 0 to its given height, but I can't quite figure it out in SwiftUI? I found this code that was helpful, but with this only the first bar (of 7) animates:
struct BarChartItem: View {
var value: Int
var dayText: String
var topOfSCaleColorIsRed: Bool
#State var growMore: CGFloat = 0
#State var showMore: Double = 0
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(getBarColor())
.frame(width: 40, height: growMore)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.linear(duration: 7.0)) {
growMore = CGFloat(value)
showMore = 1
}
}
}.opacity(showMore)
Text(dayText.uppercased())
.font(.caption)
}
}
And this is the chart:
HStack(alignment: .bottom) {
ForEach(pastRecoveryScoreObjects) { pastRecoveryScoreObject in
BarChartItem(value: pastRecoveryScoreObject.recoveryScore, dayText: "\(pastRecoveryScoreObject.date.weekdayName)", topOfSCaleColorIsRed: false)
}
}
This works: you can get the index of the ForEach loop and delay each bar's animation by some time * that index. All the bars are connected to the same state variable via a binding, and the state is set immediately on appear. You can play with the parameters to tune it to your own preferences.
struct TestChartAnimationView: View {
let values = [200, 120, 120, 90, 10, 80]
#State var open = false
var animationTime: Double = 0.25
var body: some View {
VStack {
Spacer()
HStack(alignment: .bottom) {
ForEach(values.indices, id: \.self) { index in
BarChartItem(open: $open, value: values[index])
.animation(
Animation.linear(duration: animationTime).delay(animationTime * Double(index))
)
}
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
open = true
}
}
}
}
struct BarChartItem: View {
#Binding var open: Bool
var value: Int
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(Color.blue)
.frame(width: 20, height: open ? CGFloat(value) : 0)
Text("Week".uppercased())
.font(.caption)
}
}
}
Edit: ForEach animations are kind of weird. I'm not sure why, but this works:
struct ContentView: View {
let values = [200, 120, 120, 90, 10, 80]
var body: some View {
HStack(alignment: .bottom) {
ForEach(values, id: \.self) { value in
BarChartItem(totalValue: value)
.animation(.linear(duration: 3))
}
}
}
}
struct BarChartItem: View {
#State var value: Int = 0
var totalValue = 0
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(Color.blue)
.frame(width: 20, height: CGFloat(value))
Text("Week".uppercased())
.font(.caption)
}.onAppear {
value = totalValue
}
}
}
Instead of applying the animation in the onAppear, I applied an implicit animation inside the ForEach.
Result:

Resources