Iterating an array on an #EnvironmentObject when the application closes on macOS - macos

I have an #EnvironmentObject that serves an array to my main view. it's declared as follow:
my_app.swift
#main
struct My_AppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DataModel())
}
}
}
ContentView.swift
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var changed: Bool = false
}
final class DataModel: ObservableObject {
#AppStorage("mytestapp") public var notes: [NoteItem] = []
init() {
self.notes = self.notes.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
I call this from the different views in the ContentView.swift as:
struct AllText: View {
#EnvironmentObject private var data: DataModel
}
I added to my_app.swift th ability to detect when the user closes the app so I can perform some action.
#if os(macOS)
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationWillTerminate(_ aNotification: Notification) {
// trying to iterate on the struct within DataModel() here
print("app closing")
}
}
#endif
#main
struct My_AppApp: App {
#if os(macOS)
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DataModel())
}
}
}
And now, I'm trying to access the struct within DataModel() so I can check if each element has a changed set but no matter what I try, or how I declare the environmentObject I get a segfault, or errors such as No ObservableObject of type DataModel found. A View.environmentObjectfor DataModel may be missing as an ancestor of this view.
How can I access that DataModel and iterate thru it so I can perform an action when I close the app?

Here is possible approach - to inject data model on ContentView appear, like
#if os(macOS)
class AppDelegate: NSObject, NSApplicationDelegate {
var dataModel: DataModel? // << here
func applicationWillTerminate(_ aNotification: Notification) {
print("app closing")
// use self.dataModel? here
}
}
#endif
#main
struct My_AppApp: App {
#if os(macOS)
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif
private let dataModel = DataModel() // << here !!
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel) // << here !!
.onAppear {
#if os(macOS)
appDelegate.dataModel = self.dataModel // << here !!
#endif
}
}
}
}

Related

SwiftUI & macOS : How to detect last window being closed and show alert that app will quit

I have a SwiftUI app that I am creating. Upon the user closing the last window, I would like to prompt the user and inform them that the app will also quit.
I have taken a look at both the solutions for creating an alert upon app quiting here and have also looked at the solution for closing the application when the last window closes here.
Both of which I have gotten to work however, not together. What I am looking for is a way to detect when a user closes the last window in the application, then prompt the user with an alert letting them know it will quit the application and asking if they would like to continue or cancel.
Using .onDisappear does not seem to work. I have implemented a appDelegate and it's applicationShouldTerminateAfterLastWindowClosed method, but when the last window closes, it does not seem to prompt the .alert behavior in my application.
Application class
class Application: NSObject, NSApplicationDelegate, ObservableObject {
#Published var willTerminate = false
override init() {
super.init()
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
if NSApplication.shared.windows.count == 0 {
return .terminateNow
}
self.willTerminate = true
return .terminateLater
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
func resume() {
NSApplication.shared.reply(toApplicationShouldTerminate: false)
}
func close() {
NSApplication.shared.reply(toApplicationShouldTerminate: true)
}
}
struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
ContentView
struct ContentView: View {
#State private var window: NSWindow?
#EnvironmentObject private var appDelegate: Application
var body: some View {
ZStack {
MyView()
// ...
.onDisappear(
// Code in here does not run when WindowAccessor is set to background
})
.background(WindowAccessor(window: self.$window))
.alert(isPresented: Binding<Bool>(get: { self.appDelegate.willTerminate && self.window?.isKeyWindow ?? false }, set: { self.appDelegate.willTerminate = $0 }), content: {
SoloLogger(for: .window).coreLog(message: "ApplicationClosedEvent", level: .info)
return Alert(title: Text("Quit Application?"),
message: Text("Do you really want to quit the application?"),
primaryButton: .default(Text("Cancel"), action: {self.appDelegate.resume() }),
secondaryButton: .destructive(Text("Quit"), action: {self.appDelegate.close()}))
})
}
}
}
I've been working on something similar.
You can pick up the #AppDelegate from the environment and don't need to create a WindowAccessor.
I created a view which can be added into your content view's ZStack:
struct MacOSQuitCheckView: View {
// MARK: - PROPERTIES
#EnvironmentObject private var appDelegate: AppDelegate
// MARK: - VIEW BODY
var body: some View {
EmptyView()
.alert("App wants to quit?"), isPresented: isPresented) {
Button("Do not quit", role: .cancel, action: appDelegate.resume)
Button("Quit", action: appDelegate.close)
}
}
// MARK: - PRIVATE COMPUTED PROPERTIES
private var isPresented: Binding<Bool> {
Binding<Bool>(get: { self.appDelegate.willTerminate }, set: { self.appDelegate.willTerminate = $0 })
}
}

SwiftUI #StateObject couldn't refresh view

I can't see the change in the Text object on the ContentView page. However, when I run the same code in .onReceive with print, I can see the change. What's the problem here?
I wanted to manage the state of the game from a different place and the operation of the game from a different place. Is the logic I made wrong?
Enum
enum GameSituation {
case play
}
Game Config
class GameConfig: ObservableObject {
#Published var randomCellValue: Int = 0
#Published var timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
func startGame() {
determineRandomCell()
}
func determineRandomCell() {
randomCellValue = Int.random(in: 0...11)
}
func playSound(soundfile: String, ofType: String) {
if let path = Bundle.main.path(forResource: soundfile, ofType: ofType){
do{
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print("Error")
}
}
}
}
Game Situation
class GameSituations: ObservableObject {
#Published var gameConfig = GameConfig()
func gameSituation(gameSituation: GameSituation) {
switch gameSituation {
case .play:
gameConfig.startGame()
}
}
}
Content View
struct ContentView: View {
#StateObject var gameSituation = GameSituations()
var body: some View {
Text("\(gameSituation.gameConfig.randomCellValue)")
.padding()
.onReceive(gameSituation.gameConfig.timer, perform: { _ in
gameSituation.gameSituation(gameSituation: .play)
print("random: \(gameSituation.gameConfig.randomCellValue)")
})
}
}
You had multi issue I solved all, your first and big issue was, that you are initializing an instance of an ObservableObject but using another one! second one was that you forgot using StateObject of GameConfig in your View, as you can see in your code, you did used StateObject of GameSituations but not GameConfig, if you ask why, because it is Publisher!
My best recommendation: try just use one ObservableObject! using 2 deferent ObservableObject that has binding with together has such a issue that you can see! try organise all your code to one ObservableObject.
enum GameSituation {
case play
}
class GameConfig: ObservableObject {
static let shared: GameConfig = GameConfig() // <<: Here
#Published var randomCellValue: Int = 0
#Published var timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
func startGame() {
determineRandomCell()
}
func determineRandomCell() {
randomCellValue = Int.random(in: 0...11)
}
func playSound(soundfile: String, ofType: String) {
print(soundfile)
}
}
class GameSituations: ObservableObject {
static let shared: GameSituations = GameSituations() // <<: Here
let gameConfig = GameConfig.shared // <<: Here
func gameSituation(gameSituation: GameSituation) {
switch gameSituation {
case .play:
gameConfig.startGame()
}
}
}
struct ContentView: View {
#StateObject var gameSituation = GameSituations.shared // <<: Here
#StateObject var gameConfig = GameConfig.shared // <<: Here
var body: some View {
Text(gameConfig.randomCellValue.description) // <<: Here
.onReceive(gameConfig.timer, perform: { _ in // <<: Here
gameSituation.gameSituation(gameSituation: .play)
print("random: \(gameSituation.gameConfig.randomCellValue)")
})
}
}

SwiftUI - Trigger a Modal Sheet from an ObservableObject?

I've created a random number generator that should present a new sheet if the number 3 appears.
The logic is in a separate class but when I use .sheet or .fullScreenCover on the ContentView it doesn't work.
Is it possible to trigger a modal sheet from an ObservableObject in Xcode12 / iOS 14 SwiftUI?
Minimal reproducible example below:
import SwiftUI
struct ContentView: View {
#StateObject var mathLogic = MathLogic()
var body: some View {
VStack{
Text(String(mathLogic.newNumber))
.padding(.bottom, 40)
Text("Tap for a number")
.onTapGesture{
mathLogic.generateRandomNumber()
}
}
.fullScreenCover(isPresented: mathLogic.$isLucky3, content: NewModalView.init)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct NewModalView: View {
var body: some View {
Text("You hit lucky number 3!")
}
}
class MathLogic: ObservableObject {
#Published var newNumber = 0
#State var isLucky3 = false
func generateRandomNumber() {
newNumber = Int.random(in: 1..<5)
guard self.newNumber != 3 else {
// trigger modal
self.isLucky3.toggle()
return
}
}
}
The #State is intended to be in View, in ObservableObject we use #Published, so it should be
class MathLogic: ObservableObject {
#Published var newNumber = 0
#Published var isLucky3 = false // << here !!
// .. other code
and to bind it via ObservedObject, because .$isLucky3 gives publisher
// ... other code
}
.fullScreenCover(isPresented: $mathLogic.isLucky3, content: NewModalView.init)
Tested with Xcode 12 / iOS 14

No ObservableObject of type User found

I am trying to understand #EnvironmentObject better so I wrote sample code below to replicate the issue i am facing
This is the class where i declare the array which needs to be accessed in multiple locations and be displayed and updated in ContentView
class User: ObservableObject {
#Published var array = [String]()
func diplayName(name: String){
self.array.append(name)
}
}
I want to be able to append my array in another class. Something like the below code
class myTests: ObservableObject {
#EnvironmentObject var user:User
func diplayMyName(name: String){
self.user.array.append(name)
}
}
When I call displayMyName function in myTests class i get an Error message as below
Fatal error: No ObservableObject of type User found.
A View.environmentObject(_:) for User may be missing as an ancestor of this view.
This is how my contentView looks like
struct ContentView: View {
#EnvironmentObject var user:User
var testing = myTests()
var body: some View {
VStack {
List(user.array, id: \.self){ x in
Text(x)
}
Button(action: {
self.user.diplayName(name: "Name1")
// self.testing.diplayMyName(name: "Name2")
}){
Text("Call Function")
}
}
}
}
This is how i declare my environment object in scene delegate
let contentView = ContentView().environmentObject(User())
I would really appreciate if someone can help me understand why am i getting the error when i append the published array from myTests class. Thank you.
UPDATE
To work around my issue i did the following adjustments
I returned an array in myTests class
class myTests {
var ar = [String]()
func displayMyName() -> [String] {
ar.removeAll()
ar.append(contentsOf: ["Name2", "Name3"])
return ar
}
}
And added it to the array in ContentView
struct ContentView: View {
#EnvironmentObject var user : User
var testing = myTests()
var body: some View {
VStack {
List(user.array, id: \.self){ x in
Text(x)
}
Button(action: {
self.user.array.append(contentsOf: self.testing.displayMyName())
}){
Text("Call Function")
}
}
}
}

How to display Game Center leaderboard with SwiftUI

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")

Resources