Skip to content

Commit 479a182

Browse files
authored
Merge pull request #86 from hyperoslo/improve/message-view-controller
Refactoring: introduce message view controller
2 parents 35e33b7 + b59c0af commit 479a182

File tree

8 files changed

+263
-21
lines changed

8 files changed

+263
-21
lines changed

BarcodeScanner.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
D50BE3EB1C9FE7A80000A34C /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = D50BE3E71C9FE7A80000A34C /* [email protected] */; };
1515
D55281B62016758F00FF3CDD /* HeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281B52016758F00FF3CDD /* HeaderViewController.swift */; };
1616
D55281B8201675D500FF3CDD /* MessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281B7201675D500FF3CDD /* MessageViewController.swift */; };
17-
D55281BA2016770800FF3CDD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281B92016770800FF3CDD /* SettingsViewController.swift */; };
17+
D55281BA2016770800FF3CDD /* VideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281B92016770800FF3CDD /* VideoViewController.swift */; };
1818
D55281BC2016782C00FF3CDD /* ScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281BB2016782C00FF3CDD /* ScannerViewController.swift */; };
1919
D55281BF20167DB400FF3CDD /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281BE20167DB400FF3CDD /* UIViewController+Extensions.swift */; };
20+
D55281C720168E7000FF3CDD /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281C620168E7000FF3CDD /* NSLayoutConstraint+Extensions.swift */; };
2021
D5C4E08E1CA0BFB9008D9269 /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C4E08C1CA0BFB9008D9269 /* InfoView.swift */; };
2122
D5C4E08F1CA0BFB9008D9269 /* TorchMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C4E08D1CA0BFB9008D9269 /* TorchMode.swift */; };
2223
D5F1C1C91C9C5113001E17A6 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F1C1C51C9C5113001E17A6 /* Config.swift */; };
@@ -33,9 +34,10 @@
3334
D50BE3E71C9FE7A80000A34C /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
3435
D55281B52016758F00FF3CDD /* HeaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderViewController.swift; sourceTree = "<group>"; };
3536
D55281B7201675D500FF3CDD /* MessageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewController.swift; sourceTree = "<group>"; };
36-
D55281B92016770800FF3CDD /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
37+
D55281B92016770800FF3CDD /* VideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoViewController.swift; sourceTree = "<group>"; };
3738
D55281BB2016782C00FF3CDD /* ScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannerViewController.swift; sourceTree = "<group>"; };
3839
D55281BE20167DB400FF3CDD /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = "<group>"; };
40+
D55281C620168E7000FF3CDD /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extensions.swift"; sourceTree = "<group>"; };
3941
D5B2E89F1C3A780C00C0327D /* BarcodeScanner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BarcodeScanner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4042
D5C4E08C1CA0BFB9008D9269 /* InfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = "<group>"; tabWidth = 2; };
4143
D5C4E08D1CA0BFB9008D9269 /* TorchMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = TorchMode.swift; sourceTree = "<group>"; tabWidth = 2; };
@@ -73,7 +75,7 @@
7375
D5F1C1C61C9C5113001E17A6 /* BarcodeScannerController.swift */,
7476
D55281B52016758F00FF3CDD /* HeaderViewController.swift */,
7577
D55281B7201675D500FF3CDD /* MessageViewController.swift */,
76-
D55281B92016770800FF3CDD /* SettingsViewController.swift */,
78+
D55281B92016770800FF3CDD /* VideoViewController.swift */,
7779
D55281BB2016782C00FF3CDD /* ScannerViewController.swift */,
7880
);
7981
path = Controllers;
@@ -82,6 +84,7 @@
8284
D55281BD20167D9F00FF3CDD /* Extensions */ = {
8385
isa = PBXGroup;
8486
children = (
87+
D55281C620168E7000FF3CDD /* NSLayoutConstraint+Extensions.swift */,
8588
D504555E1FD8714700E46826 /* UIView+Extensions.swift */,
8689
D55281BE20167DB400FF3CDD /* UIViewController+Extensions.swift */,
8790
);
@@ -211,9 +214,10 @@
211214
buildActionMask = 2147483647;
212215
files = (
213216
D5C4E08F1CA0BFB9008D9269 /* TorchMode.swift in Sources */,
217+
D55281C720168E7000FF3CDD /* NSLayoutConstraint+Extensions.swift in Sources */,
214218
D5C4E08E1CA0BFB9008D9269 /* InfoView.swift in Sources */,
215219
D55281BF20167DB400FF3CDD /* UIViewController+Extensions.swift in Sources */,
216-
D55281BA2016770800FF3CDD /* SettingsViewController.swift in Sources */,
220+
D55281BA2016770800FF3CDD /* VideoViewController.swift in Sources */,
217221
D5F1C1D31C9C5809001E17A6 /* HeaderView.swift in Sources */,
218222
D5F1C1C91C9C5113001E17A6 /* Config.swift in Sources */,
219223
D55281B8201675D500FF3CDD /* MessageViewController.swift in Sources */,
Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,212 @@
11
import UIKit
22

3-
final class MessageViewController: UIViewController {
3+
public enum MessageStyle {
4+
case initial
5+
case loading
6+
case error
7+
}
8+
9+
public final class MessageViewController: UIViewController {
10+
// Blur effect view.
11+
private lazy var blurView: UIVisualEffectView = .init(effect: UIBlurEffect(style: .extraLight))
12+
/// Text label.
13+
public private(set) lazy var textLabel: UILabel = .init()
14+
/// Info image view.
15+
public private(set) lazy var imageView: UIImageView = .init()
16+
/// Border view.
17+
public private(set) lazy var borderView: UIView = .init()
18+
19+
private lazy var collapsedConstraints: [NSLayoutConstraint] = self.makeCollapsedConstraints()
20+
private lazy var expandedConstraints: [NSLayoutConstraint] = self.makeExpandedConstraints()
21+
22+
var state: State = .scanning {
23+
didSet {
24+
handleStateUpdate()
25+
}
26+
}
27+
28+
// MARK: - View lifecycle
29+
30+
public override func viewDidLoad() {
31+
super.viewDidLoad()
32+
view.addSubview(blurView)
33+
blurView.contentView.addSubviews(textLabel, imageView, borderView)
34+
setupSubviews()
35+
handleStateUpdate()
36+
}
37+
38+
public override func viewDidLayoutSubviews() {
39+
super.viewDidLayoutSubviews()
40+
blurView.frame = view.bounds
41+
}
42+
43+
public override func viewWillLayoutSubviews() {
44+
super.viewWillLayoutSubviews()
45+
}
46+
47+
// MARK: - Subviews
48+
49+
private func setupSubviews() {
50+
textLabel.translatesAutoresizingMaskIntoConstraints = false
51+
textLabel.textColor = .black
52+
textLabel.numberOfLines = 3
53+
54+
imageView.translatesAutoresizingMaskIntoConstraints = false
55+
imageView.image = imageNamed("info").withRenderingMode(.alwaysTemplate)
56+
imageView.tintColor = .black
57+
58+
borderView.translatesAutoresizingMaskIntoConstraints = false
59+
borderView.backgroundColor = .clear
60+
borderView.layer.borderWidth = 2
61+
borderView.layer.cornerRadius = 10
62+
borderView.layer.borderColor = UIColor.black.cgColor
63+
}
64+
65+
private func handleStateUpdate() {
66+
borderView.isHidden = true
67+
borderView.layer.removeAllAnimations()
68+
69+
switch state {
70+
case .scanning, .unauthorized:
71+
textLabel.text = state == .scanning ? Info.text : Info.settingsText
72+
textLabel.textColor = .black
73+
textLabel.font = UIFont.boldSystemFont(ofSize: 14)
74+
textLabel.numberOfLines = 3
75+
textLabel.textAlignment = .left
76+
imageView.tintColor = .black
77+
case .processing:
78+
textLabel.text = Info.loadingText
79+
textLabel.textColor = .black
80+
textLabel.font = UIFont.boldSystemFont(ofSize: 16)
81+
textLabel.numberOfLines = 10
82+
textLabel.textAlignment = .center
83+
borderView.isHidden = false
84+
imageView.tintColor = .black
85+
case .notFound:
86+
textLabel.text = Info.notFoundText
87+
textLabel.textColor = .black
88+
textLabel.font = UIFont.boldSystemFont(ofSize: 16)
89+
textLabel.numberOfLines = 10
90+
textLabel.textAlignment = .center
91+
imageView.tintColor = .red
92+
}
93+
94+
if state == .scanning || state == .unauthorized {
95+
expandedConstraints.forEach({ $0.isActive = false })
96+
collapsedConstraints.forEach({ $0.isActive = true })
97+
} else {
98+
collapsedConstraints.forEach({ $0.isActive = false })
99+
expandedConstraints.forEach({ $0.isActive = true })
100+
}
101+
}
102+
103+
// MARK: - Animations
104+
105+
/**
106+
Animates blur and border view.
107+
*/
108+
func animateLoading() {
109+
animate(blurStyle: .light)
110+
animate(borderViewAngle: CGFloat(Double.pi/2))
111+
}
112+
113+
/**
114+
Animates blur to make pulsating effect.
115+
116+
- Parameter style: The current blur style.
117+
*/
118+
private func animate(blurStyle: UIBlurEffectStyle) {
119+
guard state == .processing else { return }
120+
121+
UIView.animate(
122+
withDuration: 2.0,
123+
delay: 0.5,
124+
options: [.beginFromCurrentState],
125+
animations: ({ [weak self] in
126+
self?.blurView.effect = UIBlurEffect(style: blurStyle)
127+
}),
128+
completion: ({ [weak self] _ in
129+
self?.animate(blurStyle: blurStyle == .light ? .extraLight : .light)
130+
}))
131+
}
132+
133+
/**
134+
Animates border view with a given angle.
135+
136+
- Parameter angle: Rotation angle.
137+
*/
138+
private func animate(borderViewAngle: CGFloat) {
139+
guard state == .processing else {
140+
borderView.transform = .identity
141+
return
142+
}
143+
144+
UIView.animate(
145+
withDuration: 0.8,
146+
delay: 0.5,
147+
usingSpringWithDamping: 0.6,
148+
initialSpringVelocity: 1.0,
149+
options: [.beginFromCurrentState],
150+
animations: ({ [weak self] in
151+
self?.borderView.transform = CGAffineTransform(rotationAngle: borderViewAngle)
152+
}),
153+
completion: ({ [weak self] _ in
154+
self?.animate(borderViewAngle: borderViewAngle + CGFloat(Double.pi / 2))
155+
}))
156+
}
157+
}
158+
159+
extension MessageViewController {
160+
private func makeExpandedConstraints() -> [NSLayoutConstraint] {
161+
let padding: CGFloat = 10
162+
let borderSize: CGFloat = 51
163+
164+
return [
165+
imageView.centerYAnchor.constraint(equalTo: blurView.centerYAnchor, constant: -60),
166+
imageView.centerXAnchor.constraint(equalTo: blurView.centerXAnchor),
167+
imageView.widthAnchor.constraint(equalToConstant: 30),
168+
imageView.heightAnchor.constraint(equalToConstant: 27),
169+
170+
textLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 14),
171+
textLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
172+
textLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding),
173+
174+
borderView.topAnchor.constraint(equalTo: imageView.topAnchor, constant: -12),
175+
borderView.centerXAnchor.constraint(equalTo: blurView.centerXAnchor),
176+
borderView.widthAnchor.constraint(equalToConstant: borderSize),
177+
borderView.heightAnchor.constraint(equalToConstant: borderSize)
178+
]
179+
}
180+
181+
private func makeCollapsedConstraints() -> [NSLayoutConstraint] {
182+
let padding: CGFloat = 10
183+
var constraints = [
184+
imageView.topAnchor.constraint(equalTo: blurView.topAnchor, constant: 18),
185+
imageView.widthAnchor.constraint(equalToConstant: 30),
186+
imageView.heightAnchor.constraint(equalToConstant: 27),
187+
188+
textLabel.topAnchor.constraint(equalTo: imageView.topAnchor, constant: -3),
189+
textLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10)
190+
]
191+
192+
if #available(iOS 11.0, *) {
193+
constraints += [
194+
imageView.leadingAnchor.constraint(
195+
equalTo: view.safeAreaLayoutGuide.leadingAnchor,
196+
constant: padding
197+
),
198+
textLabel.trailingAnchor.constraint(
199+
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
200+
constant: -padding
201+
)
202+
]
203+
} else {
204+
constraints += [
205+
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
206+
textLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding)
207+
]
208+
}
4209

