This question already has answers here:
How do i return coordinates after forward geocoding?
(3 answers)
block until reverseGeocode has returned
(1 answer)
Closed 1 year ago.
I am new with swift. For my project I need to use google geocoding and to put the result in a text. For the user interface I am using swiftUI. I tried to do the same with Completition Handler but that didn't work. Below I have the code done with DispatchQueue and DispatchGroup but the whole application freezes when I try to use this func. Please help me with this. The code for UI is just a Text calling the func.
func reverseGeocoding(lat: Double, lng: Double) -> String{
var place:String?
let url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=\(lat),\(lng)&key=KEY"
let group = DispatchGroup()
group.enter()
DispatchQueue.global(qos: .default).async {
AF.request(url).responseJSON{ response in
// group.leave()
guard let data = response.data else {
return
}
do {
let jsonData = try JSON(data: data)
let result = jsonData["results"].arrayValue
for result in result {
let address_components = result["types"].arrayValue
for component in address_components {
if(component == "locality"){
place = result["formatted_address"].stringValue
}
}
}
} catch let error {
print(error.localizedDescription)
}
}
}
group.wait()
return place ?? ""
}
continuation for #vadian answer
As mentioned in the above publisher depends on context .This will give you a rough idea.This is from what I understood..
// Replace String with [String] if you want to add multiple locations at once based on it Publisher.send() accepts [String] instead of String
var LocationPublisher = PassthroughSubject<String,Never>()
class Subscriber :ObservableObject {
#Published var currentLocation :[String] = Array<String>()
private var cancellebels = Set< AnyCancellable>()
func createSubscriber(){
let subscriber = LocationPublisher.handleEvents(
receiveSubscription: {subscription in
print("New subscription \(subscription)")},
receiveOutput: {output in
print("New Output \(output)")
},
receiveCancel: {
print("Subscription Canceled")
})
.receive(on: RunLoop.main)
// if you replace String with [String],TypeOf(value) becomes [String]
.sink{value in
print("Subscriber recieved value \(value)")
self.currentLocation.append(value)
// use self.currenLocation.append(contentsOf:value) instead
}
.store(in: &cancellebels)
}
init() {
createSubscriber()
}
}
And inside this contentView
struct ContentView: View {
#ObservedObject var locationObject:Subscriber = Subscriber()
var body: some View {
VStack{
List{
locationObject.currentLocation.forEach{ location in
Text(location)
}
}
}
}
}
and from the above answer inside the success completion handler use
LocationPublisher.send(location)
instead of print statement
it will be notified to subscribers and locationObject.currentLocation will be updated
Its just one way to do it and most basic way.
You need a completion handler like this, it returns also all errors in the Result type
enum GeoError : Error {
case locationNoFound
}
func reverseGeocoding(lat: Double, lng: Double, completion: #escaping (Result<String,Error>) -> Void) {
let url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=\(lat),\(lng)&key=KEY"
DispatchQueue.global(qos: .default).async {
AF.request(url).responseData { response in
switch response.result {
case .success(let data):
do {
let jsonData = try JSON(data: data)
let result = jsonData["results"].arrayValue
for result in result {
let addressComponents = result["types"].arrayValue
for component in addressComponents {
if component == "locality" {
completion(.success(result["formatted_address"].stringValue))
}
}
}
completion(.failure(GeoError.locationNoFound))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
And use it
reverseGeocoding(lat: 45.0, lng: 45.0) { result in
switch result {
case .success(let location): print(location)
case .failure(let error): print(error)
}
}
Related
I'm trying to fetch data and update core data based on the new updated API-Data.
I have this download function:
func download1(stock: String, completion: #escaping (Result<[Quote], NetworkError>) -> Void) {
var internalQuotes = [Quote]()
let downloadQueue = DispatchQueue(label: "com.app.downloadQueue")
let downloadGroup = DispatchGroup()
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { (result) in
switch result {
case .failure(let err):
print(err)
downloadQueue.async {
downloadGroup.leave()
}
case .success(let resp):
downloadQueue.async {
internalQuotes.append(resp.quote)
downloadGroup.leave()
}
}
}
downloadGroup.notify(queue: DispatchQueue.global()) {
completion(.success(internalQuotes))
DispatchQueue.main.async {
self.quotes.append(contentsOf: internalQuotes)
}
}
}
On the ContentView I try to implement an update function:
func updateAPI() {
for stock in depot.aktienKatArray {
download.download1(stock: stock.aKat_symbol ?? "") { _ in
//
}
for allS in download.quotes {
if allS.symbol == stock.aKat_symbol {
stock.aKat_currPerShare = Double(allS.price) ?? 0
}
}
}
PersistenceController.shared.saveContext()
}
My problem is that the for loop in the update function should only go on if the first part (download.download1) is finished with downloading the data from the API.
Don't wait! Never wait!
DispatchGroup is a good choice – however nowadays I highly recommend Swift Concurrency – but it's at the wrong place.
.enter() must be called inside the loop before the asynchronous task starts
.leave() must be called exactly once inside the completion handler of the asynchronous task (ensured by a defer statement)
I know this code won't work most likely, but I merged the two functions to the correct DispatchGroup workflow. I removed the custom queue because the NetworkManager is supposed to do its work on a custom background queue
func updateAPI() {
var internalQuotes = [Quote]()
let downloadGroup = DispatchGroup()
for stock in depot.aktienKatArray {
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { result in
defer { downloadGroup.leave() }
switch result {
case .failure(let err):
print(err)
case .success(let resp):
internalQuotes.append(resp.quote)
for allS in download.quotes {
if allS.symbol == stock.aKat_symbol {
stock.aKat_currPerShare = Double(allS.price) ?? 0
}
}
}
}
}
downloadGroup.notify(queue: .main) {
self.quotes.append(contentsOf: internalQuotes)
PersistenceController.shared.saveContext()
}
}
Im trying to implement a stock API but get an error with ".append":
No exact matches in call to instance method 'append'.
I'm not familiar with it and need help to solve this problem
final class StockQuoteManager: QuoteManagerProtocol, ObservableObject {
#Published var quotes: [Quote] = []
func download(stocks: [String], completion: #escaping (Result<[Quote], NetworkError>) -> Void) {
var internalQuotes = [Quote]()
let downloadQueue = DispatchQueue(label: "com.app.dwonloadQueue")
let downloadGroup = DispatchGroup()
stocks.forEach { (stock) in
downloadGroup.enter()
let url = URL(string: API.quoteUrl(for: stock))!
NetworkManager<GlobalQuoteResponse>().fetch(from: url) { (result) in
switch result {
case .failure(let err):
print(err)
downloadQueue.async {
downloadGroup.leave()
}
case .success(let resp):
downloadQueue.async {
internalQuotes.append(resp.quote) // <-- ERROR
downloadGroup.leave()
}
}
}
}
downloadGroup.notify(queue: DispatchQueue.global()) {
completion(.success(internalQuotes))
DispatchQueue.main.async {
self.quotes.append(contentsOf: internalQuotes)
}
}
}
}
How to get an error response with driver so I can show it in alert. When I see the trait driver is can't error out, so should I use subject or behaviourRelay to get error response when I subscribe. Actually I like how to use driver but I don't know how to passed error response using driver.
this is my network service
func getMovies(page: Int) -> Observable<[MovieItem]> {
return Observable.create { observer -> Disposable in
self.service.request(endpoint: .discover(page: page)) { data, response, error in
if let _ = error {
observer.onError(MDBError.unableToComplete)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
observer.onError(MDBError.invalidResponse)
return
}
guard let data = data else {
observer.onError(MDBError.invalidData)
return
}
if let decode = self.decode(jsonData: MovieResults.self, from: data) {
observer.onNext(decode.results)
}
observer.onCompleted()
}
return Disposables.create()
}
}
This is my viewModel
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let movies = input.viewDidLoad
.flatMap { _ in
self.service.getMovies(page: 1)
.trackActivity(loading)
.asDriver(onErrorJustReturn: [])
}
let errorResponse = movies
return Output(loading: loading.asDriver(),movies: movies)
}
}
this is how I bind the viewModel in viewController
let input = PopularViewModel.Input(viewDidLoad: rx.viewDidLoad.asDriver())
let output = viewModel.transform(input: input)
output.movies.drive { [weak self] movies in
guard let self = self else { return }
self.populars = movies
self.updateData(on: movies)
}.disposed(by: disposeBag)
output.loading
.drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
.disposed(by: disposeBag)
You do this the same way you handled the ActivityIndicator...
The ErrorRouter type below can be found here.
This is such a common pattern that I have created an API class that takes care of this automatically.
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
let displayAlertMessage: Driver<String>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let errorRouter = ErrorRouter()
let movies = input.viewDidLoad
.flatMap { [service] in
service.getMovies(page: 1)
.trackActivity(loading)
.rerouteError(errorRouter)
.asDriver(onErrorRecover: { _ in fatalError() })
}
let displayAlertMessage = errorRouter.error
.map { $0.localizedDescription }
.asDriver(onErrorRecover: { _ in fatalError() })
return Output(
loading: loading.isActive.asDriver(onErrorRecover: { _ in fatalError() }),
movies: movies,
displayAlertMessage: displayAlertMessage
)
}
}
So, I have a button and will make an API request upon tapping it. When the API request returns an error, if my understanding is correct, the sequence will be terminated and no subsequent action will be recorded. How do I handle this properly so that I can still make another API request when tapping the button.
My thoughts are to have two observables that I can subscribe to in ViewController and on button pressed, one of it will print the success response and one of it will print the error. Just not quite sure how I can achieve that.
PS: In Post.swift, I have purposely set id as String type to fail the response. It should have be an Int type.
Post.swift
import Foundation
struct Post: Codable {
let id: String
let title: String
let body: String
let userId: Int
}
APIClient.swift
class APIClient {
static func request<T: Codable> (_ urlConvertible: URLRequestConvertible, decoder: JSONDecoder = JSONDecoder()) -> Observable<T> {
return Observable<T>.create { observer in
URLCache.shared.removeAllCachedResponses()
let request = AF.request(urlConvertible)
.responseDecodable (decoder: decoder) { (response: DataResponse<T>) in
switch response.result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
switch response.response?.statusCode {
default:
observer.onError(error)
}
}
}
return Disposables.create {
request.cancel()
}
}
}
}
PostService.swift
class PostService {
static func getPosts(userId: Int) -> Observable<[Post]> {
return APIClient.request(PostRouter.getPosts(userId: userId))
}
}
ViewModel.swift
class LoginLandingViewModel {
struct Input {
let username: AnyObserver<String>
let nextButtonDidTap: AnyObserver<Void>
}
struct Output {
let apiOutput: Observable<Post>
let invalidUsername: Observable<String>
}
// MARK: - Public properties
let input: Input
let output: Output
// Inputs
private let usernameSubject = BehaviorSubject(value: "")
private let nextButtonDidTapSubject = PublishSubject<Void>()
// MARK: - Init
init() {
let minUsernameLength = 4
let usernameEntered = nextButtonDidTapSubject
.withLatestFrom(usernameSubject.asObservable())
let apiOutput = usernameEntered
.filter { text in
text.count >= minUsernameLength
}
.flatMapLatest { _ -> Observable<Post> in
PostService.getPosts(userId: 1)
.map({ posts -> Post in
return posts[0]
})
}
let invalidUsername = usernameEntered
.filter { text in
text.count < minUsernameLength
}
.map { _ in "Please enter a valid username" }
input = Input(username: usernameSubject.asObserver(),
nextButtonDidTap: nextButtonDidTapSubject.asObserver())
output = Output(apiOutput: apiOutput,
invalidUsername: invalidUsername)
}
deinit {
print("\(self) dellocated")
}
}
ViewController
private func configureBinding() {
loginLandingView.usernameTextField.rx.text.orEmpty
.bind(to: viewModel.input.username)
.disposed(by: disposeBag)
loginLandingView.nextButton.rx.tap
.debounce(0.3, scheduler: MainScheduler.instance)
.bind(to: viewModel.input.nextButtonDidTap)
.disposed(by: disposeBag)
viewModel.output.apiOutput
.subscribe(onNext: { [unowned self] post in
print("Valid username - Navigate with post: \(post)")
})
.disposed(by: disposeBag)
viewModel.output.invalidUsername
.subscribe(onNext: { [unowned self] message in
self.showAlert(with: message)
})
.disposed(by: disposeBag)
}
You can do that by materializing the even sequence:
First step: Make use of .rx extension on URLSession.shared in your network call
func networkCall(...) -> Observable<[Post]> {
var request: URLRequest = URLRequest(url: ...)
request.httpMethod = "..."
request.httpBody = ...
URLSession.shared.rx.response(request)
.map { (response, data) -> [Post] in
guard let json = try? JSONSerialization.jsonObject(with: data, options: []),
let jsonDictionary = json as? [[String: Any]]
else { throw ... } // Throw some error here
// Decode this dictionary and initialize your array of posts here
...
return posts
}
}
Second step, materializing your observable sequence
viewModel.networkCall(...)
.materialize()
.subscribe(onNext: { event in
switch event {
case .error(let error):
// Do something with error
break
case .next(let posts):
// Do something with posts
break
default: break
}
})
.disposed(by: disposeBag)
This way, your observable sequence will never be terminated even when you throw an error inside your network call, because .error events get transformed into .next events but with a state of .error.
So I have also found the way to achieve what I wanted, which is assigning the success output and error output into two different observable respectively. By using RxSwiftExt, there are two additional operators, elements() and errors() which can be used on an observable that is materialized to get the element.
Here is how I did it,
ViewModel.swift
let apiOutput = usernameEntered
.filter { text in
text.count >= minUsernameLength
}
.flatMapLatest { _ in
PostService.getPosts(userId: 1)
.materialize()
}
.share()
let apiSuccess = apiOutput
.elements()
let apiError = apiOutput
.errors()
.map { "\($0)" }
Then, just subscribe to each of these observables in the ViewController.
As reference: http://adamborek.com/how-to-handle-errors-in-rxswift/
I successfully fetched and decoded data from an API and now have access to all the data I need to be used in the algorithm I want to write in my App.
The issue is that I don't know how to access this data after I decoded it, I can print it immediately after it's decoded but I have no idea how to use it in another function or place in my app.
Here is my Playground:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError : Error {
case FoundNil(String)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
print(marketData.data[0].open)
print(marketData.data[1].open)
print("Average=", (marketData.data[0].open + marketData.data[1].open) / 2)
//completion(marketData, nil)
throw MyError.FoundNil("data")
}
} catch {
print(error)
}
}
task.resume()
}
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
How can I use .data[0], .data[1], ..., somewhere else?
You data will be available in your fecthData() call. Probably what you want is your items variable, where you're printing it. But make sure to call the completion in your fetchData implementation.
WARNING: Untested code.
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError: Error {
case FoundNil(String)
case DecodingData(Data)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
completion(nil, MyError.FoundNil("data"))
}
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
completion(marketData, nil)
} else {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
} catch {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
}
task.resume()
}
fetchData() { items, error in
if let error == error {
switch(error) {
case .foundNil(let whatsNil):
print("Something is nil: \(whatsNil)")
case .decodingData(let data):
print("Error decoding: \(data)")
}
} else {
if let items = items {
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
print(items)
} else {
print("No items to show!")
}
}
}
I don't understand what is your real issue, because you have written everything you need here, but as far I understand , to pass data
just uncomment this line completion(marketData, nil)
and in
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
items is an object of your struct Response. You can pass this anywhere in your other class , by just creating an another variable like:
var items : Response!
for example :
class SomeOtherClass : NSObject{
var items : Response!
func printSomeData()
{
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
}
}
and in fetchData method write this:
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
let otherObject = SomeOtherClass()
otherObject.items = items
otherObject.printSomeData()
}