Saturday, August 6, 2016

Rapid, Interactive Prototyping With Xcode Playgrounds_part2 (end)

4. Modifying Variables in the Playground

Let's now change the back faces of the cards from solid red to an image, specifically b.png in the Resources folder. Add the following line to the bottom of the playground.
  1. gc.backImage = UIImage(named: "b")!
After a second or two, you'll see that the back sides of the cards have changed from plain red to a cartoon hand.

Let's now try to alter the padding property, which we assigned a default value of 20 in Game.swift. The space between the cards should increase as a result. Add the following line to the bottom of the playground:
  1. gc.padding = 75
Wait for the live view to refresh and see that ... nothing has changed.

5. A Brief Detour

To understand what is going on, you have to keep in mind that entities, such as view controllers and their associated views, have a complex life cycle. We are going to focus on the latter, that is, views. Creating and updating of a view controller's view is a multistage process. At specific points in the life cycle of the view, notifications are issued to the UIViewController, informing it of what is going on. More importantly, the programmer  can hook into these notifications by inserting code to direct and customize this process.

The loadView() and viewDidAppear(_:) methods are two methods we used to hook into the view life cycle. This topic is somewhat involved and beyond the scope of this discussion, but what matters to us is that the code in the playground, after the assignment of the view controller as the playground's liveView, is executed some time between the call to viewWillAppear(_:) and the call to viewDidAppear(_:). You can verify this by modifying some property in the  playground and add print statements to these two methods to display the value of this property.

The issue with the value of padding not having the expected visual effect is that, by that time, the view and its subviews have already been laid out. Keep in mind that, whenever you make a change to the code, the playground is rerun from the beginning. In that sense, this issue isn't specific to playgrounds. Even if you were developing code to run on the simulator or on a physical device, often times you would need to write additional code to ensure that the change in a property's value has the desired effect on the view's appearance or content.

You might ask why we were able to change the value of the backImage property and see the result without doing anything special. Observe that the backImage property is actually used for the first time in viewDidAppear(_:), by which time it has already picked up its new value.

6. Observing Properties and Taking Action

Our way to deal with this situation will be to monitor changes to the value of padding and resize/reposition the view and subviews. Fortunately, this is easy to do with Swift's handy property observing feature. Start by uncommenting the code for the resetGrid() method in Game.swift:
  1. // (7): reset grid
  2. func resetGrid() {
  3.     view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight)
  4.     for v in view.subviews {
  5.         if let card = v as? Card {
  6.             card.center = centerOfCardAt(card.x, card.y)
  7.         }
  8.     }
  9.      
  10. }
This method recomputes the position of the view's frame and that of each Card object based on the new values of viewWidth and viewHeight. Recall that these properties are computed based on the value of padding, which has just been modified.

Also, modify the code for padding to use the didSet observer whose body, as the name indicates, executes whenever we set the value of padding:
  1. // (1): public variables so we can manipulate them in the playground
  2. public var padding = CGFloat(20) {
  3.     didSet {
  4.         resetGrid()
  5.     }
  6. }
The resetGrid() method kicks in and the view is refreshed to reflect the new spacing. You can verify this in the playground.


It appears we were able to fix things quite easily. In reality, when I first decided I wanted to be able to interact with the padding property, I had to go back and make changes to the code in Game.swift. For example, I had to abstract out the Card center calculation in a separate function (centerOfCardAt(_:_:)) to cleanly and independently (re)compute the positions of the cards whenever they needed to be laid out.

Making computed properties for viewWidth and viewHeight also helped. While this kind of rewrite is something you should be prepared for as a trade-off of not doing much upfront design, it can be reduced with some forethought and experience.

7. Game Logic & Touch Interaction

It is now time to implement the game's logic and enable ourselves to interact with it through touch. Begin by uncommenting the firstCard property declaration in the GameController class:
  1. var firstCard: Card?
Recall that the logic of the game involves revealing two cards, one after the other. This variable keeps track of whether a card flip performed by the player is the first of the two or not.

