SwiftUI Changing Background Color - ios

The first image shows the simulated application using Xcode
and the second image is a mockup design for the application.
Thus I want to change the background color as white and the list items as gray oppositely.
But, I tried tons of ways to set the backgrounds but none of them did not work correctly.
I just wanted to set the background color as white first, and I failed.
ListView.swift
struct ListView: View
{
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var dateHolder: DateHolder
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \TaskItem.dueDate, ascending: true)],
animation: .default)
private var items: FetchedResults<TaskItem>
var body: some View
{
NavigationView
{
VStack
{
ZStack
{
List
{
ForEach(items)
{ item in
NavigationLink(destination: EditTaskView(
passedTaskItem: item,
initialDate: Date())
.environmentObject(dateHolder))
{
Task(passedTaskItem: item)
.environmentObject(dateHolder)
}
}
.onDelete(perform: deleteItems)
}
.toolbar
{
ToolbarItem(placement: .navigationBarTrailing)
{
EditButton()
}
}
FloatingButton().environmentObject(dateHolder)
}
}
.navigationTitle("To-Do List")
}
}
private func deleteItems(offsets: IndexSet)
{
withAnimation
{
offsets.map { items[$0] }.forEach(viewContext.delete)
dateHolder.saveContext(viewContext)
}
}
}
Main.swift
#main
struct ToDoListApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
let context = persistenceController.container.viewContext
let dateHolder = DateHolder(context)
ListView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(dateHolder)
}
}
}
These are swift codes that I'm using for this application.
Is there any solution that might resolve this situation?

Related

Refresh List After a New Entity is Added in Core Data for SwiftUI App

I am building a small app using SwiftUI and Core Data. I have a main view, which launches the sheet. The sheet allows me to add a new movie to the SQLite database through Core Data. But I am having a hard time to refresh the parent view once the sheet is dismissed.
ContentView
struct ContentView: View {
#State private var isPresented: Bool = false
#StateObject private var vm = MovieListViewModel()
var body: some View {
NavigationView {
VStack {
List(vm.movies) { movie in
Text(movie.title)
}
}
.navigationTitle("Movies")
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add Movie") {
isPresented = true
}
}
})
.sheet(isPresented: $isPresented, content: {
AddMovieView()
})
.onAppear {
try? vm.populateMovies()
}.padding()
}
}
}
AddMovieView
struct AddMovieView: View {
#Environment(\.dismiss) private var dismiss
#StateObject private var vm = AddMovieViewModel()
var body: some View {
Form {
TextField("Title", text: $vm.title)
Button("Save") {
do {
try vm.saveMovie()
dismiss()
} catch {
print(error.localizedDescription)
}
}
}
}
}
Do I need to call vm.populateMovies() on the onDismiss function of the sheet from the ContentView?
You can use a #FetchRequest as follows:
struct ContentView: View {
#FetchRequest(sortDescriptors: []) var movies: FetchedResults<Movie>
#State private var isPresented: Bool = false
#StateObject private var vm = MovieListViewModel()
var body: some View {
NavigationView {
VStack {
List(movies) { movie in
Text(movie.title)
}
}
.navigationTitle("Movies")
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add Movie") {
isPresented = true
}
}
})
.sheet(isPresented: $isPresented, content: {
AddMovieView()
})
.padding()
}
}
}
You won't need a populateMovies() as the FetchRequest result is automatically populated.

SwiftUI TabView - run code in subview after sequential taps

