Swift optional syntax - swift

I wrote some code that works, but uses a few too many forced unwraps. So I fixed it but there has to a better way...
var currentTask: Tasks?
var photoArray: [Photo]?
#IBOutlet var photoButton: [TaskImageButton]!
photoArray = Array(currentTask?.photos) as? [Photo]
if photoArray!.count > 0 {
for i in (photoArray?.indices)! {
photoButton[i].setImage(UIImage(data: photoArray![i].imageData!), for: .normal)
}
}
My attempted solution:
if let photoArraySize = photoArray?.count {
if photoArraySize > 0 {
for i in (photoArray?.indices)! {
if let photoData = photoArray?[i].imageData {
photoButton[i].setImage(UIImage(data: photoData), for: .normal)
}
}
}
}

A better way is to declare the photos array as non-optional
var photoArray = [Photo]()
...
photoArray = (Array(currentTask?.photos) as? [Photo]) ?? []
...
for (index, photoData) in photoArray.enumerated() where photoData.imageData != nil {
photoButton[index].setImage(UIImage(data: photoData.imageData!), for: .normal)
}

Unwrap photoArray once and the rest of the code is much simpler:
if let photoArray = photoArray {
for i in photoArray.indices {
photoButton[i].setImage(UIImage(data: photoArray[i].imageData), for: .normal)
}
}
If imageData is optional, then you need:
if let photoArray = photoArray {
for i in photoArray.indices {
if let imageData = photoArray[i].imageData {
photoButton[i].setImage(UIImage(data: imageData), for: .normal)
}
}
}
And the line:
photoArray = Array(currentTask?.photos) as? [Photo]
can probably be written as:
photoArray = currentTask?.photos
But without more details about the types involved, it's hard to be sure.

This might be cleaner if you get the array out of optional first
Also you dont have to check if count is greater than 0 because if the array is empty (count = 0) then for loop wont even run.
if let array = photoArray {
for i in array.indices {
photoButton[i].setImage(UIImage(data: array[i].imageData), for: normal)
}
}
I dont know about Photo class but if imageData is also optional then:
if let array = photoArray {
for i in array.indices {
if let photoData = array[i].imageData {
photoButton[i].setImage(UIImage(data: photoData), for: .normal)
}
}
}

Related

How to reduce if-condition looping - Swift

