TabView keeping state on tab changes SwiftUI - swiftui

I'm having my View's reload when my TabView tab changes. I'm trying to show a ProgressView while fetching data from an API on initial data loading, but it's being reloaded every tab change, even after navigating to another view. If I remove my ProgressView it works, it just doesn't look great when loading data.
I assumed this would have been a simple task, but I haven't found a great solution. I found StatefulTabView but it seems broken on iOS 15. I'm not sure if I'm just doing something incorrectly with my view model. Any help would be appreciated.
view model
class RickAndMortyViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
#Published var dataone = RickandMorty(results: [])
#Published var datatwo = RickandMorty(results: [])
#Published var loadingone: Bool = false
#Published var loadingtwo: Bool = false
func getpageone() {
self.loadingone = true
URLSession.shared.dataTaskPublisher(for:URLRequest(url: URL(string: "https://rickandmortyapi.com/api/character/?page=1")!))
.map{ $0.data }
.decode(type: RickandMorty.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { results in
switch results {
case .finished:
self.loadingone = false
case .failure(let error):
print(error)
}
},receiveValue: { data in
self.dataone = data
})
.store(in: &cancellables)
}
func getpagetwo() {
self.loadingtwo = true
URLSession.shared.dataTaskPublisher(for:URLRequest(url: URL(string: "https://rickandmortyapi.com/api/character/?page=2")!))
.map{ $0.data }
.decode(type: RickandMorty.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { results in
switch results {
case .finished:
self.loadingtwo = false
case .failure(let error):
print(error)
}
},receiveValue: { data in
self.datatwo = data
})
.store(in: &cancellables)
}
}
view
struct ContentView: View {
#StateObject var viewmodel = RickAndMortyViewModel()
var body: some View {
TabView {
NavigationView {
if viewmodel.loadingone {
ProgressView("Loading Page 1")
.navigationTitle("Page 1")
} else {
List {
ForEach(viewmodel.dataone.results, id: \.id) { character in
NavigationLink(destination: EmptyView()) {
Text(character.name)
}
}
}
.navigationTitle("Page 1")
}
}
.onAppear {
viewmodel.getpageone()
}
.tabItem {
Label("Page 1", systemImage: "person")
}
.tag(1)
NavigationView {
if viewmodel.loadingtwo {
ProgressView("Loading Page 2")
.navigationTitle("Page 2")
} else {
List {
ForEach(viewmodel.datatwo.results, id: \.id) { character in
NavigationLink(destination: EmptyView()) {
Text(character.name)
}
}
}
.navigationTitle("Page 2")
}
}
.onAppear {
viewmodel.getpagetwo()
}
.tabItem {
Label("Page 2", systemImage: "person")
}
.tag(2)
}
}
}

I see what you mean now (I think).
My internet is very fast and so I could not see the ProgressView going on.
You could try this in RickAndMortyViewModel:
#Published var loadingone: Bool = true
#Published var loadingtwo: Bool = true
func getpageone() {
if !loadingone {return}
...
}
func getpagetwo() {
if !loadingtwo {return}
...
}

