Adding middleware to an API Client dependency ๐Ÿค”

Hey,

I'm looking for some advice/inspiration on how to add some sort of middleware to our apiClient @Dependency within the TCA app.

We use a token/secret auth system for authenticating our endpoints. If the token and secret become out of sync we need to boot the user out of the app and get them to sign in again.

Because of this I'd like to watch the api requests and be able to push the user out to the login screen if a 403 (or something is received).

Initially I was thinking about having some sort of AsyncStream that I can listen to but that won't as I can't return anything to the request if it fails. It's a one way stream of data.

I was wondering if anyone had implemented anything like this before?

My initial intention was for a top level reducer to be able to subscribe to these updates and switch the app state over if things go wrong.

Hmm... maybe that is still possible?

OK... ignore this...

The error that was occurring was not due to what I was doing. It was due to what I believe is a SwiftUI bug (detailed below).

OK... my approach to the status code 403 stuff works... But I've uncovered what I think is a bug in SwiftUI.

I've created a gist here... SwiftUI bug with TabBars and switching them out for other views ยท GitHub

Code here...

import SwiftUI

struct ContentView: View {
	@State var loggedIn: Bool = false

	var body: some View {
		switch loggedIn {
		case false:
			Button("Login") {
				loggedIn = true
			}
		case true:
			TabView {
				NavigationView {
					Text("Home")
						.navigationBarTitle("Home")
						.onAppear {
							print("๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ")
							print("Home on appear")
							print("๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ")
						}
						.toolbar {
							ToolbarItem(placement: .navigationBarTrailing) {
								Button("Logout") {
									loggedIn = false
								}
							}
						}
				}
				.tabItem {
					Image(systemName: "house")
					Text("Home")
				}

				NavigationView {
					Text("Savings")
						.navigationBarTitle("Savings")
						.onAppear {
							print("๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ")
							print("Savings tab on appear")
							print("๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ")
						}
						.toolbar {
							ToolbarItem(placement: .navigationBarTrailing) {
								Button("Logout") {
									loggedIn = false
								}
							}
						}
				}
				.tabItem {
					Image(systemName: "dollarsign.circle")
					Text("Savings")
				}

				NavigationView {
					Text("Profile")
						.navigationBarTitle("Profile")
						.onAppear {
							print("๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ")
							print("Profile tab on appear")
							print("๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ๐Ÿ")
						}
						.toolbar {
							ToolbarItem(placement: .navigationBarTrailing) {
								Button("Logout") {
									loggedIn = false
								}
							}
						}
				}
				.tabItem {
					Image(systemName: "person")
					Text("Profile")
				}
			}
		}
	}
}

And a little video here... SwiftUI bug with TabView and .onAppear - YouTube

If you put this code into a brand new SwiftUI project and run it.

What you should see is when you tap "Login" you should see "Home on appear" in the console.
And then you move to the Profile tab and you should see "Profile tab on appear" in the console.

Then, when you tap "Logout" you should not expect to see anything in the console.

BUT... we see "Home on appear" in the console again as the TabView is being removed from the stack.

What it looks like is happening is...

When a TabView is removed from the stack like this. Every tab that you have visited in that session that is not the current tab will have the onAppear triggered again.

1 Like

It's actually worse than I thought. It also triggers the .task for the tabs too...

With this code...

import SwiftUI

struct ContentView: View {
	@State var loggedIn: Bool = false

	var body: some View {
		switch loggedIn {
		case false:
			Button("Login") {
				loggedIn = true
			}
			.onAppear {
				print("๐Ÿ Login on appear")
			}
			.onDisappear {
				print("๐ŸŽ Login on disappear")
			}
			.task {
				print("๐ŸŒ Login task")
			}
		case true:
			TabView {
				NavigationView {
					Text("Home")
						.navigationBarTitle("Home")
						.onAppear {
							print("๐Ÿ Home on appear")
						}
						.onDisappear {
							print("๐ŸŽ Home on disappear")
						}
						.task {
							print("๐ŸŒ Home task")
						}
						.toolbar {
							ToolbarItem(placement: .navigationBarTrailing) {
								Button("Logout") {
									loggedIn = false
								}
							}
						}
				}
				.tabItem {
					Image(systemName: "house")
					Text("Home")
				}

				NavigationView {
					Text("Savings")
						.navigationBarTitle("Savings")
						.onAppear {
							print("๐Ÿ Savings on appear")
						}
						.onDisappear {
							print("๐ŸŽ Savings on disappear")
						}
						.task {
							print("๐ŸŒ Savings task")
						}
						.toolbar {
							ToolbarItem(placement: .navigationBarTrailing) {
								Button("Logout") {
									loggedIn = false
								}
							}
						}
				}
				.tabItem {
					Image(systemName: "dollarsign.circle")
					Text("Savings")
				}

				NavigationView {
					Text("Profile")
						.navigationBarTitle("Profile")
						.onAppear {
							print("๐Ÿ Profile on appear")
						}
						.onDisappear {
							print("๐ŸŽ Profile on disappear")
						}
						.task {
							print("๐ŸŒ Profile task")
						}
						.toolbar {
							ToolbarItem(placement: .navigationBarTrailing) {
								Button("Logout") {
									loggedIn = false
								}
							}
						}
				}
				.tabItem {
					Image(systemName: "person")
					Text("Profile")
				}
			}
			.onAppear {
				print("๐Ÿ Tabview on appear")
			}
			.onDisappear {
				print("๐ŸŽ Tabview on disappear")
			}
			.task {
				print("๐ŸŒ Tabview task")
			}
		}
	}
}

With the steps...

  1. Launch
  2. Tap Login
  3. Tap the Savings tab
  4. Tap logout

You get these logs...

  1. :green_apple: Login on appear
  2. :banana: Login task
  3. :green_apple: Tabview on appear
  4. :green_apple: Home on appear
  5. :banana: Tabview task
  6. :banana: Home task
  7. :apple: Login on disappear
  8. :green_apple: Savings on appear
  9. :banana: Savings task
  10. :apple: Home on disappear
  11. :green_apple: Login on appear
  12. :green_apple: Home on appear
  13. :apple: Home on disappear
  14. :apple: Savings on disappear
  15. :apple: Tabview on disappear
  16. :banana: Login task
  17. :banana: Home task

In the logs above... 12, 13, and 17 just shouldn't be there at all.

:man_facepalming: