[Swift] Xây dựng app quản lý ATM sử dụng Coredata

Mục tiêu của bài viết này sẽ giúp các bạn: Hiểu thêm 1 cách sử dụng coredata sao cho code trông sáng sủa, dễ bảo trì và phát triển Thao tác với location Tận dụng được thao tác kéo thả, Custom controls khi dùng storyboard Thao tác với mappin, search khi sử dụng mapkit (hoặc google map)

Bắt đầu: Hãy cùng nhau đọc qua yêu cầu của bài toán sau:

Tôi có 1 người bạn, anh ta không bao giờ mang tiền mặt, bất cứ khi nào cần tiêu tiền anh sẽ tìm 1 máy ATM để rút tiền, và đương nhiên anh ấy rất giầu (quá dị nhưng giầu thì thích gì chẳng được) Và anh ấy cần 1 cách tốt, linh động để lưu thông tin máy ATM, thông tin bao gồm tên, địa chỉ, kinh độ và vĩ độ Với các máy ATM được lưu trữ của mình anh cũng muốn 1 cách nhanh chóng để xác định vị trí máy ATM xung quang khu vực của mình, có thể tìm kiếm với 1 cái tên trong vòng bán kính nhất định Người bạn này muốn 1 ứng dụng iOS, về cơ bản, có 2 tính năng chính: Thêm một máy ATM lưu trong coredata. thông tin ATM bao gồm: tên, địa chỉ, vĩ độ, kinh độ. Tìm kiếm các máy ATM từ local. Các kết quả sẽ được hiển thị như là 1 marker trên bản đồ.

Các bạn có hướng xử lý chưa? Có thể tham khảo hướng xử lý sau:

Chúng ta sẽ xây dựng 1 app có sử dụng mapkit của apple cho việc xử lý bản đồ Tạo 1 form để thêm các máy ATM lưu vào coredata Sử dụng UITable kết hợp với 1 UISearchView để tìm kiếm các máy atm, truy vấn dữ liệu trong coredata

Bước 1: Bàn về việc sử dụng coredata

Khi tạo app thì xcode có thêm 1 tuỳ chọn là bạn có sử dụng coredata hay không, và xcode sẽ thêm 1 số đoạn code tự động vào appdelegate, theo mình thì cách này cũng ok nhưng có đôi chút bối rốt khi quản lý, vậy mình đề xuất 1 cách khác đó là dành riêng 1 thư mục cho việc quản lý coredata, sử dụng pattern singleton cho việc quản lý các đối tượng. Dữ liệu coredata: như mô tả của yêu cầu thì chỉ cần tạo 1 bảng mang tên ATMEntity, và có các trường như hình bên dưới 2. Trên menubar của xcode chọn Editor -> Create NSManagedObject Subclass... Sau đó Xcode sẽ tự tạo cho các bạn coredata class và 1 extension coredata properties tương ứng

  1. Tiếp theo hãy tạo thêm 1 class DatabaseManager.swift, trong lớp này chứa NSManagedObjectContext như sau:
class DataBaseManager: NSObject {
    
    var coreDataContext: NSManagedObjectContext
    
    init(managedObjectContext context: NSManagedObjectContext) {
        self.coreDataContext = context
    }
    
    func saveData() throws -> Bool {
        if ((self.coreDataContext.hasChanges) == true) {
            do {
                try self.coreDataContext.save()
            }catch {
                self.coreDataContext.rollback()
                return false
            }
        }
        return true
    }

}

Phương thức saveData sử dụng ,chung cho tất cả các bảng mà bạn cần thay đổi

  1. Đế sử dụng core data cần 1 số thành phần quan trong nữa như: NSManagedObjectModel, NSPersistentStoreCoordinator vì vậy tạo thêm class CoreDataService bổ sung code bên dưới:
class CoreDataService: NSObject {
    
    lazy var managedObjectContext: NSManagedObjectContext = {
        var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator
        return managedObjectContext
    }()
    
    lazy var managedObjectModel: NSManagedObjectModel = {
        let url = Bundle.main.url(forResource: "ATMData", withExtension: "momd")! as NSURL
        var mom = NSManagedObjectModel(contentsOf: url as URL)
        return mom!
    }()

    
    lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
        var persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
        
        let storeUrl = Utilities.documentsDirectory() + "/ATMData.sql"
        do{
            try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: NSURL(fileURLWithPath: storeUrl) as URL, options: nil)
        }
        catch let error as NSError {
            print("error \(error)")
            abort()
        }
        
        return persistentStoreCoordinator
    }()
    
    static let sharedInstance = CoreDataService()
    
    override init(){
        super.init()
    }

}

Để ý các đổi tượng này chỉ cần khởi tạo 1 lần nên sẽ đi kèm với từ khoá lazy.

  1. Bước cuối cùng với thao tác core data: tạo 1 class manager cho ATM model:
private let ATMEntityName = "ATMEntity"

class ATMManager: DataBaseManager {
    
    static let sharedInstance = ATMManager()
    
