Friday, September 18, 2015

Country Selection with Swift in iOS 8


Beberapa bulan terakhir ini, saya mulai mempelajari dan mengembangkan aplikasi iOS menggunakan bahasa Swift. Agar tidak lupa sekaligus menguji pengalaman yang saya peroleh, saya ingin mulai menuangkannya ke topik artikel-artikel saya ke depan. Pada artikel kali ini saya ingin membahas bagaimana cara membuat pilihan negara pada sebuah form menyerupai cara memilih negara pada Address Book iOS. Untuk lebih terbayang tujuan artikel ini, silahkan lihat video berikut ini:

Skenario yang ada pada video tersebut adalah pada scene pertama aplikasi menampilkan scene Edit Profile, di mana salah satu field profile adalah memilih negara. User kemudian memilih negara, lalu transisi ke scene selanjutnya yang muncul dari bawah berupa pilihan negara. Scene pemilihan negara ini tidak hanya berupa daftar negara saja, namun seperti pada Address Book dilengkapi juga dengan indeks huruf depan masing-masing nama negara untuk mempermudah navigasi user.
Gambar 1. Potongan Storyboard

 Storyboard yang saya gunakan pada dasarnya terdiri dari 3 scenes:
  1. EditProfileViewController, subclass dari TableViewController yang saya gunakan untuk scene edit profile.
  2. CountryHelperTableViewController, subclass dari TableViewController yang saya gunakan untuk scene dari pemilihan negara itu sendiri.
  3. NavigationViewController, scene ini bertugas menjembatani dengan EditProfileViewController. NavigationViewController ini berfungsi agar kita dapat melakukan transisi modal dengan lebih baik.
Buat sebuah Swift file bernama CountryHelperTableViewController.swift
import UIKit

class CountryHelperTableViewController: UITableViewController {
    
    var countries: [String]?
    var sectionTitles: [String]?
    var selectedCountryRowId: Int = 0
    var selectedCountryOffset: Int = 0
    var selectedSectionTitleRowId: Int = 0
    var selectedCountry: String?
    var generatedRows: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        populateCountries()
        populateSectionTitles()
        setInitialCountrySelection()
        setSelectedCountryOffset()
    }
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        let indexPath = NSIndexPath(forRow: selectedCountryOffset, inSection: selectedSectionTitleRowId)
        self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Middle, animated: true)
    }
    
    func populateCountries() {
        countries = [String]()
        
        for code in NSLocale.ISOCountryCodes() as! [String] {
            let id = NSLocale.localeIdentifierFromComponents([NSLocaleCountryCode: code])
            let name = NSLocale(localeIdentifier: "en_US").displayNameForKey(NSLocaleIdentifier, value: id) ?? "Country not found for code: \(code)"
            countries!.append(name)
        }
        
        countries!.sort({ $0 < $1 })
    }
    
    func populateSectionTitles() {
        var countrySections = [String: String]()
        
        for country in countries! {
            countrySections[country[0]] = country[0]
        }
        
        var sortedCountrySections = [String]()
        
        for (key, value) in countrySections {
            sortedCountrySections.append(key)
        }
        
        sortedCountrySections.sort({ $0 < $1 })
        sectionTitles = sortedCountrySections
    }
    
    func setInitialCountrySelection() {
        for (index, country) in enumerate(countries!) {
            if country == selectedCountry {
                selectedCountryRowId = index
                
                for (sectionIndex, sectionTitle) in enumerate(sectionTitles!) {
                    if selectedCountry![0] == sectionTitle {
                        selectedSectionTitleRowId = sectionIndex
                        break
                    }
                }
                
                break
            }
        }
    }
    
    func setSelectedCountryOffset() {
        var offset: Int = 0
        
        for country in countries! {
            if country[0] == self.sectionTitles![selectedSectionTitleRowId] {
                if selectedCountry != country {
                    offset++
                } else {
                    break
                }
            }
        }
        
        selectedCountryOffset = offset
    }

    override func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
 if section == tableView.numberOfSections() - 1 {
            return UIView(frame: CGRectMake(0, 0, 1, 1))
 } else {
     return nil
 }
    }

    override func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        if section == tableView.numberOfSections() - 1 {
     return 1
        } else {
            return 0
        }
    }
    
    func rowsInPreviousSection(section: Int) -> Int {
        var rows = 0
        
        for country in self.countries! {
            if self.sectionTitles![section] == country[0] {
                break
            } else {
                rows++
            }
        }
        
        return rows
    }
    
    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return self.sectionTitles!.count
    }
    
    override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return self.sectionTitles![section]
    }
    
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let indexTitle = self.sectionTitles![section]
        var numberOfRows = 0

        for country in self.countries! {
            if indexTitle == country[0] {
                numberOfRows++
            }
        }
        
        return numberOfRows
    }
    
    override func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
        return index
    }
    
    override func sectionIndexTitlesForTableView(tableView: UITableView) -> [AnyObject]! {
        return self.sectionTitles
    }
    
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let row = indexPath.row
        let section = indexPath.section
        let prevSectionRows = rowsInPreviousSection(section)
        let cell = self.tableView.dequeueReusableCellWithIdentifier("CountryCell") as! UITableViewCell
        
        cell.textLabel?.text = countries![row + prevSectionRows]
        
        if selectedCountryRowId == row + prevSectionRows {
            cell.accessoryType = UITableViewCellAccessoryType.Checkmark
        } else {
            cell.accessoryType = UITableViewCellAccessoryType.None
        }
        
        return cell
    }
    
    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        selectedCountryRowId = indexPath.row + rowsInPreviousSection(indexPath.section)
        tableView.reloadData()
    }
    
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "editProfileUnwindSegueWithValue" {
            if let destinationViewController = segue.destinationViewController as? EditProfileViewController {
                destinationViewController.selectedCountry = countries![selectedCountryRowId]
                destinationViewController.updateSelectedValues()
            }
        }
    }
    
}

