iOS Development: Best Practices

iOS development. Best practices

Hello. In this article, I will try to give some tips on writing a code for beginners in iOS development. I will not dive in much details, since it will take a lot of time (to be honest, I’m just too lazy). The essence of this article is to help to create good and stable applications; demonstrate how to do it, and how not to; develop your habit of writing a good and comprehensive code.

UI

Styles

Let’s start with the styles received from a designer. You will definitely not remember every time what rgb-code you need to use for a particular button? Write an code>extension for UIColor and UIFont:

extension UIColor {
    class func blueGradientColor() -> UIColor {
        return UIColor(red: 65.0 / 255.0, green: 196.0 / 255.0, blue: 254.0 / 255.0, alpha: 1.0)
    }
    class func orangeGradient() -> UIColor {
        return UIColor(red: 246.0 / 255.0, green: 117.0 / 255.0, blue: 68.0 / 255.0, alpha: 1.0)
    }
    class func orangeGradient30opacity() -> UIColor {
        return UIColor(red: 246.0 / 255.0, green: 117.0 / 255.0, blue: 68.0 / 255.0, alpha: 0.3)
    }
…
extension UIFont {
    class func L36() -> UIFont {
        return UIFont.systemFont(ofSize: 36.0, weight: UIFontWeightLight)
    }
    class func M26() -> UIFont {
        return UIFont.systemFont(ofSize: 26.0, weight: UIFontWeightMedium)
    }
    class func L26() -> UIFont {
        return UIFont.systemFont(ofSize: 26.0, weight: UIFontWeightLight)
    }
    class func M20() -> UIFont {
        return UIFont.systemFont(ofSize: 20.0, weight: UIFontWeightMedium)
    }

You can also use:

titleLbl.font = UIFont.R18()
titleLbl.textColor = UIColor.blueGreyColor()

If you need to use a different color/style later, you will only make a change in one single file while avoiding searching for the project lines with it in the whole project.

UIAppearance

Coming up. Let’s imagine you want all the title of the navigationBar to have the same style. Of course, you can write the same code in all view controllers. But it would be better to use the same approach as in css: we create the styles in advance and then the system uses them. The appearance() method:

let barBtn = UIBarButtonItem.appearance()
    barBtn.setTitleTextAttributes([NSForegroundColorAttributeName : UIColor.white(), NSFontAttributeName : UIFont.L17()], for: UIControlState()) 

All UIKit elements have the appearance() method. Here is the сustom gradient, which this method is also applicable to:

let navBar = CRGradientNavigationBar.appearance()
        let colors = [
            UIColor.orangeGradient(),
            UIColor.pinkGradient()
        ]
        navBar.setBarTintGradientColors(colors)
        navBar.isTranslucent = false
        navBar.titleTextAttributes = [NSForegroundColorAttributeName : UIColor.white(),
NSFontAttributeName : UIFont.M17()] 

Group such things and put them into one method, let’s call it interfaceAppearance() and put it somewhere in the application: application: didFinishLaunchingWithOptions.

VCManager. sharedInstance

In my last project, I faced the problem of frequent calling different controllers from the most «inappropriate» places :). For example, I have the demoVC controller and I often refer to its properties. It seems to be easy to write two lines of code, but I wouldn’t like to write them continuously since the application architecture may always change. Create a singleton:

class VCManager: NSObject {
 
