How to setup Android projects

Issue #257

checkstyle (Java)

Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard. It automates the process of checking Java code to spare humans of this boring (but important) task. This makes it ideal for projects that want to enforce a coding standard.

app/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apply plugin: 'checkstyle'

task checkstyle(type: Checkstyle) {
description 'Check code standard'
group 'verification'

configFile file('$project.rootDir/tools/checkstyle.xml')
source 'src'
include '**/*.kt'
exclude '**/gen/**'

classpath = files()
ignoreFailures = false
}

tools/chekcstyle

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
<?xml version="1.0"?><!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
<module name="Checker">
<module name="FileTabCharacter"/>
<module name="TreeWalker">

<!-- Checks for Naming Conventions -->
<!-- See http://checkstyle.sourceforge.net/config_naming.html -->
<module name="MethodName"/>
<module name="ConstantName"/>

<!-- Checks for Imports -->
<!-- See http://checkstyle.sourceforge.net/config_imports.html-->
<module name="AvoidStarImport"/>
<module name="UnusedImports"/>

<!-- Checks for Size -->
<!-- See http://checkstyle.sourceforge.net/config_sizes -->
<module name="ParameterNumber">
<property name="max" value="6"/>
</module>

<!-- other rules ignored for brevity -->
</module>
</module>

findbugs (Java)

A program which uses static analysis to look for bugs in Java code

app/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apply plugin: 'findbugs'

task findbugs(type: FindBugs) {
ignoreFailures = false
effort = "max"
reportLevel = "low"
classes = files("$project.buildDir/intermediates/javac")

excludeFilter = file("$rootProject.rootDir/tools/findbugs-exclude.xml")

source = fileTree('src/main/java/')
classpath = files()

reports {
xml.enabled = false
html.enabled = true
html.destination file("$project.buildDir/outputs/findbugs/findbugs-output.html")
}
}

tools/findbugs-exclude.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<FindBugsFilter>
<!-- Do not check auto-generated resources classes -->
<Match>
<Class name="~.*R\$.*"/>
</Match>

<!-- Do not check auto-generated manifest classes -->
<Match>
<Class name="~.*Manifest\$.*"/>
</Match>

<!-- Do not check auto-generated classes (Dagger puts $ into class names) -->
<Match>
<Class name="~.*Dagger*.*"/>
</Match>

<!-- http://findbugs.sourceforge.net/bugDescriptions.html#ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD-->
<Match>
<Bug pattern="ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD" />
</Match>
</FindBugsFilter>

pmd (Java)

PMD is a source code analyzer. It finds common programming flaws like unused variables, empty catch blocks, unnecessary object creation, and so forth

app/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apply plugin: 'pmd'

task pmd(type: Pmd) {
ruleSetFiles = files("${project.rootDir}/tools/pmd-rules.xml")
ignoreFailures = false
ruleSets = []

source 'src'
include '**/*.kt'
exclude '**/gen/**'

reports {
xml.enabled = false
html.enabled = true
html.destination = file("$project.buildDir/outputs/pmd/pmd.html")
}
}

tools/pmd-rules.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
<exclude-pattern>.*/R.java</exclude-pattern>
<exclude-pattern>.*/gen/.*</exclude-pattern>

<rule ref="rulesets/java/basic.xml" />

<rule ref="rulesets/java/braces.xml" />

<rule ref="rulesets/java/strings.xml" />

<rule ref="rulesets/java/design.xml" >
<exclude name="AvoidDeeplyNestedIfStmts"/>
</rule>

<rule ref="rulesets/java/unusedcode.xml" />

</ruleset>

lint

Android Studio provides a code scanning tool called lint that can help you to identify and correct problems with the structural quality of your code without your having to execute the app or write test cases

app/build.gradle

1
2
3
4
5
6
7
8
9
10
android {
lintOptions {
lintConfig file("$project.rootDir/tools/lint-rules.xml")
htmlOutput file("$project.buildDir/outputs/lint/lint.html")
warningsAsErrors true
xmlReport false
htmlReport true
abortOnError false
}
}

tools/lint-rules.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>

<lint>
<issue id="GoogleAppIndexWarning" severity="ignore" />

<issue id="InvalidPackage" severity="error">
<ignore regexp="okio.*jar" />
<ignore regexp="retrofit.*jar" />
</issue>

<!-- Disable the given check in this project -->
<issue id="IconMissingDensityFolder" severity="ignore" />

<!-- Change the severity of hardcoded strings to "error" -->
<issue id="HardcodedText" severity="error" />
</lint>

Strict mode

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
import android.app.Application
import android.os.StrictMode

class App: Application() {
override fun onCreate() {
super.onCreate()

enableStrictMode()
}

private fun enableStrictMode() {
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)

StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
}
}
}

Version code

tools/grgit.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.ajoberstar:grgit:1.5.0'
}
}

import org.ajoberstar.grgit.Grgit

ext {
git = Grgit.open(currentDir: projectDir)
gitCommitCount = git.log().size()
}

task printVersion() {
println("Commit count: $gitCommitCount")
}

app/build.gradle

1
2
3
4
5
android {
defaultConfig {
versionCode gitCommitCount
}
}

Obfuscation

To make your app as small as possible, you should enable shrinking in your release build to remove unused code and resources. When enabling shrinking, you also benefit from obfuscation, which shortens the names of your app’s classes and members, and optimization, which applies more aggressive strategies to further reduce the size of your app

When you use Android Studio 3.4 or Android Gradle plugin 3.4.0 and higher, R8 is the default compiler that converts your project’s Java bytecode into the DEX format that runs on the Android platform

app/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
android {
buildTypes {
debug {
signingConfig signingConfigs.debug
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), '$project.rootDir/tools/proguard-rules-debug.pro'
}

release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), '$project.rootDir/tools/proguard-rules.pro'
}
}
}

tools/proguard-rules.pro

1
2
3
4
5
6
7
8
9
10
11
-ignorewarnings

# Remove logs
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}

tools/proguard-rules-debug.pro

1
2
3
4
-ignorewarnings
-dontobfuscate
-dontoptimize
-ignorewarnings

More proguard snippets https://github.com/krschultz/android-proguard-snippets

quality

tools/quality.gradle

1
2
3
task findbugs
task pmd
task checkstyle

app/build.gradle

1
apply from: "$project.rootDir/tools/quality.gradle"

File format

You don’t need to use ktlint or detekt to ensure that your code is formatted consistently. Simply enable “File is not formatted according to project settings” in the inspection settings.

ktlint (Kotlin)

An anti-bikeshedding Kotlin linter with built-in formatter

app/build.gradle

1
apply from: "$project.rootDir/tools/ktlint.gradle"

tools/ktlint.gradle

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
repositories {
jcenter()
}

configurations {
ktlint
}

dependencies {
ktlint "com.pinterest:ktlint:0.32.0"
// additional 3rd party ruleset(s) can be specified here
// just add them to the classpath (e.g. ktlint 'groupId:artifactId:version') and
// ktlint will pick them up
}

task ktlint(type: JavaExec, group: "verification") {
description = "Check Kotlin code style."
classpath = configurations.ktlint
main = "com.pinterest.ktlint.Main"
args "src/**/*.kt", "--reporter=checkstyle, output=${buildDir}/outputs/ktlint.xml"
}


task ktlintFormat(type: JavaExec, group: "formatting") {
description = "Fix Kotlin code style deviations."
classpath = configurations.ktlint
main = "com.pinterest.ktlint.Main"
args "-F", "src/**/*.kt"
}

.editorconfig

1
2
[*.{kt,kts}]
indent_size=4

detekt (Kotlin)

Static code analysis for Kotlin

build.gradle

1
2
3
4
5
6
7
buildscript {}

plugins {
id "io.gitlab.arturbosch.detekt" version "1.0.0-RC14"
}

allprojects {}

tools/detekt.gradle

1
2
3
4
5
6
7
detekt {
toolVersion = "1.0.0-RC14"
input = files("src/main")
filters = ".*/resources/.*,.*/build/.*"
baseline = file("${project.rootDir}/tools/detekt-baseline.xml")
config = files(file("$project.rootDir/tools/detekt.yml"))
}

tools/detekt.xml

The intention of a whitelist is that only new code smells are printed on further analysis. The blacklist can be used to write down false positive detections (instead of suppressing them and polute your code base).

1
2
3
4
5
6
7
8
9
10
<SmellBaseline>
<Blacklist>
<ID>CatchRuntimeException:Junk.kt$e: RuntimeException</ID>
</Blacklist>
<Whitelist>
<ID>NestedBlockDepth:Indentation.kt$Indentation$override fun procedure(node: ASTNode)</ID>
<ID>TooManyFunctions:LargeClass.kt$io.gitlab.arturbosch.detekt.rules.complexity.LargeClass.kt</ID>
<ID>ComplexMethod:DetektExtension.kt$DetektExtension$fun convertToArguments(): MutableList&lt;String&gt;</ID>
</Whitelist>
</SmellBaseline>

tools/detekt.yml

detekt uses a yaml style configuration file for various things:

1
2
3
4
5
6
7
8
9
autoCorrect: true

build:
maxIssues: 10
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1

Run

1
./gradlew detekt

check

app/build.gradle

1
check.dependsOn 'checkstyle', 'findbugs', 'pmd', 'lint', 'ktlint', 'detekt'

Run

1
./gradlew check

Gradle Kotlin DSL

Reference

How to structure your project and manage static resources in React Native

Issue #256

Original post https://medium.freecodecamp.org/how-to-structure-your-project-and-manage-static-resources-in-react-native-6f4cfc947d92


React and React Native are just frameworks, and they do not dictate how we should structure our projects. It all depends on your personal taste and the project you’re working on.

In this post, we will go through how to structure a project and how to manage local assets. This of course is not written in stone, and you are free to apply only the pieces that suit you. Hope you learn something.

For a project bootstrapped with react-native init , we get only the basic structure.

There is the ios folder for Xcode projects, the android folder for Android projects, and an index.js and an App.js file for the React Native starting point.

ios/
android/
index.js
App.js

As someone who has worked with native on both Windows Phone, iOS and Android, I find that structuring a project all comes down to separating files by type or feature

type vs feature

Separating by type means that we organise files by their type. If it is a component, there are container and presentational files. If it is Redux, there are action, reducer, and store files. If it is view, there are JavaScript, HTML, and CSS files.

Group by type

redux
  actions
  store
  reducers
components
  container
  presentational
view
  javascript
  html
  css

This way, we can see the type of each file, and easily run a script toward a certain file type. This is general for all projects, but it does not answer the question “what is this project about?” Is it news application? Is it a loyalty app? Is it about nutrition tracking?

Organising files by type is for a machine, not for a human. Many times we work on a feature, and finding files to fix in multiple directories is a hassle. It’s also a pain if we plan to make a framework out of our project, as files are spread across many places.

Group by feature

A more reasonable solution is to organise files by feature. Files related to a feature should be placed together. And test files should stay close to the source files. Check out this article to learn more.

A feature can be related to login, sign up, onboarding, or a user’s profile. A feature can contain sub-features as long as they belong to the same flow. If we wanted to move the sub feature around, it would be easy, as all related files are already grouped together.

My typical project structure based on features looks like this:

index.js
App.js
ios/
android/
src
  screens
    login
      LoginScreen.js
      LoginNavigator.js
    onboarding
      OnboardingNavigator    
      welcome 
        WelcomeScreen.js
      term
        TermScreen.js
      notification
        NotificationScreen.js
    main
      MainNavigator.js
      news
        NewsScreen.js
      profile
        ProfileScreen.js
      search
        SearchScreen.js
  library
    package.json
    components
      ImageButton.js
      RoundImage.js
    utils
      moveToBottom.js
      safeArea.js
    networking
      API.js
      Auth.js
  res
    package.json
    strings.js
    colors.js
    palette.js
    fonts.js
    images.js
    images
      logo@2x.png
      logo@3x.png
      button@2x.png
      button@3x.png
