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
}
}
}
Related
I want to add the possibility to select items in a list when edit mode is selected, additionally to the delete and move option. Ideally I want to use the existing edit, delete and move buttons instead of writing my own. I tried the example from the documentation. It's not working for me. The value of editMode is always .inactive. I'm using XCode 14. The deployment target of my app is iOS 16.0.
This is my source code:
import SwiftUI
struct ContentView: View {
#Environment(\.editMode)
private var editMode
#State
private var name = "Maria Ruiz"
var body: some View {
NavigationView {
Form {
if editMode?.wrappedValue.isEditing == true {
TextField("Name", text: $name)
} else {
Text("test")
}
}
.animation(nil, value: editMode?.wrappedValue)
.toolbar { // Assumes embedding this view in a NavigationView.
EditButton()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
It always shows the test text. I also tried a variant with the .onChange modifier, with the same result.
Forwarding the fix from https://developer.apple.com/forums/thread/716434:
Try extracting the parts that access the editMode property from the
container that changes based on it, like List/Form.
struct ContentView: View {
var body: some View {
NavigationView {
Form {
MyForm()
}
.toolbar { // Assumes embedding this view in a NavigationView.
EditButton()
}
}
}
}
and
struct MyForm: View {
#Environment(\.editMode)
private var editMode
#State
private var name = "Maria Ruiz"
var body: some View {
Text(String(editMode!.wrappedValue.isEditing))
if editMode?.wrappedValue.isEditing == true {
TextField("Name", text: $name)
} else {
Text("test")
}
}
}
What am I doing wrong here? Tapping on the navigation link triggers the navigation, then the focusState value updates, causing body to run and trigger the navigation link again.
How do I prevent the link from being triggered twice causing my destination views init to fire twice?
import SwiftUI
struct ContentView: View {
#State var text: String = "text"
#FocusState var focussed: Bool
#State var isActive: Bool = false
var body: some View {
NavigationView {
List {
TextField("", text: $text)
.focused($focussed)
.onChange(of: focussed) { _ in }
let _ = Self._printChanges()
NavigationLink("Tap Me", destination: MyViewTwo(), isActive: $isActive)
}
}
}
}
struct MyViewTwo: View {
init() {
print("Init Called")
}
var body: some View {
Text("Hello View 2")
}
}
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)
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.
I'm writing a SwiftUI Mac app that is similar to a kanban board. The app has three lists: Todo, Doing, and Done. At the bottom of each list is a button to move a task to another list. For example the todo list has a Start Doing button. Selecting a task from the todo list and clicking the button should move the task from the todo list to the doing list.
Every SwiftUI list selection example I have seen uses a navigation link. Selecting a list item takes you to another view. But I don't want to want to navigate to another view when selecting a list item. I want the selected task so I can change its status and move it to the correct list when clicking the button.
Here's the code for one of my lists.
struct TodoList: View {
// The board has an array of tasks.
#Binding var board: KanbanBoard
#State private var selection: Task? = nil
#State private var showAddSheet = false
var body: some View {
VStack {
Text("Todo")
.font(.title)
List(todoTasks, selection: $selection) { task in
Text(task.title)
}
HStack {
Button(action: { showAddSheet = true }, label: {
Label("Add", systemImage: "plus.square")
})
Spacer()
Button(action: { selection?.status = .doing}, label: {
Label("Start Doing", systemImage: "play.circle")
})
}
}
.sheet(isPresented: $showAddSheet) {
AddTaskView(board: $board)
}
}
var todoTasks: [Task] {
// Task conforms to Identifiable.
// A task has a status that is an enum: todo, doing, or done.
return board.tasks.filter { $0.status == .todo}
}
}
When I click on a list item, it is not selected.
How do I get the selected item from the list without using a navigation link?
Workaround
Tamas Sengel's answer led me to a workaround. Give each list item a Start Doing button so I don't have to track the selection.
List(todoTasks, id: \.self) { task in
HStack {
Text(task.title)
Button {
task.status = .doing
} label: {
Text("Start Doing")
}
}
}
The workaround helps for my specific case. But I'm going to keep the question open in hopes of an answer that provides a better alternative to using a button for people who want a way to get the selected list item.
Use a Button in the List and in the action, set a #State variable to the current list item.
#State var currentTask: Task?
List(todoTasks, id: \.self) { task in
Button {
currentTask = task
} label: {
Text(task.title)
}
}
Use .environment(\.editMode, .constant(.active)) to turn on selecting capability.
import SwiftUI
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
let id = UUID()
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var multiSelection = Set<UUID>()
var body: some View {
NavigationView {
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// This is a walk-around: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
print(multiSelection)
})
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Put your 3 List with same data array but filtering by status on each one something like:
task.filter({ $0.status == .toDo })
Then on your row add the modifier .onTapGesture be sure to cover all the available space.
Inside the code block introduce your logic or func to change the item status. changeTaskStatus(item: task)