How to get value on original object with EnvironmentObject in swiftUI - ios

class GameSettings: ObservableObject {
#Published var score = 0
#Published var score1:Int? = 0
}
struct ScoreView: View {
#EnvironmentObject var settings: GameSettings
var body: some View {
NavigationView {
NavigationLink(destination: ContentView3()) {
Text("Score: \(settings.score)")
}
}
}
}
struct ContentView3: View {
#StateObject var settings = GameSettings()
#EnvironmentObject var settings111: GameSettings
var body: some View {
NavigationView {
VStack {
// A button that writes to the environment settings
Text("Current Score--->\(settings.score))")
Text(settings111.score1 == nil ? "nil" : "\(settings111.score1!)")
Button("Increase Score") {
settings.score += 1
}
NavigationLink(destination: ScoreView()) {
Text("Show Detail View")
}
}
.frame(height: 200)
}
.environmentObject(settings)
}
}
So here When user has performed some changes in ContentView3 & from navigation route if user lands to same screen i.e. ContentView3 so how can I get GameSettings object latest value on it ? I tried creating #EnvironmentObject var settings111: GameSettings but crashes.

Did you add .environmentObject() to your YourApp.swift as well?
If not, you have to add it like this
Life Cycle: SwiftUI
#main
struct YourApp: App {
var settings: GameSettings = GameSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
}
}
}
Life Cycle: UIKit
In SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
var settings: GameSettings = GameSettings() // added line
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settings))) // added ".environmentObject(settings)" after contentView
self.window = window
window.makeKeyAndVisible()
}
}
Preview
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(GameSettings())
}
}

Related

How to auto-move/auto-adjust the save button for a note-taking app in swift?

This is what the app looks like when first opened:
I am content with this as the first screen the user sees.
The problem arises when the user hits the plus button to create a new note. The user touches the screen to add a new note and the keyboard covers up the save button at the bottom right corner. Is there any way to to have button appear above the keyboard on the top right side?
This is what it looks like:
I simply want the plus button above the keyboard on the right side so that the user can save the note. How would this be done?
Please help. Thank you!
Here is the entire code:
AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
SceneDelegate.swift:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = Host(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
ContentView.swift:
import SwiftUI
import Firebase
struct ContentView: View {
var body: some View {
Home()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Home : View {
#ObservedObject var Notes = getNotes()
#State var show = false
#State var txt = ""
#State var docID = ""
#State var remove = false
var body : some View{
ZStack(alignment: .bottomTrailing) {
VStack(spacing: 0){
HStack{
Text("Notes").font(.title).foregroundColor(.white)
Spacer()
Button(action: {
self.remove.toggle()
}) {
Image(systemName: self.remove ? "xmark.circle" : "trash").resizable().frame(width: 23, height: 23).foregroundColor(.white)
}
}.padding()
.padding(.top,UIApplication.shared.windows.first?.safeAreaInsets.top)
.background(Color.red)
if self.Notes.data.isEmpty{
if self.Notes.noData{
Spacer()
Text("No Notes...")
Spacer()
}
else{
Spacer()
//Data is Loading ....
Indicator()
Spacer()
}
}
else{
ScrollView(.vertical, showsIndicators: false) {
VStack{
ForEach(self.Notes.data){i in
HStack(spacing: 15){
Button(action: {
self.docID = i.id
self.txt = i.note
self.show.toggle()
}) {
VStack(alignment: .leading, spacing: 12){
Text(i.date)
Text(i.note).lineLimit(1)
Divider()
}.padding(10)
.foregroundColor(.black)
}
if self.remove{
Button(action: {
let db = Firestore.firestore()
db.collection("notes").document(i.id).delete()
}) {
Image(systemName: "minus.circle.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.red)
}
}
}.padding(.horizontal)
}
}
}
}
}.edgesIgnoringSafeArea(.top)
Button(action: {
self.txt = ""
self.docID = ""
self.show.toggle()
}) {
Image(systemName: "plus").resizable().frame(width: 18, height: 18).foregroundColor(.white)
}.padding()
.background(Color.red)
.clipShape(Circle())
.padding()
}
.sheet(isPresented: self.$show) {
EditView(txt: self.$txt, docID: self.$docID, show: self.$show)
}
.animation(.default)
}
}
class Host : UIHostingController<ContentView>{
override var preferredStatusBarStyle: UIStatusBarStyle{
return .lightContent
}
}
class getNotes : ObservableObject{
#Published var data = [Note]()
#Published var noData = false
init() {
let db = Firestore.firestore()
db.collection("notes").order(by: "date", descending: false).addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
self.noData = true
return
}
if (snap?.documentChanges.isEmpty)!{
self.noData = true
return
}
for i in snap!.documentChanges{
if i.type == .added{
let id = i.document.documentID
let notes = i.document.get("notes") as! String
let date = i.document.get("date") as! Timestamp
let format = DateFormatter()
format.dateFormat = "MM/YY"
let dateString = format.string(from: date.dateValue())
self.data.append(Note(id: id, note: notes, date: dateString))
}
if i.type == .modified{
// when data is changed...
let id = i.document.documentID
let notes = i.document.get("notes") as! String
for i in 0..<self.data.count{
if self.data[i].id == id{
self.data[i].note = notes
}
}
}
if i.type == .removed{
// when data is removed...
let id = i.document.documentID
for i in 0..<self.data.count{
if self.data[i].id == id{
self.data.remove(at: i)
if self.data.isEmpty{
self.noData = true
}
return
}
}
}
}
}
}
}
struct Note : Identifiable {
var id : String
var note : String
var date : String
}
struct Indicator : UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<Indicator>) -> UIActivityIndicatorView {
let view = UIActivityIndicatorView(style: .medium)
view.startAnimating()
return view
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<Indicator>) {
}
}
struct EditView : View {
#Binding var txt : String
#Binding var docID : String
#Binding var show : Bool
var body : some View{
ZStack(alignment: .bottomTrailing) {
MultiLineTF(txt: self.$txt)
.padding()
.background(Color.black.opacity(0.05))
Button(action: {
self.show.toggle()
self.SaveData()
}) {
Text("Save").padding(.vertical).padding(.horizontal,25).foregroundColor(.white)
}.background(Color.red)
.clipShape(Capsule())
.padding()
}.edgesIgnoringSafeArea(.bottom)
}
func SaveData(){
let db = Firestore.firestore()
if self.docID != ""{
db.collection("notes").document(self.docID).updateData(["notes":self.txt]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
}
}
else{
db.collection("notes").document().setData(["notes":self.txt,"date":Date()]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
}
}
}
}
struct MultiLineTF : UIViewRepresentable {
func makeCoordinator() -> MultiLineTF.Coordinator {
return MultiLineTF.Coordinator(parent1: self)
}
#Binding var txt : String
func makeUIView(context: UIViewRepresentableContext<MultiLineTF>) -> UITextView{
let view = UITextView()
if self.txt != ""{
view.text = self.txt
view.textColor = .black
}
else{
view.text = "Type Something"
view.textColor = .gray
}
view.font = .systemFont(ofSize: 18)
view.isEditable = true
view.backgroundColor = .clear
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<MultiLineTF>) {
}
class Coordinator : NSObject,UITextViewDelegate{
var parent : MultiLineTF
init(parent1 : MultiLineTF) {
parent = parent1
}
func textViewDidBeginEditing(_ textView: UITextView) {
if self.parent.txt == ""{
textView.text = ""
textView.textColor = .black
}
}
func textViewDidChange(_ textView: UITextView) {
self.parent.txt = textView.text
}
}
}
Remove your .edgesIgnoringSafeArea(.bottom) attached to the ZStack.
The safe area includes the area taken up by the keyboard. Right now, because you've explicitly told it to ignore the safe area, it's not moving to accommodate the keyboard. If it respects the safe area, the button will move with the keyboard.

How to create a singleton that I can update in SwiftUI

I want to create a global variable for showing a loadingView, I tried lots of different ways but could not figure out how to. I need to be able to access this variable across the entire application and update the MotherView file when I change the boolean for the singleton.
struct MotherView: View {
#StateObject var viewRouter = ViewRouter()
var body: some View {
if isLoading { //isLoading needs to be on a singleton instance
Loading()
}
switch viewRouter.currentPage {
case .page1:
ContentView()
case .page2:
PostList()
}
}
}
struct MotherView_Previews: PreviewProvider {
static var previews: some View {
MotherView(viewRouter: ViewRouter())
}
}
I have tried the below singleton but it does not let me update the shared instance? How do I update a singleton instance?
struct LoadingSingleton {
static let shared = LoadingSingleton()
var isLoading = false
private init() { }
}
Make your singleton a ObservableObject with #Published properties:
struct ContentView: View {
#StateObject var loading = LoadingSingleton.shared
var body: some View {
if loading.isLoading {
Text("Loading...")
}
ChildView()
Button(action: { loading.isLoading.toggle() }) {
Text("Toggle loading")
}
}
}
struct ChildView : View {
#StateObject var loading = LoadingSingleton.shared
var body: some View {
if loading.isLoading {
Text("Child is loading")
}
}
}
class LoadingSingleton : ObservableObject {
static let shared = LoadingSingleton()
#Published var isLoading = false
private init() { }
}
I should mention that in SwiftUI, it's common to use .environmentObject to pass a dependency through the view hierarchy rather than using a singleton -- it might be worth looking into.
First, make LoadingSingleton a class that adheres to the ObservableObject protocol. Use the #Published property wrapper on isLoading so that your SwiftUI views update when it's changed.
class LoadingSingleton: ObservableObject {
#Published var isLoading = false
}
Then, put LoadingSingleton in your SceneDelegate and hook it into your SwiftUI views via environmentObject():
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
static let singleton = LoadingSingleton()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(SceneDelegate.singleton))
self.window = window
window.makeKeyAndVisible()
}
}
}
To enable your SwiftUI views to update when changing isLoading, declare a variable in the view's struct, like this:
struct MyView: View {
#EnvironmentObject var singleton: LoadingSingleton
var body: some View {
//Do something with singleton.isLoading
}
}
When you want to change the value of isLoading, just access it via SceneDelegate.singleton.isLoading, or, inside a SwiftUI view, via singleton.isLoading.

UIScene userActivity is nil

