SwiftUI Glitches in FormView - swift

I am facing some weird glitches in my Form implementation. I am not sure whether this is some implementation error or a bug in SwiftUI itself.
So basically what I want to do is the typical "ToDo/TimeTracking" app. I want to create a new task entity referenced to a project entity. Therefore I created a Form where I can select a project, set the title and notes of the task as well as the start and end date.
But now I am experiencing some visual glitches.
When I select a project with the Picker I am getting navigated to a separate view (as expected) but the list from which I can select the the project is moving a little bit upwards after the page transition animation as finished.
When selecting "Already finished" and then selecting a date from the DatePicker and closing the picker, the picker glitches around inside the view before it gets hidden.
A minimal viable example can be achieved with the following code snippet:
mport SwiftUI
struct Project {
let id: Int
let title: String
}
let projects = [Project(id: 0, title: "Hello"),Project(id: 1, title: "World")]
struct ContentView: View {
#State private var showingCreateTask = false
var body: some View {
NavigationView {
Text("Hello, World!")
.navigationBarTitle("Test")
.navigationBarItems(trailing: Button(action: { self.showingCreateTask.toggle() }) {
Image(systemName: "plus").imageScale(.large)
})
.sheet(isPresented: self.$showingCreateTask) {
CreateTaskView(projects: projects)
}
}
}
}
struct CreateTaskView: View {
let projects: [Project]
#State private var selectedProject = 0
#State private var taskTitle: String = ""
#State private var taskNotes: String = ""
#State private var alreadyFinished = false
#State private var startDate = Date()
#State private var endDate = Date()
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedProject, label: Text("Project")) {
ForEach(0..<self.projects.count) { index in
Text(self.projects[index].title)
}
}
}
Section {
TextField("Title", text: $taskTitle)
TextField("Notes", text: $taskNotes)
Toggle(isOn: $alreadyFinished) {
Text("Already finished ?")
}
}
if alreadyFinished {
Section {
DatePicker(selection: $startDate, displayedComponents: [.date, .hourAndMinute]) {
Text("Start Date")
}
DatePicker(selection: $endDate, in: startDate..., displayedComponents: [.date, .hourAndMinute]) {
Text("End Date")
}
}
}
Button(action: {
}) {
Text("Save changes")
}
}
.navigationBarTitle("Create a new task")
}
}
}
Maybe someone has experienced something similar and knows whether this is an SwiftUI Bug or some error in my code. Any help is appreciated.

You may improve it like this:
NavigationView {
Form {
Section {
Picker(selection: $selectedProject, label: Text("Project")) {
ForEach(0..<self.projects.count) { index in
Text(self.projects[index].title).tag(index)
}
}
}
Section {
TextField("Title", text: $taskTitle)
TextField("Notes", text: $taskNotes)
Toggle(isOn: $alreadyFinished) {
Text("Already finished ?")
}
}
Button(action: {
}) {
Text("Save changes")
}
if alreadyFinished {
Section {
DatePicker(selection: $startDate,in: startDate..., displayedComponents: [.date, .hourAndMinute]) {
Text("Start Date")
}}
Section {
DatePicker(selection: $endDate, in: startDate..., displayedComponents: [.date, .hourAndMinute]) {
Text("End Date")
}
}.transition(AnyTransition.identity.animation(nil))
}
}
.navigationBarTitle("Create a new task", displayMode: .inline)
}

Related

How can I use a variable to make a list in a form?

I have my variables working and I have an array I want to cycle through to add text to a list of each of the variables in my array. Any ideas on what im doing wrong and how I can fix it? Thanks.
Here is my code:
struct ContentView: View {
#State var enteredText = ""
#State var textItems: [String] = []
private func updateTextItems(){
textItems.append(enteredText)
print("---------------")
for i in textItems {
print(i)
}
print("---------------")
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Add Item to Array")) {
VStack {
TextField("Enter Text", text: $enteredText)
Button("Save", action: updateTextItems)
.buttonStyle(.bordered)
}
Section(header: Text("Items")){
ForEach(textItems) {textItems in
HStack{
Text(textItems)
}
}
}
}
}
.navigationTitle("Array Test")
}
}
}
first of all that foreach statement needs to fire compile error :). I think this one is working :)
struct TestView: View {
#State var enteredText = ""
#State var textItems: [String] = []
private func updateTextItems(){
textItems.append(enteredText)
print("---------------")
for i in textItems {
print(i)
}
print("---------------")
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Add Item to Array")) {
VStack {
TextField("Enter Text", text: $enteredText)
Button("Save", action: updateTextItems)
.buttonStyle(.bordered)
}
Section(header: Text("Items")){
ForEach(textItems, id: \.self) {textItems in
HStack{
Text(textItems)
}
}
}
}
}
.navigationTitle("Array Test")
}
}
}

