Facebook Tweaks with Swift Tutorial

Facebook Tweaks with Swift Tutorial

startImge

Note from Ray: This is a brand new Swift tutorial released as part of the iOS 8 Feast. Enjoy!

Have you ever been in the final stages of developing an awesome new app and spent 80 percent of your time on the last 20 percent of the project? Yeah, we’ve all been there!

It’s not you, it’s the Pareto principle, and if you work on projects you probably know all about the concept, even if you don’t know it by name. Here’s an example:

Assume you built an app that helps its user become more polite, because polite is the new cool. In the app, the user has a “Nice Jar.” Whenever they are nice to someone else, they open the app and adds a coin to the jar. When it’s full, they can reward themselves with an ice cream.

The app uses UIDynamics to add physics behavior to the coins, and the user interface shows a jar, a label and some buttons.

It’s nearly finished and you’re quite pleased with the result, so you put it onto your iPhone and show it to a friend. They look at it and say, “It’s nice, but why are the coins so small?”

Then you show it your colleagues the next time you’re at work and they say, “I can barely read the labels.” and “The coins just drop like rocks, shouldn’t they bounce?” And so on.

So you go to your computer, adjust some constants and put it on your phone again, but instead of rave reviews you get yet more suggestions and critiques. Obviously, your friends and colleagues are not pleased with the result and it seems like you’ll never break away from Xcode.

During the development of Paper, Facebook was in the same situation. To handle these small, subjective, late-stage adjustments, they came up with a rather clever solution that lets a tester change values or parameters for themselves directly from within the app, without touching code or any recompiling.
It’s called Facebook Tweaks, or FBTweak for short.

The best part is that Facebook published the source code of FBTweak under a BSD license, which means you can use it in your iOS app development. How polite — Facebook should add a coin to its Nice Jar.

In this tutorial, you’ll add FBTweak to the “Nice Jar” App.

BSD licenses are a group of lenient free software licenses, imposing minimal restrictions on the redistribution of the covered software. The original BSD license was used for its namesake, the Berkeley Software Distribution (BSD), a Unix-like operating system. You can find out more about Open Source Licenses here

Getting Started

To start this tutorial, download the starter project here. Open the project, and then build and run. You should see this:

The NiceJar App

The NiceJar App

If you touch the jar, a new coin appears and falls to the bottom of the jar, but you can only touch it if you’ve been nice to somebody today. Oh, you tipped your Barista this morning? That qualifies as polite. :]

Switch to Xcode and take a minute to look around the project, especially the interesting code in ViewController.swift. You should notice the following parts:

  • To animate the coins: UIDynamicAnimator, UIDynamicItemBehavior,
    UIGravityBehavior and UICollisionBehavior (in viewDidLoad)
  • To set the gravity direction according to the device orientation: CMMotionManager.
  • For touch handling: touchesEnded(...)
A special thanks to Vicki from GameArtGuppy.com for the fun artwork in the project. The app would not have been as cool without her magic touch.

Getting Tweaks

Open this link and click the Download ZIP button.

FBTweak on github

FBTweak on github

OS X will automatically unzip the file into a folder named Tweaks-master. Find the FBTweak folder and drag into the NiceJar project. Make sure you check Copy items if needed.

Adding FBTweak to NiceJar

Adding FBTweak to NiceJar

FBTweak is an Objective-C library, so if you want to use it in a Swift project, you have to add a bridging header.

Back in Xcode, go to File\New\File…, choose the iOS\Header File template and click Next. Name the file NiceJar-Bridging-Header.h, select the folder NiceJar and click Create.

Replace the contents of NiceJar-Bridging-Header.h with the following:

#ifndef NiceJar_NiceJar_Bridging_Header_h
#define NiceJar_NiceJar_Bridging_Header_h
 
#import "FBTweak.h"
#import "FBTweakStore.h"
#import "FBTweakCategory.h"
#import "FBTweakCollection.h"
#import "FBTweakViewController.h"
#import "FBTweakShakeWindow.h"
 
#endif

Select the Project Navigator on the left, and then select the project.
04_selectProject

