View does not update when some variables in #state struct changes - macos

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.

Related

Display subview from an array in SwiftUI

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.

SwiftUI - Edit Button causing a strange (and annoying) animation

I recently implemented an Edit Button into my app. However, it created a strange and annoying animation in the View both when it loads (everything comes in from the left), and when I sort the elements. I noticed that if I remove the .animation that is after the .environment it solves this issue, but then everything appears and moves instantly without the .easeInOut look that I wanted to give. How can I apply this animation only to the appearing and disappearing of the sort and delete buttons of the cells of the Form?
If you want to take a look at my problem (since I don't think I was able to explain it correctly) you can look at this video.
The code is this one, ContentView:
import SwiftUI
struct ContentView: View {
#ObservedObject var dm : DataManager
#ObservedObject var vm : ValueModel
#State var showAlertDeleteContact = false
#State var isEditing = false
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView {
Color(UIColor.systemGray6).ignoresSafeArea(.all).overlay(
VStack {
scrollViewFolders
Form {
ForEach(dm.storageValues) { contacts in
NavigationLink(
destination:
//Contact View,
label: {
IconView(dm: dm, vm: contacts)
})
}.onDelete(perform: { indexSet in
self.showAlertDeleteContact = true
self.indexDeleteContact = indexSet
})
.onMove(perform: onMove)
Section {
buttonNewFolder
buttonSort
}
}
}
.navigationBarTitle("Contacts")
.navigationBarItems(trailing: editButton)
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive))
.animation(.easeInOut)
//I also tried with this -> .animation(.some(Animation.default))
)
}.alert(isPresented: $showAlertDeleteContact, content: {
alertDeleteContact
})
}
If you want to recreate the project, the DataManager is:
import SwiftUI
import Combine
class DataManager : Equatable, Identifiable, ObservableObject {
static let shared = DataManager()
#Published var storageValues : [ValueModel] = []
typealias StorageValues = [ValueModel]
//The rest of the code
}
And the ValueModel is:
import SwiftUI
import Combine
class ValueModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var valueName : String
var notes : String?
var expires : Date?
init(valueName: String, notes: String?, expires: Date?) {
self.valueName = valueName
self.notes = notes
self.expires = expires
}
}
Thanks to everyone who will help me!

Build a basic Altimeter with SwiftUi for Apple Watch