TextField in a list not working well in SwiftUI

This problem is with SwiftUI for a iPhone 12 app, Using xcode 13.1.
I build a List with TextField in each row, but every time i try to edit the contents, it is only allow me tap one time and enter only one character then can not keep enter characters anymore, unless i tap again then enter another one character.Did i write something code wrong with it?
class PieChartViewModel: ObservableObject, Identifiable {
#Published var options = ["How are you", "你好", "Let's go to zoo", "OKKKKK", "什麼情況??", "yesssss", "二百五", "明天見"]
}
struct OptionsView: View {
#ObservedObject var viewModel: PieChartViewModel
var body: some View {
NavigationView {
List {
ForEach($viewModel.options, id: \.self) { $option in
TextField(option, text: $option)
}
}
.navigationTitle("Options")
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
addNewOption()
} label: {
HStack {
Image(systemName: "plus")
Text("Create a new option")
}
}
}
}
}
}
func addNewOption() {
viewModel.options.insert("", at: viewModel.options.count)
}
}
struct OptionsView_Previews: PreviewProvider {
static var previews: some View {
let pieChart = PieChartViewModel()
OptionsView(viewModel: pieChart)
}
}
Welcome to StackOverflow! Your issue is that you are directly updating an ObservableObject in the TextField. Every change you make to the model, causes a redraw of your view, which, of course, kicks your focus from the TextField. The easiest answer is to implement your own Binding on the TextField. That will cause the model to update, without constantly redrawing your view:
struct OptionsView: View {
// You should be using #StateObject instead of #ObservedObject, but either should work.
#StateObject var model = PieChartViewModel()
#State var newText = ""
var body: some View {
NavigationView {
VStack {
List {
ForEach(model.options, id: \.self) { option in
Text(option)
}
}
List {
//Using Array(zip()) allows you to sort by the element, but use the index.
//This matters if you are rearranging or deleting the elements in a list.
ForEach(Array(zip(model.options, model.options.indices)), id: \.0) { option, index in
// Binding implemented here.
TextField(option, text: Binding<String>(
get: {
model.options[index]
},
set: { newValue in
//You can't update the model here because you will get the same behavior
//that you were getting in the first place.
newText = newValue
}))
.onSubmit {
//The model is updated here.
model.options[index] = newText
newText = ""
}
}
}
.navigationTitle("Options")
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
addNewOption()
} label: {
HStack {
Image(systemName: "plus")
Text("Create a new option")
}
}
}
}
}
}
}
func addNewOption() {
model.options.insert("", at: model.options.count)
}
}

SwiftUI - Form Picker - How to prevent navigating back on selected?

