imessage extension app group conversation send fails on first run - swift

I have an imessage extension app that works fine except on the first send to a group.
iOS 14.4 multiple devices 8, 8plus, 10...
Xcode 12.4
The code goes straight from the send closure (success) to didResignActive. The app is supposed to stay active. There is no dismiss called anywhere. I've debugged w/ attached device stepped through the code, and it goes straight from the log line to didResignActive.
If I launch the app again on the same group it works fine. The app only fails if I start a new group conversation, then click on the app in tray, and send is called.
note: this only happens to group sends, and only on the first time a group is created.
thisConversation.send(message) { error in
if let error = error {
os_log("submitMessage(%#): initial send error: %#", log: .default, type: .debug, type, error.localizedDescription)
} else {
os_log("submitMessage(%#): initial send success!", log: .default, type: .debug, type)
}
}

Edit:
Just an update...
I was told to file a bug report for this (which I did).
I also supplied a video which shows how to reproduce the bug.
This is the message sent back from support...
Hi,
Thank you contacting Apple Developer Technical Support (DTS).
There is no workaround DTS can provide for Feedback ID #FB9049862; it
is still under investigation. Please continue to track the problem via
the bug report.
...
I was able to create a test app that reproduces this problem, with what I think is the minimal amount of code to demonstrate. Hope this invites a solution!
//
// MessagesViewController.swift
// testGroupSend MessagesExtension
//
import UIKit
import Messages
import os
class MessagesViewController: MSMessagesAppViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
// MARK: - Conversation Handling
func composeSelectionMsg(on conversation: MSConversation, in
session: MSSession) -> MSMessage {
let layout = MSMessageTemplateLayout()
layout.caption = "caption..."
let message = MSMessage(session: session)
message.layout = layout
message.summaryText = "summary text..."
var components = URLComponents()
var queryItems = [URLQueryItem]()
queryItems.append(URLQueryItem(name: "MessageType1", value: "msgType"))
queryItems.append(URLQueryItem(name: "Encode-Name", value: "Encode-Value"))
components.queryItems = queryItems
message.url = components.url!
return message
}
func submitMessage() {
guard let conversation = activeConversation else {
os_log("submitMessage(): guard on conversation falied!", log: .default, type: .debug)
return
}
var session : MSSession
if let tSess = conversation.selectedMessage?.session {
session = tSess
os_log("submitMessage() got a session!...", log: .default, type: .debug)
} else {
os_log("###### submitMessage() did NOT get a session, creating new MSSession() #####", log: .default, type: .debug)
session = MSSession()
}
var message: MSMessage
message = composeSelectionMsg(on: conversation, in: session)
// conversation.insert(message) { error in
conversation.send(message) { error in
if let error = error {
os_log("submitMessage(): initial send error: %#", log: .default, type: .debug, error.localizedDescription)
} else {
os_log("submitMessage(): initial send success!", log: .default, type: .debug)
}
}
}
fileprivate func loadContentView() {
os_log("loadContentView()...", log: .default, type: .debug)
let childViewCtrl = ContentViewHostController()
childViewCtrl.delegate = self
childViewCtrl.view.layoutIfNeeded() // avoids snapshot warning?
if let window = self.view.window {
childViewCtrl.myWindow = window
window.rootViewController = childViewCtrl
}
}
override func willBecomeActive(with conversation: MSConversation) {
loadContentView()
}
override func didResignActive(with conversation: MSConversation) {
os_log("didResignActive()...", log: .default, type: .debug)
}
}
//
//
import SwiftUI
import Messages
import os
final class ContentViewHostController: UIHostingController<ContentView> {
weak var delegate: ContentViewHostControllerDelegate?
weak var myWindow: UIWindow?
init() {
super.init(rootView: ContentView())
rootView.submitMessage = submitMessage
}
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: ContentView())
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
func submitMessage() {
os_log("ContentViewHostController::submitMessage(): submit message...", log: .default, type: .debug)
delegate?.contentViewHostControllerSubmitMessage(self)
}
}
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var submitMessage: (() -> Void)?
var body: some View {
VStack {
VStack {
HStack {
Button(action: { self.cancel() } ) { Image(systemName: "chevron.left") }
.padding()
Spacer()
Button(action: { self.submit() } ) {
Text("Send...")
}
.padding()
} // HStack
} // VStack
} // outside most VStack
} // body
private func cancel() {
presentationMode.wrappedValue.dismiss()
}
private func submit() {
submitMessage!()
presentationMode.wrappedValue.dismiss()
}
} // ContentView
//
//
import SwiftUI
import Messages
import os
extension MessagesViewController: ContentViewHostControllerDelegate {
// MARK: - ContenHost delegate
func contentViewHostControllerSubmitMessage(_ controller: ContentViewHostController) {
os_log("delegateSubmitMessage:...")
submitMessage()
}
}
//
//
import SwiftUI
import Messages
protocol ContentViewHostControllerDelegate: class {
func contentViewHostControllerSubmitMessage( _ controller: ContentViewHostController )
}

