NavigationStack missing root controller from path - swiftui

i am building a historical navigation (backwards, forwards) for a macOS app
It works mostly fine, but my implementation relies on the path, which however doesn't contain the root (when the sidebar is selected, and [] is assigned to path)
how to get the root, or to save the root from navigationDestination
import SwiftUI
enum NavigationDestination: Hashable {
case test1
case test2
case test3(text:String)
}
struct SidebarView: View {
var body: some View {
VStack {
NavigationLink(value: NavigationDestination.test1) {
Text("1")
}
NavigationLink(value: NavigationDestination.test2) {
Text("2")
}
NavigationLink(value: NavigationDestination.test3(text:"X")) {
Text("3")
}
}
}
}
struct TestView: View {
let text: String
var body: some View {
VStack {
Text(text)
NavigationLink(value: NavigationDestination.test1) {
Text("1")
}
NavigationLink(value: NavigationDestination.test2) {
Text("2")
}
NavigationLink(value: NavigationDestination.test3(text:"3-1")) {
Text("3-1")
}
NavigationLink(value: NavigationDestination.test3(text:"3-2")) {
Text("3-2")
}
NavigationLink(value: NavigationDestination.test3(text:"3-3")) {
Text("3-3")
}
NavigationLink(value: NavigationDestination.test3(text:"3-4")) {
Text("3-4")
}
NavigationLink(value: NavigationDestination.test3(text:"3-5")) {
Text("3-5")
}
List {
ForEach(0..<200) { index in
Text("LIST \(index)")
}
}
}
}
}
struct MainView: View {
#State private var history: [[NavigationDestination]] = []
#State private var historyIndex: Int = -1
#State private var canGoBackInHistory = false
#State private var canGoForwardInHistory = false
#State private var path: [NavigationDestination] = []
var body: some View {
NavigationSplitView(columnVisibility: .constant(.all)) {
SidebarView()
.navigationDestination(for: NavigationDestination.self) { destination in
detailBuilder(destination)
.navigationBarBackButtonHidden(true)
}
} detail: {
NavigationStack(path: $path) {
Group {
}
.navigationDestination(for: NavigationDestination.self) { destination in
detailBuilder(destination)
.navigationBarBackButtonHidden(true)
}
}
}
.onChange(of: path) { newValue in
if history[safe: historyIndex] == newValue {
return
}
if historyIndex != history.count - 1 {
history.removeLast(history.count - historyIndex)
}
if newValue != [] {
history.append(newValue)
}
historyIndex = history.count - 1
canGoBackInHistory = history.count > 0
canGoForwardInHistory = false
print("======\n \(history)")
}
.toolbar {
ToolbarItemGroup(placement: .navigation) {
Button(action: {
goBackInHistory()
}, label: {
Image(systemName: "chevron.left")
})
.disabled(!canGoBackInHistory)
Button(action: {
goForwardInHistory()
}, label: {
Image(systemName: "chevron.right")
})
.disabled(!canGoForwardInHistory)
}
}
}
func goBackInHistory() {
guard let h = history[safe: historyIndex - 1] else {
return
}
historyIndex -= 1
canGoBackInHistory = historyIndex > 0
canGoForwardInHistory = true
withAnimation {
path = h
}
}
func goForwardInHistory() {
guard let h = history[safe: historyIndex + 1] else {
return
}
historyIndex += 1
canGoBackInHistory = true
canGoForwardInHistory = historyIndex < (history.count - 1)
withAnimation {
path = h
}
}
#ViewBuilder
func detailBuilder(_ destination: NavigationDestination) -> some View {
switch destination {
case .test1:
TestView(text: "1")
case .test2:
TestView(text: "2")
case .test3(let text):
TestView(text: text)
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
extension Collection where Indices.Iterator.Element == Index {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Iterator.Element? {
return indices.contains(index) ? self[index] : nil
}
}

Related

How to popToRoot with the new iOS 16 NavigationStack inside a TabView?

I would like to be able to popToRoot from my first tabItem (when im on page2 (from first tabItem) and tap twice on first tabItem when im on second tabItem) without having to use the NavigationViewKit package and only the new NavigationStack, is that possible?
My solution works fine but I would like to get rid of the NavigationViewKit package.
Any good advice\code example will be appreciated.
My code :
Home page
import SwiftUI
import NavigationViewKit
struct Home_V: View {
#State var tabSelected: Int = 0
#State private var tappedTwice: Bool = false
// https://designcode.io/swiftui-handbook-tabbar-to-root-view
var handler: Binding<Int> { Binding(
get: { tabSelected },
set: {
if $0 == tabSelected {
// print("tabSelected == \(tabSelected)")
tappedTwice = true
}
tabSelected = $0
}
)}
// https://github.com/fatbobman/NavigationViewKit
#Environment(\.navigationManager) var nvmanager
var body: some View {
TabView(selection: handler) {
NavigationStack {
Page1()
.onChange(of: tappedTwice, perform: { tappedTwice in
guard tappedTwice else { return }
if tabSelected == 0 {
self.tappedTwice = false
nvmanager.wrappedValue.popToRoot(tag:"Page1", animated: true){}
}
})
}
.tabItem {
Label("_HomeTitle", systemImage: "house")
.environment(\.symbolVariants, tabSelected == 0 ? .fill : .none)
}
.tag(0)
.navigationViewStyle(StackNavigationViewStyle())
NavigationStack {
Page2()
}
.tabItem {
Label("_MessagesTitle", systemImage: "envelope")
.environment(\.symbolVariants, tabSelected == 1 ? .fill : .none)
}
.tag(1)
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
Page1
import SwiftUI
import NavigationViewKit
struct Page1: View {
var body: some View {
VStack {
Text("Page 1")
NavigationLink {
Page2()
} label: {
Text("Go to Page 2")
}
}
.navigationViewManager(for: "Page1", afterBackDo: {print("Back to Page1")})
}
}
I got it!
Here is my test code :
import SwiftUI
class NavigationCoordinator: ObservableObject {
#Published var path = NavigationPath()
func popToRoot() {
path.removeLast(path.count)
}
}
struct Test_PopToRoot_NavigationStack: View {
#State private var tabSelected: Int = 0
#State private var tappedTwice: Bool = false
#StateObject var navigationCoordinator = NavigationCoordinator()
#StateObject var navigationCoordinator2 = NavigationCoordinator()
// https://designcode.io/swiftui-handbook-tabbar-to-root-view
var handler: Binding<Int> { Binding(
get: { tabSelected },
set: {
if $0 == tabSelected {
// print("tabSelected == \(tabSelected)")
tappedTwice = true
}
tabSelected = $0
}
)}
var body: some View {
TabView(selection: handler) {
NavigationStack(path: $navigationCoordinator.path) {
VStack {
NavigationLink(value: 1) {
Test_PopToRoot_Tabview1()
.foregroundColor(.black)
.onChange(of: tappedTwice, perform: { tappedTwice in
guard tappedTwice else { return }
if tabSelected == 0 {
self.tappedTwice = false
print("Home tapped twice!!!")
navigationCoordinator.popToRoot()
}
})
}
}
}
.environmentObject(navigationCoordinator)
.tabItem {
Label("_HomeTitle", systemImage: "house")
.environment(\.symbolVariants, tabSelected == 0 ? .fill : .none)
}
.tag(0)
.navigationViewStyle(StackNavigationViewStyle())
NavigationStack(path: $navigationCoordinator2.path) {
VStack {
NavigationLink(value: 1) {
Test_PopToRoot_Tabview2()
.foregroundColor(.black)
.onChange(of: tappedTwice, perform: { tappedTwice in
guard tappedTwice else { return }
if tabSelected == 1 {
self.tappedTwice = false
print("2nd Tab tapped twice!!!")
navigationCoordinator2.popToRoot()
}
})
}
}
}
.environmentObject(navigationCoordinator2)
.tabItem {
Label("_MessagesTitle", systemImage: "envelope")
.environment(\.symbolVariants, tabSelected == 1 ? .fill : .none)
}
.tag(1)
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct Test_PopToRoot_Tabview1: View {
var body: some View {
VStack {
NavigationLink(value: 2) {
Text("Go To Page2")
.foregroundColor(.black)
}
}
.navigationDestination(for: Int.self) { i in
Test_PopToRoot_Page2()
}
.navigationTitle(Text("Tabview1"))
}
}

TabView disconnects when rotating to Landscape due to SwiftUI's re-render of parent-Views

Using Swift5.3.2, iOS14.4.1, XCode12.4,
As the following code shows, I am working with a quite complex TabView in Page-Mode in SwiftUI.
i.e. using iOS14's new possibility to show Pages:
.tabViewStyle(PageTabViewStyle())
Everything works.
Except, if I rotate my iPhone from Portrait to Landscape, the TabView disconnects and sets the selectedTab index to 0 (i.e. no matter where you scrolled to, rotating iPhone resets unwontedly to page 0).
The parent-View itself is in a complex View hierarchy. And one of the parent-View's of the TabView is updated during the TabView is shown (and swiped). And this might be the problem that the TabView gets re-rendered when rotating to Landscape.
What can I do to keep the TabView-Page during iPhone rotation ??
Here is the code:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
MediaTabView()
CloseButtonView()
}
}
}
And the MediaTabView at question:
struct MediaTabView: View {
#EnvironmentObject var appStateService: AppStateService
#EnvironmentObject var commService: CommunicationService
#State private var tagID = ""
#State private var selectedTab = 0
#State private var uniqueSelected = 0
#State private var IamInSwipingAction = false
var body: some View {
let myDragGesture = DragGesture(minimumDistance: 10)
.onChanged { _ in
IamInSwipingAction = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5000)) {
IamInSwipingAction = false // workaround: because onEnded does not work...
}
}
.onEnded { _ in
IamInSwipingAction = false
}
TabView(selection: self.$selectedTab) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
switch appStateService.appState {
case .content(let tagID):
return list.tagId == tagID
default:
return false
}
}) {
if list.paths.count > 0 {
ForEach(list.paths.indices, id: \.self) { index in
ZoomableScrollView {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
} else if url.containsVideo {
CustomPlayerView(url: url)
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.onAppear() {
if uniqueSelected != selectedTab {
uniqueSelected = selectedTab
if IamInSwipingAction && (commService.communicationRole == .moderatorMode) {
commService.send(thCmd: THCmd(key: .swipeID, sender: "", content: URL(fileURLWithPath: list.paths[index]).lastPathComponent))
}
}
}
}
} else {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
if Constants.TrihowAlbum.tagIdArrayTrihowAlbum.contains(tagID) {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
} else {
Text(LocalizedStringKey("TagNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
}
.onAppear() {
switch appStateService.appState {
case .content(let tagID):
self.tagID = tagID
default:
self.tagID = ""
}
}
.tabViewStyle(PageTabViewStyle())
.onTHComm_ReceiveCmd(service: commService) { (thCmd) in
switch thCmd.key {
case .swipeID:
if (commService.communicationRole == .moderatorMode) || (commService.communicationRole == .discoveryMode) {
selectTabFromCmdID(fileName: thCmd.content)
} else {
break
}
default:
break
}
}
.simultaneousGesture(myDragGesture)
}
}
extension MediaTabView {
private func selectTabFromCmdID(fileName: String) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
return list.tagId == tagID
}) {
if list.paths.count > 0 {
if let idx = list.paths.firstIndex(where: { (urlPath) -> Bool in
if let url = URL(string: urlPath) {
return url.lastPathComponent == fileName
} else { return false }
}) {
selectedTab = idx
}
}
}
}
}

How to return and store different type view

I want
MapView change by condition
store current MapView
call method in current MapView
Is this possible?
MapViewEnvironment.swift
class MapViewEnvironment: ObservableObject {
#Published var value1 = "aaa"
#Published var currentMapCompany = "apple"
}
MapViewProtocol.swift
protocol MapViewProtocol {
func aaa()
func bbb()
}
AppleMapView.swift
struct AppleMapView: UIViewRepresentable, MapViewProtocol {
func aaa() { print("AppleMapView - aaa") }
func bbb() { print("AppleMapView - bbb") }
}
GoogleMapView.swift
struct GoogleMapView: UIViewRepresentable, MapViewProtocol {
func aaa() { print("GoogleMapView - aaa") }
func bbb() { print("GoogleMapView - bbb") }
}
CommonMapView.swift
struct CommonMapView: View {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
#State var cancellable = Set<AnyCancellable>()
#State var currentMapView: AnyView? // <-- correct?
func choiceView() -> some View {
switch mapViewEnvironment.currentMapCompany {
case "apple":
currentMapView = AnyView(AppleMapView()) // Modifying state during view update, this will cause undefined behavior.
default:
currentMapView = AnyView(GoogleMapView())
}
return currentMapView
}
var body: some View {
choiceView().onAppear {
self.mapViewEnvironment.$value1
.filter { $0 == "aaa" }
.sink { _ in currentMapView.aaa() } // error
.store(in: &self.cancellable)
self.mapViewEnvironment.$value1
.filter { $0 == "bbb" }
.sink { _ in currentMapView.bbb() } // error
.store(in: &self.cancellable)
}
}
}
ContentView.swift
struct ContentView: View {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
var body: some View {
VStack{
Button(action: { self.mapViewEnvironment.value1 = "aaa" }) { Text("set aaa") }
Button(action: { self.mapViewEnvironment.value1 = "bbb" }) { Text("set bbb") }
CommonMapView()
}
}
}
Self answer.
I found another way and change some class.
ContentView.swift
struct ContentView: View {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
var body: some View {
VStack {
Button(action: { self.mapViewEnvironment.value1 = "aaa" }) { Text("set aaa") }
Button(action: { self.mapViewEnvironment.value1 = "bbb" }) { Text("set bbb") }
Button(action: { self.mapViewEnvironment.currentMapCompany = "apple" }) { Text("set apple") }
Button(action: { self.mapViewEnvironment.currentMapCompany = "google" }) { Text("set google") }
CommonMapView()
}
}
}
CommonMapView.swift
struct CommonMapView: View {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
var body: some View {
ZStack {
if mapViewEnvironment.currentMapCompany == "apple" {
AppleMapView()
} else {
GoogleMapView()
}
}
}
}
AppleMapView.swift
struct AppleMapView: UIViewRepresentable {
#EnvironmentObject var mapViewEnvironment: MapViewEnvironment
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView()
view.mapType = .standard
willAppear(context)
return view
}
...
func makeCoordinator() -> AppleMapView.Coordinator {
return Coordinator()
}
static func dismantleUIView(_ uiView: MKMapView, coordinator: AppleMapView.Coordinator) {
coordinator.cancellable.removeAll()
}
final class Coordinator {
var cancellable = Set<AnyCancellable>()
}
}
extension AppleMapView: MapViewProtocol {
func willAppear(_ context: Context) {
mapViewEnvironment.$value1.filter { $0 == "aaa" }.sink { _ in self.aaa() }.store(in: &context.coordinator.cancellable)
mapViewEnvironment.$value1.filter { $0 == "bbb" }.sink { _ in self.bbb() }.store(in: &context.coordinator.cancellable)
}
func aaa() { print("AppleMapView - aaa") }
func bbb() { print("AppleMapView - bbb") }
}

How to delete multiple rows from List in SwiftUI?

I took an example from this question: How does one enable selections in SwiftUI's List and edited the code to be able to delete rows one by one. But I don't know how to delete multiple rows from list.
Could you help me, please?
var demoData = ["Phil Swanson", "Karen Gibbons", "Grant Kilman", "Wanda Green"]
struct ContentView : View {
#State var selectKeeper = Set<String>()
var body: some View {
NavigationView {
List(selection: $selectKeeper){
ForEach(demoData, id: \.self) { name in
Text(name)
}
.onDelete(perform: delete)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))
}
}
func delete(at offsets: IndexSet) {
demoData.remove(atOffsets: offsets)
}
}
solution from SwiftUI how to perform action when EditMode changes?
struct Item: Identifiable {
let id = UUID()
let title: String
static var i = 0
init() {
self.title = "\(Item.i)"
Item.i += 1
}
}
struct ContentView: View {
#State var editMode: EditMode = .inactive
#State var selection = Set<UUID>()
#State var items = [Item(), Item(), Item()]
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items) { item in
Text(item.title)
}
}
.navigationBarTitle(Text("Demo"))
.navigationBarItems(
leading: editButton,
trailing: addDelButton
)
.environment(\.editMode, self.$editMode)
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var addDelButton: some View {
if editMode == .inactive {
return Button(action: addItem) {
Image(systemName: "plus")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
private func addItem() {
items.append(Item())
}
private func deleteItems() {
for id in selection {
if let index = items.lastIndex(where: { $0.id == id }) {
items.remove(at: index)
}
}
selection = Set<UUID>()
}
}
extension EditMode {
var title: String {
self == .active ? "Done" : "Edit"
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}

SwiftUI: Switch between List and grid/Collection View

Goal: A button that switches between List and Grid/Collection View.
For this, I am using the great WaterfallGrid:
https://github.com/paololeonardi/WaterfallGrid
I haven't managed able to make it work. I am using state, and if statement, as code bellow:
import SwiftUI
import WaterfallGrid
struct Fruit: Identifiable {
let id = UUID()
let name: String
let image: Image
}
struct ExampleView: View {
#State private var fruits = [
Fruit(name: "Apple", image: Image("apple")),
Fruit(name: "Banana", image: Image("banana")),
Fruit(name: "Grapes", image: Image("grapes")),
Fruit(name: "Peach", image: Image("peach"))]
#State private var showgrid = true
var body: some View {
NavigationView {
if showgrid == .true {
return
WaterfallGrid(fruits) { fruit in
HStack {
fruit.image.resizable().frame(width: 30, height: 30)
Text(fruit.name)
}
}
}
else {
return
List(fruits) { fruit in
HStack {
fruit.image.resizable().frame(width: 30, height: 30)
Text(fruit.name)
}
}
}
.navigationBarTitle("Fruits")
.navigationBarItems(trailing:
Button(action: { self.showmaterialrmenu.toggle() }) {
Image(systemName: "rectangle.on.rectangle.angled")
})
}
}
}
struct ExampleView_Previews: PreviewProvider {
static var previews: some View {
ExampleView()
}
}
Really appreciate any help!
Cheers
Hello it's not a good way to put all you views in one place I created one project that need to be having a list and grid mode so here's my implementation:
private enum HomeMode {
case list, grid
func icon() -> String {
switch self {
case .list: return "rectangle.3.offgrid.fill"
case .grid: return "rectangle.grid.1x2"
}
}
}
#State private var homeMode = HomeMode.list
private var swapHomeButton: some View {
Button(action: {
self.homeMode = self.homeMode == .grid ? .list : .grid
}) {
HStack {
Image(systemName: self.homeMode.icon()).imageScale(.medium)
}.frame(width: 30, height: 30)
}
}
and here's my main view:
var body: some View {
let view = Group {
if homeMode == .list {
homeAsList
} else {
homeAsGrid
}
}
.navigationBarItems(trailing:
HStack {
swapHomeButton
settingButton
}
).sheet(isPresented: $isSettingPresented,
content: { SettingsForm() })
return navigationView(content: AnyView(view))
}
HomeList:
private var homeAsList: some View {
Group {
if selectedMenu.menu == .genres {
GenresList(headerView: AnyView(segmentedView))
} else {
MoviesHomeList(menu: $selectedMenu.menu,
pageListener: selectedMenu.pageListener,
headerView: AnyView(segmentedView))
}
}
}
private var homeAsGrid: some View {
MoviesHomeGrid()
}
that's the answer If you are asking about how to arrange the views but if you are asking how to implement the WaterfallGrid hit me back so I can check it and see if we can solve this