Publish background context Core Data changes in a SwiftUI view without blocking the UI - core-data

After running a background-context core data task, Xcode displays the following purple runtime warning when the updates are published in a SwiftUI view:
"[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates."
Besides the ContentView.swift code below, I also added container.viewContext.automaticallyMergesChangesFromParent = true to init in the default Persistence.swift code.
How can I publish the background changes on the main thread to fix the warning? (iOS 14, Swift 5)
Edit: I've changed the code below, in response to the first answer, to clarify that I'm looking for a solution that doesn't block the UI when a lot of changes are saved.
struct PersistenceHelper {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) {
self.context = context
}
public func fetchItem() -> [Item] {
do {
let request: NSFetchRequest<Item> = Item.fetchRequest()
var items = try self.context.fetch(request)
if items.isEmpty { // Create items if none exist
for _ in 0 ..< 250_000 {
let item = Item(context: context)
item.timestamp = Date()
item.data = "a"
}
try! context.save()
items = try self.context.fetch(request)
}
return items
} catch { assert(false) }
}
public func updateItemTimestamp(completionHandler: #escaping () -> ()) {
PersistenceController.shared.container.performBackgroundTask({ backgroundContext in
let start = Date(), request: NSFetchRequest<Item> = Item.fetchRequest()
do {
let items = try backgroundContext.fetch(request)
for item in items {
item.timestamp = Date()
item.data = item.data == "a" ? "b" : "a"
}
try backgroundContext.save() // Purple warning appears here
let interval = Double(Date().timeIntervalSince(start) * 1000) // Artificial two-second delay so cover view has time to appear
if interval < 2000 { sleep(UInt32((2000 - interval) / 1000)) }
completionHandler()
} catch { assert(false) }
})
}
}
// A cover view with an animation that shouldn't be blocked when saving the background context changes
struct CoverView: View {
#State private var toggle = true
var body: some View {
Circle()
.offset(x: toggle ? -15 : 15, y: 0)
.frame(width: 10, height: 10)
.animation(Animation.easeInOut(duration: 0.25).repeatForever(autoreverses: true))
.onAppear { toggle.toggle() }
}
}
struct ContentView: View {
#State private var items: [Item] = []
#State private var showingCoverView = false
#State private var refresh = UUID()
let persistence = PersistenceHelper()
let formatter = DateFormatter()
var didSave = NotificationCenter.default
.publisher(for: .NSManagedObjectContextDidSave)
// .receive(on: DispatchQuene.main) // Doesn't help
var body: some View {
ScrollView {
LazyVStack {
Button("Update Timestamp") {
showingCoverView = true
persistence.updateItemTimestamp(completionHandler: { showingCoverView = false })
}
ForEach(items, id: \.self) { item in
Text(formatter.string(from: item.timestamp!) + " " + (item.data ?? ""))
}
}
}
.id(refresh)
.onAppear {
formatter.dateFormat = "HH:mm:ss"
items = persistence.fetchItem()
}
.onReceive(didSave) { _ in
items = persistence.fetchItem()
}
.fullScreenCover(isPresented: $showingCoverView) {
CoverView().onDisappear { refresh = UUID() }
}
}
}

Since you are performing a background task, you are on a background thread - rather than the main thread.
To switch to the main thread, change the line producing the runtime warning to the following:
DispatchQueue.main.async {
try backgroundContext.save()
}

You should use Combine and observe changes to your background context and update State values for your UI to react.
#State private var coreDataAttribute = ""
var body: some View {
Text(coreDataAttribute)
.onReceive(
CoreDataManager.shared.moc.publisher(for: \.hasChanges)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.global())
.map{_ in CoreDataManager.shared.fetchCoreDataValue()}
.filter{$0 != coreDataAttribute}
.receive(on: DispatchQueue.main))
{ value in
coreDataAttribute = value
}
}

Related

SwiftUI ForEach force UI update when updating the contents of a core data relationship

