Showing Interstitial Ad from button that also toggles another view from sheet - swiftui

so i have this test app that consists of 2 views. The parent view has a button that toggles the second view (Bottom Modal Sheet). My goal is to show an AdMob interstitial ad when the button is clicked. But still have the second view when the ad is dismissed. Currently, the ad appears but when dismissing the ad, it goes back to the parent view without the second view toggled. Is it possible to perhaps stack the Ad view on top of the second view so when dismissing the ad, the user is already in the second view?
import GoogleMobileAds
import SwiftUI
import UIKit
struct ContentView: View {
#State var showingTest = false
#State var showingDisclaimer = false
//Ad
var interstitial: Interstitial
init() {
self.interstitial = Interstitial()
}
var body: some View {
// QuestionsView()
// NavigationView {
VStack {
Button(action: {
self.showingTest.toggle()
self.interstitial.showAd()
}) {
Text("take the test!")
.fontWeight(.bold)
.foregroundColor(Color.white)
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.gray)
.background(Color("raisinblack"))
.cornerRadius(10)
.font(.title)
.sheet(isPresented: $showingTest) {
QuestionsView()
}.padding()
.shadow(radius: 5)
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.navigationBarTitle("rice purity test.")
}
}
I'm new to swiftUI so I'm not sure how to go about making this work. I understand that the problem arises because I'm trying to present two views at the same time.
The Error:
Warning: Attempt to present <_TtGC7SwiftUIP10$1c95b7a6c22SheetHostingControllerVS_7AnyView_: 0x1056344f0> on <_TtGC7SwiftUI19UIHostingControllerV14RicePurityTest11ContentView_: 0x10560f280> whose view is not in the window hierarchy!
The Interstitial Class
final class Interstitial: NSObject, GADInterstitialDelegate {
var interstitial: GADInterstitial = GADInterstitial(adUnitID: "ca-app-pub-3940256099942544/4411468910")
override init() {
super.init()
self.LoadInterstitial()
}
func LoadInterstitial() {
let req = GADRequest()
self.interstitial.load(req)
self.interstitial.delegate = self
}
func showAd() {
if self.interstitial.isReady {
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
} else {
print("Not Ready")
}
}
func interstitialDidDismissScreen(_ ad: GADInterstitial) {
self.interstitial = GADInterstitial(adUnitID: "ca-app-pub-3940256099942544/4411468910")
self.LoadInterstitial()
}
/// Tells the delegate an ad request succeeded.
func interstitialDidReceiveAd(_ ad: GADInterstitial) {
print("interstitialDidReceiveAd")
}
/// Tells the delegate an ad request failed.
func interstitial(_ ad: GADInterstitial, didFailToReceiveAdWithError error: GADRequestError) {
print("interstitial:didFailToReceiveAdWithError: \(error.localizedDescription)")
}
/// Tells the delegate that an interstitial will be presented.
func interstitialWillPresentScreen(_ ad: GADInterstitial) {
print("interstitialWillPresentScreen")
}
/// Tells the delegate the interstitial is to be animated off the screen.
func interstitialWillDismissScreen(_ ad: GADInterstitial) {
print("interstitialWillDismissScreen")
//showingTest.toggle()
}
/// Tells the delegate that a user click will open another app
/// (such as the App Store), backgrounding the current app.
func interstitialWillLeaveApplication(_ ad: GADInterstitial) {
print("interstitialWillLeaveApplication")
}
}

func showAd() {
if self.interstitial.isReady {
let root = UIApplication.shared.windows.last?.rootViewController
// you can also use: UIApplication.shared.keyWindow.rootViewController
self.interstitial.present(fromRootViewController: root!)
} else {
print("Not Ready")
}
}

Related

How to make a custom UIView Appear/Dissapear in SwiftUI

I have a CameraView in my app that I'd like to bring up whenever a button is to be presssed. It's a custom view that looks like this
// The CameraView
struct Camera: View {
#StateObject var model = CameraViewModel()
#State var currentZoomFactor: CGFloat = 1.0
#Binding var showCameraView: Bool
// MARK: [main body starts here]
var body: some View {
GeometryReader { reader in
ZStack {
// This black background lies behind everything.
Color.black.edgesIgnoringSafeArea(.all)
CameraViewfinder(session: model.session)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.scaledToFill()
.ignoresSafeArea()
.frame(width: reader.size.width,height: reader.size.height )
// Buttons and controls on top of the CameraViewfinder
VStack {
HStack {
Button {
//
} label: {
Image(systemName: "xmark")
.resizable()
.frame(width: 20, height: 20)
.tint(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
Spacer()
flashButton
}
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
Spacer()
flipCameraButton
}
.padding([.horizontal, .bottom], 20)
.frame(maxHeight: .infinity, alignment: .bottom)
}
} // [ZStack Ends Here]
} // [Geometry Reader Ends here]
} // [Main Body Ends here]
// More view component code goes here but I've excluded it all for brevity (they don't add anything substantial to the question being asked.
} // [End of CameraView]
It contains a CameraViewfinder View which conforms to the UIViewRepresentable Protocol:
struct CameraViewfinder: UIViewRepresentable {
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
let session: AVCaptureSession
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.backgroundColor = .black
view.videoPreviewLayer.cornerRadius = 0
view.videoPreviewLayer.session = session
view.videoPreviewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
}
}
I wish to add a binding property to this camera view that allows me to toggle this view in and out of my screen like any other social media app would allow. Here's an example
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView(showCamera: $showCamera)
}
}
I understand that the code to achieve this must be written inside the updateUIView() method. Now, although I'm quite familiar with SwiftUI, I'm relatively inexperienced with UIKit, so any help on this and any helpful resources that could help me better code situations similar to this would be greatly appreciated.
Thank you.
EDIT: Made it clear that the first block of code is my CameraView.
EDIT2: Added Example of how I'd like to use the CameraView in my App.
Judging by the way you would like to use it in the app, the issue seems to not be with the CameraViewFinder but rather with the way in which you want to present it.
A proper SwiftUI way to achieve this would be to use a sheet like this:
#State var showCamera: Bool = false
var body: some View {
mainTabView
.sheet(isPresented: $showCamera) {
CameraView()
.interactiveDismissDisabled() // Disables swipe to dismiss
}
}
If you don't want to use the sheet presentation and would like to cover the whole screen instead, then you should use the .fullScreenCover() modifier like this.
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView()
.fullScreenCover(isPresented: $showCamera)
}
}
Either way you would need to somehow pass the state to your CameraView to allow the presented screen to set the state to false and therefore dismiss itself, e.g. with a button press.

