Crash while unwrapping an Optional Value - swift

I am getting an error. It is returning nil on a DispatchQueue call.
DispatchQueue.main.async {
self.headerImageView.image = image // <- CRASH
}
Turns out that this is where I am getting a crash.
headerImageView is just a variable which is not connected to the storyboard via IBOutlet. Here is the entire code:
import UIKit
import WebKit
let offset_HeaderStop:CGFloat = 40.0
let offset_B_LabelHeader:CGFloat = 95.0
let distance_W_LabelHeader:CGFloat = 35.0
class MovieDetailsViewController: UIViewController, UIScrollViewDelegate {
var movie: Movie?
var urlBuilder = MovieUrlBuilder()
var config = TMDBConfiguration()
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var headerView: UIView!
#IBOutlet weak var headerLabel: UILabel!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var genreLabel: UILabel!
#IBOutlet weak var ratingLabel: UILabel!
#IBOutlet weak var releaseLabel: UILabel!
#IBOutlet weak var descriptionText: UITextView!
#IBOutlet weak var trailerContainerView: UIView!
#IBOutlet weak var trailerLabel: UILabel!
#IBOutlet weak var videoContainer: WKWebView!
var headerImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
headerImageView = UIImageView(frame: headerView.bounds)
headerImageView.image = UIImage(named: "Poster")
headerImageView.contentMode = .scaleAspectFill
headerView.insertSubview(headerImageView, belowSubview: headerLabel)
headerView.clipsToBounds = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchPopularMovies()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
var headerTransform = CATransform3DIdentity
if offset < 0 {
let headerScaleFactor: CGFloat = -(offset) / headerView.bounds.height
let headerSizeVariation = ((headerView.bounds.height * (1.0 + headerScaleFactor)) - headerView.bounds.height)/2.0
headerTransform = CATransform3DTranslate(headerTransform, 0, headerSizeVariation, 0)
headerTransform = CATransform3DScale(headerTransform, 1.0 + headerScaleFactor, 1.0 + headerScaleFactor, 0)
headerView.layer.transform = headerTransform
} else {
headerTransform = CATransform3DTranslate(headerTransform, 0, max(-offset_HeaderStop, -offset), 0)
let labelTransform = CATransform3DMakeTranslation(0, max(-distance_W_LabelHeader, offset_B_LabelHeader - offset), 0)
headerLabel.layer.transform = labelTransform
}
headerView.layer.transform = headerTransform
}
override var preferredStatusBarStyle: UIStatusBarStyle{
return UIStatusBarStyle.lightContent
}
private func fetchPopularMovies() {
if let poster = movie?.backdropPath {
let baseURL = URL(string: config.baseImageURL)!
let url = baseURL.appendingPathComponent("w500").appendingPathComponent(poster)
let request = URLRequest(url: url)
let task = urlBuilder.session.dataTask(with: request) {(data, response, error) in
do {
if let image = UIImage(data: data!) {
DispatchQueue.main.async {
self.headerImageView.image = image // <- CRASH
}
}
}
}
task.resume()
}
}
}
Thank you for the support!

You are initializing the image view in viewDidAppear which will be called after viewWillAppear where the image view is populated. Despite the asynchronous task it's not guaranteed that the view is initialized before being populated.
I recommend to initialize the image view lazily, the benefit is that the property is non-optional and it's initialized once when it's accessed the first time.
lazy var headerImageView: UIImageView = {
let view = UIImageView(frame: self.headerView.bounds)
view.image = UIImage(named: "Poster")
view.contentMode = .scaleAspectFill
self.headerView.insertSubview(view, belowSubview: self.headerLabel)
view.clipsToBounds = true
return view
}()

Your async call is located in fetchPopularMovies, which is called at viewWillAppear. You initialise your image view in viewDidAppear, which is called after viewWillAppear.
Basically, headerImageView is nil.
"But I put the call in an async block! So viewDidAppear should have been called already when I set the image view's image, right?" you said.
Well, relying on async to say "I want this thing to run after a certain method returns" is not a good idea. It is not always guaranteed to do that.
To fix this, you need to initialise header image view before fetchPopularMovies is called. You can put it in viewDidLoad for example.

Related

Swift: Tracking User's score

