Friday, October 13, 2017

Testing an iOS app on a device

I'm writing this post based on Xcode 8 (and Swift 3). While developing an iOS app, we can test it on either Simulator or an actual device. To test your app on the actual device, your app must be code signed and provisioned because the OS does not allow the untrusted app to run and use certain services (such as In-App Purchases, Game Center, and Push Notification).

Code signing

It is the way to ensure the authenticity of executable code (app). Using code signing, the operating system can know whether or not the app is altered by other developers (or malicious softwares) while in transit. It uses Public Key Cryptography and Hashing algorithm. The app's developer creates a hash value of the program and encrypts it with a private key. The encrypted hash and a public key are embedded with the app and sent to the users. When the users launch the app, the OS implicitly decrypts the hash with the embedded public key and compare it with its computed hash value. If they are not equal, the app was altered. Note that one public key can be used with only one private key.

The sender's or developer's digital certificate contains a public key and other identifying information such as a person's name, a company name, a domain name, and a postal address. The certificate is used to verify that the app is really sent by its developer. The OS verifies the certificate with the trusted Authority (for example, Apple). You can find more details here.

Provisioning

Provisioning is the process of preparing and configuring your app to run on a device and which services your app can access. The provisioning profile is downloaded from your developer account and embedded in the app's bundle. The whole bundle is code-signed. Before the app launches, the OS install the provisioning profile on your device. If the information in the provisioning profile does not match certain criteria, the app won't launch. You can find more details here.

Code signing and provisioning are done in Xcode using the following steps:

1. You must have an Apple developer account, which can be created with Apple ID.
2. Create signing identity and provisioning profile (find more details here)
- Open your project in Xcode and select your project in Project Navigator
- From General tab, fill in information under Identity section. 
3. Connect your device to your Mac machine. From Product menu, select Destination then your device's name. After that, press Command + R to run the project. The app will start on your device.



Wednesday, October 11, 2017

Setting background image for a table view in Swift

The UITableView has backgroundView and backgroundColor properties. You must set backgroundView property to nil if you want to set the background color of the table view. You can set the background view and background color for each cell of the table view too. The cell's backgroundView and backgroundColor properties have higher priorities than the table view's backgroundView and backgroundColor properties.

I'm using Xcode 8 and Swift 3 to develop this demo application. The goal is to set the background image for the table view. I have 3 different background images for different device's screen sizes. We divide the screen sizes into 3 different groups: 1x (for iPhone3G and priors), 2x (for iPhone4, 4s, 5, 5s, 6, 7, 8, and etc.), and 3x (for iPhone 6 Plus, 7 Plus, and 8 Plus). You can find more details here. The reason is the image could lose quality (has jagged edges or looks blur) when it scales down or up. We use one image for one group because there is no much difference between the screen sizes of the devices within the same group. It's ok to scale the image up or down a bit. The background images all have gray background but different texts in the center. The image below shows how it will look like.



1. Create background images for the device's groups

I used Photoshop to create 3 images with resolutions: 1242x2208 pixels (iPhone8 Plus), 750x1334 pixels (iPhone 8), and 320x480 pixels (iPhone 3G). I named them as background@3x.jpg, background@2x.jpg, and background@1x.jpg.

2. Add the background images to the app's bundle

- Open the project in Xcode and double click on Assets.xcassets file to open it in Assets Catalog Document
- Click on the plus sign at the bottom left corner and select New Image Set in the popup.
- Double click on the Image and rename it to background.
- Open Finder and drag the images to the assets catalog in Xcode. 


Note that if you run your application in iPhone 8 plus for example, the system uses 3x image. But if youO run it in iPhone 5 for example, the system use 2x image instead.

3. Set the table view's background image in code

Open the table view controller class and add the following codes to the viewDidLoad() method. In my project, the controller is called ItemViewsController.

let image = UIImage(named: "background", in: nil, compatibleWith: nil)
let imageView = UIImageView(image: image)
tableView.backgroundView = imageView

The background image won't show up because the table view's cell's background color has higher priority. In other words, the cells are in the front. You have to set the cell's background to transparent. It's simple by adding this line of code to the tableView(_:cellForRowAtmethod.

cell.backgroundColor = UIColor.clear







Monday, October 9, 2017

Preventing reordering a row of UITableView

