Friday, September 27, 2019

Using Expressions in LLDB

Often times we would want to hold some temporary variables when debugging with lldb. We can make use of expression in such cases. An expression, short form e or expr evaluates the given statement. If we use an assignment with a variable starting with $, that variable will be available through out and can be used in other expressions.
(lldb) expr NSRange $range1 = NSMakeRange(0, 3);
(lldb) expr [@"Hi Olive" substringWithRange:(NSRange)$range1];
(NSTaggedPointerString *) $9 = 0x7a858d27bb2607ed @"Hi "
Since we are in single-line expression mode, there are slight differences in the way the expressions work. We need to cast NSRange. A better option in this case is to use the multi-line expression mode. For that, first enter expr↩ followed by a carriage return. The lldb is now in multi-line expression mode where we can enter normal Objective-C (or Swift) code with each line terminated with a semi-colon and an empty line after the final statement will evalute all the expressions and exit the mode.
(lldb) expr
Enter expressions, then terminate with an empty line to evaluate:
1 NSRange range = NSMakeRange(9, 4);
2 NSString *str = @"Slice of Life";
3 [str substringWithRange:range];
4
(NSTaggedPointerString *) $4 = 0x7a858d42fd26039d @"Life"
(lldb)
Multi-line expression with lldb makes debugging much better.

Paginate a UITableView with a Bottom Loader

 We can paginate a UITableView by implementing the tableView:willDisplayCell:forRowAtIndexPath method. In it, we can check if the displayed row index is nearing the last row in the model and if so, we fetch new data and append it to the model and reload the table view.

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == self.dataList.count - self.dataOffset && self.shouldFetchMore && !self.isDataLoading {
UI.showLoadingForTableView(self.tableView) // Display loader at the bottom
self.api.fetchSomeData(offset: self.dataList.count, max: Const.dataFetchMaxLimit, success: { xs in
DispatchQueue.main.async {
UI.hideLoadingForTableView(self.tableView) // Hide loader
self.isDataLoading = false
self.dataList.append(contentsOf: xs)
self.reloadTableView()
if xs.count == 0 { self.shouldFetchMore = false } // If data returned is 0 => there are no more data to fetch
}
self.lastFetchedIndex += Constants.dataFetchMaxLimit // Increment the fetch index to include current offset
}, error: { _ in
self.shouldFetchMore = true
self.isDataLoading = false
DispatchQueue.main.async {
UI.hideLoadingForTableView(self.tableView)
self.reloadTableView()
}
})
}
}
Here, dataList is the table's data model, dataOffset is a constant, say 5 which means, if there are only five more cells to be displayed, paginate. The shouldFetchMore variable indicates if there are more data available to paginate and isDataLoading indicates if a fetch is already in progress.

We can show a loader activity view at the bottom of the table view to indicate that more data is being fetched.
class UI {
/// A loading indicator snipper
private static var spinner: (UITableView) -> UIActivityIndicatorView = { tableView in
let s = UIActivityIndicatorView(style: .gray)
s.startAnimating()
s.frame = CGRect(x: CGFloat(0), y: CGFloat(0), width: tableView.bounds.width, height: CGFloat(44))
return s
}

/// Show loading spinner at the bottom of the given table view
static func showLoadingForTableView(_ tableView: UITableView) {
tableView.tableFooterView = self.spinner(tableView)
tableView.tableFooterView?.isHidden = false
}

/// Hide loading spinner for the given table view if present
static func hideLoadingForTableView(_ tableView: UITableView) {
tableView.tableFooterView?.subviews.forEach({ view in
if view == self.spinner(tableView) {
view.removeFromSuperview()
}
})
tableView.tableFooterView?.isHidden = true
}
}


Tuesday, September 24, 2019

Manage Multiple Configs in Xcode with a Single Target

When developing apps that connect to a server, mostly we have a staging server and a production server. As such we need two app versions, one the dev version that connects to the staging server and the other that connects to the prod. These apps could have different configs to load, especially if it's based on Firebase. Firebase provides a GoogleService-Info.plist file for each project. Below demonstrates how we will use this as a use-case to further demonstrate working with multiple app configurations, schemas and Xcode config file, but with a single target.


1. First, download the Firebase config for both dev and prod to Config folder under the project root and name them as GoogleService-Dev-Info.plist and GoogleService-Prod-Info.plist respectively.


2. Under the Xcode configuration for the project, click on the Project > Info and under Configurations, we need to add one each for Dev Debug and Prod Release. For that, from Editor choose Add Configuration > Duplicate "Debug" Configuration and name it Dev Debug and follow the same for prod, but use Duplicate "Release" Configuration option.


3. Now under the Build Settings tab, we will add an ENV flag. For that click the plus icon next to Levels in the header menu and choose Add User-Defined Settings with the following values.
Dev Debug  DEV_DB
Dev Release DEV_RELEASE
Prod Debug PROD_DBG
Prod Release PROD_RELEASE

