Unwrapping Optional Entity - swiftui

Let's say that Item is a CoreData entity:
struct ItemDetailView: View {
#Binding var item: Item?
#Binding var isEditing: Bool
var body: some View{
if isEditing {
TextField( "Name", text: Binding($item.data)! )
}else{
Text(item!.data!)
}
}
}
Error: Value of optional type 'Item?' must be unwrapped to refer to member 'data' of wrapped base type 'Item'
Edit All the code:
import SwiftUI
import CoreData
struct ItemDetailView: View {
#Binding var item: Item?
#Binding var isEditing: Bool
var body: some View{
if isEditing {
TextField( "Name", text: Binding($item.data)! )
}else{
Text(item!.data!)
}
}
}
struct ItemEditorView: View {
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) private var dismiss
let isNew: Bool
#State var isEditing: Bool = false
#State var item: Item?
#Binding var newItem: Item?
var body: some View {
if isNew {
}
NavigationView{
ItemDetailView(item: isNew ? $newItem : $item, isEditing: $isEditing)
.toolbar {
ToolbarItem {
Button(isNew ? "Add" : (isEditing ? "Done" : "Edit")) {
//TBI
if isNew {
}
}
}
ToolbarItem(placement:.cancellationAction){
Button("Cancel"){
dismiss()
}
}
}
.navigationTitle("Item Editor")
}
}
}
struct ItemsListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.data, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
#State var presentNewItemEditorView = false
#State var newItem: Item?
var body: some View {
NavigationView {
VStack{
Text(newItem?.data ?? "nil")
List {
ForEach(items){ item in
NavigationLink(item.data!, destination: ItemEditorView(isNew: false, item:item, newItem: $newItem))
}
}
}
.fullScreenCover(isPresented: $presentNewItemEditorView, content: {
ItemEditorView(isNew: true, isEditing: true, newItem: $newItem)
})
.navigationTitle("Main")
.toolbar {
ToolbarItem {
Button("New goal"){
presentNewItemEditorView = true
}
}
}
.task {
newItem = Item(context: viewContext)
newItem!.data = "New item text"
}
}
}
}

I it unclear what are you trying to achieve. Here are options that will compile.
Value type
struct Item {
var data: String
}
struct ItemDetailView: View {
#Binding var item: Item
#Binding var isEditing: Bool
var body: some View{
if isEditing {
TextField( "Name", text: Binding(projectedValue: $item.data) )
}else{
Text(item.data)
}
}
}
Reference type and value type
class Item {
var data: String? = ""
}
struct ItemDetailView: View {
#Binding var item: Item? // Has no sense, because #Binding does't work with clsses!
#Binding var isEditing: Bool
var body: some View{
if isEditing {
TextField("Name", text: Binding(get: {
item!.data!
}, set: { value in
item?.data = value
}))
} else {
Text(item!.data!)
}
}
}

Implementation by Conny Wals sugested by #vadian
import CoreData
class EditViewModel {
/// **new** instance of item to edit. Don't edit the item, if any, that we pass to VM
var item: Item4
/// Context to use in the isolated changes
let context: NSManagedObjectContext
/// Pass an item if you want to edit one, or nil to generate one
init(item: Item4? = nil){
self.context = PersistenceController.shared.childViewContext()
if let item = item {
self.item = PersistenceController.shared.copyForEditing(of: item, in: context)
} else {
self.item = PersistenceController.shared.newTemporaryInstance(in: context)
}
}
func persist(){
PersistenceController.shared.persist(item)
}
}
import SwiftUI
struct EditItemView: View {
#State var viewModel: EditViewModel
#Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
VStack{
TextField( "Item Name", text: Binding(get: {viewModel.item.name ?? ""}, set: {viewModel.item.name = $0}))
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
}
.navigationTitle("Edit Item")
.navigationBarItems(leading: Button("Cancel"){
dismiss()
}, trailing: Button("Save"){
viewModel.persist()
dismiss()
})
}
}
}
struct ItemsView04: View {
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item3.name, ascending: true)],
animation: .default)
private var items: FetchedResults<Item4>
#State var editItemViewIsPresent = false
var body: some View {
NavigationView{
List {
ForEach(items){ item in
NavigationLink(item.name ?? ""){
EditItemView(viewModel: EditViewModel(item: item))
}
}
}
.toolbar {
ToolbarItem {
Button {
editItemViewIsPresent = true
}label:{
Text("+")
}
}
}
.fullScreenCover(isPresented: $editItemViewIsPresent){
EditItemView(viewModel: EditViewModel())
}
}
}
}