scripts
  images.js
  clear.js

Besides the traditional files App.js and index.js and the ios1 and android folders, I put all the source files inside the src folder. Inside src I have res for resources, library for common files used across features, and screens for a screen of content.

As few dependencies as possible

Since React Native is heavily dependent on tons of dependencies, I try to be pretty aware when adding more. In my project I use just react-navigation for navigation. And I’m not a fan of redux as it adds unneeded complexity. Only add a dependency when you really need it, otherwise you are just setting yourself up for more trouble than value.

The thing I like about React is the components. A component is where we define view, style and behavior. React has inline style — it’s like using JavaScript to define script, HTML and CSS. This fits the feature approach we are aiming for. That’s why I don’t use styled-components. Since styles are just JavaScript objects, we can just share comment styles in library .

src

I like Android a lot, so I name src and res to match its folder conventions.

react-native init sets up babel for us. But for a typical JavaScript project, it’s good to organise files in the src folder. In my electron.js application IconGenerator, I put the source files inside the src folder. This not only helps in terms of organising, but also helps babel transpile the entire folder at once. Just a command and I have the files in src transpiled to dist in a blink.

babel ./src --out-dir ./dist --copy-files

Screen

React is based around components. Yup. There are container and presentational components, but we can compose components to build more complex components. They usually end in showing in the full screen. It is called Page in Windows Phone, ViewController in iOS and Activity in Android. The React Native guide mentions screen very often as something that covers the entire space:

Mobile apps are rarely made up of a single screen. Managing the presentation of, and transition between, multiple screens is typically handled by what is known as a navigator.

index.js or not?

Each screen is considered the entry point for each feature. You can rename the LoginScreen.js to index.js by leveraging the Node module feature:

Modules don’t have to be files. We can also create a find-me folder under node_modules and place an index.js file in there. The same require(‘find-me’) line will use that folder’s index.js file

So instead of import LoginScreen from ‘./screens/LoginScreen’ , we can just do import LoginScreen from ‘./screens’.

Using index.js results in encapsulation and provides a public interface for the feature. This is all personal taste. I myself prefer explicit naming for a file, hence the name LoginScreen.js.

react-navigation seems to be the most popular choice for handling navigation in a React Native app. For a feature like onboarding, there are probably many screens managed by a stack navigation, so there is OnboardingNavigator .

You can think of Navigator as something that groups sub screens or features. Since we group by feature, it’s reasonable to place Navigator inside the feature folder. It basically looks like this:

import { createStackNavigator } from 'react-navigation'
import Welcome from './Welcome'
import Term from './Term'

const routeConfig = {
  Welcome: {
    screen: Welcome
  },
  Term: {
    screen: Term
  }
}

const navigatorConfig = {
  navigationOptions: {
    header: null
  }
}

export default OnboardingNavigator = createStackNavigator(routeConfig, navigatorConfig)

library

This is the most controversial part of structuring a project. If you don’t like the name library, you can name it utilities, common, citadel , whatever…

This is not meant for homeless files, but it is where we place common utilities and components that are used by many features. Things like atomic components, wrappers, quick fixes function, networking stuff, and login info are used a lot, and it’s hard to move them to a specific feature folder. Sometimes we just need to be practical and get the work done.

In React Native, we often need to implement a button with an image background in many screens. Here is a simple one that stays inside library/components/ImageButton.js . The components folder is for reusable components, sometimes called atomic components. According to React naming conventions, the first letter should be uppercase.

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
import React from 'react'
import { TouchableOpacity, View, Image, Text, StyleSheet } from 'react-native'
import images from 'res/images'
import colors from 'res/colors'

export default class ImageButton extends React.Component {
render() {
return (
<TouchableOpacity style={styles.touchable} onPress={this.props.onPress}>
<View style={styles.view}>
<Text style={styles.text}>{this.props.title}</Text>
</View>
<Image
source={images.button}
style={styles.image} />
</TouchableOpacity>
)
}
}

const styles = StyleSheet.create({
view: {
position: 'absolute',
backgroundColor: 'transparent'
},
image: {

},
touchable: {
alignItems: 'center',
justifyContent: 'center'
},
text: {
color: colors.button,
fontSize: 18,
textAlign: 'center'
}
})

And if we want to place the button at the bottom, we use a utility function to prevent code duplication. Here is library/utils/moveToBottom.js:

import React from 'react'
import { View, StyleSheet } from 'react-native'

function moveToBottom(component) {
  return (
    <View style={styles.container}>
      {component}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-end',
    marginBottom: 36
  }
})

export default moveToBottom

Use package.json to avoid relative path

Then somewhere in the src/screens/onboarding/term/Term.js , we can import by using relative paths:

import moveToBottom from '../../../../library/utils/move'
import ImageButton from '../../../../library/components/ImageButton'

This is a big red flag in my eyes. It’s error prone, as we need to calculate how many .. we need to perform. And if we move feature around, all of the paths need to be recalculated.

Since library is meant to be used many places, it’s good to reference it as an absolute path. In JavaScript there are usually 1000 libraries to a single problem. A quick search on Google reveals tons of libraries to tackle this issue. But we don’t need another dependency as this is extremely easy to fix.

The solution is to turn library into a module so node can find it. Adding package.json to any folder makes it into a Node module . Add package.json inside the library folder with this simple content:

{
  "name": "library",
  "version": "0.0.1"
}

Now in Term.js we can easily import things from library because it is now a module:

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

import React from 'react'
import { View, StyleSheet, Image, Text, Button } from 'react-native'
import strings from 'res/strings'
import palette from 'res/palette'
import images from 'res/images'
import ImageButton from 'library/components/ImageButton'
import moveToBottom from 'library/utils/moveToBottom'

export default class Term extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.heading}>{strings.onboarding.term.heading.toUpperCase()}</Text>
{
moveToBottom(
<ImageButton style={styles.button} title={strings.onboarding.term.button.toUpperCase()} />
)
}
</View>
)
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center'
},
heading: {
...palette.heading, ...{
marginTop: 72
}
}
})

res

You may wonder what res/colors, res/strings , res/images and res/fonts are in the above examples. Well, for front end projects, we usually have components and style them using fonts, localised strings, colors, images and styles. JavaScript is a very dynamic language, and it’s easy to use stringly types everywhere. We could have a bunch of #00B75D color across many files, or Fira as a fontFamily in many Text components. This is error-prone and hard to refactor.

Let’s encapsulate resource usage inside the res folder with safer objects. They look like the examples below:

res/colors

const colors = {
  title: '#00B75D',
  text: '#0C222B',
  button: '#036675'
}

export default colors

res/strings

const strings = {
  onboarding: {
    welcome: {
      heading: 'Welcome',
      text1: "What you don't know is what you haven't learn",
      text2: 'Visit my GitHub at [https://github.com/onmyway133'](https://github.com/onmyway133'),
      button: 'Log in'
    },
    term: {
      heading: 'Terms and conditions',
      button: 'Read'
    }
  }
}

export default strings

res/fonts

const fonts = {
  title: 'Arial',
  text: 'SanFrancisco',
  code: 'Fira'
}

export default fonts

res/images

const images = {
  button: require('./images/button.png'),
  logo: require('./images/logo.png'),
  placeholder: require('./images/placeholder.png')
}

export default images

Like library , res files can be access from anywhere, so let’s make it a module . Add package.json to the res folder:

{
  "name": "res",
  "version": "0.0.1"
}

so we can access resource files like normal modules:

import strings from 'res/strings'
import palette from 'res/palette'
import images from 'res/images'

Group colors, images, fonts with palette

The design of the app should be consistent. Certain elements should have the same look and feel so they don’t confuse the user. For example, the heading Text should use one color, font, and font size. The Image component should use the same placeholder image. In React Native, we already use the name styles with const styles = StyleSheet.create({}) so let’s use the name palette.

Below is my simple palette. It defines common styles for heading and Text:

res/palette

import colors from './colors'

const palette = {
  heading: {
    color: colors.title,
    fontSize: 20,
    textAlign: 'center'
  },
  text: {
    color: colors.text,
    fontSize: 17,
    textAlign: 'center'
  }
}

export default palette

And then we can use them in our screen:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center'
  },
  heading: {...palette.heading, ...{
    marginTop: 72
  }}
})

Here we use the object spread operator to merge palette.heading and our custom style object. This means that we use the styles from palette.heading but also specify more properties.

If we were to reskin the app for multiple brands, we could have multiple palettes. This is a really powerful pattern.

Generate images

You can see that in /src/res/images.js we have properties for each image in the src/res/images folder:

const images = {
  button: require('./images/button.png'),
  logo: require('./images/logo.png'),
  placeholder: require('./images/placeholder.png')
}

export default images

This is tedious to do manually, and we have to update if there’s changes in image naming convention. Instead, we can add a script to generate the images.js based on the images we have. Add a file at the root of the project /scripts/images.js:

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
const fs = require('fs')

const imageFileNames = () => {
const array = fs
.readdirSync('src/res/images')
.filter((file) => {
return file.endsWith('.png')
})
.map((file) => {
return file.replace('[@2x](http://twitter.com/2x).png', '').replace('[@3x](http://twitter.com/3x).png', '')
})

return Array.from(new Set(array))
}

const generate = () => {
let properties = imageFileNames()
.map((name) => {
return `${name}: require('./images/${name}.png')`
})
.join(',\n ')

const string = `const images = {
${properties}
}

export default images
`

fs.writeFileSync('src/res/images.js', string, 'utf8')
}

generate()

The cool thing about Node is that we have access to the fs module, which is really good at file processing. Here we simply traverse through images, and update /src/res/images.js accordingly.

Whenever we add or change images, we can run:

node scripts/images.js

And we can also declare the script inside our main package.json :

"scripts": {
  "start": "node node_modules/react-native/local-cli/cli.js start",
  "test": "jest",
  "lint": "eslint *.js **/*.js",
  "images": "node scripts/images.js"
}

Now we can just run npm run images and we get an up-to-date images.js resource file.

How about custom fonts

React Native has some custom fonts that may be good enough for your projects. You can also use custom fonts.

One thing to note is that Android uses the name of the font file, but iOS uses the full name. You can see the full name in Font Book app, or by inspecting in running app

for (NSString* family in [UIFont familyNames]) {
  NSLog(@"%@", family);

for (NSString* name in [UIFont fontNamesForFamilyName: family]) {
    NSLog(@"Family name:  %@", name);
  }
}

For custom fonts to be registered in iOS, we need to declare UIAppFonts in Info.plist using the file name of the fonts, and for Android, the fonts need to be placed at app/src/main/assets/fonts .

It is good practice to name the font file the same as full name. React Native is said to dynamically load custom fonts, but in case you get “Unrecognized font family”, then simply add those fonts to target within Xcode.

Doing this by hand takes time, luckily we have rnpm that can help. First add all the fonts inside res/fonts folder. Then simply declare rnpm in package.json and run react-native link . This should declare UIAppFonts in iOS and move all the fonts into app/src/main/assets/fonts for Android.

"rnpm": {
  "assets": [
    "./src/res/fonts/"
  ]
}

Accessing fonts by name is error prone, we can create a script similar to what we have done with images to generate a safer accession. Add fonts.js to our scripts folder

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
const fs = require('fs')

const fontFileNames = () => {
const array = fs
.readdirSync('src/res/fonts')
.map((file) => {
return file.replace('.ttf', '')
})

return Array.from(new Set(array))
}

