SwiftUI - Present 3 different Views with different parameter - ios

I need to present 3 different Views.
AddListView
ChangeColor
EditListView
They take different paramater. AddListView does not have parameter while ChangeColor and EditListView takes Color and NSManagedObject respectively. However for the sake of simplicity, EditListView's paramter is integer in this example.
I am using .fullScreenCover(item: <#T##Binding<Identifiable?>#>, content: <#T##(Identifiable) -> View#>) for presenting them.
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
}
else if type == .EditListView {
if let index = selectedIndex {
EditListView(index: index)
}
}
else if type == .ChangeColor {
if let color = selectedColor {
ColorView(color: color)
}
}
}
selectedIndex and selectedColor is nil even though I initialize them before initializing presentedViewType. And hence, an EmptyView is presented.
This is the project.
enum PresentedViewType: Identifiable {
case AddListView
case ChangeColor
case EditListView
var id: Int {
return hashValue
}
}
struct ContentView: View {
#State var presentedViewType: PresentedViewType?
#State var selectedColor: Color?
#State var selectedIndex: Int?
var body: some View {
NavigationView {
List {
Section {
NavigationLink(destination: Text("All")) {
Text("All")
}
.background(Color.blue)
.contextMenu {
Button(action: {
selectedColor = .blue
presentedViewType = .ChangeColor
}) {
Label("Change Color", systemImage: "paintbrush.pointed.fill")
}
}
}
ForEach(0..<10) { index in
NavigationLink(destination: Text("Row Details \(index)")) {
Text("Row \(index)")
}
.contextMenu {
Button(action: {
selectedIndex = index
presentedViewType = .EditListView
}) {
Label("Edit", systemImage: "square.and.pencil")
}
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
presentedViewType = .AddListView
}) {
Label("Add", systemImage: "plus")
}
}
}
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
}
else if type == .EditListView {
if let index = selectedIndex {
EditListView(index: index)
}
}
else if type == .ChangeColor {
if let color = selectedColor {
ColorView(color: color)
}
}
}
}
}
}
struct ColorView: View {
#Environment(\.presentationMode) var presentationMode
#State var color: Color
var body: some View {
NavigationView {
Text("Color View")
.background(color)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
struct AddListView: View {
#Environment(\.presentationMode) var presentationMode
#State var text: String = ""
var body: some View {
NavigationView {
TextField("", text: $text)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
struct EditListView: View {
#Environment(\.presentationMode) var presentationMode
#State var index: Int
var body: some View {
NavigationView {
Text("Row \(index)")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
I have to mention that they do not have fixed value. They have different value depending on which row you need to edit.
How to pass selectedIndex and selectedColor to EditListView and ColorView respectively?
Update
EditListView takes only selectedIndex while ColorView takes only selectedColor

You need to have #Binding properties inside EditListView and ColorView
struct EditListView: View {
#Binding var selectedIndex: Int?
// rest of view implementation
}
struct ColorView: View {
#Binding var selectedIndex: Int?
// rest of view implementation
}
and then pass the binding in the initialisers
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
} else if type == .EditListView {
EditListView(index: $selectedIndex)
} else if type == .ChangeColor {
ColorView(color: $selectedColor)
}
}

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!

Animation not working inside sheet for swiftui

I am using a sheet to present a list of options and on click of the option I want to change the view with the animation of sliding from trailing. As per my understanding and what I have read on various sites I have written this code but I am not sure why it is not working the way intended. I just want to know where exactly this code went wrong.
struct XYZ: App {
let persistenceController = PersistenceController.shared
#State var isPresented : Bool = false
#State var isSwiped : Bool = false
var body: some Scene {
WindowGroup {
optionList(isPresented: $isPresented)
.sheet(isPresented: $isPresented, content: {
Text("This is from modal view!")
.onTapGesture {
withAnimation(Animation.easeIn(duration: 10)){
isSwiped.toggle()
}
}
if isSwiped {
checkedList()
.transition(.move(edge: .trailing))
}
})
}
}
}
struct optionList : View {
#Binding var isPresented : Bool
var body: some View {
Text("This is a testView")
.onTapGesture {
withAnimation{
isPresented.toggle()
}
}
}
}
struct checkedList : View {
#State var name : String = "WatchList"
var arr = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh"]
#State var mp : [Int:Int] = [:]
var body: some View {
VStack{
HStack{
TextField("WatchlistName", text: $name)
.padding(.all)
Image(systemName: "trash.fill")
.padding(.all)
.onTapGesture {
print("Delete watchList!!")
}
}
ScrollView{
ForEach(arr.indices) { item in
HStack (spacing: 0) {
Image(systemName: mp.keys.contains(item) ? "checkmark.square" : "square")
.padding(.horizontal)
Text(arr[item])
}
.padding(.bottom)
.frame(width: UIScreen.main.bounds.width, alignment: .leading)
.onTapGesture {
if mp.keys.contains(item) {
mp[item] = nil
} else {
mp[item] = 1
}
}
}
}
Button {
print("Remove Ticked Elements!")
deleteWatchListItem(arr: Array(mp.keys))
} label: {
Text("Save")
}
}
}
func deleteWatchList(ind: Int){
print(ind)
}
func deleteWatchListItem(arr : [Int]) {
print(arr)
}
}
I tried to create a view and with the animation using withanimation with a bool variable tried to change the view.
It sounds like what you want is to push the checkedList on to a NavigationStack…
struct ContentView: View {
#State var isPresented : Bool = false
var body: some View {
Text("This is a testView")
.onTapGesture {
isPresented.toggle()
}
.sheet(isPresented: $isPresented, content: {
NavigationStack {
NavigationLink("This is from modal view!") {
checkedList()
}
}
})
}
}

EmptyBody().onAppear not works when sheet present with item

When I present sheet with .sheet(isPresented... onAppear of EmptyView() triggered
but when I use .sheet(item... then onAppear doesn't trigger. I don't understand what mistake I am doing?
item:
enum ActiveSheet: Identifiable {
var id: String { UUID().uuidString }
case customA
case customB
}
Main View:
struct ContentView: View {
#State private var activeSheet: ActiveSheet?
var body: some View {
VStack {
Button(action: { activeSheet = .customA }) {
Text("View A")
}
Button(action: { activeSheet = .customB }) {
Text("View B")
}
}
.buttonStyle(.borderedProminent)
//If I use this .sheet(isPresented... then onAppear triggers, but not with item
.sheet(item: $activeSheet) { item in
switch item {
case .customA:
CustomViewA()
case .customB:
CustomViewB()
}
}
}
}
Empty Views:
struct CustomViewA: View {
var body: some View {
EmptyView()
.onAppear {
print("OnAppear")
}
}
}
struct CustomViewB: View {
var body: some View {
EmptyView()
.onAppear {
print("OnAppear")
}
}
}

SwiftUI Form Cell losing selection UI when drilling into details?

I have the following code:
enum SelectedDetails:Int, CaseIterable {
case d0
case d1
}
struct CellSelectionTestView : View {
#State var selection:SelectedDetails? = .d0
var body: some View {
NavigationView {
Form {
Section(header: Text("Section 0")) {
NavigationLink(destination: D0DetailsView(),
tag: .d0,
selection: $selection) {
D0CellView().frame(height: 80)
}
NavigationLink(destination: D1CellView(),
tag: .d1,
selection: $selection) {
D1CellView().frame(height: 80)
}
}
}
}
}
}
struct D0CellView: View {
var body: some View {
Text("D0")
}
}
struct D0DetailsView: View {
var body: some View {
VStack {
List {
ForEach(0..<10) { n in
NavigationLink.init(destination: OptionsDetailsView(index:n)) {
Text("show \(n) details")
}
}
}
.refreshable {
}
}
}
}
struct OptionsDetailsView: View {
let index:Int
var body: some View {
Text("OptionsDetailsView \(index)")
}
}
struct D1CellView: View {
var body: some View {
Text("D1")
}
}
When I tap on D0 cell, it shows this:
D0 cell correctly shows the selected state UI.
Then I tap on one of the show <n> details cells and the selection goes away:
How do I keep D0 cell selected UI stated active until I tap on another cell like D1 for example regardless of what I do in the details view to the right? I need to keep UI context as the user does what is needed within the details shown when D0 is tapped. Why is that selection going away if I didn't even tap on D1?
Strange, but it seems like NavigationView can only keep one selection. I found a workaround by integrating a second NavigationView with .stacked style in your child view:
struct D0DetailsView: View {
var body: some View {
NavigationView {
VStack {
List {
ForEach(0..<10) { n in
NavigationLink {
OptionsDetailsView(index:n)
} label: {
Text("show \(n) details")
}
}
}
.refreshable {
}
}
}
.navigationViewStyle(.stack)
}
}
Another approach: save the last active selection and set the select background color manually:
struct CellSelectionTestView : View {
#State private var selection: SelectedDetails? = .d0
#State private var selectionSaved: SelectedDetails = .d0
var body: some View {
NavigationView {
Form {
Section(header: Text("Section 0")) {
NavigationLink(tag: .d0, selection: $selection) {
D0DetailsView()
} label: {
D0CellView().frame(height: 80)
}
.listRowBackground(selectionSaved == .d0 ? Color.gray : Color.clear)
NavigationLink(tag: .d1, selection: $selection) {
D1CellView()
} label:{
D1CellView().frame(height: 80)
}
.listRowBackground(selectionSaved == .d1 ? Color.gray : Color.clear)
}
}
}
.onChange(of: selection) { newValue in
if selection != nil { selectionSaved = selection! }
}
}
}

View not updating when accessing ObservableObject in Else block

My detail View does not update when I change its #Binding value.
struct ContentView: View {
class ViewModel: ObservableObject {
#Published var imageSize: ImageSize?
#Published var anotherValue: Int? = 5
var cancellable: AnyCancellable?
init() {
cancellable = $imageSize.sink(receiveValue: { imageSize in
print("🚨 \(imageSize?.rawValue ?? "Unknown")")
})
}
}
#ObservedObject private var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
if viewModel.anotherValue == nil {
Text("Another value is nil")
} else {
NavigationLink(destination: SelectImageSizeView(selectedImageSize: $viewModel.imageSize)
.navigationBarTitle("ImageSize", displayMode: .inline)) {
Text("ImageSize: \(viewModel.imageSize?.rawValue ?? "Not Set")")
}
}
}
}
}
}
struct SelectImageSizeView: View {
#Binding var selectedImageSize: ImageSize?
var body: some View {
List{
ForEach(ImageSize.allCases, id: \.self) { imageSize in
HStack {
Button {
withAnimation {
self.selectedImageSize = imageSize
}
} label: {
HStack {
Text(imageSize.rawValue)
Image(systemName: imageSize == selectedImageSize ? "checkmark.circle.fill" : "circle")
}
.font(.title)
}
}
}
}
}
}
public enum ImageSize: String, Codable, CaseIterable {
case small = "Small"
case medium = "Medium"
case large = "Large"
}
It does work when I move the NavigationLink to out of the if block:
NavigationView {
List {
if viewModel.anotherValue == nil {
Text("Another value is nil")
}
NavigationLink(destination: SelectImageSizeView(selectedImageSize: $viewModel.imageSize)
.navigationBarTitle("ImageSize", displayMode: .inline)) {
Text("ImageSize: \(viewModel.imageSize?.rawValue ?? "Not Set")")
}
}
}
Am I missing something here?
Hm. I don't know why that's the case. But you could instead pass over your whole ViewModel?
You would have to put it outside your ContentView. I have made it fileprivate if you don't want to access it from other files.
Works for me.
fileprivate class ViewModel: ObservableObject {
#Published var imageSize: ImageSize?
#Published var anotherValue: Int? = 5
var cancellable: AnyCancellable?
init() {
cancellable = $imageSize.sink(receiveValue: { imageSize in
print("🚨 \(imageSize?.rawValue ?? "Unknown")")
})
}
}
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
if viewModel.anotherValue == nil {
Text("Another value is nil")
} else {
NavigationLink(destination: SelectImageSizeView(viewModel: viewModel)
.navigationBarTitle("ImageSize", displayMode: .inline)) {
Text("ImageSize: \(viewModel.imageSize?.rawValue ?? "Not Set")")
}
}
}
}
}
}
struct SelectImageSizeView: View {
#ObservedObject fileprivate var viewModel: ViewModel
var body: some View {
List{
ForEach(ImageSize.allCases, id: \.self) { imageSize in
HStack {
Button {
withAnimation {
viewModel.imageSize = imageSize
}
} label: {
HStack {
Text(imageSize.rawValue)
Image(systemName: imageSize == viewModel.imageSize ? "checkmark.circle.fill" : "circle")
}
.font(.title)
}
}
}
}
}
}
public enum ImageSize: String, Codable, CaseIterable {
case small = "Small"
case medium = "Medium"
case large = "Large"
}
It is very silly but there is bug in simulator. It is working with device.

Resources