SwitfUI: access the specific scene's ViewModel on macOS - macos

In this simple example app, I have the following requirements:
have multiple windows, each having it's own ViewModel
toggling the Toggle in one window should not update the other window's
I want to also be able to toggle via menu
As it is right now, the first two points are not given, the last point works though. I do already know that when I move the ViewModel's single source of truth to the ContentView works for the first two points, but then I wouldn't have access at the WindowGroup level, where I inject the commands.
import SwiftUI
#main
struct ViewModelAndCommandsApp: App {
var body: some Scene {
ContentScene()
}
}
class ViewModel: ObservableObject {
#Published var toggleState = true
}
struct ContentScene: Scene {
#StateObject private var vm = ViewModel()// injecting here fulfills the last point only…
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(vm)
.frame(width: 200, height: 200)
}
.commands {
ContentCommands(vm: vm)
}
}
}
struct ContentCommands: Commands {
#ObservedObject var vm: ViewModel
var body: some Commands {
CommandGroup(before: .toolbar) {
Button("Toggle Some State") {
vm.toggleState.toggle()
}
}
}
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel//injecting here will result in window independant ViewModels, but make them unavailable in `ContactScene` and `ContentCommands`…
var body: some View {
Toggle(isOn: $vm.toggleState, label: {
Text("Some State")
})
}
}
How can I fulfill theses requirements–is there a SwiftUI solution to this or will I have to implement a SceneDelegate (is this the solution anyway?)?
Edit:
To be more specific: I'd like to know how I can go about instantiating a ViewModel for each individual scene and also be able to know from the menu bar which ViewModel is meant to be changed.

Long story short, see the code below. The project is called WindowSample this needs to match your app name in the URL registration.
import SwiftUI
#main
struct WindowSampleApp: App {
var body: some Scene {
ContentScene()
}
}
//This can be done several different ways. You just
//need somewhere to store multiple copies of the VM
class AStoragePlace {
private static var viewModels: [ViewModel] = []
static func getAViewModel(id: String?) -> ViewModel? {
var result: ViewModel? = nil
if id != nil{
result = viewModels.filter({$0.id == id}).first
if result == nil{
let newVm = ViewModel(id: id!)
viewModels.append(newVm)
result = newVm
}
}
return result
}
}
struct ContentCommands: Commands {
#ObservedObject var vm: ViewModel
var body: some Commands {
CommandGroup(before: .toolbar) {
Button("Toggle Some State \(vm.id)") {
vm.testMenu()
}
}
}
}
class ViewModel: ObservableObject, Identifiable {
let id: String
#Published var toggleState = true
init(id: String) {
self.id = id
}
func testMenu() {
toggleState.toggle()
}
}
struct ContentScene: Scene {
var body: some Scene {
//Trying to init from 1 windowGroup only makes a copy not a new scene
WindowGroup("1") {
ToggleView(vm: AStoragePlace.getAViewModel(id: "1")!)
.frame(width: 200, height: 200)
}
.commands {
ContentCommands(vm: AStoragePlace.getAViewModel(id: "1")!)
}.handlesExternalEvents(matching: Set(arrayLiteral: "1"))
//To open this go to File>New>New 2 Window
WindowGroup("2") {
ToggleView(vm: AStoragePlace.getAViewModel(id: "2")!)
.frame(width: 200, height: 200)
}
.commands {
ContentCommands(vm: AStoragePlace.getAViewModel(id: "2")!)
}.handlesExternalEvents(matching: Set(arrayLiteral: "2"))
}
}
struct ToggleView: View {
#Environment(\.openURL) var openURL
#ObservedObject var vm: ViewModel
var body: some View {
VStack{
//Makes copies of the window/scene
Button("new-window-of type \(vm.id)", action: {
//appname needs to be a registered url in info.plist
//Info Property List>Url types>url scheme>item 0 == appname
//Info Property List>Url types>url identifier == appname
if let url = URL(string: "WindowSample://\(vm.id)") {
openURL(url)
}
})
//Toggle the state
Toggle(isOn: $vm.toggleState, label: {
Text("Some State \(vm.id)")
})
}
}
}

Related