const generate = () => {
const properties = fontFileNames()
.map((name) => {
const key = name.replace(/\s/g, '')
return `${key}: '${name}'`
})
.join(',\n ')

const string = `const fonts = {
${properties}
}

export default fonts
`

fs.writeFileSync('src/res/fonts.js', string, 'utf8')
}

generate()

Now you can use custom font via R namespace.

1
2
3
4
5
6
7
import R from 'res/R'

const styles = StyleSheet.create({
text: {
fontFamily: R.fonts.FireCodeNormal
}
})

The R namespace

This step depends on personal taste, but I find it more organised if we introduce the R namespace, just like how Android does for assets with the generated R class.

Once you externalize your app resources, you can access them using resource IDs that are generated in your project’s Rclass. This document shows you how to group your resources in your Android project and provide alternative resources for specific device configurations, and then access them from your app code or other XML files.

This way, let’s make a file called R.js in src/res:

import strings from './strings'
import images from './images'
import colors from './colors'
import palette from './palette'

const R = {
  strings,
  images,
  colors,
  palette
}

export default R

And access it in the screen:

1
2
3
4
5
6
7
8
9
10
11
12
13
import R from 'res/R'
render() {
return (
<SafeAreaView style={styles.container}>
<Image
style={styles.logo}
source={R.images.logo} />
<Image
style={styles.image}
source={R.images.placeholder} />
<Text style={styles.title}>{R.strings.onboarding.welcome.title.toUpperCase()}</Text>
)
}

Replace strings with R.strings, colors with R.colors, and images with R.images. With the R annotation, it is clear that we are accessing static assets from the app bundle.

This also matches the Airbnb convention for singleton, as our R is now like a global constant.

23.8 Use PascalCase when you export a constructor / class / singleton / function library / bare object.

const AirbnbStyleGuide = {
  es6: {
  },
}

export default AirbnbStyleGuide

Where to go from here

In this post, I’ve shown you how I think you should structure folders and files in a React Native project. We’ve also learned how to manage resources and access them in a safer manner. I hope you’ve found it useful. Here are some more resources to explore further:

How to overlay view on another view in React Native

Issue #254

Original post https://stackoverflow.com/a/54108708/1418457


Make our own convenient OverlayContainer. The trick is to use absolute with 100% size

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
// @flow

import React from 'react'
import { View, StyleSheet } from 'react-native'

type Props = {
behind: React.Component,
front: React.Component,
under: React.Component
}

// Show something on top of other
export default class OverlayContainer extends React.Component<Props> {
render() {
const { behind, front, under } = this.props

return (
<View style={styles.container}>
<View style={styles.center}>
<View style={styles.behind}>
{behind}
</View>
{front}
</View>
{under}
</View>
)
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
height: 100,
justifyContent: 'center',
},
center: {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
},
behind: {
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%'
}
})

How to remove Cartography in iOS

Issue #252

Read more https://medium.com/flawless-app-stories/how-to-make-auto-layout-more-convenient-in-ios-df3b42fed37f

Description

This is a script to remove Cartography, and use plain NSLayoutAnchor syntax.
Use Constraint.on() from Sugar.
It will change all .swift files recursively under provided folder.

1
2
3
4
Constraint.on(
logoImageView.widthAnchor.constraint(equalToConstant: 74),
view1.leftAnchor.constraint(equalTo: view2.leftAnchor, constant: 20),
)

Features

  • Parse recursively
  • Parse a single file
  • Parse each constrain block separately
  • Handle ==, >=, <=
  • Handle center, edges
  • Infer indentation
  • Handle relation with other item
  • Handle relation with constant
  • Handle multiplier
  • Auto import Sugar
  • Infer superView

Prepare

Install tool if needed

1
brew install yarn

How to use

1
2
yarn install
yarn start /Users/khoa/path/to/project

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300

const Glob = require('glob')
const Fs = require('fs')

function main() {
const projectPath = getProjectPath()
const files = getAllFiles(projectPath)

files.forEach((file) => {
handleFile(file)
})
}

/// The 1st argument is node, 2nd is index.js, 3rd is path
function getProjectPath() {
return process.argv[2]
}

/// Only select `swift` file
function getAllFiles(projectPath) {
if (projectPath.endsWith('.swift')) {
return [projectPath]
} else {
const files = Glob.sync(`${projectPath}/**/*.swift`)
return files
}
}

/// Read file, replace by matches, and write again
function handleFile(file) {
console.log(file)

Fs.readFile(file, 'utf8', (error, data) => {
// non greedy `*?`
const pattern = '(.)*constrain\\(.+\\) {\\s(\\s|.)*?\\n(\\s)*}'
const regex = new RegExp(pattern)

let matches = data.match(regex)

// RegEx only return the 1st match per execution, let's do a recursion
while (matches != null) {
const match = matches[0]

const indentationLength = findIndentationLength(match)
const rawTransforms = handleMatch(match)
const transforms = handleSuperview(rawTransforms, match)
const statements = handleTransforms(transforms, indentationLength)
const string = handleStatements(statements, indentationLength)
data = data.replace(match, string)

// Examine again
matches = data.match(regex)
}

// Handle import
data = handleImport(data)


Fs.writeFile(file, data, 'utf8')
})
}

/// Replace or remove import
function handleImport(data) {
if (data.includes('import Sugar')) {
return data.replace('import Cartography', '')
} else {
return data.replace('import Cartography', 'import Sugar')
}
}

/// Format transform to have `,` and linebreaks
function handleTransforms(transforms, indentationLength) {
let string = ''

// Expect the first is always line break
if (transforms[0] != '\n') {
transforms.unshift('\n')
}

const indentation = makeIndentation(indentationLength + 2)

transforms.forEach((line, index) => {
if (line.length > 1) {
string = string.concat(indentation + line)
if (index < transforms.length - 1) {
string = string.concat(',\n')
}
} else {
string = string.concat('\n')
}
})

return string
}

/// Replace superView
function handleSuperview(transforms, match) {
const superView = findSuperview(match)
if (superView == null) {
return transforms
}

const superViewPattern = ' superView.'
transforms = transforms.map((transform) => {
if (transform.includes(superViewPattern)) {
return transform.replace(superViewPattern, ` ${superView}.`)
} else {
return transform
}
})

return transforms
}

/// Embed the statements inside `Constraint.on`
function handleStatements(statements, indentationLength) {
const indentation = makeIndentation(indentationLength)
return `${indentation}Constraint.on(${statements}\n${indentation})`
}

/// Turn every line into flatten transformed line
function handleMatch(match) {
let lines = match.split('\n')
lines = lines.map((line) => {
return handleLine(line.trim())
}).filter((transforms) => {
return transforms != null
})

let flatten = [].concat.apply([], lines)
return flatten
}

/// Check to handle lines, turn them into `LayoutAnchor` statements
function handleLine(line) {
const itemPattern = '\\w*\\.\\w* (=|<|>)= \\w*\\.*\\..*'
const sizePattern = '\w*\.\w* (=|<|>)= (\s|.)*'

if (line.includes('edges == ')) {
return handleEdges(line)
} else if (line.includes('center == ')) {
return handleCenter(line)
} else if (hasPattern(line, itemPattern) && !hasSizeKeywords(line)) {
return handleItem(line)
} else if (hasPattern(line, sizePattern)) {
return handleSize(line)
} else if (line.includes('>=') || line.includes('>=')) {
return line
} else if (line.includes('.')) {
// return the line itself to let the human fix
return line
} else if (line.length == 0) {
return ['\n']
} else {
return null
}
}

/// For ex: listView.bottom == listView.superview!.bottom - 43
/// listView.bottom == listView.superview!.bottom - Metrics.BackButtonWidth
function handleItem(line) {
const equalSign = getEqualSign(line)
const parts = line.split(` ${equalSign} `)
const left = parts[0]

let rightParts = parts[1].trim()
let right = rightParts.split(' ')[0]
let number = rightParts.replace(right, '').replace('- ', '-').trim()
if (number.startsWith('+ ')) {
number = number.slice(2)
}

let equal = getEqual(line)

if (number == null || number.length == 0 ) {
return [
`${left}Anchor.constraint(${equal}: ${right}Anchor)`
]
} else {
return [
`${left}Anchor.constraint(${equal}: ${right}Anchor, constant: ${number})`
]
}
}

/// For ex: segmentedControl.height == 24
/// backButton.width == Metrics.BackButtonWidth
function handleSize(line) {
const equalSign = getEqualSign(line)
const parts = line.split(` ${equalSign} `)
const left = parts[0]
const right = parts[1]

let equal = getEqual(line)

return [
`${left}Anchor.constraint(${equal}Constant: ${right})`
]
}

/// For ex: mapView.edges == listView.edges
function handleEdges(line) {
const parts = line.split(' == ')
const left = parts[0].split('.')[0]
const right = removeLastPart(parts[1])

return [
`${left}.topAnchor.constraint(equalTo: ${right}.topAnchor)`,
`${left}.bottomAnchor.constraint(equalTo: ${right}.bottomAnchor)`,
`${left}.leftAnchor.constraint(equalTo: ${right}.leftAnchor)`,
`${left}.rightAnchor.constraint(equalTo: ${right}.rightAnchor)`
]
}

/// For ex: mapView.center == listView.center
function handleCenter(line) {
const parts = line.split(' == ')
const left = parts[0].split('.')[0]
const right = removeLastPart(parts[1])

return [
`${left}.centerXAnchor.constraint(equalTo: ${right}.centerXAnchor)`,
`${left}.centerYAnchor.constraint(equalTo: ${right}.centerYAnchor)`
]
}

function hasPattern(string, pattern) {
const regex = new RegExp(pattern)
let matches = string.match(regex)

if (matches == null) {
return false
}

matches = matches.filter((match) => {
return match !== undefined && match.length > 1
})

return matches.length > 0
}

function removeLastPart(string) {
const parts = string.split('.')
parts.pop()

return parts.join('.')
}

function getEqual(line) {
if (line.includes('==')) {
return 'equalTo'
} else if (line.includes('<=')) {
return 'lessThanOrEqualTo'
} else {
return 'greaterThanOrEqualTo'
}
}

function getEqualSign(line) {
if (line.includes('==')) {
return '=='
} else if (line.includes('>=')) {
return '>='
} else {
return '<='
}
}

function findIndentationLength(match) {
return match.split('constrain')[0].length + 1
}

function makeIndentation(length) {
return Array(length).join(' ')
}

// For ex: constrain(tableView, headerView, view) { tableView, headerView, superView in
function findSuperview(match) {
const line = match.split('\n')[0]
if (!line.includes(' superView in')) {
return null
}

const pattern = ', \\w*\\)'
const regex = RegExp(pattern)
const string = line.match(regex)[0] // `, view)

return string.replace(', ', '').replace(')', '')
}

// Check special size keywords
function hasSizeKeywords(line) {
const keywords = ['Metrics', 'Dimensions', 'UIScreen']
return keywords.filter((keyword) => {
return line.includes(keyword)
}).length != 0
}

main()

How to sort strings with number in Javascript

Issue #251

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
function sort() {
const string =
`
- Favorite WWDC 2017 sessions https://github.com/onmyway133/blog/issues/56
- Favorite WWDC 2018 sessions https://github.com/onmyway133/blog/issues/245
- How to do clustering with Google Maps in iOS https://github.com/onmyway133/blog/issues/191
`

const lines = string
.split('\n')
.filter((line) => { return line.length > 0 })
.map((line) => {
let parts = line.trimEnd().split(' ')
let lastPart = parts[parts.length-1]
let number = lastPart.replace('https://github.com/onmyway133/blog/issues/', '')
return {
line,
number: parseInt(number)
}
})

lines.sort((a, b) => {
return (a.number < b.number) ? -1 : 1
})

const sortedString = lines
.map((tuple) => {
return tuple.line
})
.join('\n')

console.log(sortedString)
}

Then node index.js

