Not able go to other view from callback - ios

In the code below, why doesn't it take me to the AppView() page when onDismiss is called? I basically have a network call in LinkController that is logging someone in and then I need to progress to a different view once finished.
struct ContentView: View {
#State private var showLink = false
#State private var publicToken: String?
#State private var metadata: [String : Any]?
var body: some View {
Button(action: {
self.showLink = true
}) { Text("Add Account") }
.sheet(isPresented: self.$showLink,
onDismiss: {
// why doesn't this work
AppView(publicToken: self.publicToken!)
}, content: {
LinkController(publicToken: self.$publicToken, metadata: self.$metadata)
})
}
}

var body: some View {
Button(action: {
self.showLink = true
}) { Text("Add Account") }
.sheet(isPresented: self.$showLink,
onDismiss: {
if self.publicToken != nil { self.goHome() }
}, content: {
LinkController(publicToken: self.$publicToken, metadata: self.$metadata)
})
}
func goHome() {
if let window = UIApplication.shared.windows.first {
window.rootViewController = UIHostingController(rootView: AppView(publicToken: publicToken!))
window.makeKeyAndVisible()
}
}

Related

Swiftui view doesn't refresh when navigated to from a different view

I have, what is probably, a beginner question here. I'm hoping there is something simple I'm missing or I have done wrong.
I essentially have a view which holds a struct containing an array of id strings. I then have a #FirestoreQuery which accesses a collection which holds objects with these id's. My view then displays a list with two sections. One for the id's in the original struct, and one for the remaining ones in the collection which don't appear in the array.
Each listitem is a separate view which displays the details of that item and also includes a button. When this button is pressed it adds/removes that object from the parent list and the view should update to show that object in the opposite section of the list from before.
My issue is that this works fine in the 'preview' in xcode when I look at this view on it's own. However if I run the app in the simulator, or even preview a parent view and navigate to this one, the refreshing of the view doesn't seem to work. I can press the buttons, and nothing happens. If i leave the view and come back, everything appears where it should.
I'll include all the files below. Is there something I'm missing here?
Thanks
Main view displaying the list with two sections
import SwiftUI
import FirebaseFirestoreSwift
struct SessionInvitesView: View {
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#Binding var sessionViewModel : TrainingSessionViewModel
#State private var searchText: String = ""
#State var refresh : Bool = false
var enrolledClients : [Client] {
return clients.filter { sessionViewModel.session.invites.contains($0.id!) }
}
var availableClients : [Client] {
return clients.filter { !sessionViewModel.session.invites.contains($0.id!) }
}
var searchFilteredClients : [Client] {
if searchText.isEmpty {
return availableClients
} else {
return availableClients.filter {
$0.dogName.localizedCaseInsensitiveContains(searchText) ||
$0.name.localizedCaseInsensitiveContains(searchText) ||
$0.dogBreed.localizedCaseInsensitiveContains(searchText) }
}
}
var backButton: some View {
Button(action: { self.onCancel() }) {
Text("Back")
}
}
var body: some View {
NavigationView {
List {
Section(header: Text("Enrolled")) {
ForEach(enrolledClients) { client in
SessionInviteListItem(client: client, isEnrolled: true, onTap: removeClient)
}
}
Section(header: Text("Others")) {
ForEach(searchFilteredClients) { client in
SessionInviteListItem(client: client, isEnrolled: false, onTap: addClient)
}
}
}
.listStyle(.insetGrouped)
.searchable(text: $searchText)
.navigationTitle("Invites")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: backButton)
}
}
func removeClient(clientId: String) {
self.sessionViewModel.session.invites.removeAll(where: { $0 == clientId })
refresh.toggle()
}
func addClient(clientId: String) {
self.sessionViewModel.session.invites.append(clientId)
refresh.toggle()
}
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
}
struct SessionInvitesView_Previews: PreviewProvider {
#State static var model = TrainingSessionViewModel()
static var previews: some View {
SessionInvitesView(sessionViewModel: $model)
}
}
List item view
import SwiftUI
struct SessionInviteListItem: View {
var client : Client
#State var isEnrolled : Bool
var onTap : (String) -> ()
var body: some View {
HStack {
VStack(alignment: .leading) {
HStack {
Text(client.dogName.uppercased())
.bold()
Text("(\(client.dogBreed))")
}
Text(client.name)
.font(.subheadline)
}
Spacer()
Button(action: { onTap(client.id!) }) {
Image(systemName: self.isEnrolled ? "xmark.circle.fill" : "plus.circle.fill")
}
.buttonStyle(.borderless)
.foregroundColor(self.isEnrolled ? .red : .green)
}
}
}
struct SessionInviteListItem_Previews: PreviewProvider {
static func doNothing(_ : String) {}
static var previews: some View {
SessionInviteListItem(client: buildSampleClient(), isEnrolled: false, onTap: doNothing)
}
}
Higher level view used to navigate to this list view
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionEditView: View {
// MARK: - Member Variables
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#StateObject var sheetManager = SheetManager()
var mode: Mode = .new
var dateManager = DateManager()
#State var viewModel = TrainingSessionViewModel()
#State var sessionDate = Date.now
#State var startTime = Date.now
#State var endTime = Date.now.addingTimeInterval(3600)
var completionHandler: ((Result<Action, Error>) -> Void)?
// MARK: - Local Views
var cancelButton: some View {
Button(action: { self.onCancel() }) {
Text("Cancel")
}
}
var saveButton: some View {
Button(action: { self.onSave() }) {
Text("Save")
}
}
var addInviteButton : some View {
Button(action: { sheetManager.showInvitesSheet.toggle() }) {
HStack {
Text("Add")
Image(systemName: "plus")
}
}
}
// MARK: - Main View
var body: some View {
NavigationView {
List {
Section(header: Text("Details")) {
TextField("Session Name", text: $viewModel.session.title)
TextField("Location", text: $viewModel.session.location)
}
Section {
DatePicker(selection: $sessionDate, displayedComponents: .date) {
Text("Date")
}
.onChange(of: sessionDate, perform: { _ in
viewModel.session.date = dateManager.dateToStr(date: sessionDate)
})
DatePicker(selection: $startTime, displayedComponents: .hourAndMinute) {
Text("Start Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: startTime, perform: { _ in
viewModel.session.startTime = dateManager.timeToStr(date: startTime)
})
DatePicker(selection: $endTime, displayedComponents: .hourAndMinute) {
Text("End Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: endTime, perform: { _ in
viewModel.session.endTime = dateManager.timeToStr(date: endTime)
})
}
Section {
HStack {
Text("Clients")
Spacer()
Button(action: { self.sheetManager.showInvitesSheet.toggle() }) {
Text("Edit").foregroundColor(.blue)
}
}
ForEach(viewModel.session.invites, id: \.self) { clientID in
self.createClientListElement(id: clientID)
}
.onDelete(perform: deleteInvite)
}
Section(header: Text("Notes")) {
TextField("Add notes here...", text: $viewModel.session.notes)
}
if mode == .edit {
Section {
HStack {
Spacer()
Button("Delete Session") {
sheetManager.showActionSheet.toggle()
}
.foregroundColor(.red)
Spacer()
}
}
}
}
.navigationTitle(mode == .new ? "New Training Session" : "Edit Training Session")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
leading: cancelButton,
trailing: saveButton)
.actionSheet(isPresented: $sheetManager.showActionSheet) {
ActionSheet(title: Text("Are you sure?"),
buttons: [
.destructive(Text("Delete Session"), action: { self.onDelete() }),
.cancel()
])
}
.sheet(isPresented: $sheetManager.showInvitesSheet) {
SessionInvitesView(sessionViewModel: $viewModel)
}
}
}
func createClientListElement(id: String) -> some View {
let client = clients.first(where: { $0.id == id })
if let client = client {
return AnyView(ClientListItem(client: client))
}
else {
return AnyView(Text("Invalid Client ID: \(id)"))
}
}
func deleteInvite(indexSet: IndexSet) {
viewModel.session.invites.remove(atOffsets: indexSet)
}
// MARK: - Local Event Handlers
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
func onSave() {
self.viewModel.onDone()
self.dismiss()
}
func onDelete() {
self.viewModel.onDelete()
self.dismiss()
self.completionHandler?(.success(.delete))
}
// MARK: - Sheet Management
class SheetManager : ObservableObject {
#Published var showActionSheet = false
#Published var showInvitesSheet = false
}
}
struct TrainingSessionEditView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionEditView(viewModel: TrainingSessionViewModel(session: buildSampleTrainingSession()))
}
}
I'm happy to include any of the other files if you think it would help. Thanks in advance!

Displaying a Sheet from multiple options in swiftUI

Follow-up question to iOS14 introducing errors with #State bindings
I was displaying a modal sheet from several options, depending on which button was pressed. However, now in iOS14 I get a fatal error caused by the selectedSpeaker/selectedMicrophone/selectedAmp being nil when the sheet displays. I am trying to change to .sheet(item:, content:) but I can't see how to implement the enum and then pass in the appropriate selected object. This is what I was doing previously:
enum ActiveSheet {
case speakerDetails, micDetails, ampDetails, settings
}
struct FavoritesView: View {
#State private var selectedSpeaker: Speaker?
#State private var selectedMicrophone: Microphone?
#State private var selectedAmp: Amplifier?
#State private var showingSheet = false
#State private var activeSheet: ActiveSheet = .settings
var body: some View {
List {
Button(action: {
self.activeSheet = .settings
self.showingSheet = true
}, label: { Text("Settings")})
Button(action: {
self.activeSheet = .micDetails
self.selectedMicrophone = microphones[0]
self.showingSheet = true
}, label: { Text("Mic 1")})
Button(action: {
self.activeSheet = .micDetails
self.selectedMicrophone = microphones[1]
self.showingSheet = true
}, label: { Text("Mic 2")})
Button(action: {
self.activeSheet = .speakerDetails
self.showingSheet = true
self.selectedSpeaker = speakers[0]
}, label: { Text("Speaker 1")})
Button(action: {
self.activeSheet = .speakerDetails
self.showingSheet = true
self.selectedSpeaker = speakers[1]
}, label: { Text("Speaker 2")})
//and so on for activeSheet = .ampDetails in the same way.
}
.sheet(isPresented: self.$showingSheet) {
if self.activeSheet == .speakerDetails {
SpeakerDetailView(speaker: self.selectedSpeaker!)
}
else if self.activeSheet == .micDetails {
MicDetailView(microphone: self.selectedMicrophone!)
}
else if self.activeSheet == .ampDetails {
AmpDetailView(amp: self.selectedAmp!)
} else if self.activeSheet == .settings {
SettingsView(showSheet: self.$showingSheet))
}
}
}
}
}
Here is another approach for your problem which uses sheet(item:content:)
struct ContentView: View {
#State private var selectedSpeaker: Speaker?
#State private var selectedMicrophone: Microphone?
#State private var selectedAmp: Amplifier?
#State private var showSettingsSheet = false
var body: some View {
List {
settingsSection
microphonesSection
// more sections
}
}
var settingsSection: some View {
Button(action: {
self.showSettingsSheet = true
}) {
Text("Settings")
}
.sheet(isPresented: self.$showSettingsSheet) {
SettingsView()
}
}
#ViewBuilder
var microphonesSection: some View {
Button(action: {
self.selectedMicrophone = microphones[0]
}) {
Text("Mic 1")
}
Button(action: {
self.selectedMicrophone = microphones[1]
}) {
Text("Mic 2")
}
.sheet(item: self.$selectedMicrophone) {
MicDetailView(microphone: $0)
}
}
}
This way you also don't need enum ActiveSheet.
You can always use #Environment(\.presentationMode) to dismiss a sheet, no need to pass the variable to the sheet (as in SettingsView(showSheet: self.$showingSheet)):
struct SettingsView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
Text("SettingsView")
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}

