I cant use a lazy var with EnvironmentObject property in a struct - swiftui

struct MainView: View {
#EnvironmentObject var coreServices: CoreServices
#State lazy var watchConnectivityService: WatchConnectivityService = {
coreServices.service(byType: WatchConnectivityService.self)!
}()
var body: some View {
VStack() {
Button("Send Message") {
watchConnectivityService.session.sendMessage(["message" : self.messageText], replyHandler: nil) { (error) in
print(error.localizedDescription)
}
}
}
}
}
When I write a code like this im noticing that the #EnvironmentObject is not available after the initialization is completed. So I cant just use a #State variable it needs to be lazy...but If I use a lazy variable i get Cannot use mutating getter on immutable value: 'self' is immutable.

You cannot use lazy in SwiftUI view. Use instead function for this case:
struct MainView: View {
#EnvironmentObject var coreServices: CoreServices
private func watchConnectivityService() -> WatchConnectivityService {
coreServices.service(byType: WatchConnectivityService.self)!
}
var body: some View {
VStack() {
Button("Send Message") {
watchConnectivityService().session.sendMessage(["message" : self.messageText], replyHandler: nil) { (error) in
print(error.localizedDescription)
}
}
}
}
}

Related

Missing argument for parameter 'View Call' in call

I am struggle with understanding about why i have to give Popup view dependency named vm while calling this view since it is observable
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView() /// this line shows error
}
}
}
struct DetailView:View {
#ObservedObject var vm:ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
You have to set your vm property when you init your View. Which is the usual way.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView(vm: ViewModel()) // Initiate your ViewModel() and pass it as DetailView() parameter
}
}
}
struct DetailView:View {
var vm: ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
Or you could use #EnvironmentObject. You have to pass an .environmentObject(yourObject) to the view where you want to use yourObject, but again you'll have to initialize it before passing it.
I'm not sure it's the good way to do it btw, as an environmentObject can be accessible to all childs view of the view you declared the .environmentObject on, and you usually need one ViewModel for only one View.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView().environmentObject(ViewModel()) // Pass your ViewModel() as an environmentObject
}
}
}
struct DetailView:View {
#EnvironmentObject var vm: ViewModel // you can now use your vm, and access it the same say in all childs view of DetailView
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}

How to access to SwiftUI content view in extension delegate on Apple Watch?

I need to call loadData in my ContentView when the app becomes active. ExtensionDelegate is a class which handle app events such as applicationDidBecomeActive. But I don't understand how to get ContentView inside ExtensionDelegate.
This is my ContentView:
struct ContentView: View {
let network = Network()
#State private var currentIndex: Int = 0
#State private var sources: [Source] = []
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
}
func loadData() {
network.getSources { response in
switch response {
case .result(let result):
self.sources = result.results
case .error(let error):
print(error)
}
}
}
}
And ExtensionDelegate:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
}
func applicationDidBecomeActive() {
// Here I need to call 'loadData' of my ContentView
}
func applicationWillResignActive() {
}
...
The simplest solution as I see would be to use notification
in ContentView
let needsReloadNotification = NotificationCenter.default.publisher(for: .needsNetworkReload)
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
.onReceive(needsReloadNotification) { _ in self.loadData()}
}
and in ExtensionDelegate
func applicationDidBecomeActive() {
NotificationCenter.default.post(name: .needsNetworkReload, object: nil)
}
and somewhere in shared
extension Notification.Name {
static let needsNetworkReload = Notification.Name("NeedsReload")
}

Custom event modifier in SwiftUI

I created a custom button, that shows a popover. Here is my code:
PopupPicker
struct PopupPicker: View {
#State var selectedRow: UUID?
#State private var showPopover = false
let elements: [PopupElement]
var body: some View {
Button((selectedRow != nil) ? (elements.first { $0.id == selectedRow! }!.text) : elements[0].text) {
self.showPopover = true
}
.popover(isPresented: self.$showPopover) {
PopupSelectionView(elements: self.elements, selectedRow: self.$selectedRow)
}
}
}
PopupSelectionView
struct PopupSelectionView: View {
var elements: [PopupElement]
#Binding var selectedRow: UUID?
var body: some View {
List {
ForEach(self.elements) { element in
PopupText(element: element, selectedRow: self.$selectedRow)
}
}
}
}
PopupText
struct PopupText: View {
var element: PopupElement
#Binding var selectedRow: UUID?
var body: some View {
Button(element.text) {
self.presentation.wrappedValue.dismiss()
self.selectedRow = self.element.id
}
}
}
That works fine, but can I create a custom event modifier, so that I can write:
PopupPicker(...)
.onSelection { popupElement in
...
}
I can't give you a full solution as I don't have all of your code and thus your methods to get the selected item anyhow, however I do know where to start.
As it turns out, declaring a function with the following syntax:
func `onSelection`'(arg:type) {
...
}
Creates the functionality of a .onSelection like so:
struct PopupPicker: View {
#Binding var selectedRow: PopupElement?
var body: some View {
...
}
func `onSelection`(task: (_ selectedRow: PopupElement) -> Void) -> some View {
print("on")
if self.selectedRow != nil {
task(selectedRow.self as! PopupElement)
return AnyView(self)
}
return AnyView(self)
}
}
You could theoretically use this in a view like so:
struct ContentView: View {
#State var popupEl:PopupElement?
var body: some View {
PopupPicker(selectedRow: $popupEl)
.onSelection { element in
print(element.name)
}
}
}
However I couldn't test it properly, please comment on your findings
Hope this could give you some insight in the workings of this, sorry if I couldn't give a full solution

How to toggle animation in View from outside with preparation in SwiftUI?

I'm building a UI component with SwiftUI that should have trigger from outside to turn on animation and some inner preparations for it. In examples below it's prepareArray() function.
My first approach was to use bindings, but I've found that there is no way to listen when #Binding var changes to trigger something:
struct ParentView: View {
#State private var animated: Bool = false
var body: some View {
VStack {
TestView(animated: $animated)
Spacer()
Button(action: {
self.animated.toggle()
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#State private var array = [Int]()
#Binding var animated: Bool {
didSet {
prepareArray()
}
}
var body: some View {
Text("\(array.count): \(animated ? "Y" : "N")").background(animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
}
private func prepareArray() {
array = [1]
}
}
Why then it allows didSet listener for #Binding var if it's not working?! Then I switched to simple Combine signal since it's can be caught in onReceive closure. But #State on signal was not invalidating view on value pass:
struct ParentView: View {
#State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)
var body: some View {
VStack {
TestView(animated: animatedSignal)
Spacer()
Button(action: {
self.animatedSignal.send(!self.animatedSignal.value)
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#State private var array = [Int]()
#State var animated: CurrentValueSubject<Bool, Never>
var body: some View {
Text("\(array.count): \(animated.value ? "Y" : "N")").background(animated.value ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
if animated {
self.prepareArray()
}
}
}
private func prepareArray() {
array = [1]
}
}
So my final approach was to trigger inner state var on signal value:
struct ParentView: View {
#State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)
var body: some View {
VStack {
TestView(animated: animatedSignal)
Spacer()
Button(action: {
self.animatedSignal.send(!self.animatedSignal.value)
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#State private var array = [Int]()
let animated: CurrentValueSubject<Bool, Never>
#State private var animatedInnerState: Bool = false {
didSet {
if animatedInnerState {
self.prepareArray()
}
}
}
var body: some View {
Text("\(array.count): \(animatedInnerState ? "Y" : "N")").background(animatedInnerState ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
self.animatedInnerState = animated
}
}
private func prepareArray() {
array = [1]
}
}
Which works fine, but I can't believe such a simple task requires so complicated construct! I know that SwiftUI is declarative, but may be I'm missing more simple approach for this task? Actually in real code this animated trigger will have to be passed to one more level deeper(
It is possible to achieve in many ways, including those you tried. Which one to choose might depend on real project needs. (All tested & works Xcode 11.3).
Variant 1: modified your first try with #Binding. Changed only TestView.
struct TestView: View {
#State private var array = [Int]()
#Binding var animated: Bool
private var myAnimated: Binding<Bool> { // internal proxy binding
Binding<Bool>(
get: { // called whenever external binding changed
self.prepareArray(for: self.animated)
return self.animated
},
set: { _ in } // here not used, so just stub
)
}
var body: some View {
Text("\(array.count): \(myAnimated.wrappedValue ? "Y" : "N")")
.background(myAnimated.wrappedValue ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
}
private func prepareArray(for animating: Bool) {
DispatchQueue.main.async { // << avoid "Modifying state during update..."
self.array = animating ? [1] : [Int]() // just example
}
}
}
Variant2 (my preferable): based on view model & publishing, but requires changes both ParentView and TestView, however in general simpler & clear.
class ParentViewModel: ObservableObject {
#Published var animated: Bool = false
}
struct ParentView: View {
#ObservedObject var vm = ParentViewModel()
var body: some View {
VStack {
TestView()
.environmentObject(vm) // alternate might be via argument
Spacer()
Button(action: {
self.vm.animated.toggle()
}) {
Text("Toggle")
}
Spacer()
}
}
}
struct TestView: View {
#EnvironmentObject var parentModel: ParentViewModel
#State private var array = [Int]()
var body: some View {
Text("\(array.count): \(parentModel.animated ? "Y" : "N")")
.background(parentModel.animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
.onReceive(parentModel.$animated) {
self.prepareArray(for: $0)
}
}
private func prepareArray(for animating: Bool) {
self.array = animating ? [1] : [Int]() // just example
}
}

Invalidate List SwiftUI

Workaround at bottom of Question
I thought SwiftUI was supposed to automatically update views when data they were dependent on changed. However that isn't happening in the code below:
First I make a simple BindableObject
import SwiftUI
import Combine
class Example: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var test = 1 {
didSet {
didChange.send(())
}
}
}
Then the root view of the app:
struct BindTest : View {
#Binding var test: Example
var body: some View {
PresentationButton(destination: BindChange(test: $test)) {
ForEach(0..<test.test) { index in
Text("Invalidate Me! \(index)")
}
}
}
}
And finally the view in which I change the value of the BindableObject:
struct BindChange : View {
#Binding var test: Example
#Environment(\.isPresented) var isPresented
var body: some View {
Button(action: act) {
Text("Return")
}
}
func act() {
test.test = 2
isPresented?.value = false
}
}
When the return button is tapped there should be 2 instances of the Text View - but there is only 1. What am I doing wrong?
Also worth noting: If I change the #Binding to #EnvironmentObject the program just crashes when you tap the button producing this error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Full code below:
import SwiftUI
import Combine
class Example: BindableObject {
var didChange = PassthroughSubject<Example, Never>()
var test = 1 {
didSet {
didChange.send(self)
}
}
static let `default` = {
return Example()
}()
}
//Root View
struct BindTest : View {
#EnvironmentObject var test: Example
var body: some View {
PresentationButton(destination: BindChange()) {
ForEach(0..<test.test) { t in
Text("Invalidate Me! \(t)")
}
}
}
}
//View that changes the value of #Binding / #EnvironmentObject
struct BindChange : View {
#EnvironmentObject var test: Example
#Environment(\.isPresented) var isPresented
var body: some View {
Button(action: act) {
Text("Return")
}
}
func act() {
test.test = 2
isPresented?.value = false
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
//ContentView().environmentObject(EntryStore())
BindTest().environmentObject(Example())
}
}
#endif
EDIT 2: Post's getting a little messy at this point but the crash with EnvironmentObject seems to be related to an issue with PresentationButton
By putting a NavigationButton inside a NavigationView the following code produces the correct result - invalidating the List when test.test changes:
//Root View
struct BindTest : View {
#EnvironmentObject var test: Example
var body: some View {
NavigationView {
NavigationButton(destination: BindChange()) {
ForEach(0..<test.test) { t in
Text("Functional Button #\(t)")
}
}
}
}
}