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.swift
và pets.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/