SwiftUI: Adjust bounds of ScrollView - swift

I am trying to scale a view horizontally using scaleEffect and MagnificationGesture.
It is almost working as I want with the exception that the ScrollView does not resize when its child view resizes.
Is there a fix for this? Any solution would be greatly appreciated.
To reproduce:
run the code below
scale up the image horizontally with the pinch gesture
notice that the ScrollView scrolls as if the image still has the same size.
struct ContentView: View {
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
var body: some View {
ScrollView(.horizontal) {
Image(systemName: "star")
.resizable()
.scaledToFit()
.scaleEffect(x: finalAmount + currentAmount, y: 1)
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
}
)
}
.frame(maxHeight: 300)
}
}

This view does it.
struct HorizontalScaleView<Content: View>: View {
#ViewBuilder var content: Content
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
var body: some View {
GeometryReader { geo in
ScrollView(.horizontal) {
content
.scaledToFit()
.scaleEffect(x: finalAmount + currentAmount, y: 1)
.frame(width: (finalAmount + currentAmount) * geo.size.width, height: geo.size.width)
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { _ in
if self.finalAmount + self.currentAmount >= 1 {
self.finalAmount += self.currentAmount
} else {
self.finalAmount = 1
}
self.currentAmount = 0
}
)
}
}
}
}

Related

Determine UIScreen half in SwiftUI

I have a gesture applied to my text. Right now, it is sitting at the bottom of the screen and if I drag it to anywhere and let go, it goes back to the bottom. But if I drag it to the top half of my device screen, it should stay at the top.
The feature is almost ready to go, it just needs the correct function to split using UIScreen.main.bounds.
struct SwiftUIView: View {
#State var offset: CGSize = .zero
#State var isOnTop = false
var body: some View {
Text("Hello!")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isOnTop == true ? .top : .bottom)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
withAnimation(.spring()) {
offset = value.translation
}
}
.onEnded { value in
withAnimation(.spring()) {
offset = .zero
// if dragged to the top half of the screen, set true
//isOnTop = true
}
}
)
}
}
This is how I will do, I use GeometryReader to have the size of my screen and I will apply logic to my offset when my gesture is ended
struct SwiftUIView: View {
#State private var yOffset: CGFloat = 0.0
#State private var xOffset: CGFloat = 0.0
#State private var height: CGFloat = 0
#State private var lastXOffset: CGFloat = 0.0
#State private var lastYOffset : CGFloat = 0.0
var body: some View {
GeometryReader{ geo in
VStack{
Spacer()
Text("Hello!")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.offset(x: xOffset, y: yOffset >= 0 ? 0: yOffset)
.gesture(DragGesture().onChanged({ gesture in
self.xOffset = gesture.translation.width + lastXOffset
self.yOffset = gesture.translation.height + lastYOffset
}).onEnded({ _ in
withAnimation{
lastXOffset = xOffset
if yOffset <= -height/2 {
yOffset = -height + geo.safeAreaInsets.top
///save last offset
lastYOffset = yOffset
} else {
yOffset = 0
///save last offset
lastYOffset = yOffset
}
}
}))
}
.onAppear{
height = geo.size.height
}
}
}
}

Dismiss gesture with scale effect

When I add a drag gesture to dismiss my "big view" it scales to the amount of the gesture but if it dismisses, the view jumps to its origin scale and animates the view to the "small view".
Here is my example Code:
`
struct TestView: View {
#State var showBigView = false
#Namespace private var animationNameSpace
#State var gestureOffset: CGSize = .zero
var body: some View {
ZStack {
if !showBigView {
Image(systemName: "tshirt")
.resizable()
.scaledToFit()
.foregroundColor(.red)
.matchedGeometryEffect(id: "circel", in: animationNameSpace)
.transition(.scale(scale: 1))
.frame(width: 100, height: 100)
.onTapGesture {
withAnimation {
showBigView = true
}
}
} else {
Image(systemName: "tshirt")
.resizable()
.scaledToFit()
.foregroundColor(.red)
.matchedGeometryEffect(id: "circel", in: animationNameSpace)
.transition(.scale(scale: 1))
.frame(width: 300, height: 300)
.scaleEffect(abs(gestureOffset.height / 1000 - 1))
.gesture(DragGesture().onChanged {
guard $0.translation.height > 0 else { return }
self.gestureOffset = $0.translation
if abs($0.translation.height) > 150 {
withAnimation {
showBigView = false
gestureOffset = .zero
}
}
})
}
}
}
}
`
It should not jump when the view dismisses with a specific scaleEffect.
I believe you're trying to:
Show the tee shirt image in a 100 by 100 frame.
When tapped, it grows to a 300 by 300 frame with animation.
Dragging downward shrinks the image. When the drag height exceeds 150 points, the image shrinks to its original 100 by 100 frame with animation.
This approach works:
struct TestView: View {
#State private var isBig = false
#State private var frame = TestView.smallFrame
#State private var scale = 1.0
static private let smallFrame = 100.0
static private let bigFrame = 300.0
var body: some View {
Image(systemName: "tshirt")
.resizable()
.scaledToFit()
.frame(width: frame, height: frame)
.scaleEffect(scale)
.foregroundColor(.red)
.onTapGesture {
if !isBig {
withAnimation {
frame = TestView.bigFrame
}
isBig = true
}
}
.gesture(DragGesture().onChanged {
let dragHeight = $0.translation.height
if isBig && dragHeight > 0 {
if dragHeight >= 150 {
frame = TestView.bigFrame * scale
scale = 1
withAnimation {
frame = TestView.smallFrame
}
isBig = false
} else {
scale = 1 - (dragHeight / 1000)
}
}
}.onEnded { _ in
withAnimation {
scale = 1
}
})
}
}
It's not necessary to create two separate images and synchronize them with matchedGeometryEffect. Instead, you can use your #State boolean to control whether or not gestures have any effect.