I know it sounds crazy, but just curious how I can reduce the if loop iteration for following? I have tried using guard let but stucked at some place.
{
if arenaEventItems == nil || arenaEventItems.count <= 0 {
return
}
if (arenaEventItems.count > 0 && (self.arenaEvents?.monthsDictObjList.count)! > 0){
if (self.tableView != nil){
if let arrVisibleRows = self.tableView.indexPathsForVisibleRows as? [IndexPath]{
if (self.tableView.indexPathsForVisibleRows!.count > 0){
let indexPath : IndexPath = self.tableView.indexPathsForVisibleRows!.first!
if let dict = self.arenaEvents?.monthsDictObjList[indexPath.row] {
if(self.arenaHeaderView != nil) && (dict.count) > 0 {
self.arenaHeaderView?.setMonthTitle(string: (dict.keys.first!))
let selectedMonthTitle = (dict.keys.first!)
for month in (self.arenaEvents?.uniqueMonthOnlyList)! {
if (selectedMonthTitle.contains(month)){
selectedMonthIndex = (self.arenaEvents?.uniqueMonthOnlyList.index(of: month)!)!
break
}
}
}
}
}
}
}
}
}
You can reduce it like that, without any forced unwrapping or nesting:
guard let arenaEventItems = arenaEventItems,
!arenaEventItems.isEmpty,
let arenaEvents = self.arenaEvents,
!arenaEvents.monthsDictObjList.isEmpty,
let arenaHeaderView = self.arenaHeaderView,
let indexPath = self.tableView?.indexPathsForVisibleRows?.first,
let selectedMonthTitle = arenaEvents.monthsDictObjList[indexPath.row].keys.first
else {
return
}
arenaHeaderView.setMonthTitle(string: selectedMonthTitle)
if let monthIndex = arenaEvents.uniqueMonthOnlyList.index(where: { selectedMonthTitle.contains($0) }) {
selectedMonthIndex = monthIndex
}
you replace if ... return with guard !... else return to avoid nesting
you replace .count > 0 with !...isEmpty as best practice
you replace multiple access to self.something? with if let something = self.something to avoid threading issues
you unloop for ... in ... { if (...) { ... } } to .index(where: ...)
You can combine all the conditions in "if" and get something like this:
if let eventItems = arenaEventItems,
eventItems.count > 0,
let events = self.arenaEvents,
!events.monthsDictObjList.isEmpty,
let tableView = self.tableView,
let arrVisibleRows = self.tableView.indexPathsForVisibleRows as? [IndexPath],
!arrVisibleRows.isEmpty,
let indexPath : IndexPath = arrVisibleRows.first,
let dict = events.monthsDictObjList[indexPath.row],
let headerView = self.arenaHeaderView,
!dict.isEmpty {
headerView.setMonthTitle(string: (dict.keys.first!))
let selectedMonthTitle = (dict.keys.first!)
for month in events.uniqueMonthOnlyList! {
if (selectedMonthTitle.contains(month)){
selectedMonthIndex = (events.uniqueMonthOnlyList.index(of: month)!)!
break
}
}
}
You should consider restructuring your code, your code is not readable and incomprehensible for anyone who look at it. Since, you are using Swift, it is really easy to write such code with guard ... else, if ... let
pattern.
Some improvements that you can do on class is have your view non nil ie make them implicitly unwrapped optional, since you will always be connecting them to storyboard.
#IBOutlet var tableView: UITableView!
#IBOutlet var arenaHeaderView: ArenaHeaderView!
Also, you have arrays which can go to nil, why do you want it to be nil. You could simply initialize an empty array and dictionaries. That way you can reduce some more comparison code like so,
arenaEventItems: [String: String] = [:]
With that changes and a bit of refactoring, you could possibly rewrite your code to something like this,
guard !arenaEventItems.isEmpty,
let arenaEvents = arenaEvents,
let indexPath = tableView.indexPathsForVisibleRows?.first,
let dict = arenaEvents.monthsDictObjList[indexPath.row],
let selectedMonthTitle = dict.keys.first
else {
return
}
arenaHeaderView.setMonthTitle(string: selectedMonthTitle)
for month in arenaEvents.uniqueMonthOnlyList where selectedMonthTitle.contains(month) {
if let selectedIndex = arenaEvents.uniqueMonthOnlyList.index(of: month) {
selectedMonthIndex = selectedIndex
break
}
}

How to show edit button on uitableviewcell, only on the posts you posted yourself

