Binding’s inside NavigationSplitView detail (TextField, TextEditor) - swift

I'm using a two-column NavigationSplitView. Trying to figure out how to update the data model via .onSubmit modifier and use a TextField view without Binding.constant.
Within the detail section, I have TextField and TextEditor.
How to avoid Binding.contant()? I mean, I need mutation.
This is a correct way to update value property in Model?
I need a single selection in List.
Here's my sample code (70 line’s):
struct Model: Identifiable, Hashable {
var id = UUID()
var title: String = "Brand new"
var value: String = ""
func updateValue() async -> Model {
return Model(id: id, title: title, value: "The boar 🐗 is running through the field happily")
}
}
final class DataModel: ObservableObject {
#Published
var models: [Model] = [
.init(title: "First", value: "fur"),
.init(title: "Second", value: "meow"),
.init(title: "Another", value: "Make SwiftUI, not war")
]
#MainActor
func updateModel(for model: Model.ID) async -> Void {
var findModel = models.first { $0.id == model }
findModel = await findModel?.updateValue()
}
}
struct ModelView: View {
#StateObject
private var dataModel = DataModel()
#State
private var listSelection: Model.ID?
private var selectedModel: Model? {
guard let selection = listSelection else { return nil }
return dataModel.models.first { $0.id == selection }
}
var body: some View {
NavigationSplitView {
List(dataModel.models, selection: $listSelection) { model in
NavigationLink(model.title, value: model.id)
}
} detail: {
if let selectedModel {
VStack {
TextField("Title", text: .constant(selectedModel.title))
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
.submitLabel(.go)
.onSubmit {
Task {
// Update Model.value by hit `Go`
await dataModel.updateModel(for: selectedModel.id)
}
}
TextEditor(text: .constant(selectedModel.value))
}
.padding()
.navigationTitle(selectedModel.title)
}
}
}
}
struct ModelView_Previews: PreviewProvider {
static var previews: some View {
ModelView()
.colorScheme(.light)
}
}

After a couple of days, I realized what I could do.
No one answered the question, so I solved the problem this way.
The final solution is below:
struct Model: Identifiable, Hashable {
var id = UUID()
var title: String = "Brand new"
var value: String = ""
func updateValue() async -> Model {
return Model(id: id, title: title, value: "The boar 🐗 is running through the field happily")
}
}
final class DataModel: ObservableObject {
#Published
var models: [Model] = [
.init(title: "First", value: "fur"),
.init(title: "Second", value: "meow"),
.init(title: "Another", value: "Make SwiftUI, not war")
]
#MainActor
func updateModel(for model: Binding<Model>) async -> Void {
model.wrappedValue = await model.wrappedValue.updateValue()
}
func bindingToModel(_ model: Model.ID) -> Binding<Model> {
Binding<Model> {
guard let index = self.models.firstIndex(where: { $0.id == model }) else {
return Model()
}
return self.models[index]
} set: { newModel in
guard let index = self.models.firstIndex(where: { $0.id == model }) else { return }
self.models[index] = newModel
}
}
}
struct ModelView: View {
#StateObject
private var dataModel = DataModel()
#State
private var listSelection: Model.ID?
var body: some View {
NavigationSplitView {
List(dataModel.models, selection: $listSelection) { model in
NavigationLink(model.title, value: model.id)
}
} detail: {
if let listSelection, let bindModel = dataModel.bindingToModel(listSelection) {
VStack {
TextField("Title", text: bindModel.title)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
.submitLabel(.go)
.onSubmit {
Task {
// Update Model.value by hit `Go`
await dataModel.updateModel(for: bindModel)
}
}
TextEditor(text: bindModel.value)
}
.padding()
.navigationTitle(bindModel.title)
}
}
}
}

Related

NavigationLink In NavigationStack is disabled