My app is meant to have a bunch of workouts in core data, each with a relationship to many exercises. A view should display the data in each workout (name, description etc.) and then iterate and display each exercise belonging to that workout.
Adding exercises and displaying them works fine. If an exercise is deleted, however it:
deletes from coredata no worries
the information seems to delete from iterableExercises
however, the Text line does not disappear. it goes from, for example "Squat, Description" to simply " , "
If I close the app entirely and reopen, then the " , " lines do completely disappear.
The problem code:
if let iterableExercises = workout.exercises?.array as? [ExerciseEntity] {
ForEach(iterableExercises) {exercise in
Text("\(exercise.name ?? ""), \(exercise.desc ?? "")")
}
}
I've got the entity relationship set as ordered, but I've also tried unordered with .allObjects instead of .array. This clearly isn't the problem as it's the array iterableExercises that's not correctly being reset?
EDIT: to reproduce, here's all the code you need and some screenshots of the CoreData model.
import SwiftUI
import CoreData
class ViewModel: ObservableObject {
let container: NSPersistentCloudKitContainer
#Published var savedWorkouts: [WorkoutEntity] = []
#Published var savedExercises: [ExerciseEntity] = []
// MARK: INIT
init() {
container = NSPersistentCloudKitContainer(name: "mre")
container.loadPersistentStores { description, error in
if let error = error {
print("Error loading CoreData: \(error)")
}
}
fetchWorkoutEntities()
fetchExerciseEntities()
}
// MARK: FETCHERS
func fetchWorkoutEntities() {
let request = NSFetchRequest<WorkoutEntity>(entityName: "WorkoutEntity")
do {
savedWorkouts = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching WorkoutEntity: \(error)")
}
}
func fetchExerciseEntities() {
let request = NSFetchRequest<ExerciseEntity>(entityName: "ExerciseEntity")
do {
savedExercises = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching ExerciseEntity: \(error)")
}
}
// MARK: SAVE
func saveData() {
do {
try container.viewContext.save()
fetchWorkoutEntities()
fetchExerciseEntities()
} catch let error {
print("Error saving: \(error)")
}
}
// MARK: ADDERS
func addWorkout(name: String) {
let _ = WorkoutEntity(context: container.viewContext)
saveData()
}
func addExerciseToWorkout(workout: WorkoutEntity, name: String) {
let newExercise = ExerciseEntity(context: container.viewContext)
newExercise.name = name
workout.addToExercises(newExercise)
saveData()
}
// MARK: DELETERS
func deleteWorkout(workout: WorkoutEntity) {
container.viewContext.delete(workout)
saveData()
}
func deleteExercise(exercise: ExerciseEntity) {
container.viewContext.delete(exercise)
saveData()
}
// MARK: TODO: UPDATERS
}
struct ContentView: View {
#StateObject var data = ViewModel()
var body: some View {
VStack {
Button {
data.addWorkout(name: "workout")
data.addExerciseToWorkout(workout: data.savedWorkouts[0], name: "[exercisename]")
} label: {
Text("Click ONCE to add workout to work with")
}
Spacer()
if let iterableExercises = data.savedWorkouts[0].exercises?.array as? [ExerciseEntity] {
ForEach(iterableExercises) { exercise in
Button {
data.deleteExercise(exercise: exercise)
} label: {
Text("Click to delete \(exercise.name ?? "") AFTER DELETING IF THIS STILL SHOWS BUT DOESN'T SHOW THE EXERCISE NAME THEN IT'S BROKEN")
}
}
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
screenshots of model
I’m not sure if this is the ONLY solution as #malhal gave quite an extensive and seemingly useful response.
But I came across a much easier and immediate fix, within my original solution. The inverse relationships must be specified. Doing this resolved all issues.
We don't use view model objects in SwiftUI. You need to learn the View struct and property wrappers which gives the consistency and efficiency of value types with the benefits of reference types. The property wrapper for core data is #FetchRequest which invalidates the View when the results change. It's also a DynamicProperty (which is how it gets the context from the environment) that you can use it directly without the property wrapper syntax which allows you to use a param in a predicate, in your case to do fetch the one-to-many relation, e.g.
struct WorkoutView: View {
private var fetchRequest: FetchRequest<Exercise>
private var exercices: FetchedResults<Exercise> {
fetchRequest.wrappedValue
}
init(workout: Workout) {
let sortAscending = true
let sortDescriptors = [SortDescriptor(\Exercise.timestamp, order: sortAscending ? .forward : .reverse)]
fetchRequest = FetchRequest(sortDescriptors: sortDescriptors, predicate: NSPredicate(format: "workout = %#", workout), animation: .default)
}
var body: some View {
List(exercises) { exercise in
ExerciseView(exercise: exercise)
}
}
}
For creating the NSPersistentContainer check out the Xcode App template with Core Data checked. Looks like this:
#main
struct TestApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
The reason it is not an #StateObject is we don't want to invalidate this body when it changes and we need it to be init for previewing which is a different singleton.
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
... see template
That other code in your view model class can be moved to NSManagedObject and NSManagedObjectContext extensions. Use the Editor menu to generate the NSManagedObject extension for the model, the files need tidying up though and make sure use extension is selected for the entity.

SwiftUI: How to show/edit an int from CoreData without being in a List?

self-learning beginner here. I am trying to show an Int from Core Data in a VStack in ContentView, not in a List. But literally all the tutorials I can find about Core Data (tracking Books, Movies, Orders, Students) are using a List to show an array containing an Int. Nothing on showing an Int by itself.
Xcode can build countnum.countnum +=1 with no problem. Seems to me it is reading it fine. But once I try to show it, it just doesn’t work. I’m wrecking my brain here.
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>
var body: some View {
// let countnum = CountNum(context: moc)
VStack{
Text("+")
.padding()
.onTapGesture (count: 2){
let countnum = CountNum(context: moc)
countnum.countnum += 1
}
Text("\(countnum)") //No exact matches in call to instance method 'appendInterpolation'
}
}
}
Thanks
....all the tutorials ... show an array containing an Int. Yes, that's because CoreData
can contain many "objects". You get an array of your CountNum objects when
you do your .....var countnum: FetchedResults<CountNum>. So you need to decide which CountNum you want to
use. For example, if you want to use the first one, then:
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>
var body: some View {
VStack {
if let firstItem = countnum.first {
Text("+")
.padding()
.onTapGesture(count: 2) {
firstItem.countnum += 1
do {
try moc.save()
} catch {
print(error)
}
}
Text("\(firstItem.countnum)").foregroundColor(.green)
}
}
}
}
EDIT-1: adding new CountNum to CoreData example code in the add button.
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>
var body: some View {
Button(action: {add()}) { Text("add new CountNum").foregroundColor(.green) }
.padding(.top, 50)
List {
ForEach(countnum) { item in
HStack {
Text("++")
.onTapGesture(count: 2) { increment(item) }
Text("\(item.countnum)").foregroundColor(.blue)
Text("delete").foregroundColor(.red)
.onTapGesture { delete(item: item) }
}
}
}
}
func increment(_ item: CountNum) {
item.countnum += 1
save()
}
func add() {
let countnum = CountNum(context: moc)
countnum.countnum = 0
save()
}
func delete(item: CountNum) {
moc.delete(item)
save()
}
func save() {
do { try moc.save() } catch { print(error) }
}
}

Too Many Notifications CoreData/CloudKit Sync

I have several apps that use CoreData / iCloud syncing and they all receive a slew of update/change/insert/delete/etc notifications when they start and sometimes as they are running without any changes to the underlying data. When a new item is added or deleted, it appears that I get notifications for everything again. Even the number of notifications are not consistent.
My question is, how do I avoid this? Is there a cut-off that can be applied once I'm sure I have everything up to date on a device by device basis.
Persistence
import Foundation
import UIKit
import CoreData
struct PersistenceController {
let ns = NotificationStuff()
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.stuff = "Stuff"
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "TestCoreDataSync")
if inMemory {
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)")
}
})
}
}
class NotificationStuff
{
var changeCtr = 0
init()
{
NotificationCenter.default.addObserver(self, selector: #selector(self.processUpdate), name: Notification.Name.NSPersistentStoreRemoteChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contextDidSave(_:)), name: Notification.Name.NSManagedObjectContextDidSave, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contextObjectsDidChange(_:)), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
}
#objc func processUpdate(_ notification: Notification)
{
//print(notification)
DispatchQueue.main.async
{ [self] in
observerSelector(notification)
}
}
#objc func contextObjectsDidChange(_ notification: Notification)
{
DispatchQueue.main.async
{ [self] in
observerSelector(notification)
}
}
#objc func contextDidSave(_ notification: Notification)
{
DispatchQueue.main.async
{
self.observerSelector(notification)
}
}
func observerSelector(_ notification: Notification) {
DispatchQueue.main.async
{ [self] in
if let insertedObjects = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject>, !insertedObjects.isEmpty
{
print("Insert")
}
if let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject>, !updatedObjects.isEmpty
{
changeCtr = changeCtr + 1
print("Change \(changeCtr)")
}
if let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject>, !deletedObjects.isEmpty
{
print("Delete")
}
if let refreshedObjects = notification.userInfo?[NSRefreshedObjectsKey] as? Set<NSManagedObject>, !refreshedObjects.isEmpty
{
print("Refresh")
}
if let invalidatedObjects = notification.userInfo?[NSInvalidatedObjectsKey] as? Set<NSManagedObject>, !invalidatedObjects.isEmpty
{
print("Invalidate")
}
let mainManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
guard let context = notification.object as? NSManagedObjectContext else { return }
// Checks if the parent context is the main one
if context.parent === mainManagedObjectContext
{
// Saves the main context
mainManagedObjectContext.performAndWait
{
do
{
try mainManagedObjectContext.save()
} catch
{
print(error.localizedDescription)
}
}
}
}
}
}
ContentView
import SwiftUI
import CoreData
struct ContentView: View {
#State var stuff = ""
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
VStack
{
TextField("Type here", text: $stuff,onCommit: { addItem(stuff: stuff)
stuff = ""
})
List {
ForEach(items) { item in
Text(item.stuff ?? "??")
}
.onDelete(perform: deleteItems)
}
}.padding()
}
private func addItem(stuff: String) {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
newItem.stuff = stuff
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
The database has an Item entity with a timestamp field and a string field named stuff.
It depends on if it's for examining Production or Debug builds in the system's Console or Xcode's Console respectively.
For Production builds, my understanding is the aim is to make my messages more findable (rather than de-emphasising/hiding other messages) by consistently using something like:
let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "YourCategorisationOfMessagesGoingToThisHandle")
and then in the code I might have things like
log.debug("My debug message")
log.warning("My warning etc")
fwiw: I tend to categorise stuff by the file it's in, as that's deterministic and helps me find the file, so my source files tend to start with
fileprivate let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #file.components(separatedBy: "/").last ?? "")
If I do this, then I can easily filter the system's console messages to find stuff that's relevant to my app.
There's more on how to use this and the console to filter for the app's messages in the sytem console over here.
For Debug builds and the Xcode console the same consistent app log messages from my app could be used, e.g. my app's debug messages always start with "Some easily findable string or other". I don't believe there is a way to throttle/cut-off responses selectively. But it definitely possible to turn off debug messages from many of the noisy sub-systems completely (once happy that they are working reliably)
For Core Data and CloudKit cases mentioned, if I run the Debug builds with the -com.apple.CoreData.Logging.stderr 0 and -com.apple.CoreData.CloudKitDebug 0 launch args then that make Xcode's console a lot quieter :-). Nice instructions on how to set this up in the SO answer over here
My problem was that CoreData -> CloudKit integration was re-synching the same items over and over, thus the notifications. I discovered that I needed to add a sorted index for the modifiedTimestamp on all entities. Now things are much faster and few if any re-synched items.

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!

Trying to incorporate sound within a beacon region in Swift

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.

Resources