I have created a piece of code to send a local notification at 3 pm and the user can toggle it with a toggle switch. I used an AppStorage wrapper to remember the state it was in. The code works until I toggle the switch on and off (simulating whether the user switches it multiple times) and the number of times I switched the toggle, that same number comes as a notification (i.e. I toggle 5 times, I get 5 notifications all at once). this is the code
struct trial: View {
#AppStorage("3") var three = false
func firstNotify() {
let content = UNMutableNotificationContent()
content.title = "It is 3pm"
content.subtitle = "Read the Prime Hour"
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "notifysound.wav"))
// content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "notifysound"))
var dateComponents = DateComponents()
dateComponents.hour = 15
dateComponents.minute = 00
dateComponents.second = 00
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
// choose a random identifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
// add our notification request
UNUserNotificationCenter.current().add(request)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
print("All set!")
} else if let error = error {
print(error.localizedDescription)
}
}
}
#State private var name: String = "Tim"
var body: some View {
VStack {
Toggle(isOn: $three) {
Text("3 pm")
}
.padding(.horizontal)
.onChange(of: three) { newValue in
if newValue {
firstNotify()
}
}
}
}
}
struct trial_Previews: PreviewProvider {
static var previews: some View {
trial()
}
}
I think what's happening is that the func is not reading whether the toggle is on or off but just adds a notification each time the switch gets turned on. Would anyone know the solution to this???
The toggle is really only scheduling a new notification on every change of the value to true.
This piece of code means that a new notification will be scheduled each time this method is called.
// choose a random identifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
// add our notification request
UNUserNotificationCenter.current().add(request)
Not entirely sure what the desired logic is, but you could solve this by either updating the existing notification or clearing out the old one and adding a new one. Rather than creating a new UUID for each notification, it may be best to have an ID convention or otherwise store it so it can be accessed or removed.
You could store the rather than the toggle value:
#AppStorage("notification_id") var notificationID = nil // I don't know if this can be a string or nil, but hopefully it leads you in the right direction.
// Then when creating the request.
let id = notificationID ?? UUID().uuidString
let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
notificationID = id
And there needs to be another method when to unschedule when they toggle the other direction. You could simply check to see if the id exists.
if let id = notificationID {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers identifiers: [id])
// OR
UNUserNotificationCenter.current()removeAllPendingNotificationRequests()
}
Related
I'm creating a document-based application where data is represented by TextFields in a TableView (it could also be a List, the same issue occurs). When the app SwiftUI app on an Intel MacBook Air, I get a lot of keyboard lag whenever there are more than a dozen rows in my table. It's present on the Apple Studio too, but less noticeable. I've tried changing the table into a List and LazyVStack, but it doesn't seem to make much difference. Using the Swift UI instrument, it looks to me like every TextField on the page is being redrawn on every keystroke, even though their values haven't changed.
I also tried using a custom TextField with a debounce added in (with this as a starting point). This works well for reducing the lag, but I don't think this is how debouncing is intended to be used and I ended up with some strange behaviour.
I suspect that it is rather the case that I've misunderstood how to using #Binding variables in a Document Based application, or possibly I have misconfigured the Struct where I store the data. So here are the essential parts of my code, which will hopefully make it clear where I have gone wrong without having to run anything.
struct ClaraApp: App {
#StateObject var globalViewModel = GlobalViewModel()
var body: some Scene {
DocumentGroup(newDocument: ClaraDocument(claraDoc:GroupVocab())) { file in
MainContentView(data: file.$document)
.environmentObject(self.globalViewModel)
}
}
struct MainContentView: View {
#Binding var data: ClaraDocument // Binding to the document
#EnvironmentObject var globalViewModel : GlobalViewModel
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
HostingWindowFinder { window in
if let window = window {
self.globalViewModel.addWindow(window: window)
print("New Window", window.windowNumber)
self.globalViewModel.addViewModel(self.viewModel, forWindowNumber: window.windowNumber)
window.becomeKey()
}
}
.frame(width:0, height: 0)
VStack{
TabView(selection: $viewModel.activeTab){
VocabView(vocabs: $data.claraDoc.vocabs, selectedVocab: $viewModel.selectedVocab)
.tabItem{
Label("Vocabulary", systemImage: "tablecells")
}
.tag(TabType.vocab)
//more tabs here
}
}
}
}
struct VocabView: View{
#Binding var vocabs: [Vocab] // Binding to just the part of the document concerned by this view
#Binding var selectedVocab: Vocab.ID?
var body: some View{
VStack(){
VocabTable(vocabs: $vocabs, selection: $selectedVocab)
.padding([.top, .leading, .trailing])
HStack{
Button("-"){
if selectedVocab != nil{
let oldSelectionIndex = vocabs.firstIndex(where: {$0.id == selectedVocab!})
if oldSelectionIndex != nil{
if oldSelectionIndex! > 0{
selectedVocab = vocabs[oldSelectionIndex! - 1].id
} else {
selectedVocab = nil
}
vocabs.remove(at: oldSelectionIndex!)
}
}
}
.disabled(selectedVocab == nil)
Text("\(String(vocabs.count)) entries")
Button("+"){
let newVocab = Vocab(id: UUID(), word: "", def: "", trans: "", visNote: "", hidNote: "", link: Link(linked: false), date: Date())
vocabs.append(newVocab)
selectedVocab = newVocab.id
}
}
}
}
}
struct VocabTable: View{
#Binding var vocabs: [Vocab]
#Binding var selection: Vocab.ID?
var body: some View{
Table($vocabs, selection: $selection){
TableColumn("Word") {vocab in
TextField("Word", text: vocab.word)
}
TableColumn("Definition") {vocab in
TextField("Definition", text: vocab.def)
}
TableColumn("Translation") {vocab in
TextField("Translation", text: vocab.trans)
}
TableColumn("Visible note") {vocab in
TextField("Visible note", text: vocab.visNote)
}
TableColumn("Hidden note") {vocab in
TextField("Hidden note", text: vocab.hidNote)
}
TableColumn("Created") {vocab in
HStack{
Text(vocab.date.wrappedValue, style: .date)
Text(vocab.date.wrappedValue, style: .time)
}
}
}
.onDeleteCommand{
if selection != nil{
let oldSelectionIndex = vocabs.firstIndex(where: {$0.id == selection!})
if oldSelectionIndex != nil{
if oldSelectionIndex! > 0{
selection = vocabs[oldSelectionIndex! - 1].id
} else {
selection = nil
}
vocabs.remove(at: oldSelectionIndex!)
}
}
}
}
}
// vocab struct which is contained as an array [Vocab] inside the GroupVocab struct
struct Vocab: Identifiable, Codable, Equatable, Hashable {
let id: UUID
var word: String
var def: String
var trans: String
var visNote: String
var hidNote: String
var date: Date
init(id: UUID = UUID(), word: String? = "", def: String? = "", trans: String? = "", visNote: String? = "", hidNote: String? = "", date: Date? = Date()){
self.id = id
self.word = word ?? ""
self.def = def ?? ""
self.trans = trans ?? ""
self.visNote = visNote ?? ""
self.hidNote = hidNote ?? ""
self.date = date ?? Date()
}
static func == (lhs: Vocab, rhs: Vocab) -> Bool {
return lhs.id == rhs.id && lhs.word == rhs.word && lhs.def == rhs.def && lhs.trans == rhs.trans && lhs.visNote == rhs.visNote && lhs.date == rhs.date
}
}
struct GroupVocab: Codable{
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var groupName: String = ""
var vocabs = [Vocab]()
var learners = [Learner]()
var lessons = [Lesson]()
var startDate = Date()
var endDate = Date()
}
If that doesn't shed any light, here's my attempt at making a minimal example. It isn't nearly as laggy as the actual app, but from what I can tell it exhibits the same problems. Of course, it could be something in my actual app, which is not present in this minimal example, that I have overlooked. For example, I know that the menu bar is redrawn when editing the document, but removing the menu bar doesn't improve performance. So, I'm assuming that the reduced (albeit still present) lag is due to the general baggage of the program and not one specific element that I haven't taken into account. Obviously if there are no obvious problems in the above, I will need to go back and check everything again, but to my knowledge I have already tried removing and readding each part of the application individually to no avail.
Finally, this is what the actual app looks like in use:
As mentioned in the comments on original post.
TL;DR; For those encountering a similar lag issue, the solution here was to replace the declaration of Vocab as a struct with the use of an ObservableObject class, i.e. Vocab's definition becomes class Vocab: ObservableObject, Identifiable, Codable, Equatable.
Might also want to have a look at https://www.hackingwithswift.com/books/ios-swiftui/adding-codable-conformance-for-published-properties if #Published properties in the class have to be Codable
In a bit more detail
When struct Vocab is used each keystroke (because it is a value type) creates a new (original data + change) instance of the Vocab struct.
The problem [0] with this struct new instance is that SwiftUI cannot detect the singular property change and trigger just updating its corresponding TextField [1].
Instead SwiftUI handles each new keystroke driven struct instance as if it is a completely unrelated Vocab object; for which it has to update every TextField in the Table's entry row.
It's the updating of all of the TextFields in the entry row that causes the perceived lag.
By contrast the solution - using an ObservableObject class - enables binding the TextFields to a property on an object where the instance does not change on each keystroke. Consequently SwiftUI is able to detect and update just the individual entry changes.
The final piece in the puzzle is that when using an ObservableObject. The #Published properties that update Views nicely take some extra effort to enable them to conform with the Codeable protocol. For how to add that conformance there is a nice explanation over [here[( https://www.hackingwithswift.com/books/ios-swiftui/adding-codable-conformance-for-published-properties)
Other bits
If running on higher spec machines - or with fewer properties - issues like these can be difficult to spot.
Other approaches might be possible. For instance, if it's practicable within the context of the rest of the app, relaxing Vocab's Equatable compliance [1] might be enough to enable SwiftUI to do something more clever when determining what TextFields need recomputing.
[0] In this context; generally though, preferring value types such as structs is seen as good practice because it reduces the risk of unexpected side-effects.
[1] Possibly this might also be addressable by relaxing the implementation of Equatable conformance on the struct to just being based on id equivalence.
I develop a project which utilizes a dynamic data structure (tree-like). Both number of nodes in the structure change over time as well as data that is kept in the nodes. Some properties affect other - both within a certain node but also inside parent/children.
I've managed to get to the point where changes propagate correctly but a problem arose. The piece of code which sets up subscriptions and contains logic of propagation is a complete mess - due to the fact that I nest subscriptions set up. I'm new to the Combine framework so probably I don't know how to use it correctly. I'm hoping to get a suggestion.
Story that hopefully illustrate the problem
Imagine that you have a tree and if you subscribe to a node's data
stream you are going to receive data from the node itself as well as
from its ancestors. The problem is that in order to get a data from
one subject you must go through a different one.
Code ready for copying and pasting to the playground:
//
// Copyright © 2021 Mateusz Stompór. All rights reserved.
//
import Combine
// Helper
struct WeakRef<T: AnyObject> {
weak var reference: T?
init(_ reference: T?) {
self.reference = reference
}
}
// Parent is observable in order to react for change
class Node<T> {
let data: T
var parent: CurrentValueSubject<WeakRef<Node>, Error>
let stream: CurrentValueSubject<T, Error>
private var parentSubscription: AnyCancellable?
private var parentStreamSubscription: AnyCancellable?
init(data: T, parent: Node?) {
self.data = data
self.parent = CurrentValueSubject(WeakRef(parent))
self.stream = CurrentValueSubject(data)
setup()
}
func setup() {
parentSubscription = parent.compactMap({ $0.reference?.stream }).sink(receiveCompletion: { [weak self] _ in
self?.parentStreamSubscription?.cancel()
}, receiveValue: { [weak self] stream in
self?.parentStreamSubscription = stream.sink { _ in
// Nothing needed
} receiveValue: { value in
guard let self = self else {
return
}
self.stream.send(value)
}
})
}
}
let parent = Node(data: 2, parent: nil)
let child = Node(data: 1, parent: nil)
let subscription = child.stream.sink { _ in
// nothing needed
} receiveValue: { value in
print(value)
}
// '1' is printed right away
// Setup connection
child.parent.send(WeakRef(parent))
// '2' is printed once connection is set
parent.stream.send(3)
// '3' is printed
// Changing child's parent
let newParent = Node(data: 4, parent: nil)
child.parent.send(WeakRef(newParent))
// '4' is printed as parent change
parent.stream.send(5)
// '5' is NOT printed, node is no longer part of the tree
newParent.stream.send(6)
// '6' is printed
The core question: is there a way to avoid this kind of nesting?
I am trying and failing to produce an animation when the user taps the button to delete all entries in the ForEach below.
When the user confirms deletion of all entries in the array that populates the list (triggering the clearLog()function) the view just statically changes to the deleted state. I would like a simple animation such as the entries collapsing to the top, or swiping to the leading edge. Full code below.
I have tried making the RestRequestView struct conform to Identifiable and getting rid of the ./self with no success, and also just adding 'withAnimation' to the clearLog() call.
I haven't yet been able to post a minimal reproducible example.
The current behaviour is this one, with the list just popping empty with no animation.
The swipe to delete feature animates correctly.
Can anyone help? Thanks in advance!
struct RecentRequestsView: View {
/// database of requests to be used in the RecentRequestsView
#ObservedObject var requestLog: RequestLog
/// clears the request log
func clearLog() {
requestLog.clearLog()
}
/// triggers the deletion confirmation alert
func showAlert() {
showingClearAlert = true
}
#State private var showingClearAlert = false
let alertString = "Do you wish to delete all entries in this list?"
var body: some View {
NavigationView {
if requestLog.requests.isEmpty {
ZStack {
Color.gray
.opacity(0.1)
Text("Previous rest calculations will be shown here.")
.font(.title2)
.multilineTextAlignment(.center)
.padding()
.opacity(0.5)
}.navigationBarTitle("Recent Rests", displayMode: .inline)
} else {
List {
ForEach(requestLog.requests.sorted().reversed(), id: \.self) { request in
RestRequestView(request: request)
}
.onDelete(perform: delete)
.transition(.slide)
}.navigationBarTitle("Recent Rests", displayMode: .inline)
.toolbar {
Button(action: showAlert) {
Image(systemName: "trash")
}
}
.alert(alertString, isPresented: $showingClearAlert) {
Button("Cancel", role: .cancel) {
showingClearAlert = false
}
Button("Delete All", role: .destructive) {
clearLog()
}
}
}
}
}
/// Function called by the delete gesture which triggers the deletion of the specified rest request.
/// - Parameter offsets: offsets sent by the swipe to delete gesture
func delete(at offsets: IndexSet) {
let elementToRemove = offsets.map { requestLog.requests.sorted().reversed()[$0]}.first
guard let elementToRemove = elementToRemove else { return }
requestLog.removeRequest(elementToRemove)
}
}
You can create a tiny delay in clearLog().
Something like this should work
Button("delete all", action: {
for (n, obj) in requests.enumerated(){
DispatchQueue.main.asyncAfter(deadline: .now() + (Double(n) * 0.1) ){
viewContext.delete(obj)
}
}
})
FYI for CoreData questions it is easiest to produce a Minimal Reproducible Example with the standard project. Everybody can create a store easily to help you out we don't need to know it all.
as per the documentation, it should be pretty straightforward. example for a List: https://developer.apple.com/documentation/swiftui/list/ondrop(of:istargeted:perform:)-75hvy#
the UTType should be the parameter restricting what a SwiftUI object can receive. in my case i want to accept only Apps. the UTType is .applicationBundle: https://developer.apple.com/documentation/uniformtypeidentifiers/uttype/3551459-applicationbundle
but it doesn't work. the SwiftUI object never changes status and never accepts the drop. the closure is never run. whether on Lists, H/VStacks, Buttons, whatever. the pdf type don't seem to work either, as well as many others. the only type that i'm able to use if fileURL, which is mainly like no restriction.
i'm not sure if i'm doing something wrong or if SwiftUI is half working for the mac.
here's the code:
List(appsToIgnore, id: \.self, selection: $selection) {
Text($0)
}
.onDrop(of: [.applicationBundle, .application], isTargeted: isTargeted) { providers in
print("hehe")
return true
}
replacing or just adding .fileURL in the UTType array makes the drop work but without any type restriction.
i've also tried to use .onInsert on a ForEach instead (https://developer.apple.com/documentation/swiftui/foreach/oninsert(of:perform:)-2whxl#), and to go through a proper DropDelegate (https://developer.apple.com/documentation/swiftui/dropdelegate#) but keep getting the same results. it would seem the SwiftUI drop for macOS is not yet working, but i can't find any official information about this. in the docs it is written macOS 11.0+ so i would expect it to work?
any info appreciated! thanks.
You need to validate manually, using DropDelegate of what kind of file is dragged over.
Here is a simplified demo of possible approach. Tested with Xcode 13 / macOS 11.6
let delegate = MyDelegate()
...
List(appsToIgnore, id: \.self, selection: $selection) {
Text($0)
}
.onDrop(of: [.fileURL], delegate: delegate) // << accept file URLs
and verification part like
class MyDelegate: DropDelegate {
func validateDrop(info: DropInfo) -> Bool {
// find provider with file URL
guard info.hasItemsConforming(to: [.fileURL]) else { return false }
guard let provider = info.itemProviders(for: [.fileURL]).first else { return false }
var result = false
if provider.canLoadObject(ofClass: String.self) {
let group = DispatchGroup()
group.enter() // << make decoding sync
// decode URL from item provider
_ = provider.loadObject(ofClass: String.self) { value, _ in
defer { group.leave() }
guard let fileURL = value, let url = URL(string: fileURL) else { return }
// verify type of content by URL
let flag = try? url.resourceValues(forKeys: [.contentTypeKey]).contentType == .applicationBundle
result = flag ?? false
}
// wait a bit for verification result
_ = group.wait(timeout: .now() + 0.5)
}
return result
}
func performDrop(info: DropInfo) -> Bool {
// handling code is here
return true
}
}
I'm trying to update a progress bar with the progress of loading a load of values into CoreData. However, whenever I try to call an update on my progressView component, I get a fatal error stating that "unexpectedly found nil while unwrapping an Optional value".
The interesting thing is that this happens even if I put 'self.progressView.progress = 0.5' in the delegate method of my program - indicating that it's the progressView component it can't find rather than an issue with the value. A quick check with println also confirms the value does exist and so isn't nil. Note that if I put the 'self.progressView.progress = 0.5' statement under a function connected directly to a button, it works fine so it must be some sort of issue with the command being called from the delegate.
Can anyone work out what I'm doing wrong here? Thanks for your help.
Delegate method:
class ViewControllerUpdate: UIViewController, NSURLSessionDelegate, NSURLSessionDownloadDelegate, saveUpdate {
[....]
func updateStatus(status: String, progress: Float?) {
if let percentProgress = progress? {
self.progressView.progress = 0.5
}
//println(progress) - NOTE THIS IS CORRECTLY POPULATED WITH THE APPROPRIATE VALUE
}
Calling class:
protocol saveUpdate {
func updateStatus(status:String, progress:Float?)
}
class sqlPullSave {
let classtoUpdate: saveUpdate = ViewControllerUpdate()
func saveTSVtoSQL(fromFile: NSURL) -> Int {
//Load up the information into a Dictionary (tsv)
//let tsvURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(fromFileName, ofType: fromFileExtension)!)
let tsvURL: NSURL = fromFile
let tab = NSCharacterSet(charactersInString: "\t")
let tsv = CSV(contentsOfURL: tsvURL, separator: tab)
//let defResult: AnyObject = tsv.rows[0]["Name"]!
//let tryagain:String = AnyObjecttoString(tsv.rows[1]["Name"]!)
//load the data into the SQLite database...
dispatch_async(dispatch_get_main_queue()) {
for a in 0..<tsv.rows.count {
self.SQLsaveLine(self.AnyObjecttoString(tsv.rows[a]["Name"]!),
name_l: "",
desc: self.AnyObjecttoString(tsv.rows[a]["1"]!),
jobTitle: self.AnyObjecttoString(tsv.rows[a]["2"]!),
extn: self.AnyObjecttoString(tsv.rows[a]["3"]!)
// update status
var percentComplete: Float = (Float(a) / Float(tsv.rows.count))
self.classtoUpdate.self.updateStatus("SQLload", progress: percentComplete)
}
}
return 0
}