SwiftUI doesn't update state to #ObservedObject cameraViewModel object - swiftui

I'm new to SwiftUI and manual camera functionality, and I really need help.
So I trying to build a SwiftUI camera view that has a UIKit camera as a wrapper to control the focus lens position via SwiftUI picker view, display below a fucus value, and want to try have a correlation between AVcaptureDevice.lensPosition from 0 to 1.0 and feats that are displayed in the focus picker view. But for now, I only want to display that fucus number on screen.
And the problem is when I try to update focus via coordinator focus observation and set it to the camera view model then nothing happened. Please help 🙌
Here's the code:
import SwiftUI
import AVFoundation
import Combine
struct ContentView: View {
#State private var didTapCapture = false
#State private var focusLensPosition: Float = 0
#ObservedObject var cameraViewModel = CameraViewModel(focusLensPosition: 0)
var body: some View {
VStack {
ZStack {
CameraPreviewRepresentable(didTapCapture: $didTapCapture, cameraViewModel: cameraViewModel)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
VStack {
FocusPicker(selectedFocus: $focusLensPosition)
Text(String(cameraViewModel.focusLensPosition))
.foregroundColor(.red)
.font(.largeTitle)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.edgesIgnoringSafeArea(.all)
Spacer()
CaptureButton(didTapCapture: $didTapCapture)
.frame(width: 100, height: 100, alignment: .center)
.padding(.bottom, 20)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CaptureButton: View {
#Binding var didTapCapture : Bool
var body: some View {
Button {
didTapCapture.toggle()
} label: {
Image(systemName: "photo")
.font(.largeTitle)
.padding(30)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.red)
)
}
}
}
struct CameraPreviewRepresentable: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var didTapCapture: Bool
#ObservedObject var cameraViewModel: CameraViewModel
let cameraController: CustomCameraController = CustomCameraController()
func makeUIViewController(context: Context) -> CustomCameraController {
cameraController.delegate = context.coordinator
return cameraController
}
func updateUIViewController(_ cameraViewController: CustomCameraController, context: Context) {
if (self.didTapCapture) {
cameraViewController.didTapRecord()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self, cameraViewModel: cameraViewModel)
}
class Coordinator: NSObject, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
let parent: CameraPreviewRepresentable
var cameraViewModel: CameraViewModel
var focusLensPositionObserver: NSKeyValueObservation?
init(_ parent: CameraPreviewRepresentable, cameraViewModel: CameraViewModel) {
self.parent = parent
self.cameraViewModel = cameraViewModel
super.init()
focusLensPositionObserver = self.parent.cameraController.currentCamera?.observe(\.lensPosition, options: [.new]) { [weak self] camera, _ in
print(Float(camera.lensPosition))
//announcing changes via Publisher
self?.cameraViewModel.focusLensPosition = camera.lensPosition
}
}
deinit {
focusLensPositionObserver = nil
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
parent.didTapCapture = false
if let imageData = photo.fileDataRepresentation(), let image = UIImage(data: imageData) {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
class CameraViewModel: ObservableObject {
#Published var focusLensPosition: Float = 0
init(focusLensPosition: Float) {
self.focusLensPosition = focusLensPosition
}
}
class CustomCameraController: UIViewController {
var image: UIImage?
var captureSession = AVCaptureSession()
var backCamera: AVCaptureDevice?
var frontCamera: AVCaptureDevice?
var currentCamera: AVCaptureDevice?
var photoOutput: AVCapturePhotoOutput?
var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
//DELEGATE
var delegate: AVCapturePhotoCaptureDelegate?
func showFocusLensPosition() -> Float {
// guard let camera = currentCamera else { return 0 }
// try! currentCamera!.lockForConfiguration()
// currentCamera!.focusMode = .autoFocus
//// currentCamera!.setFocusModeLocked(lensPosition: currentCamera!.lensPosition, completionHandler: nil)
// currentCamera!.unlockForConfiguration()
return currentCamera!.lensPosition
}
func didTapRecord() {
let settings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: settings, delegate: delegate!)
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
setupCaptureSession()
setupDevice()
setupInputOutput()
setupPreviewLayer()
startRunningCaptureSession()
}
func setupCaptureSession() {
captureSession.sessionPreset = .photo
}
func setupDevice() {
let deviceDiscoverySession =
AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .unspecified)
for device in deviceDiscoverySession.devices {
switch device.position {
case .front:
self.frontCamera = device
case .back:
self.backCamera = device
default:
break
}
}
self.currentCamera = self.backCamera
}
func setupInputOutput() {
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: currentCamera!)
captureSession.addInput(captureDeviceInput)
photoOutput = AVCapturePhotoOutput()
captureSession.addOutput(photoOutput!)
} catch {
print(error)
}
}
func setupPreviewLayer() {
self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
let deviceOrientation = UIDevice.current.orientation
cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation(rawValue: deviceOrientation.rawValue)!
self.cameraPreviewLayer?.frame = self.view.frame
// view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
self.view.layer.insertSublayer(cameraPreviewLayer!, at: 0)
}
func startRunningCaptureSession() {
captureSession.startRunning()
}
}
struct FocusPicker: View {
var feets = ["∞ ft", "30", "15", "10", "7", "5", "4", "3.5", "3", "2.5", "2", "1.5", "1", "0.5", "Auto"]
#Binding var selectedFocus: Float
var body: some View {
Picker(selection: $selectedFocus, label: Text("")) {
ForEach(0 ..< feets.count) {
Text(feets[$0])
.foregroundColor(.white)
.font(.subheadline)
.fontWeight(.medium)
}
.animation(.none)
.background(Color.clear)
.pickerStyle(WheelPickerStyle())
}
.frame(width: 60, height: 200)
.border(Color.gray, width: 5)
.clipped()
}
}

The problem with your provided code is that the type of selectedFocus within the FocusPicker view should be Integer rather than Float. So one option is to change this type to Integer and find a way to express the AVCaptureDevice.lensPosition as an Integer with the given range.
The second option is to replace the feets array with an enumeration. By making the enumeration conform to the CustomStringConvertible protocol, you can even provide a proper description. Please see my example below.
I've stripped your code a bit as you just wanted to display the number in the first step and thus the code is more comprehensible.
My working example:
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var cameraViewModel = CameraViewModel(focusLensPosition: 0.5)
var body: some View {
VStack {
ZStack {
VStack {
FocusPicker(selectedFocus: $cameraViewModel.focusLensPosition)
Text(String(self.cameraViewModel.focusLensPosition))
.foregroundColor(.red)
.font(.largeTitle)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.edgesIgnoringSafeArea(.all)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class CameraViewModel: ObservableObject {
#Published var focusLensPosition: Float
init(focusLensPosition: Float) {
self.focusLensPosition = focusLensPosition
}
}
enum Feets: Float, CustomStringConvertible, CaseIterable, Identifiable {
case case1 = 0.0
case case2 = 0.5
case case3 = 1.0
var id: Float { self.rawValue }
var description: String {
get {
switch self {
case .case1:
return "∞ ft"
case .case2:
return "4"
case .case3:
return "Auto"
}
}
}
}
struct FocusPicker: View {
#Binding var selectedFocus: Float
var body: some View {
Picker(selection: $selectedFocus, label: Text("")) {
ForEach(Feets.allCases) { feet in
Text(feet.description)
}
.animation(.none)
.background(Color.clear)
.pickerStyle(WheelPickerStyle())
}
.frame(width: 60, height: 200)
.border(Color.gray, width: 5)
.clipped()
}
}

Related

How to convert value of type 'ImagePickerView' to expected argument type 'String' on Swiftui?

I am trying to build an app where a user can insert the name of the movie and can add an image directly into the app from the photo library (using UIKit. Thankfully the part where the user can insert the text and image from the photo library works. My issue is transferring that data from the .sheet to a list. The info in the TextFields that the user inserts works fine and is shown in the list, but the image doesn't show. I keep getting the error "Cannot convert value of type 'ImagePickerView' to expected argument type 'String'". I don't know how to fix this issue. This issue comes in the ContentView.swift file, in the MovieRow struct when I try to insert the Image(). Any help would be appreciated. Thanks in advance.
Below is my ContentView file. d
// ContentView.swift
// MovieListEditttt
//
import SwiftUI
struct ContentView: View {
#State var movieAdd: [MovieAdd] = []
#State private var newMovieName: String = ""
#State private var showNewMovie = false
#State private var newMovieImage = UIImage()
var body: some View {
ZStack {
VStack {
HStack {
Text("Movies Watched Ratings")
.font(.system(size: 40, weight: .black, design: .rounded
))
Spacer()
Button(action: {
self.showNewMovie = true
}) {
Image(systemName: "plus.circle.fill")
.font(.largeTitle)
.foregroundColor(.yellow)
}
}
List{
ForEach(movieAdd) {movie in
movieRow(movieAdd: movie)
}
}
}
if showNewMovie {
BlankView(bGColor: .black)
.opacity(0.5)
.onTapGesture {
self.showNewMovie = false
}
NewMovieView(isShow: $showNewMovie, addMovie: $movieAdd, newMovieName: newMovieName)
.transition(.move(edge: .bottom))
.animation(.interpolatingSpring(stiffness: 200.0, damping: 25.0, initialVelocity: 10.0))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct movieRow: View {
#ObservedObject var movieAdd : MovieAdd
var body: some View {
VStack {
Image(movieAdd.movieImage)
.resizable()
.frame(width: 100, height: 100)
Text(movieAdd.movieName)
}
}
}
struct BlankView: View {
var bGColor: Color
var body: some View {
VStack {
Spacer()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(bGColor)
.edgesIgnoringSafeArea(.all)
}
}
Here is my MovieAdd.swift file where I initialize all variables that will be put inside the list.
import Foundation
class MovieAdd: ObservableObject, Identifiable {
var id = UUID()
#Published var movieName = ""
#Published var isComplete : Bool = false
#Published var movieImage : ImagePickerView
init(movieName: String, isComplete: Bool = false, movieImage: ImagePickerView) {
self.movieName = movieName
self.isComplete = isComplete
self.movieImage = movieImage
}
}
And here is my NewMovieView.swift file where the user will be able to insert their Movie information into a TextField, and insert an image from their Photos library. Here is also where I used UIKit.
import SwiftUI
struct NewMovieView: View {
#Binding var isShow: Bool
#Binding var addMovie: [MovieAdd]
#State var newMovieName: String = ""
#State var isShowingImagePicker = false
#State var imageInBlackBox = UIImage()
var body: some View {
ScrollView {
VStack {
VStack (alignment: .leading) {
HStack {
Text("Add a New Movie")
.font(.system(.title, design: .rounded))
.bold()
}
ZStack {
VStack {
HStack (alignment: .center){
Spacer()
Image(uiImage: imageInBlackBox)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.border(Color.black, width: 3)
.clipped()
Spacer()
}
VStack {
Spacer()
Button(action: {
self.isShowingImagePicker.toggle()
}, label: {
Text("Select Image")
.font(.system(size: 15))
})
.sheet(isPresented: $isShowingImagePicker, content: { ImagePickerView(isPresented: $isShowingImagePicker, selectedImage: $imageInBlackBox)})
}
}
}
Group {
TextField("Enter the movie name", text: $newMovieName)
.padding()
.background(Color(.systemGray6))
}
Button(action: {
if self.newMovieName.trimmingCharacters(in: .whitespaces) == "" {
return
}
if self.isShowingImagePicker {
return
}
self.isShow = false
self.addMovieTask(movieName: self.newMovieName, movieImage: ImagePickerView(isPresented: $isShowingImagePicker, selectedImage: $imageInBlackBox))
}) {
Text("Save")
.font(.system(.headline, design: .rounded))
.foregroundColor(.red)
}
}
}
.background(Color.white)
}
}
private func addMovieTask(movieName: String, isComplete: Bool = false, movieImage: ImagePickerView) {
let task = MovieAdd(movieName: movieName, movieImage: movieImage)
addMovie.append(task)
}
}
struct NewMovieView_Previews: PreviewProvider {
static var previews: some View {
NewMovieView(isShow: .constant(true), addMovie: .constant([]), newMovieName: "", isShowingImagePicker: true)
}
}
struct ImagePickerView: UIViewControllerRepresentable {
#Binding var isPresented: Bool
#Binding var selectedImage: UIImage
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePickerView>) -> some UIViewController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
return controller
}
func makeCoordinator() -> ImagePickerView.Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePickerView
init(parent: ImagePickerView){
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[.originalImage] as? UIImage {
print(selectedImage)
self.parent.selectedImage = selectedImage
}
self.parent.isPresented = false
}
}
func updateUIViewController(_ uiViewController: ImagePickerView.UIViewControllerType, context: UIViewControllerRepresentableContext<ImagePickerView>) {
//
}
}
Change #1:
Your model should usually be a struct unless there's a really compelling reason to make it an ObservableObject. In this case, struct works very well:
struct MovieAdd: Identifiable {
var id = UUID()
var movieName = ""
var isComplete : Bool = false
var movieImage : UIImage
}
Note that I've made movieImage a UIImage.
Change #2:
Use Image(uiImage:) in MovieRow. The MovieAdd property no longer needs #ObservableObject since it's just a struct.
Also notice that types in Swift should be capitalized to follow convention).
struct MovieRow: View {
var movieAdd : MovieAdd
var body: some View {
VStack {
Image(uiImage: movieAdd.movieImage)
.resizable()
.frame(width: 100, height: 100)
Text(movieAdd.movieName)
}
}
}
Complete code in case I forgot to mention any other changes:
struct ContentView: View {
#State var movieAdd: [MovieAdd] = []
#State private var newMovieName: String = ""
#State private var showNewMovie = false
#State private var newMovieImage = UIImage()
var body: some View {
ZStack {
VStack {
HStack {
Text("Movies Watched Ratings")
.font(.system(size: 40, weight: .black, design: .rounded
))
Spacer()
Button(action: {
self.showNewMovie = true
}) {
Image(systemName: "plus.circle.fill")
.font(.largeTitle)
.foregroundColor(.yellow)
}
}
List{
ForEach(movieAdd) {movie in
MovieRow(movieAdd: movie)
}
}
}
if showNewMovie {
BlankView(bGColor: .black)
.opacity(0.5)
.onTapGesture {
self.showNewMovie = false
}
NewMovieView(isShow: $showNewMovie, addMovie: $movieAdd, newMovieName: newMovieName)
.transition(.move(edge: .bottom))
.animation(.interpolatingSpring(stiffness: 200.0, damping: 25.0, initialVelocity: 10.0))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct MovieRow: View {
var movieAdd : MovieAdd
var body: some View {
VStack {
Image(uiImage: movieAdd.movieImage)
.resizable()
.frame(width: 100, height: 100)
Text(movieAdd.movieName)
}
}
}
struct BlankView: View {
var bGColor: Color
var body: some View {
VStack {
Spacer()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(bGColor)
.edgesIgnoringSafeArea(.all)
}
}
struct MovieAdd: Identifiable {
var id = UUID()
var movieName = ""
var isComplete : Bool = false
var movieImage : UIImage
}
struct NewMovieView: View {
#Binding var isShow: Bool
#Binding var addMovie: [MovieAdd]
#State var newMovieName: String = ""
#State var isShowingImagePicker = false
#State var imageInBlackBox = UIImage()
var body: some View {
ScrollView {
VStack {
VStack (alignment: .leading) {
HStack {
Text("Add a New Movie")
.font(.system(.title, design: .rounded))
.bold()
}
ZStack {
VStack {
HStack (alignment: .center){
Spacer()
Image(uiImage: imageInBlackBox)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.border(Color.black, width: 3)
.clipped()
Spacer()
}
VStack {
Spacer()
Button(action: {
self.isShowingImagePicker.toggle()
}, label: {
Text("Select Image")
.font(.system(size: 15))
})
.sheet(isPresented: $isShowingImagePicker, content: { ImagePickerView(isPresented: $isShowingImagePicker, selectedImage: $imageInBlackBox)})
}
}
}
Group {
TextField("Enter the movie name", text: $newMovieName)
.padding()
.background(Color(.systemGray6))
}
Button(action: {
if self.newMovieName.trimmingCharacters(in: .whitespaces) == "" {
return
}
if self.isShowingImagePicker {
return
}
self.isShow = false
self.addMovieTask(movieName: self.newMovieName, movieImage: ImagePickerView(isPresented: $isShowingImagePicker, selectedImage: $imageInBlackBox))
}) {
Text("Save")
.font(.system(.headline, design: .rounded))
.foregroundColor(.red)
}
}
}
.background(Color.white)
}
}
private func addMovieTask(movieName: String, isComplete: Bool = false, movieImage: ImagePickerView) {
let task = MovieAdd(movieName: movieName, movieImage: movieImage.selectedImage)
addMovie.append(task)
}
}
struct NewMovieView_Previews: PreviewProvider {
static var previews: some View {
NewMovieView(isShow: .constant(true), addMovie: .constant([]), newMovieName: "", isShowingImagePicker: true)
}
}
struct ImagePickerView: UIViewControllerRepresentable {
#Binding var isPresented: Bool
#Binding var selectedImage: UIImage
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePickerView>) -> some UIViewController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
return controller
}
func makeCoordinator() -> ImagePickerView.Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePickerView
init(parent: ImagePickerView){
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[.originalImage] as? UIImage {
print(selectedImage)
self.parent.selectedImage = selectedImage
}
self.parent.isPresented = false
}
}
func updateUIViewController(_ uiViewController: ImagePickerView.UIViewControllerType, context: UIViewControllerRepresentableContext<ImagePickerView>) {
//
}
}

SwiftUI: Interaction between Views

I have the following problem and i'm completely new to the complete swift/swiftui-"thing".
I have a ContentView containing a MapView and MapViewControls - when pressing the buttons in MapViewControls the MapView should change to show a specific annotation centered on my map (2nd button is for centering on User Location).
How can i accomplish?
ContentView:
import SwiftUI
import MapKit
struct ContentView: View
{
#State private var expandedInfoView: Bool = false
var body: some View
{
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom))
{
GeometryReader
{
proxy in
VStack(alignment: .center)
{
MapView()
VStack
{
HStack
{
Spacer()
MapViewControls()
.padding(.top, -700)
}
}
LogoView()
.onTapGesture
{
withAnimation
{
expandedInfoView.toggle()
}
}
.padding(.top, -110)
}
.sheet(isPresented: $expandedInfoView)
{
InfoView()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider
{
static var previews: some View
{
ContentView()
}
}
MapView:
import SwiftUI
import MapKit
import CoreLocation
struct MapView: UIViewRepresentable
{
let fszCoordinates = CLLocation(latitude: XXX, longitude: YYY)
var locationManager = CLLocationManager()
func setupManager()
{
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.requestAlwaysAuthorization()
}
func makeUIView(context: Context) -> MKMapView
{
setupManager()
let fszAnnotation = MKPointAnnotation()
fszAnnotation.coordinate = CLLocationCoordinate2D(latitude: fszCoordinates.coordinate.latitude, longitude: fszCoordinates.coordinate.longitude)
fszAnnotation.title = "TITLE"
fszAnnotation.subtitle = "SUBTITLE"
let mapView = MKMapView(frame: UIScreen.main.bounds)
mapView.showsUserLocation = true
mapView.userTrackingMode = .followWithHeading
mapView.showsScale = true
mapView.showsTraffic = true
mapView.showsCompass = true
mapView.showAnnotations([fszAnnotation], animated: true)
let buttonLocateUser = MKUserTrackingButton(mapView: mapView)
buttonLocateUser.layer.backgroundColor = UIColor(white: 1, alpha: 0.8).cgColor
buttonLocateUser.layer.borderColor = UIColor.white.cgColor
buttonLocateUser.layer.borderWidth = 1
buttonLocateUser.layer.cornerRadius = 5
buttonLocateUser.translatesAutoresizingMaskIntoConstraints = false
mapView.addSubview(buttonLocateUser)
let scale = MKScaleView(mapView: mapView)
scale.legendAlignment = .trailing
scale.translatesAutoresizingMaskIntoConstraints = false
mapView.addSubview(scale)
NSLayoutConstraint.activate([buttonLocateUser.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -25),
buttonLocateUser.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -10),
scale.trailingAnchor.constraint(equalTo: buttonLocateUser.leadingAnchor, constant: -10),
scale.centerYAnchor.constraint(equalTo: buttonLocateUser.centerYAnchor)])
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context)
{
}
}
struct MapView_Previews: PreviewProvider
{
static var previews: some View
{
MapView()
}
}
MapViewControls:
import SwiftUI
struct MapViewControls: View
{
var body: some View
{
VStack(spacing: 6)
{
VStack(spacing: 12)
{
Button
{
buttonActionZoomToFSZIT()
}
label:
{
Image(systemName: "house.circle")
}
Divider()
Button
{
buttonActionZoomToUser()
}
label:
{
Image(systemName: "location.circle")
}
}
.frame(width: 40)
.padding(.vertical, 12)
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(8)
}
.font(.system(size: 20))
.foregroundColor(.blue)
.padding()
.shadow(color: Color(UIColor.black.withAlphaComponent(0.1)), radius: 4)
}
func buttonActionZoomToFSZIT()
{
print("button tapped")
}
func buttonActionZoomToUser()
{
print("button tapped")
}
}
struct MapViewControls_Previews: PreviewProvider
{
static var previews: some View
{
MapViewControls()
}
}
Nevermind, i solved it by myself after #vadian's comment (thank you mate).
ContentView:
import SwiftUI
import MapKit
struct ContentView: View
{
#State private var expandedInfoView: Bool = false
#State private var buttonDecision: Int = 0
var body: some View
{
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom))
{
GeometryReader
{
proxy in
VStack(alignment: .center)
{
MapView(mapCenter: $buttonDecision)
VStack
{
HStack
{
Spacer()
MapViewControls(mapCenter: $buttonDecision)
.padding(.top, -700)
}
}
LogoView()
.onTapGesture
{
withAnimation
{
expandedInfoView.toggle()
}
}
.padding(.top, -150)
}
.sheet(isPresented: $expandedInfoView)
{
InfoView()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider
{
static var previews: some View
{
ContentView()
}
}
MapView:
import SwiftUI
import MapKit
import CoreLocation
struct MapView: UIViewRepresentable
{
#Binding var mapCenter: Int
let fszCoordinates = CLLocation(latitude: XXX, longitude: YYY)
var locationManager = CLLocationManager()
func setupManager()
{
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.requestAlwaysAuthorization()
}
func makeUIView(context: Context) -> MKMapView
{
setupManager()
let fszAnnotation = MKPointAnnotation()
fszAnnotation.coordinate = CLLocationCoordinate2D(latitude: fszCoordinates.coordinate.latitude, longitude: fszCoordinates.coordinate.longitude)
fszAnnotation.title = "TITLE"
fszAnnotation.subtitle = "SUBTITLE"
let mapView = MKMapView(frame: UIScreen.main.bounds)
mapView.userTrackingMode = .follow
mapView.showsScale = true
mapView.showsCompass = true
mapView.setCenter(CLLocationCoordinate2D(latitude: fszCoordinates.coordinate.latitude, longitude: fszCoordinates.coordinate.longitude), animated: true)
mapView.showAnnotations([fszAnnotation], animated: false)
let buttonLocateUser = MKUserTrackingButton(mapView: mapView)
buttonLocateUser.layer.backgroundColor = UIColor(white: 1, alpha: 0.8).cgColor
buttonLocateUser.layer.borderColor = UIColor.white.cgColor
buttonLocateUser.layer.borderWidth = 1
buttonLocateUser.layer.cornerRadius = 5
buttonLocateUser.translatesAutoresizingMaskIntoConstraints = false
mapView.addSubview(buttonLocateUser)
let scale = MKScaleView(mapView: mapView)
scale.legendAlignment = .trailing
scale.translatesAutoresizingMaskIntoConstraints = false
mapView.addSubview(scale)
NSLayoutConstraint.activate([buttonLocateUser.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -25),
buttonLocateUser.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -10),
scale.trailingAnchor.constraint(equalTo: buttonLocateUser.leadingAnchor, constant: -10),
scale.centerYAnchor.constraint(equalTo: buttonLocateUser.centerYAnchor)])
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context)
{
if ($mapCenter.wrappedValue == 0)
{
uiView.setCenter(CLLocationCoordinate2D(latitude: fszCoordinates.coordinate.latitude, longitude: fszCoordinates.coordinate.longitude), animated: true)
}
else if ($mapCenter.wrappedValue == 1)
{
uiView.setCenter(CLLocationCoordinate2D(latitude: uiView.userLocation.coordinate.latitude, longitude: uiView.userLocation.coordinate.longitude), animated: true)
}
}
}
struct MapView_Previews: PreviewProvider
{
#State static var fszCoordinates = 0
static var previews: some View
{
MapView(mapCenter: $fszCoordinates)
}
}
MapViewControls:
import SwiftUI
struct MapViewControls: View
{
#Binding var mapCenter: Int
var body: some View
{
VStack(spacing: 6)
{
VStack(spacing: 12)
{
Button
{
buttonActionZoomToFSZIT()
}
label:
{
Image(systemName: "house.circle")
}
Divider()
Button
{
buttonActionZoomToUser()
}
label:
{
Image(systemName: "location.circle")
}
}
.frame(width: 40)
.padding(.vertical, 12)
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(8)
}
.font(.system(size: 20))
.foregroundColor(.blue)
.padding()
.shadow(color: Color(UIColor.black.withAlphaComponent(0.1)), radius: 4)
}
func buttonActionZoomToFSZIT()
{
self.mapCenter = 0
}
func buttonActionZoomToUser()
{
self.mapCenter = 1
}
}
struct MapViewControls_Previews: PreviewProvider
{
#State static var fszCoordinates = 0
static var previews: some View
{
MapViewControls(mapCenter: $fszCoordinates)
}
}

Having user add multiple Images to SwiftUI view

I am practicing with SwiftUI and making a meme maker which has labels that are produced from a textField and can be moved and resized. I also want to be able to do this with images from the users Photo library. I am able to get one image, but if I try and get more it just replaces the first image. I tried having the images added to an array, but then the images will not show up on the memeImageView.
Image property
#State private var image = UIImage()
Button
Button {
self.isShowPhotoLibrary = true
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
MemeUmageView
var memeImageView: some View {
ZStack {
KFImage(URL(string: meme.url ?? ""))
.placeholder {
ProgressView()
}
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: UIScreen.main.bounds.height / 2.5)
ForEach(addedLabels, id:\.self) { label in
DraggableLabel(text: label)
}
DraggableImage(image: image)
}
.clipped()
}
Attempt with using an array. I also tried making three buttons to add up to three images, each as its own property thinking that the initial property was being overridden.
My image array
#State private var addedImages = [UIImage?]()
Button
Button {
self.isShowPhotoLibrary = true
addedImages.append(image)
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
var memeImageView: some View {
ZStack {
KFImage(URL(string: meme.url ?? ""))
.placeholder {
ProgressView()
}
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: UIScreen.main.bounds.height / 2.5)
ForEach(addedLabels, id:\.self) { label in
DraggableLabel(text: label)
}
ForEach(0..<addedImages.count) { index in
DraggableImage(image: addedImages[index]!)
}
}
.clipped()
}
Where I call MemeImageView.
var body: some View {
VStack(spacing: 12) {
memeImageView
ForEach(0..<(meme.boxCount ?? 0)) { i in
TextField("Statement \(i + 1)", text: $addedLabels[i])
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.gray.opacity(0.25))
.cornerRadius(5)
.onTapGesture {
self.endEditing()
}
}
.padding(.horizontal)
}.onTapGesture {
self.endEditing()
}
// Gets a new Image
Button {
self.isShowPhotoLibrary = true
addedImages.append(image)
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
Spacer()
// Saves Image
Button {
// takes a screenshot and crops it
if let image = memeImageView.takeScreenshot(origin: CGPoint(x: 0, y: UIApplication.shared.windows[0].safeAreaInsets.top + navBarHeight + 1), size: CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 2.5)) {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
presentationMode.wrappedValue.dismiss() // dismisses the view
}
}
label: {
Text("Save image")
.foregroundColor(Color.yellow)
}.frame( width: 150, height: 50)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.red, lineWidth: 3)
)
.navigationBarTitle(meme.name ?? "Meme", displayMode: .inline)
.background(NavBarAccessor { navBar in
self.navBarHeight = navBar.bounds.height
})
}
For Reproducing(as close to how mine actual project is setup):
Content View
import SwiftUI
struct ContentView: View {
var body: some View {
DragImageView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DragImageView:
import SwiftUI
struct DragImageView: View {
//===================
// MARK: Properties
//===================
#State private var addedImages = [UIImage?]()
#State private var isShowPhotoLibrary = false
#State private var image = UIImage()
var body: some View {
VStack(spacing: 12) {
imageView
}
// Gets a new Image
Button {
self.isShowPhotoLibrary = true
addedImages.append(image)
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
Spacer()
}
var imageView: some View {
ZStack {
DraggableImage(image: image)
}
//.clipped()
}
// This will dismiss the keyboard
private func endEditing() {
UIApplication.shared.endEditing()
}
}
// Allows fot the keyboard to be dismissed
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
DraggableImage:
import SwiftUI
struct DraggableImage: View {
// Drag Gesture
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
// Roation Gesture
#State private var rotation: Double = 0.0
// Scale Gesture
#State private var scale: CGFloat = 1.0
// The different states the frame of the label could be
private enum WidthState: Int {
case full, half, third, fourth
}
#State private var widthState: WidthState = .full
#State private var currentWidth: CGFloat = 100 //UIScreen.main.bounds.width
var image: UIImage
var body: some View {
VStack {
Image(uiImage: self.image)
.resizable()
.scaledToFill()
.frame(width: self.currentWidth)
.lineLimit(nil)
}
.scaleEffect(scale) // Scale based on our state
.rotationEffect(Angle.degrees(rotation)) // Rotate based on the state
.offset(x: self.currentPosition.width, // Offset from the drag difference from it's current position
y: self.currentPosition.height)
.gesture(
// Two finger rotation
RotationGesture()
.onChanged { angle in
self.rotation = angle.degrees // keep track of the angle for state
}
// We want it to work with the scale effect, so they could either scale and rotate at the same time
.simultaneously(with:
MagnificationGesture()
.onChanged { scale in
self.scale = scale.magnitude // Keep track of the scale
})
// Update the drags new position to be wherever it was last dragged to. (we don't want to reset it back to it's current position)
.simultaneously(with: DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width,
height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.newPosition = self.currentPosition
})
)
/// Have to do double tap first or else it will never work with the single tap
.onTapGesture(count: 2) {
// Update our widthState to be the next on in the 'enum', or start back at .full
self.widthState = WidthState(rawValue: self.widthState.rawValue + 1) ?? .full
self.currentWidth = UIScreen.main.bounds.width / CGFloat(self.widthState.rawValue)
}
}
}
ImagePicker:
import UIKit
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var selectedImage: UIImage
#Environment(\.presentationMode) private var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.allowsEditing = false
imagePicker.sourceType = sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
parent.selectedImage = image
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
I should add this is to make memes, so the user picked images go on top the view that I save to the camera roll.
I'm not 100% clear on what the exact desired output should be, but this should get you started (explained below):
struct DragImageView: View {
//===================
// MARK: Properties
//===================
#State private var addedImages = [UIImage]()
#State private var isShowPhotoLibrary = false
var bindingForImage: Binding<UIImage> {
Binding<UIImage> { () -> UIImage in
return addedImages.last ?? UIImage()
} set: { (newImage) in
addedImages.append(newImage)
print("Images: \(addedImages.count)")
}
}
var body: some View {
VStack(spacing: 12) {
imageView
}
// Gets a new Image
Button {
self.isShowPhotoLibrary = true
} label: {
Text("Add Image")
.foregroundColor(Color.yellow)
}.sheet(isPresented: $isShowPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: bindingForImage)
}
Spacer()
}
var imageView: some View {
VStack {
ForEach(addedImages, id: \.self) { image in
DraggableImage(image: image)
}
}
}
// This will dismiss the keyboard
private func endEditing() {
UIApplication.shared.endEditing()
}
}
addedImages is now an array of non-optional UIImages
There's a custom Binding for the image picker. When it receives a new image, it appends it to the end of the array.
In var imageView, there's a VStack instead of a ZStack so that multiple images can get displayed (instead of stacked on top of each other) and a ForEach loop to iterate through the images.

Weird behaviour of SwiftUI Picker View when the other view getting new value from AVCaptureDevice API

I'm new to SwiftUI and Combine. What I trying to build is a manual camera app, and there's only 4 UI component:
CaptureButton for making a shot from the camera
FocusPicker for controlling manually camera focus exposure
OffsetView for displaying a level of exposure
CameraPreviewRepresentable for integrating UIKit camera into SwiftUI view
Also added Privacy requests into.Info.plist file from a user to allow camera feature and saving to Apple Photo App
For updating data and passing it to the UI, I'm using CameraViewModel, currentCameraSubject and currentCamera Publisher to showing new values from AVCaptureDevice and setting it to CameraViewModel.
And I'm noticing a really interesting behavior/bug of FocusPicker when I start interacting with it and piking a new focus it constantly get back to started position and when OffsetView is getting a new value each time.
But interesting enough for example when OffsetView has the same value then FocusPicker is doing normal. And I do not know why this is happening. Please help, it's really frustrating to fix for me.
By the way, it will only work on a real device only.
Here's all the code:
import SwiftUI
//#main
//struct StackOverflowCamApp: App {
// var cameraViewModel = CameraViewModel(focusLensPosition: 0)
// let cameraController: CustomCameraController = CustomCameraController()
//
// var body: some Scene {
// WindowGroup {
// ContentView(cameraViewModel: cameraViewModel, cameraController: cameraController)
// }
// }
//}
struct ContentView: View {
#State private var didTapCapture = false
#ObservedObject var cameraViewModel: CameraViewModel
let cameraController: CustomCameraController
var body: some View {
VStack {
ZStack {
CameraPreviewRepresentable(didTapCapture: $didTapCapture, cameraViewModel: cameraViewModel, cameraController: cameraController)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
VStack {
FocusPicker(selectedFocus: $cameraViewModel.focusChoice)
Text(String(format: "%.2f", cameraViewModel.focusLensPosition))
.foregroundColor(.red)
.font(.largeTitle)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.edgesIgnoringSafeArea(.all)
Spacer()
OffsetView(levelValue: cameraViewModel.exposureTargetOffset, height: 100)
.frame(maxWidth: .infinity, alignment: .leading)
CaptureButton(didTapCapture: $didTapCapture)
.frame(width: 100, height: 100, alignment: .center)
.padding(.bottom, 20)
}
}
}
struct CaptureButton: View {
#Binding var didTapCapture : Bool
var body: some View {
Button {
didTapCapture.toggle()
} label: {
Image(systemName: "photo")
.font(.largeTitle)
.padding(30)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.red)
)
}
}
}
struct OffsetView: View {
var levelValue: Float
let height: CGFloat
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.red)
.frame(maxWidth: height / 2, maxHeight: height, alignment: .trailing)
Rectangle()
.foregroundColor(.orange)
.frame(maxWidth: height / 2, maxHeight: height / 20, alignment: .trailing)
.offset(x: 0, y: min(CGFloat(-levelValue) * height / 2, height / 2))
}
}
}
struct FocusPicker: View {
#Binding var selectedFocus: FocusChoice
var body: some View {
Picker(selection: $selectedFocus, label: Text("")) {
ForEach(0..<FocusChoice.allCases.count) {
Text("\(FocusChoice.allCases[$0].caption)")
.foregroundColor(.white)
.font(.subheadline)
.fontWeight(.medium)
.tag(FocusChoice.allCases[$0])
}
.animation(.none)
.background(Color.clear)
.pickerStyle(WheelPickerStyle())
}
.frame(width: 60, height: 200)
.border(Color.gray, width: 5)
.clipped()
}
}
import SwiftUI
import Combine
import AVFoundation
struct CameraPreviewRepresentable: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var didTapCapture: Bool
#ObservedObject var cameraViewModel: CameraViewModel
let cameraController: CustomCameraController
func makeUIViewController(context: Context) -> CustomCameraController {
cameraController.delegate = context.coordinator
return cameraController
}
func updateUIViewController(_ cameraViewController: CustomCameraController, context: Context) {
if didTapCapture {
cameraViewController.didTapRecord()
}
// checking if new value is differnt from the previous value
if cameraViewModel.focusChoice.rawValue != cameraViewController.manualFocusValue {
cameraViewController.manualFocusValue = cameraViewModel.focusChoice.rawValue
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self, cameraViewModel: cameraViewModel)
}
class Coordinator: NSObject, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
let parent: CameraPreviewRepresentable
var cameraViewModel: CameraViewModel
var tokens = Set<AnyCancellable>()
init(_ parent: CameraPreviewRepresentable, cameraViewModel: CameraViewModel) {
self.parent = parent
self.cameraViewModel = cameraViewModel
super.init()
// for showing focus lens position
self.parent.cameraController.currentCamera
.filter { $0 != nil }
.flatMap { $0!.publisher(for: \.lensPosition) }
.assign(to: \.focusLensPosition, on: cameraViewModel)
.store(in: &tokens)
// for showing exposure offset
self.parent.cameraController.currentCamera
.filter { $0 != nil }
.flatMap { $0!.publisher(for: \.exposureTargetOffset) }
.assign(to: \.exposureTargetOffset, on: cameraViewModel)
.store(in: &tokens)
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
parent.didTapCapture = false
if let imageData = photo.fileDataRepresentation(), let image = UIImage(data: imageData) {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
import Combine
import AVFoundation
class CameraViewModel: ObservableObject {
#Published var focusLensPosition: Float = 0
#Published var exposureTargetOffset: Float = 0
#Published var focusChoice: FocusChoice = .infinity
private var tokens = Set<AnyCancellable>()
init(focusLensPosition: Float) {
self.focusLensPosition = focusLensPosition
}
}
enum FocusChoice: Float, CaseIterable {
case infinity = 1
case ft_30 = 0.95
case ft_15 = 0.9
case ft_10 = 0.85
case ft_7 = 0.8
case ft_5 = 0.5
case ft_4 = 0.7
case ft_3_5 = 0.65
case ft_3 = 0.6
case auto = 0
}
extension FocusChoice {
var caption: String {
switch self {
case .infinity: return "∞ft"
case .ft_30: return "30"
case .ft_15: return "15"
case .ft_10: return "10"
case .ft_7: return "7"
case .ft_5: return "5"
case .ft_4: return "4"
case .ft_3_5: return "3.5"
case .ft_3: return "3"
case .auto: return "Auto"
}
}
}
import UIKit
import Combine
import AVFoundation
class CustomCameraController: UIViewController {
var image: UIImage?
var captureSession = AVCaptureSession()
var backCamera: AVCaptureDevice?
var frontCamera: AVCaptureDevice?
lazy var currentCamera: AnyPublisher<AVCaptureDevice?, Never> = currentCameraSubject.eraseToAnyPublisher()
var photoOutput: AVCapturePhotoOutput?
var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
private var currentCameraSubject = CurrentValueSubject<AVCaptureDevice?, Never>(nil)
var manualFocusValue: Float = 1 {
didSet {
guard manualFocusValue != 0 else {
setAutoLensPosition()
return
}
setFocusLensPosition(manualValue: manualFocusValue)
}
}
//DELEGATE
var delegate: AVCapturePhotoCaptureDelegate?
func setFocusLensPosition(manualValue: Float) {
do {
try currentCameraSubject.value!.lockForConfiguration()
currentCameraSubject.value!.focusMode = .locked
currentCameraSubject.value!.setFocusModeLocked(lensPosition: manualValue, completionHandler: nil)
currentCameraSubject.value!.unlockForConfiguration()
} catch let error {
print(error.localizedDescription)
}
}
func setAutoLensPosition() {
do {
try currentCameraSubject.value!.lockForConfiguration()
currentCameraSubject.value!.focusMode = .continuousAutoFocus
currentCameraSubject.value!.unlockForConfiguration()
} catch let error {
print(error.localizedDescription)
}
}
func didTapRecord() {
let settings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: settings, delegate: delegate!)
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
setupCaptureSession()
setupDevice()
setupInputOutput()
setupPreviewLayer()
startRunningCaptureSession()
}
func setupCaptureSession() {
captureSession.sessionPreset = .photo
}
func setupDevice() {
let deviceDiscoverySession =
AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .unspecified)
for device in deviceDiscoverySession.devices {
switch device.position {
case .front:
self.frontCamera = device
case .back:
self.backCamera = device
default:
break
}
}
self.currentCameraSubject.send(self.backCamera)
}
func setupInputOutput() {
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: currentCameraSubject.value!)
captureSession.addInput(captureDeviceInput)
photoOutput = AVCapturePhotoOutput()
captureSession.addOutput(photoOutput!)
} catch {
print(error)
}
}
func setupPreviewLayer() {
self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
let deviceOrientation = UIDevice.current.orientation
cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation(rawValue: deviceOrientation.rawValue)!
self.cameraPreviewLayer?.frame = self.view.frame
self.view.layer.insertSublayer(cameraPreviewLayer!, at: 0)
}
func startRunningCaptureSession() {
captureSession.startRunning()
}
}
Your ContentView gets updated all the time from the Published values. To Fix that we first remove the declaration as ObservedObject from the ViewModel inside the ContentView and declare it like that:
let cameraViewModel: CameraViewModel
Now we will get some errors. For FocusView just use ProxyBinding.
FocusPicker(selectedFocus: Binding<FocusChoice>(
get: {
cameraViewModel.focusChoice
},
set: {
cameraViewModel.focusChoice = $0
}
))
For the updated text, just create another View. Here! we use ObservedObject.
struct TextView: View {
#ObservedObject var cameraViewModel: CameraViewModel
var body: some View {
Text(String(format: "%.2f", cameraViewModel.focusLensPosition))
.foregroundColor(.red)
.font(.largeTitle)
}
}
Same for the OffsetView. Add ObservedObject there
struct OffsetView: View {
#ObservedObject var viewModel : CameraViewModel
let height: CGFloat
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.red)
.frame(maxWidth: height / 2, maxHeight: height, alignment: .trailing)
Rectangle()
.foregroundColor(.orange)
.frame(maxWidth: height / 2, maxHeight: height / 20, alignment: .trailing)
.offset(x: 0, y: min(CGFloat(-viewModel.exposureTargetOffset) * height / 2, height / 2))
}
}
}
The ContentView will then look like that:
struct ContentView: View {
#State private var didTapCapture = false
let cameraViewModel: CameraViewModel
let cameraController: CustomCameraController
var body: some View {
VStack {
ZStack {
CameraPreviewRepresentable(didTapCapture: $didTapCapture, cameraViewModel: cameraViewModel, cameraController: cameraController)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
VStack {
FocusPicker(selectedFocus: Binding<FocusChoice>(
get: {
cameraViewModel.focusChoice
},
set: {
cameraViewModel.focusChoice = $0
}
))
TextView(cameraViewModel: cameraViewModel)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.edgesIgnoringSafeArea(.all)
Spacer()
OffsetView(viewModel: cameraViewModel, height: 100)
.frame(maxWidth: .infinity, alignment: .leading)
CaptureButton(didTapCapture: $didTapCapture)
.frame(width: 100, height: 100, alignment: .center)
.padding(.bottom, 20)
}
}
}
Hence, we do not have any ObservedObject anymore in the ContentView and our Picker just works fine.

SwiftUI Segmented Control selected segment text animation on view refresh

I am experiencing the following animation of the text in the selected segment of Segmented Controls when the View is refreshed after changing some other data in the View:
Is this a bug/feature or is there a way to eliminate this behaviour?
This is the code to reproduce the effect:
import SwiftUI
struct ContentView: View {
let colorNames1 = ["Red", "Green", "Blue"]
#State private var color1 = 0
let colorNames2 = ["Yellow", "Purple", "Orange"]
#State private var color2 = 0
var body: some View {
VStack {
VStack {
Picker(selection: $color1, label: Text("Color")) {
ForEach(0..<3, id: \.self) { index in
Text(self.colorNames1[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Color 1: \(color1)")
}
.padding()
VStack {
Picker(selection: $color2, label: Text("Color")) {
ForEach(0..<3, id: \.self) { index in
Text(self.colorNames2[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Color 2: \(color2)")
}
.padding()
}
}
}
This was run under iOS 13.4 / Xcode 11.4
rearrange you code base ... (this helps SwiftUI to "refresh" only necessary Views)
import SwiftUI
struct ContentView: View {
let colorNames1 = ["Red", "Green", "Blue"]
#State private var color1 = 0
let colorNames2 = ["Yellow", "Purple", "Orange"]
#State private var color2 = 0
var body: some View {
VStack {
MyPicker(colorNames: colorNames1, color: $color1)
.padding()
MyPicker(colorNames: colorNames2, color: $color2)
.padding()
}
}
}
struct MyPicker: View {
let colorNames: [String]
#Binding var color: Int
var body: some View {
VStack {
Picker(selection: $color, label: Text("Color")) {
ForEach(0..<colorNames.count) { index in
Text(self.colorNames[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Color 1: \(color)")
}
}
}
struct ContetView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
result
I created a custom SegmentControl to solve this problem:
import SwiftUI
struct MyTextPreferenceKey: PreferenceKey {
typealias Value = [MyTextPreferenceData]
static var defaultValue: [MyTextPreferenceData] = []
static func reduce(value: inout [MyTextPreferenceData], nextValue: () -> [MyTextPreferenceData]) {
value.append(contentsOf: nextValue())
}
}
struct MyTextPreferenceData: Equatable {
let viewIndex: Int
let rect: CGRect
}
struct SegmentedControl : View {
#Binding var selectedIndex: Int
#Binding var rects: [CGRect]
#Binding var titles: [String]
var body: some View {
ZStack(alignment: .topLeading) {
SelectedView()
.frame(width: rects[selectedIndex].size.width - 4, height: rects[selectedIndex].size.height - 4)
.offset(x: rects[selectedIndex].minX + 2, y: rects[selectedIndex].minY + 2)
.animation(.easeInOut(duration: 0.5))
VStack {
self.addTitles()
}.onPreferenceChange(MyTextPreferenceKey.self) { preferences in
for p in preferences {
self.rects[p.viewIndex] = p.rect
}
}
}.background(Color(.red)).clipShape(Capsule()).coordinateSpace(name: "CustomSegmentedControl")
}
func totalSize() -> CGSize {
var totalSize: CGSize = .zero
for rect in rects {
totalSize.width += rect.width
totalSize.height = rect.height
}
return totalSize
}
func addTitles() -> some View {
HStack(alignment: .center, spacing: 8, content: {
ForEach(0..<titles.count) { index in
return SegmentView(selectedIndex: self.$selectedIndex, label: self.titles[index], index: index, isSelected: self.segmentIsSelected(selectedIndex: self.selectedIndex, segmentIndex: index))
}
})
}
func segmentIsSelected(selectedIndex: Int, segmentIndex: Int) -> Binding<Bool> {
return Binding(get: {
return selectedIndex == segmentIndex
}) { (value) in }
}
}
struct SegmentView: View {
#Binding var selectedIndex: Int
let label: String
let index: Int
#Binding var isSelected: Bool
var body: some View {
Text(label)
.padding(.vertical, 6)
.padding(.horizontal, 10)
.foregroundColor(Color(.label))
.background(MyPreferenceViewSetter(index: index)).onTapGesture {
self.selectedIndex = self.index
}
}
}
struct MyPreferenceViewSetter: View {
let index: Int
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: MyTextPreferenceKey.self,
value: [MyTextPreferenceData(viewIndex: self.index, rect: geometry.frame(in: .named("CustomSegmentedControl")))])
}
}
}
struct SelectedView: View {
var body: some View {
Capsule()
.fill(Color(.systemBackground))
.edgesIgnoringSafeArea(.horizontal)
}
}
result