Here's my model:
struct ChatRoom: Identifiable, Equatable {
static func == (lhs: ChatRoom, rhs: ChatRoom) -> Bool {
lhs.id == rhs.id
}
struct LastMessage: Codable {
let isSeen: Bool
var isOfCurrentUser: Bool?
let createdAt: Date
let senderId, text: String
}
let id: String
let userIds: [String]
var lastMessage: LastMessage
let otherUser: User
let currentUserAvatarObject: [String : Any]?
let isTyping: Bool
var blockerIds: [String]
let archiverIds: [String]
let messages: Int
let senderMessages: Int
let receiverMessages: Int
}
I have the ChatRoomsListView view which initializes its ViewModel like so:
struct ChatRoomsListView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
VStack {
HStack {
Button {
} label: {
Image(systemName: "camera")
.scaledToFit()
.foregroundColor(.white)
.frame(width: 30, height: 30)
.padding(6)
.background(
Circle().foregroundColor(.white.opacity(0.15))
)
}
Spacer()
Text("Chats")
.foregroundColor(.white)
.offset(x: -20)
Spacer()
}
ScrollView(.vertical) {
LazyVStack {
ForEach(viewModel.chatRooms) { chatRoom in
ChatRoomView(chatRoom: chatRoom)
}
}
}
}.padding(.vertical, 44)
if viewModel.chatRooms.isEmpty {
Text("It seems you haven’t chatted with anyone yet! That’s ok!")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
}
}
}
private struct ChatRoomView: View {
let chatRoom: ChatRoom
private var text: String {
chatRoom.isTyping ? NSLocalizedString("Typing...", comment: "") : chatRoom.lastMessage.text
}
private var hasUnseenMessage: Bool {
!chatRoom.lastMessage.isSeen && chatRoom.lastMessage.isOfCurrentUser == false
}
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
VStack(spacing: 0) {
HStack {
if hasUnseenMessage {
Circle()
.frame(width: 10, height: 10)
.foregroundColor(.blue)
}
VStack(alignment: .leading) {
Text(chatRoom.otherUser.username)
.foregroundColor(.white)
Text(text)
.foregroundColor(.white)
.lineLimit(1)
}
Spacer()
if chatRoom.lastMessage.isSeen && chatRoom.lastMessage.isOfCurrentUser == true {
Image(systemName: "checkmark.circle")
.foregroundColor(.white)
}
}.padding()
}
}
}
}
And here’s the ViewModel:
extension ChatRoomsListView {
class ViewModel: ObservableObject {
#Published var chatRooms = [ChatRoom]()
// -----------------------------
#Injected private var chatRoomsService: ChatRoomsRepository
#Injected private var currentUserService: CurrentUserRepository
// -----------------------------
init() {
getChatRooms()
subscribeToChatRoomUpdates()
}
private func getChatRooms() {
guard let currentUser = currentUserService.user else { return }
chatRoomsService.getChatRooms(currentUser: currentUser) { [weak self] chatRooms in
self?.chatRooms = chatRooms
}
}
private func subscribeToChatRoomUpdates() {
guard let currentUser = currentUserService.user else { return }
chatRoomsService.subscribeToChatRoomUpdates(currentUser: currentUser) { [weak self] chatRooms in
DispatchQueue.main.async {
for chatRoom in chatRooms {
if let index = self?.chatRooms.firstIndex(where: { $0.id == chatRoom.id }) {
self?.chatRooms[index] = chatRoom
} else {
self?.chatRooms.insert(chatRoom, at: 0)
}
}
self?.chatRooms.removeAll(where: { $0.archiverIds.contains(currentUser.id) })
self?.chatRooms.sort(by: { $0.lastMessage.createdAt > $1.lastMessage.createdAt })
}
}
}
}
}
My problem is that once the getChatRooms is called and changes the chatRooms array for the first time, after that, every time the subscribeToChatRoomUpdates is called doesn't redraw the ChatRoomView child view. On the other hand, ChatRoomsListView gets updated properly.
Why is that happening?
I think your implementation of Equatable - where it only compares id has broken the ability for the ChatView to detect a change to the let chatRoom: ChatRoom, so SwiftUI doesn't call body because the ids are unchanged between the old value and the new value. Try removing Equatable.
By the way, you need to unsubscribe to updates in the object's deinit or you might get a crash. And your use of self? is problematic, look into retain cycles in blocks.
Related
well so the problem: when i open the EditViewScreen i need to get the title(-text) and body(-text) of that post. But I always get the title and body of the first cell.
then I didn’t understand the meaning of this post either: 2022-06-03 06:52:57.858609+0500 SwiftuiMVVM[4334:105229] [Presentation] Attempt to present <TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView: 0x127e734d0> on <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_: 0x127e0ac70> (from <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentGS1_VVS_22_VariadicView_Children7ElementVS_24NavigationColumnModifier_GVS_18StyleContextWriterVS_19SidebarStyleContext__: 0x127e12bc0>) which is already presenting <TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView: 0x12a604820>.
HomeViewScreen()
ZStack {
List {
ForEach(viewModel.posts, id: \.self) { post in
PostCell(post: post).onLongPressGesture {
showingEdit.toggle()
}.sheet(isPresented: $showingEdit) {
EditViewScreen(post: post)
}
}
.onDelete(perform: delete)
}.listStyle(.plain)
if viewModel.isLoading {
ProgressView()
}
}
EditViewScreen()
import SwiftUI
struct EditViewScreen: View {
#ObservedObject var viewModel = EditViewModel()
#Environment(\.presentationMode) var presentation
var post: Post
#State var p_title = ""
#State var p_body = ""
func updatePost() {
let post = Post(id: post.id!, title: p_title, body: p_body)
viewModel.apiPostUpdate(post: post, handler: { isUpdated in
if isUpdated {
presentation.wrappedValue.dismiss()
}
})
}
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 10) {
TextField("Edit title", text: $p_title)
.frame(height: 60)
.padding(.leading)
.background(.gray.opacity(0.1))
.cornerRadius(10)
.font(.system(size: 18))
TextField("Edit body", text: $p_body)
.frame(height: 60)
.padding(.leading)
.background(.gray.opacity(0.1))
.cornerRadius(10)
.font(.system(size: 18))
Button(action: {
updatePost()
}, label: {
Spacer()
Text("Update")
Spacer()
})
.padding()
.frame(height: 60)
.background(.black.opacity(0.7))
.foregroundColor(.white)
.cornerRadius(10)
.font(.system(size: 18))
Spacer()
}
.padding(.leading)
.padding(.trailing)
.padding(.top, -35)
if viewModel.isLoading {
ProgressView()
}
}
.navigationBarItems(leading: Button(action: {
presentation.wrappedValue.dismiss()
}, label: {
Text("Cancel")
.foregroundColor(.black)
.font(.system(size: 18, weight: .light))
}))
}.onAppear {
p_title = post.title!
p_body = post.body!
}
}
}
Well, some shortcomings have passed. I've made changes to the HomeViewModel().
class HomeViewModel: ObservableObject {
#Published var isLoading = false
#Published var posts = [Post]()
#Published var post = Post() // <-- here
func apiPostList() {
isLoading = true
AFHttp.get(url: AFHttp.API_POST_LIST, params: AFHttp.paramsEmpty(), handler: { response in
self.isLoading = false
switch response.result {
case .success:
let posts = try! JSONDecoder().decode([Post].self, from: response.data!)
self.posts = posts
case let .failure(error):
print(error)
}
})
}
}
HomeViewScreen()
struct HomeViewScreen: View {
#ObservedObject var viewModel = HomeViewModel()
#State private var showingEdit = false
var body: some View {
NavigationView {
ZStack {
List {
ForEach(viewModel.posts, id: \.self) { post in
PostCell(post: post)
.onLongPressGesture {
showingEdit.toggle()
viewModel.post = post // <-- here
}.sheet(isPresented: $showingEdit) {
EditViewScreen(post: viewModel.post)
// 'll be viewmodel.post, not just the post itself
}
}
.onDelete(perform: delete)
}
.listStyle(.plain)
if viewModel.isLoading {
ProgressView()
}
}
Thank you for your attention(awa #workingdog)
try this approach using #StateObject var viewModel in your HomeViewScreen,
and passing that to EditViewScreen. In addition use .sheet(item: ...)
outside the ForEach loop, as in the example code. Importantly never
use forced unwrapping, that is, do not use ! in your code at all, use if let ... or guard ....
struct HomeViewScreen: View {
#StateObject var viewModel = EditViewModel() // <-- here
#State var selectedPost: Post? // <-- here
var body: some View {
ZStack {
List {
ForEach(viewModel.posts, id: \.self) { post in
PostCell(post: post).onLongPressGesture {
selectedPost = post // <-- here
}
}
.onDelete(perform: delete)
}.listStyle(.plain)
// -- here
.sheet(item: $selectedPost) { post in
EditViewScreen(post: post, viewModel: viewModel) // <-- here
}
if viewModel.isLoading {
ProgressView()
}
}
}
}
struct EditViewScreen: View {
#ObservedObject var viewModel: EditViewModel // <-- here
#Environment(\.presentationMode) var presentation
var post: Post
#State var p_title = ""
#State var p_body = ""
//....
I have the view below which has a list of tasks with completed/incomplete sections but when trying to edit one of the items of the list, the last item of the section is always selected and the delete is not even allowed. It looks when testing that all records inside the section are one single record so the delete slide doesn't work and when editing it get the last record. Below the complete code so you can try to help me identify where the issue is.
import SwiftUI
import CoreData
import UserNotifications
struct ListView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var viewContext
#FetchRequest(fetchRequest: Task.taskList(),animation: .default) private var items: FetchedResults<Task>
#State var isAddFormPresented: Bool = false
#State var taskToEdit: Task?
init(predicate: NSPredicate?, sortDescriptor: NSSortDescriptor) {
let fetchRequest = NSFetchRequest<Task>(entityName: Task.entity().name ?? "Task")
fetchRequest.sortDescriptors = [sortDescriptor]
if let predicate = predicate {
fetchRequest.predicate = predicate
}
_items = FetchRequest(fetchRequest: fetchRequest)
UITableViewCell.appearance().backgroundColor = .white
UITableView.appearance().backgroundColor = .white
}
var body: some View {
let data = groupedEntries(self.items)
VStack(alignment: .center) {
if data.isEmpty {
Spacer()
Image("empty")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 250, height: 250)
.clipShape(Circle())
.overlay(Circle().stroke(Color.pink, lineWidth: 2))
Spacer()
} else {
List {
ForEach(data, id: \.self) { (section: [Task]) in
Section(header: Text(section[0].isComplete == false ? "Incomplete" : "Completed")
.font(.body)
.foregroundColor(section[0].isComplete == false ? Color.pink : Color.green)
){
self.completedView(section: section)
}
.sheet(item: $taskToEdit, onDismiss: {
self.taskToEdit = nil
}) { task in
TaskFormView(
taskToEdit: task,
name: task.name!,
taskDetails: task.taskDetails ?? "",
important: TaskType2(rawValue: task.important ?? "") ?? .none,
urgent: TaskType(rawValue: task.urgent ?? "") ?? .none,
secondaryCategory: Category(rawValue: task.secondaryCategory ?? "") ?? .other,
isComplete: task.isComplete,
dateAdded: task.dateAdded ?? Date()
)
}
}
}
.listStyle(SidebarListStyle())
HStack {
Spacer()
Button(action: addTapped) {
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.shadow(radius: 20)
}
.padding(.trailing, 40)
.padding(.bottom, 24)
.accentColor(Color(UIColor.systemRed))
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(UIColor.systemYellow).opacity(0.5))
.edgesIgnoringSafeArea(.all)
.sheet(isPresented: $isAddFormPresented) {
TaskFormView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
}
func groupedEntries(_ result : FetchedResults<Task>) -> [[Task]] {
return Dictionary(grouping: result) { (element : Task) in
element.isComplete
}
.values.sorted() { $0[0].dateAdded! < $1[0].dateAdded! }
}
func completedView(section: [Task]) -> some View {
ForEach(section, id: \.id) { task in
Button(action: {
taskToEdit = task
}) {
HStack {
CategoryRowView(category: Category(rawValue: task.secondaryCategory!)!, dateAdded: task.dateAdded!)
VStack(alignment: .leading) {
HStack {
if task.isComplete != false {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.pink)
.padding(.top, 10)
.padding(.leading, 5)
}
Text(task.name!)
.font(.system(size: 20, weight: .regular, design: .default))
.foregroundColor(.primary)
.padding(.top, 10)
.padding(.leading, 5)
}
Spacer()
HStack {
Image(systemName: "tag.fill")
.foregroundColor(.secondary)
Text(task.important == "none" ? "Not Important" : "Important")
.font(.system(size: 12, weight: .regular, design: .default))
.foregroundColor(.secondary)
.padding(.vertical)
Text(task.urgent == "none" ? "Not Urgent" : "Urgent")
.font(.system(size: 12, weight: .regular, design: .default))
.foregroundColor(.secondary)
.padding(.vertical)
}
.padding(.leading, 5)
}
.padding(.trailing, 2)
Spacer()
}
.frame(height: 140)
.background(Color(.systemBackground))
.cornerRadius(10)
.shadow(color: .black, radius: 4, x: 0, y: 0)
.padding(.vertical, 5)
}
}
.onDelete { row in
deleteEntry(row: row, in: section)
}
}
func addTapped() {
isAddFormPresented.toggle()
}
private func onReturnTapped() {
self.presentationMode.wrappedValue.dismiss()
}
func deleteEntry(row: IndexSet, in section: [Task]) {
let task = section[row.first!]
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [task.id!.uuidString])
viewContext.delete(task)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd MMM"
return formatter
}()
Here is a simplified version of your View so you can see how a NSFetchedResultsController would do the work. You need a List to be able to swipe a row to delete.
import SwiftUI
import CoreData
class TaskListViewModel: ObservableObject {
let persistenceController = PersistenceController.previewAware()
#Published var fetchedResultsController: NSFetchedResultsController<Task>?
init() {
setupController()
}
func setupController() {
do{
fetchedResultsController = try retrieveFetchedController(sortDescriptors: nil, predicate: nil, sectionNameKeyPath: #keyPath(Task.isComplete))
}catch{
print(error)
}
}
func deleteObject(object: Task) {
persistenceController.container.viewContext.delete(object)
save()
}
func save() {
do {
if persistenceController.container.viewContext.hasChanges{
try persistenceController.container.viewContext.save()
objectWillChange.send()
}else{
}
} catch {
print(error)
}
}
}
//MARK: FetchedResultsController setup
extension TaskListViewModel{
func retrieveFetchedController(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, sectionNameKeyPath: String) throws -> NSFetchedResultsController<Task> {
return try initFetchedResultsController(sortDescriptors: sortDescriptors, predicate: predicate, sectionNameKeyPath: sectionNameKeyPath)
}
private func initFetchedResultsController(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, sectionNameKeyPath: String) throws -> NSFetchedResultsController<Task> {
fetchedResultsController = getFetchedResultsController(sortDescriptors: sortDescriptors, predicate: predicate, sectionNameKeyPath: sectionNameKeyPath)
do {
try fetchedResultsController!.performFetch()
return fetchedResultsController!
} catch {
print( error)
throw error
}
}
func getFetchedResultsController(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, sectionNameKeyPath: String) -> NSFetchedResultsController<Task> {
return NSFetchedResultsController(fetchRequest: getEntityFetchRequest(sortDescriptors: sortDescriptors, predicate: predicate), managedObjectContext: persistenceController.container.viewContext, sectionNameKeyPath: sectionNameKeyPath, cacheName: nil)
}
private func getEntityFetchRequest(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?) -> NSFetchRequest<Task>
{
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
fetchRequest.includesPendingChanges = false
fetchRequest.fetchBatchSize = 20
if sortDescriptors != nil{
fetchRequest.sortDescriptors = sortDescriptors
}else{
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Task.dateAdded), ascending: false)]
}
if predicate != nil{
fetchRequest.predicate = predicate
}
return fetchRequest
}
}
struct TaskListView: View {
#StateObject var vm: TaskListViewModel = TaskListViewModel()
#State var taskToEdit: Task?
var body: some View {
if vm.fetchedResultsController?.sections != nil{
List{
ForEach(0..<vm.fetchedResultsController!.sections!.count){idx in
let section = vm.fetchedResultsController!.sections![idx]
TaskListSectionView(objects: section.objects as? [Task] ?? [], taskToEdit: $taskToEdit, sectionName: section.name).environmentObject(vm)
}
}.sheet(item: $taskToEdit, onDismiss: {
vm.save()
}){editingTask in
TaskEditView(task: editingTask)
}
}else{
Image(systemName: "empty")
}
}
}
struct TaskEditView: View {
#ObservedObject var task: Task
var body: some View {
TextField("name", text: $task.name.bound)
}
}
struct TaskListSectionView: View {
#EnvironmentObject var vm: TaskListViewModel
let objects: [Task]
#State var deleteAlert: Alert = Alert(title: Text("test"))
#State var presentAlert: Bool = false
#Binding var taskToEdit: Task?
var sectionName: String
var body: some View {
Section(header: Text(sectionName) , content: { ForEach(objects, id: \.self){obj in
let task = obj as Task
Button(action: {
taskToEdit = task
}, label: {
Text(task.name ?? "no name")
})
.listRowBackground(Color(UIColor.systemBackground))
}.onDelete(perform: deleteItems)
})
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
deleteAlert = Alert(title: Text("Sure you want to delete?"), primaryButton: Alert.Button.destructive(Text("yes"), action: {
let objs = offsets.map { objects[$0] }
for obj in objs{
vm.deleteObject(object: obj)
}
//Because the objects in the sections aren't being directly observed
vm.objectWillChange.send()
}), secondaryButton: Alert.Button.cancel())
self.presentAlert = true
}
}
}
struct TaskListView_Previews: PreviewProvider {
static var previews: some View {
NavigationView{
TaskListView()
}
}
}
I am trying to create a paged scrollview
I have used a TabView to create this.
here is my code
struct TT: Identifiable {
let id = UUID()
var v: String
}
struct TTest: View {
#State var currentIndex = 0
#State var data = [
TT(v: "0")
]
var body: some View {
VStack {
SwiftUI.TabView(selection: $currentIndex) {
ForEach(data.indexed(), id: \.1.id) { index, value in
TTestConsomer(data: $data[index]).tag(index)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
Spacer()
HStack {
Image(systemName: "plus")
.resizable()
.frame(width: 50, height: 50)
.onTapGesture(perform: add)
Image(systemName: "minus")
.resizable()
.frame(width: 50, height: 5)
.onTapGesture(perform: delete)
}
Text("\(currentIndex + 1)/\(data.count)")
}
}
func add() {
data.append(TT(v: "\(data.count)") )
}
func delete() {
data.remove(at: currentIndex)
}
}
struct TTestConsomer: View {
#Binding var data: TT
var body: some View {
Text(data.v)
.padding(30)
.border(Color.black)
}
}
// You also need this extension
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
When I click on the + button, I can add more tabs.
But when I click on the delete button, My app crashes.
What is the problem here? There does not seem to be anything wrong with the code.
No, I don't think you're doing anything wrong. I submitted an issue to Apple about a related issue on the TabView a few months ago but never heard anything back. The debug comes from the collection view coordinator:
#0 0x00007fff57011ca4 in Coordinator.collectionView(_:cellForItemAt:) ()
It seems like after removing an item, the underlying collection view doesn't get updated. In UIKit, we would probably call .reloadData() to force it to update. In SwiftUI, you could redraw the collection view on the screen to force it to update. Obviously, this affects the UI and isn't a perfect solution.
struct TabViewTest: View {
#State var isLoading: Bool = false
#State var data: [String] = [
"one",
"two"
]
var body: some View {
if !isLoading, !data.isEmpty {
TabView {
ForEach(data, id: \.self) { item in
Text(item)
.tabItem { Text(item) }
}
}
.tabViewStyle(PageTabViewStyle())
.overlay(
HStack {
Text("Add")
.onTapGesture {
add()
}
Text("remove")
.onTapGesture {
remove()
}
}
, alignment: .bottom
)
}
}
func add() {
data.append("three")
print(data)
}
func remove() {
isLoading = true
data.removeFirst()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
isLoading = false
}
print(data)
}
}
struct TabVIewTest_Previews: PreviewProvider {
static var previews: some View {
TabViewTest()
}
}
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()
}
}
Since, the onDelete and onMove are features of List/form I cannot use them when I have custom interfaces without them. I have used a VStack inside a ForEach. I am quite new to swiftUI and unsure on how I can implement custom code for onDelete and onMove.
Here's my code:
struct Trying: View {
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack (spacing: 10) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
.frame(width: 50, height: 50)
.background(Color.red)
}.onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
}
func removeRows(at offsets: IndexSet) {
numbers.remove(atOffsets: offsets)
}
}
The way it works right now:
Here is a simple demo of possible approach to implement custom delete (of course with move it would be more complicated due to drag/drop, but idea is the same). Tested with Xcode 12 / iOS 14.
struct DemoCustomDelete: View {
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack (spacing: 10) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
.frame(width: 50, height: 50)
.background(Color.red)
.overlay(
DeleteButton(number: number, numbers: $numbers, onDelete: removeRows)
, alignment: .topTrailing)
}.onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
}
func removeRows(at offsets: IndexSet) {
withAnimation {
numbers.remove(atOffsets: offsets)
}
}
}
struct DeleteButton: View {
#Environment(\.editMode) var editMode
let number: Int
#Binding var numbers: [Int]
let onDelete: (IndexSet) -> ()
var body: some View {
VStack {
if self.editMode?.wrappedValue == .active {
Button(action: {
if let index = numbers.firstIndex(of: number) {
self.onDelete(IndexSet(integer: index))
}
}) {
Image(systemName: "minus.circle")
}
.offset(x: 10, y: -10)
}
}
}
}
Based on #Asperi's answer, I just generalized it to accept any Equatable sequence.
struct DeleteButton<T>: View where T: Equatable {
#Environment(\.editMode) var editMode
let number: T
#Binding var numbers: [T]
let onDelete: (IndexSet) -> ()
var body: some View {
VStack {
if self.editMode?.wrappedValue == .active {
Button(action: {
if let index = numbers.firstIndex(of: number) {
self.onDelete(IndexSet(integer: index))
}
}) {
Image(systemName: "minus.circle")
}
.offset(x: 10, y: -10)
}
}
}
}
I recently had the need to delete a row and I couldn't use a LIST. Instead I had a scroll view... But I was able to implement the edit to simulate the same onDelete behavior as if it was a list. Initially I couldn't get my code to work. It wasn't until I closely examined my implementation and experimented that I stumbled on why mine worked. I'm coding for an iPad so my NavigationView uses,
.navigationViewStyle(StackNavigationViewStyle())
Once I added this to the struct's NavigationView, when you click on the EditButton it activates editMode?.wrappedValue to .active / .inactive
Below is my implementation for the code sample above...
struct Trying: View {
#State var num: Int = 0
#Environment(\.editMode) var editMode
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack {
ForEach(numbers, id: \.self) { number in
HStack {
if editMode?.wrappedValue == .active {
Button(action: { num = number
removeRows(numbr: num)
}, label: {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
})
} // END IF editMode?wrappedValue == .active
Text("\(number)")
.frame(width: 50, height: 50)
.background(Color.red)
}
}
// .onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
// FOR SOME REASON THIS ALLOWS THE EditButton() to activate editMode without a LIST being present.
.navigationViewStyle(StackNavigationViewStyle())
}
func removeRows(numbr: Int) {
print("removing \(numbr)")
}
}
It looks like: