I've tried a whole bunch of ways to get Game Center working in my SpriteKit game. Unfortunately the way I've done it in the past using ObjC and ViewControllers don't work because I'm using SKScene/ a GameScene.
This is the swift version of the code (I think):
// MARK: Game Center Integration
//login and make available
func authenticateLocalPlayer(){
let localPlayer = GKLocalPlayer()
print(localPlayer)
localPlayer.authenticateHandler = {(viewController, error) -> Void in
if ((viewController) != nil) {
self.presentViewController(viewController!, animated: true, completion: nil)
}else{
print((GKLocalPlayer.localPlayer().authenticated))
}
}
}
//submit a score to leaderboard
func reportScoreToLeaderboard(thisScore:Int){
if GKLocalPlayer.localPlayer().authenticated {
let scoreReporter = GKScore(leaderboardIdentifier: "LeaderboardID")
scoreReporter.value = Int64(thisScore)
let scoreArray: [GKScore] = [scoreReporter]
GKScore.reportScores(scoreArray, withCompletionHandler: { (error: NSError?) -> Void in
if error != nil {
print(error!.localizedDescription)
} else {
print("Score submitted")
}
})
}
}
//show leaderboard (call from button or touch)
func showLeaderboard() {
let vc = self.view?.window?.rootViewController
let gc = GKGameCenterViewController()
gc.gameCenterDelegate = self
vc?.presentViewController(gc, animated: true, completion: nil)
}
//hides view when finished
func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController){
gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
}
Unfortunetely, no matter what I try, I either get this error:
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior
... or it just crashes.
I've read that NSNotifications can be used? But how?
I'm guessing the best way is to set it all up in the GameViewController.swift and use NSNotifications to communicate with the RootViewController from the GameScene? I can't seem to find a tutorial or example though.
Use delegation when a view controller needs to change views, do not have the view itself change views, this could cause the view trying to deallocating while the new view is being presented, thus the error you are getting
Related
Hi I am writing the following code to refer a friend through SMS.
When I click on cell, the sms app opens with text but when again I tried for second time, it shows white color screen.
Here is my code
var controller1 = MFMessageComposeViewController()
extension ReferaFriendController:UICollectionViewDelegate,UICollectionViewDataSource,MFMessageComposeViewControllerDelegate
{
if indexPath.item == 0
{
if MFMessageComposeViewController.canSendText() {
let urlToShare = self.referalmodeldata[0].referralCodeOnly
controller1.body = "Hey I just gave an Awesome Assessment on App you can also try it. I scored , Try to beat my score \(String(describing: urlToShare))"
controller1.messageComposeDelegate = self
self.present(controller1, animated: true, completion: nil)
}
}
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
self.dismiss(animated: true, completion: nil)
}
}
As far as I can see, there's no need to keep a reference to the MFMessageComposeViewController. Just move it to be created at the point you need it, inside your if closure:
if MFMessageComposeViewController.canSendText() {
let controller = MFMessageComposeViewController()
// ...
}
I'm using Game Center in my app and I'm having some problem with the GKLocalPlayer.authenticated attribute. Regardless if the authentication process is successful or not, localPlayer.authenticated always returns false. This also happens if my device is already logged in to Game Center.
I get this both on actual device (iPhone 6s) and simulator (tried several).
The only information I've found about this suggests that there is a problem with the time settings but they seem to be fine.
Is this a bug or am I doing something wrong?
private static let localPlayer = GKLocalPlayer()
static func authenticateLocalPlayer() {
localPlayer.authenticateHandler = { (viewController, error) -> Void in
if let viewController = viewController {
if let rootViewController = UIApplication.sharedApplication().keyWindow?.rootViewController {
rootViewController.presentViewController(viewController, animated: true, completion: nil)
}
} else if localPlayer.authenticated {
gameCenterEnabled = true
let defaultCenter = NSNotificationCenter.defaultCenter()
defaultCenter.postNotificationName("local_player_authenticated", object: nil)
} else {
gameCenterEnabled = false
}
if let error = error {
print(error)
}
}
}
static func isAuthenticated() -> Bool {
return localPlayer.authenticated
}
My bad, looks like I made a little mistake when translating this code from Objective-C. It's supposed to be
GKLocalPlayer.localPlayer()
not
GKLocalPlayer()
I have two UIViewControllers, MainViewController and HighScoreViewController.
In the MainViewController (the initial view controller that the user sees upon using the app) I have all the methods to log into Game Center and save the high score to a leaderboard.
override func viewDidLoad() {
super.viewDidLoad()
authenticateLocalPlayer()
if totalHighScore > prevTotalHighScore {
saveHighScore("totalHighScore", score: totalHighScore)
prevTotalHighScore = totalHighScore
NSUserDefaults.standardUserDefaults().setObject(prevTotalHighScore, forKey: "prevtotalhighscore")
}
}
func authenticateLocalPlayer() {
let localPlayer: GKLocalPlayer = GKLocalPlayer.localPlayer()
localPlayer.authenticateHandler = {(ViewController, error) -> Void in
if((ViewController) != nil) {
// 1 Show login if player is not logged in
self.presentViewController(ViewController!, animated: true, completion: nil)
}
else {
print("Authentication is \(GKLocalPlayer.localPlayer().authenticated)")
}
}
}
func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController) {
gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
}
func saveHighScore(identifier: String, score: Int) {
if GKLocalPlayer.localPlayer().authenticated {
let scoreReport = GKScore(leaderboardIdentifier: identifier)
scoreReport.value = Int64(score)
let scoreArray: [GKScore] = [scoreReport]
GKScore.reportScores(scoreArray, withCompletionHandler: { (error) -> Void in
if error != nil {
print(error)
}
else {
print("Posted score of \(score)")
}
})
}
}
First of all, is this the best way to implement as such?
Secondly, my HighScoreViewController has a button that says 'LEADERBOARD' where if the user taps on, Game Center leaderboard for my game would pop up. How do I go about implementing said button? I already have a button set up and and #IBAction method linked to it, but I have no idea what code to place in it since all of the main Game Center code is placed inside the MainViewController.
Once you're authenticated with Game Center, it doesn't matter which controller retrieves the leaderboard. You say you already have a method linked to your button. So, you'll just add the retrieval code there. Apple's GKLeaderBoard reference has an example in obj-c for downloading leaderboard data:
GKLeaderboard *leaderboardRequest = [[GKLeaderboard alloc] init];
if (leaderboardRequest != nil)
{
leaderboardRequest.playerScope = GKLeaderboardPlayerScopeGlobal;
leaderboardRequest.timeScope = GKLeaderboardTimeScopeToday;
leaderboardRequest.identifier = #" ~~ your leaderboard identifier goes here ~~ "
leaderboardRequest.range = NSMakeRange(1,10);
[leaderboardRequest loadScoresWithCompletionHandler: ^(NSArray *scores, NSError *error)
{
if (error != nil)
{
// Handle the error.
}
if (scores != nil)
{
// Process the score information.
}
}];
}
I have installed the Facebook SDK, followed all the requirements, it works as I can extract data from the user's Facebook account, however I cannot perform the segue once logged in.
I get an error message:
Warning: Attempt to present <viewController1> on <FBSDKContainerViewController> whose view is not in the window hierarchy!
Here are my codes:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if (FBSDKAccessToken.currentAccessToken() == nil) {
print("not logged in")
}
else {
print("logged in")
}
let loginButton = FBSDKLoginButton()
loginButton.readPermissions = ["public_profile", "email", "user_friends"]
loginButton.center = self.view.center
loginButton.delegate = self
self.view.addSubview(loginButton)
}
func loginButton(loginButton: FBSDKLoginButton!, didCompleteWithResult result: FBSDKLoginManagerLoginResult!, error: NSError!) {
if ((error) != nil)
{
}
else if result.isCancelled {
}
else {
if result.grantedPermissions.contains("email")
{
// Do work
returnUserData()
addViewHierarchy()
// self.performSegueWithIdentifier("signUpFollowPage", sender: self)
}
}
}
Anyone can help ?
Thanks
I've been struggling with the same problem (although I'm using FBSDKLoginManager's logInWithReadPermissions).
Whilst the problem seems to be that the callback (or your didCompleteWithResult delegate call) is invoked before the Safari web view is dismissed, I can't find a nice solution.
My (horrific) answer has been to delay the segue, to give the FBSDKContainerViewController time to be dismissed naturally. In your case:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), { () -> Void in
self.performSegueWithIdentifier("signUpFollowPage", sender: self)
}
It may be possible to get away with a shorter delay.
This strikes me as a bug with the Facebook Login SDK. If I find time I might look at the SDK source. Fortunately the App I'm working on is not due for release until the new year, so I'm hoping that the issue might be fixed in a later version anyway.
You should use the viewDidAppear() function and check the FBSDKAccessToken.currentAccessToken :
override func viewDidAppear(animated:Bool){
if FBSDKAccessToken.currentAccessToken() != nil{
performSegueWithIdentifier("YOUR_SEGUE",sender:self)
}
}
I have faced the same issue while logging in with FB. Here is how I solved it. The issue is while navigating, the FBContainerView is still open. Using self.controller will refer to the FBContainerView and NOT the Topmost controller and hence no navigation. We will need to switch back to the top Most Controller and then perform the navigation.
You can do this without performing segue, by presenting view controller from top controller like this
let topController = self.topMostController()
topController.presentViewController(secondController, animated: true, completion: nil)
to get the top most controller define a method which will always return topmost controller
func topMostController()-> UIViewController {
var topController = UIApplication.sharedApplication().keyWindow?.rootViewController
while((topController?.presentedViewController) != nil){
topController = topController?.presentedViewController
}
if(topController != nil){
return topController!
}
else{
return self
}
}
My solution is similar to the ones above, but it is a bit more deterministic (and for me, safer).
This solution ensures we ARE the top most VC before presenting.
Timer.scheduledTimer(withTimeInterval: 0.01 / 30.0, repeats: true, block: { (timer) in
if let app = UIApplication.shared.delegate as? AppDelegate, let window = app.window, let rootVC = window.rootViewController {
var topController = rootVC
while let nextTop = topController.presentedViewController {
topController = nextTop;
}
if topController == self {
// we're good, go for it
self.performSegue(withIdentifier: "afterLoginSegue", sender: nil)
timer.invalidate()
} else {
log.debug("waiting to transition until we're presenting...")
}
} else {
log.error("couldn't resolve a key variable needed to segue, aborting.")
timer.invalidate()
}
})
I still feel like there should be a way to get a callback once the presenting facebook login safari VC is gone, but until that solution comes along, this is as good as i can do (obviously this code could be cleaned up and modularized for use in multiple VCs, but you can just dump this in where needed).
My game has two UIViewControllers. The first one finds a match with the matchmakerViewController(didFindMatch:) method. Once the match is found, the player is taken to the second one.
The GKVoiceChat should be active in the second one. Though, I am currently setting it up in the first UIViewController. Is this the right approach? I can't find any way to do it in the second UIViewController, as I can't create a GKMatch variable wherever I want.
To add a GKVoiceChat, I use the following code:
func matchmakerViewController(viewController: GKMatchmakerViewController!,
didFindMatch match: GKMatch!) {
println("I found a match.")
// Checking if the device has a microphone
if !GKVoiceChat.isVoIPAllowed() {
return
}
// Creation of an audio session
var audioSession:AVAudioSession = AVAudioSession.sharedInstance()
audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, error: nil)
audioSession.setActive(true, error: nil)
// Setting up a voice channel
let allChannel = match.voiceChatWithName("allChannel")
allChannel.active = true
allChannel.volume = 1.0
allChannel.start()
// Redirect the player to a new UIViewController
let storyboard = UIStoryboard(name: "Universal", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("ViewController") as UIViewController
self.dismissViewControllerAnimated(true, completion: nil)
self.presentViewController(vc, animated: true, completion: nil)
}
What works
The game successfully asks permission to use the microphone;
The code compiles just fine. No crashes or errors occur.
The game successfully authenticates the user, and redirect him/her to the second UIViewController
What doesn't
No voice is transmitted I don't hear any sounds. Why is that happening?
As mention in the comments, this looks like your allChannel object is being deallocated at the end of the method. I'm not sure what the purpose of your second view controller is, but I'd subclass UIViewController and create a property that you set and then check for, .e.g:
class VoiceViewController: UIViewController {
var voiceChat: GKVoiceChat?
override func viewDidLoad() {
super.viewDidLoad()
if let voiceChat = self.voiceChat {
// Do something with your voiceChat property
}
}
}
Then, update your method to be:
func matchmakerViewController(viewController: GKMatchmakerViewController!,
didFindMatch match: GKMatch!) {
println("I found a match.")
// Checking if the device has a microphone
if !GKVoiceChat.isVoIPAllowed() {
return
}
// Creation of an audio session
var error: NSError?
var audioSession:AVAudioSession = AVAudioSession.sharedInstance()
audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, error: &error)
if let error = error {
// Do something with the error, such as tell the user
return
}
audioSession.setActive(true, error: &error)
if let error = error {
// Do something with the error, such as tell the user
return
}
// Setting up a voice channel
let allChannel = match.voiceChatWithName("allChannel")
allChannel.active = true
allChannel.volume = 1.0
allChannel.start()
// Redirect the player to a new UIViewController
let storyboard = UIStoryboard(name: "Universal", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("ViewController") as VoiceViewController
vc.voiceChat = allChannel
self.dismissViewControllerAnimated(true, completion: nil)
self.presentViewController(vc, animated: true, completion: nil)
}
There's also a chance your issue is with your AVAudioSession, so I've also added error checking for that.
If you choose to have the voice chat managed by the view controller your original method is in, so can do:
var voiceChat: GKVoiceChat?
func matchmakerViewController(viewController: GKMatchmakerViewController!,
didFindMatch match: GKMatch!) {
println("I found a match.")
// Checking if the device has a microphone
if !GKVoiceChat.isVoIPAllowed() {
return
}
// Creation of an audio session
var error: NSError?
var audioSession:AVAudioSession = AVAudioSession.sharedInstance()
audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, error: &error)
if let error = error {
// Do something with the error, such as tell the user
return
}
audioSession.setActive(true, error: &error)
if let error = error {
// Do something with the error, such as tell the user
return
}
// Setting up a voice channel
let allChannel = match.voiceChatWithName("allChannel")
allChannel.active = true
allChannel.volume = 1.0
allChannel.start()
self.voiceChannel = allChannel
// Redirect the player to a new UIViewController
let storyboard = UIStoryboard(name: "Universal", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("ViewController") as UIViewController
self.dismissViewControllerAnimated(true, completion: nil)
self.presentViewController(vc, animated: true, completion: nil)
}