ForEach not working in SwiftUI after conforming to Identifiable protocol - swift

I have an array of Message which conforms to the Identifiable protocol but I keep getting this error: Generic parameter 'ID' could not be inferred. Even with the id: \.self won't work.
What's going on here?
struct Message: Identifiable {
var id = UUID()
var text: String
var createdAt: Date = Date()
var senderId: String
init(dictionary: [String: Any]) {
self.text = dictionary["text"] as? String ?? ""
self.senderId = dictionary["senderId"] as? String ?? ""
}
}
#State var messages: [Message] = []
ForEach(messages) { message in
// Generic parameter 'ID' could not be inferred
}

You need Text(message.id.uuidString)
import SwiftUI
struct Message: Identifiable {
var id = UUID()
var text: String
var createdAt: Date = Date()
var senderId: String
init(dictionary: [String: Any]) {
self.text = dictionary["text"] as? String ?? ""
self.senderId = dictionary["senderId"] as? String ?? ""
}
}
struct ContentView: View {
#State var messages: [Message] = [Message.init(dictionary: ["text":"t1","senderId":"1"]),Message.init(dictionary: ["text":"t2","senderId":"2"])]
var body: some View {
VStack {
ForEach(messages) { message in
Text(message.id.uuidString)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Edit:
import SwiftUI
struct Message: Identifiable {
var id = UUID()
var text: String
var createdAt: Date = Date()
var senderId: String
init(dictionary: [String: Any]) {
self.text = dictionary["text"] as? String ?? ""
self.senderId = dictionary["senderId"] as? String ?? ""
}
}
struct ContentView: View {
#State var messages: [Message] = []
var body: some View {
VStack {
ForEach(messages) { message in
Text(message.id.uuidString)
}
}.onAppear() {
self.messages = [Message.init(dictionary: ["text":"t1","senderId":"1"]),Message.init(dictionary: ["text":"t2","senderId":"2"])]
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

SwiftUI: How to get notified when a field in a singleton object get changed?

class SharedData: ObservableObject {
static let shared = SharedData()
#Published var sharedState = SharedState()
}
struct SharedState {
var allMMS: [MMS] = []
var typeTrees: [TTMaker] = []
var sampleInputs: [String: String] = [:]
var selectedTypeTreeName: String?
var selectedMMSPathName: String?
var maps: [String: FunctionalMap] = [:]
var mapId: String?
var selectedMenuItem: String? = nil
}
struct ContentView: View {
#ObservedObject var sharedData = SharedData.shared
set a file / string on the view:
SharedData.shared.sharedState.typeTrees.append(ttMaker)
I would expect the List in the same would get updated:
List {
ForEach(SharedData.shared.sharedState.typeTrees, id: \.self) { tree in
Button(action: {
SharedData.shared.sharedState.selectedTypeTreeName = tree.newTree.filename
}) {
HStack {
Text(tree.newTree.filename)
Spacer()
if tree.newTree.filename == SharedData.shared.sharedState.selectedTypeTreeName {
Image(systemName: "checkmark")
}
}
}
}
}
Is it any similar oslution than context in React?

How to change the value of a var with a TextField SwiftUI

I was trying to make a weather api call, the api call needs to have a location. The location that I pass is a variable, but now I want to change the location value based on a TextField's input.
I made the apiKey shorter just for safety measures. There's more code, but it's not relevant.
I just need to know how to change the city variable that is on the WeatherClass using the TextField that is in the cityTextField View.
Thanks.
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
var city: String = ""
func fetch() {
let location = city
let apiKey = "AP8LUYMSTHZ"
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(location)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}//----------------------------------- End of fetch()
}
struct WeatherData: Decodable {
let resolvedAddress: String
let days: [WeatherDays]
}
struct WeatherDays: Hashable, Decodable {
let datetime: String
let tempmax: Double
let tempmin: Double
let description: String
}
struct cityTextField: View {
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city).frame(height:30).multilineTextAlignment(.center).background().cornerRadius(25).padding(.horizontal)
}
}
I already watched a lot of tutorials for similar things buts none of them really helped me.
Try this approach using minor modifications to
func fetch(_ city: String){...} to fetch the weather for the city in your
TextField using .onSubmit{...}
struct ContentView: View {
#StateObject var weatherModel = WeatherClass()
var body: some View {
VStack {
cityTextField(weatherModel: weatherModel)
}
}
}
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city)
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch(city) // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
func fetch(_ city: String) { // <-- here
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}
Alternatively, as suggested by synapticloop, you could use this approach:
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
var body: some View {
TextField("Search city", text: $weatherModel.city) // <-- here
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch() // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
#Published var city: String = "" // <-- here
func fetch() {
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}

Binding value changes only once

I have the following View structure and run into the problem of the button working the first time I click and disabling the button like it should, but once I clicked one button it does not work for the other buttons. The Item is always the right one, I checked that by printing it out.
My ReelsView:
struct ReelsView: View {
#State var currentReel = ""
#State var items: [Item] = [
Item(chance: "1:1m", tradeIn: 2000, name: "Apple Watch", price: "$200", rarityType: "common", description: "The Apple Watch", reel: Reel(player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "apple-watch", ofType: "mp4") ?? "")), bid: false)),
Item(chance: "1:20m", tradeIn: 27500, name: "Ibiza vacation", price: "$2750,00", rarityType: "superRare", description: "Such a wonderful place for a vacation", reel: Reel(player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ibiza", ofType: "mp4") ?? "")), bid: false)),
]
var body: some View {
TabView(selection: $currentReel) {
ForEach($items) { $reel in
ReelsPlayer(reel: $reel.reel, currentReel: $currentReel, item: $reel)
.tag(reel.reel.id)
}
}
}
}
My ReelsPlayer:
struct ReelsPlayer: View {
#Binding var reel: Reel
#Binding var currentReel: String
#Binding var item: Item
var body: some View {
ZStack {
if let player = reel.player {
CustomVideoPlayer(player: player)
.allowsHitTesting(false)
}
}
.overlay {
BottomOverlay(item: $item)
.allowsHitTesting(true)
}
}
}
My BottomOverlay:
struct BottomOverlay: View {
#Binding var item: Item
var body: some View {
Button() {
item.reel.bid.toggle()
print("item: ", item)
print("item: ", $item)
} label: {
Text(item.reel.bid ? "Already Bid" : "Bid")
}
}
}
struct Reel: Identifiable {
var id = UUID().uuidString
var player: AVPlayer
var bid: Bool
}
struct Item: Identifiable, Hashable {
static func == (lhs: Item, rhs: Item) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(chance)
hasher.combine(name)
hasher.combine(price)
hasher.combine(tradeIn)
hasher.combine(rarityType)
hasher.combine(description)
}
var id: String = UUID().uuidString
var chance: String
var tradeIn: Int
var name: String
var price: String
var rarityType: String
var description: String
var reel: Reel
}
Here is the code I used in my test, to show that the two buttons acts separately. Click on one, and it print the item id and state. Click on the other, and same for that item. If you un-comment .disabled(item.clicked), then it only works once, because the Button (for that item) is now disabled.
struct Item: Identifiable {
let id = UUID()
var name: String
var clicked: Bool
}
struct ContentView: View {
var body: some View {
MainView()
}
}
struct MainView: View {
#State var items: [Item] = [Item(name: "item1", clicked: false),Item(name: "item2", clicked: false)]
var body: some View {
ForEach($items) { $item in
ItemView(item: $item)
}
}
}
struct ItemView: View {
#Binding var item: Item
var body: some View {
VStack {
Color.green
}.overlay {
OverlayView(item: $item)
}
}
}
struct OverlayView: View {
#Binding var item: Item
var body: some View {
VStack (spacing: 33){
Button() {
item.clicked.toggle()
print("--> item.id: \(item) item.clicked: \(item.clicked)")
} label: {
Text(item.name)
}//.disabled(item.clicked)
// Button() {
// item.clicked.toggle()
// } label: {
// Text("enable again")
// }
}
}
}
EDIT-1: in view of your new code. Try this example code, works well for me
import Foundation
import SwiftUI
import UIKit
import AVFoundation
import AVKit
struct ContentView: View {
var body: some View {
ReelsView()
}
}
struct ReelsView: View {
#State var currentReel = ""
#State var items: [Item] = [
Item(chance: "1:1m", tradeIn: 2000, name: "Apple Watch", price: "$200", rarityType: "common", description: "The Apple Watch", reel: Reel(player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "apple-watch", ofType: "mp4") ?? "")), bid: false)),
Item(chance: "1:20m", tradeIn: 27500, name: "Ibiza vacation", price: "$2750,00", rarityType: "superRare", description: "Such a wonderful place for a vacation", reel: Reel(player: AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ibiza", ofType: "mp4") ?? "")), bid: false)),
]
var body: some View {
TabView(selection: $currentReel) {
ForEach($items) { $item in
ReelsPlayer(currentReel: $currentReel, item: $item) // <-- here
.tag(item.reel.id)
}
}.tabViewStyle(.page) // <-- here
}
}
struct ReelsPlayer: View {
#Binding var currentReel: String
#Binding var item: Item // <-- here
var body: some View {
ZStack {
if let player = item.reel.player { // <-- here
// CustomVideoPlayer(player: player)
// for testing
VStack {
if item.name == "Apple Watch" { Color.yellow } else { Color.green }
}
.allowsHitTesting(false)
}
}
.overlay {
BottomOverlay(item: $item)
.allowsHitTesting(true)
}
}
}
struct BottomOverlay: View {
#Binding var item: Item
var body: some View {
Button() {
item.reel.bid.toggle()
print("----> BottomOverlay item.reel.bid: ", item.reel.bid) // <-- here
} label: {
Text(item.reel.bid ? "Already Bid" : "Bid")
}
}
}
struct Reel: Identifiable, Hashable { // <-- here
var id = UUID().uuidString
var player: AVPlayer
var bid: Bool
}
struct Item: Identifiable, Hashable { // <-- here
var id: String = UUID().uuidString
var chance: String
var tradeIn: Int
var name: String
var price: String
var rarityType: String
var description: String
var reel: Reel
}

