In this screencast, you'll learn how to set up parent / child relationships between database tables in Vapor and Fluent.
The post Screencast: Server Side Swift with Vapor: Parent-Child Relations appeared first on Ray Wenderlich.
In this screencast, you'll learn how to set up parent / child relationships between database tables in Vapor and Fluent.
The post Screencast: Server Side Swift with Vapor: Parent-Child Relations appeared first on Ray Wenderlich.
A common obstacle when starting a game project is the lack of art to use for game sprites. One way to make things easier is to pick an art style that uses very simple or stylized graphics.
For example, the popular game Color Switch (with over 125 million downloads worldwide) creates fun and attractive visuals using only simple shapes such as circles, lines, and squares that are brightly colored.
In this tutorial, you will make a game where the goal is to move a colored dot up the screen and past multi-colored obstacles — similar to Color Switch. The dot can safely pass through shapes that match its own color, but will fall back to the beginning if it contacts any shape with a different color.
You will learn how to use:
SKShapeNode
s with custom paths,SKAction
s and radians for rotating nodes indefinitely,SKCameraCode
for conveniently following the player as the game progress,Note: You’ll use SpriteKit to build this game. If you are new to SpriteKit, you might want to start with an introduction to sprites and the physics system.
Download the starter project for this tutorial. There are only minor changes from the default Xcode template to ensure everything works on both iPhone and iPad.
Build and run the project, and you should see a black screen. Unlike many games, this one is played in portrait mode, since the player movement is up and down.
The stars of the show are the multi-colored obstacles that you have to bypass to score points in this game. There are many shapes you can use as obstacles, but creating a circle is a good place to start. The end result will look like this.
Since the obstacles are made up of geometric shapes, you’re going to build them up using the SpriteKit class SKShapeNode
. To create a custom SKShapeNode
, you can define its path. The simplest way to define a path is to use the UIBezierPath
class.
UIBezierPath
provides several initializers
for simple shapes:
init(rect: CGRect)
: Creates and returns a rectangle of a given size.init(ovalIn: CGRect)
: Creates and returns an oval (or circle) that fits in a given rectangle.init(roundedRect: CGRect, cornerRadius: CGFloat)
: Creates and returns a rectangle with rounded corners. The larger the cornerRadius
, the more rounded the corners are.It might seem like init(ovalIn:)
would be what you want to make your circle obstacle, but this method gives you a complete circle, and you need it separated into four sections. That way each section can have its own color. Thankfully, UIBezierPath
provides additional methods for generic path drawing.
A good starting point is to make a node for the yellow section at the bottom-right of the circle. You’ll do this by drawing the outline of the shape.
In GameScene.swift, add the following methods below didMove(to:)
:
func setupPlayerAndObstacles() { addObstacle() } func addObstacle() { addCircleObstacle() } func addCircleObstacle() { // 1 let path = UIBezierPath() // 2 path.move(to: CGPoint(x: 0, y: -200)) // 3 path.addLine(to: CGPoint(x: 0, y: -160)) // 4 path.addArc(withCenter: CGPoint.zero, radius: 160, startAngle: CGFloat(3.0 * M_PI_2), endAngle: CGFloat(0), clockwise: true) // 5 path.addLine(to: CGPoint(x: 200, y: 0)) path.addArc(withCenter: CGPoint.zero, radius: 200, startAngle: CGFloat(0.0), endAngle: CGFloat(3.0 * M_PI_2), clockwise: false) } |
and add this to didMove(to:)
:
setupPlayerAndObstacles() |
With this code you create one circle obstacle when the game starts. Inside addCircleObstacle()
, you:
move(to: CGPoint)
, which moves to the given point, but does not draw a path. This method is generally used to move to the starting point of your shape, which you do here.startAngle
, endAngle
, and clockwise
to work the way you want it to.When defining a path, make sure to pay attention to which direction you are moving. If you leave a gap, or if a side has the wrong direction, you will get a strange shape.
The start and end angle values are in a unit called radians. A complete circle is 2π radians, and the values for other major angles are shown on the diagram. There are constants defined for several common angle values:
M_PI
is π radians, or 180°M_PI_2
is π/2 radians, or 90°M_PI_4
is π/4 radians, or 45°Note: The values for clockwise seem to be backward from what they should be. This is due the coordinate system for UIBezierPath
being different from that used in SpriteKit. UIBezierPath
, along with all of UIKit, uses the +y direction as down. SpriteKit, and other OpenGL systems, have the +y direction as up.
Now that you have a path, you can create and display an SKShapeNode
. Add this to the end of your addCircleObstacle()
method:
let section = SKShapeNode(path: path.cgPath) section.position = CGPoint(x: size.width/2, y: size.height/2) section.fillColor = .yellow section.strokeColor = .yellow addChild(section) |
The initializer for SKShapeNode
uses a CGPath
, but UIBezierPath
will do that conversion for you with the cgPath
property. You could have generated a CGPath
directly, but UIBezierPath
is often easier to work with. Then you set the position and color properties of the node, and add it to the scene to display it.
You set the strokeColor
in addition to the fillColor
, because the stroke outline defaults to a thin white line, and you do not want any outline on the shape.
Build and run the project to make sure everything worked.
Note: If you get a strange looking shape, double check your path numbers, especially the order of steps and the signs of numbers. If any of those are moved around, the shape will not be what you are expecting.
Take a look at the position of this shape on the screen. Notice how it is in the lower right quadrant of the screen? You might have expected this shape to appear more centered.
The reason is that the path you drew in the first part of this method is drawn relative to a central point, so the shape itself is offset from that central point. Setting the position of the SKShapeNode
changes the position of this central point, which lets you move the shape around.
You could add the other parts of the circle by plotting out the points and angles like you did for the first section, but it turns out there is a shortcut. The center of the first SKShapeNode
is at the center of the screen. That means that all you have to do is rotate it 90°, or π/2 radians for it to match up as the upper-right quarter of the circle.
Add the following to the end of addCircleObstacle()
in GameScene.swift:
let section2 = SKShapeNode(path: path.cgPath) section2.position = CGPoint(x: size.width/2, y: size.height/2) section2.fillColor = .red section2.strokeColor = .red section2.zRotation = CGFloat(M_PI_2); addChild(section2) |
You already have the path built, so you just need another SKShapeNode
set to a different color, rotated by changing the zRotation
property.
Build and run the project to verify your progress. You’re halfway there!
There is some repeated code used to create these two sections of the circle, which will only get worse if you draw the rest of the circle this way. To avoid this duplication, you’ll write a method that takes in a path that defines a quarter-section of an obstacle, and duplicates the path four times around with different colors.
First, add a property at the top of GameScene.swift, inside the class, but before the didMove(to:)
method:
let colors = [SKColor.yellow, SKColor.red, SKColor.blue, SKColor.purple] |
This array defines the colors that will be used in the four sections of an obstacle.
Next, add the new method after the closing brace of addCircleObstacle()
:
func obstacleByDuplicatingPath(_ path: UIBezierPath, clockwise: Bool) -> SKNode { let container = SKNode() var rotationFactor = CGFloat(M_PI_2) if !clockwise { rotationFactor *= -1 } for i in 0...3 { let section = SKShapeNode(path: path.cgPath) section.fillColor = colors[i] section.strokeColor = colors[i] section.zRotation = rotationFactor * CGFloat(i); container.addChild(section) } return container } |
This method uses a for
loop to repeat the SKShapeNode
creation four times, rotating 90° each time. Each of the different sections are added to a blank container node; that way you’ll be able to perform actions on the container and have it act on all the child nodes at the same time.
Now, clean up addCircleObstacle()
by using this new function. It should look like this:
func addCircleObstacle() { let path = UIBezierPath() path.move(to: CGPoint(x: 0, y: -200)) path.addLine(to: CGPoint(x: 0, y: -160)) path.addArc(withCenter: CGPoint.zero, radius: 160, startAngle: CGFloat(3.0 * M_PI_2), endAngle: CGFloat(0), clockwise: true) path.addLine(to: CGPoint(x: 200, y: 0)) path.addArc(withCenter: CGPoint.zero, radius: 200, startAngle: CGFloat(0.0), endAngle: CGFloat(3.0 * M_PI_2), clockwise: false) let obstacle = obstacleByDuplicatingPath(path, clockwise: true) obstacle.position = CGPoint(x: size.width/2, y: size.height/2) addChild(obstacle) } |
obstacleByDuplicatingPath(_:clockwise)
returns a container node containing the four sections of the obstacle. The obstacle is then positioned in the center of the screen and added to the scene.
Container nodes are a great way to organize related nodes so you can position, hide, or animate them as a single entity.
Build and run the project to see the completed circle.
SKAction
s are a very common and powerful way to move and animate nodes in SpriteKit. Add this to the end of addCircleObstacle()
let rotateAction = SKAction.rotate(byAngle: 2.0 * CGFloat(M_PI), duration: 8.0) obstacle.run(SKAction.repeatForever(rotateAction)) |
Make sure you use the rotate(byAngle:duration)
method, and not rotate(toAngle:duration)
. The first one rotates the node by the given angle, regardless of the current orientation. The second rotates until the angle matches the given angle, then stops. You are rotating the node by 2π, which is a full rotation. Repeating this action makes the node rotate continuously.
Shorter durations will make the circle rotate faster, but be careful as that will make the game more challenging.
Build and run the project to test out the rotation.
It’s not much of a game without a player, so that’s a good next step. Add this property to the top of GameScene.swift, just after colors
:
let player = SKShapeNode(circleOfRadius: 40) |
Next, add the following method after setupPlayerAndObstacles()
:
func addPlayer() { player.fillColor = .blue player.strokeColor = player.fillColor player.position = CGPoint(x: size.width/2, y: 200) addChild(player) } |
Finally, add a line to call the new method to the beginning of setupPlayerAndObstacles()
addPlayer() |
Build and run to see the new player node. Next, it’s time to make it move.
Player movement in this game comes in two parts. The player moves upward when the screen is tapped, and drops back down due to gravity between taps. You’ll add these parts now.
First, add this struct
to the top of GameScene.swift above the properties:
struct PhysicsCategory { static let Player: UInt32 = 1 static let Obstacle: UInt32 = 2 static let Edge: UInt32 = 4 } |
And add the following code to the end of didMove(to:)
:
let playerBody = SKPhysicsBody(circleOfRadius: 30) playerBody.mass = 1.5 playerBody.categoryBitMask = PhysicsCategory.Player playerBody.collisionBitMask = 4 player.physicsBody = playerBody let ledge = SKNode() ledge.position = CGPoint(x: size.width/2, y: 160) let ledgeBody = SKPhysicsBody(rectangleOf: CGSize(width: 200, height: 10)) ledgeBody.isDynamic = false ledgeBody.categoryBitMask = PhysicsCategory.Edge ledge.physicsBody = ledgeBody addChild(ledge) |
The categoryBitMask
values identify different types of objects, so that you can specify which objects interact with each other, and which do not. You define these categories, so using a struct with constant values helps keep things consistent and easier to maintain.
The collisionBitMask
determines which objects are solid to the player. The player node needs to collide with the ledge so that the player doesn’t drop off the bottom of the screen. You don’t want the player to collide with the obstacle, since it needs to be able to pass through unharmed if the colors match.
You set the isDynamic
property of the ledge to false
to make it a static object, that way it will not move due to gravity nor when the player collides with it.
Next, add this method to GameScene.swift:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { player.physicsBody?.velocity.dy = 800.0 } |
When the screen is tapped, you want the player to jump up, and then start to fall back down due to gravity. You achieve the jump by setting the dy
component of the player’s velocity.
Physics bodies already have gravity by default, but the current value is a bit weak for this object and gameplay. Make it stronger by adding this line at the bottom of didMove(to:)
:
physicsWorld.gravity.dy = -22 |
Some trial and error is required to get the motion to feel and work right for your games.
Build and run the project. Try moving the player node around the screen.
The player node happens to pass harmlessly through the obstacle node. You only want the player to pass through if its color matches the section of the obstacle it crosses. This is a different type of interaction than collisions, which SpriteKit calls contact.
To make this work, you first need to add physics bodies to the circle sections. Add the following to the for
loop section of obstacleByDuplicatingPath(_:clockwise)
, just before the container.addChild(section)
line:
let sectionBody = SKPhysicsBody(polygonFrom: path.cgPath) sectionBody.categoryBitMask = PhysicsCategory.Obstacle sectionBody.collisionBitMask = 0 sectionBody.contactTestBitMask = PhysicsCategory.Player sectionBody.affectedByGravity = false section.physicsBody = sectionBody |
This creates a physics body with the same shape as the section
node, reusing the same path. The categoryBitMask
defines this is as a different type of object from the other physics bodies in the game. The collisionBitMask
set to 0
means this body should not collide with any other bodies, since you want the player to be able to pass through.
Don’t forget to turn off gravity for these objects, or they will fall to the bottom of the screen.
The main new part here is the contactTestBitMask
, which says that you want to be notified when an object with category PhysicsCategory.Player
is in contact with this section of the obstacle. SpriteKit allows you to set a delegate that will receive these notifications.
First, add a method to call when the player makes contact with a section of the wrong color:
func dieAndRestart() { print("boom") player.physicsBody?.velocity.dy = 0 player.removeFromParent() // TODO: Remove obstacles setupPlayerAndObstacles() } |
This will simply reset the player’s velocity, remove it from the scene and recreate the game objects. You’ll add more later.
Next, add the following extension
to make GameScene
conform to the SKPhysicsContactDelegate
protocol, at the very bottom of GameScene.swift, outside of the closing brace for the class:
extension GameScene: SKPhysicsContactDelegate { func didBegin(_ contact: SKPhysicsContact) { if let nodeA = contact.bodyA.node as? SKShapeNode, let nodeB = contact.bodyB.node as? SKShapeNode { if nodeA.fillColor != nodeB.fillColor { dieAndRestart() } } } } |
Finally, add this line to the end of didMove(to:)
physicsWorld.contactDelegate = self |
Since you set GameScene
as the contactDelegate
of the physicsWorld
, didBegin(_:)
will be called whenever the player node is overlapping one of the obstacle sections. The if let
line checks to make sure both physics bodies have an attached SKShapeNode
. You then check the fillColor
of the two nodes. If the colors don’t match, you remove the player and reset it to the starting position.
Build and run and give it a shot. Can you get past the obstacle?
Note that when the game restarts, the circle obstacle does not get removed and a new one is stacked on top. You’ll fix this soon.
It’s time to add some more obstacles. There are a couple of changes needed to make this happen. Add two more properties to the top of GameScene.swift:
var obstacles: [SKNode] = [] let obstacleSpacing: CGFloat = 800 |
Note that the array doesn’t hold SKShapeNodes
; instead, it holds SKNodes
as these are the container nodes for the obstacles.
Next, find the line in addCircleObstacle()
that sets the position of the obstacle
node and replace it with:
obstacles.append(obstacle) obstacle.position = CGPoint(x: size.width/2, y: obstacleSpacing * CGFloat(obstacles.count)) |
Here you add the newly created obstacle node to the obstacles
tracking array, and use the count of obstacles to set the position of the container for the new obstacle.
The current obstacles need to be removed when the player dies, so replace the // TODO: Remove obstacles
line in dieAndRestart()
with:
for node in obstacles { node.removeFromParent() } obstacles.removeAll() |
These two parts may seem redundant, but clearing the obstacles array doesn’t stop displaying the existing obstacle nodes. Removing the nodes from their parent — the scene — clears them from the screen.
Finally, add this to the end of setupPlayerAndObstacles()
:
addObstacle() addObstacle() |
There should now be three calls to addObstacle()
. Build and run the project. Even though there are three obstacles, you can only see two of them on the screen. Adjusting the view as the player moves up the screen is your next task.
SpriteKit provides a built-in solution to deal with scrolling a scene that doesn’t fit on a single screen — an SKCameraNode
. By setting the position of the camera, the other nodes in the scene are moved automatically. Add a property to the top of GameScene.swift:
let cameraNode = SKCameraNode() |
Then, add this to the end of didMove(to:)
:
addChild(cameraNode) camera = cameraNode cameraNode.position = CGPoint(x: size.width/2, y: size.height/2) |
This creates an instance of SKCameraNode
, positions it in the center of the screen, and sets it as the camera
property for the scene. Now, changing the position of the camera will move everything else in the opposite direction. To actually adjust the camera position, you need to periodically check on the player’s position. Add the following method after touchesBegan(_:with)
:
override func update(_ currentTime: TimeInterval) { if player.position.y > obstacleSpacing * CGFloat(obstacles.count - 2) { print("score") // TODO: Update score addObstacle() } let playerPositionInCamera = cameraNode.convert(player.position, from: self) if playerPositionInCamera.y > 0 && !cameraNode.hasActions() { cameraNode.position.y = player.position.y } if playerPositionInCamera.y < -size.height/2 { dieAndRestart() } } |
SpriteKit calls update(_:)
each frame of the game, so it is a nice place to put code that needs to continually check for certain conditions. In this case, there are three things you are looking for.
First, if the player advances past an obstacle, you need to add a new obstacle to the scene. You check against obstacles.count - 2
, because you start with three obstacles: one just above the player, one at the top of the screen, and one more offscreen above that.
Each time the player passes an obstacle, a new one will be created, so there will always be three obstacles already added to the scene above the player. The extra obstacle offscreen is so that the new obstacle does not suddenly appear when the camera position changes.
The second condition is to see if the player node is in the top half of the screen. You convert the player position into the coordinates of the camera node, and the zero point of a node is in its center. When the player moves into the top half of the screen, you move the camera up.
Finally, the player dies if they drop the screen on the bottom. This can’t happen at the beginning because of the ledge you added, but can happen once you start scrolling up.
To reset the camera, add the following line to the end of dieAndRestart()
:
cameraNode.position = CGPoint(x: size.width/2, y: size.height/2) |
Build and run the project. You should have an endless series of circle obstacles to navigate.
It would be a more interesting game with more than one type of obstacle. There are many different things you can add, but a square is simple to build.
Add a new method to GameScene.swift below addCircleObstacle()
:
func addSquareObstacle() { let path = UIBezierPath(roundedRect: CGRect(x: -200, y: -200, width: 400, height: 40), cornerRadius: 20) let obstacle = obstacleByDuplicatingPath(path, clockwise: false) obstacles.append(obstacle) obstacle.position = CGPoint(x: size.width/2, y: obstacleSpacing * CGFloat(obstacles.count)) addChild(obstacle) let rotateAction = SKAction.rotate(byAngle: -2.0 * CGFloat(M_PI), duration: 7.0) obstacle.run(SKAction.repeatForever(rotateAction)) } |
Building the square obstacle is very similar to building the circle. You create a path for the bottom section of the square, then rotate it around to create the other three sections. Note that this shape copies around counterclockwise, and rotates at a different speed so that its behavior is different than the circle obstacle.
Next, replace addObstacle()
with the following:
func addObstacle() { let choice = Int(arc4random_uniform(2)) switch choice { case 0: addCircleObstacle() case 1: addSquareObstacle() default: print("something went wrong") } } |
Here, you generate a random integer that is either 0 or 1, and use that result to decide which obstacle to build.
Build and run the project. You may have to pass several obstacles, or die a few times, to see both types due to the nature of random numbers.
A game is much more fun when you can brag to your friends about beating their high scores. To add scores to your game, start by adding properties to the top of GameScene.swift:
let scoreLabel = SKLabelNode() var score = 0 |
Next, set up the label at the end of didMove(to:)
:
scoreLabel.position = CGPoint(x: -350, y: -900) scoreLabel.fontColor = .white scoreLabel.fontSize = 150 scoreLabel.text = String(score) cameraNode.addChild(scoreLabel) |
Note that you add the label to cameraNode
, not the scene itself, so that the label doesn’t scroll off the screen when the player moves up.
Then, replace the // TODO: Update score
line in update(_:)
with:
score += 1 scoreLabel.text = String(score) |
Finally, add the following to the end of dieAndRestart()
:
score = 0 scoreLabel.text = String(score) |
With these pieces of code, you update the score each time the player passes an obstacle, and reset it to 0 when the player dies.
Build and run to see the new score counter.
Sometimes simple mechanics can make for fun little games. You can download the completed project here.
Now that you’ve got a complete game, you may want to spruce it up a bit. One thing you could add is more randomness. For example, the player could start as a random color, and the obstacles could rotate in a random direction, at random speeds.
There are also many different obstacles you could add, such as circles inside circles or plus signs. Be creative — and if you create a new obstacle, post a screenshot in the forum. I’d love to see it!
For more on SpriteKit, check out the 2D Apple Games by Tutorials book or some of our other Apple Game Frameworks tutorials. We hope you enjoyed this tutorial!
The post How To Make A Game Like Color Switch with SpriteKit and Swift appeared first on Ray Wenderlich.
In this episode, you'll learn how properties can save you times from writing getters and setters.
The post Screencast: Beginning C# Part 20: Properties appeared first on Ray Wenderlich.
As you may or may not know, we have a tradition where each year, we sing a silly Christmas song about a geeky topic.
This year, we have made a song titled “Here Come Tutorials”, sung to the tune of “Here Comes Santa Claus.” We hope you enjoy – and we apologize in advance for our singing! :]
Here come tutorials, here come tutorials, (Namrata Bandekar)
Right down tutorial lane (Luke Parham)
We’re gonna take you step-by-step through (Eric Cerney)
Coding apps and games! (Vicki Wenderlich, Ray Wenderlich)
Code we’re slinging, Keyboards singing, (Ryan Poolos)
Getting everything right, (Kelvin Lau)
We can’t wait to learn with you (Ben Morrow, Forrest Folsom)
Cause tutorials come tonight. (Joshua Green, Marilyn Greene)
Here come tutorials, here come tutorials, (Rich Turton)
Right down tutorial lane (Rich Turton)
Santa’s got a bag with MacBook Pros (Vicki Wenderlich, Ray Wenderlich)
We know you won’t complain (Eric Cerney)
Hear those dongles jingle jangle (Vicki Wenderlich, Ray Wenderlich)
What a courageous sight (Aaron Douglas, Burkley)
Plug your headphones in and lift up your chin, (Ben Morrow, Forrest Folsom)
Cause tutorials come tonight. (Ben Morrow, Forrest Folsom)
Here come tutorials, here come tutorials, (Ryan Poolos)
Right down tutorial lane (Aaron Douglas, Burkley)
You may think this song is cheesy (Rich Turton)
Sorry for the pain! (Vicki Wenderlich, Ray Wenderlich)
Thanks for reading tutorials (Kelvin Lau)
At our community site (Namrata Bandekar)
Back to writing – it’s exciting (Luke Parham)
At raywenderlich.com tonight. (Eric Cerney)
At raywenderlich.com… (Chris Belanger)
tonight! (Rich Turton)
Special thanks to Ellen Shapiro for the guitar music, Andy Obusek for the bass and percussion, and Chris Belanger for the piano music.
If you enjoyed this video, we have a few more videos you might want to check out:
Have a Merry Christmas and very happy New Year everyone, and thanks so much for reading this site! :]
The post Merry Christmas 2016! appeared first on Ray Wenderlich.
Asynchronous programming can be a real pain in the lemon. Unless you’re extremely careful, it can easily result in humongous delegates, messy completion handlers and long nights debugging code! Lucky for you, there’s a better way: promises. Promises tame asynchronicity by letting you write code as a series of actions based on events. This works especially well for actions that must occur in a certain order. In this PromiseKit tutorial, you’ll learn how to use the third party PromiseKit to clean up your asynchronous code, and your sanity.
Typically, iOS programming involves many delegates and callbacks. You’ve likely seen a lot of code like this:
- Y manages X. - Tell Y to get X. - Y notifies its delegate when X is available. |
Promises attempt to simplify this mess to look more like this:
When X is available, do Y. |
Doesn’t that look delightful? Promises also let you separate error handling and success code, which makes it easier to write clean code that handles many different conditions. They work great for complicated, multistep workflows like logging into web services, making authenticated SDK calls, processing and displaying images and more!
Promises are becoming more common and there are multiple solutions, but in this tutorial, you’ll learn about promises using a popular, third-party Swift library called PromiseKit.
The project for this tutorial, WeatherOrNot, is a simple current weather application. It uses OpenWeatherMap for its weather API. The patterns and concepts for accessing this API are translatable to any other web service.
Download the starter project here. PromiseKit is distributed via CocoaPods, but the download already includes the pod. If you haven’t used CocoaPods before and would like to learn about it, you can read our tutorial on it. Other than noting PromiseKit has been installed via CocoaPods, however, this tutorial doesn’t require any other knowledge about CocoaPods.
Open PromiseKitTutorial.xcworkspace, and you’ll see the project is very simple. It only has five .swift
files:
Speaking of weather data, WeatherOrNot uses OpenWeatherMap to source weather information. Like most third party APIs, this requires a developer API key to access the service. Don’t worry, there is a free tier that is more than generous enough to complete this tutorial.
You’ll need to get an API key to run the app. Get one at http://openweathermap.org/appid. Once you complete the registration, you can find your API key at https://home.openweathermap.org/api_keys.
Copy that key and paste it into the appID
constant at the top of WeatherHelper.swift.
Build and run the app. If all has gone well, you should see the current weather in Athens.
Well, maybe… the app actually has a bug (you’ll fix it soon!), so the UI may be a bit slow to show.
You already know what a “promise” is in everyday life. For example, you can promise yourself a cold drink when you complete this tutorial. This statement contains an action (“have a cold drink”) to take place in the future when an action is complete (“you finish this tutorial”). Programming promises are similar in that there is an expectation something will be done in the future when some data is delivered.
Promises are about managing asynchronicity. Unlike traditional methods, such as callbacks via completions or selectors, promises can be easily chained together, so a sequence of asynchronous actions can be expressed. Promises are also like operations in that they have an execution lifecycle and can be cancelled.
A PromiseKit Promise
executes a code block that is fulfilled with a value. Upon fulfillment, its then
block is executed. If that block returns a promise, then that will be executed, fulfilled with a value and so on. If there is an error along the way, an optional catch
block will be executed instead.
For example, the colloquial promise above rephrased as a PromiseKit Promise
looks like:
doThisTutorial().then { haveAColdOne() }.catch { postToForum(error) } |
PromiseKit is a swift implementation of promises. While it’s not the only one, it’s one of the most popular. In addition to providing block-based structures for constructing promises, PromiseKit also includes wrappers for many of the common iOS SDK classes and easy error handling.
To see a promise in action, take a look at the function in BrokenPromise.swift()
:
func BrokenPromise<T>(method: String = #function) -> Promise<T> { return Promise<T>() { fulfill, reject in let err = NSError(domain: "PromiseKitTutorial", code: 0, userInfo: [NSLocalizedDescriptionKey: "'\(method)' has not been implemented yet."]) reject(err) } } |
This returns a new generic Promise
, which is the primary class provided by PromiseKit. Its constructor takes a simple execution block with two parameters:
fulfill
: A function to call when the desired value is ready to fulfill the promisereject
: A function to call if there is an errorFor BrokenPromise
, the code always returns an error. This helper object is used to indicate that there is still work to do as you flesh out the app.
Accessing a remote server is one of the most common asynchronous tasks, and a straightforward network call is a good place to start.
Take a look at getWeatherTheOldFashionedWay(latitude:longitude:completion:)
in WeatherHelper.swift. This method fetches weather data given a latitude, longitude and completion closure.
However, the completion closure is called on either success or failure. This results in a complicated closure since you’ll need code for both error handling and success within it.
Most egregiously, the data task completion is handled on a background thread, so this results in (accidentally :cough:) updating the UI in the background! :[
Can promises help here? Of course!
Add the following right after getWeatherTheOldFashionedWay(latitude:longitude:completion:)
:
func getWeather(latitude: Double, longitude: Double) -> Promise<Weather> { return Promise { fulfill, reject in let urlString = "http://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)" + "&appid=\(appID)" let url = URL(string: urlString)! let request = URLRequest(url: url) let session = URLSession.shared let dataTask = session.dataTask(with: request) { data, response, error in if let data = data, let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any], let result = Weather(jsonDictionary: json) { fulfill(result) } else if let error = error { reject(error) } else { let error = NSError(domain: "PromiseKitTutorial", code: 0, userInfo: [NSLocalizedDescriptionKey: "Unknown error"]) reject(error) } } dataTask.resume() } } |
This method also uses URLSession
like getWeatherTheOldFashionedWay
does, but instead of taking a completion closure, the networking is wrapped in a Promise
.
In the dataTask
‘s completion handler, if the data is successfully returned, a Weather
object is created from the deserialized JSON. Here the fulfill
function is called with that object, completing the promise.
If there is an error with the network request, that error object is passed to reject
.
Else, if there’s neither JSON data nor an error, then a stub error is passed to reject
as a promise rejection requires an error object.
Next, in WeatherViewController.swift replace handleLocation(city:state:latitude:longitude:)
with the following:
func handleLocation(city: String?, state: String?, latitude: CLLocationDegrees, longitude: CLLocationDegrees) { if let city = city, let state = state { self.placeLabel.text = "\(city), \(state)" } weatherAPI.getWeather(latitude: latitude, longitude: longitude).then { weather -> Void in self.updateUIWithWeather(weather: weather) }.catch { error in self.tempLabel.text = "--" self.conditionLabel.text = error.localizedDescription self.conditionLabel.textColor = errorColor } } |
Nice, using a promise is as simple as supplying then
and catch
blocks!
This new implementation of handleLocation
is superior to the previous one. First, completion handling is now broken into two easy-to-read closures: then
for success and catch
for errors. Second, by default PromiseKit executes these closures on the main thread, so there’s no change of accidentally updating the UI on a background thread.
This is pretty good but PromiseKit can do better. In addition to the code for Promise
, PromiseKit also includes extensions for common iOS SDK methods that can be expressed as promises. For example, the URLSession
data task method returns a promise instead of using a completion block.
Replace the new getWeather(latitude:longitude:)
with the following code:
func getWeather(latitude: Double, longitude: Double) -> Promise<Weather> { return Promise { fulfill, reject in let urlString = "http://api.openweathermap.org/data/2.5/weather?lat=" + "\(latitude)&lon=\(longitude)&appid=\(appID)" let url = URL(string: urlString)! let request = URLRequest(url: url) let session = URLSession.shared // 1 let dataPromise: URLDataPromise = session.dataTask(with: request) // 2 _ = dataPromise.asDictionary().then { dictionary -> Void in // 3 guard let result = Weather(jsonDictionary: dictionary as! [String : Any]) else { let error = NSError(domain: "PromiseKitTutorial", code: 0, userInfo: [NSLocalizedDescriptionKey: "Unknown error"]) reject(error) return } fulfill(result) // 4 }.catch(execute: reject) } } |
See how easy it is to use PromiseKit wrappers? Here’s the breakdown:
URLSession.dataTask(with:)
that returns a URLDataPromise
, which is just a specialized Promise
. Note the data promise automatically starts its underlying data task.asDictionary()
, which handles deserializing the JSON for you, significantly reducing the amount of code!result
. You do this using guard let
to ensure a Weather
object can be created from the dictionary. If not, you create an error and call reject
, similar to before. Otherwise, you call fulfill
with the result
. catch
block forwards any errors on through the fail
closure.In this function, two promises are chained together. The first is the data promise, which returns a Data
from the URL request. The second is asDictionary()
, which takes the data and turns it into a dictionary.
Now that the networking is bullet-proofed, take a look at the location functionality. Unless you’re lucky enough to be visiting Athens, the app isn’t giving you particularly relevant data. Change that to use the device’s current location.
In WeatherViewController.swift replace updateWithCurrentLocation()
with the following:
private func updateWithCurrentLocation() { // 1 _ = locationHelper.getLocation().then { placemark in self.handleLocation(placemark: placemark) }.catch { error in self.tempLabel.text = "--" self.placeLabel.text = "--" switch error { // 2 case is CLError where (error as! CLError).code == CLError.Code.denied: self.conditionLabel.text = "Enable Location Permissions in Settings" self.conditionLabel.textColor = UIColor.white default: self.conditionLabel.text = error.localizedDescription self.conditionLabel.textColor = errorColor } } } |
getLocation()
is a promise to get a placemark for the current location.switch
is able to provide a different message when the user hasn’t granted location privileges versus other types of errors.Next, in LocationHelper.swift replace getLocation()
with this:
func getLocation() -> Promise<CLPlacemark> { // 1 return CLLocationManager.promise().then { location in // 2 return self.coder.reverseGeocode(location: location) } } |
This takes advantage of two PromiseKit concepts already discussed: SDK wrapping and chaining.
CLLocationManager.promise()
returns a promise of the current location.CLGeocoder.reverseGeocode(location:)
, which also returns a promise to provide the reverse-coded location.With promises, two different asynchronous actions are linked in three lines of code! No explicit error handling is required because that all gets taken care of in the catch
block of the caller.
Build and run. After accepting the location permissions, the current temperature for your (simulated) location is shown. Voilà!
That’s all well and good, but what if a user wants to know the temperature somewhere else?
In WeatherViewController.swift replace textFieldShouldReturn(_:)
with the following (ignore the compiler error for now about the missing method):
func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() guard let text = textField.text else { return true } _ = locationHelper.searchForPlacemark(text: text).then { placemark -> Void in self.handleLocation(placemark: placemark) } return true } |
This uses the same pattern as all the other promises: go off and find the placemark and when that’s done, update the UI.
Next, add the following to LocationHelper.swift:
func searchForPlacemark(text: String) -> Promise<CLPlacemark> { return CLGeocoder().geocode(text) } |
It’s that simple! PromiseKit already has an extension for CLGeocoder
to find a placemark that returns a promise with a placemark.
Build and run. This time, enter a city name in the search field at the top and tap Return. This should then go and find the weather for the best match for that name.
One thing so far has been taken for granted is that all the then
blocks have been executed on the main thread. This is a great feature since most of the work done in the view controller has been to update the UI. Sometimes, however, long-running tasks are best handled on a background thread, so as not to tie up the app.
You’ll next add a weather icon from OpenWeatherMap to illustrate the current weather conditions.
Add the following method to WeatherHelper
right after getWeather(latitude:longitude:)
:
func getIcon(named iconName: String) -> Promise<UIImage> { return Promise { fulfill, fail in let urlString = "http://openweathermap.org/img/w/\(iconName).png" let url = URL(string: urlString)! let request = URLRequest(url: url) let session = URLSession.shared let dataPromise: URLDataPromise = session.dataTask(with: request) let backgroundQ = DispatchQueue.global(qos: .background) _ = dataPromise.then(on: backgroundQ) { data -> Void in let image = UIImage(data: data)! fulfill(image) }.catch(execute: fail) } } |
Here, building a UIImage
from the loaded Data
is handled on a background queue by supplying an optional on
parameter to then(on:execute:)
. PromiseKit then handles the heavy lifting of performing all the necessary dispatches.
Now the promise is fulfilled on the background queue, so the caller will need to make sure the UI is updated on the main queue.
Back in WeatherViewController.swift, replace the call to getWeather(latitude:longitude:)
inside handleLocation(city:state:latitude:longitude:)
with this:
// 1 weatherAPI.getWeather(latitude: latitude, longitude: longitude).then { weather -> Promise<UIImage> in self.updateUIWithWeather(weather: weather) // 2 return self.weatherAPI.getIcon(named: weather.iconName) // 3 }.then(on: DispatchQueue.main) { icon -> Void in self.iconImageView.image = icon }.catch { error in self.tempLabel.text = "--" self.conditionLabel.text = error.localizedDescription self.conditionLabel.textColor = errorColor } |
There are three subtle changes to this call:
getWeather(latitude:longitude:)
then
block is changed to return a Promise
instead of Void
. This means that when the getWeather
promise is complete, there will be a new promise returned.getIcon
method creates a new promise to… get the icon.then
block is added to the chain, which will be executed on the main queue when the getIcon
promise is fulfilled.Thereby, promises can be chained into a sequence of serially executing steps. After one promise fulfills, the next will be executed, and so on until the final then
or an error occurs and the catch
is invoked. The two big advantages of this approach over nested completions are:
then
block has its own context, keeping logic and state from bleeding into each other. A column of blocks is easier to read without an ever-deepening indent.Build and run, and image icons should now load!
What about using existing code, SDKs, or third party libraries that don’t have PromiseKit support built in? Well, for that PromiseKit comes with a promise wrapper.
Take, for instance, this application. Since there is a limited number of weather conditions, it’s not necessary to fetch the condition icon from the web every time; it’s inefficient and potentially costly.
In WeatherHelper.swift there are already helper functions for saving and loading an image file from a local caches directory. These functions perform the file I/O on a background thread, and use an asynchronous completion block when the operation is finished. This is a common pattern, so PromiseKit has a way of handling it.
Replace getIcon(named:)
from WeatherHelper
with the following (again, ignore the compiler error about the missing method for now):
func getIcon(named iconName: String) -> Promise<UIImage> { // 1 return wrap { // 2 getFile(named: iconName, completion: $0) } .then { image in if image == nil { // 3 return self.getIconFromNetwork(named: iconName) } else { // 4 return Promise(value: image!) } } } |
Here’s how it’s used:
wrap(body:)
takes any function that follows one of several completion handler idioms and wraps it in a promise.getFile(named: completion:)
has a completion parameter of @escaping (UIImage?) -> Void
, which becomes a Promise
. It’s in wrap
‘s body
block where the original function is called, passing in the completion parameter.This is a new way of using promises. If a promise created with a value is already fulfilled, it’s then
block will be immediately called. In such, if the image is already loaded and ready to go, it can be returned right away. This pattern is how you can create a promise that can either do something asynchronously (like load from the network) or synchronously (like use an in-memory value). This is useful when you have locally cached value, such as an image here.
To make this work, you’ll have to add the images to the cache when they come in. Add the following right below the previous method:
func getIconFromNetwork(named iconName: String) -> Promise<UIImage> { let urlString = "http://openweathermap.org/img/w/\(iconName).png" let url = URL(string: urlString)! let request = URLRequest(url: url) let session = URLSession.shared let dataPromise: URLDataPromise = session.dataTask(with: request) return dataPromise.then(on: DispatchQueue.global(qos: .background)) { data -> Promise<UIImage> in return firstly { Void in return wrap { self.saveFile(named: iconName, data: data, completion: $0)} }.then { Void -> Promise<UIImage> in let image = UIImage(data: data)! return Promise(value: image) } } } |
This is similar to the previous getIcon(named:)
except that in the dataPromise
‘s then
block, there is a call to saveFile
that is wrapped just like the use ofgetFile
.
This uses a new construct, firstly
. firstly
is functional sugar that simply executes its promise. It’s not really doing anything other than adding a layer of indirection for readability. Since the call to saveFile
is a just a side effect of loading the icon, using firstly
here enforces a little bit of ordering so that we can be confident that this promise.
All in all, here’s what happens the first time you request an icon:
If you build and run now, you shouldn’t see any difference in app functionality, but you can check the filesystem to see that the images are saved. To do that, search the console output for the term Saved image to:
. This will show the URL of the new file, which you can use to find its location on disk.
Looking at the PromiseKit syntax, you might have asked: if there is a then
and a catch
, is there a way to share code and make sure an action is always taken (like a cleanup task), regardless of success or failure? Well there is: it’s called always
.
In WeatherViewController.swift update handleLocation(city:state:latitude:longitude:)
to show a network activity indicator in the status bar while the weather is being loaded from the server.
Insert the following line before the call to weatherAPI.getWeather...
:
UIApplication.shared.isNetworkActivityIndicatorVisible = true |
Then, to the end of the catch
block add the following:
.always { UIApplication.shared.isNetworkActivityIndicatorVisible = false } |
Then, you might need to assign the whole expression to _
in order to silence an unused result warning.
This is the canonical example of when to use always
. Regardless if the weather is completely loaded or if there is an error, the network activity will be complete, so the activity indicator should always be dismissed. Similarly, this can be used to close sockets, database connections, or disconnect from hardware services.
One special case is a promise that fulfills, not when some data is ready, but after a certain time interval. Currently, after the weather is loaded, it is never refreshed. Change that to update the weather hourly.
In updateWithCurrentLocation()
, add the following code to the end of the method:
_ = after(interval: oneHour).then { self.updateWithCurrentLocation() } |
.after(interval:)
creates a promise that is fulfilled after the specified interval passes. Unfortunately, this is a one-shot timer. To do the update every hour, it was made recursive onupdateWithCurrentLocation()
.
So far, all the promises discussed have either been standalone or chained together in a sequence. PromiseKit also provides functionality for wrangling multiple promises fulfilling in parallel. There are three functions for waiting for multiple promises. The first, race
returns a promise that is fulfilled when the first of a group of promises is fulfilled. In essence, the first one completed is the winner.
The other two functions are when
and join
. Those fulfill after all the specified promises are fulfilled. Where these two differ is in the rejected case. join
always waits for all the promises to complete before rejected if one of them rejects, but when(fulfilled:)
rejects as soon as any one of the promises rejects. There’s also a when(resolved:)
that waits for all the promises to complete, but always calls the then
block and never the catch
.
Note: For all of these grouping functions, all the individual promises will continue until they fulfill or reject, regardless of the behavior of the combining function. For example, if three promises are used in a race
, the race
‘s then
block will be called after the first promise to complete. However, the other two unfilled promises keep executing until they too resolve.
Take the contrived example of showing the weather in a “random” city. Since the user doesn’t care what city it will show, the app can try to fetch weather for multiple cities, but just handle the first one to complete. This gives the illusion of randomness.
Replace showRandomWeather(_:)
with the following:
@IBAction func showRandomWeather(_ sender: AnyObject) { let weatherPromises = randomCities.map { weatherAPI.getWeather(latitude: $0.2, longitude: $0.3) } _ = race(promises: weatherPromises).then { weather -> Void in self.placeLabel.text = weather.name self.updateUIWithWeather(weather: weather) self.iconImageView.image = nil } } |
Here you create a bunch of promises to fetch the weather for a selection of cities. These are then raced against each other with race(promises:)
. The then
block will only be called when the first of those promises are fulfilled. In theory, this should be a random choice due to variation in server conditions, but it’s not a strong example. Also note that all of the promises will still resolve, so there are still five network calls, even though only one is cared about.
Build and run. Once the app is loaded, tap Random Weather.
Updating the condition icon and error handling is left as an exercise for the reader. ;]
You can download the fully finished sample project here.
You can read the documentation for PromiseKit at http://promisekit.org/, although it is hardly comprehensive. The FAQ http://promisekit.org/faq/ is useful for debugging information.
You may also want to read up on CocoaPods in order to install PromiseKit into your own apps and to keep up to date with their changes, as it is an active pod.
Finally, there are other Swift implementations of Promises. One popular alternative is BrightFutures.
If you have any comments, questions or suggestions for alternatives, promise to tell us below! :]
The post Getting Started with PromiseKit appeared first on Ray Wenderlich.
The Unity Team at raywenderlich.com has grown a lot in the past year. At this point, we’ve released over 20 free Unity tutorials, and even a brand new Unity book.
Now that the team has grown, we want to make sure we keep our style consistent, following the modern C# and Unity conventions developers are acquainted to.
So today, the team and I are proud to announce the official raywenderlich.com C# style guide!
Let’s take a look at what’s inside.
At this point, the guide is fairly basic. It contains guidance on the following items:
default
statement, this only clutters up the code and should be removed.To learn more, check out the full C# style guide.
We know everyone has a different opinion on a certain style of code and we’d love to hear yours!
If you have any suggestions on how to improve this code even further, post issues on the GitHub page or file pull requests – I’ll be watching for them, and if you can convince me (and the rest of the team), we’ll change the guide.
We hope you like the new and improved C# style guide – you can look forward to some awesome Unity tutorials coming up that will be using it! :]
The post Introducing the raywenderlich.com C# Style Guide appeared first on Ray Wenderlich.
Update note: This Collection Views in macOS tutorial has been updated to Xcode 8 and Swift 3 by Gabriel Miro.
A collection view is a powerful mechanism for laying out an ordered set of data items in a visual way. Finder and Photos both do this: they let you tab through files in a gallery layout.
Introduced in OS X 10.5, NSCollectionView
offered a handy means of arranging a group of objects in a grid of identically-sized items displayed in a scrollable view.
OS X 10.11 El Capitan gave NSCollectionView
a major overhaul inspired by UICollectionView
from iOS.
macOS 10.12 added two additional features to close the gap with iOS: collapsible sections (like in Finder) and sticky headers.
In this collection views in macOS tutorial, you’ll build SlidesMagic — your own grid-based image browsing app.
This tutorial assumes that you know the basics of writing macOS apps. If you’re new to macOS, you should take a look at the macOS tutorials available here, and then come back to learn about collection views.
The SlidesMagic app you’re going to build is a simple image browser. It’s pretty cool, but don’t get all excited and delete Photos from your Mac just yet. :]
It retrieves all image files from a folder on the file system and displays them with their names in an elegant collection view. The finished app will look like this:
Download the starter project here. Build and run:
At this point, it appears to be an empty window, but it has hidden features that will become the foundation of an image browser.
When SlidesMagic launches, it automatically loads all the images from the system’s Desktop Pictures folder. From Xcode‘s console log, you can see the file names.
That list in the console is an indicator that the model-loading logic is in place. You can choose another folder by selecting File/Open Another Folder… menu.
The starter project provides functionality that is not directly related to collection views, but is specific to SlidesMagic.
The application has two main controllers:
windowDidLoad()
: Sets the initial size of the window on the left half of the screen. openAnotherFolder(_:)
presents a standard open dialog to choose a different folder.viewDidLoad()
opens the Desktop Pictures folder as the initial folder to browse.NSCollectionView
is the main view; it displays visual items and is assisted by several key components.
NSCollectionViewLayout
: Specifies the layout of the collection view. It’s an abstract class from which all concrete layout classes inherit.
NSCollectionViewFlowLayout
: Provides a flexible grid-like layout. For most apps, this layout can be used to achieve your desired results.
NSCollectionViewGridLayout
: A pre-OS X 10.11 compatibility class, and not recommended for new apps.
Sections and IndexPath
: Allows for grouping of items into sections. The items form an ordered list of sections, each containing an ordered list of items. Each item is associated with an index that comprises of a pair of integers (section, item) encapsulated in an IndexPath
instance. When grouping of items into sections isn’t required, you still have at least one section (by default).
Like many other Cocoa frameworks, items in the collection view follow the MVC design pattern.
The Model and the View: The items’ content comes from your model’s data objects. Each individual object that becomes visible gets its own view in the larger collection view. The structure of these individual views are defined in a separate nib with file extension .xib.
The Controller: The nib mentioned above is owned by an instance of NSCollectionViewItem, which is a descendant of NSViewController
. It mediates the flow of information between the items’ views and model objects. Generally, you subclass NSCollectionViewItem
. When items are not of the same kind, you define a different subclass and nib for each variant.
To display extra information in the collection view that’s not part of an individual item, you’d use supplementary views. Some common implementations of these are section headers and footers.
NSCollectionViewDataSource
: Populates the collection view with items and supplementary views.NSCollectionViewDelegate
: Handles events related to drag-and-drop, selection and highlighting.NSCollectionViewDelegateFlowLayout
: Lets you customize a flow layout. Open Main.storyboard. Go to the Object Library, and drag a Collection View into the view of the View Controller Scene.
Resize the Bordered Scroll View so it takes up the entire area of the parent view. Then, select Editor/Resolve Auto Layout Issues/Add Missing Constraints to add the Auto Layout constraints.
You need to add an outlet in ViewController to access the collection view. Open ViewController.swift and add the following inside the ViewController
class definition:
@IBOutlet weak var collectionView: NSCollectionView! |
Open Main.storyboard, and select the View Controller inside the View Controller Scene.
Open the Connections Inspector and find the collectionView element within the Outlets section. Connect it to the collection view by dragging from the button next to it to the collection view control in the canvas.
You’ve got options here: you can set the initial layout and some of its attributes in Interface Builder, or you can set them programmatically.
For SlidesMagic, you’ll take the programmatic approach.
Open ViewController.swift and add the following method to ViewController
:
fileprivate func configureCollectionView() { // 1 let flowLayout = NSCollectionViewFlowLayout() flowLayout.itemSize = NSSize(width: 160.0, height: 140.0) flowLayout.sectionInset = EdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0) flowLayout.minimumInteritemSpacing = 20.0 flowLayout.minimumLineSpacing = 20.0 collectionView.collectionViewLayout = flowLayout // 2 view.wantsLayer = true // 3 collectionView.layer?.backgroundColor = NSColor.black.cgColor } |
Here’s what you’re doing in this method:
NSCollectionViewFlowLayout
and setting its attributes and the collectionViewLayout
property of the NSCollectionView
.NSCollectionView
is designed to be layer-backed. So, you’re setting an ancestor’s wantsLayer
property to true
.You need to call this method when the view is created, so add this to the end of viewDidLoad()
:
configureCollectionView() |
Build and run:
At this point, you have a black background and a layout.
Now you need to create an NSCollectionViewItem
subclass to display your data elements.
Go to File/New/File…, select macOS/Source/Cocoa Class and click Next.
Set the Class field to CollectionViewItem, the Subclass of field to NSCollectionViewItem
, and check Also create XIB for user interface.
Click Next, and in the save dialog, select Controllers from Group and click Create.
Open CollectionViewItem.swift and replace the entire class with this:
import Cocoa class CollectionViewItem: NSCollectionViewItem { // 1 var imageFile: ImageFile? { didSet { guard isViewLoaded else { return } if let imageFile = imageFile { imageView?.image = imageFile.thumbnail textField?.stringValue = imageFile.fileName } else { imageView?.image = nil textField?.stringValue = "" } } } // 2 override func viewDidLoad() { super.viewDidLoad() view.wantsLayer = true view.layer?.backgroundColor = NSColor.lightGray.cgColor } } |
In here, you do the following:
imageFile
property that holds the model object to be presented in this item. When set, its didSet
property observer sets the content of the item’s image and label.When you created CollectionViewItem.swift you selected “Also create a XIB” which produced the CollectionViewItem.xib nib file. For sake of order, drag the nib to the Resources group just below Main.storyboard.
The View in the nib is the root view for a subtree of controls to be displayed in each item. You’re going to add an image view for the slide and a label for the file name.
Open CollectionViewItem.xib.
Add an NSImageView
:
Add a label:
Select the Label, and in the Attributes Inspector set the following attributes:
Though the File’s Owner in the nib is of the type CollectionViewItem
, it is simply a placeholder. When the nib is instantiated, it must contain a “real” single top-level instance of NSCollectionViewItem
.
Drag a Collection View Item from the Object Library and drop it into Document Outline. Select it, and in the Identity Inspector, set its Class to CollectionViewItem
.
In the CollectionViewItem.xib, you need to connect the view hierarchy to the outlets of CollectionViewItem. In the xib:
view
outlet to the View in the Document OutlineimageView
and textField
outlets to Image View and Label in the Document OutlineYou need to implement the data source protocol so the view knows the answers to these questions:
Open ViewController.swift and add the following extension at the end of the file:
extension ViewController : NSCollectionViewDataSource { // 1 func numberOfSections(in collectionView: NSCollectionView) -> Int { return imageDirectoryLoader.numberOfSections } // 2 func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { return imageDirectoryLoader.numberOfItemsInSection(section) } // 3 func collectionView(_ itemForRepresentedObjectAtcollectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { // 4 let item = collectionView.makeItem(withIdentifier: "CollectionViewItem", for: indexPath) guard let collectionViewItem = item as? CollectionViewItem else {return item} // 5 let imageFile = imageDirectoryLoader.imageFileForIndexPath(indexPath) collectionViewItem.imageFile = imageFile return item } } |
NSCollectionViewDataSource
. Here you return the number of items in the specified section.indexPath
.identifier
parameter. It attempts to reuse an unused item of the requested type, and if nothing is available it creates a new one.IndexPath
and sets the content of the image and the label.Your next step is to define the data source.
Open Main.storyboard and select the collection view.
Open the Connections Inspector and locate dataSource in the Outlets section. Drag from the adjacent button to the View Controller in Document Outline View.
Build and run, and your collection view should display images from the Desktop Pictures folder:
Voilà! It was worth all that work!
If you don’t see images, then you probably just missed something small.
dataSource
outlet?
NSCollectionViewItem
object and change its type to CollectionViewItem
?
identifier
parameter in makeItemWithIdentifier
identical to the nib name?
To display images from a folder other than the system’s Desktop Pictures folder, select File/Open Another Folder… and choose a folder that has image formats such as jpg or png.
But nothing seems to change in the window — it still displays the images from the Desktop Pictures folder. Although when you look at the console log, you can see the file names are from the new folder.
To refresh the collection view’s visible items, you need to call its reloadData()
method.
Open ViewController.swift and add this code to the end of loadDataForNewFolderWithUrl(_:)
:
collectionView.reloadData() |
Build and run. You’ll now have the correct images displayed in the window.
SlidesMagic is doing some serious magic now. But you’re going to improve it by adding sections.
First, you need to add a check box to the bottom of the view so you can toggle between single and multi-section.
Open Main.storyboard, and in the Document Outline view, select the scroll view’s bottom constraint. Open the Size Inspector and change its Constant to 30.
This moves the collection view up to make room for the check box.
Now, drag a Check Box Button from the Object Library into the space below the collection view. Select it, and in the Attributes Inspector, set its Title to Show Sections, and its State to Off.
Then, set its Auto Layout constraints by selecting the pin button and set the top constraint to 8 and the leading constraint to 20. Choose Update Frames: Items of New Constraints and click Add 2 Constraints.
Build and run. It should look like this at the bottom:
When you click the box, the application needs to change the collection view’s appearance.
Open ViewController.swift and add the following method at the end of the ViewController
class:
@IBAction func showHideSections(sender: NSButton) { let show = sender.state // 1 imageDirectoryLoader.singleSectionMode = (show == NSOffState) // 2 imageDirectoryLoader.setupDataForUrls(nil) // 3 collectionView.reloadData() } |
Here’s what you’re doing:
nil
value passed means you skip image loading — same images, different layout.If you’re curious how images are distributed across sections, look up sectionLengthArray
in ImageDirectoryLoader
. The number of elements in this array sets the max number of sections, and the element values set the number of items in each section.
Now, open Main.storyboard. In the Document Outline, Control-drag from the Show Sections control over the View Controller. In the black pop-up window click showHideSections: to connect it. You can check if the connection was set properly in the Connections Inspector.
Build and run; check Show Sections and watch the layout change.
To get better visual separation between sections, open ViewController.swift and modify the layout’s sectionInset
property in the configureCollectionView()
method.
Replace:
flowLayout.sectionInset = EdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0) |
With this:
flowLayout.sectionInset = EdgeInsets(top: 30.0, left: 20.0, bottom: 30.0, right: 20.0) |
Build and run; check Show Sections, and note the additional spacing between sections.
Another way to see section boundaries is to add a header or footer view. To do this, you need a custom NSView
class and will need to implement a data source method to provide the header views to the collection view.
To create the header view, select File/New/File…. Select macOS/User Interface/View and click Next.
Enter HeaderView.xib as the file name and for Group select Resources.
Click Create.
Open HeaderView.xib and select the Custom View. Open the Size Inspector and change Width to 500 and Height to 40.
Drag a label from the Object Library to the left-hand side of Custom View. Open the Attributes Inspector and change Title to Section Number and Font Size to 16.
Drag a second label to the right-hand side of Custom View and change Title to Images Count and Alignment to Right.
Set the Section Number labels Auto Layout constraints by selecting the pin button and set the top constraint to 12 and the leading constraint to 20. Choose Update Frames: Items of New Constraints and click Add 2 Constraints.
Next, set the Images Count labels top constraint to 11 and the trailing constraint to 20. Be sure to choose Update Frames: Items of New Constraints and click Add 2 Constraints.
The header view should look like this:
With the interface ready for show time, the next task is to create a custom view subclass for the header view.
Select File/New/File… to create a new file.
Choose macOS/Source/Cocoa Class, name the class HeaderView
, and then make it a subclass of NSView
. Click Next, and for Group select Views. Click Create.
Open HeaderView.swift and replace the contents of the class with the following:
// 1 @IBOutlet weak var sectionTitle: NSTextField! @IBOutlet weak var imageCount: NSTextField! // 2 override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) NSColor(calibratedWhite: 0.8 , alpha: 0.8).set() NSRectFillUsingOperation(dirtyRect, NSCompositingOperation.sourceOver) } |
In here, you’re:
To connect the outlets to the labels, open HeaderView.xib and select the Custom View. Open the Identity Inspector and change the Class to HeaderView.
In the Document Outline view, Control-click on the Header View. In the black pop-up window, drag from imageCount to the Images Count label on the canvas to connect the outlet.
Repeat the operation for the second label, dragging from sectionTitle to the Section Number label in the canvas.
Your header view is in place and ready to go, and you need to pass the header views to the collection view to implement collectionView(_:viewForSupplementaryElementOfKind:at:)
.
Open ViewController.swift and add the following method to the NSCollectionViewDataSource
extension:
func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> NSView { // 1 let view = collectionView.makeSupplementaryView(ofKind: NSCollectionElementKindSectionHeader, withIdentifier: "HeaderView", for: indexPath) as! HeaderView // 2 view.sectionTitle.stringValue = "Section \(indexPath.section)" let numberOfItemsInSection = imageDirectoryLoader.numberOfItemsInSection(indexPath.section) view.imageCount.stringValue = "\(numberOfItemsInSection) image files" return view } |
The collection view calls this method when it needs the data source to provide a header for a section. The method does the following:
makeSupplementaryViewOfKind(_:withIdentifier:for:)
to instantiate a HeaderView
object using the nib with a name equal to withIdentifier
.At the end of ViewController.swift, add this NSCollectionViewDelegateFlowLayout
extension:
extension ViewController : NSCollectionViewDelegateFlowLayout { func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> NSSize { return imageDirectoryLoader.singleSectionMode ? NSZeroSize : NSSize(width: 1000, height: 40) } } |
The above method, although technically optional, is a must when you use headers because the flow layout delegate needs to provide the size of the header for every section.
When not implemented, the header won’t show because zero size is assumed. Additionally, it ignores the specified width, effectively setting it to the collection view’s width.
In this case, the method returns a size of zero when the collection view is in single section mode, and it returns 40 when in multiple sections mode.
For the collection view to use NSCollectionViewDelegateFlowLayout
, you must connect ViewController
to the delegate
outlet of NSCollectionView
.
Open Main.storyboard and select the collection view. Open the Connections Inspector, and locate the delegate in the Outlets section. Drag from the button next to it to the view controller in the Document Outline.
Build and run; check Show Sections and watch your header neatly define sections:
New to macOS 10.12 are the NSCollectionViewFlowLayout
properties sectionHeadersPinToVisibleBounds
and sectionFootersPinToVisibleBounds
.
When sectionHeadersPinToVisibleBounds
is set to true
, the header view for the topmost visible section will stay pinned to the top of the scroll area instead of being scrolled out of view. As you scroll down, the header stays pinned to the top until the next section’s header pushes it out of the way. This behavior is referred to as “sticky headers” or “floating headers”.
Setting sectionFootersPinToVisibleBounds
to true
behaves similarly, pinning footers to the bottom of the scroll area.
Open ViewController.swift and at the end of configureCollectionView()
add the line:
flowLayout.sectionHeadersPinToVisibleBounds = true |
Build and run. Check Show Sections and scroll up a bit so the first row partially scrolls out of view.
Watch how the first section header is still visible and the first row shows through the section header.
NSCollectionViewFlowLayout
and overriding the layoutAttributesForElements(in:)
method. This is described in detail in our Advanced Collection Views in OS X Tutorial.
To show an item as selected, you’ll set a white border, non-selected items will have no border.
First, you need to make the collection view selectable. Open the Main.storyboard, select the Collection View and in the Attributes Inspector, check Selectable.
Checking Selectable enables single selection, meaning you can click an item to select it. When you choose a different item, it deselects the previous item and selects the item you just picked.
When you select an item:
IndexPath
is added to the selectionIndexPaths
property of NSCollectionView
.
isSelected
property of the associated NSCollectionViewItem
is set to true
.
Open CollectionViewItem.swift. Add the following at the end of viewDidLoad()
:
// 1 view.layer?.borderColor = NSColor.white.cgColor // 2 view.layer?.borderWidth = 0.0 |
borderWidth
to 0.0
to guarantee that a new item has no border — i.e, not selectedTo set the borderWidth
each time the isSelected
property changes add the following code at the top of the CollectionViewItem
class:
override var isSelected: Bool { didSet { view.layer?.borderWidth = isSelected ? 5.0 : 0.0 } } |
Whenever isSelected
is changed, didSet
will add or remove the white border according the new value of the property.
isSelected
property is not always the right way to test whether an item is selected or not. When an item is outside the collection view’s visibleRect
the collection view isn’t maintaining an NSCollectionViewItem
instance for this item. If this is the case than the collection view’s item(at:)
method will return nil
. A general way to check whether an item is selected or not is to check whether the collection view’s selectionIndexPaths
property contains the index path in question.
Build and run.
Click an item and you’ll see highlighting. Choose a different image and you’ll see fully functional highlighting. Poof! Magic!
Download the final version of SlidesMagic here.
In this collection views in macOS tutorial, you went all the way from creating your first ever collection view, through discovering the intricacies of the data source API with sections, to handling selection. Although you covered a lot of ground, you’ve only started to explore the capabilities of collection views. Here are more great things to check out:
NSCollectionViewFlowLayout
Some of these topics are covered in our Advanced Collection Views in OS X Tutorial.
The video, documents, and code in the list below are recommended to get an even better understanding of collection views:
NSCollectionViewGridLayout
I wish you a pleasant journey with Collection View in your apps. I look forward to hearing your ideas, experiences and any questions you have in the forums below!
The post Collection Views in macOS Tutorial appeared first on Ray Wenderlich.
The Swift Algorithm Club is an open source project to implement popular algorithms and data structures in Swift.
We thought it would be useful to periodically give a status update with how things are going with the project.
Here’s our final update for 2016!
This month, the repo goes through a much needed quality of life change. Previously, the issues tab was fairly disorganized. Last week, we launched 2 new issues that should alleviate that:
This issue aggregates all the suggestions made by the community, and provide a reference to the issue that first brought it up for more information.
This issue aggregates all the bug reports made by the community.
These 2 issues are locked and are meant to be read only. If you have any suggestions/bugs/questions, creating an issue on the repo is still the way to go. We’ll add the issue to one of these lists and create a reference to it, if appropriate.
Our Swift 3 migration is coming to a close. So far, 54 of the 72 algorithms have been converted to Swift 3. Migration has generally been quite straightforward – it’s just the process of:
README.md
file reflects the updated playgroundWant to help out? It’s a great way to learn about algorithms and Swift 3 at the same time. If so, check out our Github issue and sign up!
It’s been a terrific year so far as our repo approaches 1 year old. Since then, we’ve had 86 unique contributors, 89 articles on various algorithms, 1,056 commits, 6 articles tutorialized, and over 9000 stars!
On behalf of the team, I thank everyone for making the SAC a great place to learn and have fun. I personally picked up a lot of new perspectives whilst reviewing various contributions :]
It’s been a great year so far, and we’re excited to bring SAC to new heights in the coming year. See you in 2017!
The Swift Algorithm Club is always looking for new members. Whether you’re here to learn or here to contribute, we’re happy to have you around.
To learn more about the Swift Algorithm Club, check out our introductory article. We hope to see you at the club! :]
The post Swift Algorithm Club: December Digest appeared first on Ray Wenderlich.
I hope everyone had a fantastic Christmas with friends and family. Hopefully a pair of AirPods was under your tree. ;]
I received the gift of apps! Readers like you stuffed my stocking with their latest apps. I’ve downloaded them all and picked a handful to share with you.
This month we have:
Keep reading to see the latest apps released by raywenderlich.com readers like you.
Santa needs your help!
There is a Super Moon out and all his reindeer have fled. He needs you to help fuel the sleigh with Christmas Spirit before he goes down and Christmas is ruined.
Santa has to deliver presents, angels, and of course coal. Drop the presents down the chimneys without a fire. If the chimney has a fire, they get coal. And if you see a tree, top it with an angel. Each successful delivery will help fuel the sleigh a little longer before it crashes.
Christmas is in your hands.
I have a soft spot in my heart for text based strategy games. To Russia With Love scratches that itch today.
To Russia With Love pits four world nations against each other in a world domination struggle. America, England, Russia, and China are fighting for control of the planet. Now you’re in control of one and its your job to lead your nation to victory!
You start with a little money and a daily income. You can invest in various economic boosters or you can invest in a military to conquer the world. You’ll have to choose just the right balance to make sure you’re ready for the earliest invasions with a quick military but prepared for the long battle with a strong economy. One is an investment in the now while another is an investment in the finale.
You’ll be sure to play this one more than once. Its a great reminder that complex graphics and powerful consoles aren’t required for addictive gameplay.
QuantiFocus will help you track all kinds of time.
QuantiFocus has stopwatches, timers, and incremental counters all in one dashboard. You can create multiple of each for different purposes. Use stopwatches to record how long you perform individual tasks. Use timers to limit yourself. And use incremental counters to keep track of anything you need to count. Each type of tracker can be created with a name and purpose. You can use the once or keep them for ongoing tracking.
You can see interactive usage statistics for each tracker. So if you’re using the same stopwatch daily for a specific task, you can see if you get faster or slower. You can even leave yourself comments after each session. Looking back at the statistics you can also see your comments. You could use this to see which changes you make improve you by making you faster or more consistent.
QuantiFocus is a multipurpose tool that is sure to fit into your life and help you improve your time management.
It can be hard for all of us to keep to a budget. Thankfully Finances will help you keep track of your income and expenses.
You can quickly add your transactions with a very fast keypad. You can organize transactions by categories like groceries or entertainment. You can also create your own custom categories.
Throughout the month you can see how much you’re spending in each category to help you make better decisions as your budget fills up. You can monitor your current financial status in the monthly summary. Finances also has a great section of detailed information in monthly, yearly, and longer time frames. You can get detailed, per-category breakdowns to see how your spending has changed month to month.
You can try it free for the first 150 transactions, then unlock the full app. The full app even allows multiple accounts if you’d like to manage separate finances such as personal and business.
Departed is a game thats one part puzzle and one part strategy.
You control a robot that must escape a series of 3 dimensional mazes spanning multiple floors and dimensions. As you floor hop and teleport around you need to find the exit. There are over 140 levels available in arcade mode. Full leaderboards and achievements so you’ll have to work to solve these mazes fast if you want to be the best.
Departed also has some awesome multiplayer modes. You can race to find the exit in a multidimensional world or you can play tag with friends across the maze. Powerups like freeze rays or snail times give you the upper hand.
Departed is like a competitive game of 3D chess mixed with labyrinth. Its a ton of fun.
Waay is a powerful music theory teaching app that will make you a better songwriter. Waay teaches applied music theory so everything you learn you can take right to your instrument and try.
Waay takes you through a combination of short videos and interactive lessons in two courses. The first course is free with the app and covers Melodies. You’ll cover notes and steps, intervals, keys, scale building, and more. Throughout the course you’ll learn what notes sound good together. You’ll cover how scales can help your music flow. All this will come together as you create memorable melodies for your own music.
The second course covers Chords. You’ll learn about the building blocks of chords, thirds and fifths. Waay will go over how chords mix with melodies to create a song. You’ll learn the magic formula for which chords go great together and naturally create good progression in your songs.
Waay is a fantastic app for anyone looking to get into songwriting. Whether you’re a beginner or expert musician, you can jump into songwriting and start creating your own works of art on your instrument of choice.
Bounce Finger Soccer is like playing hacky sac on your iPhone and its surprisingly addictive.
All you do is try to keep a soccer ball bouncing as long as you can using your finger. There will be various trophies appearing on the screen that you need to knock the ball into. The physics feel great. The ball bounces on your finger and you can give it a little push if you want it to bounce higher. You can move your finger in just the right place to help steer the ball any direction.
You can compare scores and challenge your friends. But perhaps my favorite feature is the store full of custom soccer balls. And oh boy do I mean custom. There is a pig ball, watermelon ball, vinyl records, tires, Captain America shield and more!
This game is super simple to try and a ton of fun to keep playing.
Protect+ is an audio/voice recorder with full encryption available for your recordings.
First Protect+ is a great audio recorder. You can record in MP3 or WAV with settings to control quality and filesize. Each recording has space for notes or comments. Protect+ can automatically remove silent seconds in recordings. The app even has a button to flip the screen upside down so you you can hold the microphone facing your subject.
When you’re finished recording, Protect+ really earns its praises. Protect+ can fully encrypt your recordings with ZIP compatible AES (256bit or 128bit varieties). Your files are encrypted on device in storage and stay encrypted when you send them. You can send them through email or messages. You can also store them in iCloud, Dropbox, or even an FTP site.
If you’re looking for a great app to record your thoughts and protect them, Protect+ is the app for you.
Winter is coming and its gonna be a cold one. You need to help Jack chop as much firewood as possible to stay warm all winter.
Jack is strong enough to get the job done but there are too many pesky beavers. No one wants to chop a beaver and Jack needs your help. You’ve got to help Jack only chop the the logs of wood and avoid all the beavers.
Chopping Jack couldn’t be simpler to play. There are two log flumes with Jack in the middle. You tap the side you want Jack to chop. Don’t chop the beavers and don’t let any logs go unchopped. We need all the wood we can get this winter.
My Scorecard is a great app for golfers. It will help you become a better golfer over time.
You can create individual courses for each of the places you frequently play. Then each time you play you keep score on your iPhone or Apple Watch. Over time you can track your progress on each course and see how you’ve been improving your game. You can perform hole by hole analysis for each course to compare how you played on various days.
You can use your iPad back at home to dig through the data on a bigger screen. And it is easy to share your scores to Twitter, Facebook, or iMessage for those days when you play a really great game. :]
Each month, I really enjoy seeing what our community of readers comes up with. The apps you build are the reason we keep writing tutorials. Make sure you tell me about your next one, submit here.
If you saw an app your liked, hop to the App Store and leave a review! A good review always makes a dev’s day. And make sure you tell them you’re from raywenderlich.com; this is a community of makers.
If you’ve never made an app, this is the month! Check out our free tutorials to become an iOS star. What are you waiting for – I want to see your app next month.
The post Readers’ App Reviews – December 2016 appeared first on Ray Wenderlich.
We’ve all heard stories about how some developer changed the label of a button or the flow of their welcome screens, and suddenly found their retention rate or their in-app purchase rate increased by some crazy amount.
Maybe somewhere on your to-do list, you’ve an “experiment with my purchase button” still sitting there, waiting to be checked off because you discovered running these experiments scientifically is actually a lot of work.
In Firebase Remote Config Tutorial for iOS we showed you how to use Remote Config to update certain parts of your app on the fly and deliver customized content to users in particular countries.
This follow-up tutorial will explore how to use Remote Config to conduct iOS A/B testing by experimenting with different values and measuring the results using Firebase Analytics to find out which sets of values work better. We’ll follow this up with a quick lesson how to perform even more advanced customization based on user properties.
Open your completed Planet Tour app from the previous Remote Config tutorial. Remember to open the xcworkspace
file instead of the xcodeproj
file.
If you didn’t complete the tutorial, download the Planet Tour 2 Starter app. You’ll need to create a Firebase project for the app and download a GoogleServices-info.plist
file from the console, which you can do by following these directions:
com.razeware.Planet-Tour
), leave the App Store ID field blank then click Add App.GoogleServices-info.plist
file for you. Planet Tour.xcworkspace
in Xcode, and drag this file into the Planet Tour
project (select Copy Items if Needed).Build and run your app; you should see a lovely tour of our solar system. Click on a few planets to see details about them.
If you’re new to the project, take a little bit of time to review RCValues.swift to understand how we’re using Remote Config within the project. Then go back to the main screen and check out the banner at the bottom where the app is asking you to sign up for the newsletter.
The higher-ups at Planet Tour, Inc. are concerned there aren’t enough people subscribing to the Planet Tour newsletter. After all, how can you build a successful business if you’re not able to email people about exciting new Planet Tour offers?
The folks from marketing have a theory. You might be able to get more subscribers if you change the label of the Subscribe button to Continue. It does sound more inviting, doesn’t it? While you’re at it, maybe you should try changing the text of the banner from Get our newsletter to Get more facts.
These are easy changes to make now that your app is wired up to use Remote Config. It would just be a few seconds of work publishing these new values in the Firebase console. Then after a week, you could see if you get more newsletter subscribers. Simple, right?
Well, hang on. How would you know the changes you made are directly responsible for the results you’re seeing? What happens if some influential blogger mentions your app’s newsletter in their latest post? Or you end up running an ad campaign in another app’s newsletter, thereby bringing in an audience more inclined to subscribe to newsletters in the first place?
These are factors you can’t really control, and they might lead you to draw the wrong conclusions.
Ideally, you’d want to release two versions of your app simultaneously. One random group of users gets to see the new newsletter labels, and the other group gets to see the current newsletter. You can compare the results between these two groups and feel reasonably confident the differences are due to the changes you made, and not some external factor.
Well, that’s exactly what iOS A/B testing is, and it’s a very common way to run these kinds of experiments. Many larger companies have built up their own infrastructure to run and measure these tests, but with Firebase Remote Config, you can do it on top of the infrastructure Firebase has already created.
Head over to the Firebase Console. Select Planet Tour from the list of projects, then select Remote Config from the list of features on the left.
Click the Add Parameter button. For the key, enter subscribeVCButton. Next, click the Add value for condition drop-down and select Define new condition.
In the dialog, give it the name Newsletter Test. Under Applies if…, select User in random percentile. Pick <= for the operator, and 50 for the percentile. When you’re all done, your dialog should look similar to below.
Click Create condition to create this condition. In the Value for Newsletter Test field, enter the string Continue. For the Default value file, click the Other empty values drop-down and select No value.
This is your way of telling the Remote Config library it should just go ahead and use whatever values are supplied locally in the code. If you picked the other option Empty string, your button would have literally had a title of “” for a value and look like it didn’t have a label at all!
Finally, click Add parameter.
Now you can move on to changing the label of the button on the front page.
Create a new parameter and give it a key of subscribeBannerButton. Select Add value for condition > Newsletter Test, and give the condition a value of Get more facts!. Make sure the default value is set to No value. Click Add parameter.
Click Publish changes to publish these changes. You should now see these two entries among your Remote Config values.
If you entered something incorrectly, don’t worry; you just need to click the little pencil icon next to each entry to edit and republish your changes.
Okay, let’s see how these new changes look!
Build and run your app. Depending on what group your assigned to, you’ll either see the old default values, or your exciting new ones.
If you don’t see your new experimental values, you can’t just re-run your app on the chance you’ll see the new values. Remote Config remembers what percentile it’s randomly assigned you to, and that won’t change for the lifetime of the app. After all, it would be very weird if your users saw different versions of your app every time they started it up.
Instead, you’ll need to delete and reinstall your app to see if you get placed into a different group.
Try the delete-reinstall cycle a few times and you should eventually see the new values. If you don’t see the new values after 8 or 9 tries, immediately step away from your computer and find the nearest roulette table. :]
You have your new newsletter text going out to half of your population, while the rest of your population still sees the old text. How do you know which version is performing better?
You need to measure your results with some analytics.
To figure out which version of your newsletter flow is performing better, you’ll need to measure the results. Specifically, you’ll want to see how many people, after seeing the main menu, decided to click on the banner at the bottom to go to the GetNewsletterViewController
. You’ll want to see how many of those people clicked on the Subscribe (or Continue) button.
This is something you can measure fairly easily with mobile analytics. If you’re not familiar with the concept of mobile analytics, there’s a great tutorial on the subject here.
Firebase Analytics is just one of many mobile analytics solutions available out there. I like using it in this situation because a) It’s free, and b) By having Remote Config installed and working, you’ve already got Firebase Analytics up and running, too!
Firebase Analytics, like most other mobile analytics solutions, is an event-based model. As users perform actions in your app, Firebase Analytics sends events to its servers. Sometime later, these servers process these events and turn them into meaningful graphs for you to analyze.
Open ContainerViewController.swift. Add the following to the top of the file:
import Firebase |
Next, add the following to the end of viewDidLoad()
:
FIRAnalytics.logEvent(withName: "mainPageLoaded", parameters: nil) |
This will send off an event named mainPageLoaded
when your user first starts up the app and makes it to your main screen. The parameters
argument is a dictionary of optional key/value pairs associated with this event. You don’t need any here, so this is left as nil
.
Next, open GetNewsletterViewController.swift. Add the following to the top of the file:
import Firebase |
Next, add the following to the end of viewDidLoad()
:
FIRAnalytics.logEvent(withName: "newsletterPageLoaded", parameters: nil) |
Finally, add the following to the end of submitButtonWasPressed(_:)
:
FIRAnalytics.logEvent(withName: "newsletterButtonPressed", parameters: nil) |
Your app will trigger events when the user first visits the main page, the newsletter page, and when they click the Submit button to sign up for the newsletter.
Before you build and run, you’ll want to turn on Firebase Analytics debug mode, which lets you see the results of all these analytics calls in the console.
To do this, select Product\Scheme\Edit Scheme. Within the Run scheme, select Arguments. Within Arguments Passed On Launch, click the + symbol and enter the argument -FIRAnalyticsDebugEnabled
. Make sure you include the dash at the beginning.
When you’re done, you should have something like the following:
Close the dialog and build and run. You should now see output in the console similar to below:
Planet Tour[34325:] <FIRAnalytics/DEBUG> Logging event: origin, name, params: app, mainPageLoaded, { "_o" = app; } |
When you click the Get more facts! (or Get our newsletter) button, this will also be logged to your console:
Planet Tour[34325:] <FIRAnalytics/DEBUG> Logging event: origin, name, params: app, newsletterPageLoaded, { "_o" = app; } |
A similar event will be logged when you click the Subscribe (or Continue) button.
If you then wait approximately 10 seconds, you’ll see some output in your log (as a giant JSON object) indicating Firebase has sent this information to its severs.
Because you’ve turned on debug mode with the -FIRAnalyticsDebugEnabled
flag, Firebase Analytics is very aggressive about sending data to its servers. It sends a batch either when it has data that’s more than 10 seconds old, or when your app moves into the background.
If you hit Command-Shift-H to simulate pressing the home button, you’ll see more JSON data in your console as Firebase Analytics sends another batch of data to its servers.
In a production app, this behavior would probably kill your phone’s battery life. Therefore, when you don’t have debug mode turned on, Firebase Analytics sends data either when it has data that’s more than an hour old, or when your app goes into the background.
Incidentally, this debug setting does persist. So if you want to turn off Analytics debug mode (because, say, you want to stress test your app’s battery performance), you either disable the flag and then delete and reinstall the app, or explicitly turn off debug mode by changing the flag to -noFIRAnalyticsDebugEnabled
You’re sending events to the servers, but how do you view this collected data?
Head on over to the Firebase console, select your Planet Tour project, then click on the Analytics section. You’ll see a summary of your app’s usage over time, but right now you’re more interested in viewing data for the individual events you just recorded.
Head over to the Events tab. You’ll probably see something like this:
Looks pretty empty, doesn’t it? Well, that’s partly because the default “Last 30 days” timeframe doesn’t include data you’ve collected from the current day. This is generally because the current day’s data is incomplete and it would be misleading, not to mention a giant bummer, to see all your graphs end in a big downturn in usage.
To select today’s data, select the Last 30 days drop-down and pick Today instead. This time when you see your events for today… it will probably look just as empty.
This is because Firebase Analytics only generates graphs and reports based on your analytics data every few hours. Your best bet is to come back in a few hours and see what your data looks like then. But that’s okay, because you’re not done yet!
While you might be recording how often people are triggering these events within the app, you’re still not keeping track of whether these users are in the new experimental flow or seeing the old values. You can’t really distinguish between the two. Luckily, this is something you can accomplish with user properties.
A user property in Firebase Analytics is simply a property associated with a particular user.
Some examples of user properties are premium users within your app, user fitness goals in your exercise app, or any other user-associated data you might want to filter your event data by.
In your case, you’ll add an experimentGroup property to record what experiment group the user belongs to. You’ll set the value of this property through Remote Config.
Go back to the Remote Config section of the Firebase console. Click Add Parameter. Give the parameter a key of experimentGroup.
Select the Add value for condition drop down, select Newsletter Test and give that group a value of newsletterFlowA. Add a default value of newsletterDefault.
Click Add parameter to add the parameter, then click Publish changes to publish your changes.
In RCValues.swift, add the following method to keep track of this experimentGroup value in a user property of the same name below activateDebugMode()
.
func recordExperimentGroups() { let myExperimentGroup = FIRRemoteConfig.remoteConfig()["experimentGroup"].stringValue ?? "none" FIRAnalytics.setUserPropertyString(myExperimentGroup, forName: "experimentGroup") } |
Next, call this method in fetchCloudValues()
, right before you set fetchComplete
to true
.
self?.recordExperimentGroups() |
Finally, you need to tell Firebase Analytics this is a user property you care about seeing in your reports. You can do this in the Firebase console. Select Analytics, then select User Properties on the top bar.
Now be careful in this next step — the Firebase console doesn’t let you edit or delete user properties once you create them!
Click on New user property and give it the name experimentGroup. If you want to be extra-sure you get the name right, you can cut-and-paste the name directly from the code. Give it any description you’d like. Then click Create.
experimentGroup
instead of something very specific like newsletterTestOneGroup
. This gives you the freedom to re-use this user property next month for a completely different type of experiment while still keeping the same somewhat accurate.Build and run the app!
Your app should look nearly the same as before, but you might notice this extra bit of output in the console:
Planet Tour[7397:] <FIRAnalytics/DEBUG> Setting user property. Name, value: experimentGroup, newsletterFlowA |
When your analytics data is sent, you’ll see something like this in the output:
user_attributes { set_timestamp_millis: 1477662722018 name: "experimentGroup" string_value: "newsletterFlowA" } |
This means your user property of newsletterFlowA is now associated with all events for this session and for all future sessions. Note, this isn’t retroactive; those old events sent to Firebase Analytics will remain propertyless.
But this does mean you can now simulate iOS A/B testing in the real world. Give it a try!
Once you’ve done that, wait a few hours for the results to process; you can always work on the “Advanced User Targeting” part of this tutorial while you’re waiting. Then come back to view your results.
Head over to the Firebase console, select Analytics, click on the Events tab, and select Today from the dropdown. Unless, that is, you decided to wait overnight to view your data, in which case select Yesterday. You should now see these new events in your console.
Click the newsletterPageLoaded event to view more details about it. You’ll probably see something like this.
In a real app where lots of people use your app over the course of several days, you’ll have much more interesting graphs. But for now, you get these fantastic dots. :]
You can see how many people have visited your newsletter page, but this still doesn’t give you the insight you need to determine which flow works better. To do that, you’re first going to need to separate this data by the experimentGroup user property.
While still looking at the newsletterPageLoaded event, click on Add Filter at the top, then click on User Property, experimentGroup, then newsletterFlowA.
You should be able to then see how many users from the newsletterFlowA group visited the newsletter page.
By changing the filter in a similar way, you can compare this value to the users in the newsletterDefault group who visited the page.
Does this mean the group with the most newsletterPageLoaded or newsletterButtonPressed events has the more effective flow? Well, not necessarily. There’s always a chance the other group had more people using your app that day.
Think about it this way; imagine 100 people visited app version A, and 20 of them signed up for the newsletter. Meanwhile 80 people visited app version B, and 18 of them signed up for the newsletter. While the first version has more total people signing up for the newsletter, version B actually has a better conversion rate — 20% vs. 22.5%.
There’s an easy way for you to make these same calculations: funnels!
Funnels are basically a group of events you’re interested in measuring as a series. By defining a funnel, you can see how many times each event was triggered in a nice bar graph, and more importantly, see the fall-off from one event to the next.
Click on the Funnels header, then click Add your first funnel. Name it Newsletter flow. Add whatever description you want. You should see two drop-downs below to select events. Make the first one mainPageLoaded, and the second newsletterPageLoaded. Click Add another event to create a third event, and select newsletterButtonPressed.
Click Create & View, and you should see a page like this.
This shows you the number of people who made it to each part of the funnel. The most important numbers, however, are the little percentage numbers at the top. Those show you what percentage of users made it from the current step to the next one.
Now, you can filter this by people who have the newsletterFlowA user property…
Or newsletterDefault.
In these screenshots, it looks like newsletterFlowA performed better, both in terms of people who made it to the newsletter page as well as people who clicked the subscribe button. Your data may look different, depending on what you did inside your app.
Once you have this data, you can then take action! If it turns out the new newsletter flow is doing better, you could change the condition so it applies to 75% of your viewers. Or just remove the condition entirely and make these new values the default. It’s up to you.
Or, “Pluto Returns!”
In our previous tutorial, you avoided an international crisis by setting shouldWeIncludePluto
to true
for your users in Scandinavia. It turns out, however, setting this by country wasn’t enough. The choice of whether or not Pluto is a planet is a deeply personal one, and many individuals from around the world feel strongly, Pluto should be a planet. How can we customize Planet Tour for all of them?
Well, rather than just altering this value by country, you’re going to add in much more fine-grained control by changing this setting based on a user property.
While there are different ways to determine if a user is a fan of small, remote, rocks in space, the easiest way is to just ask them.
At the top of PlanetsCollectionViewController.swift, add this line:
import Firebase |
Next, add the following above the reuseIdentifier
definition:
fileprivate let takenSurveyKey = "takenSurvey" |
Finally, add the following inside the extension with the MARK: -Internal
line.
func runUserSurvey() { let alertController = UIAlertController(title: "User survey", message: "How do you feel about small, remote, cold rocks in space?", preferredStyle: .actionSheet) let fanOfPluto = UIAlertAction(title: "They're planets, too!", style: .default) { _ in FIRAnalytics.setUserPropertyString("true", forName: "likesSmallRocks") } let notAFan = UIAlertAction(title: "Not worth my time", style: .default) { _ in FIRAnalytics.setUserPropertyString("false", forName: "likesSmallRocks") } alertController.addAction(fanOfPluto) alertController.addAction(notAFan) navigationController?.present(alertController, animated: true) UserDefaults.standard.set(true, forKey: takenSurveyKey) } |
Two things you should note with the survey you added: First, after you get a response back from the user, you’re recording this in a new user property called likesSmallRocks
. Second, you’re making a note in UserDefaults
this user has taken the survey, so they don’t get asked every visit.
It’s best to get into the habit of adding a user property in the Firebase console the same time you add it in code. Open the Firebase console and select Analytics, then User Properties. Select New user property and create a new one called likesSmallRocks. As before, I recommend cutting-and-pasting the exact name from the code.
Go back to PlanetsCollectionViewController
, and at the end of viewDidAppear(_:)
, add these lines to make sure you only ask this once per app install:
if !UserDefaults.standard.bool(forKey: takenSurveyKey) { runUserSurvey() } |
If you want to make testing a little easier, add this line to viewWillAppear(_:)
above customizeNavigationBar()
, which will let you re-take the survey at a future point.
let retakeSurveyButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(runUserSurvey)) parent?.navigationItem.rightBarButtonItem = retakeSurveyButton |
Build and run your app. You’ll now get asked how you feel about small rocks in space. Feel free to answer honestly. :]
Now you’ve created this user property, you can start adjusting Remote Config values based on its value. Open the Firebase console and select Remote Config. If you completed the previous tutorial, you should still see your entry for shouldWeIncludePluto.
Click the pencil icon next to this entry to edit, then select Add value for condition\Define new condition. Name the new condition Small rock fans, and state this condition applies if User property\likesSmallRocks | exactly matches | true.
Click Create condition, then set this value to true.
Click Update, then Publish changes.
Now build and run your app again.
If you said you’re a fan of small remote rocks, you should now see Pluto listed among your planets. If you didn’t, you won’t see Pluto…unless your device thinks it’s in a Scandinavian country, in which case Pluto is still there.
If you want to see what your app looks like for people who answered the user survey differently, click the bar button item on top to retake the survey, then quit and re-run your app.
How about making some more subtle changes to appeal to your Pluto fans? There’s a variable in your app — planetImageScaleFactor
— that determines how closely the size of the planet images match the actual size of their corresponding planet.
At a value of 1.0
, they’re perfectly to scale, so planets like Pluto are barely a pixel large. At a value of 0.0
, all planets are the same size. Right now, this variable has a default value of 0.33
, which gives you a sense of the planets’ relative size, while still making the smaller ones easy to see.
You’re going to make this value slightly lower for fans of small rocks, so the smaller planets like Pluto and Mercury show up bigger than they would otherwise.
Go back to Remote Config in the Firebase console, and create a new entry for planetImageScaleFactor. Give it a value of 0.2 for users in the Small rock fans condition and a default value of 0.45.
Build and run Planet Tour again. Depending on how you feel about small remote rocks, planets like Mars or Pluto should look proportionately larger or smaller.
While this might seem like a fun but inconsequential change, these types of customizations can be quite powerful. As you learn more about your users and the parts of your app they prefer, you can start to deliver a truly customized experience to your users, making sure the elements that appeal to them are always front and center.
You can download the fully completed Planet Tour 2 project for this Firebase Remote Config tutorial. Please note, however, you still need to create a project in the Firebase Console and drag in your GoogleServices-info.plist
file for the project to work.
There’s plenty more you can do with Firebase Analytics and Remote Config, and you can always read the documentation for more information.
In the meantime, think about elements of your app you’ve always wanted to experiment with. Try running one of them through iOS A/B testing, and let us know what you discovered in the comments below!
The post Firebase Tutorial: iOS A/B Testing appeared first on Ray Wenderlich.
By default, Swift is memory safe, which means that it prevents direct access to memory and makes sure everything is initialized before you use it. The key phrase is “by default.” Unsafe Swift lets you get directly at memory through pointers if you need to.
This tutorial will take you on a whirlwind tour of the so-called “unsafe” features of Swift. The term “unsafe” sometimes causes confusion. It doesn’t mean that you are writing dangerously bad code that might not work. Rather, it means you are writing code that you need to be extra careful about because the compiler is limited in how it can help you.
You may find yourself needing to use these features if you interoperate with an unsafe language such as C, need to gain additional runtime performance, or simply want to explore internals. While this is an advanced topic, you should be able to follow along if you have reasonable Swift competency. C experience will help, but is not required.
This tutorial consists of three playgrounds. In the first playground, you’ll create several short snippets that explore memory layout and use unsafe pointers. In the second playground, you will wrap a low-level C API that performs streaming data compression with a Swifty interface. In the final playground, you will create a platform independent alternative to arc4random
that, while using unsafe Swift, hides that detail from users.
Start by creating a new playground, calling it UnsafeSwift. You can select any platform, since all the code in this tutorial is platform-agnostic. Make sure to import the Foundation framework.
Unsafe Swift works directly with the memory system. Memory can be visualized as series of boxes (billions of boxes, actually), each with a number inside it. Each box has a unique memory address associated with it. The smallest addressable unit of storage is a byte, which usually consists of eight bits. Eight bit bytes can store values from 0-255. Processors can also efficiently access words of memory which are typically more than one byte. On a 64-bit system, for example, a word is 8 bytes or 64 bits long.
Swift has a MemoryLayout
facility that tells you about the size and alignment of things in your program.
Add the following to your playground:
MemoryLayout<Int>.size // returns 8 (on 64-bit) MemoryLayout<Int>.alignment // returns 8 (on 64-bit) MemoryLayout<Int>.stride // returns 8 (on 64-bit) MemoryLayout<Int16>.size // returns 2 MemoryLayout<Int16>.alignment // returns 2 MemoryLayout<Int16>.stride // returns 2 MemoryLayout<Bool>.size // returns 1 MemoryLayout<Bool>.alignment // returns 1 MemoryLayout<Bool>.stride // returns 1 MemoryLayout<Float>.size // returns 4 MemoryLayout<Float>.alignment // returns 4 MemoryLayout<Float>.stride // returns 4 MemoryLayout<Double>.size // returns 8 MemoryLayout<Double>.alignment // returns 8 MemoryLayout<Double>.stride // returns 8 |
MemoryLayout<Type>
is a generic type evaluated at compile time that determines the size
, alignment
and stride
of each specified Type
. The number returned is in bytes. For example, an Int16
is two bytes in size
and has an alignment
of two as well. That means it has to start on even addresses (evenly divisible by two).
So, for example, it is legal to allocate an Int16
at address 100, but not 101 because it violates the required alignment. When you pack a bunch of Int16
s together, they pack together at an interval of stride
. For these basic types the size
is the same as the stride
.
Next, look at the layout of some user defined struct
s and by adding the following to the playground:
struct EmptyStruct {} MemoryLayout<EmptyStruct>.size // returns 0 MemoryLayout<EmptyStruct>.alignment // returns 1 MemoryLayout<EmptyStruct>.stride // returns 1 struct SampleStruct { let number: UInt32 let flag: Bool } MemoryLayout<SampleStruct>.size // returns 5 MemoryLayout<SampleStruct>.alignment // returns 4 MemoryLayout<SampleStruct>.stride // returns 8 |
The empty structure has a size of zero. It can be located at any address since alignment
is one. (i.e. All numbers are evenly divisible by one.) The stride
, curiously, is one. This is because each EmptyStruct
that you create has to have a unique memory address despite being of zero size.
For SampleStruct
, the size
is five but the stride
is eight. This is driven by its alignment requirements to be on 4-byte boundaries. Given that, the best Swift can do is pack at an interval of eight bytes.
Next add:
class EmptyClass {} MemoryLayout<EmptyClass>.size // returns 8 (on 64-bit) MemoryLayout<EmptyClass>.stride // returns 8 (on 64-bit) MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit) class SampleClass { let number: Int64 = 0 let flag: Bool = false } MemoryLayout<SampleClass>.size // returns 8 (on 64-bit) MemoryLayout<SampleClass>.stride // returns 8 (on 64-bit) MemoryLayout<SampleClass>.alignment // returns 8 (on 64-bit) |
Classes are reference types so MemoryLayout
reports the size of a reference: eight bytes.
If you want to explore memory layout in greater detail, see this excellent talk by Mike Ash.
A pointer encapsulates a memory address. Types that involve direct memory access get an “unsafe” prefix so the pointer type is called UnsafePointer
. While the extra typing may seem annoying, it lets you and your reader know that you are dipping into non-compiler checked access of memory that when not done correctly could lead to undefined behavior (and not just a predictable crash).
The designers of Swift could have just created a single UnsafePointer
type and made it the C equivalent of char *
, which can access memory in an unstructured way. They didn’t. Instead, Swift contains almost a dozen pointer types, each with different capabilities and purposes. Using the most appropriate pointer type communicates intent better, is less error prone, and helps keep you away from undefined behavior.
Unsafe Swift pointers use a very predictable naming scheme so that you know what the traits of the pointer are. Mutable or immutable, raw or typed, buffer style or not. In total there is a combination of eight of these.
In the following sections, you’ll learn more about these pointer types.
Add the following code to your playground:
// 1 let count = 2 let stride = MemoryLayout<Int>.stride let alignment = MemoryLayout<Int>.alignment let byteCount = stride * count // 2 do { print("Raw pointers") // 3 let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment) // 4 defer { pointer.deallocate(bytes: byteCount, alignedTo: alignment) } // 5 pointer.storeBytes(of: 42, as: Int.self) pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self) pointer.load(as: Int.self) pointer.advanced(by: stride).load(as: Int.self) // 6 let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount) for (index, byte) in bufferPointer.enumerated() { print("byte \(index): \(byte)") } } |
In this example you use Unsafe Swift pointers to store and load two integers. Here’s what’s going on:
Int
Int
do
block is added, to add a scope level, so you can reuse the variable names in upcoming examples.UnsafeMutableRawPointer.allocate
is used to allocate the required bytes. This method returns an UnsafeMutableRawPointer
. The name of that type tells you the pointer can be used to load and store (mutate) raw bytes.defer
block is added to make sure the pointer is deallocated properly. ARC isn’t going to help you here – you need to handle memory management yourself! You can read more about defer
here.storeBytes
and load
methods are used to store and load bytes. The memory address of the second integer is calculated by advancing the pointer stride
bytes. Since pointers are Strideable
you can also use pointer arithmetic as in (pointer+stride).storeBytes(of: 6, as: Int.self)
.
UnsafeRawBufferPointer
lets you access memory as if it was a collection of bytes. This means you can iterate over the bytes, access them using subscripting and even use cool methods like filter
, map
and reduce
. The buffer pointer is initialized using the raw pointer.The previous example can be simplified by using typed pointers. Add the following code to your playground:
do { print("Typed pointers") let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count) pointer.initialize(to: 0, count: count) defer { pointer.deinitialize(count: count) pointer.deallocate(capacity: count) } pointer.pointee = 42 pointer.advanced(by: 1).pointee = 6 pointer.pointee pointer.advanced(by: 1).pointee let bufferPointer = UnsafeBufferPointer(start: pointer, count: count) for (index, value) in bufferPointer.enumerated() { print("value \(index): \(value)") } } |
Notice the following differences:
UnsafeMutablePointer.allocate
. The generic parameter lets Swift know the pointer will be used to load and store values of type Int
.initialize
and deinitialize
methods respectively.pointee
property that provides a type-safe way to load and store values.(pointer+1).pointee = 6
Typed pointers need not always be initialized directly. They can be derived from raw pointers as well.
Add the following code to your playground:
do { print("Converting raw pointers to typed pointers") let rawPointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment) defer { rawPointer.deallocate(bytes: byteCount, alignedTo: alignment) } let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count) typedPointer.initialize(to: 0, count: count) defer { typedPointer.deinitialize(count: count) } typedPointer.pointee = 42 typedPointer.advanced(by: 1).pointee = 6 typedPointer.pointee typedPointer.advanced(by: 1).pointee let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count) for (index, value) in bufferPointer.enumerated() { print("value \(index): \(value)") } } |
This example is similar to the previous one, except that it first creates a raw pointer. The typed pointer is created by binding the memory to the required type Int
. By binding memory, it can be accessed in a type-safe way. Memory binding is done behind the scenes when you create a typed pointer.
The rest of this example is the same as the previous one. Once you’re in typed pointer land, you can make use of `pointee` for example.
Often you have an existing instance of a type that you want to inspect the bytes that form it. This can be achieved using a method called withUnsafeBytes(of:)
.
Add the following code to your playground:
do { print("Getting the bytes of an instance") var sampleStruct = SampleStruct(number: 25, flag: true) withUnsafeBytes(of: &sampleStruct) { bytes in for byte in bytes { print(byte) } } } |
This prints out the raw bytes of the SampleStruct
instance. The withUnsafeBytes(of:)
method gives you access to an UnsafeRawBufferPointer
that you can use inside the closure.
withUnsafeBytes
is also available as an instance method on Array
and Data
.
Using withUnsafeBytes(of:)
you can return a result. An example use of this is to compute a 32-bit checksum of the bytes in a structure.
Add the following code to your playground:
do { print("Checksum the bytes of a struct") var sampleStruct = SampleStruct(number: 25, flag: true) let checksum = withUnsafeBytes(of: &sampleStruct) { (bytes) -> UInt32 in return ~bytes.reduce(UInt32(0)) { $0 + numericCast($1) } } print("checksum", checksum) // prints checksum 4294967269 } |
The reduce
call adds up all of the bytes and ~
then flips the bits. Not a particularly robust error detection, but it shows the concept.
You need to be careful when writing unsafe code so that you avoid undefined behavior. Here are a few examples of bad code.
// Rule #1 do { print("1. Don't return the pointer from withUnsafeBytes!") var sampleStruct = SampleStruct(number: 25, flag: true) let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in return bytes // strange bugs here we come ☠️☠️☠️ } print("Horse is out of the barn!", bytes) /// undefined !!! } |
You should never let the pointer escape the withUnsafeBytes(of:)
closure. Things may work today but…
// Rule #2 do { print("2. Only bind to one type at a time!") let count = 3 let stride = MemoryLayout<Int16>.stride let alignment = MemoryLayout<Int16>.alignment let byteCount = count * stride let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment) let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count) // Breakin' the Law... Breakin' the Law (Undefined behavior) let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2) // If you must, do it this way: typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) { (boolPointer: UnsafeMutablePointer<Bool>) in print(boolPointer.pointee) // See Rule #1, don't return the pointer } } |
Never bind memory to two unrelated types at once. This is called Type Punning and Swift does not like puns. Instead, you can temporarily rebind memory with a method like withMemoryRebound(to:capacity:)
. Also, the rules say it is illegal to rebind from a trivial type (such as an Int
) to a non-trivial type (such as a class
). Don’t do it.
// Rule #3... wait do { print("3. Don't walk off the end... whoops!") let count = 3 let stride = MemoryLayout<Int16>.stride let alignment = MemoryLayout<Int16>.alignment let byteCount = count * stride let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment) let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1) // OMG +1???? for byte in bufferPointer { print(byte) // pawing through memory like an animal } } |
The ever present problem of off-by-one errors are especially worse with unsafe code. Be careful, review and test!
Time to take all of your knowledge and wrap a C API. Cocoa includes a C module that implements some common data compression algorithms. These include LZ4 for when speed is critical, LZ4A for when you need the highest compression ratio and don’t care about speed, ZLIB which balances space and speed and the new (and open source) LZFSE which does an even better job balancing space and speed.
Create a new playground, calling it Compression. Begin by defining a pure Swift API that uses Data
.
Then, replace the contents of your playground with the following code:
import Foundation import Compression enum CompressionAlgorithm { case lz4 // speed is critical case lz4a // space is critical case zlib // reasonable speed and space case lzfse // better speed and space } enum CompressionOperation { case compression, decompression } // return compressed or uncompressed data depending on the operation func perform(_ operation: CompressionOperation, on input: Data, using algorithm: CompressionAlgorithm, workingBufferSize: Int = 2000) -> Data? { return nil } |
The function that does the compression and decompression is perform
which is currently stubbed out to return nil
. You will add some unsafe code to it shortly.
Next add the following code to the end of the playground:
// Compressed keeps the compressed data and the algorithm // together as one unit, so you never forget how the data was // compressed. struct Compressed { let data: Data let algorithm: CompressionAlgorithm init(data: Data, algorithm: CompressionAlgorithm) { self.data = data self.algorithm = algorithm } // Compress the input with the specified algorithm. Returns nil if it fails. static func compress(input: Data, with algorithm: CompressionAlgorithm) -> Compressed? { guard let data = perform(.compression, on: input, using: algorithm) else { return nil } return Compressed(data: data, algorithm: algorithm) } // Uncompressed data. Returns nil if the data cannot be decompressed. func decompressed() -> Data? { return perform(.decompression, on: data, using: algorithm) } } |
The Compressed
structure stores both the compressed data and the algorithm that was used to create it. That makes it less error prone when deciding what decompression algorithm to use.
Next add the following code to the end of the playground:
// For discoverability, add a compressed method to Data extension Data { // Returns compressed data or nil if compression fails. func compressed(with algorithm: CompressionAlgorithm) -> Compressed? { return Compressed.compress(input: self, with: algorithm) } } // Example usage: let input = Data(bytes: Array(repeating: UInt8(123), count: 10000)) let compressed = input.compressed(with: .lzfse) compressed?.data.count // in most cases much less than orginal input count let restoredInput = compressed?.decompressed() input == restoredInput // true |
The main entry point is an extension on the Data
type. You’ve added a method called compressed(with:)
which returns an optional Compressed
struct. This method simply calls the static method compress(input:with:)
on Compressed
.
There is an example usage at the end but it is currently not working. Time to start fixing that!
Scroll back up to the first block of code you entered, and begin the implementation of the perform(_:on:using:workingBufferSize:)
function as follows:
func perform(_ operation: CompressionOperation, on input: Data, using algorithm: CompressionAlgorithm, workingBufferSize: Int = 2000) -> Data? { // set the algorithm let streamAlgorithm: compression_algorithm switch algorithm { case .lz4: streamAlgorithm = COMPRESSION_LZ4 case .lz4a: streamAlgorithm = COMPRESSION_LZMA case .zlib: streamAlgorithm = COMPRESSION_ZLIB case .lzfse: streamAlgorithm = COMPRESSION_LZFSE } // set the stream operation and flags let streamOperation: compression_stream_operation let flags: Int32 switch operation { case .compression: streamOperation = COMPRESSION_STREAM_ENCODE flags = Int32(COMPRESSION_STREAM_FINALIZE.rawValue) case .decompression: streamOperation = COMPRESSION_STREAM_DECODE flags = 0 } return nil /// To be continued } |
This converts from your Swift types to the C types required by the compression library, for the compression algorithm and operation to perform.
Next, replace return nil
with:
// 1: create a stream var streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1) defer { streamPointer.deallocate(capacity: 1) } // 2: initialize the stream var stream = streamPointer.pointee var status = compression_stream_init(&stream, streamOperation, streamAlgorithm) guard status != COMPRESSION_STATUS_ERROR else { return nil } defer { compression_stream_destroy(&stream) } // 3: set up a destination buffer let dstSize = workingBufferSize let dstPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: dstSize) defer { dstPointer.deallocate(capacity: dstSize) } return nil /// To be continued |
This is what is happening here:
compression_stream
and schedule it for deallocation with the defer
block.pointee
property you get the stream and pass it to the compression_stream_init
function. The compiler is doing something special here. By using the inout &
marker it is taking your compression_stream
and turning it into a UnsafeMutablePointer<compression_stream>
automatically. (You could have also just passed streamPointer
and not needed this special conversion.)Finish the perform
function by replacing return nil
with:
// process the input return input.withUnsafeBytes { (srcPointer: UnsafePointer<UInt8>) in // 1 var output = Data() // 2 stream.src_ptr = srcPointer stream.src_size = input.count stream.dst_ptr = dstPointer stream.dst_size = dstSize // 3 while status == COMPRESSION_STATUS_OK { // process the stream status = compression_stream_process(&stream, flags) // collect bytes from the stream and reset switch status { case COMPRESSION_STATUS_OK: // 4 output.append(dstPointer, count: dstSize) stream.dst_ptr = dstPointer stream.dst_size = dstSize case COMPRESSION_STATUS_ERROR: return nil case COMPRESSION_STATUS_END: // 5 output.append(dstPointer, count: stream.dst_ptr - dstPointer) default: fatalError() } } return output } |
This is where the work really happens. And here’s what it’s doing:
Data
object which is going to contain the output – either the compressed or decompressed data, depending on what operation this is.compression_stream_process
as long as it continues to return COMPRESSION_STATUS_OK
.output
that is eventually returned from this function.COMPRESSION_STATUS_END
only part of the destination buffer potentially needs to be copied.In the example usage you can see that the 10,000-element array is compressed down to 153 bytes. Not too shabby.
Random numbers are important for many applications from games to machine learning. macOS provides arc4random
(A Replacement Call 4 random) that produces great (cryptographically sound) random numbers. Unfortunately this call is not available on Linux. Moreover, arc4random
only provides randoms as UInt32
. However, the file /dev/urandom provides an unlimited source of good random numbers.
In this section, you will use your new knowledge to read this file and create completely type safe random numbers.
Start by creating a new playground, calling it RandomNumbers. Make sure to select the macOS platform this time.
Once you’ve created it, replace the default contents with:
import Foundation enum RandomSource { static let file = fopen("/dev/urandom", "r")! static let queue = DispatchQueue(label: "random") static func get(count: Int) -> [Int8] { let capacity = count + 1 // fgets adds null termination var data = UnsafeMutablePointer<Int8>.allocate(capacity: capacity) defer { data.deallocate(capacity: capacity) } queue.sync { fgets(data, Int32(capacity), file) } return Array(UnsafeMutableBufferPointer(start: data, count: count)) } } |
The file
variable is declared static
so only one will exist in the system. You will rely on the system closing it when the process exits. Since it is possible that multiple threads will want random numbers, you need to protect access to it with a serial GCD queue.
The get
function is where the work happens. First you create some unallocated storage that is one beyond what you need because fgets
is always 0 terminated. Next, you get the data from the file, making sure to do so while operating on the GCD queue. Finally, you copy the data to a standard array by first wrapping it in a UnsafeMutableBufferPointer
that can act as a Sequence
.
So far this will only (safely) give you an array of Int8
values. Now you’re going to extend that.
Add the following to the end of your playground:
extension Integer { static var randomized: Self { let numbers = RandomSource.get(count: MemoryLayout<Self>.size) return numbers.withUnsafeBufferPointer { bufferPointer in return bufferPointer.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) { return $0.pointee } } } } Int8.randomized UInt8.randomized Int16.randomized UInt16.randomized Int16.randomized UInt32.randomized Int64.randomized UInt64.randomized |
This adds a static randomized
property to all subtypes of the Integer
protocol (see protocol oriented programming for more on this!). You first get the random numbers, and with the bytes of the array that is returned, you rebind (as in C++’s reinterpret_cast
) the Int8
values as the type being requested and return a copy. Simples! :]
And that’s it! Random numbers in a safe way, using unsafe Swift under the hood.
Here are the completed playgrounds. There many additional resources you can explore to learn more:
I hope you have enjoyed this tutorial. If you have questions or experiences you would like to share, I am looking forward to hearing about them in the forums!
The post Unsafe Swift: Using Pointers And Interacting With C appeared first on Ray Wenderlich.
Attending an iOS conference is a great way to learn new things and make new friends.
Also – as Ray said in one of his recent talks – good conferences give you a spark of motivation that burns even brighter even after you return home. Did you ever notice that upbeat attitude the days right after a conference?
There’s plenty of great iOS conferences out there, so choosing which one to attend can be difficult. This post will help you find the best matches for you in 2017.
Picking the top 10 is a hard task. This is why I reached out to the raywenderlich.com team and the entire iOS community to ask for their feedback in developing this list.
I’d love to hear from you too – please add a comment below if you think there’s an interesting conference missing in this list.
Let’s dive into the list!
WWDC is Apple’s official conference. Tickets are usually sold using a lottery system, so getting one – even if you can afford it – is a matter of luck.
The content is top notch, and if you want to keep up with new APIs and frameworks watching the key sessions is a must.
However, in recent years Apple started releasing videos publicly a few hours after the presentations. So why should you attend if you can watch the same content on your comfy couch?
In my opinion, despite the availability of the videos, there’s still two good reasons to attend WWDC:
“It was amazing, met so many great people , got more from speaking to people after the sessions than I did from the main contents . Everyone is in the same boat as all the content is new. Totally set me up for the next year of development.” —Russ Freeman
“WWDC is a deeply rewarding experience. Not only do you get to chat with the engineers who make your favorite (or least favorite) frameworks, but you get awesome lunch time talks. The year I went I saw talks from JJ Abrahams and Levar Burton. These are incredibly rewarding experiences. This is the toughest conference to get into and once you’ve been, you see why.” —Ish
In case you didn’t manage to buy a ticket for WWDC, you don’t necessarily have to stay at home!
During WWDC week, San Francisco is full of events related to the Apple ecosystem. Check out WWDCParties to have an idea.
The main event is AltConf, organized by the community for the community. While most of the presentations are focused on Apple’s ecosystem the topics are very diverse and include design, business, marketing and team management. Here’s the list of videos from the last edition.
“Free stuff can be hit or miss. When something is expensive, you kind of figure, it must be good, or else they couldn’t get away with charging so darn much for it. But when it comes to conferences, AltConf is the standout exception.” —Scott Gardner
“AltConf demonstrates the best of our community. Good people volunteer their time and skills to provide an inclusive, accessible event to everyone who wishes to take part in the frenzy that is WWDC week, but can’t get or afford a ticket to Apple’s event. One can only hope that Apple appreciates how much more enthusiasm and goodwill spreads throughout San Francisco every June because of AltConf’s continued presence.” —Joe Cieplinski
While WWDC has the highest concentration of iOS developers, 360 has the highest concentration of indie developers. 360iDev has been around for seven years and it’s very well organized.
To get an idea of the topics, here’s the videos from the previous edition.
“360iDev is my favorite large Apple conference. The community is fantastic. They are extremely welcoming and friendly. Every year I meet great new friends and see friends from previous years. I’ve learned so much from 360iDev. It has had a profound impact on my career.” —Ryan Poolos
“360iDev is probably my favorite iOS conference and the one I’ve reliably attended the past years. The organizers do a great job cultivating a great set of talks with a mix of well-known community leaders and those you may not have ever heard of. Everyone is super-approachable and the entire event is a lot of fun.” —Aaron Douglas
For transparency: RWDevCon is a conference organized by our team. Expect some bias :]
RWDevCon is different form other conferences on this list, in that the focus is on hands-on tutorials. The idea is rather than just watching someone speak, you code along with them.
The speakers put a huge effort in preparing the presentations, which are edited and rehearsed multiple times to get a superb quality. To give you an idea, the next conference is this March and the speakers started preparing the presentations in November.
Developers seem to like the tutorial format of the conference, given than last two editions were sold out!
“We wanted to see what a live version of our favorite tutorials would be like. The conference led to several positive transformations in our lives, and we’ve never met so many interesting people at one event before.” —The Catterwauls
“RWDevCon was my first iOS Conference, and if I had to define it in one word I’d choose “Inspirational”. It’s so cool to travel 1,883 miles (Mexico City – Washington D.C.) to meet new and old friends, with excellent tutorial sessions, a great party and my favorites: the Inspirational Talks.” —Rich Zertuche
try! Swift is a fairly new series of conferences, started by Natasha the Robot just last year.
The first edition was in Japan, a country with a big community but with very few international events. Thanks to the help of the community, the first event was so successful that next year there will be three conferences: in Tokyo, New York and Bangalore.
“try! Swift NYC was the best tech conference that I’ve been to… including WWDC. I loved the ability to connect with the speakers, and attendees. The topics were extremely relevant to my everyday work. Both the speakers and attendees were incredibly diverse which was awesome and really enriched the experience.” —Ish
“try! Swift captures all the enthusiasm for programming that Natasha obviously has, and packs it into a fun-filled conference, featuring diverse speakers from all around the world. Attendees come away not only with a ton of practical information, but also with new perspectives on the emotional and community aspects of working in our field.” —Daniel Jalkut
Release Notes is focused on the Apple ecosystem but with an interesting twist – it’s more focused on the business of making apps.
The conference features presentations by designers and developers that have a great business sense and are happy to share their experiences in building great products.
“Release Notes in Indie was a blast! I loved every minute of it. The selection of speakers was well thought out and the the lined up talks were excellent and inspiring. I really liked the idea of splitting up into small groups and dining at various restaurants around Indianapolis. That was beneficial for introverts who had nothing to do but network. The conference gave ideas, knowledge and inspiration for doing iOS and macOS app business.” —Julia Petryk
“The conference content is focused on the business aspects of app development and has a diverse group of speakers. The quality of the speakers is excellent. The conference is unique in providing time to make connections. I highly recommend Release Notes for both the quality of the content and the quality of conversations between talks and at the social events.” —Chris Morrissey
Indie DevStock is a new entry. The first edition was held in September and organized by one of our team members: Tammy Coron.
The spirit of the conference is to share, learn and have fun with the people of the indie community.
“I attended Indie DevStock organized by Tammy and Angela in Nashville, TN. Even though this was the first IndieDevStock conference Tammy and Angela have proved that they are veterans in organizing an amazing event. Apart from the unbelievably amazing venue, the speakers were very knowledgeable and passionate about development. The conference had a right balance of inspirational and technical talks. I had an amazing time meeting new people and presenting at IndieDevStock and I am already looking forward for IndieDevStock 2017.” —Mohammad Azam
“Indie DevStock was my favorite iOS conference that I have been to this year. These presentations were not just about software development. They were about sharing ideas and experiences that are actionable and inspiring for attendees like me. The conference organizers also did a wonderful job in offering plenty of opportunities to meet and talk to new people at a great venue.” —Larry Trutter
App Builders is another new entry in this list. It’s a conference held in Switzerland with a focus on mobile technologies in general.
The conference also features an interesting session named Stories, where speakers interact with the audience.
“Top-notch Speakers held amazing talks. App Builders was held in a modern Venue and Zurich itself was great to hang out afterwards. Next year it will move to Lausanne, and I am already looking forward to it!” —Stefan Völker
“The first edition of AppBuilders was a blast. Departing from traditional conferences focusing on a single platform, it brought a mix of high quality speakers from both iOS and Android sides of the mobile space. Some of the speakers were uniquely entertaining, and the venue offered ample space to mingle with peers. First announced speakers for the 2017 edition are already shaping it as another major conference in the mobile space.” —Florent Pillet
The Pragma Conference takes place in Italy and gathers developers from all over the world. Topics are well balanced between design and development, while pre-conference workshops offer the chance to learn hands-on new techniques.
“I’ve really enjoyed Pragma Conference, it was already well done in 2014 but the 2016 edition was amazing, very well organized with a great lineup of speakers. Visiting Italy is always a pleasure, especially with all the great food and wine available.” —Krzysztof Zabłocki
“I attended Pragma conference. I love the community around this event, the good organisation and how the organisers create a “relationship” between each talks and how they chosen innovative and interesting topics.” —Alessio Roberto
CocoaConf is a conference on wheels. They run multiple events in different cities of the USA, so it’s easier for people to attend. Check out which is the closest to you.
“I love going to CocoaConf because it has that small community feel. The conference is well-run, the sessions are high quality, and I always enjoy the conversations with fellow developers.” —Ben Scheirman
“Cocoaconf is a small conference dedicated to cocoa development. It happens a few times a year at different cities in the US. The presentations range from intro (including full day tutorials on Swift and iOS) to advanced (covering category theory and machine learning) and each city has a different mix of sessions and attendees. Cocoaconf is a humble conference with great people and is very reasonably priced. It’s two (or three if you go to the tutorials) jam-packed days of Xcode and learning.” —Josh Smith
Picking 10 conferences to highlight was hard. Here are a few honorable mentions:
If you’re unsure which to choose, here’s my advice – in a handy flowchart!
It’s very hard to be neutral about a conference. That’s why we have asked our awesome community to provide quotes and feedback about the conference attended this year. Thank you all!
Did you attend a conference in 2016? Please add a comment below and let us know your experience. It will help others to find the best event.
The team and I hope to see you at some iOS conferences in 2017!
The post Top 10 iOS Conferences in 2017 appeared first on Ray Wenderlich.
Note: This tutorial has been updated for Xcode 8.2, Swift 3 and Fastlane 2.6.0 by Lyndsey Scott. The original tutorial was written by Satraj Bambra.
It’s that wonderful moment: You’ve poured days, weeks, or maybe even months into building an amazing app, and it’s finally ready to share with the world. All you have to do is submit it to the App Store. How hard could that be, right?
Cue mountains of grunt work: Capturing tons of screenshots, fighting Xcode provisioning, uploading to the App Store and constant mindless work! Aargh!
Wouldn’t it be nice if there was a better way? If only you could run a single command that took all your screenshots, on all your supported devices, in every supported language automagically. If only there were a single command to upload those screenshots, generate your provisioning profiles and submit your app. Think of all the time you’d save!
Well, you’re in luck. :] Thanks to the amazing Felix Krause, there’s now a tool to do all this and more! It’s called fastlane, and it’ll become your new best friend.
In this fastlane tutorial, you’ll learn how to use fastlane to deploy an app to the App Store. You’re in for a fast ride, so buckle up and hang on tight!
Note: This tutorial uses the command line extensively. While you don’t need to be a Terminal expert, you’ll need to have basic knowledge for how the command line works.
This tutorial also assumes you know about code signing and iTunes Connect. If you’re unfamiliar with these, please read this tutorial series first.
Download the starter project here, and save it to a convenient location.
mZone, the sample app you’ll use in this tutorial, is a simple poker calculator for No Limit Texas Hold’em tournaments. It displays a recommended action based on your chip count and the current big blind level:
Open the project in Xcode to see the app yourself, then navigate to the mZone Poker target’s Build Settings. In the Product Bundle Identifier field, you’ll find com.mZone.mZone-Poker-xxx:
Replace “xxx” with your email address sans “@” and “.” so that the bundle identifier for your project is different from every other app identifier on iTunes Connect.
To get fastlane up and running the following are required:
Since fastlane is a collection of Ruby scripts, you must have the correct version of Ruby installed. Fortunately, OS X 10.9 (Mavericks) and later come with Ruby 2.0 by default. You can confirm this by opening Terminal and entering the following command:
ruby -v |
To check whether the Xcode CLT are installed, enter the following into Terminal:
xcode-select --install |
If Xcode CLT are already installed, you will get this error: command line tools are already installed, use "Software Update" to install updates
. If not, it will install Xcode CLT for you.
With the prerequisites completed, you’re ready to install fastlane. Enter the following command:
sudo gem install -n /usr/local/bin fastlane --verbose |
/usr/local/bin
is still writeable which is why you’re installing fastlane there.After entering your system password, you will see a bunch of activity in your Terminal window, indicating the installation is in progress. This could take a few minutes, so grab some coffee, walk your dog or brush up on your zombie-fighting tactics. :]
When the installation completes, you’ll be ready to set up your project to use fastlane. But before you do, let’s take a high-level look at the fastlane tools.
To work its magic, fastlane brings the following set of tools all under one roof:
You will be using many of these tools in the deployment process of the sample app.
That’s enough theory– it’s time to put fastlane in the fast lane!
First, open Terminal and cd
into your mZone project location. For example, if you’ve added the mZone folder to your desktop, you can enter:
cd ~/Desktop/mZone |
to set mZone as the working directory.
Once you are in the mZone folder, enter the following command:
fastlane init |
Note: If you get a “permission denied” error, you will need to prefix this command with sudo
.
Next, enter your Apple ID to kickstart the process.
fastlane uses deliver to sign you into both iTunes Connect and the Apple Developer Portal, and it also verifies an app exists in your account with a matching app identifier. Since this is a new app, it won’t exist, and it will need to be created.
Connection reset by peer - SSL_Connect |
try updating your ruby version as fastlane suggests to do.
Unless you’re already using rbenv or rvm (which are Ruby version managers), the easiest way to do this is via Homebrew.
First, install Homebrew by entering this Terminal command:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" |
Next, install Ruby using the `brew` command:
brew update && brew install ruby |
Homebrew might also tell you that you need to run brew link --overwrite ruby
. You may also need to open a new Terminal session. Then, install fastlane again without specifying an install path:
sudo gem install fastlane --verbose |
Lastly, run fastlane init
again, and it should be able to create the app on iTunes Connect.
You should see the following output:
This app identifier doesn't exist on iTunes Connect yet, it will be created for you This app identifier doesn't exist on the Apple Developer Portal yet, it will be created for you Please confirm the above values (y/n) |
Enter “y” to confirm the detected values.
Since I’ve already created an app called “mZone Poker” on iTunes Connect, you’ll next be prompted:
It looks like that mZone Poker has already been taken by someone else, please enter an alternative App Name: |
Type in a unique app name. (Similar to my recommendation for the bundle ID, I recommend “mZone Poker” followed by your email address sans “@” and “.” to ensure the app name is unique.)
After some time (as little as 30 seconds and as much as 30 minutes depending on the emotional state of Apple’s occasionally volatile servers), you’ll be informed that your app was created on iTunes Connect.
Open the mZone project folder. You will notice that your project now has a fastlane folder:
The relevant files are:
Within the mZone Project folder, navigate to fastlane\metadata. You will notice a bunch of text files there that contain common App Store items like the description, keywords, categories, etc. These files are used to set your app’s metadata information displayed on the App Store.
Open en-US/description.txt and add the following text:
mZone is a simple poker calculator for No Limit Texas Hold’em tournaments that displays a recommended course of action based on your chip count and the current big blind level. |
Add the following to keywords.txt:
Poker, Cards, Gambling |
Check that name.txt already contains the name of your app, then type http://www.raywenderlich.com
into both privacy_url.txt and support_url.txt.
While this app supports both French and English, only the en-US folder exists.
To fix this, simply make a copy of the folder and call it fr-FR. In the interests of keeping this fastlane tutorial shorter, we won’t actually provide real French translations now. Your project folder should now look like this:
Next, in the metadata folder:
Copyright (c) 2016 Razeware LLC
to copyright.txtGames
to primary_category.txtCard
to primary_first_sub_category.txtCasino
to primary_second_sub_category.txtThen, in the same folder, use your favorite text/code editor to create a json file named itunes_rating_config.json containing the following:
{ "CARTOON_FANTASY_VIOLENCE": 0, "REALISTIC_VIOLENCE": 0, "PROLONGED_GRAPHIC_SADISTIC_REALISTIC_VIOLENCE": 0, "PROFANITY_CRUDE_HUMOR": 0, "MATURE_SUGGESTIVE": 0, "HORROR": 0, "MEDICAL_TREATMENT_INFO": 0, "ALCOHOL_TOBACCO_DRUGS": 0, "GAMBLING": 2, "SEXUAL_CONTENT_NUDITY": 0, "GRAPHIC_SEXUAL_CONTENT_NUDITY": 0, "UNRESTRICTED_WEB_ACCESS": 0, "GAMBLING_CONTESTS": 0 } |
This iTunes rating configuration lets iTunes Connect know that out of all the ratings criteria, the app only contains “frequent/intense” simulated gambling (i.e. value = 2). This file gives iTunes Connect the information it requires to give the app the age rating Apple has deemed appropriate.
And lastly, download the App Store icon here and also add it to the metadata directory.
Congratulations! You’ve added all the metadata required for submission. It’s time to to start using fastlane. :]
Open Fastfile in a text editor of your choice, disable smart quotes if your text editor supports them, then replace the contents of the file with the following code:
# This is the minimum version number required. # Update this, if you use features of a newer version fastlane_version "2.6.0" default_platform :ios platform :ios do # 1 desc "Creating a code signing certificate and provisioning profile" # 2 lane :provision do # 3 produce( app_name: 'YOUR_UNIQUE_APP_NAME', language: 'English', app_version: '1.0', sku: '123abc' ) # 4 cert # 5 sigh(force: true) end error do |lane, exception| # This block is called if there was an error running a lane. end end |
Replace YOUR_UNIQUE_APP_NAME
with the app name you specified earlier. Your iTC username and the app identifier are loaded automatically from the Appfile, so you don’t need to provide them here.
Also replace the fastlane version with the most recent version if it’s no longer 2.6.0 (and it will most probably not be 2.6.0 since fastlane is well-maintained and updated frequently).
Note: To find out your current version, enter fastlane -v
into the terminal.
If at any point, fastlane tells you to update to a newer version, but running sudo gem update fastlane
outputs that there is “Nothing to update,” perhaps the ruby manager you’re using isn’t up to date. Run gem sources --add https://rubygems.org/
to install Ruby Gems since it’s likely to produce the most current information.
If you’ve never seen Ruby before, this may look like gibberish to you, so here’s what this code does:
force: true
, a new provisioning profile is created on each run; this ensures you’re always using the correct code signing certificate.Note: sigh
creates an App Store distribution profile by default. If you want to create an ad hoc profile you would need to specify sigh(adhoc: true)
. For a development profile it would be sigh(development: true)
. For simplicity, you’re only making a distribution profile in this tutorial.
Woohoo! You have now created your very first lane. Save the file, open Terminal inside your project folder and enter the following command:
fastlane provision |
This tells fastlane to run your provision lane.
After a minute or so, fastlane asks for your iTunes Connect password, which is safely stored in your computer’s keychain. Enter your password, and upon successful completion, your Terminal window should output the “fastlane summary”:
Note: If you see any errors, particularly one about “Creation of apps of this type is not available,” log in to iTunes Connect and make sure you don’t have any updated agreements waiting to be signed.
Log in to iTunes Connect, and voila! Your app has already been created. How cool is that?
In order to use the provisioning profile you just created, you’ll have to make modifications in Xcode. Open the mZone Poker.xcodeproj, navigate to mZone Poker Target\Build Settings\Code Signing and set your Provisioning Profile to the newly created <app ID> AppStore. Then choose your Code Signing Identity based on this provisioning profile as shown below:
Note that your code signing identity will be based on the identities available in the provisioning profile. By doing this, your app can use the newly created provisioning profile when gym builds the IPA file.
Navigate to General, deselect Automatically manage signing then set both the Signing (Debug) and Signing (Release) to that same provisioning profile you just specified.
With just a few commands and settings, you have added an app to your Dev Portal and iTunes Connect, created a provisioning profile and code signed your app. You have already saved yourself hours of work! :]
When submitting an app, taking screenshots is by far the most tedious task. The more useful your app is (meaning the more devices and languages it supports), the more hours of valuable time you’ll burn taking screenshots. Painful!
mZone supports two languages, English and French, and four screen sizes. If you had to take five screenshots per device for each language and screen size, that would be 40 screenshots! With fastlane, however, you can do all this by running a single command.
But first, set up the project for snapshot by entering the following in Terminal:
fastlane snapshot init |
A Snapfile file will now appear in your fastlane folder. Snapfile lets you specify the devices and languages you want to provide screenshots for.
Open Snapfile and replace the contents of the file with the following code:
# A list of devices you want to take the screenshots from devices([ "iPhone 5", "iPhone 6", "iPhone 6 Plus" ]) # A list of supported languages languages([ 'en-US', 'fr-FR' ]) # Where should the resulting screenshots be stored? output_directory "./fastlane/screenshots" # Clears previous screenshots clear_previous_screenshots true # Latest version of iOS ios_version '10.1' |
This simply sets the devices you want to support, the languages the app supports and specifies the location of the current screenshots directory. clear_previous_screenshots
will clear any previously taken screenshots.
Wait a second… Why isn’t the iPhone 7 and iPhone 7 Plus on that device list? That’s because the screenshots on iTunes Connect are categorized by display size, not device; and since the iPhone 6 & 7 both have 4.7 inch displays, and the iPhone 6 Plus & 7 Plus both have 5.5 inch displays, including both the 6’s and 7’s in our screenshots folder would produce duplicate screenshots in iTunes Connect.
Save the file and close it.
Return to your terminal and note the instructions that appeared after running fastlane snapshot init
:
Open your Xcode project and make sure to do the following: 1) Add a new UI Test target to your project 2) Add the ./fastlane/SnapshotHelper.swift to your UI Test target You can move the file anywhere you want 3) Call `setupSnapshot(app)` when launching your app let app = XCUIApplication() setupSnapshot(app) app.launch() 4) Add `snapshot("0Launch")` to wherever you want to create the screenshots |
So that’s what you’ll do next.
Again open mZone Poker.xcodeproj
in Xcode, then navigate to File\New\Target, within the iOS tab’s Test section, select iOS UI Testing Bundle then hit Next\Finish.
Return to your mZone folder’s fastlane directory then drag SnapshotHelper.swift into the newly created mZone PokerUITests folder in Xcode.
Open mZone_PokerUITests.swift, remove both the setUp
and tearDown
methods, then and add the following code within testExample
:
// 1 let app = XCUIApplication() setupSnapshot(app) app.launch() // 2 let chipCountTextField = app.textFields["chip count"] chipCountTextField.tap() chipCountTextField.typeText("10") // 3 let bigBlindTextField = app.textFields["big blind"] bigBlindTextField.tap() bigBlindTextField.typeText("100") // 4 snapshot("01UserEntries") // 5 app.buttons["what should i do"].tap() snapshot("02Suggestion") |
This code will help create your screenshots at certain points within the app’s execution. Here’s what you’re doing bit by bit:
Close Xcode, open Fastfile and add the following code right above error do |lane, exception|
:
desc "Take screenshots" lane :screenshot do snapshot end |
Here you’re creating a new lane called screenshot that uses snapshot to take screenshots as specified by the Snapfile, which you just edited.
Save the file, return to Terminal and enter:
fastlane screenshot |
Now watch … the screenshots are captured without you having to do anything else! Bask in the joy of skipping grunt work. :]
After the process finishes, the html file screenshots.html should automatically open upon screenshot completion and you can scroll through all the screenshots fastlane has taken.
You now have all your device screenshots in both English and French in just one Terminal command – it doesn’t get better than that!
Note: If you see warnings about ambiguous simulator names, you may need to delete some of your simulators or change the contents of your Snapfile.
snapshot can work in tandem with Xcode’s UI tests to give you screenshots of specific parts of the app as well! All the more reason to use tests. :]
Sure, it’s nice that you don’t have to create screenshots anymore, but the most time-consuming part of the submission process is actually building and signing the app. Guess what – fastlane can do this as well!
Open Fastfile and add the following code after the end
of the screenshot lane:
desc "Create ipa" lane :build do increment_build_number gym end |
This creates a lane called build which uses increment_build_number to increase the build number by 1 (so each build number is unique per iTunes Connect’s upload requirement) and gym to create a signed ipa file.
Save Fastfile, then inside the mZone project directory in Terminal, enter the following command:
fastlane build |
This calls the build lane you added above that starts the build process. Once it successfully completes, open the mZone Project folder. You should see the signed ipa file:
Done! This one command takes care of the arguably most frustrating and least-understood part of iOS development.
To upload the screenshots, metadata and the IPA file to iTunes Connect, you can use deliver, which is already installed and initialized as part of fastlane.
Open Fastfile and add the following code after the end
of the build lane:
desc "Upload to App Store" lane :upload do deliver end |
Open your Terminal window and enter the following command:
fastlane upload |
With this command, fastlane creates a preview of what it will upload in the form of an HTML page.
If everything looks good, type y into Terminal in answer to the question “Does the Preview on path ‘./Preview.html’ look okay for you? (y/n)”.
At this point you can just chill out and let the computer do all the work for you. :]
After the successful completion of the process, your Terminal window should look like this:
Log in to your iTunes Connect account. All screenshots, description and build version 1.0 should be uploaded and ready.
All that’s left for you to do is click the big “Submit for Review” button, and you’re done!
Wait just a minute… What’s all this about having to manually log in and click a button? I thought we were automating all the things.
Well, it turns out deliver can automatically submit your app for review as well!
First you need to update the upload lane:
desc "Upload to App Store and submit for review" lane :upload do deliver( submit_for_review: true ) end |
Then you need to replace Deliverfile‘s contents with the following so it contains all the additional info needed for submission:
# 1 price_tier 0 # 2 app_review_information( first_name: "YOUR_FIRST_NAME", last_name: "YOUR_LAST_NAME", phone_number: "YOUR_PHONE_NUMBER", email_address: "YOUR_EMAIL_ADDRESS", demo_user: "N/A", demo_password: "N/A", notes: "No demo account needed" ) # 3 submission_information({ export_compliance_encryption_updated: false, export_compliance_uses_encryption: false, content_rights_contains_third_party_content: false, add_id_info_uses_idfa: false }) # 4 automatic_release false # 5 app_icon './fastlane/metadata/AppIcon.png' # 6 app_rating_config_path "./fastlane/metadata/itunes_rating_config.json" |
Here you do the following:
Return to your terminal and again run:
fastlane upload |
After several minutes, fastlane should indicate that you’ve successfully submitted your app for review and iTunes Connect should confirm it!
Note: As of fastlane version 1.111.0, the text output may incorrectly indicate your app has been submitted successfully even if there’s a problem during submission. So, you should always verify your app shows as Waiting for Review in iTunes Connect.
Also note that in order to delete mZone from iTunes, your app must have been approved by an app review.
After it’s been approved, you can briefly release and then select Remove from Sale in the pricing and availability section. Then, navigate to the App Information screen, scroll to the bottom and select Delete to remove the app permanently.
You currently have separate lanes for provisioning, screenshots, building and uploading to the App Store. While you could always call each of these one by one, you don’t want to do that, right?
Oh no, you want ONE command that does everything.
Open Fastfile and add the following code after the end
of the upload lane:
desc "Provision, take screenshots, build and upload to App Store" lane :do_everything do provision screenshot build upload end |
As this lane’s description and name implies, this lane takes care of everything. :]
Try it out by running this Terminal command:
fastlane do_everything |
Great work! Just let fastlane do all the heavy lifting while you sit back and relax.
Caution: it might feel weird having so much time on your hands compared to your pre-fastlane existence, but trust us, you’ll get used to it. :]
Download the final project for this tutorial here (sans code signing, your app name, provisioning profile, certificate, ipa file, etc) to see how the app, metadata and fastlane files stack up to yours.
Today you learned how to use fastlane to deploy your apps and save you a bunch of time. Keep in mind that although there are a few steps necessary to get fastlane working, a lot of that is just first time setup.
fastlane also offers a ton of integrations that let you customize your lanes to provide real time feedback on Slack, perform unit tests and deploy TestFlight builds.
To learn more about this fantastic tool, take a look at the official fastlane website.
I hope you enjoyed this fastlane tutorial, and I look forward to seeing how you use fastlane in your deployment pipeline. If you have any questions or comments, please join the forum discussion below!
The post fastlane Tutorial: Getting Started appeared first on Ray Wenderlich.
Augmented reality is a cool and popular technique where you view the world through a device (like your iPhone camera, or Microsoft HoloLens), and the device overlays extra information on top of the real-world view.
I’m sure you’ve seen marker tracking iOS apps where you point the camera at a marker and a 3D model pops out.
In this augmented reality iOS tutorial, you will write an app that takes the user’s current position and identifies nearby points of interest (we’ll call these POIs). You’ll add these points to a MapView
and display them also as overlays on a camera view.
To find the POIs, you’ll use Google’s Places API, and you’ll use the HDAugmentedReality
library to show the POIs on the camera view and calculate the distance from the user’s current position.
This tutorial assumes you have some basic familiarity with MapKit. If you are completely new to MapKit, check out our Introduction to MapKit tutorial.
First download the starter project and make yourself familiar with the content. Select the Places project in the project navigator, the Places target in the editing pane, and in the General tab, within the Signing section, set Team to your developer account. Now you should be able to compile the project. The Main.storyboard contains a Scene with a MapView and a UIButton already hooked up for you. The HDAugmentedReality
library is included and there are files PlacesLoader.swift and Place.swift. You use them later to query a list of POIs from Googles Places API and map the result into a handy class.
Before you can do anything else, you need to obtain the user’s current location. For this, you’ll use a CLLocationManager
. Open ViewController.swift and add a property to ViewController
below the mapView
outlet and call it locationManager
.
fileprivate let locationManager = CLLocationManager() |
This just initializes the property with a CLLocationManager
object.
Next add the following class extension to ViewController.swift.
extension ViewController: CLLocationManagerDelegate { } |
Before you can get the current location you have to add a key to the Info.plist. Open the file and add the key NSLocationWhenInUseUsageDescription with a value of Needed for AR. The first time you try to access location services iOS shows an alert with this message asking the user for the permission.
Now that everything is prepared, you can get the location. To do this, open ViewController.swift and replace viewDidLoad()
with the following:
override func viewDidLoad() { super.viewDidLoad() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters locationManager.startUpdatingLocation() locationManager.requestWhenInUseAuthorization() } |
This is a basic configuration for the locationManager
. The manager needs a delegate to notify when it has updated the position of the iDevice. You set it to your view controller using self
. Then the manager needs to know how accurate the position should be. You set it to kCLLocationAccuracyNearestTenMeters
, which will be accurate enough for this example project. The last line starts the manager and asks the user to grant permission to access location services if it was not already granted or denied.
Note: For desiredAccuracy
, you should use the lowest accuracy that is good enough for your purposes. Why?
Say you only need an accuracy of some hundred meters – then the LocationManager
can use phone cells and WLANs to get the position. This saves battery life, which you know is a big limiting factor on iDevices. But if you need a better determination of the position, the LocationManager
will use GPS, which drains the battery very fast. This is also why you should stop updating the position as soon as you have an acceptable value.
Now you need to implement a delegate method to get the current location. Add the following code to the CLLocationManagerDelegate
extension in ViewController.swift:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { //1 if locations.count > 0 { let location = locations.last! print("Accuracy: \(location.horizontalAccuracy)") //2 if location.horizontalAccuracy < 100 { //3 manager.stopUpdatingLocation() let span = MKCoordinateSpan(latitudeDelta: 0.014, longitudeDelta: 0.014) let region = MKCoordinateRegion(center: location.coordinate, span: span) mapView.region = region // More code later... } } } |
Walking through this method step-by-step:
LocationManager
updates the location, it sends this message to its delegate, giving it the updated locations. The locations array contains all locations in chronological order, so the newest location is the last object in the array. First you check if there are any locations in the array and if there is at least one you take the newest. The next line gets the horizontal accuracy and logs it to the console. This value is a radius around the current location. If you have a value of 50, it means that the real location can be in a circle with a radius of 50 meters around the position stored in location
.if
statement checks if the accuracy is high enough for your purposes. 100 meters is good enough for this example and you don’t have to wait too long to achieve this accuracy. In a real app, you would probably want an accuracy of 10 meters or less, but in this case it could take a few minutes to achieve that accuracy (GPS tracking takes time).mapView
to the location.Build and run on your device, and keep your eyes on the console to see how the locations come in and how the accuracy gets better and better. Eventually you’ll see the map zoom to an area centered on your current location.
verticalAccuracy
that is the same as horizontalAccuracy
, except that it’s for the altitude of the position. So a value of 50 means that the real altitude can be 50 meters higher or lower. For both properties, negative values are invalid.
Now that you have a current location, you can load a list of POIs. To get this list, you’ll use Google’s Places API.
Google Places API requires you to register for access. If you’ve already created a Google account in the past to access APIs like Maps, go here and select Services. Then skip the following steps until you reach Enabling the Places API.
However, if you’ve never used Google Places API before, you’ll need to register for an account.
You can skip the second screen and on the third, click on Back to Developer Consoles.
Now click on Project/Create Project in the upper left and enter a name for your project. To enable the Places API, search for the line Google Places API Web Service and click the link. Click ENABLE on the top. Now click on Credentials and follow the steps to receive your API key.
Now that you have an API key open PlacesLoader.swift and find the line let apiKey = "Your API key"
and replace the value with your API key.
This is a great time for a new test, but before you build and run, open ViewController.swift and add two new properties below the locationManager
property.
fileprivate var startedLoadingPOIs = false fileprivate var places = [Place]() |
startedLoadingPOIs
tracks if there is a request in progress, it can happen that the CLLocationManagerDelegate
method is called multiple times even after you stopped updating the location. To avoid multiple requests you use this flag. places
stores the received POIs.
Now find locationManager(manager: didUpdateLocations:)
. Inside the if
statement, add the following code, right after the “More code later …” comment:
//1 if !startedLoadingPOIs { startedLoadingPOIs = true //2 let loader = PlacesLoader() loader.loadPOIS(location: location, radius: 1000) { placesDict, error in //3 if let dict = placesDict { print(dict) } } } |
This starts loading a list of POIs that are within a radius of 1000 meters of the user’s current position, and prints them to the console.
Build and run, and watch the console’s output. It should look like this, but with other POIs:
{ "html_attributions" = ( ); "next_page_token" = "CpQCAgEAAJWpTe34EHADqMuEIXEUvbWnzJ3fQ0bs1AlHgK2SdpungTLOeK21xMPoi04rkJrdUUFRtFX1niVKCrz49_MLOFqazbOOV0H7qbrtKCrn61Lgm--DTBc_3Nh9UBeL8h-kDig59HmWwj5N-gPeki8KE4dM6EGMdZsY1xEkt0glaLt9ScuRj_w2G8d2tyKMXtm8oheiGFohz4SnB9d36MgKAjjftQBc31pH1SpnyX2wKVInea7ZvbNFj5I8ooFOatXlp3DD9K6ZaxXdJujXJGzm0pqAsrEyuSg3Dnh3UfXPLdY2gpXBLpHCiMPh90-bzYDMX4SOy2cQOk2FYQVR5UUmLtnrRR9ylIaxQH85RmNmusrtEhDhgRxcCZthJHG4ktJk37sGGhSL3YHgptN2UExsnhzABwmP_6L_mg"; results = ( { geometry = { location = { lat = "50.5145334"; lng = "8.3931416"; }; viewport = { northeast = { lat = "50.51476485000001"; lng = "8.393168700000002"; }; southwest = { lat = "50.51445624999999"; lng = "8.3930603"; }; }; }; icon = "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png"; id = c64c6c1abd02f4764d00a72c4bd504ab6d152a2b; name = "Schlo\U00df-Hotel Braunfels"; photos = ( { height = 4160; "html_attributions" = ( "<a href=\"https://maps.google.com/maps/contrib/113263673214221090182/photos\">Ralph Peters</a>" ); "photo_reference" = "CoQBdwAAABZT7LYlGHmdep61gMOtwpZsYtVeHRWch0PcUZQOuICYHEWnZhKsSkVdMLx3RBTFIz9ymN10osdlqrPcxhxn-vv3iSsg6YyM18A51e3Sy0--jO2u4kCC05zeMyFp-k7C6ygsDsiOK4Dn3gsu_Bf5D-SZt_SrJqkO0Ys6CwTJ75EPEhDcRLUGnYt2tSODqn_XwxKWGhRMrOG9BojlDHFSoktoup1OsbCpkA"; width = 3120; } ); "place_id" = ChIJdadOzRdPvEcRkItOT1FMzdI; rating = "3.8"; reference = "CmRSAAAAgvVO1e988IpXI7_u0IsRFCD1U1IUoSXlW7KfXvLb0DDtToodrGbiVtGZApSKAahnClm-_o-Nuixca_azt22lrT6VGwlJ1m6P0s2TqHAEmnD2QasXW6dCaDjKxesXCpLmEhAOanf32ZUsfX7JNLfNuuUXGhRrzQg-vvkQ0pGT-iSOczT5dG_7yg"; scope = GOOGLE; types = ( lodging, "point_of_interest", establishment ); vicinity = "Hubertusstra\U00dfe 2, Braunfels"; }, |
Pardon my french! :]
If you get NULL back for a response, try increasing the radius to a larger value.
So far, your app can determine a user’s position and load a list of POIs inside the local area. You have a class that can store a place from this list, even if you don’t use it at the moment. What’s really missing is the ability to show the POIs on the map!
To make an annotation on the mapView
, you need another class. So go to File\New\File…, choose the iOS\Swift File and click Next. Name the file PlaceAnnotation.swift and click Create.
Inside PlaceAnnotation.swift replace the contents with the following:
import Foundation import MapKit class PlaceAnnotation: NSObject, MKAnnotation { let coordinate: CLLocationCoordinate2D let title: String? init(location: CLLocationCoordinate2D, title: String) { self.coordinate = location self.title = title super.init() } } |
Here you’ve made the class implement the MKAnnotation
protocol and defined two properties and a custom init method.
Now you have everything you need to show some POIs on the map!
Go back to ViewController.swift and complete the locationManager(manager: didUpdateLocations:)
method. Find the print(dict)
line and replace it with this:
//1 guard let placesArray = dict.object(forKey: "results") as? [NSDictionary] else { return } //2 for placeDict in placesArray { //3 let latitude = placeDict.value(forKeyPath: "geometry.location.lat") as! CLLocationDegrees let longitude = placeDict.value(forKeyPath: "geometry.location.lng") as! CLLocationDegrees let reference = placeDict.object(forKey: "reference") as! String let name = placeDict.object(forKey: "name") as! String let address = placeDict.object(forKey: "vicinity") as! String let location = CLLocation(latitude: latitude, longitude: longitude) //4 let place = Place(location: location, reference: reference, name: name, address: address) self.places.append(place) //5 let annotation = PlaceAnnotation(location: place.location!.coordinate, title: place.placeName) //6 DispatchQueue.main.async { self.mapView.addAnnotation(annotation) } } |
Here’s a closer look at what’s happening above:
guard
statement checks that the response has the expected formatPlace
object is created and appended to the places
array.PlaceAnnotation
that is used to show an annotation on the map view.Build and run. This time, some annotations appear on the map and when you tap one, you’ll see the name of the place. This app looks nice for now, but where is the augmented reality?!
You’ve done a lot of work so far, but they’ve been necessary preparations for what you’re about to do: it’s time to bring augmented reality to the app.
You may have seen the Camera button in the bottom right. Currently nothing happens if you tap the button. In this section you’ll add some action to this button and show a live preview of the camera with some augmented reality elements.
To make your life easier you’ll use the HDAugmentedReality
library. It is already included in the starter project you downloaded earlier, if you want to grab the latest version you can find it on Github, but what can this lib do for you?
First, HDAugmentedReality
handles the camera captioning for you so that showing live video is easy. Second, it adds the overlays for the POIs for you and handles their positioning.
As you’ll see in a moment, the last point is perhaps your greatest boon, because it saves you from having to do some complicated math! If you want to know more about the math behind HDAugmentedReality
, continue on.
If, on the other hand, you want to dig immediately into the code, feel free to skip the next two sections and jump straight to Start Coding.
You’re still here, so you want to learn more about the math behind HDAugmentedReality
. That’s great! Be warned, however, that it’s a bit more complicated than standard arithmetic. In the following examples, we assume that there are two given points, A and B, that hold the coordinates of a specific point on the earth.
A point’s coordinates consist of two values: longitude and latitude. These are the geographic names for the x- and y-values of a point in the 2D Cartesian system.
If you have a look at a standard globe, you’ll see lines of longitude that go from pole to pole – these are also known as meridians. You’ll also see lines of latitude that go around the globe that are also called parallels. You can read in geography books that the distance between two parallels is around 111 km, and the distance between two meridians is also around 111km.
There are 360 meridian lines, one for every degree out of 360 degrees, and 180 lines of parallel. With this in mind, you can calculate the distance between two points on the globe with these formulas:
This gives you the distances for latitude and longitude, which are two sides of a right triangle. Using the Pythagorean theorem, you can now calculate the hypotenuse of the triangle to find the distance between the two points:
That’s quite easy but unfortunately, it’s also wrong.
If you look again at your globe, you’ll see that the distance between the parallels is almost equal, but the meridians meet at the poles. So the distance between meridians shrinks when you come closer to the poles, and is zero on the poles. This means the formula above works only for points near the equator. The closer the points are to the poles, the bigger the error becomes.
To calculate the distance more precisely, you can determine the great-circle distance. This is the distance between two points on a sphere and, as we all know, the earth is a sphere. Well OK, it is nearly a sphere, but this method gives you good results. With a known latitude and longitude for two points, you can use the following formula to calculate the great-circle distance.
This formula gives you the distance between two points with an accuracy of around 60 km, which is quite good if you want to know how far Tokyo is from New York. For points closer together, the result will be much better.
Phew – that was hard stuff! The good news is that CLLocation
has a method, distanceFromLocation:
, that does this calculation for you. HDAugmentedReality
also uses this method.
You may be thinking to yourself “Meh, I still don’t see why I should use HDAugmentedReality.” It’s true, grabbing frames and showing them is not that hard and you can read about it on this site. You can calculate the distance between points with a method from CLLocation
without bleeding.
So why did I introduce this library? The problem comes when you need to calculate where to show the overlay for a POI on the screen. Assume you have a POI that is to the north of you and your device is pointing to the northeast. Where should you show the POI – centered or to the left side? At the top or bottom?
It all depends on the current position of the device in the room. If the device is pointing a little towards the ground, you must show the POI nearer to the top. If it’s pointing to the south, you should not show the POI at all. This could quickly get complicated!
And that’s where HDAugmentedReality
is most useful. It grabs all the information needed from the gyroscope and compass and calculates where the device is pointing and its degree of tilt. Using this knowledge, it decides if and where a POI should be displayed on the screen.
Plus, without needing to worry about showing live video and doing complicated and error-prone math, you can concentrate on writing a great app your users will enjoy using.
Now have a quick look at the files inside the HDAugmentedReality\Classes group:
ARAnnotation
: This class is used to define an POI.ARAnnotationView
: This is used to provide a view for POI.ARConfiguration
: This is used to provide some basic configuration and helper methods.ARTrackingManager
: This is where the hard work is done. Luckily you don’t have to deal with it.ARViewController
: This controller does all the visual things for you. It shows a live video and adds markers to the view.Open ViewController.swift and add another property below the places
property.
fileprivate var arViewController: ARViewController! |
Now find @IBAction func showARController(_ sender: Any)
and add the following to the body of the method:
arViewController = ARViewController() //1 arViewController.dataSource = self //2 arViewController.maxVisibleAnnotations = 30 arViewController.headingSmoothingFactor = 0.05 //3 arViewController.setAnnotations(places) self.present(arViewController, animated: true, completion: nil) |
dataSource
for the arViewController
is set. The dataSource
provides views for visible POIsarViewController
. maxVisibleAnnotations
defines how many views are visible at the same time. To keep everything smooth you use a value of thirty, but this means also that if you live in an exciting area with lots of POIs around you, that maybe not all will be shown.headingSmoothingFactor
is used to move views for the POIs about the screen. A value of 1 means that there is no smoothing and if you turn your iPhone around views may jump from one position to another. Lower values mean that the moving is animated, but then the views may be a bit behind the “moving”. You should play a bit with this value to get a good compromise between smooth moving and speed.arViewController
You should have a look into ARViewController.swift for some more properties like maxDistance
which defines a range in meters to show views within. So everything that is behind this value will not be shown.
Xcode complains the line where you assign self
as the dataSource, to make it happy ViewController
must adopt the ARDataSource
protocol. This protocol has only one required method that should return a view for a POI. In most cases and also here you want to provide a custom view. Add a new file by pressing [cmd] + [n]. Choose iOS\Swift File and save it AnnotationView.swift.
Replace the content with the following:
import UIKit //1 protocol AnnotationViewDelegate { func didTouch(annotationView: AnnotationView) } //2 class AnnotationView: ARAnnotationView { //3 var titleLabel: UILabel? var distanceLabel: UILabel? var delegate: AnnotationViewDelegate? override func didMoveToSuperview() { super.didMoveToSuperview() loadUI() } //4 func loadUI() { titleLabel?.removeFromSuperview() distanceLabel?.removeFromSuperview() let label = UILabel(frame: CGRect(x: 10, y: 0, width: self.frame.size.width, height: 30)) label.font = UIFont.systemFont(ofSize: 16) label.numberOfLines = 0 label.backgroundColor = UIColor(white: 0.3, alpha: 0.7) label.textColor = UIColor.white self.addSubview(label) self.titleLabel = label distanceLabel = UILabel(frame: CGRect(x: 10, y: 30, width: self.frame.size.width, height: 20)) distanceLabel?.backgroundColor = UIColor(white: 0.3, alpha: 0.7) distanceLabel?.textColor = UIColor.green distanceLabel?.font = UIFont.systemFont(ofSize: 12) self.addSubview(distanceLabel!) if let annotation = annotation as? Place { titleLabel?.text = annotation.placeName distanceLabel?.text = String(format: "%.2f km", annotation.distanceFromUser / 1000) } } } |
ARAnnotationView
which is used to show a view for a POIloadUI()
adds and configures the labels.To finish the class add two more methods
//1 override func layoutSubviews() { super.layoutSubviews() titleLabel?.frame = CGRect(x: 10, y: 0, width: self.frame.size.width, height: 30) distanceLabel?.frame = CGRect(x: 10, y: 30, width: self.frame.size.width, height: 20) } //2 override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { delegate?.didTouch(annotationView: self) } |
Now head back to ViewController.swift and add the following extension:
extension ViewController: ARDataSource { func ar(_ arViewController: ARViewController, viewForAnnotation: ARAnnotation) -> ARAnnotationView { let annotationView = AnnotationView() annotationView.annotation = viewForAnnotation annotationView.delegate = self annotationView.frame = CGRect(x: 0, y: 0, width: 150, height: 50) return annotationView } } |
Here you create a new AnnotaionView
and set its delegate before you return it.
Before you can test your views in action you need another extension.
extension ViewController: AnnotationViewDelegate { func didTouch(annotationView: AnnotationView) { print("Tapped view for POI: \(annotationView.titleLabel?.text)") } } |
Before you activate the camera, you have to add a key to the Info.plist. Open the file and add the key NSCameraUsageDescription with a value of Needed for AR, just like you did for accessing location information.
Build and run, and tap the camera button on the map view to go to the ar view. The first time you do so, the system will raise a permission dialog before it gives you access to the camera. Tap a POI and look at the console.
You have a complete working AR app now, you can show POIs on a camera view and detect taps on this POIs, to make your app complete you’ll add some tap handling logic now.
If you closed it open ViewController.swift and replace extension which adopts the AnnotationViewDelegate
protocol with the following:
extension ViewController: AnnotationViewDelegate { func didTouch(annotationView: AnnotationView) { //1 if let annotation = annotationView.annotation as? Place { //2 let placesLoader = PlacesLoader() placesLoader.loadDetailInformation(forPlace: annotation) { resultDict, error in //3 if let infoDict = resultDict?.object(forKey: "result") as? NSDictionary { annotation.phoneNumber = infoDict.object(forKey: "formatted_phone_number") as? String annotation.website = infoDict.object(forKey: "website") as? String //4 self.showInfoView(forPlace: annotation) } } } } } |
annotationViews
annotation to a Place
.showInfoView(forPlace:)
is a method you implement right now.Add this method below showARController(sender:)
func showInfoView(forPlace place: Place) { //1 let alert = UIAlertController(title: place.placeName , message: place.infoText, preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil)) //2 arViewController.present(alert, animated: true, completion: nil) } |
ViewController
is not a part of the view hirarchy right now, you use arViewController
to show the alert.Build and run again and see your finished app.
Here is the final project with all of the code from above.
Congratulations, you now know how to make your own location based augmented reality app! And as a bonus, you’ve also gotten a short introduction to the Google Places API.
In the meantime, if you have any comments or questions, please join the forum discussion below!
The post Augmented Reality iOS Tutorial: Location Based appeared first on Ray Wenderlich.
This is just a quick update that RWDevCon 2017 is now sold out!
I am amazed and overjoyed by the enthusiastic response from the community for this conference – we are set to have ~265 attendees at the conference this year.
The team and I are hard at work on the conference – we’ll make this an event to remember!
If you didn’t manage to get a ticket:
For the attendees and sponsors of RWDevCon – thanks so much for being a part of the conference, and we can’t wait to see you in DC! :]
The post RWDevCon 2017 Sold Out appeared first on Ray Wenderlich.
In the past few months, we’ve released new or updated courses for subscribers on Swift 3, iOS 10, Server Side Swift, Metal, Concurrency, Collection Views, iOS Unit & UI Testing, SpriteKit, and more.
Today we’re happy to announce another course: Beginning Core Data is now updated for Swift 3 & iOS 10!
In this 7-part course, you’ll learn how to get started with Core Data, including how to model your data, store and fetch objects, filter objects, set up relationships, and more.
The course covers the new NSPersistentContainer
class introduced in iOS 10, which sets up a Core Data stack for you, making Core Data much easier to get started with.
If you’ve ever wanted to get started with Core Data, or brush up on the basics, this is the course for you. Let’s take a look at what’s inside!
Video 1: Introduction. Gives an overview of what’s inside the course, how Core Data compares to other methods of saving data such as Realm or SQLite, and explains some high level concepts.
Video 2: Storing and Fetching. Learn how to create your first model object in the Core Data Model Editor, and perform a simple Fetch Request to retrieve objects you’ve stored in Core Data.
Video 3: Managed Object Subclasses. Learn how to use managed object subclasses to easily manipulate your data.
Video 4: Predicates and Sorting. Learn how to use sort descriptors and predicates to return only the data you want exactly the way you want it.
Video 5: Fetched Results Controllers. Learn how fetched results controllers can make setting up a table or collection view a lot easier!
Video 6: Relationships. Learn how to set up relationships between your managed objects, such as one to one, or one to many.
Video 7: Conclusion. Review what you learned in the course, and get a preview of what’s coming up next in the Intermediate Core Data Course (coming soon)!
Want to check out the course? You can watch the introduction for free!
The rest of the course is for raywenderlich.com subscribers only. Here’s how you can get access:
We hope you enjoy, and stay tuned for more new Swift 3 courses and updates to come! :]
The post Updated Course: Beginning Core Data appeared first on Ray Wenderlich.
Update Note: Updated for Xcode 8.2 / Swift 3 by Ernesto García. Previous update to Xcode 6.3 / Swift 1.2 by Michael Briscoe. Original post by Ernesto García.
If you’re an iOS developer and you’re interested in learning about Mac development, you’re in luck – with your iOS skills, you’ll find it quite easy to learn!
Many of the Cocoa classes and design patterns you know and love like strings, dictionaries and delegates have direct equivalents in Mac development. You’ll feel right at home!
However, one big difference with macOS development are there are different controls. Gone are UIButton
and UITextField
– instead there are similar (but slightly different) variants.
This tutorial will introduce you to some of the more common macOS controls of the user interface — the foundation upon which most Mac apps are built. You’ll learn about these controls, as well as the methods and properties you’ll need to understand in order to get up and running as a developer! :]
In this tutorial, you’ll be creating a simple Mac application like the popular game Mad Libs. Mad Libs is a word game where you can insert different words in a block of text in order to create a story — which often has hilarious results!
Once you’ve completed both parts of this tutorial, you’ll have a fundamental understanding of the following macOS controls:
The best way to learn any new programming platform is to dive right in and get started — so without further ado, here’s your introduction to macOS Controls! :]
Open Xcode, and choose File/New/Project. In the Choose a template dialog, select macOS/Application/Cocoa, which is the template you use to create an app with a GUI on macOS. Then click Next.
In the next screen, type MadLibs as the product name, and enter a unique Organization name and identifier. Make sure that Use Storyboards is checked and Swift is the selected language.
Click Next and choose the location where you’d like to save your new project. Click Create.
Open Main.storyboard. Xcode has created for you the basic skeleton of a macOS app: a Window controller and a content View controller.
Select the window in the Window Controller Scene and open the Attributes Inspector. Change the window Title to MadLibs.
A macOS app usually has a resizable window and its content has to adapt to the window size. The best tool for that is Auto Layout. To add Auto Layout to all the controls in this tutorial would create a major distraction; we want you to focus strictly on the macOS controls.
Accordingly, only the default autoresizing is applied, which means that all controls will maintain a fixed position and size, regardless of any window resizing the user performs — including the possibility that some of the controls fully or partially will be out of the window’s visible rectangle.
Note: If you want to learn more about Auto Layout and how to use it in your macOS apps, you can follow our macOS Development Tutorial for Beginners, Part 3.
During the tutorial, you’ll need to add some macOS controls to this view, and the default height may not be enough to fit them all. If you need to resize it, drag down the bottom edge of the content view, or set the view’s Height property in the Size Inspector.
Build and run.
You’ve built a working application — without any coding at all. The window is empty right now, but you’re going to fill it up with some macOS controls and make it look great! :]
Now that the basic framework has been laid down, you can move on to the main focus of this tutorial — adding macOS controls to your app.
Each of the remaining steps in this tutorial will focus on a single, different control. You’ll learn the basics of each control and how to use each one in the MadLibs app to try it out.
NSControl is the foundation upon which all other macOS controls are built. NSControl
provides three features which are pretty fundamental for user interface elements: drawing on the screen, responding to user events, and sending action messages.
As NSControl
is an abstract superclass, it’s entirely possible that you’ll never need to use it directly within your own apps unless you want to create your own custom macOS controls. All of the common controls are descendants of NSControl
, and therefore inherit the properties and methods defined in that.
The most common methods used for a control are getting and setting its value, as well as enabling or disabling the control itself. Have a look at the details behind these methods below:
If you need to display information you’ll usually change the control’s value. Depending on your needs, the value can be a string, a number or even an object. In most circumstances, you’ll use a value which matches the type of information being displayed, but NSControl
allows you to go beyond this and set several different value types!
The methods for getting and setting a control’s value are:
// getting & setting a string let myString = myControl.stringValue myControl.stringValue = myString // getting & setting an integer let myInteger = myControl.integerValue myControl.integerValue = myInteger // getting & setting a float let myFloat = myControl.floatValue myControl.floatValue = myFloat // getting & setting a double let myDouble = myControl.doubleValue myControl.doubleValue = myDouble // getting & setting an object let myObject: Any? = myControl.objectValue myControl.objectValue = myObject |
You can see how the different setters and getters fit with the type-safety of Swift.
Enabling or disabling macOS controls based on the state of an app is a very common UI task. When a control is disabled, it will not respond to mouse and keyboard events, and will usually update its graphical representation to provide some visual cues that it is disabled, such as drawing itself in a lighter “greyed out” color.
The methods for enabling and disabling a control are:
// disable a control myControl.isEnabled = false // enable a control myControl.isEnabled = true // get a control's enabled state let isEnabled = myControl.isEnabled |
Okay, that seems pretty easy — and the great thing is that these methods are common to all macOS controls. They’ll all work the same way for any control you use in your UI.
Now it’s time to take a look at the more common macOS Controls.
One of the most common controls in any UI is a field that can be used to display or edit text. The control responsible for this functionality in macOS is NSTextField.
NSTextField
is used for both displaying and editing text. You’ll notice this differs from iOS, where UILabel
is used to display fixed text, and UITextField
for editable text. In macOS these controls are combined into one, and its behavior changes according to the value of its isEditable
property.
If you want a text field to be a label, you simply set its isEditable
property to false
. To make it behave like a text field — yup, you simply set isEditable
to true
! You can change this property programmatically or from Interface Builder.
To make your coding life just a little easier, Interface Builder actually provides several pre-configured macOS controls to display and edit text which are all based on NSTextField
. These pre-configured macOS controls can be found in the Object Library:
So now that you’ve learned the basics about NSTextField
, you can add it to your Mad Libs application! :]
You will add various macOS controls to the MadLibs app, which will allow you to blindly construct a funny sentence. Once you’ve finished, you will combine all the different parts and display the result, hopefully with some comedic value. The more creative the you are, the more fun they’ll be!
The first control you’ll add is a text field where you can enter a verb to add it to the sentence, as well as a label that informs what the text field is for.
Open Main.storyboard. Locate the Label control in the Object Library and drag it onto the view in the View Controller Scene. Double-click the label to edit the default text, and change it to Past Tense Verb:.
Next, locate the Text Field control and drag it onto the view, placing it to the right of the label, like this:
Now, you’ll create an outlet to the text field in the view controller. While the Main.storyboard is open, go to the Assistant editor. Make sure that ViewController.swift is selected and Ctrl-Drag from the text field in the storyboard into the pane containing ViewController.swift, and release the mouse just below the class definition to create a new property:
In the popup window that appears, name the Outlet pastTenseVerbTextField, and click Connect.
And that’s it! You now have an NSTextField
property in your view controller that is connected to the text field in the main window.
You know, it would be great to display some default text when the app launches to give an idea of what to put in the field. Since everyone loves to eat, and food related Mad Libs are always the most entertaining, the word ate would be a tasty choice here.
A good place to put this is inside viewDidLoad()
. Now, simply set the stringValue
property you learned about earlier.
Open ViewController.swift and add the following code to the end of viewDidLoad()
:
// Sets the default text for the pastTenseVerbTextField property pastTenseVerbTextField.stringValue = "ate" |
Build and run.
Okay, that takes care of a single input with a default value. But what if you want to provide a list of values to select from?
Combo Boxes to the rescue!
A combo box is interesting — and quite handy — as it allows the user to choose one value from an array of options, as well as enter their own text.
It looks similar to a text field in which the user can type freely, but it also contains a button that allows the user to display a list of selectable items. You can find a solid example of this in macOS’s Date & Time preferences panel:
Here, the user can select from a predefined list, or enter their own server name, if they wish.
The macOS control responsible for this is NSComboBox.
NSComboBox
has two distinct components: the text field where you can type, and the list of options which appear when the embedded button is clicked. You can control the data in both parts separately.
To get or set the value in the text field, simply use the stringValue
property covered earlier. Hooray for keeping things simple and consistent! :]
Providing options for the list is a little more involved, but still relatively straightforward. You can call methods directly on the control to add elements in a manner similar to mutable Array
, or you can use a data source — anyone with experience on iOS programming and UITableViewDataSource
will feel right at home!
Note: If you are not familiar with the concept of Data Sources, you can learn about it in Apple’s Delegates and Data Sources documentation.
NSComboBox
contains an internal list of items, and exposes several methods that allow you to manipulate this list, as follows:
// Add an object to the list myComboBox.addItem(withObjectValue: anObject) // Add an array of objects to the list myComboBox.addItems(withObjectValues: [objectOne, objectTwo, objectThree]) // Remove all objects from the list myComboBox.removeAllItems() // Remove an object from the list at a specific index myComboBox.removeItem(at: 2) // Get the index of the currently selected object let selectedIndex = myComboBox.indexOfSelectedItem // Select an object at a specific index myComboBox.selectItem(at: 1) |
That’s relatively straightforward, but what if you don’t want your options hardcoded in the app — such as a dynamic list that is stored outside of the app? That’s when using a datasource comes in really handy! :]
When using a data source the combo box will query the data source for the items it needs to display as well, as any necessary metadata, such as the number of items in the list. To obtain this information, you’ll need to implement the NSComboBoxDataSource protocol in one of your classes, normally the View Controller hosting the control. From there, it’s a two-step process to configure the combo box to use the data source.
First, set the control’s usesDataSource
property to true
. Then set the control’s dataSource
property, passing an instance of the class implementing the protocol; when the class implementing the data source is the hosting View Controller a good place for this setup is viewDidLoad()
, and then you set the dataSource
property to self
as shown below:
class ViewController: NSViewController, NSComboBoxDataSource { ..... override func viewDidLoad() { super.viewDidLoad() myComboBox.usesDataSource = true myComboBox.dataSource = self } ..... } |
Note: The order of the instructions in the code above is important. An attempt to set the dataSource
property when useDataSource
is false
(which is the default) will fail and your data source will not be used.
In order to conform to the protocol, you’ll need to implement the following two methods from the data source protocol:
// Returns the number of items that the data source manages for the combo box func numberOfItems(in comboBox: NSComboBox) -> Int { // anArray is an Array variable containing the objects return anArray.count } // Returns the object that corresponds to the item at the specified index in the combo box func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? { return anArray[index] } |
Finally, whenever your data source changes, to update the control, just call reloadData()
on the combo box.
If your list of items is relatively small and you don’t expect it to change that often, adding items once to the internal list is probably the best choice. But if your list of items is large or dynamic, it can often be more efficient to handle it yourself using a data source. For this tutorial you’ll be using method 1.
Now that you’ve covered the fundamentals of the combo box, move on to implement one in your app! :]
In this section you’ll add a combo box to enter a singular noun. You can either choose from the list or enter your own.
First, add a label that describes what the control is for.
Open Main.storyboard. Locate the Label control in the the Object Library palette, and drag it onto the content view. Change its alignment to Right and its title to Singular Noun:.
Note: Alternatively as a shortcut, hold down the Option key and drag an existing label to duplicate it. This is handy so you can keep the same size and properties of an existing label.
Locate the Combo Box control and drag it onto the content view, placing it to the right of the label.
Now you need to add an NSComboBox
outlet to the view controller. Use the same technique you used for the text field: select the Assistant Editor (making sure ViewController.swift is selected) and Ctrl-Drag the combo box to the ViewController
class just below the NSTextField
:
In the popup window that appears, name the outlet singularNounCombo.
Now the NSComboBox
property is connected to the combo box control. Next you are going to add some data to populate the list.
Open ViewController.swift and add this code under the outlets:
fileprivate let singularNouns = ["dog", "muppet", "ninja", "pirate", "dev" ] |
Now, add the following code at the end of viewDidLoad():
// Setup the combo box with singular nouns singularNounCombo.removeAllItems() singularNounCombo.addItems(withObjectValues: singularNouns) singularNounCombo.selectItem(at: singularNouns.count-1) |
The first line removes any items added by default. Next, it adds the names from singularNouns
to the combo box using addItems()
. Then, it selects the last item of the list.
Build and run the application to see your combo box in action!
Great — it looks as though everything is working just right. If you click on the combo box, you can then view and select any of the other items.
Now, what if you wanted to present a list of choices, but not allow you to enter your own? Read on, there’s a control for that as well!
The pop up button allows the user to choose from an array of options, but without giving the user the option of entering their own value in the control. The macOS control responsible for this is NSPopupButton.
Pop up buttons are incredibly common in macOS, and you can find them in almost every application — including the one that you’re using right now: Xcode! :] You’re using the pop up button to set many of the properties on the macOS controls you’re using in this tutorial, as in the screenshot below:
As you might expect, adding items to NSPopUpButton
is similar to adding items to NSComboBox
— except that NSPopUpButton
doesn’t support using a data source for the content of the control. NSPopUpButton
maintains an internal list of items and exposes several methods to manipulate it:
// Add an item to the list myPopUpbutton.addItem(withTitle: "Pop up buttons rock") // Add an array of items to the list myPopUpbutton.addItems(withTitles: ["Item 1", "Item 2", "Item 3"]) // Remove all items from the list myPopUpbutton.removeAllItems() // Get the index of the currently selected item let selectedIndex = myPopUpbutton.indexOfSelectedItem // Select an item at a specific index myPopUpbutton.selectItem(at: 1) |
Pretty straightforward, isn’t it? That’s the beauty of macOS controls — there are a lot of similarities between them in terms of the methods used to manipulate the controls.
Time to implement a pop up button in your app! :]
You’ll now add a pop up button to your Mad Libs application to choose between different plural nouns to populate your comical sentence.
Open Main.storyboard. Drag a label just below the Singular Noun label.
Change the alignment to Right and the title to Plural Noun:. Next, locate the Pop Up Button control and drag it onto the window, placing it to the right of the label.
The content view should look like this:
Now you need to add an outlet to the popup button, which should be fairly familiar by now: open the Assistant editor, make sure ViewController.swift is selected, and then Ctrl-Drag the pop up button to the ViewController
class to create a new outlet:
In the popup window, name the outlet pluralNounPopup:
Now you just need some data to populate the control!
Open ViewController.swift and add this property inside the class implementation.
fileprivate let pluralNouns = ["tacos", "rainbows", "iPhones", "gold coins"] |
Now, add the following code to the bottom of viewDidLoad()
:
// Setup the pop up button with plural nouns pluralNounPopup.removeAllItems() pluralNounPopup.addItems(withTitles: pluralNouns) pluralNounPopup.selectItem(at: 0) |
The first line removes any existing items from the pop up button. The second line adds the array of nouns to the pop up button using addItems()
. Finally, it selects the first item in the list.
Build and run the application to see the result:
Once the app has launched, note that the pop up button shows the initial item, tacos, and if you click on the pop up button, you’ll see all the other items in the list.
Okay, so you now have two macOS controls that allow the user to select from lists, as well as a control that allows the user to enter a single line of text. But what if you need to type more than a few words in a text field?
Read on to learn about text views!
Text views, unlike text fields, are usually the control of choice for displaying rich text. Some implementations even allow for more advanced features such as displaying inline images.
The macOS Control responsible for this is NSTextView.
A great example of an application using all of what NSTextView
has to offer is TextEdit:
NSTextView
is so feature-rich that to cover everything would warrant a tutorial of its own, so here you’ll just see a few of the basic features in order to get you up and running! (Did you just breathe a sigh of relief?) :]
Here are the basic methods you’ll need to work with text views:
// Get the text from a text view let text = myTextView.string // Set the text of a text view myTextView.string = "Text views rock too!" // Set the background color of a text view myTextView.backgroundColor = NSColor.white // Set the text color of a text view myTextView.textColor = NSColor.black |
Relatively simple — nothing too shocking here.
NSTextView
also has built-in support for NSAttributedString. If you pass an attributed string to a text view, the string will be displayed correctly using all the appropriate attributes such as font, font size, and font color.
Note: An attributed string is a special type of string where you can tag subsets of the string with different attributes – such as its font, its color, whether its bolded, and so on. To learn all about attributed strings, check out our TextKit Tutorial. It’s an iOS tutorial, but the information about NSAttributedString
applies to Mac development as well.
Looks like you have everything you need in order to add a text view to your Mad Libs application! This text view will allow the user to enter a multi-word phrase that will be used in the final rendered Mad Lib.
Open Main.storyboard and drag a label just below the Plural Noun label (or duplicate an existing label, as mentioned earlier). Change its alignment to Right and its title to Phrase:.
Next, locate the Text View control and drag it onto the window, placing it beside the label you just created.
Your window should now look like this:
Now, if you try to resize the view and make it taller, you’ll notice something quite particular. The text view moves along, and changes its position when you resize the window.
That’s because by default, Xcode adds Auto resizing constraints to the text view, so that it repositions itself when its parent view is resized. Since you want the text view to stay put, you’ll need to disable some of those.
Select the Bordered Scroll View – Text View from the Document Outline and go to the Size Inspector.
In the AutoResizing section, you’ll see a rectangle which has four red lines connected to the parent view. Each one of these red connectors represents an Auto resizing constraint. You just need to click on the Right and Bottom red connectors to disable those, the solid red lines will turn to broken lines with a faded red color as shown below:
Now, the text view stays put and aligned with the label even if you resize the window.
Next, add an NSTextView
outlet to the view controller. Select the textview, open the Assistant editor and make sure ViewController.swift is selected. Ctrl-Drag from the text view to the ViewController
class under the existing outlets.
Important: Text views are contained inside scroll views. It’s important you make sure you’ve actually selected the text view before creating the outlet. To do so, simply click three times on the text view or select it in the Document Outline.
In the popup window, make sure the type is NSTextView
, and name the outlet phraseTextView.
Now, add the following code to the end of viewDidLoad():
// Setup the default text to display in the text view phraseTextView.string = "Me coding Mac Apps!!!" |
Build and run the application to see the result:
Superb! The Mad Libs application is really starting to take shape now.
Buttons are macOS controls designed to send a message to the app whenever they’re clicked.
The control responsible for this on macOS is NSButton.
There are many different styles of buttons (you can view them in Interface Builder’s Object Library). They all work in much the same way, the only difference being their visual representation.
You should use the style of button that best suits your application’s design — refer to the macOS Human Interface Guidelines for advice and guidance on best design practices for your app.
Typically, when working with a button, you’ll simply need to associate an action with the button and set its title. However, there may be times when you need to disable the button, or change its appearance. The following methods allow you to perform those actions:
// disable a button myButton.isEnabled = false // enable a button myButton.isEnabled = true // getting & setting a button's title let theTitle = myButton.title myButton.title = theTitle // getting & setting a button's image let theImage = myButton.image myButton.image = theImage |
Looks fairly simple — adding a button to your app in the next section should be a breeze.
Open Main.storyboard. Find the Push Button in the Object Library palette and drag it onto the content view. Double-click on it to change its title to Go! :
This time you don’t need to create an outlet for the button. However, you do need to create an action and associate it with the button, so that your app knows when the button has been clicked! :]
Open the Assistant Editor and Ctrl+Drag from the button to the ViewController
implementation.
In the popup window that appears, make sure that the connection is set to Action. Name the action goButtonClicked.
Whenever the user clicks on the button the action method goButtonClicked()
will be called. For now you’ll add some debug code, just to make sure everything’s working.
Open ViewController.swift and add the following code inside goButtonClicked()
:
let pastTenseVerb = pastTenseVerbTextField.stringValue let singularNoun = singularNounCombo.stringValue let pluralNoun = pluralNouns[pluralNounPopup.indexOfSelectedItem] let phrase = phraseTextView.string ?? "" let madLibSentence = "A \(singularNoun) \(pastTenseVerb) \(pluralNoun) and said, \(phrase)!" print("\(madLibSentence)") |
This code gets the strings from the text field, the combo box, the popup button and the text view and forms the Mad Lib sentence.
Note that for the text view, the string
property is actually an optional, so it could be nil
. To guard against that case, you’re using the nil coalescing operator ??
so if string
is nil, you’ll get the empty string ""
instead.
That’s all the code you need for now — build and run your app.
Every time you click the button, you should see a short and silly sentence appear in Xcode’s console.
A dev ate tacos and said: Me coding Mac Apps!!!! |
That’s great, but how could you make it even funnier?
How about making your computer read the sentence? Steve Jobs made the first Macintosh say hello to the world in its presentation. You can make your computer talk too… Let’s get at it!
Open ViewController.swift and add this code inside the ViewController
implementation:
// 1 fileprivate enum VoiceRate: Int { case slow case normal case fast var speed: Float { switch self { case .slow: return 60 case .normal: return 175; case .fast: return 360; } } } //2 fileprivate let synth = NSSpeechSynthesizer() |
First, you declare an enum
to represent the voice rate. Then you create and instance of NSSpeechSynthesizer
which is the class that will convert the text to speech.
Now, add this method inside the ViewController
implementation:
fileprivate func readSentence(_ sentence: String, rate: VoiceRate ) { synth.rate = rate.speed synth.stopSpeaking() synth.startSpeaking(sentence) } |
This method starts the synth
object speaking a string at the determined speed.
Time to call it! Add this code at the end of goButtonClicked()
to read the Mad Libs sentence:
readSentence(madLibSentence, rate: .normal) |
Build and run; click Go! and listen to your Mac saying your sentence out loud!
You can the download the final project containing all the source code from this tutorial up to this point.
In the second part of this tutorial, you’ll learn about more macOS controls, including sliders, date pickers, radio buttons, check boxes and image views — each of which will be added to your Mad Libs application in order to complete it.
In the meantime, if you have any questions or comments about what you’ve done so far, join in the forum discussion below!
The post macOS Controls Tutorial: Part 1/2 appeared first on Ray Wenderlich.
Update Note: Updated for Xcode 8.2 / Swift 3 by Ernesto García. Previous update to Xcode 6.3 / Swift 1.2 by Michael Briscoe. Original post by Ernesto García.
Welcome back to the second and final part of this macOS Controls tutorial series!
In the first part of this tutorial, you started building a Mad Libs style macOS application, where the user enters various words and phrases to create a funny sentence.
Along the way, you learned about some of the core UI macOS controls — namely, Text Fields, Combo Boxes, Pop Up Buttons, Push Buttons and Text Views.
In this final part of the tutorial, you’ll finish off your application, and learn how to use the following:
At the end of this two-part tutorial you’ll have a solid understanding of macOS controls.
This tutorial will pick up where you left off. If you don’t have it already, here’s the final project of the first part.
It’s time to get to work!
A slider is a control that lets the user choose from a range of values. A slider has a minimum and a maximum value, and by moving the control’s knob, the user can choose a value between those two limits. Sliders can be either linear or radial. What’s the difference between the two, you ask?
Linear sliders can be either vertical or horizontal, and they let you choose a value by moving the knob along the track. A really great example of linear sliders is in macOS’ Mouse preferences panel:
Radial sliders are a little different — they are displayed as a small circle with a knob, which can be rotated a full 360 degrees. To select a value, you click and drag the knob to the required position. You can find a great example of radial sliders in Adobe Photoshop, where they’re used to define the angle of a gradient, as such:
The control responsible for this on macOS is an NSSlider.
All three types of sliders (horizontal, vertical and radial) are in fact the same control, NSSlider
. The only difference is how they’re displayed. Interface Builder has an object in the Object Library for each of the three types, as shown below:
There are two common tasks you’re likely to perform when working with sliders: getting or setting the current value, and getting and setting the high and low limits of the slider’s range. These properties are outlined here:
// getting & setting an integer value let theInteger = mySlider.integerValue mySlider.integerValue = theInteger // getting & setting a float value let theFloat = mySlider.floatValue mySlider.floatValue = theFloat // getting & setting a double value let theDouble = mySlider.doubleValue mySlider.doubleValue = theDouble // getting & setting the minimum value of the range let theMinimumValue = mySlider.minValue mySlider.minValue = theMinimumValue // getting & setting the maximum value of the range let theMaximumValue = mySlider.maxValue mySlider.maxValue = theMaximumValue |
Again, nothing too surprising here — if you’ve learned anything by now, it’s that implementing standard UI macOS controls is a fairly straightforward exercise. Move on to the next section to include an NSSlider
in your app!
Open Main.storyboard. Locate the Label control In the Object Library palette and drag it onto the content view below the Phrase label. Resize the window vertically and move the Go! button down if you need to make space for it.
Double-click the control to edit its default text, and change it to Amount: [10]. Find the Horizontal Slider control and drag it onto the window, placing it to the right of the label.
Click on the slider to select it. Open the Attributes Inspector and set the Minimum value to 2, and the Maximum value to 10. Change the Current value to 5. This will be the default value of the slider when the user first runs the app.
Make sure that Continuous is checked. This tells the slider to notify any change in the slider’s value.
Now you need to create two outlets; one for the slider, and one for the label. Wait, you may say — that’s a little different. Why are you adding a property for the label?
That’s so you can update the label’s text to list the current amount whenever the value of the slider is changed; hence why you set the Continuous property on the slider. Aha! Makes sense now, doesn’t it?
Open the Assistant editor and make sure ViewController.swift is open. Ctrl-Drag from label to ViewController.swift to create a new outlet.
In the popup screen, name the outlet amountLabel.
Repeat the above process with the slider, naming the outlet amountSlider.
Now you need to add an action that will be called when the slider value changes. You already created an action for your button in Part 1; adding an action is very much like creating an outlet, so you’ll get a little more practice!
Select the slider and Ctrl-Drag to ViewController.swift anywhere within the class definition:
In the popup window, be sure to set the connection as an action rather than a outlet. Name it sliderChanged, like so:
Now you need to update the label whenever the action is called.
Open ViewController.swift and add the following code inside sliderChanged()
:
let amount = amountSlider.integerValue amountLabel.stringValue = "Amount: [\(amount)]" |
A quick review of the code above shows that you first read the slider’s current value. Then you set the value of the label to a string containing the slider’s value. Please note, that although amountLabel
cannot be edited by the user from the UI, it is still editable programmatically.
Note: This example uses integerValue
to get a nice round number, but if you need more precision you could use either floatValue
or doubleValue
for your slider.
Build and run the app. Try moving the slider back and forth to see the label update with the slider’s current value:
There’s one small problem. Did you notice it? The label doesn’t display the correct value when the app first launches! While it’s not a big problem, it makes the app look unfinished. It’s because the label is only updating when the slider’s knob is moved.
Fear not — it’s very easy to fix.
Add the following code to the end of viewDidLoad()
:
// Update the amount slider sliderChanged(self) |
Now the app will call sliderChanged()
at launch, and that will update the label. Neat!
Build and run. The label now displays the correct value at first run, which is a small touch, but is one of those “fit and finish” elements that make your app look polished.
What about more complicated values, such as calendar dates? Yup, macOS has those handled too! :]
Date Picker is a macOS control that display date and time values, as well as providing a method for the user to edit those values. Date Pickers can be configured to display a date, a time or both a date and time. The control responsible for this on macOS is NSDatePicker.
Date Pickers can be displayed in one of two styles: textual, where the date and time information is shown in text fields, and graphical, where the date is represented by a calendar and the time by a clock. You can find examples of all these styles in macOS’ Date & Time preferences panel, as in the screenshot below:
The most common tasks you’ll perform with a date picker are getting and setting the date or time value, and setting the minimum and maximum date or time values that are permitted in your control. The properties to do this are set out below!
// getting & setting the date/time value let myDate = myDatePicker.dateValue myDatePicker.dateValue = myDate // getting & setting the minimum date of the range let theMinimumDate = myDatePicker.minDate myDatePicker.minDate = theMinimumDate // getting & setting the maximum date of the range let theMaximumDate = myDatePicker.maxDate myDatePicker.maxDate = theMaximumDate |
Again — the controls have a very simple getter and setter style interface to update these values. Now it’s time (pardon the pun!) to put this control to work.
Following the usual procedure, add a new Label to your window. Change its title to Date: and its alignment to Right. Find the Date Picker control in the Object palette, and drag it onto the window, placing it to the right of the label. Resize the window and move the Go! button down if needed:
Create an outlet for the date picker, just as you’ve done for each of the previous macOS controls. In the popup window, name the property datePicker.
Just like the other macOS controls in your app, it’s nice to display a default value to the user when they first run your application. Picking today’s date as the default sounds like a good choice! :]
Open ViewController.swift and add the following code to the end of viewDidLoad()
:
// Set the date picker to display the current date datePicker.dateValue = Date() |
Build and run the app! You should see your shiny new date picker displaying current date, like in the screenshot below:
Radio buttons are a special type of control that always appear in groups; they are typically displayed as a list of options with selectable buttons alongside. Their behavior is also somewhat unique; any button within the group can be selected, but selecting one button will deselect all other buttons in the group. Only a single button, within the same group, can be selected at one time.
A good example of radio buttons that are used to present a set of options is the iTunes Back Up options, as shown below:
Radio buttons are organized in radio groups.
When you click a radio button in a group, it is selected and the system automatically deselects the rest of the buttons within that group. You only need to worry about getting and setting the proper values. How convenient!
But how do you define a group? Quoting the documentation:
“Radio buttons automatically act as a group (selecting one button will unselect all other related buttons) when they have the same superview and -action method.”.
So, all you need to do is add the radio buttons to a view, create an action, and assign that action to all the buttons in the group.
Then you just need to change the state of one button (On / Off) and the system will take care of deselecting the others.
// Select a radio button radioButton.state = NSOnState // Deselect a radio button radioButton.state = NSOffState // Check if a radio button is selected. let selected = (radioButton.state == NSOnState) |
Once again, a complicated control is reduced to some very simple methods. Read on to see how to implement a radio button control in your app!
Add a new Label to your app (you should be getting pretty comfortable with this by now!), and change its title to Place:. Locate the Radio Button in the Object Library palette, and drag three onto the content view, just beside the label. Double click on the radio buttons to change their titles to: RWDevCon, 360iDev and WWDC respectively.
Now, you need to create a new outlet for every radio button — another action you should be quite familiar with by now! Open the Assistant Editor and Ctrl-Drag the first radio button into the ViewController.swift source file, just below the existing properties. Name the outlet rwDevConRadioButton. Repeat the process for the other two radio buttons, naming the outlets threeSixtyRadioButton and wwdcRadioButton respectively.
Build and run.
Click on the radio buttons, and you’ll immediately notice a problem. You can select all of them. They’re not behaving as a group. Why? Because there are two conditions for different radio buttons to act as a group: They must have the same superview (which is the case), and they need to have a common action. The second condition is not met in our app.
To solve that, you need to create a common action for those buttons.
You are already an expert creating actions using Interface Builder, right? So, to learn an alternative way to do it, you’re going to create the action in code and assign it to the radio buttons in Interface Builder.
Open ViewController.swift and add the following code inside the class implementation:
@IBAction func radioButtonChanged(_ sender: AnyObject) { } |
That’s a typical action method which includes the @IBAction
annotation so that Interface Builder can find it and use it. Now, open Main.storyBoard to assign this action to the radio buttons.
Go to the Document Outline and Control-Click (or right click) on the View Controller. Go to the Received Actions section in the popup window, and drag from the circle next to radioButtonChanged:
onto the RWDevCon radio button. With that simple action you’ve just assigned the action to that radio button.
Repeat the process to assign the same action to the other two radio buttons. Your received actions section should look like this:
Build and run:
Now the radio buttons are behaving like a group.
Now you’ll need to to make the RWDevCon radio button the default when the app starts. You just need to set the radio button state to On, and then the other buttons will be automatically deselected.
Open ViewController.swift. Add the following code to the end of viewDidLoad()
:
// Set the radio group's initial selection rwDevConRadioButton.state = NSOnState |
Build and run the application. You should see the RWDevCon radio button selected when the app starts.
Radio buttons are one way to toggle values in your app, but there’s another class of macOS controls that perform a similar function — check boxes!
You typically use check boxes in an app to display the state of some boolean value. That state tends to influence the app in some way such as enabling or disabling a feature.
You will likely find check boxes where the user can enable or disable a functionality. You can find them in almost every screen of the Settings app. For instance, in the Energy Saver window you use them to enable or disable the different energy options.
Working with check boxes is relatively easy; most of the time you’ll only be concerned with getting and setting the state of the control. The state of the check box can be one of these: NSOnState (feature on everywhere), NSOffState (feature off everywhere) and NSMixedState (feature on somewhere, but not everywhere).
Here’s how you can use it:
// Set the state to On myCheckBox.state = NSOnState // Set the state to Off myCheckBox.state = NSOffState // Get the state of a check box let state = myCheckBox.state |
Super simple! Time to add a checkbox to your app.
Open Main.storyboard. Find the Check Box Button in the Object Library and drag it onto the content view. Double-click on it to change its title to Yell!! as in the image below:
Now add an outlet for the check box and name it yellCheck. You are now officially an expert creating outlets!
Now, you’ll make the check box default to the off state when the app launches. To do that, add the following at the end of viewDidLoad()
:
// set check button state yellCheck.state = NSOffState |
Build and run the application! You should see the check box, and it’s state should be unchecked. Click it to see it in action:
A segmented control, represented by the NSSegmentedControl class , represents an alternative to radio buttons when you need to make a selection from a number of options. You can see it in Xcode’s Attributes Inspector:
It’s very easy to use. You just need to get or set the selected segment to find out the user’s selection.
// Select the first segment segmentedControl.selectedSegment = 0 // Get the selected segment let selected = segmentedControl.selectedSegment |
If you remember, the readSentence()
had a parameter to control the voice speed (Normal, Fast, Slow). You’ll use a segmented control to change the speed of the voice.
Open Main.storyboard and add a Label to the content view. Change its title to Voice Speed:. Locate a Segmented Control and drag it onto the content view. You can double click on every segment of the control to set its title. Change the titles to Slow, Normal and Fast respectively.
Create an outlet for that segmented control in ViewController
, and name it voiceSegmentedControl. Now, you want to select the Normal segment when the app starts, which is the segment number 1 (segment numbers are zero based). Open ViewController.swift and add the following code to viewDidLoad()
:
// Set the segmented control initial selection voiceSegmentedControl.selectedSegment = 1 |
As easy as it looks. Just set the selectedSegment
property to 1. Build and run now and see how the Normal segment is selected.
Okay! You’ve finally added all the controls you need to create your funny mad lib sentences. All you’re missing is a way to collect the value of each control, combine those values into a sentence, and display it on-screen!
You need two more controls to show the results: a label to display the complete sentence, and an image view to display a picture, which should liven up the user interface!
Open Main.storyboard. Find the Wrapping Label in the Object Library palette and drag it onto the window, just below the Go!! button. Make it look a little more attractive by using the Attributes Inspector to change the border of the label to Frame, which is the first of the four buttons.
After that, remove the default text of the label by double-clicking it, selecting the text and deleting it.
Now you have to create an outlet to set the value of this new label to contain your new hilarious sentence! As before, Ctrl-Drag the label to the ViewController.swift file and name the property resultTextField.
Leave this control as it is for now; you’ll write the code that populates it in just a bit.
An Image View is a simple and easy to use control that — surprise! — displays an image. Bet you didn’t expect that! :]
There are very few properties you need to interact with an Image View at runtime:
// Get the image from an image view let myImage = myImageView.image // Set the image of an image view myImageView.image = myImage |
At design time, you can configure the visual aspects: the border, scaling and alignment. Yes, these properties can be set in code as well, but it’s far easier to set them in Interface Builder at design time, as below:
Time to add an image view to your application! Find the Image Well in the Object Library and drag it onto view, to the left of the wrapping label. Feel free to resize the app window if necessary.
Create a new outlet for the image view in the same way you’ve done for all the previous controls: Ctrl-Drag the image view to the ViewController.swift file, and in the popup window name the property imageView.
Build and run. Your app should now look like this:
Phew! Your user interface is finally finished — the only thing that’s left to do is to create the code that will assemble your hilarious sentence and populate the image view that you added above!
Now you need to construct the sentence based on those inputs.
When the user clicks the Go! button, you’ll collect all the values from the different controls and combine them to construct the full sentence, and then display that sentence in the wrapping label you added previously.
Then, to spice up your all-text interface, you will display a picture in the image view that you added in the last section.
Download the resources file for this project (normally goes into your Download folder). If the downloaded zip file was not unzipped automatically, unzip it to get the face.png image file. Select Assets.xcassets in the Project Navigator, and drag the image file into the assets list.
It’s finally time to add the core of the application — the code which constructs the Mad Lib sentence!
Open ViewController.swift and add the following property inside the class implementation:
fileprivate var selectedPlace: String { var place = "home" if rwDevConRadioButton.state == NSOnState { place = "RWDevCon" } else if threeSixtyRadioButton.state == NSOnState { place = "360iDev" } else if wwdcRadioButton.state == NSOnState { place = "WWDC" } return place } |
This code adds a computed property that returns the name of the place based on which radio button is selected.
Now, replace all the code inside goButtonClicked()
with this:
// 1 let pastTenseVerb = pastTenseVerbTextField.stringValue // 2 let singularNoun = singularNounCombo.stringValue // 3 let amount = amountSlider.integerValue // 4 let pluralNoun = pluralNouns[pluralNounPopup.indexOfSelectedItem] // 5 let phrase = phraseTextView.string ?? "" // 6 let dateFormatter = DateFormatter() dateFormatter.dateStyle = .long let date = dateFormatter.string(from: datePicker.dateValue) // 7 var voice = "said" if yellCheck.state == NSOnState { voice = "yelled" } // 8 let sentence = "On \(date), at \(selectedPlace) a \(singularNoun) \(pastTenseVerb) \(amount) \(pluralNoun) and \(voice), \(phrase)" // 9 resultTextField.stringValue = sentence imageView.image = NSImage(named: "face") // 10 let selectedSegment = voiceSegmentedControl.selectedSegment let voiceRate = VoiceRate(rawValue: selectedSegment) ?? .normal readSentence(sentence, rate: voiceRate) |
That may seem like a lot of code, but it’s fairly straightforward when you break it down:
pastTenseVerbTextField
stringValue
property. You might ask why you don’t just look up the selected row, and then retrieve the string associated with that row. Quite simply, it’s because the user can enter their own text into the combo box. So use the stringValue
to get the current string, which could have been either selected or typed.integerValue
method. Remember that if you need more precision with this control, you could also use floatValue
or doubleValue
.pluralNouns
array using array subscript syntax and getting the indexOfSelectedItem
property.string
property. Again, you’re using nil coalescing since the property is an optional and could be nil
.dateValue
method. Then, convert the returned date to a human-readable string using an NSDateFormatter.NSOnState
, assign yelled to the string variable. Otherwise, leave it as the default said.stringValue
property. Then, add some pizazz to the app by displaying an image to the user, which is as easy as loading the image and setting the property of the image view control.That’s it! You’re done! Build and run the app, so you can construct some hilarious sentences for yourself!
Congratulations — you’ve finished building the Mad Libs application, and have learned a ton about the most common macOS controls along the way.
Feel free to play with the controls, select different values, type funny nouns or verbs and see the results each time you click the Go! button, and see what funny stories you can create! :]
Here is the final project containing all the code from this tutorial.
In order to gain a deeper understanding of the controls provided by macOS, I recommend you have a read through the different programming guides available from Apple listed below, which contain a wealth of information about how to use the available controls.
In particular, I highly recommend you read the macOS Human Interface Guidelines. This guide explains the concepts and theories of user interfaces on macOS, and Apple’s expectations of how developers should use the controls and design their UI’s to provide a consistent and pleasurable experience. It’s essential reading for anyone intending to develop for the Mac platform, especially if they plan to distribute their applications via the Mac App Store.
Here are some useful links to reinforce or further explore the concepts you’ve learned in this tutorial:
I hope you enjoyed this tutorial, and as always if you have any questions or comments please join the forum discussion below!
The post macOS Controls Tutorial: Part 2/2 appeared first on Ray Wenderlich.
We often get emails from students who say they would love to attend RWDevCon, but can’t afford it on a Ramen-budget. Trust me, we’ve been there! :]
So today, we are happy to announce some exciting news – we are offering 5 student scholarships for RWDevCon 2017.
The 5 lucky students will win a free ticket to the conference and are invited to attend our team-only pre-conference fun day.
The conference is currently sold out, so this is the only way left to get a ticket.
Attending RWDevCon 2017 is an amazing opportunity for students – through two days packed full of high quality hands-on tutorials, you’ll bring your iOS development skills up-to-date, learn a ton, and have a blast.
Keep reading to find out who’s eligible, how to apply, and who to thank for this great opportunity!
To be eligible to win, there are three requirements:
Applying is simple:
Applications are due in 2 weeks – February 9, 2017 – so apply now! :]
The RWDevCon 2017 scholarships are sponsored by 5 generous patrons of the iOS community, who bought a special ticket that included the cost of the student tickets.
I asked each of our patrons to write some advice for students studying programming. Here’s what they had to say:
“Programming is an art and a craft. You refine your craft as you deal with the many issues and details that must be addressed. In developing your art, you hone your instincts so that you create something special, something that is more than the mere sum of solutions to hundreds of problems.”
“Let it be simple. Good code should flow from your fingertips. If you find yourself feeling as if you’re fighting against the code, that’s the first sign that you need to step back and reevaluate your approach.”
“When you start learning to program, the task may seem impossible. Just remember, the word itself says I’m possible! Make a plan to learn, develop and execute. You’ll be amazed at what you can accomplish. :-)”
“Programming is a skill that takes time to learn and years of practice to become good at. You’ll never stop learning and practicing and improving. Seek out experienced developers who have been in your shoes before and can help you navigate the seemingly infinite amount of information, opinions and trends to make your programming adventures more exciting and fulfilling.”
If you, or a student you know, wants to take advantage of an amazing learning opportunity that will pay some major dividends in life, don’t miss this chance!
Just send us an email about why you’re interested in programming and why you’d like to attend RWDevCon. Please include proof that you are in a full-time educational program (a scanned ID or receipt is fine).
We wish the best of luck to all of the applicants, and we look forward to seeing you at RWDevCon 2017!
The post RWDevCon 2017 Student Scholarships: Apply Now! appeared first on Ray Wenderlich.
In this episode, we review everything that was covered in the previous section, and then provide a roadmap of next section which covers class based objects.
The post Screencast: Beginning C# Part 22: Section Review – Intermediate Object Oriented Programming appeared first on Ray Wenderlich.