Let me know if this is a duplicate question.
I am developing an app with a photo picker. I am on view A and there is a + button on it. When I tap on the +, I want to navigate to a view B. Upon navigation I want to present a Photo Picker view automatically inside view B. I am not able to figure out how to do that presentation of the sheet in Swiftui.
In UIKit,on the viewdidappear of viewcontroller B, i would present the Pickerview.
Here's the code that I have
```import SwiftUI
struct ViewB: View {
#State private var showingImagePicker = false
#State private var uploadedPhotos = [UIImage]()
var body: some View {
VStack {
Button("Select Image") {
showingImagePicker = true
}
}
.sheet(isPresented: $showingImagePicker) {
PhotoPicker(photoList: $uploadedPhotos)
}
.onChange(of: uploadedPhotos) { _ in
}
}
}```
This code is ViewB and I will present the pickerView on tapping a button in viewB. but I don't want to tap on button but instead want the picker to show up on appear of ViewB
There is a view modifier in SwiftUI known as .onAppear(perform:), which is the SwiftUI counterpart to UIKit's viewDidAppear and gives you an entry point upon the construction and display of a SwiftUI view. Simply add this modifier to View B (the view inside the sheet that you are presenting). In the closure that you provide to the modifier, you can change the state to present the picker as needed.
If you'd like the picker view to animate in after the view appears, the appropriate place to declare the transition and animation context is on the view acting as your picker view.
struct ViewB: View {
#State private var displayPicker = false
var body: some View {
VStack {
Text("This is View B")
if displayPicker {
PickerView()
.transition(.slide)
.animation(.easeInOut, value: displayPicker)
}
}
.onAppear {
displayPicker = true
}
}
}
Read more about the modifier here:
https://developer.apple.com/documentation/SwiftUI/AnyView/onAppear(perform:)
Think I figured it out -
struct CreateView: View {
#State private var image: Image?
#State private var showingImagePicker = false
#State private var uploadedPhotos = [UIImage]()
var body: some View {
ScrollView {
HStack {
image?.resizable().scaledToFit()
}
.onAppear {
showingImagePicker = true
}
.sheet(isPresented: $showingImagePicker) {
PhotoPicker(photoList: $uploadedPhotos)
}
.onChange(of: uploadedPhotos) { _ in
guard let uiImage = uploadedPhotos.first else {return}
image = Image(uiImage: uiImage)
}
}
}
}
Here PhotoPicker() is the customview that I have
Related
I have 3 views within my app where clicking on a button on each view opens a new view. When button is clicked on 3rd view, I wish to dismiss 3rd view and 2nd view should appear. However I am noticing that app navigates back to first view instead of 2nd view.
Note: I have lots of elements, hence lots of code in my app. I removed all of them and adding minimal working code here with which I am still able to repro the problem.
// *** Main App***
#main
struct sample_sampleApp: App {
var body: some SwiftUI.Scene {
WindowGroup {
NavigationView {
ContentView().ignoresSafeArea()
}.navigationViewStyle(.stack)
}
}
}
// *** Content View or First View***
import SwiftUI
struct ContentView: View {
#State private var goToView2 = false
var body: some View {
VStack {
NavigationLink(destination: View2(), isActive: $goToView2) {
Button(action: { goToView2.toggle() }) {
Text("This is first view - Click to go to View 2").foregroundColor(.red).font(.title)
}
}
}
}
}
// *** View2 View***
import SwiftUI
import Combine
struct View2: View {
#ObservedObject var viewModel: View2ViewModel = View2ViewModel()
var body: some View {
VStack {
switch viewModel.state {
case .showView2:
VStack(alignment: .leading, spacing: 8) {
Button(action: { viewModel.navigateToView3() } ) {
Text("Second View - Click to go to View 3").foregroundColor(.blue).font(.title)
}
}
case .showView3:
View3()
}
}
.onAppear() {
viewModel.isViewVisible = true
viewModel.doSomething()
}
.onDisappear() {
viewModel.isViewVisible = false
}
}
}
// *** View model for view 2***
class View2ViewModel: ObservableObject {
enum View2AppState {
case showView2
case showView3
}
// UI changes when state gets updated.
#Published var state: View2AppState = .showView2
var isViewVisible = false
func doSomething() {
self.state = .showView2
}
func navigateToView3() {
self.state = .showView3
}
}
// *** View3***
struct View3: View {
#ObservedObject var viewModel: View3ViewModel = View3ViewModel()
#Environment(\.dismiss) var dismiss
var body: some View {
VStack {
switch viewModel.state {
case .showView3:
VStack(alignment: .leading, spacing: 8) {
Button(action: { dismiss() } ) {
Text("Third View - Click to dismiss this and to go back to view 2").foregroundColor(.green).font(.title)
}
}
}
}
.onAppear() {
viewModel.isViewVisible = true
viewModel.doSomething()
}
.onDisappear() {
viewModel.isViewVisible = false
}.navigationBarBackButtonHidden(true)
}
}
// *** View model for view 3***
class View3ViewModel: ObservableObject {
enum View3AppState {
case showView3
}
// UI changes when state gets updated.
#Published var state: View3AppState = .showView3
var isViewVisible = false
func doSomething() {
self.state = .showView3
}
}
Not sure what am I doing wrong. Sometime back I did use dismiss() while dismissing sheet and it worked fine, but not this this case. I am running this code on iOS 16.0, however my test app is set to iOS 15 as minimum version.
Edit: I tested on iOS 15.0 as well and was able to repro on it too, so something must be wrong with my code then. Not able to figure out what. I am using NavigationView in and navigation view style as Stack.
When button is clicked on 3rd view, I wish to dismiss 3rd view and 2nd view should appear.
Let's first have a look at the code of view2.
struct View2: View {
#ObservedObject var viewModel: View2ViewModel = View2ViewModel()
var body: some View {
VStack {
switch viewModel.state {
case .showView2:
VStack(alignment: .leading, spacing: 8) {
Button(action: { viewModel.navigateToView3() } ) {
Text("Second View - Click to go to View 3").foregroundColor(.blue).font(.title)
}
}
case .showView3:
View3()
}
}
}
}
// here viewModel.navigateToView3() is just changing this state
// func navigateToView3() {
// self.state = .showView3
// }
The current code behavior, when tapping to navigate to view3, replaces the content of view2 with view3 instead of actually navigating to it.
Therefore, when the dismiss function is called, it should not return to view2 as it is already in view2 displaying the content of view3.
So, going back to view1 on the dismiss press is actually the correct behavior as per the code.
If you desire the outcome you are asking, consider modifying the code using a closure passed into the child view to change the state in view2 or explore this answer to actually navigate to it.
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 toggle a sheet in SwiftUI with the following Button
Button(action: {
self.statusPopoverIsShown.toggle()
})
So the following sheet appears
.sheet(isPresented: self.$popoverIsShown) {
RandomSheet(popoverIsShown: self.$popoverIsShown)
}
Then I have a button inside the RandomSheetto dismiss the sheet (sets the popoverIsShown to false). Everything works fine.
But when I start using the app in splitscreen or somehow change the sizeclass SwiftUI transforms the sheet to a fullscreen iPhone-like sheet and the dismiss button/the binding does not work anymore.
Is there any solution to avoid this and keep the binding stable?
The following works with any size class changes. Tested with Xcode 12 / iOS 14
struct TestSheet: View {
#State private var popoverIsShown = false
var body: some View {
Button("Show Sheet") {
self.popoverIsShown = true
}
.sheet(isPresented: self.$popoverIsShown) {
RandomSheet(popoverIsShown: self.$popoverIsShown)
}
}
}
struct RandomSheet: View {
#Binding var popoverIsShown: Bool
var body: some View {
Button("Close") { self.popoverIsShown = false }
}
}
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 was surprised that whenever I tap on the button embedded in PresentationLink, the same view reference shows up all the time. What I mean is that we don't create another instance of the view.
This makes sense as the destination object is created within the body property and will therefore not be recreated unless a change occur.
Do you guys know if we have a trivial way to recreate a new view every time we hit the button? Or is it by design and should be use like this?
Thank you!
EDIT
After #dfd 's comment, it seems to be by designed.
Now how to handle this use case:
Let's say I present a NavigationView and I pushed one view. If I
dismiss and re present, I will go back on the view I previously
pushed. In this case, I believe it's wrong as I'd like the user to go
through the complete flow every single time. How can I make sure that
I go back on the first screen everytime?
Thank you (again)!
EDIT 2
Here's some code:
struct PresenterExample : View {
var body: some View {
VStack {
PresentationLink(destination: CandidateCreateProfileJobView()) {
Text("Present")
}
}
}
}
struct StackFirstView : View {
var body: some View {
NavigationLink(destination: StackSecondView()) {
Text("Got to view 2")
}
}
}
struct StackSecondView : View {
var body: some View {
Text("View 2")
}
}
In this case that would be PresenterExample presents StackFirstView which will push StackSecondView from the NavigationLink.
From there, let's say the user swipe down and therefore dismiss the presentation. When it clicks back on the PresentationLink in PresenterExample it will open back on StackSecondView, which is not what I want. I want to display StackFirstView again.
Makes more sense? :)
First Try: Failure
I tried using the id modifier to tell SwiftUI to treat each presentation of StackFirstView as a completely new view unrelated to prior views:
import SwiftUI
struct PresenterExample : View {
var body: some View {
VStack {
PresentationLink("Present", destination:
StackFirstView()
.onDisappear {
print("onDisappear")
self.presentationCount += 1
}
)
}
}
#State private var presentationCount = 0
}
struct StackFirstView : View {
var body: some View {
NavigationView {
NavigationLink(destination: StackSecondView()) {
Text("Go to view 2")
}.navigationBarTitle("StackFirstView")
}
}
}
struct StackSecondView : View {
var body: some View {
Text("View 2")
.navigationBarTitle("StackSecondView")
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(rootView: PresenterExample())
This should do the following:
It should identify the StackFirstView by presentationCount. SwiftUI should consider each StackFirstView with a different identifier to be a completely different view. I've used this successfully with animated transitions.
It should increment presentationCount when the StackFirstView is dismissed, so that the next StackFirstView gets a different identifier.
The problem is that SwiftUI never calls the onDisappear closure for the presented view or any of its subviews. I'm pretty sure this is a SwiftUI bug (as of Xcode 11 beta 3). I filed FB6687752.
Second Try: FailureSuccess
Next I tried managing the presentation myself, using the presentation(Modal?) modifier, so I wouldn't need the onDisappear modifier:
import SwiftUI
struct PresenterExample : View {
var body: some View {
VStack {
Button("Present") {
self.presentModal()
}.presentation(modal)
}
}
#State private var shouldPresent = false
#State private var presentationCount = 0
private func presentModal() {
presentationCount += 1
shouldPresent = true
}
private var modal: Modal? {
guard shouldPresent else { return nil }
return Modal(StackFirstView().id(presentationCount), onDismiss: { self.shouldPresent = false })
}
}
struct StackFirstView : View {
var body: some View {
NavigationView {
NavigationLink(destination: StackSecondView()) {
Text("Go to view 2")
}.navigationBarTitle("StackFirstView")
}
}
}
struct StackSecondView : View {
var body: some View {
Text("View 2")
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(rootView: PresenterExample())
This fails in a different way. The second and later presentations of StackFirstView simply present a blank view instead. Again, I'm pretty sure this is a SwiftUI bug. I filed FB6687804.
I tried passing the presentationCount down to StackFirstView and then applying the .id(presentationCount) modifier to the NavigationView's content. That crashes the playground if the modal is dismissed and presented again while showing StackSecondView. I filed FB6687850.
Update
This tweet from Ryan Ashcraft showed me a workaround that gets this second attempt working. It wraps the Modal's content in a Group, and applies the id modifier to the Group's content:
import SwiftUI
struct PresenterExample : View {
var body: some View {
VStack {
Button("Present") {
self.presentModal()
}.presentation(modal)
}
}
#State private var shouldPresent = false
#State private var presentationCount = 0
private func presentModal() {
presentationCount += 1
shouldPresent = true
}
private var modal: Modal? {
guard shouldPresent else { return nil }
return Modal(Group { StackFirstView().id(presentationCount) }, onDismiss: { self.shouldPresent = false })
}
}
struct StackFirstView : View {
var body: some View {
NavigationView {
NavigationLink(destination: StackSecondView()) {
Text("Go to view 2")
}.navigationBarTitle("StackFirstView")
}
}
}
struct StackSecondView : View {
var body: some View {
Text("View 2")
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(rootView: PresenterExample())
This revised second try successfully resets the state of the Modal on each presentation. Note that the id must be applied to the content of the Group, not to the Group itself, to work around the SwiftUI bug.
Third Try: Success
I modified the second try so that, instead of using the id modifier, it wraps the StackFirstView inside a ZStack when presentationCount is an odd number.
import SwiftUI
struct PresenterExample : View {
var body: some View {
VStack {
Button("Present") {
self.presentModal()
}.presentation(modal)
}
}
#State private var shouldPresent = false
#State private var presentationCount = 0
private func presentModal() {
presentationCount += 1
shouldPresent = true
}
private var modal: Modal? {
guard shouldPresent else { return nil }
if presentationCount.isMultiple(of: 2) {
return Modal(presentationContent, onDismiss: { self.shouldPresent = false })
} else {
return Modal(ZStack { presentationContent }, onDismiss: { self.shouldPresent = false })
}
}
private var presentationContent: some View {
StackFirstView()
}
}
struct StackFirstView : View {
var body: some View {
NavigationView {
NavigationLink(destination: StackSecondView()) {
Text("Go to view 2")
}.navigationBarTitle("StackFirstView")
}
}
}
struct StackSecondView : View {
var body: some View {
Text("View 2")
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(rootView: PresenterExample())
This works. I guess SwiftUI sees that the modal's content is a different type each time (StackFirstView vs. ZStack<StackFirstView>) and that is sufficient to convince it that these are unrelated views, so it throws away the prior presented view instead of reusing it.