Ở phần 3 này, chúng ta sẽ tiếp tục làm việc cùng UI trong màn hình các món ăn trong app FoodTracker.
Ở bài trước chúng ta đã thêm image view và thêm image picker vào để cho người dùng có thể chọn ảnh từ album.
Kết quả bài trước:

Trong bài học ngày hôm nay, chúng ta sẽ tìm hiểu và thực hành các kiến thức sau.

  1. Hiểu được vòng đời của view controller và callbacks của nó
  2. Chuyển dữ liệu qua lại giữa các controller
  3. Giải phóng view controller
  4. Sử dụng gesture recognizers để tạo các event
  5. Dự liệu trước trạng thái của object dựa trên cấu trúc class UIView/UIControl
  6. Thêm ảnh vào để sử dụng trong project

I. Vòng đời của View Controller

Một object của class UIViewController (hay subclass của nó) có sẵn các methods để quản lý cấu trúc view của nó. iOS tự động gọi các methods đó vào thời điểm thích hợp khi mà trạng thái của view controller thay đổi. Khi bạn tạo một subclass của view controller, nó sẽ kế thừa các class được định nghĩa ở UIViewController và cho phép bạn thêm vào các chỉnh sửa của mình nếu cần thiết.
Vì vậy việc nắm chắc khi nào hệ thống gọi tới method nào rất quan trọng để bạn đưa ra quyết định setup thêm hay ẩn một view nào đó vào bước nào cho phù hợp.
Dưới đây là sơ đồ thể hiện vòng đời của view controller:

iOS gọi các method theo thứ tự dưới đây:

  • viewDidLoad() : Được gọi khi các view ở view controller được tạo và được load từ storyboard. View controller outlet sẽ được đảm bảo có giá trị hợp lệ vào thời điểm method này được gọi ra. Bạn có thể thực hiện các setup mình muốn cho view controller tại method này.
    Đặc biệt iOS chỉ gọi method này đúng 1 lần khi các content view được tạo lần đầu; tuy nhiên các content view này không bắt buộc phải được tạo ra ngay khi controller vừa được khởi tạo. Mà nó được tạo ra thụ động khi hệ thống hay một phần code nào đó truy cập vào view property của controller.
  • viewWillAppear() : Được gọi ngay trước khi các content view của view controller được thêm vào cấu trúc view của app. Sử dụng method này khi muốn thông báo cho bất kì xử lý tiếp sau nào biết rằng nó cần phải được thực hiện trước khi content view được hiển thị trên màn hình. Mặc dù tên nó nghĩa là “Sẽ xuất hiện” nhưng không có nghĩa là content view đó được đảm bảo chắc chắn được hiển thị trên màn hình, bởi vì nó có thể ở sau một đối tượng view khác, hay bị ẩn đi. Method này chỉ đơn giản đảm bảo rằng content view sẽ được thêm vào cấu trúc view của app mà thôi.
  • viewWillAppear() : Được gọi ngay trước khi các content view của view controller được thêm vào cấu trúc view của app. Sử dụng method này khi muốn thông báo cho bất kì xử lý tiếp sau nào biết rằng nó cần phải được thực hiện trước khi content view được hiển thị trên màn hình. Mặc dù tên nó nghĩa là “Sẽ xuất hiện” nhưng không có nghĩa là content view đó được đảm bảo chắc chắn được hiển thị trên màn hình, bởi vì nó có thể ở sau một đối tượng view khác, hay bị ẩn đi. Method này chỉ đơn giản đảm bảo rằng content view sẽ được thêm vào cấu trúc view của app mà thôi.
  • viewDidAppear() : Ngược lại với method viewWillAppear(), method này được gọi ngay sau khi các content view của view controller được thêm vào cấu trúc view của app. Sử dụng method này để thông báo cho các sử lý (như là lấy dữ liệu, hiển thị animation…vv) rằng chúng cần được thực thiện ngay khi content view được hiển thị trên màn hình.
  • viewWillDisappear() : Được gọi ngay trước khi content view bị xoá khỏi cấu trúc view của app. Sử dụng method này để thực hiện dọn dẹp các tasks như là commit các thay đổi hay loại bỏ trạng thái của the first responder.
  • viewDidDisappear() : Được gọi ngay sau khi content view bị xoá khỏi cấu trúc view của app. Sử dụng method này để thực hiện các action cần thiết nhằm làm sạch và loại bỏ các phần không còn cần thiết.
    Cho tới thời điểm này chúng ta mới có hàm:
override func viewDidLoad() {
    super.viewDidLoad()
    
    // Handle the text field’s user input through delegate callbacks.
    nameTextField.delegate = self
}

II. Thêm ảnh món ăn

