Trying to incorporate sound within a beacon region in Swift - audio

I am getting "Use of unresolved identifier 'player' in my code using beacons and regions. For this particular region, I also want it to play a sound (Siren.wav). Code is below:
import Combine
import CoreLocation
import SwiftUI
import AVFoundation
class BeaconDetector: NSObject, ObservableObject, CLLocationManagerDelegate {
var objectWillChange = ObservableObjectPublisher()
var locationManager: CLLocationManager?
var lastDistance = CLProximity.unknown
var player: AVAudioPlayer?
// var audioPlayer = AVAudioPlayer()
override init() {
super.init()
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.requestWhenInUseAuthorization()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
if CLLocationManager.isMonitoringAvailable(for: CLBeaconRegion.self) {
if CLLocationManager.isRangingAvailable() {
startScanning()
}
}
}
}
func startScanning() {
let uuid = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
let constraint = CLBeaconIdentityConstraint(uuid: uuid)
let beaconRegion = CLBeaconRegion(beaconIdentityConstraint: constraint, identifier: "MyBeacon")
locationManager?.startMonitoring(for: beaconRegion)
locationManager?.startRangingBeacons(satisfying: constraint)
}
func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
if let beacon = beacons.first {
update(distance: beacon.proximity)
} else {
update(distance: .unknown)
}
}
func update(distance: CLProximity) {
lastDistance = distance
self.objectWillChange.send()
}
}
struct BigText: ViewModifier {
func body(content: Content) -> some View {
content
.font(Font.system(size: 72, design: .rounded))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
struct ContentView: View {
#ObservedObject var detector = BeaconDetector()
var body: some View {
if detector.lastDistance == .immediate {
return Text("DANGER TOO CLOSE")
.modifier(BigText())
.background(Color.red)
.edgesIgnoringSafeArea(.all)
func playSound() {
guard let url = Bundle.main.url(forResource: "Siren", withExtension: "wav") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
guard let player = player else { return }
player.play()
}
catch let error {
print(error.localizedDescription)

The reason you get an "unresolved identifier" error is because the variable player is not defined in the playSound() method. In the Swift language, each variable declaration has a specific "scope" and they cannot be accessed outside that scope.
In this case, player is defined as a member variable in the BeaconDetector class. Because the playSound() method is not in the same variable "scope", you get that error when you try to access the variable.
You might want to read this tutorial on how variable scope works in Swift.

Related

onAppear is causing problem with the preview but no error is shown

self learning beginner here.
When I remove .onAppear{add()}, the preview works fine. I tried to attach it to other the body view, the Vstack but it causes another error. I read/watched several tutorials but nothing like this is mentioned....
Any help is appreciated
struct ListView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var targets: FetchedResults<TargetEntity>
#FetchRequest(sortDescriptors: []) var positives: FetchedResults<PositiveEntity>
var body: some View {
VStack {
Text("+")
.onAppear{add()}
.onTapGesture (count: 2){
do {
increment(targets.first!) //I also sense that doing "!" is not good. But it's the only way I can keep it from causing error "Cannot convert value of type 'FetchedResults' to expected argument type 'X'"
try moc.save()
} catch {
print("error")
}
}
}
}
func increment(_ item: TargetEntity) {
item.countnum += 1
save()
}
func add() {
let countnum = TargetEntity(context: moc)
countnum.countnum = 0
save()
}
func save() {
do { try moc.save() } catch { print(error) }
}
}
EDIT 20220509:
As advised by #Yrb (great thanks), the error is likely caused by the lack of a proper set up of preview var in the persistence file. I post the relevant code here for visiblity.
Data Controller file
import CoreData
import Foundation
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "CounterLateApr")
init () {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
preview code in a view
struct ListView_Previews: PreviewProvider {
static var previews: some View {
NavigationView{
ListView()
}
}
}
[AppName].app file
import SwiftUI
#main
struct CounterLateAprApp: App {
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}

Why is my NSFetchRequest not updating my array as I expect? And does `shouldRefreshRefetchedObjects` make any difference?

I do not understand why this code does not work to update a list when navigating "back" from a DetailView(). As far as I can tell, I'm calling a new fetchRequest each time I want to update the list and it seems that request should always return object with current properties. But as others have said they are "stale", reflecting whatever was the property BEFORE the update was committed in the DetailView. And tapping a Navigation link from a "Stale" row, opens a DetailView with the current values of the properties, so I know they have been sacved to the context (haven't they?).
First I have a "dataservice" like this:
import CoreData
import SwiftUI
protocol CategoryDataServiceProtocol {
func getCategories() -> [Category]
func getCategoryById(id: NSManagedObjectID) -> Category?
func addCategory(name: String, color: String)
func updateCategory(_ category: Category)
func deleteCategory(_ category: Category)
}
class CategoryDataService: CategoryDataServiceProtocol {
var viewContext: NSManagedObjectContext = PersistenceController.shared.viewContext
///Shouldn't this next function always return an updated version of my list of categories?
func getCategories() -> [Category] {
let request: NSFetchRequest<Category> = Category.fetchRequest()
let sort: NSSortDescriptor = NSSortDescriptor(keyPath: \Category.name_, ascending: true)
request.sortDescriptors = [sort]
///This line appears to do nothing if I insert it:
request.shouldRefreshRefetchedObjects = true
do {
///A print statement here does run, so it's getting this far...
print("Inside get categories func")
return try viewContext.fetch(request)
} catch {
return []
}
}
func getCategoryById(id: NSManagedObjectID) -> Category? {
do {
return try viewContext.existingObject(with: id) as? Category
} catch {
return nil
}
}
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
func updateCategory(_ category: Category) {
saveContext()
}
func deleteCategory(_ category: Category) {
viewContext.delete(category)
saveContext()
}
func saveContext() {
PersistenceController.shared.save()
}
}
class MockCategoryDataService: CategoryDataService {
override init() {
super .init()
self.viewContext = PersistenceController.preview.viewContext
print("MOCK INIT")
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
}
}
And I have a viewModel like this:
import SwiftUI
extension CategoriesList {
class ViewModel: ObservableObject {
let dataService: CategoryDataServiceProtocol
#Published var categories: [Category] = []
init(dataService: CategoryDataServiceProtocol = CategoryDataService()) {
self.dataService = dataService
}
func getCategories() {
self.categories = dataService.getCategories()
}
func deleteCategories(at offsets: IndexSet) {
offsets.forEach { index in
let category = categories[index]
dataService.deleteCategory(category)
}
}
}
}
Then my view:
import SwiftUI
struct CategoriesList: View {
#StateObject private var viewModel: CategoriesList.ViewModel
init(viewModel: CategoriesList.ViewModel = .init()) {
_viewModel = StateObject(wrappedValue: viewModel)
}
#State private var isShowingSheet = false
var body: some View {
NavigationView {
List {
ForEach(viewModel.categories) { category in
NavigationLink(
destination: CategoryDetail(category: category)) {
CategoryRow(category: category)
.padding(0)
}
}
.onDelete(perform: { index in
viewModel.deleteCategories(at: index)
viewModel.getCategories()
})
}
.listStyle(PlainListStyle())
.onAppear(perform: {
viewModel.getCategories()
})
.navigationBarTitle(Text("Categories"))
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: { EditButton() })
ToolbarItem(placement: .navigationBarTrailing) {
Button(
action: {
isShowingSheet = true
viewModel.getCategories()
},
label: { Image(systemName: "plus.circle").font(.system(size: 20)) }
)
}
}
.sheet(isPresented: $isShowingSheet, onDismiss: {
viewModel.getCategories()
}, content: {
CategoryForm()
})
}
}
}
struct CategoriesList_Previews: PreviewProvider {
static var previews: some View {
let viewModel: CategoriesList.ViewModel = .init(dataService: MockCategoryDataService())
return CategoriesList(viewModel: viewModel)
}
}
So, when I navigate to the DetailView and change the name of the category, all is fine there. But then tapping the back button or swiping to return to the view - and the view still shows the old name.
I understand that the #Published array of [Category] is probably not looking at changes to objects inside the array, only if an object is removed or added, I guess.
But why is my list not updating anyways, since I am calling viewModel.getCategories() and that is triggering the fetch request in the dataservice getCategories function?
And if Combine is the answer, then how? Or what else am I missing? Does request.shouldRefreshRefetchedObjects = true offer anything? Or is it a bug as I read here: https://mjtsai.com/blog/2019/10/17/core-data-derived-attributes/

Is this the proper way to use PHPicker in SwiftUI? Because I'm getting a lot of leaks

I am trying to figure out if my code is causing the problem or if I should submit a bug report to Apple.
In a new project, I have this code:
ContentView()
import SwiftUI
struct ContentView: View {
#State private var showingImagePicker = false
#State private var inputImage: UIImage?
#State private var image: Image?
var body: some View {
ZStack {
Rectangle()
.fill(Color.secondary)
if image != nil {
image?
.resizable()
.scaledToFit()
} else {
Text("Tap to select a picture")
.foregroundColor(.white)
.font(.headline)
}
}
.onTapGesture {
self.showingImagePicker = true
}
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage){
SystemImagePicker(image: self.$inputImage)
}
}
func loadImage() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
}
}
SystemImagePicker.swift
import SwiftUI
struct SystemImagePicker: UIViewControllerRepresentable {
#Environment(\.presentationMode) private var presentationMode
#Binding var image: UIImage?
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.selectionLimit = 1
configuration.filter = .images
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: SystemImagePicker
init(parent: SystemImagePicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
for img in results {
guard img.itemProvider.canLoadObject(ofClass: UIImage.self) else { return }
img.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
if let error = error {
print(error)
return
}
guard let image = image as? UIImage else { return }
self.parent.image = image
self.parent.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
But when selecting just one image (as per my code, not selecting and then "changing my mind" and selecting another, different image), I get these leaks when running the memory graph in Xcode.
Is it my code, or is this on Apple?
For what it is worth, the Cancel button on the imagepicker doesn't work either. So, the user cannot just close the picker sheet, an image MUST be selected to dismiss the sheet.
Further note on old UIImagePickerController
Previously, I've used this code for the old UIImagePickerController
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var image: UIImage?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiImage = info[.originalImage] as? UIImage {
parent.image = uiImage
}
parent.presentationMode.wrappedValue.dismiss()
}
deinit {
print("deinit")
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
This also result in leaks from choosing an image, but far fewer of them:
I know it's been over a year since you asked this question but hopefully this helps you or someone else looking for the answer.
I used this code in a helper file:
import SwiftUI
import PhotosUI
struct ImagePicker: UIViewControllerRepresentable {
let configuration: PHPickerConfiguration
#Binding var selectedImage: UIImage?
#Binding var showImagePicker: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
extension ImagePicker {
class Coordinator: NSObject, PHPickerViewControllerDelegate {
private let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true) {
self.parent.showImagePicker = false
}
guard let provider = results.first?.itemProvider else { return }
if provider.canLoadObject(ofClass: UIImage.self) {
provider.loadObject(ofClass: UIImage.self) { image, _ in
self.parent.selectedImage = image as? UIImage
}
}
parent.showImagePicker = false
}
}
}
This goes in your view (I set up configuration here so I could pass in custom versions depending on what I'm using the picker for, 2 are provided):
#State private var showImagePicker = false
#State private var selectedImage: UIImage?
#State private var profileImage: Image?
var profileConfig: PHPickerConfiguration {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
config.preferredAssetRepresentationMode = .current
return config
}
var mediaConfig: PHPickerConfiguration {
var config = PHPickerConfiguration()
config.filter = .any(of: [.images, .videos])
config.selectionLimit = 1
config.preferredAssetRepresentationMode = .current
return config
}
This goes in your body. You can customize it how you want but this is what I have so I didn't want to try and piece it out:
HStack {
Button {
showImagePicker.toggle()
} label: {
Text("Select Photo")
.foregroundColor(Color("AccentColor"))
}
.sheet(isPresented: $showImagePicker) {
loadImage()
} content: {
ImagePicker(configuration: profileConfig, selectedImage: $selectedImage, showImagePicker: $showImagePicker)
}
}
if profileImage != nil {
profileImage?
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 2))
}
else {
Image(systemName: "person.crop.circle")
.resizable()
.foregroundColor(Color("AccentColor"))
.frame(width: 100, height: 100)
}
I will also give you the func for loading the image (I will be resamp:
func loadImage() {
guard let selectedImage = selectedImage else { return }
profileImage = Image(uiImage: selectedImage)
}
I also used this on my Form to update the image if it is changed but you can use it on whatever you're using for your body (List, Form, etc. Whatever takes .onChange):
.onChange(of: selectedImage) { _ in
loadImage()
}
I noticed in a lot of tutorials there is little to no mention of this line which is what makes the cancel button function (I don't know if the closure is necessary but I added it and it worked so I left it in the example):
picker.dismiss(animated: true)
I hope I added everything to help you. It doesn't appear to leak anything and gives you use of the cancel button.
Good luck!

How to use fetch data in Intent Handler for editing Widget iOS 14?

I'm currently developing an application using SwiftUI.
I'm trying to make a widget user can edit some data using IntentConfiguration
I want to use some fetch data from CoreData in IntentHandler class for editing the widget like the image below.
I tried to make some codes but They don't work...
How could I solve my codes?
Here are the codes:
IntentHandler.swift
import WidgetKit
import SwiftUI
import CoreData
import Intents
class IntentHandler: INExtension, ConfigurationIntentHandling {
var moc = PersistenceController.shared.managedObjectContext
var timerEntity_0:TimerEntity?
var timerEntity_1:TimerEntity?
var timerEntity_2:TimerEntity?
init(context : NSManagedObjectContext) {
self.moc = context
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntity_0 = result[0]
timerEntity_1 = result[1]
timerEntity_2 = result[2]
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
}
func provideNameOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: #escaping (INObjectCollection<NSString>?, Error?) -> Void) {
let nameIdentifiers:[NSString] = [
NSString(string: timerEntity_0?.task ?? "default"),
NSString(string: timerEntity_1?.task ?? "default"),
NSString(string: timerEntity_2?.task ?? "default")
// "meeting",
// "cooking",
// "shoping"
]
let allNameIdentifiers = INObjectCollection(items: nameIdentifiers)
completion(allNameIdentifiers,nil)
}
override func handler(for intent: INIntent) -> Any {
return self
}
}
Widget.swift
import WidgetKit
import SwiftUI
import Intents
struct Provider: IntentTimelineProvider {
typealias Intent = ConfigurationIntent
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent(), name: "")
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration, name: "")
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration, name: configuration.Name ?? "")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
var name:String
}
struct TimerIntentWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.name)
.font(.title)
Text(entry.date, style: .time)
}
}
#main
struct TimerIntentWidget: Widget {
let kind: String = "TimerIntentWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
TimerIntentWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
tWidget.intentdefinition
Xcode: Version 12.0.1
iOS: 14.0
Life Cycle: SwiftUI App
I could display a list in the widget using fetch data from CoreData like the code below:
IntentHandler.swift
import WidgetKit
import SwiftUI
import CoreData
import Intents
class IntentHandler: INExtension, ConfigurationIntentHandling {
var moc = PersistenceController.shared.managedObjectContext
var timerEntity_0:TimerEntity?
var timerEntity_1:TimerEntity?
var timerEntity_2:TimerEntity?
func provideNameOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: #escaping (INObjectCollection<NSString>?, Error?) -> Void) {
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntity_0 = result[0]
timerEntity_1 = result[1]
timerEntity_2 = result[2]
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let nameIdentifiers:[NSString] = [
NSString(string: timerEntity_0?.task ?? "default"),
NSString(string: timerEntity_1?.task ?? "default"),
NSString(string: timerEntity_2?.task ?? "default")
// "meeting",
// "cooking",
// "shoping"
]
let allNameIdentifiers = INObjectCollection(items: nameIdentifiers)
completion(allNameIdentifiers,nil)
}
override func handler(for intent: INIntent) -> Any {
return self
}
}

