数据持久化方案解析(七) —— 基于Realm的持久化存储(三)

版本记录

版本号 时间
V1.0 2018.12.21 星期五

前言

数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说plist文件(属性列表)、preference(偏好设置)、NSKeyedArchiver(归档)、SQLite 3CoreData,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
2. 数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)
3. 数据持久化方案解析(三) —— 基于NSCoding的持久化存储(一)
4. 数据持久化方案解析(四) —— 基于NSCoding的持久化存储(二)
5. 数据持久化方案解析(五) —— 基于Realm的持久化存储(一)
6. 数据持久化方案解析(六) —— 基于Realm的持久化存储(二)

源码

1. Swift

首先看一下工程结构

然后,我们看一下sb中的内容

下面就是源码了

1. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let rayGreen = UIColor(named: "RayGreen")
    
    UITextField.appearance().tintColor = rayGreen
    UITextView.appearance().tintColor = rayGreen
    
    return true
  }
}
2. LogCell.swift
import UIKit

class LogCell: UITableViewCell {
  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var subtitleLabel: UILabel!
  @IBOutlet weak var distanceLabel: UILabel!
  @IBOutlet weak var iconImageView: UIImageView!
}
3. Specimen.swift
import Foundation
import RealmSwift

class Specimen: Object {
  @objc dynamic var name = ""
  @objc dynamic var specimenDescription = ""
  @objc dynamic var latitude = 0.0
  @objc dynamic var longitude = 0.0
  @objc dynamic var created = Date()
  
  @objc dynamic var category: Category!
}
4. SpecimenAnnotation.swift
import UIKit
import MapKit

class SpecimenAnnotation: NSObject, MKAnnotation {
  var coordinate: CLLocationCoordinate2D
  var specimen: Specimen?
  var subtitle: String?
  var title: String?
  
  init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String, specimen: Specimen? = nil) {
    self.coordinate = coordinate
    self.title = title
    self.subtitle = subtitle
    self.specimen = specimen
  }
}
5. Category.swift
import Foundation
import RealmSwift

class Category: Object {
  @objc dynamic var name = ""
}
6. AddNewEntryViewController.swift
import RealmSwift
import UIKit

//
// MARK: - Add New Entry View Controller
//
class AddNewEntryViewController: UIViewController {
  @IBOutlet weak var categoryTextField: UITextField!
  @IBOutlet weak var descriptionTextField: UITextView!
  @IBOutlet weak var nameTextField: UITextField!
  
  //
  // MARK: - Variables And Properties
  //
  var selectedAnnotation: SpecimenAnnotation!
  var selectedCategory: Category!
  var specimen: Specimen!
  
  //
  // MARK: - IBActions
  //
  @IBAction func unwindFromCategories(segue: UIStoryboardSegue) {
    if segue.identifier == "CategorySelectedSegue" {
      let categoriesController = segue.source as! CategoriesTableViewController
      selectedCategory = categoriesController.selectedCategory
      categoryTextField.text = selectedCategory.name
    }
  }
  
  //
  // MARK: - Private Methods
  //
  func addNewSpecimen() {
    let realm = try! Realm() // 1
    
    try! realm.write { // 2
      let newSpecimen = Specimen() // 3
      
      newSpecimen.name = nameTextField.text! // 4
      newSpecimen.category = selectedCategory
      newSpecimen.specimenDescription = descriptionTextField.text
      newSpecimen.latitude = selectedAnnotation.coordinate.latitude
      newSpecimen.longitude = selectedAnnotation.coordinate.longitude
      
      realm.add(newSpecimen) // 5
      specimen = newSpecimen // 6
    }
  }
  
  func fillTextFields() {
    nameTextField.text = specimen.name
    categoryTextField.text = specimen.category.name
    descriptionTextField.text = specimen.specimenDescription
    
    selectedCategory = specimen.category
  }
  
  func updateSpecimen() {
    let realm = try! Realm()
    
    try! realm.write {
      specimen.name = nameTextField.text!
      specimen.category = selectedCategory
      specimen.specimenDescription = descriptionTextField.text
    }
  }
  
  func validateFields() -> Bool {
    if nameTextField.text!.isEmpty || descriptionTextField.text!.isEmpty || selectedCategory == nil {
      let alertController = UIAlertController(title: "Validation Error",
                                              message: "All fields must be filled",
                                              preferredStyle: .alert)
      
      let alertAction = UIAlertAction(title: "OK", style: .destructive) { alert in
        alertController.dismiss(animated: true, completion: nil)
      }
      
      alertController.addAction(alertAction)
      
      present(alertController, animated: true, completion: nil)
      
      return false
    } else {
      return true
    }
  }
  
