In an earlier article I had discussed how we can create our own frameworks to easily share reusable code. In this article we will take this a little further and create our own reusable UI Components.
Points to Note:
- The reusable component we will be creating is based on UIKit. For that reason this component can only be used in iOS Apps. However, you can follow the same steps to create a reusable component for macOS using Cocoa.
- UI Components, distributed through a framework, do not render in the project storyboard file.
- You should be familiar with creating Embedded Binaries (your own Framework). If you aren’t then please read this article first.
- These projects are created on Xcode 10 with Swift 4.2
Getting Started
We will complete the implementation as a 2 step process. Here is the screen that we are planning to implement.
Creating the Reusable Framework
- Open Xcode and create a new Framework project.
- Name the project “UIVIdentityCard”.
- Save the project in any folder.
- Create a new Swift file File > New > File > Swift.
- Name the file “GenderType.swift”. This is where we will declare the enum that holds the Gender type that we are creating.
- Add the following code to the file.
import Foundation /** Possible scores that can be given. *values* `Male` `Female` `NotSpecified` *functions* `func toString() -> String` Used to get the `String` version of the value - Author: Arun Patwardhan - Version: 1.0 */ public enum GenderType { case Male case Female case NotSpecified } /** This extension adds the Enum to String converions capability - Author: Arun Patwardhan - Version: 1.1 */ extension GenderType { /** This function converts from enum value to `String` - important: This function does not do validation - returns: `String`. - requires: iOS 11 or later - Since: iOS 11 - author: Arun Patwardhan - copyright: Copyright (c) Amaranthine 2015 - version: 1.0 */ @available(iOS, introduced: 11.0, message: "convert to String") func toString() -> String { switch self { case .Male: return "Male" case .Female: return "Female" case .NotSpecified: return "Not Specified" } } }
- Create a new Swift file called “PersonDetailsModel.swift”.
- Add the following code to the file.
import Foundation /** This struct represents the data that is to be shown in the ID card **Variables** `personName` `personIcon` `personDob` Date of Birth `personAddress` `personPhone` `personEmail` `personCompany` `personHeight` `personWeight` `personGender` **Important** There is a variable with the name `entryCount`. This variable keeps tracks of the number of stored properties that exist. The value of this variable will be used to determine the number of rows in the table.The computed property `numberOfRows` is the property used to access the value of `entryCount`. - Author: Arun Patwardhan - Version: 1.0 */ public struct PersonDetailsModel { internal var entryCount : Int = 7 public var personName : String = "" public var personIcon : UIImage public var personDob : Date public var personAddress: String = "" public var personPhone : String = "" public var personEmail : String = "" public var personCompany: String = "" public var personHeight : Double? = 0.0 { willSet { if newValue == nil & personHeight != nil { entryCount -= 1 } else if newValue != nil & personHeight == nil { entryCount += 1 } } } public var personWeight : Double? = 0.0 { willSet(newValue) { if newValue == nil & personWeight != nil { entryCount -= 1 } else if newValue != nil & personWeight == nil { entryCount += 1 } } } public var personGender : GenderType? { willSet { if newValue == nil & personGender != nil { entryCount -= 1 } else if newValue != nil & personGender == nil { entryCount += 1 } } } public var numberOfRows : Int { return entryCount } public init(withName newName : String, icon newIcon : UIImage, birthday newDob : Date, address newAddress : String, phone newPhone : String, email newEmail : String, Company newCompany : String, height newHeight : Double?, weight newWeight : Double?, andGender newGender : GenderType?) { personName = newName personIcon = newIcon personDob = newDob personAddress = newAddress personPhone = newPhone personEmail = newEmail personCompany = newCompany if newGender != nil { entryCount += 1 } if newWeight != nil { entryCount += 1 } if newHeight != nil { entryCount += 1 } personHeight = newHeight personWeight = newWeight personGender = newGender } } /** This extension adds protocol conformance for the `CustomStringConvertible` protocol. - Author: Arun Patwardhan - Version: 1.1 */ extension PersonDetailsModel : CustomStringConvertible { public var description: String { return """ NAME: \(self.personName) DATE OF BIRTH:\(self.personDob) ADDRESS: \(self.personAddress) EMAIL:\(self.personEmail) PHONE:\(self.personPhone) """ } }
- Now we will focus out attention on the View. Create a new file File > New > File > View.
- Name the view “UIVIdentityCard.swift”.
- Design the view as shown in the screenshot below.
- Create the corresponding“UIVIdentityCard.swift” file.
- Make the IBOutlet & IBAction connections for the different UI elements.
- Add the following code. This is how your file should look after its completed.
/** The UIVIdentityCard class **Functions** `public func load(data newPerson : PersonDetailsModel)` Used to load the data for the view. - Author: Arun Patwardhan - Version: 1.0 */ @IBDesignableopen class UIVIdentityCard: UIView, UITableViewDelegate, UITableViewDataSource { //IBOutlets -------------------------------------------------- @IBOutlet public weak var personIcon : UIImageView! @IBOutlet public weak var personName : UILabel! @IBOutlet public weak var personDetails : UITableView! //Variables -------------------------------------------------- public var localTableData : PersonDetailsModel! let nibName : String = "UIVIdentityCard" var view: UIView! let cellIdentifier : String = "IDCard" //Functions -------------------------------------------------- /** This function does the initial setup of the view. There are multiple things happening in this file. 1) The first thing that we do is to load the Nib file using the `nibName` we saved above. The UNIb object contains all the elements we have within the Nib file. The UINib object loads the object graph in memory but does not unarchive them. To unarchive them and get the ibjects loaded completely for use we have to instatiate the object and get the arry of top level objects. We are however interested in the first object that is there in the array which is of type `UIView`. The reference to this view is assigned to our local `view` variable. 2) Next we specify the bounds of our view 3) Finally we add this view as a subview - important: This function does not do validation - requires: iOS 11 or later, the varibale that contains the name of the nib file. - Since: iOS 11 - author: Arun Patwardhan - copyright: Copyright (c) Amaranthine 2015 - version: 1.0 */ @available(iOS, introduced: 11.0, message: "setup view") func setup() { //1) self.view = UINib(nibName: self.nibName, bundle: Bundle(for: type(of: self))).instantiate(withOwner: self, options: nil)[0] as! UIView //2) self.view.frame = bounds //3) self.addSubview(self.view) } public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if let count = localTableData?.entryCount { return count - 2 } return 0 } public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { var cell : UITableViewCell? = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) if nil == cell { cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier) } switch indexPath.row { case 0: let formatter = DateFormatter() formatter.dateStyle = .medium cell?.textLabel?.text = "Birthday\t: "+ formatter.string(from: (localTableData?.personDob)!) case 1: cell?.textLabel?.text = "Email\t: " + localTableData.personEmail case 2: cell?.textLabel?.text = "Phone\t: " + localTableData.personPhone case 3: cell?.textLabel?.text = "Address\t: " + localTableData.personAddress case 4: cell?.textLabel?.text = "Company\t: " + localTableData.personCompany case 5: cell?.textLabel?.text = "Gender\t: " + \(localTableData.personGender?.toString())! case 6: cell?.textLabel?.text = "Height\t: \((localTableData.personHeight)!)" case 7: cell?.textLabel?.text = "Weight\t: \((localTableData.personWeight)!)" default: print("error") } cell?.textLabel?.font = UIFont.boldSystemFont(ofSize: 12.0) cell?.textLabel?.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) return cell! } //Inits -------------------------------------------------- override public init(frame: CGRect) { super.init(frame: frame) self.setup() } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.setup() } override open func layoutSubviews() { super.layoutSubviews() } } /** This extension adds the function to load data - Author: Arun Patwardhan - Version: 1.1 */ extension UIVIdentityCard { /** This function loads the data for the view - important: This function does not do validation - parameter newPerson: This is the object representing the person whose information will be displayed on the screen. - requires: iOS 11 or later - Since: iOS 11 - author: Arun Patwardhan - copyright: Copyright (c) Amaranthine 2015 - version: 1.0 */ @available(iOS, introduced: 11.0, message: "load data") public func load(data newPerson : PersonDetailsModel) { self.localTableData = newPerson self.personIcon.image = localTableData.personIcon self.personName.text = localTableData.personName self.personDetails.reloadData() } }
- Add the placeholder image for the image view.
- Select any of the simulators from the list.
- Press ⌘ + B to build the project.
- From the Project navigator select the Framework file.
- Control click and select “Show in Finder”.
- Copy the framework to the “Desktop”.
We are done creating the reusable framework. We will not shift our focus towards testing this framework.
Using the Framework in a project
Let us now test the framework we created. We will do this by incorporating the code in our iOS App.
- Create a new project. Call it “IdentityCardTest”.
- Save the file in a folder of your choice.
- Select the Project file and Embed the Framework into your project.
- Add an image to your project, this will be the image that will be displayed in your custom view.
- Switch to the Main.storyboard file. Drag a UIView into the ViewControllers view.
- Set its identity to the UIVIdentityCard in the identity inspector. Also set its module to UIVIdentityCard.
- Create an IBOutlet for this custom view.
- Switch to the ViewController.swift file. Import the UIVIdentityCard framework at the top of the file.
- Add the following code to the file. We will be creating test data and displaying it on the screen using the Custom view we just designed.
//Functions -------------------------------------------------- /** This function prepares and loads the data that is to be shown in the custom view - important: This function does not do validation - requires: iOS 11 or later, the UIVIdentityCard framework. - Since: iOS 11 - author: Arun Patwardhan - copyright: Copyright (c) Amaranthine 2015 - version: 1.0 */ @available(iOS, introduced: 11.0, message: "prepares data to be shown on the ID card") func prepareIDCard() { let displayData : PersonDetailsModel = PersonDetailsModel(withName: "Arun Patwardhan", icon: UIImage(named: "iconHolder.png")!, birthday: Date(timeIntervalSince1970: 44_97_12_000), address: "Mumbai, Maharashtra, India", phone: "91-22-26486461", email: "arun@amaranthine.co.in", Company: "Amaranthine", height: 5.11, weight: nil, andGender: GenderType.Male) myIDCard.load(data: displayData) }
- Your completed ViewController.swift should look like this.
- Run the project. See if the view loads the way we wish.
Link to Sample Code
https://github.com/AmaranthineTech/ReusuableUIFramework