Saturday, July 27, 2019

Introduction to UINavigationController

What is it?


A navigation controller is a container view controller which manages one or more child view controllers in a navigation interface. The iOS's Settings app is an example of navigation controller.



The below image shows the objects managed by a navigation controller. It stores child view controllers in an array. Pushing a view controller to the array make the view controller's view presented. The last view controller in the array or stack displays over the others.



Why use it?


It provides a simple way to navigate between the view controllers with a pre-defined navigation interface. For example, it has many ways to present a child view controller and a navigation bar consisted of a back button to go to the previous view controller.


How to do it?


I have MyCalculator app with a Settings page as shown in the image below.



1). Embed the view controller in a navigation controller


Below is SettingsViewController defined in the Interface Builder (IB).


Click on the view controller and select Editor menu then Embed In > Navigation Controller. It then would look like this:



2). SettingsViewController.swift


import UIKit

class SettingsViewController: UITableViewController {
    
    typealias Item = (cellLabel: String, vcId: String?)
    
    private var items: [Item] = [
        (cellLabel: "Themes", vcId: "vcTheme"),
        (cellLabel: "Decimal Places", vcId: "vcDecimalPlaces"),
        (cellLabel: "Decimal Notation", vcId: "vcDecimalNotation"),
        (cellLabel: "Disable Auto-Lock", vcId:  nil)
    ]

    private var tapGestureRecognizer = UITapGestureRecognizer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.register(UINib(nibName: "SwitchTableViewCell", bundle: nil), forCellReuseIdentifier: "tbcSwitch")
        tableView.addGestureRecognizer(tapGestureRecognizer)
        tapGestureRecognizer.addTarget(self, action: #selector(tapGestureHandler(_:)))
        
        let closeButton = UIBarButtonItem(title: "Close", style: .done, target: self, action: #selector(closeButtonHandler(_:)))
        navigationController?.navigationBar.topItem?.rightBarButtonItem = closeButton
    }
    
    @objc public func closeButtonHandler(_ sender:UIBarButtonItem) {
        dismiss(animated: true, completion: {})
    }
    
    @objc public func tapGestureHandler(_ sender: UITapGestureRecognizer) {
        let tappedPoint = sender.location(in: tableView)
        if let indexPath = tableView.indexPathForRow(at: tappedPoint) {
            let item = items[indexPath.row]
            if let vcId = item.vcId {
                let vc = storyboard?.instantiateViewController(withIdentifier: vcId)
                vc?.title = item.cellLabel
                navigationController?.pushViewController(vc!, animated: true)
            }
        }
    }

    ...

    // Required tableView() methods 

}
    

I'm using TapGestureRecognizer to detect the selected row in the Settings page and then present the corresponding view controller.

The items variable stores identifiers of the child view controllers designed in the Interface Builder. The selected identifier is used to instantiate the corresponding view controller and present it, by pushing it to the navigation controller's array.

navigationController?.pushViewController(vc!, animated: true)
    


3). Add action to the Settings button/icon in the home page


Suppose I have ViewController for the home page designed in the Interface Builder and its file named ViewController.swift. Control-drag the Settings button in the ViewController from the Storyboard to the ViewController.swift file and create an action connection.

    @IBAction func settingsButtonTouched(_ sender: SettingsButton) {
        present(settingsViewController, animated: true, completion: {})
    }    

After that, add a variable to the ViewController class as following:

    private var settingsViewController:UIViewController!    
    

Then, initialize it in viewDidLoad() method:
    
    settingsViewController = storyboard?.instantiateViewController(withIdentifier: "vcNavigation") as! UINavigationController
    

Note that we present the SettingsViewController from the home page, not its navigation controller. The navigation controller, as its name implied, just navigates the child view controllers of the SettingsViewController.





References


Introduction to a property list with Swift

What is a property list?


A property list is a file system with the extension plist, which represents an object graph.


When to use it?


It's portable and efficient to store a small amount of data. It should not be used to store a complex object graph. If the data is not likely to change, should we hard-code (store the data with the source code) it instead? For best practices, data should be stored separately from the source code because it's less error prone and we don't need to recompile the program every time when we change the data.