Passing data with NavigationSplitView to the second column

I have a macOS app with two columns. The left column is a list that presents the filename and date of the unit (file) that I'm working on. The second column, to the right, should present the content of each file when selected.
I have an array that contains that information and I create a list for the left column that presents each item. I added a detail: with a TextEditor that allows the user to see the data and modify it if necessary. I have been trying to set the #State var text to the contents of currentunit.text but I don't know how to pass that the detail:. If I try to assign it (as in text = x) then I get an error saying that it doesn't conform to View.
I tried then to maybe load it by getting the index of the current selected unit, using the selectedUnitId, and using something like this to get the index:
func getIndex(uuid: UUID) -> Int? {
return data.units.firstIndex(where: {$0.id == uuid})
}
But I get nowhere with a collection of different errors.
Regardless, how do I pass data to the detail: part of the code? I have looked into many examples of NavigationSplitView and they are all very similar, just showing the basic usage and that's it.
Thanks!
Code:
struct Unit: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
let df = DateFormatter()
df.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return df.string(from: date)
}
var changed: Bool = false
}
final class UnitModel: ObservableObject {
#AppStorage("unit") public var units: [Unit] = []
init() {
self.units = self.units.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
func sortList() {
self.units = self.units.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
struct ContentView: View {
#EnvironmentObject private var data: UnitModel
#State var selectedUnitId: UUID?
#State var text: String = ""
var body: some View {
NavigationSplitView {
List(data.units, selection: $selectedUnitId) { currentunit in
VStack(alignment: .leading) {
Text(currentunit.filename)
Text(currentunit.dateText)
}
}
} detail: {
// here: how do I preload $text with the text from the unit?
VStack(alignment: .leading) {
TextEditor(text: $text)
}
}
}
}
I also tried:
struct ContentView: View {
#EnvironmentObject private var data: UnitModel
#State var selectedNoteId: UUID?
var body: some View {
NavigationSplitView {
List(data.units, selection: $selectedNoteId) { currentunit in
NavigationLink{
UnitView(unit: currentunit, text: currentunit.text)
} label: {
VStack(alignment: .leading) {
Text(currentunit.filename)
Text(currentunit.dateText)
}
}
}
} detail: {
Text("Select a unit.")
}
}
}
struct UnitView: View {
#EnvironmentObject private var data: UnitModel
var unit: Unit
#State var text: String
var body: some View {
VStack(alignment: .leading) {
TextEditor(text: $text)
}
}
}
But again, I don't know how to initialize the text variable with the text of the current unit. I only get the initial one selected, and even tho I can see a new unit selected, the text remains the same and doesn't update.
UPDATED if I change the code to use NavigationView then it works as it should, so what's going with the new way that Apple is make us use now? Namely NavigationSplitView and NavigationStack?
Here's the code that work as it should but it's deprecated according to Apple:
NavigationView {
List(data.units, selection: $selectedNoteId) { currentunit in
NavigationLink(
destination: UnitView(unit: currentunit, text: currentunit.text),
label: {
VStack(alignment: .leading) {
Text(currentunit.filename)
Text(currentunit.dateText)
}
}
)
}
Apple's Defining the source of truth using a custom binding
tutorial covers this. Your code would look something like this:
} detail: {
DetailView(unitID: selectedUnitID) // not sure why they used binding
}
struct DetailView: View {
let unitID: Unit.ID
#EnvironmentObject private var store: UnitModel
private var unitBinding: Binding<Unit> {
Binding {
if let id = unitID {
return store.unit(with: id) ?? Unit.emptyUnit()
} else {
return Unit.emptyUnit()
}
} set: { updatedUnit in
store.update(updatedUnit)
}
}
var body: some View {
if store.contains(unitID) {
VStack(alignment: .leading) {
TextEditor(text: unitBinding.text)
}
}
else {
Text("Select Unit")
}
}
}
Note there currently (as of Xcode 14.2) is a known bug with the text cursor when using a TextField in the detail pane. Check by entering text, move cursor to middle and try to enter a character. The bug is the cursor jumps to the end.

No ObservableObject of type error when calling variable inside struct

I am trying to create a crime tracking app, but right now I am struggling to create structs for different tabs of my app using NavigationManager and screens. However when I made a new struct and tried to use my NavigationManager, it displayed this error message:
2022-10-26 02:18:45.468441-0400 TextFieldAndSecureFieldInSwiftUI[50866:2496084] SwiftUI/EnvironmentObject.swift:70: Fatal error: No ObservableObject of type StatsNavigationManager found. A View.environmentObject(_:) for StatsNavigationManager may be missing as an ancestor of this view.
This is the code for my StatsNavigationManager object and my tab.
final class StatsNavigationManager: ObservableObject {
#Published var screen: Screens? {
didSet {
print("📱 \(String(describing: screen))")
}
}
func push(to screen: Screens) {
self.screen = screen
}
func popToRoot() {
self.screen = nil
}
}
struct StatsView: View {
let test = cityClass()
struct Input{
var zip: String = ""
}
#State private var input: Input = .init()
#FocusState private var inputFocused: Bool
#EnvironmentObject var statNavManager: StatsNavigationManager//I define the var I am struggling with here
//I also tried these approaches but each gave me an error
//weak var statNavManager : StatsNavigationManager?
//#State var statNavManager : StatsNavigationManager
var body: some View {
NavigationView {
ZStack {
Color.blue
VStack {
inputTxtVw
submitBtn
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Text("Search")
Button("Retrieve Data") {
statNavManager.push(to: .statsPage)
}
.background(
NavigationLink(destination: CityView(),
tag: .statsPage,
selection: $statNavManager.screen) { EmptyView() }//the "$statNavManager.screen" gives me the error and reads No ObservableObject of type StatsNavigationManager found when I hover
)
}
}
.onSubmit(of: .text, submit)
}
}
.environmentObject(self.statNavManager)
//also tried StatsNavigationManager.environmentObject(statNavManager)
}
}
}

