Animate removing/adding SwiftUI view while animating offset - ios

I'm trying to animate the offset of a SwiftUI view, while at the same time fading out and removing a subview of that view. The problem I'm running into is that SwiftUI performs the offset and fade-out animations, but doesn't combine them.
What I want to achieve to animate the position of the whole SubView, while simultaneously fading out the subtitle text, so that the subtitle text moves vertically while fading in or out. I can achieve this by animating the opacity of the Text instead of removing it, but that means the text will still take up "layout space".
Is there a way to achieve this animation with the if showSubtitle statement?
The following code and GIF demonstrate the problem:
struct ContentView: View {
#State private var showSubtitle = true
var body: some View {
SubView(showSubtitle: showSubtitle)
.animation(.default)
.offset(y: showSubtitle ? 100 : 0)
.onTapGesture {
self.showSubtitle.toggle()
}
}
}
struct SubView: View {
let showSubtitle: Bool
var body: some View {
VStack {
Text("Header")
if showSubtitle {
Text("Subtitle")
}
}
}
}

Actually the observed behaviour is because .offset does not change layout, the view is stand at the same place. So when you remove subview it is removed in-place and animating that removal (with default .opacity transition). The part that starts offsetting does not contain already subview, so you don't see it in moving up part.
Here is something that might give some kind of effect you expect, but transitions are based on source size, so it is not so far and manually specified distance of offset. Anyway, try:
if showSubtitle {
Text("Subtitle")
.transition(AnyTransition.opacity.combined(with: AnyTransition.move(edge: .top)))
}
Tested with Xcode 12 / iOS 14

Related

SwiftUI transmit drag focus to outer view (ScrollView inside dragable)

What I want
I have a "ScrollView" in a custom sheet that I can drag down to close. Of course, the ScrollView is above the drag area, so when I drag over the ScrollView it scrolls down or up. I want to disable the ScrollView when I am at the top of the ScrollView and scroll up so that the sheet is dragged and starts to close. This is similar to the behaviour of the Shazam sheet.
The Problem
If the ScrollView is deactivated, the current drag action is not applied to the sheet, but does nothing. Only when dragging again (on the now deactivated ScrollView) the sheet is focused on the dragging. So is there a way to transfer the focus of the drag action from the ScrollView to the outer sheet view without starting a new one?
Showcase
I created a simplified version of the problem for better understanding.
We have a container (inner) that is draggable along the y-axis. On drag release, we let it jump back to offset 0.
#State var offsetY: CGFloat = .zero
var body: some View {
Inner(outerOffset: $offsetY)
.offset(y: offsetY)
.gesture(DragGesture().onChanged { value in
offsetY = value.translation.height
}.onEnded { _ in
offsetY = 0
}
)
}
Inside the container we have our own ScrollView (ScrollViewOffset) which takes a callback with the current scroll offset. If the offset is positive (i.e. if we are scrolling upwards, even if we are at the top of the content), we want to disable the scroll and let the outer container drag along the y-axis instead of the ScrollView. To activate the ScrollView we listen for the outerOffset (the drag value) and activate the ScrollView when the offset is 0 again (this happens when releasing the drag as described before).
struct Inner: View {
#Binding var outerOffset: CGFloat
#State var disableScroll = false
var body: some View {
ScrollViewReader { proxy in
ScrollViewOffset {
ForEach(0 ... 10, id: \.self) { e in
Text(String(e))
.id(e)
.padding()
Divider()
}
} onOffsetChange: { offset in
if offset > 0 {
disableScroll = true
}
}
.background(.red)
.padding()
.frame(width: nil, height: 400)
.onChange(of: outerOffset) { _ in
if outerOffset == 0 {
proxy.scrollTo(0)
disableScroll = false
}
}
.disabled(disableScroll)
}
}
}
When we run this, it is easy to see the problem I mentioned before. The ScrollView is getting disabled as it should, but the container is not focused on the drag. Only after starting a new drag, the container is focused.
My actual case
Last but not least the shazam equivalent (left) and my project (right).

Context menu preview not with rounded corners in SwiftUI

