Ở phần 1 của bài viết, mình đã giới thiệu về Operation là gì. Ở phần 2 của loạt bài về Operation, mình sẽ nói về Dependency trong Operation.
Nội dung bài viết:
Operation Dependencies
Passing Data using Dependencies
Operation Dependencies:
Operation cho phép bạn thiết lập các sự phụ thuộc lẫn nhau. Điều này mang lại 2 lợi ích:
Giả sử operation 2 phụ thuộc vào operation 1. Khi đó operation 2 chỉ được thực hiện sau khi operation 1 đã hoàn thành.
Cung cấp 1 cách để bạn truyền data giữa các operation.
class DownloadImage: Operation {
var index: Int
init(ind: Int) {
self.index = ind
}
override func main() {
// Download image task
print("Start downloading task \(index) at time: \(Date().timeIntervalSince1970)")
sleep(5)
print("Finish downloading task \(index) at time: \(Date().timeIntervalSince1970)")
}
}
let firstOperation = DownloadImage(ind: 1)
let secondOperation = DownloadImage(ind: 2)
//firstOperation.addDependency(secondOperation)
let operationQueue = OperationQueue()
operationQueue.addOperation(firstOperation)
operationQueue.addOperation(secondOperation)
Ở trên là đoạn code khởi tạo 2 operation bình thường, giữa chúng chưa có dependency. Chạy đoạn code trên và đây là kết quả thu được:
2 task chạy song song
Giờ thì bỏ comment dòng code firstOperation.addDependency(secondOperation) và chạy thử:
Task 1 phụ thuộc vào task 2. Vì vậy, task 1 chỉ chạy khi task 2 đã hoàn thành.
Note: Bạn có thể tạo dependency cho 2 opeartion đang chạy ở 2 operation queue khác nhau.
Đây là 1 cách khá ngắn trong khi ở GCD bạn phải khai báo 1 dispatchGroup, sau đó gọi hàm enter(), leave(), notify(), … Tuy nhiên, cách làm này rất dễ gây ra deadlock.
Note: Hãy vẽ sơ đồ ra 1 tờ giấy để luôn clear về flow của bạn, tránh bị deadlock.
Truyền data giữa các operation thông qua dependency:
class Calculate: Operation {
let firstNum: Int
let secondNum: Int
var sum: Int?
init(first: Int, second: Int) {
self.firstNum = first
self.secondNum = second
}
override func main() {
sum = firstNum + secondNum
}
}
class Display: Operation {
override func main() {
// 2
let sum = dependencies.compactMap{ ($0 as? Calculate)?.sum }.first
guard let unwrappedSum = sum else {
return
}
// 3
print("Sum = \(unwrappedSum)")
}
}
let calculateOperation = Calculate(first: 5, second: 10)
let displayOperation = Display()
// 1
displayOperation.addDependency(calculateOperation)
let operationQueue = OperationQueue()
// 4
operationQueue.addOperation(calculateOperation)
operationQueue.addOperation(displayOperation)
Khởi tạo 2 operation, và set dependency để task Display chỉ chạy sau khi task Calculate hoàn thành.
Hàm main() là hãm sẽ chạy khi 1 operation được chạy.
Ở đây, ta sẽ lấy ra list operations có dependency với DisplayOperation, chọn ra operation nào là Calculate và lấy ra sum.
Nếu sum khác nil thì hiển thị ra.
Add các operation vào queue để chạy.
Kết quả hiển thị trên màn hình Console:
Dependency trong Operation là 1 trong những thứ giúp Operation vượt trội hơn so với GCD.
Ở phần tiếp, mình sẽ nói về Async Operation và xử lí cancel Opeartion.
Bài viết này tôi sẽ giới thiệu về phương pháp mở rộng lint và tạo custom rule.
Lint và cách custom rule của lint như thế nào?
Lint là một bộ phân tích tĩnh, có mục tiêu tìm lỗi trên mã nguồn của bạn mà không cần phải biên dịch hoặc chạy nó.
Để tạo custom rule của lint thì cần phải tạo Detector, Issue, Registry.
Creating your rules module
Chúng ta bắt đầu bằng cách định nghĩa một module Java / Kotlin riêng biệt. Sau đó chúng ta sẽ thêm vào build.gradle như sau:
Detector là một lớp có thể tìm thấy một hay nhiều vấn đề cụ thể hơn. Tùy thuộc vào vấn đề, chúng ta có thể sử dụng các loại phát hiện khác nhau: SourceCodeScanner – một trình phát hiện chuyên về các tệp nguồn Java / Kotlin. XmlScanner – một trình phát hiện chuyên về các tệp XML. GradleScanner – một trình phát hiện chuyên về các tệp Gradle. ResourceFolderScanner – một trình phát hiện chuyên về các thư mục tài nguyên (không phải các tệp mà nó chứa).
Bạn có thể tham khảo tạo Detector dưới đây:
package techover.rules
import com.android.tools.lint.detector.api.*
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
/**
* This detector will report any usages of the android.util.Log.
*/
class AndroidLogDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames(): List<String> =
listOf("tag", "format", "v", "d", "i", "w", "e", "wtf")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
super.visitMethodCall(context, node, method)
val evaluator = context.evaluator
if (evaluator.isMemberInClass(method, "android.util.Log")) {
reportUsage(context, node)
}
}
private fun reportUsage(context: JavaContext, node: UCallExpression) {
context.report(
issue = ISSUE,
scope = node,
location = context.getCallLocation(
call = node,
includeReceiver = true,
includeArguments = true
),
message = "android.util.Log usage is forbidden."
)
}
companion object {
private val IMPLEMENTATION = Implementation(
AndroidLogDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
val ISSUE: Issue = Issue
.create(
id = "AndroidLogDetector",
briefDescription = "The android Log should not be used",
explanation = """
For amazing showcasing purposes we should not use the Android Log. We should the
AmazingLog instead.
""".trimIndent(),
category = Category.CORRECTNESS,
priority = 9,
severity = Severity.ERROR,
androidSpecific = true,
implementation = IMPLEMENTATION
)
}
}
Nó extends Detector để Android Lint có thể sử dụng để phát hiện sự cố.
Nó extends SourceCodeScanner vì chúng ta cần kiểm tra cả hai tệp Kotlin và Java.
getApplossibleMethodNames – chỉ lọc các chữ ký phương thức tồn tại trong android.util.Log
visitMethodCall – sử dụng trình đánh giá để đảm bảo rằng phương thức này được gọi bởi android.util.Log chứ không phải bởi bất kỳ lớp nào khác. Ví dụ: AmazingLog có cùng phương thức và không nên gắn cờ.
reportUsage – được sử dụng để báo cáo sự cố khi tìm thấy.
Creating Issue
Issue là một lỗi tiềm ẩn trong ứng dụng Android. Đây là cách bạn khai báo lỗi mà quy tắc của bạn sẽ giải quyết. id – để xác định duy nhất vấn đề này. briefDescription– mô tả tóm tắt về vấn đề. explanation – nên là một mô tả sâu hơn về vấn đề và lý tưởng về cách giải quyết vấn đề. category – xác định loại vấn đề. Có rất nhiều danh mục có thể có như CORRECTNESS, USABILITY, I18N, COMPLIANCE, PERFORMANCE, … priority – một số từ 1 đến 10, trong đó số càng lớn thì vấn đề càng nghiêm trọng. severity – nó có thể là một trong những giá trị sau: FATAL, ERROR, WARNING, INFORMATIONAL and IGNORE. Lưu ý: Nếu mức độ nghiêm trọng là FATAL hoặc ERROR thì việc chạy lint sẽ thất bại và bạn sẽ phải giải quyết vấn đề. implementation – lớp chịu trách nhiệm phân tích tệp và phát hiện vấn đề.
Bạn có thể tham khảo tạo Issue dưới đây:
val ISSUE: Issue = Issue
.create(
id = "AndroidLogDetector",
briefDescription = "The android Log should not be used",
explanation = """
For amazing showcasing purposes we should not use the Android Log. We should the
AmazingLog instead.
""".trimIndent(),
category = Category.CORRECTNESS,
priority = 9,
severity = Severity.ERROR,
androidSpecific = true,
implementation = IMPLEMENTATION
)
Creating Registry
Bạn có thể tham khảo Registry dưới đây:
package techover.rules
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue
class IssueRegistry : IssueRegistry() {
override val api: Int = CURRENT_API
override val issues: List<Issue>
get() = listOf(AndroidLogDetector.ISSUE)
}
Nếu source không có Issue mà chúng ta đã Registry thì sẽ in ra nội dung như sau:
> Task :app:lintDebug
Wrote HTML report to ~/Techover/Lint/app/build/reports/lint-results-debug.html
Wrote XML report to ~/Techover/Lint/app/build/reports/lint-results-debug.xml
BUILD SUCCESSFUL
Ví dụ, trong class MainActivity bạn có log như sau:
Thì chúng ta thấy source có Issue mà chúng ta đã Registry nê sẽ in ra nội dung như sau:
> Task :app:lintDebug FAILED
Wrote HTML report to ~/Techover/Lint/app/build/reports/lint-results-debug.html
Wrote XML report to ~/Techover/Lint/app/build/reports/lint-results-debug.xml
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:lintDebug'.
> Lint found errors in the project; aborting build.
Fix the issues identified by lint, or add the following to your build script to proceed with errors:
...
android {
lintOptions {
abortOnError false
}
}
...
Errors found:
~/Techover/Lint/app/src/main/java/techover/lint/MainActivity.kt:13: Error: android.util.Log usage is forbidden. [AndroidLogDetector]
Log.d("MainActivity", "https://magz.techover.io/")
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED
Xin chào mọi người, hôm nay mình sẽ giới thiệu với các bạn về UIPanGestureReconizer. Và mình sẽ hướng dẫn mọi người cách sử dụng và ứng dụng nó vào thực tế. Mình hi vọng sau khi xem hết bài viết này, mọi người có thể áp dụng nó vào các ứng dụng sau này nếu cần đến 😀
Ý tưởng
Chắc hẳn ai sử dụng iOS đều sử dụng qua tính năng “Control Center” của iOS. Để mở Control center lên ta phải vuốt từ dưới màn hình lên và ẩn đi thì ngược lại, nó còn cho phép người dùng kéo thả một cách mượt mà.
UIGestureRecognizer là gì?
UIGestureRecognizer là class định nghĩa một tập hợp các hành vi phổ biến có thể được cấu hình cho tất cả các cử chỉ cụ thể. Dạng như chạm, kéo thả …
UIGestureRecognizer được sử dụng để nhận dạng các loại hành vi của người dùng khi tương tác lên màn hình và thực hiện hành động được cấu hình.
UIPangestureRecognizer là subclass của UIGestureRecognizer dùng để nhận biết hành vi kéo thả.
Ví dụ về UIPanGestureRecognizer
Để bắt đầu chúng ta cần mở XCode và tạo mới một Single View App. Sau khi tạo project xong chúng ta mở file Main.storyboard và kéo vào một UIView -> đổi màu nền(Background color) của view sang một màu khác cho dễ nhìn.
Khi này chúng ta cần gán constraint cho view đó theo Hình 1 Cần chú ý ở đây chúng ta sẽ constraint bottom của view vào bottom của superview thay vì Safe Area để khi build trên các dòng điện thoại tai thỏ (iphone x, xs ….) sẽ không bị trắng ở dưới của màn hình.
Hình 1
Tiếp đến chúng ta sẽ add 1 UIView nhỏ vào trong UIView vừa mới tạo để thể hiện view này có thể kéo thả được. Chúng ta cũng constraint UIView nhỏ để nó nằm trên top và giữa của superview kết quả chúng ta thu được như Hình 2
Hình 2
Vậy là chúng ta đã xong phần UI, tiếp đến chúng ta cần kéo IBOutlet top constraint cho cái view to và đặt tên nó là topViewContainer kết quả sẽ được như hình 3
Chúng ta kéo IBOutlet cho top constraint của view để làm gì? Mục đích ở đây là để sau này chúng ta sẽ thay đổi giá trị của topConstraint.constant -> thay đổi vị trí và để tạo hiệu ứng chuyển động.
Tiếp đến chúng ta kéo outlet cho thằng View to đặt tên là viewContainer
OK, giờ chúng ta sẽ đi vào code chi tiết. Giờ chúng ta mở file ViewController.swift ra và tạo enum cho các trạng thái mở rộng
// 1: Tạo 3 trạng thái mở rộng cho viewContainer
enum ExpansionState {
case compressed
case haft
case expanded
}
Tạo pangesture và gán nó cho viewContainer, trong code mình đã comment rõ từng dòng, từng hàm để các bạn có thể hiểu được nó làm gì.
// thêm UIGestureRecognizerDelegate để sử dụng được PanGesture
class ViewController: UIViewController, UIGestureRecognizerDelegate {
@IBOutlet weak var viewContainer: UIView!
@IBOutlet weak var topViewContainer: NSLayoutConstraint!
// Khởi tạo trạng thái
var expansionState: ExpansionState = .haft
// Khởi tạo giá trị cũ của topViewContainer
var oldTopViewContainer: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setupGestureRecognizers()
// Khởi tạo vị trí bắt đầu cho việc animte viewContainer.
// Ở đây chúng ta đang để nó ở trạng thái compressed có nghĩa là ở trạng thái expansion nhỏ nhất.
topViewContainer.constant = topViewContainer(forState: .compressed)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Do mình muốn viewContainer sẽ chuyển động từ dưới lên trên và dừng lại ở trạng thái expansion 1 nửa màn hình
// Nên mình gọi hàm phía dưới để nó thực hiện chuyển động vì ở viewDidLoad nó đang ở trạng thái compressed
animateTopConstraint(constant: topViewContainer(forState: .haft), velocity: CGPoint(x: 0, y: 50))
}
// Cài đặt Pangesture,
func setupGestureRecognizers() {
let panGestureRecognizer = UIPanGestureRecognizer(target: self,
action: #selector(panGestureDidMove(sender:)))
panGestureRecognizer.delegate = self
viewContainer.isUserInteractionEnabled = true
viewContainer.addGestureRecognizer(panGestureRecognizer)
}
@objc func panGestureDidMove(sender: UIPanGestureRecognizer) {
let translationPoint = sender.translation(in: view.superview)
// velocity là vận tốc chuyển động của hành động, dạng như bạn vuốt vào màn hình nhanh hay chậm
let velocity = sender.velocity(in: view.superview)
switch sender.state {
case .changed:
panGesture(didChangeTranslationPoint: translationPoint, withVelocity: velocity)
case .ended:
panGesture(didEndTranslationPoint: translationPoint, withVelocity: velocity)
default:
return
}
}
// Phương thức này để tính toán giá trị viewContainer.top theo các trạng thái thái mà nó cần tính
func topViewContainer(forState state: ExpansionState) -> CGFloat {
let heightView = self.view.bounds.height
switch state {
case .compressed:
return heightView - 100
case .haft:
return heightView / 2
case .expanded:
return 100
}
}
}
Tiếp đến chúng ta cần tạo Extenstion cho phần animation để phục vụ cho việc chuyển động view container
/ MARK: Animation
extension ViewController {
/// Animates the top constraint of the drawerViewController by a given constant
/// using velocity to calculate a spring and damping animation effect.
func animateTopConstraint(constant: CGFloat, velocity: CGPoint) {
let previousConstraint = topViewContainer.constant
let distance = previousConstraint - constant
let springVelocity = velocity.y != 0 ? max(1 / (abs(velocity.y / distance)), 0.08) : 0.08
let springDampening = CGFloat(0.6)
UIView.animate(withDuration: 0.5,
delay: 0.0,
usingSpringWithDamping: springDampening,
initialSpringVelocity: springVelocity,
options: [.curveLinear],
animations: {
self.topViewContainer.constant = constant
self.oldTopViewContainer = constant
self.view.layoutIfNeeded()
}, completion: nil)
}
}
Tiếp đến chúng ta cần các hàm để xử lí khi mà người dùng kéo viewContainer Ở trong code mình đã comment các hàm, các dòng để mọi người dễ hiểu, hãy đọc trong code nhé.
//MARK: PanGesture handle
extension ViewController {
// Hàm này được gọi khi PanGestureRecognizer nhận ra được sự thay đổi của view,
// trong hàm này chúng ta có thể thực hiện một số điều kiện để giới hạn việc kéo của người dùng
func panGesture(didChangeTranslationPoint translationPoint: CGPoint,
withVelocity velocity: CGPoint) {
let newConstraintConstant = oldTopViewContainer + translationPoint.y
// Giới hạn việc người dùng kéo quá xa so với phía trên
if newConstraintConstant >= 0 {
topViewContainer.constant = newConstraintConstant
}
}
// Phương thức này được gọi khi kết thúc pan
func panGesture(didEndTranslationPoint translationPoint: CGPoint,
withVelocity velocity: CGPoint) {
// Velocity là kiểu CGPoint vì vậy nó có 2 giá trị x và y
// x: đại diện cho vận tốc theo chiều ngang
// y: đại điện cho vận tốc theo chiều dọc
// Do mình đang muốn thực hiện kéo thả theo chiều dọc nên ở ví dụ này mình sẽ dùng thuộc tính vận tốc y
if abs(velocity.y) <= 50.0 {
// 50: ở đây là vận tốc mà mình coi nó là người dùng đang kéo từ từ. Con số này các bạn có thể tùy chỉnh
// Giá trị của y < 0 có nghĩa là người dùng đang kéo lên trên và y > 0 là kéo xuống dưới
// dấu của y sẽ thể hiện chiều di chuyển của nó.
// Vì vậy mình cần sử dụng hàm abs() trước khi thực hiện phép so sánh
drag(lowVelocity: velocity)
} else if velocity.y > 0 {
dragDown(highVelocity: velocity)
} else {
dragUp(highVelocity: velocity)
}
}
// Hàm này thực hiện việc xác định xem view đang gần vị trí của trạng thái nào hơn sẽ chuyển sang trạng thái đó.
func drag(lowVelocity velocity: CGPoint) {
let compressedTopConstraint = topViewContainer(forState: .compressed)
let haftTopConstraint = topViewContainer(forState: .haft)
let expandedTopConstraint = topViewContainer(forState: .expanded)
let expandedDifference = abs(topViewContainer.constant - expandedTopConstraint)
let haftDifference = abs(topViewContainer.constant - haftTopConstraint)
let compressedDifference = abs(topViewContainer.constant - compressedTopConstraint)
let heightArray = [expandedDifference, haftDifference, compressedDifference]
let minHeight = heightArray.min()
if expandedDifference == minHeight {
expansionState = .expanded
animateTopConstraint(constant: expandedTopConstraint, velocity: velocity)
} else if haftDifference == minHeight {
expansionState = .haft
animateTopConstraint(constant: haftTopConstraint, velocity: velocity)
} else {
expansionState = .compressed
animateTopConstraint(constant: compressedTopConstraint, velocity: velocity)
}
}
// Hàm này để xử lí khi người dùng kéo thả view với tốc độ cao,
// nó sẽ xác định và chuyển tới trạng thái liền kề theo hướng phía dưới
func dragDown(highVelocity velocity: CGPoint) {
// Handle High Velocity Pan Gesture
switch expansionState {
case .compressed, .haft:
expansionState = .compressed
animateTopConstraint(constant: topViewContainer(forState: .compressed), velocity: velocity)
case .expanded:
expansionState = .haft
animateTopConstraint(constant: topViewContainer(forState: .haft), velocity: velocity)
}
}
// Hàm này để xử lí khi người dùng kéo thả view với tốc độ cao,
// nó sẽ xác định và chuyển tới trạng thái liền kề theo hướng phía trên
func dragUp(highVelocity velocity: CGPoint) {
// Handle high Velocity Pan Gesture
switch expansionState {
case .compressed:
expansionState = .haft
animateTopConstraint(constant: topViewContainer(forState: .haft), velocity: velocity)
case .haft, .expanded:
expansionState = .expanded
animateTopConstraint(constant: topViewContainer(forState: .expanded), velocity: velocity)
}
}
}
Và đây là kết quả khi chúng ta build app lên:
Vậy là xong! Ngoài ra các bạn có thể chỉnh sửa lại view sao cho phù hợp với ứng dụng đang làm 😀
Cảm ơn mọi người đã theo dõi bài viết! Mọi ý kiến đóng góp mọi người hãy comment xuống phía dưới để mình có thể thay đổi cho bài biết được tốt hơn. :v
Cũng giống như GCD, Operations giúp chúng ta có thể thực hiện các task đa luồng. Vì Operations là API bậc cao hơn GCD, do đó, Operations cung cấp nhiều tính năng hơn so với GCD như cung cấp các state để quản lí task, dependency giữa các task,… nhưng cũng vì thế mà để khơi tạo và sử dụng operation sẽ khó hơn so với GCD.
Nếu bạn chưa hiểu về GCD, hãy đọc bài về GCD của mình tại:
Ở phần 1 của bài viết Operation, mình sẽ giới thiệu về tính chất và các thuộc tính của Operation và Operation queue.
Nội dung bài viết
Operation State
Block Operation
Subclass Operation
Operation Queue
Operation States
Operation cũng có công dụng như là 1 DispatchWorkItem vậy. Chúng đều là 1 task, bạn khai báo thân hàm cho chúng và đưa chúng vào queue để thực hiện.
Tuy nhiên, điểm vượt trội của Operation so với DispatchWorkItem là mỗi Operation đều có State của riêng mình, còn workItem thì không.
isReady: sẵn sàng để được thực hiện
isExecuting: đang được thực hiện
isCancelled: Nếu bạn gọi phương thức cancel(), operation sẽ chuyển qua state isCancelled trước khi chuyển sang state isFinished.
isFinished: Nếu operation k bị cancel, nó sẽ chuyển từ state isExecuting sang isFinished khi hoàn thành.
Note:
Các state của Operation là các thuộc tính read-only.
Bạn có thể gọi cancel() để hủy 1 operation, nhưng tương tự như workItem, chúng chỉ có thể bị cancel trước khi được thực hiện. Tuy nhiên, do Operation có state, nên bạn có thể kiểm tra state trong thân hàm để cancel hàm. Mình sẽ làm rõ hơn ở phần sau.
Block Operation
Có thể tạo 1 operation 1 cách nhanh chóng bằng cách sử dụng 1 block operation:
Gọi hàm start() để thực hiện BlockOperation
Bạn cũng có thể add thêm nhiều closures vào block operation:
Block Operation hoạt động như một DispatchGroup. Nếu bạn cung cấp 1 completionBlock closure cho blockOperation, thì nó sẽ được thực hiên khi tất cả các closure được add vào block operation đã được thực hiện hết (tương tự hàm notify của DispatchGroup).
Kết quả thu được trên màn hình console:
Note: Từ kết quả thu được ở trên, ta có thể dễ dàng kết luận:
Task trong block operation chạy concurrent.
Operation Blocks chạy default trên global queue.
Subclass Operation
BlockOperation rất dễ sử dụng và thích hợp cho các task đơn giản. Tuy nhiên, đối với những task phức tạp cần kiểm soát state, hoặc những task sử dụng nhiều lần, thì bạn nên tự tạo 1 Operation để sử dụng.
Bạn phải override lại hàm main(), đây là hàm được thực hiện khi operation đó bắt đầu thực hiện.
Gọi start() để chạy. Tuy nhiên, lưu í rằng, khi gọi start() 1 cách trực tiếp trên 1 operation, Operation đó sẽ chạy theo kiểu sync trên current thread. -> Không nên gọi start trên main thread.
Note: Không chỉ việc gọi start sẽ block current thread, mà nó còn dễ dẫn tới 1 exception rằng task đó chưa sẵn sàng để thực hiện. Vì vậy bạn không nên gọi start ở 1 operation.
Vậy làm cách nào để có thể start 1 operation ?
Câu trả lời cho việc này là tương tự như DispatchWorkItem, chúng ta sẽ đưa operation vào Operation Queue để chạy.
Operation Queue:
Bạn đưa các task vào trong operation queue, operation queue sẽ tự động thực hiện task khi thích hợp mà bạn không cần phải gọi start.
Operation Queue chỉ thực hiện task khi task đó đang ở state isReady.
Khi bạn add 1 operation vào 1 queue, task đó sẽ chạy cho đến khi hoàn thành hoặc bị hủy (cancel).
Không thể add 1 operation vào nhiều operation queues khác nhau.
Block queue:
Operation queue có 1 thuộc tính là waitUntilAllOperationsAreFinished, có tác dụng block queue hiện tại, vì vậy bạn sẽ không nên gọi trên main thread.
Trong trường hợp bạn không muốn block cả 1 queue chỉ để đợi 1 task thực hiện xong mà bạn chỉ muốn block 1 vài task, bạn có thể gọi addOperations(_:waitUntilFinished:) method on OperationQueue.
Tạm dừng queue:
Bạn có thể tạm dừng operation queue bằng cách set isSuspend = true.
-> Những task đang được thực hiện vẫn sẽ dc tiếp tục thực hiện, nhưng những task chưa dc thực hiện thì sẽ đợi cho đến khi isSuspend được set lại thành false.
Set số lượng operations tối đa
Còn nếu như bạn muốn giới hạn số lượng tối đa operations chạy trong 1 Operation Queue tại 1 thời điểm, chẳng hạn như việc chỉ load những ảnh của những visible cell của collectionView chẳng hạn?
Khi đó, bạn chỉ cần set thuộc tính maxConcurrentOperationCount của Operation Queue thành 1 số bạn muốn.
class DownloadImage: Operation {
override func main() {
// Download image task
print("Start downloading... at time: \(Date().timeIntervalSince1970)")
sleep(5)
print("Finish downloading... at time: \(Date().timeIntervalSince1970)")
}
}
let operation = DownloadImage()
let operation2 = DownloadImage()
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
operationQueue.addOperations([operation, operation2], waitUntilFinished: true)
Nếu set maxConcurrentOperationCount = 1, thì task 1 chạy xong, sau đó task 2 mới chạy. Đây là kết quả trên màn hình console:
Nếu set maxConcurrentOperationCount = 2, thì 2 task sẽ chạy song song. Đây là kết quả trên màn hình console:
Ở part cuối của Animation Basic này, mình xin giới thiệu đến mọi người phần ảo diệu nhất của animation với UIView đó là UIView.transition. Transitions được dùng khi chúng ta muốn tạo hiệu ứng cho việc hidden view hay add hoặc remove một view lên hoặc khỏi một view cha của nó hay thay thế 2 views cho nhau khá tương đồng với các slide chúng ta hay gặp trong power point.
Transition rất đơn giản và cực kỳ hiệu quả, nó giúp app của chúng ta trở lên thân thiện, gần gũi hơn đối với người dùng. Tuy nhiên, nếu trong một màn hình có quá nhiều chuyển động sẽ dễ gấy rối mắt vì vậy hãy sử dụng các hiệu ứng animation một cách hiệu quả để đạt được kết quả tối ưu nhất.
Cảm ơn mọi người đã theo dõi loạt bài về Basic Animation của mình! Hi vọng sẽ được mọi người ủng hộ ở những bài tiếp theo.
Như phần trước, mình đã đề cập đến những khái niệm cơ bản về animation trong iOS. Với phần này, mình sẽ đi sâu hơn về các thuộc tính và hiệu ứng của UIView để làm ra một animation đẹp mắt.
withDuration: giá trị TimeInterval (typealias cho Double) thời gian thực hiện animation tính bằng giây.
delay: độ trễ tính bằng giây (TimeInterval cũng vậy) trước khi animation bắt đầu. Nếu bạn muốn bắt đầu hoạt hình ngay lập tức, bạn có thể bỏ qua tham số này (chỉ trong một số trường hợp nhất định) hoặc đặt thành 0.
usingSpringWithDamping: độ nẩy của view khi gần đến điểm kết thúc( thuộc tính này giống với sự dao động của một vật thể khi được treo trên một cái lò xo trong thực tế).
initSpringVelocity: Tốc độ di chuyển của view bắt đầu ở thời điểm xuất phát.
Animation with Spring
options: hiệu ứng hoạt hình của view trên quãng đường. Dưới đây là 1 số thuộc tính cơ bản của options:
.repeat: dùng để lặp lại animation vô hạn.
.autoreverse: là hiệu ứng view tới điểm kết thúc và trở lại điểm xuất phát.
.curveLinear: animation không có sự thay đổi về tốc độ từ khi bắt đầu tới điểm kết thúc.
.curveEaseIn: Tăng tốc khi bắt đầu animation
.curveEaseOut: Giảm tốc khi kết thúc animation
.curveEaseInOut: Kết hợp của .CurveEaseIn và .CurveEaseOut, tăng tốc khi bắt đầu animation và giảm tốc khi kết thúc animation.
Keyframes giúp tạo các chuỗi animation riêng biệt
Chúng ta có thể sử dụng các UIView.animation chồng lên nhau để tạo thành các chuỗi animation tuy nhiên sẽ gây lộn xộn các dòng code. Mọi việc sẽ trở lên dễ dàng hơn rất nhiều với keyframes hỗ trợ rất tốt xử lý các chuỗi animation kế tiếp nhau.
Tương tự UIView.animation, câu lệnh của keyframe cũng gồm 1 số thuộc tính:
relativeStartTime: là thời điểm bắt đầu tính theo % trên tổng thời gian diễn ra animation. RelativeStartTime có giá trị 0 -> 1. Ví dụ: relativeStartTime = 0.2 -> animation sẽ bắt đầu sau 20% * tổng thời gian animation.
relativeDuration: là lượng thời gian tương đối tính theo % diễn ra animation cho keyframes với tổng thời gian diễn ra toàn bộ animation. Ví dụ: relativeDuration = 0.25 tức là animation sẽ diễn ra trong 25% * tổng thời gian animation.
animations: closure chỉ ra animation đối tượng gì.
Hôm nay, mình sẽ chia sẻ đôi chút kiến thức của mình về tạo animation giúp app của chúng ta trở lên thú vị và thu hút hơn đối với người dùng.
Những điều cơ bản để tạo được animation
Về cơ bản, animation là tổ hợp các thay đổi về frame, bounds, center, transform, alpla, backgroundColor của đối tượng trên mặt phẳng và không gian chứa đối tượng ấy. Tất cả những animaton phức tạp đều được tạo thành từ những phần tử cơ bản nói trên vì vậy khi gặp 1 yêu cầu về animation phức tạp, bạn đừng vội lo lắng, thay vào đó, hãy phân tích animation ấy và đưa về các khái niệm cơ bản nhất và thực hiện từng bước các thay đổi điều kì diệu sẽ xuất hiện và bạn sẽ rất thích thú với những gì mình làm được. 🙂
UIView.animation thường được sử dụng với những animation đơn giản:
Nói về ViewController thì chắc hẳn tất cả iOS Developer đều biết đến và đã sử dụng rất nhiều. Nhưng đối với các bạn mới bắt đầu với iOS, mọi người thường không chú ý nhiều đến vòng đời của ViewController, dẫn đến mắc phải một số lỗi không đáng có. Bài viết này mình sẽ giới thiệu cho các bạn mới bắt đầu với iOS về vòng đời của View Controller và cách sử dụng để tránh những lỗi không đáng có.
View Controller Lifecycle là gì?
View Controller lifecycle là vòng đời của một view controller được tính từ lúc nó được nạp vào bộ nhớ (RAM) cho tới khi nó bị giải phóng khỏi bộ nhớ.
Phân tích vòng đời của View Controller
Dưới đây là sơ đồ về vòng đời của nó:
Như các bạn đã thấy, trên sơ đồ này có khá nhiều trạng thái mà các bạn chắc dã nhìn thấy rất nhiều nhưng một số thì không phải không 😀 Và nó cũng là các phương thức tương ứng được gọi tự động trong vòng đời của View Controller
OK, bây giờ chúng ta sẽ đi vào chi tiết.
loadView
Phương thức này được gọi khi View hiện tại đang bằng nil. Cơ bản nó sẽ đưa View mà bạn tạo trong phương thức này vào view của ViewController.
NOTE: Phương thức này được sử dụng khi View Controller được tạo bằng code. Nếu chúng ta tạo View Controller từ file .xib hoặc storyboard thì tốt nhất không sử dụng hương thức này.
viewDidload:
Phương thức này được gọi một lần duy nhất trong vòng đời của ViewController. Nó được gọi khi tất cả các view đã được load vào bộ nhớ(RAM).
Ứng dụng: 1. Khi bạn muốn cài đặt giao diện người dùng (User Interface) 2. Những công việc mà bạn muốn nó chỉ chạy duy nhất một lần trên View Controller này.
viewWillAppear:
Phương thức này được gọi mỗi lần trước khi nội dung của View được thêm vào view hierarchy của ứng dụng.
Ứng dụng: Vì phương thức này sẽ được gọi mỗi lần trước khi View được xuất hiện nên nó thường dùng khi bạn muốn 1 công việc nào đó luôn được gọi mỗi khi View Controller đó hiển thị trên màn hình. VD: Kiểm tra kết nối mạng, kiểm tra service state, add observer Notification v.v.
NOTE: • Tránh làm các công việc mà bạn chỉ muốn thực hiện nó một lần trong vòng đời của View Controller trong phương thức này. • Nếu bạn add observer notification ở hàm này thì cần remove notification ở phương thức viewDidDisappear:. Để tránh trường hợp khi quay trở lại màn hình này hàm add observer notification sẽ được đăng kí một lần nữa -> nó sẽ thực thi hàm trong #selector nhiều lần. • Với tương tác tầng Application (VD: Bấm Home, show notification center, show control center… ) rồi trở lại ứng dụng thì sẽ không kích hoạt phương thức này mà nó sẽ kích hoạt các phương thức của UIApplication.
viewDidAppear:
Phương thức này được gọi mỗi lần sau khi nội dung của View được thêm vào view hierarchy của ứng dụng.
Ứng dụng: Thường được sử dụng để lưu dữ liệu, bắt đầu animation, bắt đầu chơi Video hoặc âm thanh hoặc thu thập dữ liệu từ network.
NOTE: • Tương tự như viewWillAppear, với tương tác tầng Application (VD: Bấm Home, show notification center, show control center… ) rồi trở lại ứng dụng thì sẽ không kích hoạt phương thức này mà nó sẽ kích hoạt các phương thức của UIApplication. • Nếu bạn add observer notification ở hàm này thì cũng phải xóa notification ở viewDidDisappear:
viewWillDisappear:
Phương thức này được gọi trước khi view được xóa khỏi view hierarchy. View vẫn còn trên view hierarchy nhưng chưa được xóa.
Ứng dụng: Thường dùng để quản lí các timer, ẩn bàn phím, hủy các network request và lưu lại các trạng thái.
viewDidDisappear:
Phương thức này được gọi sau khi view của ViewController được xóa khỏi view hierarchy.
Ứng dụng: Thường sử dụng để hủy việc lắng nghe các thông báo (Notification) hoặc các cảm biến của thiết bị các trên màn hình này.
deinit:
Phương thức này được gọi trước khi một view controller bị xóa khỏi bộ nhớ.
Ứng dụng: Thường được sử dụng để xóa tài nguyên mà view controller đã được phân bổ nhưng không được giải phóng bới ARC(Automatic reference counting)
NOTE: Hãy nhớ rằng một view controller không còn hiển thị trên màn hình nữa không có nghĩa là nó đã được giải phóng. Ngay cả khi màn hình bị tắt nếu nó vẫn còn trong bộ nhớ thì nó vẫn hoạt động như thường.
didReceiveMemoryWarning:
Phương thức này được gọi khi bộ nhớ (RAM) của máy gần đầy. Và iOS không tự động di chuyển dữ liệu từ bộ nhớ sang không gian ổ cứng hạn chế của nó.
Ứng dụng: Xóa một số đối tượng ra khỏi bộ nhớ.
NOTE: Hãy nhớ rằng nếu bộ nhớ của ứng dụng vượt quá một ngưỡng nhất định, iOS sẽ tắt ứng dụng của bạn. Và nó trông giống như là ứng dụng bị crash
Cảm ơn mọi người đã theo dõi bài viết! Mọi ý kiến đóng góp mọi người hãy comment xuống phía dưới để mình có thể thay đổi cho bài biết được tốt hơn. :v
Xin chào các bạn, lại là tôi đây, bài viết lần này tôi sẽ chỉ cho các bạn cách cải thiện mã nguồn của bạn với Detekt.
Detekt là gì?
Detekt là một công cụ để phân tích code cho lập trình kotlin. Nó hoạt động dựa trên các cú phúp trừu tượng được cung cấp bởi trình biên dịch kotlin.
Detekt có những đặc trưng nào?
Phân tích code smell cho các dự án kotlin của bạn.
Báo cáo độ phức tạp dựa trên các dòng code. Độ phức tạp của McCabe và số lượng code smells.
Cấu hình cao (rule set or rule level)
Loại bỏ các phát hiện với chú thích của kotlin là @Suppress và java là @SuppressWarnings
Xác định ngưỡng code smell sẽ phá vỡ bản build của bạn và in ra cảnh báo.
Code smell baseline và bỏ qua danh sách legacy của dự án.
Gradle Plugin để phân tích code qua Gradle Build.
Grade task sử dụng IntelliJ để định dạng và kiểm tra mã kotlin.
Tuỳ chọn cấu hình của detekt cho mỗi module bằng cách sử dụng profiles (gradle-plugin).
Tích hợp SonarQube.
Có thể mở rộng bằng quy tắc riêng và FileProcessListener’s.
Tích hợp IntelliJ.
Sử dụng detekt như nào?
Trước tiên, bạn hãy cấu hình trong gradle build file.
Chúng ta cùng đi tìm hiểu trong task detekt bên trên có những gì? def input = "$projectDir/src/" là phần nào trong dự án mà bạn muốn được phân tích. def config = "$rootDir/detekt/detekt-config.yml" là phần bạn cài đặt sẽ sử dụng những quy tắc nào để phân tích. def filters = ".*/techover.detekt/.*" là phần loại bỏ không cần phân tích, với nhiều đường dẫn khác nhau thì bạn sử dụng dấu phẩy để phân tách. def output = "$rootDir/detekt/reports" là phần mà khi chạy phân tích sẽ đưa ra tài liệu báo cáo.
Vậy là bạn đã có thể chạy detekt cho dự án của bạn rồi, bằng cách chạy câu lệnh:
./gradlew detekt
Sau khi chạy câu lệnh gradlew bên trên thì sẽ xuất hiện output như dưới đây:
Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details
> Task :app:detekt
Successfully generated XmlOutputReport.
Successfully generated PlainOutputReport.
detekt run within 1019 ms
BUILD SUCCESSFUL in 14s
1 actionable task: 1 executed
Như vậy là source code của dự án bạn đang không có lỗi nào.
Nếu như bạn chạy mà source code có lỗi thì sẽ hiển thị như sau:
> Task :app:detekt
Ruleset: code-smell
Ruleset: comments
Ruleset: complexity
Ruleset: empty-blocks
Ruleset: exceptions
Ruleset: performance
Ruleset: potential-bugs
Ruleset: style
WildcardImport - [ExampleInstrumentedTest.kt] at androidTest/java/techover/detekt/ExampleInstrumentedTest.kt:9:1
WildcardImport - [ExampleUnitTest.kt] at test/java/techover/detekt/ExampleUnitTest.kt:5:1
FunctionNaming - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:13:5
FunctionNaming - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:13:5
MagicNumber - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:15:22
Successfully generated XmlOutputReport.
Successfully generated PlainOutputReport.
detekt run within 1203 ms
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
Để bạn có thể hiểu hơn về tập tin cấu hình mà bạn tham khảo ở link bên trên detekt-config.yml thì dưới đây tôi sẽ nói qua về phần này cho bạn hiểu hơn.
Tập tin cấu hình detekt có những gì?
Detekt sử dụng tệp cấu hình kiểu yaml cho nhiều thứ khác nhau:
Bộ quy tắc và thuộc tính quy tắc.
Build thất bại.
Bộ xử lý tệp kotlin.
Console và định dạng đầu ra.
Bộ quy tắc và quy tắc:
Detekt cho phép dễ dàng chỉ cần chọn các quy tắc bạn muốn và cấu hình chúng theo cách bạn muốn. Ví dụ, nếu bạn muốn cho phép tối đa 20 chức năng trong tệp Kotlin thay vì ngưỡng mặc định là 10.
complexity:
TooManyFunctions:
threshold: 20
Bộ lọc đường dẫn / Không bao gồm / Bao gồm:
Bắt đầu với phiên bản RC15 bộ lọc đường dẫn có thể được xác định cho từng quy tắc hoặc bộ quy tắc:
Xây dựng UICollection View Custom Layout và làm thế nào để lưu cache và dynamically size cell.
UICollection view được giới thiệu từ bản iOS 6 và nó đã là UI Control mà các developer hay dùng nhất.
UICollection view có thể hỗ trợ các loại hiển thị khác nhau và đặc biệt là tính năng dynamically size cell hỗ trợ người dùng hiển thị những phần mô tả ngắn mà đầy đủ nội dung cần truyền tải nhất mà không làm thay đổi thiết kế của mình. Như vậy thì UICollection view được sử dụng nhiều ở các app dạng social network, news,…,.
Ở bài viết này tôi sẽ hướng dẫn các bạn
Custom layout
Xử lý dynamically cell
Những điểm cần lưu ý
Tạo custom UICollection View Layout
Bạn có thể tạo một chế độ hiển thị cho UICollection view theo cách riêng bằng cách tuỳ chỉnh layout của bạn. Collection view layout là lớp con của UICollectionViewLayout, nó xác đinh mọi thuộc tính trong chế độ hiển thị trên UICollectionView của bạn. Các thuộc tính của UICollectionViewLayout là intances của UICollectionViewLayoutAttributes. Chúng chứa các thuộc tính của từng mục trong chế độ hiển thị của bạn.
Bắt đầu
Trước tiên ta cần chuẩn bị một project sample đã được implement UICollection View. UI cần custom là dạng cột và dynamic height.
Tạo custom collection view layout
Cách dùng Custom Collection View Layout
Tạo Custom Collection View Layout
Tạo file CustomCollectionViewLayout sub class là UICollectionViewLayout
Tiếp theo config cho collection view sử dụng custom layout. Mở file view chứa collection view
Mở thanh công cụ Attributes inspector. Chọn Custom trên thuộc tính Layout
Như vậy chúng ta đã config thành công cho collection view sử dụng custom layout.
Như vậy để hiển thị chúng cần thực hiện những gì
Như vậy class CustomCollectionViewLayout vừa tạo cần thực hiện những function sau:
collectionViewContentSize: function này trả về chiều rộng chiều cao của toàn bộ collection view contents.
prepare: UIKit sẽ call function này trước khi có các thay đổi về UI. Đây là nơi mà bạn có thể chuẩn bị các tính toán liên quan đến chế độ xem, kích thước, vị trí ,…,.
layoutAttributesForElements(in:): Trong function này bạn trả về các thuộc tính bố cục cho tất cả item dưới dạng array UICollectionViewLayoutAttributes.
layoutAttributesForItem(at:): Function này cung cấp thông tin bố trí riêng cho từng item. Bạn cần override lại nó để có thể hiển thị cho từng item mà bạn mong muốn.
Tính toán bố cục
Với layout dạng cột và bạn cần height nó được dãn tự động thì bạn cần tính toán lại bố cục của layout
Mở file CustomCollectionViewLayout và tạo protocol cho nó
Trong delegate bạn mới tạo 01 function yêu cầu trả về height, bạn sẽ implement function này trong Controller của chúng.
Tiếp đến bạn cần thực hiện implement class CustomCollectionViewLayout
// 1
weak var delegate: CustomCollectionViewLayoutDelegate?
// 2
private let numberOfColumns = 2
private let cellPadding: CGFloat = 6
// 3
private var cache: [UICollectionViewLayoutAttributes] = []
// 4
private var contentHeight: CGFloat = 0
private var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
// 5
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
Bạn có thể thấy ở đây:
delegate
Số columns bạn cần hiển thị
cache để lưu trữ các thuộc tính hiển thị.
02 Thuộc tính width và height của contents
Trả về kích thước của collection view’s content. Bạn sử dụng cả 2 thuộc tính contentWidth và contentHeight để tính kích thước
Để tính toán cách hiển thị, mời bạn xem sơ đồ sau:
Bạn sẽ tính toán dựa trên số cột bạn muốn thiển thị và vị trí của mục trước đó trong cùng một cộ. Để tính toán thì bạn dùng xOffset cho cột và yOffset cho vị trí.
Tiếp tục bạn cần override lại function prepare
override func prepare() {
// 1
guard
cache.isEmpty,
let collectionView = collectionView
else {
return
}
// 2
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset: [CGFloat] = []
for column in 0..<numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset: [CGFloat] = .init(repeating: 0, count: numberOfColumns)
// 3
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
// 4
let photoHeight = delegate?.collectionView(
collectionView,
heightForPhotoAtIndexPath: indexPath) ?? 180
let height = cellPadding * 2 + photoHeight
let frame = CGRect(x: xOffset[column],
y: yOffset[column],
width: columnWidth,
height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// 5
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// 6
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
Ở function này bạn có thể thấy tính width có thể cho số columns mà bạn muốn hiển thị, và tính height cho từng item và được lưu lại các thuộc tính ở cache.
Tiếp theo bạn override lại function layoutAttributesForElements(in rect: CGRect) function này sẽ được gọi sau khi function prepare() kết thúc
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
Ở function này, bạn duyệt lại bộ cache xem frame của từng item có giao nhau với collection view không.
Cuối cùng bạn cần phải override lại function layoutAttributesForItem
Ở đây tôi đang dùng dynamic height. Height sẽ tự động theo height của image.
Tiếp theo trong function viewDidLoad() bạn thêm
if let layout = collectionView.collectionViewLayout as? CustomCollectionViewLayout {
layout.delegate = self
}
Như vậy bạn đã custom thành công layout trên UICollectionView.
Những điều cần lưu ý
Tới đây bạn sẽ gặp bug là bạn reload data cho collection view khi tăng số lượng item hiển thị. Bạn debug bạn sẽ thấy mọi quá trình bạn làm đã hoàn tất, từ việc get data và add vào list của data source cũng đã đầy đủ, nhưng chỉ thiếu một điều là collection view chỉ chạy lại với số lượng item lần đầu tiên.
Vậy vấn đề ở đây là gì?? tại sao reloadData lại không được và không có bất cứ một dòng log error nào ra ??
Vấn đề ở đây là contents size của collection view không được update, nên collection view không thể hiển thị thêm bất cứ item nào.
Vậy bug ở function nào?
Function nào tính toán lại contents size?
Là func prepare() nó là function được gọi để tính toán size. Ngay ở bước đầu tiên của func prepare()