I wrote test code for NavigationStack. The behavior of the code is a two-step transition(ContentView -> SubsubTestView -> DetailView).
But I got an error when I have selected a name in SubsubTestView.
A NavigationLink is presenting a value of type “User” but there is no matching navigationDestination declaration visible from the location of the link. The link cannot be activated.
Is there anything wrong with the wrong part of the code?
ContentView.swift
import SwiftUI
class EnvObj: ObservableObject {
#Published var users = [User(name: "a"), User(name: "b"), User(name: "c")]
}
struct User: Hashable, Identifiable, Equatable {
var id = UUID()
var name = ""
static func == (lhs: User, rhs: User) -> Bool{
return lhs.id == rhs.id
}
}
struct ContentView: View {
#EnvironmentObject var envObj: EnvObj
#State var moveToSubsub = false
var body: some View {
NavigationStack {
Button("To subsub") {
moveToSubsub = true
}
.navigationDestination(isPresented: $moveToSubsub) {
SubsubTestView(vm: VM(envObj: envObj))
}
}
}
}
struct SubsubTestView: View {
#EnvironmentObject var envObj: EnvObj
#StateObject var vm: VM
var body: some View {
VStack {
List(self.vm.envObj.users) { user in
NavigationLink(value: user) {
Text(user.name)
}
}
.navigationDestination(for: User.self) { user in
DetailView(vm: VMD(envObj: envObj, selectedUser: user))
}
}
}
}
class VM: ObservableObject {
var envObj: EnvObj = .init()
init(envObj: EnvObj) {
self.envObj = envObj
}
}
struct DetailView: View {
#StateObject var vm: VMD
var body: some View {
VStack {
TextField("Name: ", text: (self.$vm.selectedUser ?? User()).name)
Text(self.vm.selectedUser?.name ?? User().name)
Button("submit", action: self.vm.submit)
}
}
}
class VMD: ObservableObject {
var envObj: EnvObj = .init()
#Published var selectedUser: User?
init(envObj: EnvObj, selectedUser: User? = nil) {
self.envObj = envObj
self.selectedUser = selectedUser
}
private(set) lazy var submit = {
if let user = self.selectedUser {
self.update(user: user)
}
}
func update(user: User) {
self.envObj.users = self.envObj.users.map {
return $0 == user ? user : $0
}
}
}
func ??<T>(binding: Binding<T?>, fallback: T) -> Binding<T> {
return Binding(get: {
binding.wrappedValue ?? fallback
}, set: {
binding.wrappedValue = $0
})
}
Thanks,

why data are passing back but SwiftUi not updating Text

I get to pass back data via closure, so new name is passed, but my UI is not updating. The new name of the user is printed when I go back to original view, but the text above the button is not getting that new value.
In my mind, updating startingUser should be enough to update the ContentView.
my ContentView:
#State private var startingUser: UserData?
var body: some View {
VStack {
Text(startingUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser?.name)")
}
}
}
my EditView:
struct DetailView: View {
#Environment(\.dismiss) var dismiss
var user: UserData
var callBackClosure: (UserData) -> Void
#State private var name: String
var body: some View {
NavigationView {
Form {
TextField("your name", text: $name)
}
.navigationTitle("edit view")
.toolbar {
Button("dismiss") {
var newData = self.user
newData.name = name
newData.id = UUID()
callBackClosure(newData)
dismiss()
}
}
}
}
init(user: UserData, callBackClosure: #escaping (UserData) -> Void ) {
self.user = user
self.callBackClosure = callBackClosure
_name = State(initialValue: user.name)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(user: UserData.example) { _ in}
}
}
my model
struct UserData: Identifiable, Codable, Equatable {
var id = UUID()
var name: String
static let example = UserData(name: "Luke")
static func == (lhs: UserData, rhs: UserData) -> Bool {
lhs.id == rhs.id
}
}
update
using these changes solves the matter, but my question remains valid, cannot understand the right reason why old code not working, on other projects, where sheet and text depends on the same #state var it is working.
adding
#State private var show = false
adding
.onTapGesture {
startingUser = UserData(name: "Start User")
show = true
}
changing
.sheet(isPresented: $show) {
DetailView(user: startingUser ?? UserData.example) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser!.name)")
}
}
The reason Text is not showing you the updated user name that you are passing in the closure is, your startingUser property will be set to nil when you dismiss the sheet because you have bind that property with sheet. Now after calling callBackClosure(newData) you are calling dismiss() to dismiss the sheet. To overcome this issue you can try something like this.
struct ContentView: View {
#State private var startingUser: UserData?
#State private var updatedUser: UserData?
var body: some View {
VStack {
Text(updatedUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newUser in
updatedUser = newUser
print("✅ \(updatedUser?.name ?? "no name")")
}
}
}
}
I would suggest you to read the Apple documentation of sheet(item:onDismiss:content:) and check the example from the Discussion section to get more understanding.

