can we use on change for a button to present a list within the same view ? I am a beginner
struct ViewMe: View {
var body: some View {
Button (action:{
},label:{
Text("Search")
})
// can we do on change here to appear a list
}
}
According to the information you gave me, here is some code that should work with explanations for each part:
struct ContentView: View {
//Your variable. #State makes it reload the view when changed.
#State var listIsShowing = false
var body: some View {
VStack {
//Your Button
Button (action:{
//Sets variable to true, showing list
listIsShowing = true
},label:{
Text("Search")
})
//To put the button at the top
Spacer(minLength: 0)
//if variable that the button changes = true, show list
if listIsShowing {
//Your list
List {
Text("Your")
Text("list")
Text("appears")
Text("When")
Text("you")
Text("click")
Text("the")
Text("button")
}
//Use below code if you want background to match the top section
//.listStyle(.plain)
}
}
}
}
Related
I have a SwiftUI view with a search bar on iOS 15. When the search bar is activated, a search list is presented, when no search is active, a regular content view is shown.
The problem I am facing is that when I activate a navigation link from the search list, when the navigation starts to take effect, the isSearching flag is turned to false and the regular content view is shown, even though I would want to search to stay active, just like when we would have a list/table and the user would select a row: the search stays active, and when the user navigates back, the search results are still displayed.
Is there a way in SwiftUI to control how the isSearching is changed?
I put together a small sample project that demoes the problem:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: ContentView.ViewModel
var body: some View {
NavigationView {
VStack {
ContentViewWrapper(viewModel: viewModel)
}
.navigationTitle("Searchable")
.navigationBarTitleDisplayMode(.inline)
.searchable(text: $viewModel.searchString, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search")
.navigationViewStyle(.stack)
.edgesIgnoringSafeArea(.top)
}
}
}
// MARK: View model for the content view
extension ContentView {
class ViewModel: ObservableObject {
#Published var isShowingDestinationScreen = false
#Published var isSearching = false
#Published var searchString = ""
func buttonTapped() {
if !isShowingDestinationScreen {
isShowingDestinationScreen = true
}
}
func isSearchingHasChanged(newValue: Bool) {
if isSearching != newValue {
isSearching = newValue
}
}
}
}
// MARK: Wrapper for the content view so it can be used with the searchable API
struct ContentViewWrapper: View {
#ObservedObject var viewModel: ContentView.ViewModel
#Environment(\.isSearching) var isSearching
var body: some View {
VStack {
if viewModel.isSearching {
NavigationLink(
isActive: $viewModel.isShowingDestinationScreen,
destination: {
DestinationView()
.navigationTitle("Destination")
}, label: {
EmptyView()
}
)
SearchList() {
viewModel.buttonTapped()
}
} else {
ContentViewMenu()
}
}
.onChange(of: isSearching) { newValue in
viewModel.isSearchingHasChanged(newValue: newValue)
}
}
}
// MARK: Just three simple screens below
struct ContentViewMenu: View {
var body: some View {
Text("Content View Menu")
}
}
struct SearchList: View {
var destinationButtonTapped: () -> Void
var body: some View {
VStack {
Text("Search list")
Button("Go to destination") {
destinationButtonTapped()
}
}
}
}
struct DestinationView: View {
var body: some View {
Text("Destination")
}
}
Also here is a short video showing the behaviour: note how when the Go to destination button is tapped, the screen is updated to the content view because isSearching turns false.
Is there a way to keep isSearching true in this case?
I believe you have two options here:
Normally, when users click on a search field, we expect them to always enter something. It is not possible that someone clicks on a search field without typing anything, otherwise it's just an accident touch, so anything should not execute because of this. Your solution here is: you don't have to do anything at all. Just type anything to the search bar after you clicked on it; you can even just input a space, then your search and search result will always remain active no matter what.
If you still want your search bar to be active even though there is zero interaction or input with the search bar, you can adjust some part of your ContentViewWrapper as below(But I think it's not practical to do this because why would you want your search bar to be active without any input?):
code:
struct ContentViewWrapper: View {
#ObservedObject var viewModel: ContentView.ViewModel
#Environment(\.isSearching) var isSearching
//new code
#State var isShowing = false
var body: some View {
VStack {
//new code
if viewModel.isSearching || isShowing {
NavigationLink(
isActive: $viewModel.isShowingDestinationScreen,
destination: {
DestinationView()
.navigationTitle("Destination")
}, label: {
EmptyView()
}
)
.onAppear {
isShowing = true
}
}
//new code
if isShowing {
SearchList() {
viewModel.buttonTapped()
}
}
}
.onChange(of: isSearching) { newValue in
viewModel.isSearchingHasChanged(newValue: newValue)
}
}
}
Let me know if this is a duplicate question.
I am developing an app with a photo picker. I am on view A and there is a + button on it. When I tap on the +, I want to navigate to a view B. Upon navigation I want to present a Photo Picker view automatically inside view B. I am not able to figure out how to do that presentation of the sheet in Swiftui.
In UIKit,on the viewdidappear of viewcontroller B, i would present the Pickerview.
Here's the code that I have
```import SwiftUI
struct ViewB: View {
#State private var showingImagePicker = false
#State private var uploadedPhotos = [UIImage]()
var body: some View {
VStack {
Button("Select Image") {
showingImagePicker = true
}
}
.sheet(isPresented: $showingImagePicker) {
PhotoPicker(photoList: $uploadedPhotos)
}
.onChange(of: uploadedPhotos) { _ in
}
}
}```
This code is ViewB and I will present the pickerView on tapping a button in viewB. but I don't want to tap on button but instead want the picker to show up on appear of ViewB
There is a view modifier in SwiftUI known as .onAppear(perform:), which is the SwiftUI counterpart to UIKit's viewDidAppear and gives you an entry point upon the construction and display of a SwiftUI view. Simply add this modifier to View B (the view inside the sheet that you are presenting). In the closure that you provide to the modifier, you can change the state to present the picker as needed.
If you'd like the picker view to animate in after the view appears, the appropriate place to declare the transition and animation context is on the view acting as your picker view.
struct ViewB: View {
#State private var displayPicker = false
var body: some View {
VStack {
Text("This is View B")
if displayPicker {
PickerView()
.transition(.slide)
.animation(.easeInOut, value: displayPicker)
}
}
.onAppear {
displayPicker = true
}
}
}
Read more about the modifier here:
https://developer.apple.com/documentation/SwiftUI/AnyView/onAppear(perform:)
Think I figured it out -
struct CreateView: View {
#State private var image: Image?
#State private var showingImagePicker = false
#State private var uploadedPhotos = [UIImage]()
var body: some View {
ScrollView {
HStack {
image?.resizable().scaledToFit()
}
.onAppear {
showingImagePicker = true
}
.sheet(isPresented: $showingImagePicker) {
PhotoPicker(photoList: $uploadedPhotos)
}
.onChange(of: uploadedPhotos) { _ in
guard let uiImage = uploadedPhotos.first else {return}
image = Image(uiImage: uiImage)
}
}
}
}
Here PhotoPicker() is the customview that I have
So I am writing a todo list app in SwiftUI in order to get the hang of it, but I am facing a problem.
In my first view (list of items) I have a toolbar with an "add" button which uses a NavigationLink to navigate to the detail view. In the detail view I also have a toolbar button acting as a save button which dismisses this view and also adds the item to a list of items kept in the view model used by both views.
The problem is that if I save the item when tapping the save button it will first navigate back to the first view and then auto navigate to the second view again. If I instead use the built in back button this issue doesn't happen, but obviously I would like to save the item and only when pressing save. This also only happens if I add the item to the item list in the view model before dismissing the view, if I only dismiss the view without saving the item when pressing done then this bug doesn't happen.
Is this not a standard way of saving and closing a view with SwiftUI, or is there some sort of other pattern that is better? In any case I need to resolve this issue.
First view:
struct TodoListView: View {
#EnvironmentObject var viewModel: TodoListViewModel
var body: some View {
NavigationView {
List {
ForEach(viewModel.listOfTodos) { todoItem in
ItemCellView(todoItem: todoItem)
}
}
.navigationTitle("Things to do")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(
destination: AddEditTodoView(todoItem: TodoListInfo.TodoItem())
) {
Text("Add item") // The navigation bug happens when using this button
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ItemCellView: View {
var todoItem: TodoListInfo.TodoItem
var body: some View {
HStack {
NavigationLink(destination: AddEditTodoView(todoItem: todoItem)) {
Text(todoItem.title) // The navigation bug doesn't happen when editing an existing item
}
}
.padding()
}
}
Second view:
struct AddEditTodoView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var viewModel: TodoListViewModel
#State var todoItem: TodoListInfo.TodoItem
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("Title", text: $todoItem.title)
}
}
.navigationTitle(Text("Edit task"))
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
viewModel.upsert(item: todoItem) // No bug if I comment out this line
presentationMode.wrappedValue.dismiss()
}
.disabled(todoItem.title == "")
}
}
}
}
View model:
class TodoListViewModel: ObservableObject {
#Published private var todoListInfo: TodoListInfo
private var autoSaveCancellable: AnyCancellable? // even without the autoSaveCancellable part, the bug happens
init(testData: Bool = false) {
todoListInfo = TodoListInfo(testData: testData)
autoSaveCancellable = $todoListInfo.sink {
TodoListInfo.persistTodoList($0)
}
}
var listOfTodos: [TodoListInfo.TodoItem] {
todoListInfo.todos
}
func upsert(item: TodoListInfo.TodoItem) {
if let itemIndex = todoListInfo.todos.firstIndex(where: { $0.id == item.id }) {
todoListInfo.todos[itemIndex] = item
} else {
todoListInfo.todos.append(item) // This gets called when adding an item
}
}
}
The solution for this is to make the navigation be based on a binding state.
NavigationLink(
destination: ExchangeItemSelectedView(exchange: observer),
tag: exchange.id,
selection: $exchangeSelection
) {
Text("Tap Me")
}
then rather than using #State to store exchangeSelection use #SceneStorage this will let you access the binding from anywhere within your app, in the code that creates the new item it should then dispatch async to update the selection value to the new item ID.
I have a view for a list item that displays some news cards within a navigationLink.
I am supposed to add a like/unlike button within each news card of navigationLink, without being took to NavigationLink.destination page.
It seems like a small button inside a big button.
When you click that small one, execute the small one without executing the bigger one.
(note: the click area is covered by the two buttons, smaller one has the priority)
(In javascript, it seems like something called .stopPropaganda)
This is my code:
var body: some View {
NavigationView {
List {
ForEach(self.newsData.newsList, id:\.self) { articleID in
NavigationLink(destination: NewsDetail(articleID: articleID)) {
HStack {
Text(newsTitle)
Button(action: {
self.news.isBookmarked.toggle()
}) {
if self.news.isBookmarked {
Image(systemName: "bookmark.fill")
} else {
Image(systemName: "bookmark")
}
}
}
}
}
}
}
}
Currently, the button action (like/dislike) will not be performed as whenever the button is pressed, the navigationLink takes you to the destination view.
I have tried this almost same question but it cannot solve this problem.
Is there a way that makes this possible?
Thanks.
as of XCode 12.3, the magic is to add .buttonStyle(PlainButtonStyle()) or BorderlessButtonStyle to the button, when said button is on the same row as a NavigationLink within a List.
Without this particular incantation, the entire list row gets activated when the button is pressed and vice versa (button gets activated when NavigationLink is pressed).
This code does exactly what you want.
struct Artcle {
var text: String
var isBookmarked: Bool = false
}
struct ArticleDetail: View {
var article: Artcle
var body: some View {
Text(article.text)
}
}
struct ArticleCell: View {
var article: Artcle
var toggle: () -> ()
#State var showDetails = false
var body: some View {
HStack {
Text(article.text)
Spacer()
Button(action: {
self.toggle()
}) {
Image(systemName: article.isBookmarked ? "bookmark.fill" : "bookmark").padding()
}
.buttonStyle(BorderlessButtonStyle())
}
.overlay(
NavigationLink(destination: ArticleDetail(article: article), isActive: $showDetails) { EmptyView() }
)
.onTapGesture {
self.showDetails = true
}
}
}
struct ContentView: View {
#State var articles: [Artcle]
init() {
_articles = State(initialValue: (0...10).map { Artcle(text: "Article \($0 + 1)") })
}
func toggleArticle(at index: Int) {
articles[index].isBookmarked.toggle()
}
var body: some View {
NavigationView {
List {
ForEach(Array(self.articles.enumerated()), id:\.offset) { offset, article in
ArticleCell(article: article) {
self.toggleArticle(at: offset)
}
}
}
}
}
}
I have a list in a Navigation View, with a trailing navigation button to add a list item. The button opens a modal sheet. When I dismiss the sheet (by pulling it down), the sheet pops right back up again automatically and I can't get back to the first screen. Here's my code.
struct ListView: View {
#ObservedObject var listVM: ListViewModel
#State var showNewItemView: Bool = false
init() {
self.listVM = ListViewModel()
}
var body: some View {
NavigationView {
List {
ForEach(listVM.items, id: \.dateCreated) { item in
HStack {
Text(item.name)
Spacer()
Image(systemName: "arrow.right")
}
}
}
.navigationBarTitle("List Name")
.navigationBarItems(trailing: AddNewItemBtn(isOn: $showNewItemView))
}
}
}
struct AddNewItemBtn: View {
#Binding var isOn: Bool
var body: some View {
Button(
action: { self.isOn.toggle() },
label: { Image(systemName: "plus.app") })
.sheet(
isPresented: self.$isOn,
content: { NewItemView() })
}
}
I am getting this error:
Warning: Attempt to present <_TtGC7SwiftUIP13$7fff2c603b7c22SheetHostingControllerVS_7AnyView_: 0x7fc5e0c1f8f0> on which is already presenting (null)
I've tried toggling the bool within "onDismiss" on the button itself, but that doesn't work either. Any ideas?
Turns out putting the button in the navigationBarItems(trailing:) modifier is the problem. I just put the button in the list itself instead of in the nav bar and it works perfectly fine. Must be some kind of bug.