SwiftUI Schedule Local Notification Without Button? - ios

This may have a very simple answer, as I am pretty new to Swift and SwiftUI and am just starting to learn. I'm trying to schedule local notifications that will repeat daily at a specific time, but only do it if a toggle is selected. So if a variable is true, I want that notification to be scheduled. I looked at some tutorials online such as this one, but they all show this using a button. Instead of a button I want to use a toggle. Is there a certain place within the script that this must be done? What do I need to do differently in order to use a toggle instead of a button?

You can observe when the toggle is turned on and turned off -- In iOS 14 you can use the .onChange modifier to do this:
import SwiftUI
struct ContentView: View {
#State var isOn: Bool = false
var body: some View {
Toggle(isOn: $isOn, label: {
Text("Notifications?")
})
/// right here!
.onChange(of: isOn, perform: { toggleIsOn in
if toggleIsOn {
print("schedule notification")
} else {
print("don't schedule notification")
}
})
}
}
For earlier versions, you can try using onReceive with Combine:
import SwiftUI
import Combine
struct ContentView: View {
#State var isOn: Bool = false
var body: some View {
Toggle(isOn: $isOn, label: {
Text("Notifications?")
})
/// a bit more complicated, but it works
.onReceive(Just(isOn)) { toggleIsOn in
if toggleIsOn {
print("schedule notification")
} else {
print("don't schedule notification")
}
}
}
}
You can find even more creative solutions to observe the toggle change here.

Related

iOS 16 Binding issue (in this case Toggle)

What i have stumped across is that when i pass a Binding<> as a parameter in a ViewModel as you will see below, it doesn't always work as intended. It works in iOS 15 but not in iOS 16. This reduces the capability to create subviews to handle this without sending the entire Holder object as a #Binding down to it's subview, which i would say would be bad practice.
So i have tried to minimize this as much as possible and here is my leasy amount of code to reproduce it:
import SwiftUI
enum Loadable<T> {
case loaded(T)
case notRequested
}
struct SuperBinder {
let loadable: Binding<Loadable<ContentView.ViewModel>>
let toggler: Binding<Bool>
}
struct Holder {
var loadable: Loadable<ContentView.ViewModel> = .notRequested
var toggler: Bool = false
func generateContent(superBinder: SuperBinder) {
superBinder.loadable.wrappedValue = .loaded(.init(toggleBinder: superBinder.toggler))
}
}
struct ContentView: View {
#State var holder = Holder()
var superBinder: SuperBinder {
.init(loadable: $holder.loadable, toggler: $holder.toggler)
}
var body: some View {
switch holder.loadable {
case .notRequested:
Text("")
.onAppear(perform: {
holder.generateContent(superBinder: superBinder)
})
case .loaded(let viewModel):
Toggle("testA", isOn: viewModel.toggleBinder) // Works but doesn't update UI
Toggle("testB", isOn: $holder.toggler) // Works completly
// Pressing TestA will even toggle TestB
// TestB can be turned of directly but has to be pressed twice to turn on. Or something?
}
}
struct ViewModel {
let toggleBinder: Binding<Bool>
}
}
anyone else stumbled across the same problem? Or do i not understand how binders work?

UIScreen.capturedDidChangeNotification not getting called in SwiftUI

I'm trying to detect when a user enters/exits airplay mode in SwiftUI.
#State var isAirplaying = false
...
var body: some View {
ZStack {
SomeVideoView()
if isAirplaying {
Text("Connected to airplay")
}
}
.edgesIgnoringSafeArea(.all)
.onReceive(NotificationCenter.default.publisher(for: UIScreen.capturedDidChangeNotification)) { _ in
isAirplaying.toggle()
}
}
However, when entering/exiting Airplay, this capturedDidChangeNotification never gets called. What am I missing? Is there a better way to do this?
(I tried modeDidChangeNotification as well.)

SwiftUI isPresented Environment not updated for sheet dismissed

Given the following minimal reproducible example:
struct ParentView: View {
#State private var showSheet = false
var body: some View {
return Button("Show sheet") {
showSheet.toggle()
}.sheet(
isPresented: $showSheet,
onDismiss: {
print("Parent onDismiss")
},
content: {
NavigationView {
SheetView(parentShowSheet: $showSheet)
}
}
)
}
}
struct SheetView: View {
#Environment(\.dismiss) private var dismiss
#Environment(\.isPresented) private var isPresented
#Binding var parentShowSheet: Bool
var body: some View {
Button("Manual close") {
dismiss()
}
.onChange(of: isPresented) { isPresented in
print("Sheet isPresented", isPresented)
}
.onChange(of: parentShowSheet) { parentShowSheet in
print("Sheet parentShowSheet", parentShowSheet)
}
}
}
The SheetView's onChange methods are not triggering in the way I would expect. In this example there are two ways to close a sheet once it's opened :
Click the "Manual close" button which triggers dismiss(), or
Pull down the sheet to trigger an "interactive close" (the thing that is disabled when .interactiveDismissDisabled(true) is added to the view).
If you try these both out you'll see that for (1) you'll get two print statements "Sheet parentShowSheet" and "Parent onDismiss", while for (2) you'll just get one print statement "Parent onDimiss".
Three questions:
Why does "Sheet parentShowSheet" not print in case (2)?
Why does "Sheet isPresented" not print in either case? I'd think that dismissing a sheet is very literally changing it's Environment(\.isPresented) value.
How can I add a hook inside of the SheetView that triggers when it is dismissed with either method (1) or method (2) (e.g. something that operates like either of my onChange methods, but actually works in both cases)?

Receive notifications for focus changes between apps on Split View when using SwiftUI

What should I observe to receive notifications in a View of focus changes on an app, or scene, displayed in an iPaOS Split View?
I'm trying to update some data, for the View, as described here, when the user gives focus back to the app.
Thanks.
Here is a solution that updates pasteDisabled whenever a UIPasteboard.changedNotification is received or a scenePhase is changed:
struct ContentView: View {
#Environment(\.scenePhase) private var scenePhase
#State private var pasteDisabled = false
var body: some View {
Text("Some Text")
.contextMenu {
Button(action: {}) {
Text("Paste")
Image(systemName: "doc.on.clipboard")
}
.disabled(pasteDisabled)
}
.onReceive(NotificationCenter.default.publisher(for: UIPasteboard.changedNotification)) { _ in
updatePasteDisabled()
}
.onChange(of: scenePhase) { _ in
updatePasteDisabled()
}
}
func updatePasteDisabled() {
pasteDisabled = !UIPasteboard.general.contains(pasteboardTypes: [aPAsteBoardType])
}
}

How to be notified when a TextEditor loses focus?

In my SwiftUI app, I would like to be notified when the TextEditor loses focus/has finished editing. Ideally, something like TextField's onCommit callback would be perfect.
Using the new onChange as below does work for receiving every new character that the user types, but I really want to know when they are finished.
#State var notes = ""
...
TextEditor(text: $notes)
.onChange(of: notes, perform: { newString in
print(newString)
})
I figured it out, you can use the UIResponder.keyboardWillHideNotification.
TextEditor(text: $notes)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
print("done editing!")
}
On iOS 15.0, there is now #FocusState.
struct MyView: View {
#State var text: String
#FocusState private var isFocused: Bool
var body: some View {
Form {
TextEditor(text: $text)
.focused($isFocused)
.onChange(of: isFocused) { isFocused in
// do stuff based off focus state change
}
}
}
}

Resources