Can't get UIButton to call block of code in Swift 4 - ios

I am trying to call a large block of code to run once a button (GoButton) is clicked, I have tried doing it this way but it just crashes, I also sometimes get the error that my button is equal to nil. any advice? All of the code below works just fine without the button being there, I want it to run once pressed. Also I'm new to swift programming so sorry if this is really easy. Thanks
Code Below:
override func viewDidLoad() {
super.viewDidLoad()
func GoButton(_ sender: Any) {
guard let APIUrl = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=Crowland&appid=***APIKEY***&units=Metric") else { return }
URLSession.shared.dataTask(with: APIUrl) { data, response, error in
guard let data = data else { return }
let decoder = JSONDecoder()
do {
let weatherData = try decoder.decode(MyWeather.self, from: data)
if (self.MainLabel != nil)
{
if let gmain = weatherData.weather?.description {
print(gmain)
DispatchQueue.main.async {
self.MainLabel.text! = "Current Weather in: " + String (gmain)
}
}
}
if (self.LocationLabel != nil)
{
if let gmain = weatherData.name {
print(gmain)
DispatchQueue.main.async {
self.LocationLabel.text! = "Current Weather in: " + String (gmain)
}
}
}
if (self.HumidityLabel != nil)
{
if let ghumidity = weatherData.main?.humidity
{
print(ghumidity, "THIS IS HUMIDITY")
DispatchQueue.main.async {
self.HumidityLabel.text! = String (ghumidity)
}
}
}
if (self.WindLabel != nil)
{
if let gspeed = weatherData.wind?.speed {
print(gspeed, "THIS IS THE SPEED")
DispatchQueue.main.async {
self.WindLabel.text! = String(gspeed) + " mph"
}
}
}
if (self.TempLabel != nil)
{
if let ggtemp = weatherData.main?.temp {
print(ggtemp, "THIS IS THE TEMP")
DispatchQueue.main.async {
self.TempLabel.text! = String (ggtemp) + " c"
}
}
}
} catch {
print(error.localizedDescription)
}
}.resume()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}

It seems like your GoButton function is somehow inside your viewDidLoad method? This should definitely not be the case as nested functions are hidden from the outside world by default.
If you are using storyboard, the correct way to link a button click to an action is to do it through the storyboard and IBActions. There are very many video/text tutorials showing you how to do so. Here is a video tutorial that I personally liked because of its clarity given that you are new to Swift - https://www.youtube.com/watch?v=Opz3juSW43Q. (I neither own nor have any affiliation with the video content in this link, I am merely linking it here as I think it might be helpful).

Related

How to refactor duplicate Firestore document IDs in Swift?

I'm doing my very first IOS app using Cloud Firestore and have to make the same queries to my database repeatedly. I would like to get rid of the duplicate lines of code. This is examples of func where documents ID are duplicated. Also I using other queries as .delete(), .addSnapshotListener(), .setData(). Should I refactor all that queries somehow or leave them because they were used just for one time?
#objc func updateUI() {
inputTranslate.text = ""
inputTranslate.backgroundColor = UIColor.clear
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { [self] (document, error) in
if let document = document, document.exists {
let document = document
let label = document.data()?.keys.randomElement()!
self.someNewWord.text = label
// Fit the label into screen
self.someNewWord.adjustsFontSizeToFitWidth = true
self.checkButton.isHidden = false
self.inputTranslate.isHidden = false
self.deleteBtn.isHidden = false
} else {
self.checkButton.isHidden = true
self.inputTranslate.isHidden = true
self.deleteBtn.isHidden = true
self.someNewWord.adjustsFontSizeToFitWidth = true
self.someNewWord.text = "Add your first word to translate"
updateUI()
}
}
}
#IBAction func checkButton(_ sender: UIButton) {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, error) in
let document = document
let label = self.someNewWord.text!
let currentTranslate = document!.get(label) as? String
let translateField = self.inputTranslate.text!.lowercased().trimmingCharacters(in: .whitespaces)
if translateField == currentTranslate {
self.inputTranslate.backgroundColor = UIColor.green
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
updateUI()}
} else {
self.inputTranslate.backgroundColor = UIColor.red
self.inputTranslate.shakingAndRedBg()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
self.inputTranslate.text = ""
}
}
}
}
func deletCurrentWord () {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, err) in
let document = document
if let err = err {
print("Error getting documents: \(err)")
} else {
let array = document!.data()
let counter = array!.count
if counter == 1 {
// The whole document will deleted together with a last word in list.
let user = Auth.auth().currentUser?.email
self.db.collection(K.FStore.collectionName).document(user!).delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
self.updateUI()
}
}
} else {
// A current word will be deleted
let user = Auth.auth().currentUser?.email
let wordForDelete = self.someNewWord.text!
self.db.collection(K.FStore.collectionName).document(user!).updateData([
wordForDelete: FieldValue.delete()
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
self.updateUI()
}
}
}
}
}
}
Another query example
func loadMessages() {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.addSnapshotListener { (querySnapshot, error) in
self.messages = []
if let e = error {
print(e)
} else {
if let snapshotDocuments = querySnapshot?.data(){
for item in snapshotDocuments {
if let key = item.key as? String, let translate = item.value as? String {
let newMessage = Message(key: key, value: translate)
self.messages.append(newMessage)
}
}
DispatchQueue.main.async {
self.messages.sort(by: {$0.value > $1.value})
self.secondTableView.reloadData()
let indexPath = IndexPath(row: self.messages.count - 1, section: 0)
self.secondTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
}
}
}
enum Error {
case invalidUser
case noDocumentFound
}
func fetchDocument(onError: #escaping (Error) -> (), completion: #escaping (FIRQueryDocument) -> ()) {
guard let user = Auth.auth().currentUser?.email else {
onError(.invalidUser)
return
}
db.collection(K.FStore.collectionName).document(user).getDocument { (document, error) in
if let error = error {
onError(.noDocumentFound)
} else {
completion(document)
}
}
}
func updateUI() {
fetchDocument { [weak self] error in
self?.hideShowViews(shouldHide: true, newWordText: nil)
} completion: { [weak self] document in
guard document.exists else {
self?.hideShowViews(shouldHide: true, newWordText: nil)
return
}
self?.hideShowViews(shouldHide: false, newWordText: document.data()?.keys.randomElement())
}
}
private func hideShowViews(shouldHide: Bool, newWordText: String?) {
checkButton.isHidden = shouldHide
inputTranslate.isHidden = shouldHide
deleteBtn.isHidden = shouldHide
someNewWord.adjustsFontSizeToFitWidth = true
someNewWord.text = newWordText ?? "Add your first word to translate"
}
The updateUI method can easily be refactored using a simple guard statement and then taking out the common code into a separate function. I also used [weak self] so that no memory leaks or retain cycles occur.
Now, you can follow the similar approach for rest of the methods.
Use guard let instead of if let to avoid nesting.
Use [weak self] for async calls to avoid memory leaks.
Take out the common code into a separate method and use a Bool flag to hide/show views.
Update for step 3:
You can create methods similar to async APIs for getDocument() or delete() etc and on completion you can update UI or perform any action. You can also create a separate class and move the fetchDocument() and other similar methods in there and use them.

iOS - How do I wait for the code to finish before going to the next view controller?

I'm very new to coding and I'm stuck on what to do. I'm trying to get the user's geocoordinates from an address, use the coordinates to figure out some values then go to a different view controller where some code will be run to display the values that I figured out. The problem is it finds the users coordinates, Then goes to the next view controller where it doesn't have the calculated data needed to display it then tries to calculate the values needed from the first controller. How do I get this code to run in order?
My Code
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: AdressInput) { coordinate, error in
if error != nil {
// Error
return
} else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: user_lat, savedLong: user_long) // Lastly goes here
}
}
performSegue(withIdentifier: "TimeNavigation", sender: self) // Goes here second
}
The Function
func getCoordinate(addressString: String, completionHandler: #escaping (CLLocationCoordinate2D, NSError?) -> Void ) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?[0] {
let location = placemark.location!
completionHandler(location.coordinate, nil)
return
}
}
completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
}
}
getData Function
func getData(savedLat: String, savedLong: String) {
guard let url = URL(string: "http://127.0.0.1:5000/api/lat/\(savedLat)/long/\(savedLong)") else{return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
var dataAsString = String(data: data, encoding: .utf8)
let splits = dataAsString?.components(separatedBy: "|")
let Counter:Int = splits?.count ?? 0
for n in 0...(Counter-1){
let splits2 = splits?[n].components(separatedBy: ",")
for x in 0...9 {
dataArray[n][x] = String(splits2?[x] ?? "nil")
}
}
}.resume()
}
Write it inside the closure because your performSegue execute before the closure result ... so write it inside the closure but on main thread
Update you getData function
typealias CompletionHandler = (_ success:Bool) -> Void
func getData(savedLat:String,savedLong:String, completionBlock:#escaping CompletionHandler){
guard let url = URL(string: "http://127.0.0.1:5000/api/lat/\(savedLat)/long/\(savedLong)") else{return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completionBlock(false)
return
}
var dataAsString = String(data: data, encoding: .utf8)
let splits = dataAsString?.components(separatedBy: "|")
let Counter:Int = splits?.count ?? 0
for n in 0...(Counter-1){
let splits2 = splits?[n].components(separatedBy: ",")
for x in 0...9 {
dataArray[n][x] = String(splits2?[x] ?? "nil")
}
completionBlock(true)
}
}.resume()
}
And then your BSearch method
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: "AdressInput") { coordinate, error in
if error != nil {
// Error
return
}
else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: "user_lat", savedLong: "user_long", completionBlock: {[weak self] success in
DispatchQueue.main.async {
self?.performSegue(withIdentifier: "TimeNavigation", sender: self)
}
}) // Lastly goes here
}
}
}
You are calling your performSegue outside the scope of getCordinate function, which is why its getting called on click of the button and not waiting for the completion handler to finish.
Just move it inside and it will work fine.
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: AdressInput) { coordinate, error in
if error != nil {
// Error
return
}
else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: user_lat, savedLong: user_long) // Lastly goes here
DispatchQueue.main.async { //when performing UI related task, it should be on main thread
self.performSegue(withIdentifier: "TimeNavigation", sender: self)
}
}
}
}

