Optimizing iOS Development: Essential Build Phase Scripts

In today's post, we will see some essential build phase scripts that can significantly enhance your iOS project's efficiency and maintainability.

Optimizing your development process is key to delivering high-quality software efficiently. In iOS development, build phase scripts play a crucial role in automating repetitive tasks and enforcing best practices. Let’s explore some of these scripts that are essential for any iOS project.

Handling Assets

Managing assets efficiently is crucial for any iOS project. Automating tasks like image optimization can significantly improve your app’s performance.

Script for Image Optimization :

1- Install pngcrush :

brew install pngcrush

2- Create the build phase script :

find $SRCROOT/ProjectName/Resources/Assets.xcassets -type f -name "*.png" | xargs -I {} pngcrush -brute -ow {}

This script finds all PNG files in your Assets.xcassets folder and optimizes them in place using pngcrush.

Generating Code for Assets :

Automatically generating code for assets like images and fonts can save time and reduce human errors.

R.swift Integration for Assets :

1- Install R.swift :

  • With CocoaPods (Add the following to your Podfile then run pod install) :
 pod 'R.swift'

2- Create the build phase script :

"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/ProjectName/Resources/R.generated.swift"

Code Quality Checks

Maintaining code quality is essential. You can integrate linting tools into your build phase to enforce coding standards.

SwiftLint Integration :

1- Install SwiftLint :

  • With CocoaPods (Add the following to your Podfile then run pod install) :
pod 'SwiftLint'

2- Create the .swiftlint.yml file in the root of your project :

# swiftlint rules
included: # paths to include during linting. `--path` is ignored if present.
  - ./ProjectName
  - ./ProjectNameTests
  - ./ProjectNameUITests

excluded: # paths to ignore during linting. Takes precedence over `included`.
  - ./ProjectName/Resources/R.generated.swift
  - ./Pods

disabled_rules:
  - explicit_enum_raw_value
  - array_init
  - explicit_type_interface
  - nimble_operator
  - no_grouping_extension
  - attributes
  - private_over_fileprivate
  - strict_fileprivate
  - explicit_top_level_acl
  - trailing_whitespace
  - file_header
  - trailing_closure
  - switch_case_alignment
  - let_var_whitespace
  - opening_brace

opt_in_rules:
  - void_return
  - first_where
  - number_separator
  - block_based_kvo
  - class_delegate_protocol
  - closing_brace
  - closure_end_indentation
  - closure_parameter_position
  - closure_spacing
  - colon
  - comma
  - compiler_protocol_init
  - conditional_returns_on_newline
  - contains_over_first_not_nil
  - control_statement
  - discarded_notification_center_observer
  - discouraged_direct_init
  - dynamic_inline
  - empty_count
  - empty_enum_arguments
  - empty_parameters
  - empty_parentheses_with_trailing_closure
  - explicit_init
  - fallthrough
  - fatal_error_message
  - file_length
  - for_where
  - force_cast
  - force_try
  - force_unwrapping
  - function_body_length
  - function_parameter_count
  - generic_type_name
  - identifier_name
  - implicit_getter
  - implicit_return
  - implicitly_unwrapped_optional
  - is_disjoint
  - joined_default_parameter
  - large_tuple
  - leading_whitespace
  - legacy_cggeometry_functions
  - legacy_constant
  - legacy_constructor
  - legacy_nsgeometry_functions
  - literal_expression_end_indentation
  - mark
  - multiline_arguments
  - multiline_parameters
  - multiple_closures_with_trailing_closure
  - nesting
  - object_literal
  - operator_usage_whitespace
  - operator_whitespace
  - overridden_super_call
  - override_in_extension
  - pattern_matching_keywords
  - private_outlet
  - protocol_property_accessors_order
  - prohibited_super_call
  - quick_discouraged_call
  - quick_discouraged_focused_test
  - quick_discouraged_pending_test
  - redundant_discardable_let
  - redundant_nil_coalescing
  - redundant_optional_initialization
  - redundant_string_enum_value
  - redundant_void_return
  - return_arrow_whitespace
  - shorthand_operator
  - single_test_class
  - statement_position
  - superfluous_disable_command
  - switch_case_on_newline
  - syntactic_sugar
  - todo
  - trailing_newline
  - trailing_semicolon
  - type_body_length
  - type_name
  - unneeded_break_in_switch
  - unneeded_parentheses_in_closure_argument
  - unused_closure_parameter
  - unused_enumerated
  - valid_ibinspectable
  - vertical_parameter_alignment
  - vertical_parameter_alignment_on_call
  - vertical_whitespace
  - weak_delegate
  - xctfail_message



