SwiftUI: searchable weird push animation? - ios

I have the following code:
enum ContentViewRouter {
case details
}
struct ContentView: View {
var body: some View {
NavigationStack {
ZStack {
NavigationLink(value: ContentViewRouter.details) {
Text("Push")
}
}
.navigationDestination(for: ContentViewRouter.self) { destiantion in
switch destiantion {
case .details:
DetailsView()
}
}
.navigationTitle("TEST")
}
}
}
struct DetailsView: View {
#State var query = ""
var body: some View {
Form {
Section("TEST") {
Text("DETAILS")
}
}
.searchable(text: $query)
.navigationTitle("DETAILS")
}
}
When pushing the details view it creates this animation:
I want to get rid of this weird looking animation "glitch":
How can I get the push animation to come in smooth where the search bar doesn't appear for a split second and then go away? Seems like it's a bug or I am doing it wrong?

The problem of that I think because you have .searchable which will create search bar and currently conflict with your .navigationTitle which both on the top. Maybe the search bar don't know where to put it because of the .navigationTitle
Update: After doing some research, I see that maybe the behavior maybe wrong in search bar .automatic
There are three ways to workaround
If you want to keep your search bar then make NavigationView to split define navigationTitle and search bar
struct DetailsView: View {
#State var query = ""
var body: some View {
NavigationView {
Form {
Section("TEST") {
Text("DETAILS")
}
}
.searchable(text: $query)
}
.navigationTitle("DETAILS")
}
}
Keep the search bar always appear
struct DetailsView: View {
#State var query = ""
var body: some View {
Form {
Section("TEST") {
Text("DETAILS")
}
}
.searchable(text: $query, placement: .navigationBarDrawer(displayMode: .always))
.navigationTitle("DETAILS")
}
}
If you want to completely remove search bar
struct DetailsView: View {
#State var query = ""
var body: some View {
Form {
Section("TEST") {
Text("DETAILS")
}
}
//.searchable(text: $query) // remove this
.navigationTitle("DETAILS")
}
}

Related

How to add functionality to SearchResultsButton in search bar with SwiftUI?

I am trying to have a sheet pop up when the SearchResultsButton on my search bar is tapped. The below picture shows the button I want to add functionality for:
This is the code I have currently:
struct ContentView: View {
#State private var isSearching = ""
var body: some View {
NavigationView {
List {
Text("Hello")
Text("World")
}
.navigationTitle("Hello")
.searchable(text: $isSearching)
.onAppear {
UISearchBar.appearance().showsSearchResultsButton = true
}
}
}
}
How would I go about accomplishing my goal? Any help would be greatly appreciated.

SwiftUI isSearching dismiss behaviour on iOS 15

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)
}
}
}

Unwind NavigationView to root when switching tabs in SwiftUI

I have an app with a few tabs, and on one of those there is a NavigationLink which nests a couple of times.
I want to be able to switch tabs, and when going back to the other tab to have unwound all links to the root view.
I have seen these: https://stackoverflow.com/a/67014642/1086990 and https://azamsharp.medium.com/unwinding-segues-in-swiftui-abdf241be269 but they seem to be focusing on unwinding when active on the view, not switching from it.
struct MyTabView: View {
var body: some View {
TabView {
TabOne().tabItem { Image(systemName: "1.square") }
TabTwo().tabItem { Image(systemName: "2.square") }
}
}
}
struct TabOne: View {
var body: some View {
Text("1")
}
}
struct TabTwo: View {
var body: some View {
NavigationView {
NavigationLink("Go to sub view") {
TabTwoSub()
}
}
}
}
struct TabTwoSub: View {
var body: some View {
Text("Tapping \(Image(systemName: "1.square")) doesnt unwind this view back to the root of the NavigationView")
.multilineTextAlignment(.center)
}
}
Maybe I've missed something fairly basic but nothing seems to come up from searches on unwinding views when switching tabs.
I tried using the NavigationLink(isActive: , destination: , label: ) from the other SO answer but couldn't get it working in the root MyTabView.
I thought about using UserDefaults to set a isActive bool state and if not try and unwind the navigation, but that didn't seem very swifty to do.
What is happening
You'll need to keep track of the tab selection in the parent view and then pass that into the child views so that they can watch for changes. Upon seeing a change in the selection, the child view can then reset a #State variable that change the isActive property of the NavigationLink.
class NavigationManager : ObservableObject {
#Published var activeTab = 0
}
struct MyTabView: View {
#StateObject private var navigationManager = NavigationManager()
var body: some View {
TabView(selection: $navigationManager.activeTab) {
TabOne().tabItem { Image(systemName: "1.square") }.tag(0)
TabTwo().tabItem { Image(systemName: "2.square") }.tag(1)
}.environmentObject(navigationManager)
}
}
struct TabOne: View {
var body: some View {
Text("1")
}
}
struct TabTwo: View {
#EnvironmentObject private var navigationManager : NavigationManager
#State private var linkActive = false
var body: some View {
NavigationView {
NavigationLink("Go to sub view", isActive: $linkActive) {
TabTwoSub()
}
}.onChange(of: navigationManager.activeTab) { newValue in
linkActive = false
}
}
}
struct TabTwoSub: View {
var body: some View {
Text("Tapping \(Image(systemName: "1.square")) doesnt unwind this view back to the root of the NavigationView")
.multilineTextAlignment(.center)
}
}
Note: this will result in a "Unbalanced calls to begin/end appearance transitions" message in the console -- in my experience, this is not an error and not something we have to worry about

