ScrollView nested in TabView reset position to start while changing tab - swift

I'm experimenting with SwiftUI and I found a weird behaviour of ScrollView when nested in a TabView. If I swipe the ScrollView down and then do a slow gesture to swipe to the next tab, it sometimes reset the scrollView Position to the start. I'm not sure how to explain it well so here is a gif:
I'm trying to understand what cause this and if there is a way to avoid it.
Here is the code I use:
var body: some View {
TabView(selection: $selectedTab,
content: {
ForEach(coreData.pageList, id: \.self) { page in
if(coreData.pageList[page] == nil) {
ProgressView().onAppear(perform: {
loadPage(page: page)
})
} else {
ScrollView {
Text("START OF PAGE ------- -----Page Number: \(page)! This is a very short text made big to simulate scrolling. This is a very short text made big to simulate scrolling. ").font(.system(size: 90))
}
}
}
})
}
Am I missing something simple, is it a bug (with workaround), am I using these views wrong?
Ultimately I just want to have something that look like a page that you swipe right left or right but having the scroll position reset so fast would not be great user experience and doesn't look great
Edit: I haven't found any solution yet, but an interesting point: if I put the ProgressView inside the scrollView the scrollview does NOT reset position but the next tab will already be scrolled with the same amount as the previews page. This is equally "bad" if not worse :-(.

Try to put it into separated view (passing all needed parameters), so SwiftUI will see that view not changed and so not rerender it, ie. it should look like
if(coreData.pageList[page] == nil) {
ProgressView().onAppear(perform: {
loadPage(page: page)
})
} else {
DetailsView(page: page) // << ScrollView inside
}

Related

How to make mouse scroll to scroll horizontally in swiftui?

I have a View built using SwiftUI that uses Scroll view horizontally.
var body: some View {
VStack {
// this should scroll horizontally when user uses the mouse scroller
HStack {
}
}
}
Currently user can do that by pressing shift button and scroll but I want to get rid of shift button
Goal: Mouse scroller should scroll horizontally without using shift button
You can see a tutorial here:
https://www.youtube.com/watch?v=-JjN2sRQXLc&t=1176s
With the final code here:
https://github.com/gahntpo/ItunesSearchApp/blob/main/ItunesSearchApp/view/album/SongsForAlbumListView.swift
In sum you will need a ScrollViewReader that gives you a closure with a proxy that could be called to scrollTo method.
You'll need to mart the destinations to scrollTo with an identifiable object or using .id modifier.
All explained in the video.

SwiftUI: Disable ScrollView scrolling to top if top of screen is touched

I have a UI which resides in a VStack{} with a custom topbar and a ScrollView. I have buttons on my top bar which do things like open user account details. I find that if I press some of these buttons the ScrollView autoscrolls to the top of the screen. Is there a way to stop that?
My code:
struct MyView: View {
var body: some View {
VStack(spacing:0) {
TopBarView()
ScrollView(.vertical) {
ForEach(0..<100, id: \.self) { index in
Text(String(index))
}
}
}
}
struct TopBarView: View {
var body: some View {
Text("This is a top bar")
}
}
}
If I touch the top of the screen above the scrollView's frame, the scrollView scrolls all the way to the top. Is there a way to disable that behavior?
This is not a SwiftUI or Xcode or ScrollView problem.
This is a built-in feature of iPhone.
When you tap the top-center edge of the screen, displayed contents will be scrolled up.
Try opening apps like Facebook, WhatsApp, then scroll and tap at the same position, you will see the displayed contents scroll up.
For UIScrollView, there's a scrollsToTop property you can disable:
let scrollview = UIScrollView()
scrollview.scrollsToTop = false
Nothing for SwiftUI's ScrollView as far as I can tell. You could use UIViewRepresentable to wrap UIScrollView for SwiftUI.

Pass through clicks/taps to underlying UIViewRepresentable