How to move tab bar icons down in iOS

Issue #250

UITabBarItem subclasses from UIBarItem which has imageInsets. We need both top and bottom to avoid shrinking

1
viewController1.tabBarItem.imageInsets = UIEdgeInsets(top: 10, left: 0, bottom: -10, right: 0)

How to test LaunchScreen in iOS

Issue #249

Making splash screen with LaunchScreen.storyboard is now the default way to do in iOS. Testing it with UITests is a bit tricky as this screen is showed the system, and if we test that, we are just testing the system.

What we should test is the content we put in the LaunchScreen storyboard. Is it showing correctly on different screen sizes? Is it missing any texts or images?

One way to test that is via Unit Test. LaunchScreen storyboard always come with 1 UIViewController configured as an initial view controller

1
2
3
4
5
6
7
8
9
10
11
12
class LauncScreenTests: XCTestCase {
func testLaunchScreen() {
let launchScreen = UIStoryboard(name: "LaunchScreen", bundle: nil)
let viewController = launchScreen.instantiateInitialViewController()!

let label = viewController.view.subviews.compactMap({ $0 as? UILabel }).first!
XCTAssertEqual(label.text, "Welcome to my app")

let imageView = viewController.view.subviews.compactMap({ $0 as? UIImageView }).first!
XCTAssertNotNil(imageView.image)
}
}

How to use remote theme for GitHub Pages

Issue #248

Visit https://github.com/onmyway133/onmyway133.github.io


https://github.blog/2017-11-29-use-any-theme-with-github-pages/

Starting today, you can use any of the hundreds of community-curated themes on GitHub.com. To build your site with any public, GitHub-hosted theme, add the following to your site’s _config.yml file:

1
remote_theme: owner/name

Browse themes https://github.com/topics/jekyll-theme
Example config file with navigation https://github.com/daviddarnes/alembic/blob/master/_config.yml

Each page must have a description. Link must point to generated html file

1
Apps: /apps.html

Do not specify layout: default to keep overall theme

1
2
3
4
---
title: Another Page
description:
---

How to enable black mode in Google Maps in iOS

Issue #246

Use GMSMapStyle https://developers.google.com/maps/documentation/android-sdk/styling
Export styling json from https://mapstyle.withgoogle.com/

1
2
let mapStyleUrl = Bundle.main.url(forResource: "mapStyle", withExtension: "json")!
mapView.mapStyle = try? GMSMapStyle(contentsOfFileURL: mapStyleUrl)

To change overall color, search for mostly "elementType": "geometry" and "featureType": "water"

1
2
3
4
5
6
7
8
{
"elementType": "geometry",
"stylers": [
{
"color": "#424242"
}
]
}
1
2
3
4
5
6
7
8
9
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#2E2E2E"
}
]
}

Favorite WWDC 2018 sessions

Issue #245

Original post https://medium.com/fantageek/my-favourite-wwdc-2018-sessions-363d3fc9c9d5


Favourite WWDC 2018 sessions

This year I failed the lottery ticket to WWDC, and I also missed the keynote live stream because I was sailing on the Christian Radich outside Oslo fjord that day. Luckily all the videos are available on Apple Developer site very shortly, and we can watch them now on Chrome or the unofficial WWDC app on macOS. I recommend the WWDC macOS app as it allows to mark favourites and filter, also offers the ability to adjust play speed to 1.25 or 1.5 saves me some time watching.

This year WWDC focuses a lot on privacy, stability, and speed, which are all I wish, so many thanks to Apple engineers who made that happen, and the resit to install the so called more stable iOS 12 is real. As an iOS engineers, I like to focus more about the necessary things to me, that is about the Swift programming language, new changes in Cocoa Touch, enhancements in Xcode and testing tricks. I also like to explore more about machine learning so I’m very glad that Apple is investing more into this technology with the introduction of Turi Create and Create ML.

To me, APIs come and get deprecated very often and it’s good to know them, but the most important thing is to invest in your programming, debugging and testing skill which you can apply in many other platforms.

Continued from last year favourites list, below are my favourite sessions with personal notes. Things are listed in no particular order. Hope you find it useful.

Platforms State of the Union

If you don’t have time, you should watch only this session. Platform State of the Union is like keynote for developers as it highlights important changes.

  • Privacy: Apple confirms on its commitment in privacy and security, also introduces password management feature and auto fill on iOS 12. Generating strong password, integrating with 3rd password management and quickly populating OTP field from SMS message have never been easier. GateKeeper gets some improvements as well and begins to require app to be notarised.

  • iOS 12: huge improvement in performance, Siri gets smarter with Shortcut support, group calling in FaceTime and grouped notification. Also for emoji fan, Memoji was introduced.

  • macOS 10.14 Mojave: more with Dark Mode. They demo mostly with Xcode in dark mode, which looks so cool. This year WWDC banner give hints about iOS and macOS cross-platform apps, which is partially true with Marzipan, a way to allow iOS apps to run on the mac.

  • Xcode 10: with improvements in code editing and source control changes bar indicator. Debugging with memory debug tool, LLDB performance enhancement and particular the new build system completely rewritten in Swift with parallel tasks are exciting news.

  • Swift 4.2: if you follow swift repo then Swift 4.2 may not be very surprising. There are also announcements for Swift 5 plan.

  • Machine Learning: is never hotter than this. This year we see huge investments in machine learning with Create ML, Turi Create, Natural Language frameworks, CoreML 2, new detection capabilities in Vision.

  • ARKit 2, watchOS 5, tvOS 12, AppStore Connect and AppStore Connect APIs are some other important news you don’t want to miss.

What’s new in Swift

Together with this session, I recommend you to read What’s new in Swift 4.2 summary which is very succinct. Besides improvement in complication and runtime, Swift 4.2 offers some new features: iterable enum case, synthesised Equatable and Hashable, handy functions for shuffling, random generating. To me, the need to explicitly handle Implicitly unwrapped optional is also a reasonable change.

What’s New in Cocoa Touch

This is a brief introduction to all changes coming to iOS 12, together with tips on how to be a good iOS citizen. Learn what can affect scrolling experience and prefetching technique, memory consumption and automatic backing stores, how to get the best from UIImage and UIImageView . AutoLayout engine got a lot of performance improvement so it won’t bother you any more. To me the most satisfying is to get all the UIKit notifications and classes reorganised under nested types, which makes code reasoning very easy.

Getting the Most out of Playgrounds in Xcode