I am trying to implement the behavior in a TabView when the user taps the same tab multiple times, such as in the iOS AppStore app. First tap: switch to that view, second tap: pop to root, third tap: scroll to the top if needed.
The code below works fine for switching and didTap() is called for every tap.
import SwiftUI
enum Tab: String {
case one
case two
}
struct AppView: View {
#State private var activeTab = Tab.one
var body: some View {
TabView(selection: $activeTab.onChange(didTap)) {
One()
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
Two()
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
func didTap(to value: Tab) {
print(value) // this captures every tap
}
}
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler(newValue)
}
)
}
}
What I am struggling with, is how to tell either One or Two that it was tapped for a second or third time? (How to pop and scroll is not the issue).
I have seen this: TabView, tabItem: running code on selection or adding an onTapGesture but it doesn't explain how to run code in one of the views.
Any suggestions?
You can record additional taps (of same value) in an array. The array count gives you the number of taps on the same Tab.
EDIT: now with explicit subview struct.
struct ContentView: View {
#State private var activeTab = Tab.one
#State private var tapState: [Tab] = [Tab.one] // because .one is default
var body: some View {
TabView(selection: $activeTab.onChange(didTap)) {
SubView(title: "One", tapCount: tapState.count)
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
SubView(title: "Two", tapCount: tapState.count)
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
func didTap(to value: Tab) {
print(value) // this captures every tap
if tapState.last == value {
tapState.append(value) // apped next tap if same value
print("tapped \(tapState.count) times")
} else {
tapState = [value] // reset tap state to new tab selection
}
}
}
struct SubView: View {
let title: String
let tapCount: Int
var body: some View {
VStack {
Text("Subview \(title)").font(.title)
Text("tapped \(tapCount) times")
}
}
}
Although the answer by #ChrisR did answer my question, I couldn't figure out the next step, i.e. the logic when to pop-to-root or scroll-to-the-top based on the number of taps for a SubView. After lots of reading and trial and error, I recently came across this article: https://notificare.com/blog/2022/11/25/a-better-tabview-in-swiftui/
Inspired by this article, but with some modifications, I came up with the following which does exactly what I was looking for.
The two main changes are:
An EmptyView with an id is added as the first (but invisible) row in the List to be used as an anchor by proxy.scrollTo().
Instead of the global #StateObject var appState that stores the navigation paths for the subviews, I added the paths as separate #State properties. This avoids the Update NavigationAuthority bound path tried to update multiple times per frame. warning.
Hopefully this is helpful for someone.
enum Tab: String {
case one
case two
}
struct ContentView: View {
#State var selectedTab = Tab.one
#State var oneNavigationPath = NavigationPath()
#State var twoNavigationPath = NavigationPath()
var body: some View {
ScrollViewReader { proxy in
TabView(selection: tabViewSelectionBinding(proxy: proxy)) {
SubView(title: "One", path: $oneNavigationPath)
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
SubView(title: "Two", path: $twoNavigationPath)
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
}
private func tabViewSelectionBinding(proxy: ScrollViewProxy) -> Binding<Tab> {
Binding<Tab>(
get: { selectedTab },
set: { newValue in
if selectedTab == newValue {
switch selectedTab {
case .one:
if oneNavigationPath.isEmpty {
withAnimation {
proxy.scrollTo(Tab.one, anchor: .bottom)
}
} else {
withAnimation {
oneNavigationPath = NavigationPath()
}
}
case .two:
if twoNavigationPath.isEmpty {
withAnimation {
proxy.scrollTo(Tab.two, anchor: .bottom)
}
} else {
withAnimation {
twoNavigationPath = NavigationPath()
}
}
}
}
selectedTab = newValue
}
)
}
}
struct SubView: View {
let title: String
let items = Array(1 ... 100)
#Binding var path: NavigationPath
var body: some View {
NavigationStack(path: $path) {
List {
EmptyView()
.id(Tab(rawValue: title.lowercased()))
ForEach(items, id: \.self) { item in
NavigationLink(value: item) {
Text("Item \(item)")
}
}
}
.navigationTitle(title)
.navigationDestination(for: Int.self) { item in
Text("Item \(item)")
}
}
}
}

The #ObservedResults loads old data in View on deletion, creation or update

Description
I've got simple Combat model which stores name and list of actors. When I delete the Combat from List using onDelete it looks like it's working. It removes the Combat from Realm (checked with RealmStudio) and updates the view. However, if view gets redrawn (for instance, when switching Apps), the "old" data is loaded again (the very first loaded on app initialization), so all deleted rows are back again. Of course, removing them again crashes the app, because they are not present in #ObservedResults combats anymore. Restarting the app fixes the issue, because new data is loaded to #ObservedResults combats and to List, but then again, when I removed something it will be back on review draw...
What I discovered is that removing .sheet() fixes the issue! (EDIT: clarification; it doesn't matter what's inside of the sheet, it may be even empty) The view is updated correctly on redraw! The Sheet is used to display form to add new Combat (nether to say that adding new combats or editing them does not update the view as well, but let's focus on deletion). I have no idea what adding sheet() changes in behaviour of the List and "listening" to #ObservedResults combats.
As a test I used simple array of Combat classes and everything worked. So it points me to issue with #ObservedResults.
I was using the Alert before and all changes to #ObservedResults combats were seen at glance. Now I wanted to replace Alert with Sheet and… That happened.
Also, I have subview where I have almost identical code for actor and there everything works, however I use #ObservedRealmObject var combat: Combat there, and I pass the combat #ObservedResults combats, like so:
NavigationLink(destination: CombatView(combat: combat)) { Text(combat.name) }
I removed unecessary code from below examples to keep it at minimum.
Model
The Combat model:
class Combat: Object, ObjectKeyIdentifiable {
#objc dynamic var id: String = UUID().uuidString
#objc dynamic var name: String = ""
var actors = List<Actor>()
}
Actual View Code (broken using Sheet)
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
struct CombatsListView: View {
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
var body: some View {
List {
ForEach(combats) { combat in
Text(combat.name)
}.onDelete(perform: $combats.remove)
}
.sheet(isPresented: $showAddCombat) {
AddCombatView( showAddCombat: $showAddCombat)
}
}
}
Old View Code (works using Alert)
struct CombatsListView: View {
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
#State private var showAddCombat = false
#State private var addCombatNewName = ""
var body: some View {
List(combats) { combat in
Text(combat.name)
.onDelete(perform: $combats.remove)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAlert = true
}) {
Image(systemName: "plus" )
.font(.title)
Text("New Combat")
}.alert("New Combat", isPresented: $showAlert) {
TextField("write name", text: $addCombatNewName)
Button("Close", role: .cancel) {
addCombatNewName = ""
}
Button("Add") {
addNewCombat(name: addCombatNewName)
addCombatNewName = ""
}
}
}
}
}
private func addNewCombat(name: String) {
let newCombat = Combat()
newCombat.name = name
do {
try self.realm.write {
realm.add(newCombat)
}
} catch {
fatalError("Error: \(error)")
}
}
}
EDITED
I just found some new behaviour. I made a new simple view which lists elements of Collection list and you can delete or add new Collection. It works just fine, but if I include this CollectionsView under the TabView, then the effect is exactly the same as in the example above. The view stops working properly: deleted items are added back on view redraw and adding new objects doesn't refresh the View.
This makes me think more of a bug in #ObservedResults().
Below is the source code.
class Collection: Object, ObjectKeyIdentifiable {
#objc dynamic var id: String = UUID().uuidString
#objc dynamic var name: String = ""
var actors = List<Actor>()
}
#main
struct CombatTrackerApp: App {
var body: some Scene {
WindowGroup {
Tabber() // will not work
// CollectionsView() // will work
}
}
}
struct CollectionsView: View {
#ObservedResults( Collection.self ) var collections
#State private var showNewCollectionForm = false
var body: some View {
NavigationStack {
List {
ForEach(collections) { collection in
Text(collection.name)
}.onDelete(perform: $collections.remove)
}
.listStyle(.inset)
.padding()
.navigationTitle("Collections")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button() {
self.showNewCollectionForm.toggle()
} label: {
Image(systemName: "plus")
Text("Add New Collection")
}
}
}
.sheet(isPresented: $showNewCollectionForm) {
NewCollectionView( showNewCollectionForm: $showNewCollectionForm )
}
}
}
}
struct NewCollectionView: View {
let realm = try! Realm()
#Binding var showNewCollectionForm: Bool
#State private var newCollectioName: String = ""
var body: some View {
NavigationStack {
VStack {
Text("Create new Collection").font(.title).padding()
Form {
TextField("Name", text: $newCollectioName)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Close", role: .cancel) {
showNewCollectionForm.toggle()
}
}
ToolbarItem {
Button("Create") {
addCollection()
} .disabled(newCollectioName.isEmpty)
}
}
}
}
private func addCollection() {
let newCollection = Collection()
newCollection.name = newCollectioName
do {
try realm.write {
realm.add(newCollection)
}
} catch {
print("Cannot add new Collection", error)
}
showNewCollectionForm.toggle()
}
}
struct Tabber: View {
var body: some View {
TabView() {
NavigationStack {
CombatsListView()
}
.tabItem {
Text("Combats")
}
NavigationStack {
CollectionsView()
}
.tabItem {
Text("Collections")
}
SettingsView()
.tabItem {
Text("Settings")
}
}
}
}
I found out the solution (but I still don't understand why it's working).
The solution was to move NavigationStack from my TabView to the subviews. So instead of:
struct Tabber: View {
var body: some View {
TabView() {
NavigationStack {
CombatsListView()
}
.tabItem {
Text("Combats")
}
//...
I should do:
struct Tabber: View {
var body: some View {
TabView() {
CombatsListView()
.tabItem {
Text("Combats")
}
//...
struct CombatsListView: View {
var body: some View {
NavigationStack {
Confusing part was that all online tutorials and Apple Documentation suggests to wrap subviews with NavigationStack in TabView directly instead of adding NavigationStack in subviews. Maybe it's a bug, maybe it's a feature.

Core Data Object still saved on dismiss/cancel in presentationMode SwiftUI

When I'm trying to dismiss/cancel an Add Object Modal, it is creating an empty object instead of just cancelling.
I've tried deleteObject, context.rollback(), and a bunch of other random things. Would love some help and able to answer any questions.
I realize that this isn't an issue by putting the Cancel button in a NavigationBarItem but would like to be able to understand how to make an separate "cancel (or dismiss)" button.
ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Game.gameName, ascending: true)]) var games: FetchedResults<Game>
#State private var showingAddGame = false
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
ForEach(self.games, id: \.self) { games in
NavigationLink(destination: GameGoalsDetail(game: games)) {
VStack(alignment: .leading) {
Text(games.gameName ?? "Unknown Game")
Text(games.gameDescription ?? "Unknown Game Description")
}
}
}
.onDelete(perform: self.removeGames)
}
.navigationBarItems(leading:
HStack {
Button(action: {
self.showingAddGame.toggle()
}) {
Text("Add Game")
.padding(.top, 50)
.foregroundColor(Color.yellow)
}.sheet(isPresented: self.$showingAddGame) {
AddGameView().environment(\.managedObjectContext, self.moc)
}
Image("Game Goals App Logo")
.resizable()
.frame(width: 100, height: 100)
.padding(.leading, (geometry.size.width / 2.0) + -160)
.padding(.bottom, -50)
}, trailing:
EditButton()
.padding(.top, 50)
.foregroundColor(Color.yellow)
)
}
}
}
func removeGames(at offsets: IndexSet) {
for index in offsets {
let game = games[index]
moc.delete(game)
}
try? moc.save()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let newGame = Game(context: context)
newGame.gameName = "Apex Legends"
newGame.gameDescription = "Maybe this will work"
return ContentView().environment(\.managedObjectContext, context)
}
}
AddGameView.swift
import SwiftUI
import CoreData
struct AddGameView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#Environment(\.presentationMode) var presentationMode
#State private var gameName = ""
#State private var gameDescription = ""
#State private var showingAlert = false
var body: some View {
Form {
Section {
TextField("Game Name", text: $gameName)
TextField("Game Description", text: $gameDescription)
}
HStack {
Button("Add Game") {
let newGame = Game(context: self.moc)
newGame.gameName = self.gameName
newGame.gameDescription = self.gameDescription
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.padding(10)
.foregroundColor(Color.white)
.background(Color.red)
}
}
}
}
struct AddGameView_Previews: PreviewProvider {
static var previews: some View {
AddGameView()
}
}
I've searched all over so if there is something out there that I've missed as far as a stackoverflow post, please link it as I'd like to not only fix this but understand why.
Your Cancel button is not creating an empty object. The problem is that the whole row in your form that has Add and Cancel buttons is interactive and triggers actions of your both buttons.
I have found an answer here: https://stackoverflow.com/a/59402642/12315994
To keep your current layout you need to simply add one line to each of your buttons:
.buttonStyle(BorderlessButtonStyle())
After this, only taping on each button will trigger actions. Form's row with the buttons will not be clickable.
There are 2 other solutions. Both are to move your buttons out of Form.
Solution 1
is to move buttons to NavigationBarItems like this:
import SwiftUI
import CoreData
struct AddGameView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#Environment(\.presentationMode) var presentationMode
#State private var gameName = ""
#State private var gameDescription = ""
#State private var showingAlert = false
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Game Name", text: $gameName)
TextField("Game Description", text: $gameDescription)
}
}
}
.navigationBarItems(
leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.padding(10)
.foregroundColor(Color.white)
.background(Color.red)
,
trailing:
Button(action: {
let newGame = Game(context: self.moc)
newGame.gameName = self.gameName
newGame.gameDescription = self.gameDescription
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}) {
Text("Add Game")
}
)
}
}
}
Solution 2
Is to move buttons out of Form and move them to the bottom of the screen. Like this:
import SwiftUI
import CoreData
struct AddGameView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Game.entity(), sortDescriptors: []) var games: FetchedResults<Game>
#Environment(\.presentationMode) var presentationMode
#State private var gameName = ""
#State private var gameDescription = ""
#State private var showingAlert = false
var body: some View {
VStack {
Form {
Section {
TextField("Game Name", text: $gameName)
TextField("Game Description", text: $gameDescription)
}
}
HStack {
Button(action: {
let newGame = Game(context: self.moc)
newGame.gameName = self.gameName
newGame.gameDescription = self.gameDescription
do {
try self.moc.save()
self.presentationMode.wrappedValue.dismiss()
} catch {
print("Whoops! \(error.localizedDescription)")
}
}) {
Text("Add Game")
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.padding(10)
.foregroundColor(Color.white)
.background(Color.red)
}
}
}
}
Both options are better than your current layout from the UX point of view, because buttons are now in more standard locations. Especially version 1 is a more standard way of presenting buttons like this in iOS.

