Tag: xcode

  • Xcode: How to upload dSYM to Firebase Crashlytics

    Xcode: How to upload dSYM to Firebase Crashlytics

    Have you faced the missing dSYM issue? You can fix it by uploading dSYM to firebase.

    Official Document

    Build Setting – Build Options

    Check DWARD with dSYM File

    screenshot 2023 12 20 at 6.18.32e280afpm

    Reorder Build Phase in Xcode

    screenshot 2023 12 20 at 5.55.13e280afpm 1

    Edit Run Script

    screenshot 2023 12 20 at 6.00.44e280afpm
    "${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"

    Add above command into Run Script

    Add Input Files

    screenshot 2023 12 20 at 6.00.58e280afpm
    ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
    ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}
    ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist
    $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist
    $(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)

    Tap + button at Input Files and Add above paths one by one

    Check dSYM

    screenshot 2023 12 20 at 6.11.38e280afpm

    You can see uploading dsym process in All Messages

    screenshot 2023 12 20 at 6.13.30e280afpm

    Let’s check firebase Crashlytics.

    Crashlytics -> dSYMs

    You can see Uploaded

    • There is a minor issue which is Uploaded dSYM’s version is indicating Unknown

    Conclusion

    screenshot 2023 12 21 at 8.07.39e280afam

    Now I able to see all the crash issue after uploading dSYM (see above screenshot).

  • How to write a unit test for validating regex without using XCUIElement’s typeText function

    How to write a unit test for validating regex without using XCUIElement’s typeText function

    This post might be helpful if you are considering using the typeText function for validating user input using regular expressions, such as for validating passwords or usernames.

    Although XCTest enables you to write unit tests for validating the results of regular expressions, it is not suitable for writing test cases that involve inputting characters one by one.

    Let me show the example.

    d164f 7eaac screenshot2023 07 09at2.27.33pm

    Business requirements

    • Username allows alphabet + numbers only

    • Password allows alphabet + numbers + special characters

    • Address allows alphabet + numbers only

    • Phone number format is 4 digits – 4 digits

    • Postal Code is 5 numbers

    • Birthday format is yyyyMMDD

    • Username, Password, Phone, Address are required fields

    • Error message will be displayed when entering text into the TextField

    • Search button will be enabled once the postal code is validated.

    Let’s write a test cases

    ProfileInputValidatorTests: XCTestCase

    It has a sut for checking unitTests and typeText function (like a XCUIElement’s typeText) to put the characters into the active UITextField.

    4716d dcc87 screenshot2023 07 09at3.06.51pm

    class ProfileInputValidatorTests: XCTestCase {
        var sut: MockClass!
    
        override func setUp() {
            super.setUp()
            sut = MockClass()
        }
    
        override func tearDown() {
            super.tearDown()
            sut.activeTextField = nil
            sut.inputCheckType = nil
            sut.errorMessage = nil
        }
    
        //UnitTests
    }
    
    private extension ProfileInputValidatorTests {
        func typeText(_ input: String) {
            guard let activeTextField = sut.activeTextField else {
                return
            }
            for (location, character) in input.enumerated() {
                let canInputText = activeTextField.delegate?.textField?(
                    activeTextField,
                    shouldChangeCharactersIn: NSRange(location: location, length: 0),
                    replacementString: String(character)
                )
                if canInputText ?? false {
                    let lastPosition = activeTextField.endOfDocument
                    activeTextField.selectedTextRange = activeTextField
                        .textRange(from: lastPosition, to: lastPosition)
                    activeTextField.insertText(String(character))
                }
            }
        }
    }

    Here is the example of unit tests

    func test_textField_should_not_exceed_maxCharacter() {
        ProfileInputType.allCases.forEach {
            sut.setActiveTextField($0)
            let dummyText = """
            Lorem Ipsum is simply dummy text of the printing and typesetting industry.
            """
            typeText(dummyText)
            XCTAssertTrue(
                sut.activeTextField?.text?.count == sut.inputValidator.activeTextFieldType?.maxCharacter,
                "TextField tag: \($0.rawValue) should not exceed maxCharacter"
            )
        }
    }
    
    func test_username_allowance_character() {
        let username = "skboard"
        sut.setActiveTextField(.username)
        typeText(username)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
    }
    
    func test_username_not_allowance_character() {
        let username = "マーン224s한글djfタklsdf"
        sut.setActiveTextField(.username)
        typeText(username)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertEqual(ProfileInputType.username.errorMessage, sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
    }
    
    func test_postal_code_allowance_character() {
        let postalCode = "13456"
        sut.setActiveTextField(.postalCode)
        typeText(postalCode)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.postalCode)
    }
    
    func test_is_enabled_search_button_while_editing() {
        sut.setActiveTextField(.postalCode)
        let postalCode = "89761"
        typeText(postalCode)
        XCTAssertEqual(sut.activeTextField?.text, "89761")
        XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
    }
    
    func test_is_enabled_search_button_after_didEndEditing() {
        sut.setActiveTextField(.postalCode)
        let postalCode = "24028"
        typeText(postalCode)
    
        XCTAssertEqual(sut.activeTextField?.text, "24028")
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
        XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
    }
    
    func test_phone_number_format() {
        sut.setActiveTextField(.phone)
        let phoneNumber = "78761241"
        typeText(phoneNumber)
        XCTAssertEqual(sut.activeTextField?.text, "7876-1241")
    }
    
    func test_birthday_format() {
        let birthday = "19980115"
        sut.setActiveTextField(.birthday)
        typeText(birthday)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.birthday)
    }
    
    func test_phone_number_only_allow_one_hyphen_character() {
        sut.setActiveTextField(.phone)
        let phoneNumber = "1467834527891471247"
        typeText(phoneNumber)
        let hyphenCount = numberOfHyphen(in: sut.activeTextField?.text ?? "")
        XCTAssertTrue(
            hyphenCount == 1,
            "phone number only allow one hyphen character"
        )
    }

    InputValidator

    InputValidator checks the inputTypes and validates strings.

    protocol ProfileInputValidatorDelegate: AnyObject {
        var activeTextField: UITextField? { get set }
        func setActiveTextField(_ inputType: ProfileInputType)
        func updateAddressSearchButtonState(isEnabled: Bool)
        func showValidateResult(_ inputType: ProfileInputType?, error message: String?)
    }
    
    final class ProfileInputValidator: NSObject {
        var activeTextFieldType: ProfileInputType?
        var dateFormatter: DateFormatter?
        weak var delegate: ProfileInputValidatorDelegate?
        
        func validate(
            text: String,
            type: ProfileInputType,
            needToSetType: Bool = false,
            placeholder: String? = nil
        ) {
            guard isNotEmpty(text: text) else {
                showEmptyMessage(type: type, needToSetType: needToSetType, placeholder: placeholder)
                return
            }
            let errorMessage: String?
            switch type {
            case .username:
                errorMessage = isValidUsername(text) ? nil : type.errorMessage
            case .password:
                errorMessage = isValidPassword(text) ? nil : type.errorMessage
            case .phone:
                errorMessage = isValidPhoneNumber(text) ? nil : type.errorMessage
            case .address:
                errorMessage = isValidAddress(text) ? nil : type.errorMessage
            case .postalCode:
                errorMessage = isValidPostalCode(text) ? nil : type.errorMessage
            case .birthday:
                errorMessage = isValidBirthday(text) ? nil : type.errorMessage
            }
            delegate?.showValidateResult(needToSetType ? type : activeTextFieldType, error: errorMessage)
        }
        
        private var isRequiredField: Bool {
            if activeTextFieldType == .some(.username) ||
                activeTextFieldType == .some(.password) ||
                activeTextFieldType == .some(.address) ||
                activeTextFieldType == .some(.phone) {
                return true
            }
            else {
                return false
            }
        }
        
        private func showEmptyMessage(type: ProfileInputType, needToSetType: Bool, placeholder: String?) {
            let textFieldTitle = activeTextFieldType?.title ?? ""
            var emptyErrorMessage: String?
            if isRequiredField {
                emptyErrorMessage =
                    "\(placeholder ?? textFieldTitle) is required"
            }
            delegate?.showValidateResult(
                needToSetType ? type : activeTextFieldType,
                error: emptyErrorMessage
            )
        }
    
        private func isNotEmpty(text: String?) -> Bool {
            guard let inputText = text else {
                return false
            }
            let textWithOutWhiteSpace = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
            return !textWithOutWhiteSpace.isEmpty
        }
    
        private func isValidDateFormat(text: String) -> Bool {
            guard convertBirthdayFormat(text)?.count == ProfileInputType.birthday.maxCharacter else {
                return false
            }
            return true
        }
    }

    This class conforms to UITextFieldDelegate to check strings

    //MARK: UITextFieldDelegate
    extension ProfileInputValidator: UITextFieldDelegate {
        func textFieldDidBeginEditing(_ textField: UITextField) {
            guard let fieldType = ProfileInputType(textField.tag) else {
                return
            }
            delegate?.activeTextField = textField
            activeTextFieldType = fieldType
        }
    
        func textFieldDidEndEditing(_ textField: UITextField) {
            guard let inputText = textField.text, let inputType = activeTextFieldType else {
                return
            }
            validate(text: inputText, type: inputType)
        }
    
        func textField(
            _ textField: UITextField,
            shouldChangeCharactersIn range: NSRange,
            replacementString string: String
        ) -> Bool {
            let text = textField.text ?? ""
            guard let stringRange = Range(range, in: text),
                  let activeTextFieldMaxCount = activeTextFieldType?.maxCharacter
            else {
                return false
            }
            if activeTextFieldType == .phone, let phoneNumber = formattingPhoneNumber(text: text, to: string, range: range) {
                textField.text = phoneNumber
                return false
            }
            let updatedText = text.replacingCharacters(in: stringRange, with: string)
            if activeTextFieldType == .postalCode {
                delegate?.updateAddressSearchButtonState(isEnabled: updatedText.count == activeTextFieldMaxCount)
            }
            return updatedText.count <= activeTextFieldMaxCount
        }
    
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            guard let activeType = activeTextFieldType else {
                return false
            }
            textField.endEditing(false)
            // Focus on the next field
            switch activeType {
            case .username:
                delegate?.setActiveTextField(.password)
            case .password:
                delegate?.setActiveTextField(.phone)
            case .phone:
                delegate?.setActiveTextField(.address)
            case .address:
                delegate?.setActiveTextField(.postalCode)
            case .postalCode:
                delegate?.setActiveTextField(.birthday)
            default:
                break
            }
            return false
        }
    }

    MockClass

    This class has a InputValidator and conforms to ProfileInputValidatorDelegate. It is use for XCTestCase.

    class MockClass {
        var activeTextField: UITextField?
        var searchButton = UIButton()
        var inputCheckType: ProfileInputType?
        var errorMessage: String?
    
        fileprivate let inputValidator = ProfileInputValidator()
        
        var usernameTextField: UITextField!
        var passwordTextField: UITextField!
        var addressTextField: UITextField!
        var phoneTextField: UITextField!
        var postalCodeTextField: UITextField!
        var birthdayTextField: UITextField!
        
        lazy var dateFormatter: DateFormatter = {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy.MM.dd"
            formatter.calendar = Calendar(identifier: .gregorian)
            return formatter
        }()
    
        init() {
            inputValidator.delegate = self
            inputValidator.dateFormatter = dateFormatter
    
            setupTextFields()
            searchButton.isEnabled = false
        }
    
        private func setupTextFields() {
            usernameTextField = UITextField()
            passwordTextField = UITextField()
            addressTextField = UITextField()
            phoneTextField = UITextField()
            postalCodeTextField = UITextField()
            birthdayTextField = UITextField()
        
            let textFields = [
                usernameTextField,
                passwordTextField,
    
                addressTextField,
                phoneTextField,
    
                postalCodeTextField,
                birthdayTextField
            ]
    
            for (index, textField) in textFields.enumerated() {
                textField?.delegate = inputValidator
                textField?.tag = index
            }
        }
    }
    
    extension MockClass: ProfileInputValidatorDelegate {
        func setActiveTextField(_ inputType: ProfileInputType) {
            switch inputType {
            case .username:
                usernameTextField.delegate?.textFieldDidBeginEditing?(usernameTextField)
            case .password:
                passwordTextField.delegate?.textFieldDidBeginEditing?(passwordTextField)
            case .address:
                addressTextField.delegate?.textFieldDidBeginEditing?(addressTextField)
            case .phone:
                phoneTextField.delegate?.textFieldDidBeginEditing?(phoneTextField)
            case .postalCode:
                postalCodeTextField.delegate?.textFieldDidBeginEditing?(postalCodeTextField)
            case .birthday:
                birthdayTextField.delegate?.textFieldDidBeginEditing?(birthdayTextField)
            }
        }
    
        func updateAddressSearchButtonState(isEnabled: Bool) {
            searchButton.isEnabled = isEnabled
        }
    
        func showValidateResult(_ inputType: ProfileInputType?, error message: String?) {
            errorMessage = message
            inputCheckType = inputType
        }
    }

    Conclusion

    XCTest is a powerful framework for testing unit tests, but it can be challenging to test UI-related logic. In mobile development, UI logic is closely intertwined with the user interface itself. The conventional approach for testing UI logic is to use XCUITest. However, XCUITest tends to be slower compared to XCTest. If we have multiple views designed for registering or updating information using input fields, we would need to write multiple UI testing code with XCUITest.

    An alternative approach is to use XCTest and create a function that simulates user actions, such as typing text. This allows us to write a unit test for common input validator logic while considering user actions as well. This way, we can test the UI-related logic using XCTest in a more efficient manner compared to relying solely on XCUITest.

  • How to Find Unused Swift Code with Periphery Static Analyzer

    How to Find Unused Swift Code with Periphery Static Analyzer

    Recently I faced an issue with compile -O for product build. I assumed that It caused by dead function issue. I reported it

    Apple Swift Git Repo

    This post I’ll introduce static analyzer tool for checking the unused swift codes

    Install Periphery

    Link You can install periphery tool using homebrew

    Here is homebrew install command.

    brew install peripheryapp/periphery/periphery

    And then setup peripheryapp on your project

    periphery scan --setup

    34ff6 72338 screenshot2023 06 03at11.33.08pm

    Select your App’s target name. Don’t forget to save configuration to .periphery.yml

    Check unused codes

    You can see the results.

    50103 2554a screenshot2023 06 03at11.38.15pm

    Make a script file to save result as text file

    I recommend make a bash script file to save results as text file to keep track progress of reducing unused codes. Here is the bash script. (I changed ruby code (https://www.youtube.com/watch?v=2OQiCj-BG2o) to bash script)

    #!/bin/bash
    
    echo "🔍 start dead code analysis"
    
    result="$(periphery scan --config .periphery.yml)"
    
    current_dir=$(pwd)
    
    result_stripped_of_absolute_path_prefix=$(echo "$result" | sed "s|$current_dir/||g")
    
    filtered_out_result=$(echo "$result_stripped_of_absolute_path_prefix" | awk '/:[0-9]+:[0-9]+:/{ print }')
    sorted_result=$(echo "$filtered_out_result" | sort)
    
    result_with_removed_code_line_number=$(echo "$sorted_result" | sed "s|:[0-9]+:[0-9]+:|:|")
    output=$(echo "$result_with_removed_code_line_number" | tr '\n' '\n' | sort)
    
    echo "$output" > unused-codes.txt 2>&1
    echo "🙌 done with dead code analysis"

    I saved bash script file as check-unused-codes.sh

    You can run bash script file by running sh check-unused.codes.sh in terminal (You can see the results of the unused codes)

    25550 79731 screenshot2023 06 04at1.24.26am

    fd2e5 cc6e4 screenshot2023 06 04at1.24.38am

    Results will be saved into the unused-codes.txt

    Reference

    Inside iOS Dev

    https://www.parasoft.com/blog/false-positives-in-static-code-analysis/

  • How to Find the Latest SwiftUI Preview Crash Log on Mac

    How to Find the Latest SwiftUI Preview Crash Log on Mac

    SwiftUI Preview crashed

    6a3a6 a621f screenshot2023 02 12at3.15.18pm

    Preview Crashed logs are saved at “~/Library/Logs/DiagnosticReports

    on Xcode, It’s hard to check the logs. so I made a bash script to open the latest crash log file “latestPreviewCrash.sh“

    #!/bin/bash
    
    cd ~/Library/Logs/DiagnosticReports
    
    file=$(ls XCPreview* | head -1)
    
    open $file

    run this script

    sh latestPreviewCrash.sh

    It will open the crash logs.

  • How to Use an Older Version of Swift on Xcode 12 with swiftenv

    How to Use an Older Version of Swift on Xcode 12 with swiftenv

    Sometimes You need to use the older version of Swift for preparing the interview.

    For example, The Codility doesn’t support Swift 5.

    The swiftenv enable to manage the version of Swift.

    Install swiftenv using Homebrew

    step 1. Install swiftenv

    brew install kylef/formulae/swiftenv
    

    step 2. Set up zsh environment

    echo 'if which swiftenv > /dev/null; then eval "$(swiftenv init -)"; fi' >> ~/.zshrc
    

    step 3. restart terminal

     

    Check available of swift versions

    swiftenv install --list
    

     

    Install Swift 4.0

    swiftenv install 4.0
    

     

    Run Xcode and Change the Swift Version

    Xcode -&gt; Tool Chains -&gt; Select Swift 4.0 RELEASE 2017-09-19 (a)

    Xcode -> Tool Chains -> Select Swift 4.0 RELEASE 2017-09-19 (a)

     

    Conclusion

    You can efficiently manage the swift versions with the swiftenv tool. Thank you for visit my blog.

  • Xcode Shortcut Keys to Boost iOS Development Productivity

    Xcode Shortcut Keys to Boost iOS Development Productivity

    In this post, I’ll introduce useful shortcut keys to boost your productivity.

    I know it’s hard to remember shortcut keys, but when you try to use it every day, then you will be used to using them.

     

    Hide or show the navigator

    xcode_left.gif

     

    Hide or show the inspectors

    xcode_right.gif

     

    Hide or show the debug area

    xcode_bottom.gif

     

    I will be adding shortcut keys more.