Tiếp theo chúng ta sẽ hoàn thiện màn hìn hiển thị món ăn bằng việc hiển thị bức ảnh của một món ăn cụ thể. Để làm việc đó chúng ta sẽ sử dụng image view (UIImageView).
Các bước tiến hành.

  1. Mở storyboard
  2. Mở Object library (View > Utilities > Show Object Library.)
  3. Gõ “image view” để tìm Image View object nhanh hơn
  4. Kéo thả object đó vào stack và ở vị trí ngay dưới button set label
  5. Mở Size inspector
  6. Ở Intrinsic Size, chọn Placeholder.
  7. Nhập 320 cho cả chiều dài và rộng. Chú ý intrinsic content size chỉ là size để hiển thị khi chưa có ảnh được chọn. Khi một bức ảnh được upload vào thì nó sẽ hiển thị theo kích thước thực của bức ảnh đó.
  8. Ở góc trái của canvas, mở menu Pin
  9. Chọn Aspect Ratio
  10. Ở Pin menu click button Add 1 Constraints
    Hiện tại ảnh của chúng ta có tỉ lệ 1:1 nên nó sẽ luôn là hình vuông
  11. Attribute inspector tick vào checkbox User Interaction Enabled để cho phép người dùng có thể tương tác với bức ảnh.

III. Hiển thị ảnh default

Chúng ta sẽ cho hiển thị một bức ảnh default để người dùng biết rằng có thể tương tác với image view này được.
Sử dụng ảnh dưới đây làm ảnh Default nhé các bạn.

Các bước để thêm một bức ảnh vào project của các bạn.

  1. project navigator chọn Assets.xcassets để xem asset catalog.
  2. Ở dưới góc trái, click vào button (+) và chọn New Image Set từ pop-up được hiện lên
  3. Click đúp vào bức ảnh và set tên của nó là defaultPhoto
  4. Chọn bức ảnh chúng ta vừa download về ở trên
  5. Kéo thả vào ô 2x
    Ảnh 2x hiển thị cho màn hình Simulator của iPhone 7 chúng ta sử dụng trong bài học này.

Hiển thị ảnh default đã tải lên ở image view

  1. Mở storyboard
  2. Chọn image view
  3. Mở Attribute inspector
  4. Tìm trường Image và chọn defaultPhoto
    Chạy Simulator và bạn sẽ thấy kết quả hiển thị trên màn hình.

IV. Liên kết Image view vào Code

Đến thời điểm này, bạn cần phải viết các function để hiển thị ảnh tại vùng image view. Việc đầu tiên là phải kết nối image view vào source code trong file ViewController.swift

  1. Sử dụng Assistant Editor bằng cách click vào button tương ứng
  2. Ở Storyboard chọn image view
  3. Control-drag image view từ canvas vào code ngay phía dưới outlets trong file ViewController.swift
  4. Ở mục Name trong dialog mới xuất hiện, gõ photoImageView
  5. Click Connect
    Xcode sẽ tự động sinh source code cho chúng ta.
@IBOutlet weak var photoImageView: UIImageView!

Bây giờ bạn sẽ cần phải làm sao để cho phép user có thể thay đổi ảnh món ăn khi tap vào image view. Nghĩa là bạn cần định nghĩa một action để thay đổi ảnh khi người dùng tap vào.

V. Tạo Gesture Recognizer

