I'm working on a map app and I have a geojson file with a few hundred thousand items in it. I can load and show the data, can see the pins on the map, the custom annotations and the clustering all works. I'm not happy with the way I load the file though.
I have two versions:
private var thingsOnMap: [MyDataClass] = []
private func loadInitialData() {
guard let fileName = Bundle.main.url(forResource: "myGeojsonFile", withExtension: "geojson"),
let myData = try? Data(contentsOf: fileName)
else {
return
} do {
let features = try MKGeoJSONDecoder()
.decode(myData)
.compactMap { $0 as? MKGeoJSONFeature }
let allData = features.compactMap(MyDataClass.init)
thingsOnMap.append(contentsOf: allData)
} catch {
print("Unexpected error: \(error).")
}
}
then I just call this method. This works, I can see the pins within a few seconds, but it feels like a really wrong way to do it this way.
The second version looks like this:
private func loadInitialData(completion: #escaping (_ data: [MyDataClass]?, _ error: Error?) -> ()) {
var receivedError: Error?
DispatchQueue.global(qos: .background).async {
guard let fileName = Bundle.main.url(forResource: "myGeojsonFile", withExtension: "geojson"),
let myData = try? Data(contentsOf: fileName)
else {
return
} do {
let features = try MKGeoJSONDecoder()
.decode(myData)
.compactMap { $0 as? MKGeoJSONFeature }
let allData = features.compactMap(MyDataClass.init)
self.thingsOnMap.append(contentsOf: allData)
} catch {
receivedError = error
}
DispatchQueue.main.async {
completion(self.thingsOnMap, receivedError)
}
}
}
Then I call it this way:
loadInitialData { (data, error) in
if data != nil {
self.mapView.addAnnotations(self.thingsOnMap)
}
}
This works nicely and it feels safer, but it takes roughly 35-40 seconds to see the data on the map. Why is that huge difference between the loading times? I see map apps all the time with lots of items (like car charging stations across Europe - hundreds of thousands items) loading in basically instantly. How can I achieve that? Or at least, is there a way to make it at least a bit faster?
Related
I understand how to "fetch" data from a JSON API (my local server, in fact), but how should I think about the pipeline from merely having the data to displaying it in views? What I intuitively want to do is "return" the data from the fetching function, though I know that's not the paradigm that the Swift URL functions operate with. My thought is that if I can "return" the data (as a struct) it will be easy to pass into a view for visualization.
Sample Code:
This is the structure of the fetched JSON and the kind of variable I want to pass into views.
struct User: Codable {
let userID: String
let userName: String
let firstName: String
let lastName: String
let fullName: String
}
My hope is that the printUser function can return instead of print a successful fetch.
func printUser() {
fetchUser { (res) in
switch res {
case .success(let user):
print(user.userName) // return here?
// I know it won't work, but that's what seems natural
case .failure(let err):
print("Failed to fetch user: ", err)
}
}
}
func fetchUser(completion: #escaping (Result<User, Error>) -> ()) {
let urlString = "http://localhost:4001/user"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, resp, err) in
if let err = err {
completion(.failure(err))
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data!)
completion(.success(user))
} catch let jsonError {
completion(.failure(jsonError))
}
}.resume()
}
Sample view that would take a user struct
struct DisplayUser: View {
var user: User
var body: some View {
Text(user.userID)
Text(user.userName)
Text(user.lastName)
Text(user.firstName)
Text(user.fullName)
}
}
The reason that you can't just "return" is that your fetchUser is asynchronous. That means that it might return relatively instantaneously or it may take a long time (or not finish at all). So, your program needs to be prepared to deal with that eventuality. Sure, it would be "be easy to pass into a view for visualization" as you put it, but unfortunately, it just doesn't fit the reality of the situation.
What you can do (in your example) is set the User as an Optional -- that way, if it hasn't been set, you can display some sort of loading view and if it has been set (ie your async function has returned a value), you can display it. That would look something like this:
class ViewModel : ObservableObject {
#Published var user : User? //could optionally store the entire Result here
func runFetch() {
fetchUser { (res) in
switch res {
case .success(let user):
DispatchQueue.main.async {
self.user = user
}
case .failure(let err):
print("Failed to fetch user: ", err)
//set an error message here? Another #Published variable?
}
}
}
func fetchUser(completion: #escaping (Result<User, Error>) -> ()) {
//...
}
}
struct DisplayUser: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
if let user = viewModel.user {
Text(user.userID)
Text(user.userName)
Text(user.lastName)
Text(user.firstName)
Text(user.fullName)
} else {
Text("Loading...")
}
}.onAppear {
viewModel.fetchUser()
}
}
}
Note: I'd probably refactor the async stuff to use Combine if this were my program, but it's a personal preference issue
I have a List that is updated with a Fetch class, an ObservableObject. It has an init function. This is that Fetch class.
#Published private(set) var items: [ItemsResult] = []
init() {
let url = URL(string: "[redacted]")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let itemsData = data {
let decodedData = try JSONDecoder().decode([ItemsResult].self, from: itemsData)
DispatchQueue.global(qos: .utility).async {
DispatchQueue.main.async {
print("running task")
self.items = decodedData
}
}
print(self.items)
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
When the app is build, it correctly displays the data returned by the API and it matches the database. However when I tap / click on one in order to delete it, or use the textarea I've added to add a new item it doesn't update.
struct TickrApp: View {
#EnvironmentObject var fetch: Fetch
var body: some View {
NavigationView {
Form {
Section {
VStack(alignment: .center) {
Text("Welcome to Tickr")
}
}
Section {
List(fetch.items) { item in
CheckView(checked: item.done, title: item.content.replacingOccurrences(of:"_", with: " "))
}
}
AddItemView()
}.navigationBarTitle(Text("Tickr"))
}
}
}
The database is being updated as shown when I log the decodedData they respond, however in each I just call Fetch(). Requests are made the same in all three cases.
One of the calls, for text input.
func toggle() {
checked = !checked
let url = URL(string: "")!
var req = URLRequest(url: url)
req.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: req) { data, response, error in
guard let _ = data,
let response = response as? HTTPURLResponse,
error == nil else {
print("error", error ?? "Unknown error")
return
}
guard (200 ... 299) ~= response.statusCode else {
print("statusCode should be 2xx, but is \(response.statusCode)")
print("response = \(response)")
return
}
}
task.resume()
Fetch()
In order to update the list visually I need to completely quit the app / rerun it in order to have the new and/or deleted items show correctly. No errors show about background publishing changes or anything.
It appears that you're trying to call Fetch() to refresh your data. There are two things that are going to be a problem with this:
You're calling it outside of the dataTask completion handler. That means that it may get called before the write finishes
Calling Fetch() just creates a new instance of Fetch, when what you really want to do is update the results on your existing instance.
I'm assuming that your first code snipped is from Fetch. I'd change it to look more like this:
class Fetch: ObservableObject {
#Published private(set) var items: [ItemsResult] = []
init() {
performFetch()
}
func performFetch() {
//your existing fetch code that was in `init`
}
}
Then, in your AddItemView and CheckView, make sure you have this line:
#EnvironmentObject var fetch: Fetch
This will ensure you're using the same instance of Fetch so your list will reflect the same collection of results.
Once you're done with an operation like toggle(), call self.fetch.performFetch() to update your results. So, your last code snippet would turn into something like this:
let task = URLSession.shared.dataTask(with: req) { data, response, error in
//guard statements to check for errors
self.fetch.performFetch() //perform your refresh on the *existing* `Fetch` instance
}
A bigger refactor would involve moving your async code (like toggle) to a view model, instead of doing any of it in a View code. Also, look into using the URLSession Publishers using Combine, since you're using SwiftUI: https://developer.apple.com/documentation/foundation/urlsession/processing_url_session_data_task_results_with_combine
My project contains the JSON file 'LocationsData.json'
An error is displayed 'Cannot find 'LocationsData' in scope'
(If needed - the following code is contained within my 'Data.swift' file, which is what the var 'locationsDataTypes' in the 'SearchRedirect.swift' is referencing)
import UIKit
import SwiftUI
import CoreLocation
let locationsDataTypes: [LocationsDataTypes] = load("LocationsData.json")
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
final class ImageStore {
typealias _ImageDictionary = [String: CGImage]
fileprivate var images: _ImageDictionary = [:]
fileprivate static var scale = 2
static var shared = ImageStore()
func image(name: String) -> Image {
let index = _guaranteeImage(name: name)
return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(name))
}
static func loadImage(name: String) -> CGImage {
guard
let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
else {
fatalError("Couldn't load image \(name).jpg from main bundle.")
}
return image
}
fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
if let index = images.index(forKey: name) { return index }
images[name] = ImageStore.loadImage(name: name)
return images.index(forKey: name)!
}
}
As pawello2222 hinted, locationsData isn't declared anywhere.
I suspect that you have your List view builder function written incorrectly.
Instead of this...
List(LocationsData, id: \.id) { locationsDataTypes in
Try the following...
List(locationsDataTypes, id: \.id) { locationsData in
...
Image(locationsData.imageName)
.resizable
...
}
Where locationsDataTypes is the collection, in your case the array of LocationsDataTypes and locationsData is the single instance of each iteration through that collection.
Also, when writing a question in stack overflow, best practice is to include the relevant code within a code block in your question, not accessed via a hyperlink. There are many reasons for this, here are a couple...
it is easier for the SO community to copy and paste your code into their solution as a part of their response, saving us all a lot of time;
links break, leaving your question without context for anyone who might have the same problem in the future.
I'm a beginner at Swift so let me know if this doesn't quite make sense, but i have a JSON file that i can access in swift and parse into an array, from there i can get a string from the array and store it in a var. I want to be able to access this variable globally but i'm not sure how to do it.
With the help of another user "rmaddy". I have this code:
struct Games: Decodable {
let videoLink: String
}
class BroadService {
static let sharedInstance = BroadService()
func fetchBroadcasts(completion: #escaping ([Games]?) -> ()) {
let jsonUrlString = "LINK IS HERE."
guard let url = URL(string: jsonUrlString) else {
completion(nil)
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completion(nil)
return
}
do {
let games = try JSONDecoder().decode([Games].self, from: data)
completion(games)
} catch let jsonErr {
print("Error deserializing json:", jsonErr)
completion(nil)
}
}.resume()
}
}
I can then access it in another class from here:
BroadService.sharedInstance.fetchBroadcasts { (games) in
if let games = games {
let game = games[indexPath]
let videoLink = game.videoLink
}
I want to be able to access the contents of "videoLink" globally, without having to use "BroadService.sharedInstance.fetchBroadcasts { (games) in" how would i go about doing this
You shouldn't use global variables, I don't think that's recommended in any language.
Now here you have what looks like a Singleton class (BroadService), that's good because it's a nice solution for what you're looking for.
Next all you need to do is add a property to that class. Let's say videoLink is a string, you can add a string property to BroadService, for example storedVideoLink as an optional String, and the next time you need to obtain that value after you have already fetched it, you can access it like so: BroadService.sharedInstance.storedVideoLink.
One more thing, to have BroadService work properly as a singleton, you should make its init private.
To sum up, here's what I'm suggesting:
class BroadService {
static let sharedInstance = BroadService()
var storedVideoLink: String?
private init() {} // to ensure only this class can init itself
func fetchBroadcasts(completion: #escaping ([Games]?) -> ()) {
// your code here
}
}
// somewhere else in your code:
BroadService.sharedInstance.fetchBroadcasts { (games) in
if let games = games {
let game = games[indexPath]
let videoLink = game.videoLink
BroadService.sharedInstance.storedVideoLink = videoLink
}
}
// now you can access it from anywhere as
// BroadService.sharedInstance.storedVideoLink
This way it all stays cohesive in the same class. You can even add a getter method for storedVideoLink so you don't have to access it directly, and in this method you could state that if the string is nil then you fetch the data, store the link to the string, and then return the string.
You could create a file with a struct called something like Global and create a static var and set that inside your completion block once you have fetched the games.
Here is an example.
struct Global {
static var games:[Any]? = nil
static func setGames(games:[Any]) {
Global.games = games
}
}
Then you fetch the data once upon load of the app or somewhere before you use the Global and set that property:
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completion(nil)
return
}
do {
let games = try JSONDecoder().decode([Games].self, from: data)
Global.setGames(games: games)
completion(games)
} catch let jsonErr {
print("Error deserializing json:", jsonErr)
completion(nil)
}
}.resume()
Please note that this will make the Global.games accessible from everywhere but it will also not be a constant so you should be careful not to override it.
This way Global.games will be accessible from anywhere.
I am making an application which makes a lot of requests from an API. So I don't want to copy and past the code over and over. I was wondering how I can reuse my code in a some more efficient way? Maybe with extensions?
This is my code know:
func apiRequest() {
let config = URLSessionConfiguration.default
let username = "****"
let password = "****"
let loginString = String(format: "%#:%#", username, password)
let userPasswordData = loginString.data(using: String.Encoding.utf8)
let base64EncodedCredential = userPasswordData?.base64EncodedString()
let authString = "Basic " + (base64EncodedCredential)!
print(authString)
config.httpAdditionalHeaders = ["Authorization" : authString]
let session = URLSession(configuration: config)
var running = false
let urlProjects = NSURL(string: "https://start.jamespro.nl/v4/api/json/projects/?limit=10")
let task = session.dataTask(with: urlProjects! as URL) {
( data, response, error) in
if let taskHeader = response as? HTTPURLResponse {
print(taskHeader.statusCode)
}
if error != nil {
print("There is an error!!!")
print(error)
} else {
if let content = data {
do {
let dictionary = try JSONSerialization.jsonObject(with: content) as! [String:Any]
print(dictionary)
if let items = dictionary["items"] as? [[String:Any]] {
for item in items {
if let description = item["Description"] as? String {
self.projectNaam.append(description)
}
if let id = item["Id"] as? String {
self.projectId.append(id)
}
if let companyId = item["CompanyId"] as? String {
self.companyId.append(companyId)
}
}
}
self.apiRequestCompani()
}
catch {
print("Error: Could not get any data")
}
}
}
running = false
}
running = true
task.resume()
while running {
print("waiting...")
sleep(1)
}
}
Yes, you can use Extensions to create a BaseViewController and extend that where you want to use your code over and over again. Then you should abstract all dynamic data over input parameters to that method.
import UIKit
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
func getApiRequest (Parameters) {
//API Request
}
And then in your view controller you just extend BaseViewController
class ViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Call method in baseviewcontroller
getApiRequest(parameters)
//Call method in self
self.getApiRequest(parameters)
}
override func getApiRequest(Parameters) {
//IF you need to override default configuration
}
So I don't want to copy and past the code over and over.
Absolutely right, no one aiming to get duplicated code; That's the issue of massive view controller. This issue appears since the view controller layer in your application handles most of the responsibilities, such as: getting data from the network, how data should be represented, deliver the formatted data to the view layer, etc...
There are many approaches for solving such an issue (using an appropriate architectural pattern for your application), for simplicity, I would recommend to apply the MVC-N (or MVCNetworking) approach into your app, it is almost the same usual MVC, with a separated files (managers), represent a new layer for handling -for instance- the integration with the external APIs.
Applying the MVN-N should not be that complex, nevertheless it needs to be described well (which might be too abroad to be descried in the answer), I would suggest to check the above mentioned apple example, also watching this video should be useful.