I have a ScrollView layout as shown in photo.
Is it possible and how could you achieve the following in SwiftUI:
Green square area does not react to scroll gestures but is part of the ScrollView and will scroll with red squares
The rest of the screen (red square area) react to scroll gestures as usual.
The reason is ask this it that in green square area I am trying to add a SpriteView that will handle its own touch events and I don't want ScrollView to prevent this.
The only way I found is to create your own ScrollView, as shown below.
It might be easier to tackle with UIKit.
struct ContentView: View {
#State private var dragAmount = CGFloat.zero
#State private var dragPosition = CGFloat.zero
#State private var dragging = false
var body: some View {
GeometryReader { proxy in
VStack {
RoundedRectangle(cornerRadius: 20)
.fill(.red)
.frame(height: 300)
.overlay(
Text(dragging ? "dragging red ..." : "")
)
.offset(y: dragPosition + dragAmount)
.gesture(DragGesture()
.onChanged { _ in
dragging = true
}
.onEnded { _ in
dragging = false
}
)
ForEach(0..<20) { _ in
RoundedRectangle(cornerRadius: 20)
.fill(.green)
.frame(height: 100)
}
.offset(y: dragPosition + dragAmount)
.gesture(DragGesture()
.onChanged { value in
dragAmount = value.translation.height
}
.onEnded { value in
withAnimation(.easeOut) {
dragPosition += value.predictedEndTranslation.height
dragAmount = 0
dragPosition = min(0, dragPosition)
dragPosition = max(-1750, dragPosition)
}
}
)
}
}
}
}
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 need to create an infinite horizontal scrolling with ScrollView to scroll a panorama image. So I would like to make this scrolling infinite from both sides, is there any possible way to make it? I have searched maybe using ScrollViewReader can achieve this but no luck.
ScrollView(.horizontal, showsIndicators: false) {
Image("panorama")
.resizable()
.scaledToFill()
}
Actually for such prepared image it is enough to load it only once, and we need only to Image presenters for it (note: they do not copy memory, but use the same loaded image). Everything else is just about layout on the fly moving current of-screen item depending on drag direction to left or to the right of current one.
Tested with Xcode 13.4 / iOS 15.5
Main part of code:
struct PanoramaView: View {
let image: UIImage
private struct Item: Identifiable, Equatable {
let id = UUID()
var pos: CGFloat!
}
#State private var items = [Item(), Item()]
// ...
var body: some View {
GeometryReader { gp in
let centerY = gp.size.height / 2
ForEach($items) { $item in
Image(uiImage: image)
.resizable().aspectRatio(contentMode: .fill)
.position(x: item.pos ?? 0, y: centerY)
.offset(x: dragOffset)
}
}
.background(GeometryReader {
Color.clear.preference(key: ViewSizeKey.self,
value: $0.frame(in: .local).size)
})
.onPreferenceChange(ViewSizeKey.self) {
setupLayout($0)
}
.contentShape(Rectangle())
.gesture(scrollGesture)
}
and usage
let panorama = UIImage(contentsOfFile: Bundle.main.path(forResource: "panorama", ofType: "jpeg")!)!
var body: some View {
PanoramaView(image: panorama)
.frame(height: 300) // to demo of dynamic internal layout
}
Complete test code is here
You can put 3 identical panorama images next to each other (to be able to scroll over the edges) and add a custom DragGesture, that basically jumps back to the relative position of the middle image.
This image is a bad example, obviously it will work better with a real 360° image ;)
EDIT:
now with predicted end location + animation and 5 images.
struct ContentView: View {
#State private var dragOffset = CGFloat.zero
#State private var offset = CGFloat.zero
let imageWidth: CGFloat = 500
var body: some View {
HStack(spacing: 0) {
ForEach(0..<5) { _ in
Image("image")
.resizable()
.frame(width: imageWidth)
}
}
.offset(x: offset + dragOffset)
.gesture(
DragGesture()
.onChanged({ value in
dragOffset = value.translation.width
})
.onEnded({ value in
withAnimation(.easeOut) {
dragOffset = value.predictedEndTranslation.width
}
offset = (offset + dragOffset).remainder(dividingBy: imageWidth)
dragOffset = 0
})
)
}
}
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 want to create a pulsing circle with forever repeating animation. It should have a gradual change of size and a momentary change of color when animation direction changes.
I have two different animations for that, but only the last one having an effect on both (size and color change).
struct SwiftUIView: View {
#State private var animationStarted = false
let side = UIScreen.main.bounds.size.width
let animation1 = Animation.easeInOut(duration: 0.3).delay(5.7)
.repeatForever(autoreverses: true)
let animation2 = Animation.easeInOut(duration: 6)
.repeatForever(autoreverses: true)
var body: some View {
HStack {
VStack {
HStack {
Circle()
.foregroundColor(self.animationStarted ? Color.pink : Color.blue)
.animation(animation1)
.frame(width: self.animationStarted ? 10 : side, height: self.animationStarted ? 10 : side)
.animation(animation2)
.onAppear {
self.animationStarted.toggle()
}
}
}.frame(height: side)
}.frame(width: side)
}
}
I have not been able to find an equivalent to the pan gesture in SwiftUI. I do see
and use magnify, tap, drag and rotate - but I do not see any built in pan. In the
following code snippet I add an image and allow the user to zoom - but I want the
user to also move the zoomed image to focus on the area of interest. Dragging, of
course does not do the job - it just moves the frame.
I tried layering a frame on top and moving the bottom image but could not make that
work either.
struct ContentView: View {
#State var scale: CGFloat = 1.0
#State var isScaled: Bool = false
#State private var dragOffset = CGSize.zero
var body: some View {
GeometryReader { geo in
VStack {
ZStack{
RoundedRectangle(cornerRadius: 40)
.foregroundColor(Color.white)
.frame(width: geo.size.width - 45, height: geo.size.width - 45)
.shadow(radius: 10)
Image("HuckALaHuckMedium")
.resizable()
.scaleEffect(self.scale)
.frame(width: geo.size.width - 60, height: geo.size.width - 60)
.cornerRadius(40)
.aspectRatio(contentMode: .fill)
.shadow(radius: 10, x: 20, y: 20)
//need pan not drag
.gesture(
DragGesture()
.onChanged { self.dragOffset = $0.translation }
.onEnded { _ in self.dragOffset = .zero }
)
//this works but you can't "zoom twice"
.gesture(MagnificationGesture()
.onChanged { value in
self.scale = self.isScaled ? 1.0 : value.magnitude
}
.onEnded({ value in
//self.scale = 1.0
self.isScaled.toggle()
})
)
.animation(.easeInOut)
.offset(self.dragOffset)
}//zstack
Spacer()
}
}
}
}
An original image example:
And that image after zoom - but with drag not pan:
Any guidance would be appreciated. Xcode 11.3 (11C29)
To make it simpler and more readable, I created an extension/modifier for that
struct DraggableView: ViewModifier {
#State var offset = CGPoint(x: 0, y: 0)
func body(content: Content) -> some View {
content
.gesture(DragGesture(minimumDistance: 0)
.onChanged { value in
self.offset.x += value.location.x - value.startLocation.x
self.offset.y += value.location.y - value.startLocation.y
})
.offset(x: offset.x, y: offset.y)
}
}
extension View {
func draggable() -> some View {
return modifier(DraggableView())
}
}
Now all you have to do is call the modifier:
Image(systemName: "plus")
.draggable()
For others. This is really simple. Ensure that the magnification is accomplished before the drag is allowed - it will work like a pan. First define the drag gesture:
let dragGesture = DragGesture()
.onChanged { (value) in
self.translation = value.translation
}
.onEnded { (value) in
self.viewState.width += value.translation.width
self.viewState.height += value.translation.height
self.translation = .zero
}
Then create an #State value that you toggle to true after the magnification gesture has been activated. When you attach the gesture to the image, do so conditionally:
.gesture(self.canBeDragged ? dragGesture : nil)
I'm a little late, but for anyone looking, please try this as it worked for me. What you want to do is change the offset of the image right after you zoom out in your MagnificationGesture.onChanged() lifecycle.
var magnification: some Gesture {
MagnificationGesture()
.updating($magnifyBy) {
.....
}
.onChanged() { _ in
//Zoom Out value of magnifyBy < 1.0
if self.magnifyBy <= 0.6{
//Change offset of image after zoom out to center of screen
self.currentPosition = CGSize(width: 1.0, height: 1.0)
}
self.newPosition = self.currentPosition
}
.onEnded{
.....
} }
var body: some View{
Image()
.offset(x: self.currentPosition.width, y: self.currentPosition.height)}
Anyone questions please let me know