Select Build Settings, choose the filters All and Levels and then enter bridgin as the search term.
05_selectBuildSettings

Add NiceJar/NiceJar-Bridging-Header.h as an Objective-C Bridging Header. This file path is relative to your project, so if you saved NiceJar-Bridging-Header.h in another folder, make sure to use the correct path.
06_addBridgingHeaderInBuildSettings

And just like that, you’ve enabled Swift to access these Objective-C classes. You can find out more about mixing Swift and Objective-C in the same project here.

FBTweak comes with a view controller that shows the available tweaks, and allows the user make changes to them.

The easiest way to present this view controller is to exchange the UIWindow in AppDelegate.swift with an instance of FBTweakShakeWindow. Open AppDelegate.swift and change the line:

var window: UIWindow?

to

lazy var window: UIWindow = {
  let window = FBTweakShakeWindow(frame: UIScreen.mainScreen().bounds)
  return window
}()

Within the closure, an instance of FBTweakShakeWindow is instantiated and returned. The return value is then set to the window property of the project. This means instead of a UIWindow, the application window is now an instance of FBTweakShakeWindow.

Note: The lazy modifier tells the app delegate not to initialize the window until it’s actually accessed. This handy trick speeds up load times by preventing the creation of objects until they’re needed. You can read more about property modifiers here.

FBTweakShakeWindow is a subclass of UIWindow that adds methods to present the tweaks view controller with a shake gesture. Take a look at the source code to see how this happens.

At this stage, shaking works on a device but not in the simulator because of a bug in FBTweakShakeWindow.m. To enable the presentation of the tweaks view controller in the simulator, open FBTweakShakeWindow.m, find _shouldPresentTweaks (around line 44), and exchange the lines:

