In my initial ContentView() I have a button that presents a UIImagePicker, when an image is chosen I then navigate to SecondView where I can view the image and it’s data from UIImagePickerController.InfoKey
I currently have an ImageManager class that’s set as an EnviromentObject that I pass the InfoKey to that then sets up all the variables in that class — it works but this feels messy.
What I’d like to do is init the ImageManager class when I navigate to SecondView as that’s the only view that needs that data.
I’d tried passing the InfoKey as a variable:
#State var InfoKey: [IImagePickerController.InfoKey: Any]
SecondView(key: self.infoKey)
but this crashes because I don’t have any data until an image is chosen
What would be the best way to tackle this?
#State must have initial value, so use just empty container
#State var infoKey: [IImagePickerController.InfoKey: Any] = [:]
then pass it in SecondView as binding
SecondView(key: self.$infoKey)
where
struct SecondView: View {
#Binding var key: [IImagePickerController.InfoKey: Any]
Related
I don't know how to initialize a CoreData model in my ContentView:
This is the declaration line so far:
#State var selectedFriend: Friend
I want to use the type Friend in a picker but I don't know how to setup a custom picker so it works with a core data model.
Any thoughts or suggestions?
Thank you.
If you fetch the data with #FetchRequest and you want to display the data in a picker (no empty selection) my suggestion is to work with indices. A scalar value type can be initialised directly.
#State private var selectedFriendIndex = 0
...
Picker("Friend", selection: $selectedFriendIndex) {
ForEach(0..<friends.count, id: \.self) { index in
Text(friends[index].name)
}
}
I'm working in SwiftUI and have a GeometryProxy as a property of class used on a view & subview. My code is working fine, but I'd like the subview to render in Preview properly, but to do this, the PreviewProvider needs to have valid GeometryProxy data in a property (geo) of the class I've created (LayoutData). SwiftUI doesn't let me update an object's property as indicated in the comments in the code below. Is there some other way I might be able to get a valid GeometryProxy & use this to update a property in my LayoutData class when using the PreviewProvider? Code otherwise works great & I can run my Live Preview from the parent View, but it would be nice to see things rendered properly on my subview - which I could do if I could pass in my LayoutData object with a valid GeometryProxy property.
Really, all I need from the GeometryProxy is the screen width, so I could use just that in my LayoutData class, but I don't want to use UIScreen.main.width since this will be deprecated & I don't know of another way to get a valid width other than GeometryReader. Here is the code I've attempted, but of course, the comment line trying to update a variable property inside a View, as I've shown, can't be done in SwiftUI.
struct ButtonLayout_Previews: PreviewProvider {
static var previews: some View {
var layoutData = LayoutData()
GeometryReader { previewGeo in
layoutData.geo = previewGeo // You can't do this in SwiftUI
ButtonLayoutView(resultMessage: .constant(""), layoutData: layoutData)
}
}
}
I also know that I can introduce an additional, separate GeometryProxy property for the subview & pass that in as an extra property, using the GeometryReader setup, above, but I don't want to add an extra variable to my code if it's not needed & if I can use the Preview with the GeometryProxy property of my LayoutData class. Thanks for any ideas!
I was able to make the assignment using an empty let statement, and including the assignment after the equal, like below. Any concerns with this? It seems like I'm tricking SwiftUI into accepting an assignment where it normally wouldn't, but this works:
struct ButtonLayout_Previews: PreviewProvider {
static var previews: some View {
let layoutData = LayoutData()
GeometryReader { previewGeo in
let _ = layoutData.geo = previewGeo
ButtonLayoutView(resultMessage: .constant(""), layoutData: layoutData)
}
}
}
I am trying out this quick start for SwiftUI and Combine in order to try and understand how to connect my Realm database to Combine.
The example observes a RealmSwift.List and keeps a table populated with its data. This is is a linked list to a child class. I'm wondering how to observe a Results collection so I can keep track of any changes to an entire Realm class.
For example, let's say I have a Workspace class:
class Workspace: Object, ObjectKeyIdentifiable{
#objc dynamic var id = UUID().uuidString
#objc dynamic var name = ""
#objc dynamic var archived = false
}
In the state object, I can set up a Results<Workspace> variable like this:
class AppState: ObservableObject {
#Published var workspaces: Results<Workspace>?
var cancellables = Set<AnyCancellable>()
init(){
let realmPublisher = PassthroughSubject<Realm, Error>()
realmPublisher
.sink(receiveCompletion: { _ in }, receiveValue: { realm in
//Get the Results
self.workspaces = realm.objects(Workspace.self)
})
.store(in: &cancellables)
realmPublisher.send(try! Realm())
return
}
}
But when it comes time to observe the object, I can't because Results isn't an object (I assume).
struct ContentView: App {
#ObservedObject var state = AppState()
var view: some View {
ItemsView(workspaces: state.workspaces!)
}
var body: some Scene {
WindowGroup {
view.environmentObject(state)
}
}
}
struct ItemsView: View {
#ObservedObject var workspaces: Results<Workspace> //<!-- Error
var body: some View {
//...
}
}
Xcode gives a syntax error on the workspaces property:
Property type 'Results' does not match that of the 'wrappedValue' property of its wrapper type 'ObservedObject'
Is it possible to observe a set of Results just like we can have a notification listener on a collection of Results?
Technically, you could hook up a sink to state.workspaces (state.$workspaces.sink()), but in this case, I think you're overcomplicating the problem.
You already have an #ObservableObject in your ContentView (AppState) that is managing the results for you. So, change ItemsView to just take this as a parameter:
var workspaces: Results<Workspace>?
It doesn't need to be an #ObservedObject -- either way, whether it's getting observed in that view or it's parent view, it's going to get re-rendered. It does have to be optional here, since it's an optional value on your AppState, unless you want to keep passing it with the force unwrap (!), but that's generally a bad idea, since it'll crash if it ever is in fact nil.
Also, above, in your Realm code, make sure it's matching the tutorial that you were following. For example, you have Publisher.sink which should really be realmPublisher.sink
You are correct, Results is a struct, and therefore cannot be covered by #StateObject or #ObservedObject. Your workaround is suitable for now.
Once https://github.com/realm/realm-cocoa/pull/7045 is released, you will be able to use one of the new Realm property wrappers to embed your Results into the view directly. At the time of this posting, that would be #FetchRealmResults, but that is subject to change.
I understand how to use #EnvironmentObject to make global data available to any view class, but I can't find a way to pass the variable to a non-view class.
My situation, which seems like a common issue, is:
Login, returns an access token. This token is required to be used in all subsequent api calls
Store access token in a UserSettings class
Place the UserSettings in an Environment bucket
let contentView = ContentView()
.environmentObject(UserSettings())
Each view will display data based on the data returned in an api call
struct HomeView: View {
#EnvironmentObject var user: UserSettings <=== ACCESS TO GLOBAL DATA
#ObservedObject var categories = Categories() <=== GET DATA FOR THIS VIEW
var body: some View {
...
}
Categories() is a non-view Swift class that will retrieve the data, but requires the access token which is stored in UserSettings
class Categories : ObservableObject {
#Published var allCategories = CategoriesModel()
#EnvironmentObject var user: UserSettings <==== CANNOT ACCESS THIS DATA!
init() {
fetchCategories(page: 0, pageSize: 20)
}
UserSettings is empty because this class is not a View struct and doesn't seem to behave like a View class would. Since the access token I need is stored in the UserSettings class, how can I get access to this? Is there an alternative way to store/access this data?
Use a Singleton Pattern. Call UserSettings.sharedInstance from Categories & let contentView = ContentView().environmentObject(UserSettings.sharedInstance)
https://developer.apple.com/documentation/swift/cocoa_design_patterns/managing_a_shared_resource_using_a_singleton
In a SwiftUI app, I have an ObservableObject that keeps track of user settings:
class UserSettings: ObservableObject {
#Published var setting: String?
}
I have a view model to control the state for my view:
class TestViewModel: ObservableObject {
#Published var state: String = ""
}
And I have my view. When the user setting changes, I want to get the view model to update the state of the view:
struct HomeView: View {
#EnvironmentObject var userSettings: UserSettings
#ObservedObject var viewModel = TestViewModel()
var body: some View {
Text(viewModel.state)
.onReceive(userSettings.$setting) { setting in
self.viewModel.state = setting
}
}
}
When the UserSettings.setting is changed in another view it causes onReceive on my view to get called in an infinite loop, and I don't understand why. I saw this question, and that loop makes sense to me because the state of the ObservableObject being observed is being changed on observation.
However, in my case I'm not changing the observed object (environment object) state. I'm observing the environment object and changing the view model state which redraws the view.
Is the view redrawing what's causing the issue here? Does onReceive get called everytime the view is redrawn?
Is there a better way of accomplishing what I'm trying to do?
EDIT: this is a greatly simplified version of my problem. In my app, the view model takes care of executing a network request based on the user's settings and updating the view's state such as displaying an error message or loading indicator.
Whenever you have an onReceive with an #ObservedObject that sets another (or the same) published value of the #ObservedObject you risk creating an infinite loop if those published attributes are being displayed somehow.
Make your onReceive verify that the received value is actually updating a value, and not merely setting the same value, otherwise it will be setting/redrawing infinitely. In this case, e.g.,:
.onReceive(userSettings.$setting) { setting in
if setting != self.viewModel.state {
self.viewModel.state = setting
}
}
From described scenario I don't see the reason to duplicate setting in view model. You can show the value directly from userSettings, as in
struct HomeView: View {
#EnvironmentObject var userSettings: UserSettings
#ObservedObject var viewModel = TestViewModel()
var body: some View {
Text(userSettings.setting)
}
}
You might be able to prevent infinite re-rendering of the view body by switching your #ObservedObject to #StateObject.