Call async func in Combine NotificationCenter sink - swiftui

I am new to Combine and SwiftUI. I have the following publisher/subscriber.
NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
print("update server side")
self?.uploadContent()
}
.store(in: &subscriptions)
func uploadContent async () {
print("uploading...")
}
I get an error saying
'async' call in a function that does not support concurrency
Call to main actor-isolated instance method 'uploadContent()' in a synchronous nonisolated context
Is there any way to call the async func? Any help or ideas appreciated.

You could spin up a single Task and drop the use of Combine altogether, like this:
func run() {
Task { #MainActor [weak self] in
for await _ in NotificationCenter.default.notifications(named: UIApplication.willEnterForegroundNotification) {
guard let self = self else { return }
print("update server side")
await self.uploadContent()
}
}
}
If you really want to keep using Combine, you'll need to spin up a task for each element received by sink, like this:
func combine() {
NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
print("update server side")
Task { #MainActor in
await self?.uploadContent()
}
}
.store(in: &subscriptions)
}
This only works if self's type conforms to Sendable or is #MainActor-bound. If your self type is a subclass of UIViewController or UIApplicationDelegate, it's automatically #MainActor-bound.

Related

CurrentValueSubject send(value) doesn't trigger receiveValue

I have a CurrentValueSubject to hold data received from Firebase fetch request.
final class CardRepository: ObservableObject {
private let store = Firestore.firestore()
var resultSubject = CurrentValueSubject<[Card], Error>([])
init() {
}
func get() {
store.collection(StorageCollection.EnglishCard.getPath)
.addSnapshotListener { [unowned self] snapshot, err in
if let err = err {
resultSubject.send(completion: .failure(err))
}
if let snapshot = snapshot {
let cards = snapshot.documents.compactMap {
try? $0.data(as: Card.self)
}
resultSubject.send(cards)
}
}
}
}
In my ViewModel, I want whenever resultSubject sends or emits a value. It will change the state and has that value attached to the succes state.
class CardViewModel: CardViewModelProtocol, ObservableObject {
#Published var repository: CardRepository
#Published private(set) var state: CardViewModelState = .loading
private var cancellables: Set<AnyCancellable> = []
required init (_ repository: CardRepository) {
self.repository = repository
bindingCards()
}
private func bindingCards() {
let _ = repository.resultSubject
.sink { [unowned self] comp in
switch comp {
case .failure(let err):
self.state = .failed(err: err)
case .finished:
print("finised")
}
} receiveValue: { [unowned self] res in
self.state = .success(cards: res)
}
}
func add(_ card: Card) {
repository.add(card)
}
func get() {
repository.get()
}
}
On my ContentView, it will display a button that print the result.
struct ContentView: View {
#StateObject var viewModel = CardViewModel(CardRepository())
var body: some View {
Group {
switch viewModel.state {
case .loading:
ProgressView()
Text("Loading")
case .success(cards: let cards):
let data = cards
Button {
print(data)
} label: {
Text("Tap to show cards")
}
case .failed(err: let err):
Button {
print(err)
} label: {
Text("Retry")
}
}
Button {
viewModel.get()
} label: {
Text("Retry")
}
}.onAppear {viewModel.get() }
}
}
My problem is the block below only trigger once when I first bind it to the resultSubject.
} receiveValue: { [unowned self] res in
self.state = .success(cards: res)
}
I did add a debug and resultSubject.send(cards) works every time.
You need to store the Cancellable returned from the .sink in the class so it doesn't get deallocated:
Either in a set var cancellables = Set<AnyCancellable>() if you want to use multiple Publishers, or in var cancellable: AnyCancellable?.
Add .store(in &cancellables) like so:
} receiveValue: { [unowned self] res in
self.state = .success(cards: res)
}.store(in: &cancellables)
Edit:
In ObservableObject classes we don't use sink, we assign to an #Published:
let _ = repository.resultSubject
.assign(to: &$self.state)

Google Ads 8.0 Interstitial SwiftUI

