macOS SwiftUI Textfield gets deleted after losing focus - macos

I have a strange issue where the textfield gets deleted after selecting another textfield.
I have an EnvironmentObject
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let shellInteractor = ShellInteractor()
let contentView = ContentView().environmentObject(shellInteractor)
}
injected in the view
struct ContentView: View {
#EnvironmentObject var shellInteractor: ShellInteractor
var body: some View {
ScrollView {
VStack {
HStack {
Text("Enter target bundle identifier:")
TextField("com.mycompany.app", text: $shellInteractor.bundleId)
}.padding()
HStack {
Text("Enter icon badge count:")
TextField("0", text: $shellInteractor.badgeNumber)
}.padding()
HStack {
Text("Enter message identifier:")
TextField("ABCDEFGHIJ", text: $shellInteractor.messageId)
}.padding()
Text("Found Running Sim: ")
Text(self.shellInteractor.shellOutput).fontWeight(.semibold)
Button(action: {
self.shellInteractor.sendNotification()
}) {
Text("SEND!!!")
.fontWeight(.semibold)
}.padding()
}.padding()
}
}
}
class ShellInteractor: ObservableObject {
#Published var shellOutput: String = ""
public var badgeNumber: String = ""
public var messageId: String = ""
public var bundleId: String = ""
}
As I said, when I enter a text in any of the textfields and select another text field or tap the TAB key (basically when losing focus), the textfield deletes the text and shows the placeholder again.

update your model
class ShellInteractor: ObservableObject {
#Published var shellOutput: String = ""
#Published var badgeNumber: String = ""
#Published var messageId: String = ""
#Published var bundleId: String = ""
}

Related

Allow custom tap gesture in List but maintain default selection gesture

I'm trying to create a List that allows multiple selection. Each row can be edited but the issue is that since there's a tap gesture on the Text element, the list is unable to select the item.
Here's some code:
import SwiftUI
struct Person: Identifiable {
let id: UUID
let name: String
init(_ name: String) {
self.id = UUID()
self.name = name
}
}
struct ContentView: View {
#State private var persons = [Person("Peter"), Person("Jack"), Person("Sophia"), Person("Helen")]
#State private var selectedPersons = Set<Person.ID>()
var body: some View {
VStack {
List(selection: $selectedPersons) {
ForEach(persons) { person in
PersonView(person: person, selection: $selectedPersons) { newValue in
// ...
}
}
}
}
.padding()
}
}
struct PersonView: View {
var person: Person
#Binding var selection: Set<Person.ID>
var onCommit: (String) -> Void = { newValue in }
#State private var isEditing = false
#State private var newValue = ""
#FocusState private var isInputActive: Bool
var body: some View {
if isEditing {
TextField("", text: $newValue, onCommit: {
onCommit(newValue)
isEditing = false
})
.focused($isInputActive)
.labelsHidden()
}
else {
Text(person.name)
.onTapGesture {
if selection.contains(person.id), selection.count == 1 {
newValue = person.name
isEditing = true
isInputActive = true
}
}
}
}
}
Right now, you need to tap on the row anywhere but on the text to select it. Then, if you tap on the text it'll go in edit mode.
Is there a way to let the list do its selection? I tried wrapping the tap gesture in simultaneousGesture but that didn't work.
Thanks!

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.

SwiftUI presenting sheet with Binding variable doesn't work when first presented