iOS 13.4 CoreData SwiftUI app crashes with "EXC_BREAKPOINT (code=1, subcode=0x1f3751f08)" on device

A very simple CoreData app: All code provided below.
Start up with CoreData template single view app.
2 entities with a string attribute each: Message(title) and Post(name)
A NavigationView containing
NavigationLink to a list of messages
NavigationLink to a list of posts
Each linked ListView (Message/Post) has
a button to add an item to the list
a button to remove all items from the list
Now, when you run this app on a simulator (any iOS 13.x version) all runs as expected from the description above.
But on a DEVICE running iOS 13.4
Tap "Messages"
Creating/deleting messages works fine, SwiftUi view updates immediately.
Tap "back"
Tap "Messages" again. While still creating/deleting messages works fine: The debugger now shows a warning: "Context in environment is not connected to a persistent store coordinator: NSManagedObjectContext: 0x280ed72c0
Tap "Posts"
==> App crashes with EXC_BREAKPOINT (code=1, subcode=0x1f3751f08)
You can start the process with Posts first, too. Then the same crash occurs on the messages list view.
I strongly believe this is an iOS 13.4 bug because similar code ran fine on Xcode 11.3 / iOS 13.3.
Does anyone know a fix or workaround for this?
Here is a link to the full project: Full Xcode Project
The ContentView:
import SwiftUI
import CoreData
struct MessageList: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Message.entity(), sortDescriptors: [])
var messages: FetchedResults<Message>
var body: some View {
List() {
ForEach(messages, id: \.self) { message in
Text(message.title ?? "?")
}
}
.navigationBarItems(trailing:
HStack(spacing: 16) {
Button(action: deleteMessages) {
Image(systemName: "text.badge.minus")
}
Button(action: addMessage) {
Image(systemName: "plus.app")
}
}
)
}
func addMessage() {
let m = Message(context: moc)
m.title = "Message: \(Date())"
try! moc.save()
}
func deleteMessages() {
messages.forEach {
moc.delete($0)
}
}
}
struct PostList: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Post.entity(), sortDescriptors: [])
var posts: FetchedResults<Post>
var body: some View {
List {
ForEach(0..<posts.count, id: \.self) { post in
Text(self.posts[post].name ?? "?")
}
}
.navigationBarItems(trailing:
HStack(spacing: 16) {
Button(action: deletePosts) {
Image(systemName: "text.badge.minus")
}
Button(action: addPost) {
Image(systemName: "plus.app")
}
}
)
}
func addPost() {
let p = Post(context: moc)
p.name = "Post \(UUID().uuidString)"
try! moc.save()
}
func deletePosts() {
posts.forEach {
moc.delete($0)
}
try! moc.save()
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
var body: some View {
NavigationView {
VStack(alignment: .leading){
NavigationLink(destination: MessageList()) {
Text("Messages")
}.padding()
NavigationLink(destination: PostList()) {
Text("Posts")
}.padding()
Spacer()
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
static var previews: some View {
ContentView()
.environment(\.managedObjectContext, moc)
}
}
Screenshot of the Model:
The SceneDelegate (unaltered from template, provided for completeness):
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {}
func sceneDidBecomeActive(_ scene: UIScene) {}
func sceneWillResignActive(_ scene: UIScene) {}
func sceneWillEnterForeground(_ scene: UIScene) {}
func sceneDidEnterBackground(_ scene: UIScene) {
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
}
The AppDelegate (unaltered from template, provided for completeness):
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Coredata134")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
Update iOS 14.0 (beta 1):
This issue seems to have been resolved on iOS 14.
I also believe this is a bug.
You can workaround for now by setting the environment variable again within the NavigationLinks in ContentView:
NavigationLink(destination: MessageList().environment(\.managedObjectContext, moc)) {
Text("Messages")
}.padding()
NavigationLink(destination: PostList().environment(\.managedObjectContext, moc)) {
Text("Posts")
}.padding()
EDIT:
Just noticed that this workaround has at least one serious negative side effect: in case the #FetchRequest in the destination View uses a sortDescriptor and the destination View itself contains a NavigationLink, (e.g. to a DetailView), then modifying an attribute contained in the sortDescriptor in the DetailView will cause the DetailView to be popped and pushed again as soon as the new attribute value leads to a new sort order.
To demonstrate this:
a) add a new attribute of type Integer 16 named "value" to the Message entity in the Core Data model.
b) update func addMessage() as follows:
func addMessage() {
let m = Message(context: moc)
m.title = "Message: \(Date())"
m.value = 0
try! moc.save()
}
c) add the following struct to ContentView.swift
struct MessageDetailList: View {
#ObservedObject var message: Message
var body: some View {
Button(action: {
self.message.value += 1
}) {
Text("\(message.title ?? "?"): value = \(message.value)")
}
}
}
d) Update the ForEach in struct MessageList as follows:
ForEach(messages, id: \.self) { message in
NavigationLink(destination: MessageDetailList(message: message).environment(\.managedObjectContext, self.moc)) {
Text("\(message.title ?? "?"): value = \(message.value)")
}
}
e) replace #FetchRequest in MessageList with:
#FetchRequest(entity: Message.entity(), sortDescriptors: [NSSortDescriptor(key: "value", ascending: false)])
Run the code and tap on "Messages". Create three messages, then tap on the third one. In the DetailView, tap on the Button. This will increase the value attribute of this message to 1 and thus resort the fetch results on MessageList, which will trigger a pop and push again of the detail list.

Resources