UITextField to change API URL in Swift 5

I am new iOS Developer
I want to change the websiteLogo API with a textfield to change the URL.
how can I change the line with the ***
with a var and a textfield in my viewcontroller?
With screenshoot it's will be easier to understand what I want? Thank you !!! Guys. OneDriveLink. 1drv.ms/u/s!AsBvdkER6lq7klAqQMW9jOWQkzfl?e=fyqOeN
private init() {}
**private static var pictureUrl = URL(string: "https://logo.clearbit.com/:http://www.rds.ca")!**
private var task: URLSessionDataTask?
func getQuote(callback: #escaping (Bool, imageLogo?) -> Void) {
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: QuoteService.pictureUrl) { (data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil)
return
}
let quote = imageLogo(image: data)
callback(true, quote)
print(data)
}
}
task?.resume()
}
First, please don't use screenshots do show your code. If you want help, others typically copy/paste your code to check whats wrong with it.
There are some minor issues with your code. Some hints from me:
Start your types with a big letter, like ImageLogo not imageLogo:
Avoid statics
Avoid singletons (they are almost statics)
Hand in the pictureUrl into getQuote
struct ImageLogo {
var image:Data
}
class QuoteService {
private var task: URLSessionDataTask?
func getQuote(from pictureUrl:URL, callback: #escaping (Bool, ImageLogo?) -> Void) {
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: pictureUrl) {
(data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil)
return
}
let quote = ImageLogo(image: data)
callback(true, quote)
print(data)
}
}
task?.resume()
}
}
Store an instance of QuoteService in your view controller
Call getQuote on that instance, handing in the pictureUrl
class ViewController : UIViewController {
var quoteService:QuoteService!
override func viewDidLoad() {
self.quoteService = QuoteService()
}
func toggleActivityIndicator(shown:Bool) { /* ... */ }
func update(quote:ImageLogo) { /* ... */ }
func presentAlert() { /* ... */ }
func updateconcept() {
guard let url = URL(string:textField.text!) else {
print ("invalid url")
return
}
toggleActivityIndicator(shown:true)
quoteService.getQuote(from:url) {
(success, quote) in
self.toggleActivityIndicator(shown:false)
if success, let quote = quote {
self.update(quote:quote)
} else {
self.presentAlert()
}
}
}
/* ... */
}
Hope it helps.
I think you want to pass textfield Text(URL Enter By user) in Web Services
Add a parameter url_str in getQuote function definition first and pass textfield value on that parameters
fun getQuote(url_str : String, callback : #escaping(Bool, ImgaeLogo/)->void){
}

