transfer data with protocol - ios

I try to decode weather api
this is my struct class weatherModal :
import Foundation
struct WeatherModel:Decodable{
var main:Main?
}
struct Main:Decodable {
var temp : Double?
var feels_like : Double?
var temp_min:Double?
var temp_max:Double?
var pressure , humidity: Int?
}
I am trying to learn protocols. So this is where a make api call manager class :
protocol WeatherManagerProtocol:AnyObject {
func weatherData(weatherData:WeatherModel)
}
class WeatherManager{
var weather : WeatherModel?
weak var delegate :WeatherManagerProtocol?
public func callWeather(city:String) {
let url = "http://api.openweathermap.org/data/2.5/weather?q=\(city)&appid=1234"
URLSession.shared.dataTask(with:URL(string: url)!) { (data, response, err) in
if err != nil {
print(err!.localizedDescription)
} else {
do {
self.weather = try JSONDecoder().decode(WeatherModel.self, from: data!)
self.delegate?.weatherData(weatherData: self.weather!)
} catch {
print(error.localizedDescription)
}
}
}.resume()
}
}
In my ViewController what I want to do is user write city name on textfield and If user clicked the process button print the information about weather.
class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
var weatherManager = WeatherManager()
var data : WeatherModel?
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.delegate = self
// Do any additional setup after loading the view.
}
#IBAction func processButtonClicked(_ sender: Any) {
if textField.text != "" {
weatherManager.callWeather(city: textField.text ?? "nil")
print(data?.main?.humidity) // it print nil
} else{
print("empty")
}
}
extension ViewController: WeatherManagerProtocol{
func weatherData(weatherData: WeatherModel) {
self.data = weatherData
print(self.data.main)
// in here I can show my data
}
}
When I clicked process button it always print nil. Why ? What am I doing wrong?

You seem not to understand how your own code is supposed to work. The whole idea of the protocol-and-delegate pattern you've set up is that the "signal" round-trips thru the weather manager on a path like this:
You (the ViewController) say weatherManager.callWeather
The weather manager does some networking.
The weather manager calls its own delegate's weatherData.
You (the ViewController) are that delegate, so your weatherData is called and that is where you can print.
So that is the signal path:
#IBAction func processButtonClicked(_ sender: Any) {
weatherManager.callWeather(city: textField.text ?? "nil") // < TO wm
}
func weatherData(weatherData: WeatherModel) { // < FROM wm
// can print `weatherData` here
}
You cannot short circuit this path by trying to print the weather data anywhere else. Stay on the path. You cannot turn this into a "linear" simple path; it is asynchronous.
If you do want it to look more like a "linear" simple path, use a completion handler instead of a delegate callback. That's what I do in my version of this same experiment, so my view controller code looks like this:
self.jsonTalker.fetchJSON(zip:self.currentZip) { result in
DispatchQueue.main.async {
// and now `result` contains the weather data, or an error
Even better, use the Combine framework (or wait until Swift 6 implements async/await).

You call to weatherManager.callWeather that start URLSession.shared.dataTask. the task is executed asynchronously, but callWeather return immediately. So, when you print data?.main?.humidity, the task did not finish yet, and data is still nil. After the task finish, you call weatherData and assign the response to data.

Related

How do refresh my UITableView after reading data from FirebaseFirestore with a SnapShotListener?

UPDATE at the bottom.
I have followed the UIKit section of this Apple iOS Dev Tutorial, up to and including the Saving New Reminders section. The tutorials provide full code for download at the beginning of each section.
But, I want to get FirebaseFirestore involved. I have some other Firestore projects that work, but I always thought that I was doing something not quite right, so I'm always looking for better examples to learn from.
This is how I found Peter Friese's 3-part YT series, "Build a To-Do list with Swift UI and Firebase". While I'm not using SwiftUI, I figured that the Firestore code should probably work with just a few changes, as he creates a Repository whose sole function is to interface between app and Firestore. No UI involved. So, following his example, I added a ReminderRepository.
It doesn't work, but I'm so close. The UITableView looks empty but I know that the records are being loaded.
Stepping through in the debugger, I see that the first time the numberOfRowsInSection is called, the data hasn't been loaded from the Firestore, so it returns 0. But, eventually the code does load the data. I can see each Reminder as it's being mapped and at the end, all documents are loaded into the reminderRepository.reminders property.
But I can't figure out how to get the loadData() to make the table reload later.
ReminderRepository.swift
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
init() {
loadData()
}
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
}
The only difference from the Apple code is that in the ListDataSource.swift file, I added:
var remindersRepository: ReminderRepository
override init() {
remindersRepository = ReminderRepository()
}
and all reminders references in that file have been changed to
remindersRepository.reminders.
Do I need to provide a callback for the init()? How? I'm still a little iffy on the matter.
UPDATE: Not a full credit solution, but getting closer.
I added two lines to ReminderListViewController.viewDidLoad() as well as the referenced function:
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshTournaments(_:)), for: .valueChanged)
#objc
private func refreshTournaments(_ sender: Any) {
tableView.reloadData()
refreshControl?.endRefreshing()
}
Now, when staring at the initial blank table, I pull down from the top and it refreshes. Now, how can I make it do that automatically?
Firstly create some ReminderRepositoryDelegate protocol, that will handle communication between you Controller part (in your case ReminderListDataSource ) and your model part (in your case ReminderRepository ). Then load data by delegating controller after reminder is set. here are some steps:
creating delegate protocol.
protocol ReminderRepositoryDelegate: AnyObject {
func reloadYourData()
}
Conform ReminderListDataSource to delegate protocol:
class ReminderListDataSource: UITableViewDataSource, ReminderRepositoryDelegate {
func reloadYourData() {
self.tableView.reloadData()
}
}
Add delegate weak variable to ReminderRepository that will weakly hold your controller.
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
weak var delegate: ReminderRepositoryDelegate?
init() {
loadData()
}
}
set ReminderListDataSource as a delegate when creating ReminderRepository
override init() {
remindersRepository = ReminderRepository()
remindersRepository.delegate = self
}
load data after reminder is set
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
delegate?.reloadYourData()
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
Please try changing var reminders = [Reminder]() to
var reminders : [Reminder] = []{
didSet {
self.tableview.reloadData()
}
}