SwiftUI | GeometryReader: Smooth resizable Header when scrolling through List

I am new to SwiftUI and I want to recreate the Contact-Card View from the Contacts App.
I am struggling to resize the Image on the top smoothly when scrolling in the List below.
I have tried using GeometryReader, but ran into issues there.
When scrolling up for example, the picture size just jumps abruptly to the minimumPictureSize I have specified. The opposite happens when scrolling up: It stops resizing abruptly when I stop scrolling.
Wanted behaviour: https://gifyu.com/image/Ai04
Current behaviour: https://gifyu.com/image/AjIc
struct SwiftUIView: View {
#State var startOffset: CGFloat = 0
#State var offset: CGFloat = 0
var minPictureSize: CGFloat = 100
var maxPictureSize: CGFloat = 200
var body: some View {
VStack {
Image("person")
.resizable()
.frame(width: max(minPictureSize, min(maxPictureSize, minPictureSize + offset)),
height: max(minPictureSize, min(maxPictureSize, minPictureSize + offset)))
.mask(Circle())
Text("startOffset: \(startOffset)")
Text("offset: \(offset)")
List {
Section {
Text("Top Section")
}.overlay(
GeometryReader(){ geometry -> Color in
let rect = geometry.frame(in: .global)
if startOffset == 0 {
DispatchQueue.main.async {
startOffset = rect.minY
}
}
DispatchQueue.main.async {
offset = rect.minY - startOffset
}
return Color.clear
}
)
ForEach((0..<10)) { row in
Section {
Text("\(row)")
}
}
}.listStyle(InsetGroupedListStyle())
}.navigationBarHidden(true)
}
}
Not a perfect solution, but you could separate the header and List into 2 layers in a ZStack:
struct SwiftUIView: View {
#State var startOffset: CGFloat!
#State var offset: CGFloat = 0
let minPictureSize: CGFloat = 100
let maxPictureSize: CGFloat = 200
var body: some View {
ZStack(alignment: .top) {
if startOffset != nil {
List {
Section {
Text("Top Section")
} header: {
// Leave extra space for `List` so it won't clip its content
Color.clear.frame(height: 100)
}
.overlay {
GeometryReader { geometry -> Color in
DispatchQueue.main.async {
let frame = geometry.frame(in: .global)
offset = frame.minY - startOffset
}
return Color.clear
}
}
ForEach((0..<10)) { row in
Section {
Text("\(row)")
}
}
}
.listStyle(InsetGroupedListStyle())
.padding(.top, startOffset-100) // Make up extra space
}
VStack {
Circle().fill(.secondary)
.frame(width: max(minPictureSize, min(maxPictureSize, minPictureSize + offset)),
height: max(minPictureSize, min(maxPictureSize, minPictureSize + offset)))
Text("startOffset: \(startOffset ?? -1)")
Text("offset: \(offset)")
}
.frame(maxWidth: .infinity)
.padding(.bottom, 20)
.background(Color(uiColor: UIColor.systemBackground))
.overlay {
if startOffset == nil {
GeometryReader { geometry -> Color in
DispatchQueue.main.async {
let frame = geometry.frame(in: .global)
startOffset = frame.maxY + // Original small one
maxPictureSize - minPictureSize -
frame.minY // Top safe area height
}
return Color.clear
}
}
}
}
.navigationBarHidden(true)
}
}
Notice that Color.clear.frame(height: 100) and .padding(.top, startOffset-100) are intended to leave extra space for List to avoid being clipped, which will cause the scroll bar get clipped. Alternatively, UIScrollView.appearance().clipsToBounds = true will work. However, it'll make element which moves outside the bounds of List disappear. Don't know if it's a bug.

