Why does randomElement() return ?? and not? - ios

I'm learning Swift and some iOS development. Here is my code which I'm trying to figure out why XCode is complaining.
private func rollMyDice() {
let diceRollImages = [
UIImage(named: "DiceOne"),
UIImage(named: "DiceTwo"),
UIImage(named: "DiceThree"),
UIImage(named: "DiceFour"),
UIImage(named: "DiceFive"),
UIImage(named: "DiceSix")
]
diceOneImageView.image = diceRollImages.randomElement()
diceTwoImageView.image = diceRollImages.randomElement()
}
So in this case, diceOneImageView.image = diceRollImages.randomElement() will complain that it cannot assign UIImage?? to UIImage?.
ImageView.image is of type UIImage?. What I don't understand is why is randomElement() here is returning UIImage??.
What does UIImage?? mean? I thought ?? was the nil coalescsing operator, so I'm not sure why it's part of some return type.
Also reading the documentation on randomElement(), it should return ?. So in this case, I would expect diceRollImages.randomElement() to return UIImage? which should suit diceOneImageView.image.
What is happening here? I know I can fix it by using ! or using nil coalescing etc. to make it work. Just don't get what's going on.

Note that UIImage(named:) is a failable initialiser, and so the expressions UIImage(named: "DiceOne") etc are of type UIImage?. This makes the array diceRollImages of type [UIImage?]. Each Element of the array is UIImage?, alternatively written as Optional<UIImage>.
As you may know, Optional is just an enum with two cases, .some and .none (aka nil). Each element of the array can be:
.some(image), if a UIImage is created successfully from the name
.none, if there is no images with that name
randomElement is declared to return Element? (Optional<Element>), because the array could have no elements, and hence, cannot give you a random element. randomElement returns:
.none, when the array is empty
.some(elementOfTheArray), when the array is non-empty and elementOfTheArray is a random element in array.
Recall that Element is Optional<UIImage> in the case of diceRollImages, and that the array elements (elementOfTheArray) could have values .some(image) or .none.
Therefore, we can say that randomElement returns a value of type Optional<Optional<UIImage>>, aka UIImage??, and it could be one of 3 things:
.none when the array is empty
.some(.some(image)) when the array is non-empty and a successfully-created image in the array is randomly selected
.some(.none) when the array is non-empty and a unsuccessfully-created image in the array is random selected
Since you are hardcoding the array of images, you know that the array is not empty, and so it is safe to force-unwrap the outer layer of the optional:
diceOneImageView.image = diceRollImages.randomElement()!

The answer has been given already in the comments: UIImage(named:) returns (optional) UIImage? and the result of calling randomElement() on an optional is a double optional ??
This is a good example where force unwrapping is welcome.
The images are part of the application bundle which is immutable at runtime and the app is useless if one of them is missing.
Declare diceRollImages
let diceRollImages = [
UIImage(named: "DiceOne")!,
UIImage(named: "DiceTwo")!,
UIImage(named: "DiceThree")!,
UIImage(named: "DiceFour")!,
UIImage(named: "DiceFive")!,
UIImage(named: "DiceSix")!
]
If the code crashes nevertheless it reveals a design mistake which can be fixed immediately.
You can even force unwrap randomElement()! because the array is a constant and is clearly not empty.

Related

Image Literals, Arrays and randomizing in Swift. Why does this method work but the other doesn't