#if FB_TWEAK_ENABLED
  return _shaking && [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive;
#elif TARGET_IPHONE_SIMULATOR
  return YES;

With the following:

#if TARGET_IPHONE_SIMULATOR
  return YES;
#elif FB_TWEAK_ENABLED
  return _shaking && [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive;

Build and run. With the app running and the simulator in the foreground, select Hardware\Shake Gesture (^⌘Z). The tweaks view controller will be presented modally, but doesn’t yet list any tweaks.

An empty tweaks view controller

An empty tweaks view controller

The First Tweak

On the FBTweak Github page, you can read an explanation of the simplest way to add tweaks to your project, and that is to use one of the provided macros like FBTweakValue(...), FBTweakBind(...) and FBTweakAction(...).

Hold up right there! Swift doesn’t support macros, so you’ll have to add tweaks with a slightly more elaborate syntax.

Open ViewController.swift and find touchesEnded(...). Exchange the line:

let coinDiameter = CGFloat(50)

With this:

let identifier = "de.dasdev.nicejar.coinsize"
//1
let store = FBTweakStore.sharedInstance()                   
//2
var category = store.tweakCategoryWithName("Jar View")      
if category == nil {
  category = FBTweakCategory(name: "Jar View")
  store.addTweakCategory(category)
}
//3
var collection = category.tweakCollectionWithName("Coins")  
if collection == nil {
  collection = FBTweakCollection(name: "Coins")
  category.addTweakCollection(collection)
}
 
//4 
var tweak = collection.tweakWithIdentifier(identifier)      
if tweak == nil {
  tweak = FBTweak(identifier: identifier)
  tweak.name = "Size"
  tweak.defaultValue = CGFloat(50)
 
  collection.addTweak(tweak)
}
 
//5
let coinDiameter: CGFloat = CGFloat((tweak.currentValue ?? tweak.defaultValue).floatValue)
  1. You get the shared instance of the tweak store that holds a reference to all tweak categories that have been defined.
  2. Then you get the category with the name Jar View, and if there isn’t a category with that name, it’s created and added to the store.
  3. You get a collection with the name Coins, and if there isn’t a collection with that name, it’s created and added to the category.
  4. You get the tweak with the identifier de.dasdev.nicejar.coinsize, and if it can’t be found it creates a tweak with that identifier, adds it to the collection and gives it the default value of CGFloat(50).
  5. The coinDiameter is then set to the currentValue of the tweak if it’s non-nil, otherwise it’s set to its defaultValue.

Build and run. Reward your recent politeness by tapping the jar. Then go to Hardware\Shake Gesture. You should now see the entry Jar View in the list of tweaks.

The tweak view controller with one category

The tweak view controller with one category

Tap it and you’ll push the tweak category view onto the navigation stack.

The coin size tweak

The coin size tweak

Change the value of the coin size to 70 and tap Done. Tap the jar again to see bigger, more satisfying coins fall into the jar.

Coins with two different sizes

Coins with two different sizes

Congratulations! You added your very first tweak to the project. You could now hand the phone back to your friend and let them determine the perfect coin size, without ever touching Xcode.

How Does it Work?

FBTweak has properties to hold the default value and the current value. When the user changes the tweak value in the tweaks view controller, it modifies the current value and writes it to NSUserDefaults with the identifier as the key.

The next time you run the tweak, the current value is set from NSUserDefaults — this means you should always use different identifiers for the tweaks.

The rest of the classes present the tweaks view controller, store the tweaks categories and collections, and create tweaks using fancy macros in Objective-C. If you want to see some complex Objective-C macros, feel free to take a look in FBTweakInlineInternal.h.

A Helper in the Dark

If you thought the coin size tweak was cool, then prepare to be amazed – you’re going to add a lot more tweaks. Duplicating the code from your first tweak over and over again isn’t a wise thing to do, and as you already know Swift has no love for macros.

However, you can create a helper class to ease the workload.

Go to File\New\File… and chose iOS\Source\Swift File. Enter the name Tweaks.swift and click Create.

Replace the contents of the new file with the following:

import Foundation
 
class Tweaks {
  //1
  class func collectionWithName(collectionName: String, categoryName: String) -> FBTweakCollection {
     let store = FBTweakStore.sharedInstance()
 
     var category = store.tweakCategoryWithName(categoryName)
     if category == nil {
       category = FBTweakCategory(name: categoryName)
       store.addTweakCategory(category)
     }
 
     var collection = category.tweakCollectionWithName(collectionName)
     if collection == nil {
       collection = FBTweakCollection(name: collectionName)
       category.addTweakCollection(collection)
     }
     return collection
  }
 
  //2
  class func tweakValueForCategory<T:AnyObject>(categoryName: String, collectionName: String, name: String, defaultValue: T) -> T {
 
    let identifier = categoryName.lowercaseString + "." + collectionName.lowercaseString + "." + name
 
    let collection = collectionWithName(collectionName, categoryName: categoryName)
 
    var tweak = collection.tweakWithIdentifier(identifier)
    if tweak == nil {
      tweak = FBTweak(identifier: identifier)
      tweak.name = name
      tweak.defaultValue = defaultValue
 
      collection.addTweak(tweak)
    }
 
    return (tweak.currentValue ?? tweak.defaultValue) as T
  }
 
}

This code essentially does the same thing as the code of your first tweak.

  1. This method returns a collection with the name collectionName in a category with the name categoryName. If there isn’t a collection and/or a category with the specified names, they’re created and added to the store. This method is a helper method for tweakValueForCategory(...).
  2. This adds a tweak to a collection with the specified name.

What does the mean? This is a generic. If you’re unfamiliar with generics, see the explanation here.

In short it means that the parameter with the type T can be any type as long as it conforms to the AnyObject protocol. The default value has to conform to AnyObject because in FBTweak.h it’s defined to be of type FBTweakValue, and that is an alias for id.

Now you can clean up the code of your first tweak. Go to touchesEnded(...) in ViewController.swift and replace these lines:

let identifier = "de.dasdev.nicejar.coinsize"
 
let store = FBTweakStore.sharedInstance()
 
var category = store.tweakCategoryWithName("Jar View")
if category == nil {
  category = FBTweakCategory(name: "Jar View")
  store.addTweakCategory(category)
}
 
var collection = category.tweakCollectionWithName("Coins")
if collection == nil {
  collection = FBTweakCollection(name: "Coins")
  category.addTweakCollection(collection)
}
 
var tweak = collection.tweakWithIdentifier(identifier)
if tweak == nil {
  tweak = FBTweak(identifier: identifier)
  tweak.name = "Size"
  tweak.defaultValue = CGFloat(50)
 
  collection.addTweak(tweak)
}
 
let coinDiameter = CGFloat((tweak.currentValue ??  tweak.defaultValue).floatValue)

With this cute one-liner:

let coinDiameter = Tweaks.tweakValueForCategory("Jar View", collectionName: "Coins", name: "Size", defaultValue: 50)

Much better! Now the code to add a tweak won’t detract from the app’s main code.

Build and run. Make sure the tweak still works by following the same steps as before.

Note: You have to add a coin to the jar before you change the coin size, because the tweak is added to the tweak store in touchesEnded(...).

More Tweaks!

Now that you have a convenient way to add tweaks, go crazy and add a bunch more! Go to viewWillAppear(...) in ViewController.swift and add the following lines right below the call to super:

//1
let elasticity = Tweaks.tweakValueForCategory("Jar View", collectionName: "Coins", name: "Elasticity", defaultValue: 0.35)
propertyBehavior.elasticity = elasticity
 
//2
let labelConstaintConstant = Tweaks.tweakValueForCategory("Jar View", collectionName: "Label", name: "Y Offset", defaultValue: 102)
verticalLabelConstraint.constant = labelConstaintConstant
 
//3
let showButton = Tweaks.tweakValueForCategory("Summary", collectionName: "General", name: "Show", defaultValue: false)
summaryButton.hidden = !showButton
  1. The first tweak lets you change the elasticity of the coins. Allowed values are between 0.0 and 1.0.
  2. The second tweak lets you change the vertical position of the coin label.
  3. The last tweak lets you show and hide the summary button. This is especially useful if you have view controllers you’re currently working on and they should only be visible to a select group of testers. For any other testers, you can simply disable this button and hide the unfinished parts of your app.

Build and run. Add some coins to the jar and go to Hardware\Shake Gesture. There are now two tweak categories: Jar View and Summary. Tap Jar View and you should see the following:

Three tweaks in the Jar View category

Three tweaks in the Jar View category

Change the Elasticity of the coins to 0.9 and the Y Offset of the label to 200. Tap Done and add coins to the jar.

Wow! That’s some serious bounce.

Forbidden Values

With the app running, open the tweaks window again (Hardware\Shake Gesture), then open Jar View and increase the elasticity to a value that makes no sense, for instance something over 2.0.

The fact you can do this is somewhat unfortunate. How can your testers possibly know what the allowed values are if they can enter anything they want? Don’t worry, Facebook has an answer to this; take a look at the following properties declared in FBTweak.h:

/**
  @abstract The minimum value of the tweak.
  @discussion Optional. If nil, there is no minimum.
    Should not be set on tweaks representing actions.
 */
@property (nonatomic, strong, readwrite) FBTweakValue minimumValue;
 
/**
  @abstract The maximum value of the tweak.
  @discussion Optional. If nil, there is no maximum.
    Should not be set on tweaks representing actions.
 */
@property (nonatomic, strong, readwrite) FBTweakValue maximumValue;

Now you just need to add support for those to your helper class.

Open Tweaks.swift and add the two parameters minimumValue: T? = nil and maximumValue: T? = nil to tweakValueForCategory(...). The declaration should now resemble the following:

class func tweakValueForCategory<T:AnyObject>(categoryName: String, collectionName: String, name: String, defaultValue: T, minimumValue: T? = nil, maximumValue: T? = nil) -> T

Take a look at the new parameters — they’re special. First, they’re optional and that means they can also be nil. In addition, they have default values of nil. A nice side effect of this is that you don’t have to change the call to this method in your tweaks; if the default values are okay for you, you can omit these parameters in the call to this method.

Build and run. Open the tweaks view controller and tap Reset to reset the tweaks to the default values. Now the app should behave as it did before.

Next you have to use those parameters in the creation of the tweak. Open Tweaks.swift again and add the following lines below tweak.defaultValue = defaultValue in tweakValueForCategory(...):

if minimumValue != nil && maximumValue != nil {
  tweak.minimumValue = minimumValue
  tweak.maximumValue = maximumValue
}

Go to viewWillAppear(...) in ViewController.swift and change the elasticity tweak to the following:

let elasticity = Tweaks.tweakValueForCategory("Jar View", collectionName: "Coins", name: "Elasticity", defaultValue: 0.35, minimumValue: 0.0, maximumValue: 1.0)

Build and run. Open the tweak view controller, go to Jar View category and try to increase the elasticity beyond 1.0. It doesn’t work! Nice!

More Complicated Tweaks

One of your testers said that the label text should be bigger. You could use the method you already have to add a tweak for the font size, but there’s a cooler way to go about it.

You’ll add a tweak that executes a closure when the tester changes a tweak value — a tweak with action, if you will.

Instances of FBTweak can have observers. When the value of a tweak changes, the method tweakDidChange(tweak: FBTweak) is called on all observers. You’ll use this to implement the tweak with action.

Open Tweaks.swift and change the following line:

class Tweaks {

To this:

class Tweaks: NSObject, FBTweakObserver {

With this change, you tell the compiler that Tweaks will implement tweakDidChange.

Now, add the following to the beginning of the Tweaks class in Tweaks.swift:

typealias ActionWithValue = ((currentValue: AnyObject) -> ())
var actionsWithValue = [String:ActionWithValue]()

The variable actionsWithValue is a dictionary that stores functions with a parameter of type AnyObject and a return value of ().

Now, add the following method to the class:

func tweakActionForCategory<T where T: AnyObject>(categoryName: String, collectionName: String, name: String, defaultValue: T, minimumValue: T? = nil, maximumValue: T? = nil, action: (currentValue: AnyObject) -> ()) {
 
  let identifier = categoryName.lowercaseString + "." + collectionName.lowercaseString + "." + name
 
  let collection = Tweaks.collectionWithName(collectionName, categoryName: categoryName)
 
  var tweak = collection.tweakWithIdentifier(identifier)
  if tweak == nil {
    tweak = FBTweak(identifier: identifier)
    tweak.name = name
 
    tweak.defaultValue = defaultValue
 
    if minimumValue != nil && maximumValue != nil {
      tweak.minimumValue = minimumValue
      tweak.maximumValue = maximumValue
    }
    tweak.addObserver(self)
 
    collection.addTweak(tweak)
  }
 
  actionsWithValue[identifier] = action
 
  action(currentValue: tweak.currentValue ?? tweak.defaultValue)
}

The method tweakActionForCategory(...) looks very similar to tweakValueForCategory(...). However, the difference is that it has an additional parameter, action: (currentValue: AnyObject) -> (), that is added to the dictionary actionsWithValue within the method body.

The instance of the Tweaks class (self) is added as an observer to the tweak. This means when a tweak that was added via this method is changed, the method tweakDidChange(...) on the instance is called.

Finally, add the following:

func tweakDidChange(tweak: FBTweak!) {
  let action = actionsWithValue[tweak.identifier]
  action?(currentValue: tweak.currentValue ??  tweak.defaultValue)
}

In tweakDidChange(...), you use the tweak identifier to retrieve the action for this tweak from the actionsWithValue dictionary. This action is then executed with the current value, provided it’s not nil, otherwise it’s set to the default value.

To get this tweak with action to work, you need an instance of the Tweaks class in ViewController.swift.

Go to the beginning of the class ViewController and add:

let tweaks = Tweaks()

Now add the following code in viewDidLoad:

tweaks.tweakActionForCategory("Jar View", collectionName: "Coin Label", name: "Text Size", defaultValue: 30, minimumValue: 20, maximumValue: 60, action: { (currentValue) -> () in
  self.coinLabel.font = self.coinLabel.font.fontWithSize(CGFloat(currentValue.floatValue))
})

This code adds a tweak with ab action to the tweak store. When you change the value of the tweak, the action executes. In this case, you’re changing the font size of the coinLabel to the current value of the tweak.

Build and run. Pull up the tweaks view controller. Change the font size to 60 and tap Done. You should see something like this:

Larger text size

Larger text size

Even More Tweaks

Add a few more tweaks, just to practice your new skills. Add the following code to viewDidLoad() in ViewController.swift:

tweaks.tweakActionForCategory("Jar View", collectionName: "Coin Label", name: "Orange Text", defaultValue: false, action: { (currentValue) -> () in
  if currentValue.boolValue == true {
    self.coinLabel.textColor = UIColor(red: 0.98, green: 0.58, blue: 0.13, alpha: 1.0)
  } else {
    self.coinLabel.textColor = UIColor.blackColor()
  }
})

This tweak lets you toggle the text color of the coinLabel.

How would the app look if you were to change the gravity? Well, add a tweak for it and find out. Back within viewDidLoad(), find the closure of the startAccelerometerUpdatesToQueue method of motionManager, and replace these two lines:

let y = CGFloat(data.acceleration.y)
let x = CGFloat(data.acceleration.x)

With the following:

let magnitude = Tweaks.tweakValueForCategory("Jar View", collectionName: "Dynamics", name: "Gravity Magnitude", defaultValue: 1.0)
let y = magnitude * CGFloat(data.acceleration.y)
let x = magnitude * CGFloat(data.acceleration.x)

This tweak gives you and your testers control of a fundamental element — gravity! It’s like being a super hero! Unfortunately, this only works when you test on a device as the simulator doesn’t have an accelerometer and the iPhone doesn’t know how to defy gravity — yet.

Play around with the tweaks and find the values that make the app behave and look its best.

Production Code

I recommend you remove the tweaks code as soon as you settle on the right values. But just in case you forget or something weird happens, like you have one beer too many and decide to publish your app after letting your friends tweak it, you should add code to disable tweaks in your release builds.

Select your project in the Project Navigator and then open Build Settings. Search for other swift. Add the value -DDEBUG in Other Swift Flags to the Debug configuration, as shown here:

Adding a swift flag

Adding a swift flag

With this addition, you can use preprocessor directives to disable Tweaks in release builds.

Note: Preprocessor directives are like macros that tell the compiler which code to use when compiling. This allows you to conditionally execute parts of your code, so that not only will it not run in production, it is literally excluded from the build all together.

Open AppDelegate.swift and replace the existing definition of window with the following:

#if DEBUG
lazy var window: UIWindow = {
  let window = FBTweakShakeWindow(frame: UIScreen.mainScreen().bounds)
  return window
}()
#else
var window: UIWindow?
#endif

Now go to Tweaks.swift and add #if DEBUG at the beginning of both tweakActionForCategory(...) and tweakValueForCategory(...). At the end of the method tweakActionForCategory(...), add the lines:

#else
action(currentValue: defaultValue)
#endif

And add this at the end of tweakValueForCategory(...):

#else
return defaultValue
#endif

Now, when you make a release build, the tweaks fall back to default values. So even if you do publish your app when you shouldn’t the worst you’ll do is publish it exactly how you developed it to be.

Where to go From Here?

If you’d like to see the app in its perfect form, download the final project for this tutorial here.

At this point, you have a functional app that looks and behaves perfectly, thanks to Facebook Tweaks.

To improve the project, you could add a second view for the summary. Also, the alert view doesn’t quite fit with the overall look and feel of the app, so add tweaks to find the right text size, and position of the summary label.

The coin’s physics aren’t quite perfect either, hence why you occasionally see strange gaps between the coins. SpriteKit is often better suited to simulating physics in this way. Here is a starter project you can use to build NiceJar using SpriteKit instead of UIKit.

Thank you for taking the time to work through this tutorial! I hope you learned some cool new tricks which should make it faster and easier to move through that last 20 percent of your project. Feel free to weigh in, ask questions, or share your brilliant ideas for how to use Facebook Tweaks by leaving a comment below.

Facebook Tweaks with Swift Tutorial is a post from: Ray Wenderlich

The post Facebook Tweaks with Swift Tutorial appeared first on Ray Wenderlich.

8
Like
Save

Comments

Write a comment

*