App crashes when clicked on back button on a nav controller while function is executing

On a slow connection when I click on back button on a VC it crashes while accessing navigation controller. VC is already deallocated but setNavBarTitle is executed after going back to another view. I understand that function is executing while VC is already deallocated but Im not sure what's the best way to handle such scenario?
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
fetchProfile(clientId: clientId) { (result, error) in
if result?.data != nil {
if (result?.success)! {
self.clientProfile = result!.data!
// Avatar
let clientImageView = UIImageView()
if let url = URL(string: result!.data!.pic_url!) {
clientImageView.image = UIImage(named: "System/defaultAvatar")
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
// WARNING: UIImageView.image must be used from main thread only
clientImageView.image = UIImage(data: data)
self.setNavBarTitle(image: clientImageView.image!)
}
task.resume()
}
}
}
}
}
}
private func setNavBarTitle(image: UIImage) {
// Crashes here -> Thread 11: Fatal error: Unexpectedly found nil while unwrapping an Optional value
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
}
You can try using [weak self] in your method callback, so even though the method is called after the VC deallocated it wont cause a crash, since self wont be existing:
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
fetchProfile(clientId: clientId) { [weak self] (result, error) in
if result?.data != nil {
if (result?.success)! {
self?.clientProfile = result!.data!
// Avatar
let clientImageView = UIImageView()
if let url = URL(string: result!.data!.pic_url!) {
clientImageView.image = UIImage(named: "System/defaultAvatar")
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async {
clientImageView.image = UIImage(data: data)
self?.setNavBarTitle(image: clientImageView.image!)
}
}
task.resume()
}
}
}
}
}
}
private func setNavBarTitle(image: UIImage) {
// Crashes here -> Thread 11: Fatal error: Unexpectedly found nil while unwrapping an Optional value
guard let navigationBarHeight: CGFloat = self.navigationController?.navigationBar.frame.height else {
return
}
}

