young
(rtSwift)
1
Two seemingly identical .onAppear() on NavigationView root view but behave very differently: one is working correctly, the other incorrectly:
import SwiftUI
struct Child: View {
var body: some View {
Text("Child View")
.navigationTitle("Child")
}
}
struct ContentView: View {
@State var timerEventChangeThis = 0
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
// Bad: doing nothing here and this is called on timer fire while away to child view!!!
// nothing but only timer firing, this is called whle in child view
func doOnAppear() {
print("👉👉👉doOnAppear👈👈👈 👎if you navigate to Child view, this should not be called until pop back🤮🥺")
}
static func staticDoOnAppear() {
print("👍👌staticDoOnAppear👌👍")
}
var body: some View {
NavigationView {
NavigationLink(">>>> Child", destination: Child())
.navigationTitle("\(timerEventChangeThis)")
// the following three .onAppear is the same, but ...
// this .onAppear works correctly: it's called at the right time when pop back
.onAppear { print("😳 onAppear...") }
// 🥴🥴 this onAppear is called when timer fire, while away to child view !!
.onAppear(perform: doOnAppear)
// and static func works correctly, too:
.onAppear(perform: Self.staticDoOnAppear)
}
.onReceive(timer) { _ in timerEventChangeThis += 1 }
}
}
what makes the second .onAppear bad? (Edit: and static func is fine!): Is this due to something how Swift work? I just cannot see anything in SwiftUI that can tell the two form apart and do thing differently.
This is what originally got me in this wild chase: simply reference a ObservableObject in the .onAppear closure cause problem:
import SwiftUI
final class AppState: ObservableObject {
@Published var counter = 0 // nothing is changing this
}
@main
struct TimerTriggerOnAppearFuncIsEvenWorstApp: App {
@StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
}
}
}
struct Child: View {
var body: some View {
Text("Child View")
.navigationTitle("Child")
}
}
struct ContentView: View {
@State var timerEventChangeThis = 0
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
NavigationLink(">>>> Child", destination: Child())
.navigationTitle("\(timerEventChangeThis)")
// 😩😩 problem here:
// when navigate to child, on timer fire mutate `timerEventChangeThis`
// onAppear() is called while you are stil in Chid View due to simply `_ = appState.counter`!!!
// set empty capture list just to be sure this problem it's not due to capture
.onAppear { [] in
print("😾😾😾onAppear😾😾😾 👎if you navigate to Child view, this should not be called until pop back🤮🥺")
// 👎🐞🐞
// just have this ObservableObject here cause the problem!!
// commented this out, problem goes away
_ = appState.counter
// this also cause problem:
// if appState.counter == 100 {
// // nothing
// }
}
}
.onReceive(timer) { _ in timerEventChangeThis += 1 }
}
}
What could possibly be the reason for such odd behavior? There is no way that I can see someone can write code to do such thing!
AlexisQapa
(Alexis Schultz)
2
I think this is a bug in SwiftUI. I use this to overcome the issue
extension View {
public func onAppearFix(perform action: (() -> Void)? = nil ) -> some View {
self.overlay(UIKitAppear(action: action).disabled(true))
}
}
private struct UIKitAppear: UIViewControllerRepresentable {
let action: (() -> Void)?
func makeUIViewController(context: Context) -> Controller {
let vc = Controller()
vc.action = action
return vc
}
func updateUIViewController(_ controller: Controller, context: Context) {}
class Controller: UIViewController {
var action: (() -> Void)? = nil
override func viewDidLoad() {
view.addSubview(UILabel())
}
override func viewDidAppear(_ animated: Bool) {
action?()
}
}
}
1 Like
young
(rtSwift)
3
Thank you very much for confirming this is a bug and your fix!
So anytime the onAppear closure refer to self then this bug occur. Was so confusing to me when I added .onAppear to my root view and this bug is happening.
AlexisQapa
(Alexis Schultz)
4
I don't know for sure if it's actually a bug or we misuse it.
What do you mean when you say refers to self ? (self is used in the closure or ou apply the modifier on self)
I didn't manage to find a way to know if it will work or not so I always use my fix.
young
(rtSwift)
5
Swift closure can carry with it self so it can refer to the struct instance.
struct Foo: View {
let someVar = 1
func someFunc() { }
static func someStaticFunc() { }
var body: some View {
Text("Parent")
// these two trigger the bug
.onAppear { _ = someVar } // this closure carries `self` so it can refer to `someVar`
.onAppear(perform: someFunc) // `someFunc` carries `self`
// these two doesn't trigger the bug
.onAppear { print("here") } // this closure does not carry `self`
.onAppear(perform: someStaticFunc) // this does not carry `self`
}
}
young
(rtSwift)
7
I made small change to your UIKitAppear:
- action is not optional, default to
{ }
- add
Controller.init(_ action: @escaping () -> Void) so can just do Controller(action)
- I don't know UIKit at all, but this look odd:
override func viewDidLoad() {
view.addSubview(UILabel())
}
I comment out view.addSubview(UILabel()) and seem to still work.
private struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> Controller { Controller(action) }
func updateUIViewController(_ controller: Controller, context: Context) { }
class Controller: UIViewController {
let action: () -> Void
init(_ action: @escaping () -> Void) {
self.action = action
super.init(nibName: nil, bundle: nil)
}
// 'required' initializer 'init(coder:)' must be provided by subclass of 'UIViewController'
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
// view.addSubview(UILabel())
}
override func viewDidAppear(_ animated: Bool) {
action()
}
}
}
AlexisQapa
(Alexis Schultz)
8
I must say that I compiled this from different sources while looking for a solution. I'm pretty sure the viewDidload override had a use (something like forcing the view to draw) but I'm not sure. It might just be debug garbage :)
young
(rtSwift)
9
Problem is fixed Xcode 13 beta 5
1 Like