Related

How to run watch app in the background using WKExtendedRuntimeSession

Thank you for taking a look at this page.
I am developing a Watch App and would like to do some background data processing in SwiftUI.
I tried to run it using WKExtendedRuntimeSession, but it doesn't seem to be running when it becomes inactive.
import SwiftUI
#main
struct FirebaseExample_Watch_AppApp: App {
var session = WKExtendedRuntimeSession()
init() {
session.start()
// MARK:- Extended Runtime Session Delegate Methods
func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print(1)
}
func extendedRuntimeSessionWillExpire(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print(2)
}
func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
print(3)
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
But the result is nothing happens the moment I put my arm down and the app screen disappears.
I alse tried to do this;
                 
init() {
        func startTimerButtonPressed() {
print(WKApplicationState.RawValue())
var session = WKExtendedRuntimeSession()
session.start()
print(WKExtendedRuntimeSessionState.RawValue())
}
The results are all zeros and do not appear to be initiated.
If you are familiar with WKExtendedRuntimeSession, please let me know how to run it.
Reference
https://developer.apple.com/documentation/watchkit/wkextendedruntimesession
You are not setting a delegate for your session. A possible implementation would look like this:
#main
struct TestWatchOSOnly_Watch_AppApp: App {
#State var session = WKExtendedRuntimeSession()
//define and create the delegate
#State var delegate = WKDelegate()
var body: some Scene {
WindowGroup {
ContentView()
.onAppear{
//create a new session
session = WKExtendedRuntimeSession()
//assign the delegate
session.delegate = delegate
//start the session
session.start()
}
}
}
}
// define the delegate and its methods
class WKDelegate: NSObject, WKExtendedRuntimeSessionDelegate{
func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
print(reason.rawValue)
}
func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print("did start")
}
func extendedRuntimeSessionWillExpire(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print("will expire")
}
}

How can I update the view when the app is done scanning for NFC?

I have a Swift application that reads from an NFC card. I want it to show the data it read on the screen, which I am able to do if I have a button that checks for updated data using the getDetected() function. I want to, however, update the view when it is done reading the NFC tag so I can immediately display the data. How can I do this?
NFC Reader class:
import Foundation
import CoreNFC
class NFCReader: NSObject, NFCNDEFReaderSessionDelegate {
var detected = [NFCNDEFMessage]()
var session: NFCNDEFReaderSession?
func beginScanning() {
guard NFCNDEFReaderSession.readingAvailable else { return }
session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true)
session?.alertMessage = "Hold your iPhone near the reader to unlock."
session?.begin()
}
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
self.detected = messages
self.session = nil
}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
self.session = nil
}
func getDetected() -> [NFCNDEFMessage] {
return detected
}
}
you could try the following approach, using a ObservableObject.
Whenever the #Published var detected is changed, the UI will be updated.
class NFCReader: NSObject, NFCNDEFReaderSessionDelegate, ObservableObject { // <--- here
#Published var detected = [NFCNDEFMessage]() // <--- here
var session: NFCNDEFReaderSession?
func beginScanning() {
guard NFCNDEFReaderSession.readingAvailable else { return }
session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true)
session?.alertMessage = "Hold your iPhone near the reader to unlock."
session?.begin()
}
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
self.detected = messages
self.session = nil
}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
self.session = nil
}
// no real need for this
func getDetected() -> [NFCNDEFMessage] {
return detected
}
}
struct ContentView: View {
#StateObject var readerNFC = NFCReader() // <-- here
var body: some View {
ForEach(readerNFC.detected, id: \.self) { msg in
// .....
}
}
}

