How can I use multiple fullScreenCover in IOS14 - swiftui

I want to present the two destinations view in full screen mode from a single view.
Below is a sample of my code. Seem that the function only works for single presentation, if I have a second fullScreenCover defined, the first fullScreenCover didn't work properly.Is that any workaround at this moment?
import SwiftUI
struct TesFullScreen: View {
init(game : Int){
print(game)
}
var body: some View {
Text("Full Screen")
}
}
ContentView
import SwiftUI
struct ContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
.fullScreenCover(isPresented: self.$showFullScreen1){
TesFullScreen(game: 1)
}
.fullScreenCover(isPresented: self.$showFullScreen2){
TesFullScreen(game: 2)
}
}
}

Not always the accepted answer works (for example if you have a ScrollView with subviews (cells in former days) which holds the buttons, that set the navigational flags).
But I found out, that you also can add the fullScreen-modifier onto an EmptyView. This code worked for me:
// IMPORTANT: Has to be within a container (e.g. VStack, HStack, ZStack, ...)
if myNavigation.flag1 || myNavigation.flag2 {
EmptyView().fullScreenCover(isPresented: $myNavigation.flag1)
{ MailComposer() }
EmptyView().fullScreenCover(isPresented: $myNavigation.flag2)
{ RatingStore() }
}