  //
  // MARK: - View Controller
  //  
  override func shouldPerformSegue(withIdentifier identifier: String,
                                   sender: Any?) -> Bool {
    if validateFields() {
      if specimen != nil {
        updateSpecimen()
      } else {
        addNewSpecimen()
      }
      
      return true
    } else {
      return false
    }
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    if let specimen = specimen {
      title = "Edit \(specimen.name)"
      
      fillTextFields()
    } else {
      title = "Add New Specimen"
    }
  }
}

//
// MARK: - Text Field Delegate
//
extension AddNewEntryViewController: UITextFieldDelegate {
  func textFieldDidBeginEditing(_ textField: UITextField) {
    performSegue(withIdentifier: "Categories", sender: self)
  }
}
7. CategoriesTableViewController.swift
import RealmSwift
import UIKit

//
// MARK: - Categories Table View Controller
//
class CategoriesTableViewController: UITableViewController {
  //
  // MARK: - Variables And Properties
  //
  let realm = try! Realm()
  lazy var categories: Results = { self.realm.objects(Category.self) }()
  
  var selectedCategory: Category!
  
  //
  // MARK: - Private Methods
  //
  private func populateDefaultCategories() {
    if categories.count == 0 { // 1
      try! realm.write() { // 2
        let defaultCategories = ["Birds", "Mammals", "Flora", "Reptiles", "Arachnids" ] // 3
        
        for category in defaultCategories { // 4
          let newCategory = Category()
          newCategory.name = category
          
          realm.add(newCategory)
        }
      }
      
      categories = realm.objects(Category.self) // 5
    }
  }
  
  //
  // MARK: - View Controller
  //
  override func viewDidLoad() {
    super.viewDidLoad()
    
    populateDefaultCategories()
  }
}

//
// MARK: - Table View Data Source
//
extension CategoriesTableViewController {
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CategoryCell", for: indexPath)
    
    let category = categories[indexPath.row]
    cell.textLabel?.text = category.name
    
    return cell
  }
  
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return categories.count
  }
  
  override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    selectedCategory = categories[indexPath.row]
    
    return indexPath
  }
}
8. LogViewController.swift
import MapKit
import RealmSwift
import UIKit

//
// MARK: - Log View Controller
//
class LogViewController: UITableViewController {
  //
  // MARK: - IBOutlets
  //
  @IBOutlet weak var segmentedControl: UISegmentedControl!
  
  //
  // MARK: - Variables And Properties
  //
  var searchResults = try! Realm().objects(Specimen.self)
  var searchController: UISearchController!
  var specimens = try! Realm().objects(Specimen.self).sorted(byKeyPath: "name", ascending: true)
  
  //
  // MARK: - IBActions
  //
  @IBAction func scopeChanged(sender: Any) {
    let scopeBar = sender as! UISegmentedControl
    let realm = try! Realm()
    
    switch scopeBar.selectedSegmentIndex {
    case 1:
      specimens = realm.objects(Specimen.self).sorted(byKeyPath: "created", ascending: true)
    default:
      specimens = realm.objects(Specimen.self).sorted(byKeyPath: "name", ascending: true)
    }
    
    tableView.reloadData()
  }
  
  //
  // MARK: - Private Methods
  //
  func filterResultsWithSearchString(searchString: String) {
    let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString) // 1
    let scopeIndex = searchController.searchBar.selectedScopeButtonIndex // 2
    let realm = try! Realm()
    
    switch scopeIndex {
    case 0:
      searchResults = realm.objects(Specimen.self).filter(predicate).sorted(byKeyPath: "name", ascending: true) // 3
    case 1:
      searchResults = realm.objects(Specimen.self).filter(predicate).sorted(byKeyPath: "created", ascending: true) // 4
    default:
      searchResults = realm.objects(Specimen.self).filter(predicate) // 5
    }
  }
  
  //
  // MARK: - View Controller
  //
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if (segue.identifier == "Edit") {
      let controller = segue.destination as! AddNewEntryViewController
      var selectedSpecimen: Specimen!
      let indexPath = tableView.indexPathForSelectedRow
      
      if searchController.isActive {
        let searchResultsController =
          searchController.searchResultsController as! UITableViewController
        let indexPathSearch = searchResultsController.tableView.indexPathForSelectedRow
        
        selectedSpecimen = searchResults[indexPathSearch!.row]
      } else {
        selectedSpecimen = specimens[indexPath!.row]
      }
      
      controller.specimen = selectedSpecimen
    }
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    let searchResultsController = UITableViewController(style: .plain)
    searchResultsController.tableView.delegate = self
    searchResultsController.tableView.dataSource = self
    searchResultsController.tableView.rowHeight = 63
    searchResultsController.tableView.register(LogCell.self, forCellReuseIdentifier: "LogCell")
    
    searchController = UISearchController(searchResultsController: searchResultsController)
    searchController.searchResultsUpdater = self
    searchController.searchBar.sizeToFit()
    searchController.searchBar.tintColor = .white
    searchController.searchBar.delegate = self
    searchController.searchBar.barTintColor = UIColor(named: "RayGreen")
    
    tableView.tableHeaderView?.addSubview(searchController.searchBar)
    
    definesPresentationContext = true
  }
}