custom_rules:
  avoid_print:
    included: ".*.swift"
    name: "Avoid Print"
    regex: "print\\("
    message: "Consider using a logging method instead of print"
    severity: warning

# Rules configuration

closure_spacing:
  severity: error

colon:
  severity: error

force_unwrapping:
  severity: warning

force_cast:
  severity: warning

force_try:
  severity: warning

control_statement:
  severity: warning

cyclomatic_complexity:
  warning: 25
  error: 30

explicit_init:
  severity: error

file_length:
  warning: 500
  error: 800

first_where:
  severity: error

function_body_length:
  warning: 70
  error: 100

function_parameter_count:
  warning: 8
  error: 10

generic_type_name:
  min_length:
    warning: 0
    error: 1
  max_length:
    warning: 80
    error: 100

identifier_name:
  allowed_symbols: "_"
  min_length: 1
  max_length:
    warning: 40
    error: 50
  validates_start_with_lowercase: false

implicitly_unwrapped_optional:
  severity: error

large_tuple:
  warning: 4
  error: 6

leading_whitespace:
  severity: error

legacy_cggeometry_functions:
  severity: error

legacy_constant:
  severity: error

legacy_constructor:
  severity: error

legacy_nsgeometry_functions:
  severity: error

line_length:
  warning: 250
  error: 500
  ignores_function_declarations: true
  ignores_comments: true
  ignores_urls: true

mark:
  severity: warning

nesting:
  severity: warning

notification_center_detachment:
  severity: error

number_separator:
  severity: error

object_literal:
  severity: error

operator_whitespace:
  severity: error

operator_usage_whitespace:
  severity: error

overridden_super_call:
  severity: error

private_outlet:
  severity: error

private_unit_test:
  severity: error

prohibited_super_call:
  severity: error

redundant_nil_coalescing:
  severity: error

redundant_void_return:
  severity: error

return_arrow_whitespace:
  severity: error

shorthand_operator:
  severity: error

statement_position:
  statement_mode: default
  severity: error

switch_case_on_newline:
  severity: error

syntactic_sugar:
  severity: error

trailing_comma:
  severity: warning

trailing_newline: error

trailing_semicolon:
  severity: error

type_body_length:
  warning: 800
  error: 1000

type_name:
  min_length:
    warning: 0
    error: 3
  max_length:
    warning: 80
    error: 100

unused_closure_parameter:
  severity: error

unused_enumerated:
  severity: error

unused_optional_binding:
  severity: error

valid_ibinspectable:
  severity: error

vertical_parameter_alignment:
  severity: error

vertical_whitespace:
  severity: error

weak_delegate:
  severity: error

reporter: "xcode"

3- Create the build phase script :

"${PODS_ROOT}/SwiftLint/swiftlint" --fix && "${PODS_ROOT}/SwiftLint/swiftlint"

SwiftFormat Integration :

1- Install SwiftFormat :

  • With CocoaPods (Add the following to your Podfile then run pod install) :
pod 'SwiftFormat/CLI'

2- Create the .swiftformat file in the root of your project :

--exclude Pods, ProjectName/Resources/R.generated.swift