SwiftUI - NavigationLink cell in a Form stays highlighted after detail pop

In iOS 14, it appears that NavigationLinks do not become deselected after returning in a Form context.
This is also true for Form Pickers and anything else that causes the presentation of another View from a list (giving a highlight context to the presenting cell).
I didn't notice this behaviour in iOS 13.
Is there a way to 'deselect' the highlighted row once the other view is dismissed?
Example code:
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("Detail")) {
Text("Link")
}
}
}
}
(Different) Example visual:
In my case this behaviour appeared when using any Viewcontent (e.g. Text(), Image(), ...) between my NavigationView and List/Form.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
Putting the Text() beneath the List does not make any problems:
var body: some View {
NavigationView {
VStack {
List {
NavigationLink(destination: Text("Does work correct")) {
Text("Does work correct")
}
}
Text("This text doesn't make problems.")
}
}
}
This is definitely a XCode 12 bug. As more people report this, as earlier it gets resolved.
I have also run into this issue and believed I found the root cause in my case.
In my case I had.a structure like the following:
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
}
}
struct Page2View: View {
var body: some View {
List {
NavigationLink("Page 3", destination: Text("Page 3"))
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 2")
}
}
This issue would occur on the NavigationLink to Page 3. In the console output this error was showing when that link was used:
2021-02-13 16:41:00.599844+0000 App[59157:254215] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.
I discovered that I needed to apply .navigationViewStyle(StackNavigationViewStyle()) to the NavigationView and this solved the problem.
I.e.
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Been fighting this issue half day today and came to this post that helped me to understand that issue appears if Text, Button or something else placed between NavigationView and in my case List. And I found solution that worked for me. Just add .zIndex() for the item. .zIndex() must be higher than for List Tried with Xcode 12.5.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
.zIndex(1.0)
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
I did a bit more tinkering, it turns out this was caused due by having the UIHostingController being nested in a UINavigationController and using that navigation controller. Changing the navigation stack to use a SwiftUI NavigationView instead resolved this issue.
Similar to what #pawello2222 says in the question comments, I think the underlying cause is something to do with SwiftUI not understanding the proper navigation hierarchy when the external UINavigationController is used.
This is just one instance where this is fixed though, I'm still experiencing the issue in various other contexts depending on how my view is structured.
I've submitted an issue report FB8705430 to Apple, so hopefully this is fixed sometime soon.
Before (broken):
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
let nav = UINavigationController(rootViewController: host)
present(nav, animated: true, completion: nil)
After (working):
struct ContentView: View {
var body: some View {
NavigationView {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
present(host, animated: true, completion: nil)
This is definitely a bug in List, for now, my work-around is refreshing the List by changing the id, like this:
struct YourView: View {
#State private var selectedItem: String?
#State private var listViewId = UUID()
var body: some View {
List(items, id: \.id) {
NavigationLink(destination: Text($0.id),
tag: $0.id,
selection: $selectedItem) {
Text("Row \($0.id)")
}
}
.id(listViewId)
.onAppear {
if selectedItem != nil {
selectedItem = nil
listViewId = UUID()
}
}
}
}
I made a modifier based on this that you can use:
struct RefreshOnAppearModifier<Tag: Hashable>: ViewModifier {
#State private var viewId = UUID()
#Binding var selection: Tag?
func body(content: Content) -> some View {
content
.id(viewId)
.onAppear {
if selection != nil {
viewId = UUID()
selection = nil
}
}
}
}
extension View {
func refreshOnAppear<Tag: Hashable>(selection: Binding<Tag?>? = nil) -> some View {
modifier(RefreshOnAppearModifier(selection: selection ?? .constant(nil)))
}
}
use it like this:
List { ... }
.refreshOnAppear(selection: $selectedItem)
I managed to solve it by adding ids to the different components of the list, using binding and resetting the binding on .onDisappear
struct ContentView: View {
#State var selection: String? = nil
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
.padding()
List {
Section {
NavigationLink( destination: Text("Subscreen1"), tag: "link1", selection: $selection ) {
Text("Subscreen1")
}.onDisappear {
self.selection = nil
}
NavigationLink( destination: Text("Subscreen2"), tag: "link2", selection: $selection ) {
Text("Subscreen2")
}.onDisappear {
self.selection = nil
}
}.id("idSection1")
}
.id("idList")
}
}
}
}
I've also run into this issue and it seemed related to sheets as mentioned here.
My solution was to swizzle UITableView catch selections, and deselect the cell. The code for doing so is here. Hopefully this will be fixed in future iOS.
Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView fixed it for me.
Suggested in this thread: https://developer.apple.com/forums/thread/660468
This is my solution to this issue.
// This in a stack in front of list, disables large navigation title from collapsing by disallowing list from scrolling on top of navigation title
public struct PreventCollapseView: View {
#State public var viewColor: Color?
public init(color: Color? = nil) {
self.viewColor = color
}
public var body: some View {
Rectangle()
.fill(viewColor ?? Color(UIColor(white: 0.0, alpha: 0.0005)))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 1)
}
}
// handy modifier..
extension List {
public func uncollapsing(_ viewUpdater: Bool) -> some View {
VStack(spacing: 0.0) {
Group {
PreventCollapseView()
self
}.id(viewUpdater)
}
}
}
struct TestView: View {
#State var updater: Bool = false
var body: some View {
List {
Text("Item one")
Text("Item two")
Text("Manually refresh")
.onTapGesture { DispatchQueue.main.async { updater.toggle() } }
.onAppear { print("List was refreshed") }
}
.uncollapsing(updater)
.clipped()
.onAppear { DispatchQueue.main.async { updater.toggle() }} // Manually refreshes list always when re-appearing/appearing
}
}
Add a NavigationView, configure for largeTitle, and embed TestView and it's all set. Toggle updater to refresh.
Having the same Problem. The weird thing is, that the exact same code worked in iOS13.
I'm having this issue with a simple list:
struct TestList: View {
let someArray = ["one", "two", "three", "four", "five"]
var body: some View {
List(someArray, id: \.self) { item in
NavigationLink(
destination: Text(item)) {
Text(item)
}.buttonStyle(PlainButtonStyle())
}.navigationBarTitle("testlist")
}
}
This is embedded in:
struct ListControllerView: View {
#State private var listPicker = 0
var body: some View {
NavigationView{
Group{
VStack{
Picker(selection: $listPicker, label: Text("Detailoverview")) {
Text("foo").tag(0)
Text("bar").tag(1)
Text("TestList").tag(2)
}
This is inside a Tabbar.
This is the workaround I've been using until this List issue gets fixed. Using the Introspect library, I save the List's UITableView.reloadData method and call it when it appears again.
import SwiftUI
import Introspect
struct MyView: View {
#State var reload: (() -> Void)? = nil
var body: some View {
NavigationView {
List {
NavigationLink("Next", destination: Text("Hello"))
}.introspectTableView { tv in
self.reload = tv.reloadData
}.onAppear {
self.reload?()
}
}
}
}

SwiftUI - Nested NavigationLink

I have a layout that looks like this:
Layout Drawing
There is a main view which is the Feed that would be my NavigationView and then I have views inside: PostList -> Post -> PostFooter and in the PostFooter A Button that would be my NavigationLink
struct Feed: View {
var body: some View {
NavigationView {
PostList()
}
}
}
struct PostList: View {
var body: some View {
List {
ForEach(....) {
Post()
}
}
}
}
struct Post: View {
var body: some View {
PostHeader()
Image()
PostFooter()
}
}
struct PostFooter: View {
var body: some View {
NavigationLink(destination: Comment()) {
Text("comments")
}
}
}
But When when I tap on the comments, it goes to the Comment View then go back to Feed() then back to Comment() then back to Feed() and have weird behaviour.
Is there a better way to handle such a situation ?
Update
The Navigation is now working but the all Post component is Tapeable instead of just the Text in the PostFooter.
Is there any way to disable tap gesture on the cell and add multiple NavigationLink in a cell that go to different pages ?
How about programmatically active the NavigationLink, for example:
struct PostFooter: View {
#State var commentActive: Bool = false
var body: some View {
VStack{
Button("Comments") {
commentActive = true
}
NavigationLink(destination: Comment(), isActive: $commentActive) {
EmptyView()
}
}
}
}
Another benefit of above is, your NavigationLink destination View can accept #ObservedObject or #Binding for comments editing.

Resources