Dismiss NavigationView when Hidden SwiftUI [duplicate]

I was playing around with SwiftUI and want to be able to come back to the previous view when tapping a button, the same we use popViewController inside a UINavigationController.
Is there a provided way to do it so far ?
I've also tried to use NavigationDestinationLink to do so without success.
struct AView: View {
var body: some View {
NavigationView {
NavigationButton(destination: BView()) {
Text("Go to B")
}
}
}
}
struct BView: View {
var body: some View {
Button(action: {
// Trying to go back to the previous view
// previously: navigationController.popViewController(animated: true)
}) {
Text("Come back to A")
}
}
}
Modify your BView struct as follows. The button will perform just as popViewController did in UIKit.
struct BView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
Button(action: { self.mode.wrappedValue.dismiss() })
{ Text("Come back to A") }
}
}
Use #Environment(\.presentationMode) var presentationMode to go back previous view. Check below code for more understanding.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.gray.opacity(0.2)
NavigationLink(destination: NextView(), label: {Text("Go to Next View").font(.largeTitle)})
}.navigationBarTitle(Text("This is Navigation"), displayMode: .large)
.edgesIgnoringSafeArea(.bottom)
}
}
}
struct NextView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Color.gray.opacity(0.2)
}.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "arrow.left") }))
.navigationBarTitle("", displayMode: .inline)
}
}
struct NameRow: View {
var name: String
var body: some View {
HStack {
Image(systemName: "circle.fill").foregroundColor(Color.green)
Text(name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
With State Variables. Try that.
struct ContentViewRoot: View {
#State var pushed: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination:ContentViewFirst(pushed: self.$pushed), isActive: self.$pushed) { EmptyView() }
.navigationBarTitle("Root")
Button("push"){
self.pushed = true
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentViewFirst: View {
#Binding var pushed: Bool
#State var secondPushed: Bool = false
var body: some View {
VStack{
NavigationLink(destination: ContentViewSecond(pushed: self.$pushed, secondPushed: self.$secondPushed), isActive: self.$secondPushed) { EmptyView() }
.navigationBarTitle("1st")
Button("push"){
self.secondPushed = true;
}
}
}
}
struct ContentViewSecond: View {
#Binding var pushed: Bool
#Binding var secondPushed: Bool
var body: some View {
VStack{
Spacer()
Button("PopToRoot"){
self.pushed = false
} .navigationBarTitle("2st")
Spacer()
Button("Pop"){
self.secondPushed = false
} .navigationBarTitle("1st")
Spacer()
}
}
}
This seems to work for me on watchOS (haven't tried on iOS):
#Environment(\.presentationMode) var presentationMode
And then when you need to pop
self.presentationMode.wrappedValue.dismiss()
There is now a way to programmatically pop in a NavigationView, if you would like. This is in beta 5.
Notice that you don't need the back button. You could programmatically trigger the showSelf property in the DetailView any way you like. And you don't have to display the "Push" text in the master. That could be an EmptyView(), thereby creating an invisible segue.
(The new NavigationLink functionality takes over the deprecated NavigationDestinationLink)
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
struct MasterView: View {
#State var showDetail = false
var body: some View {
VStack {
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
Text("Push")
}
}
}
}
struct DetailView: View {
#Binding var showSelf: Bool
var body: some View {
Button(action: {
self.showSelf = false
}) {
Text("Pop")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
It seems that a ton of basic navigation functionality is super buggy, which is disappointing and may be worth walking away from for now to save hours of frustration. For me, PresentationButton is the only one that works. TabbedView tabs don't work properly, and NavigationButton doesn't work for me at all. Sounds like YMMV if NavigationButton works for you.
I'm hoping that they fix it at the same time they fix autocomplete, which would give us much better insight as to what is available to us. In the meantime, I'm reluctantly coding around it and keeping notes for when fixes come out. It sucks to have to figure out if we're doing something wrong or if it just doesn't work, but that's beta for you!
Update: the NavigationDestinationLink API in this solution has been deprecated as of iOS 13 Beta 5. It is now recommended to use NavigationLink with an isActive binding.
I figured out a solution for programmatic pushing/popping of views in a NavigationView using NavigationDestinationLink.
Here's a simple example:
import Combine
import SwiftUI
struct DetailView: View {
var onDismiss: () -> Void
var body: some View {
Button(
"Here are details. Tap to go back.",
action: self.onDismiss
)
}
}
struct MainView: View {
var link: NavigationDestinationLink<DetailView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
DetailView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
VStack {
Button("I am root. Tap for more details.", action: {
self.link.presented?.value = true
})
}
.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
struct RootView: View {
var body: some View {
NavigationView {
MainView()
}
}
}
I wrote about this in a blog post here.
You can also do it with .sheet
.navigationBarItems(trailing: Button(action: {
self.presentingEditView.toggle()
}) {
Image(systemName: "square.and.pencil")
}.sheet(isPresented: $presentingEditView) {
EditItemView()
})
In my case I use it from a right navigation bar item, then you have to create the view (EditItemView() in my case) that you are going to display in that modal view.
https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)
EDIT: This answer over here is better than mine, but both work: SwiftUI dismiss modal
What you really want (or should want) is a modal presentation, which several people have mentioned here. If you go that path, you definitely will need to be able to programmatically dismiss the modal, and Erica Sadun has a great example of how to do that here: https://ericasadun.com/2019/06/16/swiftui-modal-presentation/
Given the difference between declarative coding and imperative coding, the solution there may be non-obvious (toggling a bool to false to dismiss the modal, for example), but it makes sense if your model state is the source of truth, rather than the state of the UI itself.
Here's my quick take on Erica's example, using a binding passed into the TestModal so that it can dismiss itself without having to be a member of the ContentView itself (as Erica's is, for simplicity).
struct TestModal: View {
#State var isPresented: Binding<Bool>
var body: some View {
Button(action: { self.isPresented.value = false }, label: { Text("Done") })
}
}
struct ContentView : View {
#State var modalPresented = false
var body: some View {
NavigationView {
Text("Hello World")
.navigationBarTitle(Text("View"))
.navigationBarItems(trailing:
Button(action: { self.modalPresented = true }) { Text("Show Modal") })
}
.presentation(self.modalPresented ? Modal(TestModal(isPresented: $modalPresented)) {
self.modalPresented.toggle()
} : nil)
}
}
Below works for me in XCode11 GM
self.myPresentationMode.wrappedValue.dismiss()
instead of NavigationButton use Navigation DestinationLink
but You should import Combine
struct AView: View {
var link: NavigationDestinationLink<BView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
BView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
NavigationView {
Button(action:{
self.link.presented?.value = true
}) {
Text("Go to B")
}.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
}
struct BView: View {
var onDismiss: () -> Void
var body: some View {
Button(action: self.onDismiss) {
Text("Come back to A")
}
}
}
In the destination pass the view you want to redirect, and inside block pass data you to pass in another view.
NavigationLink(destination: "Pass the particuter View") {
Text("Push")
}

Resources