Một image view không phải là một đối tượng dùng cho việc điều khiển. Nghĩa là bạn không thể tạo một trigger là khi tap vào nó thì nó phản ứng hay tác động vào một đối tượng khác được.
Nhưng rất may mắn chúng ta có một cách khác để làm điều tương tự, đó là sử dụng gesture recognizer. Gesture recognizers là một object mà bạn có thể gắn vào view, cho phép phản hồi các thao tác của user. Bạn có thể viết các action để phản hồi lại các thao tác ấy và chúng sẽ được gọi ngay sau khi gesture recognizer xác nhận rằng nó đã được assigned gesture của người dùng.
Cách gắn gesture recognizer cho image view

  1. Mở Object library.
  2. tap gesture để tìm kiếm nhanh Tap Gesture Recognizer object
  3. Drag nó vào image view trên màn hình

    Khi đó Tap Gesture Recognizer sẽ xuất hiện trên [scene dock)(https://developer.apple.com/library/content/referencelibrary/GettingStarted/DevelopiOSAppsSwift/GlossaryDefinitions.html#//apple_ref/doc/uid/TP40015214-CH12-SW63)

VI. Kết nối Gesture Recognizer với Code

  1. Control-drag gesture recognizer vào code ngay dưới dòng “//MARK: Actions comment in ViewController.swift”
  2. Ở Dialog vừa xuất hiện chọn Action cho mục Connection
  3. selectImageFromPhotoLibrary cho Name
  4. UITapGestureRecognizer cho Type
  5. Click Connect
    Ta có source code được tự động sinh ra.
@IBAction func selectImageFromPhotoLibrary(_ sender: UITapGestureRecognizer) {
}

VII. Tạo Image Picker phản hồi khi User Tap vào Image View

Tại đây bạn sẽ phải mở Photo library trên máy để người dùng chọn ảnh sau khi đã tap vào image view. Và UIImagePickerController là class cho phép bạn thực hiện điều đó. Và để làm việc với image picker controller, bạn cần có delegate tương ứng; tên của nó là UIImagePickerControllerDelegate.
Mặc khác bạn cũng cần phải hiển thị Image Picker nên chúng ta cần điều hướng hợp lý với UINavigationControllerDelegate protocol.
Để đưa UIImagePickerControllerDelegate and UINavigationControllerDelegate protocols vào ViewController.

  1. Chuyển editor trở về mode standard
  2. Chọn ViewController.swift
  3. Tìm nơi định nghĩa class ViewController
class ViewController: UIViewController, UITextFieldDelegate {
  1. Thêm UIImagePickerControllerDelegate và UINavigationControllerDelegate như dưới đây
class ViewController: UIViewController, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

Implement method selectImageFromPhotoLibrary action

  1. Tìm method selectImageFromPhotoLibrary
  2. Đảm bảo khi user tap vào image view lúc đang gõ text thì ẩn bàn phím đi
// Hide the keyboard.
nameTextField.resignFirstResponder()
  1. Tạo image picker controller
// UIImagePickerController is a view controller that lets a user pick media from their photo library.
let imagePickerController = UIImagePickerController()
  1. Cho phép lấy photos
// Only allow photos to be picked, not taken.
imagePickerController.sourceType = .photoLibrary

Kiểu của imagePickerController.sourceType đã được định nghĩa sẵn trong UIImagePickerControllerSourceType rồi, nó là một enumeration. Nên thay vì viết đầy đủ UIImagePickerControllerSourceType.photoLibrary thì ta chỉ cần viết .photoLibrary là được.

  1. Set ViewController làm image picker controller delegate
// Make sure ViewController is notified when the user picks an image.
imagePickerController.delegate = self
  1. Thêm method present
present(imagePickerController, animated: true, completion: nil)

Method này yêu cầu ViewController hiển thị view controller định nghĩa bởi imagePickerController . completion refer tới completion handler, là phần code thực hiện sau khi method được hoàn thành.

Hiện tại action selectImageFromPhotoLibrary sẽ có dạng như sau

@IBAction func selectImageFromPhotoLibrary(_ sender: UITapGestureRecognizer) {
    
    // Hide the keyboard.
    nameTextField.resignFirstResponder()
    
    // UIImagePickerController is a view controller that lets a user pick media from their photo library.
    let imagePickerController = UIImagePickerController()
    
    // Only allow photos to be picked, not taken.
    imagePickerController.sourceType = .photoLibrary
    
    // Make sure ViewController is notified when the user picks an image.
    imagePickerController.delegate = self
    present(imagePickerController, animated: true, completion: nil)
}

Sau khi đã có image picker controller rồi bạn sẽ tương tác với nó thông qua các delegate method. Cụ thể, để cho phép user chọn ảnh bạn cần implement 2 delegate methode sau:

func imagePickerControllerDidCancel(_ picker: UIImagePickerController)
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any])

Method imagePickerControllerDidCancel sẽ được gọi khi user ấn button Cancel của image picker, nó sẽ giải phóng UIImagePickerController.

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    // Dismiss the picker if the user canceled.
    dismiss(animated: true, completion: nil)
}

Method imagePickerController cho phép bạn thực hiện một số xử lý với ảnh khi mà user đã chọn ảnh đó rồi, ví dụ như trong trường hợp này ta sẽ lấy ảnh đó và hiển thị trên image view.

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    
    // The info dictionary may contain multiple representations of the image. You want to use the original.
    guard let selectedImage = info[UIImagePickerControllerOriginalImage] as? UIImage else {
        fatalError("Expected a dictionary containing an image, but was provided the following: (info)")
    }
    
    // Set photoImageView to display the selected image.
    photoImageView.image = selectedImage
    
    // Dismiss the picker.
    dismiss(animated: true, completion: nil)
}

Chạy thử Simulator và bạn sẽ thấy có lỗi hiện ra.

App bị crash bởi lỗi SIGABRT, nghĩa là nó đủ nguy hiểm để có thể dừng ngay hoạt động của app. Lỗi này xảy ra khi bạn cố gắng hiển thị image picker ra mà chưa nhận được quyền cho phép của user để truy cập vào photo library. Ở iOS 10 và các phiên bản lớn hơn bạn phải cung cấp usage description để lấy quyền đó.

  1. Ở project navigator chọn Info.plist
  2. Click vào + button ở dòng cuối.
  3. Ở pop-up menu, scroll down và chọn Privacy - Photo Library Usage Description.
  4. Ở row mới xuất hiện điền Allows you to add photos to your meals
  5. Ấn Return key

Bây giờ các bạn có thể chạy app và kiểm tra kết quả.
Việc upload ảnh vào Simulator rất đơn giản, bạn chỉ việc kéo thả từ máy tính vào Simulator là được thôi nhé.

Bây giờ bạn click vào image view và chọn ảnh vừa upload.

Done!

VIII. Part 4

Bài sau chúng ta sẽ cùng tự tạo một controller của riêng mình ! (honho)