Escaping closure captures mutating 'self' parameter, Firebase - ios

I have the following code, How can i accomplish this without changing struct into class. Escaping closure captures mutating 'self' parameter,
struct RegisterView:View {
var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
self.names = name //here is the error
}){(error) in
print("Error: \(error)")
}
init(){
LoadPerson()
}a
var body:some View{
//ui code
}
}
Firebasemanager.swift
struct FirebaseManager {
func fetchPerson(
success: #escaping (Person) -> (),
failure: #escaping (String) -> ()
) {
Database.database().reference().child("Person")
.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
success(Person(dictionary: dictionary))
}
}) { (error) in
failure(error.localizedDescription)
}
}
}

SwiftUI view can be created (recreated) / copied many times during rendering cycle, so View.init is not appropriate place to load some external data. Use instead dedicated view model class and load explicitly only when needed.
Like
class RegisterViewModel: ObservableObject {
#Published var names = [String]()
func loadPerson() {
// probably it also worth checking if person has already loaded
// guard names.isEmpty else { return }
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
struct RegisterView: View {
// in SwiftUI 1.0 it is better to inject view model from outside
// to avoid possible recreation of vm just on parent view refresh
#ObservedObject var vm: RegisterViewModel
// #StateObject var vm = RegisterViewModel() // << only SwiftUI 2.0
var body:some View{
Some_Sub_View()
.onAppear {
self.vm.loadPerson()
}
}
}

Make the names property #State variable.
struct RegisterView: View {
#State var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success: { person in
guard let name = person.name else { return }
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
//...
}

Related

SwiftUI Navigation - List loading multiple time after navigating from details

I am creating a SwiftUI List with Details.
This list is fetching JSON data from Firebase Realtime. The data consist of 5 birds with an ID, a name and an image URL.
My problem is the following:
Each time I click on the back button after I navigate to details, the data get doubled every single time, what am I doing wrong? (see screenshots).
I am using MVVM design pattern, I am listening and removing that listener every time the View appears and disappears.
Please, find the code below:
Main View:
var body: some View {
NavigationStack {
List(viewModel.birds) { bird in
NavigationLink(destination: DetailsView(bird: bird)) {
HStack {
VStack(alignment: .leading) {
Text(bird.name).font(.title3).bold()
}
Spacer()
AsyncImage(url: URL(string: bird.imageURL)) { phase in
switch phase {
// downloading image here
}
}
}
}
}.onAppear {
viewModel.listentoRealtimeDatabase()
}
.onDisappear {
viewModel.stopListening()
}.navigationTitle("Birds")
}
}
DetailsView:
struct DetailsView: View {
var bird: Bird
var body: some View {
Text("\(bird.name)")
}
}
Model:
struct Bird: Identifiable, Codable {
var id: String
var name: String
var imageURL: String
}
View Model:
final class BirdViewModel: ObservableObject {
#Published var birds: [Bird] = []
private lazy var databasePath: DatabaseReference? = {
let ref = Database.database().reference().child("birds")
return ref
}()
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
func listentoRealtimeDatabase() {
guard let databasePath = databasePath else {
return
}
databasePath
.observe(.childAdded) { [weak self] snapshot in
guard
let self = self,
var json = snapshot.value as? [String: Any]
else {
return
}
json["id"] = snapshot.key
do {
let birdData = try JSONSerialization.data(withJSONObject: json)
let bird = try self.decoder.decode(Bird.self, from: birdData)
self.birds.append(bird)
} catch {
print("an error occurred", error)
}
}
}
func stopListening() {
databasePath?.removeAllObservers()
}
}
screenshot how it should be

Network request using Combine doesn't execute

I have the following classes that perform a network call -
import SwiftUI
import Combine
struct CoinsView: View {
private let coinsViewModel = CoinViewModel()
var body: some View {
Text("CoinsView").onAppear {
self.coinsViewModel.fetchCoins()
}
}
}
class CoinViewModel: ObservableObject {
private let networkService = NetworkService()
#Published var data = String()
var cancellable : AnyCancellable?
func fetchCoins() {
cancellable = networkService.fetchCoins().sink(receiveCompletion: { _ in
print("inside receive completion")
}, receiveValue: { value in
print("received value - \(value)")
})
}
}
class NetworkService: ObservableObject {
private var urlComponents : URLComponents {
var components = URLComponents()
components.scheme = "https"
components.host = "jsonplaceholder.typicode.com"
components.path = "/users"
return components
}
var cancelablle : AnyCancellable?
func fetchCoins() -> AnyPublisher<Any, URLError> {
return URLSession.shared.dataTaskPublisher(for: urlComponents.url!)
.map{ $0.data }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
What I want to achieve currently is just to print the JSON result.
This doesn't seem to work, and from debugging it never seems to go inside the sink{} method, therefor not executing it.
What am I missing?
After further investigation with Asperi's help I took the code to a clean project and saw that I have initialized a struct that wraps NSPersistentContainer which causes for some reason my network requests not to work. Here is the code, hopefully someone can explain why it prevented my networking to execute -
import SwiftUI
#main
struct BasicApplication: App {
let persistenceController = BasicApplciationDatabase.instance
#Environment(\.scenePhase)
var scenePhase
var body: some Scene {
WindowGroup {
CoinsView()
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .background:
print("Scene is background")
persistenceController.save()
case .inactive:
print("Scene is inactive")
case .active:
print("Scene is active")
#unknown default:
print("Scene is unknown default")
}
}
}
}
import CoreData
struct BasicApplciationDatabase {
static let instance = BasicApplciationDatabase()
let container : NSPersistentContainer
init() {
container = NSPersistentContainer(name: "CoreDataDatabase")
container.loadPersistentStores { NSEntityDescription, error in
if let error = error {
fatalError("Error: \(error.localizedDescription)")
}
}
}
func save(completion : #escaping(Error?) -> () = {_ in} ){
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
completion(nil)
} catch {
completion(error)
}
}
}
func delete(_ object: NSManagedObject, completion : #escaping(Error?) -> () = {_ in} ) {
let context = container.viewContext
context.delete(object)
save(completion: completion)
}
}

No options in dynamically provided intent for widget

I'm trying to implement dynamically provided options for my iOS 15 widget. So far I successfully implemented static intent parameters, and now I wanted to extend it with dynamically provided ones.
The dynamic parameter in my .intentdefinition file is called HotspotList and has type String.
I have defined a struct where I have also saved a list of availableHotspots:
struct Hotspot: Hashable, Identifiable, Codable {
...
static var availableHotspots = UserDefaults.standard.object(forKey: "hotspots") as? [String] ?? []
}
I have checked that this array is successfully saved with print(Hotspot.availableHotspots) somewhere in my main View.
Now I want to use this array in my IntentHandler.swift file:
import Intents
class IntentHandler: INExtension, WidgetConfigurationIntentHandling {
override func handler(for intent: INIntent) -> Any {
return self
}
func provideHotspotListOptionsCollection(for intent: WidgetConfigurationIntent) async throws -> INObjectCollection<NSString> {
let hotspots: [NSString] = Hotspot.availableHotspots.map { element in
let nsstring = element as NSString
return nsstring
}
let collection = INObjectCollection(items: hotspots)
return collection
}
func defaultHotspotList(for intent: WidgetConfigurationIntent) -> String? {
return "thisIsJustATest"
}
}
I see that the intent is implemented correctly, because defaultHotspotList() returns the default parameter. But somehow provideHotspotListOptionsCollection() doesn't return the list of Strings. What am I doing wrong?
Note: I also tried the non-async option of the function:
func provideHotspotListOptionsCollection(for intent: WidgetConfigurationIntent, with completion: #escaping (INObjectCollection<NSString>?, Error?) -> Void) {
let hotspots: [NSString] = Hotspot.availableHotspots.map { element in
let nsstring = element as NSString
return nsstring
}
let collection = INObjectCollection(items: hotspots)
print(collection)
completion(collection, nil)
}
So with the hint of #loremipsum I was able to fix it by myself. For anyone interested, here's my solution:
I added the following lines in my MainView in the App target:
struct MainView: View {
#AppStorage("hotspots", store: UserDefaults(suiteName: "group.com.<<bundleID>>"))
var hotspotData: Data = Data()
...
save(hotspots: miners)
...
func save(hotspots: [String]) {
do {
hotspotData = try NSKeyedArchiver.archivedData(withRootObject: hotspots, requiringSecureCoding: false)
WidgetCenter.shared.reloadAllTimelines()
} catch let error {
print("error hotspots key data not saved \(error.localizedDescription)")
}
}
and then to retrieve the data in the IntentHandler.swift:
import Intents
import SwiftUI
class IntentHandler: INExtension, WidgetConfigurationIntentHandling {
#AppStorage("hotspots", store: UserDefaults(suiteName: "group.com.<<bundleID>>"))
var hotspotData: Data = Data()
func provideHotspotListOptionsCollection(for intent: WidgetConfigurationIntent) async throws -> INObjectCollection<NSString> {
var hotspots: [String] {
var hotspots: [String]?
do {
hotspots = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(hotspotData) as? [String]
} catch let error {
print("color error \(error.localizedDescription)")
}
return hotspots ?? []
}
let NSHotspots: [NSString] = hotspots.map { element in
let nsstring = element as NSString
return nsstring
}
let collection = INObjectCollection(items: NSHotspots)
return collection
}

removing from array is not calling set

I have a list
List {
ForEach (appState.foo.indices, id: \.self) { fooIndex in
Text(foo[fooIndex].name)
}
.onDelete(perform: self.deleteRow)
}
with a function that deletes a row from the foo array:
private func deleteRow(at indexSet: IndexSet) {
self.appState.foo.remove(atOffsets: indexSet)
}
and an observable object that acts as an environment object in the view with the list:
class AppState: ObservableObject {
var foo: [Bar] {
set {
if let encoded = try? JSONEncoder().encode(newValue) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "foo")
}
objectWillChange.send()
self.myFunc()
}
get {
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
return loadedTraining
}
return []
}
}
// ....
func myFunc() {
print("I'm not printing when you delete a row")
}
}
How can I get my myFunc() triggered when I delete a row?
Use stored property instead of a computed property. To fix your issue modify the foo property in AppState like this:
struct Bar: Encodable { }
class AppState: ObservableObject {
var foo: [Bar] {
didSet {
if let encoded = try? JSONEncoder().encode(foo) {
UserDefaults.standard.set(encoded, forKey: "foo")
}
objectWillChange.send()
myFunc()
}
}
init() {
if let data = UserDefaults.standard.data(forKey: "foo"),
let savedFoo = try? JSONDecoder().decode([Bar].self, from: data) {
foo = savedFoo
} else {
foo = []
}
}
func myFunc() {
print("I'm not printing when you delete a row")
}
}