    fileprivate override init() {}
    static let sharedInstance = VCManager()
    override func copy() -> Any {
        fatalError("You are not allowed to use copy method on singleton!")
    }

Then we add the methods it can implement:

func demoVC() -> DemoVC {
        let navVC = tabBarVC.viewControllers?[2] as! UINavigationController
        return navVC.viewControllers.first as! DemoVC
    }
 
func logout() {
        if let tabBarVC = tabBarVC {
            let vc = tabBarVC.storyboard?.instantiateViewController(withIdentifier: "WelcomeVC")
            UIApplication.shared.keyWindow?.rootViewController = vc  
            //socket close
            SocketManager.sharedInstance.logoutFromWebSocket()
 
            //remove purshases
            KeychainManager.sharedInstance.removeAll()
        }
    }  

NSLayoutConstraint vs SnapKit

Another point regarding the UI is the NSLayoutConstraint. Here is an example of using this "nightmare" in the code:

private func setupConstraints() {
 
        self.backImView.translatesAutoresizingMaskIntoConstraints = false
        self.textLbl.translatesAutoresizingMaskIntoConstraints = false
        self.titleLbl.translatesAutoresizingMaskIntoConstraints = false
        self.imageView?.translatesAutoresizingMaskIntoConstraints = false
        self.button?.translatesAutoresizingMaskIntoConstraints = false
        self.customView?.translatesAutoresizingMaskIntoConstraints = false
 
        let backImLeading = NSLayoutConstraint(item: self.backImView,
                                               attribute: .Leading,
                                               relatedBy: .Equal,
                                               toItem: self,
                                               attribute: .Leading,
                                               multiplier: 1.0,
                                               constant: 15.0)
 
        let backImTop = NSLayoutConstraint(item: self.backImView,
                                           attribute: .Top,
                                           relatedBy: .Equal,
                                           toItem: self,
                                           attribute: .TopMargin,
                                           multiplier: 1.0,
                                           constant: 20.0)

Here's what the code looks like with SnapKit:

//constraints
  imageView.snp_makeConstraints(closure: { (make) in
          make.left.equalTo(customView).offset(15.0)
          make.top.equalTo(customView).offset(15.0)
          make.size.equalTo(CGSize(width: 90.0, height: 57.0))
  })
 
  whereLbl.snp_makeConstraints(closure: { (make) in
          make.left.equalTo(imageView.snp_right).offset(10.0)
          make.top.equalTo(imageView)
          make.right.equalTo(customView).offset(-15.0)
  })

Visit the official site, read the corresponding documents, and write a concise and understandable code that can then be easily corrected.

Architecture

Dependency managers

Cocoapods, Carthage or the standard Package Manager? Let’s imageine that you need to use third-party solutions that require some other dependencies. Of course, you can surf the git.hub website, look for dependencies, copy them to your project, then find out that you have used the wrong version and, finally, start over. Why? Instead, you can use one of the three above-mentioned managers, which will do everything, and in case of an update, they will also automatically update the dependencies.

In my work, I prefer Cocoapods. First, Google uses them, and it means a lot. Secondly, using cocoapods is very easy, and all third-party frameworks use this manager.

The instruction for Cocapods is here.

It is worth noting regarding Cocoapods. Let’s imagine you use swift 2.3 in your project and have not yet switched to swift 3.0. For example, you use Gloss, which has already been updated to version, which supports swift 3.0, by the publication of this article. What to do? If you write the pod ’Gloss’ in the Podfile, errors will pop up. Go to GitHub, look for Gloss, check whether they have a branch that supports swift 2.3. Then write the following in Cocoapods:

pod 'Gloss', :git => 'https://github.com/hkellaway/Gloss', :branch => ‘swift_2.3'

ЧIt means the following: use Gloss that supports swift 2.3 :)

Or just set the version for a Gloss to be updated:

pod 'Gloss', '< 1.0'

However, all the new features will no longer be available for you in this case:(

Looking for more information? Here is the material about the Package Manager and Carthage.

Gloss, JSON mapping

The project is located here. It is made for JSON mapping and creating models.

Example of use:

let dict = obj as! [AnyHashable:Any]
ChallengeModel(json: dict as! Gloss.JSON)!

Here we create a ChallengeModel object from the [AnyHashable:Any] object.

So the model class is the following (by the way, you can also use the value type structure):

class ChallengeModel: Decodable {
    var author: AuthorModel?
    var categories: [CategoriesModel]?
    var created_at: UInt?
    var id: String?
    var opponent: AuthorModel?
    var questions: [QuestionModel]?
 
    required init?(json: JSON) {
        author = "author" <~~ json
        categories = "categories" <~~ json
        created_at = "created_at" <~~ json
        id = "id" <~~ json
        opponent = "opponent" <~~ json
        questions = "questions" <~~ json
…   

We compel the Decodableprotocols, implement init?(json: JSON), and that’s it! In the end, we have a ready-to-use model. Do not use NSJSONSerializer and similar things, Gloss does it for you :)

Also quite often I am using SwiftyJSON. You can read and understand what advantages does it have in comparison with conventional approach.

Moya

I didn’t use it in its pure form (without functional programming and mvvm), but I still advise you to try Moya. This is an abstract «layer» where you can store all your endpoints.

Create an enum and we name it as you wish:). Example of use:

import Moya
 
enum Endpoints {
    case authorization(client_id: String, client_secret: String, grant_type: String, fb_access_token: String, refresh_token: String?, device_type: String, device_id: String)
    case updateRefreshToken(client_id: String, client_secret: String, grant_type: String, refresh_token: String, device_type: String, device_id: String)
    case updateUserName(name: String)
    case updatePhoto(photo: UIImage)
    case getCategories()
    case startQuestions(categories: [String], search_type: String)

We write the extension:

for it:
extension Endpoints: TargetType {
 
    var baseURL: URL { return URL(string: “http://master.you_base.url.com”)! }
 
    var path: String {
        switch self {
        case .authorization, .updateRefreshToken: return/api/some_endpoint”
        case .updateUserName: return "/api/some_endpoint"
        case .updatePhoto: return "/api/some_endpoint"
        case .getCategories: return "/api/some_endpoint"
…
 
var method: Moya.Method {
        switch self {
        case .authorization,
            .updateRefreshToken,
            .getIsFriend: return .GET
 
        case .updatePhoto,
            .startQuestions,
            .postPurchases: return .POST
 
var parameters: [String: Any]? {
        switch self {
        case .getUserFriends(let facebook_friends, let page):
            return ["page":page, "facebook_friends":facebook_friends]
 
        case .getAnswerStatistics(let categories):
            return [“categories":categories]
…
 
//for unit test
var sampleData: Data {
        return "".data(using: .utf8)!
    }
 
    var task: Task {
        switch self {
        case .updatePhoto(let photo):
            let imData = UIImagePNGRepresentation(photo)
            let multipartFormData = Moya.MultipartFormData.init(provider:
Moya.MultipartFormData.FormDataProvider.data(imData!),
                                                                name: "photo",
                                                                fileName: "file.webp",
                                                                mimeType: "image/png")
            return Task.upload(UploadType.multipart([multipartFormData]))
 
        default: return Task.request
        }
    }
… 

Every time you need to access the server, call the corresponding function in the viewModel class:

func getCategories() -> Observable<[CategoriesModel]?> {
        return ProviderManager.sharedInstance.jsonProvider
            .request(Endpoints.getCategories())
            .debug()
            .mapJSON()
            .map { (obj) -> [CategoriesModel]? in
                print(obj)
 
                var categories = [CategoriesModel]()
 
                for item in (items as! NSArray) {
                    categories.append(CategoriesModel(json: item as! Gloss.JSON)!)
                }
 
                return categories
    }
}  

Endpoints.getCategories() is the enum defined above.

I use the ProviderManager in this example. This is a singleton that has two fields:

defaultProvider = RxMoyaProvider<Endpoints>()
jsonProvider = RxMoyaProvider<Endpoints>(endpointClosure: closure)

The first field is used without parameters, the second one has too many of them to be shown here. But the point is that you will get a parsed model and the necessary data in the end. I keep using this approach during the whole cycle of working on this project. Why the ProviderManageris a singleton? The ARC will remove the object if it does not have a referred links.

«Own» DataManager

If your application uses several endpoints, you can use a different approach. Create a DataManager class and include the following methods:

class func getProfileUserId(userId: Int, handler: UserProfileHandler) {
 
            let url = "\(baseURL)/user/profile/\(userId)"
 
            NetworkManager.requestWith(url, reqMethod: .GET, dataToSend: ["userId":userId],
uploadImage: nil, handler: { (json, error) in
 
                print(json)
 
                if json != nil && error == nil {

The NetworkManager is a private class that uses Alamofire in my case, but it can use something else. The bottom line is that if something does not work for you, you can change the implementation of the class, but keep the interface intact!

private class NetworkManager {
 
    class func requestWith(url: String,
                           reqMethod: Alamofire.Method,
                           dataToSend: [String:AnyObject]?,
                           uploadImage: UIImage?,
                           handler: SuccessHandler) {
 
        var header: [String:String]?
        if DataManager.getToken().characters.count > 0 {
            let langId = NSLocale.currentLocale().objectForKey(NSLocaleLanguageCode) as! String
            let countryId = NSLocale.currentLocale().objectForKey(NSLocaleCountryCode) as! String
            let language = "\(langId)-\(countryId)"
            header = ["Authorization":"Bearer \(DataManager.getToken())", "AcceptLanguage":language]
        } else {
            header = nil
        }
 
        Alamofire.request(reqMethod, url, parameters: dataToSend, headers: header).responseJSON
{ response in
            switch response.result {
            case .Success(let JSON):
                let jsonS = SwiftyJSON.JSON(JSON)
                handler(json: jsonS, error: nil)
 
            case .Failure(let error): handler(json: nil, error: error)
            }
        }
    }
}  

The «Facade» Pattern

At the end of this section, I would like to talk about the «facade» pattern and how to use it in practice. Let’s imagine that the server returns the time stamp (seconds since Jan 01 1970.(UTC)), but it is necessary to display the time in the following format: if the needed date is today — hours: minutes, if yesterday — just write Yesterday, if later — display the date.

We should not write it in the controller every time? Create a separate DateManager class and add the necessary methods:

func gmtTimeInSeconds(date: Date) -> UInt {
        return UInt(date.timeIntervalSince1970)
    }
 
func convertSecondsToDate(seconds: UInt) -> String {
        let date = Date(timeIntervalSince1970: TimeInterval(seconds))
        let formatter = DateFormatter()
        formatter.timeZone = NSTimeZone.local
 
        if NSCalendar.current.isDateInToday(date) {
            formatter.dateFormat = "HH:mm"
            return formatter.string(from: date)
 
        } else if NSCalendar.current.isDateInYesterday(date) {
            return "Yesterday"
 
        } else {
            formatter.dateFormat = "MM"
            let monthIndex = formatter.string(from: date)
            let monthName = formatter.standaloneMonthSymbols[Int(monthIndex)!]
            formatter.dateFormat = "dd"
            return "\(monthName) \(formatter.string(from: date))"
        }
    }
 
func convertTimeIntervalInMinutesSeconds(time: UInt) -> String {
    let minutes = Int(time / 60)
    let seconds = (time % 60)
 
    if seconds < 10 {
        return "\(minutes):0\(seconds)"
    }
    return "\(minutes):\(seconds)"
} 

This class can be either a singleton or simple. In the second case. It is better to use class methods (class func) or create an instance of this class every time.

Let’s consider how to work with sockets. For this purpose, I use the proven SocketRocket framework created by Facebook. Then we write the facade again and you definitely have to use the singleton at this stage (yeaaaaaah!):

import Foundation
import SocketRocket
 
class SocketManager: NSObject {
 
    fileprivate override init() {}
    static let sharedInstance = SocketManager()
    override func copy() -> Any {
        fatalError("You are not allowed to use copy method on singleton!")
    }
 
    //------------------------------
    private var webSocket: SRWebSocket?
 
    var connectionWasOpened = true
    var lastUserId = UserDefaults.standard.string(forKey: Constants.myIdKey.rawValue)
 
    func openNewConnection(userId: String) {
        if webSocket != nil {
            webSocket?.close()
            webSocket?.delegate = nil
            webSocket = nil
        }
        lastUserId = userId
        let urlStr = “ws://your_url/\(lastUserId ?? "")"
        webSocket = SRWebSocket(url: URL(string: urlStr))
        webSocket?.delegate = self
        webSocket?.open()
}
 
func closeConnection() {
    webSocket?.close()
}
 
func logoutFromWebSocket() {
    webSocket?.close()
    webSocket?.delegate = nil
    webSocket = nil
}
 
func reconnect() {
    if lastUserId != nil &&
        (webSocket?.readyState != SRReadyState.CONNECTING) &&
        (webSocket?.readyState != SRReadyState.OPEN) {
 
        openNewConnection(userId: lastUserId!)
        }
    }
 
    deinit {
        logoutFromWebSocket()
    }
}
 
extension SocketManager: SRWebSocketDelegate {
    func webSocketDidOpen(_ webSocket: SRWebSocket!) {
        webSocket?.sendPing(nil)
        connectionWasOpened = true
    }
 
    func webSocket(_ webSocket: SRWebSocket!, didFailWithError error: Swift.Error!) {
        if error != nil {
            print("SOCKET ERROR: \(error.localizedDescription)")
        }
        if connectionWasOpened == true {
            connectionWasOpened = false
            reconnect()
        } else {
            DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: {
                self.reconnect()
            })
        }
    }
 
    func webSocket(_ webSocket: SRWebSocket!, didCloseWithCode code: Int, reason: String!, wasClean: Bool) {
        print("SOCKET ERROR: \(reason)")
        reconnect()
    }
 
    func webSocket(_ webSocket: SRWebSocket!, didReceivePong pongPayload: Data!) {
        print("PONG PAYLOAD: \(pongPayload.description)")
        if webSocket.readyState == SRReadyState.OPEN {
            webSocket.sendPing(nil)
        }
    }
 
    func webSocket(_ webSocket: SRWebSocket!, didReceiveMessage message: Any!) {
        print("MESSAGE: \(message)")
 
        EventManager.sharedInstance.performEvent(data: message)
    }
} 

You can safely (or not) use this class in your projects. Just share a small percentage (50-95%) with Stfalcon and continue working with pleasure :). Just kidding, send everything to my bank account.

SecureNSUserDefaults

Another important point. We all like to use UserDefaults. It is an easy-to-use and reliable class. Though, if a user utilizes a jailbreaked device, he/she can easily access the data. So make sure that the data is encrypted:

#Keychain
pod 'SecureNSUserDefaults', '~> 1.0.1'
pod ‘CocoaSecurity'

This extension is created for UserDefaults to encrypt your data. Therefore, you should not store important data in UserDefaults because it is only for settings.

No internet connection

There is no the Internet connection. Will we inform the user about it or let him guess when the application crashes? Let’s say that we have to inform a user, or at least warn :). Use a third-party solution called UHBConnectivityManager (available on Cocoapods). In AppDelegate, applicaiton: didFinishLaunchingWithOptions:: write the following:

//internet connection
        UHBConnectivityManager.shared().registerCallBack({ [weak self] (status:
ConnectivityManagerConnectionStatus) in
            if status == ConnectivityManagerConnectionStatusConnected {
                print("Internet connected")
                SocketManager.sharedInstance.reconnect()
 
                self?.alertVC.dismiss(animated: true, completion: nil)
 
            } else {
                print("No connection")
                SocketManager.sharedInstance.closeConnection()
 
                VCManager.sharedInstance.topViewController()?.present((self?.alertVC)!, animated: true, completion: nil)
             }
        }, forIdentifier: self.memoryAddress()) 

alertVC — is a property:

fileprivate var alertVC: UIAlertController = {
        let vc = UIAlertController(title: "Warning", message: "No internet connection",
preferredStyle: .alert)
        let action = UIAlertAction(title: "Ok", style: .default, handler: nil)
        vc.addAction(action)
        return vc
    }() 

Finally, I would like to share my experience on working with UIWebView. In my last project, I had to create a controller that actively used UIWebView. This is the Explanation Screen that shows the result of the test passed by the player. If the answer is incorrect, a red button with a response shows up and the correct answer with explanation is displayed. Of course, you can use UILabel and UIButton, and put it all in the UIScrollView cell that includes pagination (there are mutiple questions). So it was originally done like that, but (again that «but» !!!) it was necessary to use MathML in the code (to display mathematical formulas), and UILabel cannot display it properly. Moreover, UIWebView has a built-in UIScrollView, so it was not necessary to calculate the height for each element :)

I show a piece of code for a cell:

 let pathToCss = Bundle.main.path(forResource: "styles", ofType: "css")
 
        cell.webView.loadHTMLString(HtmlHelper.fontExplanation(questionTitle: questionTitle,
                                                               question: question,
                                                               wrongTitle: item!.is_correct! == false ? "YOUR ANSWER —
WRONG" : "",
                                                               wrongAnswer: item!.is_correct! == false ? wrongText : nil,
                                                               correctTitle: item!.is_correct! == false ? "CORRECT
ANSWER" : "YOUR ANSWER - CORRECT",
                                                               correctAnswer: correctText,
                                                               explanationTitle: "EXPLANATION",
                                                               explanation: explanationText,
                                                               isCorrect: item!.is_correct!),
                                     baseURL: URL(string: pathToCss!)) 

The interesting thing is the pathToCss — in the project, I also added css-styles and used the path to them as baseURL. Method of the HtmlHelper fontExplanation class:

class func fontExplanation(questionTitle: String,
                               question: String,
                               wrongTitle: String?,
                               wrongAnswer: String?,
                               correctTitle: String,
                               correctAnswer: String,
                               explanationTitle: String,
                               explanation: String,
                               isCorrect: Bool) -> String {
 
            return "<!doctype html> <html lang=\"en\"> <head> <meta charset=\"UTF-8\"> <meta
name=\"viewport\" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximumscale=1.0, minimum-scale=1.0\"> <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"> <title>Document</title> <link rel=\"stylesheet\" href=\"styles.css\"> </head> <body> <p class=\"answer-text\">\(question)</p> <div class=\"label\">\(correctTitle)</div> <div class=\"answer answer_right\">\(correctAnswer)</div> <div class=\"label\">\(explanationTitle)</div> <p
class=\"answer-text\">\(explanation)</p> </div> </body> </html>"

Using a small piece of layout (mine is 0 due to Ruslan;)) and UIWebView knowledge, you can format any text, even MathML, and represent it all in a very attractive form.

Swift

Well, as you might already understood from the title and my code, Swift is the language that I use everywhere in my work. I used to utilize Objective-C, but that time is passed, and now it is used only to support legacy projects. Despite I know programmers who still write in obj-c, but this is another story and I cannot judge them. After all, the final result is a binary code for the machine, and it does not matter how you have written it. If it works, few people care. But the support of such projects is an extremely difficult task, and no one wants to do it. If the C and C ++ languages ​​will always remain (since they are in the core of any language), then obj-c may completely disappear, because swift has already fully overtaken it:)

Since Apple has made swift open source, it is used in other areas. For example, it is used for raspberry pi 3! I am not good at it because I do not work in this area, but I have tried another thing called Vapor.

Vapor is a framework, which is created for backend development, written in swift. The useful information is here. Official documentation: here it is. I will only show a small example how to work with it.

After installing and creating the project, run the vapor xcode command from the console. Vapor will create an xcode project that you can use to «lift» the server locally on your machine.

import Vapor
 
let drop = Droplet()
 
drop.get("hello") { request in
    if let name = request.data["name"]?.string {
        return try drop.view.make("hello", ["name": name])
    }
 
    return try drop.view.make("hello", ["name": "no name"])
}
 
drop.run()

Click cmd + B, then lauch the project. The console will display:

No command supplied, defaulting to serve...
No preparations.
Server 'default' starting at 0.0.0.0:8080

Open Safari (or whatever you use as a browser) and enter 0.0.0.0:8080 or localhost: 8080 in the address line. You will see 404 page not found. Add to the address line the following:

http://localhost:8080/hello?name=enter_you_name

You will see the following result in your browser:

Of course, if you have a deep knowledge of backend-development, you can write something really good :). I just wanted to show the evolution of swift over the past few years, while remaining fairly simple and comprehensive in language.