Issue with pass by value vs pass by reference in Swift? - ios

I'm new to Swift but not new to iOS dev. I'm having an issue that I believe is caused by pass by value.
Below code is in the VC and works fine when tapping each button, i.e. each button changes background to show if it's selected or not.
button.isSelected = card.isChosen // button is selected when the card is chosen
if button.isSelected {
button.setBackgroundColor(color: UIColor.darkGray, forState: UIControlState.selected)
}
I have an issue in the game logic which is a struct. Struct contains an array of type Card which is really just a deck of cards and maps back to the UIButtons. When a card(UIButton) in the VC is touched, it calls a method (chooseCard) in the Struct. Among other things, this action will toggle if the card is selected by setting the card.isSelected = !card.isSelected and maps back to the UI to indicate if the card is selected or not. All of this works fine so far.
When 3 cards have been selected, it calls some match logic, which is currently just stubbed out. If there was no match, each of the cards .isSelected is set to false so that in the UI those cards will no longer appear selected. This is what is not working and I can't figure out why. My hunch is that I'm doing something wrong in the way I'm dealing with pass by value since the logic object is a struct. After 3 cards are selected and they are not matched (they won't since it's only stubbed), each card.isSelected should be set back to false, but they are still showing selected in the UI. I set a breakpoint in the VC and it shows the card.isSelected is still set to true. VC has has a setGame property which has access to the array of cards. Here is the relevant code...any help is appreciated!
struct SetGame {
private(set) var cards = [Card]()
mutating public func chooseCard(at index: Int) {
//TODO: Assertion here for index being < cards.count
print("chooseCard(at index: \(index)")
cards[index].isChosen = !cards[index].isChosen // toggle isChosen when card is selected
if !cards[index].isMatched && cards[index].isChosen {
//print("Card not matched, so we're good to go...")
for var card in cards {
if card.isChosen {
matchingCards.append(card)
// see if we have enough cards to match
if matchingCards.count > 2 {
//TODO: Need to call match logic here and set each matched card to .isMatched = true if matched
if card.isMatched {
print("Yay, matched!")
} else {
print("Card is not matched, flipping back over")
/*** THIS LINE NOT REFLECTING IN THE UI! ***/
card.isChosen = !card.isChosen // flip the unmatched card back over
}
}
}
}
matchingCards.removeAll() // clear out all the cards from the matching
} else {
print("Card is either matched or being deselected...")
}
}

Your problem is that Card is a struct, so this line:
for var card in cards {
creates a copy of each card in cards, so setting any properties on that copy will not modify the card in your cards array.
To fix this, loop over the indices of the array and refer to the cards as cards[idx]:
struct SetGame {
private(set) var cards = [Card]()
mutating public func chooseCard(at index: Int) {
//TODO: Assertion here for index being < cards.count
print("chooseCard(at index: \(index)")
cards[index].isChosen = !cards[index].isChosen // toggle isChosen when card is selected
if !cards[index].isMatched && cards[index].isChosen {
//print("Card not matched, so we're good to go...")
for idx in cards.indices {
if cards[idx].isChosen {
matchingCards.append(cards[idx])
// see if we have enough cards to match
if matchingCards.count > 2 {
//TODO: Need to call match logic here and set each matched card to .isMatched = true if matched
if cards[idx].isMatched {
print("Yay, matched!")
} else {
print("Card is not matched, flipping back over")
/*** THIS LINE NOT REFLECTING IN THE UI! ***/
cards[idx].isChosen = !cards[idx].isChosen // flip the unmatched card back over
}
}
}
}
matchingCards.removeAll() // clear out all the cards from the matching
} else {
print("Card is either matched or being deselected...")
}
}
or consider making Card a class so that when you are referring to a Card you know you are referring to the same object.

Related

How can I make an if statement to determine what array to use in a tableView?

Using xcode 9.4, Swift 4 and ios 11, I want to select an item from a collection view screen that goes to a table view screen. On the table view screen, I have a series of arrays and, depending on what I've chosen on the collection view screen, I want to display a specific array on the table view screen.
I've already set up the prepareForSegue function on the collection view screen that references a var on the table view, but I'm a bit stuck on the "select an array" bit. I'm pretty sure it involves an if statement, but I'm not sure how or where to put this if statement.
Any help would be greatly appreciated.
Here's the if statement that I made (I don't know if its correct):
if cellItem == "jeans" {
tableArray.append(jeanArray[])
} else if cellItem == "skirts" {
tableArray.append(skirtArray[])
}
Then in the functions for the table set up, I've referenced tableArray.
There are a number of ways you can do this, I'm guessing the arrays contain similar information (guessing from jeans and skirts its clothing).
I would have a single value for determining which product type is to be displayed, then you have a few options available....
enum TableMode {
case jeans, skirts, trousers, hats
}
The multiple products array way (not recommended)
class TableViewController: UITableViewController {
var tableMode: TableMode
func tableView(UITableView, numberOfRowsInSection: Int) -> Int {
switch tableMode {
case .jeans:
return jeansArray.count
/// and so on...
}
}
}
Another option would be to have a single array that will be used, just fill it with the correct data: (best method)
// the int value could be the category ID from your API
enum TableMode: Int {
case jeans = 13
case skirts = 2
trousers = 4
hats = 19
}
class TableViewController: UITableViewController {
var tableMode: TableMode
var products: [Products]()
override viewDidLoad() {
super.viewDidLoad()
// fetch all data from api/database where categoryId = .tableMode value
self.products = coreDataFetchResult
self.tableView.reloadData()
}
Last idea, fetch all products and just filter it as needed to show the current product type selection
class TableViewController: UITableViewController {
var tableMode: TableMode
var products: [Products]()
override viewDidLoad() {
super.viewDidLoad()
// fetch all data from core data
self.products = coreDataFetchResult.filter { $0.categoryId = self.tableMode }
self.tableView.reloadData()
}
The best option depends on where you are filling the arrays from. I wouldn't suggest having multiple arrays if you are only going to use one.

How to keep multiple SKSpriteNode loosely together

How can I ensure the cards always stay close to each other as shown below?
The player may pick one or more cards, and I need to ensure the remaining cards snap back together. Also if the player abandons picking (thats is draws out and releases), they need to come back together.
What is the best way to go about building this?
I do not know about a "best way", but one way would be to have a place holder node, and when your card is not at (0,0) on the place holder, send it back.
example:
class PlayingCard : SKSpriteNode
{
override var position : CGPoint
{
didSet
{
if self.position != CGPoint.zero, actionForKey("reset") == nil
{
self.run(SKAction.moveTo(CGPoint.zero,duration:0.5).withKey:"reset")
}
}
}
}
To use:
class GameScene
{
.....
//however you are setting this up
let placeHolder = SKNode()
placeHolder = placeHolderPosition
let card = PlayingCard()
placeHolder.addChild(card)
self.addChild(placeHolder)
....
}
Now keep in mind, depending on how you are moving your card, the didSet may not be getting called on it, so you may have to do this in the update, or you can cheat and in update just do:
func update()
{
...
// do my stuff
...
//maintain an array of all your playing cards
playingCards.forEach{$0.position = $0.position}
}

Way to query whether an object is a member of an array?

I'm designing a FreeCell card game and I have both my cards (model) and cardViews (view) in arrays that represent columns of cards – and then the whole board is an array of columns:
//in the model:
typealias Card = DeckBuilder.Card
typealias Column = [Card]
var board = [Column](repeating: [], count: 8)
//in the view controller:
var boardForView = [[PlayingCardView]](repeatElement([PlayingCardView](), count: 8))
When a user taps on a cardView, I want the controller to take the selected cardView's column & row (where the column is which column in the board and the row is which card in the column) and then find the card in the corresponding position in the model.
I've got my tap gesture recognizer set up, and have extracted the cardView from it:
func selectCard(_ sender: UITapGestureRecognizer) {
if let selectedCard = sender.view as? PlayingCardView {
print("card selected: \(selectedCard.cardDescription?.string)")
}
}
So the question is: is there a property of the cardView that is its place in a column array, and then a property of that column that is the column's place in the boardForView array?
I figured it out! One way I had thought of was iterating through each card in each column of the board and checking to see if it == my current card. Seemed way too clunky. But then I found array.index(of: element) and my day is saved! I do have to iterate through each column on the board, but it's still much more elegant:
func selectCard(_ sender: UITapGestureRecognizer) {
if let currentCard = sender.view as? PlayingCardView {
for column in 1...boardForView.count {
if let row = boardForView[column - 1].index(of: currentCard) {
print("card selected: \(currentCard.cardDescription!.string) in row \(row+1) of column \(column)")
selectedCard = freeCellGame.board[column - 1][row]
print("card selected from model: \(selectedCard!.description)")
}
}
}
}
Since it's a board game, you surely have a logic set for how the board should be laid out. Make that logic work both ways. That is, use it to provide layout info while creating your board and also use it to get information about your board.
This way, when the gesture recognizer triggers, fetch the tap point and let your layout logic tell you the row and column of the card the tap came from.
Now, you can directly fetch the Card from your board property with those two indices (row and column).

How to store button tags in a variable?

I have 5 buttons representing cities, I have assigned them tags via Attribute Inspector as follows, CityA as 0,.......CityE as 4.
Is there a way I can store these tags into a variable cityTag and make sure that if none of button is pressed while saving, I can send a message "Please select a city"
I created an action with multiple buttons, but I have no idea how to create a variable and assign tags to it.
For better readability of code yourcould define an Enum for that case which represents the name and tag as well :
for example:
enum City: Int {
case .cityA,
case .cityB,
case .cityC
}
You can store them in an Array:
var cities: [City] = []
To set a city:
if let cityA: City = City(rawValue: button.tag) {
cities.append(cityA)
}
To read the Int value:
let rawValue: Int = cityA.rawValue
Since you are adding tags via the attribute inspector, you can access the tapped button's tag via sender.tag property!
Initially create an NSMutableArray (that will hold a the tags of all buttons pressed) but will obviously be empty at the start! You can access the tag with the sender.tag property in the IBAction. If your NSMutableArray doesn't contain a tag when you try to save, you can show the alert.
Initialise citytag = -1. Change the value of citytag when you press a button like citytag = sender.tag and keep a check if citytag == -1 then send a message please select a city else selected city is ###
you can try this:
func checkbuttons -> BOOL {
var countBtn = 0
for view in self.view.subviews as [UIView]
{
if let btn = view as? UIButton
{
if btn.isSelected
{
countBtn = countBtn + 1
}
}
}
if(countBtn > 0)
{
return true
}
else
{
return false
}
}
On basis of return you will know button selected or not.
Hope it will help you

How to set accessibility only to annotations in map view?

I am trying to make my mapview accessible, but I have an isuue in doing so:
If I try to make the mapView accessible,by doing this:
self.mapView.isAccessibilityElement=YES;
Then, the map view is not read by the voice over.
If I set like this:
self.mapView.isAccessibilityElement=NO;
Then the voice over is reading everything in the map, streets, buildings, my current location and my annotations.
I have given the accessibility label and hints to my annotations,but I havent provided any other value to the mapview.
I also tried by setting the accessibility elements for map view:
[self.mapView setAccessibilityElements:#[self.mapView.annotations,self.mapView.userLocation]];
But still no luck.
Is there anyway to make voice over read only the annotations and neglect remaining elements like streets,buildings?
I think you're having difficulty because an MKMapView is a UIAccessibilityContainer, and so isAccessibilityElement if false by default.
You should look into making custom voice over rotors that allow the user to navigate your app by only your annotations.
This is a good approach, because it gives the user a custom, app specific way to navigate your map, while also not taking anything away from MKMapView's built in accessibility.
There are hardly any examples online that go into detail about creating custom rotors, but I just successfully created one doing exactly what you need it to do. I followed the WWDC Session 202 (begins at 24:17).
Here's my code:
func configureCustomRotors() {
let favoritesRotor = UIAccessibilityCustomRotor(name: "Bridges") { predicate in
let forward = (predicate.searchDirection == .next)
// which element is currently highlighted
let currentAnnotationView = predicate.currentItem.targetElement as? MKPinAnnotationView
let currentAnnotation = (currentAnnotationView?.annotation as? BridgeAnnotation)
// easy reference to all possible annotations
let allAnnotations = self.mapView.annotations.filter { $0 is BridgeAnnotation }
// we'll start our index either 1 less or 1 more, so we enter at either 0 or last element
var currentIndex = forward ? -1 : allAnnotations.count
// set our index to currentAnnotation's index if we can find it in allAnnotations
if let currentAnnotation = currentAnnotation {
if let index = allAnnotations.index(where: { (annotation) -> Bool in
return (annotation.coordinate.latitude == currentAnnotation.coordinate.latitude) &&
(annotation.coordinate.longitude == currentAnnotation.coordinate.longitude)
}) {
currentIndex = index
}
}
// now that we have our currentIndex, here's a helper to give us the next element
// the user is requesting
let nextIndex = {(index:Int) -> Int in forward ? index + 1 : index - 1}
currentIndex = nextIndex(currentIndex)
while currentIndex >= 0 && currentIndex < allAnnotations.count {
let requestedAnnotation = allAnnotations[currentIndex]
// i can't stress how important it is to have animated set to false. save yourself the 10 hours i burnt, and just go with it. if you set it to true, the map starts moving to the annotation, but there's no guarantee the annotation has an associated view yet, because it could still be animating. in which case the line below this one will be nil, and you'll have a whole bunch of annotations that can't be navigated to
self.mapView.setCenter(requestedAnnotation.coordinate, animated: false)
if let annotationView = self.mapView.view(for: requestedAnnotation) {
return UIAccessibilityCustomRotorItemResult(targetElement: annotationView, targetRange: nil)
}
currentIndex = nextIndex(currentIndex)
}
return nil
}
self.accessibilityCustomRotors = [favoritesRotor]
}

Resources