Creating a safe subscript slice of a binding array - ios

I have a safe subscript extension on Array to give me an ArraySlice from a Range like this:
extension Array {
subscript(safe range: Range<Index>) -> ArraySlice<Element>? {
if range.endIndex > endIndex {
if range.startIndex >= endIndex {
return nil
} else {
return self[range.startIndex..<endIndex]
}
} else {
return self[range]
}
}
}
I have a use case for needing a slice of a binding of an array such that I can do this:
let userSlice = $users[safe: 0..<20]
This should return Slice<Binding<[User]>>. How can I alter the above code to work with a Binding of an Array?

Here is a simplified demo of possible approach - at first, and main, is that binding requires wrapped value to be mutable, and at second binding, actually, has no sense on optional (about this see below alternate way).
Tested with Xcode 13.2 / iOS 15.2
extension Array {
subscript(safe range: Range<Index>) -> ArraySlice<Element>? {
get {
if range.endIndex > endIndex {
if range.startIndex >= endIndex {
return nil
} else {
return self[range.startIndex..<endIndex]
}
} else {
return self[range]
}
}
mutating set { // << add mutability !!
if let value = newValue {
_ = value.indices.map { self[$0] = value[$0] }
}
}
}
}
struct DemoView1: View {
struct Model: Identifiable {
let id: Int
var value: String
}
#State private var users = [
Model(id: 1, value: "1"), Model(id: 2, value: "2"), Model(id: 3, value: "3"),
Model(id: 4, value: "4"), Model(id: 5, value: "5")]
var body: some View {
VStack {
Text("Array slice")
List {
// Optional has not sense, so we must explicitly remove
// it that makes code more complicated and less readable
ForEach(Binding($users[safe: 1..<3]) ?? .constant(ArraySlice<Model>()), id: \.wrappedValue.id) {
TextField("", text: $0.value)
}
}
Divider()
Text("Full Array")
List {
ForEach(users) {
Text($0.value)
}
}
}
}
}
Alternate: due to binding has not sense for optionals and non-optional type is better controlled, I would recommend, as alternate, to consider possibility to avoid optional and use subscript with returned definite data, like
extension Array {
subscript(safe range: Range<Index>) -> ArraySlice<Element> {
get {
if range.endIndex > endIndex {
if range.startIndex >= endIndex {
// Here can be something known and defined
return ArraySlice<Element>() // << just empty !!
} else {
return self[range.startIndex..<endIndex]
}
} else {
return self[range]
}
}
mutating set {
_ = newValue.indices.map { self[$0] = newValue[$0] }
}
}
}
struct DemoView2: View {
struct Model: Identifiable {
let id: Int
var value: String
}
#State private var users = [
Model(id: 1, value: "1"), Model(id: 2, value: "2"), Model(id: 3, value: "3"),
Model(id: 4, value: "4"), Model(id: 5, value: "5")]
var body: some View {
VStack {
Text("Array slice")
List {
// Now code is simple and clear !!
ForEach($users[safe: 1..<3], id: \.wrappedValue.id) {
TextField("", text: $0.value)
}
}
Divider()
Text("Full Array")
List {
ForEach(users) {
Text($0.value)
}
}
}
}
}

Related

How to change the selection of the second picker after changing the first picker?

