Interacting with a SwiftUI View via ARKit blendshapes - ios

I have a strange bug I can't resolve.
I'm trying to create an app where the user can interact with blend shapes instead of buttons.
I have my ContentView where I define an array of animals of type Card.
What the app do is that when the user blinks, a sheetView is shown. In this sheetView it's displayed a CardView with a random animal taken from the array. The user can play the sound of that specific animal with a button (for now).
The user can also close the sheetView opening his mouth.
I'll show my code
ContentView.swift
import SwiftUI
import AVKit
class ViewModel: ObservableObject {
#Published var changeAnimal = false
#Published var realMouthOpen = false
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
let animals: [Card] = [Card(image: "monkey", heading: "monkey", tag: 1, callSound: "Monkey", animal: .monkey), Card(image: "dog", heading: "dog", tag: 2, callSound: "dog", animal: .dog), Card(image: "chick", heading: "chick", tag: 3, callSound: "chick", animal: .chick)]
var body: some View {
ZStack {
SwiftUIViewController(viewModel: viewModel) // wrapper controller uikit
CardViewPreviewScroll(card: Card(image: "unknown", heading: "random animal", tag: 0, callSound: "", animal: .unknown))
.sheet(isPresented: $viewModel.changeAnimal) { // BUG: the view is changed 2 times
changeAnimal() // l'animale corrente, nel momento in cui cambia, la view viene aggiornata
}
}
}
//MARK: Functions
func changeAnimal() -> (CardView?) {
let animal = animals.randomElement()!
return (CardView(card: animal))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Card.swift
import Foundation
import SwiftUI
enum TypeAnimal: Identifiable {
case monkey, dog, chick, unknown
var id: Int {
hashValue
}
}
struct Card: Hashable{
var image: String
var heading: String
var id = UUID()
var tag: Int
var callSound: String
var animal: TypeAnimal
static let example = Card(image: "chick", heading: "chick", tag: 0, callSound: "chick", animal: .chick)
}
CardView.swift
import SwiftUI
import AVKit
struct CardView: View {
let card: Card
#State var audioPlayer: AVAudioPlayer!
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(.green)
.shadow(radius: 3)
VStack {
Image(card.image)
.resizable()
.aspectRatio(contentMode: .fit)
VStack(alignment: .leading) {
Text(card.heading)
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
HStack {
Spacer()
Button(action: {
self.audioPlayer.play()
}) {
Image(systemName: "play.circle.fill").resizable()
.frame(width: 50, height: 50)
.aspectRatio(contentMode: .fit)
}
Spacer()
Button(action: {
self.audioPlayer.pause()
}) {
Image(systemName: "pause.circle.fill").resizable()
.frame(width: 50, height: 50)
.aspectRatio(contentMode: .fit)
}
Spacer()
}
}
.layoutPriority(100)
.padding()
}
}
.onAppear {
let sound = Bundle.main.path(forResource: card.callSound, ofType: "mp3")
self.audioPlayer = try! AVAudioPlayer(contentsOf: URL(fileURLWithPath: sound!))
}
}
}
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView(card: Card.example)
}
}
And this is my ViewController, wrapped in UIViewControllerRepresentable.
This ViewController conforms to the ARSessionDelegate protocol so that I can change the #Published property defined in my ObservableObject ViewModel and this means changing the $viewModel.changeAnimal binding in .sheet.
The fact is, as the CardView in the sheetView change whenever a blendshape of type browInnerUp is detected, I tried to catch this value only 1 time without finding a solution.
The best thing I managed to do is that, when the sheetView is displayed, an instance of CardView appears and this one is immediately replaced with another one.
I tried with DispatchQueue, timers and other things about treads in order to catch only a single value of the parameter of the blendshape in question but I failed
ViewController.swift
import UIKit
import SwiftUI
import ARKit
class ViewController: UIViewController, ARSessionDelegate {
var viewModel: ViewModel?
var session: ARSession!
override func viewDidLoad() {
super.viewDidLoad()
session = ARSession()
session.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard ARFaceTrackingConfiguration.isSupported else {print("IPhone X required"); return}
let configuration = ARFaceTrackingConfiguration()
session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
}
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
if let faceAnchor = anchors.first as? ARFaceAnchor {
update(withFaceAnchor: faceAnchor)
}
}
func update(withFaceAnchor faceAnchor: ARFaceAnchor) {
let bledShapes:[ARFaceAnchor.BlendShapeLocation:Any] = faceAnchor.blendShapes
guard let jawOpen = bledShapes[.jawOpen] as? Float else { return }
guard let browInnerUp = bledShapes[.browInnerUp] as? Float else { return }
if browInnerUp > 0.85 {
self.session.pause()
self.viewModel?.changeAnimal = true
// print("eyeBlinkLeft: \(eyeBlinkLeft)") // right eye
}
if jawOpen > 0.85 {
self.session.pause()
self.viewModel?.changeAnimal = false
// print("eyeBlinkRight: \(jawOpen)") // left eye
}
}
}
struct SwiftUIViewController: UIViewControllerRepresentable {
var viewModel: ViewModel
func makeUIViewController(context: Context) -> ViewController{
let controller = ViewController()
controller.viewModel = viewModel
return controller
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
}
}
The question is: is there a way to fix this bug or I just messed up?

