Android: Instrumentation Testing using Espresso


Introduction

Android provides a number of extra components beyond Java, for example, Activity, Service etc. Testing these extra components you will be required Instrumentation testing and it needs to be run on device/emulators. Espresso is an official instrumentation testing framework by Google.

Installation

Add this line in app level build.gradle in dependencies

androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'

add this line inside defaultConfig

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

For more information,

  1. Android dev
  2. Google github

Project Structure

An androidTest folder should automatically be generated when you created the Android application. For example,

In Android view,

Screenshot at Sep 15 11-44-37.png

Or, in Project view,

Screenshot at Sep 15 11-46-31.png

If not,

  1. Create a folder named androidTest inside src
  2. Create a folder named java inside androidTest

Now we will create our first test class.

  1. Go to the MainActivity.java class and click on MainActivity class name
  2. Select Navigate (from menu)
  3. Select Test (Shift + Cmd + T)
  4. Select, Create New Test…

Select JUnit4 and the dialog should look like this,

Screenshot at Sep 15 11-56-52.png

Click OK.

Check the correct directory is selected. We need to select androidTest directory,

Screenshot at Sep 15 11-57-34.png

Click OK.

Now Sync Gradle.

If you get the following error,

Error:Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (24.2.1) and test app (23.0.1) differ. See http://g.co/androidstudio/app-test-app-conflict for details.

The add the following line in dependencies

androidTestCompile 'com.android.support:support-annotations:24.2.1'

Note that the above line version number need to match with the expected version number indicated with the error.

Now let’s go to the newly created MainActivityTest.java


package com.genericslab.espressodemo;

import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    // This will launch the MainActivity before testing each method/case.
    @Rule
    public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);

}

Now we will write our first test case

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    // This will launch the MainActivity before testing each method/case.
    @Rule
    public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);

    @Test
    public void testCase1() {
        // Find a view with id = R.id.text
        // Check the text of that view matches with string = R.string.app_name
        onView(withId(R.id.text)).check(matches(withText(R.string.app_name)));
    }
}

Now Right click on the file MainActivityTest.java from project explorer and select “Create MainActivityTest…” -> OK.
Click on Run.

Now we will add our second test case

@Test
public void testCase2() {

    // Open options menu, whether as overflow or using a physical button
    // This will open the option menu as a pop up menu. Next we will click on an item.
    // If you need context then pass, InstrumentationRegistry.getTargetContext()
    openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getTargetContext());

    // From the options menu, we are going to click on an item having text = R.string.label_help
    onView(withText(R.string.label_help)).perform(click());

    // alternatively, you can leverage id property.
    onView(withId(R.id.menu_help)).perform(click());

    // verifying
    onView(withId(R.id.text)).check(matches(withText(R.string.label_updated)));
}
Advertisements

Android: SharedPreferences


There are several data storage options in Android. Such as,

  1. Shared Preferences
    • Stores primitive data types (int, String, float, boolean)
    • Stores as key-value pair
    • Not accessible to other applications
  2. Internal Storage
    • Stores data inside apps private storage
    • Not accessible to other applications
  3. External Storage
    • Stores data on shared external storage (SD Card)
    • Accessible to other applications/user
  4. SQLite Database
    • Stores structured data in apps private storage
    • Not accessible to other applications in general, but can be shared using ContentProviders.
  5. Network
    • Stores data in your own/company server
    • You need an active network connection for accessing/storing the data

We have two options to get the SharedPreferences object,

  1. getPreferences(int mode)
  2. getSharedPreferences(String name, int mode)

There is only one valid value for int mode and that is Context.MODE_PRIVATE

Previously, there were other valid values, such as Context.MODE_WORLD_READABLE and Context.MODE_WORLD_WRITEABLE, but these values got deprecated in API 17 and will throw a SecurityException from Android N.

If we use Context.MODE_PRIVATE then it means, no other apps will be allowed to access our shared preference values.