Add the following method to the bottom of the GameController class, before the terminating curly brace:
  1. func handleTap(gr: UITapGestureRecognizer) {
  2.     let v = view.hitTest(gr.locationInView(view), withEvent: nil)!
  3.     if let card = v as? Card {
  4.         UIView.transitionWithView(
  5.             card, duration: 0.5,
  6.             options: .TransitionFlipFromLeft,
  7.             animations: {card.image = UIImage(named: String(card.tag))}) { // trailing completion handler:
  8.                 _ in
  9.                 card.userInteractionEnabled = false
  10.                 if let pCard = self.firstCard {
  11.                     if pCard.tag == card.tag {
  12.                         UIView.animateWithDuration(
  13.                             0.5,
  14.                             animations: {card.alpha = 0.0},
  15.                             completion: {_ in card.removeFromSuperview()})
  16.                         UIView.animateWithDuration(
  17.                             0.5,
  18.                             animations: {pCard.alpha = 0.0},
  19.                             completion: {_ in pCard.removeFromSuperview()})
  20.                     } else {
  21.                         UIView.transitionWithView(
  22.                             card,
  23.                             duration: 0.5,
  24.                             options: .TransitionFlipFromLeft,
  25.                             animations: {card.image = self.backImage})
  26.                         { _ in card.userInteractionEnabled = true }
  27.                         UIView.transitionWithView(
  28.                             pCard,
  29.                             duration: 0.5,
  30.                             options: .TransitionFlipFromLeft,
  31.                             animations: {pCard.image = self.backImage})
  32.                         { _ in pCard.userInteractionEnabled = true }
  33.                     }
  34.                     self.firstCard = nil
  35.                 } else {
  36.                     self.firstCard = card
  37.                 }
  38.         }
  39.     }
  40. }
That is a lengthy method. That is because it packs all the required touch handling, game logic as well as associated animations in one method. Let's see how this method does its work:

