SwiftUI NavigationLink pops automatically back - swiftui

I am using the searchable modifier on a List of Titles (Entity) stored in CoreData.
When I click on a link without the search the NavigationLink opens the View (PlayerView) perfectly.
But when I use the search field to search for a specific title and click on the link in the search result the PlayerView opens for just a moment and then pops back to the NavigationView.
Is there anybody out there with a hint how to fix that?
Here is my code:
struct NavTest: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Title.restid, ascending: false)])
var coreList: FetchedResults<Title>
#State private var searchText = ""
var query: Binding<String> {
Binding {
searchText
} set: { newValue in
searchText = newValue
coreList.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "title CONTAINS %#", newValue)
}
}
var body: some View {
NavigationView{
List{
ForEach(coreList, id: \.self) { (item: Title) in
NavigationLink(destination: PlayerView(item.title!)){
Text(item.title!)
}
}
}
.searchable(text: query)
}
}
}

I had the same problem and this post helped me. Try adding this to the NavigationView:
.navigationViewStyle(.stack)

Related

NavigationStack Searchable .focused

I need to configure when the user clicks in the Search box to fulfill the condition (display another View). Once he clicks Cancel to display the original view (which can already be tested via .onChange(of: searchText) { value in if (!value.isEmpty) {...)
NavigationStack {
...
if showView == true {}
...
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: LocalizedStringKey("Look for something"))
.focused($focusState)
.onChange(of: focusState, perform: {
showView = true
})
When the user starts searching, I need to show a different View and hide the original one because I have search settings on the new one. As soon as he clicks on the Search button, the search starts.
#FocusState isn't the way to handle this, as the search bar does update or respond to changes in this state.
What you need to use is the isSearching Environment variable in the view on which the .searchable modifier is applied, for example:
struct ContentView: View {
#State private var searchText = ""
var body: some View {
NavigationView {
SearchingView(searchText: $searchText)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: LocalizedStringKey("Look for something"))
}
}
}
struct SearchingView: View {
#Environment(\.isSearching) private var isSearching
#Binding var searchText: String
var body: some View {
if isSearching {
// Show your filtered data view
} else {
// Show non-searching view
}
}
}

SwiftUI: NavigationView focus on last selected NavigationLink

maybe a very simple problem:
I use a navigation with a long list of entries. If the user returns from the navigationLink the list starts on the first item. How can I set the focus on the last selected navigationLink so the user don't need to scroll from the beginning again.
My app is for blind people so the scrolling from above isn't an easy thing.
´´´
struct CategoryDetailView: View {
#EnvironmentObject var blindzeln: BLINDzeln
#AppStorage ("version") var version: Int = 0
#State var shouldRefresh: Bool = false
#State private var searchText = ""
let categoryTitle: String
let catID: Int
var body: some View {
VStack{
List {
ForEach(blindzeln.results.filter { searchText.isEmpty || ($0.title.localizedCaseInsensitiveContains(searchText) || $0.textBody.localizedCaseInsensitiveContains(searchText)) }, id: \.entryID){ item in
NavigationLink(destination: ItemDetailViewStandard(item: item, isFavorite: false, catID: catID)) {DisplayEntryView(item: item, catID: catID)}.listRowSeparatorTint(.primary).listRowSeparator(.hidden)
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "") {}
.navigationTitle(categoryTitle)
.navigationBarTitleDisplayMode(.inline)
.listStyle(.inset)
}
.task(){
await blindzeln.decodeCategoryData(showCategory: categoryTitle)
}
.onAppear(){
blindzeln.resetData()
}
}
}
´´´
you could try this approach, using the List with selection, such
as in this example code. It does not scroll back to the beginning of the list
after selecting a destination.
struct ContentView: View {
#State private var selections = Set<Thing>()
#State var things: [Thing] = []
var body: some View {
NavigationStack {
List(things, selection: $selections){ thing in
NavigationLink(destination: Text("destination-\(thing.val)")) {
Text("item-\(thing.val)")
}
}
}
.onAppear {
(0..<111).forEach{things.append(Thing(val: $0))}
}
}
}
EDIT-1:
Since there are so many elements missing from you code, I can only guess
and suggest something like this:
struct CategoryDetailView: View {
#EnvironmentObject var blindzeln: BLINDzeln
#AppStorage ("version") var version: Int = 0
#State var shouldRefresh: Bool = false
#State private var searchText = ""
#State private var selections = Set<Thing>() // <-- same type as item in the List
let categoryTitle: String
let catID: Int
var body: some View {
VStack {
// -- here
List(blindzeln.results.filter { searchText.isEmpty || ($0.title.localizedCaseInsensitiveContains(searchText) || $0.textBody.localizedCaseInsensitiveContains(searchText)) },
id: \.entryID,
selection: $selections){ item in
NavigationLink(destination: ItemDetailViewStandard(item: item, isFavorite: false, catID: catID)) {
DisplayEntryView(item: item, catID: catID)
}
.listRowSeparatorTint(.primary).listRowSeparator(.hidden)
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "") {}
.navigationTitle(categoryTitle)
.navigationBarTitleDisplayMode(.inline)
.listStyle(.inset)
.task{
await blindzeln.decodeCategoryData(showCategory: categoryTitle)
}
.onAppear{
blindzeln.resetData()
}
}
}

Issue in Setting Value to #State variable in SwiftUI [duplicate]

Hello I am running into a problem here and I do not have a consistent behavior between my .sheet() view when running on ios13 or ios14
I got a view like this :
#State private var label: String = ""
#State private var sheetDisplayed = false
///Some code
var body: some View {
VStack {
Button(action: {
self.label = "A label"
self.isDisplayed = true
}) {
Text("test")
}
}.sheet(isPresented: $sheetDisplayed, onDismiss: {
self.label = ""
}) {
Text(self.label)
}
}
On ios 13 this work as expected btn click -> set label -> call sheet -> display "A label" in a Text view.
On ios14 I got an empty string in self.label when in sheet closure, hence it does not display anything.
Did I missed something ? Is it an iOS 14 bug or did I had it wrong on ios13 and that got corrected.
PS: I have a couple of other variables that are passed in the closure I simplified it.
Your code have expectation of view update/creation order, but in general it is undefined (and probably changed in iOS 14).
There is explicit way to pass information inside sheet - use different sheet creator, ie. .sheet(item:...
Here is working reliable example. Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State private var item: Item?
struct Item: Identifiable {
let id = UUID()
var label: String = ""
}
var body: some View {
VStack {
Button(action: {
self.item = Item(label: "A label")
}) {
Text("test")
}
}.sheet(item: $item, onDismiss: {
self.item = nil
}) {
Text($0.label)
}
}
}
This is some really strange behaviour in iOS 14, which doesn't appear to be documented.
Using the other answer here and the comment on this thread, I used #Binding to solve the issue as it seemed the cleanest and most SwiftUI-esq solution.
I have no idea why this behaviour has changed, and it seems less intuitive than before, so I'm assuming its a bug!
An example:
struct MainView: View {
#State private var message = ""
#State private var showSheet = false
var body: some View {
Button(action: {
self.message = "This will display the correct message"
self.showSheet = true
}, label: {
Text("Test Button")
})
.sheet(isPresented: self.$showSheet) {
SheetView(message: self.$message)
}
}
}
struct SheetView: View {
#Binding var message: Int
var body: some View {
Text(self.message)
}
}
The behaviour changed with SwiftUI 2.0, so it affects MacOS 11 as well, just adding a binding to the view fixes it even when that binding is never used, which makes me think this is an implementation bug.
Additionally just using the details state variable in a Text() within the body of the view also fixes it.
struct MyViewController : View {
#State var details: String?
#State var showDetails = false
// #Binding var havingAbindingFixesIt: String?
var body: some View {
VStack {
// Text(details ?? "")
Text("Tap here for details")
.onTapGesture {
self.details = "These are the details"
self.showDetails.toggle()
}
.sheet(isPresented: $showDetails) { Text(details ?? "") }
}
}
}

TabView is not switching tabs properly in SwiftUI

I'm having a weird problem that I can't seem to figure out with SwiftUI. I have a TabView in my ContentView, there are 3 tabs (chat list, user list, and profile) the app loads up on the chat list tab. The problem is, when I select the second tab (user list) it goes to that tab for a split second, then goes right back to the chat list. It doesn't make any sense to me. The 2 main tabs make an API call to get information from a server, and everything is working great, except that first click.
The app loads up, I click the user list tab and it shows for a split second, then goes back to the chat list tab. I can then click the user list tab again and it will go to that tab and stay there, but the first click on that tab always sends you back to the chat list tab.
I'll post up some of my code, hopefully someone will be able to see what I'm doing wrong, because I sure can't.
ContentView
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var selectedTab = 0
#State var setup: Bool = false
#State var notificationChatID: String = ""
#ObservedObject var userModel: UserModel = UserModel()
#ObservedObject var chatModel: ChatModel = ChatModel()
#ObservedObject var appState: AppState = AppState.shared
var pushNavigationBinding : Binding<Bool> {
.init { () -> Bool in
appState.selectedChatID != nil
}
set: { (newValue) in
if !newValue {
appState.selectedChatID = nil
}
}
}
let settings = UserDefaults.standard
var body: some View {
ZStack {
if setup {
TabView(selection: $selectedTab) {
ChatList(launchedChatID: appState.selectedChatID ?? "", userModel: userModel, chatModel: chatModel)
.tabItem {
Image(systemName: "message.circle.fill")
Text("Active Chats")
}
UserList(userModel: userModel)
.tabItem {
Image(systemName: "person.3.fill")
Text("User List")
}
ProfileView()
.tabItem {
Image(systemName: "person.crop.circle")
Text("Profile")
}
}
} else {
Onboarding(userModel: userModel, isSetup: $setup)
}
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name.NewMessage), perform: { notification in
if let info = notification.userInfo {
let chatID = info["chatID"] as? String ?? ""
if chatID != "" {
chatModel.selectedChat = chatID
appState.selectedChatID = chatID
self.notificationChatID = chatID
}
}
})
}
Then my UserList
struct UserList: View {
#ObservedObject var userModel: UserModel
#ObservedObject var chatModel: ChatModel = ChatModel()
#State var action: Int? = -1
var body: some View {
NavigationView {
VStack {
List {
ForEach(0..<userModel.arrayOfUsers.count, id: \.self) { index in
ZStack(alignment: .leading) {
NavigationLink(
destination: ChatView(model: chatModel, userModel: userModel, item: $action),
tag: index,
selection: $action
) {
EmptyView().frame(width: .zero, height: .zero, alignment: .center)
}
Button(action: {
print("You selected \(userModel.arrayOfUsers[index].name)")
userModel.selectedUserName = userModel.arrayOfUsers[index].name
userModel.selectedUserID = userModel.arrayOfUsers[index].id
self.action = index
}) {
Text(userModel.arrayOfUsers[index].name)
}
}
}
}
Spacer()
}.navigationBarTitle(Text("Users"), displayMode: .inline)
}.onAppear() {
print("Inside the userlist on appear")
if userModel.arrayOfUsers.count == 0 {
ApiService.getUsers() { (res) in
switch(res) {
case .success(let response):
print("In success")
let users = response!.users!
DispatchQueue.main.async {
for user in users.users {
userModel.arrayOfUsers.append(user)
}
}
break
case .failure(let error):
print("Error getting users")
print(error)
break
}
}
}
}
}
}
My userModel.arrayOfUsers is #Published
UserModel
class UserModel: ObservableObject {
var name: String = ""
var id: String = ""
var myUserID: String = ""
#Published var arrayOfUsers: [User] = []
var selectedUserID: String = ""
var selectedUserName: String = ""
}
In the console in Xcode I see
Inside the userlist on appear
...(network stuff)
In the success
In the on appear in ChatList
So it's loading the UserList, it shows the network call out to my API, it shows the In the success from the API call in the UserList, then the very next thing is back to the In the on appear in ChatList I can't figure out why it's kicking me back to the chat list.
You're binding your TabView's current tab to $selectedTab, but not providing SwiftUI with any information on how to alter that value when the user changes tabs. And so, because selectedTab hasn't changed, when the drawing system comes to review your view structure, it still concludes that you want to see the first tab.
You should add a .tag modifier after each .tabItem to tell SwiftUI what values represent each tab. Then, when the user selects each tab, selectedTab will be updated and the tab choice will "stick".
For example:
TabView(selection: $selectedTab) {
ChatList(launchedChatID: appState.selectedChatID ?? "", userModel: userModel, chatModel: chatModel)
.tabItem {
Image(systemName: "message.circle.fill")
Text("Active Chats")
}
.tag(0)
UserList(userModel: userModel)
.tabItem {
Image(systemName: "person.3.fill")
Text("User List")
}
.tag(1)
ProfileView()
.tabItem {
Image(systemName: "person.crop.circle")
Text("Profile")
}
.tag(2)
}
Note that unless you're persisting the user's choice in some way (e.g., by declaring your state variable with #SceneStorage) you can get the same effect by not using a selection argument at all.

SwiftUI #State and .sheet() ios13 vs ios14

Hello I am running into a problem here and I do not have a consistent behavior between my .sheet() view when running on ios13 or ios14
I got a view like this :
#State private var label: String = ""
#State private var sheetDisplayed = false
///Some code
var body: some View {
VStack {
Button(action: {
self.label = "A label"
self.isDisplayed = true
}) {
Text("test")
}
}.sheet(isPresented: $sheetDisplayed, onDismiss: {
self.label = ""
}) {
Text(self.label)
}
}
On ios 13 this work as expected btn click -> set label -> call sheet -> display "A label" in a Text view.
On ios14 I got an empty string in self.label when in sheet closure, hence it does not display anything.
Did I missed something ? Is it an iOS 14 bug or did I had it wrong on ios13 and that got corrected.
PS: I have a couple of other variables that are passed in the closure I simplified it.
Your code have expectation of view update/creation order, but in general it is undefined (and probably changed in iOS 14).
There is explicit way to pass information inside sheet - use different sheet creator, ie. .sheet(item:...
Here is working reliable example. Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State private var item: Item?
struct Item: Identifiable {
let id = UUID()
var label: String = ""
}
var body: some View {
VStack {
Button(action: {
self.item = Item(label: "A label")
}) {
Text("test")
}
}.sheet(item: $item, onDismiss: {
self.item = nil
}) {
Text($0.label)
}
}
}
This is some really strange behaviour in iOS 14, which doesn't appear to be documented.
Using the other answer here and the comment on this thread, I used #Binding to solve the issue as it seemed the cleanest and most SwiftUI-esq solution.
I have no idea why this behaviour has changed, and it seems less intuitive than before, so I'm assuming its a bug!
An example:
struct MainView: View {
#State private var message = ""
#State private var showSheet = false
var body: some View {
Button(action: {
self.message = "This will display the correct message"
self.showSheet = true
}, label: {
Text("Test Button")
})
.sheet(isPresented: self.$showSheet) {
SheetView(message: self.$message)
}
}
}
struct SheetView: View {
#Binding var message: Int
var body: some View {
Text(self.message)
}
}
The behaviour changed with SwiftUI 2.0, so it affects MacOS 11 as well, just adding a binding to the view fixes it even when that binding is never used, which makes me think this is an implementation bug.
Additionally just using the details state variable in a Text() within the body of the view also fixes it.
struct MyViewController : View {
#State var details: String?
#State var showDetails = false
// #Binding var havingAbindingFixesIt: String?
var body: some View {
VStack {
// Text(details ?? "")
Text("Tap here for details")
.onTapGesture {
self.details = "These are the details"
self.showDetails.toggle()
}
.sheet(isPresented: $showDetails) { Text(details ?? "") }
}
}
}