I’ve written about Playground before and I’m very glad that Apple also invests a lot in it. The way people can interact and train model [Create ML](http://Introducing Create ML) in Playground is mesmerising. People may question how Playground works so well in session’s demos, but we can’t resist the new changes coming to Playground like Step by Step execution, markup rendering improvements and also how easy it is to consume custom frameworks. We can also now publish our own Playground through subscription.

What’s New in Core ML

Apple starts the machine learning trend last year with the introduction of Core ML. We might be excited and frustrated at the same time as Core ML is powerful but there’s no way we can customise it. Now the 2 parts tell us how to implement custom layer and model, techniques to reduce model size like quantisation and flexible model. This makes the foundation for improvement in Vision in object tracking and the debut of Natural Language framework. Machine learning has never been easier.

What’s New in Testing

I can’t miss any testing sessions as it is part of every day’s work. How can your program avoids regression bugs and ready for refactoring without any tests?

This session shows improvement in coverage and the introduction of xccov tool to help us build automation on top of coverage report. Parallel distributed testing in Xcode 10 can save us some time to have coffee. Another wonderful news is that tests have multiple order execution mode to avoid bugs due to implicit dependencies.

Testing Tips & Tricks 🌟

This is my most favourite. The session starts with a pyramid of tests with unit integration and end-to-end test analogy explanation, then to some very cool tips and tricks.

  • Testing network request: I like the separation of APIRequest and APIRequestLoader with URLSession , dependency injection with default parameter and the customisation of URLProtocol in URLSessionConfiguration

  • Testing notification: Notification is system wide and I try to avoid it as much as possible. This shows how to inject dependency with default parameter and use of own NotificationCenter instead of NotificationCenter.default to ease testing

  • Testing location: build abstraction with LocationProvider and LocationFetcher . How to use custom protocol and protocol for delegate to mock during test

  • Testing timer: how to use and mock RunLoop behaviour with Timer

Advanced Debugging with Xcode and LLDB

LLDB has been improved to enable to debugging reliability experience, issues with AST context corruption, Swift type resolution are now things in the past. We can review how to use some common LLDB commands with handy arguments, and how to use Xcode breakpoint to its best.

A Tour of UICollectionView 🌟

I begin to use UICollectionView more than UITableView , and it also has same code as NSCollectionView,which is more comfortable solution than the horrible NSTableView .

  • Item size in UICollectionViewLayout : I often rely on UICollectionViewDelegateFlowLayout to specify item size but after watching this session, I feel like moving size related code to Layout object feels more like a good way to go

  • Mosaic layout: This is not new, but good to watch again. You learn how to implement custom layout using cached layout attributes

  • Data Source update: I didn’t expect Apple mentions this, but it is a good lesson on how UICollectionView handles batch update. I ‘ve written about this before in my A better way to update UICollectionView data in Swift with diff framework and that post gets a lot of attractions. In this session we need to remember that *ordering matters in data source update, but not in collection view update *❗️❗️❗️

Swift Generics

Generic was a core feature of Swift since the beginning, we all know that it helps us to implement generic code that can work with many types in a type safe manner. This session reminds that I ‘ve never learned enough, especially the reasonable design behind it.

The sessions showcases Collection protocol and its protocol inheritances: MutableCollection , BidirectionalCollection , RandomAccessCollection and how they can be combined to provide generic functionalities for conformers. The associatedtype requirement in each protocol, especially Index and Element, is very minimal and has enough constraints for the protocol to implement lots of necessary functionalities in its protocol extension, which is eye opening for me. I very like to read open source, so looking at the source code for such protocols helps me understand more.

The part about Fisher Yates shuffle algorithm details how we can come up with necessary protocol while still make them generic

Pay attention to when they mention count and map , you can learn more how each concrete type can hook into the customisation point in protocol extension

Finally learn the Liskov substitution principle with protocol in class inheritance. You should also Using Collections Effectively for how to utilise handy functions in Collection.

Data You Can Trust

Although Codable has a lot to offers in term of data integrity, this is good to know about to make sure the data you receive is actually the right data in correct format and structure. CommonCrypto is also part of new iOS SDK so you don’t need my Arcane library to handle encryption and hashing in your apps.

Embracing Algorithms

This is the most pleasant to watch as it is like a conversation between the speaker and the imaginary manager Crusty. Here I learn how to be aware of algorithm complexity and also how to utilise built in Foundation functions which are already optimised for performance.

After this session I can’t help myself but going to Swift repo to read the Algorithms.swift file immediately.

Image and Graphics Best Practices

Learn how image encoding and decoding works through data and image buffer and how that affects memory and performance. There are techniques like downsampling that can tackle this problem. This also recommends against using backing store, and instead, use UIImageView

A Guide to Turi Create

I’ve written about Turi Create before, but it is just scratching the surface of the many tasks offered by Turi. This year Apple releases Turi Create 5 with style transfer task, Vision Feature Print, GPU acceleration and recommender model improvements. I can’t wait to explore. And if you take a look at MLDataTable in Create ML framework, it looks like this has Turi ‘s SFrame under the hood.

That’s it. Thanks for reading. What are your favourite sessions this year? Please share in the comment section below

How to get running window informations in macOS

Issue #243

From https://developer.apple.com/documentation/coregraphics/1455137-cgwindowlistcopywindowinfo

Generates and returns information about the selected windows in the current user session.

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
struct MyWindowInfo {
let frame: CGRect
let name: String
let pid: Int
let number: Int

init?(json: [String: Any]) {
guard let pid = json["kCGWindowOwnerPID"] as? Int else {
return nil
}

guard let name = json["kCGWindowOwnerName"] as? String else {
return nil
}

guard let rect = json["kCGWindowBounds"] as? [String: Any] else {
return nil
}

guard let x = rect["X"] as? CGFloat else {
return nil
}

guard let y = rect["Y"] as? CGFloat else {
return nil
}

guard let height = rect["Height"] as? CGFloat else {
return nil
}

guard let width = rect["Width"] as? CGFloat else {
return nil
}

guard let number = json["kCGWindowNumber"] as? Int else {
return nil
}

self.pid = pid
self.name = name
self.number = number
self.frame = CGRect(x: x, y: y, width: width, height: height)
}
}

guard let jsons = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as? [[String: Any]] else {
return
}

let infos = jsons.compactMap({ MyWindowInfo(json: $0) })

How to show full screen window programmatically in macOS

Issue #242

1
2
3
4
5
let window = NSWindow(contentRect: mainScreen.frame, styleMask: .borderless, backing: .buffered, defer: false)
window.level = .floating
window.contentView = NSView()
window.makeKeyAndOrderFront(NSApp)
NSApp.activate(ignoringOtherApps: true)

and then later hide it

1
window.orderOut(NSApp)

Useful git commands for everyday use!

Issue #239

Original post https://medium.com/flawless-app-stories/useful-git-commands-for-everyday-use-e1a4de64037d


Do you know that questions about git get the most views on StackOverflow? I’ve searched a lot on Google how to execute certain actions with git, and this actually slowed me down a lot. There are some actions that we tend to use a lot, so it’s good to learn them. Here are my favorites, learning from friends and internet, hope you find them useful.

Before we begin, you should run git –version to check your current git version, mine is 2.12.2 as in macOS High Sierra. Here is the official git documentation, you can read details about git commands, parameters and new releases of git.

Useful commands

Diff

1
2
git diff feature/my_branch develop > file.diff
git apply file.diff

🔍 Status

Check the status of working directory and staging area:

git status

Show changes between HEAD and working directory:

git diff

Show the list of commits in one line format:

git log --oneline

Show commits that make add or remove a certain string:

git log -S 'LoginViewController'

Search commits that contain a log message:

git log — all — grep=’day of week’

🔍 Tag

List all tags:

git tag

Tag a commit:

git tag -a 1.4 -m "my version 1.4"

Delete remote tags:

git push --delete origin tagname

git push origin :tagname

Push tag to remote:

git push origin tagname

Rename tag:

git tag new old
git tag -d old
git push origin :refs/tags/old
git push --tags

Move tag from one commit to another commit:

git push origin :refs/tags/<tagname>
git tag -fa tagname
git push origin master --tags

🔍 Remote

List all remote:

git remote

Rename remote:

git remote rename old new

Remove stale remote tracking branches:

git remote prune origin

🔍 Branch

List all branches:

git branch

Create the branch on your local machine and switch in this branch:

git checkout -b branch_name

Create branch from commit:

git branch branch_name sha1_of_commit

Push the branch to remote:

git push origin branch_name

Rename other branch:

git branch -m old new

Rename current branch:

git branch -m new

Rename remote branch:

git branch -m old new               # Rename branch locally    
git push origin :old                 # Delete the old branch    
git push --set-upstream origin new   # Push the new branch, set local branch to track the new remote

Delete a branch:

git branch -D the_local_branch

git push origin :the_remote_branch

Delete all local branches but master

git branch | grep -v "master" | xargs git branch -D

🔍 Commit

Undo last commit:

git reset --hard HEAD~1

Squash last n commits into one commit:

git rebase -i HEAD~5

git reset --soft HEAD~5
git add .
git commit -m "Update"
git push -f origin master

Move last commits into new branch:

git branch newbranch
git reset --hard HEAD~3 # Go back 3 commits. You *will* lose uncommitted work.*1
git checkout newbranch

Make changes to older commit

1
2
3
4
git rebase -i HEAD^^^
// change from pick to edit, then :wq
git add .
git rebase --continue

🔍 Cherry Pick

Add some commits to the top of the current branch:

git cherry-pick hash_commit_A hash_commit_B

🔍 Reflog

Show reflog:

git reflog

Get commit:

git reset --hard 0254ea7

git cherry-pick 12944d8

🔍 Revert

Revert the previous commit:

git revert HEAD
git commit

Revert the changes from previous 3 commits without making commit:

git revert --no-commit HEAD~3..

🔍 Amend

Amend previous commit:

git commit --amend

git commit --amend --no-edit

git commit --amend -m "New commit message"

Changing git commit message after push:

git commit --amend -m "New commit message"
git push --force <repository> <branch>

🔍 Checkout

Checkout a tag:

git checkout tagname

git checkout -b newbranchname tagname

Checkout a branch:

git checkout destination_branch

Use -m if there is merge conflict:

git checkout -m master // from feature branch to master

Checkout a commit:

git checkout commit_hash

git checkout -b newbranchname HEAD~4

git checkout -b newbranchname commit_hash

git checkout commit_hash file

Checkout a file:

git checkout c5f567 -- Relative/Path/To/File

🔍 Stash

Save a change to stash:

git stash save "stash name"

git stash

List all stashes:

git stash list

Apply a stash:

git stash pop

git stash apply

git stash apply stash@{2}

🔍 Rebase

Rebase the current branch onto master:

git rebase master // rebase the current branch onto master

Continue rebase:

git rebase --continue

Abort rebase:

git rebase --abort

🔍 .gitignore

Un-track files that have just been declared in .gitignore:

git rm -r --cached .
git add .
git commit -am "Remove ignored files"

🔍 Index

Remove untracked files:

git clean

Remove file from index:

git reset file

Reset the index to match the most recent commit:

git reset

Reset the index and the working directory to match the most recent commit:

git reset --hard

🔍 Misc

Get their changes during git rebase:

git checkout --ours foo/bar.java
git add foo/bar.java

Get their changes during git merge:

git pull -X theirs

git checkout --theirs path/to/the/conflicted_file.php

git checkout --theirs .
git add .

git checkout branchA
git merge -X theirs branchB

Merge commits from master into feature branch:

git checkout feature1
git merge --no-ff master

Find bug in commit history in a binary search tree style:

git bisect start

git bisect good

git bisect bad

Git alias

If there are commands that you use a lot, then consider using git alias. This is how to make alias for git status, then you can just type git st:

git config — global alias.st status

Alias configurations are stored in .gitconfig file, you can learn some cool aliases from thoughtbot and mathiasbynens.

Delete all local branches except master

1
git config --global alias.dlb '!git branch | grep -v "master" | xargs git branch -D'

Prune upstream branches

1
git config --global alias.pu 'remote prune upstream'

GUI clients

Doing things in command line is cool and faster. However for viewing branches and commits, I find using a GUI client more visualizing and comfortable. You can see a list of all GUI clients here, I myself use SourceTree.

Check before you commit

We usually have some experiment code that we don’t want they to step into our commit. I usually mark my experiment with // but sometimes forget to unstage that.

Starting with 2.9, Git has improvement on its commit hook which makes it globally using hooksPath.

Firstly we nee to create a file called pre-commit, and place it into, for example, /Users/khoa/hooks:

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
#!/bin/sh

# https://appventure.me/2016/04/04/prevent-accidental-test-code-commits/

if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

# The special marker tag to mark things which we still need to change
marker="<TEST>"

# Redirect output to stderr.
exec 1>&2

if test $(git diff --cached -z $against | grep $marker | wc -c) != 0
then
cat <<\EOF
Error: Still has invalid debug markers in code:
EOF
echo `git diff --cached -z $against -G $marker`
exit 1
fi

In your project, run git config core.hooksPath /Users/khoa/hooks.

Whenever you commit a file with that pattern, it won’t let you commit. For how to make this work in SourceTree, check:
SourceTree and pre commit hook
Pre-commit file works perfectly in terminal, but SourceTree seems to ignore it. I use both terminal and SourceTree, as…medium.com

Where to go from here

This is just scratching the surface of what git can do, if you want to learn more, here are some links to get started.

How to work around app damaged warning in macOS

Issue #238

“App” is damaged and can’t be opened. You should move it to the Trash.

👉 Disable gate keeper

1
2
sudo spctl --master-disable
spctl --status

Current workaround is to remove Launch At Login handling code.

How to shake NSView in macOS

Issue #233

Animation on macOS using CAAnimation

Shake

1
2
3
4
5
6
7
8
9
10
let midX = box.layer?.position.x ?? 0
let midY = box.layer?.position.y ?? 0

let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.06
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = CGPoint(x: midX - 10, y: midY)
animation.toValue = CGPoint(x: midX + 10, y: midY)
box.layer?.add(animation, forKey: "position")

Animation on macOS using NSAnimationContext

Wiggle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSAnimationContext.runAnimationGroup({ context in
let animation = CAKeyframeAnimation(keyPath: "transform")
animation.beginTime = CACurrentMediaTime() + 5.0
animation.duration = 0.1
animation.autoreverses = true
let wobbleAngle: CGFloat = 0.08
animation.values = [
NSValue(caTransform3D: CATransform3DMakeRotation(wobbleAngle, 0.0, 0.0, 1.0)),
NSValue(caTransform3D: CATransform3DMakeRotation(-wobbleAngle, 0.0, 0.0, 1.0))
]
view.layer?.add(animation, forKey: "transform")
}, completionHandler: {
self.makeAnimation(view: view)
})

Animation on iOS using UIView animation block

1
2
3
4
5
6
7
8
extension UIView {
func shake() {
self.transform = CGAffineTransform(translationX: 16, y: 0)
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
self.transform = CGAffineTransform.identity
}, completion: nil)
}
}

How to use CAReplicatorLayer to make activity indicator in iOS

Issue #230

CAReplicatorLayer is a layer that creates a specified number of sublayer copies with varying geometric, temporal, and color transformations

Here we use instanceTransform which applies transformation matrix around the center of the replicator layer

Below is how we use replicatorLayer to replicate lots of line and rotate them around the center.

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
let replicatorLayer = CAReplicatorLayer()
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))

let line = CALayer()
let lineCount: Int = 12
let duration: TimeInterval = 1.0
let lineSize: CGSize = CGSize(width: 20, height: 6)
let lineColor: UIColor = UIColor.darkGray

let angle = CGFloat.pi * 2 / CGFloat(lineCount)
let rotation = CATransform3DMakeRotation(angle, 0, 0, 1.0)

replicatorLayer.instanceTransform = rotation
replicatorLayer.instanceCount = lineCount
replicatorLayer.instanceDelay = duration / TimeInterval(lineCount)

line.backgroundColor = lineColor.cgColor
line.frame.size = lineSize
line.cornerRadius = lineSize.height / 2

animation.fromValue = 1.0
animation.toValue = 0.0
animation.repeatCount = Float.greatestFiniteMagnitude
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.duration = duration

replicatorLayer.addSublayer(line)
layer.addSublayer(replicatorLayer)

// x:
// y: half the height, changing affects rotation of lines
line.position = CGPoint(x: 48, y: 75)

line.add(animation, forKey: nil)

Pay attention to position of the line. The larger the x, the closer to center. y should be half the height of the replicator layer size, changing it affects the skewness of the line.

indicator

How to do rotation for CALayer in iOS

Issue #229

Use keypath