I have a UIViewRepresentable that I am using the blend a project created with UIViews and SwiftUI.
My views are full screen and my issue is that I cannot find a way to pass touches through the transparent UINavigationBar at the top of the screen to my underlying UIView. That means that the top 100 or so pixels don't respond to touch even though they're visible. I have seen this post and solution, but it does not work in my case. I believe it's because I'm using UIViewRepresentable.
For my content view I have:
struct ContentView: View {
let values: [String] = ViewType.allCases.map { "\($0.rawValue)" }
var body: some View {
NavigationView {
List {
ForEach(values, id: \.self) { value in
NavigationLink(value, destination: CustomView(viewName: value).ignoresSafeArea())
}
.navigationTitle("Nav Title")
}
}
.accentColor(.white)
}
}
struct CustomView: UIViewRepresentable {
let viewType: String
func makeUIView(context: Context) -> CustomView {
CustomView(view: viewType)
}
func updateUIView(_ view: CustomView, context: Context) {
view.draw(view.frame)
}
}
Here is an image of my view hierarchy that shows the UINavigationBar blocking the top 100 or so pixels of my underlying UIView and preventing touches from being registered by the view.
I know I can disable the navigation bar and that does work, but I do need the back button. Ideally, I'd like to understand how I can apply the solution I linked to above which will pass touches unless there is a UIControl in the way.
UPDATE
I tried Asperi's suggestion of using transparency and then disabling the navigation bar and it occurred to me to check the view debugger with it disabled.
It turns out that the back button is still there in the view hierarchy, but it is not visible in the app when the nav bar is disabled. If there were a way to keep the nav bar disabled but enable the button, that would be the ideal situation.

How to disable opacity with transitions in SwiftUI?

I'm using SwiftUI.
I have a transition that I'm using to bring up my SignInView. However, this transition seems to be automatically applying an opacity effect on the view it's replacing. This wouldn't be a problem, however, it seems like the Safe Area on both the top and the bottom have different rates of receiving the opacity than the rest of the view.
I'm trying to find 1 of 2 solutions:
How can I get rid of the opacity effect altogether, or
How can I get the opacity effect to be applied evenly everywhere.
Here is the code for my transition:
struct AuthView: View {
#State var showSignIn: Bool = false
var body: some View {
ZStack {
if !showSignIn {
WelcomeView(showSignIn: $showSignIn)
} else {
SignInView(showSignIn: $showSignIn)
.transition(AnyTransition.move(edge: .trailing))
.zIndex(1)
}
}
}
}
Here is the button controlling the state variable:
Button(action: { withAnimation(.easeInOut) { showSignIn.toggle() } }) {
//Button text
}
I also have a video (GIF) that better shows what I'm talking about, when I mention the uneven distribution of opacity.
https://i.stack.imgur.com/KXEcF.gif
If you look closely at the top and bottom safe area it fades faster than the rest of the view, which is undesired.
Note: When I changed the appearance to Dark Mode, it turned black instead of white on the top and bottom.
I assume you want identity transition for original view (opacity transition is applied by default if none other specified)
if !showSignIn {
WelcomeView(showSignIn: $showSignIn)
.transition(.identity) // << here !!

SwiftUI onMoveCommand actions aren't executed

I'm making a macOS app with SwiftUI, and I would like to offset a SwiftUI view using the arrow keys on the built-in keyboard.
I couldn't find many resources online, but onMoveCommand() appears to be the event handler I need. Upon trying it out, I discovered that the action I specified for onMoveCommand() does not appear to be executed. Here's some code I wrote just to test it out:
struct ContentView: View {
var body: some View {
Text("Hello")
.onAppear() {
print("Appeared!")
}
.onMoveCommand() { (direction) in
print("Moved!")
}
.onTapGesture() {
print("Tapped!")
}
}
}
onMoveCommand() does not print "Moved!" when I press the arrow keys, instead I get the error alert sound played, and nothing is printed. onAppear() successfully prints the "Appeared!" message when the view appears, and onTapGesture() prints "Tapped!" correctly whenever I click the text. This seems to tell me that the basic syntax I got for these view events is correct, but I implemented onMoveCommand() incorrectly.
For now I only want my app to print something to the Xcode console when the arrow keys are pressed, and to be able to distinguish which arrow key was pressed. Can someone please explain what I did wrong?
Keyboard events are handled only by view in focus, so fix is
var body: some View {
Text("Hello")
.focusable() // << here !!
.onAppear() {
print("Appeared!")
}
.onMoveCommand() { (direction) in
print("Moved!")
}
.onTapGesture() {
print("Tapped!")
}
}
Tested with Xcode 11.4 / macOS 10.15.4. Make sure you have turned on keyboard navigation in System Preferences.