Showing posts with label Swift. Show all posts
Showing posts with label Swift. Show all posts

Monday, March 4, 2024

Swift Rule Engine using Javascript for your iOS/MacOS application


This kind of workarounds are required when we want dynamic validation or to run some algorithm dynamically at runtime. We can download the business logic [as javascript text or file] from the server or can add to the bundle and can load and execute when it's needed.

In this way, we can keep the core business logic must be the same over multiple platform and can control without updating the application.

Form your app, you can pass values as parameters and the script can evaluate it and can return the result as plain text or JSON text if needed. 

First load the script using scripRunner.loadWebViewWithScript(script), and we can invoke a function using scripRunner.evaluateFunction(fnName: "function")


See the example code of passing a contact object to a script function and that function validate the contact object and returning a json object with true or false value.



The following  JavaScript handler file will help us to do the tasks easier.


As we are using async/await instead of completion handlers, we can evaluate a js function immediately after loading the html content. 
In this example, the loading of script content is done with onAppear and evaluating on the button click. In this way, we can do the load once and evaluate any times.

Tuesday, February 27, 2024

Swift async/await - Sample API module with Unit test






In this article, we are adopting Swift concurrency in URLSession using async/await and trying to make a sample API module with Unit tests. We have tried to keep the code as minimal. 

As we keep minimal code, we considered only data task with url request. [we have not considered download/upload task, incremental-download, and task delegates] 

To create this module, we have two options, either can choose framework or using package. As this is project specific, framework is also fine. But here we are making new swift package for the API module.


Create Swift Package

In Xcode Menu, choose New -> Package from file menu.

In this example, given package name is 'APIModule'. It will automatically create a test target APIModuleTests for testing. 

Create new class HttpClient for URLSession data task

As it need to access outside from the package, we set public access. 

It shows some warning:
We have to specify the supported platform and version in the Package.swift file. Select the same file and add 'Platform' script.
platforms: [
     .iOS(.v13),
     .macOS(.v12)
]
To consume the session tasks, we need url requests, for example assume our app have a feature to fetch the contacts from server. 
Create new folder 'Contacts', under this create fie 'ContactAPI'. 

We need url request for each endpoint, so create an enum 'ContactEndPoint' to make urlrequest for each endpoint.      
Set public access because we need to access this features from outside the package when integrated to our iOS/MacOS project. 

Next we have to parse the API response. So let's create a DataMapper to convert API response to desired array of Contacts. We also need a Contact Model, And custom error to catch the parse errors.
 Also modify the getMyContacts() func.
  

public func getMyContacts() async throws -> [Contact] {

    let contactRequest = ContactEndPoint.myContacts.url(baseURL: baseURL)

    let (data, httpResponse)  = try await client.performRequest(contactRequest)

    return try ContactDataMapper.map(data, from: httpResponse)

}

Create New Unit Test Case


Create new Unit Test Case class file for testing ContactAPIs 

Select 'APIModuleTests' folder, and create new 'Unit Test Case' file named 'ContactAPITests' 
Add "import APIModule" statement. we don't need @testable here as we don't need level up the access.  

For the scheme 'APIModule, choose Mac instead of iOS device to fasten the test execution. 




To run a single test, click on the gutter button next to the function declaration.

To run all tests in a specific test case class, click on the gutter button next to the test case class declaration.  

To run all the tests within the targets in the selected scheme, we can use the default keyboard shortcut set by Xcode: CMD+U. 

To run the tests with xcodebuild (CLI), through the command line, using the following format. 

xcodebuild test -project [project name].xcodeproj -scheme “[scheme name]” 

 For example: $ xcodebuild test -scheme "APIModule" -destination 'platform=OS X,arch=x86_64' 

If it shows Command not found error, check the command line tools are installed or not. You can check the installed path in Xcode Settings, Locations tab.

If it installed, try the full path,
  /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild test -scheme "APIModule" -destination 'platform=OS X,arch=x86_64' 

Create protocol HTTPURLSession


Before starting test cases, we need protocol version of session datatask, otherwise we will endup using subclass of HttpClient. 

Create a protocol

public protocol HTTPURLSession {