I'm making an app with a login feature, in which you can post, edit, and delete your own favorite spots, and like the posts of other users.
I'm trying to implement an edit button, that only shows on the posts you posted yourself, and hidden on the posts from others.
I have my FeedViewController, in which I call the 'configureCell function' in the UITableviewCell class. This is a part of the code in the FeedViewController:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCellWithIdentifier("PostCell") as? PostCell {
let postList = searchController.active ? searchResult[indexPath.row] : posts[indexPath.row]
let post = postList
cell.request?.cancel()
var image: UIImage?
if let url = post.postImgUrl {
image = FeedVC.imageCache.objectForKey(url) as? UIImage
}
var image2: UIImage?
if let url2 = post.userImgUrl {
image2 = FeedVC.imageCache.objectForKey(url2) as? UIImage
}
cell.configureCell(post, img: image, img2: image2)
return cell
} else {
return PostCell()
}
}
This is the code in my UITableviewCell (class PostCell: UITableViewCell):
func configureCell(post: Post, img: UIImage?, img2: UIImage?) {
self.post = post
likeRef = DataService.ds.REF_USER_CURRENT.childByAppendingPath("likes").childByAppendingPath(post.postKey)
self.descriptionText.text = post.postDescription
self.descriptionText.scrollRangeToVisible(NSMakeRange(0, 0))
self.likesLbl.text = "\(post.likes)"
self.postTitle.text = post.postTitle
self.postLocation.text = post.postLocation
self.username.text = post.username
self.postKeyLbl.text = post.key
if post.postImgUrl != nil {
if img != nil {
self.showcaseImg.image = img
} else {
request = Alamofire.request(.GET, post.postImgUrl!).validate(contentType: ["image/*"]).response(completionHandler: { request, response, data, err in
if err == nil {
let _img = UIImage(data: data!)!
self.showcaseImg.image = img
FeedVC.imageCache.setObject(_img, forKey: self.post.postImgUrl!)
} else {
print(err.debugDescription)
}
})
}
} else {
self.showcaseImg.hidden = true
}
if post.userImgUrl != nil {
if img2 != nil {
self.profileImg.image = img2
} else {
request = Alamofire.request(.GET, post.userImgUrl!).validate(contentType: ["image/*"]).response(completionHandler: { request, response, data, err in
if err == nil {
let _img2 = UIImage(data: data!)!
self.profileImg.image = img2
FeedVC.imageCache.setObject(_img2, forKey: self.post.userImgUrl!)
} else {
print(err.debugDescription)
}
})
}
} else {
print("no image")
}
likeRef.observeSingleEventOfType(.Value, withBlock: { snapshot in
if snapshot.value is NSNull {
self.likesImg.image = UIImage(named: "heart")
} else {
self.likesImg.image = UIImage(named: "heart-filled")
}
})
let getUid = NSUserDefaults.standardUserDefaults().valueForKey(KEY_UID)
if String(getUid!) == (self.post.postUid) {
editBtn.hidden = false
delBtn.hidden = false
}
}
It's about the last part:
let getUid = NSUserDefaults.standardUserDefaults().valueForKey(KEY_UID)
if String(getUid!) == (self.post.postUid) {
editBtn.hidden = false
delBtn.hidden = false
}
That part isn't working. The edit (and delete) button are showing in the posts of the specific user, but also in some of the posts of other users. I don't know what I'm doing wrong. Maybe it's because I have also implemented a 'sorting data function', where I am sorting the posts on 'date' or 'likes'. When the posts reshuffle, more edit buttons appear on random cells (from other users).
But I really don't know. I hope someone is able to help me!? Let me know if you need some more code:-) Thanks a lot!
Kind regards,
Dide
Add an else clause to this:
if String(getUid!) == (self.post.postUid) {
editBtn.hidden = false
delBtn.hidden = false
} else {
editBtn.hidden = true
delBtn.hidden = true
}
Because you are reusing cells, some of the cells where the buttons may not be hidden because it is associated with a post of a certain user may be reused for another post of a different user. Since you don't have an else clause handling this situation, those buttons whose 'hidden' property were originally set to false will remain unhidden, even if the postUid and getUid do not match. Hope this helps!

runtime lldb crash - optional / unwrapping issue with data model

I just finished converting this project to Swift 2 and had a ton of optionals to fix. So naturally there were bound to be some runtime issues. Console is giving the usual optional error message. I have a data model and other classes though so I'm having difficulty sourcing the problem/s. So far I think there is only this one issue though. Could you please suggest proper optional declaration / unwrapping. I know what they mean but since I'm fairly green I need to be shown what to fix even though I know the definition of optionals. I realize the images in the navigation window are optionals. They definitely exist so they should be unwrapped. So is there a problem with nickname or SpaceObject class?
SpaceObject.swift
class SpaceObject: NSObject {
var name: String?
var gravitationalForce: Float?
var diameter: Float?
var yearLength: Float?
var dayLength: Float?
var temperature: Float?
var numberOfMoons: Int?
var nickname: String?
var interestingFact: String?
var spaceImage: UIImage?
override init() {
}
init(initWithData data:NSDictionary, andImage image:UIImage) {
if (data[PLANET_NAME] != nil) { self.name = String(format: data[PLANET_NAME] as! NSString as String) }
if (data[PLANET_GRAVITY] != nil) { self.gravitationalForce = Float(data[PLANET_GRAVITY] as! NSNumber) }
if (data[PLANET_DIAMETER] != nil) { self.diameter = Float(data[PLANET_DIAMETER] as! NSNumber) }
if (data[PLANET_YEAR_LENGTH] != nil) { self.yearLength = Float(data[PLANET_YEAR_LENGTH] as! NSNumber) }
if (data[PLANET_DAY_LENGTH] != nil) { self.dayLength = Float(data[PLANET_DAY_LENGTH] as! NSNumber) }
if (data[PLANET_TEMPERATURE] != nil) { self.temperature = Float(data[PLANET_TEMPERATURE] as! NSNumber) }
if (data[PLANET_NUMBER_OF_MOONS] != nil) { self.numberOfMoons = Int(data[PLANET_NUMBER_OF_MOONS] as! NSNumber) }
if (data[PLANET_NICKNAME] != nil) { self.nickname = String(format: data[PLANET_NICKNAME] as! NSString as String) }
if (data[PLANET_INTERESTING_FACT] != nil) { self.interestingFact = String(format: data[PLANET_INTERESTING_FACT] as! NSString as String) }
self.spaceImage = image
}
}
SpaceDataViewController.swift
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("DataCell", forIndexPath: indexPath) as UITableViewCell
switch indexPath.row {
case 0:
cell.textLabel!.text = "Nickname:"
if let nickname = self.spaceObject?.nickname {
cell.detailTextLabel!.text = "\(nickname)"
}
case 1:
cell.textLabel!.text = "Diameter (km):"
if let diameter = self.spaceObject?.diameter {
cell.detailTextLabel!.text = "\(diameter)"
}
case 2:
cell.textLabel!.text = "Gravitational Force:"
if let gravitationalForce = self.spaceObject?.gravitationalForce {
cell.detailTextLabel!.text = "\(gravitationalForce)"
}
case 3:
cell.textLabel!.text = "Length of a Year (in days):"
if let yearLength = self.spaceObject?.yearLength {
cell.detailTextLabel!.text = "\(yearLength)"
}
case 4:
cell.textLabel!.text = "Length of a Day (in hours):"
if let dayLength = self.spaceObject?.dayLength {
cell.detailTextLabel!.text = "\(dayLength)"
}
case 5:
cell.textLabel!.text = "Temperature (in celsius):"
if let temperature = self.spaceObject?.temperature {
cell.detailTextLabel!.text = "\(temperature)"
}
case 6:
cell.textLabel!.text = "Number of Moons:"
if let numberOfMoons = self.spaceObject?.numberOfMoons {
cell.detailTextLabel!.text = "\(numberOfMoons)"
}
case 7:
cell.textLabel!.text = "Interesting Fact:"
if let interestingFact = self.spaceObject?.interestingFact {
cell.detailTextLabel!.text = "\(interestingFact)"
}
default: break
}
return cell
OuterSpaceTableViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
for planetData in AstronomicalData.allKnownPlanets() {
let imageName = "\(planetData[PLANET_NAME]).jpg"
let planet = SpaceObject(initWithData: planetData as! NSDictionary, andImage: UIImage(named: imageName)!) // Mercury.jpg is an optional
self.planets += [planet]
}
if let spaceList = self.userDefaults.arrayForKey(ADDED_SPACE_OBJECTS_KEY) {
for spaceObjectItem in spaceList {
self.addedSpaceObjects += [self.spaceObjectForDictionary(spaceObjectItem)]
}
}
}
Swift-Strings-Extension.swift
extension String {
// Convert string to floats.
func toFloat() -> Float? {
return (self as NSString).floatValue
}
}
Solution to the error that you have posted is to unwrap the planet name string you fetch from the dictionary. Since its an optional, its returning the value as "Optional(Mercury)". So when you don't unwrap, the image name gets formed as "Optional(Mercury).jpg" which will obviously not be available in your resources folder.
Example:
for planetData in AstronomicalData.allKnownPlanets {
if let planetName = planetData[PLANET_NAME] as! String {
let imageName = "\(planetName).jpg" //Now it will be Mercury.jpg
}
//...
}
If you find more errors after this, they should be dealt on case by case basis.

Displaying Artwork for .MP3 file

I am trying to currently display the album artwork for a locally stored .MP3 track in an ImageView. Does anyone know how to fetch this artwork in Swift in order to accomplish this?
I have found this solution (iOS AVFoundation: How do I fetch artwork from an mp3 file?) but the code is written in Objective C. I simply want to grab the image embedded in my MP3 and display it in my ImageView.
I've looked at the API documentation for the MPMediaItemArtwork and found an example that also accomplishes what I am trying to accomplish in Objective C as well here(http://www.codeitive.com/0zHjkUjUWX/not-able-to-get-the-uiimage-from-mpmediaitempropertyartwork.html) but cannot come up with a solution. My code is as follows:
import UIKit
import AVFoundation
import MediaPlayer
class ViewController: UIViewController {
let audioPath:NSURL! = NSBundle.mainBundle().URLForResource("SippinOnFire", withExtension: "mp3")
#IBOutlet var artistImage: UIImageView!
#IBOutlet var trackLabel: UILabel!
#IBOutlet var artistLabel: UILabel!
#IBOutlet var sliderValue: UISlider!
var player:AVAudioPlayer = AVAudioPlayer()
#IBAction func play(sender: AnyObject) {
let audioInfo = MPNowPlayingInfoCenter.defaultCenter()
println(audioInfo)
player.play()
//println("Playing \(audioPath)")
let playerItem = AVPlayerItem(URL: audioPath)
let metadataList = playerItem.asset.metadata as! [AVMetadataItem]
for item in metadataList {
if let stringValue = item.value {
println(item.commonKey)
if item.commonKey == "title" {
trackLabel.text = stringValue as? String
}
if item.commonKey == "artist" {
artistLabel.text = stringValue as? String
}
if item.commonKey == "artwork" {
if let audioImage = UIImage(data: item.value as! NSData) {
let audioArtwork = MPMediaItemArtwork(image: audioImage)
println(audioImage.description)
}
}
}
}
}
#IBAction func pause(sender: AnyObject) {
player.pause()
}
#IBAction func stop(sender: AnyObject) {
player.stop()
player.currentTime = 0;
}
#IBAction func sliderChanged(sender: AnyObject) {
player.volume = sliderValue.value
}
override func viewDidLoad() {
super.viewDidLoad()
var error:NSError? = nil
player = AVAudioPlayer(contentsOfURL: audioPath!, error: &error)
player.volume = 0.5;
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here is a screen shot of my sample .mp3 file. As you can see there is indeed album artwork that is both visible in the "get info" section of Finder. I've also opened the .mp3 in my iTunes to make sure and have confirmed there is artwork in the "get info" section of it there as well as under the "artwork" tab.
However, when trying to use the commonKey to assign the image to my imageView I find that there is no commonKey for "artwork".
Thanks
Change your snippet of code into this (I already tested it):
I added println lines commented in places of interest, Feel free to uncomment in order to see what is happening.
for item in metadataList {
if item.commonKey == nil{
continue
}
if let key = item.commonKey, let value = item.value {
//println(key)
//println(value)
if key == "title" {
trackLabel.text = value as? String
}
if key == "artist" {
artistLabel.text = value as? String
}
if key == "artwork" {
if let audioImage = UIImage(data: value as! NSData) {
//println(audioImage.description)
artistImage.image = audioImage
}
}
}
}
UPDATE: A bit of clean up of this code
for item in metadataList {
guard let key = item.commonKey, let value = item.value else{
continue
}
switch key {
case "title" : trackLabel.text = value as? String
case "artist": artistLabel.text = value as? String
case "artwork" where value is NSData : artistImage.image = UIImage(data: value as! NSData)
default:
continue
}
}
UPDATE: For Swift 4
for item in metadataList {
guard let key = item.commonKey?.rawValue, let value = item.value else{
continue
}
switch key {
case "title" : trackLabel.text = value as? String
case "artist": artistLabel.text = value as? String
case "artwork" where value is Data : artistImage.image = UIImage(data: value as! Data)
default:
continue
}
}
edit/update Swift 4 or later:
import MediaPlayer
var nowPlayingInfo: [String: Any] = [:]
let playerItem = AVPlayerItem(url: url)
let metadataList = playerItem.asset.metadata
for item in metadataList {
switch item.commonKey {
case .commonKeyTitle?:
nowPlayingInfo[MPMediaItemPropertyTitle] = item.stringValue ?? ""
case .commonKeyType?:
nowPlayingInfo[MPMediaItemPropertyGenre] = item.stringValue ?? ""
case .commonKeyAlbumName?:
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = item.stringValue ?? ""
case .commonKeyArtist?:
nowPlayingInfo[MPMediaItemPropertyArtist] = item.stringValue ?? ""
case .commonKeyArtwork?:
if let data = item.dataValue,
let image = UIImage(data: data) {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
}
case .none: break
default: break
}
}
let audioInfo = MPNowPlayingInfoCenter.default()
audioInfo.nowPlayingInfo = nowPlayingInfo
Note: You will have to invoke beginReceivingRemoteControlEvents() otherwise it will not work on the actual device. You will also need to set your app Background Modes (Audio and AirPlay) and set your AVAudioSession category to AVAudioSessionCategoryPlayback and set it active:
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowAirPlay])
print("Playback OK")
try AVAudioSession.sharedInstance().setActive(true)
print("Session is Active")
} catch {
print(error)
}
Try this:
It appears that sometimes iOS 8 returns nil at first attempt of obtaining this info:
if let audioCenter = MPNowPlayingInfoCenter.defaultCenter(){
if let audioInfo = audioCenter.nowPlayingInfo{
if let artwork = audioInfo[MPMediaItemPropertyArtwork] as? MPMediaItemArtwork
{
var image: UIImage? = artwork.imageWithSize(artwork.bounds.size)
if image == nil {
image = artwork.imageWithSize(artwork.bounds.size);
}
if image != nil{
println("image loaded")
}
}
}
}

