Dynamically updating a list in SwiftUI? - xcode

I'm back again lol. My content view looks like:
struct ContentView: View {
#ObservedObject var VModel = ViewModel()
#State private var resultsNeedToBeUpdated: Bool = false
var body: some View {
VStack {
if self.resultsNeedToBeUpdated == true {
SearchResults(VModel: VModel, resultsNeedToBeUpdated: $resultsNeedToBeUpdated)
}
}
}
}
The SearchBar view looks like:
struct SearchResults: View {
var VModel: ViewModel
#Binding var resultsNeedToBeUpdated: Bool
var body: some View {
List {
ForEach(VModel.searchResults, id: \.self) { result in
Text(result)
}
}
}
}
Finally, the ViewModel class looks like:
class ViewModel: ObservableObject {
#Published var searchResults: [String] = []
func findResults(address: String) {
let Geocoder = Geocoder(accessToken: 'my access token')
searchResults = []
Geocoder.geocode(ForwardGeocodeOptions(query: address)) { (placemarks, attribution, error) in
guard let placemarks = placemarks
else {
return
}
for placemark in placemarks {
self.searchResults.append(placemark.formattedName)
print("Formatted name is: \(placemark.formattedName)") //this works
}
}
//I'm doing my printing on this line and it's just printing an empty array ;(
}
The variable 'resultsNeedToBeUpdated' is a boolean Binding that is updated when the user types some text into a search bar view, and it essentially just tells you that the SearchResults view should be displayed if it's true and it shouldn't be displayed if it's false. What I'm trying to do is update the SearchResults view depending on what the user has typed in.
The error is definitely something with the display of the SearchResults view (I think it's only displaying the initial view, before the array is updated). I tried using a binding because I thought it would cause the ContentView to reload and it would update the SearchResultsView but that didn't work.

Make view model observed, in this case it will update view every time the used #Published var searchResults property is changed
struct SearchResults: View {
#ObservedObject var VModel: ViewModel
// #Binding var resultsNeedToBeUpdated: Bool // << not needed here
additionally to above published properties should be updated on main queue, as
DispatchQueue.main.async {
self.searchResults = placemarks.compactMap{ $0.formattedName }
// for placemark in placemarks {
// print("Formatted name is: \(placemark.formattedName)") //this works
// }
}

Related

SwiftUI, How to publish data from view to a viewModel then to a second view?

I have one view (with a Form), a viewModel, and a second view that I hope to display inputs in the Form of the first view. I thought property wrapping birthdate with #Published in the viewModel would pull the Form input, but so far I can't get the second view to read the birthdate user selects in the Form of the first view.
Here is my code for my first view:
struct ProfileFormView: View {
#EnvironmentObject var appViewModel: AppViewModel
#State var birthdate = Date()
var body: some View {
NavigationView {
Form {
Section(header: Text("Personal Information")) {
DatePicker("Birthdate", selection: $birthdate, displayedComponents: .date)
}
}
}
Here is my viewModel code:
class AppViewModel: ObservableObject {
#Published var birthdate = Date()
func calcAge(birthdate: String) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
let birthdayDate = dateFormater.date(from: birthdate)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthdayDate!, to: now, options: [])
let age = calcAge.year
return age!
and here is my second view code:
struct UserDataView: View {
#EnvironmentObject var viewModel: AppViewModel
#StateObject var vm = AppViewModel()
var body: some View {
VStack {
Text("\(vm.birthdate)")
Text("You are signed in")
Button(action: {
viewModel.signOut()
}, label: {
Text("Sign Out")
.frame(width: 200, height: 50)
.foregroundColor(Color.blue)
})
}
}
And it may not matter, but here is my contentView where I can tab between the two views:
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
NavigationView {
ZStack {
if viewModel.signedIn {
ZStack {
Color.blue.ignoresSafeArea()
.navigationBarHidden(true)
TabView {
ProfileFormView()
.tabItem {
Image(systemName: "square.and.pencil")
Text("Profile")
}
UserDataView()
.tabItem {
Image(systemName: "house")
Text("Home")
}
}
}
}
else
{
SignInView()
}
}
}
.onAppear {
viewModel.signedIn = viewModel.isSignedIn
}
}
One last note, I've got a second project that requires this functionality (view to viewmodel to view) so skipping the viewmodel and going direct from view to view will not help.
Thank you so much!!
Using a class AppViewModel: ObservableObject like you do is the appropriate way to "pass" the data around your app views. However, there are a few glitches in your code.
In your first view (ProfileFormView), remove #State var birthdate = Date() and use
DatePicker("Birthdate", selection: $appViewModel.birthdate, ....
Also remove #StateObject var vm = AppViewModel() in your second view (UserDataView),
you already have a #EnvironmentObject var viewModel: AppViewModel, no need for 2 of them.
Put #StateObject var vm = AppViewModel() up in your hierarchy of views,
and pass it down (as you do) using the #EnvironmentObject with
.environmentObject(vm)
Read this info to understand how to manage your data: https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

Menu Command Buttons don't disable in reaction to Published variable changes

Situation:
I need the menu bar to recognise the active tab in a TabView, even when multiple windows are open. I have found this solution (https://stackoverflow.com/a/68334001/2682035), which seems to work in principal, but not in practice.
Problem:
Menu bar buttons do not disable immediately when their corresponding tab is changed. However, they will disable correctly if another State variable is modified.
Minimal example which now works:
I made this without the code for managing multiple windows because the problem seemed to occur without that. Thanks to the suggestion from lorem ipsum, this will now function as expected.
import SwiftUI
enum TabType: String, Codable{
case tab1 = "first tab"
case tab2 = "second tab"
}
public class ViewModel: ObservableObject {
#Published var activeTab: TabType = .tab1
}
struct MenuCommands: Commands {
#ObservedObject var viewModel: ViewModel // CHANGED
#Binding var someInformation: String
var body: some Commands{
CommandMenu("My menu"){
Text(someInformation)
Button("Go to tab 1"){
viewModel.activeTab = .tab1
}
.disabled(viewModel.activeTab == .tab1) // this now works as expected
Button("Go to tab 2"){
viewModel.activeTab = .tab2
}
.disabled(viewModel.activeTab == .tab2) // this does too
Button("Print active tab"){
print(viewModel.activeTab) // this does too
}
}
}
}
struct Tab: View{
var tabText: String
#Binding var someInformation: String
var body: some View{
VStack{
Text("Inside tab " + tabText)
TextField("Info", text: $someInformation)
}
}
}
struct ContentView: View {
#EnvironmentObject var viewModel: ViewModel
#Binding var someInformation: String
var body: some View {
TabView(selection: $viewModel.activeTab){
Tab(tabText: "1", someInformation: $someInformation)
.tabItem{
Label("Tab 1", systemImage: "circle")
}
.tag(TabType.tab1)
Tab(tabText: "2", someInformation: $someInformation)
.tabItem{
Label("Tab 2", systemImage: "circle")
}
.tag(TabType.tab2)
}
}
}
#main
struct DisableMenuButtonsMultiWindowApp: App {
#StateObject var viewModel = ViewModel() // CHANGED
#State var someInformation: String = ""
var body: some Scene {
WindowGroup {
ContentView(someInformation: $someInformation)
.environmentObject(viewModel)
}
.commands{MenuCommands(viewModel: viewModel, someInformation: $someInformation)}
}
}
Slightly less minimal example that doesn't work:
Unfortunately that didn't work in my app, so here is a new minimal example that works closer to the actual app and will observe multiple windows, except it has the same issue as before.
import SwiftUI
enum TabType: String, Codable{
case tab1 = "first tab"
case tab2 = "second tab"
}
public class ViewModel: ObservableObject {
#Published var activeTab: TabType = .tab1
}
struct MenuCommands: Commands {
#ObservedObject var globalViewModel: GlobalViewModel
#Binding var someInformation: String
var body: some Commands{
CommandMenu("My menu"){
Text(someInformation)
Button("Go to tab 1"){
globalViewModel.activeViewModel?.activeTab = .tab1
}
.disabled(globalViewModel.activeViewModel?.activeTab == .tab1) // this will not disable when activeTab changes, but it will when someInformation changes
Button("Go to tab 2"){
globalViewModel.activeViewModel?.activeTab = .tab2
}
.disabled(globalViewModel.activeViewModel?.activeTab == .tab2) // this will not disable when activeTab changes, but it will when someInformation changes
Button("Print active tab"){
print(globalViewModel.activeViewModel?.activeTab ?? "") // this always returns correctly
}
}
}
}
struct Tab: View{
var tabText: String
#Binding var someInformation: String
var body: some View{
VStack{
Text("Inside tab " + tabText)
TextField("Info", text: $someInformation)
}
}
}
struct ContentView: View {
#EnvironmentObject var globalViewModel : GlobalViewModel
#Binding var someInformation: String
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
HostingWindowFinder { window in
if let window = window {
self.globalViewModel.addWindow(window: window)
print("New Window", window.windowNumber)
self.globalViewModel.addViewModel(self.viewModel, forWindowNumber: window.windowNumber)
}
}
TabView(selection: $viewModel.activeTab){
Tab(tabText: "1", someInformation: $someInformation)
.tabItem{
Label("Tab 1", systemImage: "circle")
}
.tag(TabType.tab1)
Tab(tabText: "2", someInformation: $someInformation)
.tabItem{
Label("Tab 2", systemImage: "circle")
}
.tag(TabType.tab2)
}
}
}
#main
struct DisableMenuButtonsMultiWindowApp: App {
#StateObject var globalViewModel = GlobalViewModel()
#State var someInformation: String = ""
var body: some Scene {
WindowGroup {
ContentView(someInformation: $someInformation)
.environmentObject(globalViewModel)
}
.commands{MenuCommands(globalViewModel: globalViewModel, someInformation: $someInformation)}
}
}
// everything below is from other solution for observing multiple windows
class GlobalViewModel : NSObject, ObservableObject {
// all currently opened windows
#Published var windows = Set<NSWindow>()
// all view models that belong to currently opened windows
#Published var viewModels : [Int:ViewModel] = [:]
// currently active aka selected aka key window
#Published var activeWindow: NSWindow?
// currently active view model for the active window
#Published var activeViewModel: ViewModel?
override init() {
super.init()
// deallocate a window when it is closed
// thanks for this Maciej Kupczak 🙏
NotificationCenter.default.addObserver(
self,
selector: #selector(onWillCloseWindowNotification(_:)),
name: NSWindow.willCloseNotification,
object: nil
)
}
#objc func onWillCloseWindowNotification(_ notification: NSNotification) {
guard let window = notification.object as? NSWindow else {
return
}
var viewModels = self.viewModels
viewModels.removeValue(forKey: window.windowNumber)
self.viewModels = viewModels
}
func addWindow(window: NSWindow) {
window.delegate = self
windows.insert(window)
}
// associates a window number with a view model
func addViewModel(_ viewModel: ViewModel, forWindowNumber windowNumber: Int) {
viewModels[windowNumber] = viewModel
}
}
extension GlobalViewModel : NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
if let window = notification.object as? NSWindow {
windows.remove(window)
viewModels.removeValue(forKey: window.windowNumber)
print("Open Windows", windows)
print("Open Models", viewModels)
// windows = windows.filter { $0.windowNumber != window.windowNumber }
}
}
func windowDidBecomeKey(_ notification: Notification) {
if let window = notification.object as? NSWindow {
print("Activating Window", window.windowNumber)
activeWindow = window
activeViewModel = viewModels[window.windowNumber]
}
}
}
struct HostingWindowFinder: NSViewRepresentable {
var callback: (NSWindow?) -> ()
func makeNSView(context: Self.Context) -> NSView {
let view = NSView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
What I've tried:
Changing viewModel to Binding, StateObject and now changing to StateObject and Observed Object. It also doesn't matter if it's sent to the ContentView as a Binding or EnvironmentObject, it still happens.
In DisableMenuButtonsMultiWindowApp change
#State var viewModel = ViewModel()
To
#StateObject var viewModel: ViewModel = ViewModel()
And in the MenuCommands use
#ObservedObject var viewModel: ViewModel
instead
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
#State is used to initialize value types such as a struct and #StateObject is for reference type ObservableObjects such as your ViewModel.
Something else to note is that each #State does not know about changes to another #State an #Binding is a two-way connection.

How do I store the Input of a Textfield and display it in another View in Swift UI?

I am just learning to code and I have a question. How do I store the Input data of a Textfield and display it in another View? I tried it with Binding but it doesn't work that way. I appreciate your help
import SwiftUI
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
struct DisplayUserName: View {
#State private var name = ""
var body: some View {
// the name should be diplayed here!
Text(name)
}
}
struct DisplayUserName_Previews: PreviewProvider {
static var previews: some View {
DisplayUserName()
}
}
State should always be stored in a parent and passed down to the children. Right now, you're not showing the connection between the two views (neither reference the other), so it's a little unclear how they relate, but there are basically two scenarios:
Your current code would work if DisplayUserName was the parent of SelectUserName:
struct DisplayUserName: View {
#State private var name = ""
var body: some View {
Text(name)
SelectUserName(name: $name)
}
}
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
Or, if they are sibling views, the state should be stored by a common parent:
struct ContentView : View {
#State private var name = ""
var body: some View {
SelectUserName(name: $name)
DisplayUserName(name: name)
}
}
struct SelectUserName: View {
#Binding var name: String
var body: some View {
TextField("Name", text: self.$name)
}
}
struct DisplayUserName: View {
var name : String //<-- Note that #State isn't needed here because nothing in this view modifies the value
var body: some View {
Text(name)
}
}

SwitfUI: access the specific scene's ViewModel on 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)")
})
}
}
}