Initializers with different stored properties

I have the following view where I pass a binding to an item that I need to be selected.
struct SelectionListView<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
private let data: Data
#Binding private var isPresented: Bool
#Binding private var selectedElement: Data.Element
init(
data: Data,
selectedElement: Binding<Data.Element>,
isPresented: Binding<Bool>
) {
self.data = data
_selectedElement = selectedElement
_isPresented = isPresented
}
var body: some View {
VStack {
ForEach(data) { element in
Button(element.name) {
selectedElement = element
isPresented.toggle()
}
.foregroundColor(
selectedElement.id == item.id
? .black
: .white
)
}
}
}
}
I would need a slightly different initializer of this view where I can only pass the element ID, instead of the whole element. I'm having trouble achieving this solution. To make it even more clear, it would be great if I could have a second initializer such that:
init(
data: Data,
selectedId: Binding<Data.Element.ID>,
isPresented: Binding<Bool>
)
Here is a working version. I decided to store the element or id in their own enum cases. I made the view separate just so it is a little easier to understand what I did.
Working code:
struct SelectionListView<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
enum Selected {
case element(Binding<Data.Element>)
case id(Binding<Data.Element.ID>)
}
#Binding private var isPresented: Bool
private let data: Data
private let selected: Selected
init(
data: Data,
selectedElement: Binding<Data.Element>,
isPresented: Binding<Bool>
) {
self.data = data
selected = .element(selectedElement)
_isPresented = isPresented
}
init(
data: Data,
selectedId: Binding<Data.Element.ID>,
isPresented: Binding<Bool>
) {
self.data = data
selected = .id(selectedId)
_isPresented = isPresented
}
var body: some View {
SelectionListItem(data: data) { dataElement in
switch selected {
case .element(let element):
element.wrappedValue = dataElement
print("Selected element:", element.wrappedValue)
case .id(let id):
id.wrappedValue = dataElement.id
print("Selected element ID:", id.wrappedValue)
}
isPresented.toggle()
}
}
}
struct SelectionListItem<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
let data: Data
let action: (Data.Element) -> Void
var body: some View {
VStack {
ForEach(data) { element in
Button(element.name) {
action(element)
}
.foregroundColor(
.red // Temporary because I don't know what `item.id` is
// selectedElement.id == item.id
// ? .black
// : .white
)
}
}
}
}
Other code for minimal working example:
struct ContentView: View {
#State private var selection: StrItem
#State private var selectionId: StrItem.ID
#State private var isPresented = true
private let data: [StrItem]
init() {
data = [StrItem("Hello"), StrItem("world!")]
_selection = State(initialValue: data.first!)
_selectionId = State(initialValue: data.first!.id)
}
var body: some View {
// Comment these to try each initializer
//SelectionListView(data: data, selectedElement: $selection, isPresented: $isPresented)
SelectionListView(data: data, selectedId: $selectionId, isPresented: $isPresented)
}
}
protocol Named {
var name: String { get }
}
struct StrItem: Identifiable, Named {
let id = UUID()
let str: String
var name: String { id.uuidString }
init(_ str: String) {
self.str = str
}
}
I'm not really sure what you are trying to achieve. Something feels off :) But anyway, here's a variant of your code that would do what you want:
struct SelectionListView<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
private let data: Data
#Binding private var isPresented: Bool
#Binding private var selectedElement: Data.Element
#Binding private var selectedId: Data.Element.ID
init(
data: Data,
selectedElement: Binding<Data.Element>,
isPresented: Binding<Bool>
) {
self.data = data
_selectedElement = selectedElement
_selectedId = .constant(selectedElement.wrappedValue.id)
_isPresented = isPresented
}
init(
data: Data,
selectedId: Binding<Data.Element.ID>,
isPresented: Binding<Bool>
) {
self.data = data
_selectedElement = .constant(data.first(where: { $0.id == selectedId.wrappedValue })!)
_selectedId = selectedId
_isPresented = isPresented
}
var body: some View {
VStack {
ForEach(data) { element in
Button(element.name) {
selectedElement = element
selectedId = element.id
isPresented.toggle()
}
.foregroundColor(
selectedElement.id == element.id
? .black
: .gray
)
}
}
}
}