4. Next click on the app name under the Target section and choose Build Phases and add a new Run Script Phase with the following content.
if [ "$ENV" == "DEV_DBG" ]; then
echo "Dev debug config found"
cp -r "${PROJECT_DIR}/Config/GoogleService-Dev-Info.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
echo "Firebase staging plist copied."
elif [ "$ENV" == "PROD_RELEASE" ]; then
echo "Prod release config found."
cp -r "${PROJECT_DIR}/Config/GoogleService-Prod-Info.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
echo "Firebase production plist copied."
fi

This will copy the right plist and place it under the .app folder with plist renamed to GoogleService-Info.plist. This makes the call to [FIRApp configure]; to work without having to manually load the plist and specify the credential info.

5. Next, we will create multiple schemes, one for building the staging version and the other for prod. For that, click on the target next to the run-stop, button and choose New Scheme. For the target, choose the target app and let's name the schemes as Dev Debug FBApp and Prod Release FBApp.


6. We need to set the right configuration for each scheme. For that click Edit scheme, and for the debug scheme, under the Run section, for Build Configuration, choose Dev Debug and for the production scheme, choose, Prod Release. Under Manage schemes, make sure these schemes have Shared enabled.


With this, we have Firebase setup working with multiple environments. However, if we use other Firebase options like FirebaseAuth, we need to add additional settings like URL Type so that after the user sign-in, the invoking the app works. For this, we need to create a new xcconfig as described next.

From File > New File > File, choose Configuration Settings File and name it as DevConfig.xcconfig and another with ProdConfig.xcconfig. Do not add the xcconfig file to any target as it's a build config file and we don't want it to be shipped with the app bundle. The content of the dev config follows.
APP_NAME = FBAppDev
APP_BUNDLE_ID = org.example.app.FBAppDev
APP_FIREBASE_URL_SCHEME = com.googleusercontent.apps.25135051042-2utrt0j007lp7vf23p73fd306trszqn2
#include "Pods/Target Support Files/Pods-FBApp/Pods-FBApp.dev debug.xcconfig"
Note that in case of URLs in the xcconfig, we need to escape it like APP_SERVER_URL_LOCAL = http:/$()/localhost:4000/.


Here we define a variable APP_FIREBASE_URL_SCHEME with value taken from the reverse client ID of the dev plist. We will give a different bundle ID so that we can have both staging and prod versions of the app installed, without one overwriting the other.
7. We need to specify the xcconfig file for each project configuration we created before (in step 2). Under the Project > Configurations, for Dev Debug, expand and choose the DevConfig for the Based on Configuration File value. Same for the prod configuration name.


8. If the project uses Cocoapods, first close the workspace, delete the xcworkspace file, the Pods directory and the Podfile.lock and run pod install which will install the dependencies as usual and generate the Pod xcconfig files for each configuration we have, but does not set it as we have already set a config. So we include the corresponding Pod config in the xcconfig using the #include.

9. For the final step, we need to parameterise the variables in the Info.plist file as follows.
Bundle identifier  $(APP_BUNDLE_ID)
Bundle name $(APP_NAME)
URL Types > URL Schemes $(APP_FIREBASE_URL_SCHEME)
Voila, we now have a project setup with different configurations with a single app target.

Saturday, September 21, 2019

AppleScript to Toggle Dark Mode

We can use AppleScript to toggle between dark and light mode under macOS.
-- Toggle dark mode
tell application "System Events"
tell appearance preferences
get properties
if dark mode is false then
set dark mode to true
else
set dark mode to false
end if
end tell
end tell
Save this in a file say, toggle-dark-mode.applescript. Now we can use osascript to run this.
λ osascript ~/bin/toggle-dark-mode.applescript

Enable JavaScript From Apple Events for Safari

In macOS High Sierra, after enabling the Developer menu in Safari, clicking on "Allow JavaScript from Apple Events" does not work. We can use the below script to enable the option, once the Develop menu is enabled.
-- Enabled Apple Events in Safari if not enabled using Menu
set isEventsEnabled to false
to enableAppleEventsUsingMenu()
tell application "Safari"
tell application "System Events"
tell process "Safari"
tell menu "Develop" of menu bar item "Develop" of menu bar 1
click it
get properties of menu item "Allow JavaScript from Apple Events"
tell menu item "Allow JavaScript from Apple Events"
if value of attribute "AXMenuItemMarkChar" is not "✓" then
set isEventsEnabled to false
click
else
set isEventsEnabled to true
keystroke return
end if
delay 1
end tell
end tell
if isEventsEnabled is false then
tell front window
click button "Allow"
end tell
end if
end tell
end tell
end tell
end enableAppleEventsUsingMenu

-- Invoke the method
my enableAppleEventsUsingMenu()
Run this in the Script Editor, after launching Safari, which will then ask for confirmation. Once done, we will have a tick on that menu. This script can also be used in other automation workflows where interaction with AppleScript and JavaScript within Safari needs to be enabled beforehand.

URLSession Custom Server Trust Evaluation

