SwiftUI and UICloudSharingController hate each other - ios

I have a project using SwiftUI that requires CloudKit sharing, but I'm unable to get the UICloudSharingController to play nice in a SwiftUI environment.
First Problem
A straight-forward wrap of UICloudSharingController using UIViewControllerRepresentable yields an endless spinner (see this). As has been done for other system controllers like UIActivityViewController, I wrapped the UICloudSharingController in a containing UIViewController like this:
struct CloudSharingController: UIViewControllerRepresentable {
#EnvironmentObject var store: CloudStore
#Binding var isShowing: Bool
func makeUIViewController(context: Context) -> CloudControllerHost {
let host = CloudControllerHost()
host.rootRecord = store.noteRecord
host.container = store.container
return host
}
func updateUIViewController(_ host: CloudControllerHost, context: Context) {
if isShowing, host.isPresented == false {
host.share()
}
}
}
final class CloudControllerHost: UIViewController {
var rootRecord: CKRecord? = nil
var container: CKContainer = .default()
var isPresented = false
func share() {
let sharingController = shareController
isPresented = true
present(sharingController, animated: true, completion: nil)
}
lazy var shareController: UICloudSharingController = {
let controller = UICloudSharingController { [weak self] controller, completion in
guard let self = self else { return completion(nil, nil, CloudError.controllerInvalidated) }
guard let record = self.rootRecord else { return completion(nil, nil, CloudError.missingNoteRecord) }
let share = CKShare(rootRecord: record)
let operation = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: [])
operation.modifyRecordsCompletionBlock = { saved, _, error in
if let error = error {
return completion(nil, nil, error)
}
completion(share, self.container, nil)
}
self.container.privateCloudDatabase.add(operation)
}
controller.delegate = self
controller.popoverPresentationController?.sourceView = self.view
return controller
}()
}
This allows the controller to come up normally, but...
Second Problem
Tap the close button or swipe to dismiss and the controller will disappear, but there's no notification that it's been dismissed. The SwiftUI view's #State property that initiated presenting the controller is still true. There's no obvious method to detect dismissal of the modal. After some experimenting, I discovered the presenting controller is the original UIHostingController created in the SceneDelegate. With some hackery, you can inject an object that is referenced in a UIHostingController subclass into the CloudSharingController. This will let you detect the dismissal and set the #State property to false. However, all nav bar buttons no longer function after dismissing so you could only ever tap this thing once. The rest of the scene is completely functional, but buttons in the nav bar don't respond.
Third Problem
Even if you could get the UICloudSharingController to present and dismiss normally, tapping on any of the sharing methods (Messages, Mail, etc) makes the controller disappear with no animation and the controller for the sharing URL doesn't come up. No crash or console messages--it just disappears.
Demo
I made a quick and dirty project on GitHub to demonstrate the issue: CloudKitSharing. It just creates a single String and a CKRecord to represent it using CloudKit. The interface displays the String (a UUID) with a single nav bar button to share it:
The Plea
Is there any way to use UICloudSharingController in SwiftUI? Don't have the time to rebuild the project in UIKit or a custom sharing controller (I know--the price of being on the bleeding edge 💩)

