Core data not triggering immediate refresh with SwiftUI - core-data

I'm at a loss with this problem I've been troubleshooting the past few days.
I am drawing a custom shape, and the user can move one of the points around via a drag gesture. I want the shape to redraw, live, when moving the point around.
Here is some sample code that is working using CGPoint.
import SwiftUI
struct ShapeTest: Shape {
#State var points: [CGPoint]
#State var closed: Bool = true
func path(in rect: CGRect) -> Path {
var path = Path()
if (points.count > 0) {
path.move(to: points.first!)
path.addLines(points)
if closed { path.closeSubpath() }
}
return path
}
}
struct RedrawEdgeTestCGPoint: View {
#State var points = [CGPoint]()
#State var originalPosition: CGPoint? = nil
private var movePointDragGesture: some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { value in
if points.isEmpty {
return
}
if originalPosition == nil {
originalPosition = CGPoint(x: points.last!.x, y: points.last!.y)
}
let lastIndex = points.count - 1
points[lastIndex].x = originalPosition!.x + value.translation.width
points[lastIndex].y = originalPosition!.y + value.translation.height
}
.onEnded { value in
originalPosition = nil
}
}
private var addNewPointGesture: some Gesture {
TapGesture(count: 2)
.onEnded {
points.append(CGPoint())
}
}
var body: some View {
GeometryReader { geometry in
Group {
ShapeTest(points: points)
.stroke()
}
.contentShape(Rectangle())
.gesture(movePointDragGesture.simultaneously(with: addNewPointGesture))
}
}
}
struct RedrawEdgeTestCGPoint_Previews: PreviewProvider {
static var previews: some View {
RedrawEdgeTestCGPoint()
}
}
Now, here is another code sample. This example is generally the same thing, but this is using a Point entity I've defined in Core Data instead of CGPoint.
import SwiftUI
struct RedrawEdgeTestCoreData: View {
#Environment(\.managedObjectContext) private var viewContext
#State var points: [Point] = []
#State var originalPosition: CGPoint? = nil
private var movePointDragGesture: some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { value in
if points.isEmpty {
return
}
if originalPosition == nil {
originalPosition = CGPoint(x: points.last!.x, y: points.last!.y)
}
let lastIndex = points.count - 1
points[lastIndex].x = originalPosition!.x + value.translation.width
points[lastIndex].y = originalPosition!.y + value.translation.height
}
.onEnded { value in
originalPosition = nil
}
}
private var addNewPointGesture: some Gesture {
TapGesture(count: 2)
.onEnded {
points.append(Point(context: viewContext))
}
}
var body: some View {
GeometryReader { geometry in
Group {
ShapeTest(points: points.map { CGPoint(x: $0.x, y: $0.y) })
.stroke()
}
.contentShape(Rectangle())
.gesture(movePointDragGesture.simultaneously(with: addNewPointGesture))
}
}
}
struct RedrawEdgeTestCoreData_Previews: PreviewProvider {
static var previews: some View {
RedrawEdgeTestCoreData()
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
The version utilizing Core Data does not update while dragging. It will redraw when I double tap to add another point.
Any suggestions as to why this is happening?

I used the approach suggested by jnpdx and it works.
import SwiftUI
class PointsViewModel: ObservableObject {
#Published var points = [Point]()
func movePoint(index: Int, x: CGFloat, y: CGFloat) {
points[index].x = x
points[index].y = y
objectWillChange.send()
}
}
struct RedrawEdgeTestCoreData: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var viewModel: PointsViewModel
#State var originalPosition: CGPoint? = nil
private var movePointDragGesture: some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { value in
if viewModel.points.isEmpty {
return
}
if originalPosition == nil {
originalPosition = CGPoint(x: viewModel.points.last!.x, y: viewModel.points.last!.y)
}
let lastIndex = viewModel.points.count - 1
viewModel.movePoint(index: lastIndex, x: originalPosition!.x + value.translation.width, y: originalPosition!.y + value.translation.height)
}
.onEnded { value in
originalPosition = nil
}
}
private var addNewPointGesture: some Gesture {
TapGesture(count: 2)
.onEnded {
viewModel.points.append(Point(context: viewContext))
}
}
var body: some View {
GeometryReader { geometry in
ZStack {
ShapeTest(points: viewModel.points.map { CGPoint(x: $0.x, y: $0.y) })
.stroke()
}
.contentShape(Rectangle())
.gesture(movePointDragGesture.simultaneously(with: addNewPointGesture))
}
}
}
struct RedrawEdgeTestCoreData_Previews: PreviewProvider {
static var previews: some View {
RedrawEdgeTestCoreData(viewModel: PointsViewModel())
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

Related

SwiftUI show loading view while core data is being loaded

How I can show loading view while core data is being loaded?.
Currently my app's core data store some many images in Binary Data. So when I switch to another tab showing data stored in core data, app lags 1.5 seconds.
So here are two things I have tried:
first I tried to minimize amount of data being loaded from core data using downsample function:
func downsample(imageAt imageURL: Data, to pointSize: CGSize, scale: CGFloat = UIScreen.main.scale) -> UIImage? {
// Create an CGImageSource that represent an image
//CGImageSourceCreateWithData(_ data: CFData, _ options: CFDictionary?)
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithData(imageURL as CFData, imageSourceOptions) else {
return nil
}
// Calculate the desired dimension
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
// Perform downsampling
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
// Return the downsampled image as UIImage
return UIImage(cgImage: downsampledImage)
}
let small = downsample(imageAt: data, to: size)
Image(uiImage: small!)
But there were no difference in lagging time.
So i tried this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \ToDoItem2.createdAt, ascending: false)])
var toDoItems: FetchedResults<ToDoItem>
var body: some View {
VStack{
if toDoItems.isEmpty {
LoadingView()
} else {
List {
ForEach(toDoItems) { item in
ToDoItemView(item: item)
}
}
}
}
}
}
Tried to detect no loaded state as toDoItems.isEmpty but it doesn't work
Would be there anyway to show loading view while core data is being loaded?
Thanks
you could try using NSAsynchronousFetchRequest, something like this approach (does not have to be exactly like this (untested) code):
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var toDoItems: [ToDoItem] = []
#State var isLoading = true
var body: some View {
VStack {
if isLoading {
LoadingView()
} else {
List {
ForEach(toDoItems) { item in
ToDoItemView(item: item)
}
}
}
}
.onAppear {
isLoading = true
let fetchRequest: NSFetchRequest<ToDoItem> = ToDoItem.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \ToDoItem.createdAt, ascending: false)]
let asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { fetchResult -> Void in
if let resutls = fetchResult.finalResult {
self.toDoItems = resutls
}
self.isLoading = false
}
do {
_ = try viewContext.execute(asyncFetchRequest)
} catch {
print("error: \(error)")
}
}
}
}

Refresh a view when the children of an object are changed in SwiftUI

I am working on a CoreData application with two entities MyList and MyListItem. MyList can have many MyListItem (one to many). When the app is launched, I can see all the lists. I can tap on a list to go to the list items. On that screen, I tap a button to add an item to the selected list. After, adding the item when I go back to the all lists screen I cannot see the number of items reflected in the count. The reason is that MyListsView is not rendered again since the number of lists have not changed.
The complete code is shown below:
import SwiftUI
import CoreData
extension MyList {
static var all: NSFetchRequest<MyList> {
let request = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
struct DetailView: View {
#Environment(\.managedObjectContext) var viewContext
let myList: MyList
var body: some View {
VStack {
Text("Detail View")
Button("Add List Item") {
let myListP = viewContext.object(with: myList.objectID) as! MyList
let myListItem = MyListItem(context: viewContext)
myListItem.name = randomString()
myListItem.myList = myListP
try? viewContext.save()
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
class ViewModel: NSObject, ObservableObject {
#Published var myLists: [MyList] = []
private var fetchedResultsController: NSFetchedResultsController<MyList>
private(set) var context: NSManagedObjectContext
override init() {
self.context = CoreDataManager.shared.context
fetchedResultsController = NSFetchedResultsController(fetchRequest: MyList.all, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
guard let myLists = fetchedResultsController.fetchedObjects else { return }
self.myLists = myLists
} catch {
print(error)
}
}
}
extension ViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let myLists = controller.fetchedObjects as? [MyList] else { return }
self.myLists = myLists
}
}
struct MyListsView: View {
let myLists: [MyList]
var body: some View {
List(myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
struct ContentView: View {
#StateObject private var vm = ViewModel()
#Environment(\.managedObjectContext) var viewContext
var body: some View {
NavigationView {
VStack {
// when adding an item to the list the MyListView view is
// not re-rendered
MyListsView(myLists: vm.myLists)
Button("Change List") {
}
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
Inside ContentView there is a view called "MyListsView". That view is not rendered when the items are added. Since, according to that view nothing changed since the number of lists are still the same.
How do you solve this problem?
UPDATE:
What happens if I add one more level of views like for ListCellView as shown below:
struct MyListCellView: View {
#StateObject var vm: ListCellViewModel
init(vm: ListCellViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
HStack {
Text(vm.name)
Spacer()
Text("\((vm.items).count)")
}
}
}
#MainActor
class ListCellViewModel: ObservableObject {
let myList: MyList
init(myList: MyList) {
self.myList = myList
self.name = myList.name ?? ""
self.items = myList.items!.allObjects as! [MyListItem]
print(self.items.count)
}
#Published var name: String = ""
#Published var items: [MyListItem] = []
}
struct MyListsView: View {
#StateObject var vm: ViewModel
init(vm: ViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
let _ = Self._printChanges()
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
MyListCellView(vm: ListCellViewModel(myList: myList))
}
}
}
}
Now the count is again not being updated.
Your ViewModel is an ObserveableObject, but you are not observing it in MyListsView. When you initialized MyListsView, you set a let constant. Of course that won't update. Do this instead:
struct MyListsView: View {
#ObservedObject var vm: ViewModel
init(viewModel: ViewModel) {
self.vm = viewModel
}
var body: some View {
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
Now the #Published in ViewModel will cause MyListView to change when it does, and that includes adding a related entity.
We don't need MVVM in SwiftUI, the View data structs already fill that role and property wrappers make them behave like objects giving best of both worlds. In your case use the #FetchRequest property wrapper for the list and #ObservedObject for the detail and body will be called on any changes to the model data. Examine the code in the app template in Xcode with Core Data checked. It looks like this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
...
struct DetailView: View {
#ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")

SwifUI: How to get manual list to refresh when updating CoreData records?

I have a CoreData table that has several repeated records that don't need to be stored but do need to be displayed in my user interface. I have manually created my arrays based on the CoreData table. I have made them Observable Objects so they should automatically update and I have made them Hashable and Equatable.
My problem is that the list does not update when the database records are updated. This works fine when records are being added.
Here is my much simplified code in full that demonstrates the problem:
import SwiftUI
let persistentContainerQueue = OperationQueue()
let firstNames = ["Michael", "Damon", "Jacques", "Mika", "Fernando", "Kimi", "Lewis", "Jenson", "Sebastion", "Nico"]
let lastNames = ["Schumacher", "Hill", "Villeneuve", "Hakkinen", "Alonso", "Raikkonen", "Hamilton", "Button", "Vettel", "Rosberg"]
class RepeatedPerson: ObservableObject, Hashable
{
#Published var person: Person
#Published var index: Int
#Published var tested: Bool
init (person: Person, index: Int, tested: Bool)
{
self.person = person
self.index = index
self.tested = tested
}
func hash(into hasher: inout Hasher)
{
hasher.combine(person.firstName)
hasher.combine(person.lastName)
hasher.combine(index)
}
static func == (lhs: RepeatedPerson, rhs: RepeatedPerson) -> Bool
{
return lhs.person.firstName == rhs.person.firstName &&
lhs.person.lastName == rhs.person.lastName &&
lhs.index == rhs.index
}
}
class RepeatedPeople: ObservableObject
{
#Published var people: [RepeatedPerson] = []
}
func getRepeatedPeople() -> [RepeatedPerson]
{
var repeatedPeople:[RepeatedPerson] = []
let records = allRecords(Person.self)
for person in records
{
for index in 1...3
{
repeatedPeople.append(RepeatedPerson(person: person, index: index, tested: true))
}
}
return repeatedPeople
}
struct ContentView: View
{
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var repeatedPeople = RepeatedPeople()
init()
{
repeatedPeople.people = getRepeatedPeople()
}
var body: some View
{
VStack
{
List()
{
ForEach(repeatedPeople.people, id: \.self)
{ repeatedPerson in
Text("\(repeatedPerson.index)) \(repeatedPerson.person.firstName!) \(repeatedPerson.person.lastName!)")
}
}
HStack
{
Button("Add Record", action:
{
addItem()
repeatedPeople.people = getRepeatedPeople()
})
Button("Change Record", action:
{
let q = allRecords(Person.self)
let oldLastName = q[0].lastName
q[0].lastName = lastNames.randomElement()!
print ("changed \(q[0].firstName!) \(oldLastName!) -> \(q[0].firstName!) \(q[0].lastName!)")
saveDatabase()
})
Button("Reset Database", action:
{
deleteAllRecords(Person.self)
})
}
}
}
private func addItem()
{
withAnimation
{
let newItem = Person(context: viewContext)
newItem.timestamp = Date()
newItem.firstName = firstNames.randomElement()!
newItem.lastName = lastNames.randomElement()!
print ("added \(newItem.firstName!) \(newItem.lastName!)")
saveDatabase()
}
}
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = PersistenceController.shared.container.viewContext
let request = T.fetchRequest()
if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func deleteAllRecords<T: NSManagedObject>(_ type : T.Type)
{
let context = PersistenceController.shared.container.viewContext
let results = allRecords(T.self)
for record in results
{
context.delete(record as NSManagedObject)
}
saveDatabase()
}
func saveDatabase()
{
persistentContainerQueue.addOperation()
{
let context = PersistenceController.shared.container.viewContext
context.performAndWait
{
try? context.save()
}
}
}
To reproduce the problem, add a few records. These will be shown in the list. Then click the 'Update Record' button. The CoreData record will be updated (you can see this the next time you run the app) but the changes will not be shown.
How do I get the new changes to show?
If you add another record the changes will then be shown. A side effect is that the list introduces wild spaces between the records. I have seen this is in other places. Is this a SwiftUI bug?
OK it turned out to be really quite simple. All I actually had to do was remove some of the #Published and provide a UUID for the repeatedPerson record (and for == and hash).
import SwiftUI
import CoreData
let persistentContainerQueue = OperationQueue()
let firstNames = ["Michael", "Damon", "Jacques", "Mika", "Fernando", "Kimi", "Lewis", "Jenson", "Sebastion", "Nico"]
let lastNames = ["Schumacher", "Hill", "Villeneuve", "Hakkinen", "Alonso", "Raikkonen", "Hamilton", "Button", "Vettel", "Rosberg"]
class RepeatedPerson: ObservableObject, Hashable
{
var id: UUID = UUID()
var index: Int
var person: Person?
init (person: Person, index: Int)
{
self.person = person
self.index = index
}
func hash(into hasher: inout Hasher)
{
hasher.combine(id)
}
static func == (lhs: RepeatedPerson, rhs: RepeatedPerson) -> Bool
{
return lhs.id == rhs.id
}
}
class RepeatedPeople: ObservableObject
{
#Published var people: [RepeatedPerson] = []
}
func getRepeatedPeople() -> [RepeatedPerson]
{
var repeatedPeople:[RepeatedPerson] = []
let records = allRecords(Person.self)
for person in records
{
for index in 1...3
{
repeatedPeople.append(RepeatedPerson(person: person, index: index))
}
}
return repeatedPeople
}
struct ContentView: View
{
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var repeatedPeople = RepeatedPeople()
init()
{
repeatedPeople.people = getRepeatedPeople()
}
var body: some View
{
VStack
{
List()
{
ForEach(repeatedPeople.people, id: \.self)
{ repeatedPerson in
Text("\(repeatedPerson.index)) \(repeatedPerson.person!.firstName!) \(repeatedPerson.person!.lastName!)")
}
}
HStack
{
Button("Add Record", action:
{
addItem()
repeatedPeople.people = getRepeatedPeople()
})
Button("Change Record", action:
{
let q = allRecords(Person.self)
let r = q.randomElement()!
let oldLastName = r.lastName
r.lastName = lastNames.randomElement()!
print ("changed \(r.firstName!) \(oldLastName!) -> \(r.firstName!) \(r.lastName!)")
saveDatabase()
repeatedPeople.people = getRepeatedPeople()
})
Button("Reset Database", action:
{
print ("Reset database")
deleteAllRecords(Person.self)
repeatedPeople.people = getRepeatedPeople()
})
}
}
}
private func addItem()
{
withAnimation
{
let newItem = Person(context: viewContext)
newItem.timestamp = Date()
newItem.firstName = firstNames.randomElement()!
newItem.lastName = lastNames.randomElement()!
print ("added \(newItem.firstName!) \(newItem.lastName!)")
saveDatabase()
}
}
}
func query<T: NSManagedObject>(_ type : T.Type, predicate: NSPredicate? = nil, sort: NSSortDescriptor? = nil) -> [T]
{
let context = PersistenceController.shared.container.viewContext
let request = T.fetchRequest()
if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
if let predicate = predicate
{
request.predicate = predicate
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
return query(T.self, sort: sort)
}
func deleteAllRecords<T: NSManagedObject>(_ type : T.Type)
{
let context = PersistenceController.shared.container.viewContext
let results = allRecords(T.self)
for record in results
{
context.delete(record as NSManagedObject)
}
saveDatabase()
}
func saveDatabase()
{
persistentContainerQueue.addOperation()
{
let context = PersistenceController.shared.container.viewContext
context.performAndWait
{
try? context.save()
}
}
}

Convert address to coordinates using MKLocalSearchCompleter and CoreLocation

I have tried to make an app with a textfield to let user input a location, using MKLocalSearchCompleter to complete the searching. After that i would like to get the coordinate and display on the MapKit. However, I failed to get the coordinate using the Geocoder.
class LocationSearchService: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {
#Published var searchQuery = ""
var completer: MKLocalSearchCompleter
#Published var completions: [MKLocalSearchCompletion] = []
var cancellable: AnyCancellable?
override init() {
completer = MKLocalSearchCompleter()
super.init()
cancellable = $searchQuery.assign(to: \.queryFragment, on: self.completer)
completer.delegate = self
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
self.completions = completer.results
}
}
The location manager as follows:
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var status: CLAuthorizationStatus? {
willSet { objectWillChange.send() }
}
#Published var location: CLLocation? {
willSet { objectWillChange.send() }
}
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private func lookupLocation() {
guard let location = self.location else { return }
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
if error == nil {
self.placemark = places?[0]
} else {
self.placemark = nil
}
})
}
// !!! This is the function I would like to use to get the Coordinate from the address obtained from LocationSearchService
func getCoordinate(address: String) {
geocoder.geocodeAddressString(address, completionHandler: { (places, error) in
if error == nil {
self.placemark = places?[0]
self.location = self.placemark?.location
} else {
self.placemark = nil
self.location = nil
}
})
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.status = status
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return } //.first or .last?
self.location = location
self.lookupLocation()
}
}
Content View like this:
struct ContentView: View {
#State private var location: String = ""
#ObservedObject var lm = LocationManager()
private let completer = MKLocalSearchCompleter()
#ObservedObject var locationSearchService = LocationSearchService()
var body: some View {
NavigationView {
VStack {
AddressSearchBar(text: $locationSearchService.searchQuery)
List(locationSearchService.completions, id: \.self) { completion in
VStack(alignment: .leading) {
Text(completion.title)
// Error here, I cannot translate the address to location
//Text(lm.getCoordinate(address: completion.title))
}
}.navigationTitle("Search Location")
}
}
A few issues here:
I would like to convert the user selected item (which I failed to implement here) to the address (completion.title) -- i.e., need to get user selection on the suggested item.
I would like to convert the address found in the suggestion to a coordinate, so that I can mark on MapView.

SwiftUI - How to add "letters sections" and alphabet jumper in a Form?

How can I make a Form in which the elements are automatically divided into sections based on their first letter and add to the right the alphabet jumper to show the elements starting by the selected letter (just like the Contacts app)?
I also noted a strange thing that I have no idea how to recreate: not all letters are shown, some of them appear as "•". However, when you tap on them, they take you to the corresponding letter anyway. I tried using a ScrollView(.vertical) inside a ZStack and adding .scrollTo(selection) into the action of the Buttons, however 1) It didn't scroll to the selection I wanted 2) When I tapped on the "•", it was as if I was tapping on all of them because they all did the tapping animation 3) I wasn't able to divide the List as I wanted to.
I have this:
import SwiftUI
struct ContentView: View {
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W", "X","Y", "Z"]
let values = ["Avalue", "Bvalue", "Cvalue", "Dvalue"]
var body: some View {
ScrollViewReader{ scrollviewr in
ZStack {
ScrollView(.vertical) {
VStack {
ForEach(alphabet, id: \.self) { letters in
Button(letters){
withAnimation {
scrollviewr.scrollTo(letters)
}
}
}
}
}.offset(x: 180, y: 120)
VStack {
ForEach(values, id: \.self){ vals in
Text(vals).id(vals)
}
}
}
}
}
}
But I'd want it like this:
import SwiftUI
struct AlphabetSort2: View {
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W", "X","Y", "Z"]
let values = ["Avalue", "Bvalue", "Cvalue", "Dvalue", "Mvalue", "Zvalue"]
var body: some View {
ScrollView {
ScrollViewReader { value in
ZStack{
List{
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter)) {
ForEach(values.filter { $0.hasPrefix(letter) }, id: \.self) { vals in
Text(vals).id(vals)
}
}.id(letter)
}
}
HStack{
Spacer()
VStack {
ForEach(0..<alphabet.count, id: \.self) { idx in
Button(action: {
withAnimation {
value.scrollTo(alphabet[idx])
}
}, label: {
Text(idx % 2 == 0 ? alphabet[idx] : "\u{2022}")
})
}
}
}
}
}
}
}
}
struct AlphabetSort2_Previews: PreviewProvider {
static var previews: some View {
AlphabetSort2()
}
}
Add an swipe-up-and-down action for the alphabet jumper, so we can get an UILocalizedIndexCollation -like swipe effect, it works for view and add mode, but delete, I guess it is due to SwiftUI's UI refresh mechanism.
extension String {
static var alphabeta: [String] {
var chars = [String]()
for char in "abcdefghijklmnopqrstuvwxyz#".uppercased() {
chars.append(String(char))
}
return chars
}
}
struct Document: View {
#State var items = ["Alpha", "Ash", "Aman", "Alisia", "Beta", "Baum", "Bob", "Bike", "Beeber", "Beff", "Calipha", "Cask", "Calf", "Deamon", "Deaf", "Dog", "Silk", "Seal", "Tiger", "Tom", "Tan", "Tint", "Urshinabi", "Verizon", "Viber", "Vein", "Wallet", "Warren", "Webber", "Waiter", "Xeon", "Young", "Yoda", "Yoga", "Yoger", "Yellow", "Zeta"]
var body: some View {
ScrollViewReader { scrollView in
HStack {
List {
ForEach(String.alphabeta, id: \.self){ alpha in
let subItems = items.filter({$0.starts(with: alpha)})
if !subItems.isEmpty {
Section(header: Text(alpha)) {
ForEach(subItems, id: \.self) { item in
Text(item)
}.onDelete(perform: { offsets in
items.remove(at: offsets.first!)
})
}.id(alpha)
}
}
}
VStack{
VStack {
SectionIndexTitles(proxy: scrollView, titles: retrieveSectionTitles()).font(.footnote)
}
.padding(.trailing, 10)
}
}
}
// .navigationBarTitleDisplayMode(.inline)
// .navigationBarHidden(true)
}
func retrieveSectionTitles() ->[String] {
var titles = [String]()
titles.append("#")
for item in self.items {
if !item.starts(with: titles.last!){
titles.append(String(item.first!))
}
}
titles.remove(at: 0)
if titles.count>1 && titles.first! == "#" {
titles.append("#")
titles.removeFirst(1)
}
return titles
}
}
struct Document_Previews: PreviewProvider {
static var previews: some View {
Document()
}
}
struct SectionIndexTitles: View {
class IndexTitleState: ObservableObject {
var currentTitleIndex = 0
var titleSize: CGSize = .zero
}
let proxy: ScrollViewProxy
let titles: [String]
#GestureState private var dragLocation: CGPoint = .zero
#StateObject var indexState = IndexTitleState()
var body: some View {
VStack {
ForEach(titles, id: \.self) { title in
Text(title)
.foregroundColor(.blue)
.modifier(SizeModifier())
.onPreferenceChange(SizePreferenceKey.self) {
self.indexState.titleSize = $0
}
.onTapGesture {
proxy.scrollTo(title, anchor: .top)
}
}
}
.gesture(
DragGesture(minimumDistance: indexState.titleSize.height, coordinateSpace: .named(titles.first))
.updating($dragLocation) { value, state, _ in
state = value.location
scrollTo(location: state)
}
)
}
private func scrollTo(location: CGPoint){
if self.indexState.titleSize.height > 0{
let index = Int(location.y / self.indexState.titleSize.height)
if index >= 0 && index < titles.count {
if indexState.currentTitleIndex != index {
indexState.currentTitleIndex = index
print(titles[index])
DispatchQueue.main.async {
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
// withAnimation {
proxy.scrollTo(titles[indexState.currentTitleIndex], anchor: .top)
// }
}
}
}
}
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct SizeModifier: ViewModifier {
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}

Resources