How do I present an alert from a custom class in swiftui?

In SwiftUI, I have a network request running in scenedelegate, scenedidbecomeactive. I don't know which view the user will be on when the app becomes active, but I want to present an alert if the data in the network request changes. I simplified the code below, so it's easy to read...
func sceneDidBecomeActive(_ scene: UIScene) {
let customClass = CustomClass()
customClass.performNetworkRequest()
In CustomClass, i have...
func performNetWorkRequest() {
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let d = data {
let response = try JSONDecoder().decode(DetailResponse.self, from: d)
DispatchQueue.main.async {
//Here is where I want to either present an alert, but I can't figure out how to.
//OR do i put a func in SceneDeletegate to present the alert on the window.rootviewcontroller and then just call that func from here?
}
Any help is much appreciated!
Paul has a point - here's a possible implementation:
// In CustomClass.swift
import Combine
class CustomClass : ObservableObject {
#Published var dataRecieved = PassthroughSubject<DetailResponse, Never>()
init() {
performNetWorkRequest()
}
func performNetWorkRequest() {
URLSession.shared.dataTask(with: url) { (data, response, error) in
let response = try JSONDecoder().decode(DetailResponse.self, from: data)
DispatchQueue.main.async {
self.dataRecieved.send(response)
}
}
.resume()
}
}
// In SomeView.swift
import SwiftUI
import Combine
struct ContentView: View {
#State var showAlert = false
var customClass = CustomClass()
var body: some View {
Text("Hello, World!")
.onReceive(customClass.dataRecieved) { _ in
self.showAlert = true
}
.alert(isPresented: $showAlert) {
// your alert
}
}
}
Notice I didn't mention the SceneDelegate in any of these - this approach (called MVVM) is more flexible, in my opinion - besides, the way it is set up, performNetWorkRequest() will be executed as soon as your view is initialized, anyway.
You can also tweak the PassthroughSubject - I didn't know if you needed the DetailResponse or not.
Hope this helped!
Edit:
I just reread your question and it seems that this implementation is at fault as you noted there was no way to know what view the user would be on in the case of a network change. In that case, you can feed the same instance of CustomClass in your SceneDelegate as an EnvironmentObject.

Where is the best place to put a GET Request, and calling an init function in the the ViewController

So currently, I'm facing some issues in regards to writing a GET request in my Data Model and calling it in my ViewController. I have no issues with writing a GET request and calling it from the ViewController if there is no Init() function.
You simply initialize the ViewController like so and call it in viewDidLoad
Class ViewController {
var dataModel = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataModel.downloadInfo {
print(dataModel.info)
// Calls a completion handler, and won't print any info until the data is downloaded.
// retrieve the info from the data model.
}
}
But if in my DataModel I do something like
Class DataModel {
var info1: String!
var info2: String!
var info3: String!
init(info1: String, info2: String, info3: String) {
}
func downloadInfo(completed: #escaping downloadComplete) {
Alamofire.request(URL).responseJSON { response in
// GET request, parse data, and assign to variables
completed()
}
}
}
I can no longer initialize my ViewController without passing in these properties. But I can't call these properties until I download the data.
So I can longer do
Class ViewController {
var dataModel = DataModel()
}
but if I do
Class ViewController {
var dataModel: DataModel!
override func viewDidLoad() {
super.viewDidLoad()
dataModel.downloadInfo {
DataModel(dataModel.info1, dataModel.info2, dataModel.info3)(
// initialize after properties get downloaded.
}
I get unexpectedly returned nil because I didn't initialize in the beginning.
So I tried initializing with empty data because I can't get retrieve my real data until it gets downloaded.
Class ViewController {
var infoArray = [datModel]()
var dataModel = DataModel(info1: "", info2: "", info3: "")
override func viewDidLoad() {
super.viewDidLoad()
dataModel.downloadInfo {
let infoVar = DataModel(dataModel.info1, dataModel.info2, dataModel.info3)
self.infoArray.append(infoVar)
// append the datModel information into an array.
}
}
Technically this works, but am I doing it wrong, because this seems like a workaround, not a solution to very common task.
Lastly, the only other problem I receive is that I only get one object in the array, not the hundreds that there should be.
The easiest approach I would go for; is to make the properties optionals. It will alleviate you for having to do the work-around you have identified.
Class DataModel {
var info1: String?
var info2: String?
var info3: String?
}
And secondly, do make the download call async (with a callback as argument to the method) to allow the UI to load immediately. It will make the User experience a lot better!

Swift URLSession completion Handler

I am trying to make an API call in my Swift project. I just started implementing it and i am trying to return a Swift Dictionary from the call.
But I think i am doing something wrong with the completion handler!
I am not able to get the returning values out of my API call.
import UIKit
import WebKit
import SafariServices
import Foundation
var backendURLs = [String : String]()
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
#IBOutlet var containerView : UIView! = nil
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
func getBackendURLs(completion: #escaping (NSArray) -> ()) {
let backend = URL(string: "http://example.com")
var json: NSArray!
let task = URLSession.shared.dataTask(with: backend! as URL) { data, response, error in
guard let data = data, error == nil else { return }
do {
json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSArray
completion(json)
} catch {
#if DEBUG
print("Backend API call failed")
#endif
}
}
task.resume()
}
func extractJSON(JSON : NSArray) -> [String : String] {
var URLs = [String : String]()
for i in (0...JSON.count-1) {
if let item = JSON[i] as? [String: String] {
URLs[item["Name"]! ] = item["URL"]!
}
}
return URLs
}
}
The first print() statements gives me the correct value, but the second is "nil".
Does anyone have a suggestion on what i am doing wrong?
Technically #lubilis has answered but I couldn't fit this inside a comment so please bear with me.
Here's your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
What will happen is the following:
viewDidLoad is called, backendURLs is nil
you call getBackendURLs, which starts on another thread in the background somewhere.
immediately after that your code continues to the outer print(backendURLs), which prints nil as backendURLs is still nil because your callback has not been called yet as getBackendURLs is still working on another thread.
At some later point your getBackendURLs finishes retrieving data and parsing and executes this line completion(json)
now your callback is executed with the array and your inner print(backendURLs) is called...and backendURLs now has a value.
To solve your problem you need to refresh your data inside your callback method.
If it is a UITableView you could do a reloadData() call, or maybe write a method that handles updating the UI for you. The important part is that you update the UI inside your callback, because you don't have valid values until then.
Update
In your comments to this answer you say:
i need to access the variable backendURLs right after the completionHandler
To do that you could make a new method:
func performWhateverYouNeedToDoAfterCallbackHasCompleted() {
//Now you know that backendURLs has been updated and can work with them
print(backendURLs)
//do what you must
}
In the callback you then send to your self.getBackendURLs, you invoke that method, and if you want to be sure that it happens on the main thread you do as you have figured out already:
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
DispatchQueue.main.async {
self.performWhateverYouNeedToDoAfterCallbackHasCompleted()
}
}
Now your method is called after the callback has completed.
As your getBackendURLs is an asynchronous method you can not know when it has completed and therefore you cannot expect values you get from getBackedURLs to be ready straight after calling getBackendURLs, they are not ready until getBackendURLs has actually finished and is ready to call its callback method.
Hope that makes sense.

REST API, Swift, Automatic Update

I'm currently struggling to find an easy-to-use programming approach/design pattern, which solves the following problem:
I've got an REST API where the iOS app can request the required data. The data is needed in different ViewControllers. But the problem is, that the data should "always" be up to date. So I need to set up a timer which triggers a request every 5-20 seconds, or sth like that. Everytime the data changes, the view needs to be updated (at the current viewcontroller, which is displayed).
I tried some stuff with delegation and MVC Pattern, but it's kind a messy. How is it done the right way?
In my current implementation I only can update the whole UICollectionView, not some specific cells, because I don't know how the data changed. My controller keeps track of the data from the api and updates only if the hash has changed (if data changed on the server). My models always holds the last fetched data.
It's not the perfect solution, in my opinion..
I also thought about models, that keep themselves up to date, to abstract or virtualise my Rest-API. In this case, my controller doesn't even know, that it isn't directly accessible data.
Maybe someone can help me out with some kind of programming model, designpattern or anything else. I'm happy about anything!
UPDATE: current implementation
The Controller, which handles all the data
import Foundation
import SwiftyJSON
import SwiftyTimer
class OverviewController {
static let sharedInstance = OverviewController()
let interval = 5.seconds
var delegate : OverviewControllerUpdateable?
var model : OverviewModel?
var timer : NSTimer!
func startFetching() -> Void {
self.fetchData()
timer = NSTimer.new(every: interval) {
self.fetchData()
}
timer.start(modes: NSRunLoopCommonModes)
}
func stopFetching() -> Void {
timer.invalidate()
}
func getConnections() -> [Connection]? {
return model?.getConnections()
}
func getConnectionsSlave() -> [Connection]? {
return model?.getConnectionsSlave()
}
func getUser() -> User? {
return model?.getUser()
}
func countConnections() -> Int {
if let count = model?.getConnections().count {
return count
}
return 0
}
func countConnectionsSlave() -> Int {
if let count = model?.getConnectionsSlave().count {
return count
}
return 0
}
func fetchData() {
ApiCaller.doCall(OverviewRoute(), completionHandler: { (data, hash) in
if let actModel = self.model {
if (actModel.getHash() == hash) {
//no update required
return
}
}
var connections : [Connection] = []
var connectionsSlave : [Connection] = []
for (_,connection):(String, JSON) in data["connections"] {
let connectionObj = Connection(json: connection)
if (connectionObj.isMaster == true) {
connections.append(connectionObj)
} else {
connectionsSlave.append(connectionObj)
}
}
let user = User(json: data["user"])
//model needs update
let model = OverviewModel()
model.setUser(user)
model.setConnections(connections)
model.setConnectionsSlave(connectionsSlave)
model.setHash(hash)
self.model = model
//prevent unexpectedly found nil exception
if (self.delegate != nil) {
self.delegate!.reloadView()
}
}, errorHandler: { (errors) in
}) { (progress) in
}
}
}
protocol OverviewControllerUpdateable {
func reloadView()
}
The model, which holds the data:
class OverviewModel {
var user : User!
var connections : [Connection]!
var connectionsSlave : [Connection]!
var connectionRequests : [ConnectionRequest]!
var hash : String!
...
}
And in the ViewController, I use it like this:
class OverviewVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, OverviewControllerUpdateable {
let controller = OverviewController.sharedInstance
override func viewDidLoad() {
super.viewDidLoad()
self.controller.delegate = self
self.controller.startFetching()
}
//INSIDE THE UICOLLECTIONVIEW DELEGATE METHODS
...
if let user : User = controller.getUser() {
cell.intervalTime = interval
cell.nameLabel.text = "Ihr Profil"
}
...
func reloadView() {
self.userCollectionView.reloadData()
}
}
You could use a Singleton object to fetch your data periodically, then post notifications (using NSNotificationCenter) when the data is updated. Each view controller dependent on the data would listen for these notifications, then reload UI based on the updated data.

Resources