LazyVStack in ScrollView has jittery auto scroll

I have a chat app, where whenever a chat room is opened, I need the view to scroll to the bottom as soon as the messages are fetched.
The thing is that although it does scroll perfectly when a new message is received or sent (see ViewModel down below), it is very jittery when I tell it to scroll right after the first batch of messages is fetched, which happens once as soon as the view appears.
After a lot of trial and error, I realized that if I add a small delay to the scroll, it'll improve but not completely! It is like it's trying to scroll to the very bottom, but it'll fail just for a few inches. I also realized that if I add a bigger delay, like 2 seconds, it'll scroll just fine.
Here's the messages list view:
struct MessagesView: View {
#StateObject private var viewModel = ViewModel()
// -----------------------
let currentChatRoom: ChatRoom
// -----------------------
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
VStack {
ScrollView {
ScrollViewReader { proxy in
LazyVStack {
ForEach(viewModel.messages) { message in
MessageView(message: message)
.id(message.id)
.onTapGesture {
viewModel.shouldDismissKeyboard = true
}
}
}
.onChange(of: viewModel.shouldScrollToMessageId) { messageId in
if let messageId = messageId, !messageId.isEmpty {
proxy.scrollTo(messageId, anchor: .bottom)
}
}
}
}
VStack(alignment: .leading) {
if chatEnvironment.isOtherUserTyping {
TypingIndicationView()
}
BottomView()
.padding(.bottom, 4)
}
}
}
.onAppear {
viewModel.setUp(currentChatRoom: currentChatRoom)
}
}
}
As you can see, it’s viewModel.shouldScrollToMessageId that’s responsible for "auto-scrolling" to the last message.
Here’s MessageView:
fileprivate struct MessageView: View {
let message: Message
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 1) {
Text(message.user.isCurrentUser == true ? "You" : "\(message.user.username)")
.foregroundColor(message.user.isCurrentUser == true ? .customGreen : .customBlue)
.multilineTextAlignment(.leading)
.font(.default(size: 16))
.padding(.bottom, 1)
if let imageURL = message.postSource?.imageURL, !imageURL.isEmpty {
VStack(alignment: .leading) {
WebImage(url: .init(string: imageURL))
.resizable()
.indicator(.activity)
.scaledToFill()
.frame(width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.width / 1.45)
.cornerRadius(25)
}
}
Text(message.text)
.foregroundColor(.white)
.multilineTextAlignment(.leading)
.font(.default(size: 16))
}
Spacer()
}
.padding(.bottom, 8)
.padding(.horizontal)
.background(
Color.black
)
}
}
Here’s the ViewModel:
class ViewModel: ObservableObject {
#Published var messages = [Message]()
#Published var text = ""
#Published var shouldScrollToMessageId: String?
#Published var currentChatRoom: ChatRoom?
// -----------------------------
private var isInitialized = false
// -----------------------------
func setUp(currentChatRoom: ChatRoom) {
guard !isInitialized else { return }
isInitialized.toggle()
// -----------------------------
self.currentChatRoom = currentChatRoom
// -----------------------------
getFirstBatchOfMessages(chatRoom: chatRoom)
subscribeToNewMessages()
}
private func getFirstBatchOfMessages(chatRoom: ChatRoom) {
messagesService.getMessages(chatRoomId: chatRoom.id) { [weak self] messages in
DispatchQueue.main.async {
self?.messages = messages
}
self?.scrollToBottom(delay: 0.15)
}
}
private func subscribeToNewMessages() {
...
if !newMessages.isEmpty {
self?.scrollToBottom(delay: 0)
}
}
func scrollToBottom(delay: TimeInterval) {
DispatchQueue.main.async {
self.shouldScrollToMessageId = self.messages.last?.id
}
}
func sendMessage() {
...
scrollToBottom(delay: 0)
}
}
Here, scrollToBottom is responsible for notifying the MessagesView that shouldScrollToMessageId's value changed and that it should scroll to the last message.
Any help will be much appreciated!!!
I am also writing an application with a chat on SwiftUI and I also have a lot of headaches with a bunch of ScrollView and LazyVStack.
In my case, I load messages from the CoreData and display them in a LazyVStack, so in my case, scrolling to the last message does not work, it seems to me simply because a bottom last view did not render, because rendering starts from the top.
Therefore, I came up with a solution with placing an invisible view at the bottom and assigned it a static ID, in my case -1:
VStack(spacing: 0) {
LazyVStack(spacing: 0) {
ForEach(messages) { message in
MessageRowView(viewWidth: wholeViewProxy.size.width, message: message)
.equatable()
}
}
Color.clear.id(-1)
.padding(.bottom, inputViewHeight)
}
I call scroll to this view:
.onAppear {
scrollTo(messageID: -1, animation: nil, scrollReader: scrollReader)
}
And it works... but sometimes...
Sometimes it works correctly, sometimes it stops without scrolling a couple of screens to the end. Looks like LazyVStack is rendering the next views after the scrollTo has finished its work. Also, if add scrolling with some delay it may works better, but not always perfect.
I hope this can be a little helpful and if you find a stable solution I will be very happy if you share :)