    func data(for request: URLRequestasync throws -> (data: Data, response: HTTPURLResponse)

}


And in the HttpClient class, change the type of session variable as HTTPURLSession.  

----------------------------

We can start testing by spying our ContactAPIs. 

Create a class (HTTPURLSessionSpy) by implementing the HttpURLSession protocol, and we can use this for our unit testing.

 1. test_init_doesNotExecuteURLRequest() 

 We are testing the making of SUT instance doesn't request any urls. 

func test_init_doesNotExecuteURLRequest() async {

        let (_, session) = makeSUT()

        let executedUrls = session.executedURLs

        XCTAssertTrue(executedUrls.isEmpty)

    }


2. test_deliverConnectivityErrorOnClientError

Make sure it returns connectivity error if no response.

    func test_deliverConnectivityErrorOnClientError() async throws {

        let (sut, _) = makeSUT()

        do {

            _ = try await sut.getMyContacts()

        } catch let error {

            XCTAssertEqual((error as? APIError), APIError.connectivity)

            return

        }

        XCTFail()

    }


3. func test_deliverErrorOnInvalidJSONWith200Status()

Make sure the function return InvalidData error if the response data is unable to parse.

To validate the status code from URLResponse, we have to cast the URLResponse to HTTPURLResponse.

So changing the URLResponse type in all sections to HTTPURLResponse. And check Status Code is 200 or not.

    func test_deliverErrorOnInvalidJSONWith200Status() async throws {

        let url = URL(string: "https://aurl.com")!

        let (sut, session) = makeSUT(url: url)

        

        let myContactURL = ContactEndPoint.myContacts.url(baseURL: url).url!

        let invalidJSON = """

[

{"contactsssss_ID" : 2 }

]

"""

        session.setResponse((invalidJSON.data(using: .utf8)!, responseWithStatusCode(200, url: myContactURL)), for: myContactURL)

        

        do {

            _ = try await sut.getMyContacts()

        } catch let error {

            XCTAssertEqual((error as? APIError), APIError.invalidData)

            return

        }

        XCTFail()

    }


4. test_load_DeliverErroFor400Status

Make sure the function fails if the response status code is other than 200..300

    func test_load_DeliverErroFor400Status() async throws {

        let url = URL(string: "https://aurl.com")!

        let (sut, session) = makeSUT(url: url)


        let myContactURL = ContactEndPoint.myContacts.url(baseURL: url).url!

        session.setResponse(("".data(using: .utf8)!, responseWithStatusCode(400, url: url)), for: myContactURL)

        

        do {

            _ = try await sut.getMyContacts()

        } catch let error {

            XCTAssertEqual((error as? APIError), APIError.serverDefined("400"))

            return

        }

        XCTFail()

    }


5. test_load_deliversSuccessWith200HTTPResponseWithJSONItems

    func test_load_deliversSuccessWith200HTTPResponseWithJSONItems() async throws {

        let url = URL(string: "https://aurl.com")!

        let (sut, session) = makeSUT(url: url)


        let validJSON = """

[

{"contact_ID" : 2 }

]

"""

        let myContactURL = ContactEndPoint.myContacts.url(baseURL: url).url!

        session.setResponse((validJSON.data(using: .utf8)!, responseWithStatusCode(200, url: url)), for: myContactURL)

        

        do {

            let contacts = try await sut.getMyContacts()

            XCTAssertEqual(contacts.count, 1)

        } catch {

            XCTFail()

        }

    }


6. test_load_DeliverConnectivityErrorIfTaskIsCancelled

    @MainActor func test_load_DeliverConnectivityErrorIfTaskIsCancelled() async throws {

        let url = URL(string: "https://aurl.com")!

        

        let (sut, session) = makeSUT(url: url)


        let dataResponse = """

[

{"contact_ID" : 2 }

]

"""

        let myContactURL = ContactEndPoint.myContacts.url(baseURL: url).url!

        session.setResponse((dataResponse.data(using: .utf8)!, responseWithStatusCode(200, url: url)), for: myContactURL)

        

        let exp = expectation(description: "Wait for load completion")

        let task = Task {

            do {

                let contacts = try await sut.getMyContacts()

                exp.fulfill()

                XCTAssertEqual(contacts.count, 0)

            } catch let error {

                exp.fulfill()

                XCTAssertEqual((error as? APIError), APIError.connectivity)

            }

        }

        task.cancel()

        await fulfillment(of: [exp])

    }

    





Thursday, January 23, 2020

Swift enum - make Encodable

Suppose you have struct like below to encode as { status: "",  macName: "" }

struct Request: Encodable {

