SwiftUI - Perform action when cancel is clicked - .searchable function - search

When using the .searchable(text: $text) function, a cancel button appears in the search bar when searching.
Is there any way to perform an action when the cancel button is clicked? I would like to call a function when cancel is clicked, but cannot figure out how to perform an action when cancel is tapped.
The Apple Documentation does not mention anything about this. Back in UIKit there was the func searchBarCancelButtonClicked(searchBar: UISearchBar) { to do this.
Below is an image of the cancel button I am referring to:

You can use the isSearching environment value (https://developer.apple.com/documentation/swiftui/environmentvalues/issearching?changes=_6) to see if a search is being performed. To do an action upon cancelation, you could watch for a change from true to false using onChange:
struct ContentView: View {
#State private var searchText = ""
#Environment(\.dismissSearch) var dismissSearch
var body: some View {
NavigationView {
VStack {
ChildView()
Text("Searching for \(searchText)")
}
.searchable(text: $searchText)
.navigationTitle("Searchable Example")
}
}
}
struct ChildView : View {
#Environment(\.isSearching) var isSearching
var body: some View {
Text("Child")
.onChange(of: isSearching) { newValue in
if !newValue {
print("Searching cancelled")
}
}
}
}
Probably important to note that it seems like isSearching has to be inside a child view of the searchable modifier in order for it to work properly

Based on #jnpdx 's answer, something equivalent, but more generic is:
struct SearchView<Content: View>: View {
#Environment(\.isSearching) var isSearching
let content: (Bool) -> Content
var body: some View {
content(isSearching)
}
init(#ViewBuilder content: #escaping (Bool) -> Content) {
self.content = content
}
}
And then, use it like:
struct ContentView: View {
#State private var searchText = ""
#Environment(\.dismissSearch) var dismissSearch
var body: some View {
NavigationView {
VStack {
SearchView { isSearching in
Text("Child")
.onChange(of: isSearching) { newValue in
if !newValue {
print("Searching cancelled")
}
}
}
Text("Searching for \(searchText)")
}
.searchable(text: $searchText)
.navigationTitle("Searchable Example")
}
}
}

use isEmpty and onAppear.
struct SearchView: View {
#State var text: String = ""
var body: some View {
NavigationView {
VStack {
if text.isEmpty {
main.onAppear {
print("empty")
// code here
}
} else {
main
}
}.searchable(text: $text)
.onSubmit(of: .search) {
print("submit")
}
}
}
var main: some View {
Text("search").searchable(text: $text)
}
}

Related

SwiftUI UISearchController replacement: search field, results and some scrollable content fail to coexist in a meaningful manner

Starting with this
var body: some View {
ScrollView {
VStack(spacing: 0.0) {
Some views here
}
}
.edgesIgnoringSafeArea(.top)
}
How would I add
List(suggestions, rowContent: { text in
NavigationLink(destination: ResultsPullerView(searchText: text)) {
Text(text)
}
})
.searchable(text: $searchText)
on top if that scrollable content?
Cause no matter how I hoax this together when
#State private var suggestions: [String] = []
gets populated (non empty) the search results are not squeezed in (or, better yet, shown on top of
"Some views here"
So what I want to achieve in different terms: search field is on top, scrollable content driven by the search results is underneath, drop down with search suggestions either temporarily squeeses scrollable content down or is overlaid on top like a modal sheet.
Thanks!
If you are looking for UIKit like search behaviour you have to display your results in an overlay:
1. Let's declare a screen to display the results:
struct SearchResultsScreen: View {
#Environment(\.isSearching) private var isSearching
var results: [String]?
var body: some View {
if isSearching, let results {
if results.isEmpty {
Text("nothing to see here")
} else {
List(results, id: \.self) { fruit in
NavigationLink(destination: Text(fruit)) {
Text(fruit)
}
}
}
}
}
}
2. Let's have an ObservableObject to handle the logic:
class Search: ObservableObject {
static private let fruit = [
"Apples 🍏",
"Cherries 🍒",
"Pears 🍐",
"Oranges 🍊",
"Pineapples 🍍",
"Bananas 🍌"
]
#Published var text: String = ""
var results: [String]? {
if text.isEmpty {
return nil
} else {
return Self.fruit.filter({ $0.contains(text)})
}
}
}
3. And lastly lets declare the main screen where the search bar is displayed:
struct ContentView: View {
#StateObject var search = Search()
var body: some View {
NavigationView {
LinearGradient(colors: [.orange, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.overlay(SearchResultsScreen(results: search.results))
.searchable(text: $search.text)
.navigationTitle("Find that fruit")
}
}
}

SwiftUI CoreData master detail questions

I'm trying to create a master detail relationship with CoreData. I have a settings tab that is used to select the master (it's global and not done very often by the user). There is another tab that shows the detail entries for the current master.
The master has one field, name, a string and the details array. The detail has one field, name, a string. I'm using UUID().uuidString to populate the names for the example.
The problem I'm having is that when I select the detail tab, it shows the details for the current master. If I add details (click the + button) they do not appear until I change the master (settings -> select master). If I edit the details and delete some, the list entries go away but when I finish editing, they immediately come back. I can switch masters and then go back to the edited master and the data looks correct (I have to change the activeMaster published property).
I'm thinking that the published property isn't forcing the update to the details view because swift doesn't see the master variable change. I may also not be adding or deleting the details correctly.
How is adding details to a master typically done (here master is one to many details)
How is deleting details from a master typically done?
Is the data no showing up due to the published property not "publishing" Any ideas on how to better do this?
Thanks.
Code is below.
Here's the global application data:
import Foundation
import CoreData
import SwiftUI
class ApplicationData: ObservableObject
{
let container: NSPersistentContainer
#Published var activeMaster: Master?
init(preview: Bool = false)
{
container = NSPersistentContainer(name: "MasterDetail")
if (preview)
{
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler:
{ storeDescription, error in
if let error = error as NSError?
{
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
Just persistence and a single optional active master. The application data is created in the application code and set as an environment object:
import SwiftUI
#main
struct MasterDetailApp: App
{
#StateObject var appData = ApplicationData()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(appData)
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
}
The tab view:
import Foundation
import SwiftUI
struct MainView: View
{
#AppStorage("selectedTab") var selectedTab: Int = 0
#EnvironmentObject var appData: ApplicationData
var body: some View
{
TabView(selection: $selectedTab)
{
DetailView()
.tabItem({Label("Detail", systemImage: "house")})
.tag(0)
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1)
}
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
The detail tab allows the user to add details and to edit the list:
import Foundation
import SwiftUI
import CoreData
struct DetailView: View
{
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) var dismiss
#EnvironmentObject var appData: ApplicationData
var body: some View
{
NavigationView
{
List
{
ForEach(appData.activeMaster?.wrappedDetail ?? [])
{
detail in Text(detail.name ?? "None")
}
.onDelete(perform: { indexes in Task(priority: .high) { await deleteDetails(indexes: indexes) } } )
}
.toolbar
{
ToolbarItem(placement: .navigationBarTrailing)
{
EditButton()
}
ToolbarItem(placement: .navigationBarTrailing)
{
Button
{
let detail = Detail(context: viewContext)
detail.name = UUID().uuidString
detail.master = appData.activeMaster
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
.disabled(appData.activeMaster == nil)
}
}
}
}
/*
* Delete indexes - assumes that appData.activeWeapon is set.
*/
private func deleteDetails(indexes: IndexSet) async
{
await viewContext.perform
{
for index in indexes
{
print(index)
viewContext.delete(appData.activeMaster!.wrappedDetail[index])
}
do
{
try viewContext.save()
}
catch
{
print("Error deleting dope entry")
}
}
}
}
The settings view just has a navigation link to a view to select the master and an add button to add masters:
import Foundation
import SwiftUI
struct SettingsView: View
{
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var appData: ApplicationData
var body: some View
{
NavigationView
{
Form
{
Section(header: Text("Masters"))
{
NavigationLink(destination: SelectMastersView(selectedMaster: $appData.activeMaster), label:
{
Text(appData.activeMaster?.name ?? "Select Master")
})
Button
{
let master = Master(context: viewContext)
master.name = UUID().uuidString
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
}
}
}
}
}
The view for selecting the master just has a fetch request to get all masters and assign the selected one to the global app data published property:
import Foundation
import SwiftUI
struct SelectMastersView: View
{
#Environment(\.dismiss) var dismiss
#FetchRequest(entity: Master.entity(), sortDescriptors: [], animation: .default)
var masters: FetchedResults<Master>
#Binding var selectedMaster: Master?
var body: some View
{
List
{
ForEach(masters)
{ master in
Text(master.name ?? "None")
.onTapGesture
{
selectedMaster = master
dismiss()
}
}
}
.navigationBarTitle("Masters")
}
}
Edited to add extension to Master I forgot to post.
import Foundation
extension Master
{
var wrappedDetail: [Detail]
{
detail?.allObjects as! [Detail]
}
}
I finally figured it out this morning. I think putting the example code together last night helped quite a bit.
I got it work by creating fetch request in the detail view and passing the master into the view in init().
Here's the updated code for the tab view:
import Foundation
import SwiftUI
struct MainView: View
{
#AppStorage("selectedTab") var selectedTab: Int = 0
#EnvironmentObject var appData: ApplicationData
var body: some View
{
TabView(selection: $selectedTab)
{
DetailView(master: appData.activeMaster)
.tabItem({Label("Detail", systemImage: "house")})
.tag(0)
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1)
}
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
and the updated detail view:
import Foundation
import SwiftUI
import CoreData
struct DetailView: View
{
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) var dismiss
#EnvironmentObject var appData: ApplicationData
#FetchRequest(entity: Detail.entity(), sortDescriptors: [])
var details: FetchedResults<Detail>
let master: Master?
init(master: Master?)
{
self.master = master
if master != nil
{
let predicate = NSPredicate(format: "%K == %#", #keyPath(Detail.master), master ?? NSNull())
_details = FetchRequest(sortDescriptors: [], predicate: predicate)
}
}
#ViewBuilder
var body: some View
{
NavigationView
{
List
{
if master != nil
{
ForEach(details)
{
detail in Text(detail.name ?? "None")
}
.onDelete(perform: { indexes in Task(priority: .high) { await deleteDetails(indexes: indexes) } } )
}
}
.toolbar
{
ToolbarItem(placement: .navigationBarTrailing)
{
EditButton().disabled(master == nil || details.isEmpty)
}
ToolbarItem(placement: .navigationBarTrailing)
{
Button
{
let detail = Detail(context: viewContext)
detail.name = UUID().uuidString
detail.master = appData.activeMaster
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
.disabled(appData.activeMaster == nil)
}
}
}
}
/*
* Delete indexes - assumes that appData.activeWeapon is set.
*/
private func deleteDetails(indexes: IndexSet) async
{
await viewContext.perform
{
for index in indexes
{
print(index)
viewContext.delete(appData.activeMaster!.wrappedDetail[index])
}
do
{
try viewContext.save()
}
catch
{
print("Error deleting dope entry")
}
}
}
}
This is not as clean as I'd like. I had to move to a view build for the list. I'd like to be able to create an empty fetch request so I don't have to use a view builder.

How (or should?) I replace MVVM in this CoreData SwiftUI app with a Nested managedObjectContext?

I've written this small example of MVVM in a SwiftUI app using CoreData, but I wonder if there are better ways to do this such as using a nested viewcontext?
The object of the code is to not touch the CoreData entity until the user has updated all the fields needed and taps "Save". In other words, to not have to undo any fields if the user enters a lot of properties and then "Cancels". But how do I approach this in SwiftUI?
Currently, the viewModel has #Published vars which take their cue from the entity, but are not bound to its properties.
Here is the code:
ContentView
This view is pretty standard, but here is the NavigationLink in the List, and the Fetch:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Contact.lastName, ascending: true)],
animation: .default)
private var contacts: FetchedResults<Contact>
var body: some View { List {
ForEach(contacts) { contact in
NavigationLink (
destination: ContactProfile(contact: contact)) {
Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
}
}
.onDelete(perform: deleteItems)
} ///Etc...the rest of the code is standard
ContactProfile.swift in full:
import SwiftUI
struct ContactProfile: View {
#ObservedObject var contact: Contact
#ObservedObject var viewModel: ContactProfileViewModel
init(contact: Contact) {
self.contact = contact
self._viewModel = ObservedObject(initialValue: ContactProfileViewModel(contact: contact))
}
#State private var isEditing = false
#State private var errorAlertIsPresented = false
#State private var errorAlertTitle = ""
var body: some View {
VStack {
if !isEditing {
Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
.font(.largeTitle)
.padding(.top)
Spacer()
} else {
Form{
TextField("First Name", text: $viewModel.firstName)
TextField("First Name", text: $viewModel.lastName)
}
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarBackButtonHidden(isEditing ? true : false)
.navigationBarItems(leading:
Button (action: {
withAnimation {
self.isEditing = false
viewModel.reset() /// <- Is this necessary? I'm not sure it is, the code works
/// with or without it. I don't see a
/// difference in calling viewModel.reset()
}
}, label: {
Text(isEditing ? "Cancel" : "")
}),
trailing:
Button (action: {
if isEditing { saveContact() }
withAnimation {
if !errorAlertIsPresented {
self.isEditing.toggle()
}
}
}, label: {
Text(!isEditing ? "Edit" : "Done")
})
)
.alert(
isPresented: $errorAlertIsPresented,
content: { Alert(title: Text(errorAlertTitle)) }) }
private func saveContact() {
do {
try viewModel.saveContact()
} catch {
errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
errorAlertIsPresented = true
}
}
}
And the ContactProfileViewModel it uses:
import UIKit
import Combine
import CoreData
/// The view model that validates and saves an edited contact into the database.
///
final class ContactProfileViewModel: ObservableObject {
/// A validation error that prevents the contact from being 8saved into
/// the database.
enum ValidationError: LocalizedError {
case missingFirstName
case missingLastName
var errorDescription: String? {
switch self {
case .missingFirstName:
return "Please enter a first name for this contact."
case .missingLastName:
return "Please enter a last name for this contact."
}
}
}
#Published var firstName: String = ""
#Published var lastName: String = ""
/// WHAT ABOUT THIS NEXT LINE? Should I be making a ref here
/// or getting it from somewhere else?
private let moc = PersistenceController.shared.container.viewContext
var contact: Contact
init(contact: Contact) {
self.contact = contact
updateViewFromContact()
}
// MARK: - Manage the Contact Form
/// Validates and saves the contact into the database.
func saveContact() throws {
if firstName.isEmpty {
throw ValidationError.missingFirstName
}
if lastName.isEmpty {
throw ValidationError.missingLastName
}
contact.firstName = firstName
contact.lastName = lastName
try moc.save()
}
/// Resets form values to the original contact values.
func reset() {
updateViewFromContact()
}
// MARK: - Private
private func updateViewFromContact() {
self.firstName = contact.firstName ?? ""
self.lastName = contact.lastName ?? ""
}
}
Most of the viewmodel code is adapted from the GRDB Combine example. So, I wasn't always sure what to exclude. what to include.
I have opted to avoid a viewModel in this case after discovering:
moc.refresh(contact, mergeChanges: false)
Apple docs: https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506224-refresh
So you can toss aside the ContactViewModel, keep the ContentView as is and use the following:
Contact Profile
The enum havae been made an extension to the ContactProfile view.
import SwiftUI
import CoreData
struct ContactProfile: View {
#Environment(\.managedObjectContext) private var moc
#ObservedObject var contact: Contact
#State private var isEditing = false
#State private var errorAlertIsPresented = false
#State private var errorAlertTitle = ""
var body: some View {
VStack {
if !isEditing {
Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
.font(.largeTitle)
.padding(.top)
Spacer()
} else {
Form{
TextField("First Name", text: $contact.firstName ?? "")
TextField("First Name", text: $contact.lastName ?? "")
}
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarBackButtonHidden(isEditing ? true : false)
.navigationBarItems(leading:
Button (action: {
/// This is the key change:
moc.refresh(contact, mergeChanges: false)
withAnimation {
self.isEditing = false
}
}, label: {
Text(isEditing ? "Cancel" : "")
}),
trailing:
Button (action: {
if isEditing { saveContact() }
withAnimation {
if !errorAlertIsPresented {
self.isEditing.toggle()
}
}
}, label: {
Text(!isEditing ? "Edit" : "Done")
})
)
.alert(
isPresented: $errorAlertIsPresented,
content: { Alert(title: Text(errorAlertTitle)) }) }
private func saveContact() {
do {
if contact.firstName!.isEmpty {
throw ValidationError.missingFirstName
}
if contact.lastName!.isEmpty {
throw ValidationError.missingLastName
}
try moc.save()
} catch {
errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
errorAlertIsPresented = true
}
}
}
extension ContactProfile {
enum ValidationError: LocalizedError {
case missingFirstName
case missingLastName
var errorDescription: String? {
switch self {
case .missingFirstName:
return "Please enter a first name for this contact."
case .missingLastName:
return "Please enter a last name for this contact."
}
}
}
}
This also requires the code below that can be found at this link:
SwiftUI Optional TextField
import SwiftUI
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: { lhs.wrappedValue = $0 }
)
}

Pass FetchedResults through NavigationLink

I have a two CoreData objects:
RoadTrip
StatePlate.
Each RoadTrip items holds an NSSet of StatePlate.
Screen 1 (TripList) shows a list of all RoadTrip items. Screen 2 (StateList) shows a list of all StatePlate items in associated with the RoadTrip that a user selects. Selecting a StatePlate item in Screen 2 will toggle a bool value associated with that item.
Even though I can show the data and can toggle the bool value of each StatePlate, I am not seeing an immediate change to the UI of the screen. The StatePlate should jump from Section to Section in Screen 2 when it's bool value is toggled.
How can I pass this FetchedObject correctly from Screen 1 to Screen 2 so the UI is binded with the data?
Screen 1 (TripList)
struct TripList: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: RoadTrip.entity(), sortDescriptors: []) var roadTripItems: FetchedResults<RoadTrip>
var body: some View {
List {
ForEach(roadTripItems, id: \.self) { trip in
NavigationLink(destination: StateList(trip: trip)
.environment(\.managedObjectContext, self.managedObjectContext)) {
TripRow(roadTrip: trip)
}
}
}
}
}
Screen 2 (StateList)
struct StateList: View {
#Environment(\.managedObjectContext) var managedObjectContext
var trip: RoadTrip
var plates: [StatePlate] {
trip.plateArray
}
var unseenPlates: [StatePlate] {
trip.plateArray.filter { !$0.hasBeenSeen }
}
var seenPlates: [StatePlate] {
trip.plateArray.filter { $0.hasBeenSeen }
}
var body: some View {
List {
if !unseenPlates.isEmpty {
Section(header: Text("Unseen Plates")) {
ForEach(unseenPlates, id: \.self) { plate in
StateRow(plate: plate)
}
}
}
if !seenPlates.isEmpty {
Section(header: Text("Seen Plates")) {
ForEach(seenPlates, id: \.self) { plate in
StateRow(plate: plate)
}
}
}
}
}
}
StateRow
struct StateRow: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var plate: StatePlate
var body: some View {
Button(action: {
self.plate.hasBeenSeen.toggle()
try? self.managedObjectContext.save()
}) {
HStack {
Text(String(describing: plate.name!))
Spacer()
if plate.hasBeenSeen {
Image(systemName: "eye.fill")
} else {
Image(systemName: "")
}
}
}
}
}
Your trip as object is not changed when plate has changed, so even if it was observed UI was not refreshed.
Here is possible force-refresh approach.
struct StateList: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var trip: RoadTrip // << make observed
// .. other code
and add handling for updated plate/s
StateRow(plate: plate)
.onReceive(plate.objectWillChange) { _ in
self.trip.objectWillChange.send()
}

SwiftUI / Core Data - Involuntary navigation between list view and detailed view when updating detailed view

I am using a TabView/NavigationView & NavigationLink to programmatically navigate from a list view to detailed view and when I update the boolean property 'pinned' to true & save the core data entity in the detailed view, I am getting the following unfortunate side effects:
an involuntary navigation back to the list view and then again back to the detailed view or
an involuntary navigation to another copy of the same detailed view and then back to the list view.
I have prepared a small Xcode project with the complete sample code
In the list view I use #FetchRequest to query the list and sort on the following:
#FetchRequest(entity: Task.entity(),sortDescriptors: [NSSortDescriptor(key: "pinned", ascending: false),
NSSortDescriptor(key: "created", ascending: true),
NSSortDescriptor(key: "name", ascending: true)])
In the list view I use the following:
List() {
ForEach(tasks, id: \.self) { task in
NavigationLink(destination: DetailsView(task: task), tag: task.id!.uuidString, selection: self.$selectionId) {
HStack() {
...
}
}
}
.
(1) If I omit the 'NSSortDescriptor(key: "pinned" ...)' I don't see the behavior.
(2) If I omit the 'tag:' and the 'selection:' parameters in the NavigationLink() I don't see the behavior. But I need to be able to trigger the navigation link programmatically when I create a new Task entity.
(3) It seems never to happen when I have a single entity in the list or changing the value of the 'pinned' boolean property in the first entity in the list.
(4) I get the warning:
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window)...
The parent view to the list view (TasksListView) contains a TabView:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TasksListView()
}
.tabItem {
Image(systemName: "tray.full")
.font(.title)
Text("Master")
}
NavigationView {
EmptyView()
}
.tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
Text("Search")
}
}
}
}
struct TasksListView: View {
// NSManagedObjectContext
#Environment(\.managedObjectContext) var viewContext
// Results of fetch request for tasks:
#FetchRequest(entity: Task.entity(),sortDescriptors: [NSSortDescriptor(key: "pinned", ascending: false),
NSSortDescriptor(key: "created", ascending: true),
NSSortDescriptor(key: "name", ascending: true)])
var tasks: FetchedResults<Task>
// when we create a new task and navigate to it programitically
#State var selectionId : String?
#State var newTask : Task?
var body: some View {
List() {
ForEach(tasks, id: \.self) { task in
NavigationLink(destination: DetailsView(task: task), tag: task.id!.uuidString, selection: self.$selectionId) {
HStack() {
VStack(alignment: .leading) {
Text("\(task.name ?? "unknown")")
.font(Font.headline.weight(.light))
.padding(.bottom,5)
Text("Created:\t\(task.created ?? Date(), formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
.padding(.bottom,5)
if task.due != nil {
Text("Due:\t\t\(task.due!, formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
.padding(.bottom,5)
}
}
}
}
}
}
.navigationBarTitle(Text("Tasks"),displayMode: .inline)
.navigationBarItems(trailing: rightButton)
}
var rightButton: some View {
Image(systemName: "plus.circle")
.foregroundColor(Color(UIColor.systemBlue))
.font(.title)
.contentShape(Rectangle())
.onTapGesture {
// create a new task and navigate to it's detailed view to add values
Task.create(in: self.viewContext) { (task, success, error) in
if success {
self.newTask = task
self.selectionId = task!.id!.uuidString
}
}
}
}
}
struct DetailsView: View {
// NSManagedObjectContext
#Environment(\.managedObjectContext) var viewContext
#ObservedObject var task : Task
#State var name : String = ""
#State var dueDate : Date = Date()
#State var hasDueDate : Bool = false
#State var isPinned : Bool = false
var body: some View {
List() {
Section() {
Toggle(isOn: self.$isPinned) {
Text("Pinned")
}
}
Section() {
TextField("Name", text: self.$name)
.font(Font.headline.weight(.light))
Text("\(task.id?.uuidString ?? "unknown")")
.font(Font.headline.weight(.light))
}
Section() {
HStack() {
Text("Created")
Spacer()
Text("\(task.created ?? Date(), formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
}
Toggle(isOn: self.$hasDueDate) {
Text("Set Due Date")
}
if self.hasDueDate {
DatePicker("Due Date", selection: self.$dueDate, in: Date()... , displayedComponents: [.hourAndMinute, .date])
}
}
}
.navigationBarTitle(Text("Task Details"),displayMode: .inline)
.navigationBarItems(trailing: rightButton)
.listStyle(GroupedListStyle())
.onAppear() {
if self.task.pinned {
self.isPinned = true
}
if self.task.name != nil {
self.name = self.task.name!
}
if self.task.due != nil {
self.dueDate = self.task.due!
self.hasDueDate = true
}
}
}
// save button
var rightButton: some View {
Button("Save") {
// save values in task & save:
self.task.pinned = self.isPinned
if self.hasDueDate {
self.task.due = self.dueDate
}
if self.name.count > 0 {
self.task.name = self.name
}
Task.save(in: self.viewContext) { (success, error) in
DispatchQueue.main.async {
if success {
print("Task saved")
}
else {
print("****** Error: Task can't be saved, error = \(error!.localizedDescription)")
}
}
}
}
.contentShape(Rectangle())
}
}
extension Task {
static func save(in managedObjectContext: NSManagedObjectContext, completion: #escaping (Bool, NSError?) -> Void ) {
managedObjectContext.performAndWait() {
do {
try managedObjectContext.save()
completion(true, nil)
} catch {
let nserror = error as NSError
print("****** Error: Unresolved error \(nserror), \(nserror.userInfo)")
completion(false, nserror)
}
}
}
}
Any suggestions?
You seem to be creating your tab view in a different way than me. Those extra navigation views made cause an issue.
Not sure if it will help or not but I do it like that:
struct ContentView: View {
var body: some View {
TabView {
TasksListView()
.tabItem {
Image(systemName: "tray.full")
.font(.title)
Text("Master")
}
EmptyView()
.tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
Text("Search")
}
}
}
}
This is a bug in iOS 13.
It has been fixed since iOS 14.0 beta 3.
You will find a similar question here (the accepted answer provides a workaround):
Issue when rearranging List item in detail view using SwiftUI Navigation View and Sorted FetchRequest

Resources