I got this working -- initially, I wrapped the UICloudSharingController in a UIViewControllerRepresentable, much like the link you provided (I referenced that while building it), and simply adding it to a SwiftUI .sheet() view. This worked on the iPhone, but it failed on the iPad, because it requires you to set the popoverPresentationController?.sourceView, and I didn't have one, given that I triggered the sheet with a SwiftUI Button.
Going back to the drawing board, I rebuilt the button itself as a UIViewRepresentable, and was able to present the view using the rootViewController trick that SeungUn Ham suggested here. All works, on both iPhone and iPad - at least in the simulator.
My button:
struct UIKitCloudKitSharingButton: UIViewRepresentable {
typealias UIViewType = UIButton
#ObservedObject
var toShare: ObjectToShare
#State
var share: CKShare?
func makeUIView(context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) -> UIButton {
let button = UIButton()
button.setImage(UIImage(systemName: "person.crop.circle.badge.plus"), for: .normal)
button.addTarget(context.coordinator, action: #selector(context.coordinator.pressed(_:)), for: .touchUpInside)
context.coordinator.button = button
return button
}
func updateUIView(_ uiView: UIButton, context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UICloudSharingControllerDelegate {
var button: UIButton?
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
//Handle some errors here.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return parent.toShare.name
}
var parent: UIKitCloudKitSharingButton
init(_ parent: UIKitCloudKitSharingButton) {
self.parent = parent
}
#objc func pressed(_ sender: UIButton) {
//Pre-Create the CKShare record here, and assign to parent.share...
let sharingController = UICloudSharingController(share: share, container: myContainer)
sharingController.delegate = self
sharingController.availablePermissions = [.allowReadWrite]
if let button = self.button {
sharingController.popoverPresentationController?.sourceView = button
}
UIApplication.shared.windows.first?.rootViewController?.present(sharingController, animated: true)
}
}
}

Maybe just use rootViewController.
let window = UIApplication.shared.windows.filter { type(of: $0) == UIWindow.self }.first
window?.rootViewController?.present(sharingController, animated: true)

Related

ObservedObject is still in memory after the view is dismissed, Memory Leak?

