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")
}
}
}
}
Related
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
}
}
}
}
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)")
})
}
}
I recently implemented an Edit Button into my app. However, it created a strange and annoying animation in the View both when it loads (everything comes in from the left), and when I sort the elements. I noticed that if I remove the .animation that is after the .environment it solves this issue, but then everything appears and moves instantly without the .easeInOut look that I wanted to give. How can I apply this animation only to the appearing and disappearing of the sort and delete buttons of the cells of the Form?
If you want to take a look at my problem (since I don't think I was able to explain it correctly) you can look at this video.
The code is this one, ContentView:
import SwiftUI
struct ContentView: View {
#ObservedObject var dm : DataManager
#ObservedObject var vm : ValueModel
#State var showAlertDeleteContact = false
#State var isEditing = false
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView {
Color(UIColor.systemGray6).ignoresSafeArea(.all).overlay(
VStack {
scrollViewFolders
Form {
ForEach(dm.storageValues) { contacts in
NavigationLink(
destination:
//Contact View,
label: {
IconView(dm: dm, vm: contacts)
})
}.onDelete(perform: { indexSet in
self.showAlertDeleteContact = true
self.indexDeleteContact = indexSet
})
.onMove(perform: onMove)
Section {
buttonNewFolder
buttonSort
}
}
}
.navigationBarTitle("Contacts")
.navigationBarItems(trailing: editButton)
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive))
.animation(.easeInOut)
//I also tried with this -> .animation(.some(Animation.default))
)
}.alert(isPresented: $showAlertDeleteContact, content: {
alertDeleteContact
})
}
If you want to recreate the project, the DataManager is:
import SwiftUI
import Combine
class DataManager : Equatable, Identifiable, ObservableObject {
static let shared = DataManager()
#Published var storageValues : [ValueModel] = []
typealias StorageValues = [ValueModel]
//The rest of the code
}
And the ValueModel is:
import SwiftUI
import Combine
class ValueModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var valueName : String
var notes : String?
var expires : Date?
init(valueName: String, notes: String?, expires: Date?) {
self.valueName = valueName
self.notes = notes
self.expires = expires
}
}
Thanks to everyone who will help me!
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
How can I pass data to a view and use it directly in the "header"? All tutorials I made are accessing the data in the view body - which works fine - but I want to call a graphlql method from the UpdateAccountView and than render a view based on the result.
My class for passing data:
class Account {
var tel: Int
init(tel: Int) {
self.tel = tel
}
}
My main view where the class is initialised (simplified - normally the "tel" will come from an input)
struct ContentView: View {
var account: Account = Account(tel: 123)
var body: some View {
NavigationView {
NavigationLink(
destination: UpdateAccountView(account: account),
label: {
Text("Navigate")
})
}
}
}
The view I call to do the request and call the next view based on the result
UpdateAccount is taking tel:Int as a parameter.
And here is the problem. I cannot access account.tel from the passed data.
struct UpdateAccountView: View {
var account: Account
#ObservedObject private var updateAccount: UpdateAccount = UpdateAccount(tel: account.tel)
#ViewBuilder
var body: some View {
if updateAccount.success {
AccountVerifyView()
} else {
ContentView()
}
}
}
The error:
Cannot use instance member 'account' within property initializer; property initializers run before 'self' is available
Update method (GraphQL):
class UpdateAccount: ObservableObject {
#Published var success: Bool
init(tel: Int){
self.success = false
update(tel: tel)
}
func update(tel: Int){
Network.shared.apollo.perform(mutation: UpdateAccountMutation(tel: tel)) { result in
switch result {
case .success(let graphQLResult):
self.success = graphQLResult.data!.updateAccount.success
case .failure(let error):
print("Failure! Error: \(error)")
self.success = false
}
}
}
I saw that there is an EnvironmentObject but than the variable become available globally as far as I understood, which is not necessary here.
Thank you for your help.
You can make it in explicit init, like
struct UpdateAccountView: View {
var account: Account
#ObservedObject private var updateAccount: UpdateAccount // << declare
init(account: Account) {
self.account = account
self.updateAccount = UpdateAccount(tel: account.tel) // << here !!
}
// ... other code
}