Skip to content

Instantly share code, notes, and snippets.

@VAndrJ
Created January 6, 2024 22:01
Show Gist options
  • Save VAndrJ/b25ac4d201dbe539f76e0f58b9e605e2 to your computer and use it in GitHub Desktop.
Save VAndrJ/b25ac4d201dbe539f76e0f58b9e605e2 to your computer and use it in GitHub Desktop.
SwiftUI onAppear bug
//
// ContentView.swift
// OnAppearCheck
//
// Created by VAndrJ on 21.12.2023.
//
import SwiftUI
struct ContentView: View {
@State var isLoggedIn: Bool = false
var body: some View {
if isLoggedIn {
TabView {
TabViewView(title: "Store", tabIcon: "trash", isLoggedIn: $isLoggedIn)
TabViewView(title: "New", tabIcon: "newspaper", isLoggedIn: $isLoggedIn)
TabViewView(title: "Gallery", tabIcon: "photo", isLoggedIn: $isLoggedIn)
TabViewView(title: "Discover", tabIcon: "magnifyingglass", isLoggedIn: $isLoggedIn)
TabViewView(title: "Profile", tabIcon: "person", isLoggedIn: $isLoggedIn)
}
} else {
LoginView {
isLoggedIn = true
}
}
}
}
struct TabViewView: View {
let title: String
let tabIcon: String
@Binding var isLoggedIn: Bool
var body: some View {
NavigationStack {
Text(title)
.navigationBarTitle(title)
.task {
print("πŸ‹οΈ \(title) some task")
}
.onAppear {
print("πŸ‘‹ \(title) onAppear")
}
.onDisappear {
print("πŸ˜Άβ€πŸŒ«οΈ \(title) onDisappear")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Logout") {
isLoggedIn = false
}
}
}
}
.task {
print("πŸ‹οΈ \(title) NavigationStack some task")
}
.onAppear {
print("πŸ‘‹ \(title) NavigationStack onAppear")
}
.onDisappear {
print("πŸ˜Άβ€πŸŒ«οΈ \(title) NavigationStack onDisappear")
}
.tabItem {
Image(systemName: tabIcon)
Text(title)
}
}
}
struct LoginView: View {
let onLogin: () -> Void
var body: some View {
Button("Login", action: onLogin)
}
}
//
// ContentView.swift
// OnAppearCheck
//
// Created by VAndrJ on 21.12.2023.
//
import SwiftUI
struct ContentView: View {
@State var isLoggedIn: Bool = false
var body: some View {
if isLoggedIn {
TabView {
TabViewView(title: "Store", tabIcon: "trash")
TabViewView(title: "New", tabIcon: "newspaper")
TabViewView(title: "Gallery", tabIcon: "photo")
TabViewView(title: "Discover", tabIcon: "magnifyingglass")
TabViewView(title: "Profile", tabIcon: "person")
}
.environment(\.logout, LogoutAction(action: logout))
} else {
LoginView {
isLoggedIn = true
}
}
}
func logout() {
isLoggedIn = false
}
}
struct LogoutAction {
let action: () -> Void
func callAsFunction() {
action()
}
}
struct LogoutActionKey: EnvironmentKey {
static var defaultValue: LogoutAction? = nil
}
extension EnvironmentValues {
var logout: LogoutAction? {
get { self[LogoutActionKey.self] }
set { self[LogoutActionKey.self] = newValue }
}
}
struct TabViewView: View {
let title: String
let tabIcon: String
@Environment(\.logout) var logout
var body: some View {
NavigationStack {
Text(title)
.navigationBarTitle(title)
.task {
print("πŸ‹οΈ \(title) some task")
}
.onAppear {
print("πŸ‘‹ \(title) onAppear")
}
.onDisappear {
print("πŸ˜Άβ€πŸŒ«οΈ \(title) onDisappear")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Logout") {
logout?()
}
}
}
}
.task {
print("πŸ‹οΈ \(title) NavigationStack some task")
}
.onAppear {
print("πŸ‘‹ \(title) NavigationStack onAppear")
}
.onDisappear {
print("πŸ˜Άβ€πŸŒ«οΈ \(title) NavigationStack onDisappear")
}
.tabItem {
Image(systemName: tabIcon)
Text(title)
}
}
}
struct LoginView: View {
let onLogin: () -> Void
var body: some View {
Button("Login", action: onLogin)
}
}
@hmlongco
Copy link