getPreferences

If we use getPreferences(int mode) then an XML file will be created by name of the Activity and it’s access level will be within the Activity. Separate XML files will be created for each Activity.

getSharedPreferences

If we use getSharedPreferences(String name, int mode) then an XML file will be created by name of String name and it’s access level will be within the Application (i.e any Activity can access the values). If you use a different name then a different XML by that name will be created.

Eaxmple#1

private void saveDataInActivitySharedPreferences() {
    SharedPreferences sharedPrefs = getPreferences(Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = sharedPrefs.edit();

    editor.putString("key_string", "value_string");
    editor.putBoolean("key_boolean", true);
    editor.putInt("key_int", 5);

    editor.apply();
    // apply will not return any value and work asynchronously

    // or you could call, editor.commit();
    // commit returns boolean based on un/successful operation and works synchronously
}

private void loadDataFromActivitySharedPreferences() {
    SharedPreferences sharedPrefs = getPreferences(Context.MODE_PRIVATE);

    String valueString = sharedPrefs.getString("key_string", "Default value");
    boolean valueBoolean = sharedPrefs.getBoolean("key_boolean", false);
    int valueInt = sharedPrefs.getInt("key_int", 0);

    String logMsg = valueString + " " + valueBoolean + " " + valueInt;

    Log.d("DEBUG_TAG", logMsg);
}

You can see the XML file content by selecting Tools -> Android -> Android Device Monitor -> (Tab) File Explorer -> data -> data -> APP_PACKAGE_NAME -> shared_prefs -> ACTIVITY_NAME.xml

File content should like below,

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="key_string">value_string</string>
    <boolean name="key_boolean" value="true" />
    <int name="key_int" value="5" />
</map>

If you tap on CLEAR DATA from Settings then this fill will be deleted. DB and Cache files will be deleted as well.

We can not load the values from another Activity because the created file has Activity level access. If we want to access the values from another Activity then we need to call getSharedPreferences instead of getPreferences. Remember that, in case of getSharedPreferences too we have only one value value for int mode = Context.MODE_PRIVATE

Example#2

editor.remove("key_string");
// the key and the associated value will be deleted.
editor.apply();
// do not forget to call apply() method.

editor.clear();
// clear all the data, but the XML file will not be deleted.
editor.apply();
// do not forget to call apply() method.

Demo contacts for Android emulator/genymotion


When we create an emulator or genymotion device, it is kind of blank. What if we need a list of contacts to experiment with. In that case we have two options

  1. We can use our PERSONAL device. urgh!
  2. We can insert a bunch of demo contacts into emulator/ genymotion.

Cons

What if we accidentally delete/call/send text to our personal contacts? shoot!

Or, we might need to discard the emulator/genymotion and create a new one. In that case all our demo data will be gone!

Besides, our demo data will always look like a DEMO data. I mean, anybody can take a look at our contacts list and can identify the data are dummy.

Solution

I have created some dummy data (which would look like real data to most of sane human beings, hopefully) and exported the contacts into a .vcf file format. Anybody can transfer the file into your emulator/genymotion and import the file from contacts app.

And voila! you will get a bunch of contacts (52 contacts as of now). I have also post the file in github repository and of course you will get the latest data from there.

Github repo: github.com/tausiq/demo-contacts-for-Android

Contacts.vcf: https://github.com/tausiq/demo-contacts-for-Android/blob/master/contacts.vcf

 

 

screenshot_1488355668

screenshot_1488355704

Data source: http://www.fakenamegenerator.com and http://pngimg.com

Swift 3: Beyond the first look


Version:

Run the command to know the Swift version you are using,

~$ xcrun swift -version
Apple Swift version 3.1 (swiftlang-802.0.53 clang-802.0.42)
Target: x86_64-apple-macosx10.9
~$

Variable

Swift is a type-safe language. It can infer data type from default value provided.

Each variable need an initial value OR have to be declared with a type.

var aStringVariable = "Hello world"
var anotherStringVariable: String
var anIntVariable: Int                  // 64-bit signed integer
var anInt8Variable: Int8                // 8-bit signed integer
var anInt16Variable: Int16              // 16-bit signed integer
var anInt32Variable: Int32              // 32-bit signed integer
var anInt64Variable: Int64              // 64-bit signed integer
var anUnsignedIntVariable: UInt         // 64-bit unsigned integer
var anUnsignedInt8Variable: UInt8       // 8-bit unsigned integer
var anUnsignedInt16Variable: UInt16     // 16-bit unsigned integer
var anUnsignedInt32Variable: UInt32     // 32-bit unsigned integer
var anUnsignedInt64Variable: UInt64     // 64-bit unsigned integer
var aDoubleVariable: Double
var aFloatVariable: Float
var aBooleanVariable: Bool
var aCharacterVariable: Character

Some interesting notes:

  • Character can be double quoted in Swift. If not defined explicitly, it will be inferred as String, var myChar : Character = "a"
  • Boolean can’t be assigned to zero or one but true/false

Check variable type,

var lightSwitchOn: Bool = true
var dimmer: Int = 7
var dimmerWithDecimals: Float = 3.14
var veryPreciseDimmer: Double = 3.14159265359

// check variable type 
print(type(of: lightSwitchOn))
print(type(of: veryPreciseDimmer))

In general, there is no syntactical differences between Float and Double, but Double isn’t always better; use Float where speed is more important than accuracy.

var pi: Float = 3.14159265359
var pi2: Double = 3.14159265359

print(pi) // 3.14159
print(pi2) // 3.14159265359

Constant

Constant starts with ‘let’ keyword in Swift. Using ‘let’ is highly recommended whenever possible. Besides type safety there are some internal performance optimization by Swift compiler.

Literal vs Constants
A literal is a value that is written exactly as it’s meant to be interpreted. In contrast, a variable is a name that can represent different values during the execution of the program. And a constant is a name that represents the same value throughout a program. But a literal is not a name — it is the value itself.
Source:
http://stackoverflow.com/questions/485119/what-does-the-word-literal-mean

String

Declare String,

let aString = String()
let anotherString = ""

Swift string class is built from Unicode scalar values you can type emoji directly into string literals. To popup the emoji keyboard in Xcode playground, type control + command + space

let similarTruth = "💰can't buy me 💖"
let similarAnimal = "🐙"

Print all the characters in a String

var anotherString = "Hello"
for character in anotherString.characters {
    print (character)
}

Notice that String doesn’t have any length property

let aString = "Hello World"
print(theTruth.characters.count)

Reverse a String

var simpleString = "Hola"
var reversedString = simpleString.characters.reversed()
// Here, reversedString is a ReversedCollection<String.CharacterView>
// So you can not just write print(reversedString). Instead,

for character in reversedString {
    print (character)
}

String interpolation,

var name = "Kate"
var customizedBirthdayCheer = "Happy Birthday, \(name)!"

Finding a substring within a string,

var word = "fortunate"
word.contains("tuna")

Replacing a substring,

var password = "Mary had a little loris"
var newPassword = password.replacingOccurrences(of: "a", with: "A")

Conditional statement

  • Curly braces are REQUIRED, even if there is a single statement.
  • Parenthesis are not needed around the condition.
  • Pre/ Post -increment/ decrement are deprecated and will be removed in Swift 3.

If-else condition

let x = 5

if x < 5 {
    print("x < 5") } else if x == 5 {     print("x == 5") } else {     print ("x >= 5")
}

While loop

var index = 0
let x = 5

while index < x {
    index += 1      // index++ is deprecated
}

For loop

for i in 0 ... 10 { // for (var i = 0; i <= 10; i++ )
    print("i = \(i)")
}

for i in 0 ..< 10 { // for (var i = 0; i < 10; i++ )
    print("i = \(i) and i * i = \(i * i)")
}

Do while loop

var index = 0
let x = 5

repeat {
    index += 1
} while index < x

Optional basics

  • Variable/constant has to be intialized before used. Otherwise, compile error
  • Optional variable/constant can have a value OR it will be ‘nil’
  • Optional variable/constant is strongly typed too. Either contain a valid value or ‘nil’
  • Opitonal variable/constant can be set to ‘nil’ explicitely
  • Optional variable/constant need to be unwrapped before use. Before unwrap you need to be sure it has a value.
var optionalString: String?
var optionalInt: Int?

if optionalInt != nil {
    var anIntVariable = optionalInt! // unwrapping
    print (anIntVariable)
}

// OR

if let anIntVariable = optionalInt {
    // execution will be here iff optinalInt != nil
    print(anIntVariable) // by default unwrapped
} else {
    print("optionalInt is nil")
}

Function

Function with parameter and return type,

func calculateTip(priceOfMeal: Double) -> Double {
    return priceOfMeal * 0.15
}

let priceOfMeal = 43.27

let tip = calculateTip(priceOfMeal: priceOfMeal)

Function params are by default constant.

func placeFirstLetterLast(_ myString: String) -> String {
    var myString = myString
    myString.append(firstCharacter(of: myString))
    myString.remove(at: myString.startIndex)
    return myString
}

On calling a function, you need to specify the name of all variable as key:value (except the first one)

func foo() {
    // no params, no return type
}

func boo() -> String {
    return ""
    // no params, String return type
}

func coo(first: String) -> String {
    return first
    // one String param, String return type
}

func doo(first: String, second: Int) -> Int {
    return second
    // one String param, one Int param, Int return type
}

func moo(first: String, second: Int, third: Int = 2) -> Int {
    return third
    // Default param third set to 2
}

func koo(first: String) {
    // first = "Haha" // Error: params are constant
    // var first: String will make it a varaible
}

foo()
boo()
coo("Haha")
doo("Haha", second: 5) // Error: doo("Haha, 5)
moo("Haha", second: 5) // omit the default param
moo("Haha", second: 5, third: 3) // default param value

Chain function,

func addExcitementToString(string: String) -> String {
    return string + "!"
}

// chained together twice
let excitedString = addExcitementToString(string: addExcitementToString(string: "yay"))

// chained together 4 times
let reallyExcitedString = addExcitementToString(string: addExcitementToString(string: addExcitementToString(string: addExcitementToString(string: "wahoo"))))

External and Local Parameter Names,

// Prints out a string
func reverseAndPrint(_ string: String) {
    var reversedString = ""
    for character in string.characters {
        reversedString = "\(character)" + reversedString
    }
    print(reversedString)
}

// Takes a named parameter.
// forwardString - External parameter name 
// string - Local parameter name 
func reverseAndPrint(forwardString string: String) {
    var reversedString = ""
    for character in string.characters {
        reversedString = "\(character)" + reversedString
    }
    print(reversedString)
}

reverseAndPrint("regal")
reverseAndPrint(forwardString:"time")

If no external parameter name is needed then we can use _

Pure Functions
A pure function is a function that only operates on the values it receives, and that returns a new value.

A huge benefit of pure functions is that they are easier to understand and easier to reuse because a pure function has no effect on any existing object in the app. Instead, a pure function generates something new: a return value. We can easily integrate pure functions into any app without worrying if they will affect existing objects.

Class

  • Instance variable need to initialized with default value OR the class need to have a constructor
  • Constructor is a special function with name ‘init’
  • Current instance reference is ‘self’
class Person {
    var firstName: String = "Sherlock"
    var lastName: String = "Holms"
    var age: Int

    init() {
        // Constructor
        self.age = 50
    }

    // Method
    func getDescription() -> String {
        return "FirstName: \(self.firstName) and LastName: \(self.lastName) and Age: \(self.age)"
    }
}

var p = Person()    // Creating object
print("FirstName: \(p.firstName)")
print(p.getDescription())

instanceof OR is of type

class Person {
    var firstName: String = "Sherlock"
    var lastName: String = "Holms"
}

class Student: Person {
    var id: String = "0123456789"
}

var s = Student()

if s is Person {
    print("s is a Person too")

    var p = s as Person // casting to Person
    print(p.firstName)

} else {
    print ("This will not be executed")
}

Android Library: Gson (Usage and Example)


Serialization

Serialization is a process of writing the state of an object into a byte stream. Similarly, deserialization is the process of turning the byte stream into an object.

Installation

You can get the latest version of Gson library from here.
Right now the latest version is 2.7. Click on the version from the link and you will get the instruction to add the library for your favorite build system.

For Gradle, I would use,

compile group: 'com.google.code.gson', name: 'gson', version: '2.7'

this can be simplified to,

compile "com.google.code.gson:gson:2.7"

Example#1

We have a class named User

public class User {

    private String firstName;

    private String lastName;

    private int age;

    private List<String> emails;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public List<String> getEmails() {
        return emails;
    }

    public void setEmails(List<String> emails) {
        this.emails = emails;
    }
}

Now let’s create an object of class User and convert the object to String and after that we will create an User object from the String.


User user = new User();
user.setFirstName("Android");
user.setLastName("Studio");
user.setAge(3);
user.setEmails(Arrays.asList("android.studio@gmail.com", "android.studio@android.com"));

// converting the User object to string
Gson gson = new Gson();
String jsonStringUser = gson.toJson(user, User.class);

// Creating the User object from string
User returnedUser = gson.fromJson(jsonStringUser, User.class);

Example#2

Now, suppose, we have a generic class.

public class Employee<T> {

	private T person;

	public T getPerson() {
		return person;
	}

	public void setPerson(T person) {
		this.person = person;
	}
}

Now let’s create an object of class Employee and convert the object to String and after that we will create an Employee object from the String.

User user = new User();
user.setFirstName("Android");
user.setLastName("Studio");
user.setAge(3);
user.setEmails(Arrays.asList("android.studio@gmail.com", "android.studio@android.com"));

Employee<User> employee = new Employee<>();
employee.setPerson(user);

Gson gson = new Gson();

// This will not work
String jsonString = gson.toJson(employee, Employee.class);

We could have put any class object inside Employee. There is no way the JVM will know the object type until runtime. There is a concept of Java type erasure that will explain why the simple Employee.class will not work with Gson. To resolve the issue we need to get the type at runtime using TypeToken

User user = new User();
user.setFirstName("Android");
user.setLastName("Studio");
user.setAge(3);
user.setEmails(Arrays.asList("android.studio@gmail.com", "android.studio@android.com"));

Employee<User> employee = new Employee<>();
employee.setPerson(user);

Gson gson = new Gson();

Type type = new TypeToken<Employee<User>>() {
}.getType();

String jsonString = gson.toJson(employee, type);

// load the value
Employee<User> returnedEmployee = gson.fromJson(jsonString, type);
User returnedUser = returnedEmployee.getPerson();

iOS 9: Beyond the first look


Get all subviews

let childViews = view.subviews

for aView in childViews {
    if aView is UILabel {
        let aLabel = aView as UILabel
    } else if aView is UIButton {
        let aButton = aView as UIButton
    } else if aView is UIImageView {
        let aImageView = aView as UIImageView
    }
}

Table views

  • iOS table view is one column wide (with multiple rows)
  • One row contains one cell. This cell contains the views of that particular row

Let’s create a simple tableview

  1. Drag a Table View (not Table View Controller) into your main view controller.
  2. Resolve auto layout issues (Select Table View => Editor => Resolve Auto Layout Issues => Reset to Suggested Constraints)
  3. Connect dataSource and delegate Outlets to view controller (from Connections Inspector)

UITableViewDataSource controls the following

  • Configuring a table view
  • Inserting or deleting table rows
  • Reordering table rows

Configuring a table view

There are several methods you can use to configure your table view. Look at the apple documentation for elaborated information. For a quick overview look at the list

– tableView:cellForRowAtIndexPath: (Required)
Asks the data source for a cell to insert in a particular location of the table view.

func tableView(_ tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCell

– tableView:numberOfRowsInSection: (Required)
Tells the data source to return the number of rows in a given section of a table view.

func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int

– numberOfSectionsInTableView:
Asks the data source to return the number of sections in the table view.

optional func numberOfSectionsInTableView
(_ tableView: UITableView) -> Int

Implementation of the required methods

class ViewController: UIViewController, UITableViewDataSource {

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel!.text = "Cell number \(indexPath.row)"

        return cell
    }
}

Now we will create some dummy data and make use of the tableview accordingly

class ViewController: UIViewController, UITableViewDataSource {

    let section1Data = ["Section 1a", "Section 1b", "Section 1c", "Section 1d"]
    let section2Data = ["Section 2a", "Section 2b", "Section 2c", "Section 2d", "Section 2e"]
    let section3Data = ["Section 3a", "Section 3b", "Section 3c"]

    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 3
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 0: return section1Data.count
        case 1: return section2Data.count
        case 2: return section3Data.count
        default: return 0
        }
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = UITableViewCell()

        var item : String

        switch indexPath.section {
        case 0:
            item = section1Data [indexPath.row]
            break
        case 1:
            item = section2Data [indexPath.row]
            break
        case 2:
            item = section3Data [indexPath.row]
            break
        default:
            item = ""
        }

        cell.textLabel!.text = item

        return cell
    }

    func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        switch section {
        case 0:
            return "Section 01"
        case 1:
            return "Section 02"
        case 2:
            return "Section 03"
        default:
            return ""
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

UITableViewDelegate controls the following

  • Behaviour
  • Appearance
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    ... 

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        print("User clicked. Section \(indexPath.section) and row \(indexPath.row)")
    }

    func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
        let deleteAction = UITableViewRowAction(style: .Default, title: "Delete", handler: {
            action, indexPath in
            
            // Delete from data
            switch indexPath.section {
            case 0:
                break
            case 1:
                break
            case 2:
                break
            }
            
            tableView.reloadData()
            
            tableView.editing = false
        })
        
        let retActions = [deleteAction]
        return retActions
    }

    ...
}