1
2
3
4
5
6
let animation = CASpringAnimation(keyPath: #keyPath(CALayer.transform))
animation.fromValue = 0
animation.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
animation.timingFunction = CAMediaTimingFunction(name: .easeIn)

animation.toValue = CGFloat.pi / 4

Avoid setting frame many times

Otherwise, frame is the box that covers the rotation transform, and backgroundColor now fills that huge box

abc

1
2
3
4
5
6
7
8
9
10
11
12
override func layoutSubviews() {
super.layoutSubviews()

guard line.frame.width <= 0 else {
return
}

line.backgroundColor = UIColor.red.cgColor
line.cornerRadius = 3
line.frame.size = CGSize(width: bounds.width*0.6, height: 6)
line.position = layer.position
}

Auto Layout

Avoid using Auto Layout for the rotated view

How to not use isRemovedOnCompletion for CAAnimation in iOS

Issue #228

CAAnimation is about presentation layer, after animation completes, the view snaps back to its original state. If we want to keep the state after animation, then the wrong way is to use CAMediaTimingFillMode.forward and isRemovedOnCompletion

Animation never ends

forwards https://developer.apple.com/documentation/quartzcore/camediatimingfillmode/1427658-forwards

The receiver remains visible in its final state when the animation is completed.

isRemovedOnCompletion

https://developer.apple.com/documentation/quartzcore/caanimation/1412458-isremovedoncompletion

When true, the animation is removed from the target layer’s animations once its active duration has passed. Defaults to true.

1
2
layer.fillMode = .forwards
animation. isRemovedOnCompletion = false

This is to tell the animation to never ends and keep its last presentation state. Wrong approach ❗️

Set final state before calling animation

The presentation state is just for animation, the source of truth lies in the layer itself. We need to set the final state before calling animation

1
2
3
4
let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))

shapeLayer.strokeEnd = 1.0
shapeLayer.add(animation, forKey: "")

How to make simple search box in iOS

Issue #227

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
final class SearchBox: UIView {
lazy var textField: UITextField = {
let textField = UITextField()
let imageView = UIImageView(image: R.image.search()!)
imageView.frame.size = CGSize(width: 20 + 8, height: 20)
imageView.contentMode = .scaleAspectFit
textField.leftView = imageView
textField.leftViewMode = .always

return textField
}()

lazy var filterButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(R.image.filter()!, for: .normal)
button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

return button
}()

lazy var backView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 10
view.layer.borderColor = R.color.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.shadowOffset = CGSize(width: 1, height: 1)
view.layer.shadowOpacity = 0.8
view.layer.shadowColor = R.color.lightGray.cgColor

return view
}()

override init(frame: CGRect) {
super.init(frame: frame)

setup()
}

required init?(coder aDecoder: NSCoder) {
fatalError()
}

private func setup() {
addSubviews([backView, textField, filterButton])

NSLayoutConstraint.on([
filterButton.rightAnchor.constraint(equalTo: rightAnchor, constant: -8),
filterButton.centerYAnchor.constraint(equalTo: centerYAnchor),
filterButton.heightAnchor.constraint(equalToConstant: 44),
filterButton.widthAnchor.constraint(equalToConstant: 44),

textField.centerYAnchor.constraint(equalTo: centerYAnchor),
textField.leftAnchor.constraint(equalTo: leftAnchor, constant: 16),
textField.rightAnchor.constraint(equalTo: filterButton.leftAnchor, constant: -8)
])

NSLayoutConstraint.on([
backView.pinEdges(view: self, inset: UIEdgeInsets(top: 4, left: 4, bottom: -4, right: -4))
])
}
}

To apply padding to leftView, increase width and use contentMode

1
2
imageView.frame.size = CGSize(width: 20 + 8, height: 20)
imageView.contentMode = .scaleAspectFit

To make image in button smaller, use imageEdgeInsets with all positive values
To have round and shadow, specify shadowOpacity, cornerRadius, shadowOffset

searchbox

How to capture video in iOS simulator

Issue #226

Take screenshot

1
xcrun simctl io booted screenshot image.png

Record video

1
xcrun simctl io booted recordVideo video.mp4

How to use custom fonts in iOS

Issue #225

1
2
3
4
5
6
7
8
9
10
11
12
13
<key>UIAppFonts</key>
<array>
<string>OpenSans-Bold.ttf</string>
<string>OpenSans-BoldItalic.ttf</string>
<string>OpenSans-ExtraBold.ttf</string>
<string>OpenSans-ExtraBoldItalic.ttf</string>
<string>OpenSans-Italic.ttf</string>
<string>OpenSans-Light.ttf</string>
<string>OpenSans-LightItalic.ttf</string>
<string>OpenSans-Regular.ttf</string>
<string>OpenSans-SemiBold.ttf</string>
<string>OpenSans-SemiBoldItalic.ttf</string>
</array>

Read Adding a Custom Font to Your App

The name of the font isn’t always obvious, and rarely matches the font file name. A quick way to find the font name is to get the list of fonts available to your app, which you can do with the following code:

1
2
3
4
for family in UIFont.familyNames.sorted() {
let names = UIFont.fontNames(forFamilyName: family)
print("Family: \(family) Font names: \(names)")
}

How to create UITabBarController programmatically in iOS

Issue #224

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let tabBarController = UITabBarController()

let navigationController1 = UINavigationController(rootViewController: viewController1)
let navigationController2 = UINavigationController(rootViewController: viewController2)
let navigationController3 = UINavigationController(rootViewController: viewController3)

navigationController2.isNavigationBarHidden = true

navigationController1.tabBarItem.image = R.image.profile()
navigationController2.tabBarItem.image = R.image.books()
navigationController3.tabBarItem.image = R.image.settings()

tabBarController.tabBar.tintColor = .yellow
tabBarController.viewControllers = [navigationController1, navigationController2, navigationController3]

Use tintColor instead of the deprecated selectedImageTintColor to indicate selected item color.

For icon size, check Tab Bar Icon Size, usually 50x50 for 2x and 75x75 for 3x

In portrait orientation, tab bar icons appear above tab titles. In landscape orientation, the icons and titles appear side-by-side. Depending on the device and orientation, the system displays either a regular or compact tab bar. Your app should include custom tab bar icons for both sizes.

tab

How to run AppleScript in macOS

Issue #223

Surround script by single quotes

1
2
3
4
5
6
7
8
9
10
11
12
let script = 
"""
tell application "XcodeWay"
activate
end tell
"""

let command = "osascript -e '\(script)'"

let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["-c", command]

Run as administrator

In terminal, we can

1
2
cd ~/Library
sudo mkdir MyNewFolder

In code, we use with administrator privileges, this when run will ask for password or fingerprint

1
do shell script "mkdir MyNewFolder" with administrator privileges

How to make simple networking client in Swift

Issue #222

For more mature networking, visit https://github.com/onmyway133/Miami

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
final class NetworkClient {
let session: URLSession
let baseUrl: URL

init(session: URLSession = .shared, baseUrl: URL) {
self.session = session
self.baseUrl = baseUrl
}

func make(options: Options, completion: @escaping (Result<Data, Error>) -> Void) {
guard let request = options.toRequest(baseUrl: baseUrl) else {
completion(.failure(AppError.request))
return
}

let task = session.dataTask(with: request, completionHandler: { data, _, error in
if let data = data {
completion(.success(data))
} else if let error = error {
completion(.failure(error))
} else {
completion(.failure(AppError.unknown))
}
})

task.resume()
}

func makeJson(options: Options, completion: @escaping (Result<[String: Any], Error>) -> Void) {
make(options: options, completion: { result in
let mapped = result.flatMap({ data -> Result<[String: Any], Error> in
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
if let json = json as? [String: Any] {
return Result<[String: Any], Error>.success(json)
} else {
return Result<[String: Any], Error>.failure(AppError.parse)
}
} catch {
return Result<[String: Any], Error>.failure(error)
}
})

completion(mapped)
})
}
}

struct Options {
var path: String = ""
var httpMethod: HttpMethod = .get
var parameters: [String: Any] = [:]

func toRequest(baseUrl: URL) -> URLRequest? {
let url = baseUrl.appendingPathComponent(path)
let items: [URLQueryItem] = parameters.map({ tuple -> URLQueryItem in
return URLQueryItem(name: tuple.key, value: "\(tuple.value)")
})

var components = URLComponents(url: url, resolvingAgainstBaseURL: false)

if httpMethod == .get {
components?.queryItems = items
}

guard let finalUrl = components?.url else {
return nil
}

var request = URLRequest(url: finalUrl)

if httpMethod == .post {
let data = try? JSONSerialization.data(withJSONObject: parameters, options: [])
request.httpBody = data
}

request.httpMethod = httpMethod.rawValue
return request
}
}

enum AppError: Error {
case request
case unknown
case parse
}

enum HttpMethod: String {
case get = "GET"
case put = "PUT"
case post = "POST"
case patch = "PATCH"
}

How to ignore App Transport Security in iOS

Issue #221

Ignore a host

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
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
<key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<true/>
<key>NSThirdPartyExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
<key>NSRequiresCertificateTransparency</key>
<false/>
</dict>
</dict>
</dict>

Ignore all

1
2
3
4
5
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

How to run simple http server in Go

Issue #220

Handle url parameter

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
package main

import (
"net/http"
"log"
)

func handleGreeting(w http.ResponseWriter, r *http.Request) {
messages, ok := r.URL.Query()["message"]

if !ok || len(messages[0]) < 1 {
log.Println("Message is missing")
w.WriteHeader(400)
return
}

message := messages[0]
w.Write([]byte(message))
}

func main() {
http.HandleFunc("/greet", handleGreeting)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}

Handle body

1
2
3
4
5
6
7
8
9
10
11
12
type MyRequest struct {
Message string `json:"message"`
}

decoder := json.NewDecoder(r.Body)
var t EphemeralKeysRequest
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

message := t.Message

How to use Stripe and Apple Pay in iOS

Issue #219

Show basic add card in iOS

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
import UIKit
import Stripe

final class MainController: UIViewController {

func showPayment() {
let addCardViewController = STPAddCardViewController()
addCardViewController.delegate = self
let navigationController = UINavigationController(rootViewController: addCardViewController)

present(navigationController, animated: true, completion: nil)
}
}

extension MainController: STPAddCardViewControllerDelegate {
func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) {
dismiss(animated: true, completion: nil)
}

func addCardViewController(_ addCardViewController: STPAddCardViewController, didCreateToken token: STPToken, completion: @escaping STPErrorBlock) {
_ = token.tokenId
completion(nil)
dismiss(animated: true, completion: nil)
}
}

Generate ephemeral key

https://stripe.com/docs/mobile/ios/standard#ephemeral-key

In order for our prebuilt UI elements to function, you’ll need to provide them with an ephemeral key, a short-lived API key with restricted API access. You can think of an ephemeral key as a session, authorizing the SDK to retrieve and update a specific Customer object for the duration of the session.

Backend in Go

https://github.com/stripe/stripe-go

Need a secret key by going to Stripe dashboard -> Developers -> API keys -> Secret key

1
stripe.Key = "sk_key"

Need customer id. We can manually create one in Stripe dashboard -> Customers

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
package main

import (
"net"
"encoding/json"
"fmt"
"net/http"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/ephemeralkey"
)

func main() {
stripe.Key = "sk_test_mM2MkqO61n7vvbVRfeYmBgWm00Si2PtWab"

http.HandleFunc("/ephemeral_keys", generateEphemeralKey)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}

type EphemeralKeysRequest struct {
ApiVersion string `json:"api_version"`
}

func generateEphemeralKey(w http.ResponseWriter, r *http.Request) {
customerId := "cus_Eys6aeP5xR89ab"

decoder := json.NewDecoder(r.Body)
var t EphemeralKeysRequest
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

stripeVersion := t.ApiVersion
if stripeVersion == "" {
log.Printf("Stripe-Version not found\n")
w.WriteHeader(400)
return
}
params := &stripe.EphemeralKeyParams{
Customer: stripe.String(customerId),
StripeVersion: stripe.String(stripeVersion),
}

key, err := ephemeralkey.New(params)
if err != nil {
log.Printf("Stripe bindings call failed, %v\n", err)
w.WriteHeader(500)
return
}
w.Write(key.RawJSON)
}

