textLabel.text in UIViewController comes up nil while assigning string from NSObject - ios

I'm an iOS and programming noob so I apologize for any bad phrasing or mistakes.
I'm parsing quotes from an API for my app which displays it on a textLabel each time a UIButton is clicked. In order to keep the string from going off the textLabel or be resized to an unreadable font, I'm trying to request a new quote if the string character count is too high by calling a function in my NSObject. I set up a NSObject to do the refetching but whenever I try to reassign the the string to the textLabel.text from the NSObject or try to send the string back to the ViewController the qouteLabel.text comes back nil
Here is my viewcontroller where I'm making the initial request for the quote
import UIKit
import Alamofire
class RSQuotesViewController: RSViewController {
var ronImageView: UIImageView!
var quoteLabel = UILabel!()
override func loadView() {
let frame = UIScreen.mainScreen().bounds
let view = UIView(frame: frame)
view.backgroundColor = UIColor.grayColor()
ronImageView = UIImageView(frame: CGRectMake(frame.width/2-160, frame.height-600, 320, 600))
let ron = "ron.png"
let ronImage = UIImage(named: ron)
ronImageView.image = ronImage
view.addSubview(ronImageView);
let labelWidth = ronImageView.frame.width/2
let quoteLabelX = labelWidth-40
quoteLabel = UILabel(frame: CGRect(x: quoteLabelX, y: ronImageView.frame.height/4+15, width: labelWidth, height: 160))
quoteLabel.textAlignment = .Center
quoteLabel.text = "Click to Start"
quoteLabel.shadowColor = UIColor.grayColor()
quoteLabel.adjustsFontSizeToFitWidth = true
quoteLabel.lineBreakMode = .ByWordWrapping // or NSLineBreakMode.ByWordWrapping
quoteLabel.numberOfLines = 0
view.addSubview(quoteLabel)
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
let frame = UIScreen.mainScreen().bounds
let getQuote = UIButton(frame: CGRect(x: 0, y: 0, width: frame.size.width+50, height: frame.size.height))
getQuote.backgroundColor = UIColor.clearColor()
getQuote.setTitle("", forState: UIControlState.Normal)
getQuote.addTarget(self, action: #selector(RSQuotesViewController.getQuote(_:)), forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(getQuote)
}
// Gets quote when button is pressed
func getQuote(sender: UIButton){
let url = "http://ron-swanson-quotes.herokuapp.com/v2/quotes"
Alamofire.request(.GET, url, parameters: nil).responseJSON { response in
if let JSON = response.result.value as? Array<String>{
let quoteDict = RSQoute()
// if quote is too large get another one
if (JSON[0].characters.count > 120){
print("greater than 120")
quoteDict.fetchQuote()
} else {
self.quoteLabel.text = JSON[0]
}
}
}
}
This is my model where I'm trying to reassign the quoteLabel.text and getting nil
import UIKit
import Alamofire
class RSQoute: NSObject {
var newQuote = String()
// fetchs new quote if quote is too large
func fetchQuote(){
let url = "http://ron-swanson-quotes.herokuapp.com/v2/quotes"
Alamofire.request(.GET, url, parameters: nil).responseJSON { response in
if let JSON = response.result.value as? Array<String>{
self.newQuote = JSON[0]
if (self.newQuote.characters.count > 120) {
print("Try Again: ---->\(self.newQuote)")
return self.fetchQuote()
} else {
let quoteVC = RSQuotesViewController()
print("Retry was less than 120: ---->\(self.newQuote)")
print("quoteLabelText: ---->\(RSQuotesViewController().quoteLabel.text)")// comes back nil
RSQuotesViewController().quoteLabel.text = self.newQuote
}
}
}
}
}
Please let me know if there something I'm missing or an easier/better way of trying to fetch a new quote from the API :)

In your function fetchQuote(), you set quoteVC as a new instantiation of RSQuotesViewController() with let quoteVC = RSQuotesViewController(). Instead you should be setting the quoteLabel.text for the applications instance of RSQuotesViewController(). You are also making two API requests. Once inside the fetchQuote() function for RSQuotesViewController and once inside your fetchQuote() function for RSQuotes
I think what you are looking for would involve closures. Try this out for your fetchQuote() function in your RSQuotes class
func fetchQuote(completion: (result:String)){
let url = "http://ron-swanson-quotes.herokuapp.com/v2/quotes"
Alamofire.request(.GET, url, parameters: nil).responseJSON { response in
if let JSON = response.result.value as? Array<String>{
self.newQuote = JSON[0]
if (self.newQuote.characters.count > 120) {
print("Try Again: ---->\(self.newQuote)")
completion(result: self.newQuote)
} else {
print("Retry was less than 120: ---->\(self.newQuote)")
print("quoteLabelText: ---->\(RSQuotesViewController().quoteLabel.text)")// comes back nil
completion(result: self.newQuote)
}
}
Then, I would have a setQuote function RSQuotesViewController where you could just do something like this
func setQuote() {
let quoteObj = RSQuote()
quoteObj.fetchQuote() {
result in
quoteLabel.text = result
}
}
I would take a look at some posts related to swift closures and also check out. http://goshdarnclosuresyntax.com/
On a side note, I'm not sure if you were planning to manipulate the quoteString within your RSQuote class. If not, it might be better for fetchQuote() to be a static func. This way you can just call it without initializing the object in RSQuoteViewController. It'd be something like RSQuote.fetchQuote()

Related

How to add a parameter to UITapGestureRecognizer so that the action function can access that parameter

I have created UIViews programmatically based on the number of items i stored in my UserDefaults and each UIView represents an item from the userDefaults and have added UITapGestureRecognizer on top of it. Now this UIViews when clicked will send my user to a new view controller, now my problem is how do I pass a parameter which will hold a value so that the new view controller can determine which view was clicked. Below is my code
//Retrieving my userDefaults values
let items = preferences.object(forKey: selectedOffer)
//How i loop and create my UIViews
if let array = items as! NSArray?{
totalOffers = array.count
let castTotalOffers = CGFloat(totalOffers)
var topAnchorConstraint: CGFloat = 170
var cardHeight: CGFloat = 145
for obj in array {
if let dict = obj as? NSDictionary{
offerName = dict.value(forKey: "NAME") as! String
let offerPrice = dict.value(forKey: "PRICE") as! String
let offerDescription = dict.value(forKey: "DESCRIPTION") as! String
//creating the uiview
let offerView = UIView()
self.scrollView.addSubview(offerView)
offerView.translatesAutoresizingMaskIntoConstraints = false
offerView.topAnchor.constraint(equalTo: self.appBackImage.bottomAnchor, constant: topAnchorConstraint).isActive = true
offerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20.0).isActive = true
offerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20.0).isActive = true
offerView.backgroundColor = UIColor.white
offerView.heightAnchor.constraint(equalToConstant: 130).isActive = true
//transforming to cards
offerView.layer.cornerRadius = 2
offerView.layer.shadowOffset = CGSize(width: 0, height: 5)
offerView.layer.shadowColor = UIColor.black.cgColor
offerView.layer.shadowOpacity = 0.1
self.scrollView.contentSize.height = CGFloat(totalOffers) + topAnchorConstraint + 70
//Adding gesture
let touchRec = UITapGestureRecognizer(target: self, action: #selector(goToBuyBundle(offerClicked:offerName)))
offerView.addGestureRecognizer(touchRec)
}
}
}
//Function to go to buy offer
#objc func goToBuyBundle(_sender: UITapGestureRecognizer, offerClicked:String){
guard let moveTo = storyboard?.instantiateViewController(withIdentifier: "BuyOfferViewController") as? BuyOfferViewController else {return}
moveTo.selectedOffer = offerClicked
self.addChildViewController(moveTo)
moveTo.view.frame = self.view.frame
self.view.addSubview(moveTo.view)
moveTo.didMove(toParentViewController: self)
}
Just want a way when i navigate to the next view controller i can retrieve which UIView was clicked by using the offerName.
Thanks in Advance
Make your custom View and store the parameter that you want to pass through the Gesture Recognizer inside the view.
class GestureView: UIView{
var myViewValue: String? // Or whichever type of value you want to store and send via the gesture
}
When you initiate your view, add the value as per your requirement:
let panReceptor = GestureView()
panReceptor.myViewValue = "Hello World"
Add a simple TapGesture on this custom view and you may pass the value as below:
let tapGesture = UITapGestureRecognizer.init(target: self, action: #selector(viewTapped(sender:)))
panReceptor.addGestureRecognizer(tapGesture)
#objc func viewTapped(sender: UITapGestureRecognizer){
guard let unwrappedView = sender.view as? GestureView else { return }
print("Gesture View value : \(unwrappedView.myViewValue)")
}
In the above example I have in effect passed a String parameter through the sender.view.
You may pass any type in this manner and use the value as per your requirement in the selector method.
You could add custom variable to UITapGestureRecognizer something like:
import UIKit
private var assocKey : UInt8 = 0
extension UITapGestureRecognizer {
public var offerName:String{
get{
return objc_getAssociatedObject(self, &assocKey) as! String
}
set(newValue){
objc_setAssociatedObject(self, &assocKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
}
And then use it like:
...
let touchRec = UITapGestureRecognizer(target: self, action: #selector(goToBuyBundle(offerClicked:offerName)))
touchRec.offerName = offerName
offerView.addGestureRecognizer(touchRec)
...
#objc func goToBuyBundle(_sender: UITapGestureRecognizer, offerClicked:String){
guard let moveTo = storyboard?.instantiateViewController(withIdentifier: "BuyOfferViewController") as? BuyOfferViewController else {return}
moveTo.selectedOffer = sender.offerName
...
}

ui activity indicator is not showing up in viewcontroller, in which it has table view and web view?

var actInd: UIActivityIndicatorView = UIActivityIndicatorView()
actInd.frame = CGRect(x: 0.0, y: 0.0, width: 40.0, height: 40.0)
actInd.center = view.center
actInd.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
view.addSubview(actInd)
getinfo()
actInd.startAnimating()
func getinfo(){
//in this function theres is few image parsing codes, for table view
for i in 0 ..< minuscurent1.count
{
let adict:NSDictionary = minuscurent1[i] as! NSDictionary
let bdict:NSDictionary = adict.object(forKey: "snippet") as! NSDictionary
let cdict:NSDictionary = bdict.object(forKey: "resourceId") as! NSDictionary
let ddict:NSDictionary = bdict.object(forKey: "thumbnails") as! NSDictionary
let edict:NSDictionary = ddict.object(forKey: "medium") as! NSDictionary
self.videolist.add(bdict.object(forKey: "title") as! String)
self.videoId.add(cdict.object(forKey: "videoId") as! String)
self.image.add(edict.object(forKey: "url") as! String)
}
DispatchQueue.main.async {
self.actInd.stopAnimating()
self.actInd.hidesWhenStopped = true
self.videotable.reloadData()
}
i want the activity indicator to start from beginning and end it when all the images and web view is loaded.but the activity indicator is not showing up.i don't know what is wrong with the code.any help is appreciated...
Your image-loading is happening in the main-thread, thus blocking the UI from updating. You have to do the heavy work in a background thread and only update the activity-indicator once youre done.
DispatchQueue.global(qos: .background).async {
// Background Thread
// put your image loading here (for-loop)
DispatchQueue.main.async {
// Run UI Updates
self.actInd.stopAnimating()
self.actInd.hidesWhenStopped = true
self.videotable.reloadData()
}
}

iOS: Set setImageInputs of image slideshow to array of images

I'm using an image slideshow from here:
iconArr = [UIImage(named: "home-min")!,UIImage(named: "category-
min")!,UIImage(named: "settings-min")!,UIImage(named: "contact us-min")!,UIImage(named: "about us-min")!,UIImage(named: "logout")!]
I need to make this array as an image source.
for image in self.iconArr {
let img = image
self.SlideShow.setImageInputs([ImageSource(image: img)])
}
But that is not working, how can I do that?
you should try this way for sure, because you reset inputs in your for-loop
var imageSource: [ImageSource] = []
for image in self.iconArr {
let img = image
imageSource.append(ImageSource(image: img))
}
self.SlideShow.setImageInputs(imageSource)
As sooper stated, can be done this way
let imageSources = self.iconArr.map { ImageSource(image: $0) }
I found a solution from this url [https://stackoverflow.com/a/50461970/5628693][1]
Below is my code working fine :
var imageSDWebImageSrc = [SDWebImageSource]()
#IBOutlet weak var slideshow: ImageSlideshow!
Add below viewDidLoad()
slideshow.backgroundColor = UIColor.white
slideshow.slideshowInterval = 5.0
slideshow.pageControlPosition = PageControlPosition.underScrollView
slideshow.pageControl.currentPageIndicatorTintColor = UIColor.lightGray
slideshow.pageControl.pageIndicatorTintColor = UIColor.black
slideshow.contentScaleMode = UIViewContentMode.scaleAspectFill
// optional way to show activity indicator during image load (skipping the line will show no activity indicator)
slideshow.activityIndicator = DefaultActivityIndicator()
slideshow.currentPageChanged = {
page in
print("current page:", page)
}
let recognizer = UITapGestureRecognizer(target: self, action: #selector(Dashboard.didTap))
slideshow.addGestureRecognizer(recognizer)
} // now add below func
#objc func didTap() {
let fullScreenController = slideshow.presentFullScreenController(from: self)
// set the activity indicator for full screen controller (skipping the line will show no activity indicator)
fullScreenController.slideshow.activityIndicator = DefaultActivityIndicator(style: .white, color: nil)
}
And last step i was getting json data from below alamofire request
Alamofire.request(url, method: .post, parameters: data, encoding: JSONEncoding.default).responseJSON { response in
if(response.value == nil){
}
else {
let json2 = JSON(response.value!)
switch response.result {
case .success:
self.indicator.stopAnimating()
if let details = json2["imgs"].array {
for dItem in details {
let img = dItem["img"].stringValue
let image = SDWebImageSource(urlString: self.imgurl+img)
self.imageSDWebImageSrc.append(image!)
}
self.slideshow.setImageInputs(self.imageSDWebImageSrc)
}
break
case .failure( _):
break
}
}
}
Thanks dude :) happy coding

why does Bundle.main.path(forResource: fileName, ofType: "txt") always return nil?

I want to read a text file line by line and display it on an iOS screen using the example shown here.
Making textView.text optional was the only way I could get readDataFromFile to run. When I click load the function runs but always returns nil. I assume this means the file is not found.
For testing purposes I created the text file in Xcode. I also tried saving it on the desktop as well as in the project folder. Either way it was readable from the project navigator. I also tried creating the file using TextEdit because the app ultimately needs to read text files created outside Xcode.
I’d be grateful if someone can explain why the text file is never found, whether there is something else I need to do in order for the project to find it or if the read function returns nil for some other reason due to the way I have implemented it. Thanks.
EDIT (2)
Thanks for the feedback. In response, I’ve made four minor code changes that allow the text file contents to be written to textView. Changes include: removing the file extension from the filename, adding an array of file names, returning String instead of String? from readDataFromFile and rewriting UITextView in code. This has solved problems I am aware of.
Here's the revised code
import UIKit
class ViewController: UIViewController {
var textView = UITextView()
var arrayOfStrings: [String]?
var fileNameWithExtension = "textFile.txt"
let arrayOfFileNames = ["textFile1.txt", "textFile2.txt", "textFile3.txt", "textFile4.txt", "textFile5.txt"]
var fileName = String()
override func viewDidLoad() {
super.viewDidLoad()
// remove comment in the next statement to test files named in ArrayOfFileNames
// fileNameWithExtension = arrayOfFileNames[4]
fileName = fileNameWithExtension.replacingOccurrences(of: ".txt", with: "")
createTextView()
createButton()
}
func readDataFromFile(fileName: String) -> String {
if let path = Bundle.main.path(forResource: fileName, ofType: nil) {
print(fileName)
do {
let data = try String(contentsOfFile: path, encoding: String.Encoding.utf8)
arrayOfStrings = data.components(separatedBy: .newlines)
textView.text = arrayOfStrings?.joined(separator: "\n")
} catch {
textView.text = "file contents could not be loaded"
}
} else {
print(Bundle.main.path(forResource: fileName, ofType: "txt") as Any)
textView.text = "\(fileName) could not be found"
}
return textView.text
}
func createButton () {
let button = UIButton();
button.setTitle(String("Load"), for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.frame = CGRect(x: 100, y: 10, width: 200, height: 100)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
self.view.addSubview(button)
}
func buttonAction(myButton: UIButton) {
textView.text = readDataFromFile(fileName: fileName)
print(textView.text as Any)
}
func createTextView () {
textView = UITextView(frame: CGRect(x: 20.0, y: 75.0, width: 340.0, height: 400.0))
textView.textAlignment = NSTextAlignment.left
textView.textColor = UIColor.blue
textView.backgroundColor = UIColor.white
self.view.addSubview(textView)
}
}
EDIT (1)
The file is visible in the project navigator. I will assume that means it is in the bundle.
Here is my original code
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView?
var arrayOfStrings: [String]?
var fileName = "textFile.txt"
override func viewDidLoad() {
super.viewDidLoad()
createButton()
}
func readDataFromFile(fileName: String) -> String? {
if let path = Bundle.main.path(forResource: fileName, ofType: "txt") {
print(fileName)
do {
let data = try String(contentsOfFile: path, encoding: String.Encoding.utf8)
arrayOfStrings = data.components(separatedBy: .newlines)
print(arrayOfStrings as Any)
textView?.text = arrayOfStrings?.joined(separator: "/n")
return textView?.text
} catch {
textView?.text = "file contents could not be loaded"
return textView?.text
}
} else {
print(Bundle.main.path(forResource: fileName, ofType: "txt") as Any)
textView?.text = "\(fileName) could not be found"
return nil
}
}
func createButton () {
let button = UIButton();
button.setTitle(String("Load"), for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.frame = CGRect(x: 100, y: 15, width: 200, height: 100)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
self.view.addSubview(button)
}
func buttonAction(myButton: UIButton) {
print("works")
textView?.text = readDataFromFile(fileName: fileName)
print(textView?.text as Any)
}
textFile.txt
Line 1
Line 2
Line 3
Line 4
Line 5
1) You have mistake in this line:
var fileName = "textFile.txt"
should be:
var fileName = "textFile"
2) Check is your file connected to target:
You should consider adding class bundle owner like this:
Bundle(for: ViewController.self).path(forResource: "fileName", ofType: "txt")
This was implemented from swift 2.0 if I was right.

stopping an asynchronous call once it's out in the wild in swift

I have some problems with my version of this loadingOverlay singleton.
What's supposed to happen, is it comes onto the screen, with a view and a label that has the text, "Loading, please wait." or something like that. then if loading is longer than 2 seconds (i've changed it to 10 for debugging) the text changes to a random cute phrase.
first of all the animation that should change the text doesn't seem to happen. instead, the text just instantly changes.
more importantly, If, for some reason, my asynchronous call block is executed multiple times, I only want the most recent call to it to run, and I want the previous instances of it to terminate before running.
I was reading about callbacks and promises, which look promising. Is that a swifty pattern to follow?
by the way, as I'm learning swift and iOS, I've been experimenting, and I tried [unowned self] and now i'm experimenting with [weak self], but I'm not really certain which is most appropriate here.
// from http://stackoverflow.com/questions/33064908/adding-removing-a-view-overlay-in-swift/33064946#33064946
import UIKit
class LoadingOverlay{
static let sharedInstance = LoadingOverlay()
//above swifty singleton syntax from http://krakendev.io/blog/the-right-way-to-write-a-singleton
var overlayView = UIView()
var spring: CASpringAnimation!
var springAway: CASpringAnimation!
var hidden = false
private init() {} //This line prevents others from using the default () initializer for this class
func setupSpringAnimation(startY: CGFloat, finishY: CGFloat) {
overlayView.layer.position.y = startY
spring = CASpringAnimation(keyPath: "position.y")
spring.damping = 10
spring.fromValue = startY
spring.toValue = finishY
spring.duration = 1.0
spring.fillMode = kCAFillModeBackwards
}
func showOverlay() {
print("show overlay")
overlayView.alpha = 1
hidden = false
if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate,
let window = appDelegate.window {
setupSpringAnimation(-window.frame.height / 2, finishY: window.frame.height / 2)
let overlayViewFramesize = 0.65 * min(window.frame.height, window.frame.width)
overlayView.frame = CGRectMake(0, 0, overlayViewFramesize, overlayViewFramesize)
overlayView.center = window.center
overlayView.backgroundColor = UIColor.greenColor()
overlayView.clipsToBounds = true
overlayView.layer.cornerRadius = overlayViewFramesize / 8
let label = UILabel(frame: CGRectMake(0,0,overlayViewFramesize * 0.8 , overlayViewFramesize))
label.text = " \nLoading, please wait\n "
label.tag = 12
overlayView.addSubview(label)
label.lineBreakMode = NSLineBreakMode.ByWordWrapping
label.numberOfLines = 0 //as many as needed
label.sizeToFit()
label.textAlignment = NSTextAlignment.Center
label.center = CGPointMake(overlayViewFramesize / 2, overlayViewFramesize / 2)
overlayView.bringSubviewToFront(label)
window.addSubview(overlayView)
overlayView.layer.addAnimation(spring, forKey: nil)
RunAfterDelay(10.0) {
if self.hidden == true { return }
//strongSelf boilerplate code technique from https://www.raywenderlich.com/133102/swift-style-guide-april-2016-update?utm_source=raywenderlich.com+Weekly&utm_campaign=ea47726fdd-raywenderlich_com_Weekly4_26_2016&utm_medium=email&utm_term=0_83b6edc87f-ea47726fdd-415681129
UIView.animateWithDuration(2, delay: 0, options: [UIViewAnimationOptions.CurveEaseInOut, UIViewAnimationOptions.BeginFromCurrentState, UIViewAnimationOptions.TransitionCrossDissolve], animations: { [weak self] in
guard let strongSelf = self else { return }
(strongSelf.overlayView.viewWithTag(12) as! UILabel).text = randomPhrase()
(strongSelf.overlayView.viewWithTag(12) as! UILabel).sizeToFit()
print ((strongSelf.overlayView.viewWithTag(12) as! UILabel).bounds.width)
(strongSelf.overlayView.viewWithTag(12) as! UILabel).center = CGPointMake(overlayViewFramesize / 2, overlayViewFramesize / 2)
}, completion: { (finished: Bool)in
print ("animation to change label occured")})
}
}
}
func hideOverlayView() {
hidden = true
UIView.animateWithDuration(1.0, delay: 0.0, options: [UIViewAnimationOptions.BeginFromCurrentState], animations: { [unowned self] in
//I know this is clunky... what's the right way?
(self.overlayView.viewWithTag(12) as! UILabel).text = ""
self.overlayView.alpha = 0
}) { [unowned self] _ in
//I know this is clunky. what's the right way?
for view in self.overlayView.subviews {
view.removeFromSuperview()
}
self.overlayView.removeFromSuperview()
print("overlayView after removing:", self.overlayView.description)
}
//here i have to deinitialize stuff to prepare for the next use
}
deinit {
print("Loading Overlay deinit")
}
}
What I basically wanted, was to be able to delay a block of code, and possibly cancel it before it executes. I found the answer here:
GCD and Delayed Invoking

Resources