I am working on a SwiftUI project and attempting to use MVVM. I am using Firebase as a backend and recently introduced rules on my Firestore database and am having a lot of trouble which I believe is related to my implementation of MVVM.
I followed a tutorial from Google on MVVM and I believe the larger project I am working on is causing issues. Here is a base case for how I'm using MVVM.
Repository Files
These files are being used connect to Firestore. Here is a UserRepository example.
class ListingRepository: ObservableObject {
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistration?
#Published var listings = [Listing]()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
if snapshotListener == nil {
self.snapshotListener = db.collection(FirestoreCollection.listings).order(by: "created", descending: true).addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Listings.")
return
}
// Documents exist.
self.listings = documents.compactMap { listing in
do {
return try listing.data(as: Listing.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
func stopSnapshotListener() {
if snapshotListener != nil {
snapshotListener?.remove()
snapshotListener = nil
}
}
}
ViewModel and #main
In the app. Listings are a part of a Marketplace. I am making my ViewModel the #EnvironmentObject. In #main I have the following. I am initializing an instance of ListingRepository here.
#main
struct ExchangeApp: App {
// #EnvironmentObjects
#StateObject private var marketplaceViewModel = MarketplaceViewModel(listingRepository: ListingRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(marketplaceViewModel)
}
}
}
MarketplaceViewModel
class MarketplaceViewModel: ObservableObject {
var listingRepository: ListingRepository
#Published var listingRowViewModels = [ListingRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init(listingRepository: ListingRepository) {
self.listingRepository = listingRepository
self.startCombine()
}
func startCombine() {
listingRepository
.$listings
.receive(on: RunLoop.main)
.map { listings in
listings
.map { listing in
ListingRowViewModel(listing: listing)
}
}
.assign(to: \.listingRowViewModels, on: self)
.store(in: &cancellables)
}
}
MarketplaceView
In MarketplaceView I take each listing and present it. There are other files here that are not shown but I don't believe this is important to the question. I am happy to provide more if needed.
struct MarketplaceView: View {
let db = Firestore.firestore()
#EnvironmentObject var authSession: AuthSession
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
NavigationView {
List {
ForEach(self.marketplaceViewModel.filteredListingRowViewModels, id: \.id) { listingRowViewModel in
NavigationLink(destination: ListingDetailView(listingDetailViewModel: ListingDetailViewModel(listing: listingRowViewModel.listing, userRepository: UserRepository()))
.environmentObject(authSession)
) {
ListingRowView(listingRowViewModel: listingRowViewModel)
}
}
}
.navigationTitle("Marketplace")
}
}
}
While this works really well, I am not sure its expands to a larger app well. I have a lot of Repository files and view models which means my #main ends up looking like the following.
#main
#main
struct Global_Seafood_ExchangeApp: App {
#StateObject private var authListener = AuthSession(userRepository: UserRepository(), productRepository: ProductRepository(), listingRepository: ListingRepository(), offerRepository: OfferRepository(), orderRepository: OrderRepository(), businessAddressRepository: BusinessAddressRepository(), bankAccountRepository: BankAccountRepository())
#StateObject private var marketplaceViewModel = MarketplaceViewModel(listingRepository: ListingRepository())
#StateObject private var createListingViewModel = CreateListingViewModel(listingRepository: ListingRepository(), productRepository: SeafoodRepository())
#StateObject private var accountViewModel = AccountViewModel(listingRepository: ListingRepository(), offerRepository: OfferRepository(), orderRepository: OrderRepository())
#StateObject private var addBusinessViewModel = AddBusinessAddressViewModel(businessAddressRepository: BusinessAddressRepository())
#StateObject private var addBankAccountViewModel = AddBankAccountViewModel(bankAccountRepository: BankAccountRepository())
#StateObject private var listingHistoryViewModel = ListingHistoryViewModel(listingRepository: ListingRepository())
#StateObject private var offerHistoryViewModel = OfferHistoryViewModel(offerRepository: OfferRepository())
#StateObject private var orderHistoryViewModel = OrderHistoryViewModel(orderRepository: OrderRepository())
#StateObject private var businessAddressViewModel = BusinessAddressViewModel(businessAddressRepository: BusinessAddressRepository())
#StateObject private var bankAccountViewModel = BankAccountViewModel(bankAccountRepository: BankAccountRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(authListener)
.environmentObject(marketplaceViewModel)
.environmentObject(createListingViewModel)
.environmentObject(accountViewModel)
.environmentObject(addBusinessViewModel)
.environmentObject(addBankAccountViewModel)
.environmentObject(listingHistoryViewModel)
.environmentObject(offerHistoryViewModel)
.environmentObject(orderHistoryViewModel)
.environmentObject(businessAddressViewModel)
.environmentObject(bankAccountViewModel)
}
}
}
You can see here that Repositories are initialized multiple times. I don't think this is a good idea.
My issue really started when I added Firestore rules to my database. I am requiring someone to be authenticated to view data from the repositories. When someone logs in, I make a call to start all of the repositories and set up the Snapshot Listeners, and then when someone logs out I make a call to stop all of the Snapshot Listeners. However, even after stopping them, multiple are still running which I believe is due to the fact that I've initialized more than one in #main. I have confirmed this since I get permission request errors from Firestore letting me know the listeners are returning errors since the user is not authenticated.
Singletons
I looked into using Singletons but this seems to be a bad design choice.
StateObject
I looked at using the Repositories as StateObjects but this creates UI update issues.
My two questions.
Can someone confirm my issues is that I am instantiating multiple instances of Repositories?
What MVVM practice is best for an app this size? I understand this is a loaded question but I've read a number of tutorials and all seem to say they are a good idea. However, in practice I am running into issues with the ones I've tried.
Any help would be greatly appreciated.
Related
I would like to display multiple pins on a map. One for each of my users. The location of my users is stored on a Firebase database.
Here's my code :
import SwiftUI
import Firebase
import FirebaseFirestore
import CoreLocation
import MapKit
struct mapTimelineView: View {
#StateObject private var locationViewModel = LocationViewModel.shared
#ObservedObject var viewModel = TimelineViewModel()
#ObservedObject var authViewModel = AuthViewModel()
var body: some View {
ZStack (alignment: .bottomTrailing) {
GeoPointView(position: post.location)
}
}
}
struct GeoPointView : View {
var position : GeoPoint
struct IdentifiablePoint: Identifiable {
var id = UUID()
var position : GeoPoint
}
#StateObject private var locationViewModel = LocationViewModel.shared
var body: some View {
Map(coordinateRegion: $locationViewModel.region, showsUserLocation: true, annotationItems: [position].map { IdentifiablePoint(position: $0)}) { point in
MapMarker(coordinate: CLLocationCoordinate2D(latitude: point.position.latitude, longitude: point.position.longitude), tint: Color("accentColor"))
}
}
}
post.location retrieves the location data from the database.
I have tried to use a ForEach loop (ForEach(viewModel.posts) { post in [...code here...] }) but it did not work.
I have no errors in my code but I can't put the markers for each user. So I would like to know : How can I put different pins according to the location of my users ?
I have refactored a Web API to rely on async/await in ASP.NET Core 3.1 and I have the following scenario: a statistics method is sequentially computing a list of indicators which are defined in a list.
readonly Dictionary<StatisticItemEnum, Func<Task<SimpleStatisticItemApiModel>>> simpleItemActionMap =
new Dictionary<StatisticItemEnum, Func<Task<SimpleStatisticItemApiModel>>>();
private void InitSimpleStatisticFunctionsMap()
{
simpleItemActionMap.Add(StatisticItemEnum.AllQuestionCount, GetAllQuestionCountApiModel);
simpleItemActionMap.Add(StatisticItemEnum.AllAnswerCount, GetAllAnswerCountApiModel);
simpleItemActionMap.Add(StatisticItemEnum.AverageAnswer, GetAverageAnswer);
// other mappings here
}
private async Task<SimpleStatisticItemApiModel> GetAllQuestionCountApiModel()
{
// await for database operation
}
private async Task<SimpleStatisticItemApiModel> GetAllAnswerCountApiModel()
{
// await for database operation
}
private async Task<SimpleStatisticItemApiModel> GetAverageAnswer()
{
// await for database operation
}
The code sequentially goes through each item and computes it and after the refactoring it is looking like this:
itemIds.ForEach(itemId =>
{
var itemEnumValue = (StatisticItemEnum) itemId;
if (simpleItemActionMap.ContainsKey(itemEnumValue))
{
var result = simpleItemActionMap[itemEnumValue]().Result;
payload.SimpleStatisticItemModels.Add(result);
}
});
I know that Task.Result might lead to deadlocks, but I could not find any other way to make this work.
Question: How to execute a dynamic list of async functions in a sequential way?
You should change the ForEach call to a regular foreach, and then you can use await:
foreach (var itemId in itemIds)
{
var itemEnumValue = (StatisticItemEnum) itemId;
if (simpleItemActionMap.ContainsKey(itemEnumValue))
{
var result = await simpleItemActionMap[itemEnumValue]();
payload.SimpleStatisticItemModels.Add(result);
}
}
Do not make the ForEach lambda async; that will result in an async void method, and you should avoid async void.
I think you can do this:
itemIds.ForEach(async itemId =>
{
var itemEnumValue = (StatisticItemEnum) itemId;
if (simpleItemActionMap.ContainsKey(itemEnumValue))
{
var result = await simpleItemActionMap[itemEnumValue]();
payload.SimpleStatisticItemModels.Add(result);
}
});
I have a phone application. When a screen displays I start a timer like this:
base.OnAppearing();
{
timerRunning = true;
Device.BeginInvokeOnMainThread(() => showGridTime(5));
}
async void showGridTime(int time)
{
while (timerRunning)
{
var tokenSource = new CancellationTokenSource();
await Task.Delay(time, tokenSource.Token);
detailGrid.IsVisible = true;
}
}
The code seems to work but there is a warning message in the IDE saying that an async method cannot return null.
Given this code can someone help and give me advice on what I should return and if I am going about this in the correct way?
Just return a task:
async Task ShowGridTimeAsync(int time)
{
while (timerRunning)
{
var tokenSource = new CancellationTokenSource();
await Task.Delay(time, tokenSource.Token);
detailGrid.IsVisible = true;
}
}
This is necessary to have the caller of this method know when it is completed and act accordingly.
It is not recommended to create async void methods unless you're creating an event or forced to do that in order to meet an interface signature.
I am writing a Metro App.
I am trying to read a file and return a float[] from the data. But no matter what I do, the function seems to return null. I have tried the solutions to similar questions to no luck.
For example if I use:
float[] floatArray = new ModelReader("filename.txt").ReadModel()
The result will be a null array.
However if I use:
new ModelReader("filename.txt")
The correct array will be printed to the console because "Test" also prints the array before returning it. This seems very weird to me.
Please give me some guidance, I have no idea what is wrong.
public class ModelReader
{
float[] array;
public ModelReader(String name)
{
ReadModelAsync(name);
}
public float[] ReadModel()
{
return array;
}
private async Task ReadModelAsync(String name)
{
await readFile(name);
}
async Task readFile(String name)
{
// settings
var path = #"Assets\models\" + name;
var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
// acquire file
var file = await folder.GetFileAsync(path);
// read content
var read = await Windows.Storage.FileIO.ReadTextAsync(file);
using (StringReader sr = new StringReader(read))
{
Test test = new Test(getFloatArray(sr));
this.array = test.printArray();
}
}
private float[] getFloatArray(StringReader sr) { ... }
public class Test
{
public float[] floatArray;
public Test(float[] floatArray)
{
this.floatArray = floatArray;
}
public float[] printArray()
{
for (int i = 0; i < floatArray.Length; i++)
{
Debug.WriteLine(floatArray[i]);
}
return floatArray;
}
}
You're trying to get the result of an asynchronous operation before it has completed. I recommend you read my intro to async / await and follow-up with the async / await FAQ.
In particular, your constructor:
public ModelReader(String name)
{
ReadModelAsync(name);
}
is returning before ReadModelAsync is complete. Since constructors cannot be asynchronous, I recommend you use an asynchronous factory or asynchronous lazy initialization as described on my blog (also available in my AsyncEx library).
Here's a simple example using an asynchronous factory approach:
public class ModelReader
{
float[] array;
private ModelReader()
{
}
public static async Task<ModelReader> Create(string name)
{
var ret = new ModelReader();
await ret.ReadModelAsync(name);
return ret;
}
...
}
I am mocking a wrapper to an MSMQ. The wrapper simply allows an object instance to be created that directly calls static methods of the MessageQueue class.
I want to test reading the queue to exhaustion. To do this I would like the mocked wrapper to return some good results and throw an exception on the fourth call to the same method. The method accepts no parameters and returns a standard message object.
Can I set up this series of expectations on the method in Moq?
Yup, this is possible if you don't mind jumping through a few minor hoops. I've done this for one of my projects before. Alright here is the basic technique. I just tested it out in Visual Studio 2008, and this works:
var mockMessage1 = new Mock<IMessage>();
var mockMessage2 = new Mock<IMessage>();
var mockMessage3 = new Mock<IMessage>();
var messageQueue = new Queue<IMessage>(new [] { mockMessage1.Object, mockMessage2.Object, mockMessage3.Object });
var mockMsmqWrapper = new Mock<IMsmqWrapper>();
mockMsmqWrapper.Setup(x => x.GetMessage()).Returns(() => messageQueue.Dequeue()).Callback(() =>
{
if (messageQueue.Count == 0)
mockMsmqWrapper.Setup(x => x.GetMessage()).Throws<MyCustomException>();
});
A few notes:
You don't have to return mocked messages, but it's useful if you want to verify expectations on each message as well to see if certain methods were called or properties were set.
The queue idea is not my own, just a tip I got from a blog post.
The reason why I am throwing an exception of MyCustomException is because the Queue class automatically throws a InvalidOperationException. I wanted to make sure that the mocked MsmqWrapper object throws an exception because of Moq and not because of the queue running out of items.
Here's the complete code that works. Keep in mind that this code is ugly in some places, but I just wanted to show you how this could be tested:
public interface IMsmqWrapper
{
IMessage GetMessage();
}
public class MsmqWrapper : IMsmqWrapper
{
public IMessage GetMessage()
{
throw new NotImplementedException();
}
}
public class Processor
{
private IMsmqWrapper _wrapper;
public int MessagesProcessed { get; set; }
public bool ExceptionThrown { get; set; }
public Processor(IMsmqWrapper msmqWrapper)
{
_wrapper = msmqWrapper;
}
public virtual void ProcessMessages()
{
_wrapper.GetMessage();
MessagesProcessed++;
_wrapper.GetMessage();
MessagesProcessed++;
_wrapper.GetMessage();
MessagesProcessed++;
try
{
_wrapper.GetMessage();
}
catch (MyCustomException)
{
ExceptionThrown = true;
}
}
}
[Test]
public void TestMessageQueueGetsExhausted()
{
var mockMessage1 = new Mock<IMessage>();
var mockMessage2 = new Mock<IMessage>();
var mockMessage3 = new Mock<IMessage>();
var messageQueue = new Queue<IMessage>(new [] { mockMessage1.Object, mockMessage2.Object, mockMessage3.Object });
var mockMsmqWrapper = new Mock<IMsmqWrapper>();
mockMsmqWrapper.Setup(x => x.GetMessage()).Returns(() => messageQueue.Dequeue()).Callback(() =>
{
if (messageQueue.Count == 0)
mockMsmqWrapper.Setup(x => x.GetMessage()).Throws<InvalidProgramException>();
});
var processor = new Processor(mockMsmqWrapper.Object);
processor.ProcessMessages();
Assert.That(processor.MessagesProcessed, Is.EqualTo(3));
Assert.That(processor.ExceptionThrown, Is.EqualTo(true));
}