Related

SwiftUI: How to share Core Data between main app and extension

I am making app that stores some data into CoreData and than show it in on the Custom Keyboard. I have already made everything to store that data and created Custom Keyboard Extension, but I am not sure how can I get that data in this extension. I did something with apps group but still now not sure what am I doing wrong.
My DataController:
import Foundation
import SwiftUI
import CoreData
class DataController: NSObject, ObservableObject {
let container = NSPersistentContainer(name: "DataModel")
let storeDescription = NSPersistentStoreDescription(url: URL.storeURL(for: "group.zo", databaseName: "DataModel"))
override init() {
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores { description, error in
if let error = error {
print("\(error)")
}
}
}
}
public extension URL {
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}
My KeyboardViewController:
import UIKit
import SwiftUI
import CoreData
class KeyboardViewController: UIInputViewController {
#IBOutlet var nextKeyboardButton: UIButton!
override func updateViewConstraints() {
super.updateViewConstraints()
}
override func viewDidLoad() {
super.viewDidLoad()
let child = UIHostingController(rootView: KeyboardView())
child.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
child.view.backgroundColor = .clear
view.addSubview(child.view)
addChild(child)
}
}
My KeyboardView:
import SwiftUI
struct KeyboardView: View {
#EnvironmentObject var dataController: DataController
#FetchRequest(sortDescriptors: []) var snip: FetchedResults<Snip>
var body: some View {
HStack{
VStack(alignment: .leading){
Text("Your Snips")
.font(.system(size: 18, weight: .bold))
if snip.isEmpty {
Text("error")
}else{
ForEach(snip){ snip in
Button {
print(snip.content ?? "error")
} label: {
Text(snip.name ?? "error")
.background(Color(snip.color ?? ""))
}
}
}
Button {
print("siema")
} label: {
Text("test")
.background(Color.red)
}
Spacer()
}.padding()
Spacer()
}
}
}
Please Help!!!

How to setup NavigationLink inside SwiftUI list

