Build gRPC client iOS đơn giản

Introduction

Trong bài viết trước, chúng ta đã cùng nhau tìm hiểu về gRPC và cách để build một gRPC server bằng node.js với các chức năng CRUD đơn giản:

https://viblo.asia/p/build-crud-server-don-gian-voi-grpc-va-nodejs-maGK70rxZj2

Trong bài viết này, chúng ta sẽ tiếp tục build một gRPC client app bằng iOS để kết nối đến server gRPC local ở bài viết trên. Source code của gRPC server có thể download tại:

https://github.com/oNguyenXuanThanh/crud-grpc-nodejs

Setup and run gRPC server locally

Đầu tiên, chúng ta cần deploy gRPC server ở local. Mở terminal lên, cd đến thư mục chứa source code và chạy lệnh node index.js:

cd /Users/thanhfnx/Desktop/StudyReport/gRPC-Server
node index.js

Server running at http://127.0.0.1:50051
(node:3296) DeprecationWarning: grpc.load: Use the @grpc/proto-loader module with grpc.loadPackageDefinition instead
(Use `node --trace-deprecation ...` to show where the warning was created)

Setup Xcode project and install Cocoapod dependencies

Sau khi run được server gRPC, bước tiếp theo, chúng ta sẽ bắt đầu xây dựng gRPC client iOS. Mở Xcode lên và tạo mới một project tên là gRPC Client như sau:

Tiếp theo, mở tab mới trên Terminal, cd đến thư mục của project iOS vừa tạo và chạy lệnh pod init.

File Podfile sẽ được tự động sinh ra, dùng một text editor nào đó mở file này lên và thêm pod SwiftGRPC:

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'gRPC Client' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for gRPC Client
  pod 'SwiftGRPC'

end

Sau đó, chạy lệnh pod install để install các Cocoapod dependency.

pod install
Analyzing dependencies
Downloading dependencies
Installing BoringSSL-GRPC (0.0.4)
Installing SwiftGRPC (0.11.0)
Installing SwiftProtobuf (1.7.0)
Installing gRPC-Core (1.24.2)
Generating Pods project
Integrating client project
Pod installation complete! There is 1 dependency from the Podfile and 4 total pods installed.

Sau khi pod install xong, mở project iOS vừa tạo bằng file .xcworkspace. Bởi vì server gRPC của chúng ta chạy ở localhost nên cần phải thay đổi setting, cho phép truy cập HTTP bằng cách thêm key-value sau và file Info.plist.

	<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSAllowsArbitraryLoads</key>
		<true/>
	</dict>

Compile proto file

Đến với bước tiếp theo, chúng ta cần compile file pets.proto ở thư mục gRPC server sang file Swift, sử dụng Protocol Buffer và Swift Protobuf Compiler.

Đầu tiên, hãy tải về và cài đặt Proto Buffer Compiler chính thức của Google bằng cách gõ lệnh sau trong Terminal:

brew install grpc-swift

Tiếp theo, chúng ta cần tải và cài đặt Swift Protobuf Compiler.

brew install swift-protobuf

Sau khi cài đặt xong, chạy lệnh sau để bắt đầu compile:

protoc pets.proto \
    --swift_out=. \
    --plugin=./.build/debug/protoc-gen-grpc-swift \
    --swiftgrpc_out=Client=true,Server=false:.

Kết quả là chúng ta sẽ được 2 file pets.grpc.swiftpets.pb.swift. Kéo thả 2 file này vào thư mục của project iOS trong Xcode và đảm bảo tích option Copy Items if needed.

Swift data repository for gRPC service

Tiếp tục, chúng ta sẽ tạo một class mới có tên là DataRepository, sử dụng một instance singletion để thực hiện CRUD qua gRPC method.

Tạo mới Swift file DataRepository và thêm đoạn code sau:

import Foundation
import SwiftGRPC

class DataRepository {
    // Singletion instance
    static let shared = DataRepository()
    // Client instance với address của gRPC local server
    private let client = PetServiceServiceClient(address: "127.0.0.1:50051", secure: false)

    private init() {}
}

List pets

Để call gRPC method get list, trong DataRepository, tạo một method mới như sau:

    func getPets(completion: @escaping ([Pet]?, CallResult?) -> Void) {
        // Vì gRPC get list pet không cần parameter đầu vào nên chỉ cần khời tạo
        // Empty message
        let emptyParameter = Empty()
        // Call gRPC method và trả về list pet trên main thread
        _ = try? client.list(emptyParameter, completion: { petList, result in
            DispatchQueue.main.async {
                completion(petList?.pets, result)
            }
        })
    }