In certain situations where the server certificate is not configured properly, or when using self signed SSL certificates, requests validation fails with URLSession. URLSession provides delegate methods which can be used in situations like this and can be used as a workaround until the certificate gets fixed on the server end. We can also use manual certification validation if we want to pin to a particular certificate. To do this, we need to assign a delegate for the URLSession object and handle the validation there as shown in the snippet below.
class NetworkService: NSObject {
private lazy var opsQueue: OperationQueue = {
let q = OperationQueue.init()
q.name = "api queue"
q.qualityOfService = QualityOfService.background
return q
}()
private lazy var session: URLSession = {
return URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: opsQueue)
}()
}

extension NetworkService: URLSessionDelegate {
/// Customise server trust evaluation in case where server certificate validation fails.
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let protectionSpace: URLProtectionSpace = challenge.protectionSpace
if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
if let trust: SecTrust = protectionSpace.serverTrust {
let cred = URLCredential(trust: trust)
completionHandler(URLSession.AuthChallengeDisposition.useCredential, cred) // Say, custom SSL, so allow
} else {
completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil)
}
} else {
completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
}
}
}
Here the URLCredential object containing the SecTrust is passed to the completionHandler which makes the request go through. We can also do additional validation on the URLProtectionSpace and so on.

TeaPotServer

I wrote a TeaPotServer which is an HTTP2 server that uses Swift NIO under the hood. Swift NIO does the request, response handling with it event loop mechanism and the request processing is offloaded to GCD queue, making the event loop run as quick as possible without having to wait for a request to be processed. It's modelled as a REST API server with JSON as the main data exchange format. The server also has a logging component which conforms to SwiftLog API. The logging service uses the multiplexed logger to write to both file and console IO and does log rotation without missing any logs in the meantime. The main goal of the project is to demonstrate the development of a production server just by using Swift NIO and related libraries.

Wednesday, September 18, 2019

Using a Macro Based Function Within an XCTest Assert Macro

Using NSString initWithFormat: option within an XCTAssertEqualObjects directly will given an error.
XCTAssertEqualObjects([[NSString alloc] initWithFormat:@"Hello %@", @"olive"], @"Hello olive");
This will show error in Xcode like:
@try statement without a @catch and @finally clause
Expected ']'
Expected identifier or '('
Expected identifier or '('
Extraneous closing brace ('}')
Unexpected '@' in program
This is because the XCTAssertEqualObjects is a macro and it accepts va_args and initWithFormat: also accepts va_args. So there is a conflict and we need to wrap it within a parentheses.
XCTAssertEqualObjects(([[NSString alloc] initWithFormat:@"Hello %@", @"olive"]), @"Hello olive");

Wednesday, September 11, 2019

Fix Rename Failed in Xcode 10

Running a refactor in Xcode can sometimes show "Rename failed" message. This can be fixed by quitting Xcode, cleaning derived data folder and doing a rebuild followed by the refactoring. Below is a script to clean the derived data folder.



#!/bin/bash
# Clean Xcode cache

rm -frd ~/Library/Developer/Xcode/DerivedData/*
rm -frd ~/Library/Caches/com.apple.dt.Xcode/*
After cleaning the caches, launch Xcode, build the project. Then wait for any indexing tasks to complete. Now try Refactor > Rename and this should work.

Sunday, September 1, 2019

Implement NSFastEnumeration

Implementing NSFastEnumeration for a custom class requires us to implement one method which will be called when we use for (var in coll) // { .. } form. Let's say we have a class DLList which is backed by an array as its main data source. For iterating elements in the DLList object, we can do as follows.
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id  _Nullable __unsafe_unretained [])buffer count:(NSUInteger)len {
if (state->state == [self count]) return 0;
__unsafe_unretained id const *arr = &_array; // (1)
state->itemsPtr = (__typeof__(state->itemsPtr))arr; // (2)
state->mutationsPtr = &state->extra[0]; // (3)
state->state = [self count];
return state->state;
}
Here state->itemsPtr requires a pointer to any object of type id. We use _array and pointer to the _array can be obtained by getting the address, &_array. But holding a reference to the variable will change its retain count, which we do not want. So in statement marked (1) we use __unsafe_unretained id const * as the type. We don't check for mutation here (3) as the collection is not being mutated during enumeration.

Section 4.3.3 of the Automatic Reference counting documentation discusses the semantics of casts under ARC:
A program is ill-formed if an expression of type T* is converted, explicitly or implicitly, to the type U*, where T and U have different ownership qualification, unless:
* T is qualified with __strong, __autoreleasing, or __unsafe_unretained, and U is qualified with both const and __unsafe_unretained; or
...

In statement marked (2), we then typecast it to the type of state->itemsPtr which is same as __unsafe_unretained id * removing the const, which works because the ownership is the same.

The DLList class snippet is given below.
@implementation DLList {
NSMutableArray *_array;
}

- (NSUInteger)count {
return [_array count];
}

// ..
Now we can use fast enumeration like
DLList *list = [[DLList alloc] initWithArray:@[@(1), @(2), @(3)]];
NSNumber *num = nil;
for (num in list) { // fast enumeration which will call the protocol method
NSLog(@"Elems: %@", num);
}