View won't show when calling by function in SwiftUI

I have this problem. I'm trying to show Ads in my app using AdMob. It all works fine (i did some debug) but the view containing the video is not showing... I can't figure out why it's not showing on the top of my content.
Here is the the class that calls and opens the view with ads:
import SwiftUI
import GoogleMobileAds
import UIKit
class Rewarded: NSObject, GADRewardedAdDelegate, ObservableObject{
#Published var adLoaded: Bool = false
var rewardedAd:GADRewardedAd = GADRewardedAd(adUnitID: "ca-app-pub-3940256099942544/1712485313")
var rewardFunction: (() -> Void)? = nil
override init() {
super.init()
LoadRewarded()
}
func LoadRewarded(){
let req = GADRequest()
self.rewardedAd.load(req) { error in
if error != nil {
self.adLoaded = false
print("Ad not loaded")
} else {
self.adLoaded = true
print("Ad loaded")
}
}
}
func showAd(rewardFunction: #escaping () -> Void){
if self.rewardedAd.isReady {
print("Ad is ready to show")
self.rewardFunction = rewardFunction
if let root = UIApplication.shared.windows.first?.rootViewController {
self.rewardedAd.present(fromRootViewController: root, delegate: self)
print("Presenting")
} else {
print("Failed to open ad")
}
} else {
print("Ad not ready to show")
}
}
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
if let rf = rewardFunction {
rf()
}
}
func rewardedAdDidDismiss(_ rewardedAd: GADRewardedAd) {
self.rewardedAd = GADRewardedAd(adUnitID: "ca-app-pub-3940256099942544/1712485313")
LoadRewarded()
}
}
And here is the SwiftUI portion of the code that calls the function of the class:
Button {
self.rewardAd.showAd(rewardFunction: {
// GIVE REWARD HERE
})
} label: {
Text("Watch Ad")
.foregroundColor(Color.white)
.font(.custom(customFont, size: 17))
.fontWeight(.bold)
}
Also the console doesn't print any error...
Does anybody know why it is not showing?
Thank you all
This works for me. I had a heck of a time getting this to work and tried following all of the examples I could find, finally landed on this.
func showAd(rewardFunction: #escaping () -> Void){
if let ad = rewardedAd {
self.rewardFunction = rewardFunction
if let root = UIApplication.shared.windows.first?.rootViewController {
ad.present(fromRootViewController: root, userDidEarnRewardHandler: {
let reward = ad.adReward
// reward the user
})
}
else {
print("Error presenting ad")
}
}
else{
print("Ad Not Ready")
}
}

How do I use the Google Mobile Ads SDK in SwiftUI, or use the UIKit UIViewController within a SwiftUI view?