//
// MARK: - Search Bar Delegate
//
extension LogViewController:  UISearchBarDelegate {
}

//
// MARK: - Search Results Updatings
//
extension LogViewController: UISearchResultsUpdating {
  func updateSearchResults(for searchController: UISearchController) {
    let searchString = searchController.searchBar.text!
    filterResultsWithSearchString(searchString: searchString)
    
    let searchResultsController = searchController.searchResultsController as! UITableViewController
    searchResultsController.tableView.reloadData()
  }
}

//
// MARK: - Table View Data Source
extension LogViewController {
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = self.tableView.dequeueReusableCell(withIdentifier: "LogCell") as! LogCell
    
    let specimen = searchController.isActive ? searchResults[indexPath.row] : specimens[indexPath.row]
    
    cell.titleLabel.text = specimen.name
    cell.subtitleLabel.text = specimen.category.name
    
    switch specimen.category.name {
    case "Uncategorized":
      cell.iconImageView.image = UIImage(named: "IconUncategorized")
    case "Reptiles":
      cell.iconImageView.image = UIImage(named: "IconReptile")
    case "Flora":
      cell.iconImageView.image = UIImage(named: "IconFlora")
    case "Birds":
      cell.iconImageView.image = UIImage(named: "IconBird")
    case "Arachnid":
      cell.iconImageView.image = UIImage(named: "IconArachnid")
    case "Mammals":
      cell.iconImageView.image = UIImage(named: "IconMammal")
    default:
      cell.iconImageView.image = UIImage(named: "IconUncategorized")
    }
    
    return cell
  }
  
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return searchController.isActive ? searchResults.count : specimens.count
  }
}
9. MapViewController.swift
import CoreLocation
import MapKit
import RealmSwift
import UIKit

//
// MARK: - Map View Controller
//
class MapViewController: UIViewController {
  //
  // MARK: - IBOutlets
  //
  @IBOutlet weak var mapView: MKMapView!
  
  //
  // MARK: - Constants
  //
  let kDistanceMeters: CLLocationDistance = 500
  
  //
  // MARK: - Variables And Properties
  //
  var lastAnnotation: MKAnnotation!
  var locationManager = CLLocationManager()
  var specimens = try! Realm().objects(Specimen.self)
  var userLocated = false
  
  //
  // MARK: - IBActions
  //
  @IBAction func addNewEntryTapped() {
    addNewPin()
  }
  
  @IBAction func centerToUserLocationTapped() {
    centerToUsersLocation()
  }
  
  @IBAction func unwindFromAddNewEntry(segue: UIStoryboardSegue) {
    let addNewEntryController = segue.source as! AddNewEntryViewController
    let addedSpecimen = addNewEntryController.specimen!
    let addedSpecimenCoordinate = CLLocationCoordinate2D(latitude: addedSpecimen.latitude, longitude: addedSpecimen.longitude)
    
    if let lastAnnotation = lastAnnotation {
      mapView.removeAnnotation(lastAnnotation)
    } else {
      for annotation in mapView.annotations {
        if let currentAnnotation = annotation as? SpecimenAnnotation {
          if currentAnnotation.coordinate.latitude == addedSpecimenCoordinate.latitude && currentAnnotation.coordinate.longitude == addedSpecimenCoordinate.longitude {
            mapView.removeAnnotation(currentAnnotation)
            
            break
          }
        }
      }
    }
    
    let annotation = SpecimenAnnotation(coordinate: addedSpecimenCoordinate, title: addedSpecimen.name, subtitle: addedSpecimen.category.name, specimen: addedSpecimen)
    
    mapView.addAnnotation(annotation)
    lastAnnotation = nil;
  }
  
