SwiftUI: Transparent NavBar in 3 Column Layout on iPhone - ios

I have a 3 column layout and for some reason, the navigation bar in the middle column starts off buggy on the initial launch of the app, ie transparent and unresponsive. I have to navigate away and back in order for the navigation bar to function properly. This only happens when I run it on the iPhone simulator, iPad works fine.
Code:
struct ContentView: View {
var body: some View {
NavigationView {
Sidebar()
MainView()
Text("Select mail to read")
}
}
}
struct Sidebar: View {
var body: some View {
List {
Section(header: Text("Browse")
) {
NavigationLink(destination: MainView()) {
Text("Inbox")
}
NavigationLink(destination: SentView()) {
Text("Sent")
}
}
}
.listStyle(SidebarListStyle())
.navigationTitle("Mailboxes")
}
}
struct MainView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
Group {
if viewModel.isFetching {
ProgressView()
} else {
List {
ForEach(0..<20) { i in
NavigationLink(
destination: DetailView(item: "\(i)"),
label: {
Text("Inbox \(i)")
})
}
}
}
}
.navigationTitle("Inbox")
.listStyle(PlainListStyle())
}
}
struct SentView: View {
var body: some View {
List {
ForEach(0..<5) { i in
NavigationLink(
destination: DetailView(item: "\(i)"),
label: {
Text("Sent \(i)")
})
}
}
.navigationTitle("Sent")
.listStyle(PlainListStyle())
}
}
class ViewModel: ObservableObject {
#Published var isFetching = false
init() {
fetchData()
}
func fetchData() {
isFetching = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
self.isFetching = false
}
}
}
When running on iPhone:
When running on iPad:
EDIT:
Replaced code and gif with a more generic implementation for brevity.

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

Why are objects still in memory after emptying NavigationStack path?

