Tag: swift

  • Why Our iOS Team Now Allows Storyboard – Less Code, Faster Compile, Better Preview

    Why Our iOS Team Now Allows Storyboard – Less Code, Faster Compile, Better Preview

    Our UI creation rule was creating a UI programmatically. But recently, we changed it.

    Now our iOS team can create UI depending on their preferred approaches.

    The reason is that there are really good things if you use Storyboard

    • Less Code
    • Compile Time
    • Preview
    • Layout warnings

    Among them, the main reason for making a decision is Compile time.

    Compile Time: Swift vs Storyboard

    Imagine if you need to change the spacing in UIStackView.

    4166c 1epqfvqnf3tep7fema s ja

    The left one is programmatic UI. If you change just one line of code then the swift compiler detects source file changes and will compile it. It may take more time if the source file dependent on other source files.

    The right one is used Storyboard. If you change the spacing in StackView on Storyboard then ibtool compile a storyboard into the multiple nibs(stands for NextStep Interface Build file) at compile time.

    Results: Storyboard compile-time 6.5x faster than Swift

    • Swift: 46.8 seconds
    • Storyboard: 7.2 seconds

    As you can see from the results, compile-time when using the storyboard is much faster than swift file changes.

    7b005 1 4dwt feex5kcczlqg904g
    WWDC2018, Behind the Scenes of the Xcode Build Process

    If you want to know ibtool’s compile options then check man ibtool in /Applications/Xcode.app/Contents/Developer/usr/bin.

    298cd 1k8sex gbcwekxd36yoi rq
    /Applications/Xcode.app/Contents/Developer/usr/bin/
    3fb3b 1iwrvmsxsf12bvcneyenipg
    You can check the Interface Builder document.

    Although there were pros we’ve had some concerns about using Storyboard like below

    • PR Review
    • Conflicts Storyboard/Xib files

    To address these concerns, we need to know about the xib(stands for XML Interface Builder) format.

    xib format

    Storyboard/Xib is a XML format. It means that Human Readable format. I sometimes heard that from iOS Engineers Storyboard/Xib files are hard to review. It’s true compared to reviewing swift code.

    But once familiar with that format You may feel It is easy to review it. Because It looks like HTML format.

    769da 1romijyn5pp69xifthuecha
    The storyboard is an XML document.

    The numbers above the image mean like below

    1. toolVersion

    It means Interface builder tool’s version. It may be changed when you update Xcode. I highly recommend all team members use the latest Xcode version to avoid merge conflicts.

    1. initialViewController

    It indicates the initial view controller’s object id in Storyboard. So If you called an instantiateInitialViewController() then It returns that viewController.

    1. device

    You can use it to preview the layout on Storyboard. Anyone can change it but after checking the layout then I recommend resetting it as your team’s default device (e.g. iPhone X) to avoid merge conflicts.

    1. scenes

    A storyboard can have multiple scenes. A Scene has a ViewController.

    1. segue

    segue is a connection between ViewControllers. It has an id and a destination’s id. If the destination’s id has changed then we need to review it carefully. If there is no ViewController that has the destination’s id then It might be caused a crash at runtime.

    PR review

    I’ll show two examples.

    • Minor changes
    • Major changes (e.g. Layout, objectId, segue, etc)

    Minor Changes

    These changes normally don’t affect your UI. So You can easily review and approve it. It should be fine.

    425dd 1oiqk9iejnzpui4zzgrmcaa
    • toolsVersion / plugin version can be changed automatically when you use a different Xcode version and edited the Interface Builder.
    • rect can be changed when you change the device or move a subview. These small changes should be fine because It uses autolayout.
    • propertyAccessControl(Locking View) is controlled by the developer. I’ll explain it next section.

    Major Changes

    Major Changes will affect layout issues. I’ll show you some examples below.

    625c2 179yhthon9cb1dh9znaqnnq
    Embedded in UIStackView

    If you embedded labels in UIStackView then you have to double-check label’s id is in stackView or not.

    d1934 1e8qkfsdftr5tp dhp wlyw
    Layout Constraint Issue

    An ambiguous=”YES” indicates us there is Layout Issue! So It should be fixed.

    Pull Request Template

    I highly recommend adding a screenshot of the layout in PR. To encourage your teammate to add a screenshot, set a Pull Request Template.

    It is our team’s template.

    Description
    ===

    Screens
    ---

    Links (JIRA / Confluence)
    ---

    Related PR
    ---

    Check Lists
    ---
    - [x] multiple devices
    - [x] confirmed by developer
    c3b95 15jd7ixiiq7wiqnlugnuvvq
    Add a pull_request_template.md in .github/ path

    How to avoid merge conflicts?

    File changes cause merge conflicts. You can avoid it by dealing with file changes carefully.

    By following this rule, merge conflicts are avoided as much as possible.

    • use the latest Xcode.
    • don’t commit the changes if you are not working on that View
    • locking Views in Storyboard and XIB Files you are working on

    If your team is working on the same Storyboard, I highly recommend locking views to prevent unwanted changes.

    21324 1 gtiwzhrj9lyultmkk luq
    Show the Identity Inspector -> Lock

    You can set a lock on any UI Components(Whole Storyboard, UIViewController, UIView, Segue, any UI-related objects like a Constraint) you will be working on. As I mentioned above, propertyAccessControl will be added to the storyboard/xib file when you set a lock.

    073d2 1tzky1ldnxtiljrus fxtlw
    UIViewController is locked

    Lock Image will be shown when you try to change on Locking View.

    Conclusion

    There are cons and pros to using Storyboard/Xib. Productivity is also different depending on the iOS developer’s preferred UI creation approach. But We made a decision to use both. Because we can save compile-time and found a good solution to resolve concerns for using it.

    Also, It is a first-party tool. Apple has developed the IB tool over 34 years. And It is being actively updated come with Xcode.

    • 34 Years Ago In 1988, Interface Builder was released by Apple
    • 15 Years Ago In 2007, XML Interface Builder was released by Apple
    • 11 Years Ago In 2011, Storyboard was introduced at WWDC 2011

    I hope our decision helps you decide to use Storyboard/Xib in your team!

    References

    Locking Views in Storyboard and XIB Files

    What’s New in Storyboards

    Build interfaces with style

    Implementing UI Designs in Interface Builder

    Auto Layout Techniques in Interface Builder

    Behind the Scenes of the Xcode Build Process

  • How to Use Docker with Vapor Server-Side Swift on Local Development

    How to Use Docker with Vapor Server-Side Swift on Local Development

    I wrote about How to deploy Vapor App(Server-Side-Swift) to the AWS

    This post is about setting on a local server using Docker. (Before you read this post, I highly recommend Server Side Swift – Vapor book.)

     

    Step 1. Create local.Dockerfile

    Look at the last command.

    CMD [“serve”, “–env”, “local”, “–hostname”, “0.0.0.0”, “–port”, “8080”]

    <- I set —env local.

    # Build image
    FROM --platform=linux/x86-64 swift:5.5-focal as build
    
    RUN apt-get update -y \
        && apt-get install -y libsqlite3-dev
    
    WORKDIR /build
    
    COPY . .
    
    RUN swift build \
        --enable-test-discovery \
        -c release \
        -Xswiftc -g
    
    
    # Run image
    FROM --platform=linux/x86-64 swift:5.5-focal-slim
    
    RUN useradd --user-group --create-home --home-dir /app vapor
    
    WORKDIR /app
    
    COPY --from=build --chown=vapor:vapor /build/.build/release /app
    COPY --from=build --chown=vapor:vapor /build/Public /app/Public
    
    # ARS RDS Environment ARG
    ARG AWS_RDS_HOST
    ARG AWS_RDS_PORT
    ARG AWS_RDS_USER
    ARG AWS_RDS_PASS
    ARG AWS_RDS_DB
    
    # SignInWithApple Environment ARG
    ARG SIWA_ID
    ARG SIWA_REDIRECT_URL
    ARG SIWA_JWK_ID
    ARG SIWA_PRIVATE_KEY
    ARG SIWA_TEAM_ID
    ARG SIWA_APP_BUNDLE_ID
    
    # Set Environment
    RUN echo "SIWA_ID=${SIWA_ID}" > .env.development
    RUN echo "SIWA_REDIRECT_URL=${SIWA_REDIRECT_URL}" >> .env.development
    RUN echo "SIWA_JWK_ID=${SIWA_JWK_ID}" >> .env.development
    RUN echo "SIWA_PRIVATE_KEY=${SIWA_PRIVATE_KEY}" >> .env.development
    RUN echo "SIWA_TEAM_ID=${SIWA_TEAM_ID}" >> .env.development
    RUN echo "SIWA_APP_BUNDLE_ID=${SIWA_APP_BUNDLE_ID}" >> .env.development
    
    RUN echo "DB_HOST=${AWS_RDS_HOST}" >> .env.development
    RUN echo "DB_PORT=${AWS_RDS_PORT}" >> .env.development
    RUN echo "DB_USER=${AWS_RDS_USER}" >> .env.development
    RUN echo "DB_PASS=${AWS_RDS_PASS}" >> .env.development
    RUN echo "DB_NAME=${AWS_RDS_DB}" >> .env.development
    
    USER vapor
    
    EXPOSE 8080
    
    ENTRYPOINT ["./Run"]
    CMD ["serve", "--env", "local", "--hostname", "0.0.0.0", "--port", "8080"]

     

    Step 2. local-docker-compose.yml

    version: '3.7'
    services:
      travelcrumb-beta:
        depends_on:
          - postgres
        build:
          context: .
          args:
            AWS_RDS_HOST: postgres
            AWS_RDS_PORT: 5432
            AWS_RDS_USER: shawn
            AWS_RDS_PASS: 1234
            AWS_RDS_DB: test_db
            SIWA_ID: xxxx.xxx.xxx
            SIWA_REDIRECT_URL: localhost:8080
            SIWA_JWK_ID: xxxxx
            SIWA_PRIVATE_KEY: xxxx
            SIWA_TEAM_ID: xxxx
            SIWA_APP_BUNDLE_ID: xxxxx
          dockerfile: local.Dockerfile
        ports:
          - '8080:8080'
    
      postgres:
        image: postgres
        environment:
          POSTGRES_USER: shawn
          POSTGRES_PASSWORD: 1234
          POSTGRES_DB: test_db
    
      start_dependencies:
        image: dadarek/wait-for-dependencies
        depends_on:
          - postgres
        command: postgres:5432

     

    Step 3. local.sh

    #!/bin/bash
    echo "🐕 Build Docker"
    docker-compose -f local-docker-compose.yml build
    echo "🐕 Start dependencies"
    docker-compose -f local-docker-compose.yml run --rm
    echo "🐕 Start Vapor App"
    docker-compose -f local-docker-compose.yml up travelcrumb-beta

     

    Step 4. sh local.sh

    e9b3b 4baee screenshot2022 08 08at11.39.56pm

    Now you can set up all you need to install for running the Vapor server on your local machine. Without Docker, You have to run and stop DB every time when you run a server on your local machine.

    Thanks for reading my post

  • Swift Struct vs Class – When to Use Structs and Classes in iOS

    Swift Struct vs Class – When to Use Structs and Classes in iOS

    I was asked What is Struct and Class? Could you explain it?

    It’s the most common interview question.

    I summarized the differences between classes and structure when to use it.

     

    Struct versus Class

    Structure and Enum are value types.

    • It is copied when it’s assigned to a variable or constant, or it’s passed to a function.

    Classes are reference types.

    • Reference types are not copied when they’re assigned to a variable or constant or when they’re passed to a function. Rather than a copy, a reference to the same existing instance is used.

    Structure and Class both can

    • define properties to store values

    • define methods to provide functionality

    • define subscripts to provide access to their values using subscript syntax

    • define initializers to set up their initial state

    • be extended to expand their functionality beyond a default implementation

    • conform to protocols to provide standard functionality of a certain kind

    • Include computed properties.

    Classes have additional capabilities that structures don’t have.

    • Inheritance enables one Class to inherit the characteristics of another.

    • Typecasting enables you to check and interpret the type of a class instance at runtime.

    • Deinitializers enable an instance of a class to free up any resources it has assigned.

    • Reference counting allows more than one reference to a class instance.

    • Identity operators to check whether two constants or variables refer to the same single instance.

      • Identical to (===)

      • Not identical to (!==)

    The structure has additional capabilities that classes don’t have.

    • Memberwise initializers

      • You can use it to initialize the member properties of the new structure instance.

     

    Choosing between Structures and Classes

    Decide how to store data and model behavior.

    When designing a type, we have to think about whether ownership of a particular instance of this type “has to be shared among different parts of our program, or if multiple instances can be used interchangeably as long as they represent the same value. To share ownership of a particular instance, we have to use a class. Otherwise, we can use a struct.

    Chris Eidhof. “Advanced Swift.”

    Apple’s guide

    • Use structures by default

    • Use structures along with protocols to adopt behavior by sharing implementations.

    • Use classes when you need Objective-C interoperability

    • Use classes when you need to control the identity of the data you’re modeling

    Choose Structures

    • It makes it easier to reason about a portion of your code without needing to consider the whole state of your app. Because structures are valued types-unlike classes-local changes to a structure aren’t visible to the rest of your app unless you intentionally communicate those changes as part of the flow of your app.

    • when you don’t control identity

      • Use structure when you’re modeling data that contains information about an entity with an identity that you don’t control.

      • Local changes to model types like PenPalRecord are useful. For example, an app might recommend multiple different penpals in response to user feedback. Because the PenPalRecord structure doesn’t control the identity of the underlying database records, there’s no risk that the changes made to local PenPalRecord instances accidentally change values in the database.

      • If another part of the app changes my nickname and submits a change request back to the server, the most recently rejected change won’t mistakenly pick up penpal recommendation. Because the myId property is declared as a constant, it can’t change locally. As a result, requests to the database won’t accidentally change the wrong record.

    struct PenPalRecord {
        let myID: Int
        var myNickname: String
        var recommendedPenPalID: Int
    }
    
    var myRecord = try JSONDecoder().decode(PenPalRecord.self, from: jsonResponse)
    • Use structures and protocols to model inheritance and share behavior.

      • Structure can’t inherit from classes. However, the kinds of inheritance hierarchies you can build with class inheritance can also be modeled using protocol inheritance and structures.

      • Protocols permit classes, structures, and enumerations to participate in inheritance, while class inheritance is only compatible with other classes. When you are choosing how to model your data, try building the hierarchy of data types using protocol inheritance first, then adopt those protocols in your structures.

    Choose Classes

    • when you need to control identity (===)

      • Common use cases are file handles, network connections, and shared hardware intermediaries like CBCentralManager.

      • If you share a class instance across your app, changes you make to that instance are visible to every part of your code that holds a reference to that instance.

        • local database connection.

     

  • Build a Receipt Text Recognizer with Apple Vision Framework in Swift

    Build a Receipt Text Recognizer with Apple Vision Framework in Swift

    Overview

    overview.png

    There are only three steps. I tested these steps using the playground app.

     

    Sample Code

    Download

     

    Prepare the receipt images.

    I found a bunch of receipt images on Github. Almost 200 receipts are there.

    You can download the receipt images below

     

    Let’s coding.

    A few lines of code are needed. I used the playground app. Let’s follow the steps.

    Create a playground file in Xcode(File -&gt; New -&gt; Playground). Choose the iOS or macOS platform.

    Create a playground file in Xcode(File -> New -> Playground). Choose the iOS or macOS platform.

    Drag receipt images into the resource folder. And then click the source file and add a new Helper.swift file.

    Drag receipt images into the resource folder. And then click the source file and add a new Helper.swift file.

     

    Helper.swift

    I add the functions to load receipt image files and draw the bounds which are detected text area.

    Helper.swift file for macOS

    import Cocoa
    import Vision
    
    public func getTestReceiptImageName(_ number: Int) -> String {
        String.init(format: "%d-receipt", number)
    }
    
    //https://www.swiftbysundell.com/tips/making-uiimage-macos-compatible/
    public extension NSImage {
        var cgImage: CGImage? {
            var proposedRect = CGRect(origin: .zero, size: size)
            return cgImage(forProposedRect: &proposedRect,
                           context: nil,
                           hints: nil)
        }
    }
    
    //https://www.udemy.com/course/machine-learning-with-core-ml-2-and-swift
    public func visualization(
        _ image: NSImage,
        observations: [VNDetectedObjectObservation],
        boundingBoxColor: NSColor
    ) -> NSImage {
        var transform = CGAffineTransform.identity
        transform = transform.scaledBy(x: image.size.width, y: image.size.height)
    
        image.lockFocus()
        let context = NSGraphicsContext.current?.cgContext
        context?.saveGState()
    
        context?.setLineWidth(2)
        context?.setLineJoin(CGLineJoin.round)
        context?.setStrokeColor(.black)
        context?.setFillColor(boundingBoxColor.cgColor)
    
        observations.forEach { observation in
            let bounds = observation.boundingBox.applying(transform)
            context?.addRect(bounds)
        }
    
        context?.drawPath(using: CGPathDrawingMode.fillStroke)
        context?.restoreGState()
        image.unlockFocus()
        return image
    }

    Helper.swift file for iOS

    import Foundation
    import UIKit
    import Vision
    
    public func getTestReceiptImageName(_ number: Int) -> String {
        String.init(format: "%d-receipt.jpg", number)
    }
    
    //https://www.udemy.com/course/machine-learning-with-core-ml-2-and-swift
    public func visualization(_ image: UIImage, observations: [VNDetectedObjectObservation]) -> UIImage {
        var transform = CGAffineTransform.identity
            .scaledBy(x: 1, y: -1)
            .translatedBy(x: 1, y: -image.size.height)
        transform = transform.scaledBy(x: image.size.width, y: image.size.height)
    
        UIGraphicsBeginImageContextWithOptions(image.size, true, 0.0)
        let context = UIGraphicsGetCurrentContext()
    
        image.draw(in: CGRect(origin: .zero, size: image.size))
        context?.saveGState()
    
        context?.setLineWidth(2)
        context?.setLineJoin(CGLineJoin.round)
        context?.setStrokeColor(UIColor.black.cgColor)
        context?.setFillColor(red: 0, green: 1, blue: 0, alpha: 0.3)
    
        observations.forEach { observation in
            let bounds = observation.boundingBox.applying(transform)
            context?.addRect(bounds)
        }
    
        context?.drawPath(using: CGPathDrawingMode.fillStroke)
        context?.restoreGState()
        let resultImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return resultImage!
    }

     

    Vision Framework

    A Vision framework enables me to detect text in the receipt image. Also, I can get strings after processing the OCR.

    Step 1. Load a receipt image from Resource folder

    //Step 1. Load a receipt image from Resource folder
    import Vision
    import Cocoa
    
    let image = NSImage(imageLiteralResourceName: getTestReceiptImageName(1000))

     

    Step 2. Declare a VNRecognizeTextRequest to detect text in the receipt image

    let recognizeTextRequest = VNRecognizeTextRequest  { (request, error) in
        guard let observations = request.results as? [VNRecognizedTextObservation] else {
            print("Error: \(error! as NSError)")
            return
        }
        for currentObservation in observations {
            let topCandidate = currentObservation.topCandidates(1)
            if let recognizedText = topCandidate.first {
                //OCR Results
                print(recognizedText.string)
            }
        }
        let fillColor: NSColor = NSColor.green.withAlphaComponent(0.3)
        let result = visualization(image, observations: observations, boundingBoxColor: fillColor)
    }
    recognizeTextRequest.recognitionLevel = .accurate

     

    Step 3. Processing the receipt image using VNImageRequestHandler.

    func request(_ image: NSImage) {
        guard let cgImage = image.cgImage else {
            return
        }
        let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                try handler.perform([recognizeTextRequest])
            }
            catch let error as NSError {
                print("Failed: \(error)")
            }
        }
    }
    request(image)

     

    Step 4. Check the result

    To open the result image, click the QuickLook.

    To open the result image, click the QuickLook.

    Results.png

     

    Can Vision Framework detect other languages?

    Yes, But It supports a few languages. Take a look at the supported languages.

    let supportedLanguages = try VNRecognizeTextRequest.supportedRecognitionLanguages(for: .accurate, revision: VNRecognizeTextRequestRevision2)
    print("\(supportedLanguages.count) Languages are available. -> \(supportedLanguages)")
    
    //8 Languages are available. -> ["en-US", "fr-FR", "it-IT", "de-DE", "es-ES", "pt-BR", "zh-Hans", "zh-Hant"]

     

    NaturalLanguage framework

    You can detect the lexical class of string using the NaturalLanguage framework. I modified step2 codes to print out the tag of string.

    import NaturalLanguage
    let tagger = NLTagger(tagSchemes: [.nameTypeOrLexicalClass])
    let recognizeTextRequest = VNRecognizeTextRequest  { (request, error) in
        guard let observations = request.results as? [VNRecognizedTextObservation] else {
            print("Error: \(error! as NSError)")
            return
        }
        let ocrResults = observations.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n")
        tagger.string = ocrResults
        tagger.enumerateTags(in: ocrResults.startIndex..<ocrResults.endIndex, unit: NLTokenUnit.word, scheme: NLTagScheme.nameTypeOrLexicalClass, options: [.omitPunctuation, .omitWhitespace]) { tag, range in
            print("Tag: \(tag?.rawValue ?? "unknown") -> \(ocrResults[range])")
            return true
        }
    
        let fillColor: NSColor = NSColor.green.withAlphaComponent(0.3)
        let result = visualization(image, observations: observations, boundingBoxColor: fillColor)
    }
    recognizeTextRequest.recognitionLevel = .accurate

    It gives me more information about Noun, Number, PlaceName, PersonalName, and more.

    It gives me more information about Noun, Number, PlaceName, PersonalName, and more.

     

    Conclusion

    I’m very impressed with the Vision framework. I can implement the OCR features in just a few lines of code. Also, It works on the device. (We don’t need to connect to the internet)

    I hope Apple supports more languages like Korean, Japanese, Thailand, and more.

  • UIViewController Lifecycle Explained – loadView, viewDidLoad, viewWillAppear and More

    UIViewController Lifecycle Explained – loadView, viewDidLoad, viewWillAppear and More

    In this post, I’ll explain the life cycle of UIViewController.

    What happens when you create the UIViewController?

    1. loadView (only once called during the entire life cycles)

    2. viewDidLoad (only once called during the entire life cycles)

    3. viewWillAppear

    4. viewIsAppearing
    5. viewWillLayoutSubviews

    6. viewDidLayoutSubviews

    7. viewDidAppear

    And what happens when you dismiss or destroy the UIViewController?

    1. viewWillDisappear

    2. viewDidDisappear

    There are many steps of UIViewControllers. Let’s take a look loadView method.

    loadView

    Every view controller which subclassing the UIViewController has the view. It is the root view of the view controller. The default value is nil, and loadView is called when a view is a nil.

    There are three ways to initialize the UIViewControllers.

    1. Use Nib

    2. Use Storyboard

    3. Use Code

    You take care of loadView methods. If your view controller uses the Nib or Storyboard, then you should not override this method.

    Why?

    As I mentioned, You can initialize the UIViewController in different ways. If you use the Nib or Storyboard, the view will be set when loaded from Nib or Storyboard objects.

    Nib

    The init(nibName:bundle:) will set the view by loading the nib file.

    Storyboard

    The initiateViewController(withIdentifier:) will set the view. (UIStoryboard object create the view controller and return it to your code)

    Code

    You should override the loadView method to set the root view of the view controller. You can also put subviews into the root view on this method. Don’t call a super.loadView in the override loadView method.

    override func loadView() {
         let rootView = UIView()
         rootView.backgroundColor = .blue
         view = rootView
    
         //You can also add some more subviews into the view
     }

    ViewDidLoad

    It is called when the view controller’s view hierarchy is loaded into the memory.

    If you are using Storyboard or Nib, You can add, remove subviews on ViewDidLoad methods.

    Apple Document:

    Use this method to add or remove views, modify layout constraints, and load data for your views.

    viewWillAppear

    This method is called before configuring any animations and adding to the view hierarchy to display. You can set the style of view or set or change the orientation of the status bar. Call a super.viewWillAppear is required.

    viewWillLayoutSubviews

    When the view controller’s view is changed in its layout or bounds, then the system will call a viewWillLayoutSubviews. Compare to viewDidAppear; It will be automatically called when the view’s frame or bound changed, which means it can be called several times. On the other hand, viewDidAppear called once when the view controller’s view shows on the screen.

    roateView.gif

    Apple Document:

    Your view controller can override this method to make changes before the view lays out its subviews. The default implementation of this method does nothing.

    viewDidLayoutSubviews

    This method is called when after layout subViews. It means that the view is updated and lays out their bounds and frames. You can override this method if you need some implementation when after updated views. And You don’t need to call a super.viewDidLayoutSubviews because the default implementation does nothing.

    Apple Document:

    When the bounds change for a view controller’s view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view’s subviews have been adjusted. Each subview is responsible for adjusting its own layout.

    updateViewConstraints

    If you use autolayout, You can override this method to update the view’s constraints. This method will be called when you call a view.setNeedsUpdateConstraints().

    What happens when you call a setNeedsUpdateConstraint?

    1. viewWillLayoutSubviews

    2. updateViewConstraints

    3. viewDidLayoutSubviews

    Here are examples

    @IBOutlet weak var buttonTopConstraint: NSLayoutConstraint!
    
    override func updateViewConstraints() {
        //Implement your code in here
        buttonTopConstraint.constant = 100
        //You should call a super.updateViewConstraints at final step
        super.updateViewConstraints()
    }

    Hmm, I watched the WWDC 15 video about this. I’m not sure should I need to override this function or not. Apple also recommends setting constraints in a place when you need to change it. (e.g., When tapped the button then update the constraint, set a constraint in an IBAction method) If you are facing a performance problem to update the constraints, then you can override updateViewConstraints.

    Apple Document:

    Override this method to optimize changes to your constraints.

    It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. For example, if you want to change a constraint in response to a button tap, make that change directly in the button’s action method.

    You should only override this method when changing constraints in place is too slow, or when a view is producing a number of redundant changes.

    Your implementation must be as efficient as possible. Do not deactivate all your constraints, then reactivate the ones you need. Instead, your app must have some way of tracking your constraints, and validating them during each update pass. Only change items that need to be changed. During each update pass, you must ensure that you have the appropriate constraints for the app’s current state.

    viewDidAppear

    It is called when after all of the views in view controller are onScreen. Call a super.viewDidAppear is required.

    viewWillDisappear

    It is called the view being removed from the view hierarchy. You can use this function to resign the responder or revert the orientation or style of view set on viewWillAppear or viewDidAppear. Call a super.viewWillDisappear is required.

    viewDidDisapper

    It is called the view was removed from the view hierarchy. Use this function if you need some tasks when dismissing the view controller. Call a super.viewDidDisappear is required.

    References

    Apple Document, View Controllers

    Apple Document, View Controller Programming Guide for iOS

    WWDC 2015, Mysteries of Autolayout Part 2

  • iOS Tip Calculator Tutorial – UIKit and Storyboards Step-by-Step Guide

    iOS Tip Calculator Tutorial – UIKit and Storyboards Step-by-Step Guide

    In this tutorial, you’ll learn how to separate logics from view and to deal with user input on UITextField by making the calculator app. In this project, users can input the bill and tip, and it automatically displays the result of the total amount and tip amount when entering the bill or tip. You can also learn how to view the number of human-friendly formats.

    In the process, you’ll learn:

    • Designing the calculator app using storyboard and xib

    • Rendering the xib in storyboard by using IBDesignable and IBInspectable

    • Dealing with input using the UITextFieldDelegate and NumberFormatter

    • Reflect the result of the total amount and tip amount using the protocol.

    Key Topics

    • Storyboard

    • IBOutlet / IBAction

    • Delegate Pattern

    • NumberFormatter

     

    Designing the calculator app using storyboard and xib

    This calculator app has four components, which are two input fields and two result views. As you can see, the elements look similar. Isn’t it? It consists of UILabel, UITextField, and UIStepper. Ok, making the CustomView for a component using xib is the right choice rather than layout the all separated UILable, UITextField, and UIStepper 4 times into the Storyboard.

    Let’s create our InputView.

    ⌘ + N -> Source -> Swift File

    ⌘ + N -> User Interface -> View

    We created the Swift and Xib File. Let’s set up xib.

    widthConstraint.png

    We use the StackView to layout the item horizontally. The UILabel is set by width constraint relative to SuperView’s width and multiply the 0.15, which means UILabel’s width will place 15% of SuperView’s width.

    Look at Placeholders. The File’s Owner is pointing to InputView. The IBOutlet and IBAction are also linked to the InputView class.&nbsp;

    Look at Placeholders. The File’s Owner is pointing to InputView. The IBOutlet and IBAction are also linked to the InputView class. 

    InputViewClass.png

    In the InputView.xib, open the Assistant for linking IBOutlet and the IBAction. When you link between xib component and InputView.swift, you will see the result like the image below.

    IBOutlet.png

    We almost did for setting InputView. Let’s write the code.

    import UIKit
    
    @IBDesignable
    final class InputView: UIView {
        @IBOutlet weak var titleLabel: UILabel!
        @IBOutlet weak var inputTextField: UITextField!
        @IBOutlet weak var tipStepper: UIStepper!
    
        override func prepareForInterfaceBuilder() {
            super.prepareForInterfaceBuilder()
        }
    
    //It called when you are using the xib for creating it.
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            setupView()
        }
    
    //It called when you create view programmatically.
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupView()
        }
    
        private func setupView() {
            guard let nib = loadNib() else {
                return
            }
            nib.translatesAutoresizingMaskIntoConstraints = false
            addSubview(nib)
            NSLayoutConstraint.activate([
                nib.leadingAnchor.constraint(equalTo: self.leadingAnchor),
                nib.trailingAnchor.constraint(equalTo: self.trailingAnchor),
                nib.topAnchor.constraint(equalTo: self.topAnchor),
                nib.bottomAnchor.constraint(equalTo: self.bottomAnchor)
            ])
        }
    
        private func loadNib() -> UIView? {
            let bundle = Bundle(for: Self.self)
            return bundle.loadNibNamed(String(describing: Self.self), owner: self, options: nil)?.first as? UIView
        }
    
        @IBAction func didUpdateStepper(_ sender: UIStepper) {
            inputTextField.becomeFirstResponder()
            inputFormatter?.updateTipValue(inputTextField, value: sender.value)
        }
    }

    In the InputView.swift, We have two init function which is mandatory for making subView. To set subViews, we should load xib from Bundle, which organizes the code and resources in the Bundle directory. Then add subView into the InputView using autolayout.

    We use the @IBDesignable to load view on Storyboard. If not, the Storyboard can’t load our view. The @IBAction is aiming for handling the event like a tap, touchdown, and user actions. We linked the action from UIStepper to handle value changed event. 

     

    Rendering the xib in storyboard by using IBDesignable and IBInspectable

    As we already set @IBDesignable in InputView for displaying the view on the Storyboard. Let’s open the Storyboard to set the views.

    storyboard.png

    You can see our custom UIView called InputView is rendered on the Storyboard. Now I’ll explain the @IBInspectable for setting details on the Storyboard.

    Open the InputView.swift and add the @IBInspectable.

    @IBInspectable var title: String? = nil {
            didSet {
                titleLabel.text = title
            }
        }
    
        @IBInspectable var placeHolder: String? = nil {
            didSet {
                inputTextField.placeholder = placeHolder
            }
        }
    
        @IBInspectable var editable: Bool = true {
            didSet {
                inputTextField.isEnabled = editable
            }
        }
    
        @IBInspectable var tipInput: Bool = false {
            didSet {
                if tipInput {
                    tipStepper.isHidden = false
                }
            }
        }

    After adding the @IBInspectable var in InputView.swift, then open the Storyboard and check the InputView component.

    We added the four @IBInspectable, and you can set the values on the Storyboard. When you set the value, then It will reflect the value and rendering again.

    We added the four @IBInspectable, and you can set the values on the Storyboard. When you set the value, then It will reflect the value and rendering again.

     

    Dealing with input using the UITextFieldDelegate and NumberFormatter

    We will create the InputFormatter class to handle input from the keypad. To separating the logic from View, It handles the InputView’s UITextField event bypassing the delegate. 

    class InputFormatter: NSObject {
        enum InputType {
            case tip
            case bill
        }
    
        private var inputType: InputType = .bill
    
        private lazy var formatter: NumberFormatter = {
            let formatter = NumberFormatter()
            formatter.maximumFractionDigits = 0
            if self.inputType == .bill {
                formatter.numberStyle = .currency
                formatter.currencyCode = Locale(identifier: "en-US").currencyCode
            }
            else if self.inputType == .tip {
                formatter.numberStyle = .percent
                formatter.multiplier = 1
            }
            return formatter
        }()
    
        override init() {
            super.init()
        }
    
        convenience init(type: InputType) {
            self.init()
            inputType = type
        }
    
        private func setupKeyboardType(_ textField: UITextField) {
            textField.keyboardType = .numberPad
        }
    }
    
    extension InputFormatter: UITextFieldDelegate {    
        func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
            if textField.keyboardType == .default {
                setupKeyboardType(textField)
            }
            return true
        }
    
        func textField(
            _ textField: UITextField,
            shouldChangeCharactersIn range: NSRange,
            replacementString string: String
        ) -> Bool {
            guard let text = textField.text else {
                return true
            }
            if let formattedString = convertFormattedString(text, replacementString: string) {
                textField.text = formattedString
            }
            return false
        }
    }
    }

    This class has convenience init to handle inputType, whether tip or bill. For the tip, the NumberFormatter’s number style is percent to display the number with percentage symbol. For the bill, It will display the number with a currency symbol.

    Look at the UITextFieldDelegate functions. I handle the input stream from the number keypad and convert it for a human-readable format using the convertFormattedString function, which uses the NumberFormatter. (I attached full source code in this article. You can check it the function)

    func textField(
            _ textField: UITextField,
            shouldChangeCharactersIn range: NSRange,
            replacementString string: String
        ) -> Bool

    Let’s take a look above function. In this function, we can check the input string from the ‘replacementString’ parameter. It will be number because we set the keyboard type as numberPad. To use NumberFormatter to convert all input, we should return false to set the textField.text. 

    class ViewController: UIViewController {    
        @IBOutlet weak var resultStackView: UIStackView!
        @IBOutlet weak var resultTitleLabel: UILabel!
        @IBOutlet weak var tipResultView: InputView!
        @IBOutlet weak var totalResultView: InputView!
    
        @IBOutlet weak var inputStackView: UIStackView!
        @IBOutlet weak var inputTitleLabel: UILabel!
        @IBOutlet weak var billInputView: InputView!
        @IBOutlet weak var tipInputView: InputView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            resultStackView.setCustomSpacing(40, after: resultTitleLabel)
            inputStackView.setCustomSpacing(40, after: inputTitleLabel)
            billInputView.delegate = InputFormatter(type: .bill)
            tipInputView.delegate = InputFormatter(type: .tip)
        }
    }

    The ViewController code is pretty simple. You can just set the delegate in viewDidLoad.

     

    Reflect the result of the total amount and tip amount using the protocol

    app.png

    We are almost done. The final step reflects the result of an amount when entered bill or tip automatically. To do that, let’s create an AmountPresentable protocol and InputFormatterDelegate.

    protocol InputFormatterDelegate: AnyObject {
        func didUpdateInput(_ type: InputFormatter.InputType, number: NSNumber)
    }
    
    class InputFormatter: NSObject {
        private weak var delegate: InputFormatterDelegate?
    
    }

    We added the InputFormatterDelegate in InputFormatter and then conformed it in ViewController. It will let ViewController when detecting a typing event.

    To calculate the total amount and tip amount, We add AmountPresentable protocol.

    protocol AmountPresentable {
        var billValue: NSNumber { get }
        var tipValue: NSNumber { get }
        var tipPercentage: NSNumber { get }
        var formatter: NumberFormatter { get }
    
        func updateTipAmount(assign to: InputView)
        func updateTotalAmount(assign to: InputView)
        func updateStepperValue(assign to: InputView)
    }
    
    extension AmountPresentable {
        func updateTipAmount(assign to: InputView) {
            let tipAmount = formatter.string(from: self.tipValue)
            to.inputTextField.text = tipAmount
        }
    
        func updateStepperValue(assign to: InputView) {
            to.tipStepper.value = tipPercentage.doubleValue
        }
    
        func updateTotalAmount(assign to: InputView) {
            let totalValue = billValue.doubleValue + tipValue.doubleValue
            let totalAmount = formatter.string(from: NSNumber(value: totalValue))
            to.inputTextField.text = totalAmount
        }
    }

    This protocol binds to InputView to display the amount. It is called in the ViewController in the didUpdateAmount function. Let’s conform to the AmountPresentable on ViewController.

    class ViewController: UIViewController, AmountPresentable {
        var billValue: NSNumber = 0
        var tipValue: NSNumber = 0
        var tipPercentage: NSNumber = 0
        //…
    
        override func viewDidLoad() {
            super.viewDidLoad()
        //…
            billInputView.delegate = InputFormatter(type: .bill, delegate: self)
            tipInputView.delegate = InputFormatter(type: .tip, delegate: self)
        }
    }
    
    extension ViewController: InputFormatterDelegate {
        func didUpdateInput(_ type: InputFormatter.InputType, number: NSNumber) {
            if type == .tip {
                tipValue = NSNumber(value: billValue.doubleValue * (number.doubleValue * 0.01))
                tipPercentage = number
            }
            else if type == .bill {
                billValue = number
                tipValue = NSNumber(value: billValue.doubleValue * (tipPercentage.doubleValue * 0.01))
            }
            updateTipAmount(assign: tipResultView)
            updateTotalAmount(assign: totalResultView)
            updateStepperValue(assign: tipInputView)
        }
    }

    That’s all. When received the didUpdateInput function, then update the amount by using AmountPresentable. It enables us to reflect the amount during the enter the bill or tip.

     

    Conclusion

    We learned how to use Storyboard and Xib, UITextField, and Delegate patterns. We can improve our code logic if we use the combine framework, but It only supports iOS 13.0. If you understand this project and how delegation and protocol works, you can also quickly adapt the combined framework. I hope you enjoy this post. Thank you!

  • Best English Resources for Software Engineers – Books, Lectures, and Services

    Best English Resources for Software Engineers – Books, Lectures, and Services

    In this post, I’ll share useful English resources for Software Engineer. I struggled to express my thought to someone. It’s always challenging me. I decided to find English books focused on the Software Industry because There are many useful sentences for software engineers. It helps me a lot and saves me time.

    English Books for Software Engineer

    Top 50 Software Engineer Personal Interview Questions & Answers by Knowledge Powerhouse

    knowledge powerhouse cover
    Top 50 Software Engineer Personal Interview Questions & Answers

    I highly recommend this book. In this book, there are great examples of answers to personal interview questions. You can pick a sentence and rephrase it for your stories.

    Cracking the Behavioral Interviews: for Software Engineers by Pooya Amini

    Cracking the Behavioral Interviews cover
    Cracking the Behavioral Interviews: for Software Engineers

    It’s similar to Knowledge Powerhouse. The author is a co-founder of TechMockInterview.com. In this book, 28 questions and answers. The answers are very detailed and useful sentences to express your software engineer careers.

    Software Engineering by Express Publishing

    Express Publishing published a series of career paths. It’s one of the career path books focused on the Software Engineering domain. I highly recommend it. It looks like side by side book. There are many useful chapters to learn Business English for Software Engineer.

    Udemy Lectures

    Pass your job interview in English: Improve your English for job interviews by Terry

    Pass your job interview in English by Terry
    Udemy — Job interview English (Terry)

    I highly recommend it. You can learn how to use STAR(Situation, Tasks, Action, Result) strategies to express your story in this course. Also, Terry answered my questions very friendly.

    Business English listening training – English for companies by Terry

    Business English listening training by Terry
    Udemy — Business English listening training

    Unlike Pass your job interview in English, It’s more focused on business English for daily life in the office. I learnt how to communicate with colleagues during meetings, business trips, etc.

    Services

    Grammarly

    Grammarly screenshot
    Grammarly

    As you may know, Grammarly is such a powerful tool for correcting sentences. I use it every day in my work.

    PRAMP

    PRAMP screenshot
    PRAMP — mock interviews for software engineers

    PRAMP is a free service for practicing a mock interview for software engineers. You can not only practicing coding interviews but also behavior interviews. There is no limit to practices. You can also get feedback from your interviewer for free.

    Learn English from Conference

    There are many conferences for Software Engineers. Pick your favorite conference and watch and repeat it a lot. You can learn how to give a speech to your thought.

    WWDC

    WWDC app screenshot
    Apple WWDC app — videos with transcripts

    As an iOS engineer, WWDC is such a great conference in the year for me. Apple delivered a WWDC application. In this app, You can watch all the videos, and it provides the transcriptions!

    Conclusion

    For ESL software engineer, English is the most challenging. But I have to learn English to expand my career in the tech industries. I’m a native Korean and living in Seoul for over 30 years. I have never lived outside of Korea. Although it is challenging to access English, as I introduced you, I believe that you can learn English if you have the will through various media. Even if I am not good at English, I continue to run blogs in English to expand my career and to communicate with English-speaking engineers by sharing my knowledge.

  • 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.

  • How can I get a formatted address from CLPlacemark?

    How can I get a formatted address from CLPlacemark?

    This post is concise, but useful to represent the formatted address from CLPlacemark. The CoreLocation provides the location information called CLPlacemark, which is similar to Google’s Geocoding API.

    CLPlacemark has location pieces of information like a region, time zone, name, thoroughfare, country, etc. Although it supports these pieces of information, You may not find the formatted address property in CLPlacemark. You can get a formatted address using the Contact framework. Let’s make it possible.

    Extension CLPlacemark

    CNPostalAddressFormatter handles the international format of the postal address. It’s so easy.

    import CoreLocation
    import Contacts
    
    extension CLPlacemark {
        var formattedAddress: String? {
            guard let postalAddress = postalAddress else {
                return nil
            }
            let formatter = CNPostalAddressFormatter()
            return formatter.string(from: postalAddress)
        }
    }

    Conclusion

    Before knowing the CNPostalAddressFormatter, I built the formatted address by checking the region, country, etc. Apple’s built-in formatter is cool. You can easily represent the human-readable data not only addresses but also measurements.

  • How to get the date you want using the Calendar

    How to get the date you want using the Calendar

    In this post, I’ll write about Calendar to get the date you want.

    You can get the specific time and dates like midnight, noon, tomorrow, the start of this week, and more!

     

    WeekDay

    You can get a weekday from Calendar. I’ll use it to get the startOfThisWeek and beforeStartOfBiWeek.

    enum CalendarWeekDay: Int {
        case sun = 1
        case mon
        case tue
        case wed
        case thu
        case fri
        case sat
    }
    
    extension CalendarWeekDay {
        func startOfWeek(_ weekDay: CalendarWeekDay = .sun) -> Int? {
            guard weekDay == .sun || weekDay == .mon else {
                return nil
            }
            if weekDay == .sun {
                return (self.rawValue - 1) * -1
            }
            else {
                switch self {
                case .sun:
                    return -6
                default:
                    return (self.rawValue - 2) * -1
                }
            }
        }
    
        var beforeStartOfBiWeek: Int {
            startOfWeek(.sun)! - 7
        }
    }
    

     

    Date

    I set the start of the day to midnight(AM 00:00:00) and the end of the day to military time(PM 23:59:59).

    extension Date {
        var startOfDay: Date {
            return Calendar.current.date(
            byAdding: .day, 
            value: 0, 
            to: midNight)!
        }
    
        var yesterday: Date {
            return Calendar.current.date(
            byAdding: .day, 
            value: -1, 
            to: midNight)!
        }
    
        var tomorrow: Date {
            return Calendar.current.date(
            byAdding: .day, 
            value: 1, 
            to: midNight)!
        }
    
        var dayAfterTomorrow: Date {
            return Calendar.current.date(
            byAdding: .day, 
            value: 2, to: midNight)!
        }
    
        var midNight: Date {
            Calendar.current.date(
            bySettingHour: 00, 
            minute: 0, 
            second: 0, 
            of: self)!
        }
    
        var militaryTime: Date {
            Calendar.current.date(
            bySettingHour: 23, 
            minute: 59, 
            second: 59, of: self)!
        }
    
        var noon: Date {
            Calendar.current.date(
            bySettingHour: 12, 
            minute: 0, 
            second: 0, 
            of: self)!
        }
    
        var month: Int {
            Calendar.current.component(.month,  from: self)
        }
    
        var isLastDayOfMonth: Bool {
            tomorrow.month != month
        }
    
        var startOfThisWeek: Date {
            let weekDay = CalendarWeekDay(
                rawValue: Calendar.current.component(.weekday, from: self)
            )!
            return Calendar.current.date(
                byAdding: .day,
                value: weekDay.startOfWeek(.sun)!,
                to: startOfDay)!
        }
    
        var beforeStartOfBiWeek: Date {
            let weekDay = CalendarWeekDay(
                rawValue: Calendar.current.component(
                .weekday, 
                from: self)
            )!
            return Calendar.current.date(
            byAdding: .day, 
            value: weekDay.beforeStartOfBiWeek, 
            to: startOfDay)!
        }
    
        var startOfMonth: Date {
            return Calendar.current.date(
                from: Calendar.current.dateComponents(
                    [.year, .month],
                    from: Calendar.current.startOfDay(for: self)
                ))!
        }
    
        var endOfMonth: Date {
            return Calendar.current.date(
                byAdding: DateComponents(month: 1, day: -1),
                to: startOfMonth.militaryTime
            )!
        }
    }
    

    All done! Let’s make a test code.

    The default timeZone is UTC. I recommend setting your timeZone before checking test results.

    import XCTest
    @testable import CalendarSample
    
    fileprivate var dateformatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy.MM.dd ahh:mm"
        formatter.locale = Locale(identifier: "en_KR")
        formatter.calendar = Calendar.current
        return formatter
    }
    class CalendarTests: XCTestCase {
        func test_start_of_week_and_biweek() {
            let sunday = dateformatter.date(from: "2020.07.12 AM00:00")!
            let monday = dateformatter.date(from: "2020.07.13 AM00:00")!
            let tuesday = dateformatter.date(from: "2020.07.14 AM00:00")!
            let wednesday = dateformatter.date(from: "2020.07.15 AM00:00")!
            let thursday = dateformatter.date(from: "2020.07.16 AM00:00")!
            let friday = dateformatter.date(from: "2020.07.17 AM00:00")!
            let saturday = dateformatter.date(from: "2020.07.18 AM00:00")!
    
            XCTAssertEqual(sunday.startOfThisWeek.KST, "2020.07.12 AM12:00")
            XCTAssertEqual(sunday.beforeStartOfBiWeek.KST, "2020.07.05 AM12:00")
    
            XCTAssertEqual(monday.startOfThisWeek.KST, "2020.07.12 AM12:00")
            XCTAssertEqual(monday.beforeStartOfBiWeek.KST, "2020.07.05 AM12:00")
    
            XCTAssertEqual(tuesday.startOfThisWeek.KST, "2020.07.12 AM12:00")
            XCTAssertEqual(tuesday.beforeStartOfBiWeek.KST, "2020.07.05 AM12:00")
    
            XCTAssertEqual(wednesday.startOfThisWeek.KST, "2020.07.12 AM12:00")
            XCTAssertEqual(wednesday.beforeStartOfBiWeek.KST, "2020.07.05 AM12:00")
    
            XCTAssertEqual(thursday.startOfThisWeek.KST, "2020.07.12 AM12:00")
            XCTAssertEqual(thursday.beforeStartOfBiWeek.KST, "2020.07.05 AM12:00")
    
            XCTAssertEqual(friday.startOfThisWeek.KST, "2020.07.12 AM12:00")
            XCTAssertEqual(friday.beforeStartOfBiWeek.KST, "2020.07.05 AM12:00")
    
            XCTAssertEqual(saturday.startOfThisWeek.KST, "2020.07.12 AM12:00")
            XCTAssertEqual(saturday.beforeStartOfBiWeek.KST, "2020.07.05 AM12:00")
        }
    
        func test_get_specific_from_date() {
            let sunday = dateformatter.date(from: "2020.07.12 AM00:00")!
    
            XCTAssertEqual(sunday.midNight.KST, "2020.07.12 AM12:00")
            XCTAssertEqual(sunday.noon.KST, "2020.07.12 PM12:00")
            XCTAssertEqual(sunday.militaryTime.KST, "2020.07.12 PM11:59")
    
            XCTAssertEqual(sunday.yesterday.KST, "2020.07.11 AM12:00")
            XCTAssertEqual(sunday.tomorrow.KST, "2020.07.13 AM12:00")
            XCTAssertEqual(sunday.dayAfterTomorrow.KST, "2020.07.14 AM12:00")
        }
    
        func test_check_month_from_date() {
            let sunday = dateformatter.date(from: "2020.07.12 AM00:00")!
            let lastDayOnJuly = dateformatter.date(from: "2020.07.31 AM00:00")!
    
            XCTAssertEqual(sunday.startOfMonth.KST, "2020.07.01 AM12:00")
            XCTAssertEqual(sunday.endOfMonth.KST, "2020.07.31 PM11:59")
    
            XCTAssertEqual(sunday.isLastDayOfMonth, false)
            XCTAssertEqual(lastDayOnJuly.isLastDayOfMonth, true)
        }
    }
    
    fileprivate extension Date {
        //Set your timeZone
        var KST: String {
            dateformatter.timeZone = TimeZone(abbreviation: "KST")
            return dateformatter.string(from: self)
        }
    }
    

    test1.png

    Done! 😎

    Done! 😎

     

    Thanks for reading my post. Next time I’ll write about CoreData. If you have any questions, please leave a comment here.