Seems like there isn't many examples of using Google MobileAdsSDK 8.0 (iOS) with SwiftUI.
So far I have a class Interstitial
import GoogleMobileAds
import UIKit
final class Interstitial:NSObject, GADFullScreenContentDelegate{
var interstitial:GADInterstitialAd!
override init() {
super.init()
LoadInterstitial()
}
func LoadInterstitial(){
let req = GADRequest()
GADInterstitialAd.load(withAdUnitID: "...", request: req) { ad, error in
self.interstitial = ad
self.interstitial.fullScreenContentDelegate = self
}
}
func showAd(){
if self.interstitial != nil {
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
LoadInterstitial()
}
}
In my SwiftUI view i create a local variable Interstitial, and when an action is performed I call the showAd() function however when the ad displays it stops the code immediately following the showAd() call from running. So I think I need to somehow call showAd() and once the ad is dismissed then perform the remainder of my code in the view. As you can see above the Interstitial class is the delegate, but how do I "alert" my SwiftUI view that the ad was dismissed so I can execute the rest of the code? Below is my View.
import SwiftUI
struct MyView: View {
#Environment(\.managedObjectContext) var managedObjectContext
var interstitial : Interstitial = Interstitial()
var body: some View {
VStack{
//... Display content
}
.navigationBarItems(trailing:
HStack{
Button(action: actionSheet) {
Image(systemName: "square.and.arrow.up")
}
}
)
}
func showAd(){
interstitial.showAd()
}
func actionSheet() {
showAd()
let data = createPDF()
let temporaryFolder = FileManager.default.temporaryDirectory
let fileName = "export.pdf"
let temporaryFileURL = temporaryFolder.appendingPathComponent(fileName)
do {
try data.write(to: temporaryFileURL)
let av = UIActivityViewController(activityItems: [try URL(resolvingAliasFileAt: temporaryFileURL)], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
} catch {
print(error)
}
}
}
By adding an excaping closure, you pass a function and perform the needed actions.
final class InterstitialAd: NSObject, GADFullScreenContentDelegate {
var completion: () -> Void
var interstitial: GADInterstitialAd!
init(completion: #escaping () -> Void) {
self.completion = completion
super.init()
LoadInterstitialAd()
}
func LoadInterstitialAd() {
let req = GADRequest()
GADInterstitialAd.load(withAdUnitID: Constants.AdmobIDs.saveImageBlockID, request: req) { ad, error in
self.interstitial = ad
self.interstitial.fullScreenContentDelegate = self
}
}
func show() {
if self.interstitial != nil {
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
LoadInterstitialAd()
completion()
}
}

How to execute one function after completion of another

I have been trying to execute getData() after the completion of login(), but that never seems to work when using DispatchQueue.main.async
I have only been able to get it to work by forcing a delay, what's the best way to go about this?
Thanks
class DataTask {
func login{...}
func getData{...}
}
struct ContentView: View {
#State private var data = DataTask()
var body: some View{
Text("Hello world")
.onAppear{
data.login()
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0){
func refreshData(){
data.getData()
// update the response into core data
}
}
}
}
}
Never use hard-coded delays to work around an asynchronous task, that's a horrible practice.
To execute a function after the completion of another add a completion handler
class DataTask {
func login(completion: #escaping: () -> Void) {... completion() ...}
func getData {...}
}
...
data.login {
data.getData()
}

RealityKit – Loading Reality Composer scenes with SwiftUI

I'm trying to load different models on face using SwiftUI, RealityKit and ARKit.
struct AugmentedRealityView: UIViewRepresentable {
#Binding var modelName: String
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let configuration = ARFaceTrackingConfiguration()
arView.session.run(configuration, options: [.removeExistingAnchors,
.resetTracking])
loadModel(name: modelName, arView: arView)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) { }
private func loadModel(name: String, arView: ARView) {
var cancellable: AnyCancellable? = nil
cancellable = ModelEntity.loadAsync(named: name).sink(
receiveCompletion: { loadCompletion in
if case let .failure(error) = loadCompletion {
print("Unable to load model: \(error.localizedDescription)")
}
cancellable?.cancel()
},
receiveValue: { model in
let faceAnchor = AnchorEntity(.face)
arView.scene.addAnchor(faceAnchor)
faceAnchor.addChild(model)
model.scale = [1, 1, 1]
})
}
}
This is how I load them but when the camera view opens and loads one model then the other models won't be loaded. Can someone help me out?
When the value of your Binding changes, SwiftUI is calling your updateUIView(_:,context:) implementation, which does noting.
Additionally, you are not storing the AnyCancellable. When the token returned by sink gets deallocated the request will be cancelled. That could result in unexpected failures when trying to load lager models.
To fix both of these issue, use a Coordinator.
import UIKit
import RealityKit
import SwiftUI
import Combine
import ARKit
struct AugmentedRealityView: UIViewRepresentable {
class Coordinator {
private var token: AnyCancellable?
private var currentModelName: String?
fileprivate func loadModel(_ name: String, into arView: ARView) {
// Only load model if the name is different from the previous one
guard name != currentModelName else {
return
}
currentModelName = name
// This is optional
// When the token gets overwritten
// the request gets cancelled
// automatically
token?.cancel()
token = ModelEntity.loadAsync(named: name).sink(
receiveCompletion: { loadCompletion in
if case let .failure(error) = loadCompletion {
print("Unable to load model: \(error.localizedDescription)")
}
},
receiveValue: { model in
let faceAnchor = AnchorEntity(.camera)
arView.scene.addAnchor(faceAnchor)
faceAnchor.addChild(model)
model.scale = [1, 1, 1]
})
}
fileprivate func cancelRequest() {
token?.cancel()
}
}
#Binding var modelName: String
func makeCoordinator() -> Coordinator {
Coordinator()
}
static func dismantleUIView(_ uiView: ARView, coordinator: Coordinator) {
coordinator.cancelRequest()
}
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let configuration = ARFaceTrackingConfiguration()
arView.session.run(configuration, options: [.removeExistingAnchors,
.resetTracking])
context.coordinator.loadModel(modelName, into: arView)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
context.coordinator.loadModel(modelName, into: uiView)
}
}
We create a nested Coordinator class that holds the AnyCancellable token and move the loadModel function into the Coordinator.
Other than a SwiftUI View, the Coordinator is a class that lives while your view is visible (always a remember that SwiftUI might create and destroy your View at will, its lifecycle is not related to the actual "view" that is shown on screen).
In out loadModel class we double check that the value of our Binding actually changed so that we don't cancel an ongoing request for the same model when SwiftUI updates our View, e.g. because of a change in the environment.
Then we implement the makeCoordinator function to construct one of our Coordinator objects.
Both in makeUIView and in updateUIView we call the loadModel function on our Coordinator.
The dimantleUIView method is optional. When the Coordinator gets deconstructed our token gets released as well, which will trigger Combine into canceling ongoing requests.

SwiftUI: NavigationLink's Destination initialized whenever its parent is redrawn

So I have a ParentView, which has a NavigationLink, leading to a UIViewControllerRepresentable-conforming PageViewController.
Now that ParentView also has some subscription on some publisher. Whenever that one is fired, not only will the ParentView redraw all its content (which it should), it will also re-initialize the (already presenting) PageViewController.
That leads to stuttering/glitching, because the PageViewController is already presenting and using the controllers that are continually being resetted.
Below is the ParentView and PageViewController (without the Coordinator stuff), both is pretty vanilla. The commented guard line is a hack I tried to prevent it from updating if displayed already. It helps but it's still stuttering on every swipe.
So the question is: How can we prevent the updating of a presented ViewController-wrapped-View when its presenting View is redrawn?
struct ParentView: View {
#Binding var something: Bool
var body: some View {
NavigationLink(destination: PageViewController(controllers: controllers)) {
Text("Push me")
}
}
}
final class PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
private var currentPage = 0
init(controllers: [UIViewController]) {
self.controllers = controllers
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
// I tried this: guard pageViewController.viewControllers!.isEmpty else { return }
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: true)
}
}
If your controllers don't change after being displayed once, you can simply call:
pageViewController.setViewControllers([controllers[currentPage]], direction: .forward, animated: true)
from the makeUIViewController(context:) function instead of the updateUIViewController(:) function.