First, there is a check to ensure that the user actually touched a Card instance. This is the same as? construct that we used earlier.
If the user did touch a Card instance, we flip it over using an animation similar to the one we implemented earlier. The only new aspect is that we use the completion handler, which executes after the animation completes, to temporarily disable touch interactions for that particular card by setting the userInteractionEnabled property of the card. This prevents the player from flipping over the same card. Note the _ in construct that is used several times in this method. This is just to say that we want to ignore the Bool parameter that the completion handler takes.
We execute code based on whether the firstCard has been assigned a non-nil value using optional binding, Swift's familiar if let construct.
If firstCard is non-nil, then this was the second card of the sequence that the player turned over. We now need to compare the face of this card with the previous one (by comparing the tag values) to see whether we got a match or not. If we did, we animate the cards fading out (by setting their alpha to 0). We also remove these cards from the view. If the tags are not equal, meaning the cards don't match, we simply flip them back facing down and set userInteractionEnabled to true so that the user can select them again.
Based on the current value of firstCard, we set it to either nil or to the present card. This is how we switch the code's behavior between two successive touches.
Finally, uncomment the following two statements in the GameController's initializer that adds a tap gesture recognizer to the view. When the tap gesture recognizer detects a tap, the handleTap() method is invoked:
  1. let tap = UITapGestureRecognizer(target: self, action: #selector(GameController.handleTap(_:)))
  2. view.addGestureRecognizer(tap)
Head back to the playground's timeline and play the memory game. Feel free to decrease the large padding we assigned a bit earlier.

The code in handleTap(_:) is pretty much the unembellished version of what I wrote the first time. One might raise the objection that, as a single method, it does too much. Or that the code isn't object-oriented enough and that the card flipping logic and animations should be neatly abstracted away into methods of the Card class. While these objections aren't invalid per se, remember that quick prototyping is the focus of this tutorial and since we did not foresee any need to interact with this part of the code in the playground, we could afford to be a bit more "hack-ish".

Once we have something working and we decide we want to pursue the idea further, we would certainly have to give consideration to code refactoring. In other words, first make it work, then make it fast/elegant/pretty/...

8. Touch Handling In the Playground

While the main part of the tutorial is now over, as an interesting aside, I want to show you how we can write touch handling code directly in the playground. We will first add a method to the GameController class that allows us to peek at the faces of the cards. Add the following code to the GameController class, immediately after the handleTap(_:) method:
  1. public func quickPeek() {
  2.     for v in view.subviews {
  3.         if let card = v as? Card {
  4.             card.userInteractionEnabled = false
  5.             UIView.transitionWithView(card, duration: 1.0, options: .TransitionFlipFromLeft, animations: {card.image =  UIImage(named: String(card.tag))}) {
  6.                 _ in
  7.                 UIView.transitionWithView(card, duration: 1.0, options: .TransitionFlipFromLeft, animations: {card.image =  self.backImage}) {
  8.                     _ in
  9.                     card.userInteractionEnabled = true
  10.                 } 
  11.             }
  12.         }
  13.     }
  14. }
Suppose we want the ability to activate or deactivate this "quick peek" feature from within the playground. One way to do this would be to create a public Bool property in the GameController class that we could set in the playground. And of course, we would have to write a gesture handler in the GameController class, activated by a different gesture, that would invoke quickPeek().

Another way would be to write the gesture handling code directly in the playground. An advantage of doing it this way is that we could incorporate some custom code in addition to calling quickPeek(). This is what we will do next. Add the following code to the bottom of the playground:
  1. class LPGR {
  2.     static var counter = 0
  3.     @objc static func longPressed(lp: UILongPressGestureRecognizer) {
  4.         if lp.state == .Began {
  5.             gc.quickPeek()
  6.             counter += 1
  7.             print("You peeked \(counter) time(s).")
  8.         }
  9.     }
  10. } 
  11. let longPress = UILongPressGestureRecognizer(target: LPGR.self, action: #selector(LPGR.longPressed))
  12. longPress.minimumPressDuration = 2.0
  13. gc.view.addGestureRecognizer(longPress)
To activate the quick peek feature, we will use a long press gesture, that is, the player holds their finger on the screen for a certain amount of time. We use two seconds as the threshold.

For handling the gesture, we create a class, LPGR (long press gesture recognizer abbreviated), with a static variable property, counter, to keep track of how many times we peeked, and a static method longPressed(_:) to handle the gesture.

By using the static qualifier, we can avoid having to create an LPGR instance because the entities declared static are associated with the LPGR type (class) rather than with a particular instance.

Apart from that, there is no particular advantage to this approach. For complicated reasons, we need to mark the method as @objc to keep the compiler happy. Note the use of LPGR.self to refer to the type of the object. Also note that in the gesture handler, we check if the state of the gesture is .Began. This is because the long press gesture is continuous, that is, the handler would execute repeatedly as long as the user kept their finger on the screen. Since we only want the code to execute once per finger press, we do it when the gesture is first recognized.

The incrementing counter is the custom code that we introduced, which doesn't rely on functionality provided by the GameController class. You can view the output of the print(_:) function (after having peeked a couple of times) in the console at the bottom.


Conclusion

Hopefully, this tutorial has demonstrated an interesting example of rapid, interactive prototyping in Xcode playgrounds. Apart from the reasons for using playgrounds I mentioned earlier, you could think up other scenarios where they could be useful. For instance:
  • demonstrating prototype functionality to clients and letting them choose options and make customizations with live feedback and without having to dig into the nitty-gritty details of the code.
  • developing simulations, such as for physics, where students can play with some parameter values and observe how the simulation is affected. In fact, Apple has released an impressive playground that showcases their interactivity and the incorporation of physics via the UIDynamics API. I encourage you to check it out.
When using playgrounds for demonstration/teaching purposes such as these, you will probably want to make liberal use of the markup capabilities of playgrounds for rich text and navigation.

The Xcode team seems committed to improving playgrounds as new versions of the IDE are rolled out. The breaking news is that Xcode 8, currently in beta, will feature playgrounds for iPad. But, obviously, playgrounds are not meant to substitute the full blown Xcode IDE and the need to test on actual devices when developing a complete, functional app. Ultimately, they are just a tool to be used when it makes sense, but a very useful one.
Written  by Akiel Khan

If you found this post interesting, follow and support us.
Suggest for you:

iOS 10 Message Extension App Reskinning without Coding

iOS 10 Projects: Build Amazing Apps with Apple's Newest iOS

iOS 10 & Swift 3: From Beginner to Paid Professional

The Complete iOS 10 Developer - Build Real Apps with Swift 3

Swift 3 and iOS 10 The Final Course Learn to Code like a Pro

Master iOS 9 - 10 - Xcode 8 and Swift 3

No comments:

Post a Comment