  //
  // MARK: - Private Methods
  //
  func addNewPin() {
    if lastAnnotation != nil {
      let alertController = UIAlertController(title: "Annotation already dropped",
                                              message: "There is an annotation on screen. Try dragging it if you want to change its location!",
                                              preferredStyle: .alert)
      
      let alertAction = UIAlertAction(title: "OK", style: .destructive) { alert in
        alertController.dismiss(animated: true, completion: nil)
      }
      
      alertController.addAction(alertAction)
      
      present(alertController, animated: true, completion: nil)
      
    } else {
      let specimen = SpecimenAnnotation(coordinate: mapView.centerCoordinate, title: "Empty", subtitle: "Uncategorized")
      
      mapView.addAnnotation(specimen)
      lastAnnotation = specimen
    }
  }
  
  func centerToUsersLocation() {
    let center = mapView.userLocation.coordinate
    let zoomRegion: MKCoordinateRegion = MKCoordinateRegion(center: center, latitudinalMeters: kDistanceMeters, longitudinalMeters: kDistanceMeters)
    
    mapView.setRegion(zoomRegion, animated: true)
  }
  
  func populateMap() {
    mapView.removeAnnotations(mapView.annotations) // 1
    
    specimens = try! Realm().objects(Specimen.self) // 2
    
    // Create annotations for each one
    for specimen in specimens { // 3
      let coord = CLLocationCoordinate2D(latitude: specimen.latitude, longitude: specimen.longitude);
      let specimenAnnotation = SpecimenAnnotation(coordinate: coord,
                                                  title: specimen.name,
                                                  subtitle: specimen.category.name,
                                                  specimen: specimen)
      mapView.addAnnotation(specimenAnnotation) // 4
    }
  }
  
  //
  // MARK: - View Controller
  //
  override func viewDidLoad() {
    super.viewDidLoad()
    
    print(Realm.Configuration.defaultConfiguration.fileURL!)
    
    title = "Map"
    
    locationManager.delegate = self
    
    if CLLocationManager.authorizationStatus() == .notDetermined {
      locationManager.requestWhenInUseAuthorization()
    } else {
      locationManager.startUpdatingLocation()
    }
    
    populateMap()
  }
  
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if (segue.identifier == "NewEntry") {
      let controller = segue.destination as! AddNewEntryViewController
      let specimenAnnotation = sender as! SpecimenAnnotation
      controller.selectedAnnotation = specimenAnnotation
    }
  }
}

//MARK: - LocationManager Delegate
extension MapViewController: CLLocationManagerDelegate {
  func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    status != .notDetermined ? mapView.showsUserLocation = true : print("Authorization to use location data denied")
  }
}

//MARK: - Map View Delegate
extension MapViewController: MKMapViewDelegate {
  func mapView(_ mapView: MKMapView, annotationView: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
    if let specimenAnnotation =  annotationView.annotation as? SpecimenAnnotation {
      performSegue(withIdentifier: "NewEntry", sender: specimenAnnotation)
    }
  }
  
  func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView,
               didChange newState: MKAnnotationView.DragState, fromOldState oldState: MKAnnotationView.DragState) {
    
    if newState == .ending {
      view.dragState = .none
    }
  }
  
  func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
    for annotationView in views {
      if (annotationView.annotation is SpecimenAnnotation) {
        annotationView.transform = CGAffineTransform(translationX: 0, y: -500)
        UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveLinear, animations: {
          annotationView.transform = CGAffineTransform(translationX: 0, y: 0)
        }, completion: nil)
      }
    }
  }
  
  func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    guard let subtitle = annotation.subtitle! else {
      return nil
    }
    
    if (annotation is SpecimenAnnotation) {
      if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: subtitle) {
        return annotationView
      } else {
        let currentAnnotation = annotation as! SpecimenAnnotation
        let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: subtitle)
        
        switch subtitle {
        case "Uncategorized":
          annotationView.image = UIImage(named: "IconUncategorized")
        case "Arachnids":
          annotationView.image = UIImage(named: "IconArachnid")
        case "Birds":
          annotationView.image = UIImage(named: "IconBird")
        case "Mammals":
          annotationView.image = UIImage(named: "IconMammal")
        case "Flora":
          annotationView.image = UIImage(named: "IconFlora")
        case "Reptiles":
          annotationView.image = UIImage(named: "IconReptile")
        default:
          annotationView.image = UIImage(named: "IconUncategorized")
        }
        
        annotationView.isEnabled = true
        annotationView.canShowCallout = true
        
        let detailDisclosure = UIButton(type: .detailDisclosure)
        annotationView.rightCalloutAccessoryView = detailDisclosure
        
        if currentAnnotation.title == "Empty" {
          annotationView.isDraggable = true
        }
        
        return annotationView
      }
    }
    
    return nil
  }
}

后记

本篇主要讲述了基于Realm的持久化存储,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(数据持久化方案解析(七) —— 基于Realm的持久化存储(三))