iOS client

Networking client uses How to make simple networking client in Swift

Need an object that conforms to STPCustomerEphemeralKeyProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class EphemeralKeyClient: NSObject, STPCustomerEphemeralKeyProvider {
let client = NetworkClient(baseUrl: URL(string: "http://localhost:8080")!)

func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping STPJSONResponseCompletionBlock) {
var options = Options()
options.httpMethod = .post
options.path = "ephemeral_keys"
options.parameters = [
"api_version": apiVersion
]

client.makeJson(options: options, completion: { result in
switch result {
case .success(let json):
completion(json, nil)
case .failure(let error):
completion(nil, error)
}
})
}
}

Setting up STPCustomerContext and STPPaymentContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class MainController: UIViewController {
let client = EphemeralKeyClient()
let customerContext: STPCustomerContext
let paymentContext: STPPaymentContext

init() {
self.customerContext = STPCustomerContext(keyProvider: client)
self.paymentContext = STPPaymentContext(customerContext: customerContext)
super.init(nibName: nil, bundle: nil)
paymentContext.delegate = self
paymentContext.hostViewController = self
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func start() {
paymentContext.presentShippingViewController()
}
}

Handle charge

https://stripe.com/docs/charges

Backend in Go

If we use stripe_id from card, which has the form of card_xxx, we need to include customer info

If we use token, which has the form tok_xxx, then no need for customer info

From STPPaymentResult

When you’re using STPPaymentContext to request your user’s payment details, this is the object that will be returned to your application when they’ve successfully made a payment. It currently just contains a source, but in the future will include any relevant metadata as well. You should pass source.stripeID to your server, and call the charge creation endpoint. This assumes you are charging a Customer, so you should specify the customer parameter to be that customer’s ID and the source parameter to the value returned here. For more information, see https://stripe.com/docs/api#create_charge

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
package main

import (
"net"
"encoding/json"
"fmt"
"net/http"
"log"
"os"
"github.com/stripe/stripe-go/charge"
)

func main() {
stripe.Key = "sk_test_mM2MkqO61n7vvbVRfeYmBgWm00Si2PtWab"

http.HandleFunc("/request_charge", handleCharge)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}

var customerId = "cus_Eys6aeP5xR89ab"

type PaymentResult struct {
StripeId string `json:"stripe_id"`
}

func handleCharge(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var t PaymentResult
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

params := &stripe.ChargeParams{
Amount: stripe.Int64(150),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Description: stripe.String("Charge from my Go backend"),
Customer: stripe.String(customerId),
}

params.SetSource(t.StripeId)
ch, err := charge.New(params)
if err != nil {
fmt.Fprintf(w, "Could not process payment: %v", err)
fmt.Println(ch)
w.WriteHeader(400)
}

w.WriteHeader(200)
}

iOS client

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
final class PaymentClient {
let client = NetworkClient(baseUrl: URL(string: "http://192.168.210.219:8080")!)

func requestCharge(source: STPSourceProtocol, completion: @escaping (Result<(), Error>) -> Void) {
var options = Options()
options.httpMethod = .post
options.path = "request_charge"
options.parameters = [
"stripe_id": source.stripeID
]

client.makeJson(options: options, completion: { result in
completion(result.map({ _ in () }))
})
}
}

paymentContext.requestPayment()

extension MainController: STPPaymentContextDelegate {
func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) {
client.requestCharge(source: paymentResult.source, completion: { result in
switch result {
case .success:
completion(nil)
case .failure(let error):
completion(error)
}
})
}
}

Token from card

Use STPAPIClient.shared().createToken to get token from card https://stripe.com/docs/mobile/ios/custom#collecting-card-details

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let cardParams = STPCardParams()
cardParams.number = "4242424242424242"
cardParams.expMonth = 10
cardParams.expYear = 2021
cardParams.cvc = "123"

STPAPIClient.shared().createToken(withCard: cardParams) { (token: STPToken?, error: Error?) in
guard let token = token, error == nil else {
// Present error to user...
return
}

submitTokenToBackend(token, completion: { (error: Error?) in
if let error = error {
// Present error to user...
}
else {
// Continue with payment...
}
})
}

Payment options and shipping view controllers

Instead of using paymentContext

1
2
paymentContext.pushShippingViewController()
paymentContext.pushPaymentOptionsViewController()

We can use view controllers https://stripe.com/docs/mobile/ios/custom#stppaymentoptionsviewcontroller directly with STPPaymentOptionsViewController and STPShippingAddressViewController. Then implement STPPaymentOptionsViewControllerDelegate and STPShippingAddressViewControllerDelegate

Register merchant Id and Apple Pay certificate

https://stripe.com/docs/apple-pay/apps

Get Certificate signing request file from Stripe https://dashboard.stripe.com/account/apple_pay

We can’t register merchant id with Enterprise account

Use Apple Pay

Go backend

Use token

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
type ApplePayRequest struct {
Token string `json:"token"`
}

func handleChargeUsingApplePay(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var t ApplePayRequest
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

params := &stripe.ChargeParams{
Amount: stripe.Int64(150),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Description: stripe.String("Charge from my Go backend for Apple Pay"),
}

params.SetSource(t.Token)
ch, err := charge.New(params)
if err != nil {
fmt.Fprintf(w, "Could not process payment: %v", err)
fmt.Println(ch)
w.WriteHeader(400)
}

w.WriteHeader(200)
}

iOS client

Update client to send STPToken

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
final class PaymentClient: NSObject {
let client = NetworkClient(baseUrl: URL(string: "localhost:8080")!)

func requestCharge(token: STPToken, completion: @escaping (Result<(), Error>) -> Void) {
var options = Options()
options.httpMethod = .post
options.path = "request_charge_apple_pay"
options.parameters = [
"token": token.tokenId
]

client.make(options: options, completion: { result in
completion(result.map({ _ in () }))
})
}

func useApplePay(payment: PKPayment, completion: @escaping (Result<(), Error>) -> Void) {
STPAPIClient.shared().createToken(with: payment, completion: { (token: STPToken?, error: Error?) in
guard let token = token, error == nil else {
return
}

self.requestCharge(token: token, completion: completion)
})
}
}

Use PKPaymentAuthorizationViewController, not PKPaymentAuthorizationController

https://developer.apple.com/documentation/passkit/pkpaymentauthorizationcontroller

The PKPaymentAuthorizationController class performs the same role as the PKPaymentAuthorizationViewController class, but it does not depend on the UIKit framework. This means that the authorization controller can be used in places where a view controller cannot (for example, in watchOS apps or in SiriKit extensions).

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
extension MainController {
func showApplePay() {
let merchantId = "merchant.com.onmyway133.MyApp"
let paymentRequest = Stripe.paymentRequest(withMerchantIdentifier: merchantId, country: "US", currency: "USD")
paymentRequest.paymentSummaryItems = [
PKPaymentSummaryItem(label: "Rubber duck", amount: 1.5)
]

guard Stripe.canSubmitPaymentRequest(paymentRequest) else {
assertionFailure()
return
}

guard let authorizationViewController = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) else {
assertionFailure()
return
}

authorizationViewController.delegate = self
innerNavigationController.present(authorizationViewController, animated: true, completion: nil)
}
}

extension MainController: PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
controller.dismiss(animated: true, completion: nil)
}

func paymentAuthorizationViewController(
_ controller: PKPaymentAuthorizationViewController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {

client.useApplePay(payment: payment, completion: { result in
switch result {
case .success:
completion(.init(status: .success, errors: nil))
case .failure(let error):
completion(.init(status: .failure, errors: [error]))
}
})
}
}

Showing Apple Pay option

From appleMerchantIdentifier

The Apple Merchant Identifier to use during Apple Pay transactions. To create one of these, see our guide at https://stripe.com/docs/mobile/apple-pay . You must set this to a valid identifier in order to automatically enable Apple Pay.

1
2
3
4
5
if Stripe.deviceSupportsApplePay() {
STPPaymentConfiguration.shared().appleMerchantIdentifier = "merchant.com.onmyway133.MyApp"
}

paymentContext.pushPaymentOptionsViewController()

requestPayment not showing UI

From requestPayment

Requests payment from the user. This may need to present some supplemental UI to the user, in which case it will be presented on the payment context’s hostViewController. For instance, if they’ve selected Apple Pay as their payment method, calling this method will show the payment sheet. If the user has a card on file, this will use that without presenting any additional UI. After this is called, the paymentContext:didCreatePaymentResult:completion: and paymentContext:didFinishWithStatus:error: methods will be called on the context’s delegate.

Use STPPaymentOptionsViewController to show cards and Apple Pay options