I'm trying to create a Quiz app that consists of two buttons, false and true button. My question is, as I answer more questions in the app, I want the textLabel to keep track of my score. And then when I circle back to the first question again, it should reset. This is my code so far. I'm looking forward to your answers.
class ViewController: UIViewController {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var progressBar: UIProgressView!
#IBOutlet weak var trueButton: UIButton!
#IBOutlet weak var falseButton: UIButton!
#IBOutlet weak var scoreLabel: UILabel!
var quizBrain = QuizBrain()
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
progressBar.progress = 0.0
}
#IBAction func answerButtonPressed(_ sender: UIButton) {
let userAnswer = sender.currentTitle
let userGotItRight = quizBrain.checkAnswer(userAnswer!)
if userGotItRight {
sender.shortChangeTo(.green)
} else {
sender.shortChangeTo(.red)
}
quizBrain.nextQuestion()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.updateUI()
}
}
func updateUI() {
questionLabel.text = quizBrain.getQuestionText()
trueButton.backgroundColor = UIColor.clear
falseButton.backgroundColor = UIColor.clear
progressBar.progress = 33
scoreLabel.text = "Score: \(quizBrain.getScore)"
}
}
You don't have any code for it yet? You can create a struct like user
struct user {
var score: Int = 0
mutating func answerRight (){
self.score =+ 1 }
mutating func reset (){
self.score = 0 }
}
But I see this in your code "Score: (quizBrain.getScore)"
This says that quizBrain should know the score. But you never add a value to it So check your quizBrain and create a function or an object that can track the score and then call it here "if userGotItRight {}"

Sigbart error during prepare for segue/unwind segue

I have an app where it is a marketplace and when you click on a product, it opens a detail view controller passing the data to display on the DetailVC. Additionally, inside the DetailVC, if you click on a button to claim the product, it segues to another VC to finalize the transaction.
In the DetailVC, there is a back button which is an unwind segue back to the main marketplace VC. Inside the TransactionVC, there is a cancel button which takes you back to the DetailVC.
When I am clicking the backButton in the DetailVC to take me back to the main market VC but I am getting a SIGBART Error and this :
020-07-15 09:05:23.707490-0500 evolutionatx[707:141952] Could not cast value of type 'evolutionatx.MarketplaceViewController' (0x1032c7868) to 'evolutionatx.PopUpPurchaseViewController' (0x1032c7ba8).
Here is the code for the DetailVC
import UIKit
import iCarousel
import CoreData
class MarketDetailViewController: UIViewController, UIScrollViewDelegate, iCarouselDelegate, iCarouselDataSource {
var productImageArray = [UIImage]()
var productVideo = String()
var pointsToPurchase = String()
var productName = String()
var productDescription = String()
var companyLogo = UIImage()
var companyWebsite = String()
var additionalProductImage = [UIImage]()
var companyName = String()
var promoCODE = String()
var buyLink = String()
var slides:[Slide] = [];
//IB
#IBOutlet weak var productNameLabel: UILabel!
#IBOutlet weak var productPriceLabel: UILabel!
#IBOutlet weak var productDescLabel: UILabel!
#IBOutlet weak var claimButton: UIButton!
#IBOutlet weak var imgScrollView: UIScrollView!
#IBOutlet weak var websiteButton: UIButton!
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var logoDisplay: UIImageView!
#IBOutlet weak var carouselView: iCarousel!
#IBOutlet weak var otherProductslabe: UILabel!
var carouselImages = [UIImage]()
var evoCoin = Int()
override func awakeFromNib() {
super.awakeFromNib()
carouselImages = productImageArray
}
override func viewDidLoad() {
super.viewDidLoad()
valueSetter()
imgScrollView.delegate = self
slides = createSlides()
setupSlideScrollView(slides: slides)
pageControl.numberOfPages = slides.count
pageControl.currentPage = 0
view.bringSubviewToFront(pageControl)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex = round(scrollView.contentOffset.x/view.frame.width)
pageControl.currentPage = Int(pageIndex)
let maximumHorizontalOffset: CGFloat = scrollView.contentSize.width - scrollView.frame.width
let currentHorizontalOffset: CGFloat = scrollView.contentOffset.x
// vertical
let maximumVerticalOffset: CGFloat = scrollView.contentSize.height - scrollView.frame.height
let currentVerticalOffset: CGFloat = scrollView.contentOffset.y
let percentageHorizontalOffset: CGFloat = currentHorizontalOffset / maximumHorizontalOffset
let percentageVerticalOffset: CGFloat = currentVerticalOffset / maximumVerticalOffset
}
#IBAction func claimProduct(_ sender: Any) {
print("tap rec")
claimProductandPurchase()
}
func claimProductandPurchase(){
evoCOiner()
if(evoCoin >= Int(pointsToPurchase)!){
print("Transaction Successful")
performSegue(withIdentifier: "proceedQuestion", sender: self)
}
else{
showToast(controller: self, message: "Insufficient EvoCoins", seconds: 0.5)
}
}
func showToast(controller: UIViewController, message : String, seconds: Double) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alert.view.backgroundColor = UIColor.black
alert.view.alpha = 0.6
alert.view.layer.cornerRadius = 15
controller.present(alert, animated: true)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + seconds) {
alert.dismiss(animated: true)
}
}
func evoCoiner(){
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "EvoCoins")
request.returnsObjectsAsFaults = false
do{
let result = try context.fetch(request)
for data in result as! [NSManagedObject]
{
evoCoin = data.value(forKey: "evoCoins") as! Int
}
}catch{
print("Failed")
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let detailController = segue.destination as! PopUpPurchaseViewController
detailController.ppromo = promoCODE
detailController.link = buyLink
detailController.coinToPurchase = Int(pointsToPurchase)!
}
//This is the unwind used by the transaction back button
#IBAction func unwindToItem(segue: UIStoryboardSegue) {
}
}
Here is the code in the transaction VC
import UIKit
import AMTabView
import CoreData
class MarketplaceViewController: UIViewController, TabItem {
#IBOutlet weak var sView: UIView!
#IBOutlet weak var evoCoinLabe: UILabel!
//For the sake of simplicity I only kept the Unwind functions
//MARK: - UNWIND FUNCTIONS
#IBAction func unwindToMainMarketView(segue: UIStoryboardSegue) {
}
}
How can I fix this error?
Please advise if my question was not clear or properly phrased (if so, sorry I am pretty new to all of this)
As #matt already said in his comment and the error clearly states, you cannot cast a MarketplaceViewController to a PopUpPurchaseViewController.
Furthermore instead of doing a forced cast, always look for a safe one like below. Doing so will prevent crashes.
if let detailController = segue.destination as? PopUpPurchaseViewController {
...
}
else {
// log failed to cast
}

