Tag: Swift

  • How to add the SwiftLint without pain

    Why do you need SwiftLint?

    1. It speed ups code reviews, you stop wasting time on code formatting, force casting, and other common issues
    2. It improves the code quality, code becomes more readable, and maintainable

    Integration

    1. Add SwiftLint to a podfile

    Or use another option from the official documentation.

      # A tool to enforce Swift style and conventions.
      pod 'SwiftLint', '0.43.1'

    2. Add this .swiftlint.yml config file to the project root folder

    The main trick to make integration easy is to disable all rules at first.

    disabled_rules:
    - trailing_whitespace
    - force_cast
    - force_unwrapping
    - force_try
    - empty_string
    - line_length
    - mark
    - weak_delegate
    - unneeded_break_in_switch
    - identifier_name
    - function_parameter_count
    - unused_closure_parameter
    - vertical_whitespace
    - opening_brace
    - control_statement
    - statement_position
    - comma
    - colon
    - trailing_comma
    
    opt_in_rules:
    - empty_count
    - empty_string
    
    excluded:
    - Carthage
    - Pods
    - SwiftLint/Common/3rdPartyLib
    
    line_length:
        warning: 150
        error: 200
        ignores_function_declarations: true
        ignores_comments: true
        ignores_urls: true
    
    function_body_length:
        warning: 300
        error: 500
    
    function_parameter_count:
        warning: 6
        error: 8
    
    type_body_length:
        warning: 300
        error: 500
    
    file_length:
        warning: 1000
        error: 1500
        ignore_comment_only_lines: true
    
    cyclomatic_complexity:
        warning: 15
        error: 25
    
    reporter: "xcode"

    3. Build a project, add additional rules to the disabled_rules section, if there are many warnings for them

    4. Delete rules from the disabled_rules section one by one and fix all the warnings

    5. Leave the trailing_whitespace and other rules that you don’t want to fix in the disabled_rules section

    In the end, you will have something like this in your config file.

    disabled_rules:
    - trailing_whitespace
    
    opt_in_rules:
    - empty_count
    - empty_string
    
    excluded:
    - Carthage
    - Pods
    - SwiftLint/Common/3rdPartyLib
    
    line_length:
        warning: 150
        error: 200
        ignores_function_declarations: true
        ignores_comments: true
        ignores_urls: true
    
    function_body_length:
        warning: 300
        error: 500
    
    function_parameter_count:
        warning: 6
        error: 8
    
    type_body_length:
        warning: 300
        error: 500
    
    file_length:
        warning: 1000
        error: 1500
        ignore_comment_only_lines: true
    
    cyclomatic_complexity:
        warning: 15
        error: 25
    
    reporter: "xcode"

    Some ideas on how to fix common code issues

    Force Cast Violation: Force casts should be avoided. (force_cast)

    Problem:

    let cell = collectionView.dequeueReusableCell(
        withReuseIdentifier: "DocumentCollectionViewCell",
        for: indexPath
    ) as! DocumentCollectionViewCell
    

    Solution:

    guard let cell = collectionView.dequeueReusableCell(
        withReuseIdentifier: "DocumentCollectionViewCell",
        for: indexPath
    ) as? DocumentCollectionViewCell else { fatalError() }
    

    Force Try Violation: Force tries should be avoided. (force_try)

    Problem:

    let workingDirectory = try! FileManager.default.url(
        for: .documentDirectory,
        in: .userDomainMask,
        appropriateFor: nil,
        create: false
    )
    

    Solution:

    do {
        let workingDirectory = try FileManager.default.url(
            for: .documentDirectory,
            in: .userDomainMask,
            appropriateFor: nil,
            create: false
        )
    } catch {
        print(error)
    }
    

    Exceptions

    In very rare cases you will not be able to fix the warning. For example, when you use a framework and you cannot easily change its API. Then you can switch off a SwiftLint rule for a particular code block using // swiftlint:disable rule_name and // swiftlint:enable rule_name comments.

  • Chaining example on RxSwift

    This is one of the reasons I love RxSwift. It allows to chain requests beautifully.

    Here I authorize user by getting auth token, then I request userInfo, then I request profile.

    func loadUserInfoAndProfile(login: String, password: String) {
        UserManager.shared.auth(login: login, password: password)
            .flatMap { _ in
                return UserManager.shared.info()
            }
            .map { (result: APIResult) -> UserInfo? in
                switch result {
                case .success(let value):
                    UserManager.shared.userInfo = value
                    return value
                case .failure(let error):
                    print(error)
                    return nil
                }
            }
            .flatMap { userInfo in
                return ServiceManager.shared.profileView()
            }
            .map { (result: APIResult) -> ServiceProfile? in
                switch result {
                case .success(let value):
                    ServiceManager.shared.profile.value = value
                    return value
                case .failure(let error):
                    print(error)
                    return nil
                }
            }
            .subscribe(onNext: { _ in
                self.showTabs()
            }, onError: { error in
                self.showTutorialViewController()
            })
            .disposed(by: disposeBag)
    }
    
  • Получение строки, содержащей склонения дней дня день

    Достаточно часто используемая штука, поэтому решил сохранить сниппет:

    func dayStringFromNumberOfDays(_ numberOfDays: Int) -> String {
        let lastDigit = numberOfDays % 10
        if (numberOfDays >= 11 && numberOfDays <= 19)
        {
            return "дней";
        }
        else
        {
            if lastDigit == 1 { return "день" };
            if lastDigit > 1 && lastDigit < 5 { return "дня" };
            if lastDigit == 0 || lastDigit >= 5 { return "дней" };
        }
        return ""
    }
    

    Использование:

    let daysLeft = 11
    let dayString = self.dayStringFromNumberOfDays(daysLeft)
    let fullString = "\(daysLeft) \(dayString)"
    print(fullString)
    

    Результат: 11 дней

  • Swift build failed. No errors.

    This happened to me while I was writing in Swift 4.1 in XCode 9.3. I use RxSwift also. It never happened while I was writing in Objective C. There were no any logs in Report Navigator too. Project just compiled without errors, then failed. Couldn’t figure out why.

    In my case what I did and it helped:

    1. Commit changes in Git and then discard changes in XCode if it shows “M” near files
    2. Clear derived data: Xcode -> Preferences -> location
    3. Clean build folder: Shift + Cmd + Alt + K
    4. Restart XCode

    After this project tried to compile and showed errors. The error was here:

        do {
            let jsonData = try JSONSerialization.data(withJSONObject: timingsDict, options: .prettyPrinted)
            let decoder = JSONDecoder()
            let pTiming = try decoder.decode(PTiming.self, from: jsonData)
            observer.onNext(timing)
            observer.onCompleted()
        } catch {
            print(error.localizedDescription)
            observer.onError(error)
        }
    

    I passed wrong variable name in observer.onNext(timing). It should have been pTiming.

  • Asynchronous requests chaining using FlatMap operator in RxSwift

    One of the main features of using RxSwift and RxCocoa or FRP is the possibility to chain several asynchronous requests and avoid callback hell. It can be achieved by using FlatMap operator and creating your own Observables from your asynchronous jobs like network requests or heavy calculations. But sometimes it is not simple to understand, how to do it. In this post I would like to show an example from one of my projects. The same can be applied to Objective C + ReactiveCocoa, the idea is the same.

    First of all we should create Observables from different asynchronous requests. Then we can chain them using FlatMap operator. My example concerns a classic case – login and profile fetching. In many apps after logging in a user you have to request his profile in a separate request.

    Usually you use MVVM with RxSwift. In a ViewModel you can create observables. In my case login() method creates an Observable from Alamofire request. In loadProfile() method Observable is created from ProfileService.

    /// Logins user and saves credentials
    ///
    /// - Returns: true if login was successfull; error in other cases
    func login() -> Observable {
        
        return Observable.create { (observer) -> Disposable in
            
            let params = ["username": self.nameText.value,
                          "password": self.passwordText.value]
            
            self.inProgress.value = true
            
            Alamofire.request(APPURL.Login,
                              method: .post,
                              parameters: params)
                .responseJSON(completionHandler: { (dataResponse) in
                    
                    self.inProgress.value = false
                    
                    switch dataResponse.result {
                    case .success(let value):
                        
                        guard let jsonDictionary = value as? Dictionary else {
                            let userInfo: [String : Any] =
                                [NSLocalizedDescriptionKey : "Unable to login with the provided credentials"]
                            let error = NSError(domain:"", code:0, userInfo:userInfo)
                            return observer.onError(error)
                        }
                        
                        guard let authToken = jsonDictionary["auth_token"] as? String else {
                            let userInfo: [String : Any] =
                                [NSLocalizedDescriptionKey : "Unable to login with the provided credentials"]
                            let error = NSError(domain:"", code:0, userInfo:userInfo)
                            return observer.onError(error)
                        }
                        
                        self.saveAuthToken(authToken)
                        
                        observer.onNext(true)
                        observer.onCompleted()
                        
                    case .failure(let error):
                        
                        print("Could not login due to \(error)")
                        observer.onError(error)
                        
                    }
                })
            
            return Disposables.create()
        }
    }
    
    /// Loads profile
    ///
    /// - Returns: profile object if success, otherwise error
    func loadProfile() -> Observable {
    
        return Observable.create({ (observer) -> Disposable in
            
            self.inProgress.value = true
            
            ProfileService.shared.getProfile {
                
                self.inProgress.value = false
                
                guard let profile = ProfileService.shared.profile else {
                    let userInfo: [String : Any] =
                        [NSLocalizedDescriptionKey : "Unable to load profile"]
                    let error = NSError(domain:"", code:0, userInfo:userInfo)
                    return observer.onError(error)
                }
                
                observer.onNext(profile)
                observer.onCompleted()
            }
            
            return Disposables.create()
        })
    }
    

    This is how you chain login and loadProfile requests. When they finish, you can do some job with a result profile. In this case I check a trial period and navigate to one of the views depending on a result.

    @IBAction func signMeInAction(_ sender: AnyObject) {
        self.viewModel.login()
        .flatMap {result in
            self.viewModel.loadProfile()
        }
        .subscribe(onNext: { profile in
            let trialNotExpired = self.viewModel.checkTrialNotExpired(for: profile)
            if trialNotExpired {
                let firstAppLaunch = self.viewModel.checkFirstAppLaunch()
                if firstAppLaunch {
                    self.performSegue(withIdentifier: Segue.FromLoginToInstruction, sender: nil)
                }
                else {
                    self.navigationController?.popToRootViewController(animated: true)
                }
            }
            else {
                self.showTrialExpirePopUp()
            }
        },
        onError: { error in
            SCLAlertView().showError("Error", subTitle:error.localizedDescription)
        })
        .disposed(by: disposeBag)
    }
    

    Note that error for both requests is handled in one place using SCLAlertView. FlatMap operator maps a result of a first request to a second request and also flattens Observables. If you use just Map instead of FlatMap, the result of chaining will be an Observable of Observables.

  • Handling Swift JSONDecoder typeMismatch error while using Codable protocol

    If you have a problematic JSON, which can contain number or string for some keys, you can decode object without that property and manually set that property after decoding.

    For example, I have a Vehicle class inside HistoryItem. In Vehicle model_year can be empty String or non empty Int. Here I am decoding modelYear manually using NSDictionary and trying to get Int. Swift 4 cannot do it automatically.

        do {
            // Decoding HistoryItem from JSON
            let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
            let decoder = JSONDecoder()
            let historyItem = try decoder.decode(HistoryItem.self, from: jsonData)
            if let modelYear = (dict as NSDictionary).value(forKeyPath: "vehicle.model_year") as? Int {
                historyItem.vehicle?.modelYear = modelYear
            }
            
            // Saving HistoryItem to Realm
            let realm = try! Realm()
            try! realm.write {
                realm.add(historyItem, update: true)
            }
            
        } catch {
            print(error.localizedDescription)
        }
    

    This is my Vehicle class that is contained inside HistoryItem:

        class Vehicle: Object, Codable {
            @objc dynamic var VIN: String = ""
            @objc dynamic var make: String?
            @objc dynamic var modelName: String?
            @objc dynamic var recallCount: Int = 0
            @objc dynamic var modelYear: Int = 0
            
            override static func primaryKey() -> String? {
                return "VIN"
            }
            
            private enum CodingKeys: String, CodingKey {
                case VIN = "vin"
                case make
                case modelName = "model_name"
                case recallCount = "recall_count"
            }
        }
    

    As you see, there is no model_year key in CodingKeys.

  • Creating a cold signal in ReactiveCocoa 7

    For any case, there is an arbitrarily complex way to do it.

    This prints “HELLO WORLD!”.

    let signalProducer = SignalProducer {
                () -> String in
                return "Hello world"
                }
                .map { (s) -> String in
                    return s.uppercased() + "!"
                }
            
            signalProducer.startWithValues { (input) in
                print(input)
            }