Crash when deleting data with relation in RealmSwift

I am creating an application using RealmSwift.
The following implementation crashed when deleting related data.
After removing only "UnderlayerItem", it succeeded.
Crash when deleting UnderlayerItem and deleting Item.
The error is:
Thread 1: Exception: "The RLMArray has been invalidated or the object
containing it has been deleted."
How do I delete without crashing?
struct ListView: View {
#ObservedObject private var fetcher = Fetcher()
#State private var title = ""
var body: some View {
NavigationView {
VStack {
TextField("add", text: $title) {
let item = Item()
item.title = self.title
let realm = try! Realm()
try! realm.write {
realm.add(item)
}
self.title = ""
}
ForEach(self.fetcher.items) { (item: Item) in
NavigationLink(destination: DetailView(item: item, id: item.id)) {
Text(item.title)
}
}
}
}
}
}
struct DetailView: View {
var item: Item
var id: String
#State private var title = ""
#ObservedObject private var fetcher = Fetcher()
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
TextField("add", text: $title) {
let realm = try! Realm()
if let item = realm.objects(Item.self).filter("id == '\(self.id)'").first {
try! realm.write() {
let underlayerItem = UnderlayerItem()
underlayerItem.title = self.title
item.underlayerItems.append(underlayerItem)
}
}
self.title = ""
}
ForEach(self.item.underlayerItems) { (underlayerItems: UnderlayerItem) in
Text(underlayerItems.title)
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
self.fetcher.delete(id: self.id)
}) {
Text("Delete")
}
}
}
}
class Fetcher: ObservableObject {
var realm = try! Realm()
var objectWillChange: ObservableObjectPublisher = .init()
private(set) var items: Results<Item>
private var notificationTokens: [NotificationToken] = []
init() {
items = realm.objects(Item.self)
notificationTokens.append(items.observe { _ in
self.objectWillChange.send()
})
}
func delete(id: String) {
guard let item = realm.objects(Item.self).filter("id == '\(id)'").first else { return }
try! realm.write() {
for underlayerItem in item.underlayerItems {
realm.delete(realm.objects(UnderlayerItem.self).filter("id == '\(underlayerItem.id)'").first!)
}
}
try! realm.write() {
realm.delete(item)
}
}
}
class Item: Object, Identifiable {
#objc dynamic var id = NSUUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = NSDate()
let underlayerItems: List<UnderlayerItem> = List<UnderlayerItem>()
override static func primaryKey() -> String? {
return "id"
}
}
class UnderlayerItem: Object, Identifiable {
#objc dynamic var id = NSUUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = NSDate()
override static func primaryKey() -> String? {
return "id"
}
}
You don't need to iterate over the objects in the list to delete them. Just do this
try! realm.write() {
realm.delete(item.underlayerItems)
}
I believe it's crashing because you're attempting to access an item that was deleted
self.item.underlayerItems