Animation only works on one button (ripple view)

Site used: https://material.io/develop/ios/components/ripple/
Here is my code:
import MaterialComponents.MaterialRipple
class ViewController: UIViewController {
// let rippleView = MDCRippleView()
let rippleTouchController = MDCRippleTouchController()
#IBOutlet weak var playBtn: UIButton!
#IBOutlet weak var levelsBtn: UIButton!
#IBOutlet weak var topicsBtn: UIButton!
#IBOutlet weak var settingsBtn: UIButton!
#IBOutlet weak var instaBtn: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
//This works for instabtn (lastone)
// rippleTouchController.rippleView.rippleColor = .lightGray
// rippleTouchController.addRipple(to: playBtn)
// rippleTouchController.addRipple(to: levelsBtn)
// rippleTouchController.addRipple(to: topicsBtn)
// rippleTouchController.addRipple(to: settingsBtn)
// rippleTouchController.addRipple(to: instaBtn)
}
override func viewDidAppear(_ animated: Bool) {
// Currently only works for playbtn
rippleTouchController.rippleView.rippleColor = .lightGray
rippleTouchController.addRipple(to: levelsBtn)
rippleTouchController.addRipple(to: topicsBtn)
rippleTouchController.addRipple(to: settingsBtn)
rippleTouchController.addRipple(to: instaBtn)
rippleTouchController.addRipple(to: playBtn)
}
}
I've tried to put the code in viewDidAppear but it doesn't make a difference. Any thoughts?
Foreach button you have to create a MDCRippleTouchController.
let rippleTouchController = MDCRippleTouchController()
let rippleTouchController2 = MDCRippleTouchController()
let rippleTouchController3 = MDCRippleTouchController()
let rippleTouchController4 = MDCRippleTouchController()
let rippleTouchController5 = MDCRippleTouchController()
override func viewDidAppear(_ animated: Bool) {
rippleTouchController.rippleView.rippleColor = .lightGray
rippleTouchController.addRipple(to: levelsBtn)
rippleTouchController2.addRipple(to: topicsBtn)
rippleTouchController3.addRipple(to: settingsBtn)
rippleTouchController4.addRipple(to: instaBtn)
rippleTouchController5.addRipple(to: playBtn)
}

