Present multiple modal sheet with SwiftUI [duplicate] - ios

This question already has answers here:
Multiple sheet(isPresented:) doesn't work in SwiftUI
(21 answers)
Closed 2 years ago.
I am trying to attach multiple modal views to a NavigationView using .sheet presentation.
I tried chaining the .sheet together just to discover that only the last one can be triggered to display when bind variable is changed
Is there a way to do this for multiple modals?
var body: some View {
NavigationView {
ZStack(alignment: .leading) {
MainView()
}
// present profile page
.sheet(isPresented: self.$presentation.profile){
ProfilePage()
}
// present product page
.sheet(isPresented: self.$presentProduct) {
SingleProductView()
}
//present login
.sheet(isPresented: self.$showLogin) {
LoginView(showLogin:self.$showLogin)
}
//present cart
.sheet(isPresented: self.$showCart) {
CartView()
}
// set title
.navigationBarTitle("Title", displayMode: .inline)
// set items
.navigationBarItems(leading: (
NavigationBarLeadingItems()
),trailing: (
NavigationBarTrailingItems()
)
)

Calling the same method multiple times on the same Element in SwiftUI will always result in only the last one being applied.
Text("Some Nice Text")
.forgroundColor(.red)
.forgroundColor(.blue)
This will always result in the Text to be displayed in blue and not in red, even thou you set its color to red. Same gos for you .sheet call. The last .sheet call will kinda override its predecesors.
There are two possible solutions:
you place the .sheet call on different Views.
ViewOne().sheet(...) { ... }
ViewSecond().sheet(...) { ... }
you change the content of you .sheet call dynamically:
View()
.sheet(...) {
if someState {
return SheetViewOne()
else {
return SheetViewSecond()
}
}
}

Related

SwiftUI searchable on NavigationView always shown, hides only on scroll

I'm writing a fairly simple SwiftUI app about movies and I have this issue where the new .searchable modifier on NavigationView is always being shown, whereas it should be hidden, unless you pull down on the List.
It hides it correctly if I scroll a bit up, and if I scroll down it also hides it correctly, but other than that, it's always being shown. See gif for clarification. (basically it should behave the same as in Messages app)
https://imgur.com/R2rsqzh
My code for using the searchable is fairly simple:
var body: some View {
NavigationView {
List {
ForEach(/*** movie stuff ***/) { movie in
///// row here
}
}
.listStyle(GroupedListStyle())
.onAppear {
// load movies
}
.navigationTitle("Discover")
.searchable(text: $moviesRepository.searchText, placement: .toolbar, prompt: "Search...")
}
}
}
So, after adding a progress view above the list view, it suddenly started working the way I want it to. The code now is minimally changed and looks like this.
var body: some View {
NavigationView {
VStack {
if /** we're doing search, I'm checking search text **/ {
ProgressView()
.padding()
}
if !movies.isEmpty {
List {
ForEach(/** movies list **/) { movie in
// movie row here
}
}
.listStyle(GroupedListStyle())
.navigationTitle("Discover")
.searchable(text: $moviesRepository.searchText, placement: .toolbar,
prompt: Text("Search...")
.foregroundColor(.secondary)
)
} else {
ProgressView()
.navigationTitle("Discover")
}
}
}
.onAppear {
// load movies
}
}
And basically after adding the progress views, it started working the way I described it in my OP and the way it worked for ChrisR

Three Column Navigation View does not update until you click to show the sidebar on iPad

Given this View:
struct ContentView: View {
#State var link1Active: Bool = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: Text("Link1 Destination"), isActive: $link1Active) {
Text("Click me 1")
}
NavigationLink(destination: Text("Link2 Destination")) {
Text("Click me 2")
}
}
Text("View 1")
Text("View 2")
}
.onAppear {
link1Active = true
print("in here!")
}
}
}
When the app starts on iPad in landscape mode, I see two columns "View 1" and "View2".
I would expect to see "Link 1 Destination" in the left column, but its not shown.
However when I click the toggle sidebar button the "View 1" is replaced.
Is this an issue with the way the views are setup in swiftui?
The problem is simply that the NavigationLink is not in existence when the .onAppear is called. .onAppear sets link1Active to true (I verified this by printing the value in your print() statement in .onAppear), but nothing is using it until the main List view is opened and on screen. I really don't think this is a true "fix", but the only way you are going to show the destination is simple with a conditional like:
(!link1Active ? Text("View 1") : Text("Link1 Destination"))
Since this is a minimal reproducible example, I am not sure of your use case, but I think you need to rethink how you are showing your views.