I'm trying to implement a Coordinator for managing a flow. The state is stored inside the CoordinatorStore. There are 2 #Published properties for managing the flow. The screen property controls which View is currently shown and path controls the navigation stack of the stack view. Details of the implementation can be found below.
With the current implementation and after the following actions: showA -> showB -> showInitial -> Go to Stack
I would expect that StoreA and StoreB would be deallocated from memory since path, which holds StoreA and StoreB via enum associated values, gets emptied.
But that doesn't happen, and if I repeat the actions again there would be 2 StoreA and 2 StoreB in memory and so on. Am I missing something?
I will also attach a screenshot of the memory debugger snapshot after doing the initial set of actions.
enum Path: Hashable {
case a(StoreA)
case b(StoreB)
}
enum Screen {
case initial
case stack
}
final class CoordinatorStore: ObservableObject {
#Published var path: [Path] = []
#Published var screen: Screen = .stack
func showA() {
let store = StoreA()
path.append(.a(store))
}
func showB() {
let store = StoreB()
path.append(.b(store))
}
func showInitial() {
path = []
screen = .initial
}
func showStack() {
screen = .stack
}
}
struct Coordinator: View {
#ObservedObject var store: CoordinatorStore
var body: some View {
switch store.screen {
case .initial: initial
case .stack: stack
}
}
var stack: some View {
NavigationStack(path: $store.path) {
VStack {
Text("Root")
}
.toolbar {
Button(action: self.store.showA) {
Text("Push A")
}
}
.navigationDestination(for: Path.self) { path in
switch path {
case .a(let store):
ViewA(store: store)
.toolbar {
Button(action: self.store.showB) {
Text("Push B")
}
}
case .b(let store):
ViewB(store: store)
.toolbar {
Button(action: self.store.showInitial) {
Text("Show Initial")
}
}
}
}
}
}
var initial: some View {
VStack {
Text("Initial")
Button(action: store.showStack) {
Text("Go to Stack")
}
}
}
}
struct ViewA: View {
#ObservedObject var store: StoreA
var body: some View {
Text("View A")
}
}
final class StoreA: NSObject, ObservableObject {
deinit {
print("Deinit: \(String(describing: self))")
}
}
struct ViewB: View {
#ObservedObject var store: StoreB
var body: some View {
Text("View B")
}
}
final class StoreB: NSObject, ObservableObject {
deinit {
print("Deinit: \(String(describing: self))")
}
}
I believe this is related but not identical to:
Found a strange behaviour of #State when combined to the new Navigation Stack - Is it a bug or am I doing it wrong?
The Navigation api seems to be prioritizing efficiency (inits are expensive) and that SOMETHING must always be on screen. It doesn't seem to de-initialize views that have been disappeared until it has a replacement initialized and appeared.
That can lead to a memory leak (I believe) if you try to manage Navigation framework views with something outside of the Navigation framework, but it appears as long as the Navigation framework stays in charge things will be de-inted eventually, but not until the new view is init-ed.
NEWER VERSION
This version uses one coordinator, but preserves the separate enums and views for the initial vs main app pathways.
import Foundation
import SwiftUI
enum AppSceneTvTe:Hashable {
case setup
case app
}
enum PathTvTeOptions: Hashable {
case optionA(OptionAVM)
case optionB(OptionBVM)
}
struct SplashTVTEView: View {
#StateObject var oneCoordinator = CoordinatorTvTe()
var body: some View {
NavigationStack(path: $oneCoordinator.path) {
splash
.navigationDestination(for: AppSceneTvTe.self) { scene in
switch scene {
case .app:
SplashTvTeAppRootView().environmentObject(oneCoordinator)
default:
splash
}
}
}
}
var splash: some View {
VStack {
Text("Splash Page")
Button(action:navigateToApp) {
Text("Go App Root")
}
}.navigationBarBackButtonHidden(true)
}
func navigateToApp() {
oneCoordinator.showStack()
}
}
final class CoordinatorTvTe: ObservableObject {
#Published var path = NavigationPath()
func showA() {
path.append(PathTvTeOptions.optionA(OptionAVM()))
}
func showB() {
path.append(PathTvTeOptions.optionB(OptionBVM()))
}
func showInitial() {
unwindAll()
//path = NavigationPath()
}
func showStack() {
path = NavigationPath()
path.append(AppSceneTvTe.app)
}
func unwindAll() {
while !path.isEmpty {
path.removeLast()
}
}
}
struct SplashTvTeAppRootView: View {
#EnvironmentObject var navigation: CoordinatorTvTe
var body: some View {
VStack {
Text("Real Root")
}
.navigationBarBackButtonHidden(true)
.toolbar {
Button(action: self.navigation.showA) {
Text("Push A")
}
}
.navigationDestination(for: PathTvTeOptions.self) { path in
switch path {
case .optionA(let vm):
OptionAView(vm: vm)
.toolbar {
Button(action: self.navigation.showB) {
Text("Push B")
}
}
case .optionB(let vm):
OptionBView(vm: vm)
.toolbar {
Button(action: self.navigation.showInitial) {
Text("Show Initial")
}
}
}
}
}
}
OLDER VERSION
Currently the way out of this is to keep it all in the Navigation Stack so no separate Scene vs. Path.
This code uses a boolean to control the Initial screen, but it could be one of the path options - which is the commented out code.
EDITED TO ADD: Tuns out the boolean solution gets weird when you try to make the initial state true. The Stack keeps winning, so I've taken it out.
enum Path: Hashable {
case initial
case a(StoreA)
case b(StoreB)
}
final class CoordinatorStore: ObservableObject {
#Published var path: [Path] = [.initial]
func showA() {
let store = StoreA()
path.append(.a(store))
}
func showB() {
let store = StoreB()
path.append(.b(store))
}
func showInitial() {
path = []
path.append(.inital)
}
func showStack() {
path = []
}
}
struct Coordinator: View {
#ObservedObject var store: CoordinatorStore
var body: some View {
NavigationStack(path: $store.path) {
VStack {
Text("Real Root")
}
.toolbar {
Button(action: self.store.showA) {
Text("Push A")
}
}
.navigationDestination(for: Path.self) { path in
switch path {
case .a(let store):
ViewA(store: store)
.toolbar {
Button(action: self.store.showB) {
Text("Push B")
}
}
case .b(let store):
ViewB(store: store)
.toolbar {
Button(action: self.store.showInitial) {
Text("Show Initial")
}
}
case .initial:
initial
}
}
}
}
var initial: some View {
VStack {
Text("Initial")
Button(action: store.showStack) {
Text("Go to Stack")
}
}.navigationBarBackButtonHidden(true)
}
}

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! }
}
}
}

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