I'm creating a custom object called AsyncImageCached using the same init signature as found in AsyncImage. My question is, how do I define variables outside of init to save content and placeholder parameters to be used when my async await calls complete?
public struct AsyncImageCached<Content> : View where Content : View {
private let content: ((Image) -> I)? <--- Doesn't Work, Cannot find type 'I' in scope
private let placeholder: (() -> P)? <--- Doesn't work, Cannot find type 'P' in scope
init<I, P>(url: URL?, scale: CGFloat = 1,
#ViewBuilder content: #escaping (Image) -> I,
#ViewBuilder placeholder: #escaping () -> P)
where Content == _ConditionalContent<I, P>, I : View, P : View {
let content: (Image) -> I = content <--- Works, but can't access outside of init
let placeholder: () -> P = placeholder <--- Works, but can't access outside of init
...
}
}
Moving I, P to the structure level will break the other inits and will not match Apples AsyncImage signatures.
There must be a way to make it work because the same signature is in AsyncImage. I don't want to change the init signature function because I already have the other inits working:
public init(url: URL?, scale: CGFloat = 1) where Content == Image
public init(url: URL?, scale: CGFloat = 1, #ViewBuilder content: #escaping (AsyncImagePhase) -> Content)
Any help would be greatly appreciated, I have spent two days on this and I can't find anything online that teaches how to use the ViewBuilder outside of simple examples, non that have a custom init like this.
Well, inspecting Swift interface file, we can see the following:
public struct AsyncImage<Content> : SwiftUI.View where Content : SwiftUI.View {
/* ... */
#_alwaysEmitIntoClient public init<I, P>(url: Foundation.URL?, scale: CoreGraphics.CGFloat = 1, #SwiftUI.ViewBuilder content: #escaping (SwiftUI.Image) -> I, #SwiftUI.ViewBuilder placeholder: #escaping () -> P) where Content == SwiftUI._ConditionalContent<I, P>, I : SwiftUI.View, P : SwiftUI.View {
self.init(url: url, scale: scale) { phase in
if let i = phase.image {
content(i)
} else {
placeholder()
}
}
}
/* ... */
}
So, it turns out it just calls another init itself, which doesn't require these generics!
You can see the interface file at /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64.swiftinterface, replacing Xcode.app with whatever the name is (as betas have different names)
Related
I have a situation where I have a view model with a #Published value. Now when I'm building the UI I want to send a value via a preference every time the published value changes. The problem I'm having is that I cannot figure out how to get SwiftUI to watch the published value and send the preference.
In the code below I've endeavoured to simplify the problem.
Preference key:
struct NewValueKey: PreferenceKey {
static var defaultValue: String? = nil
static func reduce(value: inout String?, nextValue: () -> String) {
value = nextValue()
}
}
Publishing object:
public class SomeViewModel: ObservableObject {
#Published public var value: String?
}
And in the Swift UI view:
.onReceive(viewModel.$value) { _ in
let newValue = "abc"
Color.clear.preference(key: NewValueKey.self, value: newValue)
}
But I get a variety of errors such as the result of the preference not being used, etc. The problem is that I need to execute some code to build the value I want to send so I can't just forward the value I receive. I've also looked at using a #State property as some sort of intermediary but I'm not sure how to wire it all together.
Any suggestions?
It is enough to inject value directly into preference modifier attached to a view within modified context (ie. you don't need .onReceive modifer because updated value will activate .preference modifier directly).
Tested with Xcode 13.3 / iOS 15.4
Here is a snapshot of main part
Button("Generate") {
vm.value = String(Int.random(in: 0...9))
}
.preference(key: NewValueKey.self, value: vm.value) // << here !!
Completed findings report and code is here
not clear what you want, but ensure that you have something like this in your view, or equivalent #ObservedObject:
#StateObject var viewModel = SomeViewModel()
Also:
struct NewValueKey: PreferenceKey {
static var defaultValue: String = "" // <-- here
static func reduce(value: inout String, nextValue: () -> String) { // <-- here
value = nextValue()
}
}
and to get rid of the warning:
.onReceive(viewModel.$value) { _ in
let newValue = "abc"
let _ = Color.clear.preference(key: NewValueKey.self, value: newValue) // <-- here
}
or
.onReceive(viewModel.$value) { val in
if let newValue = val {
let _ = Color.clear.preference(key: NewValueKey.self, value: newValue) // <-- here
}
}
I've been tinkering for a bit with SwiftUI but some of the errors the compiler shows are a bit inexpressive and difficult to understand as I'm getting started.
I'm building a list with the latest Xcode versions with sections in the following way,
public struct DatasetView: View {
public init() {}
#ObservedObject var viewModel = DatasetViewModel()
public var body: some View {
List(viewModel.directories) { folder in
Section(header: Text("Section")) { // <-- Error here
Text(folder.path)
}
}.navigationBarTitle("Dataset")
.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
}
}
The error produced is
Cannot invoke initializer for type 'Section' with an argument list of type '(header: Text, #escaping () -> ())'
I've searched in the documentation and checked blogposts that use the same syntax but I'm lost here. What's missing/wrong?
EDIT: Added the DatasetViewModel
public class DatasetViewModel: ObservableObject {
#Published var directories: [Directory] = []
init() {
search()
}
func search() {
let docsPath = Bundle.main.resourcePath!
let urls = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: "dataset/train")
var aux: [Directory] = []
urls?.forEach({
aux.append(Directory(path: $0.pathComponents.last! ))
print($0.pathComponents.last)
})
aux.sort(by: < )
directories = aux
}
}
public struct Directory: Identifiable, Comparable {
public static func < (lhs: Directory, rhs: Directory) -> Bool {
return lhs.path < rhs.path
}
public var id = UUID()
public var path: String
}
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
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.
I recently downloaded this project and converted it to the latest swift syntax. I can't understand why I continue to get the error "Initializer for conditional binding must have Optional type, not "UIView"
Error is occuring on line:
let containerView = transitionContext.containerView
Here is the full code:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
let containerView = transitionContext.containerView
else {
return
}
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
let screenBounds = UIScreen.main.bounds
let bottomLeftCorner = CGPoint(x: 0, y: screenBounds.height)
let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
fromVC.view.frame = finalFrame
},
completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
}
It is because containerView property is not an Optional type. As you can see here (which is taken from the documentation (command + LMB) on UIViewControllerContextTransitioing):
...
public protocol UIViewControllerContextTransitioning : NSObjectProtocol {
// The view in which the animated transition should take place.
#available(iOS 2.0, *)
public var containerView: UIView { get }
...
You can place the erroneous line after the guard statement.