Contents
Chào bạn đến với Fx Studio. Chúng ta tiếp tục tìm hiểu về đối tượng TextField trong hành trình SwiftUI. Phần tiếp theo này, chúng ta sẽ đi vào custom là chủ yếu. Và bạn có thể áp dụng nó cho các đối tượng SwiftUI View khác.
Nếu bạn chưa tìm hiểu về TextField thì có thể xem tại đây.Còn nếu mọi thứ ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Về mặt tool và version, các bạn tham khảo như sau:
-
- SwiftUI 2.0
- Xcode 12
Về mặt kiến thức, bạn cần biết trước các kiến thức cơ bản với SwiftUI & SwiftUI App. Tham khảo các bài viết sau, nếu bạn chưa đọc qua SwiftUI:
Về mặt demo, hầu như là demo đơn giản, vì tập trung vào từng View riêng lẻ. Do đó, bạn cũng không cần phải quá lo lắng và việc tạo mới project cũng không ảnh hưởng gì nhiều. Ngoài ra, bạn có thể checkout mã nguồn của các bài viết tại đây.
(Mặc định, mình xem như bạn đã biết về cách tạo project với SwiftUI & SwiftUI App rồi.)
Về mặt project demo, ta sẽ sử dụng lại từ phần trước của TextField. Hoặc bạn có thể tham khảo lại đoạn code sau cũng được.
struct TextFieldDemoView: View { @State var name: String = "" @State var birthday: Date = Date() @State var age: Int = 0 static var dateformater: DateFormatter { let df = DateFormatter() df.dateStyle = .short return df } static var numberFormater: NumberFormatter { let nf = NumberFormatter() nf.numberStyle = .decimal return nf } var body: some View { VStack { // Username VStack { TextField("Username", text: $name) { isBegin in if isBegin { print("Begins editing") } else { print("Finishes editing") } } onCommit: { print("commit") } .textFieldStyle(RoundedBorderTextFieldStyle()) Spacer() Text(name == "" ? "Please, input your name" : "Hello, \(name)!") Spacer() Divider() } VStack { TextField("Age", value: $age, formatter: TextFieldDemoView.numberFormater) .textFieldStyle(RoundedBorderTextFieldStyle()) Spacer() Text(age == 0 ? "Please, input your age" : "\(age)") Spacer() Divider() } // Birthday VStack { TextField( "Birthday", value: $birthday, formatter: TextFieldDemoView.dateformater) .textFieldStyle(RoundedBorderTextFieldStyle()) Spacer() Text(TextFieldDemoView.dateformater.string(from: birthday)) Spacer() Divider() } // Button VStack { Button(action: { name = "" age = 0 birthday = Date() }, label: { Text("Clear") .foregroundColor(Color.white) }) .padding() .background(Color.blue) } } .frame(height: 300.0) .padding() } }
1. More styling
Mặc dù chúng ta đã được SwiftUI cung cấp các style cơ bản rồi, tuy nhiên chúng không đủ để cho bạn thể hiện cá tính riêng của bản thân mình. Vì vậy, chúng ta sẽ đi vào các bước make color cơ bản và hay sử dụng cho một TextField trong SwiftUI.
1.1. Change font color
Bắt đầu là bằng việc bạn có thể thay đổi màu chữ trong TextField. Nó cũng tương tự như Text, bạn hãy sử dụng tới modifier .foregroundColor
là ổn.
Code ví dụ sau, bạn chỉ cần thêm vào cuối là đẹp.
TextField("Username", text: $name) { isBegin in if isBegin { print("Begins editing") } else { print("Finishes editing") } } onCommit: { print("commit") } .textFieldStyle(RoundedBorderTextFieldStyle()) .foregroundColor(Color.green)
Xem kết quả nào!
1.2. Change background color
Bạn cũng có thể thay đổi màu nền của TextField thông qua modifier .background
. Cái này đơn giản quá chắc không cần giải thích nhiều. Ahihi!
Code ví dụ như sau:
TextField("Username", text: $name) { isBegin in if isBegin { print("Begins editing") } else { print("Finishes editing") } } onCommit: { print("commit") } //.textFieldStyle(RoundedBorderTextFieldStyle()) .foregroundColor(Color.green) .background(Color.yellow)
Tạm thời, chúng ta sẽ không cần tới .textFieldStyle
, nên mình sẽ comment nó lại. Bạn xem kết quả nha.
1.3. Vertical label
Đôi lúc bạn sẽ thấy placeholder
không có ý nghĩa mấy. Nhất khi là một số TextField luôn hiển thị giá trị mặt định. Như demo thì chúng ta có TextField cho birthday
thì sẽ hiển thị ngày hiện tại ra trước.
Muốn cho người dùng hiểu thì bạn phải custom thêm một ít nữa. Chúng ta sẽ cấu trúc chúng lại như sau:
VStack(alignment: .leading) { Text("Birthday") TextField( "Birthday", value: $birthday, formatter: TextFieldDemoView.dateformater) .textFieldStyle(RoundedBorderTextFieldStyle()) }
Trong đó:
- Tạo ra một
VStack
để chứa 2 đối tượng Text & TextField - Text vẫn như xưa
- TextField vẫn không có gì mới
Nhưng với biến tấu này đem lại một cảm giác tươi mới hơn cho giao diện của bạn.
1.4. Horizontal label
Bạn thay đổi nhẹ từ VStack
sang HStack
và cảm nhận kết quả nha.
HStack { Text("Birthday") TextField( "Birthday", value: $birthday, formatter: TextFieldDemoView.dateformater) .textFieldStyle(RoundedBorderTextFieldStyle()) }
Xem kết quả nhoé!
1.5. Custom border
Đây mới chính là cái bạn cần quan tâm nhất. Chúng ta hầu như là sẽ không sử dụng giao diện mặc định. Cái cần thay đổi đầu tiên chính là border
của TextField.
Bạn xem đoạn code ví dụ sau:
TextField("Age", value: $age, formatter: TextFieldDemoView.numberFormater) .padding() .border(Color.red, width: 2)
Bạn chỉ cần sử dụng modifier .border
để thực hiện công việc vẽ lại border của TextField. Ta không cần lệ thuộc vào đám default style
kia. Xem kết quả luôn nhoé.
Chúng ta có thêm một cách custom bữa để đem lại hiệu ứng đẹp cho border TextField. Đó là sử dụng modifier .overlay
. Bạn tham khảo thêm code ví dụ sau nha.
TextField( "Birthday", value: $birthday, formatter: TextFieldDemoView.dateformater) //.textFieldStyle(RoundedBorderTextFieldStyle()) .padding() .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) )
Trong đó:
- với
.overlay
bạn sẽ làm được nhiều thứ hơn so vớiborder
- Như ví dụ ta có thể tạo 1 border với bo tròn 4 góc
- Có thể custom màu sắc, kiểu border và độ dày cho nó
Xem kết quả nhoé.
1.6. Shadow
Bạn muốn thêm hiệu ứng bóng đổ để tạo cảm giác chiều sâu cho View, thì với TextField bạn sử dụng modifier .shadow
. Nó cũng áp dụng tương tự cho các SwiftUI View khác.
Tiếp tục xem code ví dụ nha:
TextField( "Birthday", value: $birthday, formatter: TextFieldDemoView.dateformater) //.textFieldStyle(RoundedBorderTextFieldStyle()) .padding() .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) ) .shadow(color: Color.gray.opacity(1.0), radius: 3, x: 1, y: 2)
Kết quả nhoé!
2. Creating a custom text style
Bạn xem lại code ví dụ cho TextField sau khi đã làm make color và ưng ý. Thì code khá là dài. Bên cạnh đó, nếu màn hình của bạn có nhiều TextField thì phải copy cho mỗi cái nữa.
Đáng sợ VL!
Vậy chúng ta sẽ tìm một giải pháp ổn hơn. Đó chính là custom luôn TextFieldStyle và áp dụng nó cho nhiều TextField một lúc.
Bắt đầu, bạn tạo một file swift với tên tuỳ ý. Bạn xem code ví dụ sau:
import SwiftUI struct KuteTextFieldStyle: TextFieldStyle { public func _body (configuration: TextField<Self._Label>) -> some View { return configuration } }
Trong đó:
- Struct cho style mới của chúng ta sẽ kế thừa protocol TextFieldStyle
- Nó sẽ yêu cầu bạn implement một function là
_body
- Tham số chính là
configuration
, nó đại diện cho chính View của bạn - Nhiệm vụ của bạn sẽ thay đổi & làm mọi thứ với
configuration
đó
Công việc tiếp theo, bạn sẽ copy đám code mà bạn đã làm ở phần trên sang đây. Vì tất cả là modifier, nên bạn áp dụng chúng cho configuration
luôn.
import SwiftUI struct KuteTextFieldStyle: TextFieldStyle { public func _body(configuration: TextField<Self._Label>) -> some View { return configuration .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color.white) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) ) .shadow(color: Color.gray.opacity(1.0), radius: 3, x: 1, y: 2) } }
Tiếp theo, bạn sẽ thay cho Style mặc định của TextField.
TextField("Username", text: $name) .textFieldStyle(KuteTextFieldStyle())
Khá đơn giản phải không nào. Cuối bạn, bạn áp dụng cho toàn bộ màn hình với nhiều TextField luôn nào.
var body: some View { VStack { // Username VStack { TextField("Username", text: $name) .textFieldStyle(KuteTextFieldStyle()) Spacer() Text(name == "" ? "Please, input your name" : "Hello, \(name)!") Spacer() Divider() } VStack { HStack { Text("Age") TextField("Age", value: $age, formatter: TextFieldDemo2View.numberFormater) .textFieldStyle(KuteTextFieldStyle()) } Spacer() Text(age == 0 ? "Please, input your age" : "\(age)") Spacer() Divider() } // Birthday VStack { HStack { Text("Birthday") TextField( "Birthday", value: $birthday, formatter: TextFieldDemo2View.dateformater) .textFieldStyle(KuteTextFieldStyle()) } Spacer() Text(TextFieldDemoView.dateformater.string(from: birthday)) Spacer() Divider() } // Button VStack { Button(action: { name = "" age = 0 birthday = Date() }, label: { Text("Clear") .foregroundColor(Color.white) }) .padding() .background(Color.blue) } } .frame(height: 300.0) .padding() }
Kết quả hiển thị như sau:
3. Creating a custom modifier
Bạn cũng sẽ thấy với các custom trên thì chỉ áp dụng được cho các TextField, vì chúng kế thừa từ protocol TextFieldStyle. Và cũng từ câu chuyện đó nếu như:
- Bạn muốn áp dụng nó cho 1 đối tượng SwiftUI View thì sẽ như thế nào?
- Bạn vừa áp dụng 1 style này và vừa muốn thêm một style khác nữa thì sao?
Đúng thật là
THAM LAM
3.1. Create
Tuy nhiên, không phải là không có cách. Thay vì chọn phương án custom style thì chúng ta sẽ custom một modifier
xịn sò luôn.
Cái hay là nó sẽ dùng được cho nhiều đối tượng SwiftUI View, chứ không chỉ đơn giản mỗi thể loại Style mà thôi.
Bắt đầu, bạn cũng tạo mới một file với tên tuỳ ý. Code ban đầu như sau:
struct KuteViewModifier: ViewModifier { func body(content: Content) -> some View { content } }
Trong đó:
- Struct chúng ta phải kế thừa protocol ViewModifier
- Implement thêm function
body
- Tham số
content
chính là View của bạn
Bước thứ 2, bạn sẽ hoàn thiện ViewModifier mới bằng các bước thêm modifier
như là với custom style.
struct KuteViewModifier: ViewModifier { func body(content: Content) -> some View { content .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color.clear) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(lineWidth: 2) .foregroundColor(.blue) ) .shadow(color: Color.gray.opacity(1.0), radius: 3, x: 1, y: 2) } }
3.2. ModifiedContent
Cách sử dụng đơn giản với Modifier mới của bạn cho View sẽ như sau:
ModifiedContent ( content: TextField("Username", text: $name), modifier: KuteViewModifier() )
Trong đó:
- ModifiedContent là đối tượng mà bạn có được sau khi chúng nó hợp thể
content
chính là đối tượng SwiftUI View nào đómodifier
là đối tượng chúng ta vừa mới tạo struct để custom lại
Hiệu quả đem lại vẫn lợi hại như trên.
3.3. Extend View
Cách trên cũng hay, nhưng lại thấy nó không giống như bao lần bạn sử dụng modifier trong SwiftUI. Khắc phục thì bạn sẽ chế thêm như sau.
Bước đầu tiên, bạn thêm một extension
cho SwiftUI View. Ví dụ code như sau:
extension View { func kute() -> some View { ModifiedContent( content: self, modifier: KuteViewModifier() ) } }
Cũng khá là dễ hiểu cho đoạn code trên. Chỉ là bạn tách phần sử dụng ở trên ra và bọc chúng lại bằng một function khác. Nhưng thành phần chính vẫn là đối tượng ModifierContent.
Bước tiếp theo, bạn sử dụng vào các TextField thôi. Cũng xem tiếp ví dụ code nha.
TextField("Age", value: $age, formatter: TextFieldDemo2View.numberFormater) .kute()
.kute()
chính là function mà chúng đã bọc ở trên tại bước tạo extension
cho View.
Bây giờ, bạn thấy SwiftUI là dễ chưa nào. Ahihi!
4. Handle Keyboard
Để trọn vẹn cho bài viết này thì chúng ta vẫn còn một thứ quan trọng cần xử lý nữa là ….
Keyboard (bàn phím ảo)
4.1. Vấn đề
Nếu bạn build ứng dụng lên nền tảng iOS/iPadOS, sẽ có bàn phím ảo xuất hiện theo TextField. Nó giúp người dùng nhập văn bản vào TextField. Mọi việc gặp vấn đề khi bạn bị chính Keyboard đó che mất đi
các View đang hiển thị của bạn.
Giao diện bàn phím che lấp, gây khó khăn trong việc hiển thị và tương tác
Với UIKit, chúng ta có thể dùng các thư viện để xử lý vấn đề đơn giản này. Tuy nhiên, với phạm vi bài viết chúng ta không thể nào dùng thư viện và tất cả phải code tay hết.
Cách xử lý thì có vô vàng cách, đại diện như sau:
- Thay đổi lại
frame
của màn hình - Tuỳ chỉnh là
offset
của TextField - Sử dụng Scroll và thay đổi
offset
của nó
Sẽ có nhiều cách giải quyết hay hơn nữa và mình sẽ chọn cách đơn giản nhất. Vì với SwiftUI thì mọi thứ từ đơn giản lại trở nên phức tạp. Ví dụ như:
- Bạn không có AutoLayout để neo các View lại với nhau
- Bạn không tính toán và thay đổi
frame
được - Còn phải đồng bộ dữ liệu với nhau
- …
Còn sau đây là cách giải quyết.
4.2. Keyboard Manager
Để quản lý việc ẩn hiện Keyboard, tốt nhất chúng ta phải cần một class để quản lý chúng. Class này sẽ đặc biệt một tí. Bạn xem code của nó như sau:
import SwiftUI class FXKeyboardManager : ObservableObject { @Published var customHeight: CGFloat = 0 @Published var keyboardHeight: CGFloat = 0 @Published var isVisible = false init() { NotificationCenter.default.addObserver(self, selector: #selector(keyboardVisibilityChanged), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } deinit { NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } @objc private func keyboardVisibilityChanged(_ notification: Notification) { guard let userInfo = notification.userInfo else { return } guard let keyboardEndFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } isVisible = keyboardEndFrame.minY < UIScreen.main.bounds.height keyboardHeight = isVisible ? keyboardEndFrame.height : 0 keyboardHeight = customHeight == 0 ? keyboardHeight : customHeight } }
Trong đó:
- Class phải kế thừa lại protocol ObservableObject để giúp các đối tượng của nó có thể tương tác với các View trong SwiftUI.
- Các thuộc tính được khai báo với
@Published
sẽ báo cho bên ngoài biết được khi nào chúng thay đổi giá trị. Ràng buộc các View với chúng. Khi nó thay đổi thì View sẽ thay đổi theo. - Các function còn lại chỉ liên quan tới tính toán và logic
Về mặt logic thì như sau:
- Sử dụng
UIResponder.keyboardWillChangeFrameNotification
để theo dõi việc ẩn hiện của Keyboard - Nếu xuất hiện bàn phím thì
keyboardHeight
sẽ bằng kích thước bàn phímkeyboardHeight
sẽ bằng kích thước custom nào đó, nếu ý đồ người lập trình muốn toàn bộ View di chuyển nhẹ lên thôi
- Nếu bàn phím ẩn đi thì
keyboardHeigh = 0
mọi thứ sẽ về lại như ban đầu với
Đây chính là các sử dụng Declaring Data với kiểu dữ liệu tham chiếu trong SwiftUI. Bạn xem lại tại đây.
4.3. Sử dụng
Tại file SwiftUI View mà chúng ta sử dụng, bạn thêm một thuộc tính. Điều quan trọng là ý đồ bạn muốn sử dụng thuộc tính đó như thế nào. Với tư cách là:
- Nguồn dữ liệu chính và nhận mọi cập nhật từ các View hay các nơi khác, thì bạn sẽ khai báo với
@StateObject
- Chỉ mang tính chất là
binding
dữ liệu với một nguồn nào đó ở bên ngoài, thì bạn sẽ khai báo với@ObservedObject
Với cách nào cũng okay hết, còn mình sẽ chọn theo nguồn sự thật chân lý
vì nó là duy nhất. Mình sẽ thêm thuộc tính sau vào SwiftUI View của chúng ta đang làm demo từ đầu cho đến bây giờ.
@StateObject var keyboardManager = FXKeyboardManager()
Về mặt hiển thị sao cho đẹp thì ta sẽ lợi dụng yếu tố .padding
và áp dụng nó cho phần tử lớn nhất trong SwiftUI View của chúng ta.
Ví dụ:
VStack { // ..... } .padding(.bottom, keyboardManager.keyboardHeight) .padding()
Bạn sẽ lợi dùng việc padding
cho cạnh ở dưới mà thôi. Hãy build project lên simulator và nhớ kích hoạt thêm bàn phím ảo nữa. Và cảm nhận kết quả.
Cuối cùng, nhiều lúc bạn chỉ muốn View của bạn nhích lên 1 tí thôi. Còn cách trên thì chúng nó lên cao quá, có khi đi luôn. Bạn sẽ thêm phần chỉnh sửa cho từng TextField. Xem ví dụ cho một TextField trong màn hình như sau.
TextField("Username", text: $name) { isBegin in if isBegin { keyboardManager.customHeight = 50.0 } else { keyboardManager.customHeight = 0 } } onCommit: {} .kute()
Người hùng ở đây chính là thuộc tính customHeight
của chúng ta. Bây giờ, build lại project và xem kết quả nào.
Tạm kết
- Áp dụng được nhiều modifier để thêm phần giao diện TextField được xinh đẹp
- Custom Style cho TextField
- Custom ViewModifier để áp dụng cho TextField hoặc có thể bất cứ View nào cũng được
- Xử lý bàn phím ảo và giao diện hiển thị cho phù hợp
Okay! Tới đây, mình xin kết thúc bài viết về đối tượng TextField trong SwiftUI. Và nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)