Category: Swift

  • Lottie Animation: Biến hình màn hình Login của bạn

    Lottie Animation: Biến hình màn hình Login của bạn

    Hi mọi người, ở bài viết trước mình đã hướng dẫn các bạn cách làm sao để thêm và ứng dụng Lottie vào ứng dụng của bạn. Có thể khi đọc bài viết trước các bạn sẽ chưa hình dung được sức mạnh của Lottie Animation. Để chứng minh sức mạnh của Lottie Animation hôm nay mình sẽ hướng dẫn các bạn ứng dụng nó vào màn hình Login và biến nó trở thành 1 màn hình thú vị hơn. Để hiểu rõ hơn về cách cài đặt Lottie animation bạn có thể xem lại bài viết Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Chuẩn bị

    Để có thể bắt đầu dễ dàng và nhanh chóng hơn thì chúng ta cần chuẩn bị sẵn những thứ sau:

    1. Kiến thức về Lottie, tham khảo tại đây
    2. Project demo các bạn tải tại đây

    Bắt đầu

    Đầu tiên chúng ta sẽ kéo UI vào để thực hiện thay đổi màn hình Login như sau:

    Trong màn hình Login của chúng ta sẽ có các thành phần sau:

    1. ImageView làm background cho ứng dụng
    2. 2 Lottie Animation View, 1 cái làm thành phần chính nằm trên nửa màn hình và một cái nằm ở góc nhằm trang trí thêm cho màn hình
    3. 2 UITextField để cho người dùng nhập user name và password
    4. 1 button Login

    Sau khi sử dụng interface builder thực hiện kéo các view vào màn hình thì các bạn kéo reference cho các item vào view controller để thực hiện code.

    Mở ViewController lên và import Lottie

    import UIKit
    import Lottie

    Viết thêm một số func để cài đặt lottie và cài đặt view như sau

    extension ViewController {
        func setupUI() {
            tfAccount.layer.borderWidth = 1
            tfAccount.layer.borderColor = UIColor.orange.cgColor
            tfAccount.backgroundColor = UIColor.orange.withAlphaComponent(0.2)
            tfAccount.layer.cornerRadius = 5
            tfAccount.attributedPlaceholder = NSAttributedString(string: "  Account",
                                                                 attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
            tfAccount.placeholderRect(forBounds: CGRect(x: 5, y: 5, width: 100, height: 44))
            tfPassword.layer.borderWidth = 1
            tfPassword.layer.borderColor = UIColor.orange.cgColor
            tfPassword.backgroundColor = UIColor.orange.withAlphaComponent(0.2)
            tfPassword.layer.cornerRadius = 5
            tfPassword.attributedPlaceholder = NSAttributedString(string: "  Password",
                                                                 attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
            btnLogin.layer.cornerRadius = 5
            loadingView.backgroundColor = UIColor.black.withAlphaComponent(0.4)
        }
        
        func setupLottie() {
            // 1. Set animation content mode
            animationLottieView.contentMode = .scaleAspectFit
            animationLottieView2.contentMode = .scaleAspectFit
    
            // 2. Set animation loop mode
    
            animationLottieView.loopMode = .loop
            animationLottieView2.loopMode = .loop
    
            // 3. Adjust animation speed
    
            animationLottieView.animationSpeed = 0.5
            animationLottieView2.animationSpeed = 0.5
    
            // 4. Play animation
            animationLottieView.play()
            animationLottieView2.play()
        }
        
        private func showLoading() {
            // 1. Create lottie animation view
            let lottieView: LottieAnimationView = LottieAnimationView(name: "loading_crypto")
            lottieView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
            
            // 2. Set animation content mode
            lottieView.contentMode = .scaleAspectFit
            
            // 3. Set animation loop mode
            lottieView.loopMode = .loop
            
            // 4. Adjust animation speed
            lottieView.animationSpeed = 0.5
            
            loadingView.addSubview(lottieView)
            lottieView.center = CGPoint(x: UIScreen.main.bounds.width / 2, y:  UIScreen.main.bounds.height / 2)
            view.addSubview(loadingView)
            // 5. Play animation
            lottieView.play()
            
        }
        
        @objc private func hideLoading() {
            loadingView.subviews.forEach { $0.removeFromSuperview() }
            loadingView.removeFromSuperview()
        }
    }

    Thêm loading view để thực hiện màn hình loading như sau

    class ViewController: UIViewController {
        @IBOutlet weak var tfAccount: UITextField!
        @IBOutlet weak var tfPassword: UITextField!
        @IBOutlet weak var animationLottieView: LottieAnimationView!
        @IBOutlet weak var btnLogin: UIButton!
        @IBOutlet weak var animationLottieView2: LottieAnimationView!
        
        private var loadingView: UIView = UIView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            setupUI()
            setupLottie()
        }
        
        @IBAction func login(_ sender: Any) {
            showLoading()
            perform(#selector(hideLoading), with: nil, afterDelay: 5)
        }
    }

    ở viewDidLoad chúng ta sẽ thực hiện setUpUI và setup lottie, tại func login chúng ta sẽ thực hiện showLoading và để nó ẩn đi sau 5 giây.

    Bây giờ chúng ta thực hiện cmd + R để chạy ứng dụng, kết quả thu được sẽ như sau:

    Kết quả thật tuyệt vời đúng không? Đối với các màn hình có animation phức tạp như vậy để code sử dụng animation gần như ta quá phức tạp và tốn quá nhiều effort để thực hiện, tuy nhiên nếu dùng lottie các bạn chỉ mất một chút thời gian để căn chỉnh layout.

    Vậy là chúng ta đã hoàn thành việc ứng dụng Lottie vào màn hình Login để tăng trải nghiệm của người dùng. Ngoài ra chúng ta còn có thể ứng dụng Lottie vào rất nhiều các tính năng khác để ứng dụng trở nên thú vị hơn. Ví dụ như: Màn hình launch screen với animation logo, animation trên notification, animation Tabbar icon, v.v.

    Cách sử dụng Lottie phụ thuộc rất lớn vào trí tưởng tượng và óc sáng tạo của người dùng, nếu bạn chỉ nắm vai trò là front end developer nhiều khi bạn sẽ không có quyền quyết định ứng dụng sẽ như nào, nhưng nếu bạn có ý tưởng hay ho bạn có thể đưa ý kiến lên Team leader, designer, BA, và PM của dự án để cải thiện trải nghiệm của người dùng.

    Chúc các bạn thành công với các dự án sắp tới.

  • Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Có lẽ tất cả mọi người trên thế giới đều yêu thích cái đẹp và mình cũng không ngoại lệ. Việc xây dựng những ứng dụng có UI đẹp mắt, animation mượt mà và thú vị khiến hàng triệu người dùng trầm trồ là mong muốn của rất nhiều những Developer trên toàn thế giới. Là một developer nếu bạn có tìm thấy bài viết này của mình thì chắc hẳn bạn cũng có khát khao làm những điều thú vị cho ứng dụng của mình có đúng không? Vậy chúng ta cùng đi tiếp vào bài viết này để làm cách nào để có thể làm ứng dụng của các bạn trở nên thú vị hơn nhé.

    Trong một dịp mình có cơ hội làm việc trong một dự án có sử dụng rất nhiều Animation để làm cho ứng dụng trở nên đẹp và thú vị hơn với người dùng. Lúc mới vào dự án khi sử dụng một số tính năng có sử dụng animation làm mình thấy rất hứng thú, thật khó để cưỡng lại sự đẹp đẽ và hoa mỹ của nó. Trong khi làm việc trong dự án thì mình cũng đã phát hiện ra một thư viện hỗ trợ cho việc thực hiện animation rất hay đó chính là Lottie. Vì vậy ngày hôm nay mình muốn chia sẻ với các bạn về thư viện này và cách thêm nó vào ứng dụng của các bạn.

    Lottie là gì?

    Lottie là định dạng tệp hoạt hình dựa trên JSON cho phép các nhà thiết kế gửi animation trên bất kỳ nền tảng nào dễ dàng như vận chuyển nội dung tĩnh. Bạn có thể đọc thêm về nó ở đây.

    Lottie được sử dụng khi nào?

    Lottie khá là linh hoạt và nó có thể sử dụng được trên nhiều nền tảng khác nhau từ iOS, Android, Web …. Vì vậy nó giúp đồng bộ animation trên tất cả các nền tảng mà không xảy ra sai sót nào.

    Lottie hỗ trợ làm animation rất tốt, nó có thể làm được những animation rất phức tạp mà việc code animation không thể làm được hoặc làm đơn giản hoá việc thêm animation vào trong ứng dụng.

    Hiện nay nó đang được sử dụng khá phổ biến trên các ứng dụng.

    Làm sao để sử dụng Lottie cho ứng dụng của bạn?

    Bước 1: Tạo dự án mới

    Nếu bạn muốn thêm vào dự án có sẵn của mình thì bỏ qua bước này nhé.

    Đầu tiên chúng ta mở Xcode lên và tạo một ứng dụng demo để có thể test Lottie chạy trên ứng dụng một cách dễ dàng hơn.

    Tạo ứng dụng mới

    Bước 2: Thêm thư viện Lottie vào ứng dụng

    Để thêm thư viện Lottie vào ứng dụng chúng ta có thể sử dụng bằng nhiều cách khác nhau như Cocoa Pods, Carthage hoặc Swift Package Manager. Nếu bạn chưa biết cách thêm thư viện Lottie vào dự án thì bạn có thể tham khảo hướng dẫn tại đây.

    Ở bài viết này mình sẽ hướng dẫn các bạn sử dụng Swift Package Manager để thêm vào ứng dụng như sau:

    Bạn mở dự án của bạn trên Xcode, chọn File -> Add Packages… Trên đầu bên phải của popup hiện ra có công cụ tìm kiếm các bạn đánh link: https://github.com/airbnb/lottie-ios.git để tìm thư viện. Sau đó bấm vào Add Package và chờ một lúc để Xcode tải thư viện bạn, sau khi tải xong bạn bấm Add để hoàn tất

    Bước 3: Thêm Lottie vào dự án

    Đầu tiên chúng ta cần chuẩn bị file Lottie JSON, nếu bạn chưa có thì bạn có thể download nó tại đây. Bạn nhớ tải file JSON nhé.

    Bạn cũng có thể sử dụng ứng dụng Adobe After Effect để tạo file Lottie của riêng mình.

    Khi đã có file lottie JSON rồi chúng ta sẽ thực hiện thêm file vào trong dự án bằng cách kéo thả nó trực tiếp vào thư mục trong Xcode của bạn, tại nơi mà bạn muốn lưu nó. Thông thường chúng ta tạo mới thư mục Resouces/LottieJSON và lưu nó trong đó.

    Add file lottie vào dự án

    Hãy nhớ bạn tích Copy Items if needed và các mục như trên hình nhé.

    Thêm Lottie bằng cách sử dụng code

    Đầu tiên để sử dụng được thư viện Lottie hãy nhớ import thư viện Lottie vào màn hình mà bạn sử dụng

    import Lottie

    Tiếp theo chúng ta tạo func addLottieAnimation() để thực hiện nhiệm vụ add lottie vào view hiện tại. Bạn cũng có thể custom một func riêng để handle xử lí triệt để các logic của Lottie animation. Trong bài này mình chỉ hướng dẫn cơ bản để các bạn có thể thêm Lottie vào ứng dụng của mình.

        func addLottieAnimation() {
            // 1. Create lottie animation view
            let lottieView: LottieAnimationView = LottieAnimationView(name: "404-Notfound")
            lottieView.frame = view.bounds
            
            // 2. Set animation content mode
            lottieView.contentMode = .scaleAspectFit
            
            // 3. Set animation loop mode
            lottieView.loopMode = .loop
            
            // 4. Adjust animation speed
            lottieView.animationSpeed = 0.5
            
            view.addSubview(lottieView)
            
            // 5. Play animation
            lottieView.play()
        }

    Sau đó bạn call func này ở viewDidLoad thì sẽ nhận được kết quả như sau:

    Thêm Lottie bằng cách sử dụng Builder Interface

    Đầu tiên bạn mở file giao diện của bạn lên băng Interface Builder, kéo một UIView và thực hiện constraint cho nó.

    Thêm UIview vào để sử dụng Lottie

    Sau đó ở trên cùng của tab bên phải bạn chọn Identity Inspector và thay đổi giá trị như hình dưới

    Thay đổi class sang LottieAnimationView

    Tiếp tục chuyển sang tab Attributes Inspector để điền tên file Lottie mà bạn muốn

    Tạo một liên kết giữa view của bạn với file controller để sử dụng, sau đó bạn thực hiện code như dưới đây để Lottie có thể hoạn động.

    import UIKit
    import Lottie
    
    class ViewController: UIViewController {
        @IBOutlet weak var animationLottieView: LottieAnimationView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            // 1. Set animation content mode
            animationLottieView.contentMode = .scaleAspectFit
    
            // 2. Set animation loop mode
    
            animationLottieView.loopMode = .loop
    
            // 3. Adjust animation speed
    
            animationLottieView.animationSpeed = 0.5
    
            // 4. Play animation
            animationLottieView.play()
        }
    }

    Chạy ứng dụng và bạn sẽ nhận được kết quả như mong đợi!

    Vậy là mình đã giới thiệu và hướng dẫn cho các bạn một thư viện khá hiệu quả cho các bạn sử dụng để bổ trợ cho các bạn việc thực hiện animation tốt hơn, dễ dàng hơn, và nhanh hơn so với cách code truyền thống. Từ giờ các bạn có thể tha hồ sáng tạo trên những ứng dụng sắp tới của bản thân và khiến mọi người sử dụng thích thú.

    Mình hi vọng nó sẽ giúp các bạn làm ra những ứng dụng có trải nghiệm tuyệt vời, giúp những người sử dụng ứng dụng của các bạn phải trầm trồ khi sử dụng nó. Chúc các bạn thành công với những dự án sắp tới của mình.

  • Architecture pattern: Clean Swift

    Architecture pattern: Clean Swift

    Xin chào mọi người! Lại là DaoNM2 đây! Tiếp tục với series về Architecture pattern hôm nay mình sẽ giới thiệu cho các bạn một kiến trúc khá mới so với các mẫu kiến trúc hiện tại đó là Clean Swift (VIP). Trước đây mình đã có cơ hội tiếp cận với kiến trúc này, khi tham gia vào việc xây dựng một ứng dụng rất lớn cho một công ti rất nổi tiếng về ô tô xe máy ở Việt Nam. Khi đó mình cũng đã tích luỹ được một số kinh nghiệm về kiến trúc này, vì vậy mình muốn chia sẻ với các bạn một số thông tin cũng như kinh nghiệm mà mình đã tích luỹ được khi làm việc với mẫu kiến trúc này.

    Đầu tiên nếu bạn là người mới hoặc bạn mới tìm hiều về các mẫu kiến trúc trong lập trình bao giờ đây là bài đầu tiên bạn đọc, thì để có thể hiểu kiến trúc này tốt hơn thì bạn có thể tham khảo các bài viết về các mẫu thiết kế trước khi tiếp tục ở link dưới đây:

    Bối cảnh hình thành

    Clean swift lần đầu tiên được giới thiệu bởi Raymond Law trên website clean-swift.com của anh ấy. Ý tưởng hình thành mẫu kiến trúc này là do anh ấy đã quá chán với các vấn đề của MVC(một mẫu kiến trúc mà Apple khuyên dùng) vì vậy anh ấy đã nghĩ ra Clean Swift để giải quyết các vấn đề mà các mẫu kiến trúc trước đây chưa làm được. Clean Swift được Raymond Law xây dựng dựa trên Clean Architecture của Uncle Bob.

    Clean Swift là gì?

    Clean Swift là một mẫu kiến trúc xây dựng dựa trên Clean Architecture của Uncle Bob để áp dụng cho việc xây dựng các ứng dụng iOS và MacOS.

    Trong Clean Swift Architecture pattern thì tất cả logic của ứng dụng sẽ được chia đều ra 3 thành phần chính của nó là View controller, Interactor và Presenter. Mỗi phần sẽ đảm nhiệm một số logic cụ thể, và chúng liên lạc với nhau bằng các liên kết 1 chiều, vì vậy source code của bạn sẽ luôn luôn đi theo một chiều chứ không đa chiều như các architecture pattern khác.

    Khi ứng dụng Clean Swift vào trong dự án của bạn, nó sẽ được cấu trúc theo từng màn hình của ứng dụng(scenes).

    Các thành phần của Clean Swift

    Mẫu kiến trúc Clean Swift được cấu tạo bởi các phần như sau:

    • View
    • View Controller
    • Router
    • Presenter
    • Interactor
    • Worker(optional)
    • Model(optional)
    Clean Swift architecture pattern

    Clean Swift gồm 3 phần chính là ViewController, Presenter và Interactor. 3 phần này có liên kết 1 chiều và tạo thành 1 vòng tròn. Khi View controller nhận được request nó sẽ gọi sang Interactor để nó xử lí logic, khi xong logic Interactor sẽ gửi dữ liệu sang bên Presenter để nó thực hiện format lại dữ liệu rồi trả về cho ViewController làm nhiệm vụ update lên View cho người dùng. Các thành phần này sẽ được kết nối với nhau bằng protocol.

    View

    Là các thành phần nằm trong UIKit hoặc bất kể thứ gì liên quan tới UI ví dụ như: storyboard, xib, UIView, UIControl …

    View Controller

    Định nghĩa các màn hình(scenes), nó có thể chứa 1 hoặc nhiều View

    Nó sẽ giữ các instances của Interactor và Router

    Là nơi nhận các tương tác của người dùng và gọi đến Interactor hoặc Router để xử lý, nó cũng nhận output của Presenter làm input và truyền nó lên view để hiển thị cho người dùng.

    Interactor

    Chứa các business logic của màn hình

    Giữ instance của Presenter và các Workers(nếu có)

    Nhận thông tin input từ ViewController và xử lý hoặc yêu cầu Worker làm việc để truyền kết quả sang cho Presenter

    Interactor sẽ không được import UIKit để đảm bảo source không có liên kết trực tiếp với View

    Presenter

    Giữ một tham chiếu yếu đến View Controller để truyền dữ liệu sang View Controller

    Là nơi xử lý logic hiển thị, khi nhận được input từ Interactor nó sẽ thực hiện format lại dữ liệu và truyền sang cho ViewController để nó hiển thị thông tin cho người dùng.

    Worker

    Là nơi được coi là trung tâm dữ liệu, nó sẽ thực hiện các nhiệm vụ liên quan tới việc lấy dữ liệu từ API hoặc LocalDB

    Là thành phần phụ nên ở các màn hình đơn giản không có tương tác với dữ liệu chúng ta có thể bỏ qua worker

    Router

    Nó giữ một tham chiếu yếu tới View Controller, nhằm mục đích tránh việc tham chiếu lẫn nhau(retain cycles) dẫn đến không thể release các đối tượng này khi nó không còn được sử dụng -> Lack memory.

    Router sinh ra để giảm tải công việc cho View Controller nó sẽ làm nhiệm vụ điều hướng trong ứng dụng.

    Model

    Nó là nơi định nghĩa các đối tượng cho ứng dụng, nó chỉ làm nhiệm vụ định nghĩa các đối tượng và không có xử lí logic hay liên kết trực tiếp với các thành phần khác của kiến trúc.

    Các đối tượng trong model sẽ được khai báo theo value type(struct, enum)

    Tương tự như Worker, một số màn hình đơn giản không tương tác với dữ liệu sẽ không cần đến Model

    Ưu điểm

    • Dễ maintain, fixbugs vì liên kết 1 chiều giữa các thành phần
    • Hỗ trợ viết Unit test một cách dễ dàng
    • Viết các phương thức ngắn hơn với trách nhiệm duy nhất
    • Tách được business logic sang cho Interactor xử lí
    • Có thể tái sử dụng các Workers và Services
    • Có thể áp dụng được cho các dự án lớn để giảm tình trạng conflict khi merge source.

    Nhược điểm

    • Quá nhiều các protocol với các nhiệm vụ riêng biệt, làm cho việc đặt tên trở nên khó khăn và không cẩn thận sẽ gây khó hiểu cho người đọc
    • Kích thước ứng dụng lớn do chứa nhiều protocol và nhiều file
    • Cần thời gian để cho các thành viên dự án có thể hiểu và tuân theo

    Tổng kết

    Clean swift là một mẫu kiến trúc không phổ biến như các mẫu khác, tuy nhiên ưu điểm của nó mang lại là rất lớn. Để vận hành các dự án lớn có yêu cầu viết Unitest chúng ta có thể coi Clean Swift là một trong những ứng cử viên sáng giá. Mình hi vọng bài viết có thể giúp các bạn có thêm kiến thức về mẫu kiến trúc Clean Swift và giúp các bạn có thể chọn được mẫu kiến trúc ưng ý cho những dự án sắp tới.

    Nếu các bạn muốn biết thêm nhiều thông tin hơn về Clean Swift Architecture Pattern thì các bạn có thể tham khảo tại link sau: Clean Swift

  • Architecture Pattern: MVVM trong iOS

    Architecture Pattern: MVVM trong iOS

    Chào các bạn, để tiếp tục series về Architecture pattern thì hôm nay mìn sẽ giới thiệu đến một mô hình có thể giải quyết được một số nhược điểm của các mô hình cũ như MVC, MVP.

    Nếu các bạn chưa tiếp cận hoặc chưa tìm hiều về các Architecture Pattern bao giờ thì có thể xem lại các bài viết của mình về MVC hoặc MVP tại đây:
    iOS Architecture Patterns: Cocoa MVC
    MVP Architecture Pattern và biến thể MVP-C
    Để khi đi vào bài viết này chúng ta sẽ dễ dàng hiểu được nội dung bài viết truyền tải.

    Lịch sử hình thành và phát triển

    MVVM được viết đầy đủ là Model View ViewModel, MVVM là một biến thể của mẫu thiết kế Presentation Model của Martin Fowler. Nó được sáng lập ra bởi các kiến trúc sư của Microsoft tên là Ken Cooper và Ted Peters, nó đặc biệt được sinh ra để làm đơn giản việc lập trình hướng sự kiện (event-driven programming). MVVM được tích hợp vào Windows Presentation Foundation (WPF) (hệ thống đồ họa .NET của Microsoft) và Silverlight, dẫn xuất ứng dụng Internet của WPF. John Gossman, một kiến ​​trúc sư Microsoft WPF và Silverlight, đã công bố MVVM trên blog của mình vào năm 2005.

    MVVC là gì?

    MVVC là một mẫu kiến trúc giúp tách biệt source code của bạn ra thành nhiều thành phần khác nhau. Nó giúp code của bạn có các thành phần độc lập, giúp cho quá trình phát triển và kiểm thử ứng dụng trở nên rõ dàng và dễ dàng hơn.

    Cấu tạo của MVVM

    MVVM gồm 3 phần chính là Model, View và ViewModel.

    MVVM

    Model

    Là nơi chứa dữ liệu và xử lí business logic, model sẽ thực hiện các công việc như lưu trữ các data được lấy về từ API, local storage, v.v. Nó độc lập so với View và tương tác với View thông qua ViewModel.

    View

    Là nơi hiển thị giao diện cho người dùng, nhận các sự kiện từ người dùng, xử lí và gửi các yêu cầu của người dùng cho ViewModel xử lí. View trong iOS thì thông thường là các thành phần của UIKit, storyboard, xib …, ở MVVM trong iOS thì View bao gồm cả các View Controller, nó sẽ là thành phần cài đặt cho View và gửi và nhận thông tin từ ViewModel.

    ViewModel

    Là nơi xử lí các logic hiển thị(presentation logic), nó là cầu nối giữa View và Model. ViewModel sẽ nhận yêu cầu từ View và lấy dữ liệu từ model về xử lí sau đó trả lại cho View thứ mà nó cần để hiển thị lên màn hình cho người dùng.

    Trong khi MVC thì Controller, MVP thì có Presenter làm trung gian giữa View và Model. Ở MVVM thì ViewModel cũng tương tự, nó là thành phần trung gian giúp kết nối View với Model.

    Ưu điểm của MVVM

    • Vì MVVM là mô hình nâng cấp của MVC, cho nên nó giúp app vẫn duy trì cấu trúc của mô hình MVC và bao gồm các ưu điểm của MVC
    • Giảm tải lượng code chứa trong View và View Controller.
    • Khi đó View và View Controller trở nên đơn giản hơn khi những logic.
      Ví dụ như logic về quy định cách hiển thị của dữ liệu, được chuyển hết sang ViewModel. Điều này khiến cho code trở nên dễ hiểu và dễ maintain hơn.
    • Sự liên lạc giữa các thành phần trong mô hình rõ ràng, khiến nó hoạt động tốt hơn với cơ chế binding dữ liệu.
    • Có thể thực hiện UnitTest lên tầng ViewModel.
    • Nhiệm vụ được chia đều cho các tầng

    Nhược điểm của MVVM

    • Nhiều file nên source code lại nhiều thêm
    • Tương tác giữa các thành phần phức tạp hơn các mẫu kiến trúc khác như MVC, MVP vì vậy người mới khó tiếp cận và thực hiện hơn.
    • Rắc rối trong việc phản hồi lại yêu cầu hơn so với các mẫu kiến trúc khác
    • Đối với nhưng dự án nhỏ thì nó lại quá cồng kềnh để thực hiện

    Tổng kết

    MVVM là một mẫu kiến trúc rất tốt khi bạn triển khai những ứng dụng có kích thước vừa và lớn, nó hỗ trợ UnitTest khá hiệu quả. Trong MVVM thì nhiệm vụ được chia đều cho các tầng vì vậy sẽ không quá khó để quản lý source code. Mình hi vọng bài viết giúp các bạn có thể dễ dàng hơn khi chon mẫu kiến trúc cho các dự án mới. Chúc các bạn thành công!

  • AVFoundation trên Swift và ứng dụng để xây dựng tính năng QR Scan

    AVFoundation trên Swift và ứng dụng để xây dựng tính năng QR Scan

    Xin chào tất cả các bạn

    Thời gian gần đây, như các bạn có thể thấy cứ bước chân ra khỏi nhà là thấy đâu đâu cũng có những ô mã QR. Từ việc đi đá bát phở cũng quét thanh toán QR, đăng nhập zalo hay telegram cũng có thể quét QR, hay thậm chí trà đá vỉa hè cũng có QR luôn…. Điều đó chứng tỏ mã QR đang dần được ứng dụng rất rộng rãi vào tất cả các lĩnh vực trong cuộc sống, thay thế cho giấy tờ truyền thống cũng như giúp cuộc sống trở nên tiện lợi hơn.

    Tuy nhiên nếu chỉ có mỗi mã QR thì cũng không giúp ích được gì khi mà không có những thiết bị đọc và giải mã những chiếc QR vi diệu này. Và những thiết bị để đọc mã QR cũng chẳng đâu xa ngay chính trên chiếc smart phone mà lúc nào cũng theo các bạn 24/7. Việc tích hợp QR Scan vào các ứng dụng di động ngày nay như một tính năng không thể thiếu cũng như giúp ứng dụng trở nên đa nhiệm hơn

    Thời gian vừa qua mình cũng có cơ duyên được trải qua một dự án về ngân hàng và phát triển tính năng QRPay, một tính năng mà như các bạn có thể thấy lúc nào cũng xuất hiện trên các ứng dụng Internet Banking cũng như các ví điện tử. Vậy nên ở bài viết này, mình xin chia sẻ các bạn cách tạo một ứng dụng Scan QR đơn giản và những framework liên quan trên iOS bằng ngôn ngữ lập trình Swift. Let’s go !

    Phần 1: Tìm hiểu về mã QR

    QR là viết tắt của Quick response có thể tạm dịch là mã phản hồi nhanh. Đây là dạng mã vạch có thể đọc được bởi một máy đọc chuyên dụng hoặc bằng smartphone có chức năng chụp ảnh kèm với ứng dụng cho phép quét mã. Mã QR còn có thể được gọi là Mã vạch ma trận (Matrix-barcode) hoặc Mã vạch 2 chiều (2D), là một dạng thông tin đã được mã hóa và có thể hiển thị để máy quét mã có thể đọc được.

    Mã QR là một mã vạch ma trận được phát triển bởi công ty Denso Wave vào năm 1994. Denso Wave là công ty con của Toyota. Mã QR gồm những chấm đen và các ô vuông trên nền trắng, nó thể chứa đa dạng các thông tin như URL, thông tin cá nhân, thời gian, địa điểm của một sự kiện nào đó, mô tả, giới thiệu một sản phẩm nào đó,…

    Phần 2: AV Foundation Framework trong Swift

    Như các bạn có thể thấy, để làm việc với Media trên các thiết bị iOS, Apple đã cung cấp rất nhiều các framework mạnh mẽ. Ở tầng cao nhất (high-level) là UIKit và AVKit, tầng thấp nhất (low-level) là CoreAudio, Core Media, Core Animation và ở giữa chính là nhân vật chính của chúng ta – AVFoundation

    UIKit framework giúp dễ dàng kết hợp tính năng chụp ảnh tĩnh và quay video cơ bản vào ứng dụng. Cả Mac OS X và iOS đều có thể sử dụng thẻ HTML5 <audio> và <video> bên trong WebView hoặc UIWebView để phát nội dung âm thanh và video. 

    Ngoài ra còn có AVKit framework, giúp đơn giản hóa việc xây dựng các ứng dụng phát video hiện đại. Tất cả các framework này đều thuận tiện và dễ sử dụng và nên thường được dùng khi thêm chức năng phương tiện vào ứng dụng. Tuy nhiên, mặc dù các framework này rất tiện lợi nhưng chúng thường thiếu tính linh hoạt và khả năng kiểm soát cần thiết cho các ứng dụng nâng cao hơn.

    Ở tầng thấp nhất, chúng ta có các low-level framework, cung cấp chức năng hỗ trợ và được sử dụng bởi tất cả các framework cấp cao hơn. Hầu hết chúng đều là các framework cấp độ thấp, cực kỳ mạnh mẽ và hiệu quả, nhưng rất phức tạp để tìm hiểu và sử dụng, đồng thời yêu cầu chúng ta phải hiểu rõ về cách phương tiện được xử lý ở cấp độ phần cứng.

    Chính vì vậy, vị cứu tính của chúng ta – AV Foundation nằm ở giữa low-level và high-level framework. Mang trong mình sức mạnh cũng như hiệu năng của các low-level framework nhưng lại dễ tiếp cận, dễ đọc hơn cho các developer. Nó có thể làm việc trực tiếp với các framework cấp thấp như Core Media và Core Audio và hoạt động với các high-level framework như Media Player và Assets Library. Như vậy chúng ta có thể đủ thấy sức mạnh của AVF mạnh như nào phải k ạ ^^

    AV Foundation là framework đầy đủ tính năng để làm việc với phương tiện nghe nhìn trên iOS, macOS, watchOS và tvOS. Sử dụng AV Foundation, chúng ta có thể dễ dàng phát, tạo và chỉnh sửa phim QuickTime và các tệp MPEG-4, HLS streams và xây dựng chức năng truyền thông mạnh mẽ vào ứng dụng.

    Với AV Foundation, chúng ta có thể tạo ra các ứng dụng liên quan đến chụp ảnh và quay video, phát nhạc,… nói chúng tất cả những thứ liên quan đến cụm từ Media trên mobile và ngoài ra nó còn kèm theo rất nhiều thứ hay ho như điều khiển đèn flash phía trước và phía sau, âm thanh cho video…vv.

    Như vậy có thể thấy AV Foundation framework khá lớn nên trong bài viết này, mình chỉ tìm hiểu một ứng dụng nhỏ của framework này đó là sử dụng camera trên thiết bị của Apple để đọc mã QR Code. Và chi tiết cách xây dựng ở phần 3 dưới đây.

    Phần 3: Xây dựng ứng dụng QR Scan đơn giản bằng AV Foundation framework trên Swift

    Đầu tiên chúng ta sẽ tạo một class QRScanCustomView như sau:

    import Foundation
    import UIKit
    import AVFoundation
    
    protocol QRScanCustomViewDelegate: AnyObject {
        func onDecodeFailed()
        func onDecodeSuccess(_ decodeString: String)
        func onCameraAccessDenied()
    }
    
    public class QRScanCustomView: UIView {
        weak var delegate: QRScanCustomViewDelegate?
        private var captureSession: AVCaptureSession?
        private var scanAreaWidth: CGFloat = 300.0
        private var scanAreaHeight: CGFloat = 300.0
        private var scanAreaXpos: CGFloat = 0.0
        private var scanAreaYpos: CGFloat = 0.0
        private var scanAreaCornerRadius: CGFloat = 8.0
        private var scanViewBackgroundColor = UIColor.black.withAlphaComponent(0.9)
        private var isAnimationFromTop: Bool = true
        private var animationTimer = Timer()
        private var animationView = UIView()
        private let gradientBackgroundView = UIView()
        private var gradientView = UIView()
        private let animationContainerView = UIView()
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        public override var layer: AVCaptureVideoPreviewLayer {
            if let layer = super.layer as? AVCaptureVideoPreviewLayer {
                return layer
            }
            return AVCaptureVideoPreviewLayer()
        }
    }
    
    extension QRScanCustomView {
        func startScanning() {
            captureSession?.startRunning()
        }
        
        func stopScanning() {
            captureSession?.stopRunning()
        }
        
        func startAnimation() {
            runAnimation()
            animationTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(runAnimation), userInfo: nil, repeats: true)
        }
        
        func stopAnimation() {
            animationTimer.invalidate()
        }
        
        func setSizeOfScanArea(width: CGFloat, height: CGFloat) {
            scanAreaWidth = width
            scanAreaHeight = height
        }
        
        private func commonInit() {
            self.clipsToBounds = true
            captureSession = AVCaptureSession()
            
            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
                return
            }
            let videoInput: AVCaptureDeviceInput
            do {
                videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                delegate?.onCameraAccessDenied()
                return
            }
            
            if captureSession?.canAddInput(videoInput) ?? false {
                captureSession?.addInput(videoInput)
            } else {
                scanningDidFail()
                return
            }
            
            let metadataOutput = AVCaptureMetadataOutput()
            
            if captureSession?.canAddOutput(metadataOutput) ?? false {
                captureSession?.addOutput(metadataOutput)
                
                metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.qr]
            } else {
                scanningDidFail()
                return
            }
            
            layer.session = captureSession
            layer.videoGravity = .resizeAspectFill
            layer.frame = UIScreen.main.bounds
            
            captureSession?.startRunning()
            
            // MARK: SET AVAILABLE RANGE TO SCAN
            let rectView = layer.metadataOutputRectConverted(fromLayerRect: rectForScannerRange())
            metadataOutput.rectOfInterest = rectView
        }
        
        // MARK: Get rect of scan range
        private func rectForScannerRange() -> CGRect {
            scanAreaXpos = (UIScreen.main.bounds.size.width - scanAreaWidth) / 2
            scanAreaYpos = (UIScreen.main.bounds.size.height - scanAreaHeight) / 2
            let newRect = CGRect(x: scanAreaXpos, y: scanAreaYpos, width: scanAreaWidth, height: scanAreaHeight)
            return newRect
        }
        // MARK: Add view with clear scanner range
        func addQRScannerOverlayView(view: UIView) {
            view.addSubview(overlayView())
            view.layer.addSublayer(createCornerFrame(with: rectForScannerRange()))
            // Set authorize here to run it after qrScannerDelegate was declared
            setupScanAnimationView(view: view)
        }
        // MARK: Create scan range
        private func overlayView() -> UIView {
            let overlayView = UIView(frame: frame)
            overlayView.backgroundColor = scanViewBackgroundColor
            let path = CGMutablePath()
            path.addRoundedRect(in: rectForScannerRange(), cornerWidth: scanAreaCornerRadius, cornerHeight: scanAreaCornerRadius)
            path.closeSubpath()
            path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
            
            let maskLayer = CAShapeLayer()
            maskLayer.backgroundColor = UIColor.black.cgColor
            maskLayer.path = path
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
            overlayView.layer.mask = maskLayer
            overlayView.clipsToBounds = true
            
            return overlayView
        }
        // MARK: Draw focus corner frame
        private func createCornerFrame(with rect: CGRect) -> CAShapeLayer {
            let length: CGFloat = 24.0
            let radius: CGFloat = 8.0
            let offset: CGFloat = 8.0
            let lineWidth: CGFloat = 4.0
            let distance: CGFloat = rect.size.width - offset * 2 - length * 2 - radius * 2
            
            let path = CGMutablePath()
            
            var start = CGPoint(x: rect.origin.x + offset, y: rect.origin.y + offset + radius + length)
            var end = CGPoint(x: start.x + radius + length, y: start.y - length - radius)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x, y: start.y - length))
            path.addArc(tangent1End: CGPoint(x: start.x, y: start.y - length - radius),
                        tangent2End: CGPoint(x: start.x + radius, y: start.y - length - radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x + distance, y: end.y)
            end = CGPoint(x: start.x + length + radius, y: start.y + radius + length)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x + length, y: start.y))
            path.addArc(tangent1End: CGPoint(x: start.x + length + radius, y: start.y),
                        tangent2End: CGPoint(x: start.x + length + radius, y: start.y + radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x, y: end.y + distance)
            end = CGPoint(x: start.x - radius - length, y: start.y + length + radius)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x, y: start.y + length))
            path.addArc(tangent1End: CGPoint(x: start.x, y: start.y + length + radius),
                        tangent2End: CGPoint(x: start.x - radius, y: start.y + length + radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x - distance, y: end.y)
            end = CGPoint(x: start.x - length - radius, y: start.y - radius - length)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x - length, y: start.y))
            path.addArc(tangent1End: CGPoint(x: start.x - length - radius, y: start.y),
                        tangent2End: CGPoint(x: start.x - length - radius, y: start.y - radius),
                        radius: radius)
            path.addLine(to: end)
            
            let shape = CAShapeLayer()
            shape.path = path
            shape.strokeColor = UIColor.orange.cgColor
            shape.lineWidth = lineWidth
            shape.fillColor = UIColor.clear.cgColor
            shape.lineCap = .round
            return shape
        }
        // MARK: Create scan animation view, size of inputView = size of QRScanView
        private func setupScanAnimationView(view: UIView) {
            animationContainerView.backgroundColor = .clear
            animationContainerView.frame = rectForScannerRange()
            view.addSubview(animationContainerView)
            
            animationView.frame = CGRect(x: animationContainerView.bounds.minX - 8,
                                         y: animationContainerView.bounds.minY,
                                         width: animationContainerView.bounds.width + 16,
                                         height: 3.0)
            animationView.layer.cornerRadius = animationView.frame.height / 2
            animationView.backgroundColor = UIColor.orange
            animationContainerView.addSubview(animationView)
            animationContainerView.bringSubviewToFront(animationView)
            
            gradientBackgroundView.frame = animationContainerView.bounds
            gradientBackgroundView.backgroundColor = .clear
            animationContainerView.addSubview(gradientBackgroundView)
            animationContainerView.bringSubviewToFront(gradientBackgroundView)
            gradientBackgroundView.clipsToBounds = true
        }
        
        @objc
        private func runAnimation() {
            animationView.clipsToBounds = false
            if isAnimationFromTop {
                for subView in gradientBackgroundView.subviews {
                    subView.removeFromSuperview()
                }
                gradientBackgroundView.addSubview(createGradientViewEffect(rect: CGRect(x: 0,
                                                                                        y: -20,
                                                                                        width: gradientBackgroundView.bounds.width,
                                                                                        height: 20),
                                                                           isRevertColor: false))
                UIView.animate(withDuration: 2.0, animations: {
                    self.animationView.transform = CGAffineTransform(translationX: 0,
                                                                     y: self.animationContainerView.frame.size.height)
                    self.gradientView.transform = CGAffineTransform(translationX: 0,
                                                                    y: self.animationContainerView.frame.size.height)
                    Timer.scheduledTimer(timeInterval: 1.6, target: self, selector: #selector(self.hideGradientView), userInfo: nil, repeats: false)
                })
                
            } else {
                for subView in gradientBackgroundView.subviews {
                    subView.removeFromSuperview()
                }
                gradientBackgroundView.addSubview(createGradientViewEffect(rect: CGRect(x: 0,
                                                                                        y: gradientBackgroundView.bounds.height,
                                                                                        width: gradientBackgroundView.bounds.width,
                                                                                        height: 20),
                                                                           isRevertColor: true))
                UIView.animate(withDuration: 2.0, animations: {
                    self.animationView.transform = CGAffineTransform(translationX: 0,
                                                                     y: 0)
                    self.gradientView.transform = CGAffineTransform(translationX: 0,
                                                                    y: 0)
                    Timer.scheduledTimer(timeInterval: 1.6, target: self, selector: #selector(self.hideGradientView), userInfo: nil, repeats: false)
                })
            }
            isAnimationFromTop = !isAnimationFromTop
        }
        
        @objc
        private func hideGradientView() {
            UIView.animate(withDuration: 0.4, animations: {
                self.gradientView.alpha = 0
            })
        }
        
        private func createGradientViewEffect(rect: CGRect, isRevertColor: Bool) -> UIView {
            gradientView.layer.sublayers?.removeAll()
            gradientView.frame = rect
            let gradientLayer: CAGradientLayer = CAGradientLayer()
            gradientLayer.frame = gradientView.bounds
            let firstColor = UIColor.orange.withAlphaComponent(0.01).cgColor
            let secondColor = UIColor.orange.withAlphaComponent(1).cgColor
            gradientLayer.colors = isRevertColor ? [secondColor, firstColor] : [firstColor, secondColor]
            gradientLayer.locations = [0.0, 1.0]
            gradientView.layer.insertSublayer(gradientLayer, at: 0)
            gradientView.alpha = 0.6
            return gradientView
        }
        
        func scanningDidFail() {
            delegate?.onDecodeFailed()
            captureSession = nil
        }
        
        func scanSuccess(code: String) {
            delegate?.onDecodeSuccess(code)
        }
    }
    
    extension QRScanCustomView: AVCaptureMetadataOutputObjectsDelegate {
        public func metadataOutput(_ output: AVCaptureMetadataOutput,
                                   didOutput metadataObjects: [AVMetadataObject],
                                   from connection: AVCaptureConnection) {
            stopScanning()
            
            if let metadataObject = metadataObjects.first {
                guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else {
                    return
                }
                guard let stringValue = readableObject.stringValue else {
                    return
                }
                AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                scanSuccess(code: stringValue)
            }
        }
    }

    Để có thể sử dụng thư viện, chúng ta phải import thư viện “import AVFoundation” và khởi tạo “captureSession” để quản lý session capture. Có thể hiểu đơn giản rằng captureSession giúp quản lý việc sử dụng camera của chúng ta.

    Ngoài ra, vì ứng dụng của chúng ta sử dụng camera để capture mã QR nên phải khai báo Camera Usage Description trong Info.plist. Nếu không khai báo ứng dụng của chúng ta sẽ không thể chạy vì vi phạm policy của Apple

    Tiếp theo chúng ta sẽ khởi tạo AVCaptureVideoPreviewLayer:

    public override var layer: AVCaptureVideoPreviewLayer {
            if let layer = super.layer as? AVCaptureVideoPreviewLayer {
                return layer
            }
            return AVCaptureVideoPreviewLayer()
        }

    Layer này chính là một layer hiển thị lên màn hình có vai trò thực hiện việc sử dụng camera để scan mã QR

    Tạo ra các func để quản lý start, stop capture session:

       func startScanning() {
            captureSession?.startRunning()
        }
        
        func stopScanning() {
            captureSession?.stopRunning()
        }

    Ở func commonInit(), chúng ta sẽ tạo khởi tạo videoCaptureDevice để sử dụng kiểu capture video sử dụng camera của chúng ta và add vào kiểu input của captureSession, ở đây có thể hiểu là chúng ta đang khởi tạo đầu vào cho ứng dụng của chúng ta sử dụng camera để scan QR

            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
                return
            }
            let videoInput: AVCaptureDeviceInput
            do {
                videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                delegate?.onCameraAccessDenied()
                return
            }
            
            if captureSession?.canAddInput(videoInput) ?? false {
                captureSession?.addInput(videoInput)
            } else {
                scanningDidFail()
                return
            }

    Sau khi thực hiện khởi tạo đầu vào, chúng ta sẽ khởi tạo đầu ra cho captureSession qua class AVCaptureMetadataOutput, ở đây mình đã custom một khoảng trắng giữa màn hình để tạo vùng scan bằng thuộc tính “rectOfInterest”. Nếu không gắn thuộc tính này, mặc định cả màn hình capture của chúng ra sẽ là vùng scan:

     let metadataOutput = AVCaptureMetadataOutput()
            
            if captureSession?.canAddOutput(metadataOutput) ?? false {
                captureSession?.addOutput(metadataOutput)
                
                metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.qr]
            } else {
                scanningDidFail()
                return
            }
            
            layer.session = captureSession
            layer.videoGravity = .resizeAspectFill
            layer.frame = UIScreen.main.bounds
            
            captureSession?.startRunning()
            
            // MARK: Set scan Area
            let rectView = layer.metadataOutputRectConverted(fromLayerRect: rectForScannerRange())
            metadataOutput.rectOfInterest = rectView

    Để hứng được output của captureSession. Chúng ta sẽ kế thừa delegate của AVCaptureMetadataOutput. Delegate này sẽ giúp chúng ta get được String decode được từ mã QR:

    extension QRScanCustomView: AVCaptureMetadataOutputObjectsDelegate {
        public func metadataOutput(_ output: AVCaptureMetadataOutput,
                                   didOutput metadataObjects: [AVMetadataObject],
                                   from connection: AVCaptureConnection) {
            stopScanning()
            
            if let metadataObject = metadataObjects.first {
                guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else {
                    return
                }
                guard let stringValue = readableObject.stringValue else {
                    return
                }
                AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                scanSuccess(code: stringValue)
            }
        }
    }

    Ngoài ra mình còn tạo ra các func để custom màn hình và tạo animation quét QR để trông ứng dụng của chúng ta trông đẹp mắt hơn. Các bạn có thể tham khảo ở phần source đầy đủ bên trên.

    Về cơ bản chỉ cần khởi tạo đủ AVCaptureSession, tạo ra input và output cho AVCaptureSession và kế thừa delegate để hứng output là chúng ta có thể tạo ra một ứng dụng scan QR đơn giản.

    Bài viết của mình vẫn đang trong quá trình update để đầy đủ và chi tiết hơn, nếu có góp ý gì mọi người có thể comment bên dưới để mình bổ sung và cải thiện thêm nhé.

    Cảm ơn mọi người đã quan tâm đến bài viết của mình. Chúc mọi người có một ngày làm việc hiệu quả !

  • iOS Architecture Patterns: Cocoa MVC

    iOS Architecture Patterns: Cocoa MVC

    Là một iOS developer chắc hẳn các bạn không lạ gì với Cocoa MVC. Nó được coi là một trong những architecture pattern để phát triển ứng dụng iOS phổ biến nhất. Nó rất dễ sử dụng và được chính Apple khuyên dùng. iOS, MacOS và watchOS đều sử dụng cấu trúc này làm kiến ​​trúc mặc định để phát triển. Tuy được rất phổ biến và được Apple khuyên dùng nhưng nó cũng có những ưu điểm và nhược điểm, vì vậy bài viết này mình sẽ giới thiệu và giải thích cho các bạn về Cocoa MVC, ưu điểm, nhược điểm và khi nào nên chọn Cocoa MVC sử dụng cho ứng dụng của bạn.

    Giải thích về Cocoa MVC

    Cocoa MVC là viết tắt của Cocoa Model View Controller. Cocoa MVC gán các đối tượng trong ứng dụng iOS bằng một trong 3 vai trò sau: Model, View hoặc Controller. Mẫu kiến trúc này không chỉ xác định vai trò của các đối tượng trong ứng dụng mà nó còn xác định cả cách các đối tượng giao tiếp với nhau. Ba loại đối tượng này được phân tách khỏi các loại khác bằng các ranh giới trừu tượng và giao tiếp với các đối tượng thuộc các loại khác thông qua các ranh giới đó. Tập hợp các đối tượng của một loại MVC nhất định trong một ứng dụng đôi khi được gọi là một Layer, ví dụ: Layer model.

    Cocoa MVC

    Model

    Những đối tượng được gán với vai trò model trong mẫu kiến trúc Cocoa MVC sẽ làm nhiệm vụ đóng gói dữ liệu cụ thể cho một ứng dụng và xác định logic và tính toán để thao tác và xử lý dữ liệu đó. Những đối tượng này có thể có một hoặc nhiều mối quan hệ với các đối tượng mô hình khác và do đó, đôi khi lớp Model của một ứng dụng thực sự là một hoặc nhiều đối tượng. Phần lớn dữ liệu đều nằm ở Model sau khi nó được tải vào ứng dụng bằng các cách khác nhau(API, Files, …). Vì Model đại diện cho kiến thức và chuyên môn cho một vấn đề cụ thể nên nó có thể được tái sử dụng khi có các trường hợp tương tự. Các Model sẽ không có liên kết trực tiếp với View và View cũng không được trực tiếp sửa dữ liệu của Model mà nó sẽ phải thực hiện thông qua Controller.

    Giao tiếp: Các hành động của người dùng trên View sẽ gọi đến Controller, khi này Controller sẽ gọi đến Model tương ứng để thực hiện cập nhật dữ liệu cho Model. Khi Model thay đổi (ví dụ: dữ liệu mới được nhận qua kết nối mạng), nó sẽ thông báo cho Controller, lúc này Controller sẽ cập nhật các Views thích hợp.

    View

    View là một đối tượng trong ứng dụng mà người dùng có thể nhìn thấy. Một đối tượng View biết cách tự vẽ và có thể phản hồi các hành động của người dùng. Mục đích chính của View là hiển thị dữ liệu từ Model của ứng dụng và cho phép chỉnh sửa dữ liệu đó. Mặc dù vậy, View thường được tách rời khỏi Model trong ứng dụng MVC.

    Bởi vì bạn thường sử dụng lại và cấu hình lại chúng, nên View cung cấp tính nhất quán giữa các ứng dụng. Cả UIKit và AppKit framework đều cung cấp các bộ sưu tập View classes và Trình tạo giao diện(Interface Builder) cung cấp hàng tá view objects trong Thư viện của nó.

    Giao tiếp: View tìm hiểu về các thay đổi trong dữ liệu của Model thông qua Controller của ứng dụng và truyền đạt các thay đổi do người dùng. ví dụ: văn bản được nhập vào TextField thông qua Controller đến Model của ứng dụng.

    Controller

    Controller vai trò trung gian giữa một hoặc nhiều View của ứng dụng và một hoặc nhiều Model của nó. Do đó, cController là một đường dẫn mà qua đó View tìm hiểu về những thay đổi trong Model và ngược lại. Controller cũng có thể thực hiện các tác vụ thiết lập và điều phối cho một ứng dụng và quản lý vòng đời của các đối tượng khác.

    Giao tiếp: Controller diễn giải các hành động của người dùng được thực hiện trong View và truyền dữ liệu mới hoặc dữ liệu đã thay đổi tới Model. Khi Model thay đổi, Controller sẽ giao tiếp dữ liệu Model mới đó với View để chúng có thể hiển thị nó.

    Ưu điểm của Cocoa MVC

    Cocoa MVC có khá nhiều ưu điểm như sau:

    1. Dễ hiểu và dễ sử dụng, vì vậy ai cũng có thể làm việc với nó một cách dễ dàng kể cả người mới
    2. Được Apple khuyên dùng, vì vậy nó rất phổ biến khi gặp vấn đề sẽ dễ xử lí.
    3. Giúp developer tách source của họ ra làm các đối tượng khác nhau với 3 vai trò riêng biệt. Khi có lỗi xảy ra chúng ta sẽ khoanh vùng được nơi xảy ra lỗi.
    4. Tránh việc phải tạo một file quá dài, ảnh hưởng tới việc maintain ứng dụng
    5. Có thể tái sử dụng và mở rộng

    Nhược điểm của Cocoa MVC

    1. Phân chia nhiệm vụ giữa các vai trò không đồng đều, View chỉ làm nhiệm vụ hiển thị và nhận action từ người dùng, Controller thì chỉ là trung gian điều hướng giữa View-Model, trong khi đó Model phải làm quá nhiều việc từ lưu dữ liệu, xử lí dữ liệu, thực hiện Business Logic của ứng dụng, … Đó là lí do Model còn hay được gọi với cái tên khác là Massive
    2. Không hỗ trợ tốt cho UnitTest bởi View phải phụ thuộc vào cả Controller và Model. View sẽ không thể xử lý được vấn đề gì bởi View không thể nhận yêu cầu và cũng không có dữ liệu để hiển thị. Để tiến hành UnitTest trên View, chúng ta cần giả lập cả Controller và Model.
    3. Đối với các ứng dụng quy mô lớn, quy trình xử lý nghiệp vụ có tính phức tạp cao, lượng dữ liệu lớn thì mô hình MVC trở nên rất cồng kềnh và khó để thực hiện.

    Khi nào bạn nên sử dụng Cocoa MVC

    Như đã phân tích ở trên, các bạn cũng đã nhìn thấy cách vận hành của mô hình này, các ưu điểm và nhược điểm của nó. Vậy khi nào thì chúng ta nên sử dụng Cocoa MVC cho ứng dụng của mình. Theo mình thì sẽ các tiêu chí như sau:

    1. Khi bạn không biết về các architecture patterns khác tốt hơn, bạn chỉ hiểu rõ về MVC hoặc bạn và member trong team là người mới thì nên chọn Cocoa MVC cho dự án của mình.
    2. Dự án của bạn có kích thước vừa và nhỏ, số iOS developer có số lượng ít, có ít hiểu biết về các architecture patterns khác.

    Tổng kết

    Qua bài viết trên mình đã giới thiệu cho các bạn về một architecture pattern rất phổ biến trong lập trình ứng dụng iOS. Ngoài ra cũng giúp các bạn hiểu rõ về cách hoạt động cũng như ưu điểm và nhược điểm của Cocoa MVC. Mình hi vọng bài viết này sẽ giúp các bạn có thể hiểu rõ hơn về Cocoa MVC cũng như có những lựa chọn tốt nhất cho từng dự án mà sắp tới các bạn phát triển.

  • Mutating func trong Struct và enum – Swift

    Mutating func trong Struct và enum – Swift

    Như mọi người đã biết, struct và enum trong Swift là value types(kiểu giá trị), mặc định thì các thuộc tính của kiểu giá trị thì không thể được sửa đổi ở bên trong các phương thức thể hiện của nó(instance methods).

    Tuy nhiên nếu chúng ta cần phải chỉnh sửa các thuộc tính của struct hoặc enum trong một phương thức cụ thể, thì chúng ta sẽ đặt mutating trước các func, nó sẽ giúp các func của bạn có thể thay đổi được các thuộc tính bên trong func và khi kết thúc func giá trị sẽ được nghi lại vào các thuộc tính của struct ban đầu. Phương thức này cũng có thể gán lại một instance mới cho thuộc tính self của nó và nó sẽ được thay thế khi phương thức kết thúc.

    Mutating trong Struct

    Trong ví dụ này mình sẽ tạo một Struct có tên là Counter và tạo ra một func lấy giá trị của thuộc tính count trong Counter như sau:

    struct Counter {
        private var count: Int = 0
        
        func getCount() -> Int {
            count
        }
    }

    Đây là một ví dụ bình thường về struct, hàm getCount() ở đây không thực hiện thay đổi giá trị của struct Counter mà nó chỉ lấy giá trị của thuộc tính count theo cách thông thường.

    Vậy khi chúng ta muốn viết một hàm increase() để tăng giá trị count thông thường chúng ta sẽ viết như sau:

    Nếu là class thì sẽ không vấn đề gì vì class là reference type. Ở trường họp này do chúng ta đang viết một func chỉnh sửa thuộc tính count của struct Counter nên xCode sẽ báo lỗi rằng self ở đây là immutable(không thể thay đổi), như đã giải thích ở trên thì struct là value type nên mặc định sẽ không thể thay đổi được thuộc tính của nó trong các func của struct đó.

    func getCount() không bị báo lỗi vì func này không làm thay đổi thuộc tính trong struct.

    Để func increase() không bị báo lỗi chúng ta cần thêm mutating đằng trước func để xCode biết là func này có thể thay đổi được thuộc tính của struct:

    struct Counter {
        private var count: Int = 0
        
        func getCount() -> Int {
            count
        }
        
        mutating func increase() {
            count += 1
        }
    }
    
    var counter = Counter() // count = 0
    counter.increase() // count = 1

    Để có thể thay đổi được thuộc tính của instance counter thì chúng ta cần phải khai báo nó là var, vì func increase() sẽ thay đổi giá trị của counter vì vậy cần khai báo là var để mutating func có thể gán lại giá trị mới cho instance counter.

    Nếu chúng ta để là let Xcode sẽ thông báo lỗi không thể sử dụng mutating func trên giá trị không thể thay đổi, counter đang là một “let” constant. Do struct là value type nên nó là immutable có nghĩa là không thay đổi được, nếu chúng ta cố tình khai báo let count Xcode sẽ thông báo lỗi.

    Lỗi khi khai báo let counter để call mutating func

    NOTE: Để hiểu rõ hơn các bạn có thể xem thêm thông tin ở đây: Stored Properties of Constant Structure Instances

    Mutating func không chỉ thay đổi được thuộc tính của struct mà nó còn có thể thay đổi cả giá trị của chính instance (self)

    struct Counter {
        private var count: Int = 0
        
        func getCount() -> Int {
            count
        }
        
        mutating func increase() {
            count += 1
        }
        
        mutating func resetCounter() {
            self = Counter(count: 0)
        }
    }
    
    var counter = Counter() // count = 0
    counter.increase() // count = 1
    counter.resetCounter() // count = 0

    mutating func resetCounter() là một ví dụ, ở trong func này chúng ta thực hiện tạo ra một instance Counter mới với giá trị khởi tạo là 0 và gán lại cho chính instance gọi func này.

    Mutating func trong enum

    Tương tự như struct, enum cũng là value type và để thay đổi giá trị trong func chúng cũng cần phải sửa dụng mutating cho func đó.

    Để hiểu rõ hơn ta đi vào ví dụ sau:

    Chúng ta cần tạo ra một công tắc quạt với một tính năng là mỗi khi bấm nút thì sẽ làm thay đổi tốc độ quay của quạt một cách tuần tự và lặp đi lặp lại. Để làm theo yêu cầu chúng ta sẽ tạo enum như sau:

    enum FanStateSwitch {
        case off, low, high
        mutating func next() {
            switch self {
            case .off:
                self = .low
            case .low:
                self = .high
            case .high:
                self = .off
            }
        }
    }
    
    var fanSwitch = FanStateSwitch.off
    fanSwitch.next() // fanSwitch is low
    fanSwitch.next() // fanSwitch is high
    fanSwitch.next() // fanSwitch is off

    Tương tự như struct khi khởi tạo enum hãy nhớ khởi tạo nó với var thay vì let.

    Hi vọng bài viết sẽ giúp các bạn hiểu rõ hơn về mutating func và cách sử dụng, ứng dụng nó vào trong dự án.

    Chúc các bạn thành công!

  • Xcode 14 có gì mới? (Phần 3)

    Xcode 14 có gì mới? (Phần 3)

    Swift 5.7

    Các tính năng mới

    • Thư viện tiêu chuẩn có một Regex<Output> loại mới.Loại này đại diện cho một biểu thức chính quy mở rộng , cho phép các hoạt động xử lý chuỗi trôi chảy hơn. Bạn có thể tạo một Regex bằng cách khởi tạo từ một chuỗi :
    let pattern = "a[bc]+" // matches "a" followed by one or more instances
                           // of either "b" or "c"
    let regex = try! Regex(pattern)

    Hoặc thông qua một regex literal:

    let regex = #/a[bc]+/#

    Có các thuật toán xử lý chuỗi mới hỗ trợ các loại StringRegex và arbitrary Collection. ( SE-0350 , 93923512 )

    • Xcode 14 kích hoạt cú pháp “dấu gạch chéo” của Swift 5.7 cho các ký tự biểu thức chính quy mới được giới thiệu. Trong một số trường hợp, kết quả là mã hiện có sử dụng /làm toán tử không được biên dịch. Bạn có thể định hướng điều này bằng cách thêm dấu ngoặc đơn (/). Bạn cũng có thể vô hiệu hóa hỗ trợ cho cú pháp chữ này bằng cách bỏ chọn “Bật các chữ viết Regex Bare Slash” (SWIFT_ENABLE_BARE_SLASH_REGEX = NO) trong project’s build settings. (93460568)
    • Các thao tác chuỗi trong phiên bản 5.7 của Thư viện tiêu chuẩn Swift triển khai xác thực cải tiến cho các chỉ mục chuỗi, khắc phục một số trường hợp biên trước đây dẫn đến lỗi thời gian chạy giả. Tuy nhiên, Swift hiện chẩn đoán các nỗ lực sử dụng chỉ mục ngoài giới hạn một cách đáng tin cậy hơn và điều này có thể làm lộ các lỗi lập chỉ mục chưa được phát hiện trước đó khi bạn biên dịch lại mã bằng Xcode 14. Nếu bạn thấy lỗi “Chỉ mục chuỗi nằm ngoài giới hạn” mới sau khi xây dựng lại, mã, sau đó kiểm tra kỹ xem bạn có đang áp dụng chỉ mục cũ cho giá trị chuỗi bị thay đổi không. Trong các bản phát hành trước, các trường hợp như vậy có thể âm thầm dẫn đến các giá trị bị hỏng hoặc vô nghĩa, trong khi ở Swift 5.7, giờ đây chúng gây ra lỗi thời gian chạy một cách đáng tin cậy. (89482809)
    • Giờ đây, Swift có thể suy ra loại trình giữ chỗ được viết ở nhiều vị trí cấp cao nhất. Loại suy luận hiện được đề xuất trong bản sửa lỗi.
    // error: type placeholder may not appear in function return type
    func replaceMe() -> _ { // note: replace the placeholder with the inferred type ‘Array<Int>’
      [ 42 ]
    }
    • Xcode 13 đã cung cấp cài đặt bản dựng Swift có tên là “Tối ưu hóa thời gian tồn tại của đối tượng” không khả dụng trong Xcode 14. Nếu dự án của bạn đã tùy chỉnh cài đặt bản dựng này, thì giờ đây, nó sẽ trở thành cài đặt do người dùng xác định. Nó không có hiệu lực và bạn có thể loại bỏ nó. Xcode 14 hiện liên tục tối ưu hóa thời gian tồn tại của đối tượng. (91971848)
    • Xcode 14 giới thiệu một tính năng mới cho khả năng tương tác C: “SE-0324 Thư giãn chẩn đoán cho các đối số con trỏ đối với các hàm C.” Chẩn đoán thoải mái chỉ áp dụng cho các đối số kiểu con trỏ, không phải inout đối số. Giờ đây, chẩn đoán được nới lỏng đối với các inout đối số trong cùng điều kiện như đối số con trỏ. Ví dụ: chuyển đổi này từ một inout Int đối số sang loại tham số C const char * hiện được cho phép:
      // C declaration:
      // long read_long(const char *input);
    
      func test() -> Int {
        var x = 3
        return read_long(&x)
      }

    Trước đây Swift đã chẩn đoán chuyển đổi inout-to-pointer là lỗi:

      error: cannot convert value of type 'UnsafePointer<Int>' to expected argument type
        'UnsafePointer<CChar>' (aka 'UnsafePointer<Int8>')
        return read_long(&x)
                         ^
      note: arguments to generic parameter 'Pointee' ('Int' and 'CChar' (aka 'Int8'))
      are expected to be equal
        return read_long(&x)
                         ^

    Swift hiện hỗ trợ các tham chiếu đến optional các phương thức trên siêu dữ liệu giao thức, cũng như các tham chiếu được tra cứu động trên AnyObject siêu dữ liệu. Các tham chiếu này luôn có loại hàm chấp nhận một đối số và trả về một giá trị tùy chọn của loại hàm:

    class Object {
      @objc func getTag() -> Int
    }
    
    @objc protocol P {
      @objc optional func didUpdateObject(withTag tag: Int)
    }
    
    let getTag: (AnyObject) -> (() -> Int)? = AnyObject.getTag
    
    let didUpdateObject: (any P) -> ((Int) -> Void)? = P.didUpdateObject

    Tải dữ liệu từ bộ nhớ thô được biểu thị bằng UnsafeRawPointer, UnsafeRawBufferPointer và các đối tác có thể thay đổi của chúng hiện hỗ trợ truy cập không được phân bổ. Điều này trước đây yêu cầu một cách giải quyết liên quan đến một bản sao trung gian:

    let result = unalignedData.withUnsafeBytes { buffer -> UInt32 in
      var storage = UInt32.zero
      withUnsafeMutableBytes(of: &storage) {
        $0.copyBytes(from: buffer.prefix(MemoryLayout<UInt32>.size))
      }
      return storage
    }

    Hiện nay

    let result = unalignedData.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) }

    Ngoài ra, storeBytes(of:toByteOffset:as:) đã dỡ bỏ hạn chế căn chỉnh, do đó, giờ đây việc lưu trữ thành các phần bù tùy ý của bộ nhớ thô có thể thành công. ( SE-0349 , 93654008 )

    UnsafeRawPointer và UnsafeMutableRawPointer có chức năng mới cho số học con trỏ, thêm các chức năng để lấy con trỏ nâng cao tới ranh giới căn chỉnh tiếp theo hoặc trước đó:

    ```swift
    extension UnsafeRawPointer {
      public func alignedUp<T>(for: T.type) -> UnsafeRawPointer
      public func alignedDown<T>(for: T.type) -> UnsafeRawPointer
      public func alignedUp(toMultipleOf alignment: Int) -> UnsafeRawPointer
      public func alignedDown(toMultipleOf alignment: Int) -> UnsafeRawPointer
    }
    ```
    <!– /wp:code –> <!– wp:paragraph –> <p>Bây giờ bạn có thể sử dụng con trỏ tới <code>struct</code> để lấy con trỏ tới một trong các thuộc tính được lưu trữ của nó:</p> <!– /wp:paragraph –> <!– wp:code –> <pre class="wp-block-code"><code>“`swift withUnsafeMutablePointer(to: &myStruct) { let interiorPointer = $0.pointer(to: \.myProperty)! return myCFunction(interiorPointer) } “`

    Swift đã đơn giản hóa việc so sánh giữa các con trỏ. Vì con trỏ là biểu diễn của các vị trí bộ nhớ trong một nhóm bộ nhớ cơ bản, nên giờ đây Swift cho phép so sánh con trỏ mà không yêu cầu chuyển đổi kiểu với các toán tử ==!=<<=>và >=. ( SE-0334 , 93667321 )

    • Giờ đây, bạn có thể sử dụng phương thức withMemoryRebound<T>() này trên bộ nhớ thô, bao gồm UnsafeRawPointer, UnsafeRawBufferPointer và các đối tượng có thể thay đổi của chúng. Ngoài ra, Swift đã làm rõ ngữ nghĩa withMemoryRebound<T>() khi được sử dụng trên bộ nhớ đã nhập (UnsafePointer<Pointee>, UnsafeBufferPointer<Pointee> và các đối tác có thể thay đổi của chúng). Trong khi Swift trước đây yêu cầu Pointee và T có cùng bước tiến, giờ đây bạn có thể liên kết Pointee và T lại trong các trường hợp là tổng hợp  hoặc ngược lại. Ví dụ: được cung cấp một UnsafeMutableBufferPointer<CGPoint>, bây giờ bạn có thể sử dụng withMemoryRebound để hoạt động tạm thời trên một UnsafeMutableBufferPointer<CGFloat>, bởi vì CGPoint là một tập hợp của CGFloat. ( SE- 0333 , 93668889 )
    • Giờ đây, bạn có thể gọi một hàm chung có giá trị là loại giao thức ở những nơi trước đây không thành công vì any các loại không tuân theo giao thức của chúng. Ví dụ:
    protocol P {
      associatedtype A
      func getA() -> A
    }
    
    func takeP<T: P>(_ value: T) { }
    
    func test(p: any P) {
      takeP(p) // was an error "type 'any P' cannot conform to 'P'", now accepted
    }
    • Điều này hoạt động bằng cách mở giá trị của loại giao thức và chuyển loại cơ bản trực tiếp đến chức năng chung. ( SE-0352 , 93669112 )
    • Bây giờ, bạn có thể sử dụng một biểu thức giá trị mặc định với một loại tham số chung để làm mặc định đối số và loại của nó:
    func compute<C: Collection>(_ values: C = [0, 1, 2]) {
      ...
    }
    • Trình biên dịch hiện chấp nhận một cuộc gọi đến compute()và [Int] được suy ra C tại các trang web cuộc gọi không cung cấp đối số một cách rõ ràng. ( SE-0347 , 93669409 )
    • Giờ đây, bạn có thể sử dụng các giao thức với các loại và Self yêu cầu được liên kết dưới dạng các loại giá trị với từ khoá any. Bạn có thể gọi các phương thức giao thức trả về các loại được liên kết trên một kiểu any; kết quả là loại bị xóa đến giới hạn trên của loại được liên kết, là một kiểu any khác có cùng ràng buộc với loại được liên kết. Ví dụ:
    protocol Surface {...}
      
    protocol Solid {
      associatedtype SurfaceType: Surface
      func boundary() -> SurfaceType
    }
      
    let solid: any Solid = ...
      
    // Type of 'boundary' is 'any Surface'
    let boundary = solid.boundary()
    • Bạn không thể sử dụng các phương thức giao thức có loại được liên kết hoặc Self với any; tuy nhiên, cùng với SE-0352 , bạn có thể chuyển kiểu any cho một hàm có tham số chung bị ràng buộc với giao thức. Trong ngữ cảnh chung, các mối quan hệ kiểu là rõ ràng và bạn có thể sử dụng tất cả các phương thức giao thức. ( SE-0309 , 93928911)
    • Giờ đây, các giao thức có thể khai báo danh sách một hoặc nhiều loại được liên kết chính , cho phép viết các yêu cầu cùng loại trên các loại được liên kết đó bằng cách sử dụng cú pháp dấu ngoặc nhọn:
    protocol Graph<Vertex, Edge> {
      associatedtype Vertex
      associatedtype Edge
    }

    Giờ đây, bạn có thể viết tên giao thức theo sau là các đối số loại trong dấu ngoặc nhọn, chẳng hạn như Graph<Int, String>, ở bất kỳ đâu mà yêu cầu tuân thủ giao thức có thể xuất hiện:

      func shortestPath<V, E>(_: some Graph<V, E>, from: V, to: V) -> [E]
    
      extension Graph<Int, String> {...}
    
      func build() -> some Graph<Int, String> {}

    Tên giao thức theo sau bởi dấu ngoặc nhọn là cách viết tắt của yêu cầu tuân thủ, cùng với yêu cầu cùng loại đối với các loại liên kết chính của giao thức. Hai ví dụ đầu tiên ở trên tương đương như sau:

      func shortestPath<V, E, G>(_: G, from: V, to: V) -> [E]
        where G: Graph, G.Vertex == V, G.Edge == E
    
      extension Graph where Vertex == Int, Edge == String {...}
    • Hàm build()trả về some Graph<Int, String> không thể được viết bằng một mệnh đề where; đây là một ví dụ về loại kết quả không rõ ràng bị ràng buộc, đây là biểu thức mới trong Swift 5.7. ( SE-0346 , 93929372 )
    • Bạn có thể sử dụng các giao thức áp dụng các loại được liên kết chính với any để bật các loại tồn tại bị ràng buộc. Ví dụ:
    let strings: any Collection<String> = [ "Hello" ]

    Điều này làm cho việc viết các trình bao bọc xóa kiểu cho mã chung đơn giản hơn nhiều vì một loại trình bao bọc riêng biệt không còn cần thiết nữa:

    protocol Producer<T> {
      associatedtype T
      func produce() -> T
    }
    
    typealias AnyProducer<T> = any Producer<T>
    
    /*
    struct AnyProducer<T> {
      var wrappedProduce: () -> T
    }
    */

    Giờ đây, bạn có thể sử dụng các giao thức với các loại được liên kết chính trong các loại tồn tại, cho phép các ràng buộc cùng loại đối với các loại được liên kết đó.

    let strings: any Collection<String> = [ "Hello" ]
    • Lưu ý rằng các tính năng ngôn ngữ yêu cầu hỗ trợ thời gian chạy như diễn viên động ( isas?as!), cũng như cách sử dụng chung của các tồn tại được tham số hóa trong các loại chung (ví dụ: Array<any Collection<Int>>) liên quan đến việc kiểm tra tính khả dụng bổ sung để sử dụng. Giờ đây, bạn có thể triển khai lại các tập quán ở các vị trí chung với cấu trúc trình bao bọc xóa kiểu chung, giờ đây việc triển khai đơn giản hơn nhiều:
    struct AnyCollection<T> {
      var wrapped: any Collection<T>
    }
    
    let arrayOfCollections: [AnyCollection<T>] = [ /**/ ]

    Giờ đây, bạn có thể sử dụng các loại không rõ ràng trong tham số của hàm và chỉ số con khi chúng cung cấp cú pháp tốc ký để giới thiệu tham số chung. Ví dụ như sau:

    func horizontal(_ v1: some View, _ v2: some View) -> some View {
      HStack {
        v1
        v2
      }
    }

    tương đương với:

    func horizontal<V1: View, V2: View>(_ v1: V1, _ v2: V2) -> some View {
      HStack {
        v1
        v2
      }
    }

    Với điều này, some trong một loại tham số cung cấp một khái quát hóa trong đó người gọi chọn loại tham số cũng như giá trị của nó, trong khi some ở loại kết quả cung cấp một khái quát hóa trong đó người gọi chọn loại và giá trị kết quả. ( SE- 0341 , 93675336 )

    Giờ đây, bạn có thể suy ra các loại tham số và kết quả từ phần thân của một bao đóng đa câu lệnh. Không còn sự phân biệt giữa các lần đóng đơn và đa câu lệnh.

    Việc sử dụng các bao đóng trở nên ít cồng kềnh hơn bằng cách loại bỏ nhu cầu liên tục chỉ định các loại bao đóng rõ ràng, đôi khi có thể khá lớn (ví dụ: khi có nhiều tham số hoặc loại kết quả bộ dữ liệu phức tạp).

    Ví dụ:

    func map<T>(fn: (Int) -> T) -> T {
      return fn(42)
    }
    
    func computeResult<U: BinaryInteger>(_: U) -> U { /* processing */ }
    
    let _ = map {
      if let $0 < 0 {
         // Do some processing.
      }
    
      return computeResult($0)
    }
    • Giờ đây, bạn có thể suy ra loại kết quả map từ phần thân của dấu đóng được truyền dưới dạng đối số. ( SE-0326 , 93669647 )
    • Giờ đây, bạn có thể hủy bao bọc các biến tùy chọn bằng cú pháp tốc ký làm ẩn phần khai báo hiện có. Ví dụ như sau:
    let foo: String? = "hello world"
    
    if let foo {
      print(foo) // Prints "hello world”.
    }

    tương đương với:

    let foo: String? = "hello world"
    
    if let foo = foo {
      print(foo) // Prints "hello world”.
    }
    • Giờ đây, bạn có thể làm cho các khai báo không khả dụng để sử dụng trong ngữ cảnh không đồng bộ với thuộc tính @available(*, noasync).
      Điều này bảo vệ người dùng API chống lại hành vi không xác định có thể xảy ra khi API sử dụng hoặc khuyến khích sử dụng lưu trữ cục bộ theo luồng trên các điểm tạm ngưng. Nó cũng bảo vệ các nhà phát triển chống lại việc giữ khóa trên các điểm treo, điều này có thể dẫn đến hành vi không xác định, đảo ngược mức độ ưu tiên hoặc deadlocks. ( SE-0340 , 93673989 )
    • Tập lệnh cấp cao nhất hiện hỗ trợ cuộc gọi không đồng bộ. Sử dụng một await bằng cách gọi một hàm không đồng bộ hoặc truy cập một biến bị cô lập sẽ chuyển cấp cao nhất sang ngữ cảnh không đồng bộ. Là một bối cảnh không đồng bộ, các biến cấp cao nhất được cách ly và cấp cao nhất được chạy trên .@MainActor@MainActor Lưu ý rằng quá trình chuyển đổi ảnh hưởng đến độ phân giải quá tải chức năng và bắt đầu một vòng lặp chạy ẩn để điều khiển máy móc đồng thời. Các tập lệnh chưa sửa đổi không bị ảnh hưởng bởi thay đổi này trừ khi -warn-concurrency được chuyển đến lời gọi trình biên dịch. Với -warn-concurrency, các biến ở cấp cao nhất được tách biệt với tác nhân chính và bối cảnh cấp cao nhất được tách biệt với tác nhân chính nhưng không phải là ngữ cảnh không đồng bộ. ( SE-0343 , 93674157 )
    • Swift hiện hỗ trợ lập trình phân tán với việc giới thiệu các tác nhân phân tán. Để biết thêm thông tin, hãy xem SE-0336 , SE-0344 . (70840120)
    • Bây giờ bạn có thể khai báo distributed actor và distributed func bên trong tệp distributed actor. Các tác nhân phân tán cung cấp các đảm bảo cách ly mạnh hơn so với các tác nhân “cục bộ” và chúng cho phép thực hiện các kiểm tra bổ sung đối với các loại trả về và tham số của các phương thức phân tán, ví dụ: kiểm tra xem chúng có tuân thủ Codable. Bạn có thể gọi các phương thức phân tán trên các tham chiếu “từ xa” của các tác nhân phân tán, biến các lệnh gọi đó thành các lệnh gọi thủ tục từ xa, bằng cách triển khai hệ thống tác nhân phân tán có thể cắm và mở rộng cho người dùng. Bản thân Swift không cung cấp bất kỳ hệ thống tác nhân phân tán cụ thể nào; tuy nhiên, các gói trong hệ sinh thái hoàn thành vai trò cung cấp các triển khai đó.
    distributed actor Greeter { 
      var greetingsSent = 0
      
      distributed func greet(name: String) -> String {
        greetingsSent += 1
        return "Hello, \(name)!"
      }
    }
    
    func talkTo(greeter: Greeter) async throws {
      // The isolation of distributed actors is stronger. You can't refer to
      // any stored properties of distributed actors from outside of them.
      greeter.greetingsSent // You can't access the distributed actor-isolated property 'name' from a non-isolated context.
      
      // Remote calls are implicitly throwing and async, 
      // to account for the potential networking involved.
      let greeting = try await greeter.greet(name: "Alice")
      print(greeting) // Hello, Alice!
    }

    Trình khử khởi tạo, hầu hết các trình khởi tạo cho actor các loại và các loại bị hạn chế bởi một tác nhân toàn cầu như @MainActor các quy tắc đã sửa đổi về những biểu thức nào được phép trong phần thân của chúng. Là một phần của SE-327, mục tiêu của các bản sửa đổi này là cải thiện tính an toàn và biểu cảm của ngôn ngữ. Nhiều mẫu lập trình khác hiện được cho phép trong các bộ khởi tạo này.

    Ví dụ: trình khởi tạo không đồng bộ của phiên bản actor trước Swift 5.7 đã đưa ra chẩn đoán bất kỳ lúc nào self thoát khỏi trình khởi tạo trước khi quay lại. Mục đích của chẩn đoán đó là để bảo vệ chống lại một cuộc chạy đua dữ liệu có thể xảy ra khi truy cập các thuộc tính được lưu trữ riêng biệt, nhưng nó đã được phát ra ngay cả khi không có quyền truy cập nguy hiểm.

    Trong Swift 5.7, trình biên dịch hiện kiểm tra các trình khởi tạo này để tìm quyền truy cập nguy hiểm vào các thuộc tính được lưu trữ bị cô lập xảy ra sau khi thoát self:

    actor Database {
      // ... other properties ...
      var rows: Int = 0
    
      init(_ world: DataUser) {
        defer { 
          print("last = \(self.rows)") // ❌ This access to 'rows' is illegal.
        }
        
        print("before = \(self.rows)") // ✅ This access to 'rows' is OK.
        world.publishDatabase(self)    // ✅ Passing 'self' is OK in Swift 5.7+.
        print("after = \(self.rows)")  // ❌ This access to 'rows' is illegal. 
        
        Task { [weak self] in          // ✅ Capturing 'self' is OK in Swift 5.7+.
          while let db = self { await db.prune() }
        }
      }
    }

    Đây là kiểm tra nhạy cảm với luồng điều khiển, nghĩa là truy cập bất hợp pháp không nhất thiết phải xuất hiện trên dòng nguồn sau khi thoát self(trong ví dụ trên, xem xét thời điểm defer thực thi). Trình biên dịch luôn chỉ ra một trong những lối thoát self khiến quyền truy cập trở thành bất hợp pháp.

    Ngoài ra, các công cụ khởi tạo ủy quyền của một actor không còn luôn luôn không bị cô lập. Điều này có nghĩa là trình async khởi tạo ủy quyền có thể thực hiện những việc tương tự như trình khởi tạo không ủy quyền. (84476555)

    • Trình khởi tạo actor không còn yêu cầu viết từ khóa convenience để ủy quyền (SE-327). Trước Swift 5.7, việc thêm hoặc xóa convenience đối với public init của một diễn viên là một thay đổi không linh hoạt, đối với các thư viện được biên dịch có bật tiến hóa. Các thư viện được biên dịch cho Swift 5.7+ hiện có khả năng phục hồi trước những thay đổi trong quá trình triển khai các trình khởi tạo đó để ủy quyền hay không và các chương trình hiện có được biên dịch cho Swift 5.7+ sẽ không yêu cầu biên dịch lại. (87567878)
    • Các loại mới đại diện cho thời gian và đồng hồ hiện đã có sẵn. Điều này bao gồm một giao thức Clock để xác định đồng hồ, cho phép bạn xác định khái niệm về “hiện tại” và cách đánh thức sau một khoảng thời gian nhất định. Một giao thức InstantProtocol mới để xác định thời gian cũng có sẵn. Và một giao thức DurationProtocol mới để xác định khoảng thời gian đã trôi qua giữa hai loại InstantProtocol nhất định cũng có sẵn. Các loại để sử dụng chung là phổ biến nhất ClockSuspending và ClockContinuous, đại diện cho các đồng hồ cơ bản nhất cho hệ thống. Loại ClockSuspendingClock không tiến triển khi máy bị treo, ngược lại ContinuousClock tiến triển bất kể trạng thái của máy.
    func delayedHello() async throws {
      try await Task.sleep(until: .now + .milliseconds(123), clock: .continuous)
      print("hello delayed world")
    }

    Clock cũng có các phương pháp để đo thời gian thực hiện công việc đã trôi qua. Trong trường hợp của SuspendingClock và ContinuousClock điều này đo lường với độ phân giải cao và phù hợp với điểm chuẩn.

    let clock = ContinuousClock()
    let elapsed = clock.measure {
      someLongRunningWork()
    }

    Trình biên dịch hiện phát ra cảnh báo khi một lớp không phải là lớp cuối cùng tuân theo một giao thức áp đặt yêu cầu cùng loại giữa Selfvà một loại được liên kết. Loại yêu cầu này làm cho sự phù hợp không hợp lý cho các lớp con.

    Ví dụ: Swift 5.6 cho phép đoạn mã sau, trong thời gian chạy mã này sẽ xây dựng một phiên bản C và sub C không như mong đợi:

    protocol P {
      associatedtype A : Q where Self == Self.A.B
    }
    
    protocol Q {
      associatedtype B
    
      static func getB() -> B
    }
    
    class C : P {
      typealias A = D
    }
    
    class D : Q {
      typealias B = C
    
      static func getB() -> C { return C() }
    }
    
    extension P {
      static func getAB() -> Self {
        // This is well-typed because `Self.A.getB()` returns
        // `Self.A.B`, which is equivalent to `Self`.
        return Self.A.getB()
      }
    }
    
    class SubC : C {}
    
    // P.getAB() declares a return type of `Self`, so it should
    // return `SubC`, but it actually returns a `C`.
    print(SubC.getAB())
    • Để làm cho ví dụ trên đúng, class C cần phải trở thành final(trong trường hợp đó SubC không thể được khai báo) hoặc giao thức P cần được thiết kế lại để không bao gồm yêu cầu cùng loại Self == Self.A.B. (93675134)
    • Trình biên dịch hiện đưa ra chính xác các cảnh báo cho nhiều biểu thức hơn khi sử dụng tuân thủ giao thức và có thể không khả dụng khi chạy. Trước đây, các biểu thức tham chiếu thành viên và biểu thức xóa loại sử dụng các tuân thủ có khả năng không khả dụng chưa được chẩn đoán, dẫn đến các sự cố có thể xảy ra trong thời gian chạy.
    struct Pancake {}
    protocol Food {}
    
    extension Food {
      var isGlutenFree: Bool { false }
    }
    
    @available(macOS 12.0, *)
    extension Pancake: Food {}
    
    @available(macOS 11.0, *)
    func eatPancake(_ pancake: Pancake) {
      if (pancake.isGlutenFree) { // Warning: Conformance of 'Pancake' to 'Food' is only available in macOS 12.0 or newer.
        eatFood(pancake) // Warning: Conformance of 'Pancake' to 'Food' is only available in macOS 12.0 or newer.
      }
    }
    
    func eatFood(_ food: Food) {}

    Các loại mờ (được biểu thị bằng some) hiện có thể được sử dụng ở các vị trí cấu trúc trong một loại kết quả, bao gồm cả việc có nhiều loại mờ trong cùng một kết quả. Ví dụ:

    func getSomeDictionary() -> [some Hashable: some Codable] {
      return [ 1: "One", 2: "Two" ]
    }

    Các giao thức khác nhau trong thư viện chuẩn hiện khai báo các loại liên kết chính, ví dụ Sequencevà Collectionkhai báo một loại liên kết chính duy nhất Element. Ví dụ: điều này cho phép ghi lại các loại some Collection<Int>và any Collection<Int>. ( SE-0358 , 93929895 )

    Vấn đề đã giải quyết

    • Các thuộc tính được lưu trữ trong Swift không được có thông tin loại có khả năng không khả dụng trong thời gian chạy. Tuy nhiên, trước Swift 5.7, trình biên dịch đã chấp nhận lỗi thuộc tính @available trên thuộc tính được lưu trữ khi thuộc tính có công cụ lazy sửa đổi hoặc trình bao bọc thuộc tính được đính kèm. Điều này có thể dẫn đến sự cố cho các ứng dụng chạy trên hệ điều hành cũ hơn. Trình biên dịch Swift hiện từ chối @available trên tất cả các thuộc tính được lưu trữ. (82713248) (FB9594187)
    • Phiên bản không đồng bộ addTeardownBlock của phương pháp trong XCTestCase hiện đã có. (85453819) (FB9762503)
    • Chẩn đoán về các biểu thức giá trị mặc định không tách biệt được giới thiệu cho Swift 5.6 trong bản phát hành Xcode 13.3 không còn nữa. Quy tắc được đề xuất trong SE-0327 không đủ chính xác để tránh gắn cờ một mẫu phổ biến nhưng vô thưởng vô phạt trong mã SwiftUI liên quan đến các thuộc tính @StateObject và @MainActor. (88971160)
    • Quá trình tái cấu trúc “Tạo Trình khởi tạo Memberwise” thêm chính xác các tham số cho các biến được đánh dấu bằng trình bao bọc thuộc tính. (89057767) (FB9910083)
    • Các thao tác chuỗi trong phiên bản 5.7 của Swift Standard Library triển khai xác thực cải tiến cho các chỉ mục chuỗi, khắc phục một số trường hợp biên trước đây dẫn đến lỗi thời gian chạy giả. Tuy nhiên, Swift hiện chẩn đoán các nỗ lực sử dụng chỉ mục ngoài giới hạn một cách đáng tin cậy hơn và điều này có thể làm lộ các lỗi lập chỉ mục chưa được phát hiện trước đó khi bạn biên dịch lại mã bằng Xcode 14. Nếu bạn thấy lỗi “Chỉ mục chuỗi nằm ngoài giới hạn” mới sau khi xây dựng lại, mã, sau đó kiểm tra kỹ xem bạn có đang áp dụng chỉ mục cũ cho giá trị chuỗi bị thay đổi không. Trong các bản phát hành trước, các trường hợp như vậy có thể âm thầm dẫn đến các giá trị bị hỏng hoặc vô nghĩa, trong khi ở Swift 5.7, giờ đây chúng gây ra lỗi thời gian chạy một cách đáng tin cậy. (89482809)
    • Khi xây dựng các thể hiện Chuỗi từ các chuỗi C, giờ đây Swift thực thi nghiêm ngặt việc chấm dứt null của bộ đệm đầu vào nếu bạn chuyển một đối số bằng cách chuyển đổi con trỏ. Hơn nữa, inout-to-pointer chuyển đổi hiện không được dùng cho cấu trúc Chuỗi. Các chức năng bị ảnh hưởng là String.init(cString:), String.init?(validatingUTF8:), String.decodeCString(_:as:repairingInvalidCodeUnits:) và String.init(decodingCString:as:). (90336023)
    • Chỉnh sửa tất cả trong Phạm vi đổi tên chính xác tất cả các lần xuất hiện của các biến được ghi lại bằng cách sử dụng cú pháp chụp tốc ký bao đóng [capturedVariable] hoặc cú pháp tốc ký if let optionalVariable. (91311033)
    • Bạn có thể sử dụng các giao thức áp dụng các loại được liên kết chính với từ khóa any để bật các loại tồn tại bị ràng buộc.Ví dụ:
    let strings: any Collection<String> = [ "Hello" ]

    Điều này làm cho việc viết các trình bao bọc xóa kiểu cho mã chung đơn giản hơn nhiều vì một loại trình bao bọc riêng biệt không còn cần thiết nữa:

    protocol Producer<T> {
      associatedtype T
      func produce() -> T
    }
    
    typealias AnyProducer<T> = any Producer<T>
    
    /*
    struct AnyProducer<T> {
      var wrappedProduce: () -> T
    }
    */

    Trình biên dịch Swift không còn cảnh báo về các yêu cầu dư thừa trong các khai báo chung. Ví dụ: trong phiên bản beta 1, đoạn mã sau đã chẩn đoán một cảnh báo về T.Iterator : IteratorProtocol yêu cầu là dư thừa, bởi vì nó được ngụ ý bởi T : Sequence:

    func firstElement<T: Sequence>(_: T) -> T.Element where T.Iterator: IteratorProtocol {...}

    Một yêu cầu dư thừa không chỉ ra lỗi mã hóa và đôi khi nên đánh vần chúng cho mục đích tài liệu. Vì lý do này, những cảnh báo này hiện mặc định bị tắt.

    Bạn có thể nhận hành vi trước đó và bật lại các cảnh báo này bằng cách đặt OTHER_SWIFT_FLAGS-X tùy chọn bản dựng trong Xcode thành “ frontend -warn-redundant-requirements”. (92092635)

    • Các phép chuyển động ( isas!as?) đến và từ các loại tồn tại được tham số hóa hiện kiểm tra chính xác các ràng buộc đối với các loại này. (92197049)
    • Đã sửa lỗi: Một số câu lệnh switch phức tạp nhất định trong phần đóng nhiều câu lệnh có thể khiến trình biên dịch gặp sự cố nếu chúng chứa câu lệnh fallthrough yêu cầu suy luận kiểu tại đích của nó, ví dụ: đích case sử dụng khớp mẫu với let các liên kết như case (let ..., let ...). (93796211)
    • Swift không thực hiện kiểm tra Có thể gửi khi thoát khỏi một actor để gọi vào mã không đồng bộ, không bị cô lập. Ví dụ:
    func f(_: NS) async { }
    
    actor A {
      func g(_ ns: NS) async {
        await f(ns) // Warn about passing non-Sendable type 'NS' to a non-isolated async function.
      }
    }

    Vấn đề đã biết

    • #if canImport(AudioVideoBridging)không biên dịch trên Mac Catalyst. (89289575)
      Giải pháp thay thế : Sử dụng #if os(macOS) để hạn chế AudioVideoBridging code cho các nền tảng được hỗ trợ.
    • ~= và các trường hợp trong switches thành công nếu toàn bộ chuỗi khớp, thay vì nếu có một kết quả khớp bên trong chuỗi. Điều này có thể thay đổi tùy thuộc vào kết quả của Swift Evolution cho SE-0357 . (93918632)
    • Trong Swift, một số kiểu chữ Foundation cho các loại chức năng (chẳng hạn như NSItemProvider.CompletionHandler) sẽ thấy @Sendable các loại chức năng, điều này có thể dẫn đến các cảnh báo không mong muốn trong mã chưa áp dụng Đồng thời Swift. Để loại bỏ các cảnh báo, hãy thay thế một tham chiếu đến bí danh kiểu chữ trong mã nguồn Swift bằng kiểu bên dưới của nó mà không có phần mở rộng @Sendable. (98343624)

      Ví dụ:
      thay thế: var completion: NSItemProvider.CompletionHandler
      bằng: var completion: (NSSecureCoding?, Error?) -> Void

    Bài viết được dịch dựa trên nội dung release note xCode14 của Apple
    Refer: https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes

  • XCode 14 có gì mới? (Phần 1)

    XCode 14 có gì mới? (Phần 1)

    Tổng quan

    Xcode 14 bao gồm Swift 5.7 và SDK cho iOS 16, iPadOS 16, tvOS 16, watchOS 9 và macOS Monterey 12.3. Bản phát hành Xcode 14 hỗ trợ gỡ lỗi trên thiết bị trong iOS 11 trở lên, tvOS 11 trở lên và watchOS 4 trở lên. Xcode 14 yêu cầu máy Mac chạy macOS Monterey 12.5 trở lên.

    Thay đổi chung

    Các tính năng mới

    • Xcode 14 cho phép một target duy nhất hỗ trợ nhiều nền tảng và bao gồm có điều kiện các dependencies, code, resource và build settings cho các nền tảng cụ thể. (74664328)
    • Xcode 14 hỗ trợ phát triển trình điều khiển DriverKit cho iPadOS. (81117498)
    • Xcode 14 bao gồm một mẫu mặc định cho các ứng dụng watchOS kết hợp các mục tiêu Ứng dụng WatchKit và Tiện ích mở rộng ứng dụng WatchKit thành một mục tiêu Ứng dụng Watch duy nhất, đơn giản hóa mã, nội dung và quản lý bản địa hóa. Bạn có thể triển khai các ứng dụng watchOS một mục tiêu cho watchOS 7 trở lên. (83222217)

    Vấn đề đã giải quyết

    • Đã sửa lỗi: Khi storyboard đang mở và nội dung watchOS hoặc tvOS bị xóa khỏi Cài đặt Xcode, storyboard sẽ không đóng. (87471381)

    Vấn đề đã biết

    • Nếu một file Package.swift được thêm vào thư mục chứa trong khi thư mục đó đang mở trong Xcode, Package sẽ không được nhận dạng. (85075018)
      Cách giải quyết : Thoát và chạy lại Xcode.
    • CGFLOAT_EPSILON không còn luôn là Float trên watchOS và nó có thể gây ra sự cố biên dịch. (88698530)
      Cách giải quyết: Chuyển đổi nó trước sang CGFloat bằng cách sử dụng trình khởi tạo CGFloat(CGFLOAT_EPSILON). Hiện được hỗ trợ trên cả nền tảng 32 và 64 bit
    • Xcode 14 có thể không tìm thấy các công cụ bằng cách sử dụng xcodebuild -find(được sử dụng bởi xcrun và các trình bao bọc /usr/bin chẳng hạn như /usr/bin/clang) nếu nội dung khởi chạy lần đầu chưa được cài đặt. (98008921)
      Cách giải quyết : Chạy hoặc khởi chạy Xcode.app trước .xcodebuild -runFirstLaunch
    • Xcode đôi khi có thể coi thời gian chạy sim của nền tảng bị thiếu khi thực hiện Đăng xuất và Đăng nhập của người dùng. (99200503)
      Cách giải quyết : Khởi động lại máy.
    • Khi chạy trong macOS 13 beta, mã AppIntents có thể không xây dựng được bằng Xcode 14. (99661742) (FB11470314)
      Cách giải quyết : Tạo mã AppIntents với Xcode 14 beta 6 hoặc trên máy Mac chạy macOS Monterey 12 với Xcode 14.
    • Không thể sử dụng Xcode 14 với iOS 15.7 để phát triển. (99847608)
      Cách giải quyết: Sử dụng Xcode 13.4.1 với iOS 15.7.

    Trình biên dịch Apple Clang

    Các tính năng mới

    • Các dự án C++ mới mà bạn tạo trong Xcode sử dụng phương ngữ ngôn ngữ mặc định là C++20. (93456065)
    • Một số C++20 và C++2b papers đã được triển khai:
      • Các C++20 papers đã triển khai:
        • P0692R1 – Kiểm tra quyền truy cập vào chuyên môn
        • P0388R4 – Cho phép chuyển đổi thành mảng có giới hạn không xác định
      • Các C++2b papers đã được triển khai:
        • P1938R3 –if consteval
        • P1401R5 – Thu hẹp chuyển đổi theo ngữ cảnh thành bool
        • P1949R7 – Cú pháp mã định danh C++ sử dụng Unicode Standard Annex 31
        • P2360R0 – Mở rộng init để cho phép khai báo bí danh (93898598)

    Vấn đề đã giải quyết

    • Đã sửa lỗi: Xcode không cung cấp tính năng làm nổi bật ngữ nghĩa và hỗ trợ chuyển sang định nghĩa cho các khai báo khái niệm C++20 và các mệnh đề yêu cầu trong các mẫu. (93046529)

    Tính năng không còn khả dụng trên XCode14

    • Bắt đầu với Xcode 14, bitcode không còn cần thiết cho các ứng dụng watchOS và tvOS và App Store không còn chấp nhận gửi bitcode từ Xcode 14. Xcode không còn xây dựng mã bit theo mặc định và tạo thông báo cảnh báo nếu một dự án kích hoạt rõ ràng mã bit: “Việc xây dựng bằng mã bit không được dùng nữa. Vui lòng cập nhật cài đặt dự án và/hoặc mục tiêu của bạn để tắt mã bit.” Khả năng xây dựng bằng mã bit sẽ bị xóa trong bản phát hành Xcode trong tương lai. IPA có chứa mã bit sẽ bị tước mã bit trước khi được gửi tới App Store. Chỉ có thể tải xuống các biểu tượng gỡ lỗi từ App Store Connect / TestFlight cho các lần gửi mã bit hiện có và không còn khả dụng cho các lần gửi được thực hiện bằng Xcode 14. (86118779)

    Danh mục tài sản

    Các tính năng mới

    • Đơn giản hóa biểu tượng ứng dụng bằng một hình ảnh 1024×1024 được tự động thay đổi kích thước cho mục tiêu của nó. Chọn tùy chọn Single Size trong tab Attributes inspector của mục app icon. Bạn vẫn có thể ghi đè các kích thước riêng lẻ bằng tùy chọn All Sizes. (18475136) (FB5503050)
    • Giờ đây, bạn có thể dán trực tiếp hình ảnh đã sao chép từ Finder vào asset catalog outline. (58980721)
    • Giờ đây, bạn có thể nhấp đúp vào một vị trí hình ảnh để hiển thị bảng điều khiển tệp đang mở và chọn nội dung thay thế. (81365822)
    • Bạn có thể chỉ định chế độ kết xuất mặc định cho các ký hiệu tùy chỉnh trong Asset Catalog. Đặt thuộc tính Kết xuất dưới dạng thành tự động, mẫu, nhiều màu hoặc phân cấp. Sau đó, hệ thống sẽ sử dụng chế độ kết xuất mặc định cho biểu tượng, trừ khi bạn ghi đè rõ ràng chế độ đó. Để biết thêm thông tin về biểu tượng tùy chỉnh, hãy xem Tạo hình ảnh biểu tượng tùy chỉnh cho ứng dụng của bạn . (84513859)

    Vấn đề đã giải quyết

    • Chế độ “Tất cả kích thước” cho các biểu tượng ứng dụng đã được làm linh hoạt hơn và bao gồm một số kích thước bổ sung chưa được sử dụng trước đây. Chế độ “Tất cả kích thước” không yêu cầu phải lấp đầy tất cả các vị trí, kích thước chỉ có thể được cung cấp khi cần. Các biểu tượng ứng dụng được tạo bằng Xcode 13 trở về trước có thể được chuyển đổi từ chế độ “Tất cả kích thước (Xcode 13)” thành “Tất cả các kích thước” hoặc “Một kích thước”. (93682080)

    Vấn đề đã biết

    • Các ứng dụng sử dụng biểu tượng ứng dụng có kích thước đơn lẻ có thể không xác thực được App Store nếu mục tiêu triển khai cũ hơn iOS 12 hoặc watchOS 4. (98471456)

    Build System

    Các tính năng mới

    • Xcode cung cấp một trình chỉnh sửa trợ lý mới cho nhật ký bản dựng tập trung vào tính song song để giúp xác định các vấn đề về hiệu suất bản dựng. Hình ảnh trực quan này hiển thị các sự kiện dưới dạng lưới các khối màu trong đó trục dọc biểu thị mức độ song song và trục ngang biểu thị thời gian. (47858322)
    • Xcode 14 hiện có thể biên dịch các mục tiêu song song với các phụ thuộc mục tiêu Swift của chúng. (57116972)
    • Trình điều khiển Swift, thành phần phối hợp các yêu cầu giao diện người dùng Swift, hiện được tích hợp vào hệ thống xây dựng của Xcode, cho phép phụ thuộc chi tiết hơn vào các tác vụ hệ thống xây dựng khác và lập lịch trình rõ ràng. (72440175)
    • Trong Build Phases, giờ đây bạn có thể chỉnh sửa hàng loạt tệp trong chế độ xem bảng nhiều lựa chọn. Khi bạn chỉnh sửa cột bộ lọc nền tảng của bảng đó, hệ thống sẽ áp dụng các thay đổi cho tất cả các tệp trong lựa chọn. (80683128) (FB9340886)
    • Các mục tiêu thư viện động và khung chỉ dành cho Swift có thể chọn tham gia tối ưu hóa hệ thống bản dựng mới bằng cách sử dụng cài đặt bản dựng EAGER_LINKING. Khi bạn bật tính năng này, Xcode sẽ phát ra các thành phần tạo tác bổ sung trong quá trình biên dịch Swift, cho phép Xcode bỏ chặn liên kết của các mục tiêu xuôi dòng trước đó, tăng tính song song trong các bản dựng. (82396635)
    • Hệ thống xây dựng chạy song song các tác vụ từ các giai đoạn xây dựng khác nhau khi các yếu tố phụ thuộc đầu vào và đầu ra không thực thi thứ tự của chúng. Bạn có thể chọn tham gia hành vi mới này cho các giai đoạn xây dựng tập lệnh chạy bằng cách sử dụng cài đặt bản dựng FUSE_BUILD_SCRIPT_PHASES. (82396977)
    • App Store hiện hỗ trợ làm mỏng ứng dụng cho các trình đổ bóng Metal được biên dịch sẵn. (82902821)
    • Các bản dựng Xcode cho thiết bị watchOS hiện bao gồm kiến ​​trúc arm64 theo mặc định. (83319300)
    • Đã thêm toán tử thay thế :relativeto=macro mới cho cài đặt bản dựng mà bạn có thể sử dụng để tính toán đường dẫn tương đối từ đường dẫn này sang đường dẫn khác; 
      Ví dụ: $(INSTALL_PATH:relativeto=/usr/lib)
      Chỗ INSTALL_PATH là “ /usr/bin..” và đánh giá là “../lib ”. Bạn có thể sử dụng quy tắc này trong quy tắc xây dựng để sao chép một loạt tham chiếu tệp trong khi vẫn duy trì phân cấp thư mục của chúng trong thư mục đích hoặc để tính toán mục tiêu dự kiến rpaths ​​bằng cách sử dụng đường dẫn tương đối giữa đường dẫn cài đặt của chính nó và đường dẫn cài đặt đã biết của các thành phần phụ thuộc. (88293015)
    • Xcode hiện cung cấp các cài đặt RECOMMENDED_MACOSX_DEPLOYMENT_TARGET, RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET, RECOMMENDED_TVOS_DEPLOYMENT_TARGET, RECOMMENDED_WATCHOS_DEPLOYMENT_TARGET, RECOMMENDED_DRIVERKIT_DEPLOYMENT_TARGET và xây dựng cho biết các phiên bản triển khai tối thiểu được đề xuất cho từng nền tảng Xcode được hỗ trợ. (90464341)
    • Giờ đây, bạn có thể bật Sandbox cho các giai đoạn xây dựng tập lệnh shell bằng cách sử dụng ENABLE_USER_SCRIPT_SANDBOXING trong build settings. Sandbox chặn quyền truy cập vào các tệp bên trong gốc nguồn của dự án cũng như thư mục Dữ liệu được tạo trừ khi bạn liệt kê các tệp đó dưới dạng đầu vào hoặc đầu ra. Khi được bật, quá trình xây dựng không thành công do vi phạm Sandbox nếu một giai đoạn tập lệnh cố gắng đọc hoặc ghi vào một phần phụ thuộc không được khai báo, ngăn chặn các quá trình xây dựng không chính xác. (90506067)

    Vấn đề đã giải quyết

    • Đã khắc phục sự cố trong đó nhiều dự án Xcode tham chiếu cùng một xcconfig (do đó bao gồm một xcconfig khác) đã tính toán không chính xác cùng một cài đặt bản dựng trên cả hai dự án. (84319288) (FB9707003)
    • Đã sửa lỗi: đích chạy của thiết bị watchOS không còn xuất hiện hai lần trong menu đích chạy. (85635959)
    • Cải thiện tốc độ tải của không gian làm việc lớn có nhiều mục tiêu chia sẻ .xcconfigtệp. (85985712)
    • Đã sửa lỗi: Khi bạn lưu trữ ứng dụng watchOS với kiến ​​trúc arm64e được bật, ứng dụng đó không thể xây dựng. (93550623)

    Vấn đề đã biết

    • Báo cáo sự cố từ các ứng dụng Mac Catalyst không được ký hiệu đầy đủ trong Xcode Organizer. (94820955)

    Tính năng bị loại bỏ

    • Vì mã bit hiện không được dùng nữa nên các bản dựng cho iOS, tvOS và watchOS không còn bao gồm mã bit theo mặc định. (87590506)
    • Đã thêm cài đặt bản dựng mới, SWIFT_TOOLS_DIR, thay thế SWIFT_EXEC làm cơ chế được đề xuất để sử dụng tệp thực thi giao diện người dùng nhanh tùy chỉnh. SWIFT_TOOLS_DIR phải được đặt thành đường dẫn của thư mục chứa giao diện người dùng nhanh và các công cụ liên quan. Lưu ý rằng vì trình điều khiển Swift hiện được tích hợp trong hệ thống xây dựng Xcode, không thể sử dụng SWIFT_TOOLS_DIR và SWIFT_EXEC để chỉ định nhị phân trình điều khiển tùy chỉnh trừ khi SWIFT_USE_INTEGRATED_DRIVER được đặt thành NO. (88967344)
    • Hệ thống xây dựng cũ đã bị xóa. (90801041)
    • Việc xây dựng các dự án iOS với các mục tiêu triển khai cho kiến ​​trúc armv7, armv7s và i386 không còn được hỗ trợ nữa. (92831716)
    • Bản phát hành xây dựng để triển khai cho hệ điều hành cũ hơn macOS 10.13, iOS 11, tvOS 11 và watchOS 4 không còn được hỗ trợ. (92834476)
    • Khi xóa một điều kiện khỏi trình chỉnh sửa biến thể, giá trị sẽ không được duy trì. (98149034)
      Giải pháp thay thế: Sử dụng Trình chỉnh sửa cài đặt bản dựng để xóa điều kiện.

    Debugging

    Các tính năng mới

    • Trình gỡ lỗi biểu đồ bộ nhớ hiện hiển thị tất cả các tham chiếu đến và đi trong biểu đồ bộ nhớ. Bạn có thể điều chỉnh tập hợp các nút hiển thị trong cửa sổ bật lên mới. (69454709)
    • LLDB hiện hiển thị các cập nhật tiến độ trong Xcode và dòng lệnh cho các hoạt động chạy dài. (73511008)
    • Giờ đây, bạn có thể gọi tập lệnh nhật ký sự cố của LLDB bằng xcrun crashlog <path/to/crash>. (79991815)
    • Trình kiểm tra hiệu suất luồng hiển thị các vấn đề về hiệu suất thời gian chạy trong Trình điều hướng sự cố và trình chỉnh sửa nguồn trong khi gỡ lỗi ứng dụng. Chọn hộp kiểm Trình kiểm tra hiệu suất luồng trong Lược đồ chạy của mục tiêu ứng dụng của bạn để bật tính năng này. (80048810)
    • Giờ đây, bạn có thể mở một .xcresult cho một hành động scheme khởi chạy. (88932007)
    • Xcode hiện hiển thị nhật ký khởi chạy mới trong Trình điều hướng báo cáo. Nhật ký cho biết các hành động mà Xcode thực hiện để cài đặt, khởi chạy và gỡ lỗi. (90859910)

    Vấn đề đã giải quyết

    • Cải thiện hiệu suất khi gỡ lỗi chương trình Swift trên thiết bị iOS. Thay vì sao chép siêu dữ liệu phản ánh từ thiết bị iOS, giờ đây Xcode có thể đọc siêu dữ liệu từ đĩa. (73179144)
    • Xcode hiện nhóm các mục nhật ký cho cùng một hành động sơ đồ khởi chạy trong trình điều hướng Báo cáo. (87216988)
    • Đã sửa lỗi: Thay vào đó, bước vào chức năng không đồng bộ Swift trong LLDB sẽ hoàn thành chức năng hiện tại. (88142757)

    Vấn đề đã biết

    • Không thể đính kèm trình gỡ lỗi vào Tiện ích mở rộng tiện ích màn hình khóa. (93941779)
      Giải pháp thay thế: Chỉnh sửa Extension scheme – trong tab Run action, tab Argument, hãy đặt biến môi trường ‘_XCWidgetKind’ thành một trong các tên class/struct của bạn khi triển khai widget.

    Tài liệu

    Các tính năng mới

    • Swift-DocC trong Xcode hiện hỗ trợ viết và xây dựng tài liệu cho API Objective-C và C. (58760015)
    • Các trang web tài liệu Swift-DocC mà Xcode 14 tạo ra bao gồm một thanh bên điều hướng mới để khám phá và lọc tài liệu. (89031049)
    • Theo mặc định, tài liệu Swift-DocC mà Xcode 14 tạo ra hiện tương thích với hầu hết các dịch vụ lưu trữ được quản lý, bao gồm Trang GitHub. (91173450)

    Vấn đề đã giải quyết

    • Đã sửa lỗi: Swift-DocC đưa ra chẩn đoán không chính xác về các lệnh không được hỗ trợ trong tài liệu nguồn biểu tượng khi xây dựng tài liệu cho các dự án Objective-C bao gồm các lệnh Doxygen. (90953916)
    • Đã sửa lỗi: Tài liệu Mục tiêu-C cho các API đa ngôn ngữ hiển thị các mối quan hệ ký hiệu Swift thay vì các mối quan hệ Mục tiêu-C. (91627374)
    • Đã sửa lỗi: Swift-DocC đưa ra chẩn đoán không chính xác về các liên kết ký hiệu không thể giải quyết đối với tài liệu kế thừa từ các ký hiệu trong các mô-đun khác. (92185538)
    • Đã sửa lỗi : Không thể xây dựng tài liệu cho các loại phù hợp với ChartContent protocol khi conform Swift Charts. (93610106)

    Instruments

    Các tính năng mới

    • Các hàm tổng hợp hoạt động trên các khoảng thời gian hiện tính toán chính xác hơn kết quả của chúng khi bộ lọc thời gian đang hoạt động, chỉ dựa trên phần của khoảng thời gian tổng thể giao với bộ lọc thời gian hiện tại. (32330648)
    • Bộ lọc chi tiết hiện cho phép áp dụng các bộ lọc cho một cột cụ thể khi xem Chế độ xem danh sách. Có thể thêm các bộ lọc đã nhập này bằng cách sử dụng menu ngữ cảnh trên các giá trị được hiển thị hoặc bằng cách nhập mã thông báo rồi chọn loại mã thông báo. (56080914)
    • Các rãnh Chủ đề chính hiện được sắp xếp đầu tiên khi hiển thị các rãnh trong dòng thời gian của Nhạc cụ. (59876638)
    • Công cụ có công cụ Theo dõi treo mới hiển thị khi chuỗi chính của ứng dụng không thể xử lý các sự kiện đến trong một khoảng thời gian dài, có khả năng khiến giao diện người dùng bị treo. Ngoài ra, các công cụ Time Profiler và CPU Profiler cũng hiển thị khả năng bị treo. (65694830)
    • Biểu đồ biểu đồ trong Công cụ hiện hiển thị thời lượng của phạm vi thời gian được sử dụng để tổng hợp dữ liệu trong chú giải công cụ di chuột qua. (65684291)
    • Chế độ xem danh sách của công cụ hiện hiển thị nhãn trạng thái ở dưới cùng, báo cáo về số lượng hàng hiện có và được chọn trong chế độ xem hiện tại. (82652407)
    • Mẫu Core ML mới có sẵn trong Công cụ. Mẫu này bao gồm các công cụ Core ML và Neural Engine mới cùng với các công cụ GPU và Time Profiler. Sử dụng mẫu này để giúp lập hồ sơ sử dụng Core ML và hiểu cách các mô hình của bạn đang chạy trên thiết bị. Việc kết hợp thông tin từ Core ML, Công cụ thần kinh và Công cụ GPU có thể giúp theo dõi những hoạt động nào được thực thi trên phần cứng được tăng tốc. Dữ liệu thời gian tổng hợp có sẵn cho từng sự kiện, mô hình và mô hình con. (83123510)
    • Bạn có thể mở một menu ngữ cảnh mới bằng các khoảng thời gian bấm Điều khiển trong dòng thời gian. Menu cung cấp các hành động để đặt bộ lọc thời gian theo khoảng thời gian đã chọn hoặc hiển thị thông tin tương ứng trong khu vực chi tiết. (86728567)
    • Chế độ xem Thông tin Chạy của Dụng cụ hiện bao gồm kiến ​​trúc của tệp nhị phân đích khi ghi một dấu vết quy trình. (89733709)
    • Instruments có một công cụ Runloop mới hiển thị việc sử dụng runloop và các lần lặp lại riêng lẻ, đồng thời phân biệt trực quan các chế độ ngủ của runloop và các khoảng thời gian bận cho tất cả các runloop trong một quy trình. (89746568)
    • Các giá trị trong chế độ xem chi tiết tổng hợp hoặc danh sách giờ đây sẽ được tô sáng khi bạn trỏ con trỏ qua chúng và bạn có thể giữ Control khi bấm vào một giá trị để xem menu ngữ cảnh. Menu này hiển thị các hành động để sao chép giá trị, đặt bộ lọc chi tiết và thêm hoặc ghim rãnh nếu giá trị có đại diện cho rãnh. (90817779)
    • Các công cụ hiện bao gồm một mẫu Đồng thời Swift mới để theo dõi cách sử dụng và hành vi của các nguyên mẫu đồng thời của Swift. Mẫu chứa:
      • Một công cụ Swift Tasks mới hiển thị các trạng thái nhiệm vụ theo thời gian, tóm tắt các trạng thái nhiệm vụ, cung cấp tường thuật chi tiết về nhiệm vụ, minh họa các mối quan hệ đồng thời có cấu trúc và xây dựng cây cuộc gọi của các ngăn xếp công việc tạo nhiệm vụ.
      • Một công cụ Swift Actors mới để theo dõi hành vi tác vụ giữa các tác nhân hiển thị hàng đợi tác vụ cho từng tác nhân và giúp chẩn đoán các sự cố với mã và tranh chấp được cô lập bởi tác nhân.Các công cụ này yêu cầu thiết bị trong thời gian chạy đồng thời Swift lần đầu tiên có sẵn trong macOS 13, iOS 16, tvOS 16 và watchOS 9 trở lên. (69852855)
    • heapleaks, and Instruments’ leaks tool hiện báo cáo ít tham chiếu sai hơn bắt nguồn từ phân bổ liên quan đến đồng thời Swift và xác định đây là phân bổ Swift AsyncTask. (72907112)
    • leaks --atExit -- <command> giờ đây sẽ tự động đặt trong môi trường MallocStackLogging=lite cho lệnh đã chỉ định để Công cụ có thể hiển thị dấu vết ngược ngăn xếp cho phân bổ bị rò rỉ. Để sử dụng một cài đặt khác của biến môi trường đó (chẳng hạn như CÓ hoặc KHÔNG), hãy đặt biến môi trường trước khi chạy leaks. (73779272)
    • xctrace bây giờ sẽ gửi thông báo Darwin khi bắt đầu theo dõi, thông báo này nên được ưu tiên thay vì đọc đầu ra tiêu chuẩn của lệnh. Để sử dụng nó, hãy chỉ định notify-tracing-started tùy chọn có tên thông báo mong muốn và sử dụng notify_register_dispatch để nhận thông báo không đồng bộ trong tập lệnh của bạn. (75745933)
    • Cải thiện hiệu suất phân tích của mẫu Dấu vết Hệ thống. Các công cụ hiện hiển thị các bản ghi cho người dùng nhanh hơn; tuy nhiên, khi theo dõi một quy trình đơn lẻ, bạn không thể thấy trạng thái luồng cho các quy trình khác. Thay vào đó, để xem trạng thái của các quy trình khác, hãy theo dõi “Tất cả các quy trình”. (79904471)
    • Đối với các quy trình đang chạy MallocStackLogging được bật, heap, leaks và trình gỡ rối bộ nhớ Xcode hiển thị các tên loại dành cho phân bổ không đối tượng ở dạng malloc in <function name> trong đó “function name” là khung ngăn xếp không phân bổ đầu tiên. Tính năng này giúp xác định rõ hơn mục đích của bộ nhớ phi đối tượng. (85598783)
    • heap -addresses=all hiện hiển thị các mô tả của các đối tượng CFData có chứa các .plisttệp nhị phân theo cách dễ đọc hơn đối với con người. (88466642)

    Vấn đề đã giải quyết

    • heap , leaks, và stringdups bây giờ bao gồm nội dung của một số loại chuỗi Swift được cấp phát theo đống trong phần mô tả về cấp phát của các chuỗi đó. (14902403)
    • Công cụ Hiệu suất GCD hiện diễn giải chính xác các sự kiện có mã “7” và “9”. (69721960)
    • Thiết kế trình xem nguồn trong Công cụ bao gồm các cải tiến để hiển thị dữ liệu hiệu suất tốt hơn:
      • Có một chế độ Xen kẽ mới để xem mã nguồn và tháo gỡ liên kết với nhau, giúp việc liên kết các hướng dẫn được tạo cho từng dòng mã nguồn trở nên dễ dàng hơn.
      • Trình xem nguồn hiện hiển thị các giá trị được lấy mẫu được gán cho một dòng mã hoặc phân tách trong một cột riêng biệt thay vì dưới dạng chú thích dòng.
      • Trình xem nguồn hiện hiển thị Bộ đếm CPU, sự kiện PMC và công thức động được định cấu hình trong các tùy chọn ghi bên cạnh nguồn và tháo gỡ. (80232392)
    • Đã khắc phục sự cố trong đó Chế độ xem nguồn trong Công cụ đôi khi không hiển thị chú thích hiệu suất khi được mở từ chế độ xem Sơ đồ cuộc gọi. (80501587)
    • Khi sử dụng Instruments Source Viewer ở chế độ xem tách, việc chọn tháo gỡ bây giờ cũng chọn nguồn tương ứng ở bên trái. (82642041)
    • Đã khắc phục sự cố xctrace export đôi khi xuất giá trị chuỗi UTF-16 thay vì UTF-8, dẫn đến lỗi kết xuất trong Terminal. (87594987)
    • Đã khắc phục sự cố trong đó một số sự kiện đã ghi sau khi khởi động lại sẽ bị mất khi nhập kho lưu trữ nhật ký trong quá trình khởi động lại thiết bị. (89003393)
    • Đã sửa lỗi xctrace list devices đôi khi không bao gồm tất cả các thiết bị được kết nối vật lý trong đầu ra của nó. (89142775) (FB9913864)
    • heapleaks, and Instruments hiện nhận dạng chính xác bảng phụ của các đối tượng Swift đã hủy phân bổ dưới dạng bảng phụ. (89459805)
    • Đã khắc phục sự cố trong đó Công cụ không cho phép ghi các thiết bị tvOS được kết nối qua WiFi. (89592418) (FB9935783)
    • Đã khắc phục sự cố trong đó bản dựng gói Công cụ tùy chỉnh có thể không thành công khi cùng một biến được sử dụng nhiều lần trong trường biểu thức. (89653878)
    • heapleaks, and Instruments hiện hiển thị tên dễ đọc hơn cho các loại thư viện tiêu chuẩn Foundation và Swift bổ sung. (90441836)
    • xctrace không còn trả về trạng thái lỗi khi truy tìm chỉ gây ra cảnh báo. Các vấn đề hiện được quy cho mức độ nghiêm trọng thích hợp. (90560207) (FB9963155)
    • Đã khắc phục sự cố trong xctrace export khi dữ liệu theo dõi VM và phân bổ đôi khi được xuất với các hàng dữ liệu bị thiếu. (90560947) (FB9963169)
    • Đã khắc phục sự cố trong đó Công cụ sẽ quên đối số dòng lệnh sau khi mở và đóng trình chỉnh sửa đích khi đã có đối số. (90666084)
    • Đã khắc phục sự cố khi di chuyển đầu kiểm tra trong thước theo dõi cũng sẽ cuộn trục Y của dòng thời gian. (90810506)
    • Đã sửa lỗi: Khoảng thời gian chạy vòng lặp cho thời gian chạy, lặp, chờ và bận có nhãn và độ dài không chính xác khi được hiển thị trong rãnh quy trình. (91130163)
    • Đã khắc phục sự cố Công cụ không liên tục khi ghi dấu vết với các mẫu Phân bổ hoặc Rò rỉ. (93916110)
    • Đã sửa lỗi: Tạo Báo cáo hiệu suất Core ML có thể không thành công sau khi bật Chế độ nhà phát triển trên thiết bị. (93923607)

    Vấn đề đã biết

    • Ký hiệu mã người dùng dSYM có thể không thành công đối với tệp tailspin được nhập vào Công cụ. Các ký hiệu cho thư viện hệ thống cũng không xuất hiện khi các tệp này được tải. (93261223)
      Giải pháp thay thế : Ký hiệu nhật ký treo trong Terminal bằng spindump -i <UIKit-runloop*.ips>.
    • Vô hiệu hóa Trình kiểm tra hiệu suất luồng thông qua tab Chẩn đoán trong Trình chỉnh sửa lược đồ nếu Ứng dụng iOS hoặc macOS của bạn sử dụng Fishhook để kết nối các cuộc gọi tới libSystem nhằm mục đích gỡ lỗi/theo dõi. (94724380) (FB10131267)

    Refer: https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes

  • Hướng dẫn cách làm slider hai chiều trong Swift

    Hướng dẫn cách làm slider hai chiều trong Swift

    Là một iOS developer, chắc hẳn mọi người đã từng làm những tính năng mà các UIKit của Apple không thể đáp ứng được yêu cầu. Lúc này chúng ta cần phải thực hiện custom hoặc tạo một common UI với cơ chế mới thoả mãn yêu cầu của khách hàng. Và ở dự án mình cũng có những yêu cầu như vậy, nên bài viết này mình sẽ hướng dẫn các bạn làm slider hai chiều trong Swift.

    Mục đích của chúng ta là đạt được kết quả như sau:

    Slider hai chiều

    I. Yêu cầu

    Cần tạo một common slider thoả mãn các điều kiện như sau:

    1. Slider được chia thành các đoạn bằng nhau có thể chỉnh sửa, Slider có 2 điểm kéo, có thể kéo được từ giá trị min tới giá trị max, kéo chồng lên nhau, khi kéo tới các điểm sẽ thực hiện rung.
    2. Cho phép thay đổi giao diện

    II. Giải pháp

    1. Tạo một mảng string để lưu các điểm cho phép và dùng nó để tính toán và hiển thị dữ liệu cho Slider hai chiều
    2. Dựa vào số phần tử trong mảng để tính khoảng cách giữa các điểm và vị trí của nó.
    3. Đối với tính năng kéo mình sẽ dùng UIPanGestureRecognizer để xử lí. Khi kết thúc hành động kéo thì xác định vị trí của điểm được kéo so với các điểm trong mảng, nếu gần điểm nào thì tự di chuyển tới vị trí của điểm đó.

    III. Hướng dẫn chi tiết

    Để cho tất cả mọi người đều có thể thực hiện một các dễ dàng thì mình sẽ hướng dẫn chi tiết theo các bước như sau:

    1. Tạo mới file swift

    Chúng ta cần tạo mới file swift để làm common Slider hai chiều theo các bước sau:

    Trong project, bấm chuột phải vào thư mục muốn tạo file -> new file -> chọn swift -> đặt tên cho file, ở đây mình đặt là: TWSlider.swift

    Kết quả tạo file

    2. Thêm nội dung và logic cho file TWSlider.swift

    Trước tiên chúng ta cần thêm image vào asset của project để sử dụng cho slider, các bạn cũng có thể thêm các image tương ứng với yêu cầu của dự án đang làm.

    Và chúng ta cũng cần thêm Then vào để setup UI cho tiện như sau

    public protocol Then {}
    
    public extension Then {
        @discardableResult
        func then(_ block: (Self) -> Void) -> Self {
            block(self)
            return self
        }
    }
    
    extension NSObject: Then {}
    extension CGPoint: Then {}
    extension CGRect: Then {}
    extension CGSize: Then {}
    extension CGPath: Then {}

    1. Tạo protocol để bắn dữ liệu cần thiết ra màn hình cần xử lí logic

    protocol TWSliderDelegate: AnyObject {
        func sliderScrolled(_ slider: TWSlider?,
                            toMinIndex minIndex: Int,
                            andMaxIndex maxIndex: Int,
                            endDragDrop: Bool)
    }

    2. Tạo các biến cho phép thay đổi các giá trị của slider

    class TWSlider: UIView {
        
        // PUBLIC variable
        // The number of points in slider
        var numberOfSegments: Int = 0
        
        // This value should be set if slider button should overlap or not. Default to NO. ie., 1 segment space will be present between the sliders.
        var shouldSliderButtonOverlap: Bool = false
        
        // The color that is used for rangeSlider unselected range view. (i.e., the view that is not within the slider points). Default is #DFDEE4.
        var rangeSliderBackgroundColor: UIColor = .gray
        
        // The color that is used for rangeSlider selected range view. (i.e., the view that is between the slider points). Default is #FF9800.
        var rangeSliderForegroundColor: UIColor = .orange
        
        // The label color to be used for min range display. Default is white.
        var rangeDisplayLabelColor: UIColor = .white
        
        // The label color to be used for min range display. Default is black.
        var minMaxDisplayLabelColor: UIColor = .black
        
        // The color for segment button when it is within the selected range. Default is clear.
        var segmentSelectedColor: UIColor = .clear
        
        // The color for segment button when it is outside the selected range. Default is #F8F8FA.
        var segmentUnSelectedColor: UIColor = .white
        
        // The image used for displaying slider buttons. By default "ic_sliderButton". rangeSliderButtonColor will be used if not set
        var rangeSliderButtonImage: UIImage?
        
        // The image for segment button when it is within the selected range. If not set, segmentSelectedColor will be used.
        var segmentSelectedImage: UIImage?
        
        // The image for segment button when it is outside the selected range. If not set, segmentUnSelectedColor will be used.
        var segmentUnSelectedImage: UIImage?
        
        // The size of the slider button. If not set, defaults to (16, 16).
        var sliderSize: CGSize = CGSize(width: 16, height: 16)
        
        // The size of the segments. If not set, defaults to (4, 4).
        var segmentSize: CGSize = CGSize(width: 4, height: 4)
        
        // The min and max range label text to be set by caller
        var minRangeText: String?
        var maxRangeText: String?
        
        // The delegate property
        weak var delegate: TWSliderDelegate?
        
        // PRIVATE VAR
        // Slider button size
        private var SLIDER_BUTTON_WIDTH: CGFloat = 44.0
        
        // Slider frame
        private var DEFAULT_SLIDER_FRAME: CGRect!
        
        // Segment width
        private var segmentWidth: CGFloat!
        
        // The backgroundView represent unselected/outside range view
        private var sliderBackgroundView: UIView!
        
        // The foregroundView represent selected/inside range view
        private var sliderForegroundView: UIView!
        
        // The label placed below the min and max sliders
        private var minRangeView: UIView!
        private var maxRangeView: UIView!
        private var minRangeLabel: UILabel!
        private var maxRangeLabel: UILabel!
        
        // min and max value
        private var minLabel: UILabel!
        private var maxLabel: UILabel!
        
        // Represent the range slider on either side of the slider
        private var startSliderButton: UIButton!
        private var endSliderButton: UIButton!
        
        // The segment index or percent for initial slider position loading for segmented and unsegmented respectively
        private var minRangeInitialIndex: Int?
        private var maxRangeInitialIndex: Int?
        
        // Padding range view with slider button
        private let iconRangeMidView = "ic_rangeMidView"
        private let iconRangeView = "ic_rangeView"
        private let iconSliderButton = "ic_sliderButton"
        private let iconSegmentUnSelected = "ic_segmentUnSelected"
        private let paddingRangeView: CGFloat = 4
        
        private var sliderMidView: UIView!
        private var sliderMidLabel: UILabel!
        private var checkInit: Bool = false
        // MARK: init
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            setUpUI()
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            setUpUI()
        }
        
        func setUpUI() {
            setDefaultValues()
            initSliderViews()
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            updateFrame()
        }
    }

    3. Tạo các hàm private để setup UI cho slider

    extension TWSlider {
        
        // Default Initializaer
        private func setDefaultValues() {
            checkInit = false
            numberOfSegments = 2
            shouldSliderButtonOverlap = false
            
            minRangeLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
            minRangeLabel.then {
                $0.textColor = rangeDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .medium)
            }
            
            minRangeView = UIView(frame: CGRect(x: 0, y: 0, width: 24, height: 29))
            minRangeView.then {
                $0.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: bounds.midY - (SLIDER_BUTTON_WIDTH / 2) - (paddingRangeView / 2))
                if let image = UIImage(named: iconRangeView) {
                    $0.backgroundColor = UIColor(patternImage: image)
                }
                addSubview($0)
                $0.addSubview(minRangeLabel)
            }
            
            maxRangeLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
            maxRangeLabel.then {
                $0.textColor = rangeDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .medium)
            }
            
            maxRangeView = UIView(frame: CGRect(x: 0, y: 0, width: 24, height: 29))
            maxRangeView.then {
                $0.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2) + (paddingRangeView / 2))
                if let image = UIImage(named: iconRangeView) {
                    $0.backgroundColor = UIColor(patternImage: image)
                }
                addSubview($0)
                $0.addSubview(maxRangeLabel)
            }
            
            minLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
            minLabel.then {
                $0.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2))
                $0.textColor = minMaxDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .regular)
                addSubview($0)
            }
            
            maxLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
            maxLabel.then {
                $0.center = CGPoint(x: bounds.maxX - SLIDER_BUTTON_WIDTH, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2))
                $0.textColor = minMaxDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .regular)
                addSubview($0)
            }
            
            // Mid View
            sliderMidLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 49, height: 24))
            sliderMidLabel.then {
                $0.textColor = rangeDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .medium)
            }
            sliderMidView = UIView(frame: CGRect(x: 0, y: 0, width: 49, height: 29))
            sliderMidView.then {
                $0.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2) + (paddingRangeView / 2))
                if let image = UIImage(named: iconRangeMidView) {
                    $0.backgroundColor = UIColor(patternImage: image)
                }
                addSubview($0)
                $0.addSubview(sliderMidLabel)
                $0.alpha = 0
            }
            segmentWidth = getSegmentWidth(forSegmentCount: numberOfSegments)
            rangeSliderButtonImage = UIImage(named: iconSliderButton)
            segmentUnSelectedImage = UIImage(named: iconSegmentUnSelected)
            
        }
        
        // Init the sliding representing views
        private func initSliderViews() {
            DEFAULT_SLIDER_FRAME = CGRect(x: SLIDER_BUTTON_WIDTH / 2, y: self.bounds.midY, width: self.bounds.width - SLIDER_BUTTON_WIDTH, height: 4)
            
            sliderBackgroundView = UIView(frame: DEFAULT_SLIDER_FRAME)
            sliderBackgroundView.then {
                $0.backgroundColor = rangeSliderBackgroundColor
                addSubview($0)
            }
            
            sliderForegroundView = UIView(frame: DEFAULT_SLIDER_FRAME)
            sliderForegroundView.then {
                $0.backgroundColor = rangeSliderForegroundColor
                addSubview($0)
            }
            startSliderButton = getSegmentButton(withSegmentIndex: 1, isSlider: true)
            addSubview(startSliderButton)
            
            endSliderButton = getSegmentButton(withSegmentIndex: numberOfSegments, isSlider: true)
            addSubview(endSliderButton)
            
            // Pan gesture for identifying the sliding (More accurate than touchesMoved).
            addPanGestureRecognizer()
        }
        
        // Update the frames
        private func updateFrame() {
            if self.numberOfSegments >= 2 {
                // If the range selectors are at the extreme points, then reset the frame. Else fo nothing
                if isRangeSlidersPlacedAtExtremePosition() {
                    DEFAULT_SLIDER_FRAME = CGRect(x: SLIDER_BUTTON_WIDTH / 2, y: self.bounds.midY, width: self.bounds.width - SLIDER_BUTTON_WIDTH, height: 4)
                    self.sliderBackgroundView.frame = DEFAULT_SLIDER_FRAME
                    self.sliderForegroundView.frame = DEFAULT_SLIDER_FRAME
                    
                    self.sliderBackgroundView.backgroundColor = self.rangeSliderBackgroundColor
                    self.sliderForegroundView.backgroundColor = self.rangeSliderForegroundColor
                    
                    segmentWidth = getSegmentWidth(forSegmentCount: numberOfSegments)
                    
                    startSliderButton.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: sliderBackgroundView.frame.midY)
                    endSliderButton.center = getSegmentCenterPoint(forSegmentIndex: numberOfSegments)
                    
                    minRangeView.center = CGPoint(x: startSliderButton.frame.midX, y: bounds.midY - (SLIDER_BUTTON_WIDTH / 2) - (paddingRangeView / 2))
                    maxRangeView.center = CGPoint(x: endSliderButton.frame.midX, y: bounds.midY - (SLIDER_BUTTON_WIDTH / 2) - (paddingRangeView / 2))
                    
                    minLabel.center = CGPoint(x: startSliderButton.frame.midX, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2))
                    maxLabel.center = CGPoint(x: endSliderButton.frame.midX, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2))
                    
                    sliderMidView.center = CGPoint(x: startSliderButton.frame.midX, y: bounds.midY - (SLIDER_BUTTON_WIDTH / 2) - (paddingRangeView / 2))
                    
                    setImageForSegmentOrSliderButton(startSliderButton, isSlider: true)
                    setImageForSegmentOrSliderButton(endSliderButton, isSlider: true)
                    
                    // Reset the frame of all the intermediate buttons
                    for segmentIndex in 1...numberOfSegments {
                        let segmentButton = self.viewWithTag(segmentIndex) as? UIButton
                        segmentButton?.center = getSegmentCenterPoint(forSegmentIndex: segmentIndex)
                        if let button = segmentButton {
                            setImageForSegmentOrSliderButton(button, isSlider: false)
                        }
                    }
                    
                    // Slide the buttons if the initial position is needed
                    slideRangeSliderButtonsIfNeeded()
                    checkInit = true
                }
            }
        }
    }

    4. Tạo các hàm để tính toán vị trí và di chuyển slider

    extension TWSlider {
        private func isRangeSlidersPlacedAtExtremePosition() -> Bool {
            let sliderBackgroundViewMaxX = sliderBackgroundView.frame.maxX + (SLIDER_BUTTON_WIDTH / 2)
            return (startSliderButton.frame.minX == 0.0 && endSliderButton.frame.maxX == sliderBackgroundViewMaxX)
        }
        
        private func slideRangeSliderButtonsIfNeeded() {
            var startScrollPoint = CGPoint.zero
            var endScrollPoint = CGPoint(x: bounds.size.width, y: 0)
            if let min = minRangeInitialIndex, let max = maxRangeInitialIndex, min < max {
                let startX = getSegmentCenterPoint(forSegmentIndex: min).x
                startScrollPoint.x = startX > (SLIDER_BUTTON_WIDTH / 2) ? startX : SLIDER_BUTTON_WIDTH / 2
                let endX = getSegmentCenterPoint(forSegmentIndex: max).x
                endScrollPoint.x = endX > (SLIDER_BUTTON_WIDTH / 2) ? endX : SLIDER_BUTTON_WIDTH / 2
            }
            scrollStartAndEndSlider(for: startScrollPoint, andEndScroll: endScrollPoint)
            minRangeInitialIndex = 0
            maxRangeInitialIndex = 0
        }
        
        private func scrollStartAndEndSlider(for startScrollPoint: CGPoint, andEndScroll endScrollPoint: CGPoint) {
            startSliderButton.isSelected = true
            sliderDidSlide(for: startScrollPoint)
            startSliderButton.isSelected = false
            
            endSliderButton.isSelected = true
            sliderDidSlide(for: endScrollPoint)
            endSliderButton.isSelected = false
        }
    }

    5. Tạo các hàm tính toán frame của slider

    extension TWSlider {
        private func getSliderViewWidth() -> CGFloat {
            let startMinX = startSliderButton.frame.midX
            let endMinX = endSliderButton.frame.midX
            if startMinX > endMinX {
                return startMinX - endMinX
            } else {
                return endMinX - startMinX
            }
        }
        
        private func sliderMidPoint(forPoint point: CGFloat) -> CGFloat {
            let sliderMidPoint = point - (SLIDER_BUTTON_WIDTH / 2)
            return sliderMidPoint
        }
        
        private func getSegmentWidth(forSegmentCount segmentCount: Int) -> CGFloat {
            let segmentCount = CGFloat(segmentCount - 1)
            let sliderWidth = frame.width - SLIDER_BUTTON_WIDTH
            return sliderWidth / segmentCount
        }
        
        private func getSegmentButton(withSegmentIndex segmentIndex: Int, isSlider: Bool) -> UIButton {
            // Create rounded button for representing slider segments
            let segmentButton = UIButton(type: .custom)
            segmentButton.frame = CGRect(x: 0, y: 0, width: SLIDER_BUTTON_WIDTH, height: SLIDER_BUTTON_WIDTH)
            segmentButton.center = getSegmentCenterPoint(forSegmentIndex: segmentIndex)
            setImageForSegmentOrSliderButton(segmentButton, isSlider: isSlider)
            return segmentButton
        }
        
        // MARK: - Calculation for segment button frame
        private func getSegmentCenterPoint(forSegmentIndex segmentIndex: Int) -> CGPoint {
            let pointX = CGFloat((CGFloat(segmentIndex - 1) * segmentWidth) + (SLIDER_BUTTON_WIDTH / 2))
            return CGPoint(x: pointX, y: sliderBackgroundView.frame.midY)
        }
        
        private func setImageForSegmentOrSliderButton(_ button: UIButton, isSlider: Bool) {
            if let image = rangeSliderButtonImage, isSlider {
                button.setImage(image, for: .normal)
            } else if let iamge = segmentSelectedImage {
                button.setImage(iamge, for: .normal)
            } else {
                button.setImage(getImageWithSize(isSlider ? sliderSize : segmentSize, with: segmentSelectedColor), for: .normal)
            }
            
            button.imageView?.layer.masksToBounds = true
            let buttonWidth = button.imageView?.frame.size.width ?? button.frame.size.width
            button.imageView?.layer.cornerRadius = buttonWidth / 2
        }
        
        private func getImageWithSize(_ size: CGSize, with backgroundColor: UIColor?) -> UIImage? {
            let imageView = UIView(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height))
            imageView.backgroundColor = backgroundColor
            
            UIGraphicsBeginImageContextWithOptions(CGSize(width: size.width, height: size.height), false, 1.0)
            if let context = UIGraphicsGetCurrentContext() {
                imageView.layer.render(in: context)
            }
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            return image
        }
    }

    6. Tạo các hàm để tính toán vị trí và xử lí delegate của Pangesture

    extension TWSlider {
        
        private func addPanGestureRecognizer() {
            let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture))
            panGesture.maximumNumberOfTouches = 1
            addGestureRecognizer(panGesture)
        }
        
        // Pan Gesture selector method
        
        @objc
        func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
            let point = panGesture.location(in: self)
            
            if panGesture.state == .began {
                self.setSelectedStateForSlidingButton(point)
            } else if panGesture.state == .changed {
                sliderDidSlide(for: point)
            } else if panGesture.state == .ended || panGesture.state == .failed || panGesture.state == .cancelled {
                // Move the slider to nearest segment
                moveSliderToNearestSegment(withEnding: point)
                resetSelectedStateForSlidingButtons()
            }
        }
        
        // If sliding began, check if startSlider is moved or endSlider is moved
        private  func setSelectedStateForSlidingButton(_ point: CGPoint) {
            if startSliderButton.frame.contains(point) {
                startSliderButton.isSelected = true
                endSliderButton.isSelected = false
            } else if endSliderButton.frame.contains(point) {
                endSliderButton.isSelected = true
                startSliderButton.isSelected = false
            } else {
                startSliderButton.isSelected = false
                endSliderButton.isSelected = false
            }
        }
        
        private func sliderDidSlide(for point: CGPoint) {
            var newPoint = point
            // Check if startButton is moved or endButton is moved. Based on the moved button, set the frame of the slider button and foregroundSliderView
            newPoint = resetFrameOnBoundsCross(for: point)
            
            if startSliderButton.isSelected {
                if shouldStartButtonSlide(for: newPoint) {
                    UIView.animate(withDuration: 0.1, animations: {
                        // Change only the x value for startbutton
                        self.startSliderButton.frame = CGRect(x: self.sliderMidPoint(forPoint: newPoint.x), y: self.startSliderButton.frame.origin.y, width: self.startSliderButton.frame.size.width, height: self.startSliderButton.frame.size.height)
                        
                        // Change the x and width for slider foreground view
                        if self.shouldSliderButtonOverlap {
                            let startMidX = self.startSliderButton.frame.midX
                            let endMidX = self.endSliderButton.frame.midX
                            var originX = self.startSliderButton.frame.origin.x
                            if endMidX < startMidX {
                                originX = self.endSliderButton.frame.origin.x
                            }
                            self.sliderForegroundView.frame = CGRect(x: originX + self.SLIDER_BUTTON_WIDTH / 2, y: self.sliderForegroundView.frame.origin.y, width: self.getSliderViewWidth(), height: self.sliderForegroundView.frame.size.height)
                        } else {
                            let originX = self.startSliderButton.frame.origin.x + self.SLIDER_BUTTON_WIDTH / 2
                            self.sliderForegroundView.frame = CGRect(x: originX, y: self.sliderForegroundView.frame.origin.y, width: self.getSliderViewWidth(), height: self.sliderForegroundView.frame.size.height)
                        }
                        
                        // Change the x and width for slider foreground view
                        self.minRangeView.center = CGPoint(x: self.startSliderButton.frame.midX, y: self.startSliderButton.frame.minY - self.paddingRangeView)
                        
                        self.sliderMidView.center = CGPoint(x: self.endSliderButton.frame.midX, y: self.startSliderButton.frame.minY - self.paddingRangeView)
                        
                        // Update the intermediate segment colors
                        self.updateSegmentColor(for: newPoint)
                    }, completion: { _ in
                        self.callScrollDelegate(point: newPoint, isStartSliderButton: true)
                    })
                }
            } else if endSliderButton.isSelected {
                if shouldEndButtonSlide(for: newPoint) {
                    UIView.animate(withDuration: 0.1, animations: {
                        // Change only the x value for endbutton
                        self.endSliderButton.frame = CGRect(x: self.sliderMidPoint(forPoint: newPoint.x), y: self.endSliderButton.frame.origin.y, width: self.endSliderButton.frame.size.width, height: self.endSliderButton.frame.size.height)
                        
                        // Change the x and width for slider foreground view
                        if self.shouldSliderButtonOverlap {
                            let startMidX = self.startSliderButton.frame.midX
                            let endMidX = self.endSliderButton.frame.midX
                            var originX = self.startSliderButton.frame.origin.x
                            if endMidX < startMidX {
                                originX = self.endSliderButton.frame.origin.x
                            }
                            self.sliderForegroundView.frame = CGRect(x: originX + self.SLIDER_BUTTON_WIDTH / 2, y: self.sliderForegroundView.frame.origin.y, width: self.getSliderViewWidth(), height: self.sliderForegroundView.frame.size.height)
                        } else {
                            let originX = self.startSliderButton.frame.origin.x + self.SLIDER_BUTTON_WIDTH / 2
                            self.sliderForegroundView.frame = CGRect(x: originX, y: self.sliderForegroundView.frame.origin.y, width: self.getSliderViewWidth(), height: self.sliderForegroundView.frame.size.height)
                        }
                        
                        // Change the x and width for slider foreground view
                        self.maxRangeView.center = CGPoint(x: self.endSliderButton.frame.midX, y: self.endSliderButton.frame.minY - self.paddingRangeView)
                        
                        self.sliderMidView.center = CGPoint(x: self.startSliderButton.frame.midX, y: self.endSliderButton.frame.minY - self.paddingRangeView)
                        
                        // Update the intermediate segment colors
                        self.updateSegmentColor(for: newPoint)
                    }, completion: { _ in
                        self.callScrollDelegate(point: newPoint, isStartSliderButton: false)
                    })
                }
            }
        }
        
        // Method that handles if the sliders move out of range
        private func resetFrameOnBoundsCross(for point: CGPoint) -> CGPoint {
            var newPoint = point
            if shouldSliderButtonOverlap {
                if point.x < 0 {
                    newPoint.x = 0
                } else if sliderMidPoint(forPoint: point.x) >= sliderBackgroundView.bounds.maxX {
                    newPoint.x = sliderBackgroundView.bounds.maxX + SLIDER_BUTTON_WIDTH / 2
                }
            } else {
                if startSliderButton.isSelected {
                    if sliderMidPoint(forPoint: point.x) >= endSliderButton.frame.midX - segmentWidth {
                        newPoint.x = endSliderButton.frame.midX - segmentWidth
                    } else if point.x < 0 {
                        newPoint.x = 0
                    }
                } else if endSliderButton.isSelected {
                    if point.x <= startSliderButton.frame.midX + segmentWidth {
                        newPoint.x = startSliderButton.frame.midX + segmentWidth
                    } else if sliderMidPoint(forPoint: point.x) >= sliderBackgroundView.bounds.maxX {
                        newPoint.x = sliderBackgroundView.bounds.maxX + SLIDER_BUTTON_WIDTH / 2
                    }
                }
            }
            
            return newPoint
        }
        
        private func shouldStartButtonSlide(for point: CGPoint) -> Bool {
            if shouldSliderButtonOverlap {
                return (point.x >= (SLIDER_BUTTON_WIDTH / 2)) && (point.x <= (bounds.maxX - SLIDER_BUTTON_WIDTH / 2))
            } else {
                var endButtonMidPoint = endSliderButton.frame.midX
                endButtonMidPoint -= segmentWidth
                return round(point.x) <= round(endButtonMidPoint) && point.x >= SLIDER_BUTTON_WIDTH / 2
            }
        }
        
        private func shouldEndButtonSlide(for point: CGPoint) -> Bool {
            if shouldSliderButtonOverlap {
                return point.x >= SLIDER_BUTTON_WIDTH / 2
            } else {
                var startButtonMidPoint = startSliderButton.frame.midX
                startButtonMidPoint += shouldSliderButtonOverlap ? 0 : segmentWidth
                return round(point.x) >= round(startButtonMidPoint) && point.x <= sliderMidPoint(forPoint: frame.size.width)
            }
        }
        
        // Call the delegate to set the label for min range and max range
        private func callScrollDelegate(point: CGPoint, isStartSliderButton: Bool) {
            let nearestSegmentIndex = Int(round(sliderMidPoint(forPoint: point.x) / segmentWidth))
            
            var startIndex = Int(round(startSliderButton.frame.minX / segmentWidth))
            var endIndex = Int(round(endSliderButton.frame.minX / segmentWidth))
            
            if isStartSliderButton {
                startIndex = nearestSegmentIndex
            } else {
                endIndex = nearestSegmentIndex
            }
            updateData(startIndex: startIndex, endIndex: endIndex, endDragDrop: false)
        }
        
        private func updateData(startIndex: Int, endIndex: Int, endDragDrop: Bool) {
            if startIndex > endIndex {
                let min = endIndex < 0 ? 0 : endIndex
                let max = startIndex > numberOfSegments ? numberOfSegments : startIndex
                delegate?.sliderScrolled(self, toMinIndex: min, andMaxIndex: max, endDragDrop: endDragDrop)
                minRangeLabel.text = maxRangeText
                maxRangeLabel.text = minRangeText
            } else {
                let min = startIndex < 0 ? 0 : startIndex
                let max = endIndex > numberOfSegments ? numberOfSegments : endIndex
                delegate?.sliderScrolled(self, toMinIndex: min, andMaxIndex: max, endDragDrop: endDragDrop)
                minRangeLabel.text = minRangeText
                maxRangeLabel.text = maxRangeText
            }
            
            if startIndex != endIndex {
                UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) {
                    self.sliderMidView.alpha = 0
                    self.minRangeView.alpha = 1
                    self.maxRangeView.alpha = 1
                } completion: { _ in
                    print("completion \(startIndex) = \(endIndex)")
                }
            } else {
                sliderMidLabel.text = "\(minRangeText ?? "") - \(maxRangeText ?? "")"
                UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) {
                    self.sliderMidView.alpha = 1
                    self.minRangeView.alpha = 0
                    self.maxRangeView.alpha = 0
                } completion: { _ in
                    print("completion \(startIndex) = \(endIndex)")
                }
            }
        }
        
        private func updateSegmentColor(for point: CGPoint) {
            
            if shouldSliderButtonOverlap {
                let startMinX = startSliderButton.frame.midX
                let endMinX = endSliderButton.frame.midX
                
                var min: CGFloat = 0.0
                var max: CGFloat = 0.0
                if startMinX > endMinX {
                    min = (endSliderButton.frame.minX / segmentWidth).rounded(.up)
                    max = (startSliderButton.frame.minX / segmentWidth).rounded(.down)
                } else {
                    min = (startSliderButton.frame.minX / segmentWidth).rounded(.up)
                    max = (endSliderButton.frame.minX / segmentWidth).rounded(.down)
                }
                updateSegmentColor(withStart: Int(min) + 1, andEnd: Int(max) + 1)
            } else {
                if startSliderButton.isSelected {
                    let startMinX = (sliderMidPoint(forPoint: startSliderButton.frame.midX) / segmentWidth).rounded(.up)
                    let endMinX = (sliderMidPoint(forPoint: endSliderButton.frame.midX) / segmentWidth).rounded(.up)
                    updateSegmentColor(withStart: Int(startMinX) + 1, andEnd: Int(endMinX) + 1)
                } else if endSliderButton.isSelected {
                    let startMinX = (startSliderButton.frame.minX / segmentWidth).rounded(.up)
                    let endMinX = (endSliderButton.frame.minX / segmentWidth).rounded(.down)
                    updateSegmentColor(withStart: Int(startMinX) + 1, andEnd: Int(endMinX) + 1)
                }
            }
            let pointX: Int = Int(round(point.x - SLIDER_BUTTON_WIDTH / 2))
            if pointX % Int(round(segmentWidth)) == 0 {
                let generator = UIImpactFeedbackGenerator(style: .medium)
                generator.impactOccurred()
            }
        }
        
        private func updateSegmentColor(withStart startIndex: Int, andEnd endIndex: Int) {
            // Segments before startSegment slider
            
            if startIndex > 1 {
                for segmentIndex in 1..<startIndex {
                    if let segmentButton = viewWithTag(segmentIndex) as? UIButton {
                        if let image = segmentUnSelectedImage {
                            segmentButton.setImage(image, for: .normal)
                        } else {
                            let image = getImageWithSize(segmentSize, with: segmentUnSelectedColor)
                            segmentButton.setImage(image, for: .normal)
                        }
                    }
                }
            }
            
            // Segments between startSegment slider and endSegment slider
            if startIndex <= endIndex {
                for segmentIndex in startIndex...endIndex {
                    if let segmentButton = viewWithTag(segmentIndex) as? UIButton {
                        if let image = segmentSelectedImage {
                            segmentButton.setImage(image, for: .normal)
                        } else {
                            let image = getImageWithSize(segmentSize, with: segmentSelectedColor)
                            segmentButton.setImage(image, for: .normal)
                        }
                    }
                }
            }
            
            // Segments after endSegment slider
            if endIndex + 1 <= numberOfSegments {
                for segmentIndex in (endIndex + 1)...numberOfSegments {
                    if let segmentButton = viewWithTag(segmentIndex) as? UIButton {
                        if let image = segmentUnSelectedImage {
                            segmentButton.setImage(image, for: .normal)
                        } else {
                            let image = getImageWithSize(segmentSize, with: segmentUnSelectedColor)
                            segmentButton.setImage(image, for: .normal)
                        }
                    }
                }
            }
        }
        
        // Slide to nearest position
        private func moveSliderToNearestSegment(withEnding point: CGPoint) {
            var newPoint = point
            newPoint = resetFrameOnBoundsCross(for: point)
            
            let nearestSegmentIndex = Int(round(sliderMidPoint(forPoint: newPoint.x) / segmentWidth))
            sliderDidSlide(for: CGPoint(x: CGFloat(SLIDER_BUTTON_WIDTH / 2 + CGFloat(nearestSegmentIndex) * segmentWidth), y: newPoint.y))
            
            var startIndex = Int(round(startSliderButton.frame.minX / segmentWidth))
            var endIndex = Int(round(endSliderButton.frame.minX / segmentWidth))
            
            if startSliderButton.isSelected {
                startIndex = nearestSegmentIndex
            } else if endSliderButton.isSelected {
                endIndex = nearestSegmentIndex
            }
            updateData(startIndex: startIndex, endIndex: endIndex, endDragDrop: true)
        }
        
        // After ending, reset the selected state of both buttons
        private func resetSelectedStateForSlidingButtons() {
            startSliderButton.isSelected = false
            endSliderButton.isSelected = false
        }
    }

    5. Tạo các hàm public cho phép thay đổi giá trị của TWSlider

    extension TWSlider {
        
        // Scroll to desired location on loading
        func scrollStartSlider(to startIndex: Int, andEnd endIndex: Int) {
            minRangeInitialIndex = startIndex
            maxRangeInitialIndex = endIndex
            if checkInit {
                slideRangeSliderButtonsIfNeeded()
            }
        }
        
        // MARK: - Setter methods, setup value for slider
        func setNumberOfSegments(_ numberOfSegments: Int, minText: String?, maxText: String?) {
            self.numberOfSegments = numberOfSegments
            
            // After setting the numberOfSegments, set all the necessary views
            segmentWidth = getSegmentWidth(forSegmentCount: self.numberOfSegments)
            addSegmentButtons()
            addSubview(startSliderButton)
            addSubview(endSliderButton)
            minLabel.text = minText
            maxLabel.text = maxText
        }
        
        private func addSegmentButtons() {
            for segmentIndex in 1...numberOfSegments {
                let segmentButton = getSegmentButton(withSegmentIndex: segmentIndex, isSlider: false)
                segmentButton.then {
                    $0.tag = segmentIndex
                    $0.isUserInteractionEnabled = false
                    addSubview($0)
                }
            }
        }
    }

    3. Cách sử dụng

    Bước 1: mở file storyboard hoặc file .xib và thêm UIView và constraint, sửa lại tên class thành class custom slider mới tạo ở trên ở project này là TWSlider -> Kéo outlet

    Bước 2: mở viewcontroller vừa được kéo outlet viết hàm setup cho slider như sau

    class ViewController: UIViewController {
    
        @IBOutlet weak var customSlider: TWSlider!
        private let data: [String] = ["10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"]
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            customSlider.then {
                $0.shouldSliderButtonOverlap = true
                $0.delegate = self
            }
            customSlider.then {
                $0.setNumberOfSegments(data.count, minText: data.first, maxText: data.last)
                $0.scrollStartSlider(to: 0 + 1, andEnd: 10 + 1)
            }
        }
    }
    
    extension ViewController: TWSliderDelegate {
        func sliderScrolled(_ slider: TWSlider?, toMinIndex minIndex: Int, andMaxIndex maxIndex: Int, endDragDrop: Bool) {
            customSlider.minRangeText = data[minIndex]
            customSlider.maxRangeText = data[maxIndex]
        }
    }

    Bạn có thể tuỳ chỉnh số hiển thị trên slider bằng các số tương ứng với project.

    Mình để project ở đây cho mọi người tham khảo nếu cần nhé!

    Vậy là chúng ta đã hoàn thành việc custom Slider hai chiều, mình hi vọng nó có thể giải quyết được bài toán của các bạn. Cảm ơn các bạn đã theo dõi bài viết!