Getting a notification that a volume has mounted on iOS - ios

An external drive / volume can be connected to an iPad. I want to be able to take an action in my app when this happens and my app is in the foreground - say for example bringing up an import dialog.
On macOS I can write the following to get a notification that a volume has mounted and take an action - here in this basic example I set a property to true:
struct SomeViewOfMine: View {
#State private var importDialogVisible = false
private var cancellable: Set<AnyCancellable>()
init() {
NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didMountNotification)
.sink { _ in
self.importDialogVisible = true
}
.store(in: &cancellable)
}
// Other code in my view here
}
But I can't find the same functionality within iOS. Am I missing something or is there a way to achieve this?

Related

Launch new window on iOS app using SwiftUI Lifecycle

The following code on WindowScene does indeed open a new window on macOS when a button is pressed in ContentView that opens an URL:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
WindowGroup("Viewer") {
Text("Viewer").padding()
}
.handlesExternalEvents(matching: ["*"])
}
}
However, when the same is applied to iOS, the app does not do anything when the openURL action is called.
The result from my research is that I can use the traditional requestSceneSessionActivation to open window manually. However, this will defeat the purpose of using the SwiftUI app lifecycle.
Any suggestions on a working method, without reverting back to the UIKit lifecycle?
After a very long research session, I finally found the property that does this in this blog post.
Turns out that requestSceneSessionActivation is necessary, but there's no need to use a SceneDelegate.
When creating the new session, set the targetContentIdentifier on the NSUserActivity object.
let activity = NSUserActivity(activityType: "newWindow")
activity.userInfo = ["some key":"some value"]
activity.targetContentIdentifier = "newWindow" // IMPORTANT
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil)
Finally, make sure the new WindowGroup can handle the event with the identifier:
WindowGroup(id: "newWindow") {
Text("New Window!").padding()
}
.handlesExternalEvents(matching: ["newWindow"])
This solution retains the SwiftUI Lifecycle, and also works on Catalyst.
Update for iPadOS 16/macOS Ventura:
You can now use the new openWindow environment property:
#Environment(\.openWindow) private var openWindow
...
openWindow(id: "newWindow")
Make sure the id passed to openWindow is the same as the id set in the WindowGroup or Window initializer.

Ipad iOS 14: icon badge flickering

I've found this strange behavior on ipad running ipados 14 (I've in both simulator and physical device installed 14.5, with 14.5.1 in the real one).
I need to show an icon badge with a number based on work made within the app, so there is no external push notification service involved in this.
I've set up a ViewModel that keep track of the number to show in the notification badge:
import Combine
import Foundation
class CounterViewModel: ObservableObject {
#Published private(set) var notificationCount = 42
func up() {
self.notificationCount += 1
}
func down() {
self.notificationCount -= 1
}
}
To publish this number on the notification badge on the app icon I'm using the .onAppear method of the contentView
struct ContentView: View {
#EnvironmentObject var counterViewModel: CounterViewModel
#State private var counterCancellable: AnyCancellable?
var body: some View {
Text("Hello World!")
.onAppear {
UNUserNotificationCenter.current().requestAuthorization(options: .badge) { _, error in
if error == nil {
self.counterCancellable = self.counterViewModel.$notificationCount.sink { count in
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = count
}
}
}
}
}
}
}
Finally, the EnvironmentObject is set up on the app
struct IBreviaryApp: App {
#ObservedObject private var counterViewModel = CounterViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.counterViewModel)
}
}
}
so I can share on all the view involved in this count mechanism.
The problem: on the first run of the app, and only on the first one, after accepting the alert about authorising the notifications, when the app is put on background the badge within the icon is shown, but suddenly disappear. In the dock does not even show.
If I click the app (both on dock or on the "desktop") the badge appear in the animation that open the app.
If I scroll between the pages of installed applications and return to the one where my app is installed, the badge now is fixed. If I open another app, so the dock show this new app in the recent used app list, the badge appear here also (and stays this time).
Restarting the ipad or reinstalling the app fixes the problem, but itself remain for the first time you run the app.
EDIT: repo here: https://github.com/artecoop/badgebug
To actually make the problem arise, I needed to change the count number from another function.
EDIT 2:
Here's a video: https://youtu.be/tPWYRm5xFXI
As you may already see on the repo, to mimic another interaction, I've added a 5 second delay and then set 42 in the counter.

Why are Google calendars not showing up in EKCalendarChooser on macOS in SwiftUI