UILabel Text Code

I am new to programming. Really new and am trying to get the "outputLabel" to fade in when I press the "quoteButtonTapped" button. I have tried to set the alpha to 0.0 and then with a delay to alpha 1.0 but
self.outputLabel.alpha does not work.
Wow! I am perhaps missing something really basic here but the "quoteButtonTapped" does select a random quotation from an array and it's reference label, but how can I get the label text to fade in?
I included code below:
class ViewController: UIViewController {
#IBOutlet weak var outputLabel: UILabel!
#IBOutlet weak var borderImage: UIImageView!
#IBOutlet weak var scrollViewWindow: UIScrollView!
#IBOutlet weak var referenceLabel: UILabel!
#IBAction func quoteButtonTapped(sender: AnyObject) {
scrollViewWindow.contentSize.height = 800
}
override func viewDidLoad() {
super.viewDidLoad()
}
func displayCurrentScripture() {
let range: UInt32 = UInt32(quotes.count)
let randomNumber = Int(arc4random_uniform(range))
let QuoteString = quotes.objectAtIndex(randomNumber)
let ReferenceString = references.objectAtIndex(randomNumber)
self.outputLabel.text = QuoteString as? String
self.referenceLabel.text = ReferenceString as? String
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
UILabel.referenceLabel..
}
displayCurrentScripture()
}
Try This :
override func viewDidLoad() {
super.viewDidLoad()
displayCurrentScripture();
}
func displayCurrentScripture() {
let range: UInt32 = UInt32(quotes.count)
let randomNumber = Int(arc4random_uniform(range))
let QuoteString = quotes.objectAtIndex(randomNumber)
let ReferenceString = references.objectAtIndex(randomNumber)
self.outputLabel.alpha = 0.0
self.referenceLabel.alpha = 0.0
self.outputLabel.text = QuoteString as? String
self.referenceLabel.text = ReferenceString as? String
UIView.animateWithDuration(0.2, animations: { () -> Void in
self.outputLabel.alpha = 1.0
self.referenceLabel.alpha = 1.0
});
}

Update labels from unwind segue

I'm trying to change the labels of a viewController after a certain action is taken from a modal view which triggers an unwind segue.
Once unwind segue happens the labels of the current view (the one which the modal was covering) should be changed.
My current attempt at doing this is resulting in a "unexpectedly found nil while unwrapping an Optional value" error. Here is the code:
class DataViewController: UIViewController {
var experiment: NSDictionary?
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var bodyLabel: UILabel!
#IBOutlet weak var tlRightLine: UIImageView!
#IBOutlet weak var tlLeftLine: UIImageView!
#IBOutlet weak var brRightLine: UIImageView!
#IBOutlet weak var brLeftLine: UIImageView!
#IBOutlet weak var bodyTest: UITextView!
#IBAction func removeExperimentSegue(unwindSegue:UIStoryboardSegue) {
removeExperiment = true
titleLabel.text = "Done"
bodyLabel.text = "Done"
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if let dict: NSDictionary = experiment {
if let title = dict.objectForKey("title") as? String {
self.titleLabel!.text = title
}
if let body = dict.objectForKey("body") as? String {
self.bodyTest!.text = body
}
} else {
self.titleLabel!.text = ""
self.bodyLabel!.text = ""
}
}
}
What am I doing wrong?
I did! I ended up using a global boolean variable. I set it true on the initial load of the menu and then had it flip to false during the unwind segue.
var removeExperiment = false
class DataViewController: UIViewController {
#IBAction func removeExperimentSegue(unwindSegue:UIStoryboardSegue) {
removeExperiment = true
}
if removeExperiment == true {
doneLabel.text = "You've completed this experiment. You won't see it again unless you hit 'Reset' from the Home menu."
}
}
Hope that helps!