SwiftUI - Allow a List item to have both cell tap and button tap

I am trying to have a List that each item supports both cell tap and button tap
Currently, I have the following code, but for some reason, when I tap the hello button in each cell, although I do receive the action, but it also triggers the highlight from the background button, when i m tapping the hello button, I only want the highlight on the hello button, not on the whole cell
I searched everywhere can't really find an answer
Thanks in advance!
private enum MenuItem: String, Identifiable, CaseIterable {
var id: String {
self.rawValue
}
case option1
case option2
case option3
case option4
}
private struct MainView: View {
let onSelect: (MenuItem) -> Void
var body: some View {
VStack(spacing: 0) {
Text("Demo")
.padding(16)
List {
Section("hello") {
ForEach(MenuItem.allCases) { item in
ZStack {
Button("") {
print("yes", item)
}
VStack(alignment: .leading, spacing: 5) {
Text(item.rawValue).font(.title2)
Text("something here").font(.body)
Button("Hello") {
print("no", item)
}.buttonStyle(BorderedButtonStyle())
}
.contentShape(Rectangle())
.padding(.top, 10)
.padding(.bottom, 10)
}
}
}
}.listStyle(.plain)
}.onAppear {
UITableViewCell.appearance().selectedBackgroundView = {
let view = UIView()
view.backgroundColor = .blue
return view
}()
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(onSelect: { _ in })
}
}
Basically we need a custom button style in this situation (because automatic one is handled by List and gives observed behavior).
Here is main part. Tested with Xcode 13.4 / iOS 15.5
Button("") {
print("yes", item)
}
.buttonStyle(MyStyle()) // << here !!
// ...
struct MyStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.background( // highlight on row tap
configuration.isPressed ? Color.secondary : Color.clear
)
}
}
Complete test code on GitHub