I'm trying to present a View in a sheet with a #Binding String variable that just shows/binds this variable in a TextField.
In my main ContentView I have an Array of Strings which I display with a ForEach looping over the indices of the Array, showing a Button each with the text of the looped-over-element.
The Buttons action is simple: set an #State "index"-variable to the pressed Buttons' Element-index and show the sheet.
Here is my ContentView:
struct ContentView: View {
#State var array = ["first", "second", "third"]
#State var showIndex = 0
#State var showSheet = false
var body: some View {
VStack {
ForEach (0 ..< array.count, id:\.self) { i in
Button("\(array[i])") {
showIndex = i
showSheet = true
}
}
// Text("\(showIndex)") // if I uncomment this line, it works!
}
.sheet(isPresented: $showSheet, content: {
SheetView(text: $array[showIndex])
})
.padding()
}
}
And here is the SheetView:
struct SheetView: View {
#Binding var text: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text)
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
The problem is, when I first open the app and press on the "second" Button, the sheet opens and displays "first" in the TextField. I can then dismiss the Sheet and press the "second" Button again with the same result.
If I then press the "third" or "first" Button everything works from then on. Pressing any Button results in the correct behaviour.
Preview
Interestingly, if I uncomment the line with the Text showing the showIndex-variable, it works from the first time on.
Is this a bug, or am I doing something wrong here?
You should use custom Binding, custom Struct for solving the issue, it is complex issue. See the Example:
struct ContentView: View {
#State private var array: [String] = ["first", "second", "third"]
#State private var customStruct: CustomStruct?
var body: some View {
VStack {
ForEach (array.indices, id:\.self) { index in
Button(action: { customStruct = CustomStruct(int: index) }, label: {
Text(array[index]).frame(width: 100)
})
}
}
.frame(width: 300, height: 300, alignment: .center)
.background(Color.gray.opacity(0.5))
.sheet(item: $customStruct, content: { item in SheetView(text: Binding.init(get: { () -> String in return array[item.int] },
set: { (newValue) in array[item.int] = newValue }) ) })
}
}
struct CustomStruct: Identifiable {
let id: UUID = UUID()
var int: Int
}
struct SheetView: View {
#Binding var text: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text)
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
I had this happen to me before. I believe it is a bug, in that until it is used in the UI, it doesn't seem to get set in the ForEach. I fixed it essentially in the same way you did, with a bit of subtlety. Use it in each Button as part of the Label but hide it like so:
Button(action: {
showIndex = i
showSheet = true
}, label: {
HStack {
Text("\(array[i])")
Text(showIndex.description)
.hidden()
}
})
This doesn't change your UI, but you use it so it gets properly updated. I can't seem to find where I had the issue in my app, and I have changed the UI to get away from this, but I can't remember how I did it. I will update this if I can find it. This is a bit of a kludge, but it works.
Passing a binding to the index fix the issue like this
struct ContentView: View {
#State var array = ["First", "Second", "Third"]
#State var showIndex: Int = 0
#State var showSheet = false
var body: some View {
VStack {
ForEach (0 ..< array.count, id:\.self) { i in
Button(action:{
showIndex = i
showSheet.toggle()
})
{
Text("\(array[i])")
}.sheet(isPresented: $showSheet){
SheetView(text: $array, index: $showIndex)
}
}
}
.padding()
}
}
struct SheetView: View {
#Binding var text: [String]
#Binding var index: Int
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text[index])
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
In SwiftUI2 when calling isPresented if you don't pass bindings you're going to have some weird issues.
This is a simple tweak if you want to keep it with the isPresented and make it work but i would advise you to use the item with a costum struct like the answer of swiftPunk
This is how I would do it. You'll lose your form edits if you don't use #State variables.
This Code is Untested
struct SheetView: View {
#Binding var text: String
#State var draft: String
#Environment(\.presentationMode) var presentationMode
init(text: Binding<String>) {
self._text = text
self._draft = State(initialValue: text.wrappedValue)
}
var body: some View {
VStack {
TextField("text:", text: $draft)
Button("dismiss") {
text = draft
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}

.onTapGesture Get Scrollview Item Data

My full project is here https://github.com/m3rtkoksal/TaskManager
I have made SelectedTask an environment object as below
let context = persistentContainer.viewContext
let contentView = ContentView()
.environmentObject(observer())
.environmentObject(SelectedTask())
.environment(\.managedObjectContext,context)
In my TaskElement model I have created another class called SelectedTask as below
class SelectedTask: ObservableObject {
#Published var item = [TaskElement]()
func appendNewTask(task: TaskElement) {
objectWillChange.send()
item.append(TaskElement(title: task.title, dateFrom: task.dateFrom , dateTo: task.dateTo , text: task.text))
}
}
I am trying to fetch an item inside the scroll view and get its data to be able to modify it in the NewTaskView as below
struct ScrollViewTask: View {
#ObservedObject private var obser = observer()
#EnvironmentObject var selectedTask : SelectedTask
#State var shown: Bool = false
var body: some View {
ScrollView(.vertical) {
VStack {
ForEach(self.obser.tasks) { task in
TaskElementView(task:task)
.onTapGesture {
self.selectedTask.objectWillChange.send()
self.selectedTask.appendNewTask(task: task) //THREAD 1 ERROR
print(task)
self.shown.toggle()
}
}
}
}
.onAppear {
self.obser.fetchData()
}
.fullScreenCover(isPresented: $shown, content: {
NewTaskView(isShown: $shown)
.environmentObject(selectedTask)
})
}
}
But when I tap one of the items in scrollview I am getting a Thread 1 error #self.selectedTask.appendNewTask(task: task)
Thread 1: Fatal error: No ObservableObject of type SelectedTask found. A View.environmentObject(_:) for SelectedTask may be missing as an ancestor of this view.
If I change as ScrollViewTask().environmentObject(self.obser)
then this happens
This is how my TaskFrameView is called
import SwiftUI
struct TaskListView: View {
#State private(set) var data = ""
#State var isSettings: Bool = false
#State var isSaved: Bool = false
#State var shown: Bool = false
#State var selectedTask = TaskElement(title: "", dateFrom: "", dateTo: "", text: "")
var body: some View {
NavigationView {
ZStack {
Color(#colorLiteral(red: 0.9333333333, green: 0.9450980392, blue: 0.9882352941, alpha: 1)).edgesIgnoringSafeArea(.all)
VStack {
TopBar()
HStack {...}
CustomSegmentedView()
ZStack {
TaskFrameView() // scrollview inside
VStack {
Spacer()
HStack {...}
}
NavigationLink(
destination: NewTaskView(isShown: $shown).environmentObject(selectedTask),
isActive: $shown,
label: {
Text("")
})
}
}
}
.navigationBarHidden(true)
Spacer()
}
.navigationBarHidden(true)
}
}
It looks like the selectedTask is not injected to the TaskListView.
Find the place where you call TaskListView() and inject the selectedTask as an EnvironmentObject.
In ContentView:
struct ContentView: View {
#EnvironmentObject var selectedTask : SelectedTask
...
TaskListView().environmentObject(selectedTask)
Also don't create new instances of selectedTask like:
#State var selectedTask = TaskElement(title: "", dateFrom: "", dateTo: "", text: "")
Get the already created instance from the environment instead:
#EnvironmentObject var selectedTask: SelectedTask
call scroll view by sending the observer object as environment object modifier
ScrollViewTask().environmentObject(self. observer)

Resources