Creating a dice roll app following Angela Yu's App Brewery bootcamp on Udemy. She uses image literals but those seem to have been deprecated on newer versions of Xcode.
I get the error: Cannot assign value of type 'UIImage??' to type 'UIImage?' Whenever I try to use diceArray.randomElement()
However, diceArray[Int.random(in:0...5] seems to work fine. My suspicion is maybe to use .randomElement() I need to use imageView but new to Swift so not sure if this is correct or what the difference even is. Any explanation would be appreciated. Thanks.
#IBAction func rollButtonPressed(_ sender: UIButton) {
var diceArray = [UIImage(named: "DiceOne"),UIImage(named: "DiceTwo"),UIImage(named: "DiceThree"),UIImage(named:"DiceFour"),UIImage(named:"DiceFive"),UIImage(named:"DiceSix")]
diceImageView1.image = diceArray.randomElement()
diceImageView2.image = diceArray[Int.random(in: 0...5)]
}
UIImage(named: initializer returns UIImage? and randomElement returns an Optional so it's ?? , you can force it
diceImageView1.image = diceArray.randomElement()!
As Sh_Khan pointed out, you have an array of Optionals since UIImage.named(_:) returns an Optional.
I would suggest adding a call to compactMap at the end of your code that declares your array of images:
var diceArray = [UIImage(named: "DiceOne")…].compactMap { $0 }
That will make diceArray an array of UIImages instead of Optional(UIImage) (and strip out any failed attempts to load images.)
Edit:
Alexander pointed out in the comments that this might be a case where force-unwrapping is a good thing.
If you write the code like this:
var diceArray = [
UIImage(named: "DiceOne")!,
UIImage(named: "DiceTwo")!,
UIImage(named: "DiceThree")!,
UIImage(named:"DiceFour")!,
UIImage(named:"DiceFive")!,
UIImage(named:"DiceSix")!
]
That version will also create an array of non-optional UIImages. If you mis-name any of the images, or there are other problems, the above code will crash immediately. Once it runs without crashing, though, you can be confident that the images will continue to load.
The version with compactMap() would simply create an array when any misnamed images were missing.

Swift - Array Filter not removing objects

I have an Array of Strings as shown:
I also have an array of objects that contain:
When I run the following line:
let filterNo = self.responseObjs.filter({!formItemIds.contains(String(describing: $0.formItemId))})
I expect filterNo to be empty as all the formItemIds are contained in the array. It is however not removing any of the items. Am I missing something basic?
Remove the describing from the init of String and use Nil-Coalescing Operator with $0.formItemId to unwrapped optional.
let filterNo = self.responseObjs.filter({!formItemIds.contains(String($0.formItemId ?? 0))})
You are not getting filtered data because your formItemId property is optional and using String(describing: $0.formItemId) give you output like Optional(98)

NSUserDefaults properly storing Strings in SpriteKit Swift

So I set up a NSUserDefault to store a string in my GameViewController
NSUserDefaults.standardUserDefaults().setObject("_1", forKey: "SkinSuffix")
The idea is it stores a suffix which I will attach to the end of an image name in order to save what skin of a character the player should use.
When I call the value in my GameScene class like so
var SkinSuffix = NSUserDefaults.standardUserDefaults().stringForKey("SkinSuffix")
println(SkinSuffix)
it prints "Optional("_1")" instead of just "_1" so when I try to change the name of my image file like so, it doesn't load the image file
hero = SKSpriteNode(texture: heroAtlas.textureNamed("10Xmini_wizard\(SkinSuffix)"))
How do I fix this issue?
You can unwrap the String using the Optional Binding construct. This avoids a crash of the app if the value is nil.
if let skinSuffix = NSUserDefaults.standardUserDefaults().stringForKey("SkinSuffix") {
println(skinSuffix)
}
Update: As correctly suggested in the comment below, I am putting the retrieved value in a constant (let). We should always use constants when we don't need to change the value. This way the Swift compiler can make some optimizations and does prevent us from changing that value.
That's because it's implicitly an optional not of type String. You need to case it as such or unwrap the optional in your println statement.
var SkinSuffix = NSUserDefaults.standardUserDefaults().stringForKey("SkinSuffix") as! String
Or in your println: println(SkinSuffix!)
As a side note, you should you camelCase for your variable names.
You can use "??" Nil Coalescing Operator to help you dealing with nil and it also
allows you to specify a default string value.
NSUserDefaults().setObject("_1", forKey: "SkinSuffix")
let skinSuffix = NSUserDefaults().stringForKey("SkinSuffix") ?? ""
println(skinSuffix) // "_1"

What is the existence operator in Swift?

In Objective C this is a valid line of code
self.scrollView.contentSize = self.image ? self.image.size : CGSizeZero;
Which is checking if self.image is nil or not, and choosing the left or right value.
In Swift I want to recreate the same line of code. Actually it should be exactly the same except without the semicolon
self.scrollView.contentSize = self.image ? self.image.size : CGSizeZero
But this is not valid in my Swift code getting an error, 'UIImage does not conform to protocol LogicValue'
What is the correct Swift code?
This code works if self.image is an Optional. There is no reason to have it otherwise because self.image literally cannot be nil.
The following code is completely valid:
var image : UIImage?
self.scrollView.contentSize = image ? image!.size : CGSizeZero
Note: you must use the "!" to "unwrap" the optional variable image so that you can access its size. This is safe because you just tested before hand that it is not nil.
This would also work if image is an implicitly unwrapped optional:
var image : UIImage!
self.scrollView.contentSize = image ? image.size : CGSizeZero
You are describing the conditional assignment ternary operator ?:, which operates as so:
(condition) ? (assign this value if the condition evaluates to true) : (assign this value if the condition evaluates to false)
therefore, self.image needs to be something that evaluates to true or false, which is the case in Swift for anything that conforms to the LogicValue protocol
unlike Obj-C where the mere presence of an object is equivalent to true, Swift requires a little more... we are given Optional Values that can be used as conditionals!
so what you described works if self.image is an optional value, which it sounds like it is not if you are seeing that error
to round out the answer:
if self.image is not optional, it can never be nil so you can safely make the assignment without doing the conditional check
if it is possible at some point that self.image may be nil, then it needs to be one of the optional types, either UIImage? or UIImage! (implicitly unwrapped optional, see https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/TheBasics.html)

Swift: Creating an array of UIImage

Using Swift, I'm trying to create an array of UIImage objects for a simple animation. Contextual help for animationImages reads, "The array must contain UI Image objects."
I've tried to create said array as follows, but can't seem to get the syntax correct:
var logoImages: UIImage[]
logoImages[0] = UIImage(name: "logo.png")
This throws:
! Variable logoImages used before being initialized
Then I tried
var logoImages = []
logoImages[0] = UIImage(named: "logo.png")
Which throws:
! Cannot assign to the result of this expression
I've checked the docs here, but the context isn't the same:
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/CollectionTypes.html
You have two problems (and without a regex!)
1. You aren't creating an array. You need to do:
var logoImages: [UIImage] = []
or
var logoImages: Array<UIImage> = []
or
var logoImages = [UIImage]()
or
var logoImages = Array<UIImage>()
2. If you want to add new objects to an array, you should use Array.append() or some of the equivalent syntactic sugar:
logoImages.append(UIImage(named: "logo.png")!)
or
logoImages += [UIImage(named: "logo.png")!]
or
logoImages += [UIImage(named: "logo.png")!, UIImage(named: "logo2.png")!]
You need to append to the array because (excerpt from docs):
You can’t use subscript syntax to append a new item to the end of an
array. If you try to use subscript syntax to retrieve or set a value
for an index that is outside of an array’s existing bounds, you will
trigger a runtime error. However, you can check that an index is valid
before using it, by comparing it to the array’s count property. Except
when count is 0 (meaning the array is empty), the largest valid index
in an array will always be count - 1, because arrays are indexed from
zero.
Of course you could always simplify it when possible:
var logoImage: [UIImage] = [
UIImage(named: "logo1.png")!,
UIImage(named: "logo2.png")!
]
edit: Note that UIImage now has a "failable" initializer, meaning that it returns an optional. I've updated all the bits of code to reflect this change as well as changes to the array syntax.
You are declaring the type for logoImages but not creating an instance of that type.
Use var logoImages = UIImage[]() which will create a new array for you.
...and then after creating a new empty Array instance, as described in the answer by #Jiaaro you can't use subscripting to add to an empty array
var image : UIImage = UIImage(named:"logo.png")
var logoImages = [image]

Resources