Remember #State variable after switching views - swift

I'm making an application for macOS with SwiftUI. I have a button that starts a download process asynchronously by calling Task.init and an async function. When the task starts, I set a #State busy variable to true so I can display a ProgressView while the download is happening. The problem is if I switch to a different view then back while it is downloading, the state gets reset although the task still runs. How can I make it so it remembers the state, or maybe check if the task is still running?
Here is a stripped down example:
import SwiftUI
struct DownloadRow: View {
let content: Content
#State var busy = false
var body: some View {
if !busy {
Button() {
Task.init {
busy = true
await content.download()
busy = false
}
} label: {
Label("Download")
}
} else {
ProgressView()
}
}
}

You could put the variable that tracks the download's progress into an ObservableObject class and make the progress variable #Published. Have the object that tracks your progress as an #ObservedObject in the view. By doing so, you decouple the progress tracker from the view's lifecycle. Make sure this view does not initialize the progress tracker, or a new object will be built when the view is built again.

Related

Is it possible to start the NavigationSplitView in an expanded state on macOS using SwiftUI?

I like the sidebar to be opened at launch.
However when I build and run the app, this is what I get.
So I need to click on the sidebar icon to show it. This is not the behavior I want. Is it possible to change this?
Somehow, without explicitly setting it in code, the app likes to change the column visibility to .detailOnly at launch. To avoid this behavior, I explicitly set it to .all at onAppear
#State private var columnVisibility =
NavigationSplitViewVisibility.all
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
Text("Side bar")
} detail: {
Text("Main part")
}
.onAppear() {
columnVisibility = .all
}
}

Launch windows in a SwiftUI App-based project from custom events on macOS

In a SwiftUI macOS app that uses the SwiftUI App lifecycle I have been struggling to understand how to launch a new window and then assign it some initial state.
Using Xcode 12.5’s new project we get:
import SwiftUI
#main
struct StackOverflowExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
To keep the question simple, let’s say that I would like to launch a new window every minute and send the time the window was launched to the view. For this we can use a Timer like so:
Timer.scheduledTimer(
withTimeInterval: 60,
repeats: true,
block: {_ in
// do something here that launches a new window containing
// ContentView and sends it the current time
}
)
Any ideas on how to proceed?

Binding not updated immediately inside updateUIView in UIViewRepresentable

I am adapting a Swift Project to SwiftUI. The original project has Drag and Drop on UIImages, drawing and some other manipulations which aren't possible in SwiftUI.
The code below is for explaining the problem only. Working code on:
https://github.com/rolisanchez/TestableDragAndDrop
On the project, when a user clicks on a button, a #State setChooseImage is changed to true, opening a sheet in which he is presented with a series of images:
.sheet(isPresented: self.$setChooseImage, onDismiss: nil) {
VStack {
Button(action: {
self.setChooseImage = false
self.chosenAssetImage = UIImage(named: "testImage")
self.shouldAddImage = true
}) {
Image("testImage")
.renderingMode(Image.TemplateRenderingMode?.init(Image.TemplateRenderingMode.original))
Text("Image 1")
}
Button(action: {
self.setChooseImage = false
self.chosenAssetImage = UIImage(named: "testImage2")
self.shouldAddImage = true
}) {
Image("testImage2")
.renderingMode(Image.TemplateRenderingMode?.init(Image.TemplateRenderingMode.original))
Text("Image 2")
}
}
}
After selecting the image, setChooseImage is set back to false, closing the sheet. The Image self.chosenAssetImage is also a #State and is set to the chosen Image. The #State shouldAddImage is also set to true. This chosen UIImage and Bool are used inside a UIView I created to add the UIImages. It is called inside SwiftUI like this:
DragAndDropRepresentable(shouldAddImage: $shouldAddImage, chosenAssetImage: $chosenAssetImage)
The UIViewRepresentable to add the UIView is the following:
struct DragAndDropRepresentable: UIViewRepresentable {
#Binding var shouldAddImage: Bool
#Binding var chosenAssetImage: UIImage?
func makeUIView(context: Context) -> DragAndDropUIView {
let view = DragAndDropUIView(frame: CGRect.zero)
return view
}
func updateUIView(_ uiView: DragAndDropUIView, context: UIViewRepresentableContext< DragAndDropRepresentable >) {
if shouldAddImage {
shouldAddImage = false
guard let image = chosenAssetImage else { return }
uiView.addNewAssetImage(image: image)
}
}
}
I understand updateUIView is called whenever there are some
changes that could affect the views. In this case, shouldAddImage
became true, thus entering the if loop correctly and calling
addNewAssetImage, which inserts the image into the UIView.
The problem here is that updateUIView is called multiple times, and
enters the if shouldAddImage loop all those times, before updating the shouldAddImage Binding. This means that it will add the image multiple times to the UIView.
I put a breakpoint before and after the assignment of shouldAddImage = false, and even after passing that line, the value continues to be true.
The behavior I wanted is to change the shouldAddImage immediately to false, so that even if updateUIView is called multiple times, it would only enter the loop once, adding the image only once.

Sharing ViewModel between SwiftUI components

I'd like to implement a navigator/router for an architecture implemented with SwiftUI and Combine. In a few words the View will share viewModel with Router. When the View triggers a change on the viewModel the Router should navigate to a new sheet.
This is a version of my code where I'm directly passing the viewModel from View to Router. Is there anything wrong? My biggest doubt is that since I'm using #ObservedObject on both the Router and the View, two different instances of the viewModel are created.
VIEW MODEL
class BootViewModel:ObservableObject{
#Published var presentSignIn = false
}
VIEW
struct BootView: View {
#ObservedObject var viewModel:BootViewModel
var navigator:BootNavigator<BootView>? = nil
init(viewModel:BootViewModel) {
self.viewModel = viewModel
self.navigator = BootNavigator(view: self, viewModel: viewModel)
self.navigator.setSubscriptions()
}
var body: some View {
VStack{
Text("Hello")
Button("Button"){
self.viewModel.presentSignIn.toggle()
}
}
}
}
NAVIGATOR
class BootNavigator<T:View>{
var view:T? = nil
#ObservedObject var viewModel:BootViewModel
init(view:T, viewModel:BootViewModel) {
self.view = view
self.viewModel = viewModel
}
func setSubscriptions(){
subscribe(onSigninPressed: $viewModel.presentSignIn)
}
func subscribe(onSigninPressed : Binding<Bool>){
_ = view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
}
}
}
Why the SignInView is never presented?
Without taking into account the fact that using a router with swiftUI is not needed in general(I'm mostly doing an exercise)... is there anything wrong with this implementation?
This
view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
MUST be somewhere in body (directly or via computed property or func) but inside body's ViewBuilder
Some notes I have to point out here:
ValueType
There is a difference between an UIView and a SwiftUI View. All SwiftUI Views are value type! So they get copied when you pass them around. Be aware of that.
Single instance
If you want a single instance like a regular navigator for your entire app, you can use singleton pattern. But there is a better approach in SwiftUI universe called #Environment objects. You can take advantage of that.
Trigger a view refresh
To refresh the view (including presenting something), you must code inside the var body. But it can be directly written on indirectly through a function or etc.

Using ObservableObject to navigate to different views using SwiftUI

I'm trying to create logic that will send the user to the correct view once they have entered their credentials. In the login page, when the user logs in they get a token that can be used to call an API. I store that token in KeychainSwift. I then can check to see if the field where I stored it has an entry that is not null and not empty to route the user to the main page. I'm using logic like this and this to route me to the correct view.
My content view code is as follows:
struct ContentView: View {
#ObservedObject var userAuth = UserAuth()
var body: some View {
if(self.userAuth.isLoggedIn){
return AnyView(MainPageView())
}else{
return AnyView(AccountPageView())
}
}
}
UserAuth is as follows:
import Combine
class UserAuth: ObservableObject {
// let objectWillChange = ObservableObjectPublisher()
#Published var isLoggedIn: Bool = false;
init() {
let keychain = KeychainSwift()
let userToken = keychain.get("userToken") ?? ""
if(!userToken.isEmpty || userToken != "" ){
self.isLoggedIn = true
}else{
self.isLoggedIn = false
}
}
}
The overall architecture im trying to achieve is this:
The issue is that there is no direct way to route from a view to another hence why I resorted to using logic that that displayed in ContentView. I have similar logic for login. I have a view (loginformview) that either routes you to the login form view or to the main page view. But once you login in the login form I would need to go back to the previous view (loginformview) where the check exist to see if I should return the login page or go to the main page(this time sending you to the main page). It seems very cyclical, anyone had any idea to proper tackle logic like this ?