SwiftUI: How to detect navigation up or down - ios

I have following SwiftUI code, which makes a connection (to the server) when the user navigates to this page (this is a part of navigation stack), and disconnects when the user navigate away (up).
struct ServiceView: View {
#ObservedObject var state:ServiceState
var body: some View {
ScrollView(.vertical) {
VStack {
...
NavigationLink(
destination: ChildPage(state: state),
label: {
Text("Child Page")
})
}
}
.onAppear() {
state.connection.connect()
}
.onDisappear() {
state.connection.disconnect()
}
}
}
This works fine as long as the user stays on this page, but breaks when the user clicks the "Child Page" link, because .onDisappear() event will be triggered when the user navigates down as well.
Is there any way to distinguish two different scenarios (navigate up and down)?

Related

Presence of #Environment dismiss causes list to constantly rebuild its content on scrolling

I need to build a list of TextFields where each field is associated with focus id, so that I can auto scroll to such a text field when it receives focus. In reality the real app is a bit more complex which also includes TextEditors and many other controls.
Now, I found out that if my view defines #Environment(\.dismiss) private var dismiss then the list is rebuilding all the time during manual scrolling. If I just comment out the line #Environment(\.dismiss) private var dismiss then there is no rebuilding of the list when I scroll. Obviously, I want to be able to dismiss my view when user clicks some button. In the real app it's even worse: during scrolling everything is lagging, I cannot get smooth scrolling. And my list is not huge it's just 10 items or so.
Here is a demo example:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
DismissListView()
} label: {
Text("Go to see the list")
}
}
}
}
struct DismissListView: View {
#Environment(\.dismiss) private var dismiss
enum Field: Hashable {
case line(Int)
}
#FocusState private var focus: Field?
#State private var text: String = ""
var body: some View {
ScrollViewReader { proxy in
List {
let _ = print("body is rebuilding")
Button("Dismiss me") {
dismiss()
}
Section("Section") {
ForEach((1...100), id: \.self) {num in
TextField("text", text: $text)
.id(Field.line(num))
.focused($focus, equals: .line(num))
}
}
}
.listStyle(.insetGrouped)
.onChange(of: focus) {_ in
withAnimation {
proxy.scrollTo(focus, anchor: .center)
}
}
}
}
}
The questions are:
Why is the list rebuilding during manual back and forth scrolling when #Environment(\.dismiss) private var dismiss is defined, and the same is NOT happening when dismiss is NOT defined?
Is there any workaround for this: I need to be able to use ScrollProxyReader to focus any text field when the focus changes, and I need to be able to dismiss the view, but in the same time I need to avoid constant rebuilds of the list during scrolling, because it drops app performance and scrolling becomes jagged...
P.S. Demo app constantly outputs "body is rebuilding" when dismiss is defined and the list is scrolled, but if any text field gets a focus manually, then the "body is rebuilding" is not printed anymore even if the dismiss is still defined.
I could make an assumption, but that would be really rather a guess (based on experience, observations, etc). In a fact, all WHYs like "why this sh... (bug) happens" should be asked on https://developer.apple.com/forums/ (there are Apple's engineers there) or reported to https://developer.apple.com/bug-reporting/
A solution is to separate dismiss depenent part into dedicated view, so hiding it from parent body (and so do not affect it)
struct DismissView: View {
// visible only for this view !!
#Environment(\.dismiss) private var dismiss
var body: some View {
Button("Dismiss me") {
// affects current context, so it does not matter
// in which sub-view is called
dismiss()
}
}
}
var body: some View {
ScrollViewReader { proxy in
List {
let _ = print("body is rebuilding")
DismissView() // << here !!
// ... other code

SwiftUI searchable on NavigationView always shown, hides only on scroll

I'm writing a fairly simple SwiftUI app about movies and I have this issue where the new .searchable modifier on NavigationView is always being shown, whereas it should be hidden, unless you pull down on the List.
It hides it correctly if I scroll a bit up, and if I scroll down it also hides it correctly, but other than that, it's always being shown. See gif for clarification. (basically it should behave the same as in Messages app)
https://imgur.com/R2rsqzh
My code for using the searchable is fairly simple:
var body: some View {
NavigationView {
List {
ForEach(/*** movie stuff ***/) { movie in
///// row here
}
}
.listStyle(GroupedListStyle())
.onAppear {
// load movies
}
.navigationTitle("Discover")
.searchable(text: $moviesRepository.searchText, placement: .toolbar, prompt: "Search...")
}
}
}
So, after adding a progress view above the list view, it suddenly started working the way I want it to. The code now is minimally changed and looks like this.
var body: some View {
NavigationView {
VStack {
if /** we're doing search, I'm checking search text **/ {
ProgressView()
.padding()
}
if !movies.isEmpty {
List {
ForEach(/** movies list **/) { movie in
// movie row here
}
}
.listStyle(GroupedListStyle())
.navigationTitle("Discover")
.searchable(text: $moviesRepository.searchText, placement: .toolbar,
prompt: Text("Search...")
.foregroundColor(.secondary)
)
} else {
ProgressView()
.navigationTitle("Discover")
}
}
}
.onAppear {
// load movies
}
}
And basically after adding the progress views, it started working the way I described it in my OP and the way it worked for ChrisR

How do I stop a view from refreshing each time I return from a detail view in SwiftUI?

I have a view which displays a list of posts. I have implemented infinite scrolling, and it is functioning properly. however, there is one small problem I am running into, and attempts to solve it have me going round in circles.
Main view
struct PostsHomeView: View {
#EnvironmentObject var viewModel: ViewModel
#State var dataInitiallyFetched = false
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 0) {
if self.viewModel.posts.count > 0 {
PostsListView(posts: self.viewModel.posts,
isLoading: self.viewModel.canFetchMorePosts,
onScrolledAtBottom: self.viewModel.fetchMorePosts
)
} else {
VStack {
Text("You have no posts!")
}
}
}
.onAppear() {
if !self.dataInitiallyFetched {
self.viewModel.fetchMostRecentPosts()
self.dataInitiallyFetched = true
}
}
.navigationBarTitle("Posts", displayMode: .inline)
}
}
}
List view
struct PostsListView: View {
#EnvironmentObject var viewModel: ViewModel
let posts: [Post]
let isLoading: Bool
let onScrolledAtBottom: () -> Void
var body: some View {
List {
postsList
if isLoading {
loadingIndicator
}
}
}
private var postsList: some View {
ForEach(posts, id: \.self) { post in
PostsCellView(post: post)
.onAppear {
if self.posts.last == post {
self.onScrolledAtBottom()
}
}
}
.id(UUID())
}
}
Problem
Upon tapping one of the posts in the list, I am taken to a detail view. When I tap the nav bar's back button in order go back to the posts list, the whole view is reloaded and my post fetch methods are fired again.
In order to stop the fetch method that fetches most recent posts from firing, I have added a flag that I set to true after the initial load. This stops the fetch method that grabs the initial set of posts from firing when I go back and forth between the details view and posts home screen.
I have tried various things to stop the fetchMorePosts function from firing, but I keep going in circles. I added a guard statement to the top of the fetchMorePosts function in my view model. It checks to see if string is equal to "homeview", if not, then the fetch is not done. I set this string to "detailview" whenever the detail view is visited, then I reset it back to "homeview" in the guard statement.
guard self.lastView == "homeview" else {
self.lastView = "homeview"
return
}
This works to an extent, but I keep finding scenarios where it doesn't work as expected. There must be a straight-forward way to tell SwiftUI not to reload a view. The problem is the method sits in the onAppear closure which is vital for the infinite scrolling to work. I'm not using iOS 14 yet, so I can't use #StateObject.
Is there a way to tell SwiftUI not to fire onAppear everytime I return from a detail view?
Thanks in advance
The culprit was .id(UUID()). I removed it from my list and everything worked again.
Thanks Asperi. Your help is much appreciated.