How do I add a zoomable SVG to SwiftUI?

I have a map that I want to put into my app, and I am currently learning SwiftUI. I want to put in an SVG where I can scroll, zoom, set initial x & y coordinates / initial zoom, max zoom, and tap on an element to open a page. I know I can do this with UIKit, but how do I do this with SwiftUI? If I am not able to do this with an SVG, how can I do this with a raster image that is interactive?
edit: here is my code:
struct ContentView: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
#State private var scale: CGFloat = 1.0
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
var body: some View {
Image("SVG")
.frame(width: 1100, height: 1100)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
.gesture(DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
print(self.newPosition.width)
print(self.newPosition.height)
self.newPosition = self.currentPosition
}
)
.scaleEffect(finalAmount + currentAmount)
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
}
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to make a swipeable view with SwiftUI

I tried to make a SWIFTUI View that allows card Swipe like action by using gesture() method. But I can't figure out a way to make view swipe one by one. Currently when i swipe all the views are gone
import SwiftUI
struct EventView: View {
#State private var offset: CGSize = .zero
#ObservedObject var randomView: EventViewModel
var body: some View {
ZStack{
ForEach(randomView.randomViews,id:\.id){ view in
view
.background(Color.randomColor)
.cornerRadius(8)
.shadow(radius: 10)
.padding()
.offset(x: self.offset.width, y: self.offset.height)
.gesture(
DragGesture()
.onChanged { self.offset = $0.translation }
.onEnded {
if $0.translation.width < -100 {
self.offset = .init(width: -1000, height: 0)
} else if $0.translation.width > 100 {
self.offset = .init(width: 1000, height: 0)
} else {
self.offset = .zero
}
}
)
.animation(.spring())
}
}
}
}
struct EventView_Previews: PreviewProvider {
static var previews: some View {
EventView(randomView: EventViewModel())
}
}
struct PersonView: View {
var id:Int = Int.random(in: 1...1000)
var body: some View {
VStack(alignment: .center) {
Image("testBtn")
.clipShape(/*#START_MENU_TOKEN#*/Circle()/*#END_MENU_TOKEN#*/)
Text("Majid Jabrayilov")
.font(.title)
.accentColor(.white)
Text("iOS Developer")
.font(.body)
.accentColor(.white)
}.padding()
}
}
With this piece of code, when i swipe the whole thing is gone
Basically your code tells every view to follow offset, while actually you want only the top one move. So firstly I'd add a variable that'd hold current index of the card and a method to calculate it's offset:
#State private var currentCard = 0
func offset(for i: Int) -> CGSize {
return i == currentCard ? offset : .zero
}
Secondly, I found out that if we just leave it like that, on the next touch view would get offset of the last one (-1000, 0) and only then jump to the correct location, so it looks just like previous card decided to return instead of the new one. In order to fix this I added a flag marking that card has just gone, so when we touch it again it gets right location initially. Normally, we'd do that in gesture's .began state, but we don't have an analog for that in swiftUI, so the only place to do it is in .onChanged:
#State private var didJustSwipe = false
DragGesture()
.onChanged {
if self.didJustSwipe {
self.didJustSwipe = false
self.currentCard += 1
self.offset = .zero
} else {
self.offset = $0.translation
}
}
In .onEnded in the case of success we assign didJustSwipe = true
So now it works perfectly. Also I suggest you diving your code into smaller parts. It will not only improve readability, but also save some compile time. You didn't provide an implementation of EventViewModel and those randomViews so I used rectangles instead. Here's your code:
struct EventView: View {
#State private var offset: CGSize = .zero
#State private var currentCard = 0
#State private var didJustSwipe = false
var randomView: some View {
return Rectangle()
.foregroundColor(.green)
.cornerRadius(20)
.frame(width: 300, height: 400)
.shadow(radius: 10)
.padding()
.opacity(0.3)
}
func offset(for i: Int) -> CGSize {
return i == currentCard ? offset : .zero
}
var body: some View {
ZStack{
ForEach(currentCard..<5, id: \.self) { i in
self.randomView
.offset(self.offset(for: i))
.gesture(self.gesture)
.animation(.spring())
}
}
}
var gesture: some Gesture {
DragGesture()
.onChanged {
if self.didJustSwipe {
self.didJustSwipe = false
self.currentCard += 1
self.offset = .zero
} else {
self.offset = $0.translation
}
}
.onEnded {
let w = $0.translation.width
if abs(w) > 100 {
self.didJustSwipe = true
let x = w > 0 ? 1000 : -1000
self.offset = .init(width: x, height: 0)
} else {
self.offset = .zero
}
}
}
}