I am attempting to set up a SwiftUI weather app. when the user searches for a city name in the textfield then taps the search button, a NavigationLink list item should appear in the list. Then, the user should be able to click the navigation link and re-direct to a detail view. My goal is to have the searched navigation links to populate a list. However, my search cities are not populating in the list, and I'm not sure why. In ContentView, I setup a list with a ForEach function that passes in cityNameList, which is an instance of the WeatherViewModel. My expectation is that Text(city.title) should display as a NavigationLink list item. How should I configure the ContentView or ViewModel to populate the the list with NavigationLink list items? See My code below:
ContentView
import SwiftUI
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityWeather, id: \.id) { city in
NavigationLink(destination: DetailView(detail: viewModel)) {
HStack {
Text(city.cityWeather.name)
.font(.system(size: 32))
}
}
}
}
Spacer()
}
.navigationTitle("Weather MVVM")
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel
import Foundation
class WeatherViewModel: ObservableObject {
//everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
#Published var cityWeather: WeatherModel = WeatherModel()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// get data
guard let data = data, error == nil else {
return
}
//convert data to model
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityWeather = model
}
}
catch {
print(error)
}
}
task.resume()
}
}
Model
import Foundation
struct WeatherModel: Identifiable, Codable {
var id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DetailView
import SwiftUI
struct DetailView: View {
var detail: WeatherViewModel
var body: some View {
VStack(spacing: 20) {
Text(detail.cityWeather.name)
.font(.system(size: 32))
Text("\(detail.cityWeather.main.temp)")
.font(.system(size: 44))
Text(detail.cityWeather.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherViewModel.init())
}
}
try something like this example code, works well for me:
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
}
}
}
}
Spacer()
}.navigationTitle("Weather MVVM")
}.navigationViewStyle(.stack)
}
}
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name).font(.system(size: 32))
Text("\(detail.main.temp)").font(.system(size: 44))
Text(detail.firstWeatherInfo()).font(.system(size: 24))
}
}
}
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}

How to refresh Core Data array when user enters new view with SwiftUI?