When using a plain styled List in SwiftUI with more than one Text view inside a VStack as show below, the preview of the view when showing its context menu doesn't have corner radius. If you remove one of the Text views it will have corner radius. Also the rows that you need to scroll down to will also show corner radius most of the time. I've tried using the contentShape modifier with RoundedRectangle but doesn't fix it. How can I get it to show with corner radius all the time?
List {
ForEach(1...20, id: \.self) { _ in
VStack {
Text("Hello")
Text("World")
}.contextMenu {
Button {} label: { Text("Hello") }
}
}
}.listStyle(.plain)
I thinks you should fill a bug to Apple in this case.
After some try I have notice that if you define a frame for your Label , the view is correctly rounded.
I tryed to fixedSize the text without success :
Text("world").fixedSize()
The problem seems to come from _UIMorphingPlatterView, more precisely the _UIPlatterClippingView which use _UIPortalView.
If the frame is not fixed the clip is not apply correctly.
The debug view hierarchy give this :

SwiftUI MultiviewResponder mystery crash

I came across this crash I can't explain, even after I boiling it down to the minimal components which would still cause the crash:
Repeatedly tapping the screen quickly (use two fingers) to hide/show an overlayed button with an explicit animation will crash the app under this VERY specific layout:
import SwiftUI
struct ContentView: View {
#State var showControls: Bool = true
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.gray) // Removing this would fix it
VStack {
Text("someText")
Text("anotherText") // Removing this would also fix it
}
if showControls {
Text("Button")
.zIndex(1) // Removing this would also fix it
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) { // Removing this would also fix it
showControls.toggle()
}
}
}
}
As mentioned in the code comments, any of these changes prevent the crash:
Remove the Rectangle from the bottom of the ZStack
Remove one of the two views from the VStack
Remove the zIndex (this will break the fade-out animation)
Remove the explicit animation call
Here is my console output:
I would like to know what's happening here, what's causing the crash ?

How to check if item is visible - SwiftUI ScrollView

Trying to programmatically determine when an item is being displayed on screen in a ScrollView in SwiftUI. I understand that a ScrollView renders at one time rather than rendering as items appear (like in List), but I am constrained to using ScrollView as I have .scrollTo actions.
I also understand that in UIKit with UIScrollView, it is possible to use a CGRectIntersectsRect between the item frame and the ScrollView frame in the UIScrollViewDelegate but I would prefer to find a solution in SwiftUI if possible.
Example code looks like this:
ScrollView {
ScrollViewReader { action in
ZStack {
VStack {
ForEach(//array of chats) { chat in
//chat display bubble
.onAppear(perform: {chatsOnScreen.append(chat)})
}.onReceive(interactionHandler.$activeChat, perform: { _ in
//scroll to active chat
})
}
}
}
}
Ideally, when a user scrolls, it would check which items are on screen and zoom the view to fit the largest item on screen.
When you use VStack in ScrollView all content is created at once at build time, so onAppear does not fit your intention, instead you should use LazyVStack, which will create each subview only before it appears on screen, so onAppear will be called when you expect it.
So it should be like
ScrollViewReader { action in
ScrollView {
LazyVStack { // << this !!
ForEach(//array of chats) { chat in
//chat display bubble
.onAppear(perform: {chatsOnScreen.append(chat)})
}.onReceive(interactionHandler.$activeChat, perform: { _ in
//scroll to active chat
})
}
}
}

Change background color of View inside TabView having NavigationView and ScrollView in SwiftUI

I want to build a TabView with 4 tabs having collection views in it. Below is my code of one tab named 'Gallery'.
var body: some View {
NavigationView {
ScrollView {
GridStack(rows: 3, columns: 2) { row, column, totalColumn in
CardView(card: self.cards[(row * totalColumn) + column])
}.padding().background(Color.red)
}
.navigationBarTitle("Gallery")
}
}
When I give background color for ScrollView, scrolling is not working for NavigationView largeTitle. How can I achieve this, I want to give red color for full view's background? What if I need to achieve this same backgorund color for all tabs?
Here is possible approach (scroll view is not broken in such case)
NavigationView {
GeometryReader { gp in
ScrollView {
ZStack(alignment: .top) {
Rectangle().fill(Color.red) // << background
// ... your content here, internal alignment might be needed
}.frame(minHeight: gp.size.height)
}
.navigationBarTitle("Gallery")
}
}

Resources