      var status: MacStatus
      var macName: String
}

enum MacStatus {

    case running
    case notRunning
}

Now we want to make the enum  MacStatus as encodable. So we can use singleValueContainer

extension MacStatus: Encodable {

    var josnKey: String {
        switch self {
        case . running:
            return "Running"
        case . notRunning:
            return "Not Running"
   }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(josnKey)
    }
}

Another way is below:

enum MacStatus: String, Encodable {

    case running
    case notRunning

   var rawValue {
       switch self {
           case . running:
                return "Running"
           case . notRunning:
                return "Not Running"
       }
   }
}



Sunday, March 18, 2018

Swift Code Review

When we work on a project as team, we want to make sure all of team members follow some rules

- to keep good coding practice
- to maintain the code looks similar
- to reduce bugs
- to improve performance

One of the best way to follow good swift styles is using Swift Lint

Here are some other tips for code review:

1.
Reduce the use of red text. We have to keep an eye on the text with red colour ( I am mentioning the Xcode default format).
The only text in red colour should be the key in Localizable.strings file or some print statements.

2.

Reduce the usage of 'self.'
The only place 'self.'  can exist should the init() or constructors of a class.
If we forced to use self in any closures, then apply [weak self] for the arguments and add a guard statement. Check below example:

CameraController.requestCameraAccess(completion: { [weak self] (status) in
  guard let strongSelf = self else { return }
  if status {
    globalMainQueue.async {
      strongSelf.performRoomSelection(model: model, cellIndex: cellIndex)
    }
  }
})

3. Reduce the usage of integer or string constants.

group it using enum if possible.

4. Inherit NSObject only if necessary.

5. Check all delegate vars are declared as 'weak'

Wednesday, February 21, 2018

iOS - Fix Massive View Controller using MVP+VM Architecture

To solve the Massive View Controller, we distribute the tasks to different classes:

1. Router


  • The Router class will capable to handle all navigations such as segue or custom.
  • Capable to construct the view controller
  • Capable to pass data to other view controllers

A sample Router class is shown below:


So we will always call addContracts() function when ever construct a view controller

func showLoginByRemove(_ viewController: UIViewController?) {
        let loginController = LoginViewController.getController()
        LoginRouter.addContracts(loginController, parent: self, profile: MyProfile())
        transition(fromVC: viewController, animationDuration: 0.5, toVC: loginController)
}

2. SBControl
    This class will keep all storyboard controls, and its makeups such as setting font, colours and localised texts and animations etc..

I am not repeating how to do this, Its well explained here see the section "Solution 2: Presentation Controls"

So
The VC (View Controller) can have minimum one IBOutlet reference to the Object control.

3. Presenter  - implements UIEvents protocol
    Responsible to handle user actions from View Controller. This is just a dispatcher. Handle a little business logics. Presenter will ask to viewmodel to perform the data manipulations and will return the result to the view controller using the DisplayUI protocol which is implemented by View Controller.
    Presenter will give the navigation task to the router
    Presenter will give the display task to the view controller
    Presenter will give the network operations to the service class

4. Service - optional
     Responsible to all network operations. Presenter will hold a protocol reference which is implemented by Service class

5. ViewModel
     There will be separate view models for each view. It will be responsible to handle business logic as well as the presentation logic. Both will be grouped using protocols.

6. ViewController
    Responsible to the VC life cycle. Inform all UI actions to the Presenter. ViewController implements a DisplayUI protocol.

7. UIController (optional)
    Handle the tableview/collection/text view delegates here to reduce the code in View Controller and also to distribute the functionalities.




Following is the template to create the files.

https://github.com/davidpaul0880/Swift-Template

Can post more when get more free time..

But post comment if you have any questions, or need clarifications.