Break them into Views with independent #StateObjects unless you want to share data across them
class RickAndMortyViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
#Published var dataone = RickandMorty(results: [])
#Published var datatwo = RickandMorty(results: [])
#Published var loadingone: Bool = false
#Published var loadingtwo: Bool = false
func getpageone() {
self.loadingone = true
URLSession.shared.dataTaskPublisher(for:URLRequest(url: URL(string: "https://rickandmortyapi.com/api/character/?page=1")!))
.map{ $0.data }
.decode(type: RickandMorty.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { results in
switch results {
case .finished:
self.loadingone = false
case .failure(let error):
print(error)
}
},receiveValue: { data in
self.dataone = data
})
.store(in: &cancellables)
}
func getpagetwo() {
self.loadingtwo = true
URLSession.shared.dataTaskPublisher(for:URLRequest(url: URL(string: "https://rickandmortyapi.com/api/character/?page=2")!))
.map{ $0.data }
.decode(type: RickandMorty.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { results in
switch results {
case .finished:
self.loadingtwo = false
case .failure(let error):
print(error)
}
},receiveValue: { data in
self.datatwo = data
})
.store(in: &cancellables)
}
}
struct ContentView: View {
#StateObject var viewmodel = RickAndMortyViewModel()
var body: some View {
TabView {
TabPageOne()
.tabItem {
Label("Page 1", systemImage: "person")
}
.tag(1)
TabPageTwo()
.tabItem {
Label("Page 2", systemImage: "person")
}
.tag(2)
}
}
}
struct TabPageOne: View {
#StateObject var viewmodel = RickAndMortyViewModel()
var body: some View {
NavigationView {
if viewmodel.loadingone {
ProgressView("Loading Page 1")
.navigationTitle("Page 1")
} else {
List {
ForEach(viewmodel.dataone.results, id: \.id) { character in
NavigationLink(destination: EmptyView()) {
Text(character.name)
}
}
}
.navigationTitle("Page 1")
}
}
.onAppear {
viewmodel.getpageone()
}
}
}
struct TabPageTwo: View {
#StateObject var viewmodel = RickAndMortyViewModel()
var body: some View {
NavigationView {
if viewmodel.loadingtwo {
ProgressView("Loading Page 2")
.navigationTitle("Page 2")
} else {
List {
ForEach(viewmodel.datatwo.results, id: \.id) { character in
NavigationLink(destination: EmptyView()) {
Text(character.name)
}
}
}
.navigationTitle("Page 2")
}
}
.onAppear {
viewmodel.getpagetwo()
}
}
}

Related

Getting the total for a number of fields

I have an app that records costs for a car. I can't work out how to create a field that keeps a running total for the ongoing costs. In the ContentView file I have a struct that defines what an expense is, which includes the 'amount'.
Any help is appreciated. Thanks.
There are 2 files, ContentView, and Addview;
struct ContentView: View {
#StateObject var expenseList = ExpenseList()
#State private var isShowingAddView = false
#State private var totalCost = 0.0
var body: some View {
NavigationView {
VStack {
VStack(alignment: .trailing) {
Text("Total Cost").font(.headline) //just holding a place for future code
}
Form {
List {
ForEach(expenseList.itemList) { trans in
HStack{
Text(trans.item)
.font(.headline)
Spacer()
VStack(alignment: .trailing) {
HStack {
Text("Amount: ")
.font(.caption).bold()
Text(trans.amount, format: .currency(code: "USD"))
.font(.caption)
}
}
}
}
.onDelete(perform: removeItems)
}
}
.navigationTitle("Expenditure")
.toolbar {
Button {
isShowingAddView = true
} label: {
Image(systemName: "plus")
}
}
.sheet(isPresented: $isShowingAddView) {
AddView(expenseList: expenseList)
}
}
}
}
func removeItems(at offsets: IndexSet) {
expenseList.itemList.remove(atOffsets: offsets)
}
}
class ExpenseList: ObservableObject {
#Published var itemList = [ExpenseItem]() {
didSet {
if let encoded = try? JSONEncoder().encode(itemList) {
UserDefaults.standard.set(encoded, forKey: "Things")
}
}
}
init() {
if let savedItems = UserDefaults.standard.data(forKey: "Things") {
if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
itemList = decodedItems
return
}
}
itemList = []
}
}
struct ExpenseItem: Identifiable, Codable {
var id = UUID()
let item: String
let amount: Double
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct AddView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var expenseList: ExpenseList
#State private var item = "Fuel"
#State private var amount = 0.0
let itemType = ["Fuel", "Tyres"]
var body: some View {
NavigationView {
Form {
Picker("Type", selection: $item) {
ForEach(itemType, id: \.self) {
Text($0)
}
}
TextField("Enter amount...", value: $amount, format: .currency(code: "USD"))
}
.navigationTitle("Add an item...")
.toolbar {
Button("Save") {
let trans = ExpenseItem(item: item, amount: amount)
expenseList.itemList.append(trans)
dismiss()
}
}
}
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView(expenseList: ExpenseList())
}
}
There are many ways to do ... create a field that keeps a running total for the ongoing costs. This is just one way.
Try this approach, using an extra var totalCost in your ExpenseList and a summation.
class ExpenseList: ObservableObject {
#Published private (set) var totalCost = 0.0 // <-- here
#Published var itemList = [ExpenseItem]() {
didSet {
if let encoded = try? JSONEncoder().encode(itemList) {
UserDefaults.standard.set(encoded, forKey: "Things")
}
totalCost = itemList.map{ $0.amount }.reduce(0.0, { $0 + $1 }) // <-- here
}
}
init() {
if let savedItems = UserDefaults.standard.data(forKey: "Things") {
if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
itemList = decodedItems
return
}
}
itemList = []
}
}
And use it like this:
Text("Total Cost: \(expenseList.totalCost)").font(.headline)
You can of course do this, without adding any extra var:
Text("Total Cost: \(expenseList.itemList.map{ $0.amount }.reduce(0.0, { $0 + $1 }))")

