How to integrate library via custom podspec

Issue #81

Today I am about to integrate a library that does not support Cocoapods yet. It would be cumbersome to do it manually, because you have to configure xcconfig, framework search path, assets, and these steps are not well documented.

You can do this with custom podspec. In my case, I need to install PinchSDK. First, declare a PinchSDK.podspec in your project folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Pod::Spec.new do |s|
s.name = "PinchSDK"
s.version = "1.9.14"
s.summary = "Pinch samler dessuten inn data hver gang en mobilapplikasjon oppdager en Pinch-beacon."
s.homepage = "https://bitbucket.org/fluxloop/pinch.installpackage"
s.source = { :http => "https://bitbucket.org/fluxloop/pinch.installpackage/raw/master/iOS/PinchSDK.zip" }
s.authors = 'Fluxloop'
s.license = { type: 'MIT' }
s.platform = :ios, '8.0'
s.requires_arc = true
s.resource = 'PinchSDK/Pinch.bundle'
s.vendored_frameworks = 'PinchSDK/PinchLibrary.framework'
s.xcconfig = { 'OTHER_LDFLAGS': '-ObjC' }
s.public_header_files = 'PinchSDK/PinchLibrary.framework/Headers/PinchLibrary.h'
s.source_files = 'PinchSDK/PinchLibrary.framework/Headers/PinchLibrary.h'
end

Then, in your Podfile, you can point to this podspec

1
pod 'PinchSDK', podspec: 'PinchSDK.podspec'

Finally, since this PinchSDK uses objc, you need to declare it in your bridging header

1
#import <PinchLibrary/PinchLibrary.h>

Now, just pod install and you’re done 🎉

How to handle Swift version with Cocoapods

Issue #80

Today I was migrating Imaginary to Swift 4. But I get

1
2
3
4
5
- ERROR | [OSX] xcodebuild: Returned an unsuccessful exit code.
- ERROR | [OSX] xcodebuild: Cache/Source/Mac/NSImage+Extensions.swift:32:64: error: 'png' has been renamed to 'PNG'
- NOTE | [OSX] xcodebuild: AppKit.NSBitmapImageRep:57:27: note: 'png' was introduced in Swift 4
- ERROR | [OSX] xcodebuild: Cache/Source/Mac/NSImage+Extensions.swift:32:71: error: 'jpeg' has been renamed to 'JPEG'
- NOTE | [OSX] xcodebuild: AppKit.NSBitmapImageRep:52:27: note: 'jpeg' was introduced in Swift 4

swift4 png

The project is configured to use Swift 4 and its dependency Cache correctly uses .png and .jpeg https://github.com/hyperoslo/Cache/blob/master/Source/Mac/NSImage%2BExtensions.swift

Why is that 🤔

It turns out that the .swift-version is still showing 3.0. Change it to 4.0 fixes the issue. The .swift-version is a hint to specify which Swift version should be used for a pod https://github.com/CocoaPods/CocoaPods/pull/5841

Ikigai

Issue #79

I really like the concept of Ikigai

Ikigai (生き甲斐, pronounced [ikiɡai]) is a Japanese concept that means “a reason for being.” It is similar to the French phrase Raison d’être. Everyone, according to Japanese culture, has an ikigai. Finding it requires a deep and often lengthy search of self. Such a search is important to the cultural belief that discovering one’s ikigai brings satisfaction and meaning to life.[1]

The term ikigai compounds two Japanese words: iki (wikt:生き) meaning “life; alive” and kai (甲斐) “(an) effect; (a) result; (a) fruit; (a) worth; (a) use; (a) benefit; (no, little) avail” (sequentially voiced as gai) “a reason for living [being alive]; a meaning for [to] life; what [something that] makes life worth living; a raison d’etre”.[3]

About college degree

Read more

Ad Hominem

Issue #76

I use Twitter a lot, mostly to follow people I like. They tweet cool things about tech and life. I learned a lot.

Please don’t show me the evil sides of the world ~ Michael Learn To Rock - How Many Hours

But there’s also bad side of the story. I see many retweets of people saying bad things about others, mostly in form of Ad Hominem

Attacking the person making the argument, rather than the argument itself, when the attack on the person is completely irrelevant to the argument the person is making.