everyone.
What I am doing: I am writing a program doing various date and time-related things in SwiftUI for iOS and macOS. The current version of the UI is written in SwiftUI. Since my program can use EventKit (the engine behind events in Calendar.app) and EventKitUI does not yet have a SwiftUI equivalent, I have wrappers for EventKitUI view controllers.
What is going wrong: I have (among other things) a wrapper around EKCalendarChooser to allow the user to select which external event calendars he/she wants my program to use. This works fine on iOS. On macOS, however, event calendars in my Google account do not appear, even though my program shows the events themselves in macOS. Local, iCloud, and “other” event calendars do appear.
My code:
import EventKitUI
import SwiftUI
struct ASAEKCalendarChooserView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
#Environment(\.presentationMode) var presentationMode
var externalEventManager = ASAExternalEventManager.shared
var calendars: Set<EKCalendar>? = ASAExternalEventManager.shared.calendarSet
func makeUIViewController(context: UIViewControllerRepresentableContext<ASAEKCalendarChooserView>) -> UINavigationController {
let chooser = EKCalendarChooser(selectionStyle: .multiple, displayStyle: .allCalendars, entityType: .event, eventStore: externalEventManager.eventStore)
chooser.selectedCalendars = calendars ?? []
chooser.delegate = context.coordinator
chooser.showsDoneButton = true
chooser.showsCancelButton = true
return UINavigationController(rootViewController: chooser)
}
func updateUIViewController(_ uiViewController: UINavigationController, context: UIViewControllerRepresentableContext<ASAEKCalendarChooserView>) {
}
class Coordinator: NSObject, UINavigationControllerDelegate, EKCalendarChooserDelegate {
let parent: ASAEKCalendarChooserView
init(_ parent: ASAEKCalendarChooserView) {
self.parent = parent
}
func calendarChooserDidFinish(_ calendarChooser: EKCalendarChooser) {
debugPrint(#file, #function, calendarChooser)
let calendars = calendarChooser.selectedCalendars
parent.externalEventManager.calendars = Array(calendars)
ASAUserData.shared().savePreferences(code: .events)
parent.presentationMode.wrappedValue.dismiss()
}
func calendarChooserDidCancel(_ calendarChooser: EKCalendarChooser) {
debugPrint(#file, #function, calendarChooser)
parent.presentationMode.wrappedValue.dismiss()
}
}
}
Note: ASAExternalEventManager is a class of mine to make dealing with EKEventStore easier.
Also: The “missing” event calendars show up in Calendar.app, both in macOS and iOS.
Does anyone have any idea why I am having this problem? I do not understand why the same code running on two devices using the same accounts is giving two noticeably different results.
Thanks in advance for any help anyone can provide.
I'm also having exact same problem. I do not have a solution to this... But I have tried to get the calendar's sources from Eventstore. And and I run my app on Mac, it lists all calendars including the ones from Google. So I guess somehow the EKCalendarChooser does not see calendars from Google. As אהרן אדלמן suggested, may be I have to crate my own calendar chooser interface with SwiftUI. But again, that's a lot of work compared to EKCalendarChooser which should just work.
Apple has just replied (June 2022) to my year-old bug report about this issue, and they claim that it is fixed in the macOS 13 beta. I'm unable to test and verify that at this time though.

SwiftUI CloudKit not refreshing view when remaining active

I am developing a macOS and iOS app with SwiftUI. Both are using CoreData and iCloudKit to sync data between both platforms. It is indeed working very well with the same iCloud Container.
I am facing a problem that the iCloud background update is not being triggered when staying in the application. If I make changes on both systems, the changes are being pushed, however not visible on the other device.
I need to reload the app, close the app and open again or lose focus in my Mac app and come back to it. Then my List is going to be refreshed. I am not sure why it is not working, while staying inside the app without losing focus.
I am read several threads here in Stackoverflow, however they are not working for me. This is my simple View in iOS
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#State private var refreshing = false
private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
#FetchRequest(entity: Person.entity(), sortDescriptors: []) var persons : FetchedResults<Person>
var body: some View {
NavigationView
{
List()
{
ForEach(self.persons, id:\.self) { person in
Text(person.firstName + (self.refreshing ? "" : ""))
// here is the listener for published context event
.onReceive(self.didSave) { _ in
self.refreshing.toggle()
}
}
}
.navigationBarTitle(Text("Person"))
}
}
}
In this example I am already using a workaround, with Asperi described in a different question. However, that isn't working for me either. The list is not being refreshed.
In the logs I can see that it is not pinging the iCloud for refreshing. Only when I reopen the app. Why is background modes not working? I have activate everything properly and set up my AppDelegate.
lazy var persistentContainer: NSPersistentCloudKitContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
container.persistentStoreDescriptions.forEach { storeDesc in
storeDesc.shouldMigrateStoreAutomatically = true
storeDesc.shouldInferMappingModelAutomatically = true
}
//let container = NSPersistentCloudKitContainer(name: "NAME")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
UIApplication.shared.registerForRemoteNotifications()
return container
}()
Edit:
My iOS app only keeps fetching records from iCloud, when the app is being reopened. See this gif:
So apart from my comments and without more information, I suspect you have not set up your project correctly.
Under Signings and Capabilities, your project should look similar to this...
As mentioned I suspect a lot of the code in your ContentView view is unnecessary. Try removing the notifications and simplifying your view code, for example...
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Person.entity(),
sortDescriptors: []
) var persons : FetchedResults<Person>
var body: some View {
NavigationView
{
List()
{
ForEach(self.persons) { person in
Text(person.firstName)
}
}
.navigationBarTitle(Text("Person"))
}
}
}
With your project correctly setup, CloudKit should handle the necessary notifications and the #FetchRequest property wrapper will update your data set.
Also, because each Core Data entity is by default Identifiable, there is no need to reference id:\.self in your ForEach statement, so instead of...
ForEach(self.persons, id:\.self) { person in
you should be able to use...
ForEach(self.persons) { person in
As mentioned in the comments, you have included unnecessary code in your var persistentContainer. It should work as this...
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "NAME")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
return container
}()
For anyone looking at this lately, the fix for me after a LOT of searching was simply adding container.viewContext.automaticallyMergesChangesFromParent = true inside the init(inMemory) method in Persistence.swift (all stock from Apple for a SwiftUI in Xcode 12.5.1. As soon as I added that and rebuilt the 2 simulators, everything sync'd w/in 5-15 seconds as it should.
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "StoreName")
container.viewContext.automaticallyMergesChangesFromParent = true