I'm making an app with SwiftUI and UIkit, I use UIkit for the main app controller and navigation, and I use SwiftUI for app design.
The app works very well, but I'm worried about the memory leaks. This is because the ViewModels I use to pass data between views don't call desinit whene the view disappears. I know that in SwiftUI views are not disposed immediately, but since I'm using UIKit to navigate I don't know what the problem is.
//The ViewModel for each user fetched
internal class UserViewModel: ObservableObject, Identifiable {
//MARK: - Propeties var currentListener: ListenerRegistration?
#Published var request: Request?
#Published var user: User
init(user: User) {
self.user = user
getRequest()
fetchAdmins()
}
deinit {
//Dosnt get called removeListener()
}
func getRequest() {
guard let uid = Auth.auth().currentUser?.uid else {return}
guard let id = id else {return}
self.currentListener = Collections.requests(id).document(uid).addSnapshotListener { snapshot, error in
if let error = error {
print(error.localizedDescription)
return
}
if ((snapshot?.exists) != nil) {
if let request = try? snapshot!.data(as: Request.self) {
DispatchQueue.main.async {
self.request = request
}
}
}
}
}
func removeListener() {
self.currentListener?.remove()
}
}
}
//The ViewModel to fetch all the users ViewModels
class UsersViewModel: ObservableObject {
#Published var users = [UserViewModel]()
func fetch() {
DispatchQueue.global(qos: .background).async {
Collections.users.getDocuments(completion: { snapshot, err in
guard let documents = snapshot?.documents else { return } let users = documents.compactMap({ try? $0.data(as: User.self) })
users.forEach { user in
let vm = UserViewModel(user: user)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.users.append(vm)
}
}
})
}
} }
//Show the users cells with the ViewModel
struct HomeView: View {
#ObservedObject var usersViewModels: UsersViewModel
//MARK: - Init
init() {
self.usersViewModels = UsersViewModel()
}
var body: some View {
ListView(content: {
ForEach(usersViewModels) { usersViewModel in
UserCell(viewModel: usersViewModel).id(user.id)
}
})
}
}
This is how I navigate between controllers and views of my app. I don't use NavigationLinks:
public static func push<Content: View>(view: Content) {
DispatchQueue.main.async {
guard let tabBarController = UIApplication.rootViewController as? UITabBarController, let navigationController = tabBarController.selectedViewController as? UINavigationController else { return nil }
if let navigationController = UIApplication.getCurrentNavigationController() {
navigationController.pushViewController(HostingController(content: view), animated: true)
}
}
}
Does anyone know if this method that I am using to navigate can cause me memory problems? And you know why my app doesn't reduce its memory every time I close a window, it just increases more and more.
The disappearing does not mean it is no longer in memory.
It looks like you keep pushing them onto the navigation stack which increases their retain count.
You've got a memory leak here:
struct HomeView: View {
#ObservedObject var usersViewModels: UsersViewModel
//MARK: - Init
init() {
self.usersViewModels = UsersViewModel() // here
}
View structs must not init objects because the View struct is recreated every state change thus the object is being constantly init.
SwiftUI is all about taking advantage of value semantics, try to use #State with value types (or group them in a struct) in the View struct for your view data.
Model data structs go in a singleton ObservableObject supplied to the Views using .environmentObject.

PHPickerViewController tapping on Search gets error... "Unable to load photos"

I'm trying to implement a PHPickerViewController using SwiftUI and The Composable Architecture. (Not that I think that's particularly relevant but it might explain why some of my code is like it is).
Sample project
I've been playing around with this to try and work it out. I created a little sample Project on GitHub which removes The Composable Architecture and keeps the UI super simple.
https://github.com/oliverfoggin/BrokenImagePickers/tree/main
It looks like iOS 15 is breaking on both the UIImagePickerViewController and the PHPickerViewController. (Which makes sense as they both use the same UI under the hood).
I guess the nest step is to determine if the same error occurs when using them in a UIKit app.
My code
My code is fairly straight forward. It's pretty much just a reimplementation of the same feature that uses UIImagePickerViewController but I wanted to try with the newer APIs.
My code looks like this...
public struct ImagePicker: UIViewControllerRepresentable {
// Vars and setup stuff...
#Environment(\.presentationMode) var presentationMode
let viewStore: ViewStore<ImagePickerState, ImagePickerAction>
public init(store: Store<ImagePickerState, ImagePickerAction>) {
self.viewStore = ViewStore(store)
}
// UIViewControllerRepresentable required functions
public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> some UIViewController {
// Configuring the PHPickerViewController
var config = PHPickerConfiguration()
config.filter = PHPickerFilter.images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// This is the coordinator that acts as the delegate
public class Coordinator: PHPickerViewControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let itemProvider = results.first?.itemProvider,
itemProvider.canLoadObject(ofClass: UIImage.self) else {
return
}
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self?.parent.viewStore.send(.imagePicked(image: image))
}
}
}
}
}
}
All this works in the simple case
I can present the ImagePicker view and select a photo and it's all fine. I can cancel out of it ok. I can even scroll down the huge collection view of images that I have. I can even see the new image appear in my state object and display it within my app. (Note... this is still WIP and so the code is a bit clunky but that's only to get it working initially).
The problem case
The problem is that when I tap on the search bar in the PHPickerView (which is a search bar provided by Apple in the control, I didn't create it or code it). It seems to start to slide up the keyboard and then the view goes blank with a single message in the middle...
Unable to Load Photos
[Try Again]
I also get a strange looking error log. (I removed the time stamps to shorten the lines).
// These happen on immediately presenting the ImagePicker
AppName[587:30596] [Picker] Showing picker unavailable UI (reason: still loading) with error: (null)
AppName[587:30596] Writing analzed variants.
// These happen when tapping the search bar
AppName[587:30867] [lifecycle] [u A95D90FC-C77B-43CC-8FC6-C8E7C81DD22A:m (null)] [com.apple.mobileslideshow.photospicker(1.0)] Connection to plugin interrupted while in use.
AppName[587:31002] [lifecycle] [u A95D90FC-C77B-43CC-8FC6-C8E7C81DD22A:m (null)] [com.apple.mobileslideshow.photospicker(1.0)] Connection to plugin invalidated while in use.
AppName[587:30596] [Picker] Showing picker unavailable UI (reason: crashed) with error: (null)
AppName[587:30596] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}
Tapping the "Try Again" button reloads the initial scroll screen and I can carry on using it. But tapping the search bar again just shows the same error.
I'm usually the first one to point out that the error is almost definitely not with the Apple APIs but I'm stumped on this one. I'm not sure what it is that I'm doing that is causing this to happen?
Is it the fact that it's in a SwiftUI view?
Recreated the project in UIKit
I remade the same project using UIKit... https://github.com/oliverfoggin/UIKit-Image-Pickers
And I couldn't replicate the crash at all.
Also... if you are taking any sort of screen recording of the device the crash will not happen. I tried taking a recording on the device itself and couldn't replicate it. I also tried doing a movie recording from my Mac using the iPhone screen and couldn't replicate the crash. But... the instant I stopped the recording on QuickTime the crash was replicable again.
This fixed it for me .ignoreSafeArea(.keyboard) like #Frustrated_Student mentions.
To elaborate on #Frustrated_Student this issue has to do with the UIViewControllerRepresentable treating the view like many SwiftUI views to automatically avoid the keyboard. If you are presenting the picker using a sheet as I am then you can simply add the .ignoreSafeArea(.keyboard) to the UIViewControllerRepresentable view in my case I called it ImagePicker here is a better example.
Where to add it the .ignoreSafeArea(.keyboard)
.sheet(isPresented: $imagePicker) {
ImagePicker(store: store)
.ignoresSafeArea(.keyboard)
}
This is #Fogmeister code:
public struct ImagePicker: UIViewControllerRepresentable {
// Vars and setup stuff...
#Environment(\.presentationMode) var presentationMode
let viewStore: ViewStore<ImagePickerState, ImagePickerAction>
public init(store: Store<ImagePickerState, ImagePickerAction>) {
self.viewStore = ViewStore(store)
}
// UIViewControllerRepresentable required functions
public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> some UIViewController {
// Configuring the PHPickerViewController
var config = PHPickerConfiguration()
config.filter = PHPickerFilter.images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// This is the coordinator that acts as the delegate
public class Coordinator: PHPickerViewControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let itemProvider = results.first?.itemProvider,
itemProvider.canLoadObject(ofClass: UIImage.self) else {
return
}
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self?.parent.viewStore.send(.imagePicked(image: image))
}
}
}
}
}
}
Well.. this seems to be an iOS bug.
I have cerated a sample project here that shows the bug... https://github.com/oliverfoggin/BrokenImagePickers
And a replica project here written with UIKit that does not... https://github.com/oliverfoggin/UIKit-Image-Pickers
I tried to take a screen recording of this happening but it appears that if any screen recording is happening (whether on device or via QuickTime on the Mac) this suppresses the bug from happening.
I have filed a radar with Apple and sent them both projects to have a look at and LOTS of detail around what's happening. I'll keep this updated with any progress on that.
Hacky workaround
After a bit of further investigation I found that you can start with SwiftUI and then present a PHPickerViewController without this crash happening.
From SwiftUI if you present a UIViewControllerRepresentable... and then from there if you present the PHPickerViewController it will not crash.
So I came up with a (very tacky) workaround that avoids this crash.
I first create a UIViewController subclass that I use like a wrapper.
class WrappedPhotoPicker: UIViewController {
var picker: PHPickerViewController?
override func viewDidLoad() {
super.viewDidLoad()
if let picker = picker {
present(picker, animated: false)
}
}
}
Then in the SwiftUI View I create this wrapper and set the picker in it.
struct WrappedPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var photoPickerResult: PHPickerResult?
let wrappedPicker = WrappedPhotoPicker()
func makeUIViewController(context: Context) -> WrappedPhotoPicker {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
wrappedPicker.picker = picker
return wrappedPicker
}
func updateUIViewController(_ uiViewController: WrappedPhotoPicker, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
let parent: WrappedPickerView
init(_ parent: WrappedPickerView) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.presentationMode.wrappedValue.dismiss()
parent.wrappedPicker.dismiss(animated: false)
parent.photoPickerResult = results.first
}
}
}
This is far from ideal as I'm presenting at the wrong time and stuff. But it works until Apple provide a permanent fix for this.
I started getting a weird UI bug after the PHPickerViewController crashed where the keyboard was not visible but my views were still being squashed. So I suspected a keyboard / avoidance issue. I disabled keyboard avoidance in a parent view and managed to stop it from crashing.
.ignoresSafeArea(.keyboard)
.... still a iOS bug in 15.0. I've modified Fogmeister's class Coordinator to return the image in addition to the PHPickerResult.
struct WrappedPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var photoPickerResult: PHPickerResult?
#Binding var image: UIImage?
let wrappedPicker = WrappedPhotoPicker()
func makeUIViewController(context: Context) -> WrappedPhotoPicker {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
wrappedPicker.picker = picker
return wrappedPicker
}
func updateUIViewController(_ uiViewController: WrappedPhotoPicker, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
let parent: WrappedPickerView
init(_ parent: WrappedPickerView) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
self.parent.presentationMode.wrappedValue.dismiss()
self.parent.wrappedPicker.dismiss(animated: false)
self.parent.photoPickerResult = results.first
print(results)
guard let result = results.first else {
return
}
self.parent.image = nil
DispatchQueue.global().async {
result.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
guard let imageLoaded = object as? UIImage else {
return
}
DispatchQueue.main.async {
self.parent.image = imageLoaded
}
}
}
}
}
}

