Update state of a screen struct from SceneDelegate - ios

I'm coming from react-native and a beginner at Swift and SwiftUI and I was curious how to perform an action and update state on a specific screen when the app comes back into the foreground. I want to check the status of the notifications("allowed, "denied" etc.) and update the UI.
This is some example code - Here is the view I want to update:
struct Test: View {
#State var isNotificationsEnabled : Bool
var body : some View {
Toggle(isOn: self.isNotificationsEnabled) {
Text("Notifications")
}
}
}
So far what I've been reading is that you need to do edit the func sceneWillEnterForeground(_ scene: UIScene) inside SceneDelegate.swift but how on earth do I update the state of my Test struct from there? I'm thinking we need some kind of global state but that's just a guess.
Any advice?

Here is simplest approach
struct Test: View {
#State private var isNotificationsEnabled : Bool
private let foregroundPublisher = NotificationCenter.default.publisher(for: UIScene.willEnterForegroundNotification)
var body : some View {
Toggle(isOn: self.$isNotificationsEnabled) {
Text("Notifications")
}
.onReceive(foregroundPublisher) { notification in
// do anything needed here
}
}
}
Of course, if your application can have multiple scenes and you need to differentiate them somehow then more complicated variant of this approach will be needed to differentiate which scene generates this notification.

Related

Using delegates in Swift