Today extension: syncing data with container app

Context
I've been playing around with Today Extensions using this example project.
The app is quite simple:
In the containing app, you have a list of todo items, which you can mark completed
In the Today widget, you see the same list, but you can switch between completed, and incomplete items using a segmented control.
My goal is the following: whenever there is a data change, either in the container app, or the widget, I want both to reflect the changes:
If I mark an item as completed in the container app, then pull down the Notification Center, the widget should be updated
When I do the same in the widget, then return to the app, the app's state should be updated
The implementation
I understand, that the container app, and the extension run in their separate processes, which means two constraints:
NSUserDefaultsDidChangeNotification is useless.
Managing the model instances in memory is useless.
I also know, that in order to access a shared container, both targets must opt-in to the App Groups entitlements under the same group Id.
The data access is managed by an embedded framework, TodoKit. Instead of keeping properties in memory, it goes straight to NSUserDefaults for the appropriate values:
public struct ShoppingItemStore: ShoppingStoreType {
private let defaultItems = [
ShoppingItem(name: "Coffee"),
ShoppingItem(name: "Banana"),
]
private let defaults = NSUserDefaults(suiteName: appGroupId)
public init() {}
public func items() -> [ShoppingItem] {
if let loaded = loadItems() {
return loaded
} else {
return defaultItems
}
}
public func toggleItem(item: ShoppingItem) {
let initial = items()
let updated = initial.map { original -> ShoppingItem in
return original == item ?
ShoppingItem(name: original.name, status: !original.status) : original
}
saveItems(updated)
}
private func saveItems(items: [ShoppingItem]) {
let boxedItems = items.map { item -> [String : Bool] in
return [item.name : item.status]
}
defaults?.setValue(boxedItems, forKey: savedDataKey)
defaults?.synchronize()
}
private func loadItems() -> [ShoppingItem]? {
if let loaded = defaults?.valueForKey(savedDataKey) as? [[String : Bool]] {
let unboxed = loaded.map { dict -> ShoppingItem in
return ShoppingItem(name: dict.keys.first!, status: dict.values.first!)
}
return unboxed
}
return nil
}
}
The problem
Here's what works:
When I modify the list in my main app, then stop the simulator, and then launch the Today target from Xcode, it reflects the correct state. This is true vice-versa.
This verifies, that my app group is set up correctly.
However, when I change something in the main app, then pull down the Notification Center, it is completely out of sync. And this is the part, which I don't understand.
My views get their data straight from the shared container. Whenever a change happens, I immediately update the data in the shared container.
What am I missing? How can I sync up these two properly? My data access class is not managint any state, yet I don't understand why it doesn't behave correctly.
Additional info
I know about MMWormhole. Unfortunately this is not an option for me, since I need to reach proper functionality without including any third party solutions.
This terrific article, covers the topic, and it might be possible, that I need to employ NSFilePresenter, although it seems cumbersome, and I don't completely understand the mechanism yet. I really hope, there is an easier solution, than this one.
Well, I have learned two things here:
First of all, Always double check your entitlements, mine somehow got messed up, and that's why the shared container behaved so awkwardly.
Second:
Although viewWillAppear(_:) is not called, when you dismiss the notification center, it's still possible to trigger an update from your app delegate:
func applicationDidBecomeActive(application: UIApplication) {
NSNotificationCenter.defaultCenter().postNotificationName(updateDataNotification, object: nil)
}
Then in your view controller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserverForName(updateDataNotification, object: nil, queue: NSOperationQueue.mainQueue()) { (_) -> Void in
self.tableView.reloadData()
}
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
Updating your Today widget is simple: each time the notification center is pulled down, viewWillAppear(:_) is called, so you can query for new data there.
I'll update the example project on GitHub shortly.

Resources