Apple GameKit: unable to handle dismissal of the matchmaker view controller - ios

I have a GKTurnBasedMatchmakerViewController with an associated GKTurnBasedMatchmakerViewControllerDelegate. According to this documentation page, the function turnBasedMatchmakerViewControllerWasCancelled is called when the matchmaker is dismissed (without inviting any players). But I tried dismissing the matchmaker when testing, and this function is never even entered. Am I misunderstanding something about turnBasedMatchmakerViewControllerWasCancelled?
struct MatchmakerView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> GKTurnBasedMatchmakerViewController {
let request = // ...
let matchmakerViewController = GKTurnBasedMatchmakerViewController(matchRequest: request)
let matchmakerDelegate = MatchmakerDelegate()
matchmakerViewController.turnBasedMatchmakerDelegate = matchmakerDelegate
return matchmakerViewController
}
func updateUIViewController(_ uiViewController: GKTurnBasedMatchmakerViewController, context: Context) {}
}
private class MatchmakerDelegate: NSObject, GKTurnBasedMatchmakerViewControllerDelegate {
func turnBasedMatchmakerViewControllerWasCancelled(_ viewController: GKTurnBasedMatchmakerViewController) {
// FIXME: Dismissing the matchmaker does not call this method.
viewController.dismiss(animated: true)
print("This print statement is never reached")
}
}

Resolved: MatchmakerView was missing a makeCoordinator method.

Related

Solution for the implementation of interstitial ads of admob (SwiftUI)

Starting with iOS16, there was a problem with the implementation of AdMob interstitial advertising. I am using the SwiftUI framework. I looked at many sources, but unfortunately I did not find a solution. Link to on a similar topic, but without a solution. On iOS 15 everything works fine, but on iOS 16 the app crashes with the following error:
SwiftUI/UIViewControllerRepresentable.swift:332: Fatal error:
UIViewControllerRepresentables must be value types: InterstitialAdView
2022-11-20 15:34:32.811071+0300 MyApp[38437:1156286]
SwiftUI/UIViewControllerRepresentable.swift:332: Fatal error:
UIViewControllerRepresentables must be value types: InterstitialAdView
As I understand, to implement the UIViewControllerRepresentable protocol, you must use a struct, but the NSObject and GADFullScreenContentDelegate protocols can only be applied in a class. Question: how can the code be modified to meet the above requirements? I am using the following code (original from here):
// InterstitialAdView
final class InterstitialAdView: NSObject, UIViewControllerRepresentable, GADFullScreenContentDelegate {
//Here's the Ad Object we just created
let interstitialAd = InterstitialAd.shared.interstitialAd
#Binding var isPresented: Bool
var adUnitId: String
init(isPresented: Binding<Bool>, adUnitId: String) {
self._isPresented = isPresented
self.adUnitId = adUnitId
super.init()
interstitialAd?.fullScreenContentDelegate = self //Set this view as the delegate for the ad
}
//Make's a SwiftUI View from a UIViewController
func makeUIViewController(context: Context) -> UIViewController {
let view = UIViewController()
//Show the ad after a slight delay to ensure the ad is loaded and ready to present
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) {
self.showAd(from: view)
}
return view
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
//Presents the ad if it can, otherwise dismisses so the user's experience is not interrupted
func showAd(from root: UIViewController) {
if let ad = interstitialAd {
ad.present(fromRootViewController: root)
} else {
self.isPresented.toggle()
}
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
//Prepares another ad for the next time view presented
InterstitialAd.shared.loadAd(withAdUnitId: adUnitId)
//Dismisses the view once ad dismissed
isPresented.toggle()
}
}

Passing data in between controllers using coordinator pattern

