I have a common feature in my app where users can report posts but tapping a button and selecting "Report" from an action sheet.
For some reason my action-sheet title, .destructive AND .default buttons display darker than they should while the .cancel button displays as it should, see image below..
Below is my code and structure. I do not have any Extensions for ActionSheet that would be doing this and it occurs globally in my app, why could this be happening?
Thank you
Parent List View for each post
struct NewsFeed: View {
#EnvironmentObject var session : SessionStore
#StateObject var newsfeedVM = NewsFeedViewModel()
var body: some View {
NavigationView {
List {
ForEach(newsfeedVM.posts, id: \.id) { post in
PostCell(post: post)
}
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
PostCell subview with ActionSheet
struct PostCell: View {
#StateObject var postVM = PostViewModel()
#State var post: Post
var body: some View {
VStack {
PostHeadline(postVM: postVM, post: $post)
PostMedia(postVM: postVM, post: $post)
CommentCell(postVM: postVM, post: $post)
}
.actionSheet(isPresented: $postVM.showActionSheet) {
ActionSheet(title: Text("Report a post"), buttons: [
.destructive(Text("Report")) {
self.reportPost(post: post)
},
.cancel() {
postVM.selectedPost = ""
}
])
}
}
func reportPost(post: Post) {/// }
}
Related
I have a simple view:
struct AccountView: View {
#State private var showingLogoutAlert = false
var body: some View {
Button("Log out!") {
showingLogoutAlert = true
}
.alert(isPresented: $showingLogoutAlert, content: {
Alert(title: Text("Log out of Flow?"),
primaryButton: .cancel(),
secondaryButton: .destructive(Text("Log out"), action: {
//logOut()
}))
})
}
}
When you tap the button, it will show an alert. The problem is, after the alert dismisses, the back button also disappears! I have no idea why this is happening. See the gif below.
I've tried to reduce the problem down to the bare minimum code. Replicated it on a separate, new app. Still the same issue.
this is the working code I used in my test, on MacOS 13.2, Xcode 14.2, tested on real ios 16.3 devices (not Previews), and macCatalyst.
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(destination: AccountView()) {
Text("AccountView")
}
}
}
}
struct AccountView: View {
#State private var showingLogoutAlert = false
var body: some View {
Button("Log out!") {
showingLogoutAlert = true
}
.alert(isPresented: $showingLogoutAlert, content: {
Alert(title: Text("Log out of Flow?"),
primaryButton: .cancel(),
secondaryButton: .destructive(Text("Log out"), action: {
//logOut()
}))
})
}
}
Abstract
I'm creating an app that allows for content creation and display. The UX I yearn for requires the content creation view to use programmatic navigation. I aim at architecture with a main view model and an additional one for the content creation view. The problem is, the content creation view model does not work as I expected in this specific example.
Code structure
Please note that this is a minimal reproducible example.
Suppose there is a ContentView: View with a nested AddContentPresenterView: View. The nested view consists of two phases:
specifying object's name
summary screen
To allow for programmatic navigation with NavigationStack (new in iOS 16), each phase has an associated value.
Assume that AddContentPresenterView requires the view model. No workarounds with #State will do - I desire to learn how to handle ObservableObject in this case.
Code
ContentView
struct ContentView: View {
#EnvironmentObject var model: ContentViewViewModel
var body: some View {
VStack {
NavigationStack(path: $model.path) {
List(model.content) { element in
Text(element.name)
}
.navigationDestination(for: Content.self) { element in
ContentDetailView(content: element)
}
.navigationDestination(for: Page.self) { page in
AddContentPresenterView(page: page)
}
}
Button {
model.navigateToNextPartOfContentCreation()
} label: {
Label("Add content", systemImage: "plus")
}
}
}
}
ContentDetailView (irrelevant)
struct ContentDetailView: View {
let content: Content
var body: some View {
Text(content.name)
}
}
AddContentPresenterView
As navigationDestination associates a destination view with a presented data type for use within a navigation stack, I found no better way of adding a paged view to be navigated using the NavigationStack than this.
extension AddContentPresenterView {
var contentName: some View {
TextField("Name your content", text: $addContentViewModel.contentName)
.onSubmit {
model.navigateToNextPartOfContentCreation()
}
}
var contentSummary: some View {
VStack {
Text(addContentViewModel.contentName)
Button {
model.addContent(addContentViewModel.createContent())
model.navigateToRoot()
} label: {
Label("Add this content", systemImage: "checkmark.circle")
}
}
}
}
ContentViewViewModel
Controls the navigation and adding content.
class ContentViewViewModel: ObservableObject {
#Published var path = NavigationPath()
#Published var content: [Content] = []
func navigateToNextPartOfContentCreation() {
switch path.count {
case 0:
path.append(Page.contentName)
case 1:
path.append(Page.contentSummary)
default:
fatalError("Navigation error.")
}
}
func navigateToRoot() {
path.removeLast(path.count)
}
func addContent(_ content: Content) {
self.content.append(content)
}
}
AddContentViewModel
Manages content creation.
class AddContentViewModel: ObservableObject {
#Published var contentName = ""
func createContent() -> Content {
return Content(name: contentName)
}
}
Page
Enum containing creation screen pages.
enum Page: Hashable {
case contentName, contentSummary
}
What is wrong
Currently, for each page pushed onto the navigation stack, a new StateObject is created. That makes the creation of object impossible, since the addContentViewModel.contentName holds value only for the bound screen.
I thought that, since StateObject is tied to the view's lifecycle, it's tied to AddContentPresenterView and, therefore, I would be able to share it.
What I've tried
The error is resolved when addContentViewModel in AddContentPresenterView is an EnvironmentObject initialized in App itself. Then, however, it's tied to the App's lifecycle and subsequent content creations greet us with stale data - as it should be.
Wraping up
How to keep SwiftUI from creating additional StateObjects in this custom page view?
Should I resort to ObservedObject and try some wizardry? Should I just implement a reset method for my AddContentViewModel and reset the data on entering or quiting the screen?
Or maybe there is a better way of achieving what I've summarized in abstract?
If you declare #StateObject var addContentViewModel = AddContentViewModel() in your AddContentPresenterView it will always initialise new AddContentViewModel object when you add AddContentPresenterView in navigation stack. Now looking at your code and app flow I don't fill you need AddContentViewModel.
First, update your contentSummary of the Page enum with an associated value like this.
enum Page {
case contentName, contentSummary(String)
}
Now update your navigate to the next page method of your ContentViewModel like below.
func navigateToNextPage(_ page: Page) {
path.append(page)
}
Now for ContentView, I think you need to add VStack inside NavigationStack otherwise that bottom plus button will always be visible.
ContentView
struct ContentView: View {
#EnvironmentObject var model: ContentViewViewModel
var body: some View {
NavigationStack(path: $model.path) {
VStack {
List(model.content) { element in
Text(element.name)
}
.navigationDestination(for: Content.self) { element in
ContentDetailView(content: element)
}
.navigationDestination(for: Page.self) { page in
switch page {
case .contentName: AddContentView()
case .contentSummary(let name): ContentSummaryView(contentName: name)
}
}
Button {
model.navigateToNextPage(.contentName)
} label: {
Label("Add content", systemImage: "plus")
}
}
}
}
}
So now it will push destination view on basis of the type of the Page. So you can remove your AddContentPresenterView and add AddContentView and ContentSummaryView.
AddContentView
struct AddContentView: View {
#EnvironmentObject var model: ContentViewViewModel
#State private var contentName = ""
var body: some View {
TextField("Name your content", text: $contentName)
.onSubmit {
model.navigateToNextPage(.contentSummary(contentName))
}
}
}
ContentSummaryView
struct ContentSummaryView: View {
#EnvironmentObject var model: ContentViewViewModel
let contentName: String
var body: some View {
VStack {
Text(contentName)
Button {
model.addContent(Content(name: contentName))
model.navigateToRoot()
} label: {
Label("Add this content", systemImage: "checkmark.circle")
}
}
}
}
So as you can see I have used #State property in AddContentView to bind it with TextField and on submit I'm passing it as an associated value with contentSummary. So this will reduce the use of AddContentViewModel. So now there is no need to reset anything or you want face any issue of data loss when you push to ContentSummaryView.
I have a main tab bar that has three tabs, in the first tab I have a background task that may return an error, this error is presented by an alert view. now if I moved to any tab views in the app while the background task is running and an error occurred the alert will present on the current view instead of showing in the first tab view.
struct FirstTabView: View {
// viewModel will fire the background task after init
#StateObject var viewModel: FirstViewModel = .init()
var body: some View {
Text("Hello First")
.alert("error", isPresented: .init(get: {
return viewModel.errorMessage != nil
}, set: { _ in
viewModel.errorMessage = nil
})) {
Button("OK") {
}
}
}
}
how can I limit the error alert to be presented on the first tab only?
One solution could be move the alert to the main TabView, rather than having it shown in the child view. By doing that, you will be able to track what tab is selected and trigger the alert only when both conditions are true:
the first tab is selected
the view-model's property errorMessage is not nil
The trigger is a dedicated showAlert state property in your TabView view, that will change whenever the first tab appears on the screen.
In the example here below, you can change your view-model's property from the second view, but the alert will only be shown when you move to the first tab; I hope this is what you are looking for:
// The model must be an observable class
class MyModel: ObservableObject {
// The error message must be a published property
#Published var errorMessage: String? = nil
}
struct MyTabs: View {
// viewModel will fire the background task after init
let viewModel = MyModel() // Use your ViewModel as applicable
#State private var tabSelection = 0 // This property will track the selected tab
#State private var showAlert = false // This property is the trigger to the alert
var body: some View {
TabView(selection: $tabSelection) { // The selection: parameter tracks the selected tab through the .tag()
FirstTabView()
.environmentObject(viewModel) // Pass the same model to the Views in each tab
.tabItem { Text("First") }
.tag(0) // This is View #0 for the tabSelection property
.onAppear {
// Only when this View appears the showAlert will be set to true,
// only if there is an error in the model's property and the first tab is selected
if viewModel.errorMessage != nil && tabSelection == 0 {
showAlert = true
}
}
Second()
.environmentObject(viewModel) // Pass the same model to the Views in each tab
.tabItem { Text("Second") }
.tag(1) // This is View #1 for the tabSelection property
}
// Trigger the alert in the TabView, instead of in the child View
.alert("error", isPresented: $showAlert) {
Button {
viewModel.errorMessage = nil
} label: {
Text("OK")
}
} message: {
Text(viewModel.errorMessage ?? "not available")
}
}
}
struct FirstTabView: View {
#EnvironmentObject var viewModel: MyModel
var body: some View {
VStack {
Text("Hello First")
.padding()
Text("\(viewModel.errorMessage ?? "OK")")
.padding()
}
}
}
struct Second: View {
#EnvironmentObject var viewModel: MyModel
var body: some View {
VStack {
Text("Second")
.padding()
Text("\(viewModel.errorMessage ?? "OK")")
.padding()
Button {
viewModel.errorMessage = "error"
} label: {
Text("Show alert")
}
}
}
}
One way to handle it is by updating the badge icon on the first tab when the error occurs. Then the user can finish off what they are currently doing and then inspect the first tab when they notice it has updated, say with an exclamation mark badge. At that point, you could present the alert.
I have condition to show alert in a view which can able to show from anywhere in the app. Like I want to present it from root view so it can possibly display in all view. Currently what happens when I present from very first view it will display that alert until i flow the same Navigation View. Once any sheets open alert is not displayed on it. Have any solutions in SwiftUI to show alert from one place to entire app.
Here is my current Implementation of code.
This is my contentView where the sheet is presented and also alert added in it.
struct ContentView: View {
#State var showAlert: Bool = false
#State var showSheet: Bool = false
var body: some View {
NavigationView {
Button(action: {
showSheet = true
}, label: {
Text("Show Sheet")
}).padding()
.sheet(isPresented: $showSheet, content: {
SheetView(showAlert: $showAlert)
})
}
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Alert"))
})
}
}
Here from sheet I am toggle the alert and the alert is not displayed.
struct SheetView: View {
#Binding var showAlert: Bool
var body: some View {
Button(action: {
showAlert = true
}, label: {
Text("Show Alert")
})
}
}
here is the error in debug when we toggle button
AlertDemo[14187:3947182] [Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x109009c00> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x103908b50> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x103908b50>) which is already presenting <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x103d05f50>.
Any solution for that in SwiftUI? Thanks in Advance.
I was able to achieve this with this simplified version of what #workingdog suggested in their answer. It works as follows:
create the Alerter class that notifies the top-level and asks to display an alert
class Alerter: ObservableObject {
#Published var alert: Alert? {
didSet { isShowingAlert = alert != nil }
}
#Published var isShowingAlert = false
}
render the alert at the top-most level, for example in your #main struct or the ContentView
#main
struct MyApp: App {
#StateObject var alerter: Alerter = Alerter()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(alerter)
.alert(isPresented: $alerter.isShowingAlert) {
alerter.alert ?? Alert(title: Text(""))
}
}
}
}
set the alert that should be displayed from inside a child view
struct SomeChildView: View {
#EnvironmentObject var alerter: Alerter
var body: some View {
Button("show alert") {
alerter.alert = Alert(title: Text("Hello from SomeChildView!"))
}
}
}
Note on sheets
If you present views as sheets, each sheet needs to implement its own alert, just like MyApp does above.
If you have a NavigationView inside your sheet and present other views within this navigation view in the same sheet, the subsequent sheets can use the first sheet's alert, just like SomeChildView does in my example above.
Here is a possible example solution to show an Alert anywhere in the App.
It uses "Environment" and "ObservableObject".
import SwiftUI
#main
struct TestApp: App {
#StateObject var alerter = Alerter()
var body: some Scene {
WindowGroup {
ContentView().environment(\.alerterKey, alerter)
.alert(isPresented: $alerter.showAlert) {
Alert(title: Text("This is the global alert"),
message: Text("... alert alert alert ..."),
dismissButton: .default(Text("OK")))
}
}
}
}
struct AlerterKey: EnvironmentKey {
static let defaultValue = Alerter()
}
extension EnvironmentValues {
var alerterKey: Alerter {
get { return self[AlerterKey] }
set { self[AlerterKey] = newValue }
}
}
class Alerter: ObservableObject {
#Published var showAlert = false
}
struct ContentView: View {
#Environment(\.alerterKey) var theAlerter
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: SecondView()) {
Text("Click for second view")
}.padding(20)
Button(action: { theAlerter.showAlert.toggle()}) {
Text("Show alert here")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SecondView: View {
#Environment(\.alerterKey) var theAlerter
var body: some View {
VStack {
Button(action: { theAlerter.showAlert.toggle()}) {
Text("Show alert in second view")
}
}
}
}
I'm using a custom NavigationButton from this answer to be able to load data for a specific view when navigating to a new view from a list. It works great, but the problem is when I attempt to use it inside a ForEach loop I end up with the multiple navigation links with multiple isActive #State variables that are all being told to push a new view, which causes SwiftUI to navigate to the new view then automatically back to parent view (bug). How can I preserve the functionality of being able to fire code to run while navigating to a new view without triggering the bug.
ForEach(workouts) { workout in
NavigationButton(
action: {
//load data for Destination
},
destination: {
WorkoutDetailView().environmentObject(dataStore)
},
workoutRow: { WorkoutRow(trackerWorkout: workout) }
)
}
struct NavigationButton<Destination: View, WorkoutRow: View>: View {
var action: () -> Void = { }
var destination: () -> Destination
var workoutRow: () -> WorkoutRow
#State private var isActive: Bool = false
var body: some View {
Button(action: {
self.action()
self.isActive.toggle()
}) {
workoutRow()
.background(
ScrollView { // Fixes a bug where the navigation bar may become hidden on the pushed view
NavigationLink(destination: LazyDestination { self.destination() },
isActive: self.$isActive) { EmptyView() }
}
)
}
}
}
// This view lets us avoid instantiating our Destination before it has been pushed.
struct LazyDestination<Destination: View>: View {
var destination: () -> Destination
var body: some View {
self.destination()
}
}