I have a SwiftUI view that I want to open a rewarded ad from the Google Mobile Ads SDK when I press a button. The instructions for loading the ads (https://developers.google.com/admob/ios/rewarded-ads#create_rewarded_ad) are in UIKit, and I'm struggling to use them in my SwiftUI app. Is there a way to load the ads using SwiftUI, or if I use UIKit, how do I integrate it into SwiftUI?
This is the SwiftUI parent view:
struct AdMenu: View {
var body: some View {
NavigationView {
NavigationLink(destination: Ads())
{
Text("Watch Ad")
}
}
}
}
I don't know UIKit, but I think this is the code I want to use in SwiftUI:
class ViewController: UIViewController, GADRewardedAdDelegate {
var rewardedAd: GADRewardedAd?
var adRequestInProgress = false
#IBAction func doSomething(sender: UIButton) {
if rewardedAd?.isReady == true {
rewardedAd?.present(fromRootViewController: self, delegate:self)
}else {
let alert = UIAlertController(
title: "Rewarded video not ready",
message: "The rewarded video didn't finish loading or failed to load",
preferredStyle: .alert)
let alertAction = UIAlertAction(
title: "OK",
style: .cancel,
handler: { [weak self] action in
// redirect to AdMenu SwiftUI view somehow?
})
alert.addAction(alertAction)
self.present(alert, animated: true, completion: nil)
}
}
func createAndLoadRewardedAd() {
rewardedAd = GADRewardedAd(adUnitID: "ca-app-pub-3940256099942544/1712485313")
adRequestInProgress = true
rewardedAd?.load(GADRequest()) { error in
self.adRequestInProgress = false
if let error = error {
print("Loading failed: \(error)")
} else {
print("Loading Succeeded")
}
}
return rewardedAd
}
// Tells the delegate that the user earned a reward
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
print("Reward received with currency: \(reward.type), amount \(reward.amount).")
}
// Tells the delegate that the rewarded ad was presented
func rewardedAdDidPresent(_ rewardedAd: GADRewardedAd) {
print("Rewarded ad presented.")
}
// Tells the delegate that the rewarded ad was dismissed
func rewardedAdDidDismiss(_ rewardedAd: GADRewardedAd) {
print("Rewarded ad dismissed.")
}
// Tells the delegate that the rewarded ad failed to present
func rewardedAd(_ rewardedAd: GADRewardedAd, didFailToPresentWithError error: Error) {
rewardedAd = createAndLoadRewardedAd()
print("Rewarded ad failed to present.")
}
override func viewDidLoad() {
super.viewDidLoad()
if !adRequestInProgress && !(rewardedAd?.isReady ?? false) {
rewardedAd = createAndLoadRewardedAd()
}
I have used a very simple approach. I have a delegate class which loads the rewarded Ad and informs the view that its loaded, hence the view presents it. When user watches the full Ad, same delegate gets the success callback and it informs the view about it.
The code for delegate looks like:-
class RewardedAdDelegate: NSObject, GADRewardedAdDelegate, ObservableObject {
#Published var adLoaded: Bool = false
#Published var adFullyWatched: Bool = false
var rewardedAd: GADRewardedAd? = nil
func loadAd() {
rewardedAd = GADRewardedAd(adUnitID: "ca-app-pub-3940256099942544/1712485313")
rewardedAd!.load(GADRequest()) { error in
if error != nil {
self.adLoaded = false
} else {
self.adLoaded = true
}
}
}
/// Tells the delegate that the user earned a reward.
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
adFullyWatched = true
}
/// Tells the delegate that the rewarded ad was presented.
func rewardedAdDidPresent(_ rewardedAd: GADRewardedAd) {
self.adLoaded = false
}
/// Tells the delegate that the rewarded ad was dismissed.
func rewardedAdDidDismiss(_ rewardedAd: GADRewardedAd) {}
/// Tells the delegate that the rewarded ad failed to present.
func rewardedAd(_ rewardedAd: GADRewardedAd, didFailToPresentWithError error: Error) {}
}
Now you need a view to initiate and present the RewardedAd:-
struct RewardedAd: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
if adDelegate.adLoaded && !adDelegate.adFullyWatched {
let root = UIApplication.shared.windows.first?.rootViewController
self.adDelegate.rewardedAd!.present(fromRootViewController: root!, delegate: adDelegate)
}
return Text("Load ad").onTapGesture {
self.adDelegate.loadAd()
}
}
}
Explanation:-
In the above view when user taps on Load Ad, we initiate the loading and then the delegate updates the Published Boolean. This informs our view that ad is Loaded and we call:-
let root = UIApplication.shared.windows.first?.rootViewController
self.adDelegate.rewardedAd!.present(fromRootViewController: root!, delegate: adDelegate)
The Ad is now playing on screen and if user watches is completely, the delegate will get a success callback and it'll update another Published Boolean. This will inform us that we need to reward the user now (you can add your way to handle/reward the user).
I figured out a way how to nicely translate the objective-c code from the AdMob page to something you can easily use in SwiftUI.
final class RewardedAd {
private let rewardId = "ca-app-pub-3940256099942544/1712485313" // TODO: replace this with your own Ad ID
var rewardedAd: GADRewardedAd?
init() {
load()
}
func load(){
let request = GADRequest()
// add extras here to the request, for example, for not presonalized Ads
GADRewardedAd.load(withAdUnitID: rewardId, request: request, completionHandler: {rewardedAd, error in
if error != nil {
// loading the rewarded Ad failed :(
return
}
self.rewardedAd = rewardedAd
})
}
func showAd(rewardFunction: #escaping () -> Void) -> Bool {
guard let rewardedAd = rewardedAd else {
return false
}
guard let root = UIApplication.shared.keyWindowPresentedController else {
return false
}
rewardedAd.present(fromRootViewController: root, userDidEarnRewardHandler: rewardFunction)
return true
}
}
I also added an extension to get the root view easier:
extension UIApplication {
var keyWindow: UIWindow? {
return UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first(where: { $0 is UIWindowScene })
.flatMap({ $0 as? UIWindowScene })?.windows
.first(where: \.isKeyWindow)
}
var keyWindowPresentedController: UIViewController? {
var viewController = self.keyWindow?.rootViewController
if let presentedController = viewController as? UITabBarController {
viewController = presentedController.selectedViewController
}
while let presentedController = viewController?.presentedViewController {
if let presentedController = presentedController as? UITabBarController {
viewController = presentedController.selectedViewController
} else {
viewController = presentedController
}
}
return viewController
}
}
If you add that to your project you can just call it like this in your SwiftUI code:
var rewardAd: RewardedAd
init(){
self.rewardAd = RewardedAd()
rewardAd.load()
}
var body: some View {
Button(action: {
self.rewardAd.showAd(rewardFunction: {
// TODO: give the user a reward for watching
self.collectedDiamonds += 1
})
}) {
Text("Watch Ad - Earn 💎")
.foregroundColor(.white)
.padding()
}
.background(Capsule().fill(Color.blue))
}