Displaying multiple overlays with SwiftUI

I'm trying to show multiple sheets with SwiftUI.
Overlay.swift
import SwiftUI
struct OverlayWith<Content: View>: View {
let content: () -> Content
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.compact.down")
.resizable()
.frame(width: 40, height: 15)
.accentColor(Color.gray)
.padding()
}
content()
Spacer()
}
}
}
struct OverlayView_Button1: View {
var body: some View{
Text("Button 1 triggered this overlay")
}
}
struct OverlayView_Button2: View {
var body: some View{
Text("Button 2 triggered this overlay")
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var overlayVisible: Bool = false
// Button States
#State var view1_visible: Bool = false
#State var view2_visible: Bool = false
var body: some View {
VStack() {
Button(action: {
self.showSheet1()
}, label: {
Text("Button 1")
})
Button(action: {
self.showSheet2()
}, label: {
Text("Button 2")
})
}
.sheet(isPresented: $overlayVisible, content:{
if self.view1_visible {
OverlayWith(content: {
OverlayView_Button1()
})
} else if self.view2_visible {
OverlayWith(content: {
OverlayView_Button2()
})
}
})
}
func showSheet1(){
self.resetButtonStates()
self.view1_visible = true
self.overlayVisible = true
}
func showSheet2(){
self.resetButtonStates()
self.view2_visible = true
self.overlayVisible = true
}
func resetButtonStates(){
self.view1_visible = false
self.view2_visible = false
}
}
Compiling this code for iOS 13 works as expected. For iOS 14 on the other hand showSheetX() opens a sheet with an empty view. Dismissing the sheet and opening it again shows OverlayView_ButtonX. Debugging shows that viewX_visible is false when showSheetX() was called for the first time.
Is this a bug with iOS itself or am I missing something here?
Thank you in advance!
I think I found the solution for this issue:
ContentView.swift
import SwiftUI
var view1_visible: Bool = false // <- view1_visible is not a state anymore
var view2_visible: Bool = false // <- view2_visible is not a state anymore
struct ContentView: View {
#State var overlayVisible: Bool = false
var body: some View {
VStack() {
Button(action: {
self.showSheet1()
}, label: {
Text("Button 1")
})
Button(action: {
self.showSheet2()
}, label: {
Text("Button 2")
})
}
.sheet(isPresented: $overlayVisible, content:{
if view1_visible {
OverlayWith(content: {
OverlayView_Button1()
})
} else if view2_visible {
OverlayWith(content: {
OverlayView_Button2()
})
}
})
}
func showSheet1(){
self.resetButtonStates()
view1_visible = true
self.overlayVisible = true
}
func showSheet2(){
self.resetButtonStates()
view2_visible = true
self.overlayVisible = true
}
func resetButtonStates(){
view1_visible = false
view2_visible = false
}
}
Feel free to explain to me why this code works. I'm really confused :S

SwiftUI: Using #Binding to dismiss a modal view not working

I'm passing a #State var down a few views, using #Binding on the child views and when I ultimately set the variable to back to false, sometimes my view doesn't dismiss.
It seems like I can run articleDisplayed.toggle() but if I run an additional function above or below, it won't work.
Any idea what's going on here?
Here's my code:
struct HomeView: View {
#EnvironmentObject var state: AppState
#State var articleDisplayed = false
// MARK: - Body
var body: some View {
NavigationView {
ZStack {
List {
ForEach(state.cards, id: \.id) { card in
Button(action: {
self.articleDisplayed = true // I set it to true here
self.state.activeCard = card
}) {
HomeCell(
card: card,
publicationColor: self.state.publication.brandColor
)
}.sheet(isPresented: self.$articleDisplayed) {
SafariQuickTopicView(articleDisplayed: self.$articleDisplayed)
.environmentObject(self.state)
.environment(\.colorScheme, .light)
}
}
}
}
}
}
}
Then in my SafariQuickTopicView:
struct SafariQuickTopicView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var state: AppState
#Binding var articleDisplayed: Bool
var body: some View {
NavigationView {
ZStack(alignment: .bottom) {
// doesn't matter what's in here
}
.navigationBarItems(trailing: passButton)
}
}
private var passButton: some View {
Button(action: self.state.pass {
DispatchQueue.main.async {
// self.state.removeActiveCardFromState()
self.articleDisplayed.toggle() // this will work but adding a second function in here prevents it from working, above or below the toggle.
}
}
}) {
Text("Pass")
}
}
Finally, in my AppState:
func pass(completion: () -> Void) { // need completion?
guard let activeCard = activeCard else { return }
if let index = cards.firstIndex(where: { $0.id == activeCard.id }) {
activeCard.add(comment: "pass")
rejectCurrentCard() // Does an async operation with an external API but we don't care about the result
addRemovedActiveCardToUserDefaults()
completion()
}
}
Move .sheet out of List, it must be one per view hierarchy, so like
List {
ForEach(state.cards, id: \.id) { card in
Button(action: {
self.articleDisplayed = true // I set it to true here
self.state.activeCard = card
}) {
HomeCell(
card: card,
publicationColor: self.state.publication.brandColor
)
}
}
}
.sheet(isPresented: self.$articleDisplayed) {
SafariQuickTopicView(articleDisplayed: self.$articleDisplayed)
.environmentObject(self.state)
.environment(\.colorScheme, .light)
}