Animation not working when pass single object to the view in SwiftUI

I have a rather strange problem with animation on adding elements to list in SwiftUI. I have a simple model class:
struct ShoppingList: Identifiable, Equatable {
let id: UUID
var name: String
var icon: String
var items: [Item]
init(id: UUID = UUID(), name: String, icon: String, items: [Item] = []) {
self.id = id
self.name = name
self.icon = icon
self.items = items
}
static func == (lhs: ShoppingList, rhs: ShoppingList) -> Bool {
return lhs.id == rhs.id
}
}
extension ShoppingList {
struct Item: Identifiable, Equatable {
let id: UUID
var name: String
init(id: UUID = UUID(), name: String) {
self.id = id
self.name = name
}
static func == (lhs: Item, rhs: Item) -> Bool {
return lhs.id == rhs.id
}
}
}
When I pass single ShoppingList object as binding to the view, adding animation basically doesn't happen at all:
Here is code of my view:
struct ShoppingListDetailView: View {
#Binding var shoppingList: ShoppingList
#State private var newItemName = ""
var body: some View {
List {
ForEach($shoppingList.items) { $item in
Text(item.name)
}
HStack {
TextField("Add item", text: $newItem)
Button(action: addNewItem) {
Image(systemName: "plus.circle.fill")
}
.disabled(newItemName.isEmpty)
}
}
.navigationTitle(shoppingList.name)
}
private func addNewItem() {
withAnimation {
let newItem = ShoppingList.Item(name: newItemName)
shoppingList.items.append(newItem)
}
}
}
And here is code of a parent view:
struct ShoppingListsView: View {
#Binding var shoppingLists: [ShoppingList]
var body: some View {
List {
ForEach($shoppingLists) { $list in
NavigationLink(destination: ShoppingListDetailView(shoppingList: $list)) {
ShoppingListItemView(shoppingList: $list)
}
}
}
.navigationTitle("Shopping List")
}
}
But once I pass whole list of ShoppingList objects and index for a particular one, everything works as expected:
Code with passing list to the view looks like that:
struct ShoppingListDetailView: View {
#Binding var shoppingList: [ShoppingList]
var index: Int
#State private var newItemName = ""
var body: some View {
List {
ForEach($shoppingList[index].items) { $item in
Text(item.name)
}
HStack {
TextField("Add item", text: $newItem)
Button(action: addNewItem) {
Image(systemName: "plus.circle.fill")
}
.disabled(newItemName.isEmpty)
}
}
.navigationTitle(shoppingList[index].name)
}
private func addNewItem() {
withAnimation {
let newItem = ShoppingList.Item(name: newItemName)
shoppingList[index].items.append(newItem)
}
}
}
And of course parent view:
struct ShoppingListsView: View {
#Binding var shoppingLists: [ShoppingList]
var body: some View {
List {
ForEach($shoppingLists) { $list in
NavigationLink(destination: ShoppingListDetailView(shoppingList: $shoppingLists, index: shoppingLists.firstIndex(of: list)!)) {
ShoppingListItemView(shoppingList: $list)
}
}
}
.navigationTitle("Shopping List")
}
}
I'm new to Swift (not to mention SwiftUI) and I have no idea what might be wrong here. Any ideas?

TextField in SwiftUI loses focus when I enter a character