From Ad Hominem on c2.com

An argumentum ad hominem is any kind of argument that criticizes an idea by pointing something out about the people who hold the idea rather than directly addressing the merits of the idea. ‘’Ad hominem’’ is Latin for “directed toward the man (as opposed to the issue at hand)”. An alternative expression is “playing the man and not the ball”.

Most of these people have the Twitter verified badge. They complain that they have so many followers while they themselves follow hundreds of thousands. They say bad things about others’ hair style and appearance while actively supporting equality. They argue who owns the original gif. They follow one person just to be the first to insult them.

The blue verified badge on Twitter lets people know that an account of public interest is authentic.

Blowing out someone else’s candle doesn’t make yours shine any brighter ~ Anonymous

The only thing I can do is I don't like this tweet 😞

Sync and async code in Swift

Issue #75

We should use DispatchQueue to build thread safe code. The idea is to prevent two read and write from happening at the same time from 2 different threads, which cause data corruption and unexpected behaviors. Note that you should try to avoid deadlock https://stackoverflow.com/questions/15381209/how-do-i-create-a-deadlock-in-grand-central-dispatch

All sync

Use try catch, together with serial queue. Use sync function to block current queue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func getUser(id: String) throws -> User {
var user: User!
try serialQueue.sync {
user = try storage.getUser(id)
}

return user
}

func setUser(_ user: User) throws {
try serialQueue.sync {
try storage.setUser(user)
}
}

All async

Use Result, toget with serial queue. Use async function to return to current queue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
enum Result<T> {
case value(T)
case error(Error)
}

func getUser(id: String, completion: (Result<User>) - Void) {
try serialQueue.async {
do {
user = try storage.getUser(id)
completion(.value(user))
} catch {
completion(.error(error))
}
}

return user
}

