Skip to content
This repository has been archived by the owner on Apr 5, 2024. It is now read-only.

Commit

Permalink
Feature: Use camera of iPhone to take pictures from inside app (#64)
Browse files Browse the repository at this point in the history
* Fix typos

* Make DescriptionFeatureApp and SettingsFeatureApp compile again

* Fix typos

* On ReportView: Show Date Widget after Photos Widget

* Fix typo in unit tests

* Feature: Take photo from inside app using iPhone camera

* Update WegliKit/Sources/CameraAccessClient/Live.swift

Co-authored-by: Malte Bünz <[email protected]>

* Fix unit tests

* Use asynch instead of Combine in CameraAccessClient

* Add unit tests fore camera feature

* Add coordinates to PickerImageResult after taking a photo using the camera

* Make camera button first option

Co-authored-by: Yannick Vornehm <[email protected]>
Co-authored-by: Malte Bünz <[email protected]>
  • Loading branch information
3 people authored Aug 8, 2022
1 parent 77dfac2 commit c206b6b
Show file tree
Hide file tree
Showing 21 changed files with 296 additions and 37 deletions.
2 changes: 1 addition & 1 deletion DescriptionFeatureApp/DescriptionFeatureAppApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ struct DescriptionFeatureAppApp: App {
store: .init(
initialState: .init(),
reducer: descriptionReducer,
environment: DescriptionEnvironment()
environment: DescriptionEnvironment(backgroundQueue: .main)
)
)
}
Expand Down
2 changes: 1 addition & 1 deletion SettingsFeatureApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ struct ContentView: View {
SettingsView(
store: .init(
initialState: .init(
accountSettingsState: .init(accountSettings: .init(apiKey: "")),
accountSettingsState: .init(accountSettings: .init(apiToken: "")),
contact: .empty,
userSettings: .init()
),
Expand Down
8 changes: 8 additions & 0 deletions WegliKit/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ let package = Package(
)
]
),
.target(
name: "CameraAccessClient",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
]
),
.target(
name: "ContactFeature",
dependencies: [
Expand Down Expand Up @@ -122,6 +128,7 @@ let package = Package(
.target(
name: "ImagesFeature",
dependencies: [
"CameraAccessClient",
"Helper",
"L10n",
"PhotoLibraryAccessClient",
Expand Down Expand Up @@ -300,6 +307,7 @@ package.targets.append(
.testTarget(
name: "ImagesFeatureTests",
dependencies: [
"CameraAccessClient",
"ImagesFeature",
"L10n",
"PhotoLibraryAccessClient",
Expand Down
17 changes: 17 additions & 0 deletions WegliKit/Sources/CameraAccessClient/Interface.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ComposableArchitecture
import Photos

public typealias CameraAuthorizationStatus = AVAuthorizationStatus

public struct CameraAccessClient {
public init(
requestAuthorization: @escaping () -> Effect<Bool, Never>,
authorizationStatus: @escaping () -> CameraAuthorizationStatus
) {
self.requestAuthorization = requestAuthorization
self.authorizationStatus = authorizationStatus
}

public var requestAuthorization: () -> Effect<Bool, Never>
public var authorizationStatus: () -> CameraAuthorizationStatus
}
17 changes: 17 additions & 0 deletions WegliKit/Sources/CameraAccessClient/Live.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ComposableArchitecture
import Photos

public extension CameraAccessClient {
static func live() -> Self {
Self(
requestAuthorization: {
.task {
await AVCaptureDevice.requestAccess(for: .video)
}
},
authorizationStatus: {
AVCaptureDevice.authorizationStatus(for: .video)
}
)
}
}
9 changes: 9 additions & 0 deletions WegliKit/Sources/CameraAccessClient/Mock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import ComposableArchitecture
import Photos

public extension CameraAccessClient {
static let mock = Self(
requestAuthorization: { .none },
authorizationStatus: { .notDetermined }
)
}
15 changes: 7 additions & 8 deletions WegliKit/Sources/DescriptionFeature/DescriptionCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public struct DescriptionState: Equatable {
selectedDuration: Int = 0,
selectedCharge: Charge? = nil,
blockedOthers: Bool = false,
verhicleEmpty: Bool = false,
vehicleEmpty: Bool = false,
hazardLights: Bool = false,
expiredTuv: Bool = false,
expiredEco: Bool = false
Expand All @@ -26,7 +26,7 @@ public struct DescriptionState: Equatable {
self.selectedDuration = selectedDuration
self.selectedCharge = selectedCharge
self.blockedOthers = blockedOthers
self.verhicleEmpty = verhicleEmpty
self.vehicleEmpty = vehicleEmpty
self.hazardLights = hazardLights
self.expiredTuv = expiredTuv
self.expiredEco = expiredEco
Expand All @@ -38,7 +38,7 @@ public struct DescriptionState: Equatable {
public var selectedDuration: Int
public var selectedCharge: Charge?
@BindableState public var blockedOthers = false
@BindableState public var verhicleEmpty = false
@BindableState public var vehicleEmpty = false
@BindableState public var hazardLights = false
@BindableState public var expiredTuv = false
@BindableState public var expiredEco = false
Expand Down Expand Up @@ -76,13 +76,13 @@ public enum DescriptionAction: BindableAction, Equatable {
case setBrand(CarBrand)
case setColor(Int)
case setCharge(Charge)
case setDuraration(Int)
case setDuration(Int)
case setChargeTypeSearchText(String)
case setCarBrandSearchText(String)
case toggleChargeFavorite(Charge)
case sortFavoritedCharges
case favoriteChargesLoaded(Result<[String], NSError>)
case presentCargeSelectionView(Bool)
case presentChargeSelectionView(Bool)
case presentBrandSelectionView(Bool)
}

Expand Down Expand Up @@ -130,7 +130,7 @@ public let descriptionReducer = Reducer<DescriptionState, DescriptionAction, Des
state.presentChargeSelection = false
return .none

case let .setDuraration(value):
case let .setDuration(value):
state.selectedDuration = value
return .none

Expand All @@ -151,7 +151,6 @@ public let descriptionReducer = Reducer<DescriptionState, DescriptionAction, Des
}
state.charges.update(charge, at: index)

struct FavoritedId: Hashable {}
return .concatenate(
environment.fileClient.saveFavoriteCharges(
state.charges.filter(\.isFavorite).map(\.id),
Expand Down Expand Up @@ -181,7 +180,7 @@ public let descriptionReducer = Reducer<DescriptionState, DescriptionAction, Des

return Effect(value: .sortFavoritedCharges)

case let .presentCargeSelectionView(value):
case let .presentChargeSelectionView(value):
state.chargeTypeSearchText = ""
state.presentChargeSelection = value
return .none
Expand Down
12 changes: 6 additions & 6 deletions WegliKit/Sources/DescriptionFeature/EditDescriptionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public struct EditDescriptionView: View {

blockedOthersView

verhicleEmtpyView
vehicleEmptyView

hazardLightsView

Expand Down Expand Up @@ -146,7 +146,7 @@ public struct EditDescriptionView: View {
NavigationLink(
isActive: viewStore.binding(
get: \.presentChargeSelection,
send: DescriptionAction.presentCargeSelectionView
send: DescriptionAction.presentChargeSelectionView
),
destination: {
List {
Expand Down Expand Up @@ -180,7 +180,7 @@ public struct EditDescriptionView: View {
}
.contentShape(Rectangle())
.onTapGesture {
viewStore.send(.presentCargeSelectionView(true))
viewStore.send(.presentChargeSelectionView(true))
}
}
)
Expand All @@ -191,7 +191,7 @@ public struct EditDescriptionView: View {
L10n.Description.Row.length,
selection: viewStore.binding(
get: \.selectedDuration,
send: DescriptionAction.setDuraration
send: DescriptionAction.setDuration
)
) {
ForEach(viewStore.times, id: \.self) { time in
Expand All @@ -209,10 +209,10 @@ public struct EditDescriptionView: View {
)
}

var verhicleEmtpyView: some View {
var vehicleEmptyView: some View {
ToggleButton(
label: "Das Fahrzeug war verlassen",
isOn: viewStore.binding(\.$verhicleEmpty)
isOn: viewStore.binding(\.$vehicleEmpty)
)
}

Expand Down
60 changes: 60 additions & 0 deletions WegliKit/Sources/ImagesFeature/CameraView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import PhotosUI
import SwiftUI
import SharedModels

public struct CameraView: UIViewControllerRepresentable {

@Binding var isPresented: Bool
@Binding var pickerResult: [PickerImageResult?]

public func makeUIViewController(context: UIViewControllerRepresentableContext<CameraView>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.allowsEditing = false
imagePicker.sourceType = .camera
imagePicker.delegate = context.coordinator

return imagePicker
}

public func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<CameraView>) {}

public func makeCoordinator() -> Coordinator {
Coordinator(self)
}

final public class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

var parent: CameraView

init(_ parent: CameraView) {
self.parent = parent
}

public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
defer {
parent.isPresented = false
}
guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage,
let imageData = image.jpegData(compressionQuality: 1) else {
debugPrint("originalImage info from ImagePickerController could not be casted to UIImage")
return
}

let filename = "camera\(Date().description)"
let url = FileManager.default.createDataTempFile(withData: imageData, withFileName: filename)

var coordinate: CoordinateRegion.Coordinate?
if let asset: PHAsset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset,
let imageCoordinate = asset.location?.coordinate {
coordinate = .init(imageCoordinate)
}

parent.pickerResult = [PickerImageResult(
id: filename,
imageUrl: url,
coordinate: coordinate,
creationDate: Date()
)]
}
}
}
47 changes: 46 additions & 1 deletion WegliKit/Sources/ImagesFeature/ImagesCore.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Created for weg-li in 2021.

import CameraAccessClient
import ComposableArchitecture
import CoreLocation
import Foundation
Expand All @@ -13,20 +14,23 @@ import UIKit
public struct ImagesViewState: Equatable, Codable {
public init(
alert: AlertState<ImagesViewAction>? = nil,
showCamera: Bool = false,
showImagePicker: Bool = false,
storedPhotos: [PickerImageResult?] = [],
coordinateFromImagePicker: CLLocationCoordinate2D? = nil,
dateFromImagePicker: Date? = nil
) {
self.alert = alert
self.showImagePicker = showImagePicker
self.showCamera = showCamera
self.storedPhotos = storedPhotos
self.pickerResultCoordinate = coordinateFromImagePicker
self.pickerResultDate = dateFromImagePicker
}

public var alert: AlertState<ImagesViewAction>?
public var showImagePicker: Bool
public var showCamera: Bool
public var storedPhotos: [PickerImageResult?]
public var pickerResultCoordinate: CLLocationCoordinate2D?
public var pickerResultDate: Date?
Expand All @@ -38,6 +42,7 @@ public struct ImagesViewState: Equatable, Codable {

enum CodingKeys: String, CodingKey {
case showImagePicker
case showCamera
case storedPhotos
case pickerResultCoordinate
case pickerResultDate
Expand All @@ -63,6 +68,10 @@ public enum ImagesViewAction: Equatable {
case setShowImagePicker(Bool)
case requestPhotoLibraryAccess
case requestPhotoLibraryAccessResult(PhotoLibraryAuthorizationStatus)
case takePhotosButtonTapped
case setShowCamera(Bool)
case requestCameraAccess
case requestCameraAccessResult(Bool)
case setImageCoordinate(CLLocationCoordinate2D?)
case setImageCreationDate(Date?)
case dismissAlert
Expand All @@ -74,18 +83,21 @@ public enum ImagesViewAction: Equatable {
public struct ImagesViewEnvironment {
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var backgroundQueue: AnySchedulerOf<DispatchQueue>
public var cameraAccessClient: CameraAccessClient
public let photoLibraryAccessClient: PhotoLibraryAccessClient
public let textRecognitionClient: TextRecognitionClient
public let distanceFilter: Double = 50

public init(
mainQueue: AnySchedulerOf<DispatchQueue>,
backgroundQueue: AnySchedulerOf<DispatchQueue>,
cameraAccessClient: CameraAccessClient,
photoLibraryAccessClient: PhotoLibraryAccessClient,
textRecognitionClient: TextRecognitionClient
) {
self.mainQueue = mainQueue
self.backgroundQueue = backgroundQueue
self.cameraAccessClient = cameraAccessClient
self.photoLibraryAccessClient = photoLibraryAccessClient
self.textRecognitionClient = textRecognitionClient
}
Expand Down Expand Up @@ -131,7 +143,40 @@ public let imagesReducer = Reducer<ImagesViewState, ImagesViewAction, ImagesView
default:
return .none
}


case .takePhotosButtonTapped:
switch env.cameraAccessClient.authorizationStatus() {
case .authorized:
return Effect(value: .setShowCamera(true))
case .denied:
state.alert = .init(title: TextState(L10n.Camera.Alert.accessDenied))
return .none
case .notDetermined:
return Effect(value: .requestCameraAccess)
case .restricted:
// TODO: How to handle this?
return .none
@unknown default:
return .none
}

case let .setShowCamera(value):
state.showCamera = value
return .none

case .requestCameraAccess:
return env.cameraAccessClient
.requestAuthorization()
.receive(on: env.mainQueue)
.map(ImagesViewAction.requestCameraAccessResult)
.eraseToEffect()

case let .requestCameraAccessResult(success):
if success {
return Effect(value: .setShowCamera(true))
}
return .none

case let .setPhotos(photos):
state.storedPhotos.append(contentsOf: photos)

Expand Down
Loading

0 comments on commit c206b6b

Please sign in to comment.