Running Total in SwiftUI [duplicate]

I have an app that records costs for a car. I can't work out how to create a field that keeps a running total for the ongoing costs. In the ContentView file I have a struct that defines what an expense is, which includes the 'amount'.
Any help is appreciated. Thanks.
There are 2 files, ContentView, and Addview;
struct ContentView: View {
#StateObject var expenseList = ExpenseList()
#State private var isShowingAddView = false
#State private var totalCost = 0.0
var body: some View {
NavigationView {
VStack {
VStack(alignment: .trailing) {
Text("Total Cost").font(.headline) //just holding a place for future code
}
Form {
List {
ForEach(expenseList.itemList) { trans in
HStack{
Text(trans.item)
.font(.headline)
Spacer()
VStack(alignment: .trailing) {
HStack {
Text("Amount: ")
.font(.caption).bold()
Text(trans.amount, format: .currency(code: "USD"))
.font(.caption)
}
}
}
}
.onDelete(perform: removeItems)
}
}
.navigationTitle("Expenditure")
.toolbar {
Button {
isShowingAddView = true
} label: {
Image(systemName: "plus")
}
}
.sheet(isPresented: $isShowingAddView) {
AddView(expenseList: expenseList)
}
}
}
}
func removeItems(at offsets: IndexSet) {
expenseList.itemList.remove(atOffsets: offsets)
}
}
class ExpenseList: ObservableObject {
#Published var itemList = [ExpenseItem]() {
didSet {
if let encoded = try? JSONEncoder().encode(itemList) {
UserDefaults.standard.set(encoded, forKey: "Things")
}
}
}
init() {
if let savedItems = UserDefaults.standard.data(forKey: "Things") {
if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
itemList = decodedItems
return
}
}
itemList = []
}
}
struct ExpenseItem: Identifiable, Codable {
var id = UUID()
let item: String
let amount: Double
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct AddView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var expenseList: ExpenseList
#State private var item = "Fuel"
#State private var amount = 0.0
let itemType = ["Fuel", "Tyres"]
var body: some View {
NavigationView {
Form {
Picker("Type", selection: $item) {
ForEach(itemType, id: \.self) {
Text($0)
}
}
TextField("Enter amount...", value: $amount, format: .currency(code: "USD"))
}
.navigationTitle("Add an item...")
.toolbar {
Button("Save") {
let trans = ExpenseItem(item: item, amount: amount)
expenseList.itemList.append(trans)
dismiss()
}
}
}
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView(expenseList: ExpenseList())
}
}
There are many ways to do ... create a field that keeps a running total for the ongoing costs. This is just one way.
Try this approach, using an extra var totalCost in your ExpenseList and a summation.
class ExpenseList: ObservableObject {
#Published private (set) var totalCost = 0.0 // <-- here
#Published var itemList = [ExpenseItem]() {
didSet {
if let encoded = try? JSONEncoder().encode(itemList) {
UserDefaults.standard.set(encoded, forKey: "Things")
}
totalCost = itemList.map{ $0.amount }.reduce(0.0, { $0 + $1 }) // <-- here
}
}
init() {
if let savedItems = UserDefaults.standard.data(forKey: "Things") {
if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
itemList = decodedItems
return
}
}
itemList = []
}
}
And use it like this:
Text("Total Cost: \(expenseList.totalCost)").font(.headline)
You can of course do this, without adding any extra var:
Text("Total Cost: \(expenseList.itemList.map{ $0.amount }.reduce(0.0, { $0 + $1 }))")

TabView SwiftUI return to Home page on click [duplicate]

Starting point is a NavigationView within a TabView. I'm struggling with finding a SwiftUI solution to pop to the root view within the navigation stack when the selected tab is tapped again. In the pre-SwiftUI times, this was as simple as the following:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let navController = viewController as! UINavigationController
navController.popViewController(animated: true)
}
Do you know how the same thing can be achieved in SwiftUI?
Currently, I use the following workaround that relies on UIKit:
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: MyCustomView() // -> this is a normal SwiftUI file
.environment(\.managedObjectContext, context)))
navigationController.tabBarItem = UITabBarItem(title: "My View 1", image: nil, selectedImage: nil)
// add more controllers that are part of tab bar controller
let tabBarController = UITabBarController()
tabBarController.viewControllers = [navigationController /* , additional controllers */ ]
window.rootViewController = tabBarController // UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
Here is possible approach. For TabView it gives the same behaviour as tapping to the another tab and back, so gives persistent look & feel.
Tested & works with Xcode 11.2 / iOS 13.2
Full module code:
import SwiftUI
struct TestPopToRootInTab: View {
#State private var selection = 0
#State private var resetNavigationID = UUID()
var body: some View {
let selectable = Binding( // << proxy binding to catch tab tap
get: { self.selection },
set: { self.selection = $0
// set new ID to recreate NavigationView, so put it
// in root state, same as is on change tab and back
self.resetNavigationID = UUID()
})
return TabView(selection: selectable) {
self.tab1()
.tabItem {
Image(systemName: "1.circle")
}.tag(0)
self.tab2()
.tabItem {
Image(systemName: "2.circle")
}.tag(1)
}
}
private func tab1() -> some View {
NavigationView {
NavigationLink(destination: TabChildView()) {
Text("Tab1 - Initial")
}
}.id(self.resetNavigationID) // << making id modifiable
}
private func tab2() -> some View {
Text("Tab2")
}
}
struct TabChildView: View {
var number = 1
var body: some View {
NavigationLink("Child \(number)",
destination: TabChildView(number: number + 1))
}
}
struct TestPopToRootInTab_Previews: PreviewProvider {
static var previews: some View {
TestPopToRootInTab()
}
}
Here's an approach that uses a PassthroughSubject to notify the child view whenever the tab is re-selected, and a view modifier to allow you to attach .onReselect() to a view.
import SwiftUI
import Combine
enum TabSelection: String {
case A, B, C // etc
}
private struct DidReselectTabKey: EnvironmentKey {
static let defaultValue: AnyPublisher<TabSelection, Never> = Just(.Mood).eraseToAnyPublisher()
}
private struct CurrentTabSelection: EnvironmentKey {
static let defaultValue: Binding<TabSelection> = .constant(.Mood)
}
private extension EnvironmentValues {
var tabSelection: Binding<TabSelection> {
get {
return self[CurrentTabSelection.self]
}
set {
self[CurrentTabSelection.self] = newValue
}
}
var didReselectTab: AnyPublisher<TabSelection, Never> {
get {
return self[DidReselectTabKey.self]
}
set {
self[DidReselectTabKey.self] = newValue
}
}
}
private struct ReselectTabViewModifier: ViewModifier {
#Environment(\.didReselectTab) private var didReselectTab
#State var isVisible = false
let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content
.onAppear {
self.isVisible = true
}.onDisappear {
self.isVisible = false
}.onReceive(didReselectTab) { _ in
if self.isVisible, let action = self.action {
action()
}
}
}
}
extension View {
public func onReselect(perform action: (() -> Void)? = nil) -> some View {
return self.modifier(ReselectTabViewModifier(perform: action))
}
}
struct NavigableTabViewItem<Content: View>: View {
#Environment(\.didReselectTab) var didReselectTab
let tabSelection: TabSelection
let imageName: String
let content: Content
init(tabSelection: TabSelection, imageName: String, #ViewBuilder content: () -> Content) {
self.tabSelection = tabSelection
self.imageName = imageName
self.content = content()
}
var body: some View {
let didReselectThisTab = didReselectTab.filter( { $0 == tabSelection }).eraseToAnyPublisher()
NavigationView {
self.content
.navigationBarTitle(tabSelection.localizedStringKey, displayMode: .inline)
}.tabItem {
Image(systemName: imageName)
Text(tabSelection.localizedStringKey)
}
.tag(tabSelection)
.navigationViewStyle(StackNavigationViewStyle())
.keyboardShortcut(tabSelection.keyboardShortcut)
.environment(\.didReselectTab, didReselectThisTab)
}
}
struct NavigableTabView<Content: View>: View {
#State private var didReselectTab = PassthroughSubject<TabSelection, Never>()
#State private var _selection: TabSelection = .Mood
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
let selection = Binding(get: { self._selection },
set: {
if self._selection == $0 {
didReselectTab.send($0)
}
self._selection = $0
})
TabView(selection: selection) {
self.content
.environment(\.tabSelection, selection)
.environment(\.didReselectTab, didReselectTab.eraseToAnyPublisher())
}
}
}
Here's how I did it:
struct UIKitTabView: View {
var viewControllers: [UIHostingController<AnyView>]
init(_ tabs: [Tab]) {
self.viewControllers = tabs.map {
let host = UIHostingController(rootView: $0.view)
host.tabBarItem = $0.barItem
return host
}
}
var body: some View {
TabBarController(controllers: viewControllers).edgesIgnoringSafeArea(.all)
}
struct Tab {
var view: AnyView
var barItem: UITabBarItem
init<V: View>(view: V, barItem: UITabBarItem) {
self.view = AnyView(view)
self.barItem = barItem
}
}
}
struct TabBarController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = controllers
tabBarController.delegate = context.coordinator
return tabBarController
}
func updateUIViewController(_ uiViewController: UITabBarController, context: Context) { }
}
extension TabBarController {
func makeCoordinator() -> TabBarController.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: TabBarController
init(_ parent: TabBarController){self.parent = parent}
var previousController: UIViewController?
private var shouldSelectIndex = -1
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
shouldSelectIndex = tabBarController.selectedIndex
return true
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if shouldSelectIndex == tabBarController.selectedIndex {
if let navVC = tabBarController.viewControllers![shouldSelectIndex].nearestNavigationController {
if (!(navVC.popViewController(animated: true) != nil)) {
navVC.viewControllers.first!.scrollToTop()
}
}
}
}
}
}
extension UIViewController {
var nearestNavigationController: UINavigationController? {
if let selfTypeCast = self as? UINavigationController {
return selfTypeCast
}
if children.isEmpty {
return nil
}
for child in self.children {
return child.nearestNavigationController
}
return nil
}
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.safeAreaInsets.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
}
Then in ContentView.swift I use it like this:
struct ContentView: View {
var body: some View {
ZStack{
UIKitTabView([
UIKitTabView.Tab(
view: FirstView().edgesIgnoringSafeArea(.top),
barItem: UITabBarItem(title: "Tab1", image: UIImage(systemName: "star"), selectedImage: UIImage(systemName: "star.fill"))
),
UIKitTabView.Tab(
view: SecondView().edgesIgnoringSafeArea(.top),
barItem: UITabBarItem(title: "Tab2", image: UIImage(systemName: "star"), selectedImage: UIImage(systemName: "star.fill"))
),
])
}
}
}
Note that when the user is already on the root view, it scrolls to top automatically
Here's what I did with introspect swiftUI library.
https://github.com/siteline/SwiftUI-Introspect
struct TabBar: View {
#State var tabSelected: Int = 0
#State var navBarOne: UINavigationController?
#State var navBarTwo: UINavigationController?
#State var navBarThree: UINavigationController?
var body: some View {
return TabView(selection: $tabSelected){
NavView(navigationView: $navBarOne).tabItem {
Label("Home1",systemImage: "bag.fill")
}.tag(0)
NavView(navigationView: $navBarTwo).tabItem {
Label("Orders",systemImage: "scroll.fill" )
}.tag(1)
NavView(navigationView: $navBarThree).tabItem {
Label("Wallet", systemImage: "dollarsign.square.fill" )
// Image(systemName: tabSelected == 2 ? "dollarsign.square.fill" : "dollarsign.square")
}.tag(2)
}.onTapGesture(count: 2) {
switch tabSelected{
case 0:
self.navBarOne?.popToRootViewController(animated: true)
case 1:
self.navBarTwo?.popToRootViewController(animated: true)
case 2:
self.navBarThree?.popToRootViewController(animated: true)
default:
print("tapped")
}
}
}
}
NavView:
import SwiftUI
import Introspect
struct NavView: View {
#Binding var navigationView: UINavigationController?
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: Text("Detail view")) {
Text("Go To detail")
}
}.introspectNavigationController { navController in
navigationView = navController
}
}
}
}
This actually isn't the best approach because it makes the entire tab view and everything inside of it have the double-tap gesture which would pop the view to its root. My current fix for this allows for one tap to pop up root view haven't figured out how to add double tap
struct TabBar: View {
#State var tabSelected: Int = 0
#State var navBarOne: UINavigationController?
#State var navBarTwo: UINavigationController?
#State var navBarThree: UINavigationController?
#State var selectedIndex:Int = 0
var selectionBinding: Binding<Int> { Binding(
get: {
self.selectedIndex
},
set: {
if $0 == self.selectedIndex {
popToRootView(tabSelected: $0)
}
self.selectedIndex = $0
}
)}
var body: some View {
return TabView(selection: $tabSelected){
NavView(navigationView: $navBarOne).tabItem {
Label("Home1",systemImage: "bag.fill")
}.tag(0)
NavView(navigationView: $navBarTwo).tabItem {
Label("Orders",systemImage: "scroll.fill" )
}.tag(1)
NavView(navigationView: $navBarThree).tabItem {
Label("Wallet", systemImage: "dollarsign.square.fill" )
// Image(systemName: tabSelected == 2 ? "dollarsign.square.fill" : "dollarsign.square")
}.tag(2)
}
}
func popToRootView(tabSelected: Int){
switch tabSelected{
case 0:
self.navBarOne?.popToRootViewController(animated: true)
case 1:
self.navBarTwo?.popToRootViewController(animated: true)
case 2:
self.navBarThree?.popToRootViewController(animated: true)
default:
print("tapped")
}
}
}
I took an approach similar to Asperi
Use a combination of a custom binding, and a separately stored app state var for keeping state of the navigation link.
The custom binding allows you to see all taps basically even when the current tab is the one thats tapped, something that onChange of tab selection binding doesn't show. This is what imitates the UIKit TabViewDelegate behavior.
This doesn't require a "double tap", if you just a single tap of the current, if you want double tap you'll need to implement your own tap/time tracking but shouldn't be too hard.
class AppState: ObservableObject {
#Published var mainViewShowingDetailView = false
}
struct ContentView: View {
#State var tabState: Int = 0
#StateObject var appState = AppState()
var body: some View {
let binding = Binding<Int>(get: { tabState },
set: { newValue in
if newValue == tabState { // tapped same tab they're already on
switch newValue {
case 0: appState.mainViewShowingDetailView = false
default: break
}
}
tabState = newValue // make sure you actually set the storage
})
TabView(selection: binding) {
MainView()
.tabItem({ Label("Home", systemImage: "list.dash") })
.tag(0)
.environmentObject(appState)
}
}
}
struct MainView: View {
#EnvironmentObject var appState: AppState
var body: {
NavigationView {
VStack {
Text("Hello World")
NavigationLink(destination: DetailView(),
isActive: $appState.mainViewShowingDetailView,
label: { Text("Show Detail") })
}
}
}
}
struct DetailView: View {
...
}
iOS 16 / NavigationStack approach with PassthroughSubject
Uses willSet on selectedTab to get the tap event, and uses a PassthroughSubject for sending the event to the children. This is picked up by the .onReceived and calls a function for popping the views from the NavigationStack
Did a full write up here: https://kentrobin.com/home/tap-tab-to-go-back/ and created a working demo project here: https://github.com/kentrh/demo-tap-tab-to-go-back
class HomeViewModel: ObservableObject {
#Published var selectedTab: Tab = .tab1 {
willSet {
if selectedTab == newValue {
subject.send(newValue)
}
}
}
let subject = PassthroughSubject<Tab, Never>()
enum Tab: Int {
case tab1 = 0
}
}
struct HomeView: View {
#StateObject var viewModel: HomeViewModel = .init()
var body: some View {
TabView(selection: $viewModel.selectedTab) {
Tab1View(subject: viewModel.subject)
.tag(HomeViewModel.Tab.tab1)
.tabItem {
Label("Tab 1", systemImage: "1.lane")
Text("Tab 1", comment: "Tab bar title")
}
}
}
}
struct Tab1View: View {
#StateObject var viewModel: Tab1ViewModel = .init()
let subject: PassthroughSubject<HomeViewModel.Tab, Never>
var body: some View {
NavigationStack(path: $viewModel.path) {
List {
NavigationLink(value: Tab1ViewModel.Route.viewOne("From tab 1")) {
Text("Go deeper to OneView")
}
NavigationLink(value: Tab1ViewModel.Route.viewTwo("From tab 1")) {
Text("Go deeper to TwoView")
}
}
.navigationTitle("Tab 1")
.navigationDestination(for: Tab1ViewModel.Route.self, destination: { route in
switch route {
case let .viewOne(text):
Text(text)
case let .viewTwo(text):
Text(text)
}
})
.onReceive(subject) { tab in
if case .tab1 = tab { viewModel.tabBarTapped() }
}
}
}
}
class Tab1ViewModel: ObservableObject {
#Published var path: [Route] = []
func tabBarTapped() {
if path.count > 0 {
path.removeAll()
}
}
enum Route: Hashable {
case viewOne(String)
case viewTwo(String)
}
}

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 how to perform action when EditMode changes?