The image below shows how the example application looks like. The last row "No More Items" is not editable though it is by default. (Note that i'm using Xcode 8 and Swift 3)


To prevent a row from being editable, we have to implement two methods: tableView(_:canEditRowAt:) of UITableViewDataSource protocol and tableView(_:targetIndexPathForMoveFromRowAt:toProposedIndexPath) of UITableViewDelegate protocol.

1. Disabling editable style


Implement the method tableView(_:canEditRowAt:) as following:

override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        if isLastRowOfSection(tableView, indexPath) {
            return false
        }
        return true

}

When the method tableView(_:canEditRowAt:) returns false, the deletion control (a minus sign in a red circle at the left of the cell) and moving control (gray horizontal bars at the right of the cell) does not show up for that row/cell (the "No More Items" row in this example). Then, the users can't drag that row to move it up or down anymore.

However, they can still move another editable row to the position of this non-editable row and then this non-editable row will automatically move to the position of the editable row. This is the default behavior of the table view control. Please see the image below.



2. Preventing an editable row from moving to the position of non-editable row


Implement the method tableView(_:targetIndexPathForMoveFromRowAt:toProposedIndexPath
as following:
   
    
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
        if isLastRowOfSection(tableView, proposedDestinationIndexPath) {
            return sourceIndexPath
        }
        return proposedDestinationIndexPath
}
    

When the users drag the editable row over the non-editable row (the last row in this example), the non-editable row does not move. It stays where it is. And the editable row moves back to its original position when the users release it.









Thursday, October 5, 2017

Terminating app due to uncaught exception 'CALayerInvalid', reason: 'layer is a part of cycle in its layer tree'

I'm using Xcode 8 and Swift 3 to develop an application for learning purpose. I have one controller and a table view. Everything was working fine at first. But after some silly modification in Interface Builder by a newbie like me, the app crashed when I started it in the simulator. Below is the full log message.

section=0, numOfRows=10
Item: name=Shiny Mac, serialNumber=Optional("2A725A93"), valueInDollars=70, dateCreated=2017-10-05 09:37:41 +0000
row=0, section=0
2017-10-05 16:37:41.594 Homepwner[62092:2091530] *** Terminating app due to uncaught exception 'CALayerInvalid', reason: 'layer <CALayer: 0x600000036240> is a part of cycle in its layer tree'
*** First throw call stack:
(
0   CoreFoundation                      0x0000000102251b0b __exceptionPreprocess + 171
1   libobjc.A.dylib                     0x00000001018a1141 objc_exception_throw + 48
2   CoreFoundation                      0x00000001022ba625 +[NSException raise:format:] + 197
3   QuartzCore                          0x0000000108867ea2 _ZN2CA5Layer30ensure_transaction_recursivelyEPNS_11TransactionE + 102
4   QuartzCore                          0x0000000108867eed _ZN2CA5Layer30ensure_transaction_recursivelyEPNS_11TransactionE + 177
5   QuartzCore                          0x0000000108867eed _ZN2CA5Layer30ensure_transaction_recursivelyEPNS_11TransactionE + 177
6   QuartzCore                          0x0000000108867eed _ZN2CA5Layer30ensure_transaction_recursivelyEPNS_11TransactionE + 177
7   QuartzCore                          0x000000010886fb3a _ZN2CA5Layer15insert_sublayerEPNS_11TransactionEP7CALayerm + 394
8   QuartzCore                          0x000000010886ff50 -[CALayer addSublayer:] + 180
9   UIKit                               0x0000000102e1d7ad -[UIView(Internal) _addSubview:positioned:relativeTo:] + 1322
10  UIKit                               0x000000010311d9a0 -[UITableViewCell _addSubview:positioned:relativeTo:] + 357
11  UIKit                               0x0000000102e0bcf8 -[UIView(Hierarchy) addSubview:] + 838
12  UIKit                               0x000000010323b340 -[UITableViewCellLayoutManager layoutSubviewsOfCell:] + 2386
13  UIKit                               0x000000010326db12 -[UITableViewCellLayoutManagerValue1 layoutSubviewsOfCell:] + 71
14  UIKit                               0x00000001031271d7 -[UITableViewCell layoutSubviews] + 119
15  UIKit                               0x0000000102e2155b -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1268
16  QuartzCore                          0x0000000108876904 -[CALayer layoutSublayers] + 146
17  QuartzCore                          0x000000010886a526 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 370
18  UIKit                               0x0000000102e0f334 -[UIView(Hierarchy) layoutBelowIfNeeded] + 1108
19  UIKit                               0x0000000102e1608e +[UIView(Animation) performWithoutAnimation:] + 90
20  UIKit                               0x0000000102ecac29 -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 1125
21  UIKit                               0x0000000102ecacf8 -[UITableView _createPreparedCellForGlobalRow:willDisplay:] + 74
22  UIKit                               0x0000000102e9f639 -[UITableView _updateVisibleCellsNow:isRecursive:] + 2845
23  UIKit                               0x0000000102ed3ccc -[UITableView _performWithCachedTraitCollection:] + 111
24  UIKit                               0x0000000102ebae7a -[UITableView layoutSubviews] + 233
25  UIKit                               0x0000000102e2155b -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1268
26  QuartzCore                          0x0000000108876904 -[CALayer layoutSublayers] + 146
27  QuartzCore                          0x000000010886a526 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 370
28  QuartzCore                          0x000000010886a3a0 _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24
29  QuartzCore                          0x00000001087f9e92 _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 294
30  QuartzCore                          0x0000000108826130 _ZN2CA11Transaction6commitEv + 468
31  QuartzCore                          0x0000000108826b37 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 115
32  CoreFoundation                      0x00000001021f7717 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
33  CoreFoundation                      0x00000001021f7687 __CFRunLoopDoObservers + 391
34  CoreFoundation                      0x00000001021dc038 CFRunLoopRunSpecific + 440
35  UIKit                               0x0000000102d5808f -[UIApplication _run] + 468
36  UIKit                               0x0000000102d5e134 UIApplicationMain + 159
37  Homepwner                           0x00000001012bf8a7 main + 55
38  libdyld.dylib                       0x00000001060b065d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb) 