Thread 1 EXC_BAD_ACCESS (code=2, address=0x7ffeeb1aeff8)

I am trying to learn VIPER. I followed this tutorial. I have these Interactor and Presenter:
class PPresenter: ViewToPresenterProtocol {
var view: PresenterToViewProtocol?
var router: PresenterToRouterProtocol? = PRouter()
var interactor: PresenterToInteractorProtocol? = PInteractor()
func initiateFetch() {
interactor?.fetchMatches()
}
func showMatchScreen(navigationC: UIViewController) {
router?.pushToMatchDetailScreen(navigationC: navigationC)
}
}
extension PPresenter: InteractorToPresenterProtocol {
func matchFetched(match: MatchDetails?, banner: Banner?) {
print(match!)
print(banner!)
}
func matchFetchError() {
//TODO
}
}
class PInteractor: PresenterToInteractorProtocol {
var presenter: InteractorToPresenterProtocol? = PPresenter()
var live: Live?
var upcoming: Upcoming?
var banners: Banner?
func fetchMatches() {
let parameters = ["api_token" : Constants.USER_INFO["api_token"].rawValue,"player_id" : Constants.USER_INFO["player_id"].rawValue]
ServiceHelper.sharedInstance.sendRequest(path: "get-predictor", params: parameters, showSpinner: true) { (response, error) in
if let error = error {
print("Unable to fetch match listing",error.localizedDescription)
return
} else {
guard let obj = try? JSONDecoder().decode(MatchDetails.self, from: response.rawData()) else { self.presenter?.matchFetchError(); return }
guard let bannerObj = try? JSONDecoder().decode(Banner.self,from: response.rawData()) else {self.presenter?.matchFetchError(); return }
self.presenter?.matchFetched(match: obj, banner: bannerObj)
}
}
}
}
Now, what is happening here, I get the router working, the view is coming, it is calling presenter, the presenter is calling the interactor, the interactor is successfully calling the API and getting the data and now it is time to return the data received from Interactor to Presenter and here it constantly throwing the following error:
Thread 1 EXC_BAD_ACCESS (code=2, address=0x7ffeeb1aeff8)
I think you have a cyclic call, maybe your interactor is not fully initialized and then you want data from it and then you got "Bad access error".