Như mọi người đã biết, tất cả các ứng dụng hiện nay đều có những ô nhập (TextField, TextView …) để hỗ trợ người dùng điền thông tin của họ và gửi về phía server để xử lí. Khi này để giảm tải công việc cho server chúng ta cần phải kiểm tra dữ liệu để loại bỏ bớt các trường hợp dữ liệu không đúng trước khi gửi lên server. Để làm việc này thì thông thường chúng ta sẽ nghĩ ngay đến Regex. Vậy Regex là gì? Áp dụng Regex vào source code Swift như nào? mời các bạn theo dõi tiếp bài viết của mình nhé.
Regex là gì?
Regex là viết tắt của Regular Expression, tiếng việt được gọi là Biểu Thức Chính Quy. Regex là một công cụ rất mạnh trong việc xử lí chuỗi, nó thường được dùng để kiểm tra tính hợp lệ của một chuỗi hay tìm kiếm, vì vậy chúng ta nên trang bị cho mình một số kiến thức cơ bản về Regex để có thể xử lí công việc nhẹ nhàng và nhàn hạ hơn.
Nhược điểm của Regex
Công nhận regex là một công cụ rất mạnh trong việc xử lí chuỗi, tuy nhiên nó cũng có một số nhược điểm như sau:
Khó đọc
Nhìn vào đoạn Regex: ^([A-Za-z0-9!@#$%^&*?]{8,})$
Khi mới bắt đầu chúng ta sẽ rất khó để biết được regex này đang làm nhiệm vụ gì. Vì nó được mô tả bới các kí tự không quen thuộc với ngôn ngữ chúng ta hay dùng.
Dễ quên
Do nó khó đọc, khó hiểu nên nó cũng khiến chúng ta dễ quên, vì vậy mỗi lần cần đến 1 regex nào là chúng ta thường phải tìm đến google để trợ giúp. Để khắc phục nhược điểm này mỗi khi chúng ta làm việc mới một regex khó hoặc dài thì chúng ta sẽ lưu lại vào 1 file lưu trữ, để sau này dùng đến tìm lại cho nhanh.
Regex dùng để làm gì?
Regex thường được dùng để tìm kiếm, để tách chuỗi, để kiểm tra chuỗi, …
1. Dùng để kiểm tra chuỗi
Ví dụ ta cần kiểm tra độ phức tạp của mật khẩu với yêu cầu như sau:
Phải có ít nhất một kí tự viết thường Regex: [a-z]: Sẽ khớp với bất kì kí tự viết thường nào trong chuỗi(a-z) Input: AAA -> FALSE Input: aabc hoặc Aza -> TRUE
Phải có ít nhất một kí tự viết hoa Regex: [A-Z] : Sẽ khớp với bất kì kí tự viết hoa nào trong chuỗi(A-Z) Input: AAA hoặc Abc -> TRUE Input bbca -> FALSE
Phải có ít nhất một kí tự số Regex: [0-9] : Sẽ khớp với bất kì kí tự số nào trong chuỗi(0-9) Input: 1234 hoặc A0bc -> TRUE Input bbca -> FALSE
Phải có ít nhất một trong số kí tự đặc biệt sau @!#$ Regex: [@!#$] : Sẽ khớp với bất kì kí tự số nào trong chuỗi(@!#$) Input: 12@4 hoặc A!bc -> TRUE Input bbca -> FALSE
Không được có dấu cách Regex: [ ] : Sẽ khớp với bất kì kí tự nào trong chuỗi là space Input: 12 4 hoặc A bc -> TRUE Input bbca -> FALSE
2. Dùng để tách chuỗi
Ví dụ chúng ta có một chuỗi kí tự như sau: ABCxyz123mM
Tách lấy hết các kí tự viết hoa từ chuỗi trên ta sử dụng regex: [A-Z], ta sẽ thu được kết quả từ chuỗi ABCxyz123mM -> ABCM
Tách lấy hết kí tự viết thường ta dùng regex: [a-z], ta thu được kết quả như sau: ABCxyz123mM -> abcm
Tách lấy hết kí tự số ta thường dùng regex: [0-9], Ta thu được kết quả như sau: ABCxyz123mM -> 123
3. Dùng để tìm kiếm
Regex khá là phổ biến, nó có mặt trên hầu hết các IDE hiện nay. xCode, Android studio, Subline text, note pad ++, …
Ví dụ phần mềm Subline text
Để kích hoạt tính năng này các bạn bấm command + f (tổ hợp tìm kiếm) và chọn mục regex (.*)
Ảnh dưới đây mình có viết 1 đoạn regex: ^N.*o$ để tìm ra chuỗi bắt đầu bằng kí tự N và có kết thúc bằng kí tự o
Giải thích ý nghĩa các kí tự để viết Regex
Sau đây mình sẽ giải thích kĩ hơn về các kí tự để viết regex và cách sử dụng chúng.
1.Kí tự thường
Mã Regex
Mô tả
Ghi chú
a|b
Khớp với a hoặc b
[0-9]
Khớp với các kí tự là số 0,1,2..9
[a-z]
Khớp với các kí tự viết thường từ a tới z
Nếu đổi z thành c thì sẽ là từ a tới c
[ABC]
Khớp với các kí tự là ABC
[^ABC]
Khớp với các kí tự không phải là ABC
Nếu dấu ^ xuất hiện phía trong [] có nghĩa nó là Phủ định của tập hợp đó
\d
Khớp với số bất kì
thay thế cho [0-9]
\D
Khớp với các kí tự không phải số
Phủ định của \d
\s
Khớp với tất cả kí tự là khoảng trằng, tab, hoặc xuống dòng
\S
Khớp với tất cả các kí tự không phải là khoảng trắng, tab hoặc xuống dòng
Phủ định của \s
\S+
Khớp với một hoặc nhiều kí tự không phải là khoảng trắng, tab hoặc xuống dòng
\w
Khớp với bất kì ký tự chữ
Thay thế cho [a-zA-Z0-9]
\W
Khớp với kí tự bất kì không phải chữ
phủ định của \w
\b
Khớp khi kí tự trước đó nằm ở cuối chuỗi
regex: dao\b minhdao: true daominh: false
\B
Phủ định của \b
2. Kí tự đặc biệt
Regex
Mô tả
Ghi chú
.
Khớp với tất cả các kí tự trừ kí tự xuống dòng
Thay thế cho [^\n\r]
^
Bắt đầu
regex: ^dao daominh: True minhdao: False
$
Kết thúc
regex: dao$ daominh: False minhdao: True
|
Điều kiện hoặc
a|b a: true b: true c: false
\
Biến 1 kí tự đặc biệt thành kí tự thường hoặc ngược lại
d: kí tự d thông thường \d: khớp với kí tự số bất kì
3. Lặp
Regex
Mô tả
Ghi chú
*
Xuất hiện 0 hoặc nhiều lần
Tương đương {0,}
+
Xuất hiện 1 hoặc nhiều lần
Tương đương {1,}
?
Xuất hiện 0 hoặc 1 lần
Tương đương {0,1}
{x,y}
Xuất hiện từ x lần tới y lần
{3}: xuất hiện đúng 3 lần {3,}: xuất hiện từ 3 lần hoặc nhiều hơn {3, 10} Xuất hiện từ 3 lần đến 10 lần
4. Nhóm
Regex
Mô tả
Ghi chú
()
Nhóm nhiều mã lại với nhau tạo thành nhóm điều kiện
(?:x)
Khớp với x nhưng không nhớ kết quả khớp
x(?=y)
Chỉ khớp x nếu ngay sau x là y
x(?!y)
chỉ khớp x nếu ngay sau x không phải y
Ứng dụng regex vào việc kiểm tra dữ liệu UITextField bằng ngôn ngữ swift
Để ứng dụng được regex vào việc kiểm tra dữ liệu ở swift chúng ta cần biết một chút kiến thức cơ bản về regex(mình đã giới thiệu ở phía trên) và hiểu rõ về cách hoạt động của TextField
Các hàm common cần dùng
extension String {
/// check string is match regex or not
/// - Parameter regex: regular expression
/// - Returns: true if match
func isMatches(_ regex: String) -> Bool {
do {
let regex = try NSRegularExpression(pattern: regex)
let matches = regex.matches(in: self, range: NSRange(location: 0, length: self.count))
return !matches.isEmpty
} catch {
print("Something went wrong! Error: \(error.localizedDescription)")
}
return false
}
/// Return string matchs regex
/// - Parameter regex: regular expression
/// - Returns: all string match regex
func filter(regex: String) -> String {
do {
let regex = try NSRegularExpression(pattern: regex)
let results = regex.matches(in: self, range: NSRange(self.startIndex..., in: self))
return results.map { String(self[Range($0.range, in: self)!]) }.joined(separator: "")
} catch {
print("Something went wrong! Error: \(error.localizedDescription)")
}
return ""
}
/// Remove mark in string
var folded: String {
self.folding(options: .diacriticInsensitive, locale: nil)
.replacingOccurrences(of: "đ", with: "d")
.replacingOccurrences(of: "Đ", with: "D")
}
}
func isMatches(_ regex: String) -> Bool: hàm này dùng để kiểm tra xem chuỗi có khớp với regex hay không?
func filter(regex: String) -> String: Hàm này dùng để lấy ra tất cả các kí tự thoả mãn điều kiện của regex
var folded: String : giúp biến đổi các kí tự có dấu về không dấu VD: Đ -> D, ê -> e …
extension UITextField {
/// validate input data
/// - Parameters:
/// - maxLength: max char of text field
/// - range: location and lenth of current selected text
/// - string: new string will be replacce at range
func validateInput(maxLength: Int, range: NSRange, string: String) {
guard let textFieldText = self.text else {
return
}
if self.text.safeValue.count == maxLength, !string.isEmpty {
return
}
let text: NSString = textFieldText as NSString
let finalString: String = text.replacingCharacters(in: range, with: string)
let newString = String(finalString.prefix(min(maxLength, finalString.count)))
self.text = newString
let countChange: Int = newString.count - textFieldText.count
let validateCount: Int = countChange > 0 ? countChange : 0
self.setCursorPosition(range.location + validateCount)
}
/// Set Cursor position
/// - Parameter cursorPosition: Int
func setCursorPosition(_ cursorPosition: Int) {
guard cursorPosition <= (self.text ?? "").count,
let posCursor = self.position(from: beginningOfDocument, offset: cursorPosition) else { return }
DispatchQueue.main.async {
self.selectedTextRange = self.textRange(from: posCursor, to: posCursor)
}
}
}
func validateInput(maxLength: Int, range: NSRange, string: String): Dùng khi cần tự validate các kí tự cho phép, hoặc chặn, hoặc copy paste chuỗi, rồi set lại giá trị cho text field.
func setCursorPosition(_ cursorPosition: Int): Dùng để di chuyển con trỏ khi copy paste.
Hiểu rõ các func trong UITextField delegate hoạt động
Ở đây mình chỉ nói đến các delegate hay dùng làm nhiệm vụ kiểm tra dữ liệu
func textFieldDidEndEditing(_ textField: UITextField): Dùng để kiểm tra dữ liệu khi người dùng focus out, thường được dùng khi muốn kiểm tra dữ liệu tại thời điểm người dùng kết thúc thao tác nhập dữ liệu.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool : thường được dùng khi muốn kiểm tra dữ liệu tại thời điểm người dùng nhập(realtime), dùng để chặn những kí tự không cho phép. – textField: đây là text field đang được người dùng tương tác – range: Vị trí và độ dài mã text sắp được thay đổi – chuỗi được thay thế tại range NOTE: Khi replacement string = empty có nghĩa là người dùng đang thực hiện thao tác xoá hoặc dùng gợi ý nội dung hoặc keychain của bàn phím.
event Editting Changed: Dùng khi không chặn kí tự ở hàm 2(shouldChangeCharacterIn), event này chạy khi có thay đổi kí tự trong text field, với điều kiện shouldCHangeCharacter phải return true
Sử dụng regex để kiểm tra và lọc realtime kí tự nhập vào UITextField
Từ các common code phía trên để kiểm tra chuỗi mà người dùng nhập vào chúng ta sẽ thực hiện nó trong hàm shouldChangeCharacter in range
Ví dụ: Text field chỉ cho nhập các kí tự chữ hoa chữ thường và không cho nhập tiếng việt
Nếu muốn khi copy paste một chuỗi có dấu tự convert về chuỗi không dấu ta làm như sau: Sử dụng folded để convert chuỗi về chuỗi không dấu trước khi filter nó với regex
Tương tự với từng yêu cầu chúng ta sẽ tạo ra các regex khác nhau và đưa vào sử dụng một cách dễ dàng và hiệu quả
Sử dụng Regex để kiểm tra khi người dùng kết thúc nhập liệu
Nếu bạn muốn kiểm tra chuỗi người dùng vừa xong có đúng định dạng email hay không thì ta sử dụng hàm textFieldDidEndEditing khi người dùng kết thúc nhập để kiểm tra như sau:
Để tránh sai sót khi sử dụng các regex ở nhiều chỗ khác nhau trong project, chúng ta nên tạo ra enum để quản lí các chuỗi regex này như sau:
enum Regex: String {
case none = "[\\s\\S]"
case min8CharNoSpace = "^([A-Za-z0-9!@#$%^&*?]{8,})$"
case haveCharAndNumber = "(?=.*[a-zA-Z])(?=.*[0-9])"
case haveUpercaseAndLowerCase = "(?=.*[a-z])(?=.*[A-Z])"
case haveSpecialChar = "[!@#$%^&*?]"
case vietnamese = "^[a-zA-Z0-9ÀÁÂÃÈÉÊÌÍÒÓÔÕÙÚĂĐĨŨƠàáâãèéêìíòóôõùúăđĩũơƯĂẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼỀỀỂẾưăạảấầẩẫậắằẳẵặẹẻẽềềểếỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪễệỉịọỏốồổỗộớờởỡợụủứừỬỮỰỲỴÝỶỸửữựỳỵỷỹ\\s]+$"
case email = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
case emailInput = "[a-zA-Z0-9.@]"
case password = "[a-zA-Z0-9!@#$%^&*?]"
var maxLength: Int {
switch self {
case .none:
return .max
case .min8CharNoSpace:
return 100
case .haveCharAndNumber:
return 100
case .haveUpercaseAndLowerCase:
return 100
case .haveSpecialChar:
return 100
case .vietnamese:
return 100
case .email:
return 100
case .emailInput:
return 100
case .password:
return 30
}
}
}
Mình hi vọng bài viết này giúp mọi người hiểu rõ hơn về regex và cách ứng dụng nó vào trong công việc. Chúc các bạn thành công!
Copy on Write (CoW) là 1 khái niệm ko hề mới trong Swift. Nó đã được Apple giới thiệu trong WWDC 2015 và được áp dụng từ iOS 7.0. Tuy nhiên thực chất CoW là gì và chúng có tác dụng gì? Hãy cùng tìm hiểu trong bài viết này nhé.
Table of contents
Copy on Write là gì
Kết luận
References
Copy on Write là gì?
Trong Swift, chúng ta có các kiểu Reference type và Value type. Nếu bạn gán một value type cho một biến hoặc pass nó như một parameter của function (không phải parameter kiểu inout) thì dữ liệu của value type này sẽ được copy. Lúc này, ta sẽ có hai value type có nội dung giống nhau nhưng trỏ đến hai địa chỉ bộ nhớ riêng biệt. Hôm nay ta sẽ bàn về Copy on Write – một cơ chế quan trọng trong việc tối ưu bộ nhớ của Swift.
Trong Swift, khi bạn có một khối lượng lớn các value type và muốn gán hoặc truyền chúng qua các function, nếu bạn copy tất cả dữ liệu sang một vị trí khác trong bộ nhớ thì sẽ gây ra hiện tượng lãng phí hiệu năng. Để giảm thiểu tình trạng này, Swift đã triển khai cơ chế Copy on Write cho một số kiểu dữ liệu là value type như array, dictionary,…
Hiểu 1 cách đơn giản, nếu bạn có 1 array có 1000 phần tử và bạn muốn copy mảng đó vào 1 biến khác, Swift sẽ không sao chép ngay lập tức cả 1000 phần tử này mà sẽ sử dụng đến cơ chế Copy on Write: Khi bạn trỏ 2 biến vào cùng 1 mảng, chúng đều trỏ vào cùng 1 địa chỉ ô nhớ, và chỉ đến khi bạn sửa đổi 1 trong 2 biến đó, swift mới tạo ra 1 bản copy mới để sửa và chỉ sửa trên bản copy đó và vẫn giữ nguyên biến còn lại. Bằng cách trì hoãn việc sao chép dữ liệu cho đến khi thực sự cần thiết, Swift đã đảm bảo được việc tối ưu được performance của hệ thống
Copy on Write ko phải là cơ chế mặc định cho tất cả các kiểu value type, mà chỉ được áp dụng cho 1 số kiểu như Aray, Collections,… Ngoài ra, với những kiểu value type mà bạn tự custom thì cũng ko có sẵn cơ chế này mà phải tự implement thêm.
Ví dụ về cách hoạt động của Copy on Write
import Foundation
func print(address o: UnsafeRawPointer ) {
print(String(format: "%p", Int(bitPattern: o)))
}
var array1: [Int] = [0, 1, 2, 3]
var array2 = array1
//Print with just assign
print(address: array1) //0x600000078de0
print(address: array2) //0x600000078de0
//Let's mutate array2 to see what's
array2.append(4)
print(address: array2) //0x6000000aa100
//Output
//0x600000078de0 array1 address
//0x600000078de0 array2 address before mutation
//0x6000000aa100 array2 address after mutation
Đây là 1 ví dụ đơn giản để chỉ cách hoạt động của Copy on Write. Trước hết, tạo biến array1 rồi sau đó gán aray2 bằng với array1. Khi chưa thực hiện thay đổi giá trị thì array2 vẫn trỏ vào cùng 1 địa chỉ ô nhớ với array1. Chỉ khi ta thay đổi giá trị của array2 thì nó mới được copy sang 1 địa chỉ ô nhớ khác, và giá trị mới sẽ trỏ vào địa chỉ ô nhớ này, còn array1 sẽ không có sự thay đổi gì.
Implement cơ chế Copy on Write cho các dạng value type tự tạo
Bạn có thể tự mình implement cơ chế Copy on Write cho các kiểu dữ liệu mà bạn tự custom. Đây là ví dụ trên OptimizationTips.rst trong repo chính của Swift
final class Ref<T> {
var val : T
init(_ v : T) {val = v}
}
struct Box<T> {
var ref : Ref<T>
init(_ x : T) { ref = Ref(x) }
var value: T {
get { return ref.val }
set {
if (!isUniquelyReferencedNonObjC(&ref)) {
ref = Ref(newValue)
return
}
ref.val = newValue
}
}
}
// This code was an example taken from the swift repo doc file OptimizationTips
// Link: https://github.com/apple/swift/blob/master/docs/OptimizationTips.rst#advice-use-copy-on-write-semantics-for-large-values
Đoạn code trên sử dụng loại reference type để triển khai cho kiểu giá trị dạng generics. Về cơ bản, đây là 1 warrper quản lý loại reference type và chỉ trả về 1 instance mới nếu giá trị được tham chiếu không là duy nhất. Nếu không, nó chỉ thay đổi giá trị của kiểu tham chiếu.
Kết luận:
Copy on Write là 1 cơ chế rất thông minh để tối ưu hoá việc copy giá trị của các kiểu value type. Đây là 1 cơ chế được sử dụng rất nhiều Swift, dù hầu như chúng ta ko nhìn thấy nó 1 cách rõ ràng vì chúng đã được thực hiện trên các thư viện chuẩn của Swift. Nhưng chúng ta nên biết để có thể tận dụng tối đa lợi ích mà Copy on Write mang lại.
Chào các bạn, nếu một ngày đẹp trời bạn nhận được 1 task tích hợp Unity hoặc đơn giản là bạn muốn thử tích hợp Unity vào project IOS thì hãy tham khảo thử bài viết dưới đây nhé !
1. Tạo dự án Unity
Đầu tiên để tích hợp ta cần có một cái project unity, sau đó ta export cái project unity này ra platform IOS, nếu bạn đã có project unity IOS để tích hợp rồi thì có thể bỏ qua bước này nhé
ở đây mình tạo một project unity đơn giản, tiếp theo ta export project này ra platform IOS
bên trong project unity chọn File -> Build Setting
sudo gem install cocoapods
ở đây ta chọn platform IOS -> để setting như hình bên dưới rồi nhấn build đợi một lúc sẽ ra màn hình chọn thư mục để lưu trữ
Tiếp theo ấn Choose project unity sẽ tự export ra một project IOS có tên là unity
Export project Unity ra swift IOS
2. Tạo dự án iOS
ở đây ta tạo một project đơn giản để tích hợp, trong Xcode chọn File -> new project, ở đây mình để tên project là SimpleIOS
sau khi xong các bước trên thì ta có 2 thư mục sau:
unity: được tạo bằng cách export trong project Unity dưới dạng một dự án iOS
SimpleIOS: project IOS chính cần tích hợp
3. Tích hợp Unity với IOS
Tới đây thì ta sẽ tạo một Workspace trong xcode để có thể add 2 project trên để tích hợp,
trong xcode chọn File -> New -> Workspace
ở đây bạn có thể đặt tên trùng với tên project IOS chính của mình hoặc một tên khác bất kỳ, ở đây mình tạo với tên là SimpleSwiftUnity
Lưu ý: nếu project IOS chính của bạn muốn tích hợp đã có Workspace thì có thể dùng luôn không cần tạo thêm một Workspace mới
sau khi tạo xong ta mở SimpleSwiftUnity.xcworkspace lên sau dó kéo tệp SimpleIOS.xcodeproj và Unity-iPhone.xcodeproj và workspace chính
Thêm dự án IOSThêm dự án Unity IOS
tới bước này thì cả 2 SimpleIOS.xcodeproj và Unity-iPhone.xcodeproj đều thuộc 1 workspace
Tiếp theo, nhấp vào dự án SimpleIOS chọn vào tab General cuộn xuống phần Frameworks, Libraries and Embedded Content . Nhấp vào nút + để add một framework mới.
chọn UnityFramework.framework từ trong list và add vào dự án
Tiếp theo, chọn thư mục Data trong Unity-iPhone project. Trong bảng điều khiển bên phải, bạn sẽ thấy phần Target Membership . Bạn cần tích chọn UnityFramework .
Tới đây đã dủ các bước cấu hình, bây giờ mình sẽ thêm một số dòng code để show unity kia lên project IOS chính nhé !
Tạo file UnityEmbeddedSwift.swift trong SimpleIOS project như bên dưới
tiếp theo ta tạo một UI đơn giản để add unity vào đó như hình bên đưới
Oke tới đây là xong, ta chạy thử ứng dụng và xem thành quả nhé ^^
Lưu ý: Đảm bảo rằng bạn chạy ứng dụng trên thiết bị iPhone thực chứ không phải trên máy ảo!
Các bạn cũng thể add unity thành một View Controller show full màn hình, có thể tham khảo các hàm trong UnityEmbeddedSwift ở đây! link source Chúc các bạn thành công !!!
Như mọi người đã biết, hầu hết các ứng dụng ngày nay đều hỗ trợ tính năng đa ngôn ngữ, nhằm mục đích tiếp cận được nhiều người dùng hơn, cho mọi người sử dụng ứng dụng dễ dàng hơn. Tuy nhiên hiện nay hầu hết các dự án đều làm một cách tự phát, chưa có một quy trình chuẩn. Điều này làm cho quá trình phát triển sinh ra nhiều bug UI sai Message, sai chính tả, nó làm tốn thời gian không đáng có của các bên.
Trong quá trình làm việc ở các dự án, khi tài liệu được cập nhật đồng nghĩa với việc các Dev phải quay lại kiểm tra các thay đổi message, rồi bắt đầu loay hoay tìm kiếm trong source code để thực hiện thay đổi.
Hay mỗi khi code một màn hình mới việc phải định nghĩa một đống key, định nghĩa enum/constant và copy/paste nó sang các file localization thật nhàm chán và tiềm tàng nhiều rủi ro về lỗi sai chính tả, copy nhầm. Trước đây, có một member trong dự án của mình chỉ vì copy sai text làm thừa một dấu “;” làm lỗi cả file đa ngôn ngữ, việc này làm cho toàn bộ text trên ứng dụng khi release cho bị sai. Do file đa ngôn ngữ là string nên những lỗi sai chính tả như này mất rất nhiều thời gian để điều tra lỗi.
Sau nhiều năm làm việc với các dự án khác nhau, cùng các anh em, cộng sự hoàn thành các ứng dụng to nhỏ khác nhau. Mình đã đúc kết được một quy trình giúp cải thiện năng suất làm việc của mọi người, giúp giảm các bug UI về sai message, sai chính tả, giảm thời gian thực hiện tính năng đa ngôn ngữ, giúp quá trình bảo trì, thay đổi message trở nên dễ dàng hơn.
Quy trình
Brainstoming
Trước tiên, để đạt được hiệu quả Leader của các bên gồm BA, Mobile, Web … ngồi lại với nhau để thống nhất về cách làm việc theo quy trình trên. Sau đó phổ biến lại cho member và đảm bảo việc member thực hiện đúng quy trình làm việc.
Tạo file excel chung lưu các message cần làm đa ngôn ngữ
Trong file này mình có định nghĩa sẵn các trường cần nhập và có sẵn công thức để gen ra code của swift và android. Nếu dự án của các bạn sử dụng ngôn ngữ khác thì có thể chỉnh lại công thức cho phù hợp với dự án của mình.
Nhập nội dung
Thêm/sửa message
Screen/Feature: Điền vào tiên màn hình hoặc tính năng.
MessageKey: Điền vào tên item trên màn hình, nếu SRS có mô tả thì ưu tiên sử dụng key trong SRS, nếu không hãy đặt tên sao cho đúng ý nghĩa của item.
Tiếng việt: Text được hiển thị khi ngôn ngữ là Tiếng Việt
Tiếng anh: Text được hiển thị khi ngôn ngữ là Tiếng Anh
Note: TH1. Tài liệu đã định nghĩa sẵn các message cho các ngôn ngữ: thì BA có thể cử 1 bạn ra điền hết vào file này trước khi bên dev thực hiện code. Trường hợp này thì việc làm đa ngôn ngữ khá nhàn, mình sẽ giải thích chi tiết ở các phần phía dưới. TH2: Tài liệu chưa định nghĩa message cho các ngôn ngữ(các dự án thường rơi vào trường hợp này): Khi này khi các Dev thực hiện code sẽ tự insert thêm message vào file trên teams, để file tự tạo ra code tương ứng với các ngôn ngữ.
Công thức tạo code tự động
Công thức tạo code từ excel
Các Developers sẽ không phải làm các thao tác lặp đi lặp lại nhàm chán. Ngoài ra nó giúp giảm lỗi đánh máy, lỗi copy paste, lỗi sai chính tả và giúp việc làm đa ngôn ngữ nhanh hơn. Trên file excel mình có tạo ra công thức để tự tạo ra code của swift/android. Khi bạn điền thông tin vào sheet MessageList thì code sẽ được gen tự động, việc của mọi người là chỉ cần copy code vào project và sử dụng.
Đưa ra các luật lệ
Tạo ra các luật yêu cầu member phải tuân thủ như sau:
Đặt tên màn hình theo quy định của dự án, cách đặt tên cần thống nhất giữa tài liệu SRS và các class trong code của Dev.
Dev của các bên(mobile, web) và BA khi thực hiện thêm mới, hay chỉnh sửa sẽ thực hiện trực tiếp trên files Localize chung của dự án, khi chỉnh sửa cần tìm đúng tên màn hình, message để sửa, nếu thêm mới thì thêm xuống cuối cùng của của danh sách list message của màn hình đó.
Để tránh việc conflict hoặc giẫm chân nhau khi các bên chạy song song, thì mỗi màn hình những member liên quan tới màn hình đó sẽ cử ra một bạn điển vào file message trước.
Cách thực hiện code
TH1: đối với dự án không cho sử dụng thư viện mã nguồn mở
Khi thực hiện chúng ta chỉ cần vào file excel và copy code vào project của mình rồi sử dụng bình thường.
TH2:Dự án cho phép sử dụng thư viện mã nguồn mở
Chúng ta sẽ sử dụng thư viện để tự tạo ra enum nhanh gọn lẹ hơn, ví dụ bên iOS Swift thì chúng ta có thể sử dụng Swiftgen để thực hiện. Link hướng dẫn sử dụng Swiftgen mình cũng có viết môt bài rồi các bạn có thể tham khảo link dưới đây:
Giảm thời gian làm việc, tăng năng suất làm việc của team
Giảm khả năng bị lỗi UI/Message hay các lỗi về copy/paste khi thực hiện
Giảm thời gian bảo trì ứng dụng khi thay đổi message, khi có thay đổi BA sẽ update file message list và báo lại cho Devs, lúc này Dev không cần phải tìm xem thay đổi ở đâu, mà chỉ cần copy code từ file message list là xong.
Giảm thời gian implement khi có yêu cầu hỗ trợ thêm ngôn ngữ khác. Vì khi này chúng ta chỉ cần thêm 1 cột nữa ở file excel rồi copy code sang project là xong.
Giúp tối ưu được effort khi các team không start cùng một thời điểm. Vì team start sau sẽ sử dụng lại được phần đã làm của team start trước.
Nhược điểm
Cần quản lí, phân chia công việc tốt, member có khả năng làm việc nhóm.
Thường chỉ hiệu quả cao đối với các dự án mới
Tổng kết
Phía trên là nội dung chia sẻ về cách tối ưu khi làm ứng dụng hỗ trợ đa ngôn ngữ, mình hi vọng bài viết có thể giúp mọi người phần nào trong quá trình thực hiện các ứng dụng có hỗ trợ đa ngôn ngữ. Cảm ơn mọi người đã dành thời gian đọc bài viết này.
Tìm hiểu cách mà SwiftGen giúp dễ dàng loại bỏ các chuỗi ma thuật(magic strings) trong các dự án iOS của bạn.
Là một nhà phát triển ứng dụng trên thiết bị di động, bạn có thể cảm thấy như bạn gặp phép thuật hàng ngày. Các dòng mã bạn viết sẽ được chuyển đổi thành các ứng dụng mà mọi người trên khắp thế giới có thể sử dụng. Các công cụ mà Apple cung cấp giúp biến điều kỳ diệu đó thành hiện thực và làm cho cuộc sống của bạn dễ dàng hơn. Khi tiến xa hơn vào lĩnh vực phát triển phần mềm, bạn có thể nhận ra có một thứ ma thuật mà bạn không thích: Chuỗi thần kỳ(magic strings).
Loại an toàn, khái niệm rằng các biến chỉ có thể thuộc một loại cụ thể, cung cấp cho các nhà phát triển các rào chắn(guardrails) để giữ cho các chương trình của họ an toàn. Tuy nhiên, chuỗi ma thuật đưa mã không an toàn vào các ứng dụng đó. Chuỗi ma thuật là gì? Trong quá trình phát triển iOS, bạn đã gặp phải những điều này nhiều lần. Một ví dụ trông giống như sau:
let color = UIColor(named: "green-apple")
self.title = "Welcome!"
Ví dụ này hiển thị “green-apple” và “Welcome!” được viết dưới dạng chuỗi trực tiếp trong mã của bạn. Không quá khi nói rằng tất cả các nhà phát triển đôi khi nhận thấy mình có lỗi với hành vi này.
Trên thực tế, trong quá trình phát triển iOS, bạn không có nhiều lựa chọn. Ngoài ra, Xcode không cung cấp cách nào để tránh việc này.
Những người đã làm việc trong Android có thể thấy mình run sợ trước những đoạn mã như thế này. Môi trường phát triển Android có cơ chế chuyển đổi tài nguyên ứng dụng, chẳng hạn như chuỗi, màu sắc, hình ảnh và phông chữ, thành các kiểu biến an toàn. Có rất nhiều lợi ích từ việc này, và đó là:
Giảm lỗi sai chính tả
Ngăn chặn sự trùng lặp tài nguyên không cần thiết
Cung cấp kiểm tra tài nguyên tại thời điểm biên dịch
Giúp dọn dẹp tài nguyên cũ
Và nhiều hơn thế nữa
Như đã nêu, các nhà phát triển iOS và macOS không có quyền truy cập vào hệ thống cung cấp sự an toàn cho loại tài nguyên này.
May mắn thay, có SwiftGen, một trình tạo mã để loại bỏ các chuỗi ma thuật trong ứng dụng của bạn. Có sẵn dưới dạng thư viện mã nguồn mở trên GitHub, bạn có thể thêm tính năng này vào các dự án iOS và macOS của mình để mang lại sự an toàn về loại và kiểm tra thời gian biên dịch của tất cả các Assets của bạn.
Trong hướng dẫn này bạn sẽ học được cách:
Thiết lập dự án của bạn với SwiftGen
Xác định các nội dung mà bạn muốn chuyển đổi
Xác định nơi mà code được generated
Tạo các templates cho phép SwiftGen generate khi dùng SwiftUI cho Fonts và màu sắc.
Getting Started
Để bắt đầu, hãy nhấp vào nút Tải xuống tài liệu dưới đây:
Có một số cách bạn có thể cài đặt SwiftGen để hoạt động với môi trường của mình như sau:
CocoaPods
Homebrew
Mint
Directly download a zipped release(Nên dùng cách này)
Link hướng dẫn cài ở đây: https://github.com/SwiftGen/SwiftGen
Trong hướng dẫn này, bạn sẽ sử dụng CocoaPods để quản lý SwiftGen.
Lưu ý: Nếu bạn không có CocoaPods, đừng lo lắng – dự án khởi động và dự án cuối cùng đã được tải xuống phần phụ thuộc. :]
Mở workspace, có tên là DrinksUp! .Xcworkspace. Vì dự án này sử dụng CocoaPods nên bạn sẽ không thể làm việc trực tiếp với DrinksUp! .Xcodeproj.
Hãy dành một chút thời gian để xem xét xung quanh trong Xcode. Dự án đã ở trạng thái hoàn thành nhưng sử dụng chuỗi để tham chiếu phông chữ, màu sắc, hình ảnh và chuỗi. Bạn sẽ chuyển đổi tất cả những thứ này vào cuối hướng dẫn.
Xây dựng và chạy và làm quen với ứng dụng.
Ứng dụng, DrinksUp !, là một cách để theo dõi các loại đồ uống thú vị mà bạn và gia đình đã thử khi đến nhà hàng hoặc ở nhà.
Thiết lập SwiftGen
Bắt đầu bằng cách mở Terminal và điều hướng đến thư mục gốc của dự án khởi động của bạn. Tiếp theo, nhập lệnh sau vào Terminal:
./Pods/SwiftGen/bin/swiftgen config init
Thao tác này sẽ tạo một tệp cấu hình, có tên là swiftgen.yml, tại thư mục gốc dự án của bạn. Nếu tệp này tự động mở trong Xcode, hãy tiếp tục và tắt nó đi.
Tiếp theo, trong workspace dự án của bạn, đi tới Files ▸ Add new file vào “DrinksUp!”…. Tìm swiftgen.yml. Đảm bảo bỏ chọn Sao chép các mục nếu cần và chọn Tạo tham chiếu thư mục.
Nhấp vào nút Thêm. Khi hoàn tất, bạn sẽ thấy swiftgen.yml ở đầu trình điều hướng Dự án, như bên dưới:
Lưu ý: Bạn có thể di chuyển tệp này đến vị trí giống như hình minh họa, nếu Xcode không thêm nó theo cách tương tự.
Tệp này là nơi bạn sẽ đặt các hướng dẫn cho SwiftGen biết tệp nào bạn muốn chuyển đổi thành mã được tạo. Loại tệp, YML , cho biết nó đang sử dụng YAML cho cú pháp của nó. Nếu bạn chưa sử dụng YAML trước đây, đây chỉ đơn giản là một cách dễ đọc hơn để xem dữ liệu được tuần tự hóa. Bạn có thể nghĩ về nó như là JSON , được đơn giản hóa.
Bây giờ, thay thế toàn bộ nội dung của swiftgen.yml bằng nội dung sau:
Bạn đã khai báo một biến input_dir thư mục đầu vào. Điều này cho SwiftGen biết thư mục gốc để điều hướng đến tất cả các đường dẫn tệp mà bạn sẽ sớm thêm vào.
Một biến khác xác định thư mục đầu ra của các tệp Swift được tạo. Bằng cách này, bạn sẽ dễ dàng theo dõi tất cả các tệp SwiftGen hơn.
Thêm Build Phase
Để chạy SwiftGen, bạn sẽ cần thêm một giai đoạn xây dựng mới vào dự án của mình. Để thực hiện việc này, hãy chọn dự án của bạn trong trình điều hướng Dự án, chọnBuild Phases. Chọn + và chọnNew Run Script Phase.
Đổi tên script thành SwiftGen bằng cách nhấp đúp vào tên hiện tại, Run Script . Tiếp theo, thêm phần sau vào trường văn bản của tập lệnh:
if [[ -f "${PODS_ROOT}/SwiftGen/bin/swiftgen" ]]; then
"${PODS_ROOT}/SwiftGen/bin/swiftgen"
else
echo "warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it."
fi
Cuối cùng, sắp xếp lại thứ tự tập lệnh để ngồi ngay sau tên tập lệnh [CP] Check Pods Manifest.lock . Các Build Phases của bạn bây giờ sẽ giống như sau:
Build và Run. Nếu mọi thứ được thiết lập đúng cách, bạn sẽ không có bất kỳ lỗi nào. Bạn sẽ không có bất kỳ thứ gì trong thư mục Đã tạo của mình. Được rồi chúng ta đến bước tiếp theo.
Chuyển đổi XCAssets
Bây giờ, bạn đã sẵn sàng để bắt đầu xóa các chuỗi khỏi dự án của mình! Bước đầu tiên sẽ là SwiftGen tạo mã cho các tệp XCAsset trong dự án. Mở swiftgen.yml và thêm phần sau vào cuối tệp:
Đây là ý nghĩa của mỗi dòng trong số những dòng này:
Mỗi loại tệp hoặc mẫu, bạn muốn chuyển đổi bằng SwiftGen yêu cầu một mục nhập ở cấp gốc là swiftgen.yml . Ở đây, điều này cho biết bạn muốn SwiftGen chuyển đổi các tệp là XCAsset .
Danh sách này cho biết những tệp SwiftGen nên giới hạn chuyển đổi của nó.
Bạn cần cho SwiftGen biết cách tạo đầu ra.
Bạn phải cung cấp tên mẫu. Ở đây, swift5 là một mẫu mặc định được cung cấp bởi nhóm SwiftGen. Bạn sẽ học cách sử dụng các mẫu của riêng mình sau này.
Cuối cùng, bạn cung cấp tên tệp mà bạn muốn mã Swift mới của mình tạo ra. Hãy nhớ rằng, bạn đã xác định output_dir ở đầu tệp, có nghĩa là nó sẽ xuất thành Generated / XCAssets + Generated.swift .
Build và Run. Nếu bạn không gặp bất kỳ lỗi nào, thì quá trình tạo mã của bạn đã hoạt động!
Thêm tệp
Mở rộng folder Generated trong trình Project navigator. Hiện tại, bạn vẫn sẽ không tìm thấy tệp mới của mình. Để thêm nó, nhấp chuột phải vào Generated và chọn Add Files to “DrinksUp!”… .
Chọn XCAssets + Generated.swift . Đảm bảo Copy items if needed không được chọn, sau đó nhấp vào Add . Bây giờ, hãy mở XCAssets + Generate.swift và quan sát. Bạn sẽ thấy enumAsset. Trong enum, bạn sẽ tìm thấy các bảng liệt kê khác được xác định phù hợp với các danh mục XCAsset mà bạn đã xác định. Ví dụ:
Assets: Mỗi hình ảnh trong dự án bây giờ có một thuộc tính tĩnh được xác định.
Colors: Tất cả các màu của ứng dụng cũng có các thuộc tính tĩnh để tham khảo.
Mở Assets.xcassets . Lưu ý rằng có một nhóm hình ảnh có tên là nội dung khởi chạy . Nhưng Assets đã khai báo tất cả các thuộc tính ảnh tĩnh ở cùng một mức. SwiftGen có thể duy trì tổ chức này cho bạn nhưng không làm như vậy theo mặc định. Mở swiftgen.yml và thay thế toàn bộ mục nhập xcassets bằng mục sau:
Tại đây, bạn tận dụng khả năng của SwiftGen để tùy chỉnh đầu ra mã của bạn bằng cách thực hiện như sau:
Xác định params trên outputs của bạn.
forceProvidesNamespaces: Điều này sẽ duy trì không gian tên của bạn được tìm thấy trong danh mục nội dung.
Tham số bổ sung này đảm bảo cho dù bạn đã cung cấp bao nhiêu tên tệp inputs, SwiftGen sẽ duy trì các bảng liệt kê riêng biệt để đại diện cho từng danh mục nội dung.
Xây dựng dự án, sau đó quay lại XCAssets + Generated.swift . Bây giờ bạn sẽ thấy Assets có một enum tên mới LaunchAssets để đại diện cho cấu trúc thư mục của bạn.
Bây giờ, đã đến lúc sử dụng mã mới được tạo này để xóa bất kỳ tham chiếu chuỗi nào đến hình ảnh. Mở DrinksListView.swift . Bạn sẽ thấy Image("milkshake")bên trong các mục trên thanh công cụ được thêm vào chế độ xem. Thay thế dòng bằng dòng sau:
Image(Asset.Assets.milkshake.name)
Ở đây, bạn đã tham chiếu tên hình ảnh cho milkshake. Hiện tại, SwiftGen không hỗ trợ làm việc trực tiếp với SwiftUI. Bạn sẽ học cách tự thêm cái này sau. Hiện tại, bạn vẫn có thể sử dụng những gì có sẵn để tải nội dung hình ảnh mà không cần tham chiếu trực tiếp đến chuỗi.
Sử dụng các mẫu cơ bản bổ sung
Có một số mẫu bổ sung mà bạn có thể tận dụng mà không cần tùy chỉnh.
Làm việc với trình tạo giao diện
Ứng dụng bạn đang làm việc đang sử dụng SwiftUI. Tuy nhiên, để giới thiệu khả năng của SwiftGen để làm việc với Trình tạo giao diện, dự án mẫu bao gồm một bảng phân cảnh và một số bộ điều khiển chế độ xem. Bắt đầu bằng cách tạo mã để hỗ trợ Trình tạo giao diện hoặc Bảng phân cảnh bằng cách thêm phần sau vào swiftgen.yml :
Cho SwiftGen biết rằng bạn muốn nó tìm kiếm bất kỳ tệp nào được hỗ trợ trình tạo giao diện trong thư mục gốc của dự án của bạn.
Một điều tuyệt vời về SwiftGen là nó tách biệt khái niệm Cảnh khỏi Segues . Điều này cho biết tệp mà cảnh của bạn sẽ xuất ra.
Cuối cùng, điều này cho biết tất cả thông tin giả mạo sẽ được xuất ra ở đâu.
Xây dựng dự án của bạn. Thêm IB-Scenes + Generate.swift và IB-Segues + Generated.swift vào nhóm Generated , giống như bạn đã làm đối với XCAssets + Generated.swift .
Giờ đây, bạn có thể thay thế thông tin cảnh hoặc thông tin xác thực trong ứng dụng.
Bắt đầu bằng cách mở InformationViewController.swift . Trong InformationViewController, thay thế việc triển khai showAbout()bằng những điều sau:
Tất cả các segues sẽ tạo ra dưới dạng các enum trường hợp thực tế, điều này giúp mọi thứ dễ dàng hơn khi bạn kiểm tra xem segue nào được kích hoạt. Vì đơn giản, mã bạn đã thêm chỉ đang kích hoạt segue.
Tiếp theo, bên trong InformationView, thay thế makeUIViewController(context:)bằng những thứ sau:
func makeUIViewController(context: Context) -> some UIViewController {
StoryboardScene.Main.initialScene.instantiate()
}
Ở đây, bạn đã đơn giản hóa mã cần thiết để khởi tạo bộ điều khiển chế độ xem ban đầu của bảng phân cảnh. SwiftGen cung cấp một phương thức trợ giúp để nhanh chóng truy cập cảnh ban đầu của bạn.
Xây dựng và chạy. Nhấn vào Tìm hiểu thêm ở trên cùng bên phải. Bạn sẽ thấy một phương thức xuất hiện, hiển thị cho bạn một số thông tin về ứng dụng. Điều này được gọi vào makeUIViewController(context:)để biết chế độ xem nào cần tải.
Bây giờ, hãy nhấn vào nút About . Bạn sẽ kích hoạt segue mà bạn đã sửa đổi.
Làm việc với JSON
Cuối cùng chúng ta sẽ thêm phần hỗ trợ cho JSON. Thêm phần sau vào cuối swiftgen.yml :
Điều này hiện cung cấp một cách để tham chiếu đến bất kỳ tệp JSON nào bạn có trong tài nguyên của dự án. Bạn sẽ sử dụng điều này để chuyển đổi dữ liệu giả đi kèm với ứng dụng.
Xây dựng dự án, sau đó thêm JSON + Generated.swift vào nhóm Đã tạo của bạn .
Bây giờ, hãy mở Drink.swift . Loại bỏ hoàn toàn các struct tên được đặt tên MockData. Sau đó, thay thế phần mở rộng cho DrinkStore, được tìm thấy ở dưới cùng, bằng phần mở rộng sau:
extension DrinkStore {
static var mockData: [Drink] {
do {
let data = try JSONSerialization.data(
withJSONObject: JSONFiles.starterDrinks,
options: [])
let mockDrinks = try JSONDecoder().decode([Drink].self, from: data)
return mockDrinks
} catch {
print(error.localizedDescription)
return []
}
}
}
Tại đây, bạn sẽ thấy các tham chiếu mã của mình JSONFiles.starterDrinks. Mở MockData.json và nhận thấy khóa đầu tiên trong tệp, có tên là starterDrinks . SwiftGen đã lấy đối tượng cấp cao nhất này và cung cấp nó dưới dạng thuộc tính tĩnh trên JSONFiles để bạn tham khảo khi cần.
Xây dựng và chạy. Bạn sẽ không nhận thấy bất cứ điều gì khác so với trước đây – chỉ là đồ uống hiển thị trong danh sách.
Làm việc với chuỗi
Có lẽ một trong những tiện ích lớn nhất mà SwiftGen cung cấp là khả năng sử dụng các biến để tham chiếu các chuỗi được bản địa hóa(Localized Strings). Đó là một thực tiễn tuyệt vời để đặt bất kỳ văn bản nào bạn sẽ trình bày cho người dùng ứng dụng của mình bên trong các tệp strings hoặc tệp stringsdict được bản địa hóa . Nhưng nếu bạn đã làm điều này, bạn biết rằng một khi bạn vượt quá một số chuỗi, sẽ trở nên khó khăn để nhớ những chuỗi nào có sẵn. Cũng có cảm giác thừa rằng bạn có một chuỗi trong tệp chuỗi và… một chuỗi trong mã của bạn.
Dự án này chứa các tệp chuỗi sau:
Localizable.strings : Tệp chuỗi run of the-mill của bạn, được tạo bằng các khóa và giá trị.
Localizable.stringsdict : Bạn nên sử dụng các tệp stringdict bất cứ khi nào bạn cần lo lắng về các chuỗi đa nguyên. Loại tệp này không chỉ hỗ trợ dịch các chuỗi mà còn hỗ trợ cách đa hóa các từ cho bất kỳ biến thể nào mà một ngôn ngữ yêu cầu.
Để chuyển đổi tất cả các tệp chuỗi của bạn, hãy thêm phần sau vào swiftgen.yml :
Bạn nên biết một số điều quan trọng về những gì bạn đã thêm ở đây:
Khi bạn chuyển đổi các tệp chuỗi của mình, bạn chỉ nên sử dụng một trong các thư mục được bản địa hóa. Đối với mỗi ngôn ngữ được thêm vào, một thư mục bản địa hóa mới sẽ được tạo. Trong dự án này, bản địa hóa duy nhất là tiếng Anh. Nếu bạn thêm nhiều ngôn ngữ hơn, không cần phải sửa đổi mục nhập này để nhận các bản dịch bổ sung đó. Bởi vì tệp chuỗi phải có một tập hợp các khóa và giá trị phù hợp, bạn sẽ tham chiếu các bản dịch giống như bạn sẽ làm nếu bạn không sử dụng SwiftGen.
Bạn đã thêm một tham số mới, được đặt tên publicAccess. Nếu bạn nhìn xung quanh bất kỳ tệp nào đã tạo mà bạn đã thêm, bạn sẽ thấy tất cả các loại đều có công cụ sửa đổi quyền truy cập internal. Bằng cách sử dụng tham số này, bạn có thể thay đổi công cụ sửa đổi quyền truy cập của các bảng kê đã tạo thành công khai.
Build dự án, sau đó thêm Strings + Generated.swift vào nhóm Generated . Trước khi bạn chuyển đổi tất cả các chuỗi trong ứng dụng, điều quan trọng là phải hiểu tệp này hơi khác một chút như thế nào.
Hiểu tệp
Mở Strings + Generated.swift . Bạn sẽ thấy kiểu cha được tạo, được đặt tên L10n. Trong phần này enum, bạn sẽ thấy một số kiểu con được tạo bên trong nó DrinkDetail: Navigationvà DrinkList. Chúng tương ứng với các chuỗi được khai báo trong Localizable.strings .
Mở Localizable.strings và xem cách nó khai báo mục nhập đầu tiên:
"DrinkList.Navigation.Title" = "Drinks";
Lưu ý cách khai báo khóa bằng ký hiệu không gian tên bằng dấu chấm:
DrinkList : Điều này cho biết chuỗi này thuộc về màn hình Danh sách đồ uống.
Navigation : Cho biết chuỗi này sẽ được sử dụng trong thanh điều hướng.
Title : Cuối cùng, điều này cho biết đó là tiêu đề trong thanh điều hướng.
Bây giờ, bạn có thể chọn tổ chức và đặt tên cho các chuỗi của mình theo cách khác. Không có gì sai với điều đó – kiểu đặt tên này được sử dụng để hiển thị cách SwiftGen sẽ chuyển đổi và tổ chức mã của bạn. Đối với mỗi khoảng thời gian bạn đặt trong chuỗi của mình, SwiftGen sẽ tạo thêm một kiểu con.
Quay lại Strings + Generated.swift , bạn sẽ thấy hàm tĩnh drinksCount. SwiftGen giúp bạn dễ dàng làm việc với các chuỗi đa nguyên. Thay vì phải tạo tham chiếu đến các chuỗi được bản địa hóa và sử dụng trình định dạng chuỗi, các hàm được tạo này giúp bạn dễ dàng sử dụng một hàm lấy các giá trị của chuỗi đa nguyên của bạn.
Bây giờ, chuyển đổi tất cả các chuỗi bản địa hóa được sử dụng trong ứng dụng để trỏ đến các loại được tạo. Bắt đầu bằng cách mở DrinksListView.swift . Tiếp theo, tìm dòng mã:
Text("DrinkList.Navigation.Title")
Đổi nó thành
Text(L10n.DrinkList.Navigation.title)
Chờ một chút… L10n là gì? Đây là một phím tắt cho “bản địa hóa”. Bạn cũng có thể thấy “quốc tế hóa” được viết tắt là i18n . Nếu bạn đếm các chữ cái giữa chữ cái đầu tiên và chữ “n” cuối cùng trong một trong hai từ, bạn sẽ tìm thấy 10 hoặc 18 chữ cái tương ứng. Mặc dù điều này có ý nghĩa, sẽ không tốt nếu sử dụng một tên khác cho loại chuỗi cấp cao nhất của bạn phải không?
Mở swiftgen.yml và thêm một thuộc tính vào mục nhập chuỗi của bạn, ngay sau publicAccessđó, nó trông giống như sau:
Ở đây, bạn đã thêm tham số enumName. Điều này cho phép bạn thay đổi loại từ “L10n” thành “Strings”.
Xây dựng và chạy. Lần này, bạn sẽ có một lỗi biên dịch. Điều này là do loại L10n không còn nữa. Truy cập DrinksListView.swift và tìm:
Text(L10n.DrinkList.Navigation.title)
Thay thế nó bằng
Text(Strings.DrinkList.Navigation.title)
Bây giờ, ứng dụng của bạn đang sử dụng tên loại mới mà bạn đã cung cấp ở bước trước.
Build ứng dụng của bạn. Bạn sẽ không còn gặp bất kỳ lỗi biên dịch nào nữa.
Lưu ý : Nếu bạn vẫn gặp lỗi, bạn có thể cần phải làm sạch dự án của mình. Chọn Product ▸ Clean and build folders , sau đó tạo lại.
Tiếp theo tìm tài sản drinkCountString. Đây là mã sử dụng tệp stringdict để xử lý cách hiển thị số lượng đồ uống trong danh sách. Thay thế nó bằng những thứ sau:
private var drinkCountString: String {
Strings .drinksCount (drinkStore.drinks.count)
}
Nếu bạn so sánh nó với mã ở đó trước đây, bạn có thể thấy đây là cách nhanh hơn nhiều để tham chiếu các chuỗi đa nguyên.
Bạn nên chuyển đổi tất cả các chuỗi trong dự án khỏi sử dụng chuỗi. Mở Localizable.strings và xem tất cả các khóa chuỗi. Bạn nên tìm từng cách sử dụng các khóa này trong một tệp Swift và hoán đổi nó cho các biến được tạo bởi SwiftGen.
Khi bạn hoán đổi văn bản xếp hạng trong DrinkDetailView.swift , nó sẽ sử dụng một hàm để cung cấp chuỗi, giống như cách bạn xử lý số lượng đồ uống.
Tạo mẫu tùy chỉnh
Cho đến thời điểm này, tệp swiftgen.yml của bạn đã sử dụng các mẫu mặc định do SwiftGen cung cấp. Tất cả những thứ này được lưu trữ trong SwiftGen pod. Nếu các mẫu này không cung cấp đầy đủ chức năng bạn muốn, bạn có thể tạo các mẫu của riêng mình để tạo mã theo cách bạn muốn. Các mẫu được xây dựng bằng Stencil , một dự án mã nguồn mở cung cấp ngôn ngữ tạo mẫu cho Swift. Trong phần này, bạn sẽ học cách sửa đổi các mẫu hiện có và sử dụng chúng để tạo mã của bạn.
Nếu bạn nhìn trong trình điều hướng Dự án, bạn sẽ thấy có một thư mục có tên là Mẫu . Trong đó, có hai thư mục con: Fonts và xcassets . Với những điều này, bạn sẽ được SwiftGen cung cấp hỗ trợ để sử dụng màu sắc và phông chữ trực tiếp trong SwiftUI.
Hỗ trợ màu SwiftUI
Để thêm Colorhỗ trợ SwiftUI, hãy mở asset_swift5_swiftui.stencil . Lúc đầu, mọi thứ có thể hơi choáng ngợp vì tệp không có bất kỳ hỗ trợ cú pháp mã nào.
Trên dòng 13, thêm dòng mã sau:
import SwiftUI
Tiếp theo, quay lại swiftgen.yml . Trong mục nhập đầu tiên của bạn, đối với xcassets , hãy tìm dòng nơi bạn xác định tên mẫu:
Tại đây, bạn đã thay đổi từ sử dụng templateNamesang templatePath. Điều này yêu cầu SwiftGen sử dụng mẫu tùy chỉnh của bạn thay vì mẫu tích hợp sẵn.
Xây dựng dự án, sau đó truy cập XCAssets + Generated.swift . Ở trên cùng, bây giờ bạn sẽ thấy:
import SwiftUI
Vì bạn đã thêm nhập vào tệp Stencil nên khi mã được tạo, mã sẽ chọn thay đổi này và thêm nhập mới. Khá tuyệt, phải không?
Mở asset_swift5_swiftui.stencil và thay thế:
// Add Support For SwiftUI Here
Như dưới đây:
{{accessModifier}} private(set) lazy var color: Color = {
Color(systemColor)
}()
Trong đoạn mã này, bạn có thể xem cách sử dụng Stencil nhiều hơn một chút. Đây là những gì bạn đã thêm:
{{accessModifier}}: Đây là cách bạn nói với Stencil cách thay thế bằng thứ gì đó được cung cấp trong quá trình tạo mã. Nếu bạn nhìn vào dòng 11 của tệp này, bạn sẽ thấy accessModifier được định nghĩa là một biến. Theo mặc định, công cụ sửa đổi quyền truy cập là internal. Nếu bạn nhớ từ trước, bạn đã thấy cách bạn có thể thay đổi điều này thànhpublic
Phần còn lại của điều này thực sự chỉ là mã tiêu chuẩn. Nó tạo ra màu SwiftUI từ màu UIKit.
Lưu ý : Có một sửa đổi khác được thực hiện đối với mẫu như một phần của vật liệu khởi động. Nó thay đổi loại Colorthành SystemColor.
Build lại dự án, sau đó quay lại XCAssets + Generated.swift . Trên dòng 62, bạn sẽ thấy mã từ mẫu của mình, hiện được tạo dưới dạng mã thực.
Bây giờ, bạn cần hoán đổi bất kỳ tham chiếu được mã hóa cứng nào thành một màu để sử dụng chức năng mới của mình. Mở DrinksListView.swift và tìm vị trí đặt màu nền trước trên văn bản tiêu đề điều hướng. Thay thế nó như sau:
Đây systemColor là tham chiếu đến loại màu cụ thể của nền tảng. Sẽ như UIColor vậy nếu bạn đang sử dụng iOS và NSColor nếu bạn đang sử dụng macOS.
Hai tệp có màu được khai báo bằng cách sử dụng chuỗi:
AppMain.swift
DrinkListView.swift
Hoàn tất chuyển đổi các màu còn lại từ việc sử dụng các chuỗi trong mỗi tệp này theo ví dụ trên.
Hỗ trợ Phông chữ SwiftUI
Cũng như SwiftUI Color hiện không được hỗ trợ trong SwiftGen, Font cũng không được hỗ trợ. Bắt đầu bằng cách mở font_swift5_swiftui.stencil và thêm nhập sau vào đầu tệp:
import SwiftUI
Tiếp theo, thay thế khối mã này được tìm thấy gần cuối tệp:
// Add Support For SwiftUI here
fileprivate extension Font {
}
Bằng đoạn code dưới đây:
fileprivate extension Font {
// 1
static func mappedFont(_ name: String, textStyle: TextStyle) -> Font {
let fontStyle = mapToUIFontTextStyle(textStyle)
let fontSize = UIFont.preferredFont(forTextStyle: fontStyle).pointSize
return Font.custom(name, size: fontSize, relativeTo: textStyle)
}
// 2
static func mapToUIFontTextStyle(
_ textStyle: SwiftUI.Font.TextStyle
) -> UIFont.TextStyle {
switch textStyle {
case .largeTitle:
return .largeTitle
case .title:
return .title1
case .title2:
return .title2
case .title3:
return .title3
case .headline:
return .headline
case .subheadline:
return .subheadline
case .callout:
return .callout
case .body:
return .body
case .caption:
return .caption1
case .caption2:
return .caption2
case .footnote:
return .footnote
@unknown default:
fatalError("Missing a TextStyle mapping")
}
}
}
Đây là những gì bạn đã thêm:
mappedFont(_:textStyle:)tạo một phông chữ tùy chỉnh từ tên và TextStyle. Kiểu được sử dụng để lấy kích thước phông chữ tiêu chuẩn, mặc định cho mỗi phông chữ.
mapToUIFontTextStyle(_:)chỉ đơn giản là cung cấp ánh xạ 1: 1 của SwiftUI TextStyletới UIKitTextStyle
Khối mã này tương tự như những gì bạn đã thêm để cung cấp Color hỗ trợ. Sự khác biệt duy nhất ở đây là nó dành riêng cho việc cung cấp phông chữ trực tiếp trong SwiftUI.
Ứng dụng này sử dụng hai phông chữ, cả hai đều nằm trong nhóm Tài nguyên :
NotoSans
NotoSans-Bold
Mục nhập mới này chỉ cần biết thư mục mẹ của phông chữ bạn muốn hỗ trợ ở đâu. Mọi thứ khác tương tự như tất cả các mục nhập khác mà bạn đã thêm trước đó.
Xây dựng ứng dụng của bạn và thêm Fonts+Generated.swift vào Generated . Sau khi thực hiện, hãy mở Fonts+Generated.swift . Tại đây, bạn có thể thấy cách tổ chức họ phông chữ. Bạn sẽ thấy rằng NotoSans có sẵn các biến thể sau:
Thường
In đậm
In đậm nghiêng
In nghiêng
Giống như tất cả các mã được tạo khác trong ứng dụng, nó khá dễ sử dụng. Mở AppMain.swift và thay thế dòng đầu tiên application(_:didFinishLaunchingWithOptions:)bằng dòng sau:
let buttonFont = FontFamily.NotoSans.bold.font(size: 16)
Tại đây, bạn đặt trực tiếp kích thước phông chữ thành phông chữ NotoSans Bold(in đậm) .
Tiếp theo, truy cập DrinksListView.swift , tìm tham chiếu đầu tiên đến một phông chữ, trong NavigationLink và thay thế nó bằng như sau:
Tại đây, bạn tận dụng mã của mẫu tùy chỉnh của mình để có thể tạo phông chữ SwiftUI tùy chỉnh, kích thước phù hợp với mặc định TextStyle: trong trường hợp này body.
Cuối cùng, hoàn tất việc chuyển đổi tất cả các cách sử dụng của phông chữ trong toàn bộ ứng dụng. Trong cả DrinksListView.swift và DrinkDetailView.swift , bạn sẽ tìm thấy một số nơi đặt phông chữ. Theo ví dụ trên, bạn có thể chuyển đổi mã từ việc sử dụng một chuỗi sang trọng số thích hợp của NotoSans . Mỗi vị trí trong số này đã cho biết TextStylechúng nên có vị trí nào.
Build và Run. Ứng dụng của bạn trông vẫn giống như cách nó hoạt động khi bạn bắt đầu. Nhưng bây giờ bạn sẽ có tất cả các tài nguyên được tham chiếu theo cách an toàn về kiểu loại!
Tổng kết
Bây giờ bạn có thể sử dụng SwiftGen để:
Loại bỏ nhu cầu sử dụng chuỗi để tham chiếu tài nguyên trong ứng dụng của bạn, cho dù bạn sử dụng SwiftUI hay UIKit.
Tùy chỉnh các tệp đầu ra bằng cách sử dụng các thông số cài sẵn.
Sử dụng các mẫu của riêng bạn để tạo mã.
Để tìm hiểu thêm về nó, hãy xem SwiftGen trên GitHub . Bạn cũng có thể tìm hiểu thêm về Stencil trên GitHub .
Chúng tôi hy vọng bạn thích hướng dẫn này. Nếu bạn có bất kỳ câu hỏi hoặc ý kiến nào, hãy tham gia thảo luận của diễn đàn bên dưới!
Nội dung bài viết được dịch từ link: https://www.raywenderlich.com/23709326-swiftgen-tutorial-for-ios
Trong thời đại công nghệ 4.0, các công ty đua nhau chuyển đổi số, vì vậy có rất nhiều những ứng dụng di động được phát triển để giúp tiếp cận người dùng một cách dễ dàng hơn. Để những ứng dụng có thể vươn xa ra tầm thế giới, tiếp cận được với những người dùng nước ngoài, thì ứng dụng đó cần phải hỗ trợ đa ngôn ngữ. Vì vậy hôm nay mình sẽ hướng dẫn các bạn một số cách thực hiện một ứng dụng iOS hỗ trợ đa ngôn ngữ.
Cài đặt dự án hỗ trợ đa ngôn ngữ
Bước 1: Thực hiện thêm ngôn ngữ hỗ trợ bằng cách Chọn Project -> Info(thông tin) -> Bấm nút +
Thêm ngôn ngữBỏ tích ở các file storyboard để xCode không gen ra các file String cho các files storyboard
Bước 2: Tạo file String để chứa nội dung theo các ngôn ngữ New File… -> tìm string và chọn Strings File -> Next
Bước 3: Localize file string vừa mới tạo Chọn File vừa tạo -> Bấm vào nút Localize… -> Popup hiển thị lên thì chọn Localize
Bước 4: Tích vào ngôn ngữ bạn hỗ trợ, Chọn File String localize vừa được tạo -> Ở menu bên phải mục Localization tích vào những ngôn ngữ mà ứng dụng của bạn hỗ trợ.
Chọn ngôn ngữ hỗ trợ
Vậy là việc cài đặt đa ngôn ngữ cho ứng dụng của bạn đã hoàn thành.
Thực hiện đa ngôn ngữ với các chuỗi (Localize String)
Để tránh các lỗi sai chính tả và việc thực hiện đa ngôn ngữ trở nên dễ dàng hơn thì chúng ta sẽ tạo ra một enum Localization để liệt kê các item/string dưới dạng keyword để sử dụng như sau:
enum Localization {
static let helloWorld: String = "Hello"
static let buttonChangeLanguageTitle: String = "btn.changeLanguage"
}
Với mỗi 1 key của string chúng ta cần tạo ra value tương ứng với nó ở trong file Localizable mà chúng ta đã tạo.
Thêm key/value cho các file ngôn ngữ tương ứng:
Thêm key/value cho ngôn ngữ Tiếng AnhThêm key/value cho ngôn ngữ Tiếng Việt
NOTE: – Kết thúc của dòng code phải là dấu chấm phẩy “;” nếu 1 dòng code bị thiếu nó sẽ khiến ứng dụng của bạn hiển thị không đúng ngôn ngữ. – Key phải trùng với giá trị của enum Localization
2. Tạo một file chung để quản lí ngôn ngữ như đoạn code dưới đây
// for manage language
class LanguageManager {
static let shared = LanguageManager()
private init(){}
// save language to UserDefault
func changeLanguage(_ language: Language) {
UserDefaults.standard.set(language.rawValue, forKey: "APP_LANGUAGE")
}
/// Get language of set on app, if nil use device language
/// - Returns: current language of application
func getAppLanguage() -> Language {
if let language = UserDefaults.standard.value(forKey: "APP_LANGUAGE") as? String {
return Language(rawValue: language) ?? .english
} else {
// default lan is english
let currentLanguage: String = Locale.current.languageCode ?? Language.english.rawValue
return Language(rawValue: currentLanguage) ?? .english
}
}
// define enum language
enum Language: String {
case vietnamese = "vi"
case english = "en"
}
}
Ở đây mình tạo ra một file quản lí ngôn ngữ nhằm mục đích tập trung tất cả những tính năng liên quan tới ngôn ngữ: thay đổi, lấy ra ngôn ngữ …
Chúng ta cần lưu ngôn ngữ của ứng dụng vào Local Storage để khi người dùng tắt ứng dụng vào lại thay đổi vẫn được áp dụng. Trong trường hợp này mình chọn cách dùng UserDefault vì những lí do sau: – Dễ sử dụng – Dữ liệu cần lưu không yêu cầu bảo mật – Dữ liệu lưu có dung lượng nhỏ
3. Vậy là chúng ta đã tạo xong file quản lí ngôn ngữ. Tiếp đến để việc sử dụng localization dễ dàng chúng ta sẽ tạo ra một var localized trong extension String như sau:
Sau khi tạo xong extension thì việc thực hiện code localized trở nên rất dễ dàng. Khi cần sử dụng chúng ta chỉ cần .localized là xong.
4. Demo và cách sử dụng:
Trong ví dụ này mình sẽ tạo ra 1 label hiển thị text và một nút để thay đổi ngôn ngữ của ứng dụng. Sau đó mình tạo ra một func setDataForUI() nhằm mục đích set tất cả các data liên quan tới localization ở đây. Nếu sau đó đổi ngôn ngữ ta chỉ cần gọi lại hàm này để thực hiện set lại.
Vậy là chúng ta đã hoàn thành việc thực hiện localize String cho ứng dụng. Mình hi vọng bài viết này có thể giúp được chút gì đó cho các bạn. Cảm ơn các bạn đã đọc bài viết này, chúc các bạn thành công!
Xin chào! Bài viết này mình muốn chia sẻ cho mọi người về một số cách để code SWIFT giống với Adobe xD và hạn chế phần nào việc bị bắt bug UI không đáng.
Trong nỗi trăn trở ở mỗi dự án mobile có hàng trăm, hàng ngàn các bug UI được log. Tự nhiên mình lại nghĩ phải làm việc gì đó để giúp cho anh em code UI ngon hơn, đỡ tạo ra bug UI hơn. Vì vậy mình đã viết bài viết này hi vọng sẽ giúp anh em được phần nào trong việc tránh dính phải những bug UI.
Khi làm việc với xD các anh em thường bỏ qua các chỉ số của xD mà hay tự thực hiện code để nhìn sao cho giống UI nhất có thể, nếu không hiểu rõ bản chất nó khiến cho anh em mất khá nhiều thời gian để có thể làm giống được với file thiết kế cụ thể ở đây là file Adobe xD.
Drop shadow
Để giúp mọi người làm việc dễ dàng hơn nên mình đã tạo ra một hàm trong CALayer để giúp anh em đổ bóng bao chuẩn, bao giống xD :v Việc của anh em là lấy chỉ số ở xD và truyền vào func để setup là xong.
extension CALayer {
/// make shadow like Adobe xD, all prameters using same value with Adobe xD
/// - Parameters:
/// - color: shadow color
/// - opacity: alpha of shadow color (0-100)
/// - x: x
/// - y: y
/// - b: shadow radius
func dropShadowLikeXD(color: UIColor = .black,
opacity: Int = 50,
x: CGFloat = 0,
y: CGFloat = 3,
b: CGFloat = 6) {
masksToBounds = false
shadowColor = color.cgColor
shadowOpacity = Float(opacity) / 100.0
shadowOffset = CGSize(width: x, height: y)
shadowRadius = b / 2.0
shadowPath = nil
}
}
Để anh em dễ hiểu hơn thì mình xin giải thích như sau: – color: đây là shadow color, nó là màu shadow trên xD, cái này khá đơn giản mọi người chỉ cần lấy màu trên xD và fill vào là xong – opacity: đây là độ trong suốt của shadow, trên Adobe xD không có thuộc tính này mà giá trị này sẽ là Opacity của shadow color – x: độ lệch của shadow so với view, tính từ trái qua phải. x > 0 thì shadow lệch qua phải và ngược lại – y: độ lệch của shadow so với view, tính từ trên xuống dưới, x > thì shadow lệch xuống dưới và ngược lại. – b: là thuộc tính blur trên xD, nhưng trong Swift không có thuộc tính này, mà chỉ có shadowRadius nó là bán kính của shadow, và nó bằng 1/2 blur trên xD. – masksToBounds: thuộc tính này bằng true sẽ không thể tạo được shadow vì nó sẽ cắt mất view shadow đi. vì trong hàm mình đã set lại giá trị này bằng false.
Lưu ý: Để vừa đổ bóng được kết hợp với bo góc chúng ta cần thực hiện bo góc trước khi gọi hàm dropShadowLikeXD()
Border
Border trong Adobe xD cho phép custom khá nhiều thuộc tính, tuy nhiên mấy ông Dev tạo ra CALayer của Apple lại chỉ cho set mỗi 2 thuộc tính là borderWidth và borderColor. Vì vậy để làm giống Adobe xD chúng ta sẽ mất công hơn 1 chút. Cụ thể chúng ta sẽ cần thêm 1 enum và một func trong extension của UIView như sau:
Lưu ý: Do hàm này thực hiện thêm mới sublayer nên mọi người không nên gọi nó thực hiện ở func có thể gọi nhiều lần như: viewWillAppear(), viewDidAppear() …
Line spacing
Do định nghĩa về line spacing của Apple và Adobe xD khác nhau nên chúng ta không thể sử dụng cùng chỉ số được, vì vậy chúng ta cần tạo ra một phương thức để sửa lại công thức sao cho khớp với Adobe xD.
Adobe xD định nghĩa line spacing: là khoảng cách từ Top của dòng trên so với Top của dòng dưới liền kề.
Apple định nghĩa line spacing: là khoảng cách giữa Bot của dòng trên so với Top của dòng dưới liền kề.
Line spacing
Chúng ta có thể nhận ra sự chênh lệch giá trị line spacing của Apple so với Adobe chính là chiều cao của 1 dòng. Vậy nên mình có tạo ra một func giúp mọi người set lại giá trị line spacing giống xD mà không phải đau đầu tính toán nữa.
extension UILabel {
/// Set line spacing for label
///
/// - Parameter lineSpacing: Line spacing
func setLineSpacing(_ lineSpacing: CGFloat) {
// Check label text empty
guard let labelText: String = self.text,
let font = self.font else {
return
}
let constraintRect: CGSize = CGSize(width: self.bounds.width, height: .greatestFiniteMagnitude)
let boundingBox: CGRect = "Ok".boundingRect(with: constraintRect,
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: font],
context: nil)
let heightLabel: CGFloat = ceil(boundingBox.height)
let paragraphStyle: NSMutableParagraphStyle = NSMutableParagraphStyle()
// line spacing on xD - height of one line
paragraphStyle.lineSpacing = lineSpacing - heightLabel
let attributedString: NSMutableAttributedString
if let labelattributedText: NSAttributedString = self.attributedText {
attributedString = NSMutableAttributedString(attributedString: labelattributedText)
} else {
attributedString = NSMutableAttributedString(string: labelText)
}
// Line spacing attribute
attributedString.addAttribute(NSAttributedString.Key.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0,
length: attributedString.length))
self.attributedText = attributedString
}
}
Trên đây là những gì mình muốn chia sẻ lại cho mọi người, hi vọng nó sẽ giúp được mọi người phần nào trong công việc. Nếu mọi người có câu hỏi hay thắc mắc gì có thể đặt câu hỏi ở dưới comment mình sẽ cố gắng giải đáp những thắc mắc của mọi người.
Trong Swift thì chúng ta nên sử dụng value type như là struct hay là enum.
Tuy nhiên, có một số trường hợp bắt buộc phải dùng reference type như là class thì chúng ta phải hết sức cẩn thận khi sử dụng để tránh reference cycle.
Reference type trong Swift được quản lý bộ nhớ thông qua ARC (Autoatic Reference Counting).
Trong bài viết này, chúng ta sẽ cùng tìm hiểu ARC làm việc như nào trong các version sắp tới, và một vài điểm chú ý khi sử dụng reference type.
Object’s lifetimes
An object’s lifetime in Swift begins at initialization and ends at last use.
ARC automatically manages memory, by deallocating an object after its lifetime ends.
It determines an object’s lifetime by keeping track of its reference counts.
ARC is mainly driven by the Swift compiler which inserts retain and release operations.
At runtime, retain increments the reference count and release decrements it.
When the reference count drops to zero, the object will be deallocated.
Từ trước đến giờ chúng ta vẫn luôn hiểu rằng ARC sẽ release một vùng nhớ khi không còn con trỏ nào trỏ vào nó. Ví dụ những vùng nhớ được khai báo trong block code, như function, sẽ được release sau khi content của function này kết thúc.
Tuy nhiên, trên thực tế, mọi thứ còn hơn thế. Trong một function thì Object’s life time được tính từ khi nó được khởi tạo cho đến lần cuối cùng nó được dùng. Sau đó, lifetimes của nó sẽ kết thúc, và nó sẽ được xóa đi.
Ví dụ, với những function giả sử dài 100 LOC, và chúng ta có object myObject nào đó được sử dụng trong khoảng 30 LOC đầu, thì sau khi lần cuối cùng nó được dùng, nó sẽ bị release đi, tức là trong khoảng thời gian 70 LOC còn lại được thực thi, thì vùng nhớ của myObject đã không còn tồn tại.
Tham khảo đoạn source code bên dưới:
class Traveller {
var name: String
var destination: String?
init(name: String) {
self.name = name
}
}
func test() {
let traveller1 = Traveller(name: "NhatHM") // traveller1 Object lifetime begins
let traveller2 = traveller1
traveller2.destination = "WWDC21-10216" // traveller1 Object lifetime ends
print("Done travelling \(traveller2.destination)")
}
Ở ví dụ này, ngay sau khi object traveller1 được gán cho traveller2 thì nó đã không còn được sử dụng nữa, cho nên, đó cũng chính là thời điểm kết thúc lifetime của object traveller. Và lúc này, traveller1 đã sẵn sàng để bị release.
Việc release object traveller1 là do Swift compiler tự xử lý, và thời điểm release là do ARC thực hiện.
Như hiện tại, với XCode13 thì đã có option để optimize object life time. Tức là, có thể những bug trước đây chưa xảy ra vì coding logic observer đến object life time, thì bây giờ hoàn toàn có thể xảy ra. Bản thân Apple cũng khuyến cáo là khi coding, tránh các logic mà phụ thuộc vào object life time quá nhiều, vì với mỗi version Swift mới thì Swift compiler và ARC optimization có thể thay đổi, dẫn đến những bug tiềm ẩn có thể xảy ra.
Note: ở đa số các ngôn ngữ lập trình khác, thì object chỉ bị release khi kết thúc body của function.
Obserable object lifetimes và vấn đề của nó
Thông qua:
weak và unowned reference
Deinitializer (deinit)
Dùng weak và unowned không sai, tuy nhiên trong một số trường hợp, nó rất có thể gây ra bug tiềm ẩn (phụ thuộc vào việc ARC được optimize như nào trong các phiên bản tiếp theo).
Because relying on observed object lifetimes may work today, but it is only a coincidence.
Observed object lifetimes are an emergent property of the Swift compiler and can change as implementation details change.
Xem ví dụ dưới (captured from Apple WWDC21 video)
Ở ví dụ trên, ngay sau đoạn code traveler.account = account kết thúc thì lifetimes của traveler đã kết thúc, và với các version Swift compiler sau này thì có thể xảy ra bug, vì lúc này, traveler bị release, do đó reference count đến vùng nhớ của Traveler đã bị release, dẫn đến đoạn code trong func printSummary sẽ bị crash, nếu dùng if let để unwrap value ra thì bản thân function này cũng sẽ bị sai logic, vì lúc này logic mong muốn print ra thông tin của traveler đã không còn được thực hiện.
Đương nhiên, hiện tại code như trên sẽ chưa thể bug ngay được, vì Swift compiler đang chưa optimize đến mức là vừa kết thúc lifetimes của object thì object sẽ bị release đi luôn. Tuy nhiên, với sections này thì Apple đang nhắc nhở chúng ta vì việc tương lai, chắc chắn là việc xử lý memory của object lifetimes sẽ được thực hiện mạnh tay hơn, có thể ngay sau dòng code cuối cùng mà object được gọi thì nó sẽ bị release, và như vậy, source code của chúng ta chưa bug ở thời điểm này, nhưng tương lai có thể sẽ bị bug.
Thay đổi cách code tránh các object reference lẫn nhau (khuyến khích)
Kết luận:
Object lifetimes không tồn tại suốt vòng đời của function, mà chỉ tồn tại từ khi khởi tạo object -> lần cuối cùng object được sử dụng.
ARC optimization có thể thay đổi sau mỗi version của Swift (và Swift compiler), do đó, việc implement source code phụ thuộc quá nhiều vào object lifetimes tiềm ẩn bug.
Automatic Reference Counting aka ARC, là 1 tính năng của Swift dùng để đếm số lượng strong reference, và cho phép quản lý việc giải phóng memory khi 1 instance không còn được reference(tham chiếu) đến.
Theo như doc của Apple:
Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.
Chúng ta cũng cần nhớ rằng trong swift một reference của được định nghĩa mặc định là kiểu strong
Ví dụ :
class FirstClass: UIViewController {
let secondClass = SecondClass()
}
class SecondClass {
}
Trong ví dụ này, class FirstViewController của mình đang strong reference đến instance secondClass
2-Vấn đề của strong reference cycle
Như mình đã đề cập bên trên, ARC sẽ giúp chúng ta quản lý, phân bổ memory từ những instance không còn được tham chiếu đến.
Tuy nhiên, câu hỏi đặt ra là điều gì sẽ xảy ra khi có 2 object/instance strong reference đến nhau?
Từ strong việt hóa ra là mạnh, bền, … nghe thôi cũng cảm giác khó phá hỏng, phá hủy nó rồi đúng không? :)))
Thì strong reference cũng thế, strong reference trỏ đến 1 instance/object và sở hữu nó, quyết định đến sự tồn tại của instance/object đó.
ARC không thể có cách nào giải phóng được memory từ những kiểu instace/object này, và điều này sẽ dẫn đến memory leak.
Trong thực tế những project đã làm, thì mình rất hay thường gặp những case strong reference, và mình cũng rất hay vô tình tạo ra strong reference :v
Case mình hay gặp nhất là case khi khởi tạo delegate:
protocol DemoDelegate {
func demoFunc()
}
class FirstClass {
var delegate: DemoDelegate?
}
Trông ví dụ này có vẻ quen đúng không? :))) ở đây mình có 1 protocol DemoDelegate, và khởi tạo delegate này trong FirstClass. Và FirstClass và delegate DemoDelegate đang strong reference đến nhau.
Để giải quyết vấn đề này, chúng ta có weak và unowned.
Weak và Unowned
Weak
Trái ngược với strong pointer, weak pointer trỏ đến một instance/object và không quyết định đến sự tồn tại của instance/object đó.
Vì thế nên ARC có thể dễ dàng giải phóng bộ nhớ từ instance/object này kể cả chúng ta vẫn còn đang tham chiếu đến nó
=> Weak reference rất hữu ich, giúp chúng ra tránh được việc vô tình hay cố ý tạo ra 1 strong reference cycle.
Note: Weak reference không thể sử dụng với việc khởi tạo instance/object là let bởi vì instance/object ở một vài case vẫn phải truyền nil.
Việc sử dụng weak reference thực sự quan trọng, ví dụ nếu chúng ta để ý hơn và xem source code bên trong definition của UITableView, ta có thể nhận thấy rằng tất cả delegate(bao gồm 2 var quen thuộc là dataSource và delegate) đều được sử dụng với weak khi khởi tạo.
Và mình nghĩ là đến apple còn để ý và chú trọng đến việc sử dụng weak, tại sao chúng ta lại không làm như thế ? :v
protocol DemoDelegate {
func demoFunc()
}
class FirstClass {
weak var delegate: DemoDelegate?
}
Ok, thêm weak là xong, đơn giản phải không nào.
Tuy nhiên, khi thêm weak, lại nảy sinh vấn đề không thể compile đoạn code:
Để giải quyết, theo doc Protocols của apple:
Use a class-only protocol when the behavior defined by that protocol’s requirements assumes or requires that a conforming type has reference semantics rather than value semantics.
Để fix, chỉ cần thêm keyword class là được
protocol DemoDelegate: class {
func demoFunc()
}
class FirstClass {
weak var delegate: DemoDelegate?
}
2. unowned
Một variable kiểu unownevề cơ bản giống với variable kiểu weak, nhưng khác nhau ở chỗ: compiler sẽ make sure rằng variable này khi được gọi đến sẽ không có giá trị nil.
Vậy thực sự trong trường hợp nào nên sử dụng unowned thay vì sử dụng weak ? Vẫn theo doc của Apple:
Like a weak reference, an unowned reference doesn’t keep a strong hold on the instance it refers to. Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.
Để hiểu rõ hơn, mình có 2 ví dụ như này
Ví dụ của weak reference: mình sở hữu 1 chiếc xe đạp, nhưng 1 hôm đẹp trời nào đó, mình đổi ý không muốn đạp xe nữa, thì ở đây chiếc xe đạp vẫn còn đó, có chăng chỉ là đổi chủ, trong khi mình đang ngồi 1 chiếc 4 bánh nào đó :v
Ví dụ của unowned reference: mình chơi 1 yasuo chẳng hạn :))), thằng nhân vật của mình tăng skill theo cấp độ. Tuy nhiên, khi xám màn hình, những skill đó cũng sẽ xám theo. Có nghĩa rằng là những skill này có tuổi thọ = tuổi thọ của nhân vật mình đang chơi.
Triển khai trong code:
class Yasuo {
let name: String
let skill: Skill?
init(name: String) {
self.name = name
}
}
class Skill {
let damage: Int
unowned let champ: Yasuo
init(damage: Int, champ: Yasuo) {
self.damage= power
self.champ = champ
}
}
3-Debugging
Vậy là chúng ta đã hiểu phần nào về 2 keyword weak và unowned. Chúng dùng để tránh tình trạng vô tình tạo ra memory leak.
Nhưng sao để biết được có thực sự tránh được hay không?
Cách đơn giản và xưa như quả đất, là sử dụng print trong 2 method init và deinit
class FirstClass {
init() {
print("FirstClass is being initialized")
}
deinit {
print("FirstClass is being deinitialized")
}
}
Và để ý memory trong XCode nữa :v
4-Tổng kết
Strong, weak, unowned là những thuật ngữ cơ bản trong swift, tuy nhiên chúng ta thường – do vô ý hoặc chủ quan – bỏ qua chúng.
Điều này không thực sự ảnh hưởng quá lớn với những project nhỏ.
Tuy nhiên với những app lớn, việc quản lý bộ nhớ ra sao cho hiệu quả là 1 việc quan trọng và đáng lưu ý, bởi vì khi memory leak trở thành 1 vấn đề nghiêm trọng, thì rất khó và tốn effort để fix. 😀
Nếu đã đọc bài viết trước của mình về task group, mọi người sẽ hiểu bản chất task group, cách khởi tạo, cách add 1 task con, và cách nhận result của tất cả các task con đó.
Tuy nhiên, trong bài viết đó mình không đi sâu vào 1 phần quan trọng của task group, đó là việc handle error.
Nay mình rảnh rảnh nên lên gõ 1 bài về chủ đề này ?
Như chúng ta đã biết, TaskGroup là 1 tập hợp những task con thực thi đồng thời.
Tuy nhiên, sẽ có vấn đề được đặt ra là khi có bất kỳ 1 trong những task con gặp lỗi, task group handle việc đó như nào? Điều gì sẽ xảy ra cho những task con khác đang thực thi?
Ở bài viết này, mình sẽ đưa ra 2 cách cơ bản để handle error trong một task group, đó là:
– Throw error
– Trả về result của tất cả các task con
Ta sẽ đi sâu vào từng cách ở những phần bên dưới.
2-Throw error
Cách đầu tiên là throw error thông qua withThrowingTaskGroup.
Nhưng trước tiên, chúng ta cần phải hiểu một task group sẽ hoạt động thế nào khi một task con của nó gặp lỗi:
1. Một task group sẽ chỉ throw ra error đầu tiên mà task con của nó gặp phải, task group sau đó sẽ bỏ qua tất cả những error sau. Tức là khi bất kỳ task con nào trong quá trình thực thi mà gặp phải lỗi, task group sẽ throw lỗi đó, và sẽ không quan tâm đến bất kỳ error nào task con của nó gặp phải sau đó nữa.
2. Khi một task con throw 1 error, tất cả các task con còn lại (kể cả những task đang thực thi) sẽ được đánh dấu.
3. Những task con còn lại mà đã được đánh dấu đó sẽ tiếp tục thực thi cho đến khi chúng ta can thiệp và dừng chúng lại.
4. Khi chúng ta đã can thiệp và dừng việc thực thi của những task con còn lại đó, những task con đó sẽ không trigger việc add vào result tổng của 1 task group nữa (kể cả với những task con đã hoàn thành việc thực thi).
Để hiểu rõ hơn, mình có tạo ra 1 struct DemoChildTask, mà trong đó xử lý việc chia 2 số cho nhau, và sẽ throw ra error khi có bất kỳ phép chia nào cho 0
enum ErrorCase: Error {
case divideToZero
}
struct DemoChildTask {
let name: String
let a: Double
let b: Double
let sleepDuration: UInt64
func execute() async throws -> Double {
// Sleep for x seconds
await Task.sleep(sleepDuration * 1_000_000_000)
// Throw error when divisor is zero
guard b != 0 else {
print("\(name) throw error")
throw ErrorCase.divideToZero
}
let result = a/b
print("\(name) has been completed: \(result)")
return result
}
}
Tiếp theo, mình tạo ra 1 task group và add các task con vào trong đó. Các task con đó sẽ thực thi DivideOperation và trả ra 2 giá trị name và result.
Khi tất cả các task con hoàn thành, task group sẽ add các result con đó vào một dictionary result tổng mà trong đó chứa tất cả giá trị name và result của task con.
Bất kể khi nào một trong những task con gặp error, nó sẽ throw và truyền ra cho task group, và task group sẽ throw error đó.
let demoOperation: [DemoChildTask] = [DemoChildTask(name: "operation-0", a: 4, b: 1, sleepDuration: 3),
DemoChildTask(name: "operation-1", a: 5, b: 2, sleepDuration: 1),
DemoChildTask(name: "operation-2", a: 10, b: 5, sleepDuration: 2),
DemoChildTask(name: "operation-3", a: 5, b: 0, sleepDuration: 2),
DemoChildTask(name: "operation-4", a: 4, b: 2, sleepDuration: 5)]
Task {
do {
let allResult = try await withThrowingTaskGroup(of: (String, Double).self,
returning: [String: Double].self,
body: { taskGroup in
// Loop through operations array
for operation in demoOperation {
// Add child task to task group
taskGroup.addTask {
// Execute slow operation
let value = try await operation.execute()
// Return child task result
return (operation.name, value)
}
}
// Collect results of all child task in a dictionary
var childTaskResults = [String: Double]()
for try await result in taskGroup {
// Set operation name as key and operation result as value
childTaskResults[result.0] = result.1
}
// All child tasks finish running, thus return task group result
return childTaskResults
})
print("Task group completed: \(allResult)")
} catch {
print("Task group error: \(error)")
}
}
withThrowingTaskGroup về cơ bản giống với withTaskGroup, tuy nhiên chúng ta cần sử dụng keyword try để có thể throw error. Đồng thời cũng cần dùng try khi call func execute() của task con, điều này cho phép error throw từ execute() có thể truyền ra cho task group. Bên cạnh đó, try cũng phải được dùng khi tổng hợp result từ các task con.
Khi chạy đoạn code trên, ta sẽ nhận được đoạn log như này:
Đoạn output trên cho chúng ta thấy “operation-3” sẽ throw ra lỗi vì đã chia cho 0.
Mặc dù đoạn code trên đã khá hoàn chỉnh cho việc handle error với throw, tuy nhiên để tối ưu hơn, ta sẽ không muốn các task con khác thực thi tiếp khi một task con đã throw ra error. Đây là lúc chúng ta cần phải can thiệp vào việc thực thi của chúng như mình đã note bên trên.
Để làm được việc này, đơn giản có thể sử dụng Task.checkCancellation(). Func này dùng để check những task nào còn đang thực thi.
func execute() async throws -> Double {
// Sleep for x seconds
await Task.sleep(sleepDuration * 1_000_000_000)
// Check for cancellation.
try Task.checkCancellation()
// Throw error when divisor is zero
// guard b != 0 else {
// print("\(name) throw error")
// throw ErrorCase.divideToZero
// }
//
// let result = a/b
// print("\(name) has been completed: \(result )")
// return result
}
Về cơ bản, khi func checkCancellation() gặp bất kỳ task con nào đã được đánh dấu, nó sẽ dừng task con đó và throw ra CancellationError . Tuy nhiên, như mình đã đề cập, việc throw này không mang quá nhiều ý nghĩa vì task group sẽ reject toàn bộ những error này.
Khi chạy lại đoạn code trên với checkCancellation(), ta sẽ nhận được log như này:
3-Trả về result
Cách thứ 2 này thực ra hoàn toàn ngược lại với cách đầu tiên. Cách này cơ bản task group sẽ ignore toàn bộ task con mà throw error, mà sẽ chỉ trả về tất cả result của task con thực thi thành công.
Cách 2 này code khá giống với cách 1, chỉ khác là bây giờ sẽ sử dụng non-throwing task group thay vì throw task group, và sẽ ignore toàn bộ error với try?:
Task {
let allResult = await withTaskGroup(of: (String, Double)?.self,
returning: [String: Double].self,
body: { taskGroup in
// Loop through operations array
for operation in demoOperation {
// Add child task to task group
taskGroup.addTask {
// Execute slow operation
guard let value = try? await operation.execute() else {
return nil
}
// Return child task result
return (operation.name, value)
}
}
// Collect results of all child task in a dictionary
var childTaskResults = [String: Double]()
for await result in taskGroup.compactMap({ $0 }) {
// Set operation name as key and operation result as value
childTaskResults[result.0] = result.1
}
// All child tasks finish running, thus return task group result
return childTaskResults
})
print("Task group completed: \(allResult)")
}
Để trả về result tổng của tất cả task con, mình sử dụng compactMap để loại bỏ toàn bộ giá trị nil được trả về từ task con.
Khi chạy đoạn code trên, đây sẽ là output:
4-Tổng kết
Như vậy, với 2 bài viết của mình, mọi người đã có những cái nhìn và hiểu biết rõ ràng hơn về task group, một trong những update mới và quan trọng của swift 5.5 ?