we have several SwiftUI screens that are presented as sheet.
all of then can be dismissed by clicking a button.
so basically all of them have these 2 in common:
#Environment(\.presentationMode) var presentationMode
func dismiss() {
presentationMode.wrappedValue.dismiss()
}
how can i declare them only once and just reuse them only in specific views?
i cannot use inheritance since they are stucts, extensions cannot contain state (except using a holder struct) and would add these to all instances of the same view type.
The .presentationMode is available throughout current view hierarchy, so we can use this feature to wrap & manage dismiss in some modifier.
Here is a demo of solution based on button style, so any button can be specified as dismissing and it will dismiss current presentation.
Prepared with Xcode 12.1 / iOS 14.1
struct TestReuseDismissed: View {
#State private var isActive = false
var body: some View {
Button("Show Sheet") {
isActive = true
}
.sheet(isPresented: $isActive) {
Button("Dismiss", action: {
// do something before dismiss here !!
})
.buttonStyle(DismissButtonStyle())
}
}
}
struct DismissButtonStyle: PrimitiveButtonStyle {
#Environment(\.presentationMode) var presentationMode
func makeBody(configuration: Configuration) -> some View {
Button(action: {
configuration.trigger()
dismiss()
}) { configuration.label }
}
func dismiss() {
presentationMode.wrappedValue.dismiss()
}
}
Related
I'm trying to create a IOS app with SwiftUI that uses NavigationViewand hides the Navigation Bar on the first view (and only on the first one).
So I created an ObservableObject
class NavBarShowViewModel: ObservableObject {
#Published var isHidden: Bool = true
}
and in my Content View
struct ContentView: View {
#ObservedObject var navBarShowViewModel = NavBarShowViewModel()
var body: some View {
NavigationView {
Home()
.navigationBarHidden(self.navBarShowViewModel.isHidden)
}
.environmentObject(self.navBarShowViewModel)
}
}
Now in Home I have:
struct Home: View {
#EnvironmentObject var navBarShowViewModel: NavBarShowViewModel
var body: some View {
VStack {
HStack {
NavigationLink(destination: FirstPage()) {
Text("Go!")
}
Text("Hello World")
Spacer()
}
.navigationBarTitle("Home")
}
.onAppear(perform: {
self.navBarShowViewModel.isHidden = true
})
}
}
Now FirstPage() has the exact structure of Home(), except that has a different title
.navigationBarTitle("First Page")
and onAppear has the following code:
.onAppear(perform: {
self.navBarShowViewModel.isHidden = false
})
With this setup, the app works.
But if inside FirstPage() I navigate further, for example going to SecondPage() (which is for sake of simplicity, identical to FirstPage() with a different title) and then hitting back until I return to Home, onAppear here on Home() is not called, so it shows the navigation bar title.
Could someone explain this?
It is about how SwiftUI engine tracks views and if it really appears... anyway, what it is can be determined as it is how child views appear/disappear on navigation stack, so possible solution is to add onDisappear in FirstPage, like
struct FirstPage: View {
#EnvironmentObject var navBarShowViewModel: NavBarShowViewModel
var body: some View {
... other code here
.onAppear(perform: {
self.navBarShowViewModel.isHidden = false
})
.onDisappear(perform: {
self.navBarShowViewModel.isHidden = true
})
}
}
Tested with Xcode 12.4 / iOS 14.4
Whenever my code gets too big, SwiftUI starts acting weird and generates an error:
"The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
So I started breaking up my code into Extracted Subviews, one of the problems I came across is how to dismiss a view from a subtracted subview.
Example: we have here LoginContentView this view contains a button when the button is clicked it will show the next view UsersOnlineView.
struct LoginContentView: View {
#State var showUsersOnlineView = false
var body: some View {
Button(action: {
self.showUsersOnlineView = true
}) {
Text("Show the next view")
}
.fullScreenCover(isPresented: $showUsersOnlineView, content: {
UsersOnlineView()
})
}
On the other hand, we have a button that is extracted to subview, to dismiss the modal and go back to the original view:
import SwiftUI
struct UsersOnlineView: View {
var body: some View {
ZStack {
VStack {
CloseViewButton()
}
}
}
}
struct CloseViewButton: View {
var body: some View {
Button(action: {
// Close the Modal
}) {
Text("Close the view")
}
}
}
Give the sbview the state property that defines if the view is shown.
struct CloseViewButton: View {
#Binding var showView: Bool
var body: some View {
Button(
ShowView = false
}) {
Text("Close the view")
}
}
}
When you use the sub view give it the property
CloseButtonView(showView: $showOnlineView)
To allow the sub view to change the isShown property it needs to get a binding.
On the presentation mode. I think this only works with Swiftui presentations like sheet and alert.
The simplest solution for this scenario is to use presentationMode environment variable:
struct CloseViewButton: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Close the view")
}
}
}
Tested with Xcode 12.1 / iOS 14.1
I am trying to add a close button to the modally presented View's navigation bar. However, after dismiss, my view models deinit method is never called. I've found that the problem is where it captures the self in navigationBarItem's. I can't just pass a weak self in navigationBarItem's action, because View is a struct, not a class. Is this a valid issue or just a lack of knowledge?
struct ModalView: View {
#Environment(\.presentationMode) private var presentation: Binding<PresentationMode>
#ObservedObject var viewModel: ViewModel
var body: some View {
NavigationView {
Text("Modal is presented")
.navigationBarItems(leading:
Button(action: {
// works after commenting this line
self.presentation.wrappedValue.dismiss()
}) {
Text("close")
}
)
}
}
}
You don't need to split the close button out in its own view. You can solve this memory leak by adding a capture list to the NavigationView's closure: this will break the reference cycle that retains your viewModel.
You can copy/paste this sample code in a playground to see that it solves the issue (Xcode 11.4.1, iOS playground).
import SwiftUI
import PlaygroundSupport
struct ModalView: View {
#Environment(\.presentationMode) private var presentation
#ObservedObject var viewModel: ViewModel
var body: some View {
// Capturing only the `presentation` property to avoid retaining `self`, since `self` would also retain `viewModel`.
// Without this capture list (`->` means `retains`):
// self -> body -> NavigationView -> Button -> action -> self
// this is a retain cycle, and since `self` also retains `viewModel`, it's never deallocated.
NavigationView { [presentation] in
Text("Modal is presented")
.navigationBarItems(leading: Button(
action: {
// Using `presentation` without `self`
presentation.wrappedValue.dismiss()
},
label: { Text("close") }))
}
}
}
class ViewModel: ObservableObject { // << tested view model
init() {
print(">> inited")
}
deinit {
print("[x] destroyed")
}
}
struct TestNavigationMemoryLeak: View {
#State private var showModal = false
var body: some View {
Button("Show") { self.showModal.toggle() }
.sheet(isPresented: $showModal) { ModalView(viewModel: ViewModel()) }
}
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.setLiveView(TestNavigationMemoryLeak())
My solution is
.navigationBarItems(
trailing: self.filterButton
)
..........................................
var filterButton: some View {
Button(action: {[weak viewModel] in
viewModel?.showFilter()
},label: {
Image("search-filter-icon").renderingMode(.original)
})
}
I was having a gnarly memory leak due to navigationBarItems and passing my view model to the view I was using as the bar item.
Digging around on this, I learned that navigationBarItems is deprecated
I had
.navigationBarItems(trailing:
AlbumItemsScreenNavButtons(viewModel: viewModel)
)
The replacement is toolbar.
My usage now looks like this:
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
AlbumItemsScreenNavButtons(viewModel: viewModel)
}
}
I recommend design-level solution, ie. decomposing navigation bar item into separate view component breaks that undesired cycle referencing that result in leak.
Tested with Xcode 11.4 / iOS 13.4 - ViewModel destroyed as expected.
Here is complete test module code:
struct CloseBarItem: View { // separated bar item with passed binding
#Binding var presentation: PresentationMode
var body: some View {
Button(action: {
self.presentation.dismiss()
}) {
Text("close")
}
}
}
struct ModalView: View {
#Environment(\.presentationMode) private var presentation
#ObservedObject var viewModel: ViewModel
var body: some View {
NavigationView {
Text("Modal is presented")
.navigationBarItems(leading:
CloseBarItem(presentation: presentation)) // << decompose
}
}
}
class ViewModel: ObservableObject { // << tested view model
init() {
print(">> inited")
}
deinit {
print("[x] destroyed")
}
}
struct TestNavigationMemoryLeak: View {
#State private var showModal = false
var body: some View {
Button("Show") { self.showModal.toggle() }
.sheet(isPresented: $showModal) { ModalView(viewModel: ViewModel()) }
}
}
struct TestNavigationMemoryLeak_Previews: PreviewProvider {
static var previews: some View {
TestNavigationMemoryLeak()
}
}
I want to achieve the function. Like, "Look up" view that is from Apple.
My aim is when the sheet view push another view by navigation, the user can tap the navigation item button to close the sheet view. Like, this below gif.
I try to achieve this function.
I found a problem that is when the user tap the "Done" button. The App doesn't close the sheet view. It only pop the view to parent view. Like, this below gif.
This is my code.
import SwiftUI
struct ContentView: View {
#State var isShowSheet = false
var body: some View {
Button(action: {
self.isShowSheet.toggle()
}) {
Text("Tap to show the sheet")
}.sheet(isPresented: $isShowSheet) {
NavView()
}
}
}
struct NavView: View {
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView()) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}){
Text("Done")
}
)
}
}
How did I achieve this function? :)
Please help me, thank you. :)
UPDATE: Restored original version - provided below changes should be done, as intended, to the topic starter's code. Tested as worked with Xcode 13 / iOS 15
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 11.2 / iOS 13.2)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var showingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
}.sheet(isPresented: $isShowSheet) {
NavView()
.environment(\.showingSheet, self.$isShowSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.showingSheet) var showingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
self.showingSheet?.wrappedValue = false
}
)
}
}
I haven't tried SwiftUI ever, but I came from UIKit + RxSwift, so I kinda know how binding works. I read quite a bit of sample codes from a SwiftUI Tutorial, and the way you dismiss a modal is actually correct, but apparently not for a navigation stack.
One way I learned just now is use a #Binding var. This might not be the best solution, but it worked!
So you have this $isShowSheet in your ContentView. Pass that object to your NavView struct by declaring a variable in that NavView.
ContentView
.....
}.sheet(isPresented: $isShowSheet) {
NavView(isShowSheet: self.$isShowSheet)
}
NavView
struct NavView: View {
#Binding var isShowSheet: Bool
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView(isShowSheet: self.$isShowSheet)) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
and finally, do the same thing to your subView.
NavSubView
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var isShowSheet: Bool
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
//self.presentationMode.projectedValue.wrappedValue.dismiss()
self.isShowSheet = false
}){
Text("Done")
}
)
}
}
Now as you can see, you just need to send a new signal to that isShowSheet binding var - false.
self.isShowSheet = false
Voila!
Here's an improved version of Asperi's code from above since they won't accept my edit. Main credit goes to them.
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 13 / iOS 15)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var isShowingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
#State var isShowingSheet = false
...
Button("open sheet") {
isShowingSheet?.wrappedValue = true
}
// note no $ in front of isShowingSheet
.sheet(isPresented: isShowingSheet ?? .constant(false)) {
NavView()
.environment(\.isShowingSheet, self.$isShowingSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.isShowingSheet) var isShowingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
isShowingSheet?.wrappedValue = false
}
)
}
}
I'm using swiftUI and combine, I'have some business logic in my VM. Some results have to pop my view in navigation view stack.
I'v used this one in some views to simulate backbutton event :
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
self.presentationMode.wrappedValue.dismiss()
I tried it in view model but it doesn't work. Any ideas ?
This is a follow up question that I answered previously.
You can achieve this by implementing your custom Publisher which will use .send() method to allow you to send specific values to the subscriber (in this case, your View). You will use onReceive(_:perform:) method defined on the View protocol of SwiftUI to subscribe to the output stream of the custom Publisher you defined. Inside the perform action closure where you will have the access to the latest emitted value of your publisher, you will do the actual dismissal of your View.
Enough of the theory, you can look at the code, should not be very hard to follow, below:
import Foundation
import Combine
class ViewModel: ObservableObject {
var viewDismissalModePublisher = PassthroughSubject<Bool, Never>()
private var shouldPopView = false {
didSet {
viewDismissalModePublisher.send(shouldPopView)
}
}
func performBusinessLogic() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.shouldPopView = true
}
}
}
And the views are:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
NavigationLink(destination: DetailView()) {
Text("Detail")
}
}
.navigationBarTitle(Text("Home"))
}
}
}
struct DetailView: View {
#ObservedObject var viewModel = ViewModel()
#Environment(\.presentationMode) private var presentationMode
var body: some View {
Text("Detail")
.navigationBarTitle("Detail", displayMode: .inline)
.onAppear {
self.viewModel.performBusinessLogic()
}
.onReceive(viewModel.viewDismissalModePublisher) { shouldPop in
if shouldPop {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}