Sending an NSManagedObjectID to a struct / view

I'm complete new to swift, swiftui and coredata. I have good programming experience in other languages, but swift is its own world. :-)
Important information: it's for macOS not iOS!!
My problem: I want to edit a Dataset in an separate view displayed in a sheet. I followed this example (SwiftUI update view on core data object change), but when trying to run, my NSManagedObjectID is allway nil.
The ContentView (shortened)
import SwiftUI
import CoreData
struct ContentView: View {
#State public var selectedBookId: NSManagedObjectID?
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Books.title, ascending: true)],
animation: .default)
private var books: FetchedResults<Books>
#State private var showingEditScreen = false
var body: some View {
NavigationView {
List {
ForEach(books, id: \.self) { book in
HStack {
NavigationLink {
HStack {
Button {
// here store objectID to var
selectedBookId = book.objectID
showingEditScreen.toggle()
} label: {
Label("", systemImage: "pencil")
}
}
.padding(10.0)
} label: {
Text(book.title!)
}
}
}.onDelete(perform: deleteBooks)
}
.toolbar {
ToolbarItem(placement: .automatic) {
// here goes blabla
}
}
Text("Bitte zuerst ein Buch auswählen!")
}
.sheet(isPresented: $showingEditScreen) {
// Run EditBookView an send bookId
EditBookView(bookId: selectedBookId).environment(\.managedObjectContext, self.viewContext)
}
}
}
My EditView looks like this
import SwiftUI
struct EditBookView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.dismiss) var dismiss
var bookId: NSManagedObjectID! // This is allways nil!!
var book: Books {
moc.object(with: bookId) as! Books
}
#State private var title = ""
#State private var review = ""
var body: some View {
Form {
Text("Edit Book").font(.title)
Spacer()
Section {
TextField("Buchname", text: $title)
TextEditor(text: $review)
} header: {
Text("Schreibe eine Zusammenfassung")
}
Spacer()
Section {
HStack {
Button("Save") {
// add the book
// here code for update
try? moc.save()
dismiss()
}
Button("Cancel") {
print(bookId) // shows "nil"
dismiss()
}
}
}
Spacer()
}
.onAppear {
self.title = self.book.title ?? ""
self.review = self.book.review ?? ""
}
.padding(10.0)
}
}
First: thanks for all the good hints. In the end, I could solve the problem using
#ObservedObject var aBook: Books
at the beginning of my EditView.
The button itself has the following code
Button {
showingEditScreen.toggle()
} label: {
Label("", systemImage: "pencil")
}.sheet(isPresented: $showingEditScreen) {
EditBookView(aBook: book).environment(\.managedObjectContext, self.viewContext)
}
This way, I can send the whole book object of a single book item to the edit view and I can use it.

