Stupid SwiftUI Tricks: Debugging Sheet Dismissal
source link: https://write.as/angelo/stupid-swiftui-tricks-debugging-sheet-dismissal
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Stupid SwiftUI Tricks: Debugging Sheet Dismissal
September 15, 2020Last week, I spent some time solving an odd bug with the WriteFreely client's iOS app.
When you're looking at the list of posts, you can tap the gear button to get to the settings screen, which presents you with a form for logging into your WriteFreely instance:
As you might expect, you can tap the close button in the upper-right (ⓧ) to dismiss the sheet.
Except… well, if you tapped into one of the login form's fields, you end up in a state where tapping on the close button didn't seem to have any effect.
Okay, so that's not entirely true — if you added a print statement to the button's action, you'd find that the first tap does register, toggling presenting view's the isPresentingSettingsView
flag correctly; it just doesn't have any effect.
The workaround, while I'd been testing the app, was to dismiss the sheet is by swiping down on it — a standard (if somewhat undiscoverable) system gesture.
Interestingly, when you'd tap in any form field, you'd also receive the following warning in Xcode's console:
2020-09-11 09:56:01.927435-0400 WriteFreely-MultiPlatform[37593:6860302] [Presentation] Attempt to present <_TtGC7SwiftUI22SheetHostingControllerVS_7AnyView_: 0x7fb24a7297f0> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7fb24c905ac0> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVVS_22_VariadicView_Children7ElementGVS_18StyleContextWriterVS_23ContentListStyleContext___: 0x7fb24a711ec0>) which is already presenting <_TtGC7SwiftUI22SheetHostingControllerVS_7AnyView_: 0x7fb24c80eaf0>.
There's a lot of cruft there, but it hints that SwiftUI is trying to present a view that's already being presented. This suggested to me that the hosting view is getting re-rendered when a login form field becomes the first responder, finds that the isPresentingSettingsView
flag is set, and tries to present the sheet again.
Okay! This is something we can test! Here's what the settings view looked like:
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var model: WriteFreelyModel
@Binding var isPresented: Bool
var body: some View {
VStack {
HStack {
Text("Settings")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
Button(action: {
self.isPresented = false
}, label: {
Image(systemName: "xmark.circle")
})
}
.padding()
Form {
Section(header: Text("Login Details")) {
AccountView()
}
Section(header: Text("Appearance")) {
PreferencesView(preferences: model.preferences)
}
}
}
}
}
(For debugging purposes, I've simplified this a tiny bit: originally that HStack
was in a separate SettingsHeaderView
struct.)
To test the hypothesis, I started by commenting out the entire Form
. Everything then worked fine in presenting and dismissing the sheet, but of course, it's not a very useful sheet without that form. 😅
If I just included the appearance form, that works fine too. That narrows things down here — or so I thought.
There are two ways to dismiss a sheet. The first is to pass the hosting view's presentation state as a binding to the presented sheet, which is what you see in the above listing. Simplified, the SettingsView
is presented from the PostListView
like this:
Button(action: {
self.isPresentingSettingsView = true
}, label: {
Image(systemName: "gear")
})
.sheet(
isPresented: $isPresentingSettingsView,
content: {
SettingsView(isPresented: self.$isPresentingSettingsView)
}
)
You can also use @Environment(.presentationMode) in the SettingsView
to dismiss itself. You declare the property wrapper at the top of the struct like so:
@Environment(\.presentationMode) var presentationMode
…and call its dismiss()
method in a button action, like so:
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark.circle")
})
Interestingly enough, using this method to dismiss the sheet no longer triggered the console warning when I tapped into any login form field. Could it be? Was the problem solved? 😃
Nope. 😬
If you filled out the form and logged in, then that same warning was logged three times in the console. If you logged out, the warning was logged again. But this looked like progress! It seemed likely that something in the account views was triggering this, so I explored that a little deeper.
The AccountView
swaps between an AccountLoginView
and an AccountLogoutView
based on the state of an isLoggedIn
flag in the AccountModel
. My prime suspect was the AccountLoginView
, which has an .alert(isPresented:)
modifier attached to it. If there's an error logging in, this is triggered and an alert is presented depending on which of the three AccountError
cases are present. Because the .alert(isPresented:)
and .sheet(isPresented:)
modifiers work similarly, maybe some wires were getting crossed there? This is, of course, a beta framework running on a beta operating system in a beta IDE!
So, I started with an easy test: commenting out the .alert(isPresented:)
modifier, and see what happens on login.
You guessed it: this doesn't change the behaviour — the warnings are still logged, and the sheet can't be dismissed.
Digging further and further, setting breakpoints and stepping through code, commenting out blocks to see if they were the culprit, got me nowhere. I finally started searching DuckDuckGo for SwiftUI "Attempt to present" "which is already presenting"
and eventually found this year-old forum comment on Swift.org:
Is it the current recommendation, to put modal views & the triggers outside
NavigationView
, or is it only to circumvent an existing bug?
Yep. Taking the .sheet(isPresented:)
modifier out of the PostListView and attaching it to an EmptyView outside of the NavigationView solved the issue. Nothing in the docs on NavigationView, View Modifiers, or sheet suggests this could be a thing.
So, yeah, the title of this post is a bit misleading — it turns out that I spent a couple of hours trying to figure out what was happening, when an undocumented bug in the framework was the cause.
Again: this is a beta framework, on a beta operating system, and frankly the amount of SwiftUI documentation that's already out there is surprisingly good. But it's a little frustrating to have spent a couple of hours debugging a warning that could have been avoided with a one-line disclaimer in the documentation. Hopefully, this will be helpful to anyone that searches for a similar issue!
For those of you that want to see the code, here's the fix in the app.
Enter your email to subscribe to updates:
You can also subscribe via RSS or follow @[email protected]
on Mastodon.
Recommend
-
36
These are my stupid unix tricks. I hope that they are useful to you. Platform Note I use Mac OS X (pron: “ten”). If you don’t, you might want to switch instances of ~/Library/ to something else, li...
-
22
Stupid std::tuple tricks: Getting started Raymond June 22nd, 2020 The C++ standard...
-
5
Stupid SwiftUI Tricks Single-Axis Geometry Reader I mentioned in
-
7
Stupid Swift Tricks #7½ Writing a User Guide In It In the
-
4
Stupid Swift Tricks #7 Writing a User Guide In It There’s a school of thought that says iOS apps shouldn’t hav...
-
7
Stupid Itertools Tricks for Data Science (This is a blog post version of my
-
2
Stupid Docker Tricks - Validating Outbound Connectivity from an Image Jul 25, 2020 I recently had the odd situation...
-
5
Stupid Swift Tricks #6 To Animate or Not to Animate One of the things that has characterised iOS apps from the...
-
1
Stupid SwiftUI TricksSingle-Axis Geometry ReaderI mentioned in a previous article that, to find...
-
6
Stupid SwiftUI TricksEmulating Equal-Size ConstraintsSwiftUI is quite a new technology, and it still has some rough edges. There are bugs, and there are missing features – but mostly, the design...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK