SwiftUI not updating view based on loader - swiftui

My view is like this
var body: some View {
ScrollView(.vertical) {
VStack(spacing: 10) {
ForEach(0 ..< loader.numberOfRows, id: \.self) { index in
Image("")
.frame(width: 100, height: 100)
.background(Color.random)
.clipped()
}
}
}
}
My loader is like this
final class GiphyLoader {
#Published var data: [GifData] = []
#Published var numberOfRows = 0
init() {
loadImages()
}
func loadImages() {
ImageService.getImages { [weak self] response in
self?.data = response?.data ?? []
guard let count = response?.data.count else {
return
}
self?.numberOfRows = count
print(self?.numberOfRows)
}
}
}
The print gives a count of 25 and nothing is displayed on the screen.
But when I change func loadImages to hard code 25 images then it displays 25 like this
func loadImages() {
numberOfRows = 25
}
How can I dynamically show my views based on what I get from the server?

Try the following:
struct ContentView: View {
#StateObject var loader = GiphyLoader()
var body: some View {
ScrollView(.vertical) {
VStack(spacing: 10) {
ForEach(0 ..< loader.numberOfRows, id: \.self) { index in
Image("")
.frame(width: 100, height: 100)
.background(Color.random)
.clipped()
}
}
}
}
}
If you haven't done so already, be sure to add an extension for Color to get a random color. Something like this will do:
extension Color {
static var random: Color {
return Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
}
And make your loader conform to ObservableObject like below.
final class GiphyLoader: ObservableObject {
#Published var data: [GifData] = []
#Published var numberOfRows = 0
init() {
loadImages()
}
func loadImages() {
ImageService.getImages { [weak self] response in
self?.data = response?.data ?? []
guard let count = response?.data.count else {
return
}
self?.numberOfRows = count
print(self?.numberOfRows)
}
}
}
Then make sure that any subsequent views use the #ObservedObject property wrapper instead of #StateObject, for example: #ObservedObject var viewModel: GiphyLoader.
I can't run it on my end to make sure it works, because I don't have the ImageService function or the GifData array, but this should solve your problems or at least get you going.
Be sure to understand the different property wrappers and their functions.
This Hacking with Swift page should lead you further down the rabbit hole

Related

Why Timer Change Will Trigger LazyVGrid View Update?

import SwiftUI
struct ContentView: View {
#State private var set = Set<Int>()
#State private var count = "10"
private let columns:[GridItem] = Array(repeating: .init(.flexible()), count: 3)
#State private var timer:Timer? = nil
#State private var time = 0
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Array(set)) { num in
Text(String(num))
}
}
}
.frame(width: 400, height: 400, alignment: .center)
HStack{
TextField("Create \(count) items", text: $count)
Button {
createSet(count: Int(count)!)
} label: {
Text("Create")
}
}
if let _ = timer {
Text(String(time))
.font(.title2)
.foregroundColor(.green)
}
HStack {
Button {
time = 100
let timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
time -= 10
if time == 0 {
self.timer?.invalidate()
self.timer = nil
}
}
self.timer = timer
} label: {
Text("Start Timer")
}
Button {
self.timer?.invalidate()
self.timer = nil
} label: {
Text("Stop Timer")
}
}
}
.padding()
}
private func createSet(count:Int) {
set.removeAll(keepingCapacity: true)
repeat {
let num = Int.random(in: 1...10000)
set.insert(num)
} while set.count < count
}
}
extension Int:Identifiable {
public var id:Self { self }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I made a break point on Text(String(num)). Every time the timer was trigger, the GridView updated. Why this happened? As the model of grid didn't change.
Updated
If I put the timer in another view, the grid view wouldn't be trigger.
import SwiftUI
struct ContentView: View {
#State private var set = Set<Int>()
#State private var count = "10"
private let columns:[GridItem] = Array(repeating: .init(.flexible()), count: 3)
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Array(set)) { num in
Text(String(num))
}
}
}
.frame(width: 400, height: 400, alignment: .center)
HStack{
TextField("Create \(count) items", text: $count)
Button {
createSet(count: Int(count)!)
} label: {
Text("Create")
}
}
TimerView()
}
.padding()
}
private func createSet(count:Int) {
set.removeAll(keepingCapacity: true)
repeat {
let num = Int.random(in: 1...10000)
set.insert(num)
} while set.count < count
}
}
extension Int:Identifiable {
public var id:Self { self }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct TimerView: View {
#State private var timer:Timer? = nil
#State private var time = 0
var body: some View {
if let _ = timer {
Text(String(time))
.font(.title2)
.foregroundColor(.green)
}
HStack {
Button {
time = 100
let timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
time -= 10
if time == 0 {
self.timer?.invalidate()
self.timer = nil
}
}
self.timer = timer
} label: {
Text("Start Timer")
}
Button {
self.timer?.invalidate()
self.timer = nil
} label: {
Text("Stop Timer")
}
}
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
TimerView()
}
}
That´s pretty much how SwiftUI works. Every change to a #State var triggers the View to reevaluate. If you put your ForEach in another view it will only reevaluate if you change a var that changes that view. E.g. set or columns.
struct ExtractedView: View {
var columns: [GridItem]
var set: Set<Int>
var body: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Array(set)) { num in
Text(String(num))
}
}
}
.frame(width: 400, height: 400, alignment: .center)
}
}
It is encouraged in SwiftUI to make many small Views. The system driving this is pretty good in identifying what needs to be changed and what not. There is a very good WWDC video describing this.
WWDC

How to update Navigation Link subscript with an array of views inside a ForEach Loop?

I am utilizing a search bar from a Kavsoft Tutorial here: "https://www.youtube.com/watch?v=nuag1PILxCA&t=14s", I'm wondering on how to add navigation links to each of the items, I decided on embedding the itemView inside a navigation link with an array of views to loop through but it seems that it doesn't accept the index as a parameter giving "Cannot convert value of type 'item' to expected argument type 'Int'", instead I incremented the subscript on appear in the navigation link, although that updates the variable, but it doesn't seem to work for the different views themselves only navigating to the first view.
I've linked all the code needed to reproduce the problem but due to my incredibly limited experience in reproducing the problem in as less code as possible, I am not able to do so. Below the main issue of concern is the block starting from the VStack. Starting the program can be done by just adding Search_Bar() to content view body.
struct Home: View {
let views : [AnyView] = [ AnyView(untitled_Skull()), AnyView(dogs()), AnyView(cats()) ]
#Binding var filteredItems : [item]
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
var i = 0
VStack(spacing: 15){
ForEach(filteredItems){index in
NavigationLink(destination: views[i]
) {
itemView(item: index)
}.onAppear() {
i = i + 1
}
}
}
.padding()
}
}
}
func add(value: Int) -> Int {
let value = value + 1
return value
}
struct itemView: View {
var item: item
#State var show = false
var body: some View {
HStack(spacing: 15){
VStack {
let colorArray: [Color] = [.yellowLichtenstien, .redHaring, .orangeBasquiat, .pinkWarhol]
HStack {
Text(item.name)
.foregroundColor(.white)
.bold()
.padding(.leading)
Spacer()
}
HStack {
Text(item.subText)
.bold()
.foregroundColor (.white)
.font(.subheadline)
.padding(.leading)
Circle()
.frame(width: 5, height: 5)
.foregroundColor(colorArray[item.color])
Text(item.subText2)
.bold()
.foregroundColor (.white)
.font(.subheadline)
Spacer()
}
Spacer()
}
}
.padding(.horizontal)
}
}
struct item: Identifiable {
var id = UUID().uuidString
// both Image And Name Are Same....
var name: String
// since all Are Apple Native Apps...
var color: Int
var subText: String
var subText2: String
}
var searchItems = [
item(name: "Untitled (Skull)", color: 0, subText: "1983", subText2: "yay"),
item(name: "Dogs", color: 1, subText: "1972", subText2: "wow"),
item(name: "Cats", color: 2, subText: "1968", subText2: "oof")
]
struct Search_Bar: View {
#State var filteredItems = searchItems
var body: some View {
CustomNavigationView(view: AnyView(Home(filteredItems: $filteredItems)), placeHolder: "Museums, Art or anything else.", largeTitle: true, title: "Search",
onSearch: { (txt) in
if txt != ""{
self.filteredItems = searchItems.filter{$0.name.lowercased().contains(txt.lowercased())}
}
else{
self.filteredItems = searchItems
}
}, onCancel: {
// Do Your Own Code When Search And Canceled....
self.filteredItems = searchItems
})
.ignoresSafeArea()
}
}
struct Search_Bar_Previews: PreviewProvider {
static var previews: some View {
Search_Bar()
}
}
import SwiftUI
struct CustomNavigationView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return CustomNavigationView.Coordinator(parent: self)
}
// Just Change Your View That Requires Search Bar...
var view: AnyView
// Ease Of Use.....
var largeTitle: Bool
var title: String
var placeHolder: String
// onSearch And OnCancel Closures....
var onSearch: (String)->()
var onCancel: ()->()
// requre closure on Call...
init(view: AnyView,placeHolder: String? = "Search",largeTitle: Bool? = true,title: String,onSearch: #escaping (String)->(),onCancel: #escaping ()->()) {
self.title = title
self.largeTitle = largeTitle!
self.placeHolder = placeHolder!
self.view = view
self.onSearch = onSearch
self.onCancel = onCancel
}
// Integrating UIKit Navigation Controller With SwiftUI View...
func makeUIViewController(context: Context) -> UINavigationController {
// requires SwiftUI View...
let childView = UIHostingController(rootView: view)
let controller = UINavigationController(rootViewController: childView)
// Nav Bar Data...
controller.navigationBar.topItem?.title = title
controller.navigationBar.prefersLargeTitles = largeTitle
// search Bar....
let searchController = UISearchController()
searchController.searchBar.placeholder = placeHolder
// setting delegate...
searchController.searchBar.delegate = context.coordinator
// setting Search Bar In NavBar...
// disabling hide on scroll...
// disabling dim bg..
searchController.obscuresBackgroundDuringPresentation = false
controller.navigationBar.topItem?.hidesSearchBarWhenScrolling = false
controller.navigationBar.topItem?.searchController = searchController
return controller
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
// Updating Real Time...
uiViewController.navigationBar.topItem?.title = title
uiViewController.navigationBar.topItem?.searchController?.searchBar.placeholder = placeHolder
uiViewController.navigationBar.prefersLargeTitles = largeTitle
}
// search Bar Delegate...
class Coordinator: NSObject,UISearchBarDelegate{
var parent: CustomNavigationView
init(parent: CustomNavigationView) {
self.parent = parent
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// when text changes....
self.parent.onSearch(searchText)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
// when cancel button is clicked...
self.parent.onCancel()
}
}
}
Letting the random view below for the array being for example:
import SwiftUI
struct cats: View {
var body: some View {
Text("cats") //replacing this with dogs or untitled skull as an example.
}
}
struct cats_Previews: PreviewProvider {
static var previews: some View {
cats()
}
}
You can use ForEach getting the item and its index in the closure :
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: views[index]){
Text(item.name)
}
}
For example :
struct ListItem: Identifiable {
let id = UUID()
let name: String
}
struct SwiftUIView17: View {
#State private var filteredItems = ["John", "Bob", "Maria"].map(ListItem.init)
let views = [AnyView(Text("John destination")), AnyView(Text("Bob destination")), AnyView(Text("Maria destination"))]
var body: some View {
ScrollView {
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: views[index]){
Text(item.name)
}
}
}
}
}
But it would be better not to use AnyView but a ViewBuilder :
struct SwiftUIView17: View {
#State private var filteredItems = ["John", "Bob", "Maria"].map(ListItem.init)
#ViewBuilder func destination(for itemIndex: Int) -> some View {
switch itemIndex {
case 0: Text("John destination")
case 1: Text("Bob destination").foregroundColor(.red)
case 2: Rectangle()
default: Text("error")
}
}
var body: some View {
ScrollView {
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: destination(for: index)){
Text(item.name)
}
}
}
}
}

why does not withAnimation working without .animation

i have a Shake annimation extension like this,
import SwiftUI
struct WRShake: GeometryEffect {
var amount: CGFloat = 10
var shakesPerUnit = 3
var animatableData: CGFloat
func effectValue(size: CGSize) -> ProjectionTransform {
ProjectionTransform(CGAffineTransform(translationX: amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)), y: 0))
}
}
extension View {
func wrshake(amount: CGFloat = 10, shakeUnits: Int = 4, animatableData: CGFloat) -> some View {
return modifier(WRShake(amount: amount, shakesPerUnit: shakeUnits, animatableData: animatableData))
}
}
then in the contentView, i got this
var body: some Scene {
WindowGroup {
NavigationView {
VStack {
Text("111")
.padding()
.background(Color.blue)
.wrshake(animatableData: self.shakeValue)
// .animation(.easeInOut(duration: 0.5))
.layoutPriority(1)
Button {
withAnimation(.easeInOut(duration: 1)) {
self.shakeValue += 1
}
// self.shakeValue += 1
} label: {
Text("shake")
}
}
}
}
then strange thing is,if i don't apply .animation(.easeInOut(duration: 0.5)) to then Text View, there is no animation.
anyone knows why?
thanks
I think because you are using explicit animations on the :App class and that class doesn't handle explicit animations.
Explicit animations is when you use the withAnimation block and leave the system to figure out how the animate.
And i suppose in #main class you did put the #State var shakeValue: CGFloat = 0 but becasuse it's a :App and not a View it doesn't change.
it works adding the .animation(.easeInOut(duration: 0.5)) because it returns a new view that handle the animations like when your extracting the view like ContentView().
(implicit animations are when you apply the modifier .animation(...))
WindowGroup {
NavigationView {
// StartUpView()
ContentView()
}
// .navigationViewStyle(StackNavigationViewStyle())
}
it turns out that, if i wrap everything into ContentView, it works, but why?

How can I create a pulsating opacity wave in a list with SwiftUI?

I have tried with the following code
struct ContentView: View {
#State var show = false
var body: some View {
ScrollView {
LazyVStack(alignment: .center, spacing: 0) {
ForEach(1...100, id: \.self) { index in
if self.show {
Text("Placeholder \(index)")
.padding(24)
.opacity(1)
.transition(
AnyTransition.opacity.animation(
Animation
.easeOut(duration: 0.6)
.delay(Double(index) * 0.15)
.repeatForever(autoreverses: true)
)
)
}
}
}
}.onAppear {
self.show = true
}
}
}
This works fine for the first iteration, but for the next iterations the delay is accumulated wrongly.
Wanted effect (first one on the left). Result (last one on the right).
Here would be a working solution.
The idea is to handle repeating forever manually with a timer.
The scrolling also works:
One just has to wait a bit, since the delay increases linearly with Double(index)*0.15.
Code:
import SwiftUI
import Combine
final class AnimationManager: ObservableObject {
#Published var timer: AnyCancellable!
#Published var show: Bool = false
init() {
timer = Timer.publish(every: 0.75, on: .main, in: .common)
.autoconnect()
.sink { _ in
self.show.toggle()
}
}
}
struct ContentView: View {
#StateObject private var animationManager = AnimationManager()
var body: some View {
ScrollView {
LazyVStack(alignment: .center, spacing: 0) {
ForEach(1...100, id: \.self) { index in
Text("Placeholder \(index)")
.padding(24)
.opacity(animationManager.show ? 1.0 : 0.1)
.animation(Animation.easeOut(duration: 0.6).delay(Double(index)*0.15))
}
}
}
}
}
Additional
You might want to use a VStack instead of LazyVStack. This would get rid of the increasing delay of elements down in the list since they all appear immediately. I don't know what your desired effect is.
Wrapping this in a UIView seems to work and give desired effect
struct PulsatingView<Content: View>: UIViewRepresentable {
var maxOpacity = 0.7
var minOpacity = 0.2
var duration = 0.5
var delay = 0.05
var content: Content
func makeUIView(context: Context) -> ViewContainer<Content> {
let view = ViewContainer(child: content)
return view
}
func updateUIView(_ container: ViewContainer<Content>, context: Context) {
let anim = CABasicAnimation()
anim.fromValue = minOpacity
anim.toValue = maxOpacity
anim.duration = duration
anim.autoreverses = true
anim.timingFunction = .init(name: .easeInEaseOut)
anim.repeatCount = Float.greatestFiniteMagnitude
anim.timeOffset = -delay
anim.keyPath = "opacity"
container.child.rootView = content
container.layer.add(anim, forKey: "pulsating")
}
}
class ViewContainer<Content: View>: UIView {
var child: UIHostingController<Content>
init(child: Content) {
self.child = UIHostingController(rootView: child)
super.init(frame: .zero)
addSubview(self.child.view)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
child.view.bounds = bounds
child.view.center = CGPoint(x: bounds.width/2.0, y: bounds.height/2.0)
}
override var intrinsicContentSize: CGSize {
child.view.sizeThatFits(CGSize(width: Double.greatestFiniteMagnitude, height: Double.greatestFiniteMagnitude))
}
}
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(alignment: .center, spacing: 0) {
ForEach(1...1000, id: \.self) { index in
PulsatingView(
delay: Double(index) * 0.15,
content: {
Text("Placeholder \(index)")
.padding(24)
}()
)
}
}
}
}
}
But there's some bugs with vertical placement when used in conjunction with LazyVStack 🤨

SwiftUI doesn't update state to #ObservedObject cameraViewModel object

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()
}
}