UI changes with ObservableObject just after switching tabs - ios

I have a ObservableObject that I use to update my UI when new data is sent from the server (an a class which contains an array of custom structs).
For some reason, when the data is sent, the ContentView's body is called, but the data isn't changed. I even added a print statement to check if the data that the array contains is right and it is.
When I try to switch to another tab on my TabView, and then switch back to the main view, the UI does get updated. Does anybody know why the UI updates just when I switch tabs, although the body gets recalled to update the UI when the data changed?
HomeView
struct HomeView: View {
#ObservedObject private var fbData = firebaseData
var body: some View {
TabView {
//Home Tab
NavigationView {
ScrollView(showsIndicators: false) {
ForEach(self.fbData.posts.indices, id: \.self) { postIndex in
PostView(post: self.$fbData.posts[postIndex])
.listRowInsets(EdgeInsets())
.padding(.vertical, 5)
}
}
.navigationBarTitle("MyPhotoApp", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
print("Camera btn pressed")
}, label: {
Image(systemName: "camera")
.font(.title)
})
, trailing:
Button(action: {
print("Messages btn pressed")
}, label: {
Image(systemName: "paperplane")
.font(.title)
})
)
} . tabItem({
Image(systemName: "house")
.font(.title)
})
Text("Search").tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
}
Text("Upload").tabItem {
Image(systemName: "plus.app")
.font(.title)
}
Text("Activity").tabItem {
Image(systemName: "heart")
.font(.title)
}
Text("Profile").tabItem {
Image(systemName: "person")
.font(.title)
}
}
.accentColor(.black)
.edgesIgnoringSafeArea(.top)
}
}
FirebaseData:
class FirebaseData : ObservableObject {
#Published var posts = [Post]()
let postsCollection = Firestore.firestore().collection("Posts")
init() {
self.fetchPosts()
}
//MARK: Fetch Data
private func fetchPosts() {
self.postsCollection.addSnapshotListener { (documentSnapshot, err) in
if err != nil {
print("Error fetching posts: \(err!.localizedDescription)")
return
} else {
documentSnapshot!.documentChanges.forEach { diff in
if diff.type == .added {
let post = self.createPostFromDocument(document: diff.document)
self.posts.append(post)
} else if diff.type == .modified {
self.posts = self.posts.map { (post) -> Post in
if post.id == diff.document.documentID {
return self.createPostFromDocument(document: diff.document)
} else {
return post
}
}
} else if diff.type == .removed {
for index in self.posts.indices {
if self.posts[index].id == diff.document.documentID {
self.posts.remove(at: index)
}
}
}
}
}
}
}

Your code example doesn't help to find the bug. Finally I've got how to demonstrate it. First, do it the "proper way" (copy - paste - try it yourself)
import SwiftUI
struct Data: Identifiable {
let id = UUID()
let text: String
}
class Model: ObservableObject {
#Published var data: [Data] = [Data(text: "alfa"), Data(text: "beta")]
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
TabView {
View1(model: model).tabItem {
Text("View 1")
}
View2(model: model).tabItem {
Text("View 2")
}
}
}
}
struct View1: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text("View 1").font(.largeTitle)
DataView(data: model.data)
Button(action: {
self.model.data.append(Data(text: String("ABCDEFGH".shuffled())))
}) {
Text("Add random data")
}
}
}
}
struct View2: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text("View 2").font(.largeTitle)
DataView(data: model.data.filter({ (item) -> Bool in
item.text.count < 4
}))
// to distinguish from other DataView !! it seems to be a bug in SwiftUI
// try to remove it to see the difference
.id("view2")
Button(action: {
self.model.data.append(Data(text: String("ABC".shuffled())))
}) {
Text("Add random data")
}
}
}
}
struct DataView: View {
var data: [Data]
var body: some View {
List {
ForEach(data) { (item) in
Text(item.text)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I works as it should, you can modify the data from each tab, you see refreshed data, etc.
removing "fixed - user defined" .id modifier, it changes the behaviour dramatically
This looks like a serious bug in SwiftUI ...

I think is SwiftUI bug. I solve this problem like this.
Instead of rendering your PostView(post: self.$fbData.posts[postIndex])
implement post view inside ForEach.
ForEach(self.fbData.posts.indices, id: \.self) { postIndex in
Text(self.$fbData.posts[postIndex].comment)
Text(self.$fbData.posts[postIndex].date)
....
}

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!

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")
}
}
}

Picker conflicts with NavigationLink gesture

I am trying to create the following card view.
With the following code to achieve it.
struct SimpleGame: Identifiable, Hashable {
var id = UUID()
let name: String
}
enum PlayingStatus: String {
case In = "I"
case Out = "O"
case Undecided = "U"
}
struct TestView: View {
let games: [SimpleGame] = [
.init(name: "First"),
.init(name: "Second")
]
#State private var currentStatus: PlayingStatus = .Undecided
var body: some View {
NavigationView {
List(games) { game in
Section {
VStack {
NavigationLink(value: game) {
Text("\(game.name)")
}
Divider()
Picker("Going?", selection: $currentStatus) {
Text("No Response")
.tag(PlayingStatus.Undecided)
Text("Going")
.tag(PlayingStatus.In)
Text("Not going")
.tag(PlayingStatus.Out)
}
.font(.body)
}
}
}
.navigationDestination(for: Game.self) { game in
Text("Detail View")
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Upcoming")
}
}
}
But a tap on element wrapped by NavigationLink is registering as a tap on the Picker. Anyone know of a way around this?
iOS 16/Xcode 14
you could try this:
List {
ForEach(games, id: \.name) { game in
Section {
NavigationLink(value: game) {
Text("\(game.name)")
}
// -- here
VStack {
Divider()
Picker("Going?", selection: $currentStatus) {
Text("No Response").tag(PlayingStatus.Undecided)
Text("Going").tag(PlayingStatus.In)
Text("Not going").tag(PlayingStatus.Out)
}
.font(.body)
}
}
}
}
What often works for me is extending the view.
struct TestView: View {
var body: some View {
List {
ForEach(games, id: \.name) { game in
Section {
NavigationLink(value: game) {
Text("\(game.name)")
}
// -- here
VStack {
Divider()
picker
}
}
}
}
}
}
Extension TestView {
private var picker: some View {
Picker("Going?", selection: $currentStatus) {
Text("No Response").tag(PlayingStatus.Undecided)
Text("Going").tag(PlayingStatus.In)
Text("Not going").tag(PlayingStatus.Out)
}
.font(.body)
}
}

How do I let text retrieved from Firestore load upon the view being shown?

Intro
Hi there. I recently asked a different question asking how to implement user-triggered FCMs. I quickly realised that in order to implement FCMs I needed to add another feature to my app, which is why I am here now. I'm trying, trust me.
My question / problem
In the picture I attached, you can see the text that is supposed to show when the user clicks on the friends tab. But it doesn't do that. It does when I refresh the list, because I put the function in the refreshable attribute. I did also put the function in the view initialisation, but it doesn't do what I want it to do. So my question would be, a) why it doesn't load? and b) what would be an approach to solve my problem?
Code reference
This function is called in the init{} of the view and .refreshable{} of the list inside the view. (I also tried adding it via .onAppear{} in the NavigationLink of the parent view.)
#State var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let bestie = document.get("bestie") as? String ?? "error: bestie"
bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
Image for reference
Thanks and annotation
Thank you very much in advance, I'm amazed every day by how amazing this community is and how so many people are willing to help. If you need me to add anything else, of course I will do that. I hope I'll be wise enough one day to help other people with their problems as well.
Edit
The view
import Firebase
import FirebaseAuth
import SDWebImage
import SDWebImageSwiftUI
import SwiftUI
struct View_Friend_Tab: View {
#ObservedObject var friends_model = Model_User()
#State var friendWho = ""
init() {
friends_model.getFriendlist()
//friends_model.getBestie()
getBestie()
}
//VARS
let gifurl = URL(string: "https://c.tenor.com/BTCEb08QgBgAAAAC/osita-iheme-aki-and-pawpaw.gif")
let avatarURL = URL(
string:
"https://firebasestorage.googleapis.com/v0/b/universerp-72af2.appspot.com/o/avatars%2Fanime-girl-white-hair-1-cropped.jpg?alt=media&token=efba4215-850d-41c8-8c90-385f7a572e94"
)
#State var showingAlert = false
#State var showFriendRequests = false
#State var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let bestie = document.get("bestie") as? String ?? "error: bestie"
bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
var body: some View {
if !showFriendRequests {
VStack {
NavigationView {
/*
List (friends_model.friend_list) { item in
HStack {
Text(item.email)
Spacer()
}
}
.refreshable{
friends_model.getFriendlist()
}
.listStyle(.grouped)
*/
List {
Section(header: Text("management")) {
NavigationLink(destination: View_Friend_Best_Tab()) {
Label("Select your bestie", systemImage: "star.fill")
}
NavigationLink(destination: View_Friend_Requests_Tab()) {
Label("Manage friend requests", systemImage: "person.fill.questionmark")
}
NavigationLink(destination: View_Friend_Add_Tab()) {
Label("Add friend", systemImage: "person.fill.badge.plus")
}
}
ForEach(friends_model.friend_list) { item in
let avURL = URL(string: item.avatarURL)
Section(header: Text(item.username)) {
HStack {
VStack(alignment: .leading) {
if bestfriend == item.email {
Text("Is your Shin'yū")
.foregroundColor(Color("lightRed"))
.fontWeight(.bold)
.font(.footnote)
}
Text(item.username)
.fontWeight(.bold)
.frame(alignment: .leading)
Text(item.email)
.font(.footnote)
.multilineTextAlignment(.leading)
}
Spacer()
WebImage(url: avURL)
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 1))
}
Button("Remove", role: .destructive) {
showingAlert = true
}
.alert("Do you really want to remove this friend?", isPresented: $showingAlert) {
HStack {
Button("Cancel", role: .cancel) {}
Button("Remove", role: .destructive) {
friendWho = item.email
removeFriend()
withAnimation {
friends_model.getFriendlist()
}
}
}
}
}
}
}
.navigationTitle("Your Friends")
.navigationViewStyle(.automatic)
.refreshable {
friends_model.getFriendlist()
getBestie()
}
.listStyle(.insetGrouped)
Spacer()
}
}
} else {
View_Friend_Requests_Tab()
}
}
}
struct View_Friend_Tab_Previews: PreviewProvider {
static var previews: some View {
View_Friend_Tab()
}
}
As previously stated, the function is being called in the init block and when the list is refreshed.
As a general recommendation, use view models to keep your view code clean.
In SwiftUI, a view's initialiser should not perform any expensive / long-running computations. Keep in mind that in SwiftUI, a view is only a description of your UI, not the UI itself. Any state management should be handled outside of the initialiser.
In your case, use .onAppear or .task:
import Firebase
import FirebaseAuth
import SDWebImage
import SDWebImageSwiftUI
import SwiftUI
class FriendsViewModel: ObservableObject {
#Published var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
// consider using Codable for mapping - see https://peterfriese.dev/posts/firestore-codable-the-comprehensive-guide/
let bestie = document.get("bestie") as? String ?? "error: bestie"
self.bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
// add other functions and properties here.
}
struct FriendsView: View {
#ObservedObject var viewModel = FriendsViewModel()
//VARS
let gifurl = URL(string: "https://c.tenor.com/BTCEb08QgBgAAAAC/osita-iheme-aki-and-pawpaw.gif")
let avatarURL = URL(
string:
"https://firebasestorage.googleapis.com/v0/b/universerp-72af2.appspot.com/o/avatars%2Fanime-girl-white-hair-1-cropped.jpg?alt=media&token=efba4215-850d-41c8-8c90-385f7a572e94"
)
#State var showingAlert = false
#State var showFriendRequests = false
#State var bestfriend: String = ""
var body: some View {
if !showFriendRequests {
VStack {
NavigationView {
List {
Section(header: Text("management")) {
NavigationLink(destination: SelectBestieView()) {
Label("Select your bestie", systemImage: "star.fill")
}
NavigationLink(destination: ManageFriendRequestsView()) {
Label("Manage friend requests", systemImage: "person.fill.questionmark")
}
NavigationLink(destination: AddFriendsView()) {
Label("Add friend", systemImage: "person.fill.badge.plus")
}
}
ForEach(viewModel.friends) { friend in
let avURL = URL(string: friend.avatarURL)
Section(header: Text(friend.username)) {
HStack {
VStack(alignment: .leading) {
if bestfriend == friend.email {
Text("Is your Shin'yū")
.foregroundColor(Color("lightRed"))
.fontWeight(.bold)
.font(.footnote)
}
Text(friend.username)
.fontWeight(.bold)
.frame(alignment: .leading)
Text(friend.email)
.font(.footnote)
.multilineTextAlignment(.leading)
}
Spacer()
WebImage(url: avURL)
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 1))
}
Button("Remove", role: .destructive) {
showingAlert = true
}
.alert("Do you really want to remove this friend?", isPresented: $showingAlert) {
HStack {
Button("Cancel", role: .cancel) {}
Button("Remove", role: .destructive) {
friendWho = friend.email
viewModel.removeFriend()
withAnimation {
viewModel.getFriendlist()
}
}
}
}
}
}
}
.navigationTitle("Your Friends")
.navigationViewStyle(.automatic)
.onAppear {
viewModel.getFriendlist()
viewModelgetBestie()
}
.refreshable {
viewModel.getFriendlist()
viewModelgetBestie()
}
.listStyle(.insetGrouped)
Spacer()
}
}
} else {
FriendRequestsView()
}
}
}
struct FriendsViewPreviews: PreviewProvider {
static var previews: some View {
FriendsView()
}
}

SwiftUI LazyVGrid NavigationLink has unusual animation on return

I have a NavigationLink within a LazyVGrid and am getting this animation on return from the details view. Starting at about 3.5 seconds into that video, there is an animation I wasn't expecting. There are gaps introduced between the cells and I don't like the way it looks.
Here is the code for the screen with the LazyVGrid:
import Foundation
import SwiftUI
import SFSafeSymbols
import CustomModalView
struct AlbumItemsScreen: View {
#ObservedObject var viewModel:AlbumItemsViewModel
let gridItemLayout = [GridItem(.adaptive(minimum: 50), spacing: 20)]
#State var object: ServerObjectModel?
#State var enableNavLink: Bool = false
var body: some View {
RefreshableScrollView(refreshing: $viewModel.loading) {
LazyVGrid(columns: gridItemLayout) {
ForEach(viewModel.objects, id: \.fileGroupUUID) { item in
AlbumItemsScreenCell(object: item)
.onTapGesture {
object = item
viewModel.showCellDetails = true
}
// Without this conditional, "spacer" cells show up in the grid.
if viewModel.showCellDetails, let object = object {
// The `NavigationLink` works here because the `MenuNavBar` contains a `NavigationView`.
NavigationLink(
destination:
// If I just use `item` directly in this-- oddly, it doesn't reference the same object as for `AlbumItemsScreenCell` above.
ObjectDetailsView(object: object),
isActive:
$viewModel.showCellDetails) {
}
} // end if
} // end ForEach
} // end LazyVGrid
.padding(10)
}
.alert(isPresented: $viewModel.presentAlert, content: {
let message:String = viewModel.alertMessage
viewModel.alertMessage = nil
return Alert(title: Text(message))
})
.navigationBarTitle("Album Contents")
.navigationBarItems(trailing:
AlbumItemsScreenNavButtons(viewModel: viewModel)
)
.disabled(viewModel.addNewItem)
.modal(isPresented: $viewModel.addNewItem) {
AddItemModal(viewModel: viewModel)
.padding(20)
}
.modalStyle(DefaultModalStyle())
.onDisappear {
// I'm having a problem with the modal possibly being presented, the user navigating away, coming back and the modal still being present.
// See also https://github.com/jankaltoun/CustomModalView/issues/1
if viewModel.addNewItem == true {
viewModel.addNewItem = false
}
}
}
}
private struct AlbumItemsScreenNavButtons: View {
#ObservedObject var viewModel:AlbumItemsViewModel
var body: some View {
HStack(spacing: 0) {
Button(
action: {
viewModel.sync()
},
label: {
SFSymbolNavBar(symbol: .goforward)
}
)
Button(
action: {
viewModel.startNewAddItem()
},
label: {
SFSymbolNavBar(symbol: .plusCircle)
}
)
}
}
}
(see also https://github.com/SyncServerII/Neebla/blob/main/Neebla/UI/Screens/Album%20Items/AlbumItemsScreen.swift).
Here is the code for the details view:
import Foundation
import SwiftUI
import SFSafeSymbols
struct ObjectDetailsView: View {
let object:ServerObjectModel
var model:MessagesViewModel?
#State var showComments = false
init(object:ServerObjectModel) {
self.object = object
model = MessagesViewModel(object: object)
}
var body: some View {
VStack {
AnyLargeMedia(object: object)
.onTapGesture {
if let _ = model {
showComments = true
}
}
Spacer()
}
.navigationBarItems(trailing:
Button(
action: {
showComments = true
},
label: {
SFSymbolNavBar(symbol: .message)
}
)
.enabled(model != nil)
)
.sheet(isPresented: $showComments) {
if let model = model {
CommentsView(model: model)
}
else {
// Should never get here. Should never have showComments == true when model is nil.
EmptyView()
}
}
}
}
(see also https://github.com/SyncServerII/Neebla/blob/main/Neebla/UI/Screens/Object%20Details/ObjectDetailsView.swift).
I've tried the strategy indicated here https://developer.apple.com/forums/thread/125937 with this:
NavigationLink(
destination:
// If I just use `item` directly in this-- oddly, it doesn't reference the same object as for `AlbumItemsScreenCell` above.
ObjectDetailsView(object: object),
isActive:
$viewModel.showCellDetails) {
EmptyView()
}
.frame(width: 0, height: 0)
.disabled(true)
but the same effect occurs.
Well, it helped to focus my attention on the problem by writing up this question. I've come up with a solution. I took the NavigationLink out of the scrollview and LazyVGrid:
import Foundation
import SwiftUI
import SFSafeSymbols
import CustomModalView
struct AlbumItemsScreen: View {
#ObservedObject var viewModel:AlbumItemsViewModel
let gridItemLayout = [GridItem(.adaptive(minimum: 50), spacing: 20)]
#State var object: ServerObjectModel?
var body: some View {
VStack {
RefreshableScrollView(refreshing: $viewModel.loading) {
LazyVGrid(columns: gridItemLayout) {
ForEach(viewModel.objects, id: \.fileGroupUUID) { item in
AlbumItemsScreenCell(object: item)
.onTapGesture {
object = item
viewModel.showCellDetails = true
}
} // end ForEach
} // end LazyVGrid
.padding(10)
}
if let object = object {
// The `NavigationLink` works here because the `MenuNavBar` contains a `NavigationView`.
NavigationLink(
destination:
ObjectDetailsView(object: object),
isActive:
$viewModel.showCellDetails) {
EmptyView()
}
.frame(width: 0, height: 0)
.disabled(true)
} // end if
}
.alert(isPresented: $viewModel.presentAlert, content: {
let message:String = viewModel.alertMessage
viewModel.alertMessage = nil
return Alert(title: Text(message))
})
.navigationBarTitle("Album Contents")
.navigationBarItems(trailing:
AlbumItemsScreenNavButtons(viewModel: viewModel)
)
.disabled(viewModel.addNewItem)
.modal(isPresented: $viewModel.addNewItem) {
AddItemModal(viewModel: viewModel)
.padding(20)
}
.modalStyle(DefaultModalStyle())
.onDisappear {
// I'm having a problem with the modal possibly being presented, the user navigating away, coming back and the modal still being present.
// See also https://github.com/jankaltoun/CustomModalView/issues/1
if viewModel.addNewItem == true {
viewModel.addNewItem = false
}
}
}
}
private struct AlbumItemsScreenNavButtons: View {
#ObservedObject var viewModel:AlbumItemsViewModel
var body: some View {
HStack(spacing: 0) {
Button(
action: {
viewModel.sync()
},
label: {
SFSymbolNavBar(symbol: .goforward)
}
)
Button(
action: {
viewModel.startNewAddItem()
},
label: {
SFSymbolNavBar(symbol: .plusCircle)
}
)
}
}
}

Resources