SwiftUI navigation decision based on a sheet view presenting 2 choice

I am presenting a "wizard" that will be detecting a BLE device and then if it is the correct one the last view will ask if we want to register or skip.
Edit:{
the view order is: MainView presenting in fullScreenCover a first info view informing on how to detect the BLE device then this one pushes a second view with some info on the nearest BLE device and it is in this view that we have the fork where I am presenting a sheet to ask if the user wants to continue and register the BLE device or skip.
So MAIN > INFOView -> BLE detection (> Register or skip ? RegisterView : Destack to main)
}
I have that last view come up as a sheet it has 2 buttons, the first one as mentioned says "Register" and the other one says "skip". If the user presses the register then we dismiss the sheet and navigate to a view that is gathering personal info to register the BLE device. on the other hand, if the user chooses to skip then the wizard need to de-stack back over to the main view.
Normally in UIKit I would just have a delegate inform me of the choice then if skip was selected. I would call pop to root view controller, otherwise, if the register option was selected I would dismiss the sheet view and then navigate to one more final view and get the user registered.
In SwiftUI I do not know how to deal with that navigation fork. I tried using PassthroughSubject but then I have to set the PassthroughSubject var as a state var and in the end, I just did not get the call back from sending in the selection.
Tried binding then Was hoping to make an onReceive but then it is asking for a publisher and that felt wrong to create a publisher just for that.
I am wondering g what is the best way do take care of this in. swiftUI ?
edit:
this is the code (updated with the replay from #Predrag Samardzic) for the view that shows the info on the BLE device (smart bike) and will push at first a request to know if the user wants to register or not, then if yes push that registration screen if not dismiss the entire stack.
struct A18BikeDiscoveryView: View {
#EnvironmentObject var bleManager: ArgonBLEManager
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack{
NavigationLink(
destination: A18RegistrationQuestionairy(QuestionairyViewModel()),
isActive: $isRegistrationPresented
) {
EmptyView()
}
A18ImageTextBanner(text: NSLocalizedString("bike_discovery_view_title", comment: ""))
.padding(.bottom, 35)
.navigationBarBackButtonHidden(true)
if let value = bleManager.model?.bikeInfo?.bikeModel{
Text(value)
.fontWeight(.bold)
.scaledFont(.largeTitle)
}
Image("subitoBike")
.resizable()
.frame(minWidth: 0334, idealWidth: 334, maxWidth: .infinity, minHeight: 223, idealHeight: 223, maxHeight: .infinity, alignment: .center)
.aspectRatio(contentMode: .fit)
.padding(.bottom, 10)
Divider()
VStack(alignment: .leading){
HStack{
Text("bike_discovery_view_year_created")
if let v = bleManager.model?.bikeInfo?.year{
Text(v)
}
}
HStack{
Text("bike_discovery_view_model_size")
Text("\(getSizeFromSerial())")
}
HStack{
Text("bike_discovery_view_bike_serial_number")
if let v = bleManager.model?.bikeInfo?.bikeSerialNumber {
Text(v)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 66, alignment: .leading)
.padding(.horizontal, 40)
Divider()
.padding(.bottom, 30)
Button(action: {
isSheetPresented = true
}, label: {
Text("bike_discovery_view_bike_pairing_button_title")
.fontWeight(.bold)
.foregroundColor(.white)
})
.buttonStyle(A18RoundButtonStyle(bgColor: .red))
.padding(.horizontal)
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
A18BikeParingSelection(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
Button(action: {
bleManager.disconect()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("bike_discovery_view_bike_pairing_cancel_button_title")
.fontWeight(.bold)
.foregroundColor(Color("grey55"))
})
.padding()
Spacer()
}
.navigationBarColor(backgroundColor: .white, tintColor: .black)
.navigationBarTitleDisplayMode(.inline)
}
func getSizeFromSerial() -> String {
if let serial = bleManager.model?.bikeInfo?.bikeSerialNumber {
if serial.contains("XXS"){
return "XXS"
}else if serial.contains("XSM") {
return "XS"
}else if serial.contains("SML"){
return "S"
}else if serial.contains("MED"){
return "M"
}else if serial.contains("LAR"){
return "L"
}
}
return "N/A"
}
}
This is one possible solution - using CurrentValueSubject in order to trigger dismiss and keep info about the choice made on the presented screen. Then, if registration is needed, you trigger it when sheet is dismissed.
struct MainView: View {
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack {
// this part is if you want to push registration screen, you will need to have MainView inside NavigationView for it
NavigationLink(
destination: RegistrationView(),
isActive: $isRegistrationPresented
) {
EmptyView()
}
// ----------------------------------------------------
Button {
isSheetPresented = true
} label: {
Text("Present sheet")
}
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
ChoiceView(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
// this part is if you want to present registration screen as sheet
// .sheet(
// isPresented: $isRegistrationPresented,
// content: {
// RegistrationView()
// })
}
}
}
struct ChoiceView: View {
let shouldShowRegistration: CurrentValueSubject<Bool, Never>
var body: some View {
VStack{
Button {
shouldShowRegistration.send(false)
} label: {
Text("Dismiss")
}
Button {
shouldShowRegistration.send(true)
} label: {
Text("Register")
}
}
}
}
struct RegistrationView: View {
var body: some View {
Text("Registration")
}
}

SwiftUI List rows with INFO button

UIKit used to support TableView Cell that enabled a Blue info/disclosure button. The following was generated in SwiftUI, however getting the underlying functionality to work is proving a challenge for a beginner to SwiftUI.
Generated by the following code:
struct Session: Identifiable {
let date: Date
let dir: String
let instrument: String
let description: String
var id: Date { date }
}
final class SessionsData: ObservableObject {
#Published var sessions: [Session]
init() {
sessions = [Session(date: SessionsData.dateFromString(stringDate: "2016-04-14T10:44:00+0000"),dir:"Rhubarb", instrument:"LCproT", description: "brief Description"),
Session(date: SessionsData.dateFromString(stringDate: "2017-04-14T10:44:00+0001"),dir:"Custard", instrument:"LCproU", description: "briefer Description"),
Session(date: SessionsData.dateFromString(stringDate: "2018-04-14T10:44:00+0002"),dir:"Jelly", instrument:"LCproV", description: " Description")
]
}
static func dateFromString(stringDate: String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return dateFormatter.date(from:stringDate)!
}
}
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(session: session )
}
}
.navigationTitle("Session data")
}
// without this style modification we get all sorts of UIKit warnings
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SessionRow: View {
var session: Session
#State private var presentDescription = false
var body: some View {
HStack(alignment: .center){
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
Spacer()
// SessionGraph is a place holder for the Graph data.
NavigationLink(destination: SessionGraph()) {
// if this isn't an EmptyView then we get a disclosure indicator
EmptyView()
}
// Note: without setting the NavigationLink hidden
// width to 0 the List width is split 50/50 between the
// SessionRow and the NavigationLink. Making the NavigationLink
// width 0 means that SessionRow gets all the space. Howeveer
// NavigationLink still works
.hidden().frame(width: 0)
Button(action: { presentDescription = true
print("\(session.dir):\(presentDescription)")
}) {
Image(systemName: "info.circle")
}
.buttonStyle(BorderlessButtonStyle())
NavigationLink(destination: SessionDescription(),
isActive: $presentDescription) {
EmptyView()
}
.hidden().frame(width: 0)
}
.padding(.vertical, 4)
}
}
struct SessionGraph: View {
var body: some View {
Text("SessionGraph")
}
}
struct SessionDescription: View {
var body: some View {
Text("SessionDescription")
}
}
The issue comes in the behaviour of the NavigationLinks for the SessionGraph. Selecting the SessionGraph, which is the main body of the row, propagates to the SessionDescription! hence Views start flying about in an un-controlled manor.
I've seen several stated solutions to this issue, however none have worked using XCode 12.3 & iOS 14.3
Any ideas?
When you put a NavigationLink in the background of List row, the NavigationLink can still be activated on tap. Even with .buttonStyle(BorderlessButtonStyle()) (which looks like a bug to me).
A possible solution is to move all NavigationLinks outside the List and then activate them from inside the List row. For this we need #State variables holding the activation state. Then, we need to pass them to the subviews as #Binding and activate them on button tap.
Here is a possible example:
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
// create state variables for activating NavigationLinks
#State private var presentGraph: Session?
#State private var presentDescription: Session?
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(
session: session,
presentGraph: $presentGraph,
presentDescription: $presentDescription
)
}
}
.navigationTitle("Session data")
// put NavigationLinks outside the List
.background(
VStack {
presentGraphLink
presentDescriptionLink
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var presentGraphLink: some View {
// custom binding to activate a NavigationLink - basically when `presentGraph` is set
let binding = Binding<Bool>(
get: { presentGraph != nil },
set: { if !$0 { presentGraph = nil } }
)
// activate the `NavigationLink` when the `binding` is `true`
NavigationLink("", destination: SessionGraph(), isActive: binding)
}
#ViewBuilder
var presentDescriptionLink: some View {
let binding = Binding<Bool>(
get: { presentDescription != nil },
set: { if !$0 { presentDescription = nil } }
)
NavigationLink("", destination: SessionDescription(), isActive: binding)
}
}
struct SessionRow: View {
var session: Session
// pass variables as `#Binding`...
#Binding var presentGraph: Session?
#Binding var presentDescription: Session?
var body: some View {
HStack {
Button {
presentGraph = session // ...and activate them manually
} label: {
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button {
presentDescription = session
print("\(session.dir):\(presentDescription)")
} label: {
Image(systemName: "info.circle")
}
.buttonStyle(PlainButtonStyle())
}
.padding(.vertical, 4)
}
}