My progress bar can't load after back to main view and go back to download view again

I have 2 page in storyboard by using navigation controller
I use alamofire for download file and I set progress bar in download page after I tap back to main pain and go to download view again, the progress don't load but in background it print percent normally
my custom cell code:
func downloadBook(bookID:String,downloadURLString:String) {
//set up UI
self.loadingLabel.text = "Preparing"
UserDefaults.standard.set(kStatusPreparing, forKey: "HP\(bookID).pdf")
self.isUserInteractionEnabled = false
if let url = URL(string: downloadURLString) {
self.delegate?.didTapDownloadBook(bookID: bookID, downloadURL: url)
}
}
#IBAction func selectBook(_ sender: Any) {
if UserDefaults.standard.integer(forKey: "HP\(bookID!).pdf") == kStatusSuccess {
self.delegate?.openWebView(bookId: bookID!)
}else if (UserDefaults.standard.integer(forKey: "HP\(bookID!).pdf") == kStatusNotDownload){
let urlString = "http://www.narutoroyal.com/HP\(bookID!).pdf"
downloadBook(bookID: bookID!, downloadURLString: urlString)
}
}
and in my main view download code here:
func updateCellProgress(bookID: String,progress:Double) {
let visibleCell = collectionBook.visibleCells as! [LoadingCollectionViewCell]
for cell in visibleCell {
if cell.bookID == bookID {
print("Download Progress: \(progress*100)")
cell.loadingLabel.text = String(format: "%.2f", progress*100)
}
}
}
func updateCellStatus(bookID: String) {
let visibleCell = collectionBook.visibleCells as! [LoadingCollectionViewCell]
for cell in visibleCell {
if cell.bookID == bookID {
cell.loadingLabel.text = "Open"
cell.isUserInteractionEnabled = true
}
}
}
extension ViewController : LoadingCollectionViewCellDelegate {
func didTapDownloadBook(bookID: String, downloadURL: URL) {
Alamofire.request(downloadURL).downloadProgress(closure: { (progress) in
self.updateCellProgress(bookID: bookID,progress: progress.fractionCompleted)
}).responseData { (response) in
if let data = response.result.value {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent("HP\(bookID).pdf")
do {
try data.write(to:fileURL, options: .atomic)
UserDefaults.standard.set(kStatusSuccess, forKey: "HP\(bookID).pdf")
self.updateCellStatus(bookID: bookID)
} catch {
print(error)
}
}
}
}
}
here my full code : https://github.com/dekphoenix/downloadprogress
ps. please help me I stuck in this many days T^T

Resources