--swiftversion 5.6
--self remove
--importgrouping testable-bottom
--commas inline
--trimwhitespace always
--indent 4
--ifdef no-indent
--indentstrings true
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--wrapconditions before-first
--wrapreturntype if-multiline
--closingparen same-line
--wraptypealiases before-first
--funcattributes prev-line
--typeattributes prev-line
--wrapternary before-operators
--extensionacl on-declarations
--patternlet hoist
--redundanttype inferred
--emptybraces no-space
--operatorfunc spaced
--maxwidth 130

# file options
#--exclude Package.swift

# rules
--rules anyObjectProtocol
--rules blankLinesBetweenScopes
--rules consecutiveSpaces
--rules consecutiveBlankLines
--rules duplicateImports
--rules extensionAccessControl
--rules hoistPatternLet
--rules indent
--rules redundantParens
--rules redundantReturn
--rules redundantSelf
--rules redundantType
--rules redundantPattern
--rules redundantGet
--rules redundantFileprivate
--rules redundantRawValues
--rules sortImports
--rules sortDeclarations
--rules strongifiedSelf
--rules trailingCommas
--rules trailingSpace
--rules typeSugar
--rules wrap
--rules wrapMultilineStatementBraces
--rules wrapArguments
--rules wrapAttributes
--rules braces
--rules redundantClosure
--rules redundantInit
--rules redundantVoidReturnType
--rules unusedArguments
--rules spaceInsideBrackets
--rules spaceInsideBraces
--rules spaceAroundBraces
--rules spaceInsideParens
--rules spaceAroundParens
--rules enumNamespaces
--rules spaceAroundComments
--rules spaceInsideComments
--rules spaceAroundOperators
--rules blankLinesAtStartOfScope
--rules blankLinesAtEndOfScope
--rules emptyBraces
--rules andOperator

3- Create the build phase script :

"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat" "${SRCROOT}"

Check for unused code with Periphery

1- Install Periphery :

  • With CocoaPods (Add the following to your Podfile then run pod install) :
pod 'Periphery'

2- Create the .periphery.yml file in the root of your project :

retain_objc_accessible: true
retain_public: true
schemes:
  - ProjectName
targets:
  - ProjectName
workspace: ProjectName.xcworkspace

3- Create the build phase script :

Since Periphery needs to build the project to analyze the code, It’s crucial to create a temporary file to skip the analysis after building the project for the first time (to avoid recursive builds) :

if [ -f "/tmp/skip_periphery.txt" ]; then
    rm -f "/tmp/skip_periphery.txt"  # Clean up after skipping
    exit 0
fi
touch "/tmp/skip_periphery.txt"  # Create the file as a flag
"${PODS_ROOT}/Periphery/periphery" scan

Introducing RASP Protection in iOS Apps with Swift

In today's post, we'll be discussing how to enhance the security of iOS applications by integrating Runtime Application Self-Protection (RASP) mechanisms using Swift.

What is RASP?

RASP or Runtime Application Self-Protection is an advanced security solution that identifies and prevents real-time attacks instantly within the app. It’s integrated into an app’s runtime environment and can detect threats and instantly take action without any human intervention.

Setting Up RASP in iOS

To get started with RASP, we’ll look into the implementation of some primary RASP functionalities:

Debugger Detection

Detecting if a debugger is attached to your application can be crucial in preventing runtime manipulations of your app :

protocol RaspDebuggerDetectionProtocol {
    func isDebuggerAttached() -> Bool
}
class RaspDebuggerDetection: RaspDebuggerDetectionProtocol {
    static let shared = RaspDebuggerDetection()

    private init() {}
    
    func isDebuggerAttached() -> Bool {
        var info = kinfo_proc()
        var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
        var size = MemoryLayout<kinfo_proc>.stride
        let junk = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0)
        return (info.kp_proc.p_flag & P_TRACED) != 0
    }
}
  • Example of using the RaspDebuggerDetection singleton :
    if RaspDebuggerDetection.shared.isDebuggerAttached() {
      print("Debugger detected")
    }
    

Anti-Tampering

Protecting the integrity of your application against tampering attempts:

class RaspAntiTampering {
    func verifyAppIntegrity() -> Bool {
        guard let resourceURL = Bundle.main.resourceURL else { return false }
        let codePath = resourceURL.appendingPathComponent("CodeResources").path
        return FileManager.default.fileExists(atPath: codePath)
    }
}

Jailbreak Detection

One crucial aspect of maintaining app security on iOS devices is detecting whether a device has been jailbroken. Jailbroken devices can pose a significant threat since they remove many of the built-in security features of iOS.

class RaspJailbreakDetection {

    func isDeviceJailbroken() -> Bool {
        // 1. Check for paths that are common for jailbroken devices
        let jailbreakFilePaths = [
            "/Applications/Cydia.app",
            "/Library/MobileSubstrate/MobileSubstrate.dylib",
            "/bin/bash",
            "/usr/sbin/sshd",
            "/etc/apt"
        ]
        
        for path in jailbreakFilePaths {
            if FileManager.default.fileExists(atPath: path) {
                return true
            }
        }

        // 2. Check if the app can write to /private
        let stringToWrite = "Jailbreak Test"
        do {
            try stringToWrite.write(toFile: "/private/jailbreak.txt", atomically: true, encoding: .utf8)
            // If the app is able to write to /private, it's a jailbroken device
            try FileManager.default.removeItem(atPath: "/private/jailbreak.txt")
            return true
        } catch {
            return false
        }
    }
}

To use this detection mechanism, simply create an instance of RaspJailbreakDetection and call isDeviceJailbroken()`. If the method returns true, it means the device is jailbroken, and you may want to restrict certain functionalities or even display a warning to the user about potential security risks.

Remember, while jailbreak detection can enhance the security of your application, no method is foolproof. Always stay updated with the latest techniques and methods used by jailbreak communities to ensure your detection mechanism remains effective.

Benefits of Implementing RASP

RASP allows developers to:

  • Detect and prevent real-time threats.
  • Protect sensitive data within the app.
  • Provide instant threat responses without human intervention.
  • Ensure compliance with security regulations and standards.
In conclusion, RASP is a robust layer of security that all iOS developers should consider integrating. It not only provides protection against current threats but also evolves to defend against new ones, making it a dynamic and essential tool for iOS app security.

Mastering iOS device management with libimobiledevice and ideviceinstaller

In this post, we'll explore how to interact with iOS devices using the powerful open-source tools: `libimobiledevice` and `ideviceinstaller`.

Have you ever wondered how to manage, debug, or install applications on your iOS device directly from your macOS terminal? These tools are your answers. They allow interaction with iOS devices natively, a crucial ability for developers and tech-savvy users. Let’s dive right into it!

Installing libimobiledevice and ideviceinstaller

Before anything else, we need to install our tools. Here’s how you can do it on macOS:

brew install --HEAD usbmuxd
brew install --HEAD libimobiledevice
brew install --HEAD ideviceinstaller

Querying Device Information

Using ideviceinfo, we can retrieve all sorts of information from the device. Simply connect your iOS device to your computer and run:

ideviceinfo

File Transfer

With ifuse, we can mount the iOS device’s filesystem to our local system and transfer files:

mkdir ~/myiosdevice
ifuse ~/myiosdevice

Installing Applications

You can install IPA files to your device using ideviceinstaller:

ideviceinstaller -i /path/to/app.ipa

And also uninstall them:

ideviceinstaller -U <app_id>

Debugging Applications

With idevicedebug, we can start and stop the debugging process for an application:

idevicedebug start <app_id>

Screenshot

Taking a screenshot is easy with idevicescreenshot:

idevicescreenshot screenshot.tiff

Restarting Device

You can restart your device using idevicediagnostics:

idevicediagnostics restart

Managing Provisioning Profile

To add a provisioning profile to your device, use ideviceprovision:

ideviceprovision add /path/to/provisioning/profile

And More!

There’s a lot more you can do with these tools, like managing network links, setting device names, activating/deactivating devices, and checking device “heartbeat”. You can always learn more with:

idevicetool --help
With these tools in your developer toolbox, you'll find it easier to interact with your iOS devices right from the terminal.