Swiftui how to show an image if user selected a button 2 views before

I want to show a flag in the main view if the user selected that country 2 views before getting to the main view.
The code for the page where the user selects the country is:
struct ChooseLanguageWithButtonView: View {
#State var isSelected1 = false
#State var isSelected2 = false
var body: some View {
ZStack {
Color.white
.edgesIgnoringSafeArea(.all)
VStack {
ScrollView (showsIndicators: false) {
VStack(spacing: 20){
VStack(alignment: .leading){
Text("Choose the language you want to learn!")
.font(.custom("Poppins-Bold", size: 25))
Text("Select one language")
.opacity(0.5)
.font(.custom("Poppins-Light", size: 15))
.frame(alignment: .bottomTrailing)
.padding(5)
Spacer()
}
.padding(.top, -25)
HStack (spacing: 30){
ButtonLanguage(
isSelected: $isSelected1,
color: .orangeGradient1,
textTitle: "English",
textSubtitle: "American",
imageLang: "englishAmerican",
imageFlag: "1"
)
.onTapGesture(perform: {
isSelected1.toggle()
if isSelected1 {
isSelected2 = false
}
})
ButtonLanguage(
isSelected: $isSelected2,
color: .orangeGradient1,
textTitle: "English",
textSubtitle: "British",
imageLang: "telephone",
imageFlag: "0"
)
.onTapGesture(perform: {
isSelected2.toggle()
if isSelected2 {
isSelected1 = false
}
})
}
NavigationLink(destination: {
if isSelected1 {
EnglishAmericanLevelView()
} else if isSelected2 {
EnglishBritishView()
}
}, label: {
Text("Let's Go")
.bold()
.foregroundColor(Color.white)
.frame(width: 200, height: 50)
.cornerRadius(40)
})
On the main view if the user chose english american I want to show the american flag.
Someone can help me with that please?
Assuming you are using MVVM pattern, then you can create a published Bool variable in the ViewModel and pass it in the environment.
Something passed in the environment can be accessed from all descendents of the view.
class MainViewModel: ObservableObject {
#Published var showAmericanFlag: Bool = false
}
Pass the ViewModel it to either the MainView or ChildView as an environment object.
// Wherever you are using MainView
MainView()
.environmentObject(MainViewModel())
// or create MainViewModel inside MainView and pass it to ChildView
struct MainView: View {
private var mainViewModel = MainViewModel()
var body: some View {
// stuff
ChildView()
.environmentObject(mainViewModel())
// stuff
}
}
You can get a reference to the MainViewModel inside the ChildView from the
environment and use the showAmericanFlag variable.
struct ChildView: View {
#EnvironmentObject private var mainViewModel: MainViewModel
var body: some View {
// stuff
if mainViewModel.showAmericanFlag {
Image("americanFlag")
}
// stuff
}
}
It sounds like a global class might be good to use here, so you can set variables like this that later you can reference in your views.
final class GlobalClass: ObservableObject {
#Published public var showFlag: Bool = false
}
In your main project app file you can initialize the class with the .environmentObject method
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(GlobalClass())
}
}
}
You can then reference the global class in any view as follows
#EnvironmentObject private var globalObj: GlobalClass
Then you can set the variable to be whatever you'd like and then use it in an if statement to show your image. For example . . .
if (globalObj.showFlag){
Image("flag").onTapGesture{
globalObj.showFlag = false
}
}
Otherwise you will have to pass the show flag object from view to view

why is a passed parameter displaying the previous content of an EnvironmentObject

this is a Macos app where the parsclass is setup in a previous view that contains the YardageRowView below. That previous view is responsible for changing the contents of the parsclass. This is working is other views that use a NavigationLink to display the views.
When the parsclass is changed, this view is refreshed, but the previous value is put in the text field on the holeValueTestView.
I cannot comprehend how the value is not being passed into the holeValueTestView correctly
This is a view shown as a .sheet, and if I dismiss it and display it again, everything is fine.
if you create a macOS project called YardageSample and replace the ContentView.swift and YardageSampleApp.swift with the two files below, you can see that the display in red changes and the black numbers do not change until you click Done and redisplay the .sheet
//
// YardageSampleApp.swift
// YardageSample
//
// Created by Brian Quick on 2021-04-12.
//
import SwiftUI
#main
struct YardageSampleApp: App {
#StateObject var parsclass = parsClass()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(parsclass)
}
}
}
//
// ContentView.swift
// YardageSample
//
// Created by Brian Quick on 2021-04-12.
//
import SwiftUI
struct ContentView: View {
#StateObject var parsclass = parsClass()
enum ActiveSheet : String , Identifiable {
case CourseMaintenance
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil
var body: some View {
Button(action: {
self.activeSheet = .CourseMaintenance
}) {
Text("Course Maintenance")
}
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .CourseMaintenance:
CourseMaintenance()
}
}.frame(width: 200, height: 200, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
}
}
class parsClass: ObservableObject {
#Published var pars = [parsRec]()
init() {
self.pars = [parsRec]()
self.pars.append(parsRec())
}
func create(newpars: [parsRec]) {
pars.removeAll()
pars = newpars
}
}
class parsRec: Identifiable, Codable {
var id = UUID()
var Hole = 1
var Yardage = 1
}
struct CourseMaintenance: View {
#EnvironmentObject var parsclass: parsClass
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button(action: {presentationMode.wrappedValue.dismiss()}, label: {
Text("Done")
})
Button(action: {switchScores(number: 1)}, label: {
Text("Button 1")
})
Button(action: {switchScores(number: 2)}, label: {
Text("Button 2")
})
Button(action: {switchScores(number: 3)}, label: {
Text("Button 3")
})
CourseDetail().environmentObject(parsclass)
}.frame(width: 400, height: 400, alignment: .center)
}
func switchScores(number: Int) {
var newparRecs = [parsRec]()
for i in 0..<17 {
let myrec = parsRec()
myrec.Hole = i
myrec.Yardage = number
newparRecs.append(myrec)
}
parsclass.create(newpars: newparRecs)
}
}
struct CourseDetail: View {
#EnvironmentObject var parsclass: parsClass
var body: some View {
HStack(spacing: 0) {
ForEach(parsclass.pars.indices, id: \.self) { indice in
// this displays the previous value
holeValueTestView(value: String(parsclass.pars[indice].Yardage))
// this displays the correct value after parsclass has changed
Text(String(parsclass.pars[indice].Yardage))
.foregroundColor(.red)
}
}
}
}
struct holeValueTestView: View {
#State var value: String
var body: some View {
//TextField(String(value), text: $value)
Text(value)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
There are a couple of issues going on:
You have multiple instances of parsClass. One is defined in YardageSampleApp and passed into the view hierarchy as a #EnvironmentObject. The second is defined in ContentView as a #StateObject. Make sure you're only using one.
On holeValueTestView, you defined value as a #State variable. That gets set initially when the view is created by its parent and then it maintains its own state. So, when the environmentObject changed, because it was in charge of its own state at this point, it didn't update the value. You can simply remove #State and see the behavior that you want.
struct ContentView: View {
#EnvironmentObject var parsclass : parsClass //<-- Here
enum ActiveSheet : String , Identifiable {
case CourseMaintenance
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil
var body: some View {
Button(action: {
self.activeSheet = .CourseMaintenance
}) {
Text("Course Maintenance")
}
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .CourseMaintenance:
CourseMaintenance()
}
}.frame(width: 200, height: 200, alignment: .center)
}
}
struct holeValueTestView: View {
var value: String //<-- Here
var body: some View {
Text(value)
}
}
As a side note:
In Swift, normally type names are capitalized. If you want to write idiomatic Swift, you would change your parsClass to ParsClass for example.

Resources