How to implement simple MVC design pattern in Swift?

I am new to MVC design pattern. I created "DataModel" it will make an API call, create data, and return data to the ViewController using Delegation and "DataModelItem" that will hold all data. How to call a DataModel init function in "requestData" function. Here is my code:
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[DataModelItem])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
func requestData() {
}
private func setDataWithResponse(response:[AnyObject]){
var data = [DataModelItem]()
for item in response{
if let tableViewModel = DataModelItem(data: item as? [String : String]){
data.append(tableViewModel)
}
}
delegate?.didRecieveDataUpdata(data: data)
}
}
And for DataModelItem:
class DataModelItem{
var name:String?
var id:String?
init?(data:[String:String]?) {
if let data = data, let serviceName = data["name"] , let serviceId = data["id"] {
self.name = serviceName
self.id = serviceId
}
else{
return nil
}
}
}
Controller:
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
How to implement simple MVC design pattern in Swift?
As a generic answer, in iOS development you're already doing this implicitly! Dealing with storyboard(s) implies the view layer and controlling the logic of how they work and how they are connected to the model is done by creating view controller, that's the default flow.
For your case, let's clarify a point which is: according to the standard MVC, by default the responsible layer for calling an api should be -logically- the view controller. However for the purpose of modularity, reusability and avoiding to create massive view controllers we can follow the approach that you are imitate, that doesn't mean that its the model responsibility, we can consider it a secondary helper layer (MVC-N for instance), which means (based on your code) is DataModel is not a model, its a "networking" layer and DataModelItem is the actual model.
How to call a DataModel init function in "requestData" function
It seems to me that it doesn't make scene. What do you need instead is an instance from DataModel therefore you could call the desired method.
In the view controller:
let object = DataModel()
object.delegate = self // if you want to handle it in the view controller itself
object.requestData()
I am just sharing my answer here and I am using a codable. It will be useful for anyone:
Model:
import Foundation
struct DataModelItem: Codable{
struct Result : Codable {
let icon : String?
let name : String?
let rating : Float?
let userRatingsTotal : Int?
let vicinity : String?
enum CodingKeys: String, CodingKey {
case icon = "icon"
case name = "name"
case rating = "rating"
case userRatingsTotal = "user_ratings_total"
case vicinity = "vicinity"
}
}
let results : [Result]?
}
NetWork Layer :
import UIKit
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[String])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
var theatreNameArray = [String]()
var theatreVicinityArray = [String]()
var theatreiconArray = [String]()
func requestData() {
Service.sharedInstance.getClassList { (response, error) in
if error != nil {
self.delegate?.didFailUpdateWithError(error: error!)
} else if let response = response{
self.setDataWithResponse(response: response as [DataModelItem])
}
}
}
private func setDataWithResponse(response:[DataModelItem]){
for i in response[0].results!{
self.theatreNameArray.append(i.name!)
self.theatreVicinityArray.append(i.vicinity!)
self.theatreiconArray.append(i.icon!)
}
delegate?.didRecieveDataUpdata(data: theatreNameArray)
print("TheatreName------------------------->\(self.theatreNameArray)")
print("TheatreVicinity------------------------->\(self.theatreVicinityArray)")
print("Theatreicon------------------------->\(self.theatreiconArray)")
}
}
Controller :
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
APIManager :
class Service : NSObject{
static let sharedInstance = Service()
func getClassList(completion: (([DataModelItem]?, NSError?) -> Void)?) {
guard let gitUrl = URL(string: "") else { return }
URLSession.shared.dataTask(with: gitUrl) { (data, response
, error) in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(DataModelItem.self, from: data)
completion!([gitData],nil)
} catch let err {
print("Err", err)
completion!(nil,err as NSError)
}
}.resume()
}
}
I would recommend using a singleton instance for DataModel, since this would be a class you would be invoking from many points in your application.
You may refer its documentation at :
Managing Shared resources using singleton
With this you wont need to initialise this class instance every time you need to access data.

Resources