Skip to content

Commit 1764d98

Browse files
Merge pull request #13 from regulaforensics/person-search
Person search
2 parents 22c3c3f + 0331522 commit 1764d98

28 files changed

Lines changed: 2431 additions & 9 deletions

Catalog.xcodeproj/project.pbxproj

Lines changed: 88 additions & 0 deletions
Large diffs are not rendered by default.

Catalog/Core/CatalogTableDataProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class CatalogTableDataProvider {
2828
MatchFacesRequestItem(),
2929
DetectFacesItem(),
3030
ImageQualityItem(),
31-
31+
PersonDatabaseItem(),
3232
LivenessAttemptsCountItem(),
3333
FaceCaptureCameraPositionItem(),
3434
FaceCaptureHideTorchConfigurationItem(),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// Person+Metadata.swift
3+
// Catalog
4+
//
5+
// Created by Serge Rylko on 27.06.23.
6+
// Copyright © 2023 Regula. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import FaceSDK
11+
12+
extension PersonDatabase.Person {
13+
14+
var surname: String? {
15+
get {
16+
metadata["surname"] as? String
17+
}
18+
set {
19+
metadata["surname"] = newValue
20+
}
21+
}
22+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// UIImageView+Load.swift
3+
// Catalog
4+
//
5+
// Created by Serge Rylko on 15.05.23.
6+
// Copyright © 2023 Regula. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import UIKit
11+
12+
extension UIImageView {
13+
14+
func load(url: URL) {
15+
let request = URLRequest(url: url)
16+
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
17+
if let data = data, let image = UIImage(data: data) {
18+
DispatchQueue.main.async {
19+
self?.image = image
20+
}
21+
}
22+
}.resume()
23+
}
24+
}
25+

Catalog/Core/ImagePicker.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// ImagePicker.swift
3+
// Catalog
4+
//
5+
// Created by Serge Rylko on 26.08.22.
6+
// Copyright © 2022 Regula. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import UIKit
11+
12+
protocol ImagePickerDelegate: AnyObject {
13+
func didPickImage(delegate: ImagePicker, image: UIImage)
14+
}
15+
16+
final class ImagePicker: NSObject, UINavigationControllerDelegate {
17+
18+
private let pickerController = UIImagePickerController()
19+
private unowned let presenter: UIViewController
20+
private unowned let delegate: ImagePickerDelegate
21+
22+
init(presenter: UIViewController, delegate: ImagePickerDelegate) {
23+
self.presenter = presenter
24+
self.delegate = delegate
25+
super.init()
26+
setup()
27+
}
28+
29+
private func setup() {
30+
pickerController.delegate = self
31+
pickerController.allowsEditing = true
32+
}
33+
34+
private func action(for type: UIImagePickerController.SourceType, title: String) -> UIAlertAction? {
35+
guard UIImagePickerController.isSourceTypeAvailable(type) else {
36+
return nil
37+
}
38+
39+
return UIAlertAction(title: title, style: .default) { [unowned self] _ in
40+
self.pickerController.sourceType = type
41+
self.presenter.present(self.pickerController, animated: true)
42+
}
43+
}
44+
45+
func presentDefaultActions(from sourceView: UIView) {
46+
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
47+
48+
if let action = self.action(for: .camera, title: "Take photo") {
49+
alertController.addAction(action)
50+
}
51+
if let action = self.action(for: .savedPhotosAlbum, title: "Camera roll") {
52+
alertController.addAction(action)
53+
}
54+
if let action = self.action(for: .photoLibrary, title: "Photo library") {
55+
alertController.addAction(action)
56+
}
57+
58+
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
59+
60+
if UIDevice.current.userInterfaceIdiom == .pad {
61+
alertController.popoverPresentationController?.sourceView = sourceView
62+
alertController.popoverPresentationController?.sourceRect = sourceView.bounds
63+
alertController.popoverPresentationController?.permittedArrowDirections = [.down, .up]
64+
}
65+
66+
presenter.present(alertController, animated: true)
67+
}
68+
}
69+
70+
extension ImagePicker: UIImagePickerControllerDelegate {
71+
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
72+
guard let image = info[.editedImage] as? UIImage else { return }
73+
74+
delegate.didPickImage(delegate: self, image: image)
75+
presenter.dismiss(animated: true)
76+
}
77+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
//
2+
// DatabaseCreatePersonViewController.swift
3+
// Catalog
4+
//
5+
// Created by Serge Rylko on 21.06.23.
6+
// Copyright © 2023 Regula. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import FaceSDK
11+
12+
class DatabaseCreatePersonViewController: UIViewController {
13+
14+
@IBOutlet private weak var addImageButton: UIButton!
15+
@IBOutlet private weak var createPersonButton: UIButton!
16+
@IBOutlet private weak var nameTextField: UITextField!
17+
@IBOutlet private weak var surnameTextField: UITextField!
18+
@IBOutlet private weak var loadingIndicator: UIActivityIndicatorView!
19+
@IBOutlet private weak var collectionView: UICollectionView!
20+
21+
private lazy var imagePicker: ImagePicker = ImagePicker(presenter: self, delegate: self)
22+
private let database: PersonDatabase = FaceSDK.service.personDatabase
23+
24+
private var images: [UIImage] = []
25+
private var name: String?
26+
private var surname: String?
27+
private let group: PersonDatabase.PersonGroup
28+
29+
init(group: PersonDatabase.PersonGroup) {
30+
self.group = group
31+
super.init(nibName: nil, bundle: nil)
32+
}
33+
34+
required init?(coder: NSCoder) {
35+
fatalError("init(coder:) has not been implemented")
36+
}
37+
38+
override func viewDidLoad() {
39+
super.viewDidLoad()
40+
title = "Create Person in group"
41+
42+
let button = UIBarButtonItem(title: "Clear", style: .plain, target: self, action: #selector(didTapClearButton))
43+
navigationItem.setRightBarButton(button, animated: false)
44+
45+
collectionView.register(PersonImageCell.self, forCellWithReuseIdentifier: PersonImageCell.reuseID)
46+
}
47+
48+
//MARK: - Database
49+
private func createPerson() {
50+
showLoadingActivity()
51+
createPerson { [weak self] result in
52+
self?.hideLoadingActivity()
53+
switch result {
54+
case .success(let person):
55+
self?.handleSuccess(person: person)
56+
case .failure(let error):
57+
self?.handleFailure(error: error)
58+
}
59+
}
60+
}
61+
62+
private func createPerson(completion: @escaping ((Result<PersonDatabase.Person, Error>) -> Void)) {
63+
guard let name = name, name.count > 0 else {
64+
completion(.failure(CreatePersonError.requiredNameParameterMissed))
65+
return
66+
}
67+
let metadata = ["surname": surname ?? ""]
68+
69+
database.createPerson(name: name, metadata: metadata, groupIds: [group.itemId]) { [weak self] response in
70+
guard let self = self else { return }
71+
if let person = response.item {
72+
var error: Error?
73+
let group = DispatchGroup()
74+
for image in self.images {
75+
guard let imageData = image.pngData() else { continue }
76+
let upload = PersonDatabase.ImageUpload(imageData:imageData)
77+
group.enter()
78+
self.database.addPersonImage(personId: person.itemId, imageUpload: upload) { response in
79+
error = response.error
80+
group.leave()
81+
}
82+
}
83+
group.notify(queue: .main) {
84+
if let error = error {
85+
completion(.failure(error))
86+
} else {
87+
completion(.success(person))
88+
}
89+
}
90+
} else if let error = response.error {
91+
completion(.failure(error))
92+
}
93+
}
94+
}
95+
96+
//MARK: Actions
97+
@IBAction private func didTapAddPersonImage(_ sender: Any) {
98+
imagePicker.presentDefaultActions(from: view)
99+
}
100+
101+
@IBAction private func didTapCreatePerson(_ sender: Any) {
102+
createPerson()
103+
}
104+
105+
@objc private func didTapClearButton(_ sender: Any) {
106+
resetPersonData()
107+
}
108+
109+
private func resetPersonData() {
110+
name = nil
111+
nameTextField.text = nil
112+
surname = nil
113+
surnameTextField.text = nil
114+
images = []
115+
collectionView.reloadData()
116+
}
117+
118+
//MARK: - Dialogs
119+
private func handleSuccess(person: PersonDatabase.Person) {
120+
let message = "id: \(person.itemId) \nname: \(person.name) \nsurname: \(person.surname ?? "")"
121+
let action = UIAlertAction(title: "OK", style: .cancel)
122+
let alert = UIAlertController(title: "Person created.", message: message, preferredStyle: .alert)
123+
alert.addAction(action)
124+
present(alert, animated: true)
125+
}
126+
127+
private func handleFailure(error: Error) {
128+
let message = "Error: \(error.localizedDescription)"
129+
let action = UIAlertAction(title: "OK", style: .cancel)
130+
let alert = UIAlertController(title: "Failed to add Person.", message: message, preferredStyle: .alert)
131+
alert.addAction(action)
132+
present(alert, animated: true)
133+
}
134+
135+
//MARK: - Loading states
136+
private func showLoadingActivity() {
137+
addImageButton.isEnabled = false
138+
createPersonButton.isEnabled = false
139+
loadingIndicator.startAnimating()
140+
}
141+
142+
private func hideLoadingActivity() {
143+
addImageButton.isEnabled = true
144+
createPersonButton.isEnabled = true
145+
loadingIndicator.stopAnimating()
146+
}
147+
}
148+
149+
extension DatabaseCreatePersonViewController: UITextFieldDelegate {
150+
151+
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
152+
textField.resignFirstResponder()
153+
}
154+
155+
func textFieldDidEndEditing(_ textField: UITextField) {
156+
switch textField {
157+
case nameTextField:
158+
name = textField.text
159+
case surnameTextField:
160+
surname = textField.text
161+
default: break
162+
}
163+
}
164+
}
165+
166+
extension DatabaseCreatePersonViewController: UICollectionViewDelegateFlowLayout {
167+
168+
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
169+
let divider = UIDevice.current.orientation.isPortrait ? 2.0 : 3.0
170+
let side = collectionView.bounds.width / divider
171+
let size = CGSize(width: side, height: side)
172+
return size
173+
}
174+
}
175+
176+
extension DatabaseCreatePersonViewController: UICollectionViewDataSource {
177+
178+
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
179+
images.count
180+
}
181+
182+
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
183+
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PersonImageCell.reuseID, for: indexPath) as? PersonImageCell else {
184+
fatalError("Unable to reuse cell")
185+
}
186+
187+
cell.imageView.image = images[indexPath.row]
188+
return cell
189+
}
190+
}
191+
192+
extension DatabaseCreatePersonViewController: ImagePickerDelegate {
193+
194+
func didPickImage(delegate: ImagePicker, image: UIImage) {
195+
images.append(image)
196+
collectionView.reloadData()
197+
}
198+
}
199+
200+
private enum CreatePersonError: LocalizedError {
201+
case requiredNameParameterMissed
202+
203+
var errorDescription: String? {
204+
"Person name is required"
205+
}
206+
}

0 commit comments

Comments
 (0)