Setelah selesai membuat class CountryHelperTableViewController, assign class tersebut ke scene paling kanan. Buat juga String extension seperti berikut ini untuk fungsi mengambil indeks huruf pertama pada nama negara.
import Foundation

extension String {
    
    subscript (i: Int) -> Character {
        return self[advance(self.startIndex, i)]
    }
    
    subscript (i: Int) -> String {
        return String(self[i] as Character)
    }
    
    subscript (r: Range) -> String {
        return substringWithRange(Range(start: advance(startIndex, r.startIndex), end: advance(startIndex, r.endIndex)))
    }
}

Method tableView(_:viewForFooterInSection:) dan tableView(_:heightForFooterInSection:) digunakan agar garis border horizontal dari TableView berakhir pada baris atau nama negara terakhir di dalam array. Perilaku default jika kedua method ini tidak diaplikasikan adalah garis border horizontal akan terus berulang hingga batas layar. Saya lebih suka jika garis horizontal berakhir sesuai dengan data.

Method viewDidLoad() dipanggil pada saat pertama kali saja untuk me-load daftar negara dari library Swift melalui method populateCountries(). Setelah negara-negara ter-load, kita membutuhkan daftar indeks huruf pertama dari masing-masing negara yang ter-load dengan menggunakan method populateSectionTitles(). Method setInitialCountrySelection() digunakan untuk menentukan posisi indeks yang sudah pernah dipilih oleh User dengan nilai default 0 jika User belum pernah melakukan pemilihan negara. Sedangkan method setSelectedCountryOffset() digunakan untuk menentukan offset ke-berapakah negara yang terpilih berdasarkan dari indeks section title. Method ini bermanfaat untuk menentukan TableViewCell manakah yang perlu ditandai dengan checkmark.

Method viewWillAppear() ini pada dasarnya bertujuan untuk melakukan autoscroll terhadap daftar negara, agar fokus dari view berada di tengah-tengah sesuai dengan negara yang pernah dipilih oleh User sebelumnya. Pada method ini tidak perlu lagi me-load daftar negara yang lengkap, karena daftar negara sudah di-load oleh viewDidLoad() untuk pertama kalinya. 

Method tableView(_:numberOfRowsInSection:) merupakan salah satu fungsi penting dalam menampilkan  daftar negara ini. Method inilah yang akan menyaring negara-negara yang sesuai dengan indeks huruf pertamanya. Sementara method tableView(_:sectionForSectionIndexTitle:) dan method sectionIndexTitlesForTableView() berguna untuk menampilkan bar horizontal di bagian kanan yang scene sebagai indeks huruf pertama dari masing-masing negara.

Sekian artikel saya tentang pengembangan iOS kali ini, semoga bermanfaat!

No comments:

Post a Comment