func setUser(_ user: User, completion: (Result<()>) -> Void) {
try serialQueue.async {
do {
try storage.setUser(user)
completion(.value(())
} catch {
completion(.error(error))
}
}
}

Sync read, async write

Use try catch for read, Result for write, together with concurrent queue. Use sync function for read to block current thread, while using async function with barrier flag for write to return to current queue. This is good for when multiple reads is preferred when there is no write. When write with barrier comes into the queue, other operations must wait.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func getUser(id: String) throws -> User {
var user: User!
try concurrentQueue.sync {
user = try storage.getUser(id)
}

return user
}

func setUser(_ user: User, completion: (Result<()>) -> Void) {
try concurrentQueue.async(flags: .barrier) {
do {
try storage.setUser(user)
completion(.value(())
} catch {
completion(.error(error))
}
}
}

Testing for asynchrony

Before we could use dispatch_apply to submits a block to a dispatch queue for multiple invocations. Starting with Swift, the equivalence is concurrentPerform

1
2
3
4
DispatchQueue.concurrentPerform(iterations: 1000) { index in
let last = array.last ?? 0
array.append(last + 1)
}

Reference

How to check generic type in Swift

Issue #74

When dealing with generic, you shouldn’t care about the types. But if you need, you can

1
2
3
4
5
6
7
8
9
10
11
 func isPrimitive<T>(type: T.Type) -> Bool {
let primitives: [Any.Type] = [
Bool.self, [Bool].self,
String.self, [String].self,
Int.self, [Int].self,
Float.self, [Float].self,
Double.self, [Double].self
]

return primitives.contains(where: { $0.self == type.self })
}

How to use Given When Then in Swift tests

Issue #73

Spec

Using spec testing framework like Quick is nice, which enables BDD style.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
describe("the 'Documentation' directory") {
it("has everything you need to get started") {
let sections = Directory("Documentation").sections
expect(sections).to(contain("Organized Tests with Quick Examples and Example Groups"))
expect(sections).to(contain("Installing Quick"))
}

context("if it doesn't have what you're looking for") {
it("needs to be updated") {
let you = You(awesome: true)
expect{you.submittedAnIssue}.toEventually(beTruthy())
}
}
}

But in case you don’t want additional frameworks, and want to live closer to Apple SDKs as much as possible, here are few tips.

Naming

This is from the book that I really like The Art of Unit Testing. If you don’t mind the underscore, you can follow UnitOfWork_StateUnderTest_ExpectedBehavior structure

1
2
3
func testSum_NegativeNumberAs1stParam_ExceptionThrown()
func testSum_NegativeNumberAs2ndParam_ExceptionThrown()
func testSum_simpleValues_Calculated()

Given When Then

This is from BDD, and practised a lot in Cucumber. You can read more on https://martinfowler.com/bliki/GivenWhenThen.html.

First, add some more extensions to XCTestCase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import XCTest

extension XCTestCase {
func given(_ description: String, closure: () throws -> Void) throws {
try closure()
}

func when(_ description: String, closure: () throws -> Void) throws {
try closure()
}

func then(_ description: String, closure: () throws -> Void) throws {
try closure()
}
}

Then, in order to test, just follow given when then

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func testRemoveObject() throws {
try given("set to storage") {
try storage.setObject(testObject, forKey: key)
}

try when("remove object from storage") {
try storage.removeObject(forKey: key)
}

try then("there is no object in memory") {
let memoryObject = try? storage.memoryCache.object(forKey: key) as User
XCTAssertNil(memoryObject)
}

try then("there is no object on disk") {
let diskObject = try? storage.diskCache.object(forKey: key) as User
XCTAssertNil(diskObject)
}
}

I find this more interesting than comments. All are code and descriptive. It can also be developed further to throw the description text.


Updated at 2020-12-18 15:15:26

Understanding Instance property vs parameter in Swift

Issue #72

The other day I was refactor my code. I have

1
2
3
4
5
6
7
8
extension MainController: TabBarViewDelegate {

func buttonDidPress index: Int) {
let initialIndex = tabBarView.selectedIndex
let wholeAppContentView = updateWholeAppContentView()
view.addSubview(wholeAppContentView)
}
}

The delegate method does not look right, as it’s hard to tell between required delegate method, or just instance method. Also it lacks a subject. I like this post API Design, you can read section Rule 19: Always say who’s talking

This is a simple rule, and an equally simple mistake to make. In your delegate methods, always pass the sender as a parameter. Always. Even for singletons. Even for things you cannot conceive would ever be used more than once simultaneously. No exceptions.

So I refactor the delegate, and conform to it.

1
2
3
4
5
6
7
8
extension MainController: TabBarViewDelegate {

func tabBarView(_ view: TabBarView, buttonDidPress index: Int) {
let initialIndex = tabBarView.selectedIndex
let wholeAppContentView = updateWholeAppContentView()
view.addSubview(wholeAppContentView) // This is the culprit ⚠️
}
}

Even with just 1 line change in MainController.swift, the whole UI breaks, as all the views were added to the tab bar. Strange 😡 .

It didn’t take long until I remember that parameter takes precedence over instance property if they have same name. So in this case, the compiler, without warning, assume you’re dealing with view from TabBarView ⚠️

That’s why you often use self to disambiguate.

1
2
3
4
5
6
7
8
9
struct User: Codable, Equatable {
let firstName: String
let lastName: String

init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}

Back to our code. The workaround is to specify self to specify view of MainController

1
self.view.addSubview(wholeAppContentView)

Well, you may say, who should add view again in case of tab bar changes 😬 This is a bad example, but the lesson is learned 😇

How to push to GitHub gist

Issue #71

Creating a new gist

  • Go to https://gist.github.com/ and create a new gist
  • Note that you need to include filename + extension to enable automatic language markup
  • Click Add file to add more files

Cloning the gist

  • If you’ve enabled 2 factor authentication, you need to use personal acccess token with https, or use ssh.

If you have enabled two-factor authentication, or if you are accessing an organization that uses SAML single sign-on, you must provide a personal access token instead of entering your password for HTTPS Git.

1
2
git remote add origin git@gist.github.com:c486939f82fc4d3a8ed4be21538fdd32.git
git clone
  • You have branch master by default
1
git push origin master

Ignoring directories

remote: Gist does not support directories.

  • In my cases I’m using node, so I need to ignore node_modules directory
  • Also need to untrack if necessary
1
2
git rm --cached -r .
git add .

How to observe object deinit in Swift

Issue #70

  • Today I was browsing through Suas-iOS and the subscription links to life cycle of another object
1
subscription.linkLifeCycleTo(object: self)

It observes the deinit of another job, interesting approach 👍 , take a look in https://github.com/zendesk/Suas-iOS/blob/master/Sources/StoreDeinitCallback.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var deinitCallbackKey = "DEINITCALLBACK_SUAS"

// MARK: Registartion
extension Suas {

static func onObjectDeinit(forObject object: NSObject,
callbackId: String,
callback: @escaping () -> ()) {
let rem = deinitCallback(forObject: object)
rem.callbacks.append(callback)
}

static fileprivate func deinitCallback(forObject object: NSObject) -> DeinitCallback {
if let deinitCallback = objc_getAssociatedObject(object, &deinitCallbackKey) as? DeinitCallback {
return deinitCallback
} else {
let rem = DeinitCallback()
objc_setAssociatedObject(object, &deinitCallbackKey, rem, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return rem
}
}
}

@objc fileprivate class DeinitCallback: NSObject {
var callbacks: [() -> ()] = []

deinit {
callbacks.forEach({ $0() })
}
}

How to deal with NODE_MODULE_VERSION in electron

Issue #69

NODE_MODULE_VERSION

Today I was trying to install sharp with yarn add sharp to work in my electron app, but I get the following error

Uncaught Error: The module ‘/Users/khoa/MyElectronApp/node_modules/sharp/build/Release/sharp.node’
was compiled against a different Node.js version using
NODE_MODULE_VERSION 57. This version of Node.js requires
NODE_MODULE_VERSION 54. Please try re-compiling or re-installing
the module

Native node module

Searching a bit, it was because sharp is a native node module which uses libvips under the hood

Node.js Addons are dynamically-linked shared objects, written in C++, that can be loaded into Node.js using the require() function, and used just as if they were an ordinary Node.js module. They are used primarily to provide an interface between JavaScript running in Node.js and C/C++ libraries.

More on how to build native node module can be found here https://blog.risingstack.com/writing-native-node-js-modules/

Node version

I’m using nvm to manage node version, and nvm list shows 8.4.0 as the latest node version I’m using.

1
2
3
4
5
6
v6.10.1
v7.8.0
v7.9.0
v8.0.0
-> v8.4.0
system

Searching on Node releases reveals that Node 8.4.0 has NODE_MODULE_VERSION of 57, so that is the node version npm uses to compile sharp

However, I can’t seem to find the NODE_MODULE_VERSION 54 that sharp is using. I tried node 8.0.0 which is believed to have NODE_MODULE_VERSION 54 but it didn’t work

Electron version

As the time of this post, electron is at version 7.9.0, you can check here https://github.com/electron/electron/blob/master/.node-version or by running process.versions in Javascript console

Using electron-rebuild

So after I read this Using Native Node Modules, I install electron-rebuilder to recompile sharp

1
2
3
yarn add electron-rebuild --dev
yarn add sharp
./node_modules/.bin/electron-rebuild

It works now 🎉

Read more

How to support copy paste in electron

Issue #67

After running electron-packager, the app does not accept copy, paste anymore. This is because the release build does not have menu with key binding to the clipboard by default. We can solve this by manually declaring the menu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
const {app} = require('electron')
const Menu = require('electron').Menu

app.on('ready', () => {
createWindow()
createMenu()
})

function createMenu() {
const application = {
label: "Application",
submenu: [
{
label: "About Application",
selector: "orderFrontStandardAboutPanel:"
},
{
type: "separator"
},
{
label: "Quit",
accelerator: "Command+Q",
click: () => {
app.quit()
}
}
]
}

const edit = {
label: "Edit",
submenu: [
{
label: "Undo",
accelerator: "CmdOrCtrl+Z",
selector: "undo:"
},
{
label: "Redo",
accelerator: "Shift+CmdOrCtrl+Z",
selector: "redo:"
},
{
type: "separator"
},
{
label: "Cut",
accelerator: "CmdOrCtrl+X",
selector: "cut:"
},
{
label: "Copy",
accelerator: "CmdOrCtrl+C",
selector: "copy:"
},
{
label: "Paste",
accelerator: "CmdOrCtrl+V",
selector: "paste:"
},
{
label: "Select All",
accelerator: "CmdOrCtrl+A",
selector: "selectAll:"
}
]
}

const template = [
application,
edit
]

Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}

Reference

How to change app icon in electron

Issue #66

Generate icns

  • Generate .iconset
  • Run iconutil -c icns "Icon.iconset". Note that icon names must be first letter lowsercased, and use _ instead of -

icns

Use icns

  • In main.js, specify icon
1
2
3
4
5
win = new BrowserWindow({
width: 800,
height: 600,
icon: __dirname + '/Icon/Icon.icns'
})

You can also use helper url methods

1
2
3
4
5
6
7
8
const path = require('path')
const url = require('url')

const iconUrl = url.format({
pathname: path.join(__dirname, 'Icon/Icon.icns'),
protocol: 'file:',
slashes: true
})

If app icon is not updated

  • I get a problem that electron always shows default app icon. I tried using png, NativeImage, different icon sizes but still the problem. When I use electron-packager to make release build, the icon shows correctly, so it must be because of Electron caching or somehow 😠
  • Go to node_modules -> electron -> dist, right click on Electron, choose View Info
  • Drag another icns into the icon on the top left

info

Release with electron-packager

  • icon must be specified with __dirname (we already did) for electron-packager to pick up correct icons

Updated at 2020-09-26 18:02:30

How to do implement notification in iOS with Firebase

Issue #64

Note: This applies to Firebase 4.0.4

Preparing push notification certificate

Go to Member Center -> Certificates -> Production

Certificate

You can now use 1 certificate for both sandbox and production environment
push

Auth Key

Configure push notification

  • Go to Firebase Console -> Settings -> Project Settings -> Cloud Messaging -> iOS app configuration
    • If you use certificate, use just 1 Apple Push Notification service SSL for both fields
    • If you use Authenticate Key, fill in APNS auth key

firebase

Adding pod

In your Podfile, declare

1
2
pod 'Firebase/Core'
pod 'Firebase/Messaging'

Disabling app delegate swizzling

1
2
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>

Read more on Messaging.messaging().apnsToken

This property is used to set the APNS Token received by the application delegate.
FIRMessaging uses method swizzling to ensure the APNS token is set automatically. However, if you have disabled swizzling by setting FirebaseAppDelegateProxyEnabled to NO in your app’s Info.plist, you should manually set the APNS token in your application delegate’s -application:didRegisterForRemoteNotificationsWithDeviceToken: method.
If you would like to set the type of the APNS token, rather than relying on automatic detection, see: -setAPNSToken:type:.

Configuring Firebase

You can and should configure Firebase in code

1
2
3
4
5
6
7
8
import Firebase

let options = FirebaseOptions(googleAppID: "", gcmSenderID: "")
options.bundleID = ""
options.apiKey = ""
options.projectID = ""
options.clientID = ""
FirebaseApp.configure(options: options)

Handling device token

1
2
3
4
5
import Firebase

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Messaging.messaging().apnsToken = deviceToken
}

Getting FCM token

Retrieving FCM token

Read Access the registration token

By default, the FCM SDK generates a registration token for the client app instance on initial startup of your app. Similar to the APNs device token, this token allows you to target notification messages to this particular instance of the app.

1
Messaging.messaging().fcmToken

Observing for FCM token change

Read Monitor token generation

1
2
3
4
5
6
7
Messaging.messaging().delegate = self

// MARK: - MessagingDelegate

func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) {
print(fcmToken)
}