Để test thử, đơn giản nhất, trong Main.storyboard, tạo mới button Get list và set method handle touch up inside như sau:

    @IBAction private func getListButtonTapped(_ sender: Any) {
        DataRepository.shared.getPets { pets, result in
            if let result = result {
                print("Call result: \(result)")
            }
            print("Fetched pets: \(pets ?? [])")
        }
    }

Kết quả thu được:

Call result: successful, status ok: OK
resultData: 55 bytes
initialMetadata: [:]
trailingMetadata: [:]
Fetched pets: [gRPC_Client.Pet:
id: "1"
name: "Alaska"
description: "Description 1"
, gRPC_Client.Pet:
id: "2"
name: "Husky"
description: "Description 2"
]

Create new pet

Trước tiên, tạo mới extension cho struct Pet, thêm method init từ các kiểu dữ liệu đơn giản:

extension Pet {
    init(name: String, description: String) {
        self.name = name
        self.description_p = description
    }
}

Trong DataRepository, implement method addPet:

    func addPet(_ pet: Pet, completion: @escaping (Pet?, CallResult?) -> Void) {
        _ = try? client.insert(pet, completion: { createdPet, result in
            DispatchQueue.main.async {
                completion(createdPet, result)
            }
        })
    }

Và cuối cùng, thêm button Add new với handler như sau:

    @IBAction private func addNewButtonTapped(_ sender: Any) {
        let alertController = UIAlertController(title: "Add new pet", message: nil, preferredStyle: .alert)
        alertController.addTextField(configurationHandler: { $0.placeholder = "Pet name" })
        alertController.addTextField(configurationHandler: { $0.placeholder = "Pet description" })
        alertController.addAction(UIAlertAction(title: "Save", style: .default, handler: { _ in
            let nameTextField = alertController.textFields![0]
            let descriptionTextField = alertController.textFields![0]
            guard let name = nameTextField.text, !name.isEmpty,
                  let description = descriptionTextField.text, !description.isEmpty else {
                return
            }
            let newPet = Pet(name: name, description: description)
            DataRepository.shared.addPet(newPet) { insertedPet, result in
                if let result = result {
                    print("Call result: \(result)")
                } else {
                    print("Call result: nil")
                }
                if let insertedPet = insertedPet {
                    print("Inserted pet: \(insertedPet)")
                } else {
                    print("Inserted pet: nil")
                }
            }
        }))
        alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        present(alertController, animated: true, completion: nil)
    }

Kết quả sau khi thêm mới thành công:

Call result: successful, status ok: OK
resultData: 60 bytes
initialMetadata: [:]
trailingMetadata: [:]
Inserted pet: gRPC_Client.Pet:
id: "d919ffa0-6a5b-11eb-8a35-ac3965b004a8"
name: "Chihuahua"
description: "Chihuahua"

Delete existing pet

Cuối cùng, method để xóa một record pet đã tồn tại được implement như sau:

extension PetRequestId {
    init(id: String) {
        self.id = id
    }
}
    func delete(petId: String, completion: @escaping (Bool) -> Void) {
        _ = try? client.delete(PetRequestId(id: petId), completion: { pet, result in
            DispatchQueue.main.async {
                completion(pet != nil)
            }
        })
    }

Handler cho button delete:

    @IBAction private func deleteButtonTapped(_ sender: Any) {
        let alertController = UIAlertController(title: "Delete pet by ID", message: nil, preferredStyle: .alert)
        alertController.addTextField(configurationHandler: { $0.placeholder = "Pet id" })
        alertController.addAction(UIAlertAction(title: "Delete", style: .default, handler: { _ in
            let petIdTextField = alertController.textFields![0]
            guard let petId = petIdTextField.text, !petId.isEmpty else {
                return
            }
            DataRepository.shared.delete(petId: petId) { success in
                print(success ? "Delete successfully" : "Delete failed")
            }
        }))
        alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        present(alertController, animated: true, completion: nil)
    }

Conclusion

Trên đây chỉ là một ví dụ đơn giản cho việc implement gRPC client iOS dưới dạng in ra màn hình console kết quả. Trong thực tế chúng ta cần phải kết hợp với xây dựng UI/UX cụ thể hơn để có thể vận dụng nhưng ưu điểm của gRPC vào một project hoàn chỉnh.

Final project: https://github.com/oNguyenXuanThanh/crud-grpc-ios

Source article: https://www.alfianlosari.com/posts/building-grpc-client-swift-note-taking-app/


All Rights Reserved