SwiftUI: How to make network request after tapping button in List (custom row view)?

I'm recreating functionality similar to a "Follow" button, like that on Instagram. 1. On button tap, send a follow or unfollow request, and toggle the state of the button.
Problem:
I have a Follow button in a custom row view in a list, and for the life of me, I can't figure out how and where to make the network request and manage the model's state.
Desired Functionality:
1. User taps "Follow"
2. Send a network request to follow User and toggle button state from "Follow" to "Unfollow"
3. If error, flip button back to original state (maybe display error, not critical) by modifying the model
4. If success, do nothing because state change was optimistically performed after (2.)
For the sake of simplicity, I simplified models and views. Here's what I have
struct UserRowView: View {
#ObservedObject var user: User
var body: some View {
Button(action: {
// 1. The user object has an ID property, which I'll need for the network request
// 2. Do I handle state management, and send network request here? Seems strange from a List's cell.
}) {
Text(user.isFollowing ? "Unfollow" : "Follow")
}
.background(user.isFollowing ? Color.green : Color.red)
}
}
And the List...
struct ContentView: View {
#ObservedObject var userManager = UserManager()
var body: some View {
VStack {
List() {
Section(header:
Text("USERS")
) {
ForEach(0 ..< userManager.users.count, id: \.self) {
// Do I pass a closure to UserRowView, so I can access the
// UserManager() instance? If so, how do I get the index of the
// cell containing the button?
UserRowView(user: self.userManager.users[$0])
}
}
}
}
}
}
My questions are noted inline in both code blocks.
Thanks

Prevent NavigationLink from processing state variable

I don't like my title, but it's the best I could think of. Basically, I have two Views: SearchInput and SearchResultList. The user enters a search term with SearchInput, taps "Go" and then sees the results of the search with SearchResultList. The code is:
struct SearchInput: View {
#State private var searchTerm: String = ""
var searchResult: [RTDocument] {
return RuntimeDataModel.shared.fetchRTDocuments(matching: self.searchTerm)
}
var body: some View {
NavigationView {
VStack {
TextField("Enter search term...", text: self.$searchTerm)
NavigationLink(destination: SearchResultList(
rtDocuments: self.searchResult
)) { Text("Go") } //NavigationLink
} //VStack
} //NavigationView
} //body
} //SearchInput
The issue I'm having is that every time the user enters a character in TextField, the bound-state variable searchTerm is updated which causes NavigationLink to reevaluate its destination -- which causes a Core Data fetch up in the computed searchResult variable.
I'd like the fetch to happen only once, when the user taps "Go". Is there a way to accomplish that?
This is expected behaviour, so in general you should redesign your SearchResultList, but this can help as a walkaround:
Declare a state on your input:
#State var shouldNavigate = false
Then separate the button form the navigation link.
Group {
Button("Go") {
self.shouldNavigate = true
}
NavigationLink(destination: Text("SearchResultList"),
isActive: $shouldNavigate) {
EmptyView()
}
}

Resources