I'm implementing Form and Picker with SwiftUI. There is a problem that it automatically navigates back to Form screen when I select a Picker option, how to keep it stay in selection screen?
Code:
struct ContentView: View {
#State private var selectedStrength = "Mild"
let strengths = ["Mild", "Medium", "Mature"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Strength", selection: $selectedStrength) {
ForEach(strengths, id: \.self) {
Text($0)
}
}
}
}
.navigationTitle("Select your cheese")
}
}
}
Actual:
Expect: (sample from Iphone Settings)
You may have to make a custom view that mimics what the picker looks like:
struct ContentView: View {
let strengths = ["Mild", "Medium", "Mature"]
#State private var selectedStrength = "Mild"
var body: some View {
NavigationView {
Form {
Section {
NavigationLink(destination: CheesePickerView(strengths: strengths, selectedStrength: $selectedStrength)) {
HStack {
Text("Strength")
Spacer()
Text(selectedStrength)
.foregroundColor(.gray)
}
}
}
}
.navigationTitle("Select your cheese")
}
}
}
struct CheesePickerView: View {
let strengths: [String]
#Binding var selectedStrength: String
var body: some View {
Form {
Section {
ForEach(0..<strengths.count){ index in
HStack {
Button(action: {
selectedStrength = strengths[index]
}) {
HStack{
Text(strengths[index])
.foregroundColor(.black)
Spacer()
if selectedStrength == strengths[index] {
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
}
}
}

Edit details of a dynamic list of core data object

I used the Xcode default CoreData template to build my app.
I have tried to use CoreData and create an entity like this:
I then created a AddItemView which allows me to add item to the view.
struct AddItemView: View {
#Environment(\.managedObjectContext) var viewContext
#Environment(\.presentationMode) var presentationMode
#State private var notes = ""
#State private var selectedDate = Date()
var body: some View {
NavigationView {
Form {
Section {
TextField("notes", text: $notes)
}
Section {
DatePicker("", selection: $selectedDate, displayedComponents: .date)
Text("Your selected date: \(selectedDate)")
}
Section {
Button("Save") {
let newItem = Item(context: self.viewContext)
newItem.notes = self.notes
newItem.recordDate = self.selectedDate
newItem.timestamp = Date()
try? self.viewContext.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Add Item")
}
}
}
It works well and can add items.
Then I want to click on each of the item to go to a Detail View. In the DetailView, there should be an edit button to allow me to modify the object.
I therefore created three files for the purpose: ItemHost, DetailView, EditorView
The Navigation Destination of the item will go to the ItemHost.
struct ItemListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
#State private var showingAddScreen = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: ItemHost(item: item)) {
VStack {
Text("Item at \(item.timestamp!, formatter: FormatterUtility.dateTimeFormatter)")
Text("notes: \(item.notes ?? "")")
Text("Item Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
#if os(iOS)
EditButton()
#endif
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {self.showingAddScreen.toggle()}) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(isPresented: $showingAddScreen) {
AddItemView().environment(\.managedObjectContext, self.viewContext)
}
}
}
The ItemHost as follows:
struct ItemHost: View {
#Environment(\.editMode) var editMode
#Environment(\.managedObjectContext) var contextView
#State var item: Item
var body: some View {
NavigationView {
if editMode?.wrappedValue == .active {
Button("Cancel") {
editMode?.animation().wrappedValue = .inactive
}
}
if editMode?.wrappedValue == .inactive {
ItemDetailView(item: item)
} else {
ItemEditor(item: item)
}
}.navigationBarTitle("EditMode Problem")
.navigationBarItems(trailing: EditButton())
}
}
The DetailView is just a view to display the details, without any special.
struct ItemDetailView: View {
#Environment(\.managedObjectContext) var contextView
#Environment(\.presentationMode) var presentationMode
#State private var showingDeleteAlert = false
let item: Item
var body: some View {
VStack {
Text("notes: \(item.notes ?? "")")
Text("Record Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
.navigationBarTitle(Text("Item Detail"), displayMode: .inline)
.alert(isPresented: $showingDeleteAlert) {
Alert(title: Text("Delete Item"), message: Text("Are you sure?"),
primaryButton: .destructive(Text("Delete")) {
self.deleteItem()
}, secondaryButton: .cancel()
)
}
.navigationBarItems(trailing: Button(action: {
self.showingDeleteAlert = true
}) {
Image(systemName: "trash")
})
}
// Problem here
// Can delete the item and go back to list page. But the actual item in the CoreData has not been removed. If I call contextView.save() it will crash.
func deleteItem() {
contextView.delete(item)
presentationMode.wrappedValue.dismiss()
}
}
The EditorView like this:
struct ItemEditor: View {
#Environment(\.presentationMode) var presentation
#State var item: Item
var body: some View {
List {
HStack {
Text("Notes").bold()
TextField("Notes", text: $item.notes) // Error
}
// Error
DatePicker(selection: $item.recordDate, displayedComponents: .date) {
Text("Record Date").bold()
}
}
}
}
A few problem here:
ItemEditor: Cannot convert value of type 'Binding<String?>' to expected argument type 'Binding'. I have no way to pick the original item object values and display it to let the user know what was the old value inside the object.
Nothing to be displayed once I click on the individual navigation item. I expect that it will originally (not edit mode) and then show the detail view. If it is edit mode, then show the editor.
I get confused with the #binding and how to pass the item into the DetailView and also the Editor. How the editor save the data back to the item object in the contextView?
For the deleteItem() in the ItemDetailView. It can remove the item and go back to the ItemListView apparently. However, when I quit the app, and then run again. I found that the item re-appeared again, not really deleted.
Click on the item now, it shows this:
Don't use #State to var Item in Core Data. You should use #ObservedObject instead. It will refresh a parent view after updating data.
Please read this article:
https://purple.telstra.com/blog/swiftui---state-vs--stateobject-vs--observedobject-vs--environme

Is it possible to show a Picker permanently in a Form in SwiftUI?

I have a simple question for Pickers in Forms and Sections: Is it possible to show a Picker permanently in the WheelPickerStyle in this construct?
struct ContentView: View {
var body: some View {
Form {
Section {
DatePicker(selection:.constant(Date()), label: { Text("") })
}.pickerStyle(WheelPickerStyle())
}
}
}
Here is how it should be
Form {
Section {
DatePicker(selection:.constant(Date()), label: { Text("") })
.datePickerStyle(WheelDatePickerStyle())
}
}
or in variant with tracking state:
#State private var date = Date()
var body: some View {
Form {
Section {
DatePicker(selection:$date, label: { Text("") })
.datePickerStyle(WheelDatePickerStyle())
}
}
}