Usually some same modifier added one after another is ignored. So the simplest fix is to attach them to different views, like
struct FullSContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
.fullScreenCover(isPresented: self.$showFullScreen1){
Text("TesFullScreen(game: 1)")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
.fullScreenCover(isPresented: self.$showFullScreen2){
Text("TesFullScreen(game: 2)")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
}
}
Alternate is to have one .fullScreenCover(item:... modifier and show inside different views depending on input item.

The only thing that worked for me was the answer in this link:
https://forums.swift.org/t/multiple-sheet-view-modifiers-on-the-same-view/35267
Using the EmptyView method or other solutions always broke a transition animation on one of the two presentations. Either transitioning to or from that view and depending on what order I chose them.
Using the approach by Lantua in the link which is using the item argument instead of isPresented worked in all cases:
enum SheetChoice: Hashable, Identifiable {
case a, b
var id: SheetChoice { self }
}
struct ContentView: View {
#State var sheetState: SheetChoice?
var body: some View {
VStack {
...
}
.sheet(item: $sheetState) { item in
if item == .a {
Text("A")
} else {
Text("B")
}
}
}
}
The sheetState needs to be optional for it to work.

Related

SwiftUI Navigation Controller stuttering with two Navigationlinks per List row

I am trying to create two NavigationLinks in a repeating List. Each has a separate destination. The code all works fine until I imbed the call to the root view in a List/ForEach loop. At which point the navigation becomes very strange.
Try to click on either link and then click the back indicator at the top. It will go to one NavigationLink, and then the other. Sometimes in a different order, and sometimes it will auto-return from one of the links, and othertimes it won't open the second detail view until you return from the first detail view. It does this both in Preview, as well as if you build and run the application.
I have distilled down the code to the most basic below. If you comment the 2 lines as indicated in ContentView, you will then see correct behavior.
I am running Catalina 10.15.5, xCode 11.6, with the application target of IOS 13.6.
How can I modify the code, so that it will work with the List/ForEach loop?
import SwiftUI
struct DetailView1: View {
var body: some View {
HStack {
Text("Here is Detail View 1." )}
.foregroundColor(.green)
}
}
struct DetailView2: View {
var body: some View {
HStack {
Text( "Here is Detail View 2.") }
.foregroundColor(.red)
}
}
struct RootView: View {
var body: some View {
HStack {
VStack {
NavigationLink(destination: DetailView1())
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}.buttonStyle(PlainButtonStyle())
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
NavigationLink(destination: DetailView2())
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
// Comment the following line for correct behavior
List { ForEach(0..<3) {_ in
RootView()
// Comment the following line for correct behavior
} }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
.navigationBarTitle("Strange Behavior")
}
}
}
In your case both navigation links are activated at once user tap a row, to avoid this below is possible approach
Tested with Xcode 12 / iOS 14
The idea is to have one link which is activated programmatically and destination is selected dynamically depending on which button is clicked
struct RootView: View {
#State private var isFirst = false
#State private var isActive = false
var body: some View {
HStack {
VStack {
Button(action: {
self.isFirst = true
self.isActive = true
})
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
Button(action: {
self.isFirst = false
self.isActive = true
})
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
.background(
NavigationLink(destination: self.destination(), isActive: $isActive) { EmptyView() }
)
}
.buttonStyle(PlainButtonStyle())
}
#ViewBuilder
private func destination() -> some View {
if isFirst {
DetailView1()
} else {
DetailView2()
}
}
}

Updating EnvironmentObject pushed me back

I have two views ListView and DetailView
ListView:
#EnvironmentObject var userData: UserData
var body: some View {
VStack {
ForEach(userData.packs) { pack in
if pack.added {
NavigationLink(destination: DetailView(packIndex: self.userData.packs.firstIndex(where: { $0.id == pack.id })!)) {
MyRowViewDoesntMatter(pack: pack)
}
}
}
}
.padding(.horizontal)
}
DetailView:
#EnvironmentObject var userData: UserData
var packIndex: Int
VStack {
List {
VStack {
.... some Vies ... doesn't matter
.navigationBarItems(trailing:
THE PROBLEM IS HERE (BELOW)
Button(action: {
self.userData.packs[self.packIndex].added.toggle()
}) {
Image(systemName: self.userData.packs[self.packIndex].added ? "plus.circle.fill" : "plus.circle")
}
...
The problem is when I click on button in the navigationBarItems in DetailView. The "added" property of the "#EnvironmentObject var userData: UserData" is updated and the user's screen is going back (to the RowView). I fond out that the problem with EnvironmentObject, because the data is updated and View tries to rerender (?) that is why it pushes me back?
How to fix it? I want to stay at the DetailView screen after clicking the button.
P.S. I need to use EnvironmentObject type because then when I go back I need to see the results.
Thank you very much!
Here is possible approach (by introducing some kind of selection). As NavigationView does not allow to remove link from stack (as identifier of stacked navigation), probably also worth considering separate view model for DetailView to be applied into common container on finish editing.
Tested with Xcode 11.4 / iOS 13.4.
Some replication of your code, used for testing:
struct ListView: View {
#EnvironmentObject var userData: PushBackUserData
#State private var selectedPack: Pack? = nil
var body: some View {
NavigationView {
VStack {
ForEach(Array(userData.packs.enumerated()), id: \.element.id) { i, pack in
NavigationLink("Pack \(pack.id)", destination:
DetailView(pack: self.$selectedPack)
.onAppear {
self.selectedPack = pack
}
.onDisappear {
self.userData.packs[i].added = self.selectedPack?.added ?? false
}
).isHidden(!pack.added)
}
}
.padding(.horizontal)
}
}
}
struct DetailView: View {
#Binding var pack: Pack?
var body: some View {
VStack {
List {
VStack {
Text("Pack \(pack?.id ?? "<none>")")
}
}
.navigationBarItems(trailing:
Button(action: {
self.pack?.added.toggle()
}) {
Image(systemName: pack?.added ?? false ? "plus.circle.fill" : "plus.circle")
}
)
}
}
}
just convenient helper extension
extension View {
func isHidden(_ hidden: Bool) -> some View {
Group {
if hidden { self.hidden() }
else { self }
}
}
}

Is it possible for a NavigationLink to perform an action in addition to navigating to another view?

I'm trying to create a button that not only navigates to another view, but also run a function at the same time. I tried embedding both a NavigationLink and a Button into a Stack, but I'm only able to click on the Button.
ZStack {
NavigationLink(destination: TradeView(trade: trade)) {
TradeButton()
}
Button(action: {
print("Hello world!") //this is the only thing that runs
}) {
TradeButton()
}
}
You can use .simultaneousGesture to do that. The NavigationLink will navigate and at the same time perform an action exactly like you want:
NavigationLink(destination: TradeView(trade: trade)) {
Text("Trade View Link")
}.simultaneousGesture(TapGesture().onEnded{
print("Hello world!")
})
You can use NavigationLink(destination:isActive:label:). Use the setter on the binding to know when the link is tapped. I've noticed that the NavigationLink could be tapped outside of the content area, and this approach captures those taps as well.
struct Sidebar: View {
#State var isTapped = false
var body: some View {
NavigationLink(destination: ViewToPresent(),
isActive: Binding<Bool>(get: { isTapped },
set: { isTapped = $0; print("Tapped") }),
label: { Text("Link") })
}
}
struct ViewToPresent: View {
var body: some View {
print("View Presented")
return Text("View Presented")
}
}
The only thing I notice is that setter fires three times, one of which is after it's presented. Here's the output:
Tapped
Tapped
View Presented
Tapped
NavigationLink + isActive + onChange(of:)
// part 1
#State private var isPushed = false
// part 2
NavigationLink(destination: EmptyView(), isActive: $isPushed, label: {
Text("")
})
// part 3
.onChange(of: isPushed) { (newValue) in
if newValue {
// do what you want
}
}
This works for me atm:
#State private var isActive = false
NavigationLink(destination: MyView(), isActive: $isActive) {
Button {
// run your code
// then set
isActive = true
} label: {
Text("My Link")
}
}
Use NavigationLink(_:destination:tag:selection:) initializer and pass your model's property as a selection parameter. Because it is a two-way binding, you can define didset observer for this property, and call your function there.
struct ContentView: View {
#EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationView {
List(0 ..< 10, id: \.self) { row in
NavigationLink(destination: DetailView(id: row),
tag: row,
selection: self.$navigationModel.linkSelection) {
Text("Link \(row)")
}
}
}
}
}
struct DetailView: View {
var id: Int;
var body: some View {
Text("DetailView\(id)")
}
}
class NavigationModel: ObservableObject {
#Published var linkSelection: Int? = nil {
didSet {
if let linkSelection = linkSelection {
// action
print("selected: \(String(describing: linkSelection))")
}
}
}
}
It this example you need to pass in your model to ContentView as an environment object:
ContentView().environmentObject(NavigationModel())
in the SceneDelegate and SwiftUI Previews.
The model conforms to ObservableObject protocol and the property must have a #Published attribute.
(it works within a List)
I also just used:
NavigationLink(destination: View()....) {
Text("Demo")
}.task { do your stuff here }
iOS 15.3 deployment target.

Remove the text from back button in SwiftUI

In short, I want to do this, but with SwiftUI.
(Home should be removed)
So far, I have not found a way to access the NavigationBarButton directly, and have managed to find the following that appears to be the only way I can find to date for modifying the button:
struct MyList: View {
var body: some View {
Text("MyList")
.navigationBarTitle(Text(verbatim: "MyList"), displayMode: .inline)
.navigationBarItems(leading: Text("<"))
}
}
However, I lose the default return image and get an ugly < instead.
You need to set the title of the view that the back button will pop to:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("push view")
}
}.navigationBarTitle("", displayMode: .inline)
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail View")
}
}
Alternatively, to conditionally set or unset the title of the source view, depending on the presentation status you can use the code below.
Beware that the isActive parameter has a bug, but that will most likely be solved soon. Here's a reference to the bug mentioned SwiftUI: NavigationDestinationLink deprecated
struct ContentView: View {
#State private var active: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView(), isActive: $active) {
Text("push view")
}
}.navigationBarTitle(!active ? "View Title" : "", displayMode: .inline)
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail View")
}
}
Works on iOS 16
Since you can update NavigationItem inside the init of the View. You can solve this in 2 steps:
Get visible View Controller.
// Get Visible ViewController
extension UIApplication {
static var visibleVC: UIViewController? {
var currentVC = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController
while let presentedVC = currentVC?.presentedViewController {
if let navVC = (presentedVC as? UINavigationController)?.viewControllers.last {
currentVC = navVC
} else if let tabVC = (presentedVC as? UITabBarController)?.selectedViewController {
currentVC = tabVC
} else {
currentVC = presentedVC
}
}
return currentVC
}
}
Update NavigationItem inside init of the View.
struct YourView: View {
init(hideBackLabel: Bool = true) {
if hideBackLabel {
// iOS 14+
UIApplication.visibleVC?.navigationItem.backButtonDisplayMode = .minimal
// iOS 13-
let button = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
UIApplication.visibleVC?.navigationItem.backBarButtonItem = button
}
}
}