210+
return constraints
211+
}
5212
}

Sources/Controllers/ScannerViewController.swift

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ open class ScannerController: UIViewController {
3535
private lazy var captureSession: AVCaptureSession = AVCaptureSession()
3636

3737
/// Information view with description label.
38-
private lazy var infoView: InfoView = InfoView()
38+
private lazy var messageViewController: MessageViewController = .init()
39+
40+
private var messageView: UIView {
41+
return messageViewController.view
42+
}
3943

4044
/// Button to change torch mode.
4145
public lazy var flashButton: UIButton = { [unowned self] in
@@ -88,7 +92,7 @@ open class ScannerController: UIViewController {
8892
) ? 0.5 : 0.0
8993

9094
guard status.state != .notFound else {
91-
infoView.status = status
95+
messageViewController.state = status.state
9296

9397
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) {
9498
self.status = Status(state: .scanning)
@@ -103,19 +107,20 @@ open class ScannerController: UIViewController {
103107
resetState()
104108
}
105109

110+
self.messageViewController.state = self.status.state
106111
UIView.animate(withDuration: duration,
107112
animations: {
108-
self.infoView.frame = self.infoFrame
109-
self.infoView.status = self.status
113+
self.messageView.layoutIfNeeded()
114+
self.messageView.frame = self.messageViewFrame
110115
},
111116
completion: { [weak self] _ in
112117
if delayReset {
113118
self?.resetState()
114119
}
115120

116-
self?.infoView.layer.removeAllAnimations()
121+
self?.messageView.layer.removeAllAnimations()
117122
if self?.status.state == .processing {
118-
self?.infoView.animateLoading()
123+
self?.messageViewController.animateLoading()
119124
}
120125
})
121126
}
@@ -139,7 +144,7 @@ open class ScannerController: UIViewController {
139144
}
140145

141146
/// Calculated frame for the info view.
142-
private var infoFrame: CGRect {
147+
private var messageViewFrame: CGRect {
143148
let height = status.state != .processing ? 75 : view.bounds.height
144149
return CGRect(x: 0, y: view.bounds.height - height,
145150
width: view.bounds.width, height: height)
@@ -183,7 +188,8 @@ open class ScannerController: UIViewController {
183188

184189
view.layer.addSublayer(videoPreviewLayer)
185190

186-
[infoView, settingsButton, flashButton, focusView].forEach {
191+
add(childViewController: messageViewController)
192+
[settingsButton, flashButton, focusView].forEach {
187193
view.addSubview($0)
188194
view.bringSubview(toFront: $0)
189195
}
@@ -335,7 +341,7 @@ open class ScannerController: UIViewController {
335341
width: flashButtonSize,
336342
height: flashButtonSize
337343
)
338-
infoView.frame = infoFrame
344+
messageView.frame = messageViewFrame
339345

340346
if let videoPreviewLayer = videoPreviewLayer {
341347
videoPreviewLayer.frame = view.layer.bounds
@@ -480,6 +486,7 @@ extension ScannerController: AVCaptureMetadataOutputObjectsDelegate {
480486

481487
extension ScannerController: HeaderViewControllerDelegate {
482488
public func headerViewControllerDidTapCloseButton(_ controller: HeaderViewController) {
489+
status = Status(state: .scanning)
483490
dismissalDelegate?.scannerDidDismiss(self)
484491
}
485492
}

Sources/Controllers/SettingsViewController.swift

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import UIKit
2+
3+
final class VideoViewController: UIViewController {
4+
5+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import UIKit
2+
3+
extension NSLayoutConstraint {
4+
static func activate(_ constraints: NSLayoutConstraint? ...) {
5+
for case let constraint in constraints {
6+
guard let constraint = constraint else {
7+
continue
8+
}
9+
10+
(constraint.firstItem as? UIView)?.translatesAutoresizingMaskIntoConstraints = false
11+
constraint.isActive = true
12+
}
13+
}
14+
}

Sources/Extensions/UIView+Extensions.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,14 @@ extension UIView {
88

99
return .zero
1010
}
11+
12+
func addSubviews(_ subviews: UIView...) {
13+
addSubviews(subviews)
14+
}
15+
16+
func addSubviews(_ subviews: [UIView]) {
17+
subviews.forEach {
18+
addSubview($0)
19+
}
20+
}
1121
}

0 commit comments

Comments
 (0)