I'm using Parchment to add menu items at the top. The hierarchy of the main view is the following:
NavigationView
-> TabView
--> Parchment PagingView
---> NavigationLink(ChildView)
All works well going to the child view and then back again repeatedly. The issue happens when I go to ChildView, then go to the background/Home Screen then re-open. If I click back and then go to the child again the back button and the whole navigation bar disappears.
Here's code to replicate:
import SwiftUI
import Parchment
#main
struct ParchmentBugApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
PagingView(items: [
PagingIndexItem(index: 0, title: "View 0"),
]) { item in
VStack {
NavigationLink(destination: ChildView()) {
Text("Go to child view")
}
}
.navigationBarHidden(true)
}
}
}
}
}
struct ChildView: View {
var body: some View {
VStack {
Text("Child View")
}
.navigationBarHidden(false)
.navigationBarTitle("Child View")
}
}
To replicate:
Launch and go to the child view
Click the home button to send the app to the background
Open the app again
Click on back
Navigate to the child view. The nav bar/back button are not there anymore.
What I noticed:
Removing the TabView makes the problem go away.
Removing PagingView also makes the problem go.
I tried to use a custom PagingController and played with various settings without success. Here's the custom PagingView if someone would like to tinker with the settings as well:
struct CustomPagingView<Item: PagingItem, Page: View>: View {
private let items: [Item]
private let options: PagingOptions
private let content: (Item) -> Page
/// Initialize a new `PageView`.
///
/// - Parameters:
/// - options: The configuration parameters we want to customize.
/// - items: The array of `PagingItem`s to display in the menu.
/// - content: A callback that returns the `View` for each item.
public init(options: PagingOptions = PagingOptions(),
items: [Item],
content: #escaping (Item) -> Page) {
self.options = options
self.items = items
self.content = content
}
public var body: some View {
PagingController(items: items, options: options,
content: content)
}
struct PagingController: UIViewControllerRepresentable {
let items: [Item]
let options: PagingOptions
let content: (Item) -> Page
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PagingController>) -> PagingViewController {
let pagingViewController = PagingViewController(options: options)
return pagingViewController
}
func updateUIViewController(_ pagingViewController: PagingViewController, context: UIViewControllerRepresentableContext<PagingController>) {
context.coordinator.parent = self
if pagingViewController.dataSource == nil {
pagingViewController.dataSource = context.coordinator
} else {
pagingViewController.reloadData()
}
}
}
class Coordinator: PagingViewControllerDataSource {
var parent: PagingController
init(_ pagingController: PagingController) {
self.parent = pagingController
}
func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int {
return parent.items.count
}
func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController {
let view = parent.content(parent.items[index])
return UIHostingController(rootView: view)
}
func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem {
return parent.items[index]
}
}
}
Tested on iOS Simulator 14.4 & 14.5, and device 14.5 beta 2.
Any tips or ideas are very much appreciated.
Okay I found the issue while debugging something else that was related to Parchment as well.
The issue is updateUIViewController() gets called each time the encompassing SwiftUI state changes (and when coming back to the foreground), and the PageController wrapper provided by the library will call reloadData() since the data source data has already been set. So to resolve this just remove/comment out the reloadData() call since the PageController will be re-built if the relevant state changes. The same issue was the cause for the bug I was debugging.
func updateUIViewController(_ pagingViewController: PagingViewController, context: UIViewControllerRepresentableContext<PagingController>) {
context.coordinator.parent = self
if pagingViewController.dataSource == nil {
pagingViewController.dataSource = context.coordinator
}
//else {
// pagingViewController.reloadData()
//}
}
Related
I'm trying to migrate a coordinator pattern using UINavigationController into the new NavigationStack.
The navigation flow is quite complex, but I've made a simple project to simplify it to this:
NavigationStack
-> MainScreen
-> TabView
-> FirstTab
-> NavigationStack
-> FirstTabFirst
-> FirstTabSecond
-> SecondTab
-> SecondTabScreen
-> Second Top Screen
Althoug the first Screens in each NavigatorStack are actually the root view of the navigator, it shouldn't be a problem as I can't even get it to navigate to something.
All the navigators have their state defined by a #StateObject to allow for both programatic imperative navigation and NavigationLink, similar to what a Coordinator pattern provides.
Upon launching the app, it immediatelly throws in the #main line, with no further information about the call stack:
Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: SwiftUI.AnyNavigationPath.Error.comparisonTypeMismatch
This is the code for the whole app:
import SwiftUI
protocol NavigationRoute: Hashable {
associatedtype V: View
#ViewBuilder
func view() -> V
}
enum Top: NavigationRoute {
case first, second
#ViewBuilder
func view() -> some View {
switch self {
case .first: TopFirst()
case .second: TopSecond()
}
}
}
enum Tab: NavigationRoute {
case first, second
#ViewBuilder
func view() -> some View {
switch self {
case .first: TabFirst()
case .second: TabSecond()
}
}
var label: String {
switch self {
case .first: return "First"
case .second: return "Second"
}
}
var systemImage: String {
switch self {
case .first: return "house"
case .second: return "person.fill"
}
}
#ViewBuilder
func tab() -> some View {
view()
.tabItem { Label(label, systemImage: systemImage) }
.tag(self)
}
}
enum FirstTab: NavigationRoute {
case first, second
#ViewBuilder
func view() -> some View {
switch self {
case .first: FirstTabFirst()
case .second: FirstTabSecond()
}
}
}
class StackNavigator<Route: NavigationRoute>: ObservableObject {
#Published var routes: [Route] = []
}
class TabNavigator<Route: NavigationRoute>: ObservableObject {
#Published public var tab: Route
public init(initial: Route) {
tab = initial
}
public func navigate(_ route: Route) {
tab = route
}
}
#main
struct NavigationStackTestApp: App {
#StateObject var navigator = StackNavigator<Top>()
var body: some Scene {
WindowGroup {
NavigationStack(path: $navigator.routes) {
TopFirst().navigationDestination(for: Top.self) {
$0.view()
}
}
}
}
}
struct TopFirst: View {
#StateObject var navigator = TabNavigator<Tab>(initial: .first)
var body: some View {
TabView(selection: $navigator.tab) {
Tab.first.tab()
Tab.second.tab()
}
}
}
struct TopSecond: View {
var body: some View {
Text("Top Second")
}
}
struct TabFirst: View {
#StateObject var navigator = StackNavigator<FirstTab>()
var body: some View {
NavigationStack(path: $navigator.routes) {
FirstTabFirst().navigationDestination(for: FirstTab.self) {
$0.view()
}
}
}
}
struct TabSecond: View {
var body: some View {
Text("Tab Second")
}
}
struct FirstTabFirst: View {
var body: some View {
Text("First Tab First")
}
}
struct FirstTabSecond: View {
var body: some View {
Text("First Tab Second")
}
}
To avoid it from crashing I have to replace the NavigationStack of the first tab with an EmptyView(), that is changing:
case .first: TabFirst()
to
case .first: EmptyView()
But of course after doing that the first tab is missing and the app doesn't do what it's supposed to.
Has anyone encountered something like this or has an idea of how to get this working? SwiftUI is closed source and it's documentation is very limited so it's really hard to know what's really going on under the hood.
EDIT:
Using NavigationPath as state for StackNavigator doesn't crash the app, but any navigation in the first tab's NavigationStack pushes a new view in the top navigator and hides the bottom tab bar.
I'm assuming you just can't have two navigation stacks in the same hierarchy and expect it to work as you would assume.
Using two NavigationStacks in the same hierarchy doesn't work very well as their internal states collide, I would not recommend it and it seems to cause the error. The error is thrown when the lower navigator is declared because it's declared type is also handled by the top navigator, which doesn't have a handler declared for the lower navigator's tap in its navigationDestination.
It is better to use conditional rendering and manual animation/transitions in the upper level and then from there render only one NavigationStack per hierarchy. There is no issue using NavigationStack inside TabView as long as that TabView is not itself inside a NavigationStack.
NavigationPath can be used to support multiple unrelated types in the state stack.
Parent view has list with searchable functionality, trying to search for a recipe and then navigate to the child view that has toolbar with button, the child view get frozen when navigating from the searchbar, if I navigate from the normal list without searching in the parent view, everything works as expected, but when I try to search and then navigate, the child view get frozen, I tried:
removing the toolbar and navigated from the searchbar, worked as expected.
created empty toolbar in the child view without buttons, worked as expected.
I have an old app that started having this issue, I created a new project to check if this issue happens only in my code, but no, this issue also happens in a new project with iOS 15.2 and Xcode 13.2.1.
I noticed that I started having this issue when I tried to release a new version with iOS 15.2, I suppose this is an iOS and SwiftUI issue but not sure how to work around on it.
Parent View:
import SwiftUI
struct RecipesView: View {
#State private var recipes: [String] = ["Brownies", "Cake", "Bread", "Pan"]
#State private var filteredRecipes: [String] = []
#State private var searchText: String = ""
var body: some View {
NavigationView {
List {
ForEach(self.filteredRecipes, id: \.self) { recipe in
NavigationLink {
RecipeView(recipe: recipe)
} label: {
Text(recipe)
}
}
}
.searchable(text: self.$searchText)
.onChange(of: self.searchText, perform: { newValue in self.filter() })
.onAppear { self.filter() }
}
}
private func filter() {
if self.searchText.isEmpty {
self.filteredRecipes = self.recipes
} else {
self.filteredRecipes = self.recipes.filter {
return ($0.lowercased().contains(self.searchText.lowercased()))
}
}
}
}
Child View:
import SwiftUI
struct RecipeView: View {
var recipe: String
var body: some View {
List {
Section {
Text(self.recipe)
}
Section {
Text("Another Text")
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
} label: {
Text("Edit")
}
}
}
}
}
I have this:
VStack {
List {
LazyVGrid(columns: gridItemLayout) {
ForEach(viewModel.objects, id: \.fileGroupUUID) { item in
AlbumItemsScreenCell(object: item, viewModel: viewModel, config: Self.config)
.onTapGesture {
switch viewModel.changeMode {
case .moving, .sharing, .moveAll:
viewModel.toggleItemToChange(item: item)
case .none:
object = item
viewModel.showCellDetails = true
}
}
.onLongPressGesture {
viewModel.restartDownload(fileGroupUUID: item.fileGroupUUID)
}
} // end ForEach
} // end LazyVGrid
}
.listStyle(PlainListStyle())
.refreshable {
viewModel.refresh()
}
.padding(5)
// Mostly this is to animate updates from the menu. E.g., the sorting order.
.animation(.easeInOut)
// Had a problem with return animation for a while: https://stackoverflow.com/questions/65101561
// The solution was to take the NavigationLink out of the scrollview/LazyVGrid above.
if let object = object {
// The `NavigationLink` works here because the `MenuNavBar` contains a `NavigationView`.
NavigationLink(
destination:
ObjectDetailsView(object: object, model: ObjectDetailsModel(object: object)),
isActive:
$viewModel.showCellDetails) {
EmptyView()
}
.frame(width: 0, height: 0)
.disabled(true)
} // end if
} // end VStack
AlbumItemsScreenCell:
struct AlbumItemsScreenCell: View {
#StateObject var object:ServerObjectModel
#StateObject var viewModel:AlbumItemsViewModel
let config: IconConfig
#Environment(\.colorScheme) var colorScheme
var body: some View {
AnyIcon(model: AnyIconModel(object: object), config: config,
emptyUpperRightView: viewModel.changeMode == .none,
upperRightView: {
UpperRightChangeIcon(object: object, viewModel: viewModel)
})
}
}
When a user taps one of the cells, this causes navigation to a details screen. Sometimes when the user returns from that navigation, the cell in the upper left disappears:
https://www.dropbox.com/s/mi6j2ie7h8dcdm0/disappearingCell.mp4?dl=0
My current hypothesis about the issue is that when user actions in that details screen take actions which change viewModel.objects, this causes the disappearing cell problem. I'll be testing this hypothesis shortly.
----- Update, 11/1/21 ------
Well, that hypothesis was wrong. I now understand the structure of the problem more clearly. Still don't have a fix though.
Tapping on one of the AlbumItemsScreenCells navigates to a details screen (I've added to the code above to show that). In the details screen user actions can cause a comment count to get reset, which sends a Notification.
A model in the AlbumItemsScreenCell listens for these notification (for the specific cell) and resets a badge on the cell.
Here is that model:
class AnyIconModel: ObservableObject, CommentCountsObserverDelegate, MediaItemBadgeObserverDelegate, NewItemBadgeObserverDelegate {
#Published var mediaItemBadge: MediaItemBadge?
#Published var unreadCountBadgeText: String?
#Published var newItem: Bool = false
var mediaItemCommentCount:CommentCountsObserver!
let object: ServerObjectModel
var mediaItemBadgeObserver: MediaItemBadgeObserver!
var newItemObserver: NewItemBadgeObserver!
init(object: ServerObjectModel) {
self.object = object
// This is causing https://stackoverflow.com/questions/69783232/cell-in-list-with-lazyvgrid-disappears-sometimes
mediaItemCommentCount = CommentCountsObserver(object: object, delegate: self)
mediaItemBadgeObserver = MediaItemBadgeObserver(object: object, delegate: self)
newItemObserver = NewItemBadgeObserver(object: object, delegate: self)
}
}
The unreadCountBadgeText gets changed (on the main thread) by the observer when the Notification is received.
So, in summary, the badge on the cell gets changed while the screen with the cells is not displayed-- the details screen is displayed.
I had been using the following conditional modifier:
extension View {
public func enabled(_ enabled: Bool) -> some View {
return self.disabled(!enabled)
}
// https://forums.swift.org/t/conditionally-apply-modifier-in-swiftui/32815/16
#ViewBuilder func `if`<T>(_ condition: Bool, transform: (Self) -> T) -> some View where T : View {
if condition {
transform(self)
} else {
self
}
}
}
to display the badge on the AlbumItemsScreenCell.
The original badge looked like this:
extension View {
func upperLeftBadge(_ badgeText: String) -> some View {
return self.modifier(UpperLeftBadge(badgeText))
}
}
struct UpperLeftBadge: ViewModifier {
let badgeText: String
init(_ badgeText: String) {
self.badgeText = badgeText
}
func body(content: Content) -> some View {
content
.overlay(
ZStack {
Badge(badgeText)
}
.padding([.top, .leading], 5),
alignment: .topLeading
)
}
}
i.e., the usage looked like this in the cell:
.if(condition) {
$0.upperLeftBadge(badgeText)
}
when I changed the modifier to use it with out this .if modifier, and used it directly, the issue went away:
extension View {
func upperLeftBadge(_ badgeText: String?) -> some View {
return self.modifier(UpperLeftBadge(badgeText: badgeText))
}
}
struct UpperLeftBadge: ViewModifier {
let badgeText: String?
func body(content: Content) -> some View {
content
.overlay(
ZStack {
if let badgeText = badgeText {
Badge(badgeText)
}
}
.padding([.top, .leading], 5),
alignment: .topLeading
)
}
}
It may sounds weird, but in my tvOS app i disable the animations
and cells(Views) stops to disappear
So try to remove .animation(.easeInOut)
I'm using a custom NavigationButton from this answer to be able to load data for a specific view when navigating to a new view from a list. It works great, but the problem is when I attempt to use it inside a ForEach loop I end up with the multiple navigation links with multiple isActive #State variables that are all being told to push a new view, which causes SwiftUI to navigate to the new view then automatically back to parent view (bug). How can I preserve the functionality of being able to fire code to run while navigating to a new view without triggering the bug.
ForEach(workouts) { workout in
NavigationButton(
action: {
//load data for Destination
},
destination: {
WorkoutDetailView().environmentObject(dataStore)
},
workoutRow: { WorkoutRow(trackerWorkout: workout) }
)
}
struct NavigationButton<Destination: View, WorkoutRow: View>: View {
var action: () -> Void = { }
var destination: () -> Destination
var workoutRow: () -> WorkoutRow
#State private var isActive: Bool = false
var body: some View {
Button(action: {
self.action()
self.isActive.toggle()
}) {
workoutRow()
.background(
ScrollView { // Fixes a bug where the navigation bar may become hidden on the pushed view
NavigationLink(destination: LazyDestination { self.destination() },
isActive: self.$isActive) { EmptyView() }
}
)
}
}
}
// This view lets us avoid instantiating our Destination before it has been pushed.
struct LazyDestination<Destination: View>: View {
var destination: () -> Destination
var body: some View {
self.destination()
}
}
I am a newbie in IOS Programming and also SwiftUI. Coming from Java/Kotlin Android. I want to learn SwiftUI.
I have a WKWebView. I want to change SwiftUI page according to url changes in WKWebView. I have done a lot of work as you can see below. But I am struggled in navigation.
struct ContentView: View, Listener {
func onFetched() {
NavigationLink(destination: MainView()) {/* HERE this is not working.
App never goes to MainView.swift page.*/
Text("Show Detail View")
}
}
var body: some View {
NavigationView {
VStack {
WebView(authListener: self)
}.navigationBarTitle(Text("PEAKUP Velocity"))
}
}
}
struct WebView: UIViewRepresentable {
var listener: Listener
#ObservedObject var observe = observable()
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
observe.observation = uiView.observe(\WKWebView.url, options: .new) { view, change in
if let url = view.url {
self.observe.loggedIn = true // We loaded the page
self.listener.onFetched()
uiView.isHidden = true
}
}
uiView.load("https://google.com")
}
}
protocol Listener {
func onFetched()
}
Update: I tried this in onFetched():
NavigationLink(destination: MainView()) { Text("") }''''
And I tried this piece of code:
NavigationView {
NavigationLink(destination: SecondView()){
Text("Navigation Link")
}
}
Update 2: I tried this code also in onFetched:
self.presentation(Model(MainView(), onDismiss: nil))
Update 3: I tried this:
self.sheet(isPresented: $sayHello) {
MainView()
}
** Disclaimer I haven't used WKWebviews in the context of SwiftUI, but I assume same would be true. **
I would recommend looking into WKNavigationDelegate. Unless your class is of type WKWebView you need to ensure to assign a delegate to handle any navigation events.
Perhaps you'll find the below article helpful: https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebview