Passing a managed object context to a view in SwiftUI is easy enough using an #Environment variable. But getting that same context to the model and view model, not so much. Here is what I tried:
I created a property called context in the view model. In the view, I pass the managed object context lazily. The problem is that now I'm getting an error when I call a view model's method from the view - "Cannot use mutating getter on immutable value: 'self' is immutable". The method worked before I added the context, so why did it stop working? And, more importantly, how do I make it work?!
Model:
struct Model {
//use fetch request to get users
func checkLogin(username: String, password: String) {
for user in users {
if username == user.userEmail && password == user.password {
return true
}
}
return false
}
}
View Model:
class ViewModel {
var context: NSManagedObjectContext
private var model = Model()
init(context: NSManagedObjectContext) {
self.context = context
}
func checkLogin(username: String, password: String) -> Bool {
model.checklogin(username: username, password: password)
}
}
And finally, the View:
struct LoginView: View {
#Environment(\.managedObjectContext) var moc
lazy var viewModel = ViewModel(context: moc)
//Login form
Button(action: {
if self.viewModel.checkLogin(username: self.email, password: self.password) {
//ERROR: Cannot use mutating getter on immutable value: 'self' is immutable
//allow login
}
}) {
Text("login")
}
}
You cannot use lazy var in View, because it is immutable. Here is possible approach to solve your case
class ViewModel {
var context: NSManagedObjectContext?
private var model = Model()
init(context: NSManagedObjectContext? = nil) {
self.context = context
}
func checkLogin(username: String, password: String) -> Bool {
return model.checkLogin(username: username, password: password)
}
}
struct LoginView: View {
#Environment(\.managedObjectContext) var moc
private let viewModel = ViewModel() // no context yet here, so just default
//Login form
var body: some View {
Button(action: {
if self.viewModel.checkLogin(username: self.email, password: self.password) {
//allow login
}
}) {
Text("login")
}
.onAppear {
self.viewModel.context = moc // << set up context here
}
}
}
Related
Suppose I have some environmental object that stores the states of a user being signed in or not;
class Account: ObservableObject{
#Published var isSignedIn: Bool = false
}
I want to conditional display a view if the user is signed in. So,
app loads -> RootView(). If user is signed In -> go to Profile, otherwise go to LoginScreen
struct RootView: View {
#EnvironmentObject private var account: Account
var body: some View {
NavigationStackViews{
// This is just a custom class to help display certain views with a transition animation
if account.isSignedIn{ // I only want to read this ONCE. Otherwise, when this value changes, my view will abruptly change views
ProfileView()
} else { SignInView() }
}
}
}
Set a #State variable in onAppear(perform:) of the current isSignedIn value. Then use that in the view body instead.
Code:
struct RootView: View {
#EnvironmentObject private var account: Account
#State private var isSignedIn: Bool?
var body: some View {
NavigationStackViews {
if let isSignedIn = isSignedIn {
if isSignedIn {
ProfileView()
} else {
SignInView()
}
}
}
.onAppear {
isSignedIn = account.isSignedIn
}
}
}
In my app, there is a singleton instance, AppSetting, which is used in the entire views and models. AppSetting has a variable, userName.
class AppSetting: ObservableObject {
static let shared = AppSetting()
private init() { }
#Published var userName: String = ""
}
ParentView prints userName when it is not empty. At first, it is empty.
struct ParentView: View {
#State var isChildViewPresented = false
#ObservedObject var appSetting = AppSetting.shared
var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)
VStack {
Button(action: { self.isChildViewPresented = true }) {
Text("Show ChildView")
}
if !appSetting.userName.isEmpty { // <--- HERE!!!
Text("\(appSetting.userName)")
}
}
if isChildViewPresented {
ChildView(isPresented: $isChildViewPresented)
}
}
}
}
When a user taps the button, userName will be set.
struct ChildView: View {
#Binding var isPresented: Bool
#ObservedObject var childModel = ChildModel()
var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)
VStack {
Button(action: { self.childModel.setUserName() }) { // <--- TAP BUTTON HERE!!!
Text("setUserName")
}
Button(action: { self.isPresented = false }) {
Text("Close")
}
}
}
}
}
class ChildModel: ObservableObject {
init() { print("init") }
deinit { print("deinit") }
func setUserName() {
AppSetting.shared.userName = "StackOverflow" // <--- SET userName HERE!!!
}
}
The problem is when userName is set, the instance of ChildModel is invalidated. I think when ParentView adds Text("\(appSetting.userName)"), it changes its view hierarchy and then it makes SwiftUI delete the old instance of ChildModel and create a new one. Sadly, it gives me tons of bug. In my app, the ChildModel instance must be alive until a user explicitly closes ChildView.
How can I make the ChildModel instance alive?
Thanks in advance.
It is possible when to de-couple view & view model and inject dependency via constructor
struct ChildView: View {
#Binding var isPresented: Bool
#ObservedObject var childModel: ChildModel // don't initialize
// ... other your code here
store model somewhere externally and inject when show child view
if isChildViewPresented {
// inject ref to externally stored ChildModel()
ChildView(isPresented: $isChildViewPresented, viewModel: childModel)
}
I have multiple classes that I want to use with a budget picker view. They all have this budgetable protocol defined.
import SwiftUI
struct BudgetPickerView: View {
#EnvironmentObject var userData: UserData
#State var budgetable: Budgetable
...
}
import Foundation
protocol Budgetable
{
var budgetId: String { get set }
}
For example this Allocation class
import Foundation
import Combine
class Allocation: ObservableObject, Identifiable, Budgetable {
let objectWillChange = ObservableObjectPublisher()
let id: String?
var amount: String { willSet { self.objectWillChange.send() } }
var budgetId: String { willSet { self.objectWillChange.send() } }
init(id: String? = nil, amount: String, budgetId: String) {
self.id = id
self.amount = amount.removePrefix("-")
self.budgetId = budgetId
}
}
However, when I try to pass an allocation into my budget picker view I get an error
NavigationLink(destination: BudgetPickerView(budgetable: allocation))...
Cannot convert return expression of type 'NavigationLink>, BudgetPickerView>' to return type 'some View'
Expression type 'BudgetPickerView' is ambiguous without more context
Change as bellow code
struct BudgetPickerView: View {
#EnvironmentObject var userData: UserData
var budgetable: Budgetable
var body: some View {
...
}
}
and
NavigationLink(destination: BudgetPickerView(budgetable: allocation).EnvironmentObject(UserData()))
By SwiftUI concept you are not allowed to work with #State outside of View, but the following works well (having other your parts unchanged)
struct BudgetPickerView: View {
#State private var budgetable: Budgetable
init(budgetable: Budgetable) {
_budgetable = State<Budgetable>(initialValue: budgetable)
}
var body: some View {
Text("Hello, World!")
}
}
struct TestBudgetPickerView: View {
var body: some View {
NavigationView {
NavigationLink(destination:
BudgetPickerView(budgetable: Allocation(amount: "10", budgetId: "1")))
{ Text("Item") }
}
}
}
BTW, just incase, again by design #State is intended to hold temporary-view-state-only data, not a model. For model is more preferable to use ObservableObject. In your case Budgetable looks like a model.
For the following code, I am getting the following error. I don't know how to work around this. How can I call volumeCheck() upon the button click?
struct ContentView: View {
var player = AVAudioPlayer()
var body: some View {
HStack {
Button(action: {
self.volumeCheck()
}) {
Text("Click to test chimes volume")
}
}
}
mutating func volumeCheck() {
guard let url = Bundle.main.url(
forResource: "chimes",
withExtension: "mp3"
) else { return }
do {
player = try AVAudioPlayer(contentsOf: url)
player.prepareToPlay()
player.volume = Float(self.sliderValue)
player.play()
} catch let error {
print(error.localizedDescription)
}
print("volume checked print")
}
}
The problem is that View is a struct and it's body field is a computed property with a nonmutating getter. In your code it happens mutating method to be called in that nonmutating getter. So all you need to do is put your player to some kind of model:
class Model {
var player: AVPlayerPlayer()
}
struct ContentView: View {
var model = Model()
// player can be changed from anywhere
}
P.S. In some other cases you may want changes in your model be reflected in view so you'd have to add #ObservedObject just before model declaration.
Hope that helps
You are trying to set player to a new object in volumeCheck(). Set the player in your initialiser:
struct ContentView: View {
private var player: AVAudioPlayer?
public init() {
if let url = Bundle.main.url(forResource: "chimes",
withExtension: "mp3") {
self.player = try? AVAudioPlayer(contentsOf: url)
}
}
var body: some View {
HStack {
Button("Click to test chimes volume") {
self.volumeCheck()
} .disabled(player == nil)
}
}
private func volumeCheck() {
player?.prepareToPlay()
player?.volume = Float(self.sliderValue)
player?.play()
print("volume checked print")
}
}
Please note that you are using sliderValue in your code even though it is not defined anywhere.
I want a #Published variable to be persisted, so that it's the same every time when I relaunch my app.
I want to use both the #UserDefault and #Published property wrappers on one variable. For example I need a '#PublishedUserDefault var isLogedIn'.
I have the following propertyWrapper
import Foundation
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
This is my Settings class
import SwiftUI
import Combine
class Settings: ObservableObject {
#Published var isLogedIn : Bool = false
func doLogin(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
self.login = myresponse.login
}
}
}
}
My View class
struct HomeView : View {
#EnvironmentObject var settings: Settings
var body: some View {
VStack {
if settings.isLogedIn {
Text("Loged in")
} else{
Text("Not Loged in")
}
}
}
}
Is there a way to make a single property wrapper that covers both the persisting and the publishing?
import SwiftUI
import Combine
fileprivate var cancellables = [String : AnyCancellable] ()
public extension Published {
init(wrappedValue defaultValue: Value, key: String) {
let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
self.init(initialValue: value)
cancellables[key] = projectedValue.sink { val in
UserDefaults.standard.set(val, forKey: key)
}
}
}
class Settings: ObservableObject {
#Published(key: "isLogedIn") var isLogedIn = false
...
}
Sample: https://youtu.be/TXdAg_YvBNE
Version for all Codable types check out here
To persist your data you could use the #AppStorage property wrapper.
However, without using #Published your ObservableObject will no longer put out the news about the changed data. To fix this, simply call objectWillChange.send() from the property's willSet observer.
import SwiftUI
class Settings: ObservableObject {
#AppStorage("Example") var example: Bool = false {
willSet {
// Call objectWillChange manually since #AppStorage is not published
objectWillChange.send()
}
}
}
It should be possible to compose a new property wrapper:
Composition was left out of the first revision of this proposal,
because one can manually compose property wrapper types. For example,
the composition #A #B could be implemented as an AB wrapper:
#propertyWrapper
struct AB<Value> {
private var storage: A<B<Value>>
var wrappedValue: Value {
get { storage.wrappedValue.wrappedValue }
set { storage.wrappedValue.wrappedValue = newValue }
}
}
The main benefit of this approach is its predictability: the author of
AB decides how to best achieve the composition of A and B, names it
appropriately, and provides the right API and documentation of its
semantics. On the other hand, having to manually write out each of the
compositions is a lot of boilerplate, particularly for a feature whose
main selling point is the elimination of boilerplate. It is also
unfortunate to have to invent names for each composition---when I try
the compose A and B via #A #B, how do I know to go look for the
manually-composed property wrapper type AB? Or maybe that should be
BA?
Ref: Property WrappersProposal: SE-0258
You currently can't wrap #UserDefault around #Published since that is not currently allowed.
The way to implement #PublishedUserDefault is to pass an objectWillChange into the wrapper and call it before setting the variable.
struct HomeView : View {
#StateObject var auth = Auth()
#AppStorage("username") var username: String = "Anonymous"
var body: some View {
VStack {
if username != "Anonymous" {
Text("Logged in")
} else{
Text("Not Logged in")
}
}
.onAppear(){
auth.login()
}
}
}
import SwiftUI
import Combine
class Auth: ObservableObject {
func login(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
UserDefaults.standard.set(myresponse.login, forKey: "username")`
}
}
}
}