SwiftUI animations differ if called from within a VStack or NavigationItem

I've got a very simple VStack, based directly off of one of Paul Hudson's excellent SwiftUI samples. There are two lines of Text, one hidden. There's a method to toggle an #State var which controls the hidden Text.
If I call that function from within the VStack, it animates properly. If I call it from a navigationBarItems, it loses the animation. Am I missing something about how views are composed?
struct ContentView: View {
#State var showDetails = false
func toggleDetails() { withAnimation { self.showDetails.toggle() } }
var body: some View {
NavigationView() {
VStack {
Button(action: { self.toggleDetails() }) { Text("Tap to show details") }
if showDetails { Text("Details go here.") }
}
.navigationBarTitle(Text("Nav Bar"))
.navigationBarItems(trailing:
Button(action: { self.toggleDetails() }) {
Text("Details")
})
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Beta 5 Update
It seems beta 5 fixed this problem. Workaround no longer needed.
Workaround for beta 4 and previous versions
I think the reason it does not work, is because you are calling withAnimation from a different branch of the view tree. The "Details" button and the views that need to be animated are on different branches of the hierarchy. I am just guessing, but it seems to be supported by the workaround I posted here.
If instead of using explicit animations (i.e., withAnimation), you use implicit animations on both the VStack and the Text, it works:
struct ContentView: View {
#State var showDetails = false
func toggleDetails() { self.showDetails.toggle() }
var body: some View {
NavigationView() {
VStack {
Button(action: { self.toggleDetails() }) { Text("Tap to show details") }
if showDetails {
Text("Details go here.").animation(.basic())
}
}
.animation(.basic())
.navigationBarTitle(Text("Nav Bar"))
.navigationBarItems(trailing:
Button(action: {
self.toggleDetails()
}) { Text("Details") })
}
}
}