SwiftUI View Extension is called on View load/refresh

We created a custom view extension to extend the functionality of .alert and add a bunch of customizations to fit with our needs;
import SwiftUI
extension View {
public func alertX(isPresented: Binding<Bool>, content: () -> AlertX) -> some View {
let alertX_view = AlertX_View(visible: isPresented, alertX: content())
let alertXVC = AlertXViewController(alertX_view: alertX_view, isPresented: isPresented)
alertXVC.modalPresentationStyle = .overCurrentContext
alertXVC.view.backgroundColor = UIColor.clear
alertXVC.modalTransitionStyle = .crossDissolve
if isPresented.wrappedValue == true {
if AlertX_View.currentAlertXVCReference == nil {
AlertX_View.currentAlertXVCReference = alertXVC
}
let viewController = self.topViewController()
viewController?.present(alertXVC, animated: true, completion: nil)
} else {
alertXVC.dismiss(animated: true, completion: nil)
}
return self
}
... truncated for brevity
}
It is called on a view in the same way as .alert is called;
.alertX(isPresented: $viewModel.showAlert) {
AlertX(logo: networkStore.currentNetwork.networkTheme.systemLogo ,
logoColor: networkStore.activeColors.lightTextColor ,
backgroundColor: networkStore.activeColors.primaryColor,
title: Text("AlertX Test"))
}
Where $viewModel.showAlert is #Published var showAlert: Bool and AlertX is a custom view that contains our proprietary customizations to fit with our B2B application.
The issue I am having is that the information inside the closure for .alertX(isPresented: is called every time the view loads or changes state, regardless of whether or not the $viewModel.showAlert binding changes. The same is NOT true of the built in .alert view extension which is ONLY called when the $viewModel.showAlert value changes.
What modifications do I need to make in my implementation of public func alertX(isPresented: Binding<Bool>, content: () -> AlertX) -> some View { so that the information inside the closure it is only called when the binding value changes?
Remember that SwiftUI will reload all of the structs and functions all the time - and then decides, based on the data dependencies, which part to redraw. So any code in your view functions will run. A lot.
Try this: at the top of that function, put
guard isPresented.wrappedValue == true else {
return
}
Instead of checking it later on in the function.
Related, you might also want to look into making this code into a ViewModifier, and then your alertX function just applies the viewModifier based on the isPresented binding, otherwise returns self.

CNContactViewControllerDelegate not called when using the Coordinator pattern in SwiftUI

I've been grinding on this issue for quite a few days now and it seems like SwiftUI's relative "newness" doesn't seem to help me with this.
My gut feeling is that I'm somehow using CNContactViewControllerDelegate wrong but both Apple's documentation as well as other questions on SO make it seem like it should work. Maybe, it is also caused by .sheet's handling, but I wasn't able to isolate the issue as well. Not wrapping NewContactView inside NavigationView also made no difference and removed the default navigation bar (as expected).
I'm showing the CNContactViewController with init(forNewContact:) to give the app's users an ability to add new contacts. Persisting and everything works fine, however, none of the delegate's functions seem to get called.
Neither dismissing the modal with the "swipe to dismiss" gesture introduced in iOS 13 calls the delegate function, nor using the navigation buttons provided by CNContactViewController.
import Foundation
import SwiftUI
import ContactsUI
struct NewContactView: UIViewControllerRepresentable {
class Coordinator: NSObject, CNContactViewControllerDelegate, UINavigationControllerDelegate {
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
if let c = contact {
self.parent.contact = c
}
viewController.dismiss(animated: true)
}
func contactViewController(_ viewController: CNContactViewController, shouldPerformDefaultActionFor property: CNContactProperty) -> Bool {
return true
}
var parent: NewContactView
init(_ parent: NewContactView) {
self.parent = parent
}
}
#Binding var contact: CNContact
init(contact: Binding<CNContact>) {
self._contact = contact
}
typealias UIViewControllerType = CNContactViewController
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<NewContactView>) -> NewContactView.UIViewControllerType {
let vc = CNContactViewController(forNewContact: CNContact())
vc.delegate = makeCoordinator()
return vc
}
func updateUIViewController(_ uiViewController: NewContactView.UIViewControllerType, context: UIViewControllerRepresentableContext<NewContactView>) {
}
}
The view's code for showing the controller looks like this:
.sheet(isPresented: self.$viewModel.showNewContact, onDismiss: { self.viewModel.fetchContacts() }) {
NavigationView() {
NewContactView(contact: self.$viewModel.newContact)
}
}
Thank you for any pointers! Rubberducking sadly didn't help...
SwiftUI creates coordinator by itself and provides it to representable in context, so just use
func makeUIViewController(context: UIViewControllerRepresentableContext<NewContactView>) -> NewContactView.UIViewControllerType {
let vc = CNContactViewController(forNewContact: CNContact())
vc.delegate = context.coordinator // << here !!
return vc
}

