whoistuanhai
[createdBy: @"TranTuanHai"]
60 posts
Don't wanna be here? Send us removal request.
whoistuanhai · 5 years ago
Text
ForEach với index trong SwiftUI
Một trong những việc cơ bản chúng ta thường làm là hiển thị một mảng các phần tử. Trong SwiftUI, bạn có thể sử dụng List hoặc ForEach. Ví dụ:
struct Person { let id: UUID let name: String } ForEach(persons, id: \.id) { person in Text(person.name) }
Lưu ý: Nếu Person adopt Identifiable protocol, ta có thể bỏ tham số id trong ForEach.
struct Person: Identifiable { let id: UUID let name: String } ForEach(persons) { person in Text(person.name) }
Nếu muốn hiển thị cả index của phần tử trong mảng, có nhiều cách, tốt có, dở có. Sau đây là các option mà bạn có thể chọn.
enumerated()
Hàm enumerated khi được gọi trên một Array trả về kiểu EnumeratedSequence, cũng là một Collection. Mỗi phần tử của Collection này là một tuple gồm 2 giá trị với label tương ứng là offset và element.
Để sử dụng ForEach với cách này, trước tiên phải cung cấp cho nó một ID, vì tuple không adopt Identifiable protocol.
ForEach(persons.enumerated(), id: \.offset ) { index, person in Text("\(index): \(person) ") }
Nhưng thế là chưa đủ, vì compiler sẽ báo lỗi:
Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'EnumeratedSequence<[Person]>' conform to 'RandomAccessCollection'
Đơn giản vì ForEach yêu cầu một RandomAccessCollection, mà EnumeratedSequence thì không. Ta có thể giải quyết một cách đơn giản bằng cách tạo một Array với EnumeratedSequence vừa tạo, vì Array là một kiểu RandomAccessCollection.
ForEach(Array(persons.enumerated()), id: \.offset ) { index, person in Text("\(index): \(person) ") }
Range
Sử dụng hàm enumerated thì tiện thật vì nó trả về cùng lúc index và element ta cần dùng. Nhưng ta cũng có thể lấy về index trước, rồi từ index lấy về object tương ứng bằng cách sử dụng literal Range như sau:
ForEach(0..<persons.count), id: \.offset ) { index in Text("\(index): \(persons[index]) ") }
Cách này được nhiều người khuyên dùng, nhưng "có thể" gây crash nếu collection bị thay đổi, dẫn tới việc SwiftUI vô tình truy cập vào invalid index.
Một cách an toàn hơn là sử dụng indices, vì indices cũng trả về một Range:
ForEach(persons.indices){ index in Text("\(index): \(persons[index]) ") }
Lưu ý: Bạn cần chắc chắn dữ liệu của mình thuộc kiểu Array chứ không phải ArraySlice khi sử dụng range. Giả sử dữ liệu của bạn là suffix của một mảng:
let persons = [ Person(id: UUID(), name: "Hai"), Person(id: UUID(), name: "Duc"), Person(id: UUID(), name: "Minh"), Person(id: UUID(), name: "Chien") ] let personSlice = persons.suffix(2) print(personSlice.count) // 2 for index in personSlice.indices { print(index) /* 2 : Person(id: 4BAE4FFC-FD72-4A35-B9F7-79E04E1393E7, name: "Minh") 3 : Person(id: 40ABBB3E-8337-4C8B-A520-E956FC37C8B8, name: "Chien") */ }
Mặc dù personSlice có count = 2, nhưng index của nó vẫn dựa trên mảng persons. Vì thế, nếu thay indices bằng literal range, ta sẽ nhận được một lỗi Index out of bound và crash app.
3 notes · View notes
whoistuanhai · 5 years ago
Text
Cài hackintosh trên HP Elitebook 8470p sử dụng Clover UEFI hotpatch
Bài này chủ yếu để note lại để tra cứu nên mình sẽ không giải thích nhiều mà chủ yếu hướng dẫn kiểu mì ăn liền, phù hợp cho những ai đang sở hữu laptop HP Elitebook 8470p với cấu hình giống như mình 😀
Mình không phải là một chuyên gia cài hackintosh. Ban đầu khi mới học iOS, mình được cài miễn phí do đăng ký khóa học iOS của Techmaster. Lúc đó, năm 2014, con laptop của mình là Asus K40IJ, vừa cùi vừa khó cài, lại còn không nhận được QE/CI làm xem video còn bị giật tung đít. Sau đó, mình mua con HP Elitebook 8470p này và tự tìm hiểu để cài. May mắn là con laptop sau này của mình khá dễ cài và có guide rất chi tiết.
Nội dung của bài viết chủ yếu dựa trên hướng dẫn sau, do RehabMan, một moderator nổi tiếng trên trang tonymacx86. Mình đã sử dụng hướng dẫn này để cài macOS các version từ 10.11 đến 10.14 dual boot với Windows 8 và chạy khá ổn.
Tại thời điểm này (9/11/2019) thì macOS đã ra version 10.15 Catalina. Tuy nhiên theo tham khảo từ nhiều nguồn thì bản mới này còn nhiều lỗi nên mình chưa cài vội, vì thế mình sẽ chọn bản 10.14.5 trong bài viết này.
Cấu hình máy
Brand: HP Elitebook 8470p - 7th series laptop
CPU: Intel core i5 3320M (Ivy Bridge)
GPU: Intel HD4000 (không card rời)
Wifi: phải thay do card wifi của máy không được hỗ trợ bởi hackintosh. Mình dùng con Broadcom 943224 mua 200k của bạn Sơn nhà ở Ngụy Như Kon Tum, dùng 5 năm rồi vẫn chạy tốt, mỗi tội không có bluetooth 😛. Bạn Sơn này cũng là người cài cho mình con máy đầu tiên để học iOS.
RAM: 8 Gb (2 thanh 4 Gb 1333 Mhz).
Hard drive: 1 ổ HDD 500GB, chia làm 3 phân vùng. Phân vùng cài macOS 150 Gb, phân vùng cài Windows 100 Gb, còn lại để chứa dữ liệu. Lưu ý là ổ đĩa phải được định dạng kiểu GPT thay vì MBR. Nếu ổ của bạn đang được định dạng là MBR thì bạn phải chuyển đổi sang GPT. Kinh nghiệm của mình là backup tất cả dữ liệu của mình lên mây rồi format lại cả ổ đĩa và chia lại ổ từ đầu.
BIOS setting
Khởi động máy và ấn F10 để vào BIOS setting. Sửa lại các option sau:
Enabled UEFI boot (hybrid/with CSM)
Secure boot - Disabled
Fast boot - Disabled
Serial port - Disabled
LAN/WLAN switching - Disabled
Extended Idle Power States - Disabled
Wake on LAN and Wake on USB - Disabled
Firewire/IEEE 1394 - Disabled
Save và khởi động lại máy.
Cài đặt Window
Việc tiếp theo bạn cần làm là cài Window. Lưu ý là bạn phải tạo bộ cài Window theo chế độ UEFI vì mặc định Window sẽ được cài theo chế độ legacy. Hướng dẫn tạo bộ cài ở đây
Sau đó thì cài thôi, cài Win dễ mà!
Khi cài Window theo chế độ UEFI, Window sẽ tự động tạo ra một phân vùng EFI để phục vụ cho việc cài macOS sau này.
Chuẩn bị USB cài đặt macOS
Chuẩn bị một USB dung lượng 8 Gb trở lên, khuyến khích nên dùng USB 2.0 để tránh lỗi. Lưu ý để tạo USB cài đặt bạn cần phải truy cập được vào một bản macOS thật. Bạn có thể mượn máy macbook hoặc máy đã cài hackintosh của bạn bè hoặc đập vào mặt chúng nó cái guide này và bảo nó tạo hộ bạn, dễ mà 😄.
1. Cài Clover lên USB
1.1. Phân vùng USB
Liệt kê các ổ đĩa trên máy:
diskutil list
Format và chia USB ra thành 2 partition, một partition để cài clover, một partition để cài macOS:
diskutil partitionDisk /dev/disk1 2 MBR FAT32 "CLOVER EFI" 200Mi HFS+J "install_osx" R
Lưu ý: trên máy mình /dev/disk1 là USB, tùy vào máy mà thay đổi phần này.
Sau bước này, USB của bạn sẽ có 2 phân vùng với tên và định dạng và dung lượng như sau:
CLOVER EFI - FAT32 - 200 Mb
install_osx - HFS+ - dung lượng còn lại của USB
1.2. Cài Clover
Chọn bản Clover mới nhất ở đây: Download Clover. Phiên bản tại ngày mình viết bài là v2.5k r5098.
Cài Clover trên CLOVER EFI partition của USB với các option sau trong trang Customize :
Install for UEFI booting only
Install Clover in the ESP
Recommended driver (sub-menu của UEFI Drivers)
ApfsDriverLoader (sub-menu của File System drivers)
AptioMemoryFix (sub-menu của UEFI Drivers->Memory fix drivers)
OsxFatBinaryDrv (sub-menu của UEFI Drivers->Additional drivers)
Black Green Moody (sub-menu của Theme)
2. Copy driver còn thiếu lên USB
Sau khi cài Clover lên USB, trong folder EFI/CLOVER/drivers/ đã có gần như đầy đủ driver. Nhưng vẫn còn thiếu 2 driver quan trọng:
HFSPlus.efi
HPFanReset.efi
Download và copy vào folder EFI/CLOVER/drivers/UEFI
3. Copy kext lên USB
Xóa hết các folder trong EFI/CLOVER/kexts mà chỉ giữ lại folder Other. Download và copy các kext sau vào thư mục ``EFI/CLOVER/kexts/Other` trên USB:
FakeSMC.kext
VoodooPS2Controller.kext
USBInjectAll.kext
Lilu.kext
WhateverGreen.kext
SATA-unsupported.kext
IntelMausiEthernet
4. Config
Thay thế file confilg.plist mà Clover tạo ra trong USB bằng file config.plist của Rehabman đã chỉnh sửa.
Download (nhớ save dưới định dạng .plist nhé) và copy vào EFI/CLOVER
5. Copy macOS installer vào USB
Trước tiên, ta cần phải download macOS Installer. Thông thường, bạn chỉ cần lên Mac App Store và search macOS Mojave và download. Tuy nhiên, Apple sau khi ra một phiên bản mới của macOS thường giấu đi các phiên bản cũ trên Mac App Store. Tại thời điểm này, macOS Catalina đã ra. Vì thế, khi bạn lên Mac App Store và search "macOS Mojave" thì không có kết quả. Mình có google và tìm ra được link này, click vào thì sẽ nhảy thẳng đến trang download của macOS Mojave trên Mac App Store.
Sau khi download, ta sẽ sử dụng createinstallmedia method để copy macOS installer vào phân vùng install_osx trên USB.
Copy installer image:
sudo "/Applications/Install macOS Mojave.app/Contents/Resources/createinstallmedia" --volume /Volumes/install_osx --nointeraction
Đổi tên phân vùng:
sudo diskutil rename "Install macOS Mojave" install_osx
Vậy là chúng ta đã hoàn tất việc tạo một USB boot để cài đặt macOS. Bước tiếp theo sẽ là cài đặt macOS lên HDD.
Cài macOS lên HDD
Khởi động máy và ấn F9 để vào boot menu. Chọn USB device mà ta vừa tạo.
Màn hình Clover xuất hiện, chọn Install macOS Mojave.
Sau khi load xong, màn hình cài đặt xuất hiện. Sử dụng Disk Utility để tạo một phân vùng 150 Gb với định dạng Mac OS Extended (Journaled) và cài macOS lên đó.
Khởi động lại và lại boot vào USB giống bước 1
Màn hình Clover xuất hiện. Chọn phân vùng mà ta vừa cài macOS lên.
Màn hình cài đặt xuất hiện và tiếp tục công việc của nó.
Khởi động lại máy và boot vào USB giống bước 1
Màn hình Clover xuất hiện. Chọn Boot macOS from PARTITION_NAME . PARTITION_NAME sẽ là tên phân vùng bạn đã đặt ở bước 3. Máy mình đặt là MOJAVE.
Nếu mọi thứ suôn sẻ, bạn sẽ kết thúc quá trình cài đặt và đến với desktop của macOS
Cài đặt Clover lên HDD như cách mà bạn đã cài với USB. Đừng quên copy driver, kext và config vào phân vùng EFI trên HDD nhé.
Cách mount phân vùng EFI:
diskutil list // Check cột IDENTIFIER của EFI nhé. Của mình ở đây là `disk0s1` /dev/disk0 (internal, physical): #: TYPE NAME SIZE IDENTIFIER 0: GUID_partition_scheme *500.1 GB disk0 1: EFI EFI 209.7 MB disk0s1 2: Microsoft Basic Data WINDOW 100.0 GB disk0s2 3: Microsoft Basic Data DATA 250.1 GB disk0s3 4: Apple_APFS Container disk1 149.8 GB disk0s4 /dev/disk1 (synthesized): #: TYPE NAME SIZE IDENTIFIER 0: APFS Container Scheme - +149.8 GB disk1 Physical Store disk0s4 1: APFS Volume MOJAVE 109.1 GB disk1s1 2: APFS Volume Preboot 43.9 MB disk1s2 3: APFS Volume Recovery 506.8 MB disk1s3 4: APFS Volume VM 5.4 GB disk1s4 sudo mkdir /Volumes/EFI sudo mount -t msdos /dev/disk0s1 /Volumes/EFI/
Hoàn thiện
Sau khi cài Clover lên HDD, bạn có thể boot vào macOS mà không cần đến USB nữa. Giờ chỉ cần cài kext, patch DSDT/SSDT, sửa file config nữa là xong. Rehabman đã cung cấp sẵn các script trong repo để đơn giản hóa việc này. Việc của chúng ta là clone repo về và chạy các script có sẵn.
Trước tiên phải cài git:
xcode-select --install
Sau đó, clone repo của Rehabman trên Github:
mkdir ~/Projects cd ~/Projects git clone https://github.com/RehabMan/HP-ProBook-4x30s-DSDT-Patch probook.git
Download và cài đặt các tool và kext:
cd ~/Projects/probook.git ./download.sh ./install_downloads.sh
Power Management
Disable hibernation, do hackintosh không hỗ trợ:
sudo pmset -a hibernatemode 0 sudo rm /var/vm/sleepimage sudo mkdir /var/vm/sleepimage
Download và chạy ssdtPRGen script để patch SSDT:
cd ~/Projects/probook.git curl --fail -o ./ssdtPRGen.sh https://raw.githubusercontent.com/Piker-Alpha/ssdtPRGen.sh/master/ssdtPRGen.sh chmod +x ./ssdtPRGen.sh ./ssdtPRGen.sh
Mount EFI và copy file ssdt.aml đã được patch vào EFI/CLOVER/ACPI/patched:
cd ~/Projects/probook.git ./mount_efi.sh cp ~/Library/ssdtPRGen/ssdt.aml /Volumes/EFI/EFI/Clover/ACPI/patched/SSDT.aml
ACPI Configuration
Build add-on SSDTs:
cd ~/Projects/probook.git ./build.sh
Copy các add-on SSDTs (SSDT-8x70.aml, SSDT-FANREAD.aml, SSDT-IGPU.aml) vào EFI/CLOVER/ACPI/patched trên HDD
./install_acpi.sh install_8x70
Config
Bước cuối là chọn file config.plist cho máy:
cd ~/Projects/probook.git cp ./config/config_8x70.plist /Volumes/EFI/EFI/Clover/config.plist
Review
Hoàn thành các bước trên coi như bạn thành công 95% việc cài hackintosh. Các tính năng đã hoạt động cho đến giờ:
UEFI booting với Clover
Power Management
Bàn phím
Trackpad
Wifi
Ethernet
Audio
Graphic
Mic
Camera
USB 2.0/3.0
Pin - hiển thị dung lượng, trạng thái
Mac App Store
Các tính năng chưa hoạt động, lỗi nhỏ:
Messages/Facetime - do chưa config SMBIOS trong config.plist. Mình không dùng đến tính năng này nên bỏ qua. Nếu muốn bạn có thể làm theo hướng dẫn này hoặc hướng dẫn này.
Khi khởi động luôn hiện lên thông báo:
This Mac can't connect to iCloud because of a problem with ...
Lỗi này cũng do chưa config SMBIOS trong config.plist. Sửa bằng cách làm theo một trong 2 hướng dẫn ở trên
Kết
Đấy, cài hackintosh cũng dễ mà 😁. Đùa thôi, đấy là với những máy mà có guide chi tiết và dễ cài như máy mình thôi.
Nếu bạn thực sự cần dùng macOS cho công việc mà không có thời gian tìm hiểu thì đi cài dịch vụ cho nhanh cũng được. Giá cả thì cũng không quá đắt, dao động từ 200-300k, tùy máy và tùy hình thức. Một số bạn cài dịch vụ mà mình biết:
Sơn Công Nghệ - 078.706.5634 - Hà Nội
Sơn Huỳnh - 093.622.5565 - Hà Nội
Tô Hùng Dương - Sài Gòn
Còn nếu có thời gian thì đây là một số địa chỉ để bạn có thể tìm hiểu:
Forum tonymacx86
Forum insanelymac
Thời gian tới mình cũng sẽ viết thêm về hackintosh, các bạn chú ý đón đọc nhé.
0 notes
whoistuanhai · 5 years ago
Text
Giả lập Push Notification trên iOS Simulator
Phiên bản Xcode 11.4 đã cho phép giả lập push notification trên iOS Simulator. Sau đây là hướng dẫn của mình về tính năng mới này.
Yêu cầu
Xcode version >= 11.4
Sample app với chức năng request push notification permission
APNS (Apple Push Notification Service) Payload File
Giả lập push notification bằng cách kéo thả
1. Download Xcode 11.4
Vào thời điểm của bài viết này (23/2/2020), Xcode 11.4 mới ra bản beta, bạn có thể download ở đây.
Nếu bạn đọc được bài viết này vào khi Xcode 11.4 đã release bản chính thức, bạn lên Mac App Store mà download nhé
2. Tạo sample app và request permission để nhận push notification
Trong AppDelegate, import UserNotifications framework:
import UserNotifications
Request permission trong application(_:didFinishLaunchingWithOptions:):
UNUserNotificationCenter.current() .requestAuthorization(options: [.alert, .sound]) {(granted, error) in print("Permission is granted: \(granted)") }
Chạy app và grant permission khi hiện thông báo.
3. Đưa sample app vào background
Nhấn tổ hợp phím Shift + ⌘ + H để đưa app chạy ở background. Để banner của push notification hiển thị trên màn hình, bắt buộc phải thực hiện bước này.
Chúng ta sẽ thực hiện việc hiển thị push notification khi simulator ở foreground sau.
4. Tạo APNS payload file và kéo vào iOS Simulator
APNS payload file là một JSON file chứa các thông tin hiển thị của push notification. Tìm hiểu thêm ở đây.
Đây là file APNS mình tạo cho sample app này.
{ "aps": { "alert": { "title": "Hello", "body": "This is an push notification", "sound": "default" }, "badge": 10 }, "Simulator Target Bundle": "com.tumblr.whoistuanhai.PushNotificationDemo" }
Lưu ý: Giá trị của key Simulator Target Bundle chính là bundle identifier của project.
Giờ thì chỉ việc kéo thả file vừa tạo vào simulator thôi.
Giả lập push notification sử dụng Terminal
Đầu tiên, cần tìm identifier của simulator đang chạy. Trong Xcode, nhấn tổ hợp phím Shift + ⌘ + 2 để cửa sổ "Devices and Simulators". Right click vào simulator đang sử dụng trong list các simulator, chọn Copy Identifier.
Lấy được identifier rồi, chạy câu lệnh sau trong Termial:
xcrun simctl push <simulator-identifier><bundle-identifier><path-to-apns-payload-file>
Nếu muốn bỏ qua bước tìm identifier của simulator và simulator của bạn đang chạy, chạy lệnh sau:
xcrun simctl push --booted <bundle-identifier><path-to-apns-payload-file>
Lưu ý: Nếu APNS payload file đã có key Simulator Target Bundle thì có thể bỏ qua trong câu lệnh.
Ngoài ra, developer @twannl có viết ra Poes - một command line tool giúp ta dễ dàng giả lập push notification mà không cần phải tạo APNS payload file.
Cài đặt Poes sử dụng Mint:
mint install AvdLee/Poes
Send notification:
Poes --bundle-identifier <your-app-bundle-identifier> --verbose
Giả lập push notification khi app đang ở foreground
Implement userNotificationCenter(_:willPresent:withCompletionHandler:) function của UNUserNotificationCenterDelegate protocol trong AppDelegate:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { print("Push notification received in foreground.") completionHandler([.alert, .sound, .badge]) }
Set AppDelegate là delegate của UNUserNotificationCenter trong application(_:didFinishLaunchingWithOptions:):
UNUserNotificationCenter.current().delegate = self
0 notes
whoistuanhai · 8 years ago
Photo
Tumblr media
1 note · View note
whoistuanhai · 8 years ago
Text
Tắt debug logging Xcode 8 iOS Simulator
Khi run 1 project với Xcode 8 với simulator, tự dưng xuất hiện một số logging ở debug console, trông khá khó chịu:
Tumblr media
Không biết dụng ý của apple là gì, nhưng những dòng logging trên đối với mình khá là vô dụng 😂. Để tắt nó đi, thêm vào một Enviroment Variable trong scheme của project với name là OS_ACTIVITY_MODE và giá trị là disable.
Tumblr media
0 notes
whoistuanhai · 9 years ago
Text
Parsing JSON với Swift 2.2 - Part 5 - Lý do bạn viết unit test
Trong phần 3 của series, chúng ta đã nói về một trong những lợi ích của việc tách JSON parsing code ra thành một class riêng đó là chúng ta có thể viết test cho nó một cách dễ dàng. Nhưng tại sao lại phải viết test? Nó có lợi ích gì không? (lưu ý rằng test ở đây là automated test hay unit test)
Có đấy! Unit test cung cấp cho chúng ta ít nhất 5 lợi ích chính:
Đảm bảo sự chính xác
Giúp dễ dàng bắt lỗi trong các feature đang tồn tại
Cho phép bạn refactor code một cách tự tin mà không làm hỏng các tính năng sẵn có trong hệ thống
Chạy nhanh hơn nhiều so với manual test
Không yêu cầu bạn có mặt trong khi test chạy
1. Unit test đảm bảo sự chính xác cho code của bạn
Khi bạn viết code, các unit test tốt có thể cho bạn sự tự tin rằng code bạn viết làm những gì mà bạn nghĩ. Và khi code của bạn không ổn, unit test sẽ nhanh chóng chỉ ra ngay. Bắt lỗi và sửa lỗi ở giai đoạn này sẽ rất dễ dàng vì nó vẫn còn tươi mới trong đầu của bạn và bạn có thể nhanh chóng quay lại sửa chữa những sai lầm trong code của mình.
Nếu bạn phát triển một đoạn code nhỏ, sau đó bạn chạy test của đoạn code đó. Test fail, bạn sẽ nhanh chóng tìm ra vấn đề.
Vẫn trường hợp trên, nhưng bạn không viết test. Bạn đưa app của mình vào production, sau đó nhận được các báo cáo lỗi của người dùng, lúc đó, sẽ mất nhiều thời gian hơn để bạn phát hiện và sửa lỗi.
2. Unit test giúp bạn dễ dàng bắt lỗi trong các feature đang tồn tại
Nếu bạn có được những unit test tốt và được chạy thường xuyên, nó sẽ giúp bạn bắt được các lỗi trước khi mà bạn đưa sản phẩm đến QA team, hay tệ hơn là người dùng.
Khi bạn đã viết một test cho một feature, chạy nó, test success, OK, bạn chuyển qua viết một feature khác. Sau khi viết xong feature mới này, bạn chạy lại test, boom, test fail. Rõ ràng test đã giúp bạn chỉ ra rằng bạn đã làm gì đó mà ảnh hưởng tới feature cũ trong khi viết feature mới.
Hãy thiết lập thói quen chạy test của bạn mỗi lần commit code và cài đặt một CI Server (Continuous Integration Server) để chạy các bộ test mỗi khi code được push lên central repository. Với những thói quen đó, bạn có thể đảm bảo rằng test của bạn sẽ được chạy thường xuyên.
3. Unit test cho phép bạn tự tin refactor code
Qua thời gian, hầu hết mọi dòng code đều phải được refactor. Thậm chí khi bạn có một quyết định hoàn hảo trước đó, nhưng sau này yêu cầu của app tăng lên và thay đổi, và tại một thời điểm nào đó bạn có nh��ng dòng code khiến cho codebase của bạn khó bảo trì và thêm các chức năng mới. Đó chính là lúc cần phải refactor lại code.
Refactor là một phần trong cuộc đời của một developer, và nó có thể rất đáng sợ nếu bạn không có một bộ unit test tốt trong tay. Nếu không có unit test, thực sự rất khó để bạn biết được liệu mình có làm hỏng gì đó trong quá trình refactor không. Mặc dù bạn có thể luôn luôn test thủ công sau khi hoàn thành việc refactoring, nhưng unit test có thể nói cho bạn biết ngay lập tức khi có gì đó "sai sai" xảy ra mà không cần bạn phải chạy app và lướt qua tất cả các màn hình của app. Chúng sẽ chỉ ra chính xác method mà bị fail, khiến cho việc fix lỗi trở nên nhanh chóng và dễ dàng hơn.
4. Unit test chạy nhanh hơn manual test
Một unit test có thể chạy trong chưa đầy một giây. Không thể chạy một manual test trong khoảng thời gian đó. Thậm chí cả một bộ unit test có thể thường chạy trong ít hơn một phút - khoảng thời gian để chạy một đến hai manual test.
5. Unit có thể chạy mà không cần bạn có mặt
Một khi bạn đã viết unit test, chúng có thể chạy đi chạy lại nhiều lần mà không cần bạn có mặt. Bạn có thể chạy chúng trong khi nhâm nhi một ly cà phê hoặc để chúng tự động chạy trên một CI Server mỗi lần bạn push code lên. Thời gian của bạn là vô cùng giá trị, trong khi thời gian của máy tính là rẻ mạt. Nếu bạn có thể viết một unit test để kiểm chứng được các hành vi của app thay vì phải test thủ công mỗi lần bạn tạo ra sự thay đổi trong codebase, lúc đó unit test được coi là đáng giá ngàn vàng.
Nhưng bạn vẫn băn khoăn , với tất cả các lợi ích kể trên, liệu có đáng bỏ thời gian ra để viết unit test không? Vì bạn vẫn phải chạy các manual test, nên nếu viết các unit test, chẳng phải là đang bỏ phí thời gian mà đáng lẽ dùng để viết production code.
Mình nghĩ rằng thực ra viết unit test là đang tiết kiệm thời gian cho bạn. Vì với việc giảm được các lỗi trong code của bạn, bạn sẽ đỡ mất thêm thời gian để test thủ công và tìm kiếm các lỗi. Và bạn đang tự mang lại cho bản thân thêm sự tự tin mà bạn không thể có được nếu không viết unit test.
Viết unit test là một phần không nhỏ trong quá trình để giúp mình trở thành developer. Và mình hoàn toàn tin tưởng rằng nó giúp phần mềm mình viết ra trở nên ổn định hơn, ít lỗi hơn và dễ bảo trì hơn.
Vậy làm sao để viết test cho class JSONParser? Câu trả lời sẽ có ở phần 6 của series.
0 notes
whoistuanhai · 9 years ago
Text
Parsing JSON với Swift 2.2 - Part 4 - Xử lý lỗi với do-try-catch
Ở phần 1 của series, chúng ta đã làm quen với bộ 3 do - try - catch nhưng chưa đi sâu vào chi tiết. Ở phần này, mình sẽ nói về cách hoạt động của chúng và các trường hợp cần sử dụng.
Trong Swift, error được xử lý bên trong do - catch block. Bất cứ khi nào bạn gọi một method mà có khả năng throw ra một error, bạn bị yêu cầu bắt buộc phải xử lý error đó hoặc bỏ qua và throw error từ chính method của bạn. Xét ví dụ gọi method JSONObjectWithData:options:, method này được đánh dấu với từ khóa throws trong documentation:
func parseDictionary(data: NSData?) -> [String: AnyObject]? { do { if let data = data, json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] { return json } } catch { print("Couldn't parse JSON. Error: \(error)") } return nil }
Ở đây, ta chọn cách xử lý error throw từ method JSONObjectWithData:options: bằng cách catching error trong catch block. Khi catching error , chúng ta dùng tên biến là error(mặc định) để tham chiếu tới nó
Một lựa chọn khác là bỏ qua error và để caller của method parseDictionary: xử lý error này. Để làm vậy, ta chỉ cần thêm throws vào method signature và bỏ đi do - catch block.
func parseDictionary(data: NSData?) throws -> [String: AnyObject]?
Nhưng đây có vẻ không phải phương án hay trong trường hợp này. Lý do vì bất cứ method nào gọi method parseDictionary: có lẽ sẽ không quan tâm tới liệu nó sẽ throw error hay không, cái chúng muốn chỉ là nhận về một dictionary. Nếu có lỗi xảy ra, chúng chỉ cần xử lý giá trị nil trả về mà không phải lo lằng về việc catching error.
Nếu muốn tìm hiểu sâu hơn về error handling trong Swift, bạn nên đọc chương Error Handling trong documentation.
Giờ thì chúng ta có thể trở lại với câu hỏi từ phần trước, tại sao chúng ta lại muốn viết test cho class JSONParser. Câu trả lời sẽ có ở phần 5 của series.
0 notes
whoistuanhai · 9 years ago
Text
Parsing JSON trong Swift 2.2 - Part 3 - Nên đặt JSON parsing code ở đâu
Hãy nhìn lại JSON parsing code của chúng ta đến thời điểm này:
do { if let data = data, json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] { if let id = json["id"] as? Int, name = json["name"] as? String { print("found repo with id: \(id) name: \(name)") } else { print("couldn't parse the id and name") } } catch { }
Ngay lúc này, có thể bạn đang đặt đoạn code trên trong một view controller, code chạy OK. Nhưng sớm hay muộn, bạn nên tách nó ra khỏi view controller, nếu không, rất có thể, bạn sẽ biến view controller của mình thành một thứ mà cộng đồng lập trình iOS thường gọi với cái tên trìu mến đó là Massive View Controller.
Một Massive View Controller thường khó bảo trì, khó để viết test và vi phạm nguyên tắc SRP(Single Resposibilities Principle) trong lập trình. Code trong view controller cũng không thể tái sử dụng, vì thế, networking và JSON parsing code không nên nằm trong đó.
Giờ thì, hãy chuyển JSON parsing code sang một class giành riêng cho việc parsing JSON. Có thể sau này, bạn muốn nhiều class để parsing JSON, nhưng chúng ta sẽ bắt đầu với một class. Hãy đặt tên nó là JSONParser và định nghĩa một method như sau:
class JSONParser { func parseDictionary(data: NSData?) -> [String: AnyObject]? }
Đầu tiên, method này nhận một NSData? làm parameter, đó chính là data mà bạn nhận được khi request từ API, bất chấp sử dụng NSURLSession hay một third-party library nào đó để tạo request. Method trả về một optional dictionary với key có kiểu String và value có kiểu AnyObject.
Trong implementation của method, nếu có thể parsing JSON ra khỏi data, chúng ta sẽ trả về một dictionary, nếu không, chúng ta sẽ trả về nil:
func parseDictionary(data: NSData?) -> [String: AnyObject]? { do { if let data = data, json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] { return json } catch { print("couldn't parse JSON") } return nil }
Sự khác biệt của method này so với lúc trước là chúng ta trả về dictionary đại diện cho JSON data nếu parsing thành công, nếu không, chúng ta trả về nil. Chúng ta để mặc cho caller của method này xử lý trường hợp trả về nil. Nhưng tại sao lại trả về nil mà không trả về một empty dictionary hay throw một error?
Trả về nil cho phép caller của method này dễ dàng hơn khi làm việc với giá trị trả về của method. Nếu method trả về nil, caller có thể sử dụng cơ chế optional binding (if let) để xử lý dữ liệu. Nếu thay vào đó, chúng ta trả về một empty dictionary, caller sẽ không biết được khi nào dữ liệu trả về bị empty nếu không kiểm tra hoặc sử dụng dữ liệu đó.
Một cách nữa là throw ra error thay vì trả về nil nếu chúng ta muốn kết nối với caller một cách chính xác rằng điều gì đã gây ra lỗi. Trong một số trường hợp, cách này có thể hữu ích, nhưng với method này, mình không chắc đó là cách hay. Caller sẽ làm gì khi nó biết chắc rằng lỗi nào đã xảy ra trong khi parsing JSON? Liệu nó sẽ thông báo lỗi cho người dùng? Có lẽ là không, vì người dùng đâu có quan tâm hay có thể làm được gì với thông báo lỗi "JSON parsing failed". Vì thế, trả về nil trong method parseDictionary là tối ưu nhất vì nó đưa cho caller thông tin vừa đủ, rõ ràng cho biết rằng parsing JSON đã thất bại.
Có 2 lợi ích của việc tạo ra class JSONParser và method parseDictionary:
Không còn JSON parsing code trong view controller.
Từ giờ chúng ta có thể viết test cho JSON parsing code khá dễ dàng. Chỉ cần dữ liệu với kiểu NSData là chúng ta đã có thể test xem chúng có thể trả về dictionary hay không.
Vậy làm thế nào để viết test? Và tại sao lại phải quan tâm đến test? Chúng ta sẽ trả lời câu hỏi đó sớm thôi, nhưng trước tiên, chúng ta cần phải hiểu cách hoạt động của error handling trong swift ở phần 4 của series.
0 notes
whoistuanhai · 9 years ago
Text
Parsing JSON trong Swift 2.2 - Part 2 - Sự nguy hiểm của forced type casting
Ở phần trước, ta đã casting kiểu với as? như sau:
let id = json["id"] as? Int let name = json["name"] as? String print("found repo with id: \(id) name: \(name)")
Và cho ra output:
found repo with id: Optional(22458259) name: Optional("Alamofire")
Output trên không hoàn toàn là những gì chúng ta mong muốn, do có sự xuất hiện của optional. Chúng ta có thể giải quyết vấn đề trên bằng forced casting (as!):
let id = json["id"] as! Int let name = json["name"] as! String print("found repo with id: \(id) name: \(name)")
Mặc dù output sẽ không còn optional value 🎉, nhưng rất có thể, app của bạn sẽ bị crash 😱. Vì sao? Vì nếu tồn tại một và chỉ một điều sau thì crash sẽ xuất hiện:
id trong dictionary có giá trị nil
id trong dictionary có giá trị khác nil nhưng không thể casting sang kiểu Int
name trong dictionary có giá trị nil
name trong dictionary có giá trị khác nil nhưng không thể casting sang kiểu String
Bạn có thể nghĩ rằng: Thế thì sao, tôi chắc rằng tôi sẽ nhận được một "id" với kiểu Int và một "name" với kiểu String dựa tr��n JSON output của API. Hiện giờ, bạn có thể đúng, nhưng nhỡ có sự thay đổi của API trong tương lai thì sao? Ví dụ như API bị lỗi và trả về một id với kiểu String, hay API sẽ thay thế property name bằng title chẳng hạn. Bạn sẽ không thể chắc chắn 100% sẽ nhận được gì từ API, vì thế, hãy sẵn sàng cho những tình huống xấu nhất có thể xảy ra. Vì thế, thay vì forced casting, hãy sử dụng cách sau:
if let id = json["id"] as? Int, name = json["name"] as? String { print("found repo with id: \(id) name: \(name)") } else { print("couldn't parse the id and name properties") }
Ở đây, chúng ta dùng optional binding một lần nữa. Nếu id và name có giá trị khác nil và có thể casting thành các kiểu tương ứng là Int và String, đoạn code trong if block sẽ chạy và cho ra output mong muốn mà không có optional values. Nếu có lỗi xảy ra khi parsing id và name, code trong else block sẽ chạy và in lỗi ra console.
Đoạn code trên an toàn khi có sự thay đổi của API, giờ ta có thể chắc rằng nó sẽ không bị crash nếu ta nhận được một JSON key hoặc value không mong muốn từ API.
Bài học rút ra là hãy tránh sử dụng forced casting(as!) bất cứ khi nào có thể. Thay vào đó, kết hợp sử dụng optional binding(if let) với toán tử as? để có được kết quả tương tự với sự an toàn cao hơn. Ở một bài sau này của series, chúng ta sẽ tìm hiểu về guard statement, một cách gọn gàng hơn để thay thế if let.
Ở phần 3 của series, chúng ta sẽ refactor lại đống code trên vào một class mới để dễ dàng cho việc tái sử dụng. Stay tune!
0 notes
whoistuanhai · 9 years ago
Text
Parsing JSON trong Swift 2.2- Part 1 - Deserializing JSON
Giả sử bạn đang viết một app hiển thị các popular repo trên Github. Với search API của Github, bạn có thể tìm được các Swift repo có nhiều star nhất bằng request sau:
https://api.github.com/search/repositories?q=language:swift&sort=stars&order=desc
Request này trả về một mảng các items. Hiện tại, chúng ta chỉ quan tâm tới item đầu tiên trong mảng. Để cho ngắn gọn, mình sẽ chỉ quan tâm tới 2 thông tin của item này là id và name:
{ "id": 22458259, "name": "Alamofire" }
Làm sao để deserialize item này thành các kiểu của Swift để ta có thể sử dụng id và name trong app của mình? Giả sử JSON trả về chứa trong một variable là data với kiểu NSData?, ta sẽ sử dụng NSJSONSerialization:
do { if let data = data, json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] { let id = json["id"] as? Int let name = json["name"] as? String print("found repo with id: \(id) name: \(name)") } } catch { print("couldn't parse JSON") }
Thứ nhất, tại sao mọi thứ lại ở bên trong do - catch block?
Trong Swift, các method mà throws error được xử lý bên trong do - catch block. Trong class NSJSONSerialization, method JSONObjectWithData:options được đánh dấu với throws, cho chúng ta biết rằng nó có thể sẽ throws một error. Swift vì thế sẽ buộc chúng ta phải xử lý điều này, bằng một trong 2 cách sau:
Bắt lỗi bằng cách sử dụng do - catch block như trên. Ở đây trong catch block, ta print lỗi ra console.
Đánh dấu function dùng để gọi method JSONObjectWithData:options với throws để yêu cầu method gọi method này xử lý lỗi
Ở đây mình chọn cách thứ nhất do mình không muốn caller của method đang viết quan tâm tới việc xử lý lỗi.
Bên trong do block, dòng đầu tiên, mình sử dụng optional binding để unwrap data. data có kiểu NSData? (optional NSData) nên có thể có giá trị = nil. Nếu chưa làm việc với Swift đủ lâu, có thể khái niệm optional binding khiến bạn cảm thấy bối rối. Và thực tế là ở đây ta tạo ra một constant data trùng tên với optional data tạo ra sự khó hiểu. Thực ra, đây là một pattern phổ biến trong Swift. Nói ngắn gọn thì constant data sẽ được tạo khi và chỉ khi nào mà optional data có giá trị khác nil, và constant này sẽ được sử dụng sau đó trong if block với đảm bảo là nó sẽ khác nil. Nếu muốn đọc thêm về optional binding, bạn nên đọc chương Optional Binding trong Swift Programming Language của Apple.
Tới dòng thứ 2:
json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject]
Để sử dụng method JSONObjectWithData:options, bạn phải dùng từ khóa try trước lời gọi của method do JSONObjectWithData:options có thể throws ra error. Trong Objective-C, bạn thường tạo ra một NSError và truyền vào method. Nhưng trong Swift, mọi error sẽ được xử lý trong catch block.
Mình truyền vào method JSONObjectWithData:options hai parameter: data chứa JSON content và một empty array cho parameter options vì thông thường, ta không cần thêm option nào ở đây.
Method JSONObjectWithData:options trả về giá trị có kiểu AnyObject, không phải kiểu chúng ta mong muốn. Chúng ta cần một dictionary có key là String và giá trị là AnyObject. Vì thế, mình sẽ casting AnyObject với từ khóa as?.
Tại sao lại là as? chứ không phải là as. Ví dụ, nếu có 2 kiểu Media và Movie, Movie thừa kế từ Media thì ta có thể casting từ Movie sang Media sử dụng as. AnyObject, thực chất là một protocol, không thừa kế từ kiểu nào hết, vì thế sử dụng as?. Ở đây, as? - optional downcasting, sẽ casting AnyObject thành kiểu optional [String: AnyObject]. as? còn có một người anh em là as! - forced downcasting. Nếu sử dụng as! thay vì as? ở đây, giá trị trả về sẽ có kiểu [String: AnyObject].
Do casting bằng as?, giá trị trả về là một optional, nên mình phải unwrap nó trước khi sử dụng. Vì thế, trước dấu =, mình tạo ra một constant json để sử dụng trong if block.
Giả sử chúng ta có được dữ liệu non-nil và casting thành công, code trong if block sẽ được chạy:
let id = json["id"] as? Int let name = json["name"] as? String print("found repo with id: \(id) name: \(name)")
Ở đây, ta lấy id và name từ dictionary json và casting chúng thành các kiểu Int và String. Do vẫn sử dụng as? để casting nên các giá trị id và name sẽ có kiểu lần lượt là optional Int và optional String. Vì thế, output của câu lệnh print sẽ là:
found repo with id: Optional(22458259) name: Optional("Alamofire")
Nếu muốn lấy giá trị thật của id và name thay vì nhận về các optional, mình sẽ dùng as! thay cho as?. Nhưng làm vậy có nên hay không? Câu trả lời sẽ có ở phần 2 của series.
0 notes
whoistuanhai · 9 years ago
Text
Parsing JSON với Swift 2.2 -  Part 0 - Giới thiệu
Ngày nay, hầu hết các app đều có kết nối với ít nhất một API và nhận kết quả trả về từ server dưới dạng JSON. Vì thế, parsing JSON có thể coi là một công việc điển hình của một iOS Developer.
Từ iOS 5.0, Apple giới thiệu class NSJSONSerialization để giúp cho việc parsing JSON trở nên dễ dàng hơn bao giờ hết. Với sự xuất hiện của Swift, công việc này nảy sinh ra một số vấn đề mà không những khiến những người mới bước chân vào lập trình iOS mà ngay cả những iOS developer có nhiều kinh nghiệm với Objective-C bối rối.
Vì thế, trong loạt bài này, mình sẽ tổng hợp các kiến thức về parsing JSON trong Swift cùng với các "best practice" mà mình học được. Sau khi đọc xong loạt bài này, mong rằng các bạn có thể parse JSON với Swift mà không lo lắng rằng app của bạn bị crash hay dữ liệu không thể hiển thị. Trong loạt bài cũng nhắc đến và giải thích một số quy tắc, khái niệm giúp bạn viết code Swift dễ đọc, dễ hiểu, an toàn và ngắn gọn hơn.
Mình sẽ chia nhỏ các bài viết để dễ theo dõi. Sau đây là danh sách các bài viết trong series:
Deserializing JSON
Sự nguy hiểm của forced type casting
Nên đặt JSON parsing code ��� đâu
Xử lý lỗi với do-try-catch
Lý do bạn viết unit test
Viết Unit test cho JSON parsing code
Xử lý khi xuất hiện test failure
Tăng sự tự tin của bạn với test coverage
Tránh sự tự mãn với strong assertion
Testing error path
Giảm thiểu sự hỗn loạn
Sử dụng typealias
Pyramid of doom - Thảm họa kim tự tháp :))
Model objects
Tạo các thông báo lỗi giúp cho việc debug
Array và nil
0 notes
whoistuanhai · 9 years ago
Text
Có gì mới ở Swift 2.2
Các toán tử ++ và -- bị deprecate
Deprecate ở đây nghĩa là vẫn dùng được nhưng khi dùng thị nhận được warning từ Xcode. Các toán tử trên sẽ bị loại bỏ hoàn toàn ở Swift 3.0. Vì vậy thay vì ++ hay -- thì ta sẽ viết += 1 hay -= 1. Các lý do để tạo nên sự thay đổi này:
Viết ++ hay -- không ngắn hơn bao nhiêu so với += 1 hay -= 1
Viết ++ hay -- sẽ có ý nghĩa khác nhau khi là tiền tố hay hậu tố. Ví dụ:
var a = 5 let b = a++ // b = 5 let c = ++a // c = 7 var d = 5 let e = d-- // e = 5 let f = --d // f = 3
Trường hợp ++ hay -- là hậu tố của variable, phép toán sẽ trả về một bản copy giá trị của variable trước khi thực hiện thay đổi giá trị của variable (post-increment/post-decrement). Còn trong trường hợp là tiền tố, phép toán sẽ trả về giá trị thực của variable sau khi tính toán (pre-increment/pre-decrement). Ít người biết được điều này nên dễ gây ra nhầm lẫn => Loại bỏ
C-style for loop bị deprecate
Vì ++ và -- bị deprecate mà chúng hay được dùng trong C-style for loop => deprecate. Một số ứng viên thay thế:
Range loop:
for i in 1...10 { print(i) }
Nếu muốn loop ngược, thì đừng viết:
for i in 10...1 { print(i) }
mà hãy viết:
for i in (1...10).reverse() { print(i) }
Fast enumeration của array:
var array = Array(1...10) for number in array { print(number) }
Array có thêm method removeFirst
Swift 2.2 thêm vào method removeFirst giúp xóa phần tử đầu tiên của một array và trả về phần tử đã xóa. Lưu ý là nếu gọi removeFirst từ một empty array, nó sẽ gây ra crash.
Cho phép so sánh trực tiếp 2 tuple
Cho 2 tuple:
let hai = ("TuanHai", 25) let duc = ("Minh Duc", 24)
Trước Swift 2.2, ta phải viết một method như sau để so sánh 2 tuple trên:
func == (t1: (T, T), t2: (T, T)) -> Bool { return t1.0 == t2.0 && t1.1 == t2.1 }
Phải viết đoạn code như vậy thật dài dòng và dĩ nhiên nó chỉ so sánh được 2 tuple có 2 phần tử. Với Swift 2.2, ta có thể trực tiếp so sánh 2 tuple trên với toán tử ==:
if hai == duc { print("Matching tuple") }else { print("Not matching tuple") }
Swift 2.2 cho phép so sánh 2 tuple với cao nhất là 6 phần tử. Có 2 lý do để Apple hạn chế số phần tử là 6:
Càng nhiều phần tử cần so sánh => cần nhiều code hơn trong Swift Standard Library
Sử dụng tuple với nhiều hơn 6 phần tử có lẽ là một "code smell" và bạn nên chuyển sang dùng struct để thay thế.
Tuple splat syntax bị deprecate
Tuple splat là một cú pháp cho phép sử dụng một tuple làm argument của một method nếu thỏa mãn các điều kiện sau:
Các phần tử của tuple được đặt label và các label đó lần lượt trùng với label của các argument của method
Phần tử đầu tiên của tuple không được đặt label
Các kiểu của các phần tử của tuple cần lần lươt trùng với kiểu của các argument của method
Ví dụ:
func printNameAndAge(name: String, age: Int) { print("\(name) is \(age) years old") } let hai = ("Tuan Hai", 25) printNameAndAge(hai)
Đây là một cú pháp mà ít người biết tới và sử dụng tới và có thể gây "bối rối" khi đọc code, vì thế nó sẽ bị deprecate trong Swift 2.2 và loại bỏ hoàn toàn trong tương lai.
var argument bị deprecate
Mặc định, một method argument là một constant, không thể bị mutate trong method. Nếu muốn mutate một argument, ta thêm từ khóa var vào trước method argument. Ví dụ:
func changeCase(var name: String) { name = name.uppercaseString print(name) } changeCase("tuan hai")
var argument thường bị nhầm lẫn với inout argument vì thế nó bị deprecate. Sự khác biệt của chúng là var argument cho phép thay đổi giá trị của argument trong scope của method nhưng vẫn giữ nguyên giá trị của argument bên ngoài scope của method trong khi inout thì vẫn giữ nguyên giá trị sau khi thay đổi của argument bên ngoài scope của method.
Để thay thế var argument ở ví dụ trên, ta đơn giản là thêm vào một bản copy của argument như sau:
func changeCase(name: String) { let upperName = name.uppercaseString print(upperName) } changeCase("tuan hai")
Đổi tên các debug indentifiers
Swift 2.1 và các version trước đó sử dụng các symbol: __FILE__, __LINE__, __COLUMN__, __FUNCTION__, __DSO_HANDLE để debug. Với Swift 2.2, các symbol trên được đổi tên lần lượt thành #file, #line, #column, #function, #dsohandle.
Stringified selector bị deprecate
Trước Swift 2.2, một selector có thể được viết bằng một String như sau:
let barButton = UIBarButtonItem(title: "Tap!", style: .Plain, target: self, action: "buttonTaped")
Nếu để ý kỹ, buttonTaped không phải là một selector chuẩn mà phải là buttonTapped. Nhưng Xcode đã không phát hiện được lỗi này cho ta vì thế khi chạy sẽ bị crash.
Với Swift 2.2, sử dụng String cho một selector đã bị deprecate, thay vào đó ta sẽ viết #selector(buttonTapped) trong đoạn code ở trên. Nếu lỡ viết sai tên selector, ta sẽ nhận được một compile time error để sửa ngay.
Kiểm tra phiên bản swift đang chạy
#if swift(>=2.2) print("Running Swift 2.2 or later") #else print("Running Swift 2.1 or earlier") #endif
Các từ khóa mới dùng cho việc viết documentation
Swift hỗ trợ sử dụng markdown syntax để thêm meta-data vào code, vì thế ta có thể viết như sau:
/** Say hello to a specific person - parameters: - name: The name of the person to greet - returns: Absolutely nothing - authors: Paul Hudson Bilbo Baggins - bug: This is a deeply dull function */ func sayHello(name: String) { print("Hello, \(name)!") } sayHello("Bob")
Meta-data của method sayHello ở trên được dùng trong code completion. Ở ví dụ trên sử dụng các từ khóa như parameters, name, returns, author. Swift 2.2 bổ xung thêm các từ khóa: recommended, recommendedover, keyword. Từ khóa recommended dùng để khuyên người dùng nên sử dụng một method khác, còn từ khóa recommendedover dùng để khuyên người dùng sử dụng method đang dùng hơn là một method khác. Ví dụ:
/** Say hi to named person - recommendedover: sayHiToTuanHai - keyword: hi */ func sayHi(name: String) { } /** Say hi to Tuan Hai - recommended: sayHi */ func sayHiToTuanHai { }
0 notes
whoistuanhai · 9 years ago
Text
Singleton trong Swift - The right way
Một singleton phải đảm bảo sự unique, nghĩa là chỉ có một instance được tồn tại trong vòng đời của một application. Một số ví dụ như NSNotificationCenter, UIApplication, NSUserDefaults... Để duy trì được unique, initializer của một singleton phải là private.
Đây là cách mà ta thường dùng để viết một singleton trong Objective-C, sử dụng dispatch_once để đảm bảo tính unique của singleton:
@interface GameManager : NSObject @end @implementation GameManager + (instancetype)sharedInstance { static GameManager *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[GameManager alloc] init]; }); return sharedInstance; } @end
Trong swift, có nhiều cách để viết một singleton. Ta sẽ đi từng bước để tìm ra cách viết một singleton tốt nhất trong swift. Tốt nhất ở đây có nghĩa là dễ hiểu, dễ đọc, ngắn ngọn, an toàn, đúng với ý tưởng của swift.
Cách 1: dispatch_once
Đây là cách "truyền thống" của Objective-C được port sang Swift
class GameManager { class var sharedInstance: GameManager { struct Static { static var onceToken: dispatch_once_t = 0 static var instance: GameManager? = nil } dispatch_once(&Static.onceToken) { Static.instance = GameManager() } return Static.instance! } }
Ở đây ta sử dụng một type computed property với kiểu của class. Trong closure để khởi tạo giá trị của property, ta định nghĩa thêm một struct Static để lưu các tham số của hàm dispatch_once với từ khóa static. Bạn có thể băn khoăn vì sao cần phải tạo thêm struct Static mà không khai báo hai tham số luôn ở ngoài. Đó là vì trong Swift, static property chỉ có thể được khai báo trong phạm vi của một kiểu (class, struct). Cách này dùng OK, nhưng dài.
Cách 2: Static type property - Struct
class GameManager { class var sharedInstance: GameManager { struct Static { static let instance = GameManager() } return Static.instance } }
Khác với cách 1, cách 2 không sử dụng dispatch_once mà dùng static type property để đảm bảo tính unique của singleton. Cách này được sử dụng ở Swift 1.0 do lúc đó class chưa cho khai báo static type property nhưng struct thì có. Cách này tốt hơn cách 1, more swifty!
Cách 3: Global variable
private let sharedInstance = GameManager() class GameManager { class var sharedInstance: GameManager { return sharedInstance } }
Từ swift 1.2, chúng ta đã có thể sử dụng các access control specifier như private, public... Cách này sử dụng một private global variable với initializer của class singleton. Trong một blog của Apple, họ đã đảm bảo rằng initializer này được chạy trong một hàm dispatch_once, vì vậy sẽ đảm bảo được tính unique của singleton.
Cách 4: Static type property- Class - The right way
class GameManager { static let sharedInstance = GameManager() }
Cũng từ swift 1.2, swift cho phép khai báo static type property trong class. Vì vậy, ta có thể rút ngắn cách 2 thành cách 4 như trên mà không cần khai báo thêm struct. Siêu ngắn, dễ nhớ, dễ đọc, không còn gì để chê.
Kết luận
Cách 4 ngon nhất. Hãy dùng cách 4.
Bonus: Private initializer
Đừng quên nếu muốn ngăn các object khác sử dụng default initializer của class, ta phải override initializer và làm nó private:
class GameManager { static let sharedInstance = GameManager() private init() {} }
1 note · View note
whoistuanhai · 9 years ago
Text
Debug autolayout trong Swift
Trong objective-c, ta có thể gọi được các private API trong LLDB, nhưng với swift thì không. Vì vậy các method như _autolayoutTrace và recursiveDescription của UIView để kiểm tra ambiguous layout không thể gọi trực tiếp được.
Tumblr media
Có nhiều cách khác nhau để giải quyết vấn đề này.
Cách 1
expr -l objc++ -o -- [[UIWindow keyWindow] _autolayoutTrace]
Ở đây ta dùng expr command để nói với LLDB biết rằng ta đang truyền vào một objective-c code để chạy. Câu lệnh trên khá dài và khó nhớ nên ta sẽ tạo một alias và lưu vào trong file .lldbinit:
command alias autolayoutTrace expr -l objc++ -O --[[UIWindow keyWindow] _autolayoutTrace]
Cách 2
Nếu sử dụng chung objective-c và swift trong 1 project, ta sẽ có file bridging header. Ta có thể tạo một category của UIView với các method trùng với tên của private API và expose category đó với swift trong bridging header.
#ifdef DEBUG @interface UIView (LayoutDebugging) #pragma clang push #pragma clang diagnostic ignored “-Wincomplete-implementation” - (id)recursiveDescription; - (id)_autolayoutTrace; #pragma clang pop @end #endif #import “UIView+LayoutDebugging.h” #ifdef DEBUG @implementation UIView (LayoutDebugging) // Nothing to see here! @end #endif
Thứ nhất, do ta làm việc với private API nên phải wrap đống code này với #ifdef DEBUG để cho Apple biết rằng ta chỉ sử dụng private API này trong quá trình DEBUG, nếu không, rất có thể app của chúng ta sẽ bị reject bởi Apple.
Thứ 2, ta sử dụng #pragma clang diagnostic để nói với compiler bỏ qua các warning unimplemented method của 2 method chúng ta định nghĩa trong category. Bởi vì 2 method này đã có sẵn implementation nên chúng ta không cần reimplement chúng nữa.
Giờ thì khi chúng ta gọi po self.view._autolayoutTrace() hay po self.view.recursiveDescription() trong LLDB của swift, ta sẽ nhận được output như mong đợi.
0 notes
whoistuanhai · 9 years ago
Text
Lazy init trong swift
Lazy initialization (lazy init, lazy loading, lazy instantiation) là một kỹ thuật trì hoãn sự khởi tạo của một object, property... đến khi nào sử dụng nó, giúp tối ưu việc sử dụng bộ nhớ. Ta sẽ so sánh cách thực hiện kỹ thuật này trong Objective-C và Swift
Objective-C:
@property (nonatomic, strong) NSMutableArray *players; - (NSMutableArray *)players { if (!_players) { _players = [[NSMutableArray alloc] init]; } return _players; }
Ở đây ta lazy init property players bằng cách sử dụng hàm getter của property. Đầu tiên, kiểm tra sự tồn tại của instance variable _players. Nếu không tồn tại, khởi tạo, ngược lại thì trả về luôn.
Swift
lazy var players: [String] = []
Swift cung cấp cho ta từ khóa lazy, giúp đơn giản hóa kỹ thuật lazy loading. Lưu ý , lazy luôn đi với var vì khi khai báo property với let, property phải có giá trị trước khi object khởi tạo xong. Yên tâm là compiler sẽ ra thông báo lỗi khi sử dụng lazy với let.
Nếu muốn thêm logic vào quá trình lazy init, ta có thể sử dụng closure, instace method, class method. Closure là cách được ưa thích:
lazy var players: [String] = { var tempPlayers: String = [] tempPlayers.append("Hai") return tempPlayers }()
Khi nào sử dụng lazy init
Khi mà giá trị ban đầu của property chỉ có thể biết được sau khi object hoàn thành việc khởi tạo:
class Person { var name: String lazy var personalizedGreeting: String = { [unowned self] in return "Hello, \(self.name)!" }() init(name: String) { self.name = name } }
Ở ví dụ trên, khi khởi tạo một object Person, property personalizedGreeting chưa được khởi tạo:
let person = Person(name: "Hai") // person.personalizedGreeting is nil
Nhưng khi muốn in ra personalizedGreeting thì property sẽ được tính toán:
NSLog(person.personalizedGreeting) // personalizedGreeting is calculated when used // and now contains the value "Hello, Hai!"
Khi giá trị ban đầu của property sử dụng nhiều bộ nhớ để tính toán:
class MathHelper { lazy var pi: Double = { // Calculate pi to a crazy number of digits return resultOfCalculation }() }
0 notes
whoistuanhai · 9 years ago
Text
Xcode VCS symbol reference
Hay gặp
M: Working copy is modified A: This file will be added to version control (after commit) D: This file will be deleted (after commit)
Ít gặp
U: Working file was updated G: Changes on the repo were automatically merged into the working copy C: This file conflicts with the version in the repo ?: This file is not under version control !: This file is under version control but is missing or incomplete A+: This file will be moved or rename (after commit) S: This signifies that the file or directory has been switched from the path of the rest of the working copy (using svn switch) to a branch I: Ignored X: External definition ~: Type changed R: Item has been replaced in your working copy. This means the file was scheduled for deletion, and then a new file with the same name was scheduled for addition in its place. L : Item is locked E: Item existed, as it would have been created, by an svn update
0 notes
whoistuanhai · 9 years ago
Video
twitter
0 notes