swift ? must be followed by a call, member lookup, or subscript

I think I'm looking at some outdated code:
#IBAction func stockLevelDidChange(sender: AnyObject) {
if var currentCell = sender as? UIView {
while (true) {
currentCell = currentCell.superview!;
if let cell = currentCell as? ProductTableCell {
if let id = cell.productId? {
var newStockLevel:Int?;
if let stepper = sender as? UIStepper {
newStockLevel = Int(stepper.value);
}
else if let textfield = sender as? UITextField {
if let newValue = textfield.text.toInt()? {
newStockLevel = newValue;
}
}
if let level = newStockLevel {
products[id].4 = level;
cell.stockStepper.value = Double(level);
cell.stockField.text = String(level);
}
}
break;
}
}
displayStockTotal();
}
}
But in the first line of the function I get " '?' must be followed by a call, member lookup, or subscript" (for the question mark after as)
What does this error mean and how does this code change for Swift 1.2?
Actually the as? are all fine. The problem is this line:
if let id = cell.productId?
Just remove the question mark at the end of that. It makes no sense.
In 1.2, toInt is gone. So,
if let newValue = textfield.text.toInt()?
Should be replaced with:
if let newValue:Int? = Int(textField.text!)
The problem is the if let newValue = textfield.text.toInt()? { .. If toInt() returns an Int? then just get rid of the ? there.