Code for requestPayment

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
- (void)requestPayment {
WEAK(self);
[[[self.didAppearPromise voidFlatMap:^STPPromise * _Nonnull{
STRONG(self);
return self.loadingPromise;
}] onSuccess:^(__unused STPPaymentOptionTuple *tuple) {
STRONG(self);
if (!self) {
return;
}

if (self.state != STPPaymentContextStateNone) {
return;
}

if (!self.selectedPaymentOption) {
[self presentPaymentOptionsViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self requestPaymentShouldPresentShippingViewController]) {
[self presentShippingViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self.selectedPaymentOption isKindOfClass:[STPCard class]] ||
[self.selectedPaymentOption isKindOfClass:[STPSource class]]) {
self.state = STPPaymentContextStateRequestingPayment;
STPPaymentResult *result = [[STPPaymentResult alloc] initWithSource:(id<STPSourceProtocol>)self.selectedPaymentOption];
[self.delegate paymentContext:self didCreatePaymentResult:result completion:^(NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
if (error) {
[self didFinishWithStatus:STPPaymentStatusError error:error];
} else {
[self didFinishWithStatus:STPPaymentStatusSuccess error:nil];
}
});
}];
}
else if ([self.selectedPaymentOption isKindOfClass:[STPApplePayPaymentOption class]]) {
// ....

Payment options

1
2
3
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didSelect paymentOption: STPPaymentOption) {
// No op
}

After user selects payment option, the change is saved in dashboard https://dashboard.stripe.com/test/customers, but for card only. Select Apple Pay does not reflect change in web dashboard.

Apple pay option is added manually locally, from STPCustomer+SourceTuple.m 😲

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
- (STPPaymentOptionTuple *)filteredSourceTupleForUIWithConfiguration:(STPPaymentConfiguration *)configuration {
id<STPPaymentOption> _Nullable selectedMethod = nil;
NSMutableArray<id<STPPaymentOption>> *methods = [NSMutableArray array];
for (id<STPSourceProtocol> customerSource in self.sources) {
if ([customerSource isKindOfClass:[STPCard class]]) {
STPCard *card = (STPCard *)customerSource;
[methods addObject:card];
if ([card.stripeID isEqualToString:self.defaultSource.stripeID]) {
selectedMethod = card;
}
}
else if ([customerSource isKindOfClass:[STPSource class]]) {
STPSource *source = (STPSource *)customerSource;
if (source.type == STPSourceTypeCard
&& source.cardDetails != nil) {
[methods addObject:source];
if ([source.stripeID isEqualToString:self.defaultSource.stripeID]) {
selectedMethod = source;
}
}
}
}

return [STPPaymentOptionTuple tupleWithPaymentOptions:methods
selectedPaymentOption:selectedMethod
addApplePayOption:configuration.applePayEnabled];
}

STPApplePayPaymentOptionis not available inpaymentContext.paymentOptions` immediately

Change selected payment option

In STPPaymentContext

setSelectedPaymentOption is read only and trigger paymentContextDidChange, but it checks if the new selected payment option is equal to existing selected payment option

1
2
3
4
5
6
7
8
9
10
11
- (void)setSelectedPaymentOption:(id<STPPaymentOption>)selectedPaymentOption {
if (selectedPaymentOption && ![self.paymentOptions containsObject:selectedPaymentOption]) {
self.paymentOptions = [self.paymentOptions arrayByAddingObject:selectedPaymentOption];
}
if (![_selectedPaymentOption isEqual:selectedPaymentOption]) {
_selectedPaymentOption = selectedPaymentOption;
stpDispatchToMainThreadIfNecessary(^{
[self.delegate paymentContextDidChange:self];
});
}
}

There is retryLoading which is called at init

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
- (void)retryLoading {
// Clear any cached customer object before refetching
if ([self.apiAdapter isKindOfClass:[STPCustomerContext class]]) {
STPCustomerContext *customerContext = (STPCustomerContext *)self.apiAdapter;
[customerContext clearCachedCustomer];
}
WEAK(self);
self.loadingPromise = [[[STPPromise<STPPaymentOptionTuple *> new] onSuccess:^(STPPaymentOptionTuple *tuple) {
STRONG(self);
self.paymentOptions = tuple.paymentOptions;
self.selectedPaymentOption = tuple.selectedPaymentOption;
}] onFailure:^(NSError * _Nonnull error) {
STRONG(self);
if (self.hostViewController) {
[self.didAppearPromise onSuccess:^(__unused id value) {
if (self.paymentOptionsViewController) {
[self appropriatelyDismissPaymentOptionsViewController:self.paymentOptionsViewController completion:^{
[self.delegate paymentContext:self didFailToLoadWithError:error];
}];
} else {
[self.delegate paymentContext:self didFailToLoadWithError:error];
}
}];
}
}];
[self.apiAdapter retrieveCustomer:^(STPCustomer * _Nullable customer, NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
STRONG(self);
if (!self) {
return;
}
if (error) {
[self.loadingPromise fail:error];
return;
}
if (!self.shippingAddress && customer.shippingAddress) {
self.shippingAddress = customer.shippingAddress;
self.shippingAddressNeedsVerification = YES;
}

STPPaymentOptionTuple *paymentTuple = [customer filteredSourceTupleForUIWithConfiguration:self.configuration];

[self.loadingPromise succeed:paymentTuple];
});
}];
}

Which in turns call STPCustomerEphemeralKeyProvider. As stripe does not save Apple Pay option in dashboard, this method return list of card payment options, together with the default card as selected payment option 😲

Although the new STPCard has a different address, it is the exact same card with the same info, and the isEqual method of STPCard is

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)isEqualToCard:(nullable STPCard *)other {
if (self == other) {
return YES;
}

if (!other || ![other isKindOfClass:self.class]) {
return NO;
}

return [self.stripeID isEqualToString:other.stripeID];
}

I raised an issue How to change selected payment option? hope it gets resolved soon 😢

How to test PublishSubject in RxSwift

Issue #218

Use homemade Recorder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Recorder<T> {
var items = [T]()
let bag = DisposeBag()

func on(arraySubject: PublishSubject<[T]>) {
arraySubject.subscribe(onNext: { value in
self.items = value
}).disposed(by: bag)
}

func on(valueSubject: PublishSubject<T>) {
valueSubject.subscribe(onNext: { value in
self.items.append(value)
}).disposed(by: bag)
}
}

Then test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final class BookViewModelTests: XCTestCase {
func testBooks() throws {
let expectation = self.expectation(description: #function)
let recorder = Recorder<Book>()
let viewModel = BookViewModel(bookClient: MockBookClient())
recorder.on(arraySubject: viewModel.books)
viewModel.load()

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
expectation.fulfill()
})

wait(for: [expectation], timeout: 2)
XCTAssertEqual(recorder.items.count, 3)
}
}

Need to use great timeout value as DispatchQueue is not guaranteed to be precise, a block needs to wait for the queue to be empty before it can be executed

Make expectation less cumbersome

1
2
3
4
5
6
7
8
9
10
11
extension XCTestCase {
func waitOrFail(timeout: TimeInterval) {
let expectation = self.expectation(description: #function)

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + timeout, execute: {
expectation.fulfill()
})

wait(for: [expectation], timeout: timeout + 2)
}
}

How to fix not found zlib problem in macOS Mojave

Issue #217

https://developer.apple.com/documentation/xcode_release_notes/xcode_10_release_notes

The command line tools will search the SDK for system headers by default. However, some software may fail to build correctly against the SDK and require macOS headers to be installed in the base system under /usr/include. If you are the maintainer of such software, we encourage you to update your project to work with the SDK or file a bug report for issues that are preventing you from doing so. As a workaround, an extra package is provided which will install the headers to the base system. In a future release, this package will no longer be provided. You can find this package at:

/Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg


To make sure that you’re using the intended version of the command line tools, run xcode-select -s or xcode select -s /Library/Developer/CommandLineTools after installing.

Run

1
open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg

How to use Sonarqube in Swift projects

Issue #216

Install Sonarqube

https://docs.sonarqube.org/latest/setup/get-started-2-minutes/

  • Download Sonarqube for macOS https://www.sonarqube.org/downloads/
  • Put it in ~/sonarqube
  • Run localhost server ~/sonarqube/bin/macosx-universal-64/sonar.sh console
  • Login http://localhost:9000 with admin/admin
  • Create new project

Install Sonar scanner

https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner

  • Download for macOS 64 bit
  • Put it in ~/sonarscanner
  • export PATH=$PATH:/Users/khoa/sonarscanner/bin
  • Go to project, create sonar-project.properties
1
2
3
4
5
6
7
8
9
10
11
12
# must be unique in a given SonarQube instance
sonar.projectKey=my-app
# this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
sonar.projectName=My App
sonar.projectVersion=1.0

# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
# This property is optional if sonar.modules is set.
sonar.sources=.

# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8
  • Run sonar-scanner

Install swift plugin

https://github.com/Backelite/sonar-swift

a

Skip some tools

Modify run-sonar-swift.sh

1
2
3
4
5
6
7
8
9
vflag=""
nflag=""
unittests="on"
swiftlint="on"
tailor="off"
lizard="on"
oclint="off"
fauxpas="off"
sonarscanner=""

Git ignore

1
2
3
.scannerwork/
sonar-reports/
compile_commands.json

run-sonar-swift.sh

  • Update sonar-project.properties
1
2
3
4
sonar.swift.appScheme=MyApp Staging
sonar.swift.project=MyApp.xcodeproj
sonar.swift.workspace=MyApp.xcworkspace
sonar.swift.simulator=platform=iOS Simulator,name=iPhone Xʀ
  • Run chmod +x run-sonar-swift.sh
  • Run ./run-sonar-swift.sh

Troubleshooting

failed with error code: 64 https://github.com/Backelite/sonar-swift/issues/222

When run ./run-sonar-swift.sh

1
2
3
4
xcodebuild: error: ''MyApp.xcodeproj'' does not exist.
2019-04-29 12:10:17.486 defaults[4134:569992]
Domain CFBundleShortVersionString does not exist
.Extracting Xcode project informationxcodebuild: error: option 'Destination' requires at least one parameter of the form 'key=value'

👉 Remove quotes in sonar-project.properties
👉 Modify run-sonar-swift.sh, add these before Check for mandatory parameters section

Surround by double quotes

1
2
3
4
projectFile="\"$projectFile\""
workspaceFile="\"$workspaceFile\""
appScheme="\"$appScheme\""
destinationSimulator="\"$destinationSimulator\""

😢 Does not work
👉 Need to create a scheme name without space

Error code 65

👉Specify team in Xcode project

destination

Need double quotes

destinationSimulator="\"$destinationSimulator\""

Use equal sign =

1
-destination="$destinationSimulator"

instead of space ‘ ‘

1
-destination "$destinationSimulator"

Metric ‘files’ should not be computed by a Sensor

When run sonar-scanner https://github.com/Backelite/sonar-swift/issues/212

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
11:01:14.406 INFO: Sensor JaCoCo XML Report Importer [jacoco]
11:01:14.409 DEBUG: No reports found
11:01:14.409 INFO: Sensor JaCoCo XML Report Importer [jacoco] (done) | time=3ms
11:01:14.409 INFO: Sensor SwiftLint [backelitesonarswiftplugin]
11:01:14.417 INFO: Sensor SwiftLint [backelitesonarswiftplugin] (done) | time=8ms
11:01:14.417 INFO: Sensor Tailor [backelitesonarswiftplugin]
11:01:14.418 INFO: Sensor Tailor [backelitesonarswiftplugin] (done) | time=1ms
11:01:14.418 INFO: Sensor OCLint [backelitesonarswiftplugin]
11:01:14.419 INFO: Sensor OCLint [backelitesonarswiftplugin] (done) | time=1ms
11:01:14.419 INFO: Sensor FauxPas [backelitesonarswiftplugin]
11:01:14.419 INFO: Sensor FauxPas [backelitesonarswiftplugin] (done) | time=0ms
11:01:14.419 INFO: Sensor Swift Squid [backelitesonarswiftplugin]
11:01:14.526 INFO: ------------------------------------------------------------------------
11:01:14.526 INFO: EXECUTION FAILURE
11:01:14.526 INFO: ------------------------------------------------------------------------
11:01:14.527 INFO: Total time: 6.180s
11:01:14.603 INFO: Final Memory: 25M/566M
11:01:14.603 INFO: ------------------------------------------------------------------------
11:01:14.603 ERROR: Error during SonarQube Scanner execution
java.lang.UnsupportedOperationException: Metric 'files' should not be computed by a Sensor
at org.sonar.scanner.sensor.DefaultSensorStorage.saveMeasure(DefaultSensorStorage.java:168)
```

👉Install maven https://maven.apache.org/download.cgi
Edit `ObjectiveCSquidSensor.java` and `SwiftSquidSensor`, remove line with `CoreMetrics.FILES`
Run `export PATH=$PATH:/Users/khoa/apache-maven/bin`
Run `./build-and-deploy.sh`
Or `~/apache-maven/bin/mvn clean install`

🎉 Built jar is in `sonar-swift-plugin/target/backelite-sonar-swift-plugin-0.4.4.jar`, copy back to `extensions/plugins`


### How to enable SwiftLint as default profile 🤔

👉 Need to close current Sonar tab and restart server

### Testing failed: unable to attach DB

Modify `run-sonar-swift.sh` to add `-UseModernBuildSystem=NO` to `buildCmd+=(-destination`

### slather No coverage directory found

Try running

slather coverage –input-format profdata –cobertura-xml –output-directory sonar-reports –workspace MyApp.xcworkspace –scheme MyAppStaging MyApp.xcodeproj

1
2
3
4
5
6
7
8

👉 Enable coverage option in scheme -> Test

![](https://github.com/SlatherOrg/slather/raw/master/README_Images/test_scheme.png)

Optional: declare `.slather.yml` file https://github.com/SlatherOrg/slather

### Unable to execute SonarQube

14:53:23.251 ERROR: Error during SonarQube Scanner execution
org.sonarsource.scanner.api.internal.ScannerException: Unable to execute SonarQube
at org.sonarsource.scanner.api.internal.IsolatedLauncherFactory.lambda$createLauncher$0(IsolatedLauncherFactory.java:85)

1
2
3
4

👉Start sonar server

### LizardReportParser$SwiftFunction cannot be cast

Error during SonarQube Scanner execution
java.lang.ClassCastException: com.backelite.sonarqube.swift.complexity.LizardReportParser$SwiftFunction cannot be cast to org.sonar.api.batch.fs.internal.DefaultInputComponent

1
2

👉 Run [lizard](https://github.com/terryyin/lizard) manually

lizard –xml sonar-reports/lizard-report.xml
```