I want to make my custom list in SwiftUI. I tried to mimic this List’s initializer:
public init<Data, RowContent>(
_ data: Data,
selection: Binding<SelectionValue?>?,
#ViewBuilder rowContent: #escaping (Data.Element) -> RowContent
) where
Content == ForEach<Data, Data.Element.ID, RowContent>,
Data: RandomAccessCollection,
RowContent: View,
Data.Element: Identifiable
I implemented my own List that way:
struct CustomList<Data: RandomAccessCollection,
Selection: Identifiable,
Content: View>: View where Data.Element: Identifiable {
#Binding var selection: Selection?
let content: Content
init<RowContent>(
_ data: Data,
selection: Binding<Selection?>?,
#ViewBuilder rowContent: #escaping (Data.Element) -> RowContent
) where
Content == ForEach<Data, Data.Element.ID, RowContent>,
Data: RandomAccessCollection,
RowContent: View,
Data.Element: Identifiable {
self._selection = selection ?? .constant(nil)
self.content = ForEach(data, content: rowContent)
}
var body: some View {...}
}
I used list with data, defined as
#FetchedResults(…) var myData: FetchedResults<MyData>
MyData declaration:
class MyData: NSManagedObject {
#NSManaged public var id: UUID?
…
}
extension MyData: Identifiable { }
It seems, like everything is fine, but when I try to change List to CustomList, code does not compile.
List(myData, selection: $selectedID) { data in // Works fine
MyDataDetail(data: data, selection: $selectedID)
}
CustomList(myData, selection: $selectedID) { data in // ERROR: Generic struct 'CustomList' requires that 'MyData' (aka 'Optional<UUID>') conform to 'Identifiable'
MyDataDetail(data: data, selection: $selectedID)
}
What should be changed in CustomList declaration, so it will be able to replace List?
So, after some time working on this issue, I’ve found out that issue as not in ForEach, but in requirement of Selection to conform to Identifiable protocol
I just changed
struct CustomList<Data: RandomAccessCollection,
Selection: Identifiable,
Content: View>: View where Data.Element: Identifiable {…
to
struct CustomList<Data: RandomAccessCollection,
Selection,
Content: View>: View where Data.Element: Identifiable {…
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 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 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!
I have a number of SwapItem structs, each with a child SwapItemChild. Then, using a ForEach of SwiftUI, I would like to display the name of each SwapItem, called the item view, containing also a circle in the color of its respective SwapItemChild, called the child view. Subsequently, I would like to swap the children of two items, and have the respective child views change places animated. This was inspired by other examples of this effect by this extensive tutorial, but not specifically the children view swapping.
I attempt to do so using a matchedGeometryEffect identifying each child view by the id of the respective SwapItemChild. However, this leads to a jumpy animation, where only the top child view moves down, whereas the bottom child view instantaneously jumps to the top.
The functional example code is as follows.
// MARK: - Model
struct SwapItem: Identifiable {
let id = UUID()
let name: String
var child: SwapItemChild
}
struct SwapItemChild: Identifiable {
let id = UUID()
let color: Color
}
class SwapItemStore: ObservableObject {
#Published private(set) var items = [SwapItem(name: "Task 1", child: SwapItemChild(color: .red)),
SwapItem(name: "Task 2", child: SwapItemChild(color: .orange))]
func swapOuterChildren(){
let tmpChild = items[0].child
items[0].child = items[1].child
items[1].child = tmpChild
}
}
// MARK: - View
struct SwapTestView: View {
#StateObject private var swapItemStore = SwapItemStore()
#Namespace private var SwapViewNS
var body: some View {
VStack(spacing: 50.0) {
Button(action: swapItemStore.swapOuterChildren){
Text("Swap outer children")
.font(.title)
}
VStack(spacing: 150.0) {
ForEach(swapItemStore.items){ item in
SwapTestItemView(item: item, ns: SwapViewNS)
}
}
}
}
}
struct SwapTestItemView: View {
let item: SwapItem
let ns: Namespace.ID
var body: some View {
HStack {
Circle()
.fill(item.child.color)
.frame(width: 100, height: 100)
.matchedGeometryEffect(id: item.child.id, in: ns)
.animation(.spring())
Text(item.name)
}
}
}
What is the correct implementation of matchedGeometryEffect to have these child views swapping places seamlessly?
I have already encountered this kind of problem, try this :
ForEach(swapItemStore.items, id: \.self.child.id)
Another way :
struct SwapItem: Identifiable, Hashable {
let id = UUID()
let name: String
var child: SwapItemChild
}
struct SwapItemChild: Identifiable, Hashable {
let id = UUID()
let color: Color
}
with :
ForEach(swapItemStore.items, id: \.self)
See : https://www.hackingwithswift.com/books/ios-swiftui/why-does-self-work-for-foreach
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