Reading a property list into an array (of a dictionary)


    private var _decimalPlaces:[DecimalPlace] = []
    var decimalPlaces:[DecimalPlace] {
        get {
            if _decimalPlaces.isEmpty {
                do {
                    let dp = try loadAsArray(filename: "DecimalPlaces", format: .xml)
                    _decimalPlaces = toDecimalPlaces(dp)
                } catch {
                    fatalError("Failed to load decimal places from property list file.")
                }
            }
            return _decimalPlaces
        }
    }
    
    func loadAsArray(filename: String, format: PropertyListSerialization.PropertyListFormat = .xml) throws -> [AnyObject] {
        var result:[AnyObject] = []
        if let path = Bundle.main.path(forResource: filename, ofType: "plist") {
            if let xml = FileManager.default.contents(atPath: path) {
                var plistFormat = format
                result = try PropertyListSerialization.propertyList(from: xml, options: .mutableContainersAndLeaves, format: &plistFormat) as! [AnyObject]
            }
        }
        return result
    }
    
    private func toDecimalPlaces(_ src:[AnyObject]) -> [DecimalPlace] {
        var result:[DecimalPlace] = []
        for i in 0..

The source code above works with the property list file, DecimalPlaces.plist, with the contents below. It's in the root directory of the project like Info.plist file.


Below is the content of DecimalPlace class:
    
class DecimalPlace {
    var id:Int!
    var value:UInt?
    var label:String!
    var description:String!
    
    init() {       
    }
}
    


Reading a property list into a dictionary


    func load(filename: String, format: PropertyListSerialization.PropertyListFormat = .xml) throws -> [String:AnyObject] {
        var result:[String:AnyObject] = [:]
        if let path = Bundle.main.path(forResource: filename, ofType: "plist") {
            if let xml = FileManager.default.contents(atPath: path) {
                var plistFormat = format
                result = try PropertyListSerialization.propertyList(from: xml, options: .mutableContainersAndLeaves, format: &plistFormat) as! [String:AnyObject]
            }
        }
        return result
    }
    

The code snippets above works with the property list file with the contents below:






References
https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/PropertyList.html

Thursday, July 18, 2019

Embedding a view controller's root view in another view controller's view

We can add a view controller's root view to another view controller's view for reusability purpose.


We can programmatically add a view controller's root view to a subview of another view controller as following:

public class ThemesViewController : UIViewController {

 @IBOutlet weak var previewView: UIStackView!
 var vcMain:ViewController!

 ...

 public override func viewDidLoad() {
   vcMain = storyboard?.instantiateViewController(withIdentifier: "vcMain") as! ViewController
 } 
 
 public override func viewWillAppear(_ animated: Bool) {
                // The system does not wait until each view in the hierarchy
                // has updated its layout before calling the viewWillAppear() method
                // so some subviews still have incorrect bounds
                // and will change in viewDidAppear() method.
                // Calling layoutIfNeeded() method forces the previewView to
                // update its layout immediately so we get the correct bounds here.
                previewView.layoutIfNeeded()   
        
                self.addChildViewController(vcMain)
  previewView.addSubview((vcMain.view)!)
  vcMain.didMove(toParentViewController: self)
 }
}
    

The addChildViewController() method calls the willMove(toParentViewController:) of the child view controller for us so we can override the method and do more things if needed.

The didMove(toParentViewController: self) method of the child view controller have to be called after the child view controller's root view has been added to the parent view controller's view and any necessary constraints between the views have been defined. This method gives the chance to the child view controller to perform any actions if needed, responding to the change in view ownership (by overriding the method). Note that my project still worked correctly even if I didn't call didMove(toParentViewController:) method.

In my case, the size and position of the previewView were already set in the Interface Builder so I don't have to programmatically create constraints for the previewView, after calling previewView.addSubview((vcMain.view)!) as mentioned here.


Monday, July 8, 2019

Introduction to User's Defaults Database

To store user's preferences in an app, we can use the user's defaults database, which will be created by the system on each device (iOS or Mac OS) by the time we access it. Note that the database is not suitable for large data and heavy transactions. Preferences are grouped into domains. There are 5 different types of domains:
1). The argument domains (UserDefaults.argumentDomain): Volatile
2). Application (identified by the app's identifier, for example, com.vathanakmao.MyCalculator): Persistent, should be used by their corresponding apps.
3). The global domains (UserDefaults.globalDomain): Persistent, used by system frameworks and accessed by different apps.
4). The languages domains: Volatile
5). The registration domain (UserDefaults.registrationDomain): Volatile

The lifetime of a preference depends on the domain, in which it is stored in. Some domains store preferences persistently, which means that the preferences continue to exist across app launches. Some others store preferences in more volatile way (shorter life), keeping preference values only for the life of the corresponding user defaults object (the user defaults object may be destroyed by the system or when the app exits).

For example, calling UserDefaults.standard.register(default: [String, Any]) method stores the preference dictionary object in the registration domain, and the object is deleted when the app exits. We can test it while in development by adding the code in the AppDelegate class and run the project in the simulator from Xcode. After that, quit the simulator and remove the code from the AppDelegate then run the project again. In contrast, calling the UserDefaults.standard.set(_,forKey)method stores the preference object persistently, which means that the object can still be accessed in the next sub sequent project launches even if after the method call is removed.

Storing value


UserDefaults.standard.set(1000, forKey: "item1")
    

It's recommended that the value should be as simple as possible for better performance even though the Data type is supported. The value of a primitive type would be ideal.

Getting value


UserDefaults.standard.integer(forKey: "item1")
    

If the object does not exist, the returned value falls back to the default value of its corresponding type. It would be 0 (zero) in the case above (, and false in case it's Bool)

The system searches for the key in all domains one by one based on their orders listed above until the key is found.

Registering default value for certain key


For example, calling UserDefaults.standard.integer(forKey) method on non-existent key would return 0 (zero), but 0 is meaningful in our case and we want it to return a different number like -1 (minus one). We should register the default value for that key in AppDelegate class as following:
    
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
 let preferences = ["item1", -1]
 UserDefaults.standard.register(defaults: preferences)
}
    



References
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/UserDefaults/AboutPreferenceDomains/AboutPreferenceDomains.html#//apple_ref/doc/uid/10000059i-CH2-SW6