Related

SwiftUI- passing #State variables to multiple views trouble

Im trying to make an app similar to iphones reminders app in terms of UI. I have a view where I have a list that I can add and delete items from, I also have a view for adding an item that allows me to name the item and select some options, and I have another view for when an item in the list is selected and I want to be able to show the name and options I made but it doesn't display. The code for the list view
struct DotView: View {
enum ActiveSheet: String, Identifiable {
case SwiftUIView, EditView
var id: String {
return self.rawValue
}
}
#EnvironmentObject var listViewModel: ListViewModel
#AppStorage("dotApiKey") var selectedDotApi: String = ""
#State var dotName:String = ""
#State var dotNumber:String = ""
#State var selection:String = ""
#State var triggerSelection:String = ""
#State var searchText:String = ""
#State var plugUsername:String = ""
#State var plugPassword:String = ""
#State var toggleNotification:Bool
#State var activeSheet : ActiveSheet? = nil
#Binding var show : Bool
var body: some View {
ZStack{
HStack {
EditButton()
.padding(.leading)
Spacer()
Button(action: {
self.activeSheet = .SwiftUIView
}, label: {
Text("Add")
})
.padding(.trailing)
}
ZStack {
List {
ForEach(listViewModel.dotitems, id:\.dotId){ dotitem in
Button(action: { self.activeSheet = .EditView }, label: {
DotItemListView(dotitem: dotitem)
})
}
.onDelete(perform: listViewModel.deleteItem)
.onMove(perform: listViewModel.moveItem)
.listRowBackground(Color("textBG"))
}.listStyle(PlainListStyle())
.background(Color("textBG"))
.frame(height: 300)
.cornerRadius(10)
.padding(.horizontal)
}
}
.sheet(item: $activeSheet){ sheet in
switch sheet {
case .SwiftUIView:
SwiftUIView(dotName: dotName, dotNumber: dotNumber, selection: selection, triggerSelection: triggerSelection, searchText: searchText, plugUsername: plugUsername, plugPassword: plugPassword, show: $show)
case .EditView:
EditView(show:$show)
}
}
When I add an item to the list it shows this in each row
struct DotItemListView:View {
let dotitem: DotItem
var body: some View{
HStack {
Text(dotitem.dotName)
Spacer()
Text(dotitem.selection)
Spacer()
Text(dotitem.dotNumber)
Spacer()
}
}
}
This is how I'm adding each item to the list
struct DotItem:Equatable, Codable{
var dotId = UUID().uuidString
let dotName:String
let dotNumber:String
let selection:String
}
class ListViewModel: ObservableObject {
#Published var dotitems: [DotItem] = [] {
didSet {
saveItem()
}
}
let dotitemsKey: String = "dotitems_list"
init() {
getDotItems()
}
func getDotItems() {
guard
let data = UserDefaults.standard.data(forKey: dotitemsKey),
let savedDotItems = try? JSONDecoder().decode([DotItem].self, from: data)
else { return }
self.dotitems = savedDotItems
}
func deleteItem(indexSet: IndexSet){
dotitems.remove(atOffsets: indexSet)
}
func moveItem(from: IndexSet, to: Int){
dotitems.move(fromOffsets: from, toOffset: to)
}
func addItem(dotName: String, dotNumber: String, selection: String){
let newItem = DotItem(dotName: dotName, dotNumber: dotNumber, selection: selection)
dotitems.append(newItem)
print(newItem)
}
func saveItem() {
if let encodedData = try? JSONEncoder().encode(dotitems) {
UserDefaults.standard.set(encodedData, forKey: dotitemsKey)
}
}
}
This is the view that I'm entering the data for each item
struct SwiftUIView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var listViewModel: ListViewModel
#AppStorage("dotApiKey") var selectedDotApi: String = ""
#State var dotName:String
#State var dotNumber:String
#State var selection:String
#State var triggerSelection:String
#State var selectedColor = Color.black
#State var searchText:String
#State var plugUsername:String
#State var plugPassword:String
#ObservedObject var vm = getDeviceNames()
#State var triggerDot:Bool = false
#State var toggleOnOff:Bool = false
#State var toggleLightColor:Bool = false
#State var isSearching:Bool = false
#StateObject var camera = CameraModel()
#Binding var show : Bool
var body: some View {
NavigationView {
Form {
Section(header: Text("Info")) {
TextField("Name", text: $dotName)
TextField("Number", text: $dotNumber)
Picker(selection: $selection, label: Text("Discover Plug")) {
ForEach(vm.dataSet, id:\.self) { item in
Text(item.Device).tag(item.Device)
}
}
Toggle(isOn: $isSearching, label: {
Text("Have smart plugs?")
})
if isSearching {
HStack{
Text("Casa")
Spacer()
Button(action: {sendPlugDict()}, label: {
Text("Login")
})
}
TextField("Username", text: $plugUsername)
TextField("Password", text: $plugPassword)
}
}
Section {
Toggle(isOn: $toggleOnOff, label: {
Text("On/Off")
})
}
Section {
Toggle(isOn: $toggleLightColor, label: {
Text("Light Color")
})
if toggleLightColor {
ColorPicker("Choose Light Color", selection: $selectedColor)
}
}
Section {
if listViewModel.dotitems.isEmpty == false {
Toggle(isOn: $triggerDot, label: {
Text("Add a DOT to trigger")
})
if triggerDot {
Picker(selection: $triggerSelection, label: Text("Select DOT")) {
ForEach(listViewModel.dotitems, id:\.dotId){ dotitem in
DotItemListView(dotitem: dotitem)
}
}
}
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
and this is the view that I'm trying to show the data when any list item is selected which is basically the same as above except in a few places
struct EditView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var listViewModel: ListViewModel
#ObservedObject var vm = getDeviceNames()
#State var dataSet = [Result]()
#State var dotName:String = ""
#State var dotNumber:String = ""
#State var selection:String = ""
#State var triggerSelection:String = ""
#State var selectedColor = Color.black
#State var searchText:String = ""
#State var plugUsername:String = ""
#State var plugPassword:String = ""
#State var triggerDot:Bool = false
#State var toggleOnOff:Bool = false
#State var toggleLightColor:Bool = false
#State var isSearching:Bool = false
#Binding var show : Bool
var body: some View {
NavigationView {
Form {
Section(header: Text("Info")) {
TextField(dotName, text: $dotName)
TextField(dotNumber, text: $dotNumber)
Picker(selection: $selection, label: Text("Discorver Plug")) {
ForEach(dataSet, id:\.self) { item in
Text(item.Device).tag(item.Device)
}
}
Toggle(isOn: $isSearching, label: {
Text("Have smart plugs?")
})
if isSearching {
HStack{
Text("Casa")
Spacer()
Button(action: {
SwiftUIView(dotName: dotName, dotNumber: dotNumber, selection: selection, triggerSelection: triggerSelection, searchText: searchText, plugUsername: plugUsername, plugPassword: plugPassword, show: $show).sendPlugDict()}, label: {
Text("Login")
})
}
TextField("Username", text: $plugUsername)
TextField("Password", text: $plugPassword)
}
}
Section {
Toggle(isOn: $toggleOnOff, label: {
Text("On/Off")
})
}
Section {
Toggle(isOn: $toggleLightColor, label: {
Text("Light Color")
})
if toggleLightColor {
ColorPicker("Choose Light Color", selection: $selectedColor)
}
}
Section {
if listViewModel.dotitems.isEmpty == false {
Toggle(isOn: $triggerDot, label: {
Text("Add a DOT to trigger")
})
if triggerDot {
Picker(selection: $triggerSelection, label: Text("Select DOT")) {
ForEach(listViewModel.dotitems, id:\.dotId){ dotitem in
DotItemListView(dotitem: dotitem)
}
}
}
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
I know this is a lot to go through but any help would be greatly appreciated
use #Binding on the view that you want to pass state to .
View1 :
#State var a = true
View2(a: $a)
View2 :
#Binding var a : Bool
for passing data globally use #EnvoirmentObject :
example :
#main
struct YourApp: App {
#StateObject var session = Session()
var body: some Scene {
WindowGroup {
SplashView()
.environmentObject(session)
}
}
}
Session :
class Session: ObservableObject {
/// user signin state
#Published var isSignedIn : Bool = false
}
View1 ,View2 , View3 .... :
struct SplashView: View {
#EnvironmentObject var session : Session
var body: some View {
VStack{
SignInView()
.opacity(session.isSignedIn ? 0:1)
}
.background(Color.background.ignoresSafeArea())
}
}

Wrong List Item Selected in NavigationLink

I'm trying to build out a simple navigation where you can click on items in a link and pop back to the root controller from a sheet view. As you can see from the video below, when I tap on an item in the list, the wrong item is loaded (there's an offset between the row I click and the one that gets highlighted and loaded).
I also get the error SwiftUI encountered an issue when pushing aNavigationLink. Please file a bug.
Here's all my code:
import SwiftUI
struct ContentView: View {
#State var rootIsActive:Bool = false
var body: some View {
NavigationView{
AllProjectView(rootIsActive: self.rootIsActive)
}
.navigationBarTitle("Root")
.navigationViewStyle(StackNavigationViewStyle())
.environment(\.rootPresentationMode, self.$rootIsActive)
}
}
struct AllProjectView: View {
#State var rootIsActive:Bool = false
#State var projects: [String] = ["1", "2", "3"]
var body: some View{
List{
ForEach(projects.indices, id: \.self){ idx in
ProjectItem(name: self.$projects[idx], rootIsActive: self.$rootIsActive)
}
}.navigationBarTitle("All Projects")
}
}
struct ProjectItem: View{
#Binding var name: String
#Binding var rootIsActive: Bool
init(name: Binding<String>, rootIsActive: Binding<Bool>){
self._name = name
self._rootIsActive = rootIsActive
}
var body: some View{
NavigationLink(
destination: ProjectView(name: self.name),
isActive: self.$rootIsActive){
Text(name)
}
.isDetailLink(false)
.padding()
}
}
struct ProjectView: View {
var name: String
#State var isShowingSheet: Bool = false
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
#Environment(\.rootPresentationMode) private var rootPresentationMode: Binding<RootPresentationMode>
var body: some View{
VStack{
Text(name)
Button("Show Sheet"){
self.isShowingSheet = true
}
}
.sheet(isPresented: $isShowingSheet){
Button("return to root"){
self.isShowingSheet = false
print("pop view")
self.presentationMode.wrappedValue.dismiss()
print("pop root")
self.rootPresentationMode.wrappedValue.dismiss()
}
}
.navigationBarTitle("Project View")
}
}
// from https://stackoverflow.com/a/61926030/1720985
struct RootPresentationModeKey: EnvironmentKey {
static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}
extension EnvironmentValues {
var rootPresentationMode: Binding<RootPresentationMode> {
get { return self[RootPresentationModeKey.self] }
set { self[RootPresentationModeKey.self] = newValue }
}
}
typealias RootPresentationMode = Bool
extension RootPresentationMode {
public mutating func dismiss() {
self.toggle()
}
}
You only have one isRootActive variable that you're using. And, it's getting repeated for each item on the list. So, as soon as any item on the list is tapped, the isActive property for each NavigationLink turns to true.
Beyond that, your isRootActive isn't actually doing anything right now, since your "Return to root" button already does this:
self.isShowingSheet = false
self.presentationMode.wrappedValue.dismiss()
At that point, there's nothing more to dismiss -- it's already back at the root view.
My removing all of the root and isActive stuff, you get this:
struct ContentView: View {
var body: some View {
NavigationView{
AllProjectView()
}
.navigationBarTitle("Root")
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct AllProjectView: View {
#State var projects: [String] = ["1", "2", "3"]
var body: some View{
List{
ForEach(projects.indices, id: \.self){ idx in
ProjectItem(name: self.$projects[idx])
}
}.navigationBarTitle("All Projects")
}
}
struct ProjectItem: View{
#Binding var name: String
var body: some View{
NavigationLink(
destination: ProjectView(name: self.name)
){
Text(name)
}
.isDetailLink(false)
.padding()
}
}
struct ProjectView: View {
var name: String
#State var isShowingSheet: Bool = false
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View{
VStack{
Text(name)
Button("Show Sheet"){
self.isShowingSheet = true
}
}
.sheet(isPresented: $isShowingSheet){
Button("return to root"){
self.isShowingSheet = false
print("pop view")
self.presentationMode.wrappedValue.dismiss()
}
}
.navigationBarTitle("Project View")
}
}
If you had an additional view in the stack, you would need a way to keep track of if the root were active. I've used a custom binding here that converts an optional String representing the project's name to a Bool value that gets passed down the view hierarchy:
struct ContentView: View {
var body: some View {
NavigationView{
AllProjectView()
}
.navigationBarTitle("Root")
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct AllProjectView: View {
#State var projects: [String] = ["1", "2", "3"]
#State var activeProject : String?
func activeBindingForProject(name : String) -> Binding<Bool> {
.init {
name == activeProject
} set: { newValue in
activeProject = newValue ? name : nil
}
}
var body: some View{
List{
ForEach(projects.indices, id: \.self){ idx in
InterimProjectView(name: self.$projects[idx],
isActive: activeBindingForProject(name: self.projects[idx]))
}
}.navigationBarTitle("All Projects")
}
}
struct InterimProjectView: View {
#Binding var name : String
#Binding var isActive : Bool
var body : some View {
NavigationLink(destination: ProjectItem(name: $name, isActive: $isActive),
isActive: $isActive) {
Text("Next : \(isActive ? "true" : "false")")
}
}
}
struct ProjectItem: View {
#Binding var name: String
#Binding var isActive: Bool
var body: some View{
NavigationLink(
destination: ProjectView(name: self.name, isActive: $isActive)
){
Text(name)
}
.isDetailLink(false)
.padding()
}
}
struct ProjectView: View {
var name: String
#Binding var isActive : Bool
#State var isShowingSheet: Bool = false
var body: some View{
VStack{
Text(name)
Button("Show Sheet"){
self.isShowingSheet = true
}
}
.sheet(isPresented: $isShowingSheet){
Button("return to root"){
self.isShowingSheet = false
print("pop root")
self.isActive.toggle()
}
}
.navigationBarTitle("Project View")
}
}

Creating an edit detail view sheet that needs confirmation in SwiftUI

On Xcode 13 Beta 3, I am trying to find a good solution for an edit detail view presented in a sheet that needs to explicitly be confirmed.
In the DetailEditView, I initialise a #State property (editingModel) which is initialised from a #Binding (model) that I hand down.
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
//...
When I tap/press the confirm button in my sheet, I want to assign the altered editingModel to the passed model.
Button {
#warning("My expectation (saving changes by assigning `editingModel` to `model`) fails here…")
model = editingModel
isEditing = false
} label: {
Text("Done")
}
//...
While I do not have any build errors, the code does not work as expected–and I don't understand why. Look out for my #warning: that's where my code does not work as expected.
For all I know this could be a bug in the Xcode 13 Beta–or am I misunderstanding something fundamentally?
Here's all the code:
import SwiftUI
//MARK: - Main
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelStore)
}
}
}
//MARK: - Views
struct ContentView: View {
#SceneStorage("selection") var selection: Model.ID?
var body: some View {
NavigationView {
SidebarView(selection: $selection)
DetailView(modelSelection: $selection)
}
}
}
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var selection: Model.ID?
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $selection)
} label: {
Text(modelItem.id)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var modelSelection: Model.ID?
#State private var isEditing = false
var body: some View {
Form {
Text(modelBinding.wrappedValue.id)
}
.sheet(isPresented: $isEditing) {
DetailEditView(model: modelBinding, isEditing: $isEditing)
}
.toolbar {
ToolbarItem {
Button {
isEditing = true
} label: {
Label("Edit", systemImage: "pencil")
}
}
}
}
var modelBinding: Binding<Model> {
$modelStore[modelSelection]
}
}
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.id)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
#warning("My expectation (saving changes by assigning `editingModel` to `model`) fails here…")
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
//MARK: - Store
class ModelStore: ObservableObject {
#Published var models: [Model] = Model.mockModelArray()
subscript(modelId: Model.ID?) -> Model {
get {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
return models[modelIndex]
}
}
if models.isEmpty {
return Model(id: UUID().uuidString)
} else {
return models[0]
}
}
set(newValue) {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
models[modelIndex] = newValue
}
}
}
}
}
//MARK: - Models
struct Model: Identifiable {
var id: String
static func mockModel() -> Model {
Model(id: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
At first, do not edit id of Model. Instead use a new property and edit it.
//MARK: - Models
struct Model: Identifiable {
let id = UUID()
var content: String
static func mockModel() -> Model {
Model(content: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
For the first time you are in DetailView, selected model is not among the $modelStore.models. You need to send the first object of `` to the DetailsView.
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView(selection: $modelStore.models.first!)
.environmentObject(modelStore)
}
}
}
When you choose a model from SidebarView, the model in DetailView does not get updated. Send $modelItem to DetailView instead.
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $modelItem)
} label: {
Text(modelItem.content)
}
}
}
}
}
In DetailView, remove modelBinding and send modelSelection to DetailEditView.
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.content)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
All the code
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView(selection: $modelStore.models.first!)
.environmentObject(modelStore)
}
}
}
//MARK: - Views
struct ContentView: View {
#Binding var selection: Model
var body: some View {
NavigationView {
SidebarView()
DetailView(modelSelection: $selection)
}
}
}
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $modelItem)
} label: {
Text(modelItem.content)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var modelSelection: Model
#State private var isEditing = false
var body: some View {
Form {
Text(modelSelection.content)
}
.sheet(isPresented: $isEditing) {
DetailEditView(model: $modelSelection, isEditing: $isEditing)
}
.toolbar {
ToolbarItem {
Button {
isEditing = true
} label: {
Label("Edit", systemImage: "pencil")
}
}
}
}
}
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.content)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
//MARK: - Store
class ModelStore: ObservableObject {
#Published var models: [Model] = Model.mockModelArray()
subscript(modelId: Model.ID?) -> Model {
get {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
return models[modelIndex]
}
}
if models.isEmpty {
return Model(content: UUID().uuidString)
} else {
return models[0]
}
}
set(newValue) {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
models[modelIndex] = newValue
}
}
}
}
}
//MARK: - Models
struct Model: Identifiable {
let id = UUID()
var content: String
static func mockModel() -> Model {
Model(content: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
Now upon confirmation the selected model is edited in all the views.

How can I have multiple instance of a Class/Model in SwiftUI?

The first part of question is answered. Let's elaborate this example to:
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// leads to that I create a new viewModel every time, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}
My problem is now that when I type something into the TextField and press the return button on the keyboard the text is removed.
This is the most strange way of coding that i seen, how ever I managed to make it work:
I would like say that you can use it as leaning and testing, but not good plan for real app, How ever it was interesting to me to make it working.
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
class CreateNewCardViewModel: ObservableObject, Identifiable, Equatable {
init(_ id: Int) {
self.id = id
}
#Published var id: Int
#Published var definition: String = ""
#Published var show = false
static func == (lhs: CreateNewCardViewModel, rhs: CreateNewCardViewModel) -> Bool {
return lhs.id == rhs.id
}
}
let arrayOfModel: [CreateNewCardViewModel] = [ CreateNewCardViewModel(0), CreateNewCardViewModel(1), CreateNewCardViewModel(2),
CreateNewCardViewModel(3), CreateNewCardViewModel(4), CreateNewCardViewModel(5),
CreateNewCardViewModel(6), CreateNewCardViewModel(7), CreateNewCardViewModel(8),
CreateNewCardViewModel(9) ]
struct ReadModelView: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
struct MainView: View {
#State private var arrayOfModelState = arrayOfModel
#State private var showModel: Int?
#State private var isPresented: Bool = false
var body: some View {
VStack {
ForEach(Array(arrayOfModelState.enumerated()), id:\.element.id) { (index, item) in
Button(action: { showModel = index; isPresented = true }, label: { Text("Show Model " + item.id.description) }).padding()
}
if let unwrappedValue: Int = showModel {
Color.clear
.sheet(isPresented: $isPresented, content: { ReadModelView(viewModel: arrayOfModelState[unwrappedValue]) })
}
}
.padding()
}
}

How is it possible to work with a struct inside of a struct in SwiftUI in different Views?

I have gut a simple project with a struct inside of a struct and want to add at first the names and hobbies of a single user and than want to add this user to a whole pool of users. The code is the following:
import SwiftUI
struct User: Identifiable, Hashable {
var id = UUID()
var firstName = ""
var lastName = ""
var hobbiesOfUser = [Hobbies]()
}
struct Hobbies: Identifiable, Hashable {
var id = UUID()
var nameOfHobby = ""
var nameClub = ""
}
class UsersStorage: ObservableObject {
#Published var users = [User]()
}
struct ContentView: View {
#EnvironmentObject var userStorage: UsersStorage
#State private var isPresented = false
var body: some View {
NavigationView {
List(userStorage.users) { singleUser in
VStack {
HStack {
Text(singleUser.firstName)
Text(singleUser.lastName)
}
}
}
.navigationBarItems(trailing:
Button(action: {
self.isPresented = true
}) {
Text("New User")
}.sheet(isPresented: $isPresented, onDismiss: {
self.isPresented = false
}) {
AddUserView(isPresented: self.$isPresented, user: User()).environmentObject(self.userStorage)
}
)
}
}
}
struct AddUserView: View {
#EnvironmentObject var userStorage: UsersStorage
#Binding var isPresented: Bool
#State var user: User
#State var hobbiesOfUser = [Hobbies]()
var body: some View {
NavigationView {
VStack {
Text("First Name")
TextField("First Name", text: $user.firstName)
Text("Last Name")
TextField("Last Name", text: $user.lastName)
NavigationLink(destination: AddHobbieView(hobbie: Hobbies())) {
Text("Add New Hobbie")
}
List(user.hobbiesOfUser) { singleHobbie in
VStack {
HStack {
Text(singleHobbie.nameOfHobby)
Text(singleHobbie.nameClub)
}
}
}
HStack {
Button(action: {
self.isPresented = false
}){
Text("Cancel")
}
Button(action: {
self.userStorage.users.append(self.user)
self.isPresented = false
}){
Text("Save")
}
}
}
}
}
}
struct AddHobbieView: View {
#EnvironmentObject var userStorage: UsersStorage
#State var hobbie: Hobbies
var body: some View {
VStack {
Text("Hobby")
TextField("First Name", text: $hobbie.nameOfHobby)
Text("Club")
TextField("Last Name", text: $hobbie.nameClub)
HStack {
Button(action: {
// self.userStorage.users.append(self.hobbie)
}){
Text("Cancel")
}
Button(action: {
}){
Text("Save")
}
}
}
}
}
My question is: how can I add hobbies to the list in the AddUserView and get the buttons in the AddHobbieView let me go back to the AddUserView.
You add #Environment(\.presentationMode) var presentationMode to AddHobbieView and call self.presentationMode.wrappedValue.dismiss() when you want to dismiss the view
I hope this helps!