I have a problem when I enter a character within a TextField (Within ExerciseSetView), I have to re-click the text box to make it so I can enter another character. If I remove the bindings from the Textfield I can enter text fluidly.
I think it has something to do with my presenter class and the updateSet function recreating a set instance because I have to replace some values two levels deep within an array.
//
// ContentView.swift
// test
//
//
import SwiftUI
import Combine
import CoreData
class WorkoutExerciseSetVM: Hashable, ObservableObject {
#Published public var id: Int
#Published public var reps: String
#Published public var weight: String
init(id: Int, reps: String, weight: String) {
self.id = id
self.reps = reps
self.weight = weight
}
static func ==(lhs: WorkoutExerciseSetVM, rhs: WorkoutExerciseSetVM) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self)) }
}
class WorkoutExerciseVM: Hashable, ObservableObject {
#Published public var id: UUID
#Published public var name: String
#Published public var sets: [WorkoutExerciseSetVM]
init(id: UUID, name: String, sets: [WorkoutExerciseSetVM]) {
self.id = id
self.name = name
self.sets = sets
}
static func ==(lhs: WorkoutExerciseVM, rhs: WorkoutExerciseVM) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self)) }
}
class WorkoutVM: Hashable, ObservableObject {
#Published public var id = UUID()
#Published public var name: String
#Published public var exercises: [WorkoutExerciseVM]
#Published public var started: Date? = Date()
#Published public var completed: Date? = Date()
init(id: UUID, name: String, exercises: [WorkoutExerciseVM], started: Date?, completed: Date?) {
self.id = id
self.name = name
self.exercises = exercises
self.started = started
self.completed = completed
}
static func ==(lhs: WorkoutVM, rhs: WorkoutVM) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
func hash(into hasher: inout Hasher) { return hasher.combine(ObjectIdentifier(self)) }
}
class WorkoutPresenter: ObservableObject {
#Published public var id: UUID
#Published public var exercises: [WorkoutExerciseVM]
#Published public var name: String
#Published public var started: Date?
#Published public var completed: Date?
init(routine: WorkoutVM) {
self.id = UUID()
self.name = routine.name
self.started = Date()
self.completed = nil
self.exercises = routine.exercises.map{ exercise in
return WorkoutExerciseVM(
id: UUID(),
name: exercise.name,
sets: [
WorkoutExerciseSetVM(id: 1, reps: "0", weight: "0")
]
)
}
}
func removeExercise(id: UUID) {
let exerciseId = id.uuidString;
self.exercises = self.exercises.filter{$0.id.uuidString != exerciseId}
}
func addSet(id: UUID) {
let exerciseId = id.uuidString;
self.exercises = self.exercises.map {
if ($0.id.uuidString == exerciseId) {
if ($0.sets.count == 0) {
$0.sets.append(WorkoutExerciseSetVM(id: 1, reps: "0", weight: "0"))
}
if let lastSet = $0.sets.last {
$0.sets.append(WorkoutExerciseSetVM(id: lastSet.id + 1, reps: lastSet.reps, weight: lastSet.weight))
}
}
return $0
}
}
func updateSet(id: UUID, set: WorkoutExerciseSetVM) {
let exerciseId = id.uuidString
self.exercises = self.exercises.map{
if $0.id.uuidString == exerciseId {
$0.sets = $0.sets.map{(oldExerciseSet) -> WorkoutExerciseSetVM in
if oldExerciseSet.id == set.id {
return set
}
return oldExerciseSet
}
return $0
}
return $0;
}
}
func removeSet(id: UUID) {
let exerciseId = id.uuidString;
self.exercises = self.exercises.map{(exercise) -> WorkoutExerciseVM in
if exercise.id.uuidString == exerciseId {
let newExercise = exercise
if newExercise.sets.count > 1 {
newExercise.sets.removeLast()
}
return newExercise
}
return exercise;
}
}
}
struct ContentView: View {
var body: some View {
VStack {
WorkoutView(presenter: WorkoutPresenter(routine: WorkoutVM(id: UUID(), name: "Test", exercises: [WorkoutExerciseVM(id: UUID(), name: "Exercise", sets: [WorkoutExerciseSetVM(id: 1, reps: "0", weight: "0")])], started: nil, completed: nil)))
}
}
}
struct WorkoutView: View {
#ObservedObject var presenter: WorkoutPresenter
var body: some View {
return GeometryReader { geo in
ZStack {
VStack {
ScrollView {
ForEach(self.presenter.exercises, id: \.self) { exercise in
ExerciseView(presenter: self.presenter, exercise: exercise)
}
}
}
}
}
}
}
struct ExerciseView: View {
#ObservedObject var presenter: WorkoutPresenter
var exercise: WorkoutExerciseVM
var body: some View {
VStack {
VStack(alignment: .leading) {
VStack {
VStack {
ForEach(exercise.sets, id: \.self) { exerciseSet in
ExerciseSetView(
set: exerciseSet,
onUpdate: { newExerciseSet in
self.presenter.updateSet(id: self.exercise.id, set: newExerciseSet)
}
)
}
}
}
}
HStack {
Button(action: {
self.presenter.addSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "plus")
Text("Add Set")
}
}
Button(action: {
self.presenter.removeSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "minus")
Text("Remove Set")
}
}
}
}
}
}
struct ExerciseSetView: View {
var set: WorkoutExerciseSetVM
var onUpdate: (_ set: WorkoutExerciseSetVM) -> Void
var body: some View {
let repBinding = Binding(
get: {
String(self.set.reps)
},
set: {
if ($0 as String?) != nil {
self.onUpdate(WorkoutExerciseSetVM(id: self.set.id, reps: $0 , weight: self.set.weight))
}
}
)
let weightBinding = Binding(
get: {
String(self.set.weight)
},
set: {
if ($0 as String?) != nil {
self.onUpdate(WorkoutExerciseSetVM(id: self.set.id, reps: self.set.reps, weight: $0 ))
}
}
)
return HStack {
Spacer()
// textfield that isn't working
TextField("", text: repBinding)
Spacer()
// textfield that isn't working
TextField("", text: weightBinding)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Your code has some fundamental errors. Please research about ObservableObject and Published values before going into production with this code. Otherwise, it would be quite hard to deal with this code later.
I have updated your views and that seems to work. You are not using ObservableObject as they should be used. Just pass ObservableObjectss around let them do the bindings for you instead of setting custom bindings.
struct ExerciseView: View {
#ObservedObject var presenter: WorkoutPresenter
#ObservedObject var exercise: WorkoutExerciseVM
var body: some View {
VStack {
VStack(alignment: .leading) {
ForEach(exercise.sets, id: \.self) { exerciseSet in
ExerciseSetView(set: exerciseSet)
}
}
HStack {
Button(action: {
self.presenter.addSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "plus")
Text("Add Set")
}
}
Button(action: {
self.presenter.removeSet(id: self.exercise.id)
}) {
HStack {
Image(systemName: "minus")
Text("Remove Set")
}
}
}
}
}
}
struct ExerciseSetView: View {
#ObservedObject var set: WorkoutExerciseSetVM
var body: some View {
HStack {
Spacer()
TextField("", text: $set.reps)
Spacer()
TextField("", text: $set.weight)
}
}
}
Let me know if this works for you.

Update a row in a list (SwiftUI)

I'm an early bird in programming so I know this question can be ridiculous from the point of view of an expert but I'm stuck in this situation from several days.
I would like to update a row by using a button "Edit" (pencil) after having used another button to store the item with a TextField.
Here's the code:
class Food: Hashable, Codable, Equatable {
var id : UUID = UUID()
var name : String
init(name: String) {
self.name = name
}
static func == (lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var shoppingChart: [Food] = []
init() {
let milk = Food(name: "Milk")
let coffee = Food(name: "Coffee")
shoppingChart.append(milk)
shoppingChart.append(coffee)
}
func newFood(name: String) {
let food = Food(name: name)
shoppingChart.insert(food, at: 0)
}
}
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
var body: some View {
VStack {
List {
ForEach(self.dm.shoppingChart, id:\.self) { food in
HStack {
Text(food.name)
Image(systemName: "pencil")
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}
struct Add: View {
#ObservedObject var dm : Manager
#State var fieldtext : String = ""
#Binding var isAddFoodOpened : Bool
var body: some View {
VStack {
TextField("Write a food", text: $fieldtext)
buttonSave
}
}
var buttonSave : some View {
Button(action: {
self.dm.newFood(name: self.fieldtext)
self.isAddFoodOpened = false
}) {
Text("Save")
}
}
}
The #ObservedObject var dm : Manager object is never initialized.
Try initialized dm in ContentView like this:
#ObservedObject var dm = Manager()
Ok, so if I understand correctly you want to update/edit a row by using a button "Edit".
This will do it:
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
#State var isEditOpened = false
#State var fieldtext : String = ""
var body: some View {
VStack {
List {
ForEach(0..<self.dm.shoppingChart.count, id:\.self) { i in
HStack {
Text(self.dm.shoppingChart[i].name)
Button(action: { self.isEditOpened.toggle() }) {
Image(systemName: "pencil")
}.sheet(isPresented: self.$isEditOpened) {
TextField(self.dm.shoppingChart[i].name, text: self.$fieldtext, onEditingChanged: { _ in
self.dm.shoppingChart[i].name = self.fieldtext
})
}
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}