Alert view

let confirmAlert = UIAlertController(title: "Deleting row", message: "Do you really want to delete?", preferredStyle: .Alert)
let posAction = UIAlertAction(title: "Yes", style: .Destructive, handler: {
    action in
    print("Yes button clicked")
})
let negAction = UIAlertAction(title: "No", style: .Default, handler: nil)
        
confirmAlert.addAction(posAction)
confirmAlert.addAction(negAction)
        
presentViewController(confirmAlert, animated: true, completion: nil)

Add App icon

  • Select Assets.xcassets -> AppIcon -> Attirbutes Inspector
  • You should see all possible image sizes you would like to provide
  • Select each of the icon and see the Image section in Attirbutes Inspector
  • You will see the Size property, but it is in point mode, not pixel. Then you will see the Scale property and know the actual pixel sized image you need to provide. px = pt * scale

Launcher screen

  • Usually launcher screen would be the first screen of the application without any data loaded.
  • You can mimic the controls of the first screen and put those is same fashion in launcher screen.

For more informaiton, follow this guide

Add a new ViewController

  • Add a new ViewController (or, variant) in storyboard
  • Create a new Cocoa Touch Class (Swift file), make it a subclass of UIViewController (or, variant)
  • Select the ViewController from storyboard and open Identity Inspector and link with the Swift file
  • Now we need to make a way to go to the new screen. Let’s review show method first.
    • Create a button in FirstViewController and Control+Drag it to the SecondViewController and select Action Segue (Show)
  • Another way is to create a NavigationViewController.
    • Select the FirstViewController and Editor => Embed In => Navigation Controller
    • For displaying a title, go to viewDidLoad() method and override self.title = “”

Add a new TabbarController

  • We can start by opening a new project with Tabbed Application option
  • Create a new ViewController in storyboard and control+drag from TabbarController (bottom) to new ViewController
  • This time select Relationship Segues (view controllers)
  • Individual tabbar icon or text can be change by selecting that individual tab text/icon.

Auto Layout and handling multiple screens

  • Auto Layout is enabled by default. Still if you need to check, Open storyboard => Show File Inspector => Check (Use Auto Layout)
  • First place the controls in square sized view controller and then make use of Resolve Auto Layout Issues (Bottom-Right corner) => Reset to Suggessted Constraints
  • General suggestion: If you get yellow/red sign in Auto Layout then go to Document Outline screen of storyboard and tap on the top right corner button
  • Clear constraint of a paritcular view by selecting Resolve Auto Layout Issues (Bottom-Right corner) => Clear Constraints
  • For more info: https://developer.apple.com/design/adaptivity/
  • Alternatively you can use a handy library: http://snapkit.io

Stack Views

  • Stack views are much more like LinearLayout in Android. They can either be horizontal or vertical. Unlike LinearLayout they can NOT have any view properties (color etc)
  • Unlike Android, you are supposed to use nested stack views in iOS
  • There are 3 properties in stack views that you need to take care of
    • Alignment
    • Distribution
    • Spacing
  • General suggestion: First place controls roughly on the storyboard
    • Select the controls
    • Editor -> Embed In -> Stack view
    • Xcode should embed the controls with proper subviews. If not you can change it from Attributes Inspector.
  • Selecting stack view from storyboard can be tricky. Also, dragging the stack view to setup it’s width and height will not work as expected. Workaround is, select stack view from Document Outline view. Setup constraint using Pin button (Bottom-right corner)
  • Now play with the 3 attributes (Alignment, Distribution and Spacing) from Attributes Inspector to make things right your choice.
  • You might see some warnings regarding stack views and those might be a false alarm. Select the stack view, go to the Size Inspector and decrement the value of x by one and change it back to original. If it was a false alarm it should go away.