I have 3 views. Content View, TrainingView and TrainingList View. I want to list exercises from Core Data but also I want to make some changes without changing data.
In ContentView; I am trying to fetch data with CoreData
struct ContentView: View {
// MARK: - PROPERTY
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Training.timestamp, ascending: false)],
animation: .default)
private var trainings: FetchedResults<Training>
#State private var showingAddProgram: Bool = false
// FETCHING DATA
// MARK: - FUNCTION
// MARK: - BODY
var body: some View {
NavigationView {
Group {
VStack {
HStack {
Text("Your Programs")
Spacer()
Button(action: {
self.showingAddProgram.toggle()
}) {
Image(systemName: "plus")
}
.sheet(isPresented: $showingAddProgram) {
AddProgramView()
}
} //: HSTACK
.padding()
List {
ForEach(trainings) { training in
TrainingListView(training: training)
}
} //: LIST
Spacer()
} //: VSTACK
} //: GROUP
.navigationTitle("Good Morning")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
print("test")
}) {
Image(systemName: "key")
}
}
} //: TOOLBAR
.onAppear() {
}
} //: NAVIGATION
}
private func showId(training: Training) {
guard let id = training.id else { return }
print(id)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
In TrainingView; I am getting exercises as a array list and I am pushing into to TrainingListView.
import SwiftUI
struct TrainingView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State var training: Training
#State var exercises: [Exercise]
#State var tempExercises: [Exercise] = [Exercise]()
#State var timeRemaining = 0
#State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var isTimerOn = false
var body: some View {
VStack {
HStack {
Text("\(training.name ?? "")")
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Finish")
}
}
.padding()
ZStack {
Circle()
.fill(Color.blue)
.frame(width: 250, height: 250)
Circle()
.fill(Color.white)
.frame(width: 240, height: 240)
Text("\(timeRemaining)s")
.font(.system(size: 100))
.fontWeight(.ultraLight)
.onReceive(timer) { _ in
if isTimerOn {
if timeRemaining > 0 {
timeRemaining -= 1
} else {
isTimerOn.toggle()
stopTimer()
removeExercise()
}
}
}
}
Button(action: {
startResting()
}) {
if isTimerOn {
Text("CANCEL")
} else {
Text("GIVE A BREAK")
}
}
Spacer()
ExerciseListView(exercises: $tempExercises)
}
.navigationBarHidden(true)
.onAppear() {
updateBigTimer()
}
}
private func startResting() {
tempExercises = exercises
if let currentExercise: Exercise = tempExercises.first {
timeRemaining = Int(currentExercise.rest)
startTimer()
isTimerOn.toggle()
}
}
private func removeExercise() {
if let currentExercise: Exercise = tempExercises.first {
if Int(currentExercise.rep) == 1 {
let index = tempExercises.firstIndex(of: currentExercise) ?? 0
tempExercises.remove(at: index)
} else if Int(currentExercise.rep) > 1 {
currentExercise.rep -= 1
let index = tempExercises.firstIndex(of: currentExercise) ?? 0
tempExercises.remove(at: index)
tempExercises.insert(currentExercise, at: index)
}
updateBigTimer()
}
}
private func updateBigTimer() {
timeRemaining = Int(tempExercises.first?.rest ?? 0)
}
private func stopTimer() {
timer.upstream.connect().cancel()
}
private func startTimer() {
timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}
}
struct TrainingView_Previews: PreviewProvider {
static var previews: some View {
TrainingView(training: Training(), exercises: [Exercise]())
}
}
In TrainingListView; I am listing all exercises.
struct TrainingListView: View {
#ObservedObject var training: Training
#Environment(\.managedObjectContext) private var managedObjectContext
var body: some View {
NavigationLink(destination: TrainingView(training: training, exercises: training.exercises?.toArray() ?? [Exercise]())) {
HStack {
Text("\(training.name ?? "")")
Text("\(training.exercises?.count ?? 0) exercises")
}
}
}
}
Also, I am adding video: https://twitter.com/huseyiniyibas/status/1388571724346793986
What I want to do is, when user taps any Training Exercises List should refreshed. It should be x5 again like in the beginning.
I had a hard time understanding your question but I guess I got the idea.
My understanding is this:
You want to store the rep count in the Core Data. (Under Training > Exercises)
You want to count down the reps one by one as the user completes the exercise.
But you don't want to change the original rep count stored in the Core Data.
I didn't run your code since I didn't want to recreate all the models and Core Data files. I guess I've spotted the problem. Here I'll explain how you can solve it:
The Core Data models are classes (reference types). When you pass around the classes (as you do in your code) and change their properties, you change the original data. In your case, you don't want that.
(Btw, being a reference type is a very useful and powerful property of classes. Structs and enums are value types, i.e. they are copied when passed around. The original data is unchanged.)
You have several options to solve your problem:
Just generate a different struct (something like ExerciseDisplay) from Exercise, and pass ExerciseDisplay to TrainingView.
You can write an extension to Exercise and "copy" the model before passing it to TrainingView. For this you'll need to implement the NSCopying protocol.
extension Exercise: NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
return Exercise(...)
}
}
But before doing this I guess you'll need to change the Codegen to Manual/None of your entry in your .xcdatamodeld file. This is needed when you want to create the attributes manually. I'm not exactly sure how you can implement NSCopying for a CoreDate model, but it's certainly doable.
The first approach is easier but kinda ugly. The second is more versatile and elegant, but it's also more advanced. Just try the first approach first and move to the second once you feel confident.
Update:
This is briefly how you can implement the 1st approach:
struct ExerciseDisplay: Identifiable, Equatable {
public let id = UUID()
public let name: String
public var rep: Int
public let rest: Int
}
struct TrainingView: View {
// Other properties and states etc.
let training: Training
#State var exercises: [ExerciseDisplay] = []
init(training: Training) {
self.training = training
}
var body: some View {
VStack {
// Views
}
.onAppear() {
let stored: [Exercise] = training.exercises?.toArray() ?? []
self.exercises = stored.map { ExerciseDisplay(name: $0.name ?? "", rep: Int($0.rep), rest: Int($0.rest)) }
}
}
}

SwiftUI: Cannot Delete List Item If Just Viewed

I have a very odd issue. I have a list app that crashes when I delete a list item that I just viewed. I can delete an item that is different than the one I just viewed without the crash. The crash error is:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file /Users/XXX/Documents/Xcode Projects Playground/Test-Camera-CloudKit/Test-Camera-CloudKit/DetailView.swift, line 20
Line 20 of the DetailView.swift file is the line that displays the image/photo [Image(uiImage: UIImage(data: myItem.photo!) ?? UIImage(named: "gray_icon")!)]. Below are the files from my stripped down app to try to run this issue to ground. I am using CoreData and CloudKit.
ContentView.swift:
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Item.entity(), sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)]) var items: FetchedResults<Item>
#State private var showingAddScreen = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: DetailView(myItem: item)) {
HStack {
Text(item.name ?? "Unknown name")
}
}
}.onDelete(perform: delete)
}
.navigationBarTitle("Items")
.navigationBarItems(trailing:
Button(action: {
self.showingAddScreen.toggle()
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $showingAddScreen) {
AddItemView().environment(\.managedObjectContext, self.moc)
}
}
}
func delete(at offsets: IndexSet) {
for index in offsets {
let item = items[index]
moc.delete(item)
}
do {
try moc.save()
} catch {
print("Error deleting objects")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AddItemView.swift:
import SwiftUI
struct AddItemView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
#State private var image : Data = .init(count: 0)
#State private var name = ""
#State private var show = false
var body: some View {
NavigationView {
Form {
Section {
TextField("Name of item", text: $name)
}
Section {
VStack {
Button(action: {self.show = true}) {
HStack {
Image(systemName: "camera")
}
}
Image(uiImage: UIImage(data: self.image) ?? UIImage(named: "gray_icon")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 300, alignment: Alignment.center)
.clipped()
}
.sheet(isPresented: self.$show, content: {
ImagePicker(show: self.$show, image: self.$image)
})
}
Section {
Button("Save") {
let newItem = Item(context: self.moc)
newItem.name = self.name
newItem.photo = self.image
try? self.moc.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Add Item")
}
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
DetailView.swift
import SwiftUI
struct DetailView: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var myItem: Item
var body: some View {
VStack {
Text(myItem.name ?? "Unknown")
Image(uiImage: UIImage(data: myItem.photo!) ?? UIImage(named: "gray_icon")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 300, alignment: Alignment.center)
.clipped()
}
.navigationBarTitle(Text(myItem.name ?? "Unknown"), displayMode: .inline)
}
}
struct DetailView_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let item = Item(context: moc)
item.name = "Test"
return NavigationView {
DetailView(myItem: item)
}
}
}
ImagePicker.swift
import SwiftUI
import Combine
struct ImagePicker : UIViewControllerRepresentable {
#Binding var show : Bool
#Binding var image : Data
func makeCoordinator() -> ImagePicker.Coordinator {
return ImagePicker.Coordinator(child1: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .photoLibrary
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
class Coordinator : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var child : ImagePicker
init(child1: ImagePicker) {
child = child1
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.child.show.toggle()
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let image = info[.originalImage]as! UIImage
let data = image.jpegData(compressionQuality: 0.45)
self.child.image = data!
self.child.show.toggle()
}
}
}
I am really struggling with how does the app have an error in a view that is not being shown when an item is deleted. The list items does get deleted and removed from CloudKit. The delete operation works. The crash and error happens whether the coredata attribute for the photo has a photo or not. In other words, it has the error even when the item does have a photo and is not nil. Where am I going wrong? How do I get it to allow me to delete an item that I just viewed in the DetailView without a nil error? Any help is greatly appreciated. Thanks.

NavigationView with Loading More in a List Crashes with Long Press only on iPad

I am creating a list that loads data when the user reaches the bottom of the list. I can crash the app when I load more elements and long-press an element within the list. The view is wrapped in a NavigationView and a NavigationLink. When the app crashes, you get EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) with the thread 1 specialized saying "RandomAccessCollection<>.index(_:offsetBy:))". Looking into the EXC_BAD_INSTRUCTION I thought it could be force unwrapping, but I don't see anywhere in the code that could cause this issue.
The issue only occurs on an iPad and happens randomly. With WWDC being yesterday, I thought this would have been fixed, so we downloaded the beta for Xcode 12, and this error still occurs.
Here is the full code:
import UIKit
import SwiftUI
import Combine
struct ContentView: View {
var body: some View {
RepositoriesListContainer(viewModel: RepositoriesViewModel())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
enum GithubAPI {
static let pageSize = 10
static func searchRepos(query: String, page: Int) -> AnyPublisher<[Repository], Error> {
let url = URL(string: "https://api.github.com/search/repositories?q=\(query)&sort=stars&per_page=\(Self.pageSize)&page=\(page)")!
return URLSession.shared
.dataTaskPublisher(for: url) // 1.
.tryMap { try JSONDecoder().decode(GithubSearchResult<Repository>.self, from: $0.data).items } // 2.
.receive(on: DispatchQueue.main) // 3.
.eraseToAnyPublisher()
}
}
struct GithubSearchResult<T: Codable>: Codable {
let items: [T]
}
struct Repository: Codable, Identifiable, Equatable {
let id: Int
let name: String
let description: String?
let stargazers_count: Int
}
class RepositoriesViewModel: ObservableObject {
#Published private(set) var state = State()
private var subscriptions = Set<AnyCancellable>()
// 2.
func fetchNextPageIfPossible() {
guard state.canLoadNextPage else { return }
GithubAPI.searchRepos(query: "swift", page: state.page)
.sink(receiveCompletion: onReceive,
receiveValue: onReceive)
.store(in: &subscriptions)
}
private func onReceive(_ completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break
case .failure:
state.canLoadNextPage = false
}
}
private func onReceive(_ batch: [Repository]) {
state.repos += batch
state.page += 1
state.canLoadNextPage = batch.count == GithubAPI.pageSize
}
// 3.
struct State {
var repos: [Repository] = []
var page: Int = 1
var canLoadNextPage = true
}
}
struct RepositoriesListContainer: View {
#ObservedObject var viewModel: RepositoriesViewModel
var body: some View {
RepositoriesList(
repos: viewModel.state.repos,
isLoading: viewModel.state.canLoadNextPage,
onScrolledAtBottom: viewModel.fetchNextPageIfPossible
)
.onAppear(perform: viewModel.fetchNextPageIfPossible)
}
}
struct RepositoriesList: View {
// 1.
let repos: [Repository]
let isLoading: Bool
let onScrolledAtBottom: () -> Void // 2.
var body: some View {
NavigationView {
List {
reposList
if isLoading {
loadingIndicator
}
}
}
// .OnlyStackNavigationView()
}
private var reposList: some View {
ForEach(repos) { repo in
// 1.
RepositoryRow(repo: repo).onAppear {
// 2.
if self.repos.last == repo {
self.onScrolledAtBottom()
}
}
.onTapGesture {
print("TAP")
}
.onLongPressGesture {
print("LONG PRESS")
}
}
}
private var loadingIndicator: some View {
Spinner(style: .medium)
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
}
}
struct RepositoryRow: View {
let repo: Repository
var body: some View {
NavigationLink(destination: LandmarkDetail()){VStack {
Text(repo.name).font(.title)
Text("⭐️ \(repo.stargazers_count)")
repo.description.map(Text.init)?.font(.body)
}}
}
}
struct Spinner: UIViewRepresentable {
let style: UIActivityIndicatorView.Style
func makeUIView(context: Context) -> UIActivityIndicatorView {
let spinner = UIActivityIndicatorView(style: style)
spinner.hidesWhenStopped = true
spinner.startAnimating()
return spinner
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {}
}
struct LandmarkDetail: View {
var body: some View {
VStack {
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack(alignment: .top) {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
Spacer()
}
}
}

Resources