    init() {
        super.init(managedObjectContext: CoreDataService().managedObjectContext)
    }
    
    
    func listOfATM() -> [ATMEntity] {
        let fetchRequest:NSFetchRequest<ATMEntity> = ATMEntity.fetchRequest()
        let entity = NSEntityDescription.entity(forEntityName: ATMEntityName, in: coreDataContext)
        fetchRequest.entity = entity
        let sortDescriptions = NSSortDescriptor(key: "name", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptions]
        var fetchObj: [ATMEntity] = []
        do {
            fetchObj = try coreDataContext.fetch(fetchRequest) as [ATMEntity]
            
        } catch _ {}
        return fetchObj
    }
    
    func newATM() -> ATMEntity {
        let atmEntity = NSEntityDescription.insertNewObject(forEntityName: ATMEntityName, into: coreDataContext) as! ATMEntity
        return atmEntity
    }
}

hiện tại mình đã thêm sẵn 2 phương thức dùng đúng với yêu cầu bài toán, là lấy danh sách toàn bộ ATM hiện có trong local và tạo mới 1 atm để insert vào core data.

Thế là đã xử lý xong những vấn đề của core data.

Bước 2: Tiếp theo UI: Trong MainController (cái đầu) Các bạn kéo 1 MKMapkitView và 1 tableView vào như hình, add constraint cho nó, đặt identifier cho (2) là "AddNewATMSegue" Trong AddNewATMController view controller mình có đặt 1 vài custom control như: RoundView, CustomTextField, các bạn nhớ sử dụng "@IBDesignable, @IBInspectable" để có thể dễ dàng sử dụng trên storyboard Nhớ kéo reference cho mapkitview và tableview

Bước 3: Setup search function, chúng ta sẽ sử dụng UISearchController, trong maincontroller
khai báo các biến cho chức năng tìm kiếm.

let searchController = UISearchController(searchResultsController: nil)

Bổ sung code trong viewDidLoad:

 // Setup the Search Controller
        searchController.searchResultsUpdater = self
        searchController.searchBar.delegate = self
        definesPresentationContext = true
        searchController.dimsBackgroundDuringPresentation = false
        tableView.tableHeaderView = searchController.searchBar

Khai báo 2 mảng cho dữ liệu atm, và dữ liệu sau khi filter:

//search
private var atmData: [ATMEntity] = ATMManager.sharedInstance.listOfATM()
private var filteredEntities = [ATMEntity]()

**Bước 4: ** Tạo pin để hiển thị trên bản đồ: Tạo ATMAnnotation để lưu trữ thông tin ATM trên map:

class ATMAnnotation: NSObject, MKAnnotation {
    
    var coordinate: CLLocationCoordinate2D
    var entity: ATMEntity
    
    init(entity: ATMEntity) {
        self.entity = entity
        coordinate = CLLocationCoordinate2D(latitude: entity.lat, longitude: entity.long)
    }
}

Trong phương thức delegate của MKMapView bổ sung đoạn code sau để cập nhật pin lên bản đồ:

 //MARK: MKMapViewDelegate
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        
        if annotation is MKUserLocation {
            return nil
        }
        var annotationView = self.mapView.dequeueReusableAnnotationView(withIdentifier: "ATMPin")
        if annotationView == nil{
            annotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: "ATMPin")
            annotationView?.canShowCallout = false
        }else{
            annotationView?.annotation = annotation
        }
        annotationView?.image = UIImage(named: "map-marker")
        return annotationView
    }

Với đoạn code này thì ta có thể thêm 1 pin custom vào map thông quá phương thức addAnnotations của mapView

**Bước 5: **

Trong AddNewATMController thực hiện add new ATM, thông qua phương thức sau:

let newEntity = ATMManager.sharedInstance.newATM()
newEntity.name = name.characters.count == 0 ? "Name Test \(arc4random()%200)" : name
newEntity.address = address.characters.count == 0 ? "Address Test \(arc4random()%200)" : address
newEntity.lat = lat
newEntity.long = long
do {
    let result = try ATMManager.sharedInstance.saveData()
    if result {
        print("OK ")
        mainVC?.addNew(atm: newEntity)
        close(nil)
    }
} catch _ {
    print("error")
}

**Bước 6: ** Hoàn thiện chức năng search: Trong MainController bổ sung phương thức:

  func filterContentForSearchText(_ searchText: String) {
        filteredEntities = atmData.filter({( entity : ATMEntity) -> Bool in
            return (entity.name?.lowercased().contains(searchText.lowercased()))!
        })
        tableView.reloadData()
    }

ở phương thức này mình đã filter theo tên, nếu cần theo bất cứ thông tin nào khác nữa thì chỉ việc chỉnh sửa hàm này

Đừng quên add thêm key yêu cầu Location In Use từ phía người dùng nhé: <key>NSLocationWhenInUseUsageDescription</key> <string>App use location to detect ATM near you</string>

Vậy là đã xong, mời các bạn xem thành quả:

Thách thức nằm ngoài code của bài viết này:

"Tìm kiếm trong các khoảng cách(1km, 2km, 3km, 4km, 5km, 7km, 10km, 15km, 20km) khi ở khoảng cách gần không có thì tự động tăng khoảng cách cho đến khi có 1 máy atm được tìm thấy, hoặc tăng lên bán kính tối đa 20Km, có 1 vòng tròn trên bản đồ tìm kiếm để hiển thị giới hạn tìm kiếm hiện thời"

Source code của lần này https://github.com/vanthanh88/ATMDiary

Hãy cùng trao đổi thêm để có thể giúp ích được mọi người trong những tình huống thực tế nhé, thanks.