Dynamic layout for different screens

You can take advantage of size classes as it will allow you to design specific layout for specific screen. For example, you can reduce the font size only when the layout will render in iPhone 4 landscape mode. Remember, Auto layout constraints will be applied first than Size classes.

You should see “w any h Any” at the bottom of the storyboard. Tap on it and select a variation. At the bottm of the new dialog you can see the list of the screen this particular variation will take effect.

There are two keywords: Compact and Regular. Here, Compact essentially means, limited. You need to take extra care when you see compact width/height.

For more information, follow this guide

General suggestion: You should play with the size classes at the very end. In addition, you should do the experiments in separate branch, thus you can always revert back to the previous working branch.

After selecting a variant the bottom bar of the storyboard will turn into blue and the storyboard view controller will take the specific device frame that you selected in variant.

Change the font

  • Select the label.
  • Go to Attributes Inspector
  • Do not change the font. Instead, click on the (+) button beside font.
  • Select the proper variation base and you should see a separate font change inputbox
  • Change font and size using the input box.
  • Use Assistance Editor (Preview mode) to see how things will look finally in different devices

Change size of a view

  • Make sure the auto layout constraints have been set.
  • Select the control you would like to change the size.
  • Go to Size Inspector. In the Constraints section, change the constraint value (width/height) by clicked Edit button
  • Check your work in Assistance Editor (Preview mode)
  • You might need to set the auto layout contraints again based on the changes you made

Remove a view/item

  • Select the control you would like to delete.
  • Hit Command + Delete.
  • The control should still be visible in Document Outline view but greyed out