In this video, you'll learn how to add 3D models to scenes. You'll do this by way of planes and then by way of feature points.
The post Screencast: Introduction to ARKit: Adding 3D Models appeared first on Ray Wenderlich.
In this video, you'll learn how to add 3D models to scenes. You'll do this by way of planes and then by way of feature points.
The post Screencast: Introduction to ARKit: Adding 3D Models appeared first on Ray Wenderlich.
In this video, you'll learn how the leverage SceneKit's API in ARKit to measure distances between two nodes.
The post Screencast: Introduction to ARKit: Measuring Distances appeared first on Ray Wenderlich.
If you’re starting out in game development, one of the common pieces of advice is to create a basic game. This is because it teaches you how to create simple mechanics and how objects interact with each other.
In this tutorial, you will create a first-person game that continues indefinitely. You will learn how to:
At the end, you will have a game that looks like this:
Please note, you will be using Blueprints and UMG in this tutorial. If you need a refresher, check out our Blueprints and UI tutorial.
Download the starter project and unzip it. Go to the project folder and open InfiniteMatrix.uproject.
Press Play to test out the movement controls. You can move vertically and horizontally by moving your mouse.
The first thing you will do is make the player move forward continuously.
Navigate to the Blueprints folder and open BP_Player.
To move the player forward, you will add an offset to the player’s location each frame.
First, you need to create a variable to define the speed of the player’s forward movement. Create a Float variable named ForwardSpeed and set its default value to 2000.
Next, make sure you are in the Event Graph and then locate the Event Tick node. Create the following setup:
By multiplying ForwardSpeed with Delta Seconds, you get a frame rate independent result.
Next, you will use this result to move the player along a single axis.
To move the player, create an AddActorWorldOffset node. Set Sweep to true by left-clicking its checkbox.
If you try to connect the Float result to the Delta Location input, Unreal will automatically convert it to a Vector.
However, this will put the Float value into the X, Y and Z components of the Vector. For this game, the forward movement should only be along the X-axis. Luckily, you can split a Vector into three Float components.
Make sure the Delta Location pin of the AddActorWorldOffset node has no connections. Right-click the Delta Location pin and select Split Struct Pin.
Finally, connect everything like so:
Let’s recap:
Click Compile and then go back to the main editor. If you press Play, you will move through the tunnel.
Instead of placing tunnels manually, you can create a Blueprint that spawns tunnels automatically.
Go to the Content Browser and make sure you are in the Blueprints folder. Create a new Blueprint Class with Actor as the parent class. Name it BP_TunnelSpawner and then open it.
Since the game will be spawning tunnels constantly, it’s a good idea to create a spawning function. Go to the My Blueprint panel and create a new function named SpawnTunnel. The purpose of this function will be to spawn a tunnel at a provided location.
To pass a location to the function, the function needs an input parameter. These will appear as input pins when you call the function.
They will also appear as output pins on the Entry node of the function.
Let’s go ahead and create an input parameter. Make sure you are in the graph for the SpawnTunnel function. Select the Entry node and then go to the Details panel. Click the + sign next to the Inputs section.
Rename the input parameter to SpawnLocation and change its type to Vector.
To spawn a tunnel, add a Spawn Actor From Class node. Click the drop-down located to the right of the Class pin and select BP_Tunnel.
To set the spawn location, right-click the Spawn Transform pin and select Split Struct Pin. Afterwards, link the Spawn Actor From Class node to the Entry node like so:
Now, whenever you call the SpawnTunnel function, it will spawn an instance of BP_Tunnel at the provided location.
Let’s test it out!
Switch to the Event Graph and locate the Event BeginPlay node. Add a SpawnTunnel node and connect it to the Event BeginPlay node.
On the SpawnTunnel node, set Spawn Location to (2000, 0, 500).
Now, when the game starts, it will spawn a tunnel up and away from the player. Click Compile and then go back to the main editor.
First, delete BP_Tunnel from the level. Do this by left-clicking on BP_Tunnel in the World Outliner. Afterwards, press the Delete key to remove it from the level.
Next, go to the Content Browser. Left-click and drag BP_TunnelSpawner into the Viewport. This will add an instance of it to the level.
If you press Play, the game will spawn a tunnel above and away from the player.
Once you are done testing, go back to BP_TunnelSpawner. Reset the Spawn Location of the SpawnTunnel node to (0, 0, 0).
Afterwards, click Compile and then go back to the main editor.
In the next section, you will set up functionality for BP_Tunnel.
BP_Tunnel will be responsible for two things. The first is detecting when the game should spawn a new tunnel. To do that, you will create a trigger zone. Once triggered, BP_Tunnel will tell BP_TunnelSpawner to spawn a new tunnel. By doing this, you can create the illusion of an endless tunnel.
The second thing it will do is define a spawn point. BP_TunnelSpawner will then use this point as the next spawn location.
Let’s start with creating the trigger zone.
Open BP_Tunnel and then go to the Components panel. Add a Box Collision component and name it TriggerZone.
The collision area is quite small at the moment. Go to the Details panel and locate the Shape section. Set the Box Extent property to (32, 500, 500).
Next, set the Location property to (2532, 0, 0). This will place TriggerZone right at the end of the tunnel mesh. This means a new tunnel should only spawn when the player reaches the end of a tunnel.
Now, it’s time to create the spawn point
To define the location of the spawn point, you can use a Scene component. These components are perfect for defining locations because they only contain a Transform. They are also visible in the Viewport so you can see where your spawn point is.
Go to the Components panel and make sure you don’t have anything selected. Add a Scene component and rename it to SpawnPoint.
The tunnel mesh is 2500 units long on the X-axis so that’s where the attach point should be. Go to the Details panel and set the Location property to (2500, 0, 0).
The next thing to do is to create a function that spawns a tunnel at SpawnPoint.
Click Compile and then switch to BP_TunnelSpawner.
The next BP_Tunnel should spawn at the SpawnPoint of the furthest tunnel. By doing this, the tunnel will always continue.
Since the furthest tunnel is always the last spawned tunnel, you can easily get a reference to it.
Open the graph for SpawnTunnel. Right-click the Return Value pin of the Spawn Actor From Class node. Select Promote to Variable and rename the variable to NewestTunnel.
Now, you will always have a reference to the furthest tunnel.
Next, create a new function and name it SpawnTunnelAtSpawnPoint. Create the following graph:
This setup will get the newest tunnel and the location of its SpawnPoint component. It will then spawn a new tunnel at this location.
In order for BP_Tunnel to communicate with BP_TunnelSpawner, it needs a reference. Without communication, BP_TunnelSpawner won’t know when to spawn the next tunnel.
Click Compile and then close the SpawnTunnelAtSpawnPoint graph. Afterwards, switch to BP_Tunnel.
Add a new variable and name it TunnelSpawner. Set its Variable Type to BP_TunnelSpawner\Object Reference.
Click Compile and then switch back to BP_TunnelSpawner.
Open the graph for SpawnTunnel and add the indicated nodes:
Now, every tunnel will have a reference to BP_TunnelSpawner.
Next, you will tell BP_TunnelSpawner to spawn the next tunnel when the player enters TriggerZone.
Click Compile and then switch to BP_Tunnel.
Go to the Components panel and right-click on TriggerZone. Select Add Event\Add OnComponentBeginOverlap. This will add the following node to your Event Graph:
This node will execute whenever another Actor overlaps TriggerZone.
First, you should check if the Actor that overlapped TriggerZone is the player.
Left-click and drag the Other Actor pin. Release left-click on an empty area and select Cast to BP_Player from the menu.
Next, add the indicated nodes after the Cast to BP_Player node:
Let’s go through this step-by-step:
Click Compile, go back to the main editor and then press Play. Once you reach the end of a tunnel, the game will spawn a new one.
Although the game is endlessly spawning tunnels, it doesn’t look endless. You can mitigate this by always having a few tunnels visible. Later on, when you combine this with obstacles, the player won’t be able to see the tunnels spawning in.
The first thing to do is create a function that spawns a certain number of tunnels.
Open BP_TunnelSpawner and create a new function called SpawnInitialTunnels.
To spawn a specified number of tunnels, you can use a ForLoop node. This node will execute connected nodes a specified amount of times. Add a ForLoop node and connect it to the Entry node.
To make the ForLoop node execute n amount of times, you need to set Last Index to n – 1.
For this tutorial, you will spawn three tunnels. To perform three loops, set the Last Index value to 2.
When the game starts, the player should always start in a tunnel. To do this, you can spawn the first tunnel at the player’s location.
To determine if the first tunnel has spawned, you can check if NewestTunnel is set. If it is not set, that means the first tunnel has not spawned. This is because NewestTunnel is only set after the game spawns a tunnel.
To perform this check, add an IsValid node (the one with a question mark icon) after the ForLoop node.
Next, get a reference to NewestTunnel and connect it to the Input Object pin of the IsValid node.
If NewestTunnel is not set, the Is Not Valid pin will execute and vice versa.
Add the following and connect it to the Is Not Valid pin of the IsValid node:
This setup will spawn a tunnel at the location of the player Pawn.
Next, you will spawn the subsequent tunnels.
Add a SpawnTunnelAtAttachPoint node and connect it to the Is Valid pin of the IsValid node.
Here is the final graph:
Summary:
Next, go to the Event Graph and delete the SpawnTunnel node. Afterwards, add a SpawnInitialTunnels node after Event BeginPlay.
Now, when the game starts, it will spawn three tunnels.
Click Compile, go back to the main editor and then press Play. The tunnel is now much longer!
The game isn’t very challenging at the moment so let’s add some obstacles.
Here are the meshes you will use as obstacles:
Open BP_Tunnel and go to the Components panel. Add a Static Mesh component and name it WallMesh.
Go to the Details panel and change its Static Mesh property to SM_Hole_01.
Next, set its Location property to (2470, 0, 0). This will place it at the end of the tunnel.
To make the game more interesting, the walls will also be rotating. Add a new Float variable and name it RotateSpeed. Set the Default Value to 30.
Switch to the Event Graph and locate the Event Tick node. Create the following setup:
This will make WallMesh rotate every frame by the supplied amount.
Click Compile and then go back to the main editor. Press Play to see the spinning walls.
Let’s spice it up by adding some variations to the walls.
Instead of creating a new Blueprint for every variation, you can just randomize WallMesh.
Open BP_Tunnel and create a new function called RandomizeWall. Afterwards, create the following graph:
As its name suggests, the Set Static Mesh node will set WallMesh to the supplied mesh.
To make a list of static meshes, you can use a Select node.
Left-click and drag the New Mesh pin. Release left-click on an empty area and then add a Select node.
The Select node allows you to set a list of options. The Index input determines what option the Select node outputs.
Since there are four wall meshes available, you need to create two more Option pins. You can do this by right-clicking the Select node and selecting Add Option Pin. Do this until you have four Option pins.
Next, set each option to the following:
Now, let’s select a random option.
You can use a Random Integer in Range node to get a random number. This node will return a value that is >= Min and <= Max.
Add a Random Integer in Range node and connect it to the Index pin of the Select node.
Set the Max value to 3. This will give you four possible numbers: 0, 1, 2 and 3.
To create a bit more randomization, let’s add a random rotation to WallMesh. Add the following after the Set Static Mesh node:
This will add a random rotation between 0 and 360 degrees to WallMesh.
Here is the final graph:
Summary:
Click Compile and then close the RandomizeWall graph.
Switch to BP_TunnelSpawner and open the SpawnTunnel graph. Add the highlighted node:
Now, whenever a tunnel spawns, it will have a random wall mesh.
Close the SpawnTunnel graph and then click Compile. Go back to the main editor and press Play to see all the wall variations!
If you hit a wall, you will stop moving forward. However, if you move around and go through a hole, you will start moving forward again.
The next step is to disable forward movement when the player collides with a wall.
To enable or disable forward movement, you can use a Boolean variable. These only have two states: true and false.
Open BP_Player and then create a new Boolean variable named IsDead.
Next, go to the Event Tick node and create a Branch node.
Afterwards, get a reference to IsDead and connect it to the Condition pin of the Branch node.
Connect the Event Tick node to the Branch node. Afterwards, connect the False pin of the Branch node to the AddActorWorldOffset node.
Now, whenever IsDead is set to true, the player will stop moving forward.
Next, let’s set the IsDead variable when the player hits a wall.
Click Compile and then switch to BP_Tunnel. In the Components panel, right-click on WallMesh and select Add Event\Add OnComponentHit. This will add the following node to your Event Graph:
This node will execute whenever another Actor collides with WallMesh.
First, you need to check if the Actor that collided with WallMesh is the player.
Left-click and drag the Other Actor pin. Release left-click on an empty area and select Cast to BP_Player from the menu.
Next, left-click and drag the BP_Player pin of the Cast to BP_Player node. Release left-click on an empty space and then add a Set Is Dead node.
Set IsDead to true by left-clicking the checkbox.
Click Compile and then go back to the main editor. Press Play and try hitting a wall. If you move around to a hole, you will no longer move through it.
In the next section, you will display a restart button when the player hits a wall.
The widget that you will display is named WBP_Restart. You can find it in the UI folder. This is what it looks like:
To display or hide the widget, you need a reference to it. Open BP_Player and then create a new variable named RestartWidget. Change the Variable Type to WBP_Restart\Object Reference.
Next, go to the Event Graph and locate the Event BeginPlay node.
Add a Create Widget node and set the Class value to WBP_Restart.
Afterwards, add a Set Restart Widget node and then connect everything like so:
Now, when the player spawns, it will create an instance of WBP_Restart. The next step is to make a function that displays this instance.
Create a new function and name it DisplayRestart. Once you have done that, create the following graph:
Summary:
To display the restart button, all you need to do is call DisplayRestart after the player collides with a wall.
Close the DisplayRestart graph and then click Compile.
Switch to BP_Tunnel and then locate the On Component Hit (WallMesh) node.
Add a DisplayRestart node to the end of the node chain.
Click Compile and then close BP_Tunnel. Go back to the main editor and press Play. If you hit a wall, the restart button will appear.
The last step is to restart the game when the player clicks the button.
There are two things the game needs to do when restarting:
Let’s start with resetting the player.
Open BP_Player and then create a new function called RestartGame. Create the following graph:
Summary:
Next, let’s respawn the tunnels.
Click Compile and then close BP_Player.
Open BP_TunnelSpawner and make sure you are in the SpawnInitialTunnels graph.
First, you need to remove the existing tunnels before you spawn new ones.
Add a Sequence node after the Entry node. Connect the Then 1 pin to the ForLoop node.
Next, create the following nodes:
This setup will get all the existing tunnels and remove them from the game.
Finally, connect the Then 0 pin of the Sequence node to the Get All Actors of Class node. This will make sure the tunnels are removed before the spawning process.
Here is the final graph:
The last thing to do is handle the button click.
Click Compile and then close BP_TunnelSpawner.
Go to the Content Browser and navigate to the UI folder. Double-click on WBP_Restart to open it.
Select RestartButton and then go to the Details panel. Go to the Events section and click the button next to OnClicked.
This will create a node called On Clicked (RestartButton). This node will execute when the player clicks on RestartButton.
Recreate the following:
Summary:
Click Compile and then close the Blueprint editor. Press Play to test out the restart button!
You can download the completed project here.
Now that you have a simple game, you can start building on it. Try adding a score counter that increases when the player avoids a wall.
You should also try to build classic games like Pong and Tetris. The mechanics of these games are simple but can be a challenge to implement.
If you’d like to learn how to make a specific type of game, leave a comment below!
The post How to Create a Simple Game in Unreal Engine 4 appeared first on Ray Wenderlich.
When working with ARKit, it is inevitable that users will run into errors such as low light and failed motion tracking. In this video, you'll learn how to diagnose such issues.
The post Screencast: Introduction to ARKit: Error Management appeared first on Ray Wenderlich.
Next April, we are running an iOS conference focused on high quality hands-on tutorials called RWDevCon 2018.
One of the unique things about RWDevCon is it’s coordinated as a team.
That means we can do some cool things, like let you decide the content of the conference. Here’s how it works:
There’s no other conference like this – RWDevCon is truly a conference where you decide what’s inside.
This process is starting next week, so if you’d like to be a part of the decision making process, grab your ticket now.
We can’t wait to see what you choose this year! :]
The post RWDevCon 2018: Choose Your Topics in One Week! appeared first on Ray Wenderlich.
Update note: This Menus and Popovers in Menu Bar Apps for macOS tutorial has been updated to Xcode 9 and Swift 4 by Warren Burton. The original tutorial was written by Mikael Konutgan.
Menu bar apps have been staple of macOS for a long time. Many apps like 1Password and Day One have companion menu bar apps. Others like Fantastical live exclusively in macOS’s menu bar.
In this menu bar app tutorial, you’ll build a menu bar app that shows inspirational quotes in a popover. While you do this, you’ll learn:
Fire up Xcode. Go to File/New/Project… then select the macOS/Application/Cocoa App template and click Next.
On the next screen, enter Quotes as the Product Name, choose your desired Organization Name and Organization Identifier. Then make sure that Swift is selected as the language, and that Use Storyboards is checked. Uncheck Create Document-Based Application, Use Core Data, Include Unit tests and Include UI Tests.
Finally, click Next again, choose a place to save the project and click Create.
Once the new project is set up, open AppDelegate.swift and add the following property to the class:
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
This creates a Status Item — aka application icon — in the menu bar with a fixed length that the user will see and use.
Next, you’ll need to associate an image to the status item to make your app recognizable in the menu bar.
Go to Assets.xcassets
in the project navigator, download this image StatusBarButtonImage@2x.png and drag it into the asset catalog.
Select the image and open the attributes inspector. Change the Render As option to Template Image.
If you use your own custom image, make sure that the image is black and white and configured as a template image so the Status Item looks great against both light and dark menu bars.
Back in AppDelegate.swift, add the following code to applicationDidFinishLaunching(_:)
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
button.action = #selector(printQuote(_:))
}
This will configure the status item with an icon of the image you just added, and an action for when you click on the item. This will create an error but you’ll fix that now.
Add the following method to the class:
@objc func printQuote(_ sender: Any?) {
let quoteText = "Never put off until tomorrow what you can do the day after tomorrow."
let quoteAuthor = "Mark Twain"
print("\(quoteText) — \(quoteAuthor)")
}
This method will simply log out the quote text to the console.
Take note of the @objc
directive in the signature. This exposes the method to the Objective-C runtime to allow the button to use it as an action.
Build and run the app, and you should see a new menu bar app available. You did it!
Note: If you have too many menu bar apps, you might not be able to see your button. Switch to an app with fewer menus than Xcode (like Finder) and you should be able to see it.
Every time you click on the menu bar icon, you’ll see the quote printed out in the Xcode console.
There are still two small things to do before you have a functional menu bar app.
To disable the dock icon, open Info.plist. Add a new key Application is agent (UIElement) and set its value to YES.
LSUIElement
.
Now it’s time to handle the main window.
Main.storyboard
Build and run. You’ll see the app has no main window, no pesky dock icon and only a tidy status item in the menu bar. High five yourself :]
Usually, a measly single action on click is not enough for a menu bar app. The easiest way to add more functionality to your app is to add a menu. Add the following function to the end of AppDelegate
.
func constructMenu() {
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P"))
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
statusItem.menu = menu
}
and then add this call to the end of applicationDidFinishLaunching(_:)
constructMenu()
Here you create an NSMenu
, add 3 instances of NSMenuItem
to it, and then set the status item’s menu to that new menu.
A few things to note here:
keyEquivalent
is a keyboard shortcut that you can use to activate the menu item. A lowercase letter uses Cmd as the modifier key and an uppercase letter uses Cmd+Shift. This keyboard shortcut only works if the application is front-most and active. So, in this case, the menu or any other window needs to be visible, since the app has no dock icon.separatorItem
is a stock inactive menu item that appears as a simple gray line between other menu items. Use it to group functionality in the menu.printQuote:
action is the method you already defined in AppDelegate
while terminate:
is an action method defined by NSApplication
.Build and run, and you should see a menu when clicking on the status item. Progress!
Try out your options – selecting Print Quote will display the quote in the Xcode console, while Quit Quotes will quit the app.
You’ve seen how easy it is to set up a menu from code, but showing the quote in the Xcode console won’t cut it for most of your end users. The next step is to replace the menu with a simple view controller to show a quote right in place.
Go to File/New/File…, select the macOS/Source/Cocoa Class template and click Next.
QuotesViewController
.NSViewController
.Finally, click Next again, choose a place to save the file (In the Quotes subfolder of the project folder is a good place) and click Create.
Now open Main.storyboard. Expand the View Controller Scene and select the View Controller instance.
First select the Identity Inspector and change the Class to QuotesViewController
, next set the Storyboard ID to QuotesViewController
Next add the following code to the end of QuotesViewController.swift
extension QuotesViewController {
// MARK: Storyboard instantiation
static func freshController() -> QuotesViewController {
//1.
let storyboard = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil)
//2.
let identifier = NSStoryboard.SceneIdentifier(rawValue: "QuotesViewController")
//3.
guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? QuotesViewController else {
fatalError("Why cant i find QuotesViewController? - Check Main.storyboard")
}
return viewcontroller
}
}
What happens here is…
You create this method so that anything thats using QuotesViewController
doesn’t need to know how to instantiate it. It just works :]
Notice the fatalError
inside the guard
statement. Its often good to use this or assertionFailure
to let yourself or other team members know when you have messed up during development.
Now go back to AppDelegate.swift. Start by adding a new property declaration to the class:
let popover = NSPopover()
Next, replace applicationDidFinishLaunching(_:)
with the following:
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
button.action = #selector(togglePopover(_:))
}
popover.contentViewController = QuotesViewController.freshController()
}
You’ve changed the button action to togglePopover(_:)
which you’ll implement next. Also, rather than set up a menu, you’re setting up the popover to show whatever’s in QuotesViewController.
Add the following three methods to AppDelegate
@objc func togglePopover(_ sender: Any?) {
if popover.isShown {
closePopover(sender: sender)
} else {
showPopover(sender: sender)
}
}
func showPopover(sender: Any?) {
if let button = statusItem.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
func closePopover(sender: Any?) {
popover.performClose(sender)
}
showPopover()
displays the popover to the user. You just need to supply a source rect and macOS will position the popover and arrow so it looks like it’s coming out of the menu bar icon.
closePopover()
simply closes the popover, and togglePopover()
is the action method that will either open or close the popover depending on its current state.
Build and run, and then click on the menu bar icon to check that it shows and then hides an empty popover.
Your popover works great, but where’s all the inspiration? All you see is an empty view and no quotes. Guess what you’ll do next?
First, you need a model to store the quotes and attributions. Go to File/New/File… and select the macOS/Source/Swift File template, then click Next. Name the file Quote and click Create.
Open Quote.swift and add the following code to the file:
struct Quote {
let text: String
let author: String
static let all: [Quote] = [
Quote(text: "Never put off until tomorrow what you can do the day after tomorrow.", author: "Mark Twain"),
Quote(text: "Efficiency is doing better what is already being done.", author: "Peter Drucker"),
Quote(text: "To infinity and beyond!", author: "Buzz Lightyear"),
Quote(text: "May the Force be with you.", author: "Han Solo"),
Quote(text: "Simplicity is the ultimate sophistication", author: "Leonardo da Vinci"),
Quote(text: "It’s not just what it looks like and feels like. Design is how it works.", author: "Steve Jobs")
]
}
extension Quote: CustomStringConvertible {
var description: String {
return "\"\(text)\" — \(author)"
}
}
This defines a simple quote structure and a static property that returns all the quotes. Since you also make Quote
conform to CustomStringConvertible
, you can easily get a nice formatted string.
You’re making progress, but you now need some function in the UI to display all these famous quotes.
Open Main.storyboard
and drag 3 Push Button instances, and a Multiline Label into the view controller.
Drag the buttons and label into place until they look like this layout. The dotted blue layout guides that appear will help you get the items into place:
Can you add the auto layout constraints to make the user interface match? Give it a few good attempts before you open the spoiler below. If you get it right, skip the spoiler and give yourself a gold star.
Solution Inside | SelectShow> |
---|---|
Here are the auto layout constraints you need to get the correct layout:
You are going to see a set of layout errors as there isn’t enough information for auto layout to figure things out yet. Set the Horizontal Content Hugging Priority of the label to 249 to allow the label to grow properly.
|
Once you have the layout setup to your satisfaction set up the elements like this:
NSGoLeftTemplate
and delete the title.NSGoRightTemplate
and delete the title.Now open QuotesViewController.swift and add the the following code to the class implementation of QuotesViewController
:
@IBOutlet var textLabel: NSTextField!
Add this extension after the class implementation. You will now have two extensions in QuotesViewController.swift.
// MARK: Actions
extension QuotesViewController {
@IBAction func previous(_ sender: NSButton) {
}
@IBAction func next(_ sender: NSButton) {
}
@IBAction func quit(_ sender: NSButton) {
}
}
You have just added an outlet for the text label, which you’ll use to display the inspirational quote and 3 stub actions which you will connect to the 3 buttons.
You’ll notice that Xcode has placed circles in the left hand margin of your source editor. The circles are handles that appear when you use the @IBAction
and @IBOutlet
keywords.
You will now use them to connect your code to the UI.
While holding down alt click on Main.storyboard in the project navigator. This should open the storyboard in the Assistant Editor on the right and the source on the left.
Drag from the circle next to textLabel
to the label in interface builder. In the same way connect the previous, next and quit actions to the left, right and bottom buttons respectively.
Note: If you have trouble with any of the above steps, refer to our library of macOS tutorials, where you’ll find introductory tutorials that will walk you through many aspects of macOS development, including adding views/constraints in interface builder and connecting outlets and actions.
Stand up, stretch and maybe do a quick victory lap around your desk because you just flew through a bunch of interface builder work.
Build and run, and your popover should look like this now:
You used the default size of the view controller for the popover above. If you want a smaller or bigger popover, all you need to do is resize the view controller in the storyboard.
The interface is finished, but you’re not done yet. Those buttons are waiting on you to know what to do when the user clicks them — don’t leave them hanging.
If you haven’t already dismiss the Assistant Editor with Cmd-Return or View > Standard Editor > Show Standard Editor
Open QuotesViewController.swift and add the following properties to the class implementation:
let quotes = Quote.all
var currentQuoteIndex: Int = 0 {
didSet {
updateQuote()
}
}
The quotes
property holds all the quotes, and currentQuoteIndex
holds the index of the current quote displayed. currentQuoteIndex
also has a property observer to update the text label string with the new quote when the index changes.
Next, add the following methods to the class:
override func viewDidLoad() {
super.viewDidLoad()
currentQuoteIndex = 0
}
func updateQuote() {
textLabel.stringValue = String(describing: quotes[currentQuoteIndex])
}
When the view loads, you set the current quote index to 0, which in turn updates the user interface. updateQuote()
simply updates the text label to show whichever quote is currently selected according to currentQuoteIndex
.
To tie it all together, update the three action methods as follows;
@IBAction func previous(_ sender: NSButton) {
currentQuoteIndex = (currentQuoteIndex - 1 + quotes.count) % quotes.count
}
@IBAction func next(_ sender: NSButton) {
currentQuoteIndex = (currentQuoteIndex + 1) % quotes.count
}
@IBAction func quit(_ sender: NSButton) {
NSApplication.shared.terminate(sender)
}
In next()
and previous()
, you cycle through the all the quotes and wrap around when you reach the ends of the array. quit
terminates the app.
Build and run again, and now you can cycle back and forward through the quotes and quit the app!
There is one feature your users will want in your unobtrusive, small menu bar app, and that’s when you click anywhere outside the app, the popover automatically closes.
Menu bar apps should open the UI on click, and then disappear once the user moves onto the next thing. For that, you need an macOS global event monitor.
Next you’ll make an event monitor thats reusable in all your projects and then use it when showing the popover.
Bet you’re feeling smarter already!
Create a new Swift File and name it EventMonitor, and then replace its contents with the following class definition:
import Cocoa
public class EventMonitor {
private var monitor: Any?
private let mask: NSEvent.EventTypeMask
private let handler: (NSEvent?) -> Void
public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
self.mask = mask
self.handler = handler
}
deinit {
stop()
}
public func start() {
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
}
public func stop() {
if monitor != nil {
NSEvent.removeMonitor(monitor!)
monitor = nil
}
}
}
You initialize an instance of this class by passing in a mask of events to listen for – things like key down, scroll wheel moved, left mouse button click, etc – and an event handler.
When you’re ready to start listening, start()
calls addGlobalMonitorForEventsMatchingMask(_:handler:)
, which returns an object for you to hold on to. Any time the event specified in the mask occurs, the system calls your handler.
To remove the global event monitor, you call removeMonitor()
in stop()
and delete the returned object by setting it to nil
.
All that’s left is calling start()
and stop()
when needed. How easy is that? The class also calls stop()
for you in the deinitializer, to clean up after itself.
Open AppDelegate.swift one last time, and add a new property declaration to the class:
var eventMonitor: EventMonitor?
Next, add the code to configure the event monitor at the end of applicationDidFinishLaunching(_:)
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
if let strongSelf = self, strongSelf.popover.isShown {
strongSelf.closePopover(sender: event)
}
}
This notifies your app of any left or right mouse down event and closes the popover when the system event occurs. Note that your handler will not be called for events that are sent to your own application. That’s why the popover doesn’t close when you click around inside of it. :]
You use a weak
reference to self to avoid a potential retain cycle between AppDelegate
and EventMonitor
. It’s not essential in this particular situation because there’s only one setup cycle but is something to watch out for in your own code when you use block handlers between objects.
Add the following code to the end of showPopover(_:)
:
eventMonitor?.start()
This will start the event monitor when the popover appears.
Then, you’ll need to add the following code to the end of closePopover(_:)
:
eventMonitor?.stop()
This will stop the event monitor when the popover closes.
All done! Build and run the app one more time. Click on the menu bar icon to show the popover, and then click anywhere else and the popover closes. Awesome!
Here is the final project with all of the code you’ve developed in the above tutorial.
You’ve seen how to set up both menus and popovers in your menu bar status items – why not keep experimenting with using attributed text or multiple labels to style the quote, or connecting to a web backend to pull new quotes. Maybe you can discover how to use the keyboard to cycle through the quotes.
A good place to look for other possibilities is reading the official documentation for NSMenu
, NSPopover
and NSStatusItem
.
One thing to consider is that you are asking your customers for a very privileged piece of screen real estate so while you might think its cool to have a status bar item your users might not feel the same way. A lot of apps manage this by providing preferences to show or hide the item. You can use that as an advanced exercise for yourself.
Thanks for taking the time to learn how to make a cool popover menu app for macOS. For now, it’s pretty simple, but you can see that the concepts you’ve learned here are an excellent foundation for a variety of apps.
If you have any questions, awesome discoveries or ideas you’d like to bounce off others as you configure status items, menus or popovers in your apps, please let me know in the forum discussion below! :]
The post Menus and Popovers in Menu Bar Apps for macOS appeared first on Ray Wenderlich.
Update note:: This tutorial has been updated for Xcode 9, iOS 11 and Swift 4 by Michael Katz. The original tutorial was written by Rounak Jain.
A long while ago, Michael Villar created a really interesting loading animation for his post on Motion Experiments.
The GIF to the right shows the loading animation, which marries a circular progress indicator with a circular reveal animation. The combined effect is fascinating, unique, and more than a little mesmerizing! :]
This CAShapeLayer
tutorial will show you how to recreate this exact effect in Swift and Core Animation. Let’s get animating!
First download the starter project for this CAShapeLayer
tutorial.
Take a minute and browse through the project once you’ve extracted it. There’s a ViewController
that has a UIImageView
subclass named CustomImageView
, along with a SDWebImage
method call to load the image. The starter project already has the views and image loading logic in place.
Build and run. After a moment, you should see a simple image displayed as follows:
You might notice when you first run the app, the app seems to pause for a few seconds while the image downloads, then the image appears on the screen without fanfare. Of course, there’s no circular progress indicator at the moment – that’s what you’ll create in this CAShapeLayer
tutorial!
You’ll create this animation in two distinct phases:
Follow along closely to prevent yourself from going “round in circles”! :]
Think for a moment about the basic design of the progress indicator. The indicator is initially empty to show a progress of 0%, then gradually fills in as the image is downloaded. This is fairly simple to achieve with a CAShapeLayer
whose path
is a circle.
CAShapeLayer
(or CALayers
in general, check out Scott Gardner’s CALayer in iOS with Swift article.You can control the start and end position of the outline, or stroke, of your shape with the CAShapeLayer
properties strokeStart
and strokeEnd
. By varying strokeEnd
between 0 and 1, you can fill in the stroke appropriately to show the progress of the download.
Let’s try this out. Create a new file with the iOS\Source\Cocoa Touch Class template. Name it CircularLoaderView
and set subclass of to UIView
as shown below:
Click Next, and then Create. This new subclass of UIView
will house all of your new animation code.
Open CircularLoaderView.swift and add the following properties to the top of the class:
let circlePathLayer = CAShapeLayer()
let circleRadius: CGFloat = 20.0
circlePathLayer
represents the circular path, while circleRadius
will be the radius of the circular path. Rocket science! I know.
Next, add the following initialization code right below circleRadius
to configure the shape layer:
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
func configure() {
circlePathLayer.frame = bounds
circlePathLayer.lineWidth = 2
circlePathLayer.fillColor = UIColor.clear.cgColor
circlePathLayer.strokeColor = UIColor.red.cgColor
layer.addSublayer(circlePathLayer)
backgroundColor = .white
}
Both of the initializers call configure()
. configure()
sets up circlePathLayer
to have a frame that matches the view’s bounds, a line width of 2 points, a clear fill color and a red stroke color. Next, it adds the shape layer as a sublayer of the view’s own layer and sets the view’s backgroundColor
to white so the rest of the screen is blanked out while the image loads.
Now you’ve configured the layer, it’s time to set its path. Start by adding the following helper method right below configure()
:
func circleFrame() -> CGRect {
var circleFrame = CGRect(x: 0, y: 0, width: 2 * circleRadius, height: 2 * circleRadius)
let circlePathBounds = circlePathLayer.bounds
circleFrame.origin.x = circlePathBounds.midX - circleFrame.midX
circleFrame.origin.y = circlePathBounds.midY - circleFrame.midY
return circleFrame
}
In this simple method you calculate the CGRect
to contain the indicator’s path. You set the bounding rectangle to have a width and a height equals to 2 * circleRadius
and position it at the center of the view. The reason why you wrote a separate method to handle this simple operation is you’ll need to recalculate circleFrame
each time the view’s size changes.
Next, add the following method below circleFrame()
to create your path:
func circlePath() -> UIBezierPath {
return UIBezierPath(ovalIn: circleFrame())
}
This simply returns the circular UIBezierPath
as bounded by circleFrame()
. Since circleFrame()
returns a square, the “oval” in this case will end up as a circle.
Since layers don’t have an autoresizingMask
property, you’ll override layoutSubviews
to respond appropriately to changes in the view’s size.
Override layoutSubviews()
by adding the following code:
override func layoutSubviews() {
super.layoutSubviews()
circlePathLayer.frame = bounds
circlePathLayer.path = circlePath().cgPath
}
You’re calling circlePath()
here because a change in the frame should also trigger a recalculation of the path.
Open CustomImageView.swift. Add the following property to the top of the class:
let progressIndicatorView = CircularLoaderView(frame: .zero)
This property is an instance of the CircularLoaderView
class you just created.
Next, add the following to init(coder:)
, right before let url...
:
addSubview(progressIndicatorView)
addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "V:|[v]|", options: .init(rawValue: 0),
metrics: nil, views: ["v": progressIndicatorView]))
addConstraints(NSLayoutConstraint.constraints(
withVisualFormat: "H:|[v]|", options: .init(rawValue: 0),
metrics: nil, views: ["v": progressIndicatorView]))
progressIndicatorView.translatesAutoresizingMaskIntoConstraints = false
Here you add the progress indicator view as a subview of the custom image view. Then you add two layout constraints to ensure the progress indicator view remains the same size as the image view. Finally, you set translatesAutoresizingMaskIntoConstraints
to false
so the autoresizing mask doesn’t interfere with the Auto Layout engine.
Build and run your project; you should see a red, hollow circle appear like so:
Awesome! Your progress indicator is showing on the screen.
Open CircularLoaderView.swift and add the following lines directly below the other properties in the file:
var progress: CGFloat {
get {
return circlePathLayer.strokeEnd
}
set {
if newValue > 1 {
circlePathLayer.strokeEnd = 1
} else if newValue < 0 {
circlePathLayer.strokeEnd = 0
} else {
circlePathLayer.strokeEnd = newValue
}
}
}
Here you create a computed property — that is, a property without any backing variable — that has a custom setter and getter. The getter simply returns circlePathLayer.strokeEnd
, and the setter validates the input is between 0 and 1 and sets the layer’s strokeEnd
property accordingly.
Add the following line at the top of configure()
to initialize progress
on first run:
progress = 0
Build and run your project; you should see nothing but a blank white screen. Trust me! This is good news! :] Setting progress
to 0 in turn sets the strokeEnd
to 0, which means no part of the shape layer was drawn.
The only thing left to do with your indicator is to update progress
in the image download callback.
Open CustomImageView.swift and replace the comment Update progress here
with the following:
self?.progressIndicatorView.progress = CGFloat(receivedSize) / CGFloat(expectedSize)
Here you calculate the progress by dividing receivedSize
by expectedSize
.
Build and run your project. You'll see the progress indicator begin to move like so:
Even though you didn't add any animation code yourself, CALayer
handily detects any animatable property on the layer and smoothly animates it as it changes. Neat!
That takes care of the first phase. Now on to the second and final phase — the big reveal! :]
The reveal phase gradually displays the image in a window in the shape of an expanding circular ring. If you’ve read this tutorial on creating a Ping-style view controller animation, you'll know this is a perfect use-case of the mask
property of a CALayer
.
Open CircularLoaderView.swift and add the following method:
func reveal() {
// 1
backgroundColor = .clear
progress = 1
// 2
circlePathLayer.removeAnimation(forKey: "strokeEnd")
// 3
circlePathLayer.removeFromSuperlayer()
superview?.layer.mask = circlePathLayer
}
This is an important method to understand, so let's go over this section by section:
progress
to 1.strokeEnd
property, which may have otherwise interfered with the reveal animation. For more about implicit animations, check out iOS Animations by Tutorials.circlePathLayer
from its superLayer
and assign it instead to the superView
’s layer mask, so the image is visible through the circular mask "hole". This lets you reuse the existing layer and avoid duplicating code. Now you need to call reveal()
from somewhere. Replace the Reveal image here
comment in CustomImageView.swift with the following:
if let error = error {
print(error)
}
self?.progressIndicatorView.reveal()
Build and run. Once the image downloads you'll see it partially revealed through a small ring:
You can see your image in the background — but just barely! :]
Your next step is to expand this ring both inwards and outwards. You could do this with two separate, concentric UIBezierPath
, but you can do it in a more efficient manner with just a single Bezier path.
How? You simply increase the circle’s radius to expand outward by changing the path
property, while simultaneously increasing the line's width to make the ring thicker and expand inward by changing the lineWidth
property. Eventually, both values grow enough to reveal the entire image underneath.
Open CircularLoaderView.swift and add the following code to the end of reveal()
:
// 1
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let finalRadius = sqrt((center.x*center.x) + (center.y*center.y))
let radiusInset = finalRadius - circleRadius
let outerRect = circleFrame().insetBy(dx: -radiusInset, dy: -radiusInset)
let toPath = UIBezierPath(ovalIn: outerRect).cgPath
// 2
let fromPath = circlePathLayer.path
let fromLineWidth = circlePathLayer.lineWidth
// 3
CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
circlePathLayer.lineWidth = 2*finalRadius
circlePathLayer.path = toPath
CATransaction.commit()
// 4
let lineWidthAnimation = CABasicAnimation(keyPath: "lineWidth")
lineWidthAnimation.fromValue = fromLineWidth
lineWidthAnimation.toValue = 2*finalRadius
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.fromValue = fromPath
pathAnimation.toValue = toPath
// 5
let groupAnimation = CAAnimationGroup()
groupAnimation.duration = 1
groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
groupAnimation.animations = [pathAnimation, lineWidthAnimation]
circlePathLayer.add(groupAnimation, forKey: "strokeWidth")
This might look like a lot of code, but what you're doing here is fairly simple:
CGRect
that would fully bound this circle. toPath
represents the final shape of the CAShapeLayer
mask like so:lineWidth
and path
to match the current values of the layer.lineWidth
and path
to their final values. This prevents them from jumping back to their original values when the animation completes. By wrapping this changes in a CATransaction
with kCATransactionDisableActions
set to true
you disable the layer’s implicit animations.CABasicAnimation
: one for path
and the other for lineWidth
. lineWidth
has to increase twice as fast as the radius increases in order for the circle to expand inward as well as outward.CAAnimationGroup
, and add the animation group to the layer.Build and run your project. You’ll see the reveal animation kick-off once the image finishes downloading:
Notice a portion of the circle remains on the screen once the reveal animation is done. To fix this, add the following extension to the end of CircularLoaderView.swift implementing animationDidStop(_:finished:)
:
extension CircularLoaderView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
superview?.layer.mask = nil
}
}
This code removes the mask on the super layer, which removes the circle entirely.
Finally, at the bottom of reveal()
, just above the line circlePathLayer.add(groupAnimation, forKey: "strokeWidth")
add the following line:
groupAnimation.delegate = self
This assigns the delegate so the animationDidStop(_:finished:)
gets called.
Build and run your project. Now you’ll see the full effect of your animation:
Congratulations, you've finished creating the circular image loading animation!
You can download the completed project here.
From here, you can further tweak the timing, curves and colors of the animation to suit your needs and personal design aesthetic. One possible improvement is to use kCALineCapRound
for the shape layer's lineCap
property to round off the ends of the circular progress indicator. See what improvements you can come up with on your own!
If you enjoyed this CAShapeLayer
tutorial and would like to learn how to create more animations like these, check out Marin Todorov's book iOS Animations by Tutorials, which starts with basic view animations and moves all the way to layer animations, animating constraints, view controller transitions, and more.
If you have any questions or comments about the CAShapeLayer
tutorial, please join the discussion below. I'd also love to see ways in which you've incorporated this cool animation in your app!
The post How To Implement A Circular Image Loader Animation with CAShapeLayer appeared first on Ray Wenderlich.
Great news everyone: The second early access release of iOS 11 by Tutorials is now available!
This release has seven chapters:
This is the second early access release for the book – stay tuned for another early access release soon!
Here’s how you can get your early access copy of iOS 11 by Tutorials:
If you haven’t yet pre-ordered iOS 11 by Tutorials
, we’re offering a limited-time, pre-order sale price of $44.99.When you pre-order the book, you’ll get exclusive access to the upcoming early access releases of the book so you can get a jumpstart on learning all the new APIs. The full edition of the book will be released in Fall 2017.
Gone are the days when every third-party developer knew everything there is to know about iOS. The sheer size of iOS can make new releases seem daunting. That’s why the Tutorial Team has been working really hard to extract the important parts of the new APIs, and to present this information in an easy-to-understand tutorial format. This means you can focus on what you want to be doing — building amazing apps!
What are you most looking forward to learning about in iOS 11? Respond in the comments below and let us know!
The post iOS 11 by Tutorials: First 7 Chapters Now Available! appeared first on Ray Wenderlich.
While many of you spent your week at 360iDev to expand your programming knowledge, I’ve been enjoying our reader’s apps for this month!
I’m honored you are taking a quick break to see what the community has been building. Every month, readers like you release great apps built with a little help from our tutorials, books, and videos. I want to highlight a few today to give just a peek at what our fantastic readers are creating.
This month we have:
Keep reading to see the latest apps released by raywenderlich.com readers just like you.
If you’re the type of person who always loves to know what your friends are doing, while also making sure that they know what you’re up to, then Pingtumi is the app for you.
Pingtumi is a notification/social media service that allows you to follow your friends, as well as other people and be notified of where they are and what they are doing through the posts that they make.
The way these posts work is similar to a subscription feed. First, you create a feed around whatever theme you want by giving it a title and picture. Second, you invite people to subscribe to your feed.
Finally, you add posts to your feed that can contain your location, a title, a picture, and a message. All of your subscribers will be notified about your post and be able to leave comments on it.
It’s a very simple and focused alternative to other social media outlets, and I suggest that you give it a try and leave a review!
This app is for all you coffee addicts out there! Roastafarian is an app that helps you keep track of your favorite brews.
You are able to keep a journal including the coffee’s name, price, retailer, roaster, your personal rating of it, and any additional notes you might have about its flavor or preparation.
Then, when you aren’t quite sure of what you thought about that weird coffee from a few months ago, you can easily search through your list of coffee brews by using keywords, ratings, origin, or most recent. You’ll always know exactly what to order or how to make your favorite coffee brews.
If you’re a coffee nut, you should definitely give Roastafarian a download to help feed your coffee cravings.
Sometimes map apps can be a bit hard to deal with, by filtering certain locations based on its complex algorithms and searching within its own, unchangeable radius. Near Vicinity eliminates those problems by allowing you to search for keywords in location titles and determining your own search radius.
Just search for location titles or tags in the search bar. Then, pick your search radius, which can be anywhere from 50 meters to 10,000 meters.
Near Vicinity will show you all of the locations that meet your search criteria and provide them for you as map pins, or in a list. You can tap on them for more information about the location and also favorite that location so you can find it easily the next time.
Near Vicinity is a great way for you to keep track of your favorite locations, or just search nearby to see if your favorite restaurant exists while you’re visiting a new place.
Let’s be honest, the default calculator app is somewhat limited in its capabilities. The aioCalculator (all in one Calculator) is for those of us who need to do more than what the basic calculator app allows.
The aioCalculator app really is all inclusive as the name suggests. It includes multitasking, a unit converter, a history page that allows you to see previous calculations, undo and redo capabilities, and many other features.
It even allows you to share the results of your calculations with your contacts so that they can stay up to speed with how much you’re getting done, thanks to your fancy new calculator app.
If you are in need of a calculator that allows you to perform strenuous calculations, as well as reviewing previous calculations and sharing them with your contacts, the aioCalculator is here to help you out. Download it for free and without ads!
Everyone loves a good bit of trivia, especially whenever it’s in the form of a competitive game. Eventology allows you to pick whether or not certain events happened before or after a given date and then gives you points based on how many you got correct in that round.
You can play solo to test your history knowledge from a range of different historical periods or be more focused and complete every challenge in the “Journey Through Time” tab. This challenge mode takes you through different eras of time from The Big Bang to Stardate 41254.7.
Then, once you feel confident, challenge your friends or anyone else online to show them that you are the true history buff and that your knowledge cannot be surpassed!
I’ve had a great time playing and learning with Eventology and I bet you will too!
If you travel internationally a lot and would like to keep track of different countries exchange rates, as the name suggests, Rapid Currency Converter will help you to do just that.
You pick your home country and then pick from a list of many other countries to compare your currency rates with theirs. You will easily be able to see what your buying capacity in that country will be before you get there and it is all stored in one place. The app can also be used offline, though it may be a bit inaccurate depending on when you were connected to the internet last.
The Rapid Currency Converter World Exchange Rates List app is the perfect companion for anyone that does a great deal of international travel, stock market trading, or just has an interest in world economics. Try it out, it’s a lot easier than sifting through google search results.
If you want to be able to track your monthly income and daily expenses without all of the hassle of inputting every bit of personal information, then Fialy is the perfect financial assistant for you.
Fialy helps you to keep track of your income and expenses in a simple and intuitive way. You just tell it when and how much you received from your paycheck each time you get paid.
Then, whenever you make a purchase, just log it in Fialy and it will tell you your remaining budget for the month. It is much less complicated than the other financial assistant apps out there and is great for those people that like to manually input their budget data.
If you’re looking to get into budgeting in a simple way, then use Fialy for a bit and see what you think.
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 you 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 Reader’s App Reviews – August 2017 appeared first on Ray Wenderlich.
In this introduction, you'll be introduced to Firebase, why using it is a good idea, and what the course will cover.
The post Video Tutorial: Beginning Firebase Part 1: Introduction appeared first on Ray Wenderlich.
In this video, you'll learn how to install Firebase into your app by way of the dependency management framework, CocoaPods.
The post Video Tutorial: Beginning Firebase Part 2: Installing Firebase appeared first on Ray Wenderlich.
Installing Firebase is not enough. You actually have to configure it in order to work with your app. In this video, you'll learn how to do this.
The post Video Tutorial: Beginning Firebase Part 3: Configuring Firebase appeared first on Ray Wenderlich.
We’ve recently given you the chance to earn your #GitWarrior badge with our Beginning Git course, and complete your Git training with Mastering Git. Today, we’re continuing our coverage of cross-platform tools with another new course: Beginning Firebase.
The Firebase database is a network-centric database run by Google that allows you to develop realtime-centric applications, such as chat applications, that can connect with hundreds of simultaneous users.
In this course, you’ll start from the beginning, learning how to set up and connect to Firebase, and how to save and retrieve data. By the end of the course, you’ll know how to implement a user management system. You’ll learn about user authentication, how to register users, and log users in an out, and more!
Let’s take a look at what’s inside:
Video 1: Introduction (Free!)
In this introductory video, you’ll learn about Firebase, why using it is a good idea, and what the course will cover.
Video 2: Installing Firebase
In this video, you’ll learn how to install Firebase into your app by way of the dependency management framework, CocoaPods.
Video 3: Configuring Firebase
Installing Firebase is not enough. You actually have to configure it in order to work with your app. In this video, you’ll learn how to do this.
Video 4: JSON Challenge
In your first challenge of the course, you will design the structure of the JSON that you will use in this course.
Video 5: References (Free!)
References are what you use to save and read data to Firebase. In this video, you’ll learn how to use references in your Swift app.
Video 6: Saving Data
Saving data in Firebase is handled by references. In this video, you’ll set values to references and save them to Firebase.
Video 7: Reading Data
Saving data is one part of the equation. You also have to read it back. This video will show you how to do that with Firebase
Video 8: Reading Data Challenge
In your next challenge, you’ll read some data from Firebase. I’ll get you started and you’ll finish the rest.
Video 9: Updating and Deleting
Nothing lasts forever. Especially data. In this video, you’ll learn how to update data, but also, how to delete it.
Video 10: Deleting Data Challenge
In your third and final challenge of this section, you’ll delete some data using an alternative method briefly discussed in a previous video.
Video 11: Querying Data
You’ll often times need to get specific information from the database. This video will introduce you to query methods.
Video 12: Section 1 Conclusion
This video concludes the first section on Firebase. It reviews what was covered, and what is coming up next.
Video 1: Introduction – User Accounts
This video will introduce you to Firebase’s registration system which you’ll use to register and log-in users.
Video 2: User Authentication
Firebase has many different authentication methods available to you. In this video, you’ll learn how to setup email authentication.
Video 3: Keychain Sharing
Working with Firebase, you’ll be required to enable keychain sharing. Curious about the keychain? Watch on to find out about it.
Video 4: User Creation
When making a user account system, users will need to create accounts. This video will walk you through the process.
Video 5: Error Handling Challenge
When working user registration systems, you’ll encounter lots of errors. This challenge will introduce to the process of handling them.
Video 6: User Login
Once you have user accounts created, you have to login those users. This video will cover the process of handling user login.
Video 7: User Login Challenge
When creating a new user account, users will expect to be automatically logged into the system. Your challenge is to make it happen.
Video 8: Online Users
Firebase has the ability to allow you to display online user information. This isn’t an API. It’s just a clever use of references.
Video 9: Online User Count Challenge
You’ve learned how to get a list of currently online users. Your final challenge in this course is to display a user count to the end user.
Video 10: Course Conclusion
In your third and final challenge of this section, you’ll delete some data using an alternative method briefly discussed in a previous video.
Want to check out the course? You can watch two of the videos for free:
The rest of the course is for raywenderlich.com subscribers only. Here’s how you can get access:
There’s much more in store for raywenderlich.com subscribers – if you’re curious, you can check out our full schedule of upcoming courses.
I hope you enjoy our new course, and stay tuned for many more new courses and updates to come! :]
The post New Course: Beginning Firebase appeared first on Ray Wenderlich.
Update note: This tutorial has been updated to iOS 11, Xcode 9, and Swift 4 by Ryan Ackermann. The original tutorial was written by Matthijs Hollemans.
Have you ever been frustrated trying to make your apps look good in both portrait and landscape orientation? Is making screen layouts that support both the iPhone and iPad driving you to the brink of madness?
Despair no longer, I bring you good news!
Not only does Auto Layout make it easy to support different screen sizes in your apps, as a bonus it also makes internationalization almost trivial. You no longer have to make new nibs or storyboards for every language that you wish to support, and this includes right-to-left languages such as Hebrew or Arabic.
In this Auto Layout tutorial, you’ll learn all about constraints and how to apply them!
If you’re relatively new to iOS development, the phrase “Auto Layout” may be completely foreign to you. So what is it exactly? Well, in the beginning, Apple made only one screen size for the iPhone. And it was good. Developers had an easy time creating interfaces that didn’t have to be terribly flexible, as they really only had to fit that one size. Fast forward to today, and you’re in a much different world: different sized iPhones, iPads, and more emphasis on landscape mode all demand user interfaces of different sizes. Rather than expect developers to create different interfaces for each screen size, Apple came up with a system for allowing elements within a user interface to grow, shrink, and move depending on the size of the screen.. and they called it Auto Layout.
Auto Layout is a system of constraints, or UI based rules, that govern the size and position of elements on the screen. While that may sound simple on the surface, as you’ll see in the tutorial Auto Layout can get quite complex very quickly! Before you dive in, it will be helpful to orient yourself with all the tools in Xcode and Interface Builder.
Before you start creating a simple user interface, you should take a tour of Xcode and Interface Builder. Interface Builder is a graphical tool inside Xcode that allows developers like you to create user interfaces using Auto Layout. Here’s some of the common terminology you will come across when using Interface Builder.
To start learning the ins and outs of Interface Builder, you’ll first create an empty iPhone application.
Open up Xcode, and select Create a new Xcode project. Select the Single View App template and enter “Buttons” for the Product Name. Leave other options to their defaults.
Select Main.storyboard and make sure that Use Auto Layout, Use Trait Variations, and Use Safe Area Layout Guides are all enabled via the Storyboard’s File Inspector:
Alright it’s time to get your hands dirty! The best way to get comfortable with Auto Layout is to dive right in and create a user interface, so proceed to the next section and get ready to move fast and furious!
Drag two buttons into the scene and give them a background color of yellow and green. Put the yellow button below the green button.
Select the green button and use the Pin menu on the bottom right to make a constraint to its nearest bottom neighbor (40 points). Then select the lower button and do the same (8 points).
Open the Align menu with the yellow button selected, and check Horizontally in Container. Now, select both buttons, and in the Align menu check Leading Edges and press Add Constraints.
You might see a red circle with an arrow in it on the top left. Usually, this indicates an issue that you need to fix. Don’t worry about this for now, as you’ll fix it later. :]
Playing with this in Interface Builder is all well and good, but now you’ll see how this works at runtime. Add the following method to ViewController.swift:
@IBAction func buttonTapped(_ sender: UIButton) {
if sender.title(for: .normal) == "X" {
sender.setTitle("A very long title for this button", for: .normal)
} else {
sender.setTitle("X", for: .normal)
}
}
This toggles between a long title and a short title for the button that triggered the event. Connect this action method to both of the buttons in Interface Builder. Ctrl-drag from each button to the view controller and select buttonTapped: in the popup.
Run the app and tap the buttons to see how it behaves. Perform the test in both portrait and landscape orientations.
Regardless of which button has the long title and which has the short title, the layout always satisfies the constraints you have given it:
That is the entire specification for your user interface.
For fun, remove the Leading Alignment constraint (select it in the
and press Delete on your keyboard), then select both buttons in Interface Builder and from the Align menu select the Trailing Edges option.
Now run the app again and notice the differences.
Repeat, but now choose Align\Horizontal Centers. That will always center the top button with respect to the bottom button. Run the app and see how the buttons act when you tap them.
However sometimes the issue might be more tricky to resolve. Selecting the right-most Resolve Auto Layout Issues button presents a menu of options that can help by updating your existing constraints, add missing constraints, resetting your constraints to constraints suggested by Xcode, or clearing the constraints so that you can start over.
The Pin menu has a Widths Equally option. If you set this constraint on two views, then Auto Layout will always make both views equally wide, based on which one is the largest.
Select both buttons and choose Pin\Equal Widths. This adds a new constraint to both buttons:
Even though there are two T-bars, in the Document Outline this shows up as a single Equal Widths constraint:
Run the app and tap the buttons. The buttons always have the same width, regardless of which one has the largest label.
Of course, when both labels are very short, both buttons will shrink equally. After all, unless there is a constraint that prevents it, buttons will size themselves to fit their content exactly, no more, no less. What was that called again? Right, the intrinsic content size.
Before Auto Layout, you always had to tell buttons and other controls how big they should be, either by setting their frame or bounds properties or by resizing them in Interface Builder. But it turns out that most controls are perfectly capable of determining how much space they need, based on their content.
A label knows how wide and tall it is because it knows the length of the text that has been set on it, as well as the font size for that text. Likewise for a button, which might combine the text with a background image and some padding.
This is known as the intrinsic content size, and it is an important concept in Auto Layout. You have already seen it in action with the buttons. Auto Layout asks your controls how big they need to be and lays out the screen based on that information.
Usually you want to use the intrinsic content size, but there are some cases where you may not want to do that. You can prevent this by setting an explicit Width or Height constraint on a view.
So what happens when one of the buttons has a fixed width constraint on it? Buttons calculate their own size, but you can override this by giving them a fixed width. Select the top button and choose Pin\Width from the menu. This adds a solid T-bar below the button:
Because this sort of constraint only applies to the button itself, not to its superview, it is listed in the Document Outline below the button object. In this case, you have fixed the button to a width of 46 points:
You cannot simply drag the button’s resize handles to make the button wider. If you do, you’ll end up with a whole bunch of orange boxes. If you make a change to the button’s frame, it’s up to you to make the constraints match. The alternative approach is to simply change the constraint.
Select the width = 46 constraint and go to the Attributes inspector. Change Constant to 80:
Run the app and tap the buttons. What happens? The button text changes, but it gets truncated because there is not enough room:
Because the top button has a fixed-width constraint and both buttons are required to be the same size, they will never shrink or grow.
Play around with this stuff for a bit to get the hang of pinning and aligning views. Get a feel for it, because not everything is immediately obvious. Just remember that there must always be enough constraints so that Auto Layout can determine the position and size for all views.
You should now have an idea of what constraints are and how you can build up your layouts by forging relationships between the different views. In the following sections, you will see how to use Auto Layout and constraints to create layouts that meet real-world scenarios.
Pretend you want to make an app that has a gallery of your favorite programmers. It looks like this in portrait and landscape:
The screen is divided into four equal quarters. Each quarter has an image view and a label. How would you approach this?
Download and open the starter project so you can get started. The sample project is a blank slate that includes the images you’ll need to display in the gallery. Open Main.storyboard.
Now select the iPhone SE size from the view as panel on the bottom left.
From the Object Library, drag a plain View object onto the canvas. With the view selected, open the Size Inspector, and set the Width to 160, and Height to 284 points.
Next, in the Attributes inspector set the view’s background color to green:
There are two main reasons why you would drop a plain UIView
onto a storyboard: a) You’re going to use it as a container for other views, which helps with organizing the content of your scenes; or b) It’s a placeholder for a custom view or control, and you will also set its Class attribute to the name of your own UIView
or UIControl
subclass.
Select the green view and open the Pin menu. A popup appears which lets you add a variety of constraints, and you will also see a checkbox labeled Constrain to margins:
Remember the blue guides that appear when you drag a view near to the edge of its superview? Instead of creating a constraint from the very edge of the view, you can create a constraint from the view’s margin. A view can define its own margin sizes which allows you to be more flexible about your layouts. For the purposes of this Auto Layout tutorial, you’ll stick to making constraints to the edges of the view. Go ahead and uncheck that box, and create 4 constraints to all four of the green view’s nearest neighbors in each direction by clicking the connector’s between the middle square and text field:
This will create four new constraints between the green view and its superview, one for each side of the view. The actual spacing values may be different for you, depending on where you placed the view – you don’t have the change the values to match the ones above exactly. Click Add 4 Constraints to finish.
Your storyboard should now look something like this:
Maybe you wondered why the constraint at the top of the view doesn’t go all the way up to the top of the screen:
Instead it stops at the status bar. But since iOS 7 the status bar is always drawn on top of the view controller — it is no longer a separate bar — so what gives? When you created the constraint it didn’t actually attach to the top of the screen but the top of an invisible rectangle called the Safe Area.
The Safe Area is the part of the view controller that isn’t obscured by any bars like the status, navigation or tab bar. Because the navigation bar has a different height in landscape, the Safe Area resizes with the bar when the device is rotated. That makes it easy to place views relative to the navigation bar. The same goes for tab bars.
This view needs four constraints to keep it in place. Unlike a button or label, a plain UIView
does not have an intrinsic content size. There must always be enough constraints to determine the position and size of each view, so this view also needs constraints to tell it what size it needs to be.
You may wonder, where are these size constraints? In this case, the size of the view is implied by the size of the superview. The constraints in this layout are a leading, trailing, top and bottom constraint, and these all have fixed lengths. You can see this in the Document Outline:
The width of the green view is calculated by the formula “width of safe area minus (80 + 80)” and its height by the formula “height of safe area minus (13 + 163)”. The constraints are fixed, so the view has no choice but to resize. (Again, your values may be different depending on where you put the view.)
When you rotate the app, the dimensions of the superview change, so the view’s size changes with it. Build and run the app, and rotate the simulator to see how the view behaves.
You may not always want your UIView
to resize when the device rotates, so you can use constraints to give the view a fixed width and/or height. Do that now. Select the green view and click the Pin button; in the popup put checkmarks in front of Width and Height.
Click Add 2 Constraints to finish. You have now added two new constraints to the view, a 160 point width constraint and a 372 point height constraint:
Because width and height constraints apply to just this view, they are located in the Document Outline under the View. Usually, constraints express a relationship between two different views, but you can consider the width and height constraints to be a relationship between the green view and itself.
Run the app. Yup, looks good in portrait.
Now flip over to landscape. Whoops! Not only does it not look like you wanted – the view has changed size again – but the Xcode debug pane has dumped a nasty error message that looks like this at the top:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this: (1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(...)
Will attempt to recover by breaking constraint....
Remember when I said that there must be enough constraints so that Auto Layout can calculate the positions and sizes of all the views? Well, this is an example where there are too many constraints. Whenever you get the error “Unable to simultaneously satisfy constraints”, it means that your constraints are conflicting somewhere.
Look at those constraints again:
There are six constraints set on the green view, the four constraints you saw earlier (1-4) and the new width and height constraints that you have just set on it (5 and 6). So where is the conflict?
In portrait mode there shouldn’t be a problem because the math adds up. If you add the lengths of the leading and trailing constraints and the width constraints, then you should also end up at the width of the super view. Likewise for the vertical constraints.
But when you rotate the device to landscape, the math is off and Auto Layout doesn’t know what to do.
The conflict here is that either the width of the view is fixed and one of the margins must be flexible, or the margins are fixed and the width must be flexible. You can’t have both. So one of these constraints has to go. In the above example, you want the view to have the same width in both portrait and landscape, so the trailing Horizontal Space has got to go.
Remove the Safe Area.trailing = View.trailing at the right and the Safe Area.bottom = View.bottom at the bottom by selecting them in the Document Outline and hitting the delete key. The storyboard should look like this:
Now the view has just the right number of constraints to determine its size and position — no more, no less. Run the app and verify that the error message is gone and that the view stays the same size after rotating.
From the Object library, drag a Label onto the green view. Notice that now the guides appear within that green view, because it will be the superview for the label.
Position the label against the bottom margin, spaced equally from the guides. Do this by dding a space constraint to anchor the label against the bottom, left and right of the green view, at 20 points distance. The quickest way is to use the Pin button and selecting the T-bar at the bottom, left and right, and entering 20 for the value:
Now, with the Label selected, in the Attributes inspector, selected the Center Alignment button.
It’s always important to have a right and left constraint on UILabel
s, since otherwise they would show outside their superview if the text is too long.
Notice that these new constraints are listed under the green view’s own Constraints section, not in the main view.
Drag a new Image View object onto the storyboard, and make the layout look like this:
The image view’s top, left, and right edges are pinned to its superview, but its bottom is connected to the top of the label with a standard spacing of 8 points. If you’re unsure of how to do this, then follow these steps.
1. Drag the image view into the green view but don’t worry too much about its size or position:
2. With the image view selected, press the Pin button and choose the following options:
The top, left, and right T-bars are set to 20 points but the bottom one is set to 8 points.
The constraints you chose result in a different frame than the image view’s current position and size. Interface Builder will automatically adjust the frame as it adds the constraints and everything looks dandy:
As mentioned above if you do end up with a misplaced frame, you can always use the Update Frames (1) or the Resolve Auto Layout Issues (2) button to fix it:
Now you are ready to start adding the images and names to your view. You’ll use the bundled images included in the Assets.xcassets.
Open Main.storyboard. Select the Image View, and in the Attributes Inspector, set Ray as the Image. Also set the Content Mode to Aspect Fit. Then, set the Background to White Color
Double click the Label, and set it’s title to “Ray”.
Next, select the green view’s height constraint, and in the Size Inspector, change its constant to 284. This will make sure it’s half as large as the screen.
Your layout should look like this:
Notice that the constraints inside the green view turned red. This happened the moment you set the image on the image view. How come your layout is suddenly invalid? Fortunately you can take the guesswork out of it and let Xcode tell you exactly what’s wrong.
Click the small red arrow next to View Controller Scene in the Document Outline to view the issues:
You have a Content Priority Ambiguity error. That’s quite the mouthful. This is what it means: If neither the image view nor the label has a fixed height, then Auto Layout doesn’t know by how much to scale each if the height of the green view should change.
Say at some point in your app the green view becomes 100 points taller. How should Auto Layout distribute these new 100 points among the label and the image view? Does the image view become 100 points taller while the label stays the same size? Or does the label become taller while the image view stays the same? Do they both get 50 points extra, or is it split 25/75, 40/60, or some other combination?
If you don’t solve this problem somehow then Auto Layout is going to have to guess and the results may be unpredictable.
The proper solution is to change the Content Hugging Priority of the label. You can imagine “hugging” here to mean “size to fit” – the bounds of the view will “hug” or be close to the intrinsic content size. A higher value here means the view will be less likely to grow and more likely to stay the same.
Go into the Size inspector for the label and set the Vertical Content Hugging Priority to 252. That makes it one higher than the priority of the image view. When the superview changes size, that means the image view will be the one to resize, and the label will stay pretty much the same size. Also change it’s Vertical Content Compression Resistance Priority to 751, which will make sure it doesn’t shrink.
The T-bars should turn blue again and the Auto Layout warnings are gone.
Drag the green view into the main view’s top-left corner. Recall that the green view had Horizontal Space and Vertical Space constraints that determined its position in the parent view. It still has those and they cause the frame of the view to be misaligned.
To fix this, use the Resolve Auto Layout Issues button and choose Update Constraints Constants. This will update the constraints to match the frame.
The leading constraint now has size 0 and is represented by a thick blue line at the left edge of the window. Even though the view sits completely in the corner, it still needs constraints to anchor it there:
Xcode was able to generate most of the constraints correctly however, you’ll need to modify the view’s top constraint. In the size inspector double click on the Align Top to Safe Area row in the Sibling & Ancestor Constraints section and set the Second Item to Superview:
Now you are going to start adding more views to show more people. Select the green view and tap ⌘D to duplicate it. Move the duplicate into the top-right corner:
Notice that the T-bars are red. When you made the duplicate, it lost its constraints for the X and Y position. To fix that, pin the view to the top and the right edges of the window (make sure to uncheck Constrain to margins).
Duplicate two more times and put these copies in the bottom-left and bottom-right corners, respectively. The bottom-left view should have its leading and bottom pinned, while the bottom right should have its trailing and bottom edges pinned to the superview.
Time to make the screen more colorful. Select each of the green views and change their backgrounds like you did before, to a different color. Also change the labels’ titles and the images views’ images to represent the programmers. In the end, your screen should look something like this:
Those are some good-looking programmers! :]
Run the app on the iPhone SE simulator. It looks good in portrait, but not in landscape:
It should be pretty obvious what went wrong: you’ve set a fixed width and height on the four brightly-colored container views, so they will always have those sizes, regardless of the size of their superview.
Select the fixed width and fixed height constraints from all four views and delete them (this is easiest in the Document Outline). If you run the app now, you’ll get something like this:
To achieve your desired layout your going to set each view’s width and height to be 50% of the ViewController’s view. Start by Ctrl + dragging from the green Ray view to the white containing view:
While holding Shift click Equal Widths and Equal Heights then press Enter. This will allow you to select multiple items at once. Once those constraints are active Ray will fill the screen, which is not the intention. :]
In the size inspector click the edit button on the Equal Width to Superview row in the Sibling & Ancestor Constraints section:
In the multiplier field enter 0.5 and press Enter. This will set the width of Ray’s view to be 50% of Ray’s container view. Repeat this for the Equal Height to Superview row and you should see that Ray is now the correct size:
Now repeat this for the remaining 3 views. Build and run. Everything looks great!
You can find the final project for this Auto Layout tutorial here.
If you’ve made it this far through this Auto Layout tutorial, congratulations – you now know what Auto Layout is all about, and have experimented with the basics! But there’s a lot left to learn…
To continue, check out our Auto Layout tutorial video series.
If you have any questions or comments as you continue on you Auto Layout journey, join the forum discussion below!
The post Auto Layout Tutorial in iOS 11: Getting Started appeared first on Ray Wenderlich.
Update Note: This tutorial has been updated to Kotlin by Joe Howard. The original tutorial was written by Megha Bambra.
Google’s material design brings with it exciting ways to delight your users with a visually appealing Android app. But wait—what is material design?
Google has described it as an interface that incorporates “tactile surfaces, bold graphic design and fluid motion to create beautiful, intuitive experiences.” Material design is the “user experience philosophy” for Android apps!
In this tutorial, you’ll integrate material design into an app called Travel Wishlist. Along the way, you’ll learn how to:
RecyclerView
and CardView
;This tutorial assumes you have a basic familiarity with Android programming including Kotlin, XML, Android Studio and Gradle. If you’re completely new to Android, you might want to go through our Beginning Android Development series and Kotlin Introduction first.
To follow along with this tutorial, you’ll need to use Android Studio 3.0 Beta 2 or later and Kotlin 1.1.4-2 or later.
Let’s get started!
Download the starter project, then fire up Android Studio.
To import the starter project, first select Open an existing Android Studio project from the Welcome to Android Studio window:
Then select the downloaded project and click Open:
Travel Wishlist will be a very simple app. Users will see a grid of pictures from locations around the world, and be able to tap each picture to add notes about what to do and see.
Build and run the starter project, and you should see a screen with the most basic of interfaces:
Right now, the world is empty! You’re going to add material components to this existing project, including dynamic views, color schemes and animations that will truly complement the beauty of the photos in your dataset.
Open build.gradle for the app module and add RecyclerView, CardView, Palette, and Picasso to your dependencies:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.0.1'
implementation 'com.android.support:recyclerview-v7:26.0.1'
implementation 'com.android.support:cardview-v7:26.0.1'
implementation 'com.android.support:palette-v7:26.0.1'
implementation 'com.squareup.picasso:picasso:2.5.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.0'
}
Here you’re simply declaring the dependencies that you’ll use throughout the rest of the tutorial. The first few of the added dependencies are Google-provided APIs, but the final one, Picasso, is a fantastic image downloading and caching library provided by the good folks at Square.
With the dependencies declared, it’s time to begin incorporating Material Design into your app!
Before doing anything else, you will update the theme. Open style.xml under the res/values directory. By default, the theme selected is Theme.AppCompat.Light.DarkActionBar
. Add the following items inside the theme tag:
<item name="android:navigationBarColor">@color/primary_dark</item>
<item name="android:displayOptions">disableHome</item>
Android automatically applies colorPrimary
to the action bar, colorPrimaryDark
to status bar and colorAccent
to UI widgets like text fields and checkboxes.
In the code above, you also alter the color of the navigation bar. For android:displayOptions
, you pass disableHome
to accommodate the screen layouts in this sample app.
Build and run, and you’ll see the new color scheme in the app.
It’s a subtle change, but like every trip on your travel wishlist, upgrading this design begins with a single step!
To give your users a window into all the cool places they might go, you need a view. You can use RecyclerView
as a replacement for ListView
, but it’s much more versatile than that. Google describes RecyclerView
as “a flexible view for providing a limited window into a large data set.” In this section, you’re going to demonstrate this by switching the view from a list to a custom grid that uses the same data source which supplies the the users locations.
First, open activity_main.xml and add the following inside the LinearLayout
tag:
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_gray"/>
Here you’re adding a RecyclerView
to the activity’s layout, and specifying it should match the entire size of the parent view.
Before adding Kotlin code, configure Android Studio so that it automatically inserts import statements to save you having to add each one manually.
Go to Preferences\Editor\General\Auto Import and select the Add unambiguous imports on the fly checkbox. In MainActivity.kt, add the following to the top of the class:
lateinit private var staggeredLayoutManager: StaggeredGridLayoutManager
Here you’re simply declaring a property to hold a reference to the LayoutManager
.
Next, add the following to the bottom of onCreate()
:
staggeredLayoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL)
list.layoutManager = staggeredLayoutManager
In the code above, you set the layout manager of the RecyclerView
to a StaggeredGridLayoutManager
, which you’ll use to create two types of vertically staggered grids. Here you start with the first type, passing 1
for the span count and StaggeredGridLayoutManager.VERTICAL
for the orientation. A span count of 1 makes this a list rather than a grid, as you’ll soon see. Later, you’ll add a compact grid formation with two columns.
Note that you’re using Kotlin Android Extensions to find list
, so there is no need for a call to findViewById()
. Make sure that the following line is present in your import statements, as it should be automatically added when you type in list
:
import kotlinx.android.synthetic.main.activity_main.*
CardView
provides a consistent backdrop for your views, including rounded corners and a drop shadow. You’re going to implement it for the row/cell layout of your RecyclerView
. By default, CardView
extends FrameLayout
and therefore includes the ability to host other child views.
From the res\layout directory, create a new Layout resource file and call it row_places.xml. Press OK to create it.
To create your desired cell layout, replace all the contents of this file with the code below:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/placeCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
card_view:cardCornerRadius="@dimen/card_corner_radius"
card_view:cardElevation="@dimen/card_elevation">
<ImageView
android:id="@+id/placeImage"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop" />
<!-- Used for the ripple effect on touch -->
<LinearLayout
android:id="@+id/placeHolder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:selectableItemBackground"
android:orientation="horizontal" />
<LinearLayout
android:id="@+id/placeNameHolder"
android:layout_width="match_parent"
android:layout_height="45dp"
android:layout_gravity="bottom"
android:orientation="horizontal">
<TextView
android:id="@+id/placeName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@android:color/white" />
</LinearLayout>
</android.support.v7.widget.CardView>
By adding xmlns:card_view="http://schemas.android.com/apk/res-auto"
, you can define attributes like card_view:cardCornerRadius
and card_view:cardElevation
that are responsible for giving Material Design enabled Android apps their signature card-like look.
Notice that for mainHolder
, you’ve added ?android:selectableItemBackground
as the background. This enables the ripple effect animation when the user touches a cell, as seen in many Android apps now. You’ll get to see it in action soon.
You’re going to use an adapter for the RecyclerView
to bind data to the view. In the main/java folder, right-click on the package com.raywenderlich.android.travelwishlist package and select New\Kotline File/Class. Call the class TravelListAdapter.
Add the following code to the class, taking care to preserve the package statement at the top of the file:
// 1
class TravelListAdapter(private var context: Context) : RecyclerView.Adapter<TravelListAdapter.ViewHolder>() {
override fun getItemCount(): Int {
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
}
override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
}
// 2
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
}
A couple of things are happening above:
TravelListAdapter
extend Recycler.Adapter
so that you can implement logic for the override methods you’ll add soon. You also setup the constructor with a Context
that will be passed in when you create an instance of TravelListAdapter
in MainActivity
, which you’ll do a bit later in the tutorial.ViewHolder
class. Whereas the use of the ViewHolder
pattern is optional in ListView
, RecyclerView
enforces it. This improves scrolling and performance by avoiding findViewById()
for each cell.Update the RecyclerView.Adapter
methods in TravelListAdapter
to the following:
// 1
override fun getItemCount() = PlaceData.placeList().size
// 2
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.row_places, parent, false)
return ViewHolder(itemView)
}
// 3
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val place = PlaceData.placeList()[position]
holder.itemView.placeName.text = place.name
Picasso.with(context).load(place.getImageResourceId(context)).into(holder.itemView.placeImage)
}
Here’s what’s happening:
getItemCount()
returns the number of items from your data array. In this case, you’re using the size of the PlaceData.placeList()
.onCreateViewHolder(...)
returns a new instance of your ViewHolder
by passing an inflated view of row_places
.onBindViewHolder(...)
binds the Place
object to the UI elements in ViewHolder
. You’ll use Picasso to cache the images for the list.Add a field in MainActivity
that will hold a reference to your adapter:
lateinit private var adapter: TravelListAdapter
And then create an instance of your adapter and pass it to the RecyclerView
at the bottom of onCreate()
, just after you configure the layout manager:
adapter = TravelListAdapter(this)
list.adapter = adapter
Now build and run the app, and you’ll see a populated list of places.
Which place is calling your name? I like the look of that turquoise water. But wherever you want to go, you’ll want to cultivate your dream by taking notes about what to do there. First, you need to make the cells respond to a user’s touch.
Unlike ListView
, RecyclerView
doesn’t come with an onItemClick
interface, so you have to implement one in the adapter. In TravelListAdapter
, create a property to hold an instance of OnItemClickListener
. Add the following to the top of TravelListAdapter
:
lateinit var itemClickListener: OnItemClickListener
Now implement View.OnClickListener
by adding the interface to the ViewHolder
inner class definition like this:
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
Then add the following method stub to the inner ViewHolder
class:
override fun onClick(view: View) {
}
Hook the two up by adding the following init
block to the top of ViewHolder
:
init {
itemView.placeHolder.setOnClickListener(this)
}
Above, you initiate setOnClickListener
for placeHolder
and implement the onClick
override method.
You need to do a few more things to implement the onClick
interface for the RecyclerView
. First, after the inner ViewHolder
class definition add the following:
interface OnItemClickListener {
fun onItemClick(view: View, position: Int)
}
Next, add the setter method of the onClickListener
to TravelListAdapter
:
fun setOnItemClickListener(itemClickListener: OnItemClickListener) {
this.itemClickListener = itemClickListener
}
Now implement the logic in the empty onClick()
stub within the inner ViewHolder
class:
override fun onClick(view: View) = itemClickListener.onItemClick(itemView, adapterPosition)
In MainActivity
, create an instance of OnItemClickListener
above onCreate()
:
private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
Toast.makeText(this@MainActivity, "Clicked " + position, Toast.LENGTH_SHORT).show()
}
}
Finally, set the listener to the adapter by adding the following code to the bottom of onCreate()
, just after where you set the adapter:
adapter.setOnItemClickListener(onItemClickListener)
Build and run. Now when you tap a cell you’ll see ripple effect every time you touch a row, and a Toast notification displaying the position of the cell in the list.
StaggeredLayoutManager
lets you add versatility to your layouts. To change your existing list to a more compact two-column grid, you simply have to change the spanCount
of the StaggeredLayoutManager
in MainActivity
.
In toggle()
, add the following to the top of the showGridView()
:
staggeredLayoutManager.spanCount = 2
And now add the following to the top of showListView()
:
staggeredLayoutManager.spanCount = 1
Here you’re simply switching between single and double span counts, which displays single and double columns respectively.
Build and run and use the action bar button to toggle between list and grid views.
Now you can add some interesting Material Design features into the mix, starting with the Palette API. Head back to TravelListAdapter
, where you’ll define a background color for placeNameHolder
that will be determined dynamically using the colors in the image.
Add the following to the bottom of onBindViewHolder(...)
:
val photo = BitmapFactory.decodeResource(context.resources, place.getImageResourceId(context))
Palette.from(photo).generate { palette ->
val bgColor = palette.getMutedColor(ContextCompat.getColor(context, android.R.color.black))
holder.itemView.placeNameHolder.setBackgroundColor(bgColor)
}
The generate(...)
method creates a color palette in the background, and is passed a lambda that is called when the palette has been generated. Here you can access the generated color palette and set the background color of holder.itemView.placeNameHolder
. If the color doesn’t exist, the method will apply a fallback color — in this case, android.R.color.black
.
Build and run to see the Palette API in action!
I encourage you to experiment with these. Instead of palette.getMutedColor(...)
, try palette.getVibrantColor(...)
, palette.getDarkVibrantColor(...)
and so on.
In this section, you’ll use DetailActivity
and its corresponding activity_detail
layout, and make them cooler by infusing some of the new Material Design APIs.
First, you’ll want to see how the detail view currently looks in the starter project. To see this, first add the following to the companion object of DetailActivity
:
fun newIntent(context: Context, position: Int): Intent {
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(EXTRA_PARAM_ID, position)
return intent
}
Then, go to MainActivity
and replace the Toast
in onItemClick(...)
of onItemClickListener with the following:
startActivity(DetailActivity.newIntent(this@MainActivity, position))
You can pass the position of the place object via the intent so that DetailActivity
can retrieve the information and use it to layout the interface. That’s what you’re doing here.
Build and run.
There isn’t anything crazy going on here (yet!), but you’ve got a nice foundation on which to start adding those highly anticipated Material Design APIs. You also see a cool FloatingActionButton
, one of the widgets introduced by Material Design.
Now you want to give your users the ability to add notes about what they’d like to do in each of these stunning places. For this, activity_detail.xml already has an edittext
that is hidden by default. When a user taps the FAB, it reveals itself with a cool animation like below:
Open DetailActivity
. There are two methods you have yet to implement:
revealEditText()
hideEditText()
First, add the following lines inside revealEditText()
:
val cx = view.right - 30
val cy = view.bottom - 60
val finalRadius = Math.max(view.width, view.height)
val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, 0f, finalRadius.toFloat())
view.visibility = View.VISIBLE
isEditTextVisible = true
anim.start()
The two int
values are getting the x
and y
positions of the view with a slight offset. This offset gives the illusion that the reveal is happening from the direction of your FAB.
Next, the radius gives the reveal the circular outline that you can see in the GIF above. All of these values — the x-position, y-position and the radius — you pass into the animation instance. This animation is using ViewAnimationUtils
, which gives you the ability to create this circular reveal.
Since the EditText
view is initially hidden, you set the view’s visibility to VISIBLE
and set your boolean check isEditTextVisible
to true
. Finally, you can call start()
on the animation.
To dismiss the view, add the following to hideEditText()
:
val cx = view.right - 30
val cy = view.bottom - 60
val initialRadius = view.width
val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, initialRadius.toFloat(), 0f)
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
view.visibility = View.INVISIBLE
}
})
isEditTextVisible = false
anim.start()
Here your goal is to hide the view and show the circular animation in the opposite direction. Therefore, you make the initial radius the width of the view and the ending radius 0, which shrinks the circle.
You want to show the animation first and then hide the view. To do this, you implement an animation listener and hide the view when the animation ends.
Now build and run and see this animation in action!
Note: If the keyboard presents itself, you’ll need to dismiss it explicitly to see the effect without obstruction. Comment out the call to inputManager.showSoftInput(...)
in DetailActivity
, but don’t forget to uncomment it. Oh, and don’t worry that your button doesn’t show the plus icon yet, you’ll fix that soon.
Now that you have your reveal animation hiding and showing the edit text field, you can coordinate the icon on your FAB to look and respond just like the one shown below:
The starter project includes the vector paths for the plus and checkmark icons. You’ll learn how to animate – or morph – the paths from the plus to the checkmark, and vice versa.
Under the res/drawables directory, create a new resource file by going to New\Drawable resource file. Call it icn_morph and define animated-vector
as the root element:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/icn_add">
</animated-vector>
animated-vector
requires an existing android:drawable
. In this case, the animated vector will start with a plus sign and morph into a checkmark, so you’ll set the drawable to icn_add
.
Now for the actual morphing, add the following inside the animated-vector
tag:
<target
android:animation="@anim/path_morph"
android:name="sm_vertical_line" />
<target
android:animation="@anim/path_morph_lg"
android:name="lg_vertical_line" />
<target
android:animation="@anim/fade_out"
android:name="horizontal_line" />
With the code above, you are essentially transforming the vertical line of the plus icon into a checkmark while fading out the horizontal line, as the diagram below illustrates:
Furthermore, the vertical line is comprised of two paths, a smaller vertical line and a larger one:
You can see from the diagram above that you can transform the first two targets, sm_vertical_line
and lg_vertical_line
, into a checkmark by drawing their paths at different angles, which is exactly what you do in the previous code block, along with fading out horizontal_line
.
Next, you need to reverse this animation to transform the checkmark back into a plus sign. Create another drawable resource file, this time calling it icn_morph_reverse, and replace it’s contents with the following:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/icn_add">
<target
android:animation="@anim/path_morph_reverse"
android:name="sm_vertical_line"/>
<target
android:animation="@anim/path_morph_lg_reverse"
android:name="lg_vertical_line" />
<target
android:animation="@anim/fade_in"
android:name="horizontal_line" />
</animated-vector>
The two lines that make up the final vertical line in the plus icon will now morph back into their original states and the horizontal line will fade into view, creating a smooth effect.
Now, to complete the animation. Open DetailActivity.kt and add the following to onClick()
, at the end of the if
branch before the else:
addButton.setImageResource(R.drawable.icn_morph)
val animatable = addButton.drawable as Animatable
animatable.start()
Here you set the image resource of the button to the icn_morph
drawable you created earlier, extract the animatable from it, and then kick-off the animation.
Finally, add the following to the very bottom of the else
branch:
addButton.setImageResource(R.drawable.icn_morph_reverse)
val animatable = addButton.drawable as Animatable
animatable.start()
Here you’re doing almost exactly the same as the previous step, except you assign icn_morph_reverse
as the image resource so the animation plays out in the opposite direction.
Along with morphing the icon, the user’s click also adds the text from todoText
to the toDoAdapter
and refreshes the place activity list. This is not yet visible because of the white text, but in the next section, you’ll add vibrant color to your views so that the text stands out.
Build and run, and watch the magic unfold before your eyes! The FAB icon now morphs between a checkmark and a plus sign when it’s tapped.
It’s time to add colors to this view using the Palette API. And not just any colors—as before, they will be dynamic colors!
In DetailActivity
, flesh out colorize()
by adding the following:
val palette = Palette.from(photo).generate()
applyPalette(palette)
Just like you did previously, you generate a palette from a photo – although this time you do it synchronously – and then pass that palette onto applyPalette()
. Replace the existing method stub for applyPalette()
with this code:
private fun applyPalette(palette: Palette) {
window.setBackgroundDrawable(ColorDrawable(palette.getDarkMutedColor(defaultColor)))
placeNameHolder.setBackgroundColor(palette.getMutedColor(defaultColor))
revealView.setBackgroundColor(palette.getLightVibrantColor(defaultColor))
}
Here you’re you’re using the dark muted color, the muted color, and the light vibrant color as the background colors of the window, title holder, and reveal view respectively.
Finally, to kick-off this chain of events add the following line to the bottom of getPhoto()
:
colorize(photo)
It’s that time again… build and run your app! You can see the detail activity is now using a color scheme derived from the palette of the associated image.
We’ve all seen and wondered about those nice image and text transitions in Google’s app which have been updated to use Material Design. Well, wait no more—you’re about to learn the intricacies of achieving smooth animation.
Between the places list view, MainActivity
, and the places detail view, DetailActivity
, you’re going to transition the following elements:
Open row_places.xml and add the following to the declaration of the ImageView
tag with an id of placeImage
:
android:transitionName="tImage"
And then add this to the LinearLayout
tag with an id of placeNameHolder
:
android:transitionName="tNameHolder"
Notice that placeName
doesn’t have a transition name. This is because it is the child of placeNameHolder
, which will transition all of its child views.
In activity_detail.xml, add a transitionName
to the ImageView
tag with the id placeImage
:
android:transitionName="tImage"
And, in a similar fashion, add a transitionName
to the LinearLayout
tag that has an id of placeNameHolder
:
android:transitionName="tNameHolder"
Shared elements between activities that you want to transition should have the same android:transitionName
, which is what you’re setting up here. Also, notice that the size of the image, as well as the height of the placeNameHolder
are much larger in this activity. You’re going to animate all of these layout changes during the activity transition to provide some nice visual continuity.
In onItemClickListener()
found in MainActivity
, update the method to the following:
override fun onItemClick(view: View, position: Int) {
val intent = DetailActivity.newIntent(this@MainActivity, position)
// 1
val placeImage = view.findViewById<ImageView>(R.id.placeImage)
val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)
// 2
val imagePair = Pair.create(placeImage as View, "tImage")
val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")
// 3
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,
imagePair, holderPair)
ActivityCompat.startActivity(this@MainActivity, intent, options.toBundle())
}
After adding this code, you will need to manually add the following import statement to the top of the file as Android Studio cannot automatically determine that this is the intended package.
import android.support.v4.util.Pair
There are a couple of things to highlight here:
placeImage
and placeNameHolder
for the given position of the RecyclerView
. You’re not relying on Kotlin Android Extensions here since you need the placeImage
and placeNameHolder
from the specific view
.Pair
containing the view and the transitionName
for both the image and the text holder view. Note that you will once again have to manually add the import statement to the top of the file: android.support.v4.util.Pair
.Pair
instances and start the activity with your options
bundle.Build and run to see the image transition from the main activity to the detail activity:
However, the animation is a bit jumpy in two areas:
DetailActivity
.You’ll solve the FAB button issue first. Open DetailActivity.kt and add the following to windowTransition()
:
window.enterTransition.addListener(object : Transition.TransitionListener {
override fun onTransitionEnd(transition: Transition) {
addButton.animate().alpha(1.0f)
window.enterTransition.removeListener(this)
}
override fun onTransitionResume(transition: Transition) { }
override fun onTransitionPause(transition: Transition) { }
override fun onTransitionCancel(transition: Transition) { }
override fun onTransitionStart(transition: Transition) { }
})
The listener you add to the enter transition is triggered when the window transition ends, which you use to fade in the FAB button. For this to be effective, set the alpha
to 0
for the FAB in activity_detail.xml:
android:alpha="0.0"
Build and run! You’ll notice the FAB transition is much smoother!:
As for the action bar and navigation bar issues, begin by updating styles.xml, to set the parent theme to Theme.AppCompat.Light.NoActionBar
:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
Since there is no action bar defined in styles.xml, you’ll have to add it using individual XML views.
Open activity_main.xml and add the following inside LinearLayout
, just above the RecyclerView
tag:
<include layout="@layout/toolbar" />
This simply includes a toolbar layout that's provided as part of the starter project into the current layout. Now you need to make a similar change to the detail activity's layout.
Open activity_detail.xml and add the following at the very bottom of the first FrameLayout
, just below the closing tag of the inner LinearLayout
:
<include layout="@layout/toolbar_detail"/>
Next in MainActivity
, you need to initialize the toolbar. Add the following to the bottom of the onCreate()
method:
setUpActionBar()
Here you assign the result of the findViewById
call to the new field, and then call setUpActionBar()
. At the moment it's just an empty method stub. Fix that now by adding the following to setUpActionBar()
:
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
supportActionBar?.setDisplayShowTitleEnabled(true)
supportActionBar?.elevation = 7.0f
Here you set the action bar to be an instance of your custom toolbar, set the visibility of the title, disable the home button, and add a subtle drop shadow by setting the elevation.
Build and run. You'll notice that nothing much has changed, but these changes have laid the foundations of properly being able to transition the toolbar.
Open MainActivity
and replace the existing onItemClickListener
with this one:
private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
// 1
val transitionIntent = DetailActivity.newIntent(this@MainActivity, position)
val placeImage = view.findViewById<ImageView>(R.id.placeImage)
val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)
// 2
val navigationBar = findViewById<View>(android.R.id.navigationBarBackground)
val statusBar = findViewById<View>(android.R.id.statusBarBackground)
val imagePair = Pair.create(placeImage as View, "tImage")
val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")
// 3
val navPair = Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)
val statusPair = Pair.create(statusBar, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)
val toolbarPair = Pair.create(toolbar as View, "tActionBar")
// 4
val pairs = mutableListOf(imagePair, holderPair, statusPair, toolbarPair)
if (navigationBar != null && navPair != null) {
pairs += navPair
}
// 5
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,
*pairs.toTypedArray())
ActivityCompat.startActivity(this@MainActivity, transitionIntent, options.toBundle())
}
}
The differences between the original implementation and this one are thus:
Pair
- one for the navigation bar, one for the status bar, and one for the toolbar;navPair
is null.
*
on pairs
, after changing it to a typed array.Great! Build and run, and you’ll see a much smoother animation:
Now if you tap on a row under the action/toolbar or navigation bar, it doesn't jump before the transition; it transitions with the rest of the shared elements, which is much more pleasing to the eye. Switch to the grid view and you'll notice that the transitions work very nicely with that layout as well.
Ta-da! Here is a video of the final app in action:
Be proud of yourself: You’ve created a full-blown Android material app! To challenge yourself, try the following:
StaggeredLayoutManager
to make a grid with three columns instead of two.Palette
API in both MainActivity
and DetailActivity
using different palette options.animated-vectors
.And of course, apply this knowledge to your apps so that they can be as cool as this one. :]
To find out more about Material Design then be sure to check out Google's recently redesigned Google Design website.
You can get the completed project here.
Feel free to share your feedback or ask any questions in the comments below or in the forums.
The post Android: An Introduction to Material Design with Kotlin appeared first on Ray Wenderlich.
In your first challenge of the course, you will design the structure of the JSON that you will use in this course.
The post Video Tutorial: Beginning Firebase Part 4: JSON Challenge appeared first on Ray Wenderlich.
References are what you use to save and read data to Firebase. In this video, you'll learn how to use references in your Swift app.
The post Video Tutorial: Beginning Firebase Part 5: References appeared first on Ray Wenderlich.
Saving data in Firebase is handled by references. In this video, you'll set values to references and same them Firebase.
The post Video Tutorial: Beginning Firebase Part 6: Saving Data appeared first on Ray Wenderlich.
Saving data is one part of the equation. You also have to read it back. This video will show you how to do that with Firebase.
The post Video Tutorial: Beginning Firebase Part 7: Reading Data appeared first on Ray Wenderlich.
In your next challenge, you'll read some data from Firebase. I'll get you started and you'll finish the rest.
The post Video Tutorial: Beginning Firebase Part 8: Reading Data Challenge appeared first on Ray Wenderlich.