Updating SwiftUI View based on Observable in Preview

Trying to implement a Login screen in SwiftUI. Based on other similar questions, I'm going the approach of using an Observable EnvironmentObject and a ViewBuilder in the main ContentView that reacts to that and displays the appropriate screen.
However, even though the property is updating as expecting the view never changes in Preview. Everything works fine when built and run in the Simulator but in Preview the change never happens.
Below is the code reduced to the smallest possible example in a single file (only missing passing the environment object in SceneDelegate, which doesn't affect Preview anyway).
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
#ViewBuilder
var body: some View {
if !userAuth.person.isLoggedin {
FirstView()
} else {
SecondView()
} }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
struct Person {
var isLoggedin: Bool
init() {
self.isLoggedin = false
}
}
class UserAuth: ObservableObject {
#Published var person: Person
init(){
self.person = Person()
}
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
willChange.send(self)
self.person.isLoggedin = true
didChange.send(self)
}
}
struct SecondView: View {
var body: some View {
Text("Second View!")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView().environmentObject(UserAuth())
}
}
struct FirstView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
VStack {
Button(action: {
self.userAuth.login()
}) {
Text("Login")
}
Text("Logged in: " + String(self.userAuth.person.isLoggedin))
}
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView().environmentObject(UserAuth())
}
}
EDIT: Based on the answer below, I've added the environment object to the interior views, but unfortunately the view still doesn't change in Preview mode.
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView().environmentObject(UserAuth())
}
}
environment object must be set in PreviewProvider as well
UPDATE
struct ContentView: View {
#ObservedObject var userAuth = UserAuth () // #ObservedObject
var body: some View {
NavigationView{ // Navigation
}.environmentObject(UserAuth) //.environmentObject
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
struct SecondView: View {
#EnvironmentObject var userAuth: UserAuth // only EnvironmentObject
var body: some View {
Text("Second View!")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView().environmentObject(UserAuth())
}
}
The issue I was having with Canvas not giving previews is that my ObservableObject was reading from User Defaults
#Published var fName: String = Foundation.UserDefaults.standard.string(forKey: "fName") !
{ didSet {
Foundation.UserDefaults.standard.set(self.fName, forKey: "fName")
}
}
So works in simulator and on device but no Canvas Previews. I tried many ways to give Preview data to use since Preview can't read from UserDefaults (not a device), and realized I can put an initial/ default value if the UserDefault is not there:
#Published var fName: String = Foundation.UserDefaults.standard.string(forKey: "fName") ?? "Sean"
{ didSet {
Foundation.UserDefaults.standard.set(self.fName, forKey: "fName")
}
}
Now Preview/ Canvas is showing my view and I can continue coding with my Observable Object. The aim was to put in the Observable Object some default code to use.

Resources