I have a struct model called Questions stored in DataRep which is also stored in EnvironmentObject. There are 100+ questions in 12 categories.
struct Question: Hashable, Codable, Identifiable {
var id: Int
var questionText: String
var questionComment: String
var category: String
}
class DataRep: ObservableObject {
#Published var QuestionList : [Question] = QuestionListData
#Published var selectedCategory = "all"
}
On the user interface, I placed 12 buttons at top and list view down to list the questions in that category.
When user clicks on a new category, I update the selectedCategory parameter and filter the main questions list object to select the relevant questions.
struct QuestionList: View {
#EnvironmentObject var datarep: DataRep
var body: some View {
NavigationView{
Form {
ForEach(self.filterQuestions(datarep.QuestionList)) { question in
HStack{
QuestionView (question: question)
}
}
}//List
.navigationBarTitle(self.datarep.selectedCategory )
.labelsHidden()
.listStyle(GroupedListStyle())
}
}
func filterQuestions(_ activeList : [Question]) -> [Question]
{
if self.datarep.selectedCategory != "all" {
return activeList.filter{ $0.category.contains(self.datarep.selectedCategory) }
}
return activeList
}
}
However I am running into issues with filter method as it is generating a new array each time category is changed. There is no way I know to create a binding.
any suggestions?
Regards,
M
Assuming the QuestionView will take binding (to change the question), as
struct QuestionView: View {
#Binding var question: Question
...
it can be bound (even after filtering) via main container like following
ForEach(self.filterQuestions(datarep.QuestionList)) { question in
HStack{
QuestionView (question:
self.$datarep.QuestionList[self.datarep.QuestionList.firstIndex(of: question)!])
}
}
Related
I read similar questions here, but still don't get it.
I have a struct (I've maked little changes)
import Foundation
import CoreGraphics
typealias NodeID = UUID
struct Node: Identifiable {
var id: NodeID = NodeID()
var NodeWidth: CGFloat = 60.0
var position: CGPoint = .zero
var text: String = ""
var visualID: String {
return id.uuidString
+ "\(text.hashValue)"
}
}
I have a view with a State of this struct
import SwiftUI
struct NodeView: View {
#State var node: Node
var body: some View {
Capsule()
.fill(Color.gray)
.frame(width: node.NodeWidth, height: 50)
}
}
Have view that assemble Nodes in one view...
import SwiftUI
struct NodeMapView: View {
#Binding var nodes: [Node]
var body: some View {
ZStack {
ForEach(nodes, id: \.visualID) { node in
NodeView(node: node)
.offset(x: node.position.x, y: node.position.y)
}
}
}
}
And another view (in future will add view with links to nodes)
import SwiftUI
struct GraphView: View {
#ObservedObject var mech: Mechanics
var body: some View {
ZStack {
//add links view
NodeMapView(selection: selection, nodes: $mech.nodes)
}
}
}
And I have several functions in class Mechanics
import Foundation
import CoreGraphics
class Mechanics: ObservableObject {
let rootNodeID: NodeID
#Published var nodes: [Node] = []
init() {
let root = Node(text: "root")
rootNodeID = root.id
addNode(root)
}
func rootNode() -> Node {
guard let root = nodes.filter({ $0.id == rootNodeID }).first else {
fatalError("mechanics failure: no root node")
}
return root
}
func replace(_ OldNode: Node, with NewNode: Node) {
var newSet = nodes.filter { $0.id != OldNode.id }
newSet.append(NewNode)
nodes = newSet
}
}
extension Mechanics {
func updateNodeText(_ srcNode: Node, string: String) {
var newNode = srcNode
newNode.text = string
replace(srcNode, with: newNode)
}
func updateNodeWidth(_ srcNode: Node, string: String) {
var newNode = srcNode
if let n = NumberFormatter().number(from: string) {
newNode.NodeWidth = CGFloat(truncating: n)
}
replace(srcNode, with: newNode)
}
func updatePosX(_ srcNode: Node, string: String) {
var newNode = srcNode
if let n = NumberFormatter().number(from: string) {
newNode.position.x = CGFloat(truncating: n)
}
replace(srcNode, with: newNode)
}
}
Last view is
import SwiftUI
struct SurfaceView: View {
#ObservedObject var mesh: Mechanics
var body: some View {
GeometryReader { geometry in
ZStack {
Rectangle().fill(Color(red: 154/255, green: 154/255, blue: 154/255))
GraphView(mech: self.mesh)
.animation(.easeIn(duration: 0.1))
}//zstack
}//geometryreader
}//vstack
}//body end
}//view end
And the problem is:
When I use updateNodeText or updatePosX functions - View has been changed - all OK, but when I use updateNodeWidth function nothing happens.
Parameters were changed, I can see it in text fields, but view has not.
What I am doing wrong?
Update: While debugging I found out that in NodeView struct parametrs is unchanged (i dont know why) but when I'm in function parameters are correct. But when I use updateNodeText function parameters in NodeView updated to normal.
Update2: till debug I've found out that variables in cycle ForEach of NodeMapView displayed correctly but when I go into NodeView all variables displayed wrong (outdated data)
TLDR: It looks like you are using #State incorrectly in NodeView. Changing the line to let node: Node should solve the issue.
Details
In SwiftUI views react to changes in their state. Moreover a view can either own its state or reference some state it does not own. This functioning ensures that there is a unique source of truth for the state triggering view updates.
SwiftUI provide few mechanisms to declare states that are owned or not by a view.
Firstly, owned states can be declared in different ways depending on the state data type (value type vs reference type such as classes) and mutability. They have in common that they must be initialized to some value in the view itself (in the init) since by definition the view own the state and must not acquire it from an external dependency. These include:
#State for value types (Int, Double, String, ...),
#StateObject for reference types (classes declared as ObservableObject).
Here is an example:
import SwiftUI
struct Node {
var text: String
}
// NodeView owns and can mutate its node state
struct NodeView: View {
#State private var node = Node(text: "root")
var body: some View {
Text(node.text)
}
}
You should declare owned state as private to prevent issues such as breaking the unique source of truth rule.
Secondly, unowned state works the opposite of owned state: it should not be initialized by the view itself (the state is passed from another view / retrieved from the environment) and thus should not be declared private in general. Several methods exist depending on the data type and mutability (not exhaustive):
Plain let properties, for immutable unowned state,
#Binding for value types unowned state,
#ObservedObject for reference types injected by a parent view and
#EnvironmentObject for a reference type injected through the environment.
Here are some examples:
import SwiftUI
struct Node {
var text: String
}
// `NodeView` cannot mutate `node` and does not own this state...
struct NodeView: View {
let node: Node
var body: some View {
Text(node.text)
}
}
// ... since it is owned by `ContentView` and is the unique source of truth
struct ContentView: View {
#State private var node = Node(text: "root")
var body: some View {
NodeView(node: node)
}
}
import SwiftUI
struct Node {
var text: String
}
// `NodeView` can mutate this shared state...
struct NodeView: View {
#Binding var node: Node
var body: some View {
Text(node.text)
.onTapGesture {
node.text.append("!")
}
}
}
// ... which is owned by `ContentView`
struct ContentView: View {
#State private var node = Node(text: "root")
var body: some View {
NodeView(node: node)
}
}
To go back to your code, the node property of your NodeView should not be annotated with #State since the state is owned by the Mechanics object (which you'll probably instantiate in a parent view of SurfaceView as a #StateObject). Since there is no mutation, it should be a plain let property here, and if you need mutation it should be a #Binding since Node is a value type (struct).
I tested your code with this fix and it solved the issue.
I am struggling to extract single values (strings and Int) from the data I fetched from an API. I fetch the data in the form of a list:
class apiCall {
func getRockets(completion:#escaping ([Rockets]) -> ()) {
guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let rockets = try! JSONDecoder().decode([Rockets].self, from: data!)
print(rockets)
DispatchQueue.main.async {
completion(rockets)
}
}
.resume()
}
}
Then when I try using it in a View I succeed when using a List to view the values, like it shows the list of the names from the API data. But then when I want to view a single value like this:
import SwiftUI
struct RocketStatistics: View {
#State var rockets: [Rockets] = []
var body: some View {
VStack{
Text(rockets[1].name)
}
.onAppear {
apiCall().getRockets { (rockets) in
self.rockets = rockets
}
}
}
}
struct RocketStatistics_Previews: PreviewProvider {
static var previews: some View {
RocketStatistics()
}
}
It does not even give me an error, but my preview just won't update and keeps crashing.
So my question is how can I extract single values from this API data in List form and use these single values in my whole project?
To keep it simple and make it work first I started just with fetching the "name" from the API:
import Foundation
import SwiftUI
struct Rockets: Codable, Identifiable {
let id = UUID()
let name : String
}
When it all works I would also want to use Integer values from the API in my project, so tips on how to that are also welcome!
Never ever get items by a hard-coded index in a SwiftUI view without any check. When the view is rendered the first time the array is empty and any index subscription will crash.
Always check if the array contains the required number of items. In this case the array must contain at least two items
VStack{
Text(rockets.count > 1 ? rockets[1].name : "")
}
I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.
I have this model where I have a list of boat, and a list of works (which are linked to a boat). All are linked to the user. My data is stored in Firestore by my repository.
struct boat: Codable, Identifiable {
#DocumentID var id : String?
var name: String
var description: String
#ServerTimestamp var createdtime: Timestamp?
var userId: String?
}
struct Work: Identifiable, Codable {
#DocumentID var id : String?
var title: String
var descriptionpb: String
var urgent: Bool
#ServerTimestamp var createdtime: Timestamp?
var userId: String?
var boatId: String // (boatId = id of struct boat just above)
}
I have a view in which I want to display (and let the user edit) the details of the work (such as the title and descriptionpb), I manage to display the boatId (see below), however I want to display the boat name. How should I go about it?
import SwiftUI
struct WorkDetails: View {
#ObservedObject var wcvm: WorkCellVM
#ObservedObject var wlvm = WorklistVM()
#State var presentaddwork = false
var onCommit: (work) -> (Void) = { _ in }
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(wcvm.work.boatId) // <-- THIS IS WHAT I WANT TO CHANGE INTO boat name instead of boatId
.padding()
TextField("Enter work title", text: $wcvm.work.title, onCommit: {
self.onCommit(self.wcvm.work)
})
.font(.title)
.padding()
HStack {
TextField("Enter problem description", text: $wcvm.work.descriptionpb, onCommit: {
self.onCommit(self.wcvm.work)
})
}
.font(.subheadline)
.foregroundColor(.secondary)
.padding()
}
}
}
}
Essentially you have a Data Model problem, not a SwiftUI problem. I would be keeping all of this in Core Data and linking the various models(Entities in Core Data) with relationships. So your Work(Essentially a work order) would link to the boat that the work was being performed on.
Otherwise, you need to add a Boat as a parameter to Work. Either way would give you the desired syntax, but Core Data is much more efficient. It is also your data persistence model so you would kill two birds with one stone.
Solution found: when creating a work order, I was assigning the boat id, I am now assigning the boat name as well (and calling it in the work order display). Essentially keeping the same code as above, tweaking it a little bit so that it does what I want to do.
I looked everywhere for a solution to my problem, but I couldn't find any. There's this question that
is similar, but I think I'm having a different problem here. So, my code (Xcode 12.1, developing for iOS 14.0) looks like this:
import SwiftUI
struct ContentView: View {
#ObservedObject var cm : FolderModel //Which is conformed to Codable, Identifiable, Equatable, ObservableObject
#ObservedObject var dm : DataManager //Which is conformed to Equatable, Identifiable, ObservableObject
#State var pressedFolder = false
#State var valview : ValuesView
NavigationView {
VStack{
ScrollView(.horizontal) {
HStack { ForEach(dm.storageFolders) { foldersForEach in
Button(action: {
valview = ValuesView(cm: foldersForEach, dm: dm)
pressedFolder = true
}, label: {
Text(foldersForEach.folderName)})
}
if pressedFolder == false {
Form {
ForEach(dm.values) { passwordDelForEach in
NavigationLink(//This works correctly)
}
}
} else if pressedFolder == true {
valview //This is the thing that it's not updating when values are added to the folders
}
}
struct ValuesView : View {
#ObservedObject var cm : FolderModel //Which is conformed to Codable, Identifiable, Equatable, ObservableObject
#ObservedObject var dm : DataManager //Which is conformed to Equatable, Identifiable, ObservableObject
var body : some View {
Form {
ForEach (cm.folderValues) { folderValuesForEach in
NavigationLink(//This works correctly)
}
}
}
}
The arrays into the DataManager are all declared like this:
#Published var storage : [StorageModel] = [] {
didSet {
objectWillChange.send()
}
}
typealias Storage = [StorageModel]
If I add anything into the arrays (from another View), data is stored correctly because by opening the .plist file (that the DataManager creates) I can see it gets correctly updated. Plus, every Button that I use has either the func of the DataManager save() (which has objectWillChange.send() within it) or I manually add dm.objectWillChange.send() to the action of the Button.
Despite all this, the things into the ForEach don't update. I only see the things that were there the first time I open the app, and to see the changes I have to close the app and reopen it.
What am I doing wrong?
Thanks to everyone who will answer!
I am seeing the same thing, only as of using the 14.2 simulator. I'm still trying to figure it out, but it seems like views inside of a ForEach are not properly re-rendered on data change.