I am trying to understand the working of Coordinator Pattern.
Here is my code
import UIKit
import Foundation
class CheckoutCoordinator: Coordinator, ScheduleDelegate {
var childCoordinator: [Coordinator] = [Coordinator]()
var navigationController: UINavigationController
init(nav: UINavigationController) {
self.navigationController = nav
}
func start() {
let ctrl = CheckoutController.initFromStoryboard()
ctrl.coordinator = self
self.navigationController.pushViewController(ctrl, animated: true)
}
func openSchedule() {
let ctrl = ScheduleController.initFromStoryboard()
ctrl.delegate = self
self.navigationController.pushViewController(ScheduleController.initFromStoryboard(), animated: true)
}
func didSelectTimings(date: NSDate, timings: NSString, distance: Double) {
}
}
From CheckoutController, i go to ScheduleController, do some work which calls its delegate method. The delegate should update some value in CheckoutController and pop scheduleController. I am unable to find any concrete explanation of above senario and how to implement it "properly".
Note that schedule controller has no navigation forward hence no coordinator class for it.
Any guidance will be appreciated
I would not handle the delegate logic in the coordinator. Instead I would move it right into your CheckoutController. So when calling the ScheduleController it would look in your coordinator like this:
func openSchedule(delegate: ScheduleDelegate?) {
let ctrl = ScheduleController.initFromStoryboard()
ctrl.delegate = delegate
navigationController.pushViewController(ScheduleController.initFromStoryboard(), animated: true)
}
And in your CheckoutController, conform to the ScheduleDelegate delegate:
class CheckoutController: ScheduleDelegate {
func didSelectTimings(date: NSDate, timings: NSString, distance: Double) {
// Do your staff
}
}
Then in your ScheduleController after calling the delegate method, I would call the coordinator to pop the self(in that case the ScheduleController).
delegate?.didSelectTimings(date: yourDate, timings: someTiming, distance: distance)
if let checkoutCoordinator = coordinator as? CheckoutCoordinator {
checkoutCoordinator.popViewController()
}
The popping logic can be solely in your viewController, but I like to keep the navigation in the Coordinator only. And in your CheckoutCoordinator, or better in your Coordinator(as this function is pretty general), implement the pop function.
extension Coordinator {
function popViewController(animated: Bool = true) {
navigationController?.popViewController(animated: animated)
}
}

RxSwift concurrency problems with coordinator

What I want to do:
Present VC1
When VC1 is dismissed, present VC2
Problem:
When VC1 is dismissed, VC2 does not present
Dirty Fix:
Put milisecond delay. It fixes the problem, but want to know why it happens
Explanation: I get viewDidDissapear event when VC1 dismisses so I can present VC2
If you need more details, please ask.
Code:
class ViewModel {
let coordinator = Coordinator()
struct Input {
let itemSelected: Driver<IndexPath>
}
struct Output {
let presentVC1: Driver<Void>
let presentVC2: Driver<Void>
}
func transform(input: Input) -> Output {
let navigateToVC1 = input.itemSelected
.flatMap { [coordinator] in
return coordinator.transition(to: Scene.VC1)
}
let navigateToVC2 = navigateToVC1
.delay(.milliseconds(1))
.flatMap { [coordinator] in
return coordinator.transition(to: Scene.VC2)
}
return Output(presentVC1: presentVC1, presentVC2: presentVC2)
}
Coordinator code:
func transition(to scene: TargetScene) -> Driver<Void> {
let subject = PublishSubject<Void>()
switch scene.transition {
case let .present(viewController):
_ = viewController.rx
.sentMessage(#selector(UIViewController.viewDidDisappear(_:)))
.map { _ in }
.bind(to:subject)
currentViewController.present(viewController, animated: true)
return subject
.take(1)
.asDriverOnErrorJustComplete()
}
The viewDidDisappear method is called before the view controller is fully dismissed. You should not try to present the second view controller until the callback of dismiss is called.
Wherever you are dismissing your view controller, use the below instead and don't present the next view controller until after the observable emits a next event.
extension Reactive where Base: UIViewController {
func dismiss(animated: Bool) -> Observable<Void> {
Observable.create { [base] observer in
base.dismiss(animated: animated) {
observer.onNext(())
observer.onCompleted()
}
return Disposables.create()
}
}
}
I suggest you consider using my Cause-Logic-Effect architecture which contains everything you need to properly handle view controller presentation and dismissal.
https://github.com/danielt1263/CLE-Architecture-Tools
A portion of the interface is below:
/**
Presents a scene onto the top view controller of the presentation stack. The scene will be dismissed when either the action observable completes/errors or is disposed.
- Parameters:
- animated: Pass `true` to animate the presentation; otherwise, pass `false`.
- sourceView: If the scene will be presented in a popover controller, this is the view that will serve as the focus.
- scene: A factory function for creating the Scene.
- Returns: The Scene's output action `Observable`.
*/
func presentScene<Action>(animated: Bool, overSourceView sourceView: UIView? = nil, scene: #escaping () -> Scene<Action>) -> Observable<Action>
extension NSObjectProtocol where Self : UIViewController {
/**
Create a scene from an already existing view controller.
- Parameter connect: A function describing how the view controller should be connected and returning an Observable that emits any data the scene needs to communicate to its parent.
- Returns: A Scene containing the view controller and return value of the connect function.
Example:
`let exampleScene = ExampleViewController().scene { $0.connect() }`
*/
func scene<Action>(_ connect: (Self) -> Observable<Action>) -> Scene<Action>
}
struct Scene<Action> {
let controller: UIViewController
let action: Observable<Action>
}
The connect function is your view model, when its Observable completes or its subscriber disposes, the view controller will automatically dismiss.
The presentScene function is your coordinator. It handles the actual presenting and dismissing of scenes. When you dismiss and present a new scene, it will properly handle waiting until the previous view controller is dismissed before presenting the next one.

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
}

SwiftUI and UICloudSharingController hate each other

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)

Resources