SwiftUI NavigationView nested bar issue

I have a View with a list with navigationLinks, and show a list of items to DetailView, and DetailView can go to another View and so on. Therefore it will have a full navigation stack in here.
However, in our app, we have two ways to show this view, first, a presenting View to show this view. Second, by other navigationLink under another NavigationView to push to this view. In the first case, it is fine. however in second case, it will show nested navigation bar, which I don't really like it.
Is there any possible way to show the following view without any nested navigationBar, in the pushing and presenting(UIKit wording) way
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(destination: DetailView(item)) {
ItemRow(item)
}
}
.navigationBarTitle("Item list")
}
}
thanks a lot!
As Asperi says, you must separate the View into two Views. You have double NavigationBar because in the case where you already enter with the previous NavigationView at the top level of the View hierarchy, having another NavigatioView makes it double the NavigationBar.
You must do something like this in the View before this one:
var body: some View {
VSTack {
Text("Hello Word")
Button(action: {
if isShowWithPresent { // Some Bool
NavigationView {
SomeView()
}
} else {
AnotherView() // Some View without the NavigationView in the code
}
}) {
Text("Hit Me!")
}
}
}

Why is SwiftUI picker in form repositioning after navigation?

After clicking the picker it navigates to the select view. The item list is rendered too far from the top, but snaps up after the animation is finished. Why is this happening?
Demo: https://gfycat.com/idioticdizzyazurevase
I already created a minimal example to rule out navigation bar titles and buttons, form sections and other details:
import SwiftUI
struct NewProjectView: View {
#State var name = ""
var body: some View {
NavigationView {
Form {
Picker("Client", selection: $name) {
Text("Client 1")
Text("Client 2")
}
}
}
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
This happens in preview mode, simulator and on device (Xcode 11.2, iOS 13.2 in simulator, 13.3 beta 1 on device).
The obviously buggy behavior can be worked around when forcing the navigation view style to stacked:
NavigationView {
…
}.navigationViewStyle(StackNavigationViewStyle())
This is a solution to my problem, but I won‘t mark this as accepted answer (yet).
It seems to be a bug, even if it may be triggered by special circumstances.
My solution won‘t work if you need another navigation view style.
Additionally, it won‘t fix the horizontal repositioning mentioned by DogCoffee in the comments.
In my opinion, it has something to do with the navigation bar. In default (no mention of .navigationBarTitle extension), the navigation display mode is set to .automatic, this should be amend to .inline. I came across another post similar to this and use their solution to combine with yours, by using .navigationBarTitle("", displayMode: .inline) should help.
import SwiftUI
struct NewProjectView: View {
#State var name = ""
var body: some View {
NavigationView {
Form {
Picker("Client", selection: $name) {
Text("Client 1")
Text("Client 2")
}
}
.navigationBarTitle("", displayMode: .inline)
}
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
Until this bug is resolved another way to work around this issue while retaining the DoubleColumnNavigationViewStyle for iPads would be to conditionally set that style:
let navView = NavigationView {
…
}
if UIDevice.current.userInterfaceIdiom == .pad {
return AnyView(navView.navigationViewStyle(DoubleColumnNavigationViewStyle()))
} else {
return AnyView(navView.navigationViewStyle(StackNavigationViewStyle()))
}
Thanks for this thread everyone! Really helped me understand things more and get a hold of one of my problems. To share with others, I was having this problem to but I was also having this problem when I set a section to appear in a if/else statement set on a section with a toggle. When the toggle was activated it would shift the section header horizontally a few pixels.
The following is how I fixed it
Section(header: Text("Subject Identified").listRowInsets(EdgeInsets()).padding(.leading)) {
Picker(selection: $subIndex, label: Text("Test")) {
ForEach(0 ..< subIdentified.count) {
Text(self.subIdentified[$0]).tag($0)
}
}
.labelsHidden()
.pickerStyle(SegmentedPickerStyle())
I'm still having horizontal shift on my picker selection view and not sure how to fix. I created another thread to received input. Thanks again! SwiftUI Shift Picker Text Horizontal

SwiftUI: NavigationDestinationLink deprecated

After installing Xcode 11 beta 5 this morning, I noticed that NavigationDestinationLink was deprecated in favor of NavigationLink.
Also, that's what Apple says about it in the release notes:
NavigationDestinationLink and DynamicNavigationDestinationLink are deprecated; their functionality is now included in NavigationLink. (50630794)
The way I use NavigationDestinationLink is to programmatically push a new view into the stack via self.link.presented?.value = true. That functionality doesn't seem to be present in NavigationLink.
Any idea anyone?
I would rather not use NavigationDestinationLink anymore as it's deprecated...
Thank you!
UPDATE:
Actually, the NavigationDestinationLink way does not work anymore so I guess we have no way of pushing programmatically anymore?
UPDATE 2:
NavigationLink(destination: CustomView(), isActive: $isActive) {
return Text("")
}
This works but when you pass isActive to true, any state update will trigger this code and push over and over... Also, if you pass it back to false, it will pop the view.
Not only updates, if you set isActive to true, it will push the view (good) and if we press the back button, it will go back then immediately push again since it's still true.
Playing with onAppear was my hope but it's not called when going back to it...
I'm not sure how we're supposed to use this.
After spending some time with NavigationLink(destination:isActive), I am liking it a lot more than the old NavigationDestinationLink. The old view was a little confusing, while the new approach seems much more elegant. And once I figure out how to push without animations, it would make state restoration at application launch very easy.
There is one problem though, a big and ugly bug. :-(
Pushing a view programatically works fine, and popping it programatically does too. The problem starts when we use the BACK button in the pushed view which behaves oddly every other time. The first time the view is popped, the view pops and pushes again immediately. The second time around it works fine. Then the third time it starts all over again.
I have created a bug report (number here). I recommend you do the same and reference my number too, to help Apple group them together and get more attention to the problem.
I designed a workaround, that basically consists of replacing the default Back button, with our own:
class Model: ObservableObject {
#Published var pushed = false
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
VStack {
Button("Push") {
// view pushed programmatically
self.model.pushed = true
}
NavigationLink(destination: DetailView(), isActive: $model.pushed) { EmptyView() }
}
}
}
}
struct DetailView: View {
#EnvironmentObject var model: Model
var body: some View {
Button("Bring me Back (programatically)") {
// view popped programmatically
self.model.pushed = false
}
// workaround
.navigationBarBackButtonHidden(true) // not needed, but just in case
.navigationBarItems(leading: MyBackButton(label: "Back!") {
self.model.pushed = false
})
}
}
struct MyBackButton: View {
let label: String
let closure: () -> ()
var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}
To improve the workaround without replacing the back button with a custom one, you can use the code above :
NavigationLink(destination: Test().onAppear(perform: {
self.editPushed = nil
}), tag: 1, selection: self.$editPushed) {
Button(action: {
self.editPushed = 1
}) {
Image(systemName: "plus.app.fill")
.font(.title)
}
}
The onAppear block will erase the selection value preventing to display the detail view twice
You can also use NavigationLink(destination:tag:selection)
NavigationLink(destination: MyModal(), tag: 1, selection: $tag) {
EmptyView()
}
So programmatically you can set tag to 1 in order to push MyModal. This approach has the same behaviour as the one with the Bool binding variable, so when you pop the first time it pushes the view immediately, hopefully they'll fix it in next beta.
The only downside I see with this approach, compared to DynamicNavigationDestinationLink is that you need to provide a View to NavigationLink, even if you don't need one. Hopefully they'll find a cleaner way to allow us to push programmatically.
The solution is to create custom back button for your detail view and pop detail view manually.
.navigationBarItems(leading:
Button(action: {
self.showDetail = false
}) {
Image(systemName: "chevron.left").foregroundColor(.red)
.font(.system(size: 24, weight: .semibold))
Text("Back").foregroundColor(.red)
.font(.system(size: 19))
}
)
The method used in the selected answer has been deprecated again. Here's the solution copied from this answer in this post.
#State private var readyToNavigate : Bool = false
var body: some View {
NavigationStack {
VStack {
Button {
//Code here before changing the bool value
readyToNavigate = true
} label: {
Text("Navigate Button")
}
}
.navigationTitle("Navigation")
.navigationDestination(isPresented: $readyToNavigate) {
MyTargetView()
}
}
}

Resources