I am trying to build a MVP with SwiftUI that simply shows me the changes in altitude on my Apple Watch. From there I will figure out where to go next (I want to use it for paragliding and other aviation things).
I have previous experience in python, but nothing in Swift, so even after a ton of tutorials I am very unsure about how and where to declare and then use functionalities.
Here is my code so far:
//
// ContentView.swift
// Altimeter WatchKit Extension
//
// Created by Luke Crouch on 29.09.20.
//
import SwiftUI
import CoreMotion
//class func isRelativeAltitudeAvailable() -> Bool
struct ContentView: View {
let motionManager = CMMotionManager()
let queue = OperationQueue()
let altimeter = CMAltimeter()
let altitude = 0
var relativeAltitude: NSNumber = 0
var body: some View {
if motionManager.isRelativeAltitudeAvailable() {
switch CMAltimeter.authorizationStatus() {
case .notDetermined: // Handle state before user prompt
fatalError("Awaiting user prompt...")
case .restricted: // Handle system-wide restriction
fatalError("Authorization restricted!")
case .denied: // Handle user denied state
fatalError("Auhtorization denied!")
case .authorized: // Ready to go!
print("Authorized!")
#unknown default:
fatalError("Unknown Authorization Status")
}
altimeter.startRelativeAltitudeUpdates(to: queue, withHandler: CMAltitudeHandler)
}
// something like relative Altitude = queue[..]
Text("\(relativeAltitude)")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.green)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am getting multiple errors that I dont know how to deal with:
Type 'Void' cannot conform to 'View', only struct/enum/class types can conform to protocols.
Value of type CMMotionManager has no member 'isRelativeAltitudeAvailable'
Type '()' cannot conform to View...
Cannot convert value of type 'CMAltitudeHandler.Type' (aka '((Optional, Optional) -> ()).Type') to expected argument type 'CMAltitudeHandler' (aka '(Optional, Optional) -> ()')
Could you please give me some hints?
Thank you so much!
Luke
I figured it out after trying around a lot:
//
// ContentView.swift
// Altimeter WatchKit Extension
//
// Created by Lukas Wheldon on 29.09.20.
//
import SwiftUI
import CoreMotion
struct ContentView: View {
#State var relativeAltitude: NSNumber = 0
#State var altitude = 0
let altimeter = CMAltimeter()
func update(d: CMAltitudeData?, e: Error?){
print("altitude \(altitude)")
print("CMAltimeter \(altimeter)")
print("relative Altitude \(relativeAltitude))")
}
var body: some View {
VStack {
Text("\(altimeter)")
.fontWeight(.bold)
.foregroundColor(Color.green)
Button(action: {
print("START")
self.startAltimeter()
}, label: {
Text("Start Altimeter")
.bold()
.foregroundColor(.green)
})
}
}
func startAltimeter() {
if CMAltimeter.isRelativeAltitudeAvailable() {
switch CMAltimeter.authorizationStatus() {
case .notDetermined: // Handle state before user prompt
print("bb")
//fatalError("Awaiting user prompt...")
case .restricted: // Handle system-wide restriction
fatalError("Authorization restricted!")
case .denied: // Handle user denied state
fatalError("Authorization denied!")
case .authorized: // Ready to go!
let _ = print("Authorized!")
#unknown default:
fatalError("Unknown Authorization Status")
}
self.altimeter.startRelativeAltitudeUpdates(to: OperationQueue.main) {(data,error) in DispatchQueue.main.async {
print("\(altitude)")
print("\(relativeAltitude)")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Next steps will be to check if I can access the barometer raw data and calculate altitudes on that.

Swift 5.0 Generate subset of object array

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)!])
}
}

Published works for single object but not for array of objects

I am trying to make individually moveable objects. I am able to successfully do it for one object but once I place it into an array, the objects are not able to move anymore.
Model:
class SocialStore: ObservableObject {
#Published var socials : [Social]
init(socials: [Social]){
self.socials = socials
}
}
class Social : ObservableObject{
var id: Int
var imageName: String
var companyName: String
#Published var pos: CGPoint
init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
self.id = id
self.imageName = imageName
self.companyName = companyName
self.pos = pos
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
Multiple image (images not following drag):
struct ContentView : View {
#ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)
#ObservedObject var images: Social = testData[2]
var body: some View {
VStack {
ForEach(socialObject.socials, id: \.id) { social in
Image(social.imageName)
.position(social.pos)
.gesture(social.dragGesture)
}
}
}
}
Single image (image follow gesture):
struct ContentView : View {
#ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)
#ObservedObject var images: Social = testData[2]
var body: some View {
VStack {
Image(images.imageName)
.position(images.pos)
.gesture(images.dragGesture)
}
}
}
I expect the individual items to be able to move freely . I see that the coordinates are updating but the position of each image is not.
First, a disclaimer: The code below is not meant as a copy-and-paste solution. Its only goal is to help you understand the challenge. There may be more efficient ways of resolving it, so take your time to think of your implementation once you understand the problem.
Why the view does not update?: The #Publisher in SocialStore will only emit an update when the array changes. Since nothing is being added or removed from the array, nothing will happen. Additionally, because the array elements are objects (and not values), when they do change their position, the array remains unaltered, because the reference to the objects remains the same. Remember: Classes create objects, Structs create values.
We need a way of making the store, to emit a change when something in its element changes. In the example below, your store will subscribe to each of its elements bindings. Now, all published updates from your items, will be relayed to your store publisher, and you will obtain the desired result.
import SwiftUI
import Combine
class SocialStore: ObservableObject {
#Published var socials : [Social]
var cancellables = [AnyCancellable]()
init(socials: [Social]){
self.socials = socials
self.socials.forEach({
let c = $0.objectWillChange.sink(receiveValue: { self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
}
}
class Social : ObservableObject{
var id: Int
var imageName: String
var companyName: String
#Published var pos: CGPoint
init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
self.id = id
self.imageName = imageName
self.companyName = companyName
self.pos = pos
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
struct ContentView : View {
#ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)
var body: some View {
VStack {
ForEach(socialObject.socials, id: \.id) { social in
Image(social.imageName)
.position(social.pos)
.gesture(social.dragGesture)
}
}
}
}
For those who might find it helpful. This is a more generic approach to #kontiki 's answer.
This way you will not have to be repeating yourself for different model class types.
import Foundation
import Combine
import SwiftUI
class ObservableArray<T>: ObservableObject {
#Published var array:[T] = []
var cancellables = [AnyCancellable]()
init(array: [T]) {
self.array = array
}
func observeChildrenChanges<K>(_ type:K.Type) throws ->ObservableArray<T> where K : ObservableObject{
let array2 = array as! [K]
array2.forEach({
let c = $0.objectWillChange.sink(receiveValue: { _ in self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
return self
}
}
class Social : ObservableObject{
var id: Int
var imageName: String
var companyName: String
#Published var pos: CGPoint
init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
self.id = id
self.imageName = imageName
self.companyName = companyName
self.pos = pos
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
struct ContentView : View {
//For observing changes to the array only.
//No need for model class(in this case Social) to conform to ObservabeObject protocol
#ObservedObject var socialObject: ObservableArray<Social> = ObservableArray(array: testData)
//For observing changes to the array and changes inside its children
//Note: The model class(in this case Social) must conform to ObservableObject protocol
#ObservedObject var socialObject: ObservableArray<Social> = try! ObservableArray(array: testData).observeChildrenChanges(Social.self)
var body: some View {
VStack {
ForEach(socialObject.array, id: \.id) { social in
Image(social.imageName)
.position(social.pos)
.gesture(social.dragGesture)
}
}
}
}
There are two ObservableObject types and the one that you are interested in is Combine.ObservableObject. It requires an objectWillChange variable of type ObservableObjectPublisher and it is this that SwiftUI uses to trigger a new rendering. I am not sure what Foundation.ObservableObject is used for but it is confusing.
#Published creates a PassthroughSubject publisher that can be connected to a sink somewhere else but which isn't useful to SwiftUI, except for .onReceive() of course.
You need to implement
let objectWillChange = ObservableObjectPublisher()
in your ObservableObject class

Resources