SwiftUI- can't drag a view outside of a ScrollView - ios

Here is a very simplified version of my layout. I have a text view that I'm able to drag around...
struct DragView: View {
var text:String
#State var dragAmt = CGSize.zero
var body: some View {
Text(text)
.padding(15)
.background(Color.orange)
.cornerRadius(20)
.padding(5)
.frame(width: 150, height: 60, alignment: .leading)
.offset(dragAmt)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged {
self.dragAmt = CGSize(width: $0.translation.width, height: $0.translation.height)
}
.onEnded {_ in
self.dragAmt = CGSize.zero
})
}
}
And then I arrange these views like so:
struct ContentView: View {
var body: some View {
HStack {
ScrollView {
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
Divider()
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
}
}
The views that are not in the ScrollView act as expected, and I can drag them around the entire screen. The views that are in the ScrollView, however, will disappear if brought outside the bounds of the ScrollView.
Why does this occur? Is there any way to enable these views to be dragged outside the ScrollView?

Why does this occur?
You drag, you just don't see it because ScrollView clips its content view. Stacks do not, by default. If you add .clipped() for bottom VStack you'll see the same.
Is there any way to enable these views to be dragged outside the
ScrollView?
Solution might be using ZStack. Say on drag you hide one view in ScrollView and show same out-of ScrollView at the same global location, so visually it would behave as would drag original.

Related

How to Increase size of one side of a frame in swiftUI? (iMessage Drag to reveal time effect)

I have been trying to get this effect in SwiftUi, iMessage like time-showing when a message is swiped in TableView
After searching and searching, I figured I would increase the width of my scrollView.
when I do this, it increases on both sides and offsets the recipients chat bubbles off screen to the left as well. Is there a way to only increase the width on one side of the .frame() of the ScrollView to hide just the time?
^Main Question^
Also, I tried offsetting the HStack off screen to the left (to hide time only, right element of HStack) which contains the time and the message bubble, although when I do this, the text is clipped and is not visible when I use .gesture(DragGesture). Is there a better way I can achieve this in swiftUI?
Quick Skeleton of my actual code below.
import SwiftUI
struct ContentView: View {
#State private var draggedOffset = CGSize.zero
var body: some View {
VStack{
ScrollView {
ForEach(0 ..< 20) { item in
VStack {
HStack {
Spacer()
Text("(Message text)")
.padding(30)
.clipShape(Rectangle())
.background(Color.blue)
.padding()
Text("TI:ME ")
.offset(x: 30)
}
}
}
}
.offset(x: draggedOffset.width)
.gesture(DragGesture()
.onChanged { value in
self.draggedOffset = value.translation
}
.onEnded { value in
self.draggedOffset = CGSize.zero
}
)
Spacer()
Text("Message Text Field")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
So I figured out that due to the ScrollView in my project being rotated twice, I needed to use a negative x offset for it to work. Although still wondering about the frame.

Possible to allowsHitTesting on part of view?

I have a button behind a ScrollView, but cannot tap it since it's under it. I have a Spacer at the top of the scroll view that shows the button. I tried putting allowsHitTesting(false) on the Spacer, but this is still not letting it pass underneath the ScrollView.
This is what the `ScrollView looks like, I cannot tap the "Press" button:
This is the code, notice the button is in the ZStack, and the Spacer in the ScrollView has the allowsHitTesting(false):
struct ContentView: View {
#State private var isPresented = false
var body: some View {
ZStack(alignment: .top) {
Button("Press") {
isPresented = true
}
.frame(maxWidth: .infinity, maxHeight: 200)
.background(Color(.label).ignoresSafeArea())
ScrollView {
Spacer()
.frame(height: 200)
.allowsHitTesting(false) // <---- Will not tap thru ScrollView!!
VStack {
Button("Another Press") {
isPresented = true
}
.padding(50)
ForEach((0...50), id: \.self) {
Text("Some text \($0)")
}
}
.frame(maxWidth: .infinity)
.background(Color(.white))
}
}
.navigationBarHidden(true)
.alert(isPresented: $isPresented) {
Alert(title: Text("Button tapped"))
}
}
}
Tapping the button in the background under the Spacer doesn't work. Is there a way to apply the allowsHitTesting(false) to part of the ScrollView that the Spacer occupies? I obviously don't want to apply allowsHitTesting(false) on the entire ScrollView because this is a simplistic example but a real app would have tons of interaction views within the ScrollView. Thanks for any help or insight!

In SwiftUI, how do I animate a view from its current position to the center of the screen?

In this sample app, I have a title in the top left of the screen, and a button in the bottom right. I'm using stacks and spacers to align them.
Currently, when you press the button, it animates up/left a little. But I want the button to animate to the exact center of the screen (or safe area), regardless of device or button size. The code is shown below, along with images of the start and end of the animation I want.
struct ContentView: View {
#State var buttonIsMoved = false
var body: some View {
VStack {
HStack {
Text("Title")
.font(.largeTitle)
Spacer()
}
Spacer()
HStack {
Spacer()
// This is the button I want to animate to the center
Button(action: {
self.buttonIsMoved.toggle()
}) {
Text("This is a button")
.foregroundColor(.black)
.padding(16)
.background(Color.green)
}
// Currently I'm just using fixed offset values,
// but I want it to move to the actual center of the screen
.offset(buttonIsMoved ? CGSize(width: -50, height: -200) : .zero)
.animation(.easeInOut)
}
}
.padding(32)
}
}
Start of animation
End of animation
If I use .offset(), I don't know how to calculate the distance between the button's center and the center of the screen. I've also tried to use .position() but it's based on the parent view, which in this case is an HStack below the title, so it wouldn't be centered within the whole screen. I've also heard of GeometryReader, but I can't figure out how to use it for this purpose.
Here is possible solution - no hardcoding, based on SwiftUI native layout engine.
Tested with Xcode 11.4 / iOS 13.4
struct DemoAnimateLayout: View {
#State var buttonIsMoved = false
var body: some View {
ZStack {
VStack {
HStack {
Text("Title")
.font(.largeTitle)
Spacer()
}
Spacer()
}
VStack {
if !buttonIsMoved { // << here !!
Spacer()
}
HStack {
if !buttonIsMoved { // << here !!
Spacer()
}
// This is the button I want to animate to the center
Button(action: {
self.buttonIsMoved.toggle()
}) {
Text("This is a button")
.foregroundColor(.black)
.padding(16)
.background(Color.green)
}
}
}
.animation(.easeInOut) // << animate container layout !!
}.padding(32)
}
}

Issues with SwiftUI List NavigationLink frame(height: CGFloat)

I am trying to display a custom view for my List Row, it is an HStack with an image, a title Text and another Text containing a short paragraph. I am having issues with the height of the NavigationLink item. I have tried setting the frame to a specific height but it does not display optimally on different devices (the small resolution difference between the iPhone XR and iPhone XS is enough to make me have to adjust the height manually).
Is there a way to have the frame height be set automatically? I have tried many things such as setting the frame height on my PlanetRow View, adding padding, setting the lineLimit to: 0, 3, 5, ... Whatever I do, if I don't set the height for the NavigationLink View, the PlanetRow extends past the NavigationLink View (visible beyond the List separators).
Here's the code for my views:
struct PlanetRow : View {
var planet: Planet
var body: some View {
HStack {
Image("Mars")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text(planet.name)
.font(.largeTitle)
Text(planet.overview)
.font(.caption)
}
.lineLimit(nil)
}
}
}
struct PlanetList : View {
var body: some View {
NavigationView {
List(planetData.identified(by: \.id)) { planet in
NavigationLink(destination: PlanetDetail(planet: planet)) {
PlanetRow(planet: planet)
}
.frame(height: 150)
}
.navigationBarTitle(Text("Planets"))
}
}
}

ForEach inside ScrollView doesn't take whole width

I'm trying to re-create UI of my current app using SwiftUI. And it is way more difficult than I initially though.
I wanted to achieve card-like cells with some background behind them. I found that List doesn't support that, at least yet. List is so limited - it doesn't allow you to remove cell separator.
So I moved to ForEach inside ScrollView. I guess that isn't something which should be used in production for long tables but that should work for now. The problem I have is that ForeEach view doesn't take all the width ScrollView provides. I can set .frame(...) modifier but that will require hardcoding width which I definitely don't want to do.
Any ideas how to force VStack take full width of the ScrollView? I tried to use ForeEach without VStack and it has the same issue. It seems like ScrollView (parent view) "tells" its child view (VStack) that its frame is less that actual ScrollView's frame. And based on that information child views build their layout and sizes.
Here is my current result:
And here is the code:
struct LandmarkList : View {
var body: some View {
NavigationView {
ScrollView() {
VStack {
Spacer().frame(height: 160)
ForEach(landmarkData) { landmark in
LandmarkRow(landmark: landmark).padding([.leading, .trailing], 16)
}
}.scaledToFill()
.background(Color.pink)
}
.background(Color.yellow)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkRow : View {
var landmark: Landmark
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(landmark.name).font(.title)
Text("Subtitle")
.font(.callout)
.color(.gray)
}
Spacer()
Text("5 mi")
.font(.largeTitle)
}.frame(height: 80)
.padding()
.background(Color.white)
.cornerRadius(16)
.clipped()
.shadow(radius: 2)
}
}
I've got the same issue, the only way I have found so far is to fix the ScrollView and the content view width, so that every subview you add inside the content view will be centered.
I created a simple wrapper that take the width as init parameter
struct CenteredList<Data: RandomAccessCollection, Content: View>: View where Data.Element: Identifiable {
public private(set) var width: Length
private var data: Data
private var contentBuilder: (Data.Element.IdentifiedValue) -> Content
init(
width: Length = UIScreen.main.bounds.width,
data: Data,
#ViewBuilder content: #escaping (Data.Element.IdentifiedValue) -> Content)
{
self.width = width
self.data = data
self.contentBuilder = content
}
var body: some View {
ScrollView {
VStack {
ForEach(data) { item in
return self.contentBuilder(item)
}.frame(width: width)
}
.frame(width: width)
}
.frame(width: width)
}
}
By default it takes the screen width (UIScreen.main.bounds.width).
It works just like a List view:
var body: some View {
TileList(data: 0...3) { index in
HStack {
Text("Hello world")
Text("#\(index)")
}
}
}
Its possible that the answer to this might just be wrapping your scrollView inside of a GeometryReader
Like done in the answer here -> How do I stretch a View to its parent frame with SwiftUI?

Resources