I'd like to perform an action when the EditMode changes.
Specifically, in edit mode, the user can select some items to delete. He normally presses the trash button afterwards. But he may also press Done. When he later presses Edit again, the items that were selected previously are still selected. I would like all items to be cleared.
struct ContentView: View {
#State var isEditMode: EditMode = .inactive
#State var selection = Set<UUID>()
var items = [Item(), Item(), 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.$isEditMode)
}
}
private var addDelButton: some View {
if isEditMode == .inactive {
return Button(action: reset) {
Image(systemName: "plus")
}
} else {
return Button(action: reset) {
Image(systemName: "trash")
}
}
}
private func reset() {
selection = Set<UUID>()
}
}
Definition of Item:
struct Item: Identifiable {
let id = UUID()
let title: String
static var i = 0
init() {
self.title = "\(Item.i)"
Item.i += 1
}
}
UPDATED for iOS 15.
This solution catches 2 birds with one stone:
The entire view redraws itself when editMode is toggle
A specific action can be performed upon activation/inactivation of editMode
Hopes this helps someone else.
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)
}
}
.navigationTitle(Text("Demo"))
.environment(\.editMode, self.$editMode)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
editButton
}
ToolbarItem(placement: .navigationBarTrailing) {
addDelButton
}
}
}
}
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
}
}
I was trying forever, to clear List selections when the user exited editMode. For me, the cleanest way I've found to react to a change of editMode:
Make sure to reference the #Environment variable:
#Environment(\.editMode) var editMode
Add a computed property in the view to monitor the state:
private var isEditing: Bool {
if editMode?.wrappedValue.isEditing == true {
return true
}
return false
}
Then use the .onChange(of:perform:) method:
.onChange(of: self.isEditing) { value in
if value == false {
// do something
} else {
// something else
}
}
All together:
struct ContentView: View {
#Environment(\.editMode) var editMode
#State private var selections: [String] = []
#State private var colors: ["Red", "Yellow", "Blue"]
private var isEditing: Bool {
if editMode?.wrappedValue.isEditing == true {
return true
}
return false
}
var body: some View {
List(selection: $selections) {
ForEach(colors, id: \.self) { color in
Text("Color")
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
.onChange(of: isEditing) { value in
if value == false {
selection.removeAll()
}
}
}
}
In case someone want to use SwiftUI's EditButton() instead of custom a Button and still want to perform action when isEditing status changes
You can use View extension
extension View {
func onChangeEditMode(editMode: EditMode?, perform: #escaping (EditMode?)->()) -> some View {
ZStack {
Text(String(describing: editMode))
.opacity(0)
.onChange(of: editMode, perform: perform)
self
}
}
}
Then you can use it like this
struct TestEditModeView: View {
#Environment(\.editMode) var editMode
#State private var editModeDescription: String = "nil"
var body: some View {
VStack {
Text(editModeDescription)
EditButton()
}
.onChangeEditMode(editMode: editMode?.wrappedValue) {
editModeDescription = String(describing: $0)
}
}
}