SwiftUI: Why UIHostingController(rootView:) can pass nil value? - swiftui

In Xcode playground :
func makeView<T:View>(v: T) -> T?{
nil
}
let v0 = makeView(v: Text(""))
let view = UIHostingController(rootView: v0)
note that func UIHostingController(rootView: ) signature not allowed pass nil value:
open class UIHostingController<Content> : UIViewController where Content : View {
public init(rootView: Content)
}
So why I can pass nil to UIHostingController(rootView:) ???
thanks ;)
Update:
So I try to write some one like class UIHostingController:
protocol P{
var name: String {get}
}
class Container<T> where T: P{
init(a:T){
print(a.name)
}
}
struct A: P {
var name:String
init?(name:String) {
if name.isEmpty{
return nil
}
self.name = name
}
}
but when I create a instance of Container, some wrong happens:
let p = Container(a: A(name: ""))
compiler complain me :
Argument type 'A?' does not conform to expected type 'P'
So how UIHostingController did that???

In the below code, it's true that V0 is nil. But, it's important to note that v0 is of type Text?.
let view = UIHostingController(rootView: v0)
Even though below line doesn't work
let view = UIHostingController(rootView: nil)
This works.
let view = UIHostingController<Text?>(rootView: nil)
To fix the error "Generic class 'Container' requires that 'A?' conform to 'P'", Container class can be updated as below
class Container<T>: X where T: P{
init(a:T?){
if let a = a {
print(a.name)
}
}
}

Related

How to infer a generic paramater with an async/await function

I have an async/await function to make sure that the data gets passed along first;
func downloadFirebaseData() async -> String {
let group = DispatchGroup()
group.enter() // stop the thread/enter the function
let db = Firestore.firestore()
withUnsafeThrowingContinuation { continuation in
db.collection("annotations")
.getDocuments { (querySnapshot, error) in
defer {
group.leave() // << end on any return
}
if let Lng = i.document.get("lng") as? String {
DispatchQueue.main.async {
annotationLng.append(Lng) //edit the array
print("downloadLngServerData ()\(annotationLng)")
}
}
if let Lat = i.document.get("lat") as? String {
DispatchQueue.main.async {
annotationLat.append(Lat) //edit the array
print("downloadLatServerData ()\(annotationLat)")
}
}
}
}
group.wait() // clear up the thread now, exit the function
}
And its called under my view with;
.task {
try await downloadFirebaseData() //error 1
}
#State annotationLat: [String] = []
#State annotationLng: [String] = []
Inside of firebase database:
annotationLat = ["42.828392","29.18273","97.27352"]
annotationLng = ["42.828392","29.18273","97.27352"]
I have 2 errors;
Invalid conversion from throwing function of type '#Sendable () async throws -> Void' to non-throwing function type '#Sendable () async -> Void'
This was under the .task
My second error:
Generic parameter 'T' could not be inferred
This was under withUnsafeThrowingContinuation
The first error I somewhat get, but even after I modified from my original code, the error still persisted.
For the second error, I know that I might have to define that this is a string somewhere, because I don't think that the app knows that I'm trying to work with a string.
This assumes that the Firestore path for the documents is
annotations/{id}
and that each document has variables lat and lng of type String
import Foundation
import FirebaseFirestoreSwift
import FirebaseFirestore
import CoreLocation
//struct to keep the latitude and longitude together, they should not be in separate arrays
struct Annotation: Codable, Identifiable{
#DocumentID var id: String?
var lat: String?
var lng: String?
}
extension Annotation{
//Safely unwrap the Strings into doubles and then create the coordinate
var coordinate: CLLocationCoordinate2D? {
guard let latStr = lat, let lngStr = lng, let latitude = Double(latStr), let longitude = Double(lngStr) else{
print("Unable to get valid latitude and longitude")
return nil
}
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
return coordinate
}
}
struct CustomFirestoreService{
let store: Firestore = .firestore()
init(){}
func getAnnotations() async throws -> [Annotation]{
let ANNOTATIONS_PATH = "annotations"
return try await retrieve(path: ANNOTATIONS_PATH)
}
///retrieves all the documents in the collection at the path
private func retrieve<FC : Codable>(path: String) async throws -> [FC]{
//Firebase provided async await.
let querySnapshot = try await store.collection(path).getDocuments()
return querySnapshot.documents.compactMap { document in
do{
return try document.data(as: FC.self)
}catch{
print(error)
return nil
}
}
}
}
Then in your View
import SwiftUI
struct AnnotationsView: View {
let service: CustomFirestoreService = CustomFirestoreService()
#State private var annotations: [Annotation] = []
var body: some View {
if annotations.isEmpty{
Text("Hello, World!")
.task {
do{
annotations = try await service.getAnnotations()
//Do any other work here, this line won't run unless the annotations are populated.
}catch{
print(error)
}
}
}else{
List(annotations){ annotation in
if let coord = annotation.coordinate{
VStack{
Text("Latitude = \(coord.latitude)")
Text("Longitude = \(coord.longitude)")
}
}else{
Text("Invalid Coordinate Value. Check firestore values for document \(annotation.id ?? "no id")")
}
}
}
}
}
struct AnnotationsView_Previews: PreviewProvider {
static var previews: some View {
AnnotationsView()
}
}
This makes some assumptions but if you paste it into your project you should get some working code.
You don't need this for your code but this is what a conversion from the "old" closures to the new async await would look like.
public func retrieve<FC : Codable>(path: String) async throws -> [FC]{
typealias MyContinuation = CheckedContinuation<[FC], Error>
return try await withCheckedThrowingContinuation { (continuation: MyContinuation) in
store.collection(path)
.getDocuments() { (querySnapshot, err) in
if let err = err {
//This throws an error
continuation.resume(throwing: err)
} else {
let array = querySnapshot?.documents.compactMap { document in
try? document.data(as: FC.self)
} ?? []
//This returns an array
continuation.resume(returning: array)
}
}
}
}
If you aren't calling continuation there is no point in returning a continuation of any kind.

How do you use the result of a function as a Bindable object in Swiftui?

I'm developing a simple SwiftUI app, using Xcode 11 beta5.
I have a list of Place, and i want to display the list, and add / edit them.
The data come from core data.
I have 3 classes for this :
- CoreDataController, which handle the connection to core data
- PlaceController, which handle operation on the Places.
public class CoreDataController {
static let instance = CoreDataController()
private let container = NSPersistentContainer(name: "RememberV2")
private init() {
print("Start Init DataController")
container.loadPersistentStores { (storeDescription, error) in
if let error = error {
fatalError("Failed to load store: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
print("End Init DataController")
}
func getContext() -> NSManagedObjectContext {
return container.viewContext
}
func save() {
print("Start Save context")
do{
try container.viewContext.save()
} catch {
print("ERROR - saving context")
}
print("End Save context")
}
}
public class PlaceController {
static let instance = PlaceController()
private let dc = CoreDataController.instance
private let entityName:String = "Place"
private init() {
print("Start init Place Controller")
print("End init Place Controller")
}
func createPlace(name:String) -> Bool {
let newPlace = NSEntityDescription.insertNewObject(forEntityName: entityName, into: dc.getContext())
newPlace.setValue(UUID(), forKey: "id")
newPlace.setValue(name, forKey: "name")
dc.save()
DataController.instance.places = getAllPlaces()
return true
}
func createPlace(name:String, comment:String) -> Bool {
print("Start - create place with comment")
let newPlace = NSEntityDescription.insertNewObject(forEntityName: entityName, into: dc.getContext())
newPlace.setValue(UUID(), forKey: "id")
newPlace.setValue(name, forKey: "name")
newPlace.setValue(comment, forKey: "comment")
dc.save()
print("End - create place with comment")
DataController.instance.places = getAllPlaces()
return true
}
func getAllPlaces() -> [Place] {
let r = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
if let fp = try? dc.getContext().fetch(r) as? [Place] {
return fp
}
return [Place]()
}
func truncatePlaces() -> Bool {
let r = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let batch = NSBatchDeleteRequest(fetchRequest: r)
if (try? dc.getContext().execute(batch)) != nil {
return true
}
return false
}
}
In my view i simply use the function :
List (pc.getAllPlaces(), id: \.id) { place in
NavigationLink(destination: PlaceDetail(place: place)) {
PlacesRow(place:place)
}
}
It works to display the information, but if i add a new place, the list is not updated.
I have to go back to the home screen, then display again the Places screen for the list to be updated.
So i use another controller :
class DataController: ObservableObject {
#Published var places:[Place] = []
static let instance = DataController()
private init() {
print("Start init Place Controller")
print("End init Place Controller")
}
}
In my view, i just display the ObservedObject places.
#ObservedObject var data: DataController = DataController.instance
And in my PlaceController, i update the table in the DataController
DataController.instance.places = getAllPlaces()
That works, but i have this warning :
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes
Also i'm pretty sure there is a better way to do this ...
Any idea what is this better way ?
Thanks,
Nicolas

How to implement a type erased struct like AnyView in SwiftUI?

I'm curious about the default implementation of AnyView in SwiftUI. How to put structs with different generic types into a protocol array?
For example:
let a = AnyView(Text("hello"))
let b = AnyView(Image(systemName: "1.circle"))
let genericViews = [a, b] // No compile error
And my implementation:
struct TypeErasedView<V: View>: View {
private var _view: V
init(_ view: V) {
_view = view
}
var body: V {
_view
}
}
let a = TypeErasedView(Text("Hello"))
let b = TypeErasedView(Image(systemName: "1.circle"))
let genericViews = [a, b] // compile error
The compile error will be "Heterogeneous collection literal could only be inferred to '[Any]'; add explicit type annotation if this is intentional".
Does anyone have any ideas?
Here is a demo of possible approach. It is simplified, but shows the generic idea of how this might be done... or at least a direction.
Full compilable & working module. Tested on Xcode 11.2 / iOS 13.2
import SwiftUI
private protocol TypeErasing {
var view: Any { get }
}
private struct TypeEraser<V: View>: TypeErasing {
let orinal: V
var view: Any {
return self.orinal
}
}
public struct MyAnyView : View {
public var body: Never {
get {
fatalError("Unsupported - don't call this")
}
}
private var eraser: TypeErasing
public init<V>(_ view: V) where V : View {
eraser = TypeEraser(orinal: view)
}
fileprivate var wrappedView: Any { // << they might have here something specific
eraser.view
}
public typealias Body = Never
}
struct DemoAnyView: View {
let container: [MyAnyView]
init() {
let a = MyAnyView(Text("Hello"))
let b = MyAnyView(Image(systemName: "1.circle"))
container = [a, b]
}
var body: some View {
VStack {
// dynamically restoring types is different question and might be
// dependent on Apple's internal implementation, but here is
// just a demo that it works
container[0].wrappedView as! Text
container[1].wrappedView as! Image
}
}
}
struct DemoAnyView_Previews: PreviewProvider {
static var previews: some View {
DemoAnyView()
}
}
It's because there's a generic constraint on yours. AnyView has no generic constraint. You instantiate it with an underlying generic View, but its Body is always declared as Never. There might be compiler magic happening here as I couldn't get a generic constraint-less version to work.

How to update any specific MKAnnotationView image on click button from NavigationBar?

I have added some annotationViews at Map with init method (initialised by there id). Now I want to update specific id annotation view on click button from navigation bar.
Suppose I have added 5 annotation with ids (1, 2, 3, 4, and 5)
Added from VC:
let annotation = MapPinAnnotation(title: storeItem.name!, location: CLLocationCoordinate2DMake(Double(lat), Double(long)), id: storeItem.storeId!)
self.mapview.addAnnotation(annotation)
Initialised AnnotationView:
class MapPinAnnotation: NSObject, MKAnnotation {
var title:String?
var id:String?
private(set) var coordinate = CLLocationCoordinate2D()
init(title newTitle: String, location: CLLocationCoordinate2D, id: String) {
super.init()
self.title = newTitle
self.coordinate = location
self.id = id
}
}
ViewFor annotation method:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if (annotation is MKUserLocation) {
return nil
}
if (annotation is MapPinAnnotation) {
let pinLocation = annotation as? MapPinAnnotation
// Try to dequeue an existing pin view first.
var annotationView: MKAnnotationView? = mapView.dequeueReusableAnnotationView(withIdentifier: "MapPinAnnotationView")
if annotationView == nil {
annotationView?.image = UIImage(named: Constants.Assets.PinGreen)
}
else {
annotationView?.annotation = annotation
}
return annotationView
}
return nil
}
Now I want to change image of annotation view(id 4) on click button from navigation bar.
How can I update? Please help.
Thanks in advance.
You can get specific MKAnnotationView with view(for: ) method. Try the following code:
func clickButton() {
for annotation in self.mapView.annotations {
if annotation.id == 4 {
let annotationView = self.mapView.view(for: annotation)
annotationView?.image = UIImage(named: "Image name here")
}
}
}

swift 3, PHFetchResult.enumerateObjects error

In swift 3,the method is show me "ambiguous use of 'enumerateObjects'",what happen.how can i do?
extension PHFetchResult {
public func assetCollection() -> [PHAssetCollection] {
var list :[PHAssetCollection] = []
self.enumerateObjects { (object, index, stop) in
if object is PHAssetCollection {
let collection = object as! PHAssetCollection
list.append(collection)
}
}
return list
}
}
Swift 3.0: Just add the Round Brackets before Curly Brackets starts after enumerateObjects.
extension PHFetchResult {
public func assetCollection() -> [PHAssetCollection] {
var list :[PHAssetCollection] = []
self.enumerateObjects ({ (object, index, stop) in
if object is PHAssetCollection {
let collection = object as! PHAssetCollection
list.append(collection)
}
})
return list
}
}
Do something like this noh. You can't directly add extension for PHFetchResult because it has other ObjectType as its generic parameter PHFetchResult<ObjectType> . So you must do something else.
class FetchPhoto {
class func assetCollection() -> [PHAssetCollection] {
var list :[PHAssetCollection] = []
PHAssetCollection.fetchMoments(with: nil).enumerateObjects(EnumerationOptions.concurrent) { (collection, _, _) in
list.append(collection)
}
return list
}
}
PHAssetCollection.fetchMoments returns PHFetchResult<PHAssetCollection> here PHAssetCollection is the ObjectType for the PHFetchResult. You got the ambiguous error because you have not specified the objectType.
A generic way to approach this.
class FetchPhoto {
class func assetCollection<T : PHObject>(result : PHFetchResult<T>) -> [T] {
var list : [T] = []
result.enumerateObjects(EnumerationOptions.concurrent) { (object, _, _) in
list.append(object)
}
return list
}
}
Swift 3
class PhotosHelper {
class func fetchAllLocalIdentifiersOfPhotos(completion : (_ localIdentifiers : [String]) -> ()) {
let photos : PHFetchResult<PHAsset> = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: nil)
photos.enumerateObjects ({ _,_,_ in
// Do your operations, you can see that there is no warnings/errors in this one
})
}
}