Where to call NotificationCenter.default.addObserver() ? in my Xcode Game Project
I successfully call the following from my func application (AppDelegate), but when I toggle the Gamepad on/off, my selectors are not being called.
class GameScene: SKScene {
func ObserveForGameControllers() {
// print("ObserveForGameControllers")
NotificationCenter.default.addObserver(
self,
selector: #selector(connectControllers),
name: NSNotification.Name.GCControllerDidConnect,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(disconnectControllers),
name: NSNotification.Name.GCControllerDidDisconnect,
object: nil)
} // ObserveForGameControllers
}
My selectors look like this:
#objc func connectControllers() {}
#objc func disconnectControllers() {}
One last thing:
Here are my Gamepad settings in my Project
It seems I really need some suggestions here.
Appreciate it.
EDIT
I have been in contact with a very talented jrturton on trying to discover why I am unable to detect the presence of my Gamepad as documented above.
He has asked for a more complete presentation of my Swift code. I initially thought of Dropbox, but he has asked for this EDIT .. so here goes:
I began with a iOS Game Project which presented me with AppDelegate, GameScene, GameViewController + Storyboard.
I’ve already covered AppDelegate above, which per jrturton’s recommendation is now reduced to the standard AppDelegate func’s which essentially are empty, such as:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// currently empty
}
Next, the GameScene ..
import SwiftUI
import WebKit
import SpriteKit
import GameplayKit
import GameController
class GameScene: SKScene {
override func sceneDidLoad() {
super.sceneDidLoad()
// print("sceneDidLoad")
ObserveForGameControllers()
} // sceneDidLoad
func ObserveForGameControllers() {
// print("ObserveForGameControllers")
NotificationCenter.default.addObserver(
self,
selector: #selector(connectControllers),
name: NSNotification.Name.GCControllerDidConnect,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(disconnectControllers),
name: NSNotification.Name.GCControllerDidDisconnect,
object: nil)
} // ObserveForGameControllers
#objc func connectControllers() {
// print("CONNECT")
self.isPaused = false
var indexNumber = 0
for controller in GCController.controllers() {
if controller.extendedGamepad != nil {
controller.playerIndex = GCControllerPlayerIndex.init(rawValue: indexNumber)!
indexNumber += 1
setupControllerControls(controller: controller)
}
}
} // connectControllers
#objc func disconnectControllers() {
// print("DIS-CONNECT")
self.isPaused = true
} // disconnectControllers
func setupControllerControls(controller: GCController) {
controller.extendedGamepad?.valueChangedHandler = {
(gamepad: GCExtendedGamepad, element: GCControllerElement) in
self.controllerInputDetected(gamepad: gamepad,
element: element,
index: controller.playerIndex.rawValue)
}
} // setupControllerControls
func controllerInputDetected(gamepad: GCExtendedGamepad,
element: GCControllerElement,
index: Int) {
// A-Button
if (gamepad.buttonA == element)
{
if (gamepad.buttonA.value != 0)
{
// These print(..) statements will be replaced later
// by code to access my Javascript methods.
print("movePaddleDown")
}
}
// B-Button
else if (gamepad.buttonB == element)
{
if (gamepad.buttonB.value != 0)
{
print("movePaddleRight")
}
}
// Y-Button
else if (gamepad.buttonY == element)
{
if (gamepad.buttonY.value != 0)
{
print("movePaddleUp")
}
}
// X-Button
else if (gamepad.buttonX == element)
{
if (gamepad.buttonX.value != 0)
{
print("movePaddleLeft")
}
}
// leftShoulder
else if (gamepad.leftShoulder == element)
{
if (gamepad.leftShoulder.value != 0)
{
print("cyclePages")
}
}
// rightShoulder
else if (gamepad.rightShoulder == element)
{
if (gamepad.rightShoulder.value != 0)
{
print("newGame")
}
}
// leftTrigger
else if (gamepad.leftTrigger == element)
{
if (gamepad.leftTrigger.value != 0)
{
print("pauseGame")
}
}
// rightTrigger
else if (gamepad.rightTrigger == element)
{
if (gamepad.rightTrigger.value != 0)
{
print("resumeGame")
}
}
// Left Thumbstick
else if (gamepad.leftThumbstick == element)
{
if (gamepad.leftThumbstick.xAxis.value > 0)
{
print("movePaddleRight")
}
else if (gamepad.leftThumbstick.xAxis.value < 0)
{
print("movePaddleLeft")
}
else if (gamepad.leftThumbstick.xAxis.value == 0)
{
print("decreaseSpeed")
}
else if (gamepad.leftThumbstick.yAxis.value > 0)
{
print("movePaddleDown")
}
else if (gamepad.leftThumbstick.yAxis.value < 0)
{
print("movePaddleUp")
}
else if (gamepad.leftThumbstick.yAxis.value == 0)
{
print("decreaseSpeed")
}
}
// Right Thumbstick
if (gamepad.rightThumbstick == element)
{
if (gamepad.rightThumbstick.xAxis.value > 0)
{
print("movePaddleRight")
}
else if (gamepad.rightThumbstick.xAxis.value < 0)
{
print("movePaddleLeft")
}
else if (gamepad.rightThumbstick.xAxis.value == 0)
{
print("decreaseSpeed")
}
else if (gamepad.rightThumbstick.yAxis.value > 0)
{
print("movePaddleDown")
}
else if (gamepad.rightThumbstick.yAxis.value < 0)
{
print("movePaddleUp")
}
else if (gamepad.rightThumbstick.yAxis.value == 0)
{
print("decreaseSpeed")
}
}
// D-Pad
else if (gamepad.dpad == element)
{
if (gamepad.dpad.xAxis.value > 0)
{
print("scrollWindowRight")
}
else if (gamepad.dpad.xAxis.value < 0)
{
print("scrollWindowLeft")
}
else if (gamepad.dpad.yAxis.value > 0)
{
print("scrollWindowDown")
}
else if (gamepad.dpad.yAxis.value < 0)
{
print("scrollWindowUp")
}
}
} // controllerInputDetected
} // class GameScene: SKScene
Now, the GameViewController ..
import UIKit
import SpriteKit
import GameplayKit
import WebKit
// This is now available across Classes
var theWebView: WKWebView!
class GameViewController: UIViewController, WKNavigationDelegate {
override func loadView() {
// print("loadView")
let webConfiguration = WKWebViewConfiguration()
theWebView = WKWebView(frame: .zero, configuration: webConfiguration)
theWebView.navigationDelegate = self
view = theWebView
} // loadView
override func viewDidLoad() {
super.viewDidLoad()
// print("viewDidLoad")
loadURL(webAddress: "https://www.lovesongforever.com/firstgame")
} // viewDidLoad
func loadURL(webAddress: String) {
let theURL = URL(string: webAddress)
let theRequest = URLRequest(url: theURL!)
theWebView.load(theRequest)
theWebView.allowsBackForwardNavigationGestures = false
} // loadURL
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
}
else {
return .all
}
} // supportedInterfaceOrientations
override var prefersStatusBarHidden: Bool {
return true
} // prefersStatusBarHidden
} // class GameViewController
Note that when I RUN my iOS App, thanks to the overridden loadView() above, it presents the following in the Simulator:
Simulator presentation
But, that’s is as far as it goes, because pressing all the buttons on my Gamepad does not result in detection of my Gamepad, as evidenced when I UN-comment all the above print(..) statements. In particular, those within:
#objc func connectControllers() and
#objc func disconnectControllers() and
func controllerInputDetected( .. )
So, hopefully that is all there currently is ..
You are doing this in your app delegate:
let itsGameScene = GameScene()
itsGameScene.ObserveForGameControllers()
You're creating an instance of GameScene, making it listen for notifications (adding self as the observer)... and then probably throwing that instance away.
Your game isn't actually using SpriteKit, and SpriteKit isn't needed to deal with game controllers, you just need GameController.
You have no need to create this scene at all. The best place to observe the game controller notifications would be in your GameViewController in viewDidLoad. Move the code (the observation method and the controller-related ones) you had in your scene into the view controller, and delete the scene file.
Related
I'm adding a custom delegate to my app and, for some reason, it is not working.
My app has a map where I show several markers of different company types. There is also a button that, once pressed, takes me to another viewController where the user can input some filters. The user then presses "Apply" which would pass the filtering data to the map viewController.
The issue here is that no data is being passed.
As reference I followed the guideline https://medium.com/#jamesrochabrun/implementing-delegates-in-swift-step-by-step-d3211cbac3ef which works perfectly fine.
Here is the full project code https://github.com/afernandes0001/Custom-Delegate
I use Firebase but code below just shows pieces related to the delegate.
mapViewController - you will notice that I added a print to the prepareForSegue. When first loading the app and clicking "Search" button it shows nav1 as nil (which is expected) but, if I click Search and Apply (in filterVC), that print is never done.
import UIKit
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, FilterVCDelegate {
#IBOutlet weak var map: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
map.register(MyAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "clinicDetailsSegue" {
let clinicsDetailsViewController = segue.destination as! ClinicsDetailsViewController
clinicsDetailsViewController.id = self.note.mapId
} else if segue.identifier == "searchSegue" {
print("segue call")
let nav1 = segue.destination as? UINavigationController
print("nav1 \(nav1)")
if let nav = segue.destination as? UINavigationController, let filterVC = nav.topViewController as? FilterViewController {
filterVC.delegate = self
}
}
}
func chosenData(clinicNameFilter: String, stateFilter: String, cityFilter: String, esp1Filter: String, esp2Filter: String) {
print("Received data \(clinicNameFilter), \(stateFilter), \(cityFilter), \(esp1Filter), \(esp2Filter)")
}
}
FilterViewController
import UIKit
protocol FilterVCDelegate: class {
func chosenData(clinicNameFilter: String, stateFilter: String, cityFilter: String, esp1Filter: String, esp2Filter: String)
}
class FilterViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
weak var delegate: FilterVCDelegate?
var selectedName = ""
var statesJSON = [Estado]()
var cities = [Cidade]()
var state : Estate? // Selected State identifier
var city : City? // Selected City identifier
var selectedState = "" // Used to retrieve info from Firebase
var selectedCity = "" // Used to retrieve info from Firebase
var specialtiesJSON = [Specialty]()
var specialties2 = [Specialty2]()
var specialty1 : Specialty? // Selected Specialty1 identifier
var specialty2 : Specialty2? // Selected Specialty2 identifier
var selectedSpecialty1 = ""
var selectedSpecialty2 = ""
#IBOutlet weak var clinicName: UITextField!
#IBOutlet weak var statePicker: UIPickerView!
#IBOutlet weak var esp1Picker: UIPickerView!
#IBOutlet weak var esp2Picker: UIPickerView!
override func viewDidLoad() {
readJsonStates()
readJsonSpecialties()
super.viewDidLoad()
clinicName.text = ""
}
#IBAction func applyFilter(_ sender: Any) {
if clinicName.text == nil {
clinicName.text = ""
}
if selectedState != "" {
if selectedCity != "" {
if selectedSpecialty1 != ""{
if selectedSpecialty2 != "" {
delegate?.chosenData(clinicNameFilter: clinicName.text!, stateFilter: selectedState, cityFilter: selectedCity, esp1Filter: selectedSpecialty1, esp2Filter: selectedSpecialty2)
let viewControllers: [UIViewController] = self.navigationController!.viewControllers as [UIViewController]
self.navigationController?.popToViewController(viewControllers[viewControllers.count - 2], animated: true)
} else {
print("Fill in all filter data")
}
} else {
print("Fill in all filter data")
}
} else {
print("Fill in all filter data")
}
} else {
print("Fill in all filter data")
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
esp1Picker.reloadComponent(0)
esp2Picker.reloadComponent(0)
statePicker.reloadAllComponents()
if pickerView == statePicker {
if component == 0 {
self.state = self.statesJSON[row]
self.coties = self.statesJSON[row].cities
statePicker.reloadComponent(1)
statePicker.selectRow(0, inComponent: 1, animated: true)
} else {
self.city = self.cities[row]
statePicker.reloadAllComponents()
}
} else if pickerView == esp1Picker {
self.specialty1 = self.specialtiesJSON[row]
self.specialties2 = self.specialtiesJSON[row].specialty2
esp1Picker.reloadComponent(0)
esp2Picker.reloadComponent(0)
esp2Picker.selectRow(0, inComponent: 0, animated: true)
} else if pickerView == esp2Picker {
self.specialty2 = self.specialties2[row]
esp1Picker.reloadComponent(0)
esp2Picker.reloadComponent(0)
}
let indexSelectedState = statePicker.selectedRow(inComponent: 0)
let indexSelectedCity = statePicker.selectedRow(inComponent: 1)
let indexSelectedEsp1 = esp1Picker.selectedRow(inComponent: 0)
let indexSelectedEsp2 = esp2Picker.selectedRow(inComponent: 0)
if indexSelectedState >= 0 {
if indexSelectedCity >= 0 {
selectedState = estadosJSON[indexSelectedState].name
selectedCity = cidades[indexSelectedCity].name
}
}
if indexSelectedEsp1 >= 0 {
if indexSelectedEsp2 >= 0 {
selectedSpecialty1 = specialtiesJSON[indexSelectedEsp1].name
selectedSpecialty2 = specialtiesJSON[indexSelectedEsp1].specialty2[indexSelectedEsp2].name
}
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
if pickerView == statePicker {
return 2
} else if pickerView == esp1Picker {
return 1
} else if pickerView == esp2Picker {
return 1
}
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if pickerView == statePicker {
if component == 0 {
return statesJSON.count
} else {
return cities.count
}
} else if pickerView == esp1Picker {
return self.specialtiesJSON.count
} else if pickerView == esp2Picker {
return specialties2.count
}
return 1
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
var rowTitle = ""
let pickerLabel = UILabel()
pickerLabel.textColor = UIColor.black
if pickerView == statePicker {
if component == 0 {
rowTitle = statesJSON[row].name
} else {
rowTitle = cities[row].name
}
} else if pickerView == esp1Picker {
rowTitle = specialtiesJSON[row].name
} else if pickerView == esp2Picker {
rowTitle = specialties2[row].name
}
pickerLabel.text = rowTitle
pickerLabel.font = UIFont(name: fontName, size: 16.0)
pickerLabel.textAlignment = .center
return pickerLabel
}
func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
if pickerView == statePicker {
if component == 0 {
return 50
} else {
return 300
}
}
return 300
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}
func readJsonStates() {
let url = Bundle.main.url(forResource: "StatesAndCities", withExtension: "json")!
do {
let data = try Data(contentsOf: url)
let jsonResult = try JSONDecoder().decode(RootState.self, from: data)
//handles the array of countries on your json file.
self.statesJSON = jsonResult.state
self.cities = self.statesJSON.first!.cities
} catch {
}
}
func readJsonSpecialties() {
let url = Bundle.main.url(forResource: "Specialties", withExtension: "json")!
do {
let data = try Data(contentsOf: url)
let jsonResult = try JSONDecoder().decode(RootEsp.self, from: data)
//handles the array of specialties on your json file.
self.specialtiesJSON = jsonResult.specialty
self.specialties2 = self.specialtiesJSON.first!.specialty2
} catch {
}
}
}
Any idea why, when I click ApplyFilter, delegate is not updated in the MapViewController?
Thanks
I found the error in my project.
The issue was with my Navigation Controller.
When I posted the error above, my Storyboard looked like the below
To make it work, I added the Navigation Controller to the Filter View Controller as below
That did the work and protocol is working as expected.
I created a tester app to test adding a GameCenter leaderboard to a simple SwiftUI game I am creating. I have been unable to figure out how to display the Game Center leaderboard with all the scores.
I have created a class containing all the Game Center functions (authentication and adding score to the leaderboard. This is called from the main ContentView view. I can't figure out how to make it show the leaderboard (or even the gamecenter login screen if the player isn't already logged in.)
This is my GameCenterManager class:
class GameCenterManager {
var gcEnabled = Bool() // Check if the user has Game Center enabled
var gcDefaultLeaderBoard = String() // Check the default leaderboardID
var score = 0
let LEADERBOARD_ID = "grp.colorMatcherLeaderBoard_1" //Leaderboard ID from Itunes Connect
// MARK: - AUTHENTICATE LOCAL PLAYER
func authenticateLocalPlayer() {
let localPlayer: GKLocalPlayer = GKLocalPlayer.local
localPlayer.authenticateHandler = {(ViewController, error) -> Void in
if((ViewController) != nil) {
print("User is not logged into game center")
} else if (localPlayer.isAuthenticated) {
// 2. Player is already authenticated & logged in, load game center
self.gcEnabled = true
// Get the default leaderboard ID
localPlayer.loadDefaultLeaderboardIdentifier(completionHandler: { (leaderboardIdentifer, error) in
if error != nil { print(error ?? "error1")
} else { self.gcDefaultLeaderBoard = leaderboardIdentifer! }
})
print("Adding GameCenter user was a success")
} else {
// 3. Game center is not enabled on the users device
self.gcEnabled = false
print("Local player could not be authenticated!")
print(error ?? "error2")
}
}
} //authenticateLocalPlayer()
func submitScoreToGC(_ score: Int){
let bestScoreInt = GKScore(leaderboardIdentifier: LEADERBOARD_ID)
bestScoreInt.value = Int64(score)
GKScore.report([bestScoreInt]) { (error) in
if error != nil {
print(error!.localizedDescription)
} else {
print("Best Score submitted to your Leaderboard!")
}
}
}//submitScoreToGc()
}
and here is the ContentView struct:
struct ContentView: View {
//GameCenter
init() {
self.gameCenter = GameCenterManager()
self.gameCenter.authenticateLocalPlayer()
}
#State var score = 0
var gcEnabled = Bool() //Checks if the user had enabled GameCenter
var gcDefaultLeaderboard = String() //Checks the default leaderboard ID
let gameCenter: GameCenterManager
/*End GameCenter Variables */
var body: some View {
HStack {
Text("Hello, world!")
Button(action: {
self.score += 1
print("Score increased by 10. It is now \(self.score)")
self.gameCenter.submitScoreToGC(self.score)
}) {
Text("Increase Score")
}
}
}
}
Would greatly appreciate any help in fixing the problem.
I have a fix.
I use Game Center successfully in my SwiftUI App Sound Matcher. Code snippets to follow.
The code doesn't exactly follow the SwiftUI declarative philosophy but it works perfectly. I added snippets to SceneDelegate and ContentView plus used I used a GameKitHelper class similar to the one Thomas created for his test app. I based my version on code I found on raywenderlich.com.
I actually tried using a struct conforming to UIViewControllerRepresentable as my first attempt, following the same line of thought as bg2b, however it kept complaining that the game centre view controller needed to be presented modally. Eventually I gave up and tried my current more successful approach.
For SwiftUI 1.0 and iOS 13
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let contentView = ContentView()
.environmentObject(GameKitHelper.sharedInstance) // publish enabled state
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
// new code to create listeners for the messages
// you will be sending later
PopupControllerMessage.PresentAuthentication
.addHandlerForNotification(
self,
handler: #selector(SceneDelegate
.showAuthenticationViewController))
PopupControllerMessage.GameCenter
.addHandlerForNotification(
self,
handler: #selector(SceneDelegate
.showGameCenterViewController))
// now we are back to the standard template
// generated when your project was created
self.window = window
window.makeKeyAndVisible()
}
}
// pop's up the leaderboard and achievement screen
#objc func showGameCenterViewController() {
if let gameCenterViewController =
GameKitHelper.sharedInstance.gameCenterViewController {
self.window?.rootViewController?.present(
gameCenterViewController,
animated: true,
completion: nil)
}
}
// pop's up the authentication screen
#objc func showAuthenticationViewController() {
if let authenticationViewController =
GameKitHelper.sharedInstance.authenticationViewController {
self.window?.rootViewController?.present(
authenticationViewController, animated: true)
{ GameKitHelper.sharedInstance.enabled =
GameKitHelper.sharedInstance.gameCenterEnabled }
}
}
}
// content you want your app to display goes here
struct ContentView: View {
#EnvironmentObject var gameCenter : GameKitHelper
#State private var isShowingGameCenter = false { didSet {
PopupControllerMessage
.GameCenter
.postNotification() }}
var body: some View {
VStack {
if self.gameCenter.enabled
{
Button(action:{ self.isShowingGameCenter.toggle()})
{ Text(
"Press to show leaderboards and achievements")}
}
// The authentication popup will appear when you first enter
// the view
}.onAppear() {GameKitHelper.sharedInstance
.authenticateLocalPlayer()}
}
}
import GameKit
import UIKit
// Messages sent using the Notification Center to trigger
// Game Center's Popup screen
public enum PopupControllerMessage : String
{
case PresentAuthentication = "PresentAuthenticationViewController"
case GameCenter = "GameCenterViewController"
}
extension PopupControllerMessage
{
public func postNotification() {
NotificationCenter.default.post(
name: Notification.Name(rawValue: self.rawValue),
object: self)
}
public func addHandlerForNotification(_ observer: Any,
handler: Selector) {
NotificationCenter.default .
addObserver(observer, selector: handler, name:
NSNotification.Name(rawValue: self.rawValue), object: nil)
}
}
// based on code from raywenderlich.com
// helper class to make interacting with the Game Center easier
open class GameKitHelper: NSObject, ObservableObject, GKGameCenterControllerDelegate {
public var authenticationViewController: UIViewController?
public var lastError: Error?
private static let _singleton = GameKitHelper()
public class var sharedInstance: GameKitHelper {
return GameKitHelper._singleton
}
private override init() {
super.init()
}
#Published public var enabled :Bool = false
public var gameCenterEnabled : Bool {
return GKLocalPlayer.local.isAuthenticated }
public func authenticateLocalPlayer () {
let localPlayer = GKLocalPlayer.local
localPlayer.authenticateHandler = {(viewController, error) in
self.lastError = error as NSError?
self.enabled = GKLocalPlayer.local.isAuthenticated
if viewController != nil {
self.authenticationViewController = viewController
PopupControllerMessage
.PresentAuthentication
.postNotification()
}
}
}
public var gameCenterViewController : GKGameCenterViewController? { get {
guard gameCenterEnabled else {
print("Local player is not authenticated")
return nil }
let gameCenterViewController = GKGameCenterViewController()
gameCenterViewController.gameCenterDelegate = self
gameCenterViewController.viewState = .achievements
return gameCenterViewController
}}
open func gameCenterViewControllerDidFinish(_
gameCenterViewController: GKGameCenterViewController) {
gameCenterViewController.dismiss(
animated: true, completion: nil)
}
}
Update: For SwiftUI 2.0 and iOS 14 the code is lot easier
import GameKit
enum Authenticate
{
static func user() {
let localPlayer = GKLocalPlayer.local
localPlayer.authenticateHandler = { _, error in
guard error == nil else {
print(error?.localizedDescription ?? "")
return
}
GKAccessPoint.shared.location = .topLeading
GKAccessPoint.shared.isActive =
localPlayer.isAuthenticated
}
}
}
import SwiftUI
// content you want your app to display goes here
struct ContentView: View {
var body: some View {
Text( "Start Game")
// The authentication popup will appear when you first enter
// the view
}.onAppear() { Authenticate.user()}
}
}
EDIT 2023: as mentioned in comments, GKScore is now deprecated. I don't have an updated solution to present.
Partial answer for you here. I'm able to download leaderboard scores and display them in a SwiftUI list provided the device (or simulator) is logged into iCloud and has GameCenter already enabled in settings. I have not attempted to make a gameCenter authentication view controller appear if that is not the case.
Thank you for the code in your question. I used your GameCenterManager() but put it in my AppDelegate:
let gameCenter = GameCenterManager()
Below is my ShowRankings.swift SwiftUI View. I'm able to successfully authenticate and get the scores. But I still have "anomalies". The first time I run this (in simulator) I get the expected "User is not logged into Game Center" error indicating the ViewController in your GameCenterManager is not nil (I never even attempt to display it). But then I'm able to successfully get the scores and display them in a list.
import SwiftUI
import GameKit
struct ShowRankings: View {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let leaderBoard = GKLeaderboard()
#State var scores: [GKScore] = []
var body: some View {
VStack {
Button(action: {
self.updateLeader()
}) {
Text("Refresh leaderboard")
}
List(scores, id: \.self) { score in
Text("\(score.player.alias) \(score.value)")
}
}.onAppear() {
self.appDelegate.gameCenter.authenticateLocalPlayer()
self.updateLeader()
}
}
func updateLeader() {
let leaderBoard: GKLeaderboard = GKLeaderboard()
leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
leaderBoard.timeScope = .allTime
leaderBoard.loadScores { (scores, error) in
if let error = error {
debugPrint("leaderboard loadScores error \(error)")
} else {
guard let scores = scores else { return }
self.scores = scores
}
}
}
}
An alternative solution is to create a UIViewControllerRepresentable for GameCenter which takes a leaderboard ID to open. This makes it simple to open a specific leader board.
public struct GameCenterView: UIViewControllerRepresentable {
let viewController: GKGameCenterViewController
public init(leaderboardID : String?) {
if leaderboardID != nil {
self.viewController = GKGameCenterViewController(leaderboardID: leaderboardID!, playerScope: GKLeaderboard.PlayerScope.global, timeScope: GKLeaderboard.TimeScope.allTime)
}
else{
self.viewController = GKGameCenterViewController(state: GKGameCenterViewControllerState.leaderboards)
}
}
public func makeUIViewController(context: Context) -> GKGameCenterViewController {
let gkVC = viewController
gkVC.gameCenterDelegate = context.coordinator
return gkVC
}
public func updateUIViewController(_ uiViewController: GKGameCenterViewController, context: Context) {
return
}
public func makeCoordinator() -> GKCoordinator {
return GKCoordinator(self)
}
}
public class GKCoordinator: NSObject, GKGameCenterControllerDelegate {
var view: GameCenterView
init(_ gkView: GameCenterView) {
self.view = gkView
}
public func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
gameCenterViewController.dismiss(animated: true, completion: nil)
}
}
To use just add the below wherever it is needed to display a leaderboard.
GameCenterView(leaderboardID: "leaderBoardID")
i have added a page view controller and a 3 pages.
I set Transition Style to Scroll and implemented the presentationCount and the presentationIndex method. now i get a black bar with gray/white dots at the bottom of my view. However, but i want the view to goright to the bottom and the make the dots appear over it(without the black background.
How do i do that?
here is my code:
import UIKit
class FilterViewController: UIPageViewController, UIPageViewControllerDataSource {
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
if let firstViewController = orderedViewControllers.first {
setViewControllers([firstViewController],
direction: .forward,
animated: true,
completion: nil)
}
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
private(set) lazy var orderedViewControllers: [UIViewController] = {
return [self.newViewController(category: "first"),
self.newViewController(category: "second"),
self.newViewController(category: "third")]
}()
private func newViewController(category: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil) .
instantiateViewController(withIdentifier: "\(category)ViewController")
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return orderedViewControllers.last
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let orderedViewControllersCount = orderedViewControllers.count
guard orderedViewControllersCount != nextIndex else {
return orderedViewControllers.first
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return orderedViewControllers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard let firstViewController = viewControllers?.first,
let firstViewControllerIndex = orderedViewControllers.index(of: firstViewController) else {
return 0
}
return firstViewControllerIndex
}
}
ok, i got it myself. If anyone has the same problem, just override the viewDidLayoutSubviews() of the Page View Controller:
override func viewDidLayoutSubviews() {
let v = self.view
let subviews = v?.subviews
if subviews?.count == 2 {
var sv:UIScrollView?
var pc:UIPageControl?
for t in subviews! {
if t is UIScrollView {
sv = t as! UIScrollView
} else {
pc = t as! UIPageControl
}
}
if(sv != nil && pc != nil) {
sv?.frame = (v?.bounds)!
v?.bringSubview(toFront: pc!)
}
}
super.viewDidLayoutSubviews()
}
Is it possible to change the order of views in NSStackView by dragging the subviews, just like we do it in NSTableView ?
Here's an implementation of an NSStackView subclass whose contents can be reordered via dragging:
//
// DraggingStackView.swift
// Analysis
//
// Created by Mark Onyschuk on 2017-02-02.
// Copyright © 2017 Mark Onyschuk. All rights reserved.
//
import Cocoa
class DraggingStackView: NSStackView {
var isEnabled = true
// MARK: -
// MARK: Update Function
var update: (NSStackView, Array<NSView>)->Void = { stack, views in
stack.views.forEach {
stack.removeView($0)
}
views.forEach {
stack.addView($0, in: .leading)
switch stack.orientation {
case .horizontal:
$0.topAnchor.constraint(equalTo: stack.topAnchor).isActive = true
$0.bottomAnchor.constraint(equalTo: stack.bottomAnchor).isActive = true
case .vertical:
$0.leadingAnchor.constraint(equalTo: stack.leadingAnchor).isActive = true
$0.trailingAnchor.constraint(equalTo: stack.trailingAnchor).isActive = true
}
}
}
// MARK: -
// MARK: Event Handling
override func mouseDragged(with event: NSEvent) {
if isEnabled {
let location = convert(event.locationInWindow, from: nil)
if let dragged = views.first(where: { $0.hitTest(location) != nil }) {
reorder(view: dragged, event: event)
}
} else {
super.mouseDragged(with: event)
}
}
private func reorder(view: NSView, event: NSEvent) {
guard let layer = self.layer else { return }
guard let cached = try? self.cacheViews() else { return }
let container = CALayer()
container.frame = layer.bounds
container.zPosition = 1
container.backgroundColor = NSColor.underPageBackgroundColor.cgColor
cached
.filter { $0.view !== view }
.forEach { container.addSublayer($0) }
layer.addSublayer(container)
defer { container.removeFromSuperlayer() }
let dragged = cached.first(where: { $0.view === view })!
dragged.zPosition = 2
layer.addSublayer(dragged)
defer { dragged.removeFromSuperlayer() }
let d0 = view.frame.origin
let p0 = convert(event.locationInWindow, from: nil)
window!.trackEvents(matching: [.leftMouseDragged, .leftMouseUp], timeout: 1e6, mode: .eventTrackingRunLoopMode) { event, stop in
if event.type == .leftMouseDragged {
let p1 = self.convert(event.locationInWindow, from: nil)
let dx = (self.orientation == .horizontal) ? p1.x - p0.x : 0
let dy = (self.orientation == .vertical) ? p1.y - p0.y : 0
CATransaction.begin()
CATransaction.setDisableActions(true)
dragged.frame.origin.x = d0.x + dx
dragged.frame.origin.y = d0.y + dy
CATransaction.commit()
let reordered = self.views.map {
(view: $0,
position: $0 !== view
? NSPoint(x: $0.frame.midX, y: $0.frame.midY)
: NSPoint(x: dragged.frame.midX, y: dragged.frame.midY))
}
.sorted {
switch self.orientation {
case .vertical: return $0.position.y < $1.position.y
case .horizontal: return $0.position.x < $1.position.x
}
}
.map { $0.view }
let nextIndex = reordered.index(of: view)!
let prevIndex = self.views.index(of: view)!
if nextIndex != prevIndex {
self.update(self, reordered)
self.layoutSubtreeIfNeeded()
CATransaction.begin()
CATransaction.setAnimationDuration(0.15)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut))
for layer in cached {
layer.position = NSPoint(x: layer.view.frame.midX, y: layer.view.frame.midY)
}
CATransaction.commit()
}
} else {
view.mouseUp(with: event)
stop.pointee = true
}
}
}
// MARK: -
// MARK: View Caching
private class CachedViewLayer: CALayer {
let view: NSView!
enum CacheError: Error {
case bitmapCreationFailed
}
override init(layer: Any) {
self.view = (layer as! CachedViewLayer).view
super.init(layer: layer)
}
init(view: NSView) throws {
self.view = view
super.init()
guard let bitmap = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { throw CacheError.bitmapCreationFailed }
view.cacheDisplay(in: view.bounds, to: bitmap)
frame = view.frame
contents = bitmap.cgImage
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private func cacheViews() throws -> [CachedViewLayer] {
return try views.map { try cacheView(view: $0) }
}
private func cacheView(view: NSView) throws -> CachedViewLayer {
return try CachedViewLayer(view: view)
}
}
The code requires your stack to be layer backed, and uses sublayers to simulate and animate its content views during drag handling. Dragging is detected by an override of mouseDragged(with:) so will not be initiated if the stack's contents consume this event.
There's no built-in support for re-ordering NSStackView subviews.
I implemented quick look in my project in the following way in Swift 2 (I'm including this here for reference and because it might help someone else set it up).
My NSViewController contains a NSTableView subclass where I implemented keyDown to listen to the spacebar key being pressed (maybe not the best way but it works):
override func keyDown(theEvent: NSEvent) {
let s = theEvent.charactersIgnoringModifiers!
let s1 = s.unicodeScalars
let s2 = s1[s1.startIndex].value
let s3 = Int(s2)
if s3 == Int(" ".utf16.first!) {
NSNotificationCenter.defaultCenter().postNotification(NSNotification(name: "MyTableViewSpacebar", object: nil))
return
}
super.keyDown(theEvent)
}
In my view controller, I have an observer for this notification and the functions required by the QLPreviewPanel:
//...
class ViewController: NSViewController {
#IBOutlet weak var myTableView: MyTableView!
var files = [FilesListData]() //array of custom class
//...
override func viewDidLoad() {
//...
NSNotificationCenter.defaultCenter().addObserver(self, selector: "spaceBarKeyDown:", name: "MyTableViewSpacebar", object: nil)
}
func spaceBarKeyDown(notification: NSNotification) {
if let panel = QLPreviewPanel.sharedPreviewPanel() {
panel.makeKeyAndOrderFront(self)
}
}
override func acceptsPreviewPanelControl(panel: QLPreviewPanel!) -> Bool {
return true
}
override func beginPreviewPanelControl(panel: QLPreviewPanel!) {
panel.delegate = self
panel.dataSource = self
}
override func endPreviewPanelControl(panel: QLPreviewPanel!) {
}
}
extension ViewController: QLPreviewPanelDataSource {
func numberOfPreviewItemsInPreviewPanel(panel: QLPreviewPanel!) -> Int {
return self.myTableView.selectedRowIndexes.count
}
func previewPanel(panel: QLPreviewPanel!, previewItemAtIndex index: Int) -> QLPreviewItem! {
if self.myTableView.selectedRow != -1 {
var items = [QLPreviewItem]()
let manager = NSFileManager.defaultManager()
for i in self.myTableView.selectedRowIndexes {
let path = self.files[i].path //path to a MP3 file
if manager.fileExistsAtPath(path) {
items.append(NSURL(fileURLWithPath: path))
} else {
items.append(qm_url) //image of a question mark used as placeholder
}
}
return items[index]
} else {
return qm_url //image of a question mark used as placeholder
}
}
}
What I would like to do now is listen to the keys "up arrow" and "down arrow" being pressed while the quick look panel is open, in order to change the selected row in the NSTableView, much like Finder behaves when you preview files with quick look. I have no clue as to how I could implement this. Any ideas?
Thanks.
Finally found what I was looking for and it's actually pretty simple.
Since my main view controller is also my delegate for the QLPreviewPanel, I added this:
extension ViewController: QLPreviewPanelDelegate {
func previewPanel(panel: QLPreviewPanel!, handleEvent event: NSEvent!) -> Bool {
let kc = event.keyCode
if (kc == 126 || kc == 125) { //up and down arrows
if event.type == NSEventType.KeyDown {
self.myTableView.keyDown(event) //send the event to the table
} else if event.type == NSEventType.KeyUp {
self.myTableView.keyUp(event)
}
return true
}
return false
}
}
Then in my table view delegate:
func tableViewSelectionDidChange(notification: NSNotification) {
guard myTableView.numberOfSelectedRows > 0 else {
if let panel = QLPreviewPanel.sharedPreviewPanel() {
if panel.visible {
panel.close()
}
}
return
}
if let panel = QLPreviewPanel.sharedPreviewPanel() {
if panel.visible {
panel.reloadData()
}
}
}
That's it! The QLPreviewPanelDataSource handles the rest.