According to http://blog.benjamin-encz.de/post/ios-9-detects-cycles-in-layout-trees/, I found the problem. It's because I assigned the tableView object to the accessoryView property of the UITableViewCell. I knew that because when I selected the UITableViewCell in Interface Builder and opened the Connection Inspector, I saw the accessoryView is connecting to Table View in Outlets section. Please see the image below.



To understand about Outlets and Referencing Outlets sections, you can go to this post. According to this post, the accessoryView property (an instance of UIView) represents the small area to the right of the content view of a table view's cell. See the image below.


The table view has a cell. The cell has an accessory view. But, I made the table view as the accessory view of its own cell. This does not make sense and make a circular relationship between the table view and its cell.

The solution is deleting the connection between the accessoryView and Table View from the Outlets section.







Understanding Outlets and Referencing Outlets sections in Interface Builder

I'm using Xcode 8 and Swift 3. I created a single view application, in which there was one table view controller (an instance of UITableViewController). Here is how it looks like in the Interface Builder.


By default, there is one outlet and two referencing outlets come with the UITableViewController such as view and dataSource and delegate, respectively. UITableViewController class inherits from UIViewController and so it inherits view property from UIViewController. The view property is of type UIView, but the table view controller initializes it with an object of UITableView named tableView. In the image above, the outlet view is connecting to Table View. Therefore, the Outlets section tells us the inner objects or the properties of the selected object (or control in Interface Builder) are assigned with what objects (or controls).

The UITableView class has dataSource and delegate properties. In Referencing Outlets section, the dataSource and delegate are referencing the Table View control. It means that the Table View, which is the inner control of the Items View Controller control, has two outlets. In other words, the tableView property of the ItemsViewController class has two properties: dataSource and delegate. However from the image above, we can't see which objects were assigned to those two properties. To see it, we have to select on the Table View control and open the Connection Inspector then look under the Outlets section. The image below shows that those two properties were referencing to Items View Controller control or ItemsViewController object.




UITableView does not show all items

I'm using Xcode 8 and Swift 3 to develop an application for learning purpose. The app used UITableView to display a list of items. I had 20 items but only 12 of them were shown in the simulator. I tried to scroll down to the bottom but no more items. Note that I'm using Macbook Air 11'' mid 2013. I tried to scroll in the simulator using 2 fingers as I always did on browsers and other applications.

After that, I found that it was not the problem with my codes. Everything was fine. It's just the behavior of UITableView. It loads only some items to display which can fit the device's screen. It would show more if you use 3-finger scroll. 3-finger scroll in the simulator works the same as 1-finger tap on mobile device.