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.
Related
I am building an app with SwiftUI and would like to use requestReview() to display a review request popup. However, the popup is not displaying. From what I understand from the documentation, when the app is in development mode, the popup should display every time.
I've tested the app in the Xcode simulator and on a real device, I'm using Xcode 14.1. and the app builds without issue. (I'm also a SwiftUI newbie, I think I must be missing something obvious).
import SwiftUI
import StoreKit
struct DetailView: View {
#Environment(\.requestReview) var requestReview
#AppStorage("AppUsedCount") private var appUsedCount: Int = 0
var body: some View {
VStack {...}.onAppear{
let updatedAppUsedCount: Int = appUsedCount + 1
UserDefaults.standard.set(updatedAppUsedCount, forKey: "AppUsedCount")
if (appUsedCount > 5){
requestReview()
}
}
}
}
I also tested without the IF statement, but still no popup. appUsedCount updates as expected.
Thanks in advance!
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.
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?
It's my first question on stackoverflow. Please correct me if I wrong with formatting topic.
I get crash of application when try use long press (?haptic touch?) on real iPad. Also it's reproducible on Simulators iPad with Force Touch click on Magic Trackpad.
Which devices have this bug?
Every iPad since iOS 13.4 until the last iOS 13.
Every Simulator iPad since iOS 13.4 until the last iOS 13.
I never saw this bug on iPhone.
My set up for testing: MacOS Catalina 10.15.6, xCode 11.7,
Simulator iPad mini 2019 5-gen iOS 13.7,
iPad 2018 6-gen iOS 13.7.
Upd 26.10.2020. Also, I try to use xCode 12.0.1 on MacOS Catalina 10.15.7,
iPad 2018 6-gen iOS 13.7. I retested, looks like the bug still available on iOS 13.4-13.7, but is gone for iOS 14. Good decision for iOS 13 I didn't find.
Steps to reproduce the bug.
I use SwiftUI view (TestCrashIPAD) with List, ForEach and dynamic sections (source code below).
Some static sections are predefined, some dynamic sections appear after fetching from server.
IMPORTANT the bug reproducible only when first appear view! Don't go to another screen or minimize the app!
Just open this view (TestCrashIPAD).
Wait until dynamic sections appers.
As soon as the sections appear, try long-press on the cell with text "r1" or "r2" or ... (screenshot)
Get crash (screenshot)
CRASH
I get crash on AppDelegate with:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
I investigated a little bit, and found: this bug reproducible since iOS 13.4. Also with iOS 13.4 there is a View Modifier onDrag (link). May be it's somehow connected, because in call stack I see:
UIDragInteractionLongPressDriver
UIGestureRecognizer
May be something wrong with dynamic sections array, because I see:
SwiftUI.Sections.rowIDs(forSectionAt: Swift.Int)
SwiftUI.ListCoreDataSource.rowIndex(at: Foundation.IndexPath)
Question
How to fix this issue? How to work around without crash in this case? Please, say any idea.
Source Code
Please have a look the source code:
import SwiftUI
struct TestCrashIPAD: View {
#State var sections = [TestSection]()
var body: some View {
List {
// predefined sections
Section(header: Text("Section predefined")) {
Text("1")
Text("2")
}
Section(header: Text("Section predefined")) {
ForEach(1..<7) {
Text("\($0)")
}
}
// dynamic content
ForEach(sections) { section in
Section(header: Text(section.title)) {
ForEach(section.rows) { row in
HStack {
Text(row.str)
Spacer()
}
}
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("iPad crash with long press")
.onAppear(perform: onAppearAction)
}
func onAppearAction() {
DispatchQueue.global().async {
// fetch some data from the server
sleep(3)
// show data in UI
DispatchQueue.main.async {
self.sections = [TestSection](TestSection.array)
}
}
}
}
struct TestSection: Identifiable {
let id = UUID()
let title: String
let rows: [TestRow]
static var array: [TestSection] {
var result = [TestSection]()
for idx in 0..<50 {
result.append(TestSection(title: "s\(idx)", rows: TestRow.array))
}
return result
}
}
struct TestRow: Identifiable {
let id = UUID()
let str: String
static var array: [TestRow] {
var result = [TestRow]()
for idx in 0..<Int.random(in: 3..<7) {
result.append(TestRow(str: "r\(idx)"))
}
return result
}
}
Looks like the answer is to add .onLongPressGesture { } to your lists that are crashing
(i got this from comments, but this resolved my issues, so seems like an answer. posting as an answer bc not everyone reads comments
In my first SwiftUI app, I have Remote Notifications and Background Processes enabled.
I did add an AppDelegate class, to support notification.
The notifications set the app badge to an appropriate value.
Since this app has these background modes enabled, several lifecycle events are not working:
applicationDidBecomeActive
applicationWillResignActive
applicationDidEnterBackground
applicationWillEnterForeground
Question: where/how do I reset the badge?
Here is how you can observe didBecomeActiveNotification:
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
UIApplication.shared.applicationIconBadgeNumber = 0
}
}
}
}
You can observe other notifications in the same way.
Alternatively you can use an #EnvironmentObject to track the application state:
How can I use a method without any page transition or any reboot app