Here I'm trying to create the first little app that helps to convert a value from one unit of measurement to another.
The first conversion picker helps me to select data for the second picker(from which measure I'm going to converse) and the third picker(to which measure I'm going converse).
But when I'm changing the first picker - it doesn't change the #State value of the second and third pickers. I tried different approaches but the result is the same. Could you please help me?
struct ContentView: View {
#State private var userInput = 0.0
#State var selectionOfTypeOfConversion: Conversions = .temperature
#State var selectionFromConversion: String = TemperatureConversions.celsius.rawValue
#State var selectionToConversion: String = TemperatureConversions.celsius.rawValue
#FocusState private var inputIsFocused: Bool
var output: Double {
//doing all counting
}
var body: some View {
VStack {
NavigationView {
Form {
Section {
Picker("Type of conversion", selection: $selectionOfTypeOfConversion) {
ForEach(Conversions.allCases, id: \.self) {
Text($0.rawValue)
}
}.onSubmit {
checkConvention()
}
TextField("", value: $userInput, format: .number)
.keyboardType(.decimalPad)
.focused($inputIsFocused)
Picker("From conversion", selection: $selectionFromConversion) {
switch selectionOfTypeOfConversion {
case Conversions.temperature:
ForEach(TemperatureConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
case Conversions.length:
ForEach(LengthConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
case Conversions.volume:
ForEach(VolumeConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
}
}
} header: {
Text("From")
}
Section {
Picker("To conversion", selection: $selectionToConversion) {
switch selectionOfTypeOfConversion {
case Conversions.temperature:
ForEach(TemperatureConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
case Conversions.length:
ForEach(LengthConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
case Conversions.volume:
ForEach(VolumeConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
}
}
Text(output, format: .number)
} header: {
Text("To")
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
inputIsFocused = false
}
}
}
}
}
}
private func checkConvention() {
if selectionOfTypeOfConversion == Conversions.temperature {
selectionFromConversion = TemperatureConversions.celsius.rawValue
selectionToConversion = TemperatureConversions.celsius.rawValue
} else if selectionOfTypeOfConversion == Conversions.length{
selectionFromConversion = LengthConversions.meters.rawValue
selectionToConversion = LengthConversions.meters.rawValue
} else if selectionOfTypeOfConversion == Conversions.volume{
selectionFromConversion = VolumeConversions.milliliters.rawValue
selectionToConversion = VolumeConversions.milliliters.rawValue
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
enum Conversions: String, CaseIterable {
case temperature = "temperature"
case length = "length"
case volume = "volume"
}
enum TemperatureConversions: String, CaseIterable {
var id: Self { self }
case celsius = "celsius"
case fahrenheit = "fahrenheit"
case kelvin = "kelvin"
}
enum LengthConversions: String, CaseIterable {
var id: Self { self }
case meters = "meters"
case feet = "feet"
case miles = "miles"
}
enum VolumeConversions: String, CaseIterable {
var id: Self { self }
case milliliters = "ml"
case pints = "pints"
case gallons = "gallons"
}
Expecting: change the formula which counts input into output.

Encoding to JSON format is not encoding the toggled boolean value in Swift

I am making an app that has information about different woods, herbs and spices, and a few other things. I am including the ability to save their favorite item to a favorites list, so I have a heart button that the user can press to add it to the favorites. Pressing the button toggles the isFavorite property of the item and then leaving the page calls a method that encodes the data to save it to the user's device. The problem that I am running into is that it is not encoding the updated value of the isFavorite property. It is still encoding the value as false, so the favorites list is not persisting after closing and reopening the app.
Here is my Wood.swift code, this file sets up the structure for Wood items. I also included the test data that I was using to make sure that it displayed properly in the Wood extension:
import Foundation
struct Wood: Identifiable, Codable {
var id = UUID()
var mainInformation: WoodMainInformation
var preparation: [Preparation]
var isFavorite = false
init(mainInformation: WoodMainInformation, preparation: [Preparation]) {
self.mainInformation = mainInformation
self.preparation = preparation
}
}
struct WoodMainInformation: Codable {
var category: WoodCategory
var description: String
var medicinalUses: [String]
var magicalUses: [String]
var growZone: [String]
var lightLevel: String
var moistureLevel: String
var isPerennial: Bool
var isEdible: Bool
}
enum WoodCategory: String, CaseIterable, Codable {
case oak = "Oak"
case pine = "Pine"
case cedar = "Cedar"
case ash = "Ash"
case rowan = "Rowan"
case willow = "Willow"
case birch = "Birch"
}
enum Preparation: String, Codable {
case talisman = "Talisman"
case satchet = "Satchet"
case tincture = "Tincture"
case salve = "Salve"
case tea = "Tea"
case ointment = "Ointment"
case incense = "Incense"
}
extension Wood {
static let woodTypes: [Wood] = [
Wood(mainInformation: WoodMainInformation(category: .oak,
description: "A type of wood",
medicinalUses: ["Healthy", "Killer"],
magicalUses: ["Spells", "Other Witchy Stuff"],
growZone: ["6A", "5B"],
lightLevel: "Full Sun",
moistureLevel: "Once a day",
isPerennial: false,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet]),
Wood(mainInformation: WoodMainInformation(category: .pine,
description: "Another type of wood",
medicinalUses: ["Healthy"],
magicalUses: ["Spells"],
growZone: ["11G", "14F"],
lightLevel: "Full Moon",
moistureLevel: "Twice an hour",
isPerennial: true,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet])
]
}
Here is my WoodData.swift file, this file contains methods that allow the app to display the correct wood in the list of woods, as well as encode, and decode the woods:
import Foundation
class WoodData: ObservableObject {
#Published var woods = Wood.woodTypes
var favoriteWoods: [Wood] {
woods.filter { $0.isFavorite }
}
func woods(for category: WoodCategory) -> [Wood] {
var filteredWoods = [Wood]()
for wood in woods {
if wood.mainInformation.category == category {
filteredWoods.append(wood)
}
}
return filteredWoods
}
func woods(for category: [WoodCategory]) -> [Wood] {
var filteredWoods = [Wood]()
filteredWoods = woods
return filteredWoods
}
func index(of wood: Wood) -> Int? {
for i in woods.indices {
if woods[i].id == wood.id {
return i
}
}
return nil
}
private var dataFileURL: URL {
do {
let documentsDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
return documentsDirectory.appendingPathComponent("evergreenData")
}
catch {
fatalError("An error occurred while getting the url: \(error)")
}
}
func saveWoods() {
if let encodedData = try? JSONEncoder().encode(woods) {
do {
try encodedData.write(to: dataFileURL)
let string = String(data: encodedData, encoding: .utf8)
print(string)
}
catch {
fatalError("An error occurred while saving woods: \(error)")
}
}
}
func loadWoods() {
guard let data = try? Data(contentsOf: dataFileURL) else { return }
do {
let savedWoods = try JSONDecoder().decode([Wood].self, from: data)
woods = savedWoods
}
catch {
fatalError("An error occurred while loading woods: \(error)")
}
}
}
Finally, this is my WoodsDetailView.swift file, this file displays the information for the wood that was selected, as well as calls the method that encodes the wood data:
import SwiftUI
struct WoodsDetailView: View {
#Binding var wood: Wood
#State private var woodsData = WoodData()
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses.indices, id: \.self) { index in
let medicinalUse = wood.mainInformation.medicinalUses[index]
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses.indices, id: \.self) { index in
let magicalUse = wood.mainInformation.magicalUses[index]
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone.indices, id: \.self) { index in
let zone = wood.mainInformation.growZone[index]
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
wood.isFavorite.toggle()
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
#State static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: $wood)
}
}
This is my MainTabView.swift file:
import SwiftUI
struct MainTabView: View {
#StateObject var woodData = WoodData()
var body: some View {
TabView {
NavigationView {
List {
WoodsListView(viewStyle: .allCategories(WoodCategory.allCases))
}
}
.tabItem { Label("Main", systemImage: "list.dash")}
NavigationView {
List {
WoodsListView(viewStyle: .favorites)
}
.navigationTitle("Favorites")
}.tabItem { Label("Favorites", systemImage: "heart.fill")}
}
.environmentObject(woodData)
.onAppear {
woodData.loadWoods()
}
.preferredColorScheme(.dark)
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView()
}
}
This is my WoodListView.swift file:
import SwiftUI
struct WoodsListView: View {
#EnvironmentObject private var woodData: WoodData
let viewStyle: ViewStyle
var body: some View {
ForEach(woods) { wood in
NavigationLink(wood.mainInformation.category.rawValue, destination: WoodsDetailView(wood: binding(for: wood)))
}
}
}
extension WoodsListView {
enum ViewStyle {
case favorites
case singleCategory(WoodCategory)
case allCategories([WoodCategory])
}
private var woods: [Wood] {
switch viewStyle {
case let .singleCategory(category):
return woodData.woods(for: category)
case let .allCategories(category):
return woodData.woods(for: category)
case .favorites:
return woodData.favoriteWoods
}
}
func binding(for wood: Wood) -> Binding<Wood> {
guard let index = woodData.index(of: wood) else {
fatalError("Wood not found")
}
return $woodData.woods[index]
}
}
struct WoodsListView_Previews: PreviewProvider {
static var previews: some View {
WoodsListView(viewStyle: .singleCategory(.ash))
.environmentObject(WoodData())
}
}
Any assistance into why it is not encoding the toggled isFavorite property will be greatly appreciated.
Your problem is that structs are value types in Swift. Essentially this means that the instance of Wood that you have in WoodsDetailView is not the same instance that is in your array in your model (WoodData); It is a copy (Technically, the copy is made as soon as you modify the isFavourite property).
In SwiftUI it is important to maintain separation of responsibilities between the view and the model.
Changing the favourite status of a Wood is something the view should ask the model to do.
This is where you have a second issue; In your detail view you are creating a separate instance of your model; You need to refer to a single instance.
You have a good start; you have put your model instance in the environment where views can access it.
First, change the detail view to remove the binding, refer to the model from the environment and ask the model to do the work:
struct WoodsDetailView: View {
var wood: Wood
#EnvironmentObject private var woodsData: WoodData
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses, id: \.self) { medicinalUse in
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses, id: \.self) { magicalUse in
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone, id: \.self) { zone in
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
self.woodsData.toggleFavorite(for: wood)
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: wood)
}
}
I also got rid of the unnecessary use of indices when listing the properties.
Now, add a toggleFavorite function to your WoodData object:
func toggleFavorite(for wood: Wood) {
guard let index = self.woods.firstIndex(where:{ $0.id == wood.id }) else {
return
}
self.woods[index].isFavorite.toggle()
}
You can also remove the index(of wood:Wood) function (which was really just duplicating Array's firstIndex(where:) function) and the binding(for wood:Wood) function.
Now, not only does your code do what you want, but you have hidden the mechanics of toggling a favorite from the view; It simply asks for the favorite status to be toggled and doesn't need to know what this actually involves.

SwiftUI List with different cells per section

I'm trying to create a List of questions.
I was planning to create a 'section' per question and have each row change upon the type
Now I have a lot of different types of questions.
Let's say for example:
Ask some text input
Select from a picker
Multiple select (so simply show all options)
I have this kind of setup working in 'regular' iOS
However, when trying to implement such a thing in SwiftUI, the preview keeps freeing, I can't seem to get the build working either. I don't really get any feedback from xcode.
Example code:
import SwiftUI
struct Question: Hashable, Codable, Identifiable {
var id: Int
var label: String
var type: Int
var options: [Option]?
var date: Date? = nil
}
struct Option : Hashable, Codable, Identifiable {
var id: Int
var value: String
}
struct MyList: View {
var questions: [Question] = [
Question(id: 1, label: "My first question", type: 0),
Question(id: 2, label: "My other question", type: 1, options: [Option(id: 15, value: "Yes"), Option(id: 22, value: "No")]),
Question(id: 3, label: "My last question", type: 2, options: [Option(id: 4, value: "Red"), Option(id: 5, value: "Green"), Option(id: 6, value: "Blue")])
]
var body: some View {
List {
ForEach(questions) { question in
Section(header: Text(question.label)) {
if(question.type == 0)
{
Text("type 0")
//Show text entry row
}
else if(question.type == 1)
{
Text("type 1")
//Show a picker containing all options
}
else
{
Text("type 2")
//Show rows for multiple select
//
// IF YOU UNCOMMENT THIS, IT STARTS FREEZING
//
// if let options = question.options {
// ForEach(options) { option in
// Text(option.value)
// }
// }
}
}
}
}
}
}
struct MyList_Previews: PreviewProvider {
static var previews: some View {
MyList()
}
}
Is such a thing possible in SwiftUI ?
What am I missing ?
Why is it freezing ?
Your code is confusing the Type checker. If you let it build long enough Xcode will give you an error stating that the type checker could not be run in a reasonable amount of time. I would report this as a bug to Apple, perhaps they can improve the type checker. In the mean time you can get your code to work by simplifying the expression that the ViewBuiler is trying to work through. I did it like this:
import SwiftUI
struct Question: Hashable, Codable, Identifiable {
var id: Int
var label: String
var type: Int
var options: [Option]?
var date: Date? = nil
}
struct Option : Hashable, Codable, Identifiable {
var id: Int
var value: String
}
struct Type0View : View {
let question : Question
var body : some View {
Text("type 0")
}
}
struct Type1View : View {
let question : Question
var body : some View {
Text("type 1")
}
}
struct Type2View : View {
let question : Question
var body : some View {
Text("type 1")
if let options = question.options {
ForEach(options) { option in
Text(option.value)
}
}
}
}
struct ContentView: View {
var questions: [Question] = [
Question(id: 1, label: "My first question", type: 0),
Question(id: 2, label: "My other question", type: 1, options: [Option(id: 15, value: "Yes"), Option(id: 22, value: "No")]),
Question(id: 3, label: "My last question", type: 2, options: [Option(id: 4, value: "Red"), Option(id: 5, value: "Green"), Option(id: 6, value: "Blue")])
]
var body: some View {
List {
ForEach(questions) { question in
Section(header: Text(question.label)) {
if(question.type == 0)
{
Type0View(question: question)
}
else if(question.type == 1)
{
Type1View(question: question)
}
else
{
Type2View(question: question)
}
}
}
}
}
}
struct MyList_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to display the selected menu item in SwiftUI?

I want to selected menu to get displayed and saved when the user comes back later. my current code just displays the selected item, but not getting saved when I close the sheet and comes back.
#State var selectedAge: Int = .zero
var body: some View {
Menu {
ForEach(myViewModel.MyModel.selectAge.indices, id: \.self) { indice in
Button(action: {
selectedAge = indice
}) {
if selectedAge == indice {
Text("\(myViewModel.MyModel.selectAge[indice])")
}
else {
Text("")
}
}
}
} label: {
Text("\(myViewModel.MyModel.selectAge[selectedAge])")
}
}
This code from my Model
var selectedAge: [String] = ["12", "15", "18", "21", "24"]
Please guide me to solve this issue.
I used this code to solve my issue. thank you for other developers who had taken their time to help me out!.
Menu{
ForEach(myViewModel.myModel.selectAge, id: \.self){ index in
Button(action : {
myViewModel.myModel.selectAge = "\(index)"
}) {
if myViewModel.myModel.selectedAge == index{
Label("\(index)", systemImage: "checkmark")
}else {
Text("\(index)")
}
}
}
} label: {
Text("\(myViewModel.myModel.selectedAge)")
}
and this is insert in my model
var selectedAge = "12"
var selectedAge: [String] = ["12", "15", "18", "21", "24"]
Try this:
// selectedAge1 array list replace with your data model list...
import Combine
class MenuAgeSettings: ObservableObject {
#Published var selectedAge: String {
didSet {
UserDefaults.standard.set(selectedAge, forKey: "selectedAge")
}
}
init() {
self.selectedAge = UserDefaults.standard.object(forKey: "selectedAge") as? String ?? ""
}
}
struct MenuView: View {
var selectedAge1: [String] = ["12", "15", "18", "21","24"]
#ObservedObject var menuSettings = MenuAgeSettings()
var body: some View {
Menu{
ForEach(self.selectedAge1.indices, id: \.self){ indice in
let text = self.selectedAge1[indice]
Button(action : {
menuSettings.selectedAge = text
}) {
if menuSettings.selectedAge == text {
Label(text, systemImage: "checkmark")
} else {
Text(text)
}
}
}
} label: {
if menuSettings.selectedAge.isEmpty {
Text(selectedAge1[.zero])
} else {
Text(menuSettings.selectedAge)
}
}
}
}

How can I fix "index out of Range" for a multi-dimensional view in SwiftUI

I tried as much as I could before asking the next "Index out of Range" question, because generally I understand why an index out of range issue occurs, but this specific issues makes me crazy:
struct Parent: Identifiable {
let id = UUID()
let name: String
var children: [Child]?
}
struct Child: Identifiable {
let id = UUID()
let name: String
var puppets: [Puppet]?
}
struct Puppet: Identifiable {
let id = UUID()
let name: String
}
class AppState: ObservableObject {
#Published var parents: [Parent]
init() {
self.parents = [
Parent(name: "Foo", children: [Child(name: "bar", puppets: [Puppet(name: "Tom")])]),
Parent(name: "FooBar", children: [Child(name: "foo", puppets: nil)]),
Parent(name: "FooBar", children: nil)
]
}
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
List {
ForEach (appState.parents.indices, id: \.self) { parentIndex in
NavigationLink (destination: ChildrenView(parentIndex: parentIndex).environmentObject(self.appState)) {
Text(self.appState.parents[parentIndex].name)
}
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.appState.parents.append(Parent(name: "Test", children: nil))
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Parents"))
}
}
private func deleteItem(at indexSet: IndexSet) {
self.appState.parents.remove(atOffsets: indexSet)
}
}
struct ChildrenView: View {
#EnvironmentObject var appState: AppState
var parentIndex: Int
var body: some View {
let children = appState.parents[parentIndex].children
return VStack {
List {
if (children?.indices != nil) {
ForEach (children!.indices, id: \.self) { childIndex in
NavigationLink (destination: PuppetsView(parentIndex: self.parentIndex, childIndex: childIndex).environmentObject(self.appState)) {
Text(children![childIndex].name)
}
}
.onDelete(perform: deleteItem)
}
}
Button(action: {
var children = self.appState.parents[self.parentIndex].children
if (children != nil) {
children?.append(Child(name: "Teest"))
} else {
children = [Child(name: "Teest")]
}
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Children"))
}
private func deleteItem(at indexSet: IndexSet) {
if (self.appState.parents[self.parentIndex].children != nil) {
self.appState.parents[self.parentIndex].children!.remove(atOffsets: indexSet)
}
}
}
struct PuppetsView: View {
#EnvironmentObject var appState: AppState
var parentIndex: Int
var childIndex: Int
var body: some View {
let child = appState.parents[parentIndex].children?[childIndex]
return VStack {
List {
if (child != nil && child!.puppets?.indices != nil) {
ForEach (child!.puppets!.indices, id: \.self) { puppetIndex in
Text(self.appState.parents[self.parentIndex].children![self.childIndex].puppets![puppetIndex].name)
}
.onDelete(perform: deleteItem)
}
}
Button(action: {
var puppets = self.appState.parents[self.parentIndex].children![self.childIndex].puppets
if (puppets != nil) {
puppets!.append(Puppet(name: "Teest"))
} else {
puppets = [Puppet(name: "Teest")]
}
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Puppets"))
}
private func deleteItem(at indexSet: IndexSet) {
if (self.appState.parents[self.parentIndex].children != nil) {
self.appState.parents[self.parentIndex].children![self.childIndex].puppets!.remove(atOffsets: indexSet)
}
}
}
I can remove both children of Foo and FooBar without issues, but when I remove the Puppet of child bar first, then the app crashes like shown in the comments.
I unterstand that the childIndex doesn't exist anymore, but I don't understand why the view gets built again when there is no child with puppets.
All the referencing of array indices looks pretty awful to me. Using array indices also requires that you pass the various objects down to the subviews.
To address this I started by changing your models - Make them classes rather than structs so you can make them #ObservableObject. They also need to be Hashable and Equatable.
I also added add and remove functions to the model objects so that you don't need to worry about indices when adding/removing children/puppets. The remove methods use an array extension that removes an Identifiable object without needing to know the index.
Finally, I changed the children and puppets arrays to be non-optional. There is little semantic difference between a nil optional and an empty non-optional array, but the latter is much easier to deal with.
class Parent: ObservableObject, Hashable {
static func == (lhs: Parent, rhs: Parent) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
let id = UUID()
let name: String
#Published var children: [Child]
init(name: String, children: [Child]? = nil) {
self.name = name
self.children = children ?? []
}
func remove(child: Child) {
self.children.remove(child)
}
func add(child: Child) {
self.children.append(child)
}
}
class Child: ObservableObject, Identifiable, Hashable {
static func == (lhs: Child, rhs: Child) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
let id = UUID()
let name: String
#Published var puppets: [Puppet]
init(name: String, puppets:[Puppet]? = nil) {
self.name = name
self.puppets = puppets ?? []
}
func remove(puppet: Puppet) {
self.puppets.remove(puppet)
}
func add(puppet: Puppet) {
self.puppets.append(puppet)
}
}
struct Puppet: Identifiable, Hashable {
let id = UUID()
let name: String
}
class AppState: ObservableObject {
#Published var parents: [Parent]
init() {
self.parents = [
Parent(name: "Foo", children: [Child(name: "bar", puppets: [Puppet(name: "Tom")])]),
Parent(name: "FooBar", children: [Child(name: "foo", puppets: nil)])
]
}
}
extension Array where Element: Identifiable {
mutating func remove(_ object: Element) {
if let index = self.firstIndex(where: { $0.id == object.id}) {
self.remove(at: index)
}
}
}
Having sorted out the model, the views then only need to know about their specific item:
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
List {
ForEach (appState.parents, id: \.self) { parent in
NavigationLink (destination: ChildrenView(parent: parent)) {
Text(parent.name)
}
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.appState.parents.append(Parent(name: "Test", children: nil))
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Parents"))
}
}
private func deleteItem(at indexSet: IndexSet) {
self.appState.parents.remove(atOffsets: indexSet)
}
}
struct ChildrenView: View {
#ObservedObject var parent: Parent
var body: some View {
VStack {
List {
ForEach (self.parent.children, id: \.self) { child in
NavigationLink (destination: PuppetsView(child:child)) {
Text(child.name)
}
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.parent.add(child: Child(name: "Test"))
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Children"))
}
private func deleteItem(at indexSet: IndexSet) {
let children = Array(indexSet).map { self.parent.children[$0]}
for child in children {
self.parent.remove(child: child)
}
}
}
struct PuppetsView: View {
#ObservedObject var child: Child
var body: some View {
VStack {
List {
ForEach (child.puppets, id: \.self) { puppet in
Text(puppet.name)
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.child.add(puppet:Puppet(name: "Test"))
})
{
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Puppets"))
}
func deleteItem(at indexSet: IndexSet) {
let puppets = Array(indexSet).map { self.child.puppets[$0] }
for puppet in puppets {
self.child.remove(puppet:puppet)
}
}
}
The problem with your optional chaining is that this line produces the result of type Child and not Child?:
appState.parents[parentIndex].children?[childIndex]
And if it's not an optional you can't call puppets on children?[childIndex] without checking if childIndex is valid:
// this will crash when childIndex is out of range
appState.parents[parentIndex].children?[childIndex].puppets?.indices
I recommend to use safeIndex subscript for accessing possible empty elements:
var body: some View {
let child = appState.parents[safeIndex: parentIndex]?.children?[safeIndex: childIndex]
return VStack {
List {
if (child != nil && child!.puppets?.indices != nil) {
ForEach ((appState.parents[parentIndex].children?[childIndex].puppets!.indices)!, id: \.self) { puppetIndex in
Text(self.appState.parents[self.parentIndex].children![self.childIndex].puppets![puppetIndex].name)
}
.onDelete(perform: deleteItem)
}
}
...
}
To do this you would need an Array extension which allows to access array elements in a safe way (ie. return nil instead of throwing an error):
extension Array {
public subscript(safeIndex index: Int) -> Element? {
guard index >= 0, index < endIndex else {
return nil
}
return self[index]
}
}
Note: You'd need to do the same for the ParentView, so in overall Paulw11's answer is cleaner.

Resources