How do I present an alert from a custom class in swiftui?

In SwiftUI, I have a network request running in scenedelegate, scenedidbecomeactive. I don't know which view the user will be on when the app becomes active, but I want to present an alert if the data in the network request changes. I simplified the code below, so it's easy to read...
func sceneDidBecomeActive(_ scene: UIScene) {
let customClass = CustomClass()
customClass.performNetworkRequest()
In CustomClass, i have...
func performNetWorkRequest() {
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let d = data {
let response = try JSONDecoder().decode(DetailResponse.self, from: d)
DispatchQueue.main.async {
//Here is where I want to either present an alert, but I can't figure out how to.
//OR do i put a func in SceneDeletegate to present the alert on the window.rootviewcontroller and then just call that func from here?
}
Any help is much appreciated!
Paul has a point - here's a possible implementation:
// In CustomClass.swift
import Combine
class CustomClass : ObservableObject {
#Published var dataRecieved = PassthroughSubject<DetailResponse, Never>()
init() {
performNetWorkRequest()
}
func performNetWorkRequest() {
URLSession.shared.dataTask(with: url) { (data, response, error) in
let response = try JSONDecoder().decode(DetailResponse.self, from: data)
DispatchQueue.main.async {
self.dataRecieved.send(response)
}
}
.resume()
}
}
// In SomeView.swift
import SwiftUI
import Combine
struct ContentView: View {
#State var showAlert = false
var customClass = CustomClass()
var body: some View {
Text("Hello, World!")
.onReceive(customClass.dataRecieved) { _ in
self.showAlert = true
}
.alert(isPresented: $showAlert) {
// your alert
}
}
}
Notice I didn't mention the SceneDelegate in any of these - this approach (called MVVM) is more flexible, in my opinion - besides, the way it is set up, performNetWorkRequest() will be executed as soon as your view is initialized, anyway.
You can also tweak the PassthroughSubject - I didn't know if you needed the DetailResponse or not.
Hope this helped!
Edit:
I just reread your question and it seems that this implementation is at fault as you noted there was no way to know what view the user would be on in the case of a network change. In that case, you can feed the same instance of CustomClass in your SceneDelegate as an EnvironmentObject.

Resources