How to use JSON Codable in Swift 4

Issue #63

Codable in Swift 4 changes the game. It deprecates lots of existing JSON libraries.

Generic model

API responses is usually in form of an object container with a key. Then it will be either nested array or object. We can deal with it by introducing a data holder. Take a look DataHolder

1
2
3
4
5
6
7
8
9
10
{
"data": [
{
"comment_id": 1
},
{
"comment_id": 2
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ListHolder<T: Codable>: Codable {
enum CodingKeys: String, CodingKey {
case list = "data"
}

let list: [T]
}

struct OneHolder<T: Codable>: Codable {
enum CodingKeys: String, CodingKey {
case one = "data"
}

let one: T
}

then with Alamofire, we can just parse to data holder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func loadComments(mediaId: String, completion: @escaping ([Comment]) -> Void) {
request("https://api.instagram.com/v1/media/\(mediaId)/comments",
parameters: parameters)
.responseData(completionHandler: { (response) in
if let data = response.result.value {
do {
let holder = try JSONDecoder().decode(ListHolder<Comment>.self, from: data)
DispatchQueue.main.async {
completion(holder.list)
}
} catch {
print(error)
}
}
})
}

Read more

How to deal with windows-1252 encoding in Node

Issue #60

Today I use node-fetch and cheerio to fetch a webpage. It looks good in Chrome and Sublime Text when it displays html entities like &#7901

However, it does not render correctly in iTerm, Terminal and Visual Studio Code. It just shows fffd

I think the problem is because of my code, so I change to request and try to play with different options in cheerio but still the problem.

It didn’t take me long to figure it out that the format of the html is windows-1252

1
<html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">

So I need to use windows-1252, legacy-encoding, iconv-lite

FYI

Pixel and point

Issue #59

TL;DR: Don’t use nativeScale and nativeBounds, unless you’re doing some very low level stuff

What is point and pixel

From https://developer.apple.com/library/content/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GraphicsDrawingOverview/GraphicsDrawingOverview.html

In iOS there is a distinction between the coordinates you specify in your drawing code and the pixels of the underlying device

The purpose of using points (and the logical coordinate system) is to provide a consistent size of output that is device independent. For most purposes, the actual size of a point is irrelevant. The goal of points is to provide a relatively consistent scale that you can use in your code to specify the size and position of views and rendered content

On a standard-resolution screen, the scale factor is typically 1.0. On a high-resolution screen, the scale factor is typically 2.0

How about scale and nativeScale

From https://developer.apple.com/documentation/uikit/uiscreen

  • var bounds: CGRect: The bounding rectangle of the screen, measured in points.
  • var nativeBounds: CGRect: The bounding rectangle of the physical screen, measured in pixels.
  • var scale: CGFloat: The natural scale factor associated with the screen.
  • var nativeScale: CGFloat: The native scale factor for the physical screen.

The scale factor and display mode

See this for a whole list of devices and their scale factors https://www.paintcodeapp.com/news/ultimate-guide-to-iphone-resolutions

The iPhone 6 and 6+ introduced display mode https://www.cnet.com/how-to/explaining-display-zoom-on-iphone-6-and-6-plus/

You can see that currently the iPhone 6+, 6s+, 7+ phones have scale factor of 2.88 in zoomed mode, and 2.6 in standard mode

You can also see that in zoomed mode, iPhone 6 has the same logical size as the iPhone 5

Simulator vs device

This is to show you the differences in nativeScale in simulators and devices in zoomed mode, hence differences in nativeBounds.

iPhone 6+ simulator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(lldb) po UIScreen.main.scale
3.0

(lldb) po UIScreen.main.bounds
▿ (0.0, 0.0, 414.0, 736.0)
▿ origin : (0.0, 0.0)
- x : 0.0
- y : 0.0
▿ size : (414.0, 736.0)
- width : 414.0
- height : 736.0

(lldb) po UIScreen.main.nativeScale
3.0

(lldb) po UIScreen.main.nativeBounds
▿ (0.0, 0.0, 1242.0, 2208.0)
▿ origin : (0.0, 0.0)
- x : 0.0
- y : 0.0
▿ size : (1242.0, 2208.0)
- width : 1242.0
- height : 2208.0

iPhone 6+ device

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(lldb) po UIScreen.main.scale
3.0

(lldb) po UIScreen.main.bounds
▿ (0.0, 0.0, 375.0, 667.0)
▿ origin : (0.0, 0.0)
- x : 0.0
- y : 0.0
▿ size : (375.0, 667.0)
- width : 375.0
- height : 667.0

(lldb) po UIScreen.main.nativeScale
2.88

(lldb) po UIScreen.main.nativeBounds
▿ (0.0, 0.0, 1080.0, 1920.0)
▿ origin : (0.0, 0.0)
- x : 0.0
- y : 0.0
▿ size : (1080.0, 1920.0)
- width : 1080.0
- height : 1920.0

Optional of optional in Swift

Issue #58

Do you know that an optional can itself contain an optional, that contains another optional? In that case, we need to unwrap multiple times

optionals

You mostly see it when you try to access window

1
let window = UIApplication.shared.delegate?.window // UIWindow??

It is because delegate can be nil, and its window can be nil too.

1
window??.backgroundColor = .yellow

Package node.js application

Issue #57

I like node.js because it has many cool packages. I wish the same goes for macOS. Fortunately, the below solutions provide a way to package node.js modules and use them inside macOS applications. It can be slow, but you save time by using existing node.js modules. Let’s give it a try.

  • pkg Package your Node.js project into an executable 🚀
  • nexe create a single executable out of your node.js apps
  • enclose js Compile your Node.js project into an executable http://enclosejs.com

Favorite WWDC 2017 sessions

Issue #56

  1. Introducing Core ML
  • Core ML
  1. Introducing ARKit: Augmented Reality for iOS
  • ARKit
  1. What’s New in Swift
  • String
  • Generic
  • Codable
  1. Advanced Animations with UIKit
  • Multiple animation
  • Interactive animation
  1. Natural Language Processing and your Apps
  • NSLinguisticTagger
  1. What’s New in Cocoa Touch
  • Large title
  • Drag and drop
  • File management
  • Safe area
  1. What’s New in Foundation
  • KeyPath
  • Observe
  • Codable
  1. Debugging with Xcode 9
  • Wireless debugging
  • View controller debugging
  1. Core ML in depth
  • Model
  • Core ML tools
  1. Vision Framework: Building on Core ML
  • Detection
  • Track
  1. What’s New in Testing
  • Parallel testing
  • Wait
  • Screenshot
  • Multiple app scenario

How to implement a tracker in Swift

Issue #55

I’m trying to implement a tracker, so the idea is that it can inject subscription upon method calls. It is best suit for logging, analytics, and it leverages RxCocoa

Usage

1
2
3
4
5
6
7
8
9
10
11
track(ListController.self) { _ in
print("")
}

track(ListController.self, selector: #selector(ListController.hello)) { _ in
print("")
}

track(DetailController.self) { _ in
print("")
}

The code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import UIKit
import RxSwift
import RxCocoa

var mapping: [String: [Item]] = [:]
var hasSwizzled = false
let bag = DisposeBag()

public func track<T: UIViewController>(_ type: T.Type, selector: Selector? = nil, block: @escaping (T) -> Void) {
let typeName = NSStringFromClass(type)
if !hasSwizzled {
let original = #selector(UIViewController.viewDidLoad)
let swizled = #selector(UIViewController.trackers_viewDidLoad)
swizzle(kClass: UIViewController.self, originalSelector: original, swizzledSelector: swizled)
hasSwizzled = true
}

let selector = selector ?? #selector(UIViewController.viewDidAppear(_:))

let item = Item(selector: selector, block: { (controller) in
if let controller = controller as? T {
block(controller)
}
})

if var items = mapping[typeName] {
items.append(item)
mapping[typeName] = items
} else {
mapping[typeName] = [item]
}
}

class Item {
let selector: Selector
let block: (UIViewController) -> Void

init(selector: Selector, block: @escaping (UIViewController) -> Void) {
self.selector = selector
self.block = block
}
}

extension UIViewController {
func trackers_viewDidLoad() {
trackers_viewDidLoad()

let typeName = NSStringFromClass(type(of: self))
let items = mapping[typeName]
items?.forEach({ (item) in
self
.rx
.sentMessage(item.selector)
.subscribe(onNext: { _ in
item.block(self)
}, onCompleted: {
print("completed")
})
.disposed(by: bag)
})
}
}

func swizzle(kClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
let originalMethod = class_getInstanceMethod(kClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(kClass, swizzledSelector)

let didAddMethod = class_addMethod(kClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

if didAddMethod {
class_replaceMethod(kClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}

How to change year in Date in Swift

Issue #54

Today I’m trying to change the year of a Date object to 2000 in Swift.

1
let date = Date()

Firstly, I tried with date(bySetting:) but it does not work with past year. It simply returns nil

1
Calendar.current.date(bySetting: .year, value: 2000, of: date)

Secondly, I tried with dateComponents. The component.year has changed, but it calendar still returns the original date, very strange !!. No matter what timezone and calendar I use, it still has this problem

1
2
3
var component = calendar.dateComponents(in: TimeZone.current, from: base)
component.year = year
Calendar.current.date(from: component)

Finally, I tried to be more explicit, and it works 🎉

1
2
3
var component = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: base)
component.year = year
Calendar.current.date(from: component)

How to test for viewDidLoad in iOS

Issue #52

Suppose we have the following view controller

1
2
3
4
5
6
class ListController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
}
}

Get to know viewDidLoad

We know that viewDidLoad is called when view is created the first time. So in the the Unit Test, if you use viewDidLoad to trigger, you will fall into a trap

1
2
3
4
func testSetup() {
let controller = ListController()
controller.viewDidLoad()
}

Why is viewDidLoad called twice?

  • It is called once in your test
  • And in your viewDidLoad method, you access view, which is created the first time, hence it will trigger viewDidLoad again

The correct way

The best practice is not to trigger events yourself, but do something to make event happen. In Unit Test, we just access view to trigger viewDidLoad

1
2
3
4
func testSetup() {
let controller = ListController()
let _ = controller.view
}

How to initialize Enums With Optionals in Swift

Issue #49

Today someone showed me https://medium.com/@_Easy_E/initializing-enums-with-optionals-in-swift-bf246ce20e4c which tries to init enum with optional value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Planet: String {
case mercury
case venus
case earth
case mars
case jupiter
case saturn
case uranus
case neptune
}

extension RawRepresentable {
init?(optionalValue: RawValue?) {
guard let value = optionalValue else { return nil }
self.init(rawValue: value)
}
}

let name: String? = "venus"
let planet = Planet(optionalValue: name)

One interesting fact about optional, is that it is a monad, so it has map and flatMap. Since enum init(rawValue:) returns an optional, we need to use flatMap. It looks like this

1
2
let name: String? = "venus"
let planet = name.flatMap({ Planet(rawValue: $0) })

🎉

How to run UI Test with system alert in iOS

Issue #48

Continue my post https://github.com/onmyway133/blog/issues/45. When you work with features, like map view, you mostly need permissions, and in UITests you need to test for system alerts.

Add interruption monitor

This is the code. Note that you need to call app.tap() to interact with the app again, in order for interruption monitor to work

1
2
3
4
5
6
addUIInterruptionMonitor(withDescription: "Location permission", handler: { alert in
alert.buttons["Allow"].tap()
return true
})

app.tap()

Note that you don’t always need to handle the returned value of addUIInterruptionMonitor

Only tap when needed

One problem with this approach is that when there is no system alert (you already touched to allow before), then app.tap() will tap on your main screen. In my app which uses map view, it will tap on some pins, which will present another screen, which is not correct.

Since app.alerts does not work, my 2nd attempt is to check for app.windows.count. Unfortunately, it always shows 5 windows whether alert is showing or not. I know 1 is for main window, 1 is for status bar, the other 3 windows I have no idea.

The 3rd attempt is to check that underlying elements (behind alert) can’t be touched, which is to use isHittable. This property does not work, it always returns true

Check the content

This uses the assumption that we only tests for when user hits Allow button. So only if alert is answered with Allow, then we have permission to display our content. For my map view, I check that there are some pins on the map. See https://github.com/onmyway133/blog/issues/45 on how to mock location and identify the pins

1
2
3
if app.otherElements.matching(identifier: "myPin").count == 0 {
app.tap()
}

When there is no permission

So how can we test that user has denied your request? In my map view, if user does not allow location permission, I show a popup asking user to go to Settings and change it, otherwise, they can’t interact with the map.

I don’t know how to toggle location in Privacy in Settings, maybe XCUISiriService can help. But 1 thing we can do is to mock the application

Before you launch the app in UITests, add some arguments

1
app.launchArguments.append("--UITests-mockNoLocationPermission")

and in the app, we need to check for this arguments

1
2
3
4
5
func checkLocationPermission() {
if CommandLine.arguments.contains("--UITests-mockNoLocationPermission") {
showNoLocationPopupAndAskUserToEnableInSettings()
}
}

That’s it. In UITests, we can test whether that no location permission popup appears or not

Updated at 2020-06-02 02:12:36