Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
537 changes: 537 additions & 0 deletions IDNowTest.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "94B469D6-F9EE-46E7-8AC4-2287DF97F088"
type = "1"
version = "2.0">
</Bucket>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>IDNowTest.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>
36 changes: 36 additions & 0 deletions IDNowTest/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// AppDelegate.swift
// IDNowTest
//
// Created by Kristian Rusyn on 22/09/2024.
//

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}

// MARK: UISceneSession Lifecycle

func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}

func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}


}

11 changes: 11 additions & 0 deletions IDNowTest/Assets.xcassets/AccentColor.colorset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
13 changes: 13 additions & 0 deletions IDNowTest/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 6 additions & 0 deletions IDNowTest/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
25 changes: 25 additions & 0 deletions IDNowTest/Base.lproj/LaunchScreen.storyboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
61 changes: 61 additions & 0 deletions IDNowTest/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Camera View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="CameraViewController" customModule="IDNowTest" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="captureButtonID">
<rect key="frame" x="159" y="776" width="75" height="32"/>
<color key="backgroundColor" systemColor="systemBlueColor"/>
<inset key="contentEdgeInsets" minX="5" minY="5" maxX="5" maxY="5"/>
<state key="normal" title="Capture"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="4"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<action selector="captureButtonPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="hDM-oR-XIP"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="captureButtonID" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="MHP-Ni-HXw"/>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="captureButtonID" secondAttribute="bottom" constant="10" id="biS-Ew-hAh"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="keyPath" value="YES"/>
</userDefinedRuntimeAttributes>
</view>
<connections>
<outlet property="captureButton" destination="captureButtonID" id="DZJ-Gn-NNu"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-106" y="4"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBlueColor">
<color red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
41 changes: 41 additions & 0 deletions IDNowTest/CameraViewController/CameraViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// ViewController.swift
// IDNowTest
//
// Created by Kristian Rusyn on 22/09/2024.
//

import UIKit
import AVFoundation

class CameraViewController: UIViewController {

private let cameraService = CameraServiceImpl()

@IBOutlet weak var captureButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
try! cameraService.setupCameraStream(view: view)
}

@objc func capturePhoto() {
captureButton.isEnabled = false
cameraService.capturePhoto(imageClosure: { [weak self] image, error in
self?.captureButton.isEnabled = true
if let error {
let alert = UIAlertController(title: "Error",
message: error.localizedDescription,
preferredStyle: .alert)
self?.present(alert, animated: true)
} else if let image {
self?.openImageViewController(image: image)
}

})
}

private func openImageViewController(image: UIImage) {
let imageViewController = ImageViewController(viewModel: ImageViewModel(image: image))
present(UINavigationController(rootViewController: imageViewController), animated: true)
}
}
153 changes: 153 additions & 0 deletions IDNowTest/ImageViewController/ImageViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// ImageViewController.swift
// IDNowTest
//
// Created by Kristian Rusyn on 22/09/2024.
//

import Combine
import UIKit
import Photos

class ImageViewController: UIViewController {
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
@IBOutlet private weak var priceLabel: UILabel!

@IBOutlet private weak var imageView: UIImageView!
@IBOutlet private weak var activityIndicator: UIActivityIndicatorView!

private var viewModel: ImageViewModel

private var cancellables = Set<AnyCancellable>()

init(viewModel: ImageViewModel) {
self.viewModel = viewModel
super.init(nibName: String(describing: ImageViewController.self), bundle: nil)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()

setupNavigationBar()
setupBindings()
imageView.image = viewModel.image
viewModel.fetchProduct()

// Hide the activity indicator initially
activityIndicator.hidesWhenStopped = true
activityIndicator.stopAnimating()
}

private func setupNavigationBar() {
let downloadButton = UIBarButtonItem(title: "Download",
style: .plain,
target: self,
action: #selector(downloadImage))
navigationItem.rightBarButtonItem = downloadButton
}

private func setupBindings() {
viewModel.$isLoading
.receive(on: DispatchQueue.main)
.sink { [weak self] isLoading in
if isLoading {
self?.activityIndicator.startAnimating()
} else {
self?.activityIndicator.stopAnimating()
}
}
.store(in: &cancellables)

viewModel.$title
.receive(on: DispatchQueue.main)
.sink { [weak self] title in
self?.titleLabel.text = title
self?.navigationItem.title = title
}
.store(in: &cancellables)

viewModel.$description
.receive(on: DispatchQueue.main)
.sink { [weak self] description in
self?.descriptionLabel.text = description
}
.store(in: &cancellables)

viewModel.$price
.receive(on: DispatchQueue.main)
.sink { [weak self] price in
self?.priceLabel.text = price
}
.store(in: &cancellables)

viewModel.$image
.receive(on: DispatchQueue.main)
.sink { [weak self] image in
self?.imageView.image = image
}
.store(in: &cancellables)

viewModel.$errorMessage
.receive(on: DispatchQueue.main)
.sink { [weak self] errorMessage in
if let message = errorMessage {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self?.present(alert, animated: true, completion: nil)
}
}
.store(in: &cancellables)
}

@objc private func downloadImage() {
PHPhotoLibrary.requestAuthorization { [weak self] status in
DispatchQueue.main.async {
switch status {
case .authorized, .limited:
self?.saveImageToPhotoLibrary()
case .denied, .restricted:
let alert = UIAlertController(title: "Access Denied",
message: "Please allow photo library access in Settings to save images.",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self?.present(alert, animated: true, completion: nil)
case .notDetermined:
break
@unknown default:
let alert = UIAlertController(title: "Unknown Error",
message: "An unknown error occurred while accessing the photo library.",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self?.present(alert, animated: true, completion: nil)
}
}
}
}

private func saveImageToPhotoLibrary() {
guard let image = viewModel.image else { return }
UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
}

@objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
var alert: UIAlertController
if let error = error {
alert = UIAlertController(title: "Save Error",
message: error.localizedDescription,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
} else {
alert = UIAlertController(title: "Saved",
message: "Image has been saved to your photos.",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
}

present(alert, animated: true, completion: nil)
}
}
Loading