I know that this may be tagged as a repeat question, but the ones that exist have not helped me implement delegates in Swift with SwiftUI. My issue can be see in the code below. I have a View and VM, TodaysVM. In here, I do a network request, that downloads movie data from an API, and saves the data to CoreData. In order to show graphs, the HabitVM needs to fetch the data from CoreData, and then do some long processing on it.
For this, I was looking into a way for one class to send a message notification to another class. So I stumbled onto using delegates (not sure if this is correct?). But essentially the idea would be that the TodayVM refreshes its data (which the user can do a pull to refresh on this page), it also sends a message to HabitsVM to start processing (so the data can be done by the time the user navigates to this page. I am not sure if there is a better way to do this, and I was trying to stay away from a singleton approach, passing one class into class, or creating a new instance of one of the classes.
In the code below, I have the delegates set up but 'print("Data update required!")' is never executed. From my research I understand this to be because I never set a value to the delegate? So the delegate value is nil, and therefore the
delegate?.requireUpdate(status: true)
Is never actually executed. Which makes sense, since delegate is an optional, but never assigned a value. So I was curious how I would assign it a value in SwiftUI? And if using delegates is even the best method to have to separate classes send updateRequired status updates.
import SwiftUI
import Foundation
// protocol
protocol MovieNotifierDelagate: AnyObject {
func requireUpdate(status: Bool)
}
struct HabitView {
#StateObject var habitVM = HabitsVM()
var body: some View {
Text("Hello")
}
}
struct TodaysView {
#StateObject var todayVM = TodayVM()
var body: some View {
Text("Hi!")
}
}
class HabitsVM: ObservableObject, MovieNotifierDelagate {
init() {
}
func requireUpdate(status: Bool) {
print("Data update required!")
reprocessData()
}
func reprocessData() {
print("Processing")
}
}
class TodayVM: ObservableObject {
weak var delegate: MovieNotifierDelagate?
init() {
//Does some network call and downloads data, and saves to CoreData
sendUpdate(status: true)
}
func sendUpdate(status: Bool) {
guard status else {
print ("No update")
return
}
delegate?.requireUpdate(status: true)
}
}
// Controlling Page
struct HomeView: View {
//Default the user to the today view
#State private var selection = 1
var body: some View {
TabView(selection: $selection) {
HabitView()
.tag(0)
.tabItem {
VStack {
Image(systemName: "books.vertical")
Text("Habits")
}
}
TodaysView()
.tag(1)
.tabItem {
VStack {
Image(systemName: "backward.end.alt")
Text("Rewind")
}
}
}
}
}

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?

How do you edit an ObservableObject’s properties in SwiftUI from another class?

I’m looking for the proper pattern and syntax to address my goal of having an ObservableObject instance that I can share amongst multiple views, but while keeping logic associated with it contained to another class. I’m looking to do this to allow different ‘controller’ classes to manipulate the properties of the state without the view needing to know which controller is acting on it (injected).
Here is a simplification that illustrates the issue:
import SwiftUI
class State: ObservableObject {
#Published var text = "foo"
}
class Controller {
var state : State
init(_ state: State) {
self.state = state
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var state = State()
var controller: Controller!
init() {
controller = Controller(state)
}
var body: some View {
VStack {
Text(controller.state.text) // always shows 'foo'
Button("Press Me") {
print(controller.state.text) // prints 'foo'
controller.changeState()
print(controller.state.text) // prints 'bar'
}
}
}
}
I know that I can use my ObservableObject directly and manipulate its properties such that the UI is updated in response, but in my case, doing so prevents me from having different ‘controller’ instances depending on what needs to happen. Please advise with the best way to accomplish this type of scenario in SwiftUI
To make SwiftUI view follow state updates, your controller needs to be ObservableObject.
SwiftUI view will update when objectWillChange is triggered - it's done automatically for properties annotated with Published, but you can trigger it manually too.
Using same publisher of your state, you can sync two observable objects, for example like this:
class Controller: ObservableObject {
let state: State
private var cancellable: AnyCancellable?
init(_ state: State) {
self.state = state
cancellable = state.objectWillChange.sink {
self.objectWillChange.send()
}
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var controller = Controller(State())

How to know if app is removed from background in SwiftUI?

How to know if app is removed from background in SwiftUI? Is there any method just like Swift and how to use it?
I'd research the Scene protocol.
I quote...
The Scene protocol provides scene modifiers, defined as protocol
methods with default implementations, that you use to configure a
scene. For example, you can use the onChange(of:perform:) modifier to
trigger an action when a value changes. The following code empties a
cache when all of the scenes in the window group have moved to the
background:
and to be clear this is the example provided by Apple...
struct MyScene: Scene {
#Environment(\.scenePhase) private var scenePhase
#StateObject private var cache = DataCache()
var body: some Scene {
WindowGroup {
MyRootView()
}
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .background {
cache.empty()
}
}
}
}
I place this in my App file, like in the following example (with Core Data implementation):
import SwiftUI
#main
struct MyApp: App {
let persistenceController = PersistenceController.shared
#Environment(\.scenePhase) var scenePhase
var body: some Scene {
WindowGroup {
NavigationView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
.onChange(of: scenePhase) { _ in
persistenceController.save()
}
}
}

How do I show/hide a view with SwiftUI based on PHPhotoLibraryAuthorizationStatus?

I'm trying to get a view to show in SwiftUI if the user has not given permission for access to the PhotoLibrary (Giving them instructions to allow access to the Photo Library, either with a prompt or to go into Settings and fix the status). Now I'm assuming that PHPhotoLibrary.authorizationStatus is my source of truth, but maybe I haven't fully understood the concept
var canAccessPhotos : Bool {
return PHPhotoLibrary.authorizationStatus() == .authorized
}
...
.sheet(isPresented: canAccessPhotos) { //Error: Cannot convert value of type 'Bool' to expected argument type 'Binding<Bool>'
AccessPhotoLibraryView() //New View I want to show the user with instructions or prompt
}
Now that doesn't work because canAccessPhotos is not bindable, but I also can't put a #Binding on a computed property and there's currently no mechanism for the variable to update when authorizationStatus changes, so I'm stuck on the next bit on how to solve it.
How do I get the property from PHPhotoLibrary to automatically show the view if/when it changes. Or is there a different way to do it and I've gone in the wrong direction?
Thank you in advanced 🙂
Ok, you'd better use a coordinator (or a view model) for this problem. Have a look at the code below and let me know if it works for you.
class Coordinator: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
#Published var isPhotoLibraryAuthorized = false
override init() {
super.init()
PHPhotoLibrary.shared().register(self)
updateStatus()
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
updateStatus()
}
private func updateStatus() {
DispatchQueue.main.async {
self.isPhotoLibraryAuthorized = PHPhotoLibrary.authorizationStatus() == .authorized
}
}
}
struct ContentView: View {
#EnvironmentObject var coordinator: Coordinator
var body: some View {
Group {
if coordinator.isPhotoLibraryAuthorized {
// some view
} else {
// another view
}
}
}
}

Resources