I want to display different type of View depending on what is assigned to the items.
Each relevant views conforms to this protocol:
public protocol ItemDisplayer {
init(item: ItemObject)
}
This struct stores the Item object and its associated appearance View object.
public struct ItemWithDisplay {
private let item: ItemObject
private let itemDisplayer : ItemDisplayer
public init(item : ItemObject, displayer : ItemDisplayer) {
self.item = item
self.itemDisplayer = displayer
}
public func getDisplayer() -> TimeCounterDisplayer {
return itemDisplayer
}
public func getItem() -> ItemObject {
return item
}
}
There is a few type of View which show the data some kind of way, depends on what was assigned to ItemObject.
import SwiftUI
struct OtherItemView: View, ItemDisplayer {
private var item: ItemObject
init(item: ItemObject) { self.item = item }
var body: some View { ... }
}
struct DefaultItemView: View, ItemDisplayer {
private var item: ItemObject
init(item: ItemObject) { self.item = item }
var body: some View { ... }
}
// other ItemViews ...
I'd like to seperate the items with views in the modelView class, something like this:
switch displayerType {
case .other:
self.itemWithDisplayArray.append(ItemWithDisplay(item: item, displayer: OtherItemView(itemToDisplay: item)))
case .another:
self.itemWithDisplayArray.append(ItemWithDisplay(item: item, displayer: AnotherItemView(itemToDisplay: item)))
default:
self.itemWithDisplayArray.append(ItemWithDisplay(item: item, displayer: DefaultItemView(itemToDisplay: item)))
}
This was the information required for the question below.
My question concerns the following section:
ScrollView {
ForEach(viewModel.itemWithDisplayArray, id: \.itemId) { item in
VStack {
//item.getDisplayer() as! DefaultItemView // It works
item.getDisplayer() as! AnyView // Cast from 'ItemDisplayer' to unrelated type 'AnyView' always fails // how could I fix this?
}
}
}
The cast works if the ItemDisplayer casts directly to the DefaultTimerView, or OtherItemView... but I'd like to cast to any View type.
EDIT: I've made like this in UIKit:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let itemViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ItemViewCell", for: indexPath) as! ItemViewCell
let itemWithDisplay = itemWithDisplayArray[indexPath.row]
itemViewCell.containerView = UIView(frame: itemViewCell.contentView.frame)
itemViewCell.containerView!.addSubview(itemWithDisplay.getDisplayer() as! UIView) // <--the POINT is here: "as! UIView"
itemViewCell.contentView.addSubview(itemViewCell.containerView!)
itemWithDisplay.display()
return itemViewCell
}
Or in Cocoa:
class ItemGridNSView : NSView {
private let itemWithDisplay : ItemWithDisplay
init(frame frameRect: NSRect, itemWithDisplay: ItemWithDisplay) {
self.itemWithDisplay = itemWithDisplay
super.init(frame:frameRect);
self.addSubview(itemWithDisplay.getDisplayer() as! NSView) // <-- Here too
itemWithDisplay.display()
}
...
I'm looking for same solution in SwiftUI.
I want to keep the independence of the domain layer from view logic, i.e. any view specific information (import SwiftUI).
I want to avoid this sorting logic in the View layer.
Could you help me please with this?
Thank you in advance for any help you can provide!
Make ItemDisplayer conform to View. This is so you can use it like a normal View.
public protocol ItemDisplayer: View {
init(item: ItemObject)
}
And then change ItemWithDisplay so you can use ItemDisplayer (because it now has associated type requirements):
public struct ItemWithDisplay<Displayer: ItemDisplayer> {
private let item: ItemObject
private let itemDisplayer : Displayer
public init(item : ItemObject, displayer : Displayer) {
self.item = item
self.itemDisplayer = displayer
}
public func getDisplayer() -> Displayer {
return itemDisplayer
}
public func getItem() -> ItemObject {
return item
}
}
Finally, you can now use getDisplayer() to get the view, without even needing to convert it to an AnyView:
VStack {
item.getDisplayer()
}
However, to make something an AnyView, you would do the following:
VStack {
AnyView(item.getDisplayer())
}
Related
I am having an issue with my Coordinator. I am interacting with a MKMapView via SwiftUI. I am passing in a Binding to the UIViewRepresentable and need to access that same Binding in the Coordinator. Inside the Coordinator I determine what strokeColor to use for my polyline. When I try to access the routes Binding from my Coordinator it is always empty. When I set a breakpoint inside the MapView on the updateUIView function the binding is indeed populated.
Heres the code:
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
var region: MKCoordinateRegion
#Binding var routes: [RouteData]
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
mapView.setRegion(region, animated: true)
addOverlays(mapView)
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
addOverlays(view)
removeOverlays(view)
}
private func addOverlays(_ view: MKMapView) {
for route in routes {
for point in route.points {
let waypoints = point.waypoints
let polyline = MKPolyline(coordinates: waypoints, count: waypoints.count)
polyline.title = route.routeID
view.addOverlay(polyline)
}
}
}
private func removeOverlays(_ view: MKMapView) {
for overlay in view.overlays {
if let routeID = overlay.title!, routes.first(where: { $0.routeID == routeID }) == nil {
view.removeOverlay(overlay)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
class Coordinator: NSObject, MKMapViewDelegate {
let parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let routePolyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: routePolyline)
// Always prints route is empty even though I set a break point inside the parents' updateUIView func and the route is populated.
print("parents routes: \(self.parent.routes)")
if let title = routePolyline.title, let route = self.parent.routes.first(where: { $0.routeID == title }) {
renderer.strokeColor = UIColor(convertRGBStringToColor(color: route.route.rtclr))
} else {
renderer.strokeColor = UIColor.blue
}
renderer.lineWidth = 5
return renderer
}
return MKOverlayRenderer()
}
}
A few mistakes
#Binding var routes: [RouteData] should be let routes: [RouteData] because you don’t change it so don’t need the write access.
Coordinator(self) Should be Coordinator(), self is an old value the Coordinator should not hang on to.
Subclass MKPolyline to add your colour properties eg https://stackoverflow.com/a/44294417/259521
makeUIView Should return context.coordinator.mapView
addOverlays should only add ones that are not already added. You need to essentially implement a diff in updateUIView.
Update is called after make so no need to add overlays in make.
Below is my code to draw a SwiftUI view that use a publisher to get its items that need drawing in a list. The items all have boolean values drawn with a Toggle.
My view is dumb so I can use any type of boolean value, perhaps UserDefaults backed, core data backed, or simply a boolean property somewhere... anyway, this doesn't redraw when updating a bool outside of the view when one of the booleans is updated.
The onReceive is called and I can see the output change in my console, but binding isn't a part of my struct of ToggleItem and so SwiftUI doesn't redraw.
My code...
I have a struct that looks like this, note the binding type here...
struct ToggleItem: Identifiable, Equatable {
let id: String
let name: String
let isOn: Binding<Bool>
public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
lhs.id == rhs.id
}
}
And in my SwiftUI I have this...
struct MyView: View {
#State private var items: [ToggleItem] = []
let itemsPublisher: AnyPublisher<[ToggleItem], Never>
// ...
var body: some View {
List {
// ...
}
.onReceive(itemsPublisher) { newItems in
print("New items: \(newItems)")
items.removeAll() // hacky redraw
items = newItems
}
}
I can see what's going on here, as Binding<Bool> isn't a value, so SwiftUI sees the array of newItems equal to the items it's already drawn, as a result, this doesn't redraw.
Is there something I'm missing, perhaps some ingenious bit of SwiftUI/Combine that redraws this for me?
how about doing something like this instead to keep one source of truth:
struct ToggleItem: Identifiable, Equatable {
let id: String
let name: String
var isOn: Bool
public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
lhs.id == rhs.id
}
}
class ItemsPublisher: ObservableObject {
#Published var items: [ToggleItem] = [ToggleItem(id: "1", name: "name1", isOn: false)] // for testing
}
struct ContentView: View {
#ObservedObject var itemsPublisher = ItemsPublisher() // to be passed in from parent
var body: some View {
VStack {
Button("Add item") {
let randomString = UUID().uuidString
let randomBool = Bool.random()
itemsPublisher.items.append(ToggleItem(id: randomString, name: randomString, isOn: randomBool))
}
List ($itemsPublisher.items) { $item in
Toggle(isOn: $item.isOn) {
Text(item.name)
}
}
Spacer()
}.padding(.top, 50)
.onReceive(itemsPublisher.items.publisher) { newItem in
print("----> new item: \(newItem)")
}
}
}
It seems as though removing the bindings is the right way forward!
Looking at the docs this makes sense now https://developer.apple.com/documentation/swiftui/binding
Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data.
Although not explicit, this does suggest that the binding "property wrapper" is only to be used as a property in a SwiftUI view rather than a data model.
My changes
I added a closure to my view
let itemDidToggle: (ToggleItem, Bool) -> Void
and this is called in the Toggle binding's set() function which updates the value outside of the view, keeping the view dumb. This triggers the publisher to get called and update my stack. This coupled with updating the == equatable function to include the isOn property makes everything work...
My code
import UIKit
import PlaygroundSupport
import Combine
import SwiftUI
public struct MyItem {
public let identifier: String
public let description: String
}
public class ItemsManager {
public private(set) var items: [MyItem]
public let itemsPublisher: CurrentValueSubject<[MyItem], Never>
private let userDefaults: UserDefaults
public init(items: [MyItem], userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
self.items = items
self.itemsPublisher = .init(items)
}
public func isItemEnabled(identifier: String) -> Bool {
guard let item = item(for: identifier) else {
return false
}
if let isOnValue = userDefaults.object(forKey: item.identifier) as? NSNumber {
return isOnValue.boolValue
} else {
return false
}
}
public func setEnabled(_ isEnabled: Bool, forIdentifier identifier: String) {
userDefaults.set(isEnabled, forKey: identifier)
itemsPublisher.send(items)
}
func item(for identifier: String) -> MyItem? {
return items.first { $0.identifier == identifier }
}
}
struct MyView: View {
#State private var items: [ToggleItem] = []
let itemsPublisher: AnyPublisher<[ToggleItem], Never>
let itemDidToggle: (ToggleItem, Bool) -> Void
public init(
itemsPublisher: AnyPublisher<[ToggleItem], Never>,
itemDidToggle: #escaping (ToggleItem, Bool) -> Void
) {
self.itemsPublisher = itemsPublisher
self.itemDidToggle = itemDidToggle
}
var body: some View {
List {
Section(header: Text("Items")) {
ForEach(items) { item in
Toggle(
item.name,
isOn: .init(
get: { item.isOn },
set: { itemDidToggle(item, $0) }
)
)
}
}
}
.animation(.default, value: items)
.onReceive(itemsPublisher) { newItems in
print("New items: \(newItems)")
items = newItems
}
}
struct ToggleItem: Swift.Identifiable, Equatable {
let id: String
let name: String
let isOn: Bool
public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
lhs.id == rhs.id && lhs.isOn == rhs.isOn
}
}
}
let itemsManager = ItemsManager(items: (1...10).map { .init(identifier: UUID().uuidString, description: "item \($0)") })
let publisher = itemsManager.itemsPublisher
.map { myItems in
myItems.map { myItem in
MyView.ToggleItem(id: myItem.identifier, name: myItem.description, isOn: itemsManager.isItemEnabled(identifier: myItem.identifier))
}
}
.eraseToAnyPublisher()
let view = MyView(itemsPublisher: publisher) { item, newValue in
itemsManager.setEnabled(newValue, forIdentifier: item.id)
}
I am trying to wrap NSComboBox with NSViewRepresentable for use in SwiftUI. I would like to pass in both the list of dropdown options and the text value of the combo box as bindings. I would like the text value binding to update on every keystroke, and on selection of one of the dropdown options. I would also like the text value/selection of the combo box to change if binding is changed externally.
Right now, I am not seeing my binding update on option selection, let alone every keystroke, as demonstrated by the SwiftUI preview at the bottom of the code.
My latest lead from reading old documentation is that maybe in an NSComboBox the selection value and the text value are two different properties and I've written this wrapping as if they are one and the same? Trying to run that down. For my purposes, they will be one and the same, or at least only the text value will matter: it is a form field for arbitrary user string input, that also has some preset strings.
Here is the code. I think this should be paste-able into a Mac-platform playground file:
import AppKit
import SwiftUI
public struct ComboBoxRepresentable: NSViewRepresentable {
private var options: Binding<[String]>
private var text: Binding<String>
public init(options: Binding<[String]>, text: Binding<String>) {
self.options = options
self.text = text
}
public func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.delegate = context.coordinator
comboBox.usesDataSource = true
comboBox.dataSource = context.coordinator
return comboBox
}
public func updateNSView(_ comboBox: NSComboBox, context: Context) {
comboBox.stringValue = text.wrappedValue
comboBox.reloadData()
}
}
public extension ComboBoxRepresentable {
final class Coordinator: NSObject {
var options: Binding<[String]>
var text: Binding<String>
init(options: Binding<[String]>, text: Binding<String>) {
self.options = options
self.text = text
}
}
func makeCoordinator() -> Coordinator {
Coordinator(options: options, text: text)
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDelegate {
public func comboBoxSelectionDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox else { return }
text.wrappedValue = comboBox.stringValue
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDataSource {
public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
guard options.wrappedValue.indices.contains(index) else { return nil }
return options.wrappedValue[index]
}
public func numberOfItems(in comboBox: NSComboBox) -> Int {
options.wrappedValue.count
}
}
#if DEBUG
struct ComboBoxRepresentablePreviewWrapper: View {
#State private var text = "four"
var body: some View {
VStack {
Text("selection: \(text)")
ComboBoxRepresentable(
options: .constant(["one", "two", "three"]),
text: $text
)
}
}
}
struct ComboBoxRepresentable_Previews: PreviewProvider {
#State private var text = ""
static var previews: some View {
ComboBoxRepresentablePreviewWrapper()
.frame(width: 200, height: 100)
}
}
#endif
Thank you in advance if you have any suggestions!
public struct ComboBoxRepresentable: NSViewRepresentable {
//If the options change the parent should be an #State or another source of truth if they don't change just remove the #Binding
#Binding private var options: [String]
#Binding private var text: String
public init(options: Binding<[String]>, text: Binding<String>) {
self._options = options
self._text = text
}
public func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.delegate = context.coordinator
comboBox.usesDataSource = true
comboBox.dataSource = context.coordinator
comboBox.stringValue = text
comboBox.reloadData()
return comboBox
}
public func updateNSView(_ comboBox: NSComboBox, context: Context) {
//You don't need anything here the delegate updates text and the combobox is already updated
}
}
public extension ComboBoxRepresentable {
final class Coordinator: NSObject {
//This is a much simpler init and injects the new values directly int he View vs losing properties in a class updates can be unreliable
var parent: ComboBoxRepresentable
init(_ parent: ComboBoxRepresentable) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDelegate {
public func comboBoxSelectionDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox else { return }
//It is a known issue that this has to be ran async for it to have the current value
//https://stackoverflow.com/questions/5265260/comboboxselectiondidchange-gives-me-previously-selected-value
DispatchQueue.main.async {
self.parent.text = comboBox.stringValue
}
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDataSource {
public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
guard parent.options.indices.contains(index) else { return nil }
return parent.options[index]
}
public func numberOfItems(in comboBox: NSComboBox) -> Int {
parent.options.count
}
}
#if DEBUG
struct ComboBoxRepresentablePreviewWrapper: View {
#State private var text = "four"
//If they dont update remove the #Binding
#State private var options = ["one", "two", "three"]
var body: some View {
VStack {
Text("selection: \(text)")
ComboBoxRepresentable(
options: $options,
text: $text
)
}
}
}
struct ComboBoxRepresentable_Previews: PreviewProvider {
#State private var text = ""
static var previews: some View {
ComboBoxRepresentablePreviewWrapper()
.frame(width: 200, height: 100)
}
}
#endif
Okay, I think I have come to a solution that satisfies the requirements I laid out in the question:
public struct ComboBoxRepresentable: NSViewRepresentable {
private let title: String
private var text: Binding<String>
private var options: Binding<[String]>
private var onEditingChanged: (Bool) -> Void
public init(
_ title: String,
text: Binding<String>,
options: Binding<[String]>,
onEditingChanged: #escaping (Bool) -> Void = { _ in }
) {
self.title = title
self.text = text
self.options = options
self.onEditingChanged = onEditingChanged
}
public func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.delegate = context.coordinator
comboBox.usesDataSource = true
comboBox.dataSource = context.coordinator
comboBox.placeholderString = title
comboBox.completes = true
return comboBox
}
public func updateNSView(_ comboBox: NSComboBox, context: Context) {
comboBox.stringValue = text.wrappedValue
comboBox.reloadData()
}
}
public extension ComboBoxRepresentable {
final class Coordinator: NSObject {
private var parent: ComboBoxRepresentable
init(parent: ComboBoxRepresentable) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDelegate {
public func comboBoxSelectionDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox,
parent.options.wrappedValue.indices.contains(comboBox.indexOfSelectedItem) else { return }
parent.text.wrappedValue = parent.options.wrappedValue[comboBox.indexOfSelectedItem]
}
public func controlTextDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox else { return }
parent.text.wrappedValue = comboBox.stringValue
}
public func controlTextDidBeginEditing(_ notification: Notification) {
parent.onEditingChanged(true)
}
public func controlTextDidEndEditing(_ notification: Notification) {
parent.onEditingChanged(false)
}
}
extension ComboBoxRepresentable.Coordinator: NSComboBoxDataSource {
public func comboBox(_ comboBox: NSComboBox, completedString string: String) -> String? {
parent.options.wrappedValue.first { $0.hasPrefix(string) }
}
public func comboBox(_ comboBox: NSComboBox, indexOfItemWithStringValue string: String) -> Int {
guard let index = parent.options.wrappedValue.firstIndex(of: string) else { return NSNotFound }
return index
}
public func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
guard parent.options.wrappedValue.indices.contains(index) else { return nil }
return parent.options.wrappedValue[index]
}
public func numberOfItems(in comboBox: NSComboBox) -> Int {
parent.options.wrappedValue.count
}
}
On the point about updating the bound value as the user types, to get that you have implement the parent NSTextField delegate method controlTextDidChange.
And then in comboBoxSelectionDidChange, you need to update the bound value from the bound options using the combo box's newly-selected index.
I'm looking to call a function inside a UIKit UIViewController from a button managed by Swift UI
In my Swift UI View I have:
struct CameraView: View {
var body: some View {
cameraViewController = CameraViewController()
...
which I see creates two instances, one directly created just like calling any class, and the other created by the required makeUIViewController method needed for Swift UI to manage UIKit UIViewControllers.
However when I attached a function to a button in my Swift UI say, cameraViewController.takePhoto() The instance that is referenced is not the one displayed.
How can I obtain the specific instance that is displayed?
There are probably multiple solutions to this problem, but one way or another, you'll need to find a way to keep a reference to or communicate with the UIViewController. Because SwiftUI views themselves are pretty transient, you can't just store a reference in the view itself, because it could get recreated at any time.
Tools to use:
ObservableObject -- this will let you store data in a class instead of a struct and will make it easier to store references, connect data, etc
Coordinator -- in a UIViewRepresentable, you can use a Coordinator pattern which will allow you to store references to the UIViewController and communicate with it
Combine Publishers -- these are totally optional, but I've chosen to use them here since they're an easy way to move data around without too much boilerplate code.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var vcLink = VCLink()
var body: some View {
VStack {
VCRepresented(vcLink: vcLink)
Button("Take photo") {
vcLink.takePhoto()
}
}
}
}
enum LinkAction {
case takePhoto
}
class VCLink : ObservableObject {
#Published var action : LinkAction?
func takePhoto() {
action = .takePhoto
}
}
class CustomVC : UIViewController {
func action(_ action : LinkAction) {
print("\(action)")
}
}
struct VCRepresented : UIViewControllerRepresentable {
var vcLink : VCLink
class Coordinator {
var vcLink : VCLink? {
didSet {
cancelable = vcLink?.$action.sink(receiveValue: { (action) in
guard let action = action else {
return
}
self.viewController?.action(action)
})
}
}
var viewController : CustomVC?
private var cancelable : AnyCancellable?
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIViewController(context: Context) -> CustomVC {
return CustomVC()
}
func updateUIViewController(_ uiViewController: CustomVC, context: Context) {
context.coordinator.viewController = uiViewController
context.coordinator.vcLink = vcLink
}
}
What happens here:
VCLink is an ObservableObject that I'm using as a go-between to communicate between views
The ContentView has a reference to the VCLink -- when the button is pressed, the Publisher on VCLink communicates that to any subscribers
When the VCRepresented is created/updated, I store a reference to the ViewController and the VCLink in its Coordinator
The Coordinator takes the Publisher and in its sink method, performs an action on the stored ViewController. In this demo, I'm just printing the action. In your example, you'd want to trigger the photo itself.
It's possible to make a reference from SwiftUI to your view controller if you need to call its functions directly and without unnecessary code:
class Reference<T: AnyObject> {
weak var object: T?
}
class PlayerViewController: UIViewController {
func resume() {
print("resume")
}
func pause() {
print("pause")
}
}
struct PlayerView: UIViewControllerRepresentable {
let reference: Reference<PlayerViewController>
func makeUIViewController(context: Context) -> PlayerViewController {
let controller = PlayerViewController()
// Set controller to the reference
reference.object = controller
return controller
}
func updateUIViewController(_ viewController: PlayerViewController, context: Context) {
}
}
struct ContentView: View {
let reference = Reference<PlayerViewController>()
var body: some View {
Button("Resume") {
reference.object?.resume()
}
Button("Pause") {
reference.object?.pause()
}
PlayerView(reference: reference)
}
}
I want to use a realm database in my SwiftUI app and I would like to apply the MVVM pattern. Unfortunately when I create a list with the elements in my database I get a Fatal error: Unexpectedly found nil while unwrapping an Optional value: error message
DatabaseManager:
class DatabaseManager{
private let realm: Realm
public static let sharedInstance = DatabaseManager()
private init(){
realm = try! Realm()
}
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = Realm.objects(type)
return results
}
}
Model:
class FlashcardDeck: Object, Codable, Identifiable{
#objc private (set) dynamic var id = NSUUID().uuidString
#objc dynamic var title: String?
var cards = RealmSwift.List<Flashcard>()
convenience init(title: String?, cards: [Flashcard]){
self.init()
self.title = title
self.cards.append(objectsIn: cards)
}
override class func primaryKey() -> String? {
return "id"
}
}
ViewModel
class FlashcardDeckViewModel: ObservableObject{
let realm = DatabaseManager.sharedInstance
#Published var decks: Results<FlashcardDeck>?
public func fetchDecks(){
decks = realm.fetchData(type: FlashcardDeck.self)
}
}
View
struct FlashcardDeckView: View {
private let gridItems = [GridItem(.flexible())]
#StateObject var viewModel = FlashcardDeckViewModel()
var body: some View {
NavigationView{
ScrollView{
LazyVGrid(columns: gridItems, spacing: 30){
ForEach(viewModel.decks!) { item in // <----- ERROR APPEARS HERE
FlashcardDeckItem(deck: item)
}
}
}
.navigationTitle("Flashcard decks")
}
.onAppear{
self.viewModel.fetchDecks()
print(self.viewModel.cards?[0].title) // <------ prints the title of the deck! So this element exists
}
}
}
I'm pretty sure that my database has an element and if I try to print the name of the deck in the fetchData()function it will be displayed. I know the line ForEach(viewModel.decks!)isn't beautiful code, but this is just for testing/debugging now.
Include it conditionally, like
NavigationView{
if viewModel.decks == nil {
Text("Loading...")
} else {
ScrollView{
LazyVGrid(columns: gridItems, spacing: 30){
ForEach(viewModel.decks!) { item in // <----- ERROR APPEARS HERE
FlashcardDeckItem(deck: item)
}
}
}
.navigationTitle("Flashcard decks")
}
}