UIScreen.capturedDidChangeNotification not getting called in SwiftUI - ios

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.)

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?

Move to other Picker View tabs only after the user taps on a button in an alert view - SwiftUI

What I would like to be able to do is that when the user taps on a color tab, first present an alert view asking before actually moving to the other tab and if the user decides to Cancel it will not move to the other tab but if the user taps the OK button, the Picker will actually move to the selected tab.
How can I move to a different color tab only after the user taps on the YES button in the alert view?
struct ContentView: View {
#State private var favoriteColor = 0
#State private var showAlert = false
var body: some View {
VStack {
Picker("What is your favorite color?", selection: $favoriteColor) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.pickerStyle(.segmented)
.onChange(of: favoriteColor) { tag in
showAlert = true
}
.alert("By changing color also modifieds other things...change color?", isPresented: $showAlert) {
Button("YES") {
// move to selected color tab
}
Button("Cancel") {
// do nothing
}
}
}
}
}
SwiftUI is reactive world, means our code receives control post-factum... use UIKit representable instead for that...
... Or, here is a possible workaround with proxy binding (however not sure if behavior persists in all OS versions)
Tested with Xcode 13.4 / iOS 15.5
Main part:
var selected: Binding<Int> { // << proxy !!
Binding<Int>(
get: { favoriteColor },
set: {
toConfirm = $0 // store for verification !!
showAlert = true // show alert !!
}
)
}
var body: some View {
VStack {
// << bind to proxy binding here !!
Picker("What is your favorite color?", selection: selected) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.pickerStyle(.segmented)
.alert("By changing color also modifieds other things...change color?", isPresented: $showAlert, presenting: toConfirm) { color in // << inject for verification !!
Button("YES") {
favoriteColor = color // << do verification here !!
}
Button("Cancel") {
// do nothing
}
}
}
Test code on GitHub

SwiftUI Schedule Local Notification Without Button?

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.

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])
}
}

Why is it in SwiftUI, when presenting modal view with .sheet, init() is called twice

I am producing the situation on WatchOS with the following code
struct Modal : View {
#Binding var showingModal : Bool
init(showingModal : Binding<Bool>){
self._showingModal = showingModal
print("init modal")
}
var body: some View {
Button(action: {
self.showingModal.toggle()
}, label: {
Text("TTTT")
})
}
}
struct ContentView: View {
#State var showingModal = false
var body: some View {
Button(action: {
self.showingModal.toggle()
}, label: {
Text("AAAA")
}).sheet(isPresented: $showingModal, content: {Modal(showingModal: self.$showingModal)})
}
}
Every time I press the button in the master view to summon the modal with .sheet, Two instances of the modal view are created.
Could someone explain this phenomenon?
I tracked this down in my code to having the following line in my View:
#Environment(\.presentationMode) var presentation
I had been doing it due to https://stackoverflow.com/a/61311279/155186, but for some reason that problem seems to have disappeared for me so I guess I no longer need it.
I've filed Feedback FB7723767 with Apple about this.
It is probably a bug, as of Xcode 11.4.1 (11E503a).
Beware, that if for example initializing view models (or anything else for that matter) like so:
.sheet(isPresented: $isEditingModalPresented) {
LEditView(vm: LEditViewModel(), isPresented: self.$isEditingModalPresented)
}
the VM will be initialized twice.
Comment out/remove the init() method from Modal with everything else the same. You should be able to solve the issue of two instances of Modal being created, its because you are explicitly initializing the binding (showingModal) in the init() of Modal. Hope this makes sense.
private let uniqueId: String = "uniqueId"
Button(action: {
self.showingModal.toggle()
}, label: {
Text("AAAA")
})
.sheet(isPresented: $showingModal) {
Modal(showingModal: self.$showingModal)
.id("some-unique-id")
}
Ex:
.id(self.uniqueId)
Add unique id to your .sheet and not't worry :)
But, do not use UUID(), because sheet view will be represented on every touch event

Resources