I have a (Catalyst) app, that is able to have several windows. I created a button and if the user presses this button, the app creates a new window. To know, what View needs to be shown, I have different NSUserActivity activityTypes. In the new window there should be a button, that can close this new window. The problem is, that on looping through the open sessions, all the userActivities are nil (and I need the correct UISceneSession for the UIApplication.shared.requestSceneSessionDestruction.
This is my code:
struct ContentView: View {
var body: some View {
VStack {
// Open window type 1
Button(action: {
UIApplication.shared.requestSceneSessionActivation(nil,
userActivity: NSUserActivity(activityType: "window1"),
options: nil,
errorHandler: nil)
}) {
Text("Open new window - Type 1")
}
// Open window type 2
Button(action: {
UIApplication.shared.requestSceneSessionActivation(nil,
userActivity: NSUserActivity(activityType: "window2"),
options: nil,
errorHandler: nil)
}) {
Text("Open new window - Type 2")
}
}
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
if connectionOptions.userActivities.first?.activityType == "window1" {
window.rootViewController = UIHostingController(rootView: Window1())
} else if connectionOptions.userActivities.first?.activityType == "window2" {
window.rootViewController = UIHostingController(rootView: Window2())
} else {
window.rootViewController = UIHostingController(rootView: ContentView())
}
self.window = window
window.makeKeyAndVisible()
}
}
...
struct Window1: View {
var body: some View {
VStack {
Text("Window1")
Button("show") {
for session in UIApplication.shared.openSessions {
// this prints two times nil !
print(session.scene?.userActivity?.activityType)
}
}
}
}
}
struct Window2: View {
var body: some View {
Text("Window2")
}
}
I ran into the same issue. I ended up assigning unique delegate classes to each scene and checking that instead.

SwiftUI View stops updating when application goes to background

I'm using the Spotify APK to authorize a connection to the Spotify app. All communication with Spotify is done via the Scene Delegate. My issue is that when I call for authorization and I'm taken to and from the Spotify app, the current view seems to stop updating with #Published variable changes. However, I want the view to change upon successful authorization/connection.
I've tried having the MainView update with different changes to different variables, but it seems that no matter what I do, the view stops updating with changes to published variables once the app leaves and reenters the foreground.
SceneDelegate:
class SceneDelegate: UIResponder, UIWindowSceneDelegate, SPTAppRemoteDelegate, SPTAppRemotePlayerStateDelegate {
#ObservedObject var MainVM = MainViewModel()
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
MainVM.viewSwitch = false
}
}
MainViewModel:
class MainViewModel: ObservableObject {
#Published var viewSwitch: Bool = true
var appRemote: SPTAppRemote {
get {
let scene = UIApplication.shared.connectedScenes.first
let sd : SceneDelegate = (scene?.delegate as? SceneDelegate)!
return sd.appRemote
}
}
func connectAppRemote() {
appRemote.authorizeAndPlayURI("")
}
}
MainView:
struct MainView: View {
#ObservedObject var MainVM = MainViewModel()
var body: some View {
if MainVM.viewSwitch {
Text("View 1 Displayed")
} else {
Text("View 2 Displayed")
}
}
.onAppear {
MainVM.connectAppRemote()
}
}
You work with different objects:
A. SceneDelegate has own instance (btw, here you don't need ObservedObject)
class SceneDelegate: UIResponder, UIWindowSceneDelegate,
SPTAppRemoteDelegate, SPTAppRemotePlayerStateDelegate {
#ObservedObject var MainVM = MainViewModel()
and
B. MainView has own
struct MainView: View {
#ObservedObject var MainVM = MainViewModel() // << recreated
You need to pass that one in SceneDelegate as environmentObject in MainView, like
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
let contentView = MainView().environmentObject(MainVM)
and declare it correspondingly
struct MainView: View {
#EnvironmentObject var MainVM: MainViewModel

Why update variable in observable object does not update the view?

I am new to SwiftUI. Learning new properties such as #State, #Binding, #EnvironmentObject etc.
I am currently working on the login template, define a binding #Published variable in observable object which allows to switch between login page and main page. However, when I update the variable inside the observable object, the main page does not show up. It is still in the login page. What is missing in my code?
struct ContentView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
var body: some View {
return Group {
if loginViewModel.signInSuccess {
MainPageView()
}
else {
LoginView(signInSuccess: $loginViewModel.signInSuccess).environmentObject(LoginViewModel())
}
}
}
}
final class LoginViewModel: ObservableObject {
#Published var signInSuccess:Bool = false;
func performLogin() {
signInSuccess = true;
}
}
struct LoginView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
#Binding var signInSuccess: Bool;
var body: some View {
Button(action: submit) {
Text("Login")
.foregroundColor(Color.white)
}
}
func submit() {
loginViewModel.performLogin()
// signInSuccess = true;
}
}
If I tried to update binding 'signInSuccess' in the loginView, it can successfully update the view to the mainView. However, is there a way that I can update signInSuccess inside the Observable Object that also update the ContentView to the MainView?
Yes, you just change the view code as the following.
The binding actually is not necessary.
struct ContentView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
var body: some View {
return Group {
if loginViewModel.signInSuccess {
MainPageView()()
}
else {
LoginView(signInSuccess: $loginViewModel.signInSuccess).environmentObject(self.loginViewModel)
}
}
}
}
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(LoginViewModel())
If you want to use an environmental variable, you have to declare it in your SceneDelegate and set it on the ContontentView:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var loginViewModel = LoginViewModel()
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = HostingViewController(rootView: contentView.environmentObject(loginViewModel))
self.window = window
window.makeKeyAndVisible()
}
}
// etc.
Then in your ContenView you don't need to set it in any way on the LoginView as it is held by the enviroment:
struct ContentView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
var body: some View {
return Group {
if loginViewModel.signInSuccess {
MainPageView()
} else {
LoginView()
}
}
}
}
In your model, make sure you declare your signInSuccess as private(set) so it can only be set from within the class and only read from elsewhere:
final class LoginViewModel: ObservableObject {
#Published private(set) var signInSuccess:Bool = false;
func performLogin() {
signInSuccess = true;
}
}
And finally in the LoginView you just need to include the #EnvironmentObject and everything else will work.
struct LoginView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
var body: some View {
Button(action: { self.loginViewModel.performLogin() }) {
Text("Login")
.foregroundColor(Color.white)
}
}
}

Resources