Learn how to move Swift code from a project to an independent, reusable framework complete with unit tests.
The post Creating a Framework appeared first on Ray Wenderlich.
Learn how to move Swift code from a project to an independent, reusable framework complete with unit tests.
The post Creating a Framework appeared first on Ray Wenderlich.
Update note: This tutorial has been updated to Xcode 7.3, iOS 9.3, and Swift 2.2. The original tutorial was written by Michael Katz.
CloudKit is Apple’s remote data storage service based on iCloud. It provides a low-cost option to store and share app data using your users’ iCloud accounts as a back-end storage service.
There are two main components to CloudKit:
CloudKit is secure as well. Users’ private data is completely protected, as developers can only access their own private database and aren’t able to look at any users’ private data.
CloudKit is a good option for iOS-only applications that use a lot of data, but don’t require a great deal of server-side logic. In addition, CloudKit can be used for web and server applications.
In this CloudKit tutorial, you’ll get hands-on experience using CloudKit by creating a restaurant rating app with a twist called, BabiFüd.
You might wonder why you should choose CloudKit over Core Data, other commercial BaaS (Back end as a Service) offerings, or even rolling your own server.
There are three reasons: simplicity, trust, and cost.
Unlike other backend solutions, CloudKit requires little setup. You don’t have to choose, configure, or install servers. Security and scaling are handled by Apple as well.
Simply registering for the iOS Developer Program makes you eligible to use CloudKit. You don’t have to register for additional services or create new accounts. When you enable CloudKit capabilities in your app all necessary server setup magic happens automatically.
There’s no need to download additional libraries and configure them. CloudKit is imported like any other iOS framework. The CloudKit framework itself also provides a level of simplicity by offering convenient APIs for common operations.
It’s also easy for users. Since CloudKit uses the iCloud credentials entered when the device is set up (or entered after set up via the Settings app), there’s no need to build complicated login screens. As long as they are logged in, users can seamlessly start using your app. That should put you on Cloud 9!
Another benefit of CloudKit is that users can trust the privacy and security of their data by relying on Apple rather than app developers. CloudKit insulates user data from you, the developer.
While this lack of access can be frustrating while debugging, it is a net plus since you don’t have to worry about security or convince users their data is secure. If an app user trusts iCloud, then they can also trust you.
Finally, for any developer, the cost of running a service is a huge deal. Even the least expensive server hosts cannot offer low-cost solutions for small, free or cheap apps. So there will always be a cost associated with running an app.
With CloudKit, you get a reasonable amount of storage and data transfer for public data for free. There is a very thorough explanation of fees in the What’s New in CloudKit WWDC 2015 Video.
These strengths make the CloudKit service a low-hassle solution for Mac and iOS apps.
The sample app for this tutorial, BabiFüd, is the freshest take on the standard “rate a restaurant” style of app. Instead of reviewing restaurants based upon food quality, speed of service, or price, users rate child-friendliness. This includes availability of changing facilities, booster seats, and healthy food options.
The app contains four tabs: a list of nearby restaurants, a map of nearby restaurants, user-generated notes, and settings. The list of nearby restaurants is the only tab you’ll use in this tutorial. You can get a glimpse of the app in action below.
A model class backs these views and wraps the calls to CloudKit. CloudKit objects are called records. The main record type in your model is an Establishment
, which represents the various restaurants in your app.
Start by downloading the starter project for this tutorial.
You’ll have to change the Bundle Identifier and Team of your app before you can start coding. You need to set the team in order to get the necessary entitlements from Apple. Having a unique bundle identifier makes the process a whole lot easier.
Open BabiFud.xcodeproj in Xcode. Select the BabiFud project in the Project Navigator, then select the BabiFud target. With the General tab selected, replace the Bundle Identifier with something unique. Standard practice is to use reverse domain name notation and include the project name. Next, select the appropriate Team:
That takes care of the Bundle Identifier and Team. Now you’ll need to get your app set up for CloudKit and create some containers to hold your data.
You’ll need a container to hold the app’s records before you can add any data via your app. A container is the term for the conceptual location of all the app’s data on the server. It is the grouping of public and private databases.
To create a container, you first need to enable the iCloud entitlements for your app. Select the Capabilities tab in the target editor. Next flip the switch in the iCloud section to ON.
At this point, Xcode might prompt you to enter the Apple ID associated with your iOS developer account. If so, then type it in as requested. Finally, enable CloudKit by checking the CloudKit checkbox in the Services group.
This creates a default container named iCloud.<your app’s bundle id>, as illustrated below:
If you see any warnings or errors when creating entitlements, building the project, or running the app, and Xcode is complaining about the container ID, then here are some troubleshooting tips:
After setting up the necessary entitlements, the next step is to create some record types that define the data used by your app. You can do this using the CloudKit dashboard. Click CloudKit Dashboard, found in the target’s Capabilities pane, under iCloud.
Here’s what the dashboard looks like:
The CloudKit dashboard consists of four sections: Schema, Public Data, Private Data, and Admin.
The SCHEMA section represents the high level objects of a CloudKit container: Record Types, Security Roles, and Subscription Types. You’ll only be concerned with Record Types in this tutorial.
A Record Type is a set of fields that defines individual records. In terms of object-oriented programming, a Record Type is like a class. A record can be considered an instance of a particular Record Type. It represents structured data in the container, much like a typical row in a database, and encapsulates a series of key/value pairs.
The PUBLIC DATA and PRIVATE DATA sections let you add data to, or search for data in the databases to which you have access. Remember, as a developer you access all public data, but only your own private data. The User Records store data about the current iCloud user such as name and email. A Record Zone (here noted as the Default Zone) is used to provide a logical organization to a private database by grouping records together. Custom zones support atomic transactions by allowing multiple records to be saved at the same time before processing other operations. Custom zones are outside the scope of this tutorial.
The ADMIN section provides the ability to configure the dashboard permissions for your team members. If you have multiple development team members, you can restrict their ability to edit data here. This, too, is out-of-scope for this tutorial.
Think about the design of your app for a moment. Each establishment you’ll want to track has lots of data: name, location, and availability of various child-friendly options. Record types use fields to define the various pieces of data contained in each record.
With Record Types selected, click the + icon in the top left of the detail pane to add a new record type.
Name your new record type Establishment.
You’ll see a row of fields where you can define the Field Name, Field Type, and Index, as shown below. A field with the default name of, StringField, has been automatically created for you.
Start by replacing StringField with Name. The Field Type and Index defaults already match what you need for this first field definition, but you’ll need to make changes to the Field Type and Index for some of the other fields. Click Add Field… to add new fields as required. Add the following fields:
When you’re done, your list of fields should look like this:
Click Save at the bottom of the page to save your new record type.
You’re now ready to add some sample establishment records to your database.
Select Default Zone under the PUBLIC DATA section in the navigation pane on the left. This zone will contain the public records for your app. Select the Establishment record type from the dropdown list in the center pane if it’s not already selected. Then click the + icon or the New Record button in the right detail pane, as shown in the screenshot below:
This will create a new, empty Establishment record.
At this point you’re ready to enter some test data for your app.
The following sample establishment data is fictional. The establishments are located near Apple’s headquarters so they’re easy to find in the simulator.
Enter each record as described below:
Once all three records have been saved, the dashboard should look like this:
For each record, the values entered are the database representation of the data. On the app side, the data types are different. For example, SeatingType and ChangingTable are structs. So the specified Int
value for a SeatingType might correspond to a “high chair” or a “booster” seat. For HealthyOption and KidsMenu, the Int
values represent Boolean types: a 0 means that establishment doesn’t have that option and a 1 means that it does.
Running the app requires you to have an iCloud account that can be used for development.
Creating an iCloud Account for Development.
You will also need to enter into the iOS Simulator the iCloud credentials associated with this account.
Enter iCloud Credentials Before Running Your App
Return to Xcode. It’s time to start integrating this data into your app!
CKQuery
objects are used to select records from a database. A CKQuery
describes how to find all records of a specified record type that match certain criteria. These criteria can be something like “all records with a Name field that starts with ‘M’”, or “all records that have booster seats”, or “all records within 3km.” These types of expressions are coded in Cocoa with NSPredicate
objects. An NSPredicate
evaluates objects to see if they match the specified criteria. Predicates are also used in Core Data and are a natural fit for CloudKit, because predicates commonly are defined as a comparison on a field.
CloudKit supports only a subset of available NSPredicate
functions. These include mathematical comparisons, some string and set operations (such as “field matches one of the items in a list”), and a special distance function. The function distanceToLocation:fromLocation:
was added to NSPredicate
for CloudKit to match records with a location field within a specified radius from a known location. This type of predicate is covered in detail below. For other types of queries, the CKQuery Class Reference contains a detailed list of the supported functions and descriptions of how to use them.
CLLocation
objects. These are Core Location Framework objects that contain geospatial coordinates. This makes it quite easy to create a query for finding establishments inside of a geographic region – without doing all of the messy coordinate math yourself.So, in Xcode, open Model/Model.swift. This file contains stubs for all the server calls your app will make.
Replace the fetchEstablishments(_:radiusInMeters:)
method with the following:
func fetchEstablishments(location:CLLocation, radiusInMeters:CLLocationDistance) { // 1 let radiusInKilometers = radiusInMeters / 1000.0 // 2 let locationPredicate = NSPredicate(format: "distanceToLocation:fromLocation:(%K,%@) < %f", "Location", location, radiusInKilometers) // 3 let query = CKQuery(recordType: EstablishmentType, predicate: locationPredicate) // 4 publicDB.performQuery(query, inZoneWithID: nil) { [unowned self] results, error in if let error = error { dispatch_async(dispatch_get_main_queue()) { self.delegate?.errorUpdating(error) print("Cloud Query Error - Fetch Establishments: \(error)") } return } self.items.removeAll(keepCapacity: true) results?.forEach({ (record: CKRecord) in self.items.append(Establishment(record: record, database: self.publicDB)) }) dispatch_async(dispatch_get_main_queue()) { self.delegate?.modelUpdated() } } } |
Taking each numbered comment in turn:
radiusInMeters
to kilometers.CKQuery
objects are created using a predicate and a record type. Both will be used when performing the query.performQuery(_:inZoneWithID:completionHandler:)
sends your query up to iCloud, and waits for any matching results. By passing nil
as the inZoneWithID
parameter, you’re running the query against your default zone; that is, your public database. If you want to retrieve records from both public and private databases, then you have to query each database using a separate call.Oh. That reminds me. What did CKQuery
say to iCloud?
After making a miraculous recovery from such a bad joke, you’re probably wondering where the CKDatabase
instance, publicDB
, comes from. Take a look the top of Model.swift.
let container: CKContainer let publicDB: CKDatabase let privateDB: CKDatabase init() { // 1 container = CKContainer.defaultContainer() // 2 publicDB = container.publicCloudDatabase // 3 privateDB = container.privateCloudDatabase } |
Here you define your databases:
This code will retrieve some local establishments from the public database, but it has to be wired up to a view controller in order to see anything in the app.
You can take care of notifications with the familiar delegate pattern. Here’s the protocol from the top of Model.swift that you’ll implement in your view controller:
protocol ModelDelegate { func errorUpdating(error: NSError) func modelUpdated() } |
Open MasterViewController.swift and replace the modelUpdated()
method with the following:
func modelUpdated() { refreshControl?.endRefreshing() tableView.reloadData() } |
This is called when new data is available. All the wiring up of the table view cells to CloudKit objects has already been taken care of in tableView(_:cellForRowAtIndexPath:)
. Feel free to take a look.
Next, in MasterViewController.swift, replace the errorUpdating(_:)
method with the following:
func errorUpdating(error: NSError) { let alertController = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .Alert) alertController.addAction(UIAlertAction(title: "Dismiss", style: .Default, handler: nil)) presentViewController(alertController, animated: true, completion: nil) } |
This method is called when the query produces an error. Errors can occur as a result of poor network conditions or CloudKit-specific issues (such as missing or incorrect user credentials or no records being returned from the query).
Good error handling is essential when dealing with any kind of remote server. For now, this code just displays to the user the message returned with the error.
One very common issue, however, is the user not being logged into iCloud and/or not having the iCloud drive turned on for this app. Can you modify errorUpdating(_:)
to at least handle these situations? Hint: Both errors return a CKErrorCode
of 1.
Solution Inside | SelectShow> | |
---|---|---|
In MasterViewController.swift, replace the errorUpdating(_:) method with the following:
|
If you’re using the iOS simulator and the list doesn’t populate, then make sure you have the correct location set by selecting from the Xcode menu, Debug\Simulate Location\San Francisco, CA, USA. If you needed to change the location in Xcode, then pull the table down in the app to force a refresh instead of waiting for a location trigger.
If you’re using an iPhone or iPad, location services are enabled, and the list still isn’t populating, then the establishments aren’t close enough to your current location. You have two options: change the coordinates of the sample data to be closer to your current location or use the simulator to run the app. There is a third option, but it’s not terribly practical as you’d have to travel to Cupertino and hang around the Apple campus.
If the data isn’t appearing properly – or isn’t appearing at all – inspect the sample data using the CloudKit dashboard. Make sure all of the records are present, you’ve added them to the default zone and they have the correct values. If you need to re-enter the data, then you can delete records by clicking the trash icon.
Debugging CloudKit errors can be tricky at times. As of this writing, CloudKit error messages don’t contain a tremendous amount of information. To determine the cause of the error you’ll need to look at the error code in conjunction with the particular database operation you’re attempting. Using the numerical error code, look up the matching CKErrorCode
enum. The name and description in the documentation will help narrow down the cause of the issue. See below for some examples.
Note: For a list of error codes that can be returned by CloudKit, read the CloudKit Framework Constants Reference.
Here are some common error enums and related descriptions:
.BadContainer
– The specified container is unknown or unauthorized..NotAuthenticated
– The current user is not authenticated and no user record was available. This might happen if the user is not logged into iCloud..UnknownItem
– The specified record does not exist.When you fetched the list of establishments, you probably noticed that you can see the establishment name and the services the establishments offer. But none of the images are being displayed! Are the clouds in the way?
When you retrieved the establishment records, you automatically retrieved the images as well. You still, however, need to perform the necessary steps to load the images into your app. That’ll chase those clouds away! :]
An asset is binary data, such as an image, that you associate with a record. In your case, your app’s assets are the establishment photos shown in the MasterViewController
table view.
In this section you’ll add the logic to load the assets that were downloaded when you retrieved the establishment records.
Open Model/Establishment.swift and replace the loadCoverPhoto(_:)
method with the following code:
func loadCoverPhoto(completion:(photo: UIImage!) -> ()) { // 1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) { var image: UIImage! // 4 defer { completion(photo: image) } // 2 guard let asset = self.record["CoverPhoto"] as? CKAsset, path = asset.fileURL.path, imageData = NSData(contentsOfFile: path) else { return } // 3 image = UIImage(data: imageData) } } |
This method loads the image from the asset attribute as follows:
dispatch_async
block.CKRecord
as instances of CKAsset
, so cast appropriately. Next load the image data from the local file URL provided by the asset.UIImage
.defer
block gets executed regardless of which return
is executed. For example, if there is no image asset, then image
never gets set upon the return and no image appears for the restaurant.Build and run. You’ve chased the clouds away and the establishment images should now appear. Great job!
There are two gotchas with CloudKit assets:
You can see what’s been built so far in the Final Project. The app is now able to download Establishment records and load their details and photos into the table view.
You can enhance the app in several ways:
CKQuery
with a distance predicate, but this can be modified to be a more complex predicate. CloudKit supports text searching of string fields as well.CKDatabase
also have NSOperation
-based methods that provide more control over how the API is executed.You can also check out Brian Moakley’s Video Tutorial Series on CloudKit.
If you have any questions or comments about this tutorial, then please join the forum discussion below!
With CloudKit you can really take your apps to the next level and beyond with this great Apple-provided backend API. We can’t wait to see what you make. In the meantime, feel free to join us and our kids for a slice at Caesar’s Pizza!
The post CloudKit Tutorial: Getting Started appeared first on Ray Wenderlich.
Note from Ray: At our recent RWDevCon tutorial conference, in addition to hands-on tutorials, we also had a number of “inspiration talks” – non-technical talks with the goal of giving you a new idea, some battle-won advice, and leaving you excited and energized.
We recorded these talks so that you can enjoy them, even if you didn’t get to attend the conference. Here’s our next talk – The Power of Small by Cesare Rocchi – I hope you enjoy!
Grow, grow, grow!
We are surrounded by podcasts, conferences, books, blog posts, all forcing us to grow. Grow our user base, grow the number of downloads of our apps, grow our mailing lists. Always grow!
This is probably propelled by the Silicon Valley movement in which hockey stick growth is a requirement if you want to chase the next round of funding.
We also are used to hearing that size matters.
Even excluding all the nasty interpretations of that, probably you’ll think that the bigger the better. The bigger my team is, the more projects I can take on. The bigger is the market, the bigger is the potential income. I don’t want to even mention the bigger the code base…
It’s all true. Grow, grow, it’s all true. Probably. But is it worth it? Have you ever asked yourself is it worth it to chase something bigger? Do I want to give up the power of small?
Well, in the 70’s IBM was the king – I’d say the emperor of computers. Then out of the blue, two dudes in a garage started small – and you know the rest of the story.
Today I’m going to talk about the power of small. I’m going to show you some example of the advantages of working in a small market with a small team and exploiting the advantages of a small launch.
Let’s start with the advantages of a small market or niche.
Note it’s pronounced niche, not nitch, because it’s French.
Here’s a quick example of a small successful market:
This is Soda Pop Stop. John Nese and his family have been running this business in LA for a long time. They started as a grocery store and now they have a website that sells and ships sodas worldwide. Probably you’ve never heard of this company. We all know that the sodas market is dominated by huge companies that I’m not even going to mention.
But Johnny’s family has been able to carve out a small niche in which people prefer something alternative, I’d say unique, like for example the Werewolf Howling Ginger Beer or the Pumpkin Spice Tonic, which you’ve probably never heard of. That’s fine. Because that’s a nitch or niche?
Audience replies: Niche.
Great. Johnny’s family have been able to run this company for 100 years. Are you seeing the power of small already?
Let’s talk about the advantages of a small team. You can move faster.
Let me tell you a quick story. I joined a startup a few years ago. I was the only mobile developer, front end developer, and the other developers took care of the back end. We used Git but honestly we could have used a shared Dropbox folder because we never had a merge issue, our changes never overlapped, and after years that we both spent in the enterprise and convoluted processes we felt wild and young and free.
Really, I was committing code to the master branch and I felt like this when I did it.
But things changed. The company grew. We had to put in place formal process and documents and agreeing on a time slot for a meeting took longer than the meeting itself.
I could not commit to the master branch anymore unfortunately. You know the drill. You had to branch out, do your thing, run the tests, code review and then merge, and then Q&A, and then staging, and then production.
Don’t get me wrong. I mean, having a process is a great thing, because you have fewer chances to screw up. But it also can enlarge the distance between you building the product and the people using the product.
Now a small team is a competitive advantage so make the most of it. In a small team for example making decisions is much easier.
This is a story that Luke Parham, an iOS team member at raywenderlich.com, told me. He worked at a company. They had an app that allowed to consult used car reports. So instead of going to the dealer you just browse cars in the app.
They had a back end in Java. It was in Struts framework. (I can’t believe I mentioned both Java and Struts at a Mac and iOS friendly conference!)
Now if you look in the dictionary for the definition of “long”, there are a few examples there. One of them is the discussion that you can spark when you ask a bunch of developers, “Should I use vi or Emacs?”
Still in the dictionary, if you look up the definition of endless, one of the examples says, “The discussion that you can spark when somebody in a room full of developer says, ‘We should use a more modern framework for our back end.’”
Now at Luke’s company there were 7 people, 7 developers. It’s an odd number. It should be easy to come up with a majority. 2 wanted to go with Rails, 2 with Django, 1 with node JS, 1 didn’t care, the other one wanted to stick with the old framework. You can image which kind of discussions they had.
At some point somebody started building prototypes trying to show the advantages of a framework over the other. As it happens usually the meeting started with 7 people and after 2 months 20 people were attending the meeting and contributing and discussing and essentially wasting time.
Guess what? In the end they decided to stick to the old framework.
Now who here heard or lived a similar story? I see somebody can relate to this. This is what happens in a big, big team unfortunately.
Now let’s see the advantages of a small launch.
Imagine you had your idea. You designed it. You built it. You are ready to ship. But at some point one thought, especially if your app has some back end, this thought is going to hit you: “What if it doesn’t scale? What if some famous blogger talks about us and all the customers use our app and hog our servers?”
Have you ever heard something along the lines of this? Yeah.
A few observations.
Take Buffer for example.
They started with a simple landing page. Buffer if you don’t know is a service that allows you to schedule social media posts. They support Twitter, Facebook, Pinterest, LinkedIn, and so on. They started just with Twitter. They started just with 1 web page that explained the product and they noticed that people clicked.
After a while they added a second page, fake, showing the pricing plans. It was fake and again people were clicking. Bottom line after 7 weeks they build a product just with Twitter and they had the first few paying customers. They did exactly that, start slow.
A small launch can help you in testing the ground, testing the code, testing the design, spotting bugs.
It’s not a new idea at all. Restaurants do soft openings every time to give the stuff a test for example and to spot issues in the menu, typos in the menu for example.
Have you ever watched the Ocean’s Thirteen? A few of you, okay. So no spoilers, but there’s a pretty good scene of a casino doing a soft opening in the movie.
Now let’s see some tricks that you can exploit to stay small and focused.
The first one probably my preferred one is to set a time limit.
Think along the lines of will submit this app in 3 weeks with feature A, B, and C, and that’s it. Or let’s take big app, break it down in 6 milestones, and then finish just the first one and ship it to the store in 3 weeks.
Set a time limit because if you do that … Well, first of all, the end is in sight, so I got to get there and not somewhere unknown. You go with small steps. That helps the morale because you easily notice the progress day by day.
Set a limit on features. I’m sure any of us probably at some point in their career started with an app, big ideas, many features, I’m going to do everything, and then you end up struggling along the way, you are 40% done and you don’t even know if you’re ever going to finish the app.
Why not starting with fewer features? By the way, you don’t even know if all those features are relevant and valuable to your customers. So why not just starting with fewer features and maybe talking to your customers about those features? Much easier.
The third suggestion is break it down.
Try to break it down as much as possible. Now, I am a father, I have a family. I cannot afford anymore 10 hours of uninterrupted work a day. Although sometimes I think if I could have 3 good solid days, but I can’t. But I happen to have on the spot 20 minutes here and there.
Now if I carefully organize my to-do list with small actions easy to chew, then it’s great. What can I do in 20 minutes? Well,
There’s so many things that I can do in this on the spot 20 minutes that I happen to have.
Summing up here is the list of my suggestions.
Let’s see a few examples in the world of software. You probably are familiar with Basecamp.
They’re not a publicly traded company so they are not forced to share numbers at all. Pretty good place to be. But we can make an educated guess that they have around 10 million users.
Do you want to venture and answer to the question how many people are working at Basecamp? 50. That’s CEO, CTO, designers, developers, janitors, everybody. 50 people doing everything.
There’s a lot of advantages in this situation. They’re also bootstrapped. First of all, they’ve been able to grow the company culture slowly. No revolutions. Also that means that people tend to leave the company less often.
Also they are careful in hiring because you know in this case hiring means cutting a part of your check every month to pay someone else to work for the company. But it’s been working great for them so far because they’ve been around for 12 or 13 years as far as I remember.
Another example is Quip.
This is a VC funded company in the Valley so they have money, but probably they don’t spend it on developers because they have an app that is compatible with the Mac, Windows, Linux, Android tablets, iPhone, iPad, Apple watch. And it’s just 13 people, including CEO and CTO which probably are not going to code every single day.
They have a small team, tight-knit, super effective communication and they are forced to prioritize. This is the result and it’s great.
There’s also solo developers. Have you ever heard of the app Desk PM?
It’s made by John Saddington. Just one guy. He won the Apple award 2 years in a row. It’s a blogging application. I’m sure I can say there’s no shortage of blogging applications in the App Store and yet he’s been so successful.
Now when you’re solo, again, there are advantages. Like you can choose your own pace. It took John 13 years to build the app.
You can experiment and you can decide where do you want to go. Also, how to reinvest your income. For example, there’s a pretty famous blog post in which he describes how he dropped either 9 or 10k as sponsorship on Daring Fireball and it was totally worth it.
Summing up, being small has advantages. So being bigger isn’t always the best option.
I’d like to close the presentation with a Chinese proverb that really distills my message.
Now, what is your next small step?
The post RWDevCon 2016 Inspiration Talk – The Power of Small by Cesare Rocchi appeared first on Ray Wenderlich.
iCloud does a lot of great things. It bridges the gap between iOS and macOS by storing and syncing app data and files so the user can access their stuff from any Apple device. Notes and Photos are excellent examples of the power of this service — if your stuff’s on one device, it’s reliably on the rest of them within moments. It’s almost magical.
In addition to the obvious use cases, iCloud hosts apps’ public and private databases and handles user authentication.
CloudKit is the framework that affords access to iCloud, providing tons of APIs to make it easier to incorporate iCloud’s magical ways into your creations.
When Apple announced CloudKit in 2014, web services topped the list of feature requests. Then in 2015, Apple announced CloudKit Web Services, a JSON/HTTPS interface for CloudKit.
Apple took it a step further and provided CloudKit JS, which makes it simple to create a web-based interface to access a CloudKit app’s databases. CloudKit JS wraps the REST API in concise code, so you don’t have to compose paths manually or parse JSON.
As you work through this CloudKit JS tutorial, you’ll create a web app to access the database of a CloudKit iOS app. When you’re done, it’ll provide web access to users and even make app data available to non-iOS users!
You’ll need basic understanding of HTML and JavaScript for this tutorial. For a quick refresher, check out W3Schools.
This CloudKit JS tutorial also assumes you have working knowledge of CloudKit. If you don’t, we suggest starting with our CloudKit Tutorial: Getting Started.
Also nice to have, but not required:
CloudKit JS features a similar design to CloudKit iOS, which makes it easier for iOS developers to create CloudKit web apps. Its main features include:
Best of all, because CloudKit JS feels familiar to web developers, you can simply give them your container ID, API key and database schema and let them build the web app. ;]
CloudKit JS works on mainstream browsers, including Safari, Firebox, Chrome, Internet Explorer and Microsoft Edge.
In February 2016, Apple announced that CloudKit now supports server-to-server web service requests, enabling reading and writing to CloudKit databases from server-side processes or scripts. You’ll see why this is significant at the end of this tutorial.
As a relative n00b to social media, I often learn new acronyms from people I follow on Twitter.
For instance, TIL means “Today I Learned” — I felt it’s the perfect name for this sample app. It’s a simple premise: users can view and add acronym definitions to a public CloudKit database.
To run the sample CloudKit iOS app, you must be a member of the Apple Developer Program or the Apple Developer Enterprise Program. You won’t be able to run the iOS app if you’re not. :[
However, non-members can still build the web app to access my CloudKit container. You’ll just need to skip certain sections related to CloudKit setup — it’ll be clear what to skip.
If you don’t have a Mac, that’s okay! You don’t need one to build the web app.
Download and unzip TIL Starter. It contains the starter iOS app, web app, and server files.
Note: This is one of those spots where “Apple Developers” part ways with those who are not. ;]
Open TIL.xcodeproj. Select the TIL project in the project navigator, and then under Targets\TIL\General change the Bundle Identifier and Team to match your information. Tap Fix Issue to get Xcode to create the provisioning profile.
In Targets\TIL\Capabilities, toggle iCloud OFF then ON again, and select Key-value storage, CloudKit, and Use default container.
Note: Enabling CloudKit in your app automatically creates a default container to store the app’s databases and user records. However, it only stores your app’s public data. Private data saves directly to users’ iCloud accounts. Fortunately, iCloud automatically handles user registration and creates a unique identifier for each user. You’ll add authentication later in this tutorial.
Tap CloudKit Dashboard to open the CloudKit Dashboard in a browser. You can use the dashboard to access, set up and view records online, and manage your CloudKit app.
Sign in to the iCloud account that matches the Team you set in Targets\TIL\General. Select the TIL container from the list.
Note: If you don’t see TIL, try refreshing the page. Sometimes CloudKit’s dashboard won’t show new data, but waiting a few seconds and refreshing typically fixes the issue.
Select Schema\Record Types and press + to create a new record type. Name it Acronym and add two fields. Name the first short and the second long. Make sure both fields’ type is set to String. Tap Save.
In Public Data\Default Zone, create two new Acronym records. For the first, set its long value to Today I Learned and short to TIL. For the second, set long to For The Win and short to FTW.
Note: As with the TIL container, you may not see Acronym appear under Default Zone straight away. You might just see NewRecordType until you refresh the page.
Go to the iPhone simulator, open Settings and sign in to iCloud. Build and run the TIL iOS app. Your new records should appear!
In the iOS app, tap the + button and add two items: BTW/By The Way and TBH/To Be Honest.
Back in your browser, change the Sort by order in the CloudKit dashboard’s Default Zone to sort by new items — it’s a little hack that “refreshes” the records in the CloudKit dashboard.
Your web app will update its UI when users sign in or out and when the database changes by using knockout.js to create dynamic data bindings between the UI in index.html and the data model in TIL.js.
If you’re familiar with the MVVM design pattern, then know that the CloudKit database is the model, index.html is the view, and TIL.js is the view model.
Knockout provides data bindings for text, appearance, control flow and forms. You’ll inform Knockout of which view model properties can change by declaring them as observable.
You’ll also use the Skeleton responsive CSS framework to create a grid-based web UI. Each row contains 12 columns to use to display elements.
Inside the TIL Starter/Web folder are TIL.js and index.html. You’ll add JavaScript code to TIL.js to fetch and save Acronym records, and you’ll update index.html to display and get user input for Acronym records.
Open TIL.js and index.html in Xcode and go to Xcode\Preferences. In Text Editing\Indentation, uncheck Syntax-aware indenting: Automatically indent based on syntax. Xcode’s auto-indent doesn’t work well with JavaScript, but its Editor\Structure functions are still useful.
Take a look at the code in TIL.js:
// 1 window.addEventListener('cloudkitloaded', function() { console.log("listening for cloudkitloaded"); // 2 CloudKit.configure({ containers: [{ // 3 containerIdentifier: 'iCloud.com.raywenderlich.TIL', apiToken: '1866a866aac5ce2fa732faf02fec27691027a3662d3af2a1456d8ccabe9058da', environment: 'development' }] }); console.log("cloudkitloaded"); // 4 function TILViewModel() { var self = this; console.log("get default container"); var container = CloudKit.getDefaultContainer(); } // 5 ko.applyBindings(new TILViewModel()); }); |
window
is the browser window. Apple recommends loading cloudkit.js asynchronously, so window.addEventListener('cloudkitloaded', function() { ... }
attaches the function
as the event handler of the cloudkitloaded
event. The rest of the code in TIL.js is the cloudkitloaded
event handler. containerIdentifier
to match the value of the default container, which should be iCloud, followed by the iOS app’s bundle identifier you set earlier.TILViewModel()
simply renames JavaScript’s this
to self
, which is more familiar to iOS developers, and gets the default container.TILViewModel()
.In the CloudKit dashboard, create an API token for your container. Select Admin\API Access and then Add new API token. Name it JSToken, check Discoverability and click Save.
Copy the token.
Back in TIL.js, paste your new token in place of the current apiToken
. Leave environment set to development and save.
TIL.js contains console.log
statements that perform the same role as debug print
statements in Swift. The messages appear in the browser’s console — Safari’s Error Console, Chrome’s JavaScript Console, or Firefox’s Browser Console.
Open index.html in Safari. To show the error console, select Safari\Preferences\Advanced, and check Show Develop menu in menu bar.
Close the preferences window and select Develop\Show Error Console. Refresh the page to see console.log
messages.
Note: If you’re using Chrome, its console is in the View\Developer menu.
If you’re using Firefox, find it under the Tools\Web Developer menu.
Anyone can view the public database, even without signing in to iCloud, as long as you fetch the public records and display them on a webpage. That’s what you’ll do in this section.
In TIL.js, add the following to TILViewModel()
, right before the method’s ending curly brace:
console.log("set publicDB"); var publicDB = container.publicCloudDatabase; self.items = ko.observableArray(); |
This little block gets you a reference to the public database via publicDB
then it declares items
as a Knockout observable array that contains the public records.
Add the following right after the lines you just added:
// Fetch public records self.fetchRecords = function() { console.log("fetching records from " + publicDB); var query = { recordType: 'Acronym', sortBy: [{ fieldName: 'short'}] }; // Execute the query. return publicDB.performQuery(query).then(function(response) { if(response.hasErrors) { console.error(response.errors[0]); return; } var records = response.records; var numberOfRecords = records.length; if (numberOfRecords === 0) { console.error('No matching items'); return; } self.items(records); }); }; |
Here you define the fetchRecords
function, which retrieves the public records sorted by the short
field and stores them in items
.
Note: If you want to create a web app to access someone else’s CloudKit container, you need to know how its databases are organized, namely record types, field names and field types.
TIL’s public database stores records of type Acronym, which has two String
fields named short and long.
Lastly, add the following right after the previous lines:
container.setUpAuth().then(function(userInfo) { console.log("setUpAuth"); self.fetchRecords(); // Don't need user auth to fetch public records }); |
Here you run container.setUpAuth()
to check whether a user is signed in, and then it presents the appropriate sign-in/out button. You don’t need authentication yet, but you still call fetchRecords()
to get the Acronyms to display.
Save TIL.js.
Next, open index.html and scroll to the bottom. Add the following right above the End Document
comment:
<div data-bind="foreach: items"> <div class="row"> <div class="three columns"> <h5><span data-bind="text: fields.short.value"></span></h5> </div> <div class="nine columns"> <p><span data-bind="text: fields.long.value"></span></p> </div> </div> </div> |
Here you iterate through the items
array. Each element in items
is an acronym record.
Knockout’s text
binding displays the short
and long
text values. The foreach
control flow binding duplicates the Skeleton row for each element in items
, and it binds each row to the corresponding items
element.
Because items
is an observable array, this binding efficiently updates the displayed rows every time items
changes.
Save index.html and reload it in the browser. Public database records appear in the browser window and console.log
messages still show in the error console.
421
error appears when you initialize publicDB
. Don’t worry, this won’t stop the list from appearing. Select Logs instead of All to see only log messages. If the list doesn’t appear, check the console messages to see if CloudKit failed to load, and if so, reload the web page.To add items to the public database, users must sign in to iCloud. Apple handles user authentication directly and provides sign-in and sign-out buttons. If a user has no Apple ID, the sign-in dialogue lets them create one.
In TIL.js, add this code to the end of container.setUpAuth()
, just below the call to fetchRecords()
:
if(userInfo) { self.gotoAuthenticatedState(userInfo); } else { self.gotoUnauthenticatedState(); } |
container.setUpAuth()
is a JavaScript promise — the outcome of an asynchronous task. In this case, the task determines whether there’s an active CloudKit session with an authenticated iCloud user. When the task finishes, the promise resolves to a CloudKit.UserIdentity
dictionary or null
, or it rejects to a CloudKit.CKError
object.
When the promise resolves, the CloudKit.UserIdentity
dictionary becomes available to the then
function as the parameter userInfo
. You just added the body of the then
function; if userInfo
isn’t null
, you pass it to gotoAuthenticatedState(userInfo)
; otherwise, you call gotoUnauthenticatedState()
.
Now, you’ll define these two functions, starting with gotoAuthenticatedState(userInfo)
.
Add these lines right above container.setUpAuth().then(function(userInfo) {
:
self.displayUserName = ko.observable('Unauthenticated User'); self.gotoAuthenticatedState = function(userInfo) { if(userInfo.isDiscoverable) { self.displayUserName(userInfo.firstName + ' ' + userInfo.lastName); } else { self.displayUserName('User Who Must Not Be Named'); } container .whenUserSignsOut() .then(self.gotoUnauthenticatedState); }; |
Because you checked Request user discoverability at sign in when you created the API key, users can choose to let the app know their names and email addresses.
If the user isDiscoverable
, the web page will display their name. Otherwise, they’ll be called the User Who Must Not Be Named; while you could display the unique userInfo.userRecordName
returned by the iCloud sign-in, that’d be far less amusing. ;]
Either way, iCloud remembers the user’s choice and doesn’t ask again.
container.whenUserSignsOut()
is another promise — its then
function calls gotoUnauthenticatedState()
.
Right after the code you just inserted, add the following to define gotoUnauthenticatedState()
:
self.gotoUnauthenticatedState = function(error) { self.displayUserName('Unauthenticated User'); container .whenUserSignsIn() .then(self.gotoAuthenticatedState) .catch(self.gotoUnauthenticatedState); }; |
When no user is signed in, you reset displayUserName
and wait for a user to sign in. If the container.whenUserSignsIn()
promise rejects to an error object, the app remains in an unauthenticated state.
Save TIL.js, and go back to index.html in Xcode.
Add the following right after <h2>TIL: Today I Learned <small>CloudKit Web Service</small></h2>
:
<h5 data-bind="text: displayUserName"></h5> <div id="apple-sign-in-button"></div> <div id="apple-sign-out-button"></div> |
The h5
header creates a text binding to the observable displayUserName
property in TIL.js. To fulfill its promise, container.setUpAuth()
displays the appropriate sign-in/out button.
Save and reload index.html to see Unauthenticated User and the sign-in button.
Click the sign-in button and login to an iCloud account. Any Apple ID works; it need not be an Apple Developer account.
After you sign in, the web page will update displayUserName
and display the sign-out button.
Sign out of iCloud before continuing — the next step will clear userInfo
and display the sign-in button. You’ll feel more in control if you sign out now. :]
You need a web form where users can add new items and some corresponding JavaScript that’ll save items to the public database.
In TIL.js, right after the fetchRecords()
definition, add these lines:
self.newShort = ko.observable(''); self.newLong = ko.observable(''); self.saveButtonEnabled = ko.observable(true); self.newItemVisible = ko.observable(false); |
Here you declare and initialize observable properties that you’ll bind to the UI elements in index.html.
There will be two input fields, newShort
and newLong
and a submit button that you’ll disable when saving the new item.
Only authenticated users can add items to the database, so newItemVisible
controls whether the new-item form is visible. Initially, it’s set to false
.
Add this line to the gotoAuthenticatedState
function right after its open curly brace:
self.newItemVisible(true); |
This block makes the new-item form visible after a user signs in.
Add this to the top of the gotoUnauthenticatedState
function:
self.newItemVisible(false); |
In here, you’re hiding the new-item form when the user signs out.
Next, add the following to define the saveNewItem
function, right below the self.newItemVisible = ko.observable(false);
line:
self.saveNewItem = function() { if (self.newShort().length > 0 && self.newLong().length > 0) { self.saveButtonEnabled(false); var record = { recordType: "Acronym", fields: { short: { value: self.newShort() }, long: { value: self.newLong() }} }; publicDB.saveRecord(record).then(function(response) { if (response.hasErrors) { console.error(response.errors[0]); self.saveButtonEnabled(true); return; } var createdRecord = response.records[0]; self.items.push(createdRecord); self.newShort(""); self.newLong(""); self.saveButtonEnabled(true); }); } else { alert('Acronym must have short and long forms'); } }; |
This checks that input fields are not empty, disables the submit button, creates a record and saves it to the public database. The save operation returns the created record, which you push
(append) to items
instead of fetching all the records once again. Lastly, you clear the input fields and enable the submit button.
Save TIL.js, and return to index.html in Xcode.
Add the following right before <div data-bind="foreach: items">
:
<div data-bind="visible: newItemVisible"> <div class="row"> <div class="u-full-width"> <h4>Add New Acronym</h4> </div> </div> <form data-bind="submit: saveNewItem"> <div class="row"> <div class="three columns"> <label>Acronym</label> <input class="u-full-width" placeholder="short form e.g. FTW" data-bind="value: newShort"> </div> <div class="nine columns"> <label>Long Form</label> <input class="u-full-width" placeholder="long form e.g. For the Win" data-bind="value: newLong"> <input class="button-primary" type="submit" data-bind="enable: saveButtonEnabled" value="Save Acronym"> </div> </div> </form> <hr> </div> |
The heart of this code is a web form with two input fields — short
gets three columns and long
gets nine. You also created a submit
button that’s left-aligned with the long
input field. Whenever the submit button is tapped, saveNewItem
is invoked.
For the value bindings, you name the observable properties newShort
and newLong
that saveNewItem
uses. The visible binding will show or hide the web form, according to the value of the observable property newItemVisible
. Lastly, the enable binding enables or disables the submit
button, according to the value of the observable property saveButtonEnabled
.
Save the file and reload index.html in a browser.
Sign in to an iCloud account and the fancy new form should show itself. Try adding a new item, such as YOLO/You Only Live Once:
Click Save Acronym and watch your new item appear at the end of the list!
Go back to the CloudKit Dashboard \ Default Zone and change the sort order to see your new item appear.
When users add or delete items the web page should update to show the current list. Like any respectable CloudKit iOS app, your app can subscribe to the database for updates.
In TIL.js, add the following inside the gotoAuthenticatedState
function, right after self.newItemVisible(true)
:
//1 var querySubscription = { subscriptionType: 'query', subscriptionID: userInfo.userRecordName, firesOn: ['create', 'update', 'delete'], query: { recordType: 'Acronym', sortBy: [{ fieldName: 'short'}] } }; //2 publicDB.fetchSubscriptions([querySubscription.subscriptionID]).then(function(response) { if(response.hasErrors) { // subscription doesn't exist, so save it publicDB.saveSubscriptions(querySubscription).then(function(response) { if (response.hasErrors) { console.error(response.errors[0]); throw response.errors[0]; } else { console.log("successfully saved subscription") } }); } }); //3 container.registerForNotifications(); container.addNotificationListener(function(notification) { console.log(notification); self.fetchRecords(); }); |
Here’s what you set up:
subscriptionID
to userInfo.userRecordName
. fetchRecords()
to get the new items in the correct sorted order.Save TIL.js, reload index.html in a browser and sign in. Add a new acronym (perhaps AFK/Away From Keyboard) in the CloudKit dashboard, another browser window, or in the iOS app.
The notification appears in the console and the list of updates on the page! Magic!
Sometimes the list doesn’t update, even when the notification appears and fetchRecords
successfully completes.
The reason this happens is that race conditions are possible with asynchronous operations, and fetchRecords
sometimes runs before the new item is ready. Try printing records.length
to the console at the end of the performQuery(query)
handler so you can see that this number doesn’t always increase after a notification.
You can mitigate this risk by replacing the first class="row"
div of index.html with the code below to provide a manual refresh button:
<div class="row"> <div class="u-full-width"> <h2>TIL: Today I Learned <small>CloudKit Web Service</small></h2> </div> </div> <div class="row"> <div class="six columns"> <h5 data-bind="text: displayUserName"></h5> </div> <div class="four columns"> <div id="apple-sign-in-button"></div> <div id="apple-sign-out-button"></div> </div> <div class="two columns"> <div><button data-bind="click: fetchRecords">Manual Refresh</button></div> </div> </div> |
Save and reload index.html to see the new button. By the way, it even works without signed in users:
On February 5, 2016 — a week after Facebook announced plans to retire the Parse service — Apple announced that CloudKit now supports server-to-server web service requests, enabling reading and writing to CloudKit databases from server-side processes or scripts.
Talk about a Big Deal: now you can use CloudKit as the backend for web apps that rely on admin processes to update data — like most modern web apps.
However, the API key isn’t enough. You need a server key.
Open Terminal, cd
to the TIL Starter/Server directory, and enter this:
openssl ecparam -name prime256v1 -genkey -noout -out eckey.pem |
This creates a server-to-server certificate: eckey.pem
contains the private key.
Still in Terminal, enter the following to display the new certificate’s public key:
openssl ec -in eckey.pem -pubout |
In the CloudKit Dashboard, navigate to API Access\Server-to-Server Keys and click Add Server-to-Server Key.
Name the key Server2ServerKey.
Copy the public key from Terminal’s output, paste it into Public Key and tap Save. Then, copy the generated Key ID.
Open config.js in Xcode, and replace my containerIdentifier
and keyID
with your own:
module.exports = { // Replace this with a container that you own. containerIdentifier:'iCloud.com.raywenderlich.TIL', environment: 'development', serverToServerKeyAuth: { // Replace this with the Key ID you generated in CloudKit Dashboard. keyID: '1f404a6fbb1caf8cc0f5b9c017ba0e866726e564ea43e3aa31e75d3c9e784e91', // This should reference the private key file that you used to generate the above key ID. privateKeyFile: __dirname + '/eckey.pem' } }; |
To run index.js from a command line, you’ll need to complete a few more steps.
First, go to nodejs.org and install Node.js on your computer if you don’t have it. Next, follow the advice at the end of the setup and add /usr/local/bin to your $PATH, if it’s not there already.
Back in Terminal and still in the TIL Starter/Server directory, run these commands:
npm install
npm run-script install-cloudkit-js |
These commands install the npm
module and the CloudKit JS library, which index.js uses.
Now, enter this command in Terminal to run index.js:
node index.js |
The output of this command looks similar to the following:
CloudKitJS Container#fetchUserInfo --> userInfo: a { userRecordName: '_a4050ea090b8caace16452a2c2c455f4', emailAddress: undefined, firstName: undefined, lastName: undefined, isDiscoverable: false } CloudKitJS CloudKit Database#performQuery { recordType: 'Acronym', sortBy: [ { fieldName: 'short' } ] } {} --> FOMO: Fear Of Missing Out Created Sun Jun 19 2016 20:16:32 GMT+1000 (AEST) ... --> YOLO: You Only Live Once Created Fri Jun 17 2016 14:37:04 GMT+1000 (AEST) Done |
In here you’ve adapted config.js and the index.js from Apple’s CloudKit Catalog source code to query the TIL public database and print the short
, long
and created
fields.
Here’s the final version of the web app.
You covered quite a bit in this CloudKit JS tutorial and know the basics of how to use CloudKit JS to make your iOS CloudKit app available to a wider audience via a web interface.
Watch CloudKit JS and Web Services from WWDC 2015, and take some of the features in CloudKit Catalog for a test drive. Explore additional features like user discoverability, record zones and syncToken
.
Watch What’s New with CloudKit from WWDC 2016 for an in-depth look at record sharing and the new record sharing UI — you can try this out in CloudKit Catalog, too. Apple keeps refining CloudKit to make it easier for developers to create reliable apps: look at CKOperation
‘s QualityOfService
to handle long-running operations and CKDatabaseSubscription
and CKFetchDatabaseChanges
to get changes to record zones that didn’t even exist when your app started!
I hope you enjoyed this tutorial — I sure had fun putting it together! Please join the discussion below to share your observations, feedback, ask questions or share your “ah-ha” moments!
The post CloudKit JS Tutorial for iOS appeared first on Ray Wenderlich.
Last week, we launched a major overhaul to our videos platform at raywenderlich.com: videos.raywenderlich.com.
The new site includes a brand new visual design, along with a ton of new features subscribers have been asking for.
To celebrate the launch, we had a huge giveaway, where the grand prize winner gets a 1-year raywenderlich.com all-access pass of over $1,000 in value.
Keep reading to find out who the lucky winners are – and how to get the launch discount before it’s too late!
To enter the giveaway, all you had to do was reply to the announcement post with the answer to one simple question:
The team and I were amazed and overwhelmed by the response. Over 150 people left comments, and your kind words have meant so much to myself and the rest of the team – we have literally been talking about it all week. You folks are amazing!
We’ve randomly selected 10 second prize winners, who each win a free PDF book of their choice from this site. Below is each winner, and their (abbreviated) quote:
10) steffanestorov
9) jlchapman
8) uwalum
7) kwalkerk
6) hwilms
5) rntec
4) chavarria
3) jsd
2) jcfmunoz.gabhel
1) cretech
Congratulations to all of the lucky winners, and thanks so much for supporting all we do on this site! We are so happy to hear you have been enjoying the videos and we will definitely keep them coming.
All second prize winners receive a free PDF book of your choice from this site – my coworker Christine will be in touch with further details.
But that’s not all… we have one lucky grand prize winner too!
The lucky grand prize winner gets a 1-year raywenderlich.com all-access pass. This includes:
This represents over $1,000 in value – w00t!
We’ve randomly selected a grand prize winner, and the result is… drum roll please…
…
…
…
…
1) nkvintage
Congratulations! Christine will be in touch soon to deliver your prizes.
If you haven’t checked out the new design for videos.raywenderlich.com, please check it out and let me know what you think!
And remember – today is your last chance for the special 50% off your first month launch promotion we are currently running to celebrate – so grab it while you still can!
Thanks again for all of your kind words about our new videos platform, and our video tutorials in general. Stay tuned for much more in the year to come! :]
The post videos.raywenderlich.com Giveaway – and Last Day For Discount! appeared first on Ray Wenderlich.
Learn how to add your own custom units and dimensions to Foundation's units and measurements system.
The post iOS 10 Screencast: Custom Units appeared first on Ray Wenderlich.
These days, people are always being told to relax, go off-grid and put their smartphones down. But where’s the fun in that? I’m here to encourage you not to get off the grid. Instead, get onto our grid—our Android GridView
tutorial, that is!
Today you’ll learn how to tame this widely-used Android component and construct your own Bookcase application containing various books for babies.
If you’re familiar with Android’s ListView
component, you should feel right at home here. We also have a great tutorial on ListViews if you’d like to familiarize yourself with them first (although you don’t need to for this tutorial).
GridView
follows the same formula to render each cell. Basically, you:
BaseAdapter
subclass.GridView
.getView()
on your adapter.Android’s GridView
is a useful component for constructing easy-to-browse lists. And since grids are everywhere these days (notably in the Instagram app) it’s a necessary skill. After completing this tutorial, you’ll be ready to conquer the world with your own amazing grid views!
Download the starter project here. Included are the image assets for each book and a list of Book objects describing the contents of our Baby Bookcase. Open the project by starting Android Studio and selecting Open an existing Android Studio project:
Navigate to the directory where you unzipped the sample project and hit OK:
Build and run the project on your preferred emulator or Android device—you should see the most amazing blank screen ever:
You are now ready to start integrating your GridView
.
Navigate to app\res\layout\activity_main.xml and insert the following block of XML between the RelativeLayout
tags (make sure to view the file as “Text” by clicking the tab near the bottom):
<GridView android:id="@+id/gridview" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:columnWidth="100dp" android:numColumns="auto_fit" android:verticalSpacing="24dp" android:horizontalSpacing="10dp" android:stretchMode="spacingWidthUniform" /> |
Your editor should now show a preview of your GridView
:
The snippet above specifies a GridView
with some customized properties. We’ll discuss property customization in more detail later on.
Empty grids are hungry for data to display. To feed this one, you’ll set up a class called BooksAdapter
, an adapter which is a subclass of the BaseAdapter
class, as your data provider for the GridView
. In the project navigator pane, right-click on com.raywenderlich.babybookcase, and select New/Java Class. After that, type in BooksAdapter for Name, keep Class for Kind, and hit OK.
The adapter acts as the middleman between the GridView
and the data source. It loads the information to be displayed in your grid view from a data source, such as an array or database query, then creates a view for each item. If you’re interested, the ListView tutorial has more information on adapters.
Replace the contents of this file with the following:
package com.raywenderlich.babybookcase; import android.content.Context; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; public class BooksAdapter extends BaseAdapter { private final Context mContext; private final Book[] books; // 1 public BooksAdapter(Context context, Book[] books) { this.mContext = context; this.books = books; } // 2 @Override public int getCount() { return books.length; } // 3 @Override public long getItemId(int position) { return 0; } // 4 @Override public Object getItem(int position) { return null; } // 5 @Override public View getView(int position, View convertView, ViewGroup parent) { TextView dummyTextView = new TextView(mContext); dummyTextView.setText(String.valueOf(position)); return dummyTextView; } } |
Here’s the play-by-play of each step in the above code:
BooksAdapter
.id
for this tutorial, so just return 0
. Android still requires you to provide an implementation for this method.null
.TextView
as the cell view for your GridView
.Now that you have a basic adapter implementation, you can use this class as the data provider for the GridView
in MainActivity
. Within onCreate()
in MainActivity.java, underneath
setContentView(R.layout.activity_main); |
insert the following:
GridView gridView = (GridView)findViewById(R.id.gridview); BooksAdapter booksAdapter = new BooksAdapter(this, books); gridView.setAdapter(booksAdapter); |
Build and run. You should see something like this:
Great—we’re getting somewhere!
To provide the proper view for the cells that represent each Book
object, create a new XML layout file by right-clicking on the layout directory and selecting New/Layout resource file:
Use linearlayout_book as the file name and hit OK:
Replace the file contents with the following:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" > <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/imageview_cover_art" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop"/> <ImageView android:id="@+id/imageview_favorite" android:layout_width="24dp" android:layout_height="24dp" android:src="@drawable/star_disabled" android:layout_gravity="bottom|right"/> </FrameLayout> <TextView android:id="@+id/textview_book_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" tools:text="Are You My Mother" android:textStyle="bold" android:paddingTop="4dp" android:paddingLeft="8dp" android:paddingRight="8dp" android:gravity="center_horizontal"/> <TextView android:id="@+id/textview_book_author" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" tools:text="Dr. Seuss" android:gravity="center_horizontal"/> </LinearLayout> |
This new layout file will be used to represent your Book
objects inside the grid. Each book will be drawn inside a cell that contains four items:
ImageView
for the cover art.ImageView
for a “favorited” star icon.TextView
for the book name.TextView
for the book’s author.Here’s a sample of what a book cell will look like:
Now that your layout is established, you can return a more complex view for your cells. Open BooksAdapter.java again and finish the implementation of getView()
by replacing it with the following:
@Override public View getView(int position, View convertView, ViewGroup parent) { // 1 final Book book = books[position]; // 2 if (convertView == null) { final LayoutInflater layoutInflater = LayoutInflater.from(mContext); convertView = layoutInflater.inflate(R.layout.linearlayout_book, null); } // 3 final ImageView imageView = (ImageView)convertView.findViewById(R.id.imageview_cover_art); final TextView nameTextView = (TextView)convertView.findViewById(R.id.textview_book_name); final TextView authorTextView = (TextView)convertView.findViewById(R.id.textview_book_author); final ImageView imageViewFavorite = (ImageView)convertView.findViewById(R.id.imageview_favorite); // 4 imageView.setImageResource(book.getImageResource()); nameTextView.setText(mContext.getString(book.getName())); authorTextView.setText(mContext.getString(book.getAuthor())); return convertView; } |
Here’s an explanation of each step:
position
index.GridView
optimizes memory usage by recycling the cells. This means that if convertView
is null, you instantiate a new cell view by using a LayoutInflater
and inflating your linearlayout_book layout.Build and run the application again. You should see the following:
Now you’ve got a pretty grid of books. Hooray! But what if your user wants to favorite a book by tapping on it? To allow this, set up an on-click listener for your GridView
. Open MainActivity.java and add the following snippet at the bottom of onCreate()
:
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Book book = books[position]; book.toggleFavorite(); // This tells the GridView to redraw itself // in turn calling your BooksAdapter's getView method again for each cell booksAdapter.notifyDataSetChanged(); } }); |
A variable must be declared as final
to be used inside an anonymous nested class (like our on-click listener), so to change the booksAdapter
variable’s declaration to final, replace this line in onCreate()
:
BooksAdapter booksAdapter = new BooksAdapter(this, books); |
with:
final BooksAdapter booksAdapter = new BooksAdapter(this, books); |
To toggle the star icon in your cell, add this line of code in getView
in BooksAdapter
, right before you return the convertView
:
imageViewFavorite.setImageResource( book.getIsFavorite() ? R.drawable.star_enabled : R.drawable.star_disabled); |
Build and run. You can now tap on each book to favorite your selections:
You’ve happily selected some favorite books … but when you rotate the screen (CTRL + F11 for emulator rotation), the selections aren’t remembered. What!?
This is because Android Activities are destroyed and then recreated when the screen rotates. By default, the system uses the Bundle
instance state to save information about each View
object in your activity layout (such as the text displayed in the TextView
s of your GridView
). When your activity instance is destroyed and recreated, the state of the layout is restored to its previous state with no code required by you.
This means that to make the selections behave correctly, you need to save the selection information yourself. Thankfully, Android Activities have two useful methods for this:
onSaveInstanceState()
: Android will call this method right before the activity is destroyed.onRestoreInstanceState()
: Android will call this method when recreating the activity.To remember the selected books, the app needs to save the selection information to a list, then use that list to select the books when the activity is recreated.
First, establish an identifying key for the list at the beginning of your class by adding the following line before onCreate()
in MainActivity.java:
private static final String favoritedBookNamesKey = "favoritedBookNamesKey"; |
Add the following to MainActivity.java under onCreate()
:
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // construct a list of books you've favorited final ArrayList<Integer> favoritedBookNames = new ArrayList<>(); for (Book book : books) { if (book.getIsFavorite()) { favoritedBookNames.add(book.getName()); } } // save that list to outState for later outState.putIntegerArrayList(favoritedBookNamesKey, favoritedBookNames); } |
To restore the selections when the activity is recreated, add the following to MainActivity.java under onSaveInstanceState()
:
@Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); // get our previously saved list of favorited books final ArrayList<Integer> favoritedBookNames = savedInstanceState.getIntegerArrayList(favoritedBookNamesKey); // look at all of your books and figure out which are the favorites for (int bookName : favoritedBookNames) { for (Book book : books) { if (book.getName() == bookName) { book.setIsFavorite(true); break; } } } } |
Build and run. Now when you rotate the screen, your books are still favorited!
As you play around with the GridView
component, you may run into situations where the layout doesn’t suit your design perfectly. Luckily, the great folks at Google have provided a few properties that allow you to tweak how each cell is rendered.
You can modify columnWidth
, horizontalSpacing
and verticalSpacing
with a dimension value such as 14.5dp
. These properties can be set programmatically or from the XML layout files.
To find properties to set, type gridView.set
at the bottom of onCreate()
in MainActivity.java and wait for Android Studio to show suggestions.
Alternatively, take a look at the GridView documentation for a list of available methods, some of which set these properties. You can also head back to app/res/layout/activity_layout.xml and play around with the properties within the GridView
tag. The properties are fairly self-explanatory, so I definitely encourage you to experiment with different values.
To specify the number of columns to render, provide an integer value for the property numColumns
. If you want to display as many columns as possible to fill the available horizontal space, use numColumns="auto_fit"
. Here’s a comparison between the two options in landscape orientation (I’ve used darker grey cell backgrounds for clarity purposes):
Finally, to control how your columns stretch to fill available space, specify stretchMode
as one of the following:
none
: Stretching is disabled.spacingWidth
: Spacing between each column is stretched.columnWidth
: Each column is stretched equally.spacingWidthUniform
: Spacing between each column is uniformly stretched.The following screenshot illustrates those options:
Using the customizable properties, you can easily get your GridView
looking just the way you want it!
Your first few implementations of GridView
will likely be simple in terms of view layouts, but as your app scales and you get more content, the stress on your users’ devices can become quite high in terms of loading cells and maintaining smooth scrolling.
The current implementation of this sample app recycles each cell view as needed within BooksAdapter.getView()
, so make sure to use this strategy whenever you need to provide views for your cells.
One technique to improve performance is called the ViewHolder design pattern. As the name suggests, you implement a class to “hold” the subviews inside of your cells. This avoids memory-expensive calls to findViewById()
.
To see what this pattern looks like, add this private class inside BooksAdapter
, directly below getView()
. Remember to paste inside the BooksAdapter
class brackets!
// Your "view holder" that holds references to each subview private class ViewHolder { private final TextView nameTextView; private final TextView authorTextView; private final ImageView imageViewCoverArt; private final ImageView imageViewFavorite; public ViewHolder(TextView nameTextView, TextView authorTextView, ImageView imageViewCoverArt, ImageView imageViewFavorite) { this.nameTextView = nameTextView; this.authorTextView = authorTextView; this.imageViewCoverArt = imageViewCoverArt; this.imageViewFavorite = imageViewFavorite; } } |
Your ViewHolder
is quite simple. It holds references to each subview and doesn’t bother with anything else.
To take advantage of your ViewHolder
, change getView()
to the following:
@Override public View getView(int position, View convertView, ViewGroup parent) { final Book book = books[position]; // view holder pattern if (convertView == null) { final LayoutInflater layoutInflater = LayoutInflater.from(mContext); convertView = layoutInflater.inflate(R.layout.linearlayout_book, null); final ImageView imageViewCoverArt = (ImageView)convertView.findViewById(R.id.imageview_cover_art); final TextView nameTextView = (TextView)convertView.findViewById(R.id.textview_book_name); final TextView authorTextView = (TextView)convertView.findViewById(R.id.textview_book_author); final ImageView imageViewFavorite = (ImageView)convertView.findViewById(R.id.imageview_favorite); final ViewHolder viewHolder = new ViewHolder(nameTextView, authorTextView, imageViewCoverArt, imageViewFavorite); convertView.setTag(viewHolder); } final ViewHolder viewHolder = (ViewHolder)convertView.getTag(); viewHolder.imageViewCoverArt.setImageResource(book.getImageResource()); viewHolder.nameTextView.setText(mContext.getString(book.getName())); viewHolder.authorTextView.setText(mContext.getString(book.getAuthor())); viewHolder.imageViewFavorite.setImageResource(book.getIsFavorite() ? R.drawable.star_enabled : R.drawable.star_disabled); return convertView; } |
The ViewHolder
is stuffed into convertView
‘s tag using setTag()
and retrieved using getTag()
. With your cells scrolling by, it helps them load quick quickly when they can skip over all the inflation, right to the getTag()
call and the fun part of inserting content.
Build and run. Your app will look the same, but underneath the hood it is now capable of handling much faster view recycling.
For additional documentation, visit Google’s page on smooth scrolling here.
What if your application requires loading external images inside each individual cell? This task generally requires subclassing AsyncTask
and firing off an image download task for each cell as the user scrolls down the list and back up.
Having implemented a custom solution for this in the past, I can tell you that it gets complicated. For example, you’d need to cache images in memory as each download is successfully completed, while making sure you’re not firing off multiple download tasks for the same image file download.
Fortunately, there is a fantastic library available from Square called Picasso that solves many of the common issues with list views and image downloads. To install the library, open up build.grade (Module: app) in your project and add the following line of code inside dependencies
:
dependencies { ... compile 'com.squareup.picasso:picasso:2.5.2' } |
Your build file should look like this:
Next, click the Sync Now link to install Picasso and update your project:
After successfully syncing your project, you can let Picasso do all the heavy lifting of downloading your images. To do this, comment out the line in BooksAdapter.getView()
where the cover art is set and include Picasso like this:
// make sure to comment out this image setter //viewHolder.imageViewCoverArt.setImageResource(book.getImageResource()); Picasso.with(mContext).load(book.getImageUrl()).into(viewHolder.imageViewCoverArt); |
Build and run, then scroll through the GridView to test the external image loading. Note that the images are still the same for each book, but they are coming from a remote URL now.
Congratulations! You’ve conquered the powerful GridView
component, created a library for your favorite kids’ books and learned how to do the following:
GridView
to your layouts.GridView
with the proper cell views.GridView
.You can download the completed project here. If you’re curious about what else you can do with GridView
s, check out Google’s documentation.
I hope you’ve enjoyed this introduction to Android’s GridView
. If you have any questions, comments or feedback please join the forum discussion below.
The post Android GridView Tutorial appeared first on Ray Wenderlich.
Over the past few months, we’ve been working hard on a secret book project.
Next Monday we’re going to reveal what it is. But first, you get a chance to guess!
What’s your best guess? Simply comment on this post and answer the following question:
To make it fun, we’ll give away a free copy of the book to two lucky winners:
That way, even if you’re not sure what the secret book will be, you can still make a creative stab at it in the comments.
Good luck to everyone — and don’t forget to check back here Monday to find out what the new book is!
The post A Secret Book Coming Next Monday! appeared first on Ray Wenderlich.
In this episode, you'll learn about the ternary operator as well as be introduced to nullable types.
The post Screencast: Beginning C# Part 9: Ternary Operator appeared first on Ray Wenderlich.
The Swift Algorithm Club is an open source project on implementing data structures and algorithms in Swift.
Every month, Chris Pilcher and I feature a cool data structure or algorithm from the club in a tutorial on this site. If you want to learn more about algorithms and data structures, follow along with us!
In this tutorial, you’ll learn how about binary trees and binary search trees. The binary tree implementation was first implemented by Matthijs Hollemans, and the binary search tree was first implemented by Nico Ameghino.
The Binary Tree is one of the most prevalent data structures in computer science. More advanced trees like the Red Black Tree and the AVL Tree evolved from the binary tree.
Binary trees themselves evolved from the general purpose tree. If you don’t know what that is, check out last month’s tutorial on Swift Tree Data Structure.
Let’s see how this works.
A binary tree is a tree where each node has 0, 1, or 2 children. The important bit is that 2 is the max – that’s why it’s binary.
Here’s what it looks like:
Before we dive into the code, it’s important that you understand some important terminology first.
On top of all the terms related to a general purpose tree, a binary tree adds the notion of left and right children.
The left child descends from the left side:
Surprisingly, the right side is the right child:
If a node doesn’t have any children, it’s called a leaf node:
The root is the node at the top of the tree (programmers like their trees upside down):
Like other trees, a binary tree composed of nodes. One way to represent a node is using a class (don’t enter this into a Playground yet, this is just an example):
class Node<T> { var value: T var leftChild: Node? var rightChild: Node? init(value: T) { self.value = value } } |
In a binary tree, every node holds some data (value
), and has a left and right child (leftChild
and rightChild
). In this implementation, the leftChild
and rightChild
are optionals, meaning they can be nil
.
That’s the traditional way to build trees. However, the thrill seeker you are shall rejoice today, because you’ll try something new! :]
One of the core ideas of Swift is using value types (like struct
and enum
) instead of reference types (like class
) where appropriate. Well, creating a binary tree is a perfect case to use a value type – so in this tutorial, you’ll you’ll implement the binary tree as an enum type.
Create a new Swift playground (this tutorial uses Xcode 8 beta 5) and add the following enum declaration:
enum BinaryTree<T> { } |
You’ve declared a enum named BinaryTree
. The
syntax declares this to be a generic enum that allows it to infer it’s own type information at the call site.
Enumerations are rigid, in that they can only be in one state or another. Fortunately, this fits into the idea of binary trees quite elegantly. A binary tree is a finite set of nodes that is either empty, or consists of the value at the node and references to it’s left and right children.
Update your enum accordingly:
enum BinaryTree<T> { case empty case node(BinaryTree, T, BinaryTree) } |
If you’re coming from another programming language, the node
case may seem a bit foreign. Swift enums allow for associated values, which is a fancy term for saying you can attach stored properties with a case.
In node(BinaryTree, T, BinaryTree)
, the parameter types inside the brackets correspond to the left child, value, and the right child, respectively.
That’s a fairly compact way of modelling a binary tree. However, you’re immediately greeted with a compiler error:
Recursive enum 'BinaryTree<T>' is not marked 'indirect' |
Xcode should make an offer to fix this for you. Accept it, and your enum should now look like this:
indirect enum BinaryTree<T> { case empty case node(BinaryTree, T, BinaryTree) } |
Enumerations in Swift are value types. When Swift tries to allocate memory for value types, it needs to know exactly how much memory it needs to allocate.
The enumeration you’ve defined is a recursive enum. That’s an enum that has an associated value that refers to itself. Recursive value types have a indeterminable size.
So you’ve got a problem here. Swift expects to know exactly how big the enum is, but the recursive enum you’ve created doesn’t expose that information.
Here’s where the indirect
keyword comes in. indirect
applies a layer of indirection between two value types. This introduces a thin layer of reference semantics to the value type.
The enum now holds references to it’s associated values, rather than their value. References have a constant size, so you no longer have the previous problem.
While the code now compiles, you can be a little bit more concise. Update BinaryTree
to the following:
enum BinaryTree<T> { case empty indirect case node(BinaryTree, T, BinaryTree) } |
Since only the node
case is recursive, you only need to apply indirect
to that case.
An interesting exercise to check out is to model a series of calculations using a binary tree. Take this for an example for modelling (5 * (a - 10)) + (-4 * (3 / b))
:
Write the following at the end of your playground file:
// leaf nodes let node5 = BinaryTree.node(.empty, "5", .empty) let nodeA = BinaryTree.node(.empty, "a", .empty) let node10 = BinaryTree.node(.empty, "10", .empty) let node4 = BinaryTree.node(.empty, "4", .empty) let node3 = BinaryTree.node(.empty, "3", .empty) let nodeB = BinaryTree.node(.empty, "b", .empty) // intermediate nodes on the left let Aminus10 = BinaryTree.node(nodeA, "-", node10) let timesLeft = BinaryTree.node(node5, "*", Aminus10) // intermediate nodes on the right let minus4 = BinaryTree.node(.empty, "-", node4) let divide3andB = BinaryTree.node(node3, "/", nodeB) let timesRight = BinaryTree.node(minus4, "*", divide3andB) // root node let tree = BinaryTree.node(timesLeft, "+", timesRight) |
You need to build up the tree in reverse, starting with the leaf nodes and working your way up to the top.
Verifying a tree structure can be hard without any console logging. Swift has a handy protocol named CustomStringConvertible
, which allows you define a custom output for print
statements. Add the following code just below your BinaryTree
enum:
extension BinaryTree: CustomStringConvertible { var description: String { switch self { case let .node(left, value, right): return "value: \(value), left = [" + left.description + "], right = [" + right.description + "]" case .empty: return "" } } } |
Print the tree by writing the following at the end of the file:
print(tree) |
You should see something like this:
value: +, left = [value: *, left = [value: 5, left = [], right = []], right = [value: -, left = [value: a, left = [], right = []], right = [value: 10, left = [], right = []]]], right = [value: *, left = [value: -, left = [], right = [value: 4, left = [], right = []]], right = [value: /, left = [value: 3, left = [], right = []], right = [value: b, left = [], right = []]]] |
With a bit of imagination, you can see the tree structure. ;-) It helps if you indent it:
value: +, left = [value: *, left = [value: 5, left = [], right = []], right = [value: -, left = [value: a, left = [], right = []], right = [value: 10, left = [], right = []]]], right = [value: *, left = [value: -, left = [], right = [value: 4, left = [], right = []]], right = [value: /, left = [value: 3, left = [], right = []], right = [value: b, left = [], right = []]]] |
Another useful feature is being able to get the number of nodes in the tree. Add the following just inside your BinaryTree
enumeration:
var count: Int { switch self { case let .node(left, _, right): return left.count + 1 + right.count case .empty: return 0 } } |
Test it out by adding this to the end of your playground:
tree.count |
You should see the number 12 in the sidebar, since there are 12 nodes in the tree.
Great job making it this far. Now that you’ve got a good foundation for binary trees, it’s time to get acquainted with the most popular tree by far – the Binary Search Tree!
A binary search tree is a special kind of binary tree (a tree in which each node has at most two children) that performs insertions and deletions such that the tree is always sorted.
Here is an example of a valid binary search tree:
Notice how each left child is smaller than its parent node, and each right child is greater than its parent node. This is the key feature of a binary search tree.
For example, 2 is smaller than 7 so it goes on the left; 5 is greater than 2 so it goes on the right.
When performing an insertion, starting with the root node as the current node:
You traverse your way down the tree until you find an empty spot where you can insert the new value.
For example, imagine you want to insert the value 9 to the above tree:
The new tree now looks like this:
Here’s another example. Imagine you want to insert 3 to the above tree:
The new tree now looks like this:
There is always only one possible place where the new element can be inserted in the tree. Finding this place is usually pretty quick. It takes O(h) time, where h is the height of the tree.
Now that you’ve got an idea of how insertion works, it’s implementation time. Add the following method to your BinaryTree
enum:
// 1. mutating func naiveInsert(newValue: T) { // 2. guard case .node(var left, let value, var right) = self else { // 3. self = .node(.empty, newValue, .empty) return } // 4. TODO: Implement rest of algorithm! } |
Let’s go over this section by section:
mutating
keyword in front of your method.guard
statement to expose the left child, current value, and right child of the current node. If this node is empty
, then guard
will fail into it’s else
block.self
is empty
. You’ll insert the new value here.In a moment, you will try to implement section 4 based on the algorithm discussed above. This is a great exercise not only for understanding binary search trees, but also honing your recursion skills.
But before you do, you need to make a change to the BinaryTree
signature. In section 4, you’ll need to compare whether the new value with the old value, but you can’t do this with the current implementation of the binary tree. To fix this, update the BinaryTree
enum to the following:
enum BinaryTree<T: Comparable> { // stuff inside unchanged } |
The Comparable
protocol enforces a guarantee that the type you’re using to build the binary tree can be compared using the comparison operators, such as the <
operator.
Now, go ahead and try to implement section #4 based on the algorithm above. Here it is again for your reference:
If you get stuck, you can check the solution below.
Solution Inside: Solution | SelectShow> | |
---|---|---|
|
Though this is a great implementation, it won't work. Test this by writing the following at the end of your playground:
var binaryTree: BinaryTree<Int> = .empty binaryTree.naiveInsert(newValue: 5) // binaryTree now has a node value with 5 binaryTree.naiveInsert(newValue: 7) // binaryTree is unchanged binaryTree.naiveInsert(newValue: 9) // binaryTree is unchanged |
Copy-on-write is the culprit here. Every time you try to mutate the tree, a new copy of the child is created. This new copy is not linked with your old copy, so your initial binary tree will never be updated with the new value.
This calls for a different way to do things. Write the following at the end of the BinaryTree
enum:
private func newTreeWithInsertedValue(newValue: T) -> BinaryTree { switch self { // 1 case .empty: return .node(.empty, newValue, .empty) // 2 case let .node(left, value, right): if newValue < value { return .node(left.newTreeWithInsertedValue(newValue: newValue), value, right) } else { return .node(left, value, right.newTreeWithInsertedValue(newValue: newValue)) } } } |
This is a method that returns a new tree with the inserted element. The code is relatively straightforward:
Write the following method inside your BinaryTree
enum:
mutating func insert(newValue: T) { self = newTreeWithInsertedValue(newValue: newValue) } |
Test your code by replacing the test lines at the bottom of your playground:
binaryTree.insert(newValue: 5) binaryTree.insert(newValue: 7) binaryTree.insert(newValue: 9) |
You should end up with the following tree structure:
value: 5, left = [], right = [value: 7, left = [], right = [value: 9, left = [], right = []]] |
Congratulations - now you've got insertion working!
As discussed in the spoiler section, you need to create a new copy of the tree every time you make an insertion. Creating a new copy requires going through all the nodes of the previous tree. This gives the insertion method a time complexity of O(n).
Traversal algorithms are fundamental to tree related operations. A traversal algorithm goes through all the nodes in a tree. There are three main ways to traverse a binary tree:
In-order traversal of a binary search tree is to go through the nodes in ascending order. Here's what it looks like to perform an in-order traversal:
Starting from the top, you head to the left as much as you can. If you can't go left anymore, you'll visit the current node and attempt to traverse to the right side. This procedure continues until you traverse through all the nodes.
Write the following inside your BinaryTree
enum:
func traverseInOrder(process: @noescape (T) -> ()) { switch self { // 1 case .empty: return // 2 case let .node(left, value, right): left.traverseInOrder(process: process) process(value) right.traverseInOrder(process: process) } } |
This code is fairly straightforward:
To see this in action, you'll create the binary tree shown above. Delete all the test code at the bottom of your playground and replace it with the following:
var tree: BinaryTree<Int> = .empty tree.insert(newValue: 7) tree.insert(newValue: 10) tree.insert(newValue: 2) tree.insert(newValue: 1) tree.insert(newValue: 5) tree.insert(newValue: 9) tree.traverseInOrder { print($0) } |
You've created a binary search tree using your insert method. traverseInOrder
will go through your nodes in ascending order, passing the value in each node to the trailing closure.
Inside the trailing closure, you're printing the value that was passed in by the traversal method. $0
is a shorthand syntax that references the parameter that is passed in to the closure.
You should see the following output in your console:
1 2 5 7 9 10 |
Pre-order traversal of a binary search tree is to go through the nodes whilst visiting the current node first. The key here is calling process
before traversing through the children. Write the following inside your BinaryTree
enum:
func traversePreOrder( process: @noescape (T) -> ()) { switch self { case .empty: return case let .node(left, value, right): process(value) left.traversePreOrder(process: process) right.traversePreOrder(process: process) } } |
Post-order traversal of a binary search tree is to visit the nodes only after traversing through it's left and right children. Write the following inside your BinaryTree
enum:
func traversePostOrder( process: @noescape (T) -> ()) { switch self { case .empty: return case let .node(left, value, right): left.traversePostOrder(process: process) right.traversePostOrder(process: process) process(value) } } |
These 3 traversal algorithms serve as a basis for many complex programming problems. Understanding them will prove useful for many situations, including your next programming interview!
What is the time complexity of the traversal algorithms?
Solution Inside: Solution | SelectShow> |
---|---|
The time complexity is O(n), where n is the number of nodes in the tree. This should be obvious, since the idea of traversing a tree is to go through all the nodes! |
As the name suggests, a binary search tree is known best for facilitating efficient searching. A proper binary search tree will have all it's left child less than it's parent node, and all it's right children equal or greater than it's parent node.
By exploiting this guarantee, you'll be able to determine which route to take - the left child, or the right child - to see if your value exists within the tree. Write the following inside your BinaryTree
enum:
func search(searchValue: T) -> BinaryTree? { switch self { case .empty: return nil case let .node(left, value, right): // 1 if searchValue == value { return self } // 2 if searchValue < value { return left.search(searchValue: searchValue) } else { return right.search(searchValue: searchValue) } } } |
Much like the traversal algorithms, searching involves traversing down the binary tree:
Unlike the traversal algorithms, the search algorithm will traverse only 1 side at every recursive step. On average, this leads to a time complexity of O(log n), which is considerably faster than the O(n) traversal.
You can test this by adding the following to the end of your playground:
tree.search(searchValue: 5) |
I hope you enjoyed this tutorial on making a Swift Binary Tree data structure!
Here is a Swift playground with the above code. You can also find alternative implementations and further discussion in the Binary Search Tree section of the Swift Algorithm Club repository.
This was just one of the many algorithm clubs focused on the Swift Algorithm Club repository. If you're interested in more, check out the repo.
It's in your best interest to know about algorithms and data structures - they're solutions to many real world problems, and are frequently asked as interview questions. Plus it's fun!
So stay tuned for many more tutorials from the Swift Algorithm club in the future. In the meantime, if you have any questions on implementing trees in Swift, please join the forum discussion below!
The post Swift Algorithm Club: Swift Binary Search Tree Data Structure appeared first on Ray Wenderlich.
Xcode 8 allows you to create plugins to help you work with source code. Discover how in this iOS 10 screencast.
The post iOS 10 Screencast: Creating an Xcode 8 Extension appeared first on Ray Wenderlich.
Learn how to create a CocoaPod. Enhance discoverability of your framework and automate installation with the popular CocoaPods dependency manager.
The post Screencast: Creating a CocoaPod appeared first on Ray Wenderlich.
For the past six months, we’ve been working on a top-secret book project: Unity Games by Tutorials.
Today, we are happy to announce that the first 8 chapters of the book are available now. These chapters teach you how to make a twin-stick shooter called Bobblehead Wars. See for yourself below:
Trust me, all the games are as good at this! :]
To celebrate the early access release, we’ll be posting a few free chapters from the book, announcing the winners from last week’s “secret announcement” post, and giving away some free copies of the book.
Along with all that, we’re also giving everyone who orders Unity Games by Tutorials now an early access discount.
Read on for all the details!
Unity is a a professional game engine used to create games like City Skylines, Hearthstone, the Long Dark, and more.
Unity’s aim is to “democratize” game development, by providing a AAA-level engine to independent game developers in a way that is both affordable and accessible.
Here are our top 5 reasons why Unity is great:
Here’s our recommendation:
Unity Games by Tutorials is for complete beginners to Unity, or for those who’d like to bring their Unity skills to a professional level. Its goal is to teach you everything you need to know to make your own Unity games – via hands-on experience.
In Unity Games by Tutorials, you’ll learn how to build four games:
Here’s a sneak peek of what you can look forward to in the full release:
This section covers everything you need to know to get started with Unity. You’ll learn your way around the UI, how to work with game assets and physics, and create a 3D twin-stick combat game: Bobblehead Wars.
Here’s what you’ll cover while saving the world from creepy-crawly aliens:
Now that you’re up to speed with Unity game development, you can move on to more complex game development topics, such as adding in-game UI elements, advanced camera techniques and using multiple weapons.
In this section, you’ll build a fast-paced first-person shooter: Robot Rampage.
Here’s a brief outline of what this section has in store:
3D games are undeniably awesome, but you just can’t beat a classic 2D platformer game.
In this section, you’ll build a game to test your reflexes as you help your hero battle his way to a well-deserved lunch in Super Soy Boy:
Here’s the chapters and topics you’ll cover while helping Super Soy Boy avoid those terrifying buzzsaws:
Combining strategy and action results in compelling games that are easy to pick up — and hard to put down.
In this section, you’ll create a 3D tower defense game — Runestrife — with such beautifully-rendered enemies it almost seems a shame to shoot them:
When you’re not busy defending your towers from advancing waves of enemies, here’s what you’ll learn:
The book also will also include a few bonus chapters beyond this – but our lips are sealed! :]
To help celebrate the early access release, each day this week we’ll do something special:
Giveaway details: Three lucky readers will win a free PDF copy of Unity Games by Tutorials (or any one of our books, if you’ve already purchased Unity Games by Tutorials).
To enter into the giveaway, simply leave a comment on this post answering the following question:
Last week, we gave everyone a chance to guess the topic of this secret book for a chance to win a free copy, and today we’re happy to announce the winners:
Both readers earned a free copy of Unity Games by Tutorials. Thanks to everyone for your creative guesses!
There’s one final piece of good news.
To celebrate the early access release of Unity Games by Tutorials, you can order the book and receive an automatic $10 early access discount.
To take advantage of the special discount and get the first early access release, order your copy now.
The Unity team and I hope you enjoy the book, and we can’t wait to see your future games!
The post Introducing Unity Games by Tutorials! appeared first on Ray Wenderlich.
Many of the biggest tech firms in the world are investing heavily in virtual reality technologies: Facebook spent $2 billion to acquire Oculus Rift; Disney invested $65 million into a VR film company called Jaunt; Microsoft introduced its HoloLens this year and is selling the device to developers for $3000 apiece.
Apple spent $32 million to acquire the engineers from Metaio and $345 million to acquire PrimeSense, two companies of VR innovators, and pre-orders for Sony’s Playstation VR sold out even quicker than they’d expected, proving there’s a strong consumer market for VR.
Of all the exciting new VR technology being introduced, however, Google Cardboard VR has made virtual reality most accessible to the hobbyist. In fact, with just a low-cost Google Cardboard VR headset, a smartphone and your iOS skills, you can go farther than you ever thought possible.
In this intro to Google Cardboard VR tutorial, fellow jetsetters, you’ll embark on a worldwide 360° vacation by clicking through multiple 360° vacation images and pausing/playing your way through a 360° vacation video.
Note: This Google Cardboard VR tutorial assumes you know the basics of iOS and Swift development. If you’re new to iOS development and Swift, check out our “Learn to Code iOS Apps with Swift Tutorial” series first.
Download the starter project and open Vacation 360.xcodeproj in Xcode. In the 360 images folder in the Navigation Bar on the left-hand side of your Xcode console, you’ll see three 360° panorama images you’ll eventually display in the app. In Main.storyboard
, you’ll find a VacationViewController
containing a few labels, two empty UIView
s and some constraints.
The interface may not look like much yet, but you’ll eventually change these two UIView
s into Google Cardboard photo and video VR views. Build and run your app; you won’t see much except for the labels describing the behaviors you’ll implement in this Google Cardboard VR tutorial.
Before you start coding your Google Cardboard VR app, the first step is to install the Google Cardboard VR SDK with CocoaPods. Or rather, if you’ve never used CocoaPods before, the first step is to install CocoaPods and the second is to install Google Cardboard VR using CocoaPods.
As described in Joshua Green’s great tutorial on How to Use CocoaPods with Swift, install CocoaPods and the Google VR SDK using the following steps.
Open Terminal, then execute the following command:
sudo gem install cocoapods |
Enter your computer’s password when requested. To install the Google VR SDK in the project, navigate to the Vacation_360 starter project folder by using the cd command:
cd ~/ComputerLocation/Vacation_360 |
Next, create a Podfile for your project:
pod init |
Then open the Vacation_360 folder, open the Podfile with a text editor and replace all of its current text with the following:
target "Vacation 360" do pod 'GVRSDK' end |
This tells CocoaPods that you want to include the GVRSDK, i.e. the Google VR SDK, as a dependency for your project. Save and close Podfile. Then in Terminal, in the same directory to which you navigated earlier, enter:
pod install |
That’s it! You’ve installed Google’s VR SDK. As the log output states, “Please close any current Xcode sessions and use `Vacation 360.xcworkspace` for this project from now on.”
As with any app in which you install CocoaPods, you’ll be working from the .xcworkspace file instead of the .xcodeproj from this point forward.
Because the Google VR SDK is an Objective-C framework, you’ll have to use an Objective-C bridging header to access it from your Swift code. Go to File\New\File…, select iOS\Source\Header File and then click Next. Name it Vacation 360-Bridging-Header, select the Vacation 360 folder for the Group, then hit Create.
Next, select the Vacation 360 project in the Project Navigator, then select Vacation 360 under TARGETS. Go to Build Settings, look for Objective-C Bridging Header under the Swift Compiler – Code Generation section and enter “Vacation 360/Vacation 360-Bridging-Header.h”.
As you might have guessed, this tells the compiler where to look for your bridging header.
Replace the contents of Vacation 360-Bridging-Header.h with the following:
#import "GVRPanoramaView.h" #import "GVRWidgetView.h" #import "GVRVideoView.h" |
Now you have access to the three Google Cardboard VR SDK classes you’ll be using in this tutorial.
Start packing those bags; Vacation 360, here we come! :]
The Google Cardboard VR SDK has three VR view types: GVRCardboardView
, GVRPanoramaView
, and GVRVideoView
. GVRCardboardView
is by far the most powerful of the three, since in VR mode it lets you determine the user’s head and eye positions, layout 3D audio, and dynamically alter the landscape. Unfortunately, GVRCardboardView
requires complex OpenGL rendering beyond the scope of this tutorial; but GVRPanoramaView
, Google VR’s image viewer, and GVRVideoView
, Google VR’s video viewer, are more than adequate vessels for an international adventure.
Before you start diving into the Google VR methods, you’ll have to lay some groundwork. First, you’ll connect and load the Google Cardboard 360° panoramic photo and video views.
Navigate to Main.storyboard
and select the transparent UIView
directly under the Click through Sindhu Beach, Grand Canyon, & Underwater Photos label. In the Utilities menu on the right hand side of Xcode, navigate to the Identity Inspector, then in the Custom Class section, enter “GVRPanoramaView” in the Class field. Similarly, select the UIView
directly below Play/Pause “Living with Elephants” Safari Video by Photos of Africa and enter “GVRVideoView” in the Class field.
Using the Assistant Editor, control-drag from these two views in Interface Builder to the top of the VacationViewController
class in VacationViewController.swift and create Outlets. For GVRPanoramaView
, use the name imageVRView
and for GVRVideoView
use videoVRView
. Similarly connect the label outlets to the top of VacationViewController
. Name the top label imageLabel
and the bottom label videoLabel
.
Next, make the media file URLs accessible by defining them in an enumeration. Directly under the IBOutlet
s, define enum Media
as follows:
enum Media { static var photoArray = ["sindhu_beach.jpg", "grand_canyon.jpg", "underwater.jpg"] static let videoURL = "https://s3.amazonaws.com/ray.wenderlich/elephant_safari.mp4" } |
In this enumeration, photoArray
holds the file names of the 360° images stored in the bundle and videoUrl
holds the URL of the 360 video you’ll display. The photoArray
is variable and not constant so you can alter the array to easily cycle through the images later on.
Set the initial photo and video by adding the following in viewDidLoad()
just below super.viewDidLoad()
:
imageVRView.loadImage(UIImage(named: Media.photoArray.first!), ofType: GVRPanoramaImageType.Mono) videoVRView.loadFromUrl(NSURL(string: Media.videoURL)) |
You load imageVRView
‘s UIImage
with Media.photoArray.first!
while passing the type GVRPanoramaImageType.Mono
to indicate that the image is monoscopic.
Then you load videoVRView
with an NSURL created from the Media.videoURL
string.
There are two format types for 360° images and videos: stereoscopic and monoscopic – stereo and mono for short. Although both formats render 360° media by converting a rectangular panorama into a spherical layout, the stereoscopic format adds some depth to the viewing experience by including two slightly offset images or videos, one stacked above the other, that mimic the viewer’s perspective.
Monoscopic media features the image or video from a single point of view. Google Cardboard VR supports both formats, but displays either format as stereoscopic. When viewing either format in fullscreen VR mode, the viewer shows two side-by-side images or videos slightly offset from one another. You can find the ideal specifications for both mono and stereo here.
Build and run the app without your Google Cardboard VR headset to see what these views provide with minimal configuration. You’ll see the 360° image in the top view and, as long as you’re connected to the internet and thus able to load from the web URL, you’ll see the 360° video in the bottom view.
Rotating the device will rotate both embedded views. Tap the info symbol at the bottom left corner of either VR view and you’ll be directed to a Google Cardboard VR help webpage. Tap elsewhere on the VR views, and nothing will happen.
In order to enable the fullscreen mode and fullscreen VR mode, you’ll need to enable the corresponding buttons on the GVRPanoramaView
and GVRVideoView
. Add the following below the line in viewDidLoad()
where you load imageVRView
‘s image:
imageVRView.enableCardboardButton = true imageVRView.enableFullscreenButton = true |
Next, add the following below the line where you load videoVRView
‘s video:
videoVRView.enableCardboardButton = true videoVRView.enableFullscreenButton = true |
Build and run again; two new buttons should appear on the bottom right of each VR view. Tap the outer right button, i.e. the Fullscreen Button, on either VR view, and the view will resize to fit the full screen. While in full screen, tap the back button on the top left corner to return to the main view.
Next tap the inner right button, i.e. the Cardboard Button, and a view should appear with instructions on how to connect your viewer. To view the media in fullscreen VR mode, insert your device into your Google Cardboard VR headset as instructed such that the top of the device is on the left side of the viewer and the bottom is on the right:
To get the full immersive VR experience from this tutorial, I suggest you use a Google Cardboard compatible device. You can purchase a headset on the Google Cardboard website for as little as $14.95.
Or you can even make your own! To do this, just sit back and order a pizza. … What? You’re still VR-less 30 minutes later? Oh right – you also have to cut up that box, add some lenses, a magnet, velcro, and a rubber band to MacGyver your way into your worldwide vacation. The do-it-yourself instructions are available at that same Google Cardboard website link towards the bottom of the page.
But if you’re content with viewing a 360° panorama from your device without being fully immersed, this tutorial will still work for you without a viewer.
In Vacation 360, the user can click through images while viewing imageVRView
and can play and pause video while viewing videoVRView
. The backing classes for each of these inherit from GVRWidgetView
, which implements the GVRWidgetViewDelegate
protocol. This protocol lets GVRWidgetView
notify its delegate of various state changes and interactions, which will come in handy while customizing the VR view behaviors.
Directly underneath the closing bracket of the VacationViewController
class, add the following:
extension VacationViewController: GVRWidgetViewDelegate { func widgetView(widgetView: GVRWidgetView!, didLoadContent content: AnyObject!) { } func widgetView(widgetView: GVRWidgetView!, didFailToLoadContent content: AnyObject!, withErrorMessage errorMessage: String!) { } func widgetView(widgetView: GVRWidgetView!, didChangeDisplayMode displayMode: GVRWidgetDisplayMode) { } func widgetViewDidTap(widgetView: GVRWidgetView!) { } } |
You’ve implemented the GVRWidgetViewDelegate
and all of its methods in an extension. It’s time to fill in each method one by one.
widgetView(_:didLoadContent:)
is called when a VR view has loaded its content successfully. What’s that you say? How about using this method to only reveal elements of the view once they have loaded? Sounds like a great idea! :]
First, head back to viewDidLoad()
and add the following just below super.viewDidLoad()
:
imageLabel.hidden = true imageVRView.hidden = true videoLabel.hidden = true videoVRView.hidden = true |
This will hide all of the labels and VR views initially, so that they won’t appear until the content loads.
Now, grouped with the other imageVRView
code in viewDidLoad()
, add the following:
imageVRView.delegate = self |
Similarly, add the following with the videoVRView
code:
videoVRView.delegate = self |
This sets both view’s GVRWidgetViewDelegate
s to the VacationViewController
.
You can now unhide the labels and views once their corresponding content has loaded. Back in widgetView(_:didLoadContent:)
, add the following:
if content is UIImage { imageVRView.hidden = false imageLabel.hidden = false } else if content is NSURL { videoVRView.hidden = false videoLabel.hidden = false } |
widgetView(_:didLoadContent:)
is passed the loaded content
object – in this case a UIImage
for GVRPanoramaView
s and an NSURL
for GVRVideoView
s. For the UIImage
, unhide the imageVRView
and its corresponding label. For an NSURL
, unhide the videoVRView
and its corresponding label.
Build and run; you’ll see that each VR view and label now only appears when its corresponding content has loaded:
Add the following to widgetView(_:didFailToLoadContent:withErrorMessage:)
:
print(errorMessage) |
This is called when there’s an issue loading the content. In that case, you simply print the passed errorMessage
.
Next you’ll use widgetView(_:didChangeDisplayMode:)
to store the current display mode and selected view, so that tapping on the widgets will trigger the appropriate actions only when a VR view is in fullscreen or VR mode. This method is called each time you switch between embedded, fullscreen, or VR mode and passes the involved widgetView
.
First, in the main VacationViewController
class below the enum Media
declaration block, add the following:
var currentView: UIView? var currentDisplayMode = GVRWidgetDisplayMode.Embedded |
currentView
will maintain a reference to the view currently displayed in fullscreen or VR mode. currentDisplayMode
is used to hold the current display mode. It is initialized to GVRWidgetDisplayMode.Embedded
which represents the initial display mode when the VR views are embedded in the VacationViewController
.
Then in widgetView(_:didChangeDisplayMode:)
, add the following:
currentView = widgetView currentDisplayMode = displayMode |
widgetView(_:didChangeDisplayMode:)
passes the new displayMode
as well as the widgetView
where the mode was changed. These are stored in currentDisplayMode
and currentView
, respectively, for later reference.
Knowing the display mode and displayed view, you can finally start to implement the interactive behaviors while in fullscreen or VR mode. Tapping the magnetic widget button on Cardboard or directly tapping the screen should cycle through the photos in Media.photoArray
when the imageVRView
is in fullscreen or VR mode. To implement this behavior, add the following to widgetViewDidTap(_:)
:
// 1 guard currentDisplayMode != GVRWidgetDisplayMode.Embedded else {return} // 2 if currentView == imageVRView { Media.photoArray.append(Media.photoArray.removeFirst()) imageVRView?.loadImage(UIImage(named: Media.photoArray.first!), ofType: GVRPanoramaImageType.Mono) } |
currentDisplayMode
of .Embedded
triggers an early return.currentView
is imageVRView
, advance the Media.photoArray
queue by removing the first element of the array and appending it to the end. Since Media.photoArray.removeFirst()
returns the removed element, you can remove the first element and append it to the end in a single line of code. Then you load imageVRView
‘s UIImage
using the file now in the first index of the photo array.Build and run your project; open the image VR view into either fullscreen or VR fullscreen, then tap either the device screen or the widget button respectively. You should be able to click through the images.
You may notice one minor issue: the other elements of the view show through between each click. To prevent that, hide the elements whenever you’re not viewing the embedded display mode by adding the following to the end of widgetView(_:didChangeDisplayMode:)
:
if currentView == imageVRView && currentDisplayMode != GVRWidgetDisplayMode.Embedded { view.hidden = true } else { view.hidden = false } |
If the currentView
is the imageVRView
and the currentDisplayMode
isn’t embedded, this means you’re viewing the image view in fullscreen or VR mode. In that case, hide the view controller’s root view; otherwise, unhide it.
Build and run; click through the images again, and the transition between images should be almost seamless:
Note: Even after setting view.hidden
to true, the GVRPanoramaView
stays unhidden because it and the GVRVideoView
are not subviews of the VacationViewController
‘s view when in fullscreen or VR mode.
Congrats on successfully creating a 360 image slideshow!
But don’t get too lost in your vacation yet. The interactions are only halfway done!
You still have to implement the video view interaction which will let you pause and play the “Living with Elephants” video by Photos of Africa so that each moment of your vacation can last even longer.
Back in the main VacationViewController
class, add the following new variable underneath the var currentDisplayMode
declaration:
var isPaused = true |
There is no property inherent to the GVRVideoView
that indicates whether the video is paused or playing, so the variable you’ve just created can serve as a state marker.
Also create a method to set the initial play state behavior dependent on the display mode. Underneath viewDidLoad()
add:
func refreshVideoPlayStatus() { // 1 if currentView == videoVRView && currentDisplayMode != GVRWidgetDisplayMode.Embedded { videoVRView?.resume() isPaused = false } // 2 else { videoVRView?.pause() isPaused = true } } |
videoVRView
is in fullscreen or VR mode, calling this method plays the video and appropriately sets isPaused
to false
.videoVRView
‘s display mode is in its embedded state or the imageVRView
is in fullscreen, calling this method pauses the video and appropriately sets isPaused
to true
.Then within widgetView(_:didLoadContent:)
‘s else if
block, add:
refreshVideoPlayStatus() |
This pauses the video as soon as it loads.
Now in widgetView(_:didChangeDisplayMode:)
, just before the if
block and after setting currentDisplayMode
, insert the following:
refreshVideoPlayStatus() |
By calling refreshVideoPlayStatus()
whenever the display mode changes, the video will play when entering fullscreen or VR mode, and pause otherwise.
Now that you’ve set videoVRView
‘s initial display mode behaviors, you can implement a play/pause toggle triggered by user interaction. In widgetViewDidTap(_:)
, after the if block, add:
else { if isPaused { videoVRView?.resume() } else { videoVRView?.pause() } isPaused = !isPaused } |
You hit this else
clause when you tap a videoVRView
. If the video is paused, resume playing; if the video is playing, pause it. Then you update isPaused
to reflect the new play state.
Build and run your app, open the video into fullscreen or VR mode, then tap the screen or widget button and the video should toggle between play and pause states:
You don’t want your vacation to stop after a few minutes though, do you? Of course not! So therefore you’ll loop the video to the beginning once it reaches the end.
Add a GVRVideoViewDelegate
extension below the final bracket of the GVRWidgetViewDelegate
extension to implement the looping behavior:
extension VacationViewController: GVRVideoViewDelegate { func videoView(videoView: GVRVideoView!, didUpdatePosition position: NSTimeInterval) { if position >= videoView.duration() { videoView.seekTo(0) videoView.resume() } } } |
videoView(_:didUpdatePosition:)
is called at approximately one-second intervals as the video plays. Once the NSTimeInterval
position
of the video is greater than or equal to the duration
of the video, the video has reached its end. videoView.seekTo(0)
then sets the position back to the beginning before resuming the video.
Note: There is no need to set VacationViewController
as the GVRVideoViewDelegate
because…it’s already set! GVRVideoViewDelegate
inherits from GVRWidgetViewDelegate
, which you’ve already adopted and set up in viewDidLoad()
of VacationViewController
.
There you have it! Now you can build, run, sit back and enjoy the worldwide vacation you’ve just created with your bare hands. Isn’t iOS development spectacular?
Download the finished project here, and check out Google’s sample projects here.
The Google Cardboard mobile SDK had originally only been available for Android devices until Google graciously opened up its Cardboard SDK to iOS earlier this year. Google VR for iOS is still very new and will evolve over time, so keep an eye on raywenderlich.com for Google Cardboard VR tutorials using the enthralling new features Google will undoubtedly roll out in the near future.
Have comments or questions? Please join the forum discussion below!
The post Introduction to Google Cardboard for iOS appeared first on Ray Wenderlich.
Note from Ray: At our recent RWDevCon tutorial conference, in addition to hands-on tutorials, we also had a number of “inspiration talks” – non-technical talks with the goal of giving you a new idea, some battle-won advice, and leaving you excited and energized.
We recorded these talks so that you can enjoy them, even if you didn’t get to attend the conference. Here’s our next talk – Embracing Failure by Janie Clayton – I hope you enjoy!
Hi, everybody. I am Janie Clayton and I’m going to talk about something that a lot of us really don’t like to think about. I’m going to talk about how to embrace failure.
Back when I was a little kid, I started playing the piano and I was really good at it immediately. I was the best person in my class of like 60 people and my town of 2000 people. I could play anything I wanted without having to practice too much.
So I thought that I was like a total prodigy genius person. I thought that when I grew up I was going to be a concert pianist and I was going to go to Juilliard and perform in front of all of these giant multitudes of people. This was what I was going to do with my life.
Well, then something unfortunate happened. I went to a different school where I met other people who’d actually been playing the piano longer than I had and who were a lot better than I was. I would talk and I’m like, “Holy crap. You guys are amazing.” They were like, “No, no, no. We’re not. We’re actually not all that great.”
I was really disappointed because I thought that this was something that I was going to do with my life. Finding out that I wasn’t the greatest piano player in the world, that this wasn’t what I was going to do, I got really discouraged and I gave up on it because I’d never practiced before and I never had to practice. I didn’t have any discipline to go in and actually do things to make myself better.
I just assumed that you’re either born good at something, or you weren’t good at it immediately and it just wasn’t something that you were supposed to be doing.
I’ve noticed over the last couple of years, I’ve been really thinking about how we go about what we do with our lives. When you’re a kid and later in high school, you’re trying to figure out who you are. When you’re a kid you think you’re going to be an astronaut or you think you’re going to be president of the United States.
You have all of these ideas about what you’re going to do and we have all of these high stakes things where you have to decide immediately when you’re a little kid about what you’re going to do so that you can take all the right classes in high school and later go on to college.
Then you get into high school and you don’t really have time to go and pick up any of these cool, neat hobbies that you have. You have to specialize in what it is that you’re going to do. There’s a lot of pressure on you, when you’re a high school student, to go and actually continue to make straight A’s. If you don’t get an A, if you get a B on a test, then your GPA goes down. Then you can’t get into Harvard and everything is ruined.
This is pressure that we’re putting on these poor 16 year-old kids to tell them that they can’t make any mistakes because if they make one mistake, their entire future is completely screwed. It’s really hard to get yourself out of that mindset even after you get out of high school.
We as a society are terrified of failure. We have this idea that everything that we do has to lead to something else. Like I said:
You figure out when you’re like 40 that you have this life that you have to keep living, that you didn’t really want because you were so afraid of acknowledging that you didn’t like something. You weren’t willing to go in and make changes when it was possible for you to actually do so.
This leads to a lot of really bad, toxic mindsets and behaviors because everybody’s completely absorbed with trying to maintain this façade that we’re all perfect, that we don’t make any mistakes, that we don’t have failures. Nobody is willing to ask for help; everybody has to pretend like everything that they do is perfect. You get people threatening to kill each over on Stack Overflow because they’re putting their semicolons on the wrong lines.
This is just a really bad way of going about living, so I just wanted to talk a little bit about my own experiences of failing upwards through life over the last 20 years or so. I want to give people an idea about how we can break out of this cycle and embrace failure so that we can learn and be more happy and productive people.
One of the first things I want to talk about is the cycle of learning. Back when I was trying to play the piano and I figured out that I wasn’t actually all that good at it, I quit on it because it was something that I didn’t think I could better at by training.
The Viking Code School blog had this great thing about why programming is hard and it was talking about this cycle of coding competence versus confidence.
I’m going to talk about my own experiences with programming, even though this is applicable to basically anything that you’d want to be doing.
When you first start out, you have this nice hand-holding honeymoon stage. When I learned programming, I learned on Codecademy, where you’d go and you would for-loop. Then you get a nice little, “Hey, you did a great job! Here’s a cookie!”
You feel really good and awesome. It’s cool, because you went and you did something successful and everything was broken down into nice happy, little competent tasks that you could do modularly and you feel really good.
Then you get done with that and then you wind up on the Cliffs of Confusion. You get done with programming and you go and find your first job or you find other people. Then all of a sudden, you hear all of this stuff that nobody talked about when you were learning how to program because it was such an overwhelming thing that there wasn’t any time to go over it.
You go to a conference and you hear people talk about the Gang of Four and you’re like, “What’s the Gang of Four?” Or people telling you that you need to do things on GitHub or contribute to open source and you’re asking, “How do you contribute to open source?” and then they give you a look like, “Oh, you’re one of those people.”
You’re too afraid to ask about it ever again and then everybody thinks poorly of you. So you’re spending all of this time bouncing around, trying to figure out all of these institutional knowledge that it seems like everybody else has that you didn’t have.
It gets very confusing and then eventually that leads to the Desert of Despair. You feel like you’re never going to catch up. That everybody knows all of this stuff. They were born with this knowledge. It wasn’t that they learned it over a course of time. You feel like you’re a failure and there’s nothing you can do. You get on your first project and you don’t have all of these nice little modular tasks like you did on Codecademy.
You have to figure out what you’re supposed to be doing. You’re talking to a client that doesn’t know what they want to be doing. It leads to this really powerful pit of despair where you just feel like you’re a failure and you don’t think you’re going to be able to do anything. You feel like you have to keep working harder and harder and harder to keep up.
Then everybody else around you is also trying to work harder and harder and harder to keep up. It leads to this horrible endless cycle of everybody trying to pretend like we all know what we’re doing when we really don’t.
This is the cycle we’re in right now and we don’t have to live this way. It’s important to be able to realize that you can fail at something and that your world is not going to end. If you do one thing wrong, it isn’t going to lead to you having your entire life be destroyed. You’re not going to lose everything.
I want to talk a little bit about a couple of pieces of advice that I have from my own failures. Now unfortunately, failing is incredibly painful.
In the original version of this talk, I was talking about the worst day of my life, which happened in 2008. I was going to school for audio engineering, video editing and other stuff after going to school to get a journalism degree. I’d spent about 10 years and 60 thousand dollars going to school trying to pursue this career.
In one day all of that was gone. My career was gone, all my network was gone. Everything was just completely gone. It was a really horrible, painful experience that I never want to go through ever again.
I learned so much from that experience that I decided to write this talk. It was such a horrible, painful experience. I learned all of these things that I knew I never wanted to deal with ever again.
For some reason we remember painful things a little bit better than we remember things that were successful and things that made us happy. Failing is a good thing because, if you think about it properly, you can really learn a lot of stuff about yourself and how to avoid having to deal with this painful stuff in the future.
Anything that you do is going to be difficult at first.
I don’t know how many people have not actually been programming for very long, but I haven’t been programming for very long. I talk to people who’ve been programming since they were seven and they don’t remember that programming used to be hard.
When you first start off, you’re really slow and there’s a bunch of stuff that you don’t know, but you don’t think about it. Then as you get better and better, you start getting faster. Then eventually things get better.
Nobody is born being able to do something like this.
This takes a lot of time and work and dedication. We just see the end result. You need to be aware of the fact that when you first start out with something, you’re not going to be like this, but you can get to be this skilled if you work really hard and you are focused and willing to work through all of the pain and the Cliffs of Confusion and the Desert of Despair.
We also need to stop trying to find something that’s going to define us.
I have heard of all of these problems in the last couple of years with gamer gate, where you have these people who have adopted the persona of thinking of themselves as a gamer. They don’t think of gaming as something that they enjoy doing. It’s just who they are.
That causes a lot of problems, especially when you’re in online communities of people. Then people can’t give you advice about how you can be better at what you do because you confuse what you’re doing with who you are. Every time somebody tells you that you could be making your code more efficient, you take it as a personal attack because you completely adopted that as your persona.
You don’t have your nice patronus out there that can absorb all of the negative energy that you’re getting from people. You have no buffer because you have decided to identify yourself with what it is that you’re doing.
It’s really important to try to get away from that mindset so that you can be a happier person and be able to accept that sometimes you’re going to do something that doesn’t quite work out.
We also need to stop comparing ourselves to other people.
As Vicky mentioned in my bio, I spent a year working for Brad Larson. Brad Larson is the creator of GPUImage, he’s a moderator on Stack Overflow and he had one of the first video series on iOS development before raywenderlich.com really got big.
He’s been programming for 30 years. I’ve been programming for 3 years. I was working with him and I kept comparing myself to him because I thought he was really cool.
We have people in our community that we think are really cool and want to be like them, but we have to acknowledge that we’re all different people. We have different experiences and we’ve been doing things for different periods of time.
I would get really frustrated and depressed because I knew that I was never going to be as good as Brad. I actually had a discussion with another developer, Jeff Biggus, where I was telling him about how I felt like a failure because I was working for this genius guy and I just felt stupid.
Jeff just looked at me. He’s like, “Do you remember what you were doing last year? Last year you were unemployed. Last year you were doing your first conference talk, you were just getting started out and now you have a book. You’re working for Brad. You’ve done a dozen conference talks. Everybody in the community knows who you are. Look at how far you’ve come in the last year.”
I hadn’t thought about it that way because I was so busy comparing myself to the people that I admired and wanted to be like, that I didn’t think about who I used to be. If you told me 3 years ago I’d be doing this, I would have thought, “Nah. There’s no way.”
One of the reasons why I talk about all of this stuff in the community is because there might be people out in the audience who compare themselves to me and go, “Oh, man. Janie, she’s only been doing this for a few years but she’s really successful. I’m never going to be.” I don’t want people to feel that way.
I want everybody to be able to be happy with what they’re doing on their own without comparing themselves to anybody else. I want everybody to feel okay with saying, “You know what? I’m going to learn something that’s really hard and I’m going to go for it. I’m not going to worry about how I compare to somebody else.”
Also, just do things that make you happy.
I know for a really long time, we’ll have the mindset that any hobby or thing that we do has to eventually lead to something else. I don’t know about anybody else, but I’m bad at doing the away from keyboard thing. For me, my hobbies are going and writing code for myself as opposed to code that I’m being paid to write for other people.
I recently started cooking because it was something I needed to learn how to do. I’d never been on my own before. I was getting really tired of all of the nasty packaged food that I was getting from the grocery store. It was just something I wanted to do for myself.
It wasn’t because I wanted to be a chef or because I wanted to write a cook book, even though I’ve been told I should do that at some point. It was a thing that I wanted to do because it was important to me and I wanted to know that I could cook. I wanted to be able to make food and feed myself and do things properly because it was something that was important to me.
Do things that make you happy, without thinking about where it’s going to lead or what it’s going to do for you. Go out and dance badly if it makes you happy because our lives do not revolve around other people. We have to do what we want to do to make ourselves feel good.
That also includes being able acknowledge if you’ve made a mistake.
I know a lot of people who, from the time they were really little, they wanted to be game developers because they loved video games and that was the only thing they ever wanted to do.
So they spent all of their school time trying to learn how to do game development. They went to college for game development. Then they get into the industry and game development is not the best place to work for a lot of people because there is a lot more people who want to do it than there is market for it.
I can understand. If you spent all of your life thinking that you were going to do something and you spent a lot of money and you went into a lot of debt. Then you go into this field and find out that it’s something you don’t really like doing. It can be really hard to acknowledge that this is something that you don’t want to do and to walk away from it.
Since we’re at an Apple developer conference, I feel it is necessary to mention Steve Jobs. Steve Jobs is one of my heroes.
I know I really shouldn’t think of Steve Jobs as one of my heroes because he’s a person who was paying orphans in China 20 cents to make iPhones, but one of the things that I really admire and respect about him that I’ve taken away from his story was what happened to him when he got forcibly removed from Apple.
This was a horrible, awful experience that he went through where he lost the company that he founded. There were a couple of different ways he could respond to this.
He could have taken all of his Apple money and go on and bought a scuba shop on the beach and told everybody about how he used to be a big tech guy, but he didn’t do that. He wanted to be able to move forward and do something else.
He dusted himself off and moved on. He started NeXT. He bought Pixar. He did a whole bunch of stuff after that happened and eventually he came back and he led the company to what it is today.
I think it’s important for everybody to understand that we’re all going to fail and what happens after you fail is just as important as what happened before you failed.
If you’re able to analyze your failures and look at what happened and what led to it, you’re able to get an idea about how you can avoid failing again in the future. I know people who horrible things happened to them because they made really bad decisions, but they didn’t want to acknowledge that they had any personal responsibility for what happened to them. You know, it was just a fluke of the universe.
I think of my ex-husband, who lost his job while we were still married because he started taking every single Friday off and made it very clear to his company that he didn’t want to be there. When I talked to him afterward I’m like, “Okay. Do you understand why you lost your job.” He’s like, “Yeah. It’s because the universe sucks” and he wouldn’t acknowledge that he had any control over what happened to him.
When you fail, you can either walk away and decide the universe sucks, or you can do the really hard thing of taking a look in the mirror and saying, “What can I do to avoid having to do this ever again?”
I really hope that everybody here feels a little bit more comfortable about trying to embrace their failures and being able to go out and try something that might be a little bit less familiar. It’s not the end of the world if you don’t do particularly well at it the first time. It takes a little while and if you keep working at it, you’ll do better.
So go forth and be awesome!
The post RWDevCon 2016 Inspiration Talk – Embracing Failure by Janie Clayton appeared first on Ray Wenderlich.
To say game development is a challenge would be the understatement of the year.
Until recently, making 3D games required low-level programming skills and advanced math knowledge. It was akin to a black art reserved only for super developers that never saw the sunlight.
That all changed with Unity. Unity has made this game programming into a craft that’s now accessible to mere mortals. Yet, Unity still contains those complicated AAA features, so as you grow as a developer you can begin to leverage them in your games.
Just like every game has a beginning, so does your learning journey — and this one will be hands-on. Sure, you could pore over pages and pages of brain-numbing documentation until a lightbulb appears above your head, or you can learn by creating a game.
You obviously would prefer the latter, so in this Unity tutorial series you’ll build a game: Bobblehead Wars.
In this game, you take the role of a kickass space marine, who just finished obliterating an alien ship. You may have seen him before; he also starred in our book 2D iOS & tvOS Games in a game called Drop Charge.
After destroying the enemy ship, our space marine decides to vacation on a desolate alien planet. However, the aliens manage to interrupt his sun tan — and they are out for blood. After all, space marines are delicacies in this parts of the galaxy!
This game is a twin-stick shooter, where you blast hordes of hungry aliens that relentlessly attack:
You’ll toss in some random powerups to keep gameplay interesting, but success lies in fast footwork and a happy trigger finger.
You’ll start by exploring the Unity interface and learn how to import assets into your project.
Before you can take on aliens, you need to download the Unity engine itself.
Head over to the following URL: http://unity3d.com/get-unity
You’ll see a page with many options from which to choose:
You can go Pro if you’d like, but it’s excessive at this stage of your journey. For this tutorial, you only need the free version. In fact, you can even release a complete game and sell it on Steam with the free version.
Previously, certain engine features were disabled in the free version. With the release of Unity 5, all those closed-off features are now available to everybody that uses the personal version.
In case you are curious, here are what the three options mean:
There’s also an enterprise edition for large organizations that want access to the source code and enterprise support.
Under Unity Personal, click Download Now.
Give it a moment to download then double-click it to start the installation.
Click through the installer until you reach the following screen where you select components:
The screenshots in this tutorial were made on Windows, because that is what the majority of Unity developers use (mainly because Windows is a more popular gaming platform).
If you are developing on the Mac, your screenshots may look slightly different, but don’t worry — you should still be able to follow along with the tutorial just fine. Several of our tech editors are Mac users and have done exactly that! :]
By default, you should select the Unity engine, Documentation and Standard Assets. Here’s why they are significant:
If you plan on reading the book chapter about publishing your game, then make sure to check Android build support and iOS build support.
After you finish reading the book, you may find that you need additional platform support, such as tvOS build support. In that case, you just run the installer again, uncheck everything then check the required platforms. Follow the installer to completion to install those components.
Run the program once installation completes. The first thing you’ll see is a dialog box asking for your Unity credentials.
If you don’t have an account, click create one and follow the steps. Unity accounts are free. You’ll have to log in every time you fire it up, but the engine does have an offline mode for those times when you have no network.
Once you’re logged in, you’ll be presented with a project list that provides an easy place to access all of your projects. Since you don’t have any projects, click the New button.
You should be looking at the project creation dialog. You’ll notice that you have a few options, so fill them in as follows:
Here’s what everything on this screen means:
Once you’re ready, click the Create project button.
Welcome to the world of Unity!
When your project loads, you’ll see a screen packed full of information. It’s perfectly normal to feel a little overwhelmed at first, but don’t worry — you’ll get comfortable with everything as you work through the first few chapters.
Your layout will probably look like this:
If not, click the Layout button in the top-right and select 2 by 3 from the dropdown.
Each layout is composed of several different views. A view is simply a panel of information that you use to manipulate the engine. For instance, there’s a view made for placing objects in your world. There’s another view for you to play the game.
Here’s what the interface looks like when broken down into individual views. Each red rectangle outlines a view that has its own purpose, interface and ways that you interact with it.
Throughout the book, you’ll learn about many of these views. To see a list of all views, click the Window option on the menu bar.
The Unity user interface is completely customizable so you can add, remove, and rearrange views as you see fit.
When working with Unity, you’ll typically want to rearrange views into a Layout that’s ideal for a given task. Unity allows you to save layouts for future use.
In the Editor, look for the Game tab (the view to the lower right) and right-click it. From the drop-down, select Add Tab then choose Profiler.
The Profiler view lets you analyze your game while it’s running. Unfortunately, the profiler is also blocking the Game view, so you won’t be able to play the game while you profile it — not so helpful.
Click and hold the Profiler tab and drag it to the Scene tab above.
As you see, views can be moved, docked and arranged. They can also exist outside the editor as floating windows.
To save the layout, select Window\Layouts\Save Layout… and name it Debugging.
Whenever you need to access this particular layout, you can select the Layout button and choose Debugging.
You can also delete layouts. If you ever accidentally trash a stock layout, you can restore the default layouts.
Beginners to Unity might imagine that you develop your game from start to finish in Unity, including writing code, creating 3D models and textures, and so on.
In reality, a better way of thinking about Unity is as an integration tool. Typically you will write code or create 3D models or textures in a separate program, and use Unity to wire everything together.
For Bobblehead Wars, we’ve created some 3D models for you, because learning how to model things in Blender would take an entire book on its own! In this tutorial, you will learn how to import them into the game.
But before you do, it pays to be organized. In this game, you’re going to have a lot of assets, so it’s critical to organize them in way that makes them easy to find.
The view where you import and organize assets is called the Project Browser. It mimics the organization of your file system.
In the Project Browser, select the Assets folder and click the Create button. Select Folder from the drop-down and name it Models.
This will be home to all your models. You may feel tempted to create folders and manipulate files in your file system instead of the Project Browser. That’s a bad idea — do not do that, Sam I Am!
Unity creates metadata for each asset. Creating, altering or deleting assets on the file system can break this metadata and your game.
Create the following folders: Animations, Materials, Models, Prefabs, Scenes, Scripts and Textures.
Your Project Browser should look like this:
Personally, I find large folder icons to be distracting. If you also have a preference, you can increase or decrease the size by using the slider at the bottom of the Project Browser.
Now that you’ve organized your folders, you’re ready to import the assets for the game.
To start, download the starter resources for this tutorial here.
First, you’ll import the star of the show: the space marine.
Open the resources folder and look for three files:
Drag these three files into the Models folder. Don’t copy BobbleWars.unitypackage: that comes later.
Select the Models folder and you’ll see that you have a bunch of new files. Unity imported and configured the models for you and created a folder named Materials.
To keep things tidy, move Bobble Wars Marine texture from the Models folder to the Textures folder. Also move the contents of the newly generated Materials folder (in the Models folder) into the parent-level Materials folder, and then delete that new Materials folder by pressing delete (or command-delete on the Mac).
What are materials? Materials provide your models with color and texture based upon lighting conditions. Materials use what are known as shaders that ultimately determines what appears onscreen. Shaders are small programs written in a specific shader language which is far beyond the score of this intro book. You can learn more about materials through Unity’s included documentation.
Switch back to the Models folder and select BobbleMarine-Body. The Inspector view will now display information specific to that model as well as a preview.
If you see no preview, then its window is closed. At the bottom of the Inspector, find a gray bar then drag it upwards with your mouse to expand the preview.
The Inspector allows you to make changes to the model’s configuration, and it allows changes to any selected object’s properties. Since objects can be vastly different from one another, the Inspector will change context based on the object selected.
At this point, you’ve imported the models and texture for the space marine. The models were in FBX format, and the texture was in PSD format.
We supplied the space marine models to you in the FBX format, as this is a popular format for artists to deliver assets. But there’s another popular format you should understand how to use as well: Blender files.
Unlike FBX, Blender files contain the source model data. This means you can actually edit these files inside Blender, and the changes will immediately take effect in Unity, unlike an FBX file.
With an FBX, you’d need to export and re-import the model into Unity every time you change it.
There is a small tradeoff for all this delicious functionality. For Unity to work with Blender files, you need Blender installed on your computer. Blender is free, and you’ll be happy to know that you’ll use it to make your own models in a few chapters.
Download and install Blender from the following URL: https://www.blender.org/download/
After you install Blender, run the app and then quit. That’s it: you can now use Blender files with Unity.
Now that you’ve installed Blender, you can now import the rest of the assets.
The rest of the assets are combined into a single bundle called a Unity package. This is a common way that artists deliver assets for Unity, especially when you purchase them from the Unity store.
Let’s try importing a package. Select Assets\Import Package\Custom Package…, navigate to your resources folder and select BobbleheadWars.unitypackage then click Open.
You’ll be presented with a list of assets included in that package, all of which are selected by default. Note that some of these are Blender files, but there are also other files like textures and sounds as well. Click the Import button to import them into Unity.
The import will add a bunch of additional assets to your project. If you get a warning, just dismiss it.
As you did before, to keep things tidy, single click the newly generated Materials folder (in the Models folder) and rename it to Models. Drag this new folder into the parent-level Materials folder.
At this point, you have imported everything into Unity. It’s time to start putting your game together, and you’ll kick it off by adding your models to the Scene view.
The Scene view is where game creation happens. It’s a 3D window where you’ll place, move, scale and rotate objects.
First, make sure to select the Scene view tab. Then, in the Project Browser, select BobbleArena from the Models subfolder and drag it into the Scene view.
Check out the arena in the Scene view:
Pretty cool, eh?
The Scene view gives you a way to navigate your game in 3D space:
By default, the view displays everything with textures in a shaded mode. You can switch to other viewing modes such as wireframes or shaded wireframe.
Let’s try this out. Just underneath the Scene tab, click the Shaded dropdown and select Wireframe. Now you’ll see all your meshes without any textures, which is useful when you’re placing meshes by eye.
Switch the Scene view back to Shaded textures.
In the Scene view, you’ll notice a gizmo in the right-hand corner with the word Persp underneath it. This means the Scene view is in perspective mode; objects that are closer to you appear larger than those that are farther away.
Clicking on a colored axis will change your perspective of the scene. For example, click the green axis and the Scene view will look down from the Y-axis. In this case, the Persp will read Top because you’re looking at the world from that perspective. How’s it feel to be on top of the world? :]
Clicking the center box will switch the view into Isometric mode aka Orthographic mode. Essentially, objects are the same size regardless of their proximity to you.
To return to Perspective mode, simply click the center box again. You’ll learn more about Isometric mode later in the book.
At this point, you have the arena set up, but it’s missing the guest of honor!
To fix this, in the Project Browser find and open the Models folder then drag BobbleMarine-Body into the Hierarchy view.
In Unity, games are organized by scenes. For now, you can think of a scene as a level of your game. The Hierarchy view is a list of all objects that are currently present in the scene.
Note that your scene already contains several objects. At this point, it contains your arena, the space marine, and two default objects: the main camera and a directional light.
With the marine body still selected, hover the mouse over the Scene view and press the F key to zoom to the marine. This shortcut is useful when you have many objects in your scene, and you need to quickly get to one of them.
Don’t worry if your space marine isn’t placed at this exact position. You’ll fix that later.
While the space marine’s dismembered body is pretty intimidating, he’s going to have a hard time seeing and bobbling without a head!
Drag the BobbleMarine-Head from the Project Browser into the Hierarchy. Chances are, the head will not go exactly where you’d hope.
Select the head in the Hierarchy to see its navigation gizmos. By selecting and dragging the colored arrows, you can move the head in various directions.
The colored faces of the cube in the middle of the head allow you to move the object along two axes at the same time. For example, selecting and dragging the red face — the x-axis — allows you to move the head along the y- and z-axes.
You can use the toolbar to make other adjustments to an object. The first item is the hand tool. This allows you to pan the Scene and is equivalent to holding down the middle mouse button.
Select the position tool to see the position gizmo that lets you reposition the selected object.
Use the rotate tool to rotate the selected object.
The scale tool allows you to increase and decrease the size of an object.
The rect tool lets you rotate, scale and reposition sprites. You’ll be using this when working with the user interface and Unity 2D.
Using the aforementioned tools, tweak the position of the helmet so it sits on the neck. Also, see if you can move the space marine so he’s centered in the arena. In the next chapter, you’ll position these precisely with the Inspector. When done, the marine should look like this:
After positioning the space marine’s head, select File\Save Scene. Unity will present you with a save dialog. Name the scene Main, and after you finish creating it drag the file to the Scenes folder.
You can download the completed project for this tutorial here.
You should keep on reading to the second part of this tutorial series, available tomorrow, where you’ll get to add some creepy-crawly enemies and learn how to work with Prefabs and GameObjects!
If you’re enjoying this tutorial series and want to learn more, you should definitely check out Unity Games by Tutorials.
The book teaches you everything you need to know about building games in Unity, whether you’re a beginner or a more experienced game developer. In the book, you’ll build four great games:
The book is still in development, but here’s a preview of the first game you’ll build in the book:
If you have questions or comments on this tutorial, please leave them in the discussion below!
The post Unity Tutorial Part 1: Getting Started appeared first on Ray Wenderlich.
Foundation now includes the ability to specify a DateInterval - discover how to use it in this screencast.
The post iOS 10 Screencast: Introducing DateInterval appeared first on Ray Wenderlich.
Welcome to the second part of this Unity tutorial mini-series! In this part, you’ll learn all about GameObjects in Unity while you give your hero some creepy-crawly aliens to keep him company!
This tutorial continues on from Unity Tutorial Part 1: Getting Started. You can continue on with your completed project from the last chapter, or if you would prefer to start over fresh, you can download the starter project for this tutorial here.
GameObjects represent pretty much all elements in Unity: your player, the aliens, bullets on the screen, the actual level geometry — basically, everything in the game itself.
Just like the Project Browser contains all assets, the Hierarchy contains a list of GameObjects in your scene.
If you don’t have your project open, open it now in Unity.
Or, if you start Unity or click File\Open Project, you’ll see a project list. Select the desired project and Unity will take care of the rest.
If your project isn’t listed, click the Open button to bring up a system dialog box. Instead of searching for a particular file, try navigating to the top-level directory of your Unity project and click Select Folder. The engine will detect the Unity project within the folder and open it.
Once your project is open, take a look at your Hierarchy and count the GameObjects.
Your first thought may be three because you added three GameObjects in the last chapter: arena, space marine body and space marine head.
However, there are two other GameObjects: Main Camera and Directional Light. Remember how Unity creates these by default? Yes, these are also GameObjects.
Yet, there are even more GameObjects. You’ll notice that there are disclosure triangles to the left of the GameObjects you imported.
Holding down the Alt button on PC or Option on Mac, click each disclosure triangle.
As you can see, you have a pile of GameObjects:
There’s three important points to remember about GameObjects:
Our hero is so bored that he’s picking his nose with his gun. You need to get him moving, but first, you need to reposition your GameObjects.
Before starting, collapse all the GameObject trees by clicking the disclosure triangles.
Select BobbleArena in the Hierarchy and take a moment to observe the Inspector, which provides information about the selected GameObject.
GameObjects contain a number of components, which you can think of as small units of functionality. There is one component that all GameObjects contain: the Transform component.
The Transform component contains the position, rotation, and scale of the GameObject. Using the inspector, you can set these to specific numbers instead of having to rely upon your eyeballs. When hovering the mouse over the axis name, you’ll see arrows appear next to the pointer.
Press the left mouse button and drag the mouse either left or right to adjust those numbers. This trick is an easy way to adjust the values by small increments.
With BobbleArena selected, set Position to (X:-6.624, Y:13.622, Z:6.35)
. As I developed this game, the arena ended up in this position. You could just as well place it in the center of the game of the game world.
Set the Scale to (X:2.0, Y:2.0, Z:2.0)
. This gives the player more room to navigate the arena.
If you zoom out of the Scene view, you’ll probably notice the space marine is suspended in the void; mostly likely, he’s questioning his assumptions about gravity. You could move his head and then his body, but your life will be much easier if you group his body parts into one GameObject.
In the Hierarchy, click the Create button and select Create Empty.
An empty is a GameObject that has only the one required component of all GameObjects — the Transform component.
In the Hierarchy, you’ll see your new GameObject creatively named: GameObject
. Single-click the GameObject and name it SpaceMarine.
You can insert spaces in GameObjects’ names, e.g., Space Marine. However, for the sake of consistency, you’ll use camel case for names in this book.
Drag BobbleMarine-Body and BobbleMarine-Head into the SpaceMarine GameObject.
A few things happen when you parent GameObjects. In particular, the position values for the children change even though the GameObjects don’t move. This modification happens because GameObject positions are always relative to the parent GameObject.
Select the SpaceMarine in the Hierarchy. Go to the Scene view and press F to focus on it. Chances are, the arena is blocking your view.
Thankfully, you don’t need to get Dumbledore on speed dial. You can make it disappear yourself! Select BobbleArena in the Hierarchy, and in the Inspector, uncheck the box to the left of the GameObject’s name. This will make the arena disappear.
You only should see the hero now. Select the SpaceMarine GameObject. In the Inspector, mouse over the X position label until you see the scrubber arrows. Hold the left mouse button and move your mouse left or right. Notice how all the GameObjects move relative to the parent.
As you can see, having parents does have its advantages. No offense to any parentless deities out there.
When you parent one GameObject in another, the position of the child GameObject won’t change. The difference is the child GameObject is now positioned relative to the parent. That is, setting the child to (0, 0, 0) will move the child to the center of the parent versus the center of the game world.
You’ll do this now to assemble your marine.
Select BobbleMarine-Body, and in the Inspector, set Position to (X:0, Y:0, Z:0)
. Go select BobbleMarine-Head and set Position to (X:-1.38, Y:6.16, Z:1.05)
in the Inspector.
Congratulations! Your hero is assembled.
Now to it’s time to place the space marine in his proper starting position. Select BobbleArena, and in the Inspector, check the box next to the name to reenable it.
Select SpaceMarine, and in the Inspector, set its position to (X:-4.9, Y:12.54, Z:5.87)
. Also, set the rotation to (X:0, Y:0, Z:0)
. Your marine should end up directly over the hatch. If this isn’t the case, then feel free to tweak the values until that is the case.
Once the hero is in place, press F in the Scene view so you can see him standing proudly.
The hero should now be positioned precisely over the elevator, ready to rock. Unfortunately for him, his grandiose rock party will soon degrade into a bug hunt.
This game features creepy crawly bugs, and like the hero, they’re composed of many pieces — some assembly required.
In the Hierarchy, click the Create button and select Create Empty from the drop-down. Single-click the GameObject to name it Alien.
Select Alien in the Hierarchy, and in the Inspector, set the position to: (X:2.9, Y:13.6, Z:8.41)
.
From the Project Browser, drag BobbleEnemy-Body from the Models folder into the Alien GameObject.
Set BobbleEnemy-Body Position to(X:0, Y:0, Z:0)
. Now the alien and hero should be side by side in the arena.
As creepy as the alien is without a head, the hero needs more to shoot at than that spindly little frame. From the Project Browser, drag BobbleEnemy-Head into the Alien GameObject. Set Position to (X:0.26, Y:1.74, Z:0.31)
, Rotation to (X:-89.96, Y:0, Z:0)
and Scale to (X:100, Y:100, Z:100)
.
That’s one fierce little bug. They go together so well that you could mistake them for the next superstar crime-fighting duo.
At this point, you have one parent GameObject for the hero and another for the alien. For the hero, this works great because you need only one. For the alien, you’re going to need many — so, so many.
You could copy and paste the alien to make clones, but they’d all be individuals. If you needed to make a change to the alien’s behavior, you’d have to change each instance.
For this situation, it’s best to use a prefab, which is a master copy that you use to make as many individual copies as you want. Prefabs are your friend because when you change anything about them, you can apply the same to the rest of the instances.
Making a prefab is simple. Select the Alien GameObject and drag it into the Prefabs folder in the Project Browser.
A few things changed: there’s a new entry in your Prefabs folder with an icon beside it. You’ll also note the name of the GameObject in the Hierarchy is now blue:
The blue indicates the GameObject has been either instanced from a prefab or a model, such as the BobbleArena. Select the Alien GameObject in the Hierarchy and look at the Inspector; you’ll notice some additional buttons:
Here’s the breakdown of these new buttons:
Creating a prefab instance is quite easy. Select the Alien prefab in the Project Browser and drag it next to your other Alien in the Scene view.
You can also drag an instance to the Hierarchy. As you can see, creating more aliens is as easy as dragging the Alien prefab
from the Project Browser. But you don’t need droves of aliens yet, so delete all the Aliens from the Hierarchy. You delete a GameObject by selecting it in the Hierarchy, and pressing Delete on your keyboard, (Command–Delete on a Mac), or you can right-click it and select Delete.
The next to-do is fixing some of your models. In this case, Unity imported your models but lost references to the textures.
In the Models folder of your Project Browser, drag a BobbleArena-Column into the Scene view. You’ll find a dull white material.
If a material name or texture name changes in the source package, Unity will lose connection to that material. It tries to fix this by creating a new material for you that has no textures attached to it.
To fix this, you have to assign a new texture to the material. In the Project Browser, select the Models subfolder and then, expand the BobbleArena-Column to see all the child objects.
Next, select Cube_001 in the Project Browser, and in the Inspector, click the disclosure triangle for the Main_Material shader.
You’ll see that there are a lot of options! These options configure how this material will look.
For example, the Metallic slider determines the metal-like quality of the material. A high value metallic value means the texture will reflect light much like metal. You’ll notice a grey box to the left of most of the properties. These boxes are meant for textures.
In this case, all you want is a an image on your model. In the Project Browser, select the Textures folder. Click and drag the Bobble Wars Marine texture to the Albedo property box located in the shader properties.
The Albedo property is for textures, but you can put colors there are well. Once you do this, your columns will now have a texture.
The arena also suffered a texture mishap. In the Hierarchy, expand the BobbleArena and then expand Floor. Select the Floor Piece.
In the Inspector, you’ll notice two materials attached to the floor piece. The BobbleArena-Main_Texture material does not have a texture assigned to it. You can tell because the material preview is all white.
Like you did for the column, select the Textures folder in the Project Browser. Click and drag the Bobble Wars Marine texture to the Albedo property box located in the shader properties.
Your floor will now acquire borders. How stylish!
You’ll also notice that not just one, but all floor sections acquired borders. This is because they all use the same material.
Now, that you have your models fixed, it’s time add a bunch of columns to the arena. You’ll make a total of seven columns, and you’ll use prefabs for this task.
The thinking is that it’s much easier to create a prefab and make duplicates than it is to turn a group of existing GameObjects into prefab instance. The former method requires minimal work whereas the latter requires you to extract the commonalities into a prefab while maintaining any unique changes for each instance. This results in a lot of work.
In the Hierarchy, drag the BobbleArena-Column into your Prefabs folder to turn it into a prefab. With BobbleArena-Column instance still selected in the Hierarchy View, go to the Inspector and set position to (X:1.66, Y:12.83, Z:-54.48)
. Set scale to (X:3.5, Y:3.5, Z:3.5)
. You do want all the prefabs to be the same scale, so click the Apply button.
Now it’s time for you to make the rest of the columns. Dragging one column at a time from project to project in the Scene view can be a little tedious, especially when there are several instances. Duplicate a column by selecting one in the Hierarchy and pressing Ctrl–D on PC or Command–D on Mac.
Create a total of six duplicates and give them following positions:
(X:-44.69, Y:12.83, Z:-28.25)
(X:42.10, Y:12.83, Z:30.14)
(X:-8.29, Y:12.83, Z:63.04)
(X:80.40, Y:12.83, Z:-13.65)
(X:-91.79, Y:12.83, Z:-13.65)
(X:-48.69, Y:12.83, Z:33.74)
You should now have seven columns in the arena.
The arena looks good, but the Hierarchy is a bit messy. Tidy it up by clicking the Create button and select Create Empty. Rename the GameObject to Columns and drag each column into it.
You’ll notice the columns have a similar name with unique numbers appended to them. Since they essentially act as one entity, it’s fine for them to share a name. Hold the Shift key and select all the columns in the Hierarchy. In the Inspector, change the name to Column.
What good are bloodthirsty aliens unless they spawn in mass quantities? In this section, you’ll set up spawn points to produce enemies to keep our hero on his toes.
So far, you’ve assembled the game with GameObjects that you want the player to see. When it comes to spawn points, you don’t want anybody to see them. Yet, it’s important that you know where they are.
You could create a 3D cube and place it in your scene to represent a spawn point, then remove it when the game starts, but that’s a clunky approach.
Thankfully, Unity provides a simpler mechanism called labels. These are GameObjects that are visible in the Scene view, but invisible during gameplay.
To see them in action, you’ll create a bunch of different spawn points similar to how you created the columns.
Click the Create button in the Hierarchy and select Create Empty. Give it the name Spawn and set position to (X:-5.44, Y:13.69, Z:90.30)
.
In the Inspector, click the colored cube next to the checkmark. A flyout with all the different label styles will appear. Click blue capsule.
Look at your Scene view; you’ll see it’s been annotated with the Spawn point.
You need to create ten more spawn points. Make placement easier by doing the following:
Now duplicate and reposition ten more spawn points, according to the following image:
Don’t worry if you don’t get them exactly the same — game design is more of an art than a science!
Once you’re done, click the Create button in the Hierarchy, select Create Empty and name it SpawnPoints. Drag all the spawn points into it. Batch rename them like you did with the columns to Spawn.
Congratulations! Your game is now properly set up. Make sure to save!
You can download the completed project for this tutorial here.
At this point, you have your hero, his enemy the alien, and the arena in which they will battle to the death. You’ve even created spawn points for the little buggers. As you set things up, you learned about the following:
In the next section of this tutorial mini-series, you’ll add some action to this game, learn how to work with Components and let your space marine fire away at will!
If you’re enjoying this tutorial series and want to learn more, you should definitely check out Unity Games by Tutorials.
The book teaches you everything you need to know about building games in Unity, whether you’re a beginner or a more experienced game developer. In the book, you’ll build four great games:
The book is still in development, but here’s a preview of the first game you’ll build in the book:
If you have questions or comments on this tutorial, please leave them in the discussion below!
The post Unity Tutorial Part 2: GameObjects appeared first on Ray Wenderlich.
Like clockwork, another month has marched by. That brings us closer to all the goodies Apple will hopefully share with us this fall. I’m sure we’re all getting excited for a new iPhone, an updated MacBook, and maybe even a surprise or two.
But for me, August has brought another swarm of great apps and games from our readers. :] We write our tutorials for you and its exciting for us to see them put to use in some fantastic apps.
I’ve tried out all the ones sent to me and I’ve picked just a few to share with you. As always, I don’t have time to write about them all, so make sure you check out the honorable mentions too.
This month we have:
Keep reading to see the the latest apps released by raywenderlich.com readers like you.
Pokemon Go has swept the world, and that means the competition couldn’t be higher. To gain the edge you’ll need to know everything there is about your Pokemon and their abilities. The GoTypeChart can help with that. It has a ton of information on each and every Pokemon from Pokemon Go.
GoTypeChart lets your search for Pokemon by type, name, move type, index, attack damage, defense, and stamina. Each move is listed with its DPS and type. You can easily find just the right attack to give you the edge. Or you can easily look for fire Pokemon to battle the grass type.
GoTypeChart also makes it easy to browse Pokemon, their abilities, and their attributes. So it’s easy to find new Pokemon to add to your list to catch to improve your battles.
AnyMap is a very cool custom map application that allows you to get all the benefits we’re used to with regular iPhone maps but while using your custom map. Maybe it’s a trail map or a city landmark map. You can continue to use your custom map while taking advantage of GPS, pinch to zoom, and more.
AnyMap just needs a picture of the mall you’d like. Then you orient it by walking to a few easy to find landmarks. Now AnyMap knows how to navigate your custom map. You can now easily see where you are on the map at any time as you move around. It doesn’t even require cell service now that it’s synced your map to GPS coordinates.
Food allergies are a common problem for many people. It may be manageable at home but when you go out it can be hard to find a safe entree. Now imagine being in another country without being a native speaker! Thank goodness for Can’t Eat That, an app that will help you communicate your food allergies in a variety of languages.
Can’t Eat That lets you select your food allergy and then a language you need to express it in. You can then show the app to your waiter in their native language and they’ll be able to help you find something safe to eat. The app can even read the translation aloud if necessary. Can’t Eat That could be a truly life saving app depending on how severe your allergy may be. You can finally travel and try new foods without fear.
Crop-Size is a photo editing app with some powerful features to make it a step above the rest. It has all the expected features like resizing, cropping, filters, and color correction. But it takes a few of those features to the next level.
While resizing a photo, you can choose from a ton of preset sizes common for photos. You can input exact sizes to the pixel. You can also choose between cropping, stretching, or a border when resizing the photo. Cropping has easy custom controls, pixel precise inputs, and tons of common presets. Rotation is even a breeze. All the filters, controls, and effects you’d expect are available as well. You can even edit full metadata and choose which data is exported to exercise full control over your private data.
The best feature of all is the ability to save your edits as a template. After you finish getting an image just right, you can save all your crops, filters, and metadata changes to a template. That template can be used on future photos or on large batches all at once.
What could be better than large batch processing right on your iPhone? Editing in the field has never been so good.
Coffee Phone is a unique social network based entirely around coffee. Having a coffee alone wishing you could meet some new people? You can offer to buy a stranger a coffee if they’ll come enjoy it with you.
Anyone can open the app and see available coffees near them. So if you’re looking for a coffee to meet new people or looking to meet new people by offering a coffee you can easily find someone nearby.
The conversation is up to you, but you already know you both like coffee. ;] So next time you’re relaxing and looking to meet someone knew, grab a coffee and offer one to your next friend.
Tandem is an app for those of us with scheduled medications. Sometimes life is just too busy to remember our medication but often it’s very important we take our pills at the right time. Tandem is here to help us by reminding us to take the right medication at the right time.
Tandem starts by asking you for the medications you need to track as well as dosages in case you may forget that as well. It will ask you about your dosage schedule. From there it can handle the rest. You’ll get a notification when it’s time to take it and you can even mark it as taken right from the lock screen.
But if you need a little extra motivation, Tandem has a special feature just for you. You can invite a friend or family member to be your companion. Tandem will text your companion when it’s time to take your medicine. They can reply straight to the text message and you’ll get it in Tandem as an extra reminder. And when you mark your medication as taken, Tandem will also let your companion know. The companion doesn’t need Tandem at all. They can help remind you with simple text messages.
No more boring videos for you! Video Subtitle Maker will help you spice up your videos.
Video Subtitle Maker makes it easy to add a ton of effects to your videos. You can add a subtitle with tons of font options, colors, and size. You can even use emoji in your subtitle. But Video Subtitle Maker isn’t just about subtitles. You can easily add a caption or floating text. You can add stickers, artwork, watermarks, or even your own photo overlays.
Video Subtitle Maker also makes it easy to add background music to make you video epic. Or its just as easy to add sound effecst to bring your video to life. There are tons of built in sound effects like bubbles, censors, or popular quotes like THIS IS SPARTA!!!
Video Subtitle Maker makes it easy to add all these things to your video. You can easily edit your video right on your iPhone then share it with the world.
First impressions can be hard. But I’m pretty sure a video of your face in a cartoon is a great way to break the ice.
Snapwit has tons of cartoon overlays it calls MOJIs. You can record your face inside the box and boom there is a video of you as a cartoon super hero. There are tons to choose from monkeys to construction workers to cavemen. It couldn’t be easier. Choose your MOJI, align your face, and record your message. Snapwith makes it quick and easy to share with everyone.
There are also plenty of video greetings you can create as well. These are more traditional effects and overlays like Happy Birthday, Invitations, or film borders. They are just as easy to create and share as the MOJIs right on your phone.
Every month I get way more submissions than I have time to write about. I download every app to try them out, but only have time to review a select few. I love seeing our readers through the apps submitted to me every month.
It’s not a popularity contest or even a favorite picking contest — I just try to share a snapshot of the community through your submissions. Please take a moment and try these other fantastic apps from readers like you.
super meow cat
Deep Sleep Fan – White Noise for Bedtime
toGETher Tasks Done GOLD – Eisenhower Matrix Planner
Hungry Hipster
Tap Tap Go!
Boston Truck Tracker
BalloonPop99
Budge
Molecular
Tagalot Cards
Sky Up
Each month, I really enjoy seeing what our community of readers comes up with. The apps you build are the reason we keep writing tutorials. Make sure you tell me about your next one, submit here.
If you saw an app your liked, hop to the App Store and leave a review! A good review always makes a dev’s day. And make sure you tell them you’re from raywenderlich.com; this is a community of makers.
If you’ve never made an app, this is the month! Check out our free tutorials to become an iOS star. What are you waiting for – I want to see your app next month.
The post Readers’ App Reviews – August 2016 appeared first on Ray Wenderlich.
Join Mic, Jake, and Brian as they discuss how to best learn Unity and what resources are available, before moving on to chat about neural networks and some brand new APIs provided by Apple to make leveraging them a little easier.
[Subscribe in iTunes] [RSS Feed]
Hired is the platform for the best iOS developer jobs.
Candidates registered with Hired receive an average of 5 offers on the platform, all from a single application. Companies looking to hire include Facebook, Uber and Stripe.
With Hired, you get job offers and salary and/or equity before you interview, so you don’t have to waste your time interviewing for jobs you might not end up wanting, and it’s totally free to use!
Plus you will receive a $2000 bonus from Hired if you find a job through the platform, just for signing up using the show’s exclusive link.
Interested in sponsoring a podcast episode? We sell ads via Syndicate Ads, check it out!
We hope you enjoyed this episode of our podcast. Be sure to subscribe in iTunes to get notified when the next episode comes out.
We’d love to hear what you think about the podcast, and any suggestions on what you’d like to hear in future episodes. Feel free to drop a comment here, or email us anytime at podcast@raywenderlich.com.
The post Learning Unity, and Neural Networks on iOS – Podcast S06 E07 appeared first on Ray Wenderlich.