hmlongco commented Jan 8, 2024

Looks like most of your problem is failing to provide the TabView with its selection state and also not properly providing the appropriate tag for each tab item. The following code should work as expected.

import SwiftUI

struct MyTabView: View {
    @State var tab: Int = 1
    @State var isLoggedIn: Bool = false
    var body: some View {
        if isLoggedIn {
            TabView(selection: $tab) {
                TabViewView(title: "Store", tabIcon: "trash", isLoggedIn: $isLoggedIn)
                    .tag(1)
                TabViewView(title: "New", tabIcon: "newspaper", isLoggedIn: $isLoggedIn)
                    .tag(2)
                TabViewView(title: "Gallery", tabIcon: "photo", isLoggedIn: $isLoggedIn)
                    .tag(3)
                TabViewView(title: "Discover", tabIcon: "magnifyingglass", isLoggedIn: $isLoggedIn)
                    .tag(4)
                TabViewView(title: "Profile", tabIcon: "person", isLoggedIn: $isLoggedIn)
                    .tag(5)
            }
        } else {
            LoginView(isLoggedIn: $isLoggedIn)
        }
    }
}

struct TabViewView: View {
    let title: String
    let tabIcon: String
    @Binding var isLoggedIn: Bool
    var body: some View {
        NavigationStack {
            Text(title)
                .navigationBarTitle(title)
                .task {
                    print("πŸ‹οΈ \(title) some task")
                }
                .onAppear {
                    print("πŸ‘‹ \(title) onAppear")
                }
                .onDisappear {
                    print("πŸ˜Άβ€πŸŒ«οΈ \(title) onDisappear")
                }
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Logout") {
                            isLoggedIn = false
                        }
                    }
                }
        }
        .tabItem {
            Image(systemName: tabIcon)
            Text(title)
        }
        .task {
            print("πŸ‹οΈ \(title) NavigationStack some task")
        }
        .onAppear {
            print("πŸ‘‹ \(title) NavigationStack onAppear")
        }
        .onDisappear {
            print("πŸ˜Άβ€πŸŒ«οΈ \(title) NavigationStack onDisappear")
        }
    }
}

struct LoginView: View {
    @Binding var isLoggedIn: Bool
    var body: some View {
        Button("Login") {
            isLoggedIn = true
        }
    }
}

@VAndrJ
Copy link
Author

VAndrJ commented Jan 9, 2024

@hmlongco , only this workaround helps:

public extension View {

    func onDidAppear(perform action: @escaping () -> Void) -> some View {
        background(DidAppearView(action: action))
    }
}

struct DidAppearView: UIViewControllerRepresentable {
    class Coordinator: ActionControllerRepresentableDelegate {
        var action: () -> Void

        init(action: @escaping () -> Void) {
            self.action = action
        }

        func performAction() {
            action()
        }
    }

    let action: () -> Void

    func makeUIViewController(context: Context) -> DidAppearViewController {
        let controller = DidAppearViewController()
        controller.delegate = context.coordinator

        return controller
    }

    func updateUIViewController(_ controller: DidAppearViewController, context: Context) {
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(action: action)
    }
}

protocol ActionControllerRepresentableDelegate: AnyObject {
    func performAction()
}

class DidAppearViewController: UIViewController {
    weak var delegate: ActionControllerRepresentableDelegate?

    override func viewDidAppear(_ animated: Bool) {
        delegate?.performAction()
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment