SwiftUI closure update #State inside init - swiftui

When I create a view I wish to login the user and when the login is complete, the view will change.
As I am quite new to SwiftUI it seems the way I am trying to this is not working. (I do know it has something to do with SwiftUI structs)
I have a state variable to know if the progressView should animate
#State private var isLoading: Bool = true
I have an escaping closure to login user
init() {
userService.login { (didError, msg) in
}
}
Progress view(haven't tested it out yet)
ProgressView().disabled(isLoading).frame(alignment: .center)
When I try to do:
userService.login { (didError, msg) in
isLoading.toggle() or self.isLoading.toggle()
}
Xcode return: Escaping closure captures mutating 'self' parameter.
Does anyone know how I can make something like this work?

Instead of init call it in .onAppear somewhere inside body, like
var body: some View {
VStack {
// .. some content
}
.onAppear {
userService.login { (didError, msg) in
self.isLoading.toggle()
}
}
}

Related

Task in SwiftUI runs but view that is inside a sheet is not updated

I have a simple view that is using a class to generate a link for the user to share.
This link is generated asynchronously so is run by using the .task modifier.
class SomeClass : ObservableObject {
func getLinkURL() async -> URL {
try? await Task.sleep(for: .seconds(1))
return URL(string:"https://www.apple.com")!
}
}
struct ContentView: View {
#State var showSheet = false
#State var link : URL?
#StateObject var someClass = SomeClass()
var body: some View {
VStack {
Button ("Show Sheet") {
showSheet.toggle()
}
}
.padding()
.sheet(isPresented: $showSheet) {
if let link = link {
ShareLink(item: link)
} else {
HStack {
ProgressView()
Text("Generating Link")
}
}
}.task {
let link = await someClass.getLinkURL()
print ("I got the link",link)
await MainActor.run {
self.link = link
}
}
}
}
I've simplified my actual code to this example which still displays the same behavior.
The task is properly executed when the view appears, and I see the debug print for the link. But when pressing the button to present the sheet the link is nil.
The workaround I found for this is to move the .task modifier to be inside the sheet, but that doesn't make sense to me nor do I understand why that works.
Is this a bug, or am I missing something?
It's because the sheet's closure is created before it is shown and it has captured the old value of the link which was nil. To make it have the latest value, i.e. have a new closure created that uses the new value, then just add it to the capture list, e.g.
.sheet(isPresented: $showSheet) { [link] in
You can learn more about this problem in this answer to a different question. Also, someone who submitted a bug report on this was told by Apple to use the capture list.
By the way, .task is designed to remove the need for state objects for doing async work tied to view lifecycle. Also, you don't need MainActor.run.

SwiftUI pop view upon observable event

I am trying to pop a SwiftUI view upon a particular event from an observed object. How can I do this? This code does not work because I can't refer to self inside the sink method.
struct MyView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var observable: MyObservable
init() {
observable.$state.sink { state in // !! Escaping closure captures mutating 'self' parameter !!
presentationMode.wrappedValue.dismiss()
}
}
}
You don't need publisher here and definitely should not do this in init because at least environment is not available there (even you'd solve all others errors).
You just need to observe changes of state in regular way, like below
var body: some View {
Text("Some view here")
.onChange(of: observable.state) { newState in
// depending on newState your decision here
presentationMode.wrappedValue.dismiss()
}
}

Capturing SwiftUI View body self and context

I want to implement a SwiftUI button, that accepts onTapGesture and onLongPressGesture, with an overridable handler
I have been unable to get the native button to work with both so have resorted to an HStack. The buttons use the CommandController pattern and dispatches the command from onTapGesture. The basic code was:
struct CmdButton : View {
#EnvironmentObject var appController : AppController
var command : AppCommand
init(command : AppCommand) {
self.command = command
self.onTapGesture {
self.doCommand()
}
}
var body: some View {
HStack {
Image(self.command.icon).resizable().scaledToFit().frame(width: 35)
}
.padding(6)
.cornerRadius(5)
.contentShape(Rectangle())
.animation(.default)
}
func doCommand(){
appController.dispatchCommand(command: self.command)
}
}
The goal was that CmdButton should call doCommand() from onTapGesture by default unless it was overridden by the implementer, thus:
CmdButton(command: SomeCommand)
.onTapGesture {
doSomethingFirst()
instance.doCommand()
}
.onLongPressGesture {
doLongPressAction()
}
I have 2 core issues.
There seems to be no way I can capture the correct self context in the CmdButton implementation to add the default onTapGesture, if I add during init I get the mutating self error. If I add the gesture to the HStack it cannot be overridden and I can't see how to cast some View to a concrete type to assign directly on the var body.
When I override onTapGesture I am unable to get the self context of the instance in the overridden onTapGesture handler to call the doCommand() method on.
I'm aware I could pass around callbacks and attach them in the implementation, but it might pose other issues capturing context and just seems hacky for something so basic and trivial.
EDIT
To clarify, according Apple docs it is valid to:
struct MyView : View {
let img : String = "asset"
let name : String = "Some Name"
var body: some View {
Image(img).onTapGesture {
onTap()
}
}
func onTap(){
print(name)
}
}
Where the correct context is captured and the gesture handler attached to the Image. My question is instead of the gesture being assigned to the inner implementation how can I assign it to the outer View body so it can be overridden.
MyView()
.onTapGesture {
doSomethingElse()
self_instance.onTap()
}
Again, I can call:
MyView().onTap()
How do I get the View instance to call onTap inside the handler?

#State variable is cleared once new SwiftUI view has been fully loaded

Whenever I call the view CardronaView() the #State variable will be loaded with the correct data for about 3 seconds then it will be reset back to it's default which is an empty string. How can I fix this? I am calling the new view with this:
NavigationView {
List {
// MARK: - Display Cardrona
NavigationLink(destination: CardronaView()) {
ZStack(alignment: .topLeading) {
Image("Cardrona-header")
Text("Cardrona").font(.largeTitle).fontWeight(.semibold).foregroundColor(Color.black)
Text(cardTemp).foregroundColor(Color.black).padding(.top, 50).font(.title3)
}
}
}
}
The code in the new view is this:
struct CardronaView: View {
#State var cardDate: String = ""
var body: some View {
List {
Text(cardDate)
}.onAppear {
Api().getJsonCard { (Cardrona) in
cardDate = "\(Cardrona.data_updated)"
}
}
}
}
When making a network request, it is good practice to perform it before loading the next view. There are many good reasons to do so, as in case the call is not successful, the new view is not loaded and avoid a bad user experience. This is where you show an alert to your user saying that the call couldn't be made.
If the network call succeeds, you pass your result to the new view and populate it.
As a network call is asynchronous and you make this call .onAppear, if the call is taking too long, you will see the empty String because your view as already been loaded.
I changed your code in a way that the network call is being made on the precedent view. But maybe you should add the code when the user taps the ListCell and if the call is successful, then the NavigationLink triggers its destination parameter, which is CardonaView(cardDate:).
struct ContentView: View {
#State var cardDate: String = ""
var body: some View {
NavigationView {
List {
// MARK: - Display Cardrona
NavigationLink(destination: CardronaView(cardDate: cardDate)) {
ZStack(alignment: .topLeading) {
Image("Cardrona-header")
Text("Cardrona").font(.largeTitle).fontWeight(.semibold).foregroundColor(Color.black)
Text(cardTemp).foregroundColor(Color.black).padding(.top, 50).font(.title3)
}
}
}
}
.onAppear {
Api().getJsonCard { (Cardrona) in
cardDate = "\(Cardrona.data_updated)"
}
}
}
struct CardronaView: View {
var cardDate: String
var body: some View {
List {
Text(cardDate)
}
}
}

SwiftUI - onReceive not being called for an ObservableObject property change when the property is updated after view is loaded

I have a view that displays a few photos that are loaded from an API in a scroll view. I want to defer fetching the images until the view is displayed. My view, simplified looks something like this:
struct DetailView : View {
#ObservedObject var viewModel: DetailViewModel
init(viewModel: DetailViewModel) {
self.viewModel = viewModel
}
var body: some View {
GeometryReader { geometry in
ZStack {
Color("peachLight").edgesIgnoringSafeArea(.all)
if self.viewModel.errorMessage != nil {
ErrorView(error: self.viewModel.errorMessage!)
} else if self.viewModel.imageUrls.count == 0 {
VStack {
Text("Loading").foregroundColor(Color("blueDark"))
Text("\(self.viewModel.imageUrls.count)").foregroundColor(Color("blueDark"))
}
} else {
VStack {
UIScrollViewWrapper {
HStack {
ForEach(self.viewModel.imageUrls, id: \.self) { imageUrl in
LoadableImage(url: imageUrl)
.scaledToFill()
}.frame(width: geometry.size.width, height: self.scrollViewHeight)
}.edgesIgnoringSafeArea(.all)
}.frame(width: geometry.size.width, height: self.scrollViewHeight)
Spacer()
}
}
}
}.onAppear(perform: { self.viewModel.fetchDetails() })
.onReceive(viewModel.objectWillChange, perform: {
print("Received new value from view model")
print("\(self.viewModel.imageUrls)")
})
}
}
my view model looks like this:
import Foundation
import Combine
class DetailViewModel : ObservableObject {
#Published var imageUrls: [String] = []
#Published var errorMessage : String?
private var fetcher: Fetchable
private var resourceId : String
init(fetcher: Fetchable, resource: Resource) {
self.resourceId = resource.id
// self.fetchDetails() <-- uncommenting this line results in onReceive being called + a view update
}
// this is a stubbed version of my data fetch that performs the same way as my actual
// data call in regards to ObservableObject updates
// MARK - Data Fetching Stub
func fetchDetails() {
if let path = Bundle.main.path(forResource: "detail", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let parsedData = try JSONDecoder().decode(DetailResponse.self, from: data)
self.imageUrls = parsedData.photos // <-- this doesn't trigger a change, and even manually calling self.objectWillChange.send() here doesn't trigger onReceive/view update
print("setting image urls to \(parsedData.photos)")
} catch {
print("error decoding")
}
}
}
}
If I fetch my data within the init method of my view model, the onReceive block on my view IS called when the #Published imageUrls property is set. However, when I remove the fetch from the init method and call from the view using:
.onAppear(perform: { self.viewModel.fetchDetails() })
the onReceive for viewModel.objectWillChange is NOT called, even though the data is updated. I don't know why this is the case and would really appreciate any help here.
Use instead
.onReceive(viewModel.$imageUrls, perform: { newUrls in
print("Received new value from view model")
print("\(newUrls)")
})
I tested this as I found the same issue, and it seems like only value types can be used with onReceive
use enums, strings, etc.
it doesn't work with reference types because I guess technically a reference type doesn't change reference location and simply points elsewhere when changed? idk haha but ya
as a solution, you can set a viewModel #published property which is like a state enum, make changes to that when you have new data, and then on receive can access that...hope that makes sense, let me know if not