How to implement function like onAppear in SwiftUI?

So, I would like to create a custom view and add function. How can I implement a function like .onAppear(perform: (() -> Void)?)? My code does not work, onDismiss closure does not call in the DashboardView.
struct DashboardView: View {
#State var employees = ["Alex", "Olga", "Mark"]
#State var presentEmployeeView = false
var body: some View {
NavigationView {
List {
Section {
Button(action: {
self.presentEmployeeView = true
}, label: {
Text("All employees")
}).buttonStyle(BorderlessButtonStyle())
}
}
}
.sheet(isPresented: $presentEmployeeView) {
EmployeesView(employees: self.employees).onDismiss {
self.presentEmployeeView = false
}
}
}
}
struct EmployeesView: View {
let employees: [String]
#State private var onDismissClosure: (() -> Void)? = nil
func onDismiss(perform action: (() -> Void)? = nil) -> some View {
self.onDismissClosure = action
return self
}
var body: some View {
NavigationView {
List {
ForEach(employees) { employee in
EmployeeCell(employee: employee)
}
}.navigationBarItems(leading:
Button(action: {
self.onDismissClosure?()
}, label: {
Text("Close")
})
)
}
}
}
Here is possible approach. Tested & worked with Xcode 11.4 / iOS 13.4
struct DashboardView: View {
#State var employees = ["Alex", "Olga", "Mark"]
#State var presentEmployeeView = false
var body: some View {
NavigationView {
List {
Section {
Button(action: {
self.presentEmployeeView = true
}, label: {
Text("All employees")
}).buttonStyle(BorderlessButtonStyle())
}
}
}
.sheet(isPresented: $presentEmployeeView) {
EmployeesView(employees: self.employees) {
self.presentEmployeeView = false
}
}
}
}
struct EmployeesView: View {
let employees: [String]
var onDismiss = {}
var body: some View {
NavigationView {
List {
ForEach(employees, id: \.self) { employee in
Text("\(employee)")
}
}.navigationBarItems(leading:
Button(action: {
self.onDismiss()
}, label: {
Text("Close")
})
)
}
}
}
Update: possible alternate for usage with modifier:
...
.sheet(isPresented: $presentEmployeeView) {
EmployeesView(employees: self.employees).onDismiss {
self.presentEmployeeView = false
}
}
}
}
struct EmployeesView: View {
let employees: [String]
var onDismiss = {}
func onDismiss(_ callback: #escaping () -> ()) -> some View {
EmployeesView(employees: employees, onDismiss: callback)
}
var body: some View {
NavigationView {
List {
ForEach(employees, id: \.self) { employee in
Text("\(employee)")
}
}.navigationBarItems(leading:
Button(action: {
self.onDismiss()
}, label: {
Text("Close")
})
)
}
}
}

Resources