iOS SwiftUI - View closes after changing #AppStorage value - swiftui

I'm absolute beginner in iOS development and I'm wondering why nobody else faced this issue because I can't find similar question.
At the beginning I'm declaring:
#AppStorage("userid") var userid: Int = 0
then couple lines of code down below:
if(userid == 0){
NavigationLink(destination: Login(), label: {
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit).frame(width: 32)
.foregroundColor(Color(UIColor(named: "IconColor")!))
})
}else{
NavigationLink(destination: Login(), label: {
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit).frame(width: 32)
.foregroundColor(Color.blue)
})
}
in the Login() view:
struct Login: View {
#AppStorage("userid") var userid: Int = 0
var body: some View{
Button(action: {
if(userid == 0){
userid = 1
}else{
userid = 0
}
}) {
Text("weird")
}
}
}
After taping on weird, Login() closes and I see the icon color changing which means #AppStorage value changed.
But why does the view closes on changing a #AppStorage value??

It closes because your NavigationLink is inside an if clause. When the condition changes, the result of the link disappears with the link.
It looks like you should have one NavigationLink with a conditional statement within the foregroundColor:
.foregroundColor(userid == 0 ? Color(UIColor(named: "IconColor")!) : .blue)

Related

LazyVStack in ScrollView has jittery auto scroll

I have a chat app, where whenever a chat room is opened, I need the view to scroll to the bottom as soon as the messages are fetched.
The thing is that although it does scroll perfectly when a new message is received or sent (see ViewModel down below), it is very jittery when I tell it to scroll right after the first batch of messages is fetched, which happens once as soon as the view appears.
After a lot of trial and error, I realized that if I add a small delay to the scroll, it'll improve but not completely! It is like it's trying to scroll to the very bottom, but it'll fail just for a few inches. I also realized that if I add a bigger delay, like 2 seconds, it'll scroll just fine.
Here's the messages list view:
struct MessagesView: View {
#StateObject private var viewModel = ViewModel()
// -----------------------
let currentChatRoom: ChatRoom
// -----------------------
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
VStack {
ScrollView {
ScrollViewReader { proxy in
LazyVStack {
ForEach(viewModel.messages) { message in
MessageView(message: message)
.id(message.id)
.onTapGesture {
viewModel.shouldDismissKeyboard = true
}
}
}
.onChange(of: viewModel.shouldScrollToMessageId) { messageId in
if let messageId = messageId, !messageId.isEmpty {
proxy.scrollTo(messageId, anchor: .bottom)
}
}
}
}
VStack(alignment: .leading) {
if chatEnvironment.isOtherUserTyping {
TypingIndicationView()
}
BottomView()
.padding(.bottom, 4)
}
}
}
.onAppear {
viewModel.setUp(currentChatRoom: currentChatRoom)
}
}
}
As you can see, it’s viewModel.shouldScrollToMessageId that’s responsible for "auto-scrolling" to the last message.
Here’s MessageView:
fileprivate struct MessageView: View {
let message: Message
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 1) {
Text(message.user.isCurrentUser == true ? "You" : "\(message.user.username)")
.foregroundColor(message.user.isCurrentUser == true ? .customGreen : .customBlue)
.multilineTextAlignment(.leading)
.font(.default(size: 16))
.padding(.bottom, 1)
if let imageURL = message.postSource?.imageURL, !imageURL.isEmpty {
VStack(alignment: .leading) {
WebImage(url: .init(string: imageURL))
.resizable()
.indicator(.activity)
.scaledToFill()
.frame(width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.width / 1.45)
.cornerRadius(25)
}
}
Text(message.text)
.foregroundColor(.white)
.multilineTextAlignment(.leading)
.font(.default(size: 16))
}
Spacer()
}
.padding(.bottom, 8)
.padding(.horizontal)
.background(
Color.black
)
}
}
Here’s the ViewModel:
class ViewModel: ObservableObject {
#Published var messages = [Message]()
#Published var text = ""
#Published var shouldScrollToMessageId: String?
#Published var currentChatRoom: ChatRoom?
// -----------------------------
private var isInitialized = false
// -----------------------------
func setUp(currentChatRoom: ChatRoom) {
guard !isInitialized else { return }
isInitialized.toggle()
// -----------------------------
self.currentChatRoom = currentChatRoom
// -----------------------------
getFirstBatchOfMessages(chatRoom: chatRoom)
subscribeToNewMessages()
}
private func getFirstBatchOfMessages(chatRoom: ChatRoom) {
messagesService.getMessages(chatRoomId: chatRoom.id) { [weak self] messages in
DispatchQueue.main.async {
self?.messages = messages
}
self?.scrollToBottom(delay: 0.15)
}
}
private func subscribeToNewMessages() {
...
if !newMessages.isEmpty {
self?.scrollToBottom(delay: 0)
}
}
func scrollToBottom(delay: TimeInterval) {
DispatchQueue.main.async {
self.shouldScrollToMessageId = self.messages.last?.id
}
}
func sendMessage() {
...
scrollToBottom(delay: 0)
}
}
Here, scrollToBottom is responsible for notifying the MessagesView that shouldScrollToMessageId's value changed and that it should scroll to the last message.
Any help will be much appreciated!!!
I am also writing an application with a chat on SwiftUI and I also have a lot of headaches with a bunch of ScrollView and LazyVStack.
In my case, I load messages from the CoreData and display them in a LazyVStack, so in my case, scrolling to the last message does not work, it seems to me simply because a bottom last view did not render, because rendering starts from the top.
Therefore, I came up with a solution with placing an invisible view at the bottom and assigned it a static ID, in my case -1:
VStack(spacing: 0) {
LazyVStack(spacing: 0) {
ForEach(messages) { message in
MessageRowView(viewWidth: wholeViewProxy.size.width, message: message)
.equatable()
}
}
Color.clear.id(-1)
.padding(.bottom, inputViewHeight)
}
I call scroll to this view:
.onAppear {
scrollTo(messageID: -1, animation: nil, scrollReader: scrollReader)
}
And it works... but sometimes...
Sometimes it works correctly, sometimes it stops without scrolling a couple of screens to the end. Looks like LazyVStack is rendering the next views after the scrollTo has finished its work. Also, if add scrolling with some delay it may works better, but not always perfect.
I hope this can be a little helpful and if you find a stable solution I will be very happy if you share :)

SwiftUI navigation decision based on a sheet view presenting 2 choice

I am presenting a "wizard" that will be detecting a BLE device and then if it is the correct one the last view will ask if we want to register or skip.
Edit:{
the view order is: MainView presenting in fullScreenCover a first info view informing on how to detect the BLE device then this one pushes a second view with some info on the nearest BLE device and it is in this view that we have the fork where I am presenting a sheet to ask if the user wants to continue and register the BLE device or skip.
So MAIN > INFOView -> BLE detection (> Register or skip ? RegisterView : Destack to main)
}
I have that last view come up as a sheet it has 2 buttons, the first one as mentioned says "Register" and the other one says "skip". If the user presses the register then we dismiss the sheet and navigate to a view that is gathering personal info to register the BLE device. on the other hand, if the user chooses to skip then the wizard need to de-stack back over to the main view.
Normally in UIKit I would just have a delegate inform me of the choice then if skip was selected. I would call pop to root view controller, otherwise, if the register option was selected I would dismiss the sheet view and then navigate to one more final view and get the user registered.
In SwiftUI I do not know how to deal with that navigation fork. I tried using PassthroughSubject but then I have to set the PassthroughSubject var as a state var and in the end, I just did not get the call back from sending in the selection.
Tried binding then Was hoping to make an onReceive but then it is asking for a publisher and that felt wrong to create a publisher just for that.
I am wondering g what is the best way do take care of this in. swiftUI ?
edit:
this is the code (updated with the replay from #Predrag Samardzic) for the view that shows the info on the BLE device (smart bike) and will push at first a request to know if the user wants to register or not, then if yes push that registration screen if not dismiss the entire stack.
struct A18BikeDiscoveryView: View {
#EnvironmentObject var bleManager: ArgonBLEManager
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack{
NavigationLink(
destination: A18RegistrationQuestionairy(QuestionairyViewModel()),
isActive: $isRegistrationPresented
) {
EmptyView()
}
A18ImageTextBanner(text: NSLocalizedString("bike_discovery_view_title", comment: ""))
.padding(.bottom, 35)
.navigationBarBackButtonHidden(true)
if let value = bleManager.model?.bikeInfo?.bikeModel{
Text(value)
.fontWeight(.bold)
.scaledFont(.largeTitle)
}
Image("subitoBike")
.resizable()
.frame(minWidth: 0334, idealWidth: 334, maxWidth: .infinity, minHeight: 223, idealHeight: 223, maxHeight: .infinity, alignment: .center)
.aspectRatio(contentMode: .fit)
.padding(.bottom, 10)
Divider()
VStack(alignment: .leading){
HStack{
Text("bike_discovery_view_year_created")
if let v = bleManager.model?.bikeInfo?.year{
Text(v)
}
}
HStack{
Text("bike_discovery_view_model_size")
Text("\(getSizeFromSerial())")
}
HStack{
Text("bike_discovery_view_bike_serial_number")
if let v = bleManager.model?.bikeInfo?.bikeSerialNumber {
Text(v)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 66, alignment: .leading)
.padding(.horizontal, 40)
Divider()
.padding(.bottom, 30)
Button(action: {
isSheetPresented = true
}, label: {
Text("bike_discovery_view_bike_pairing_button_title")
.fontWeight(.bold)
.foregroundColor(.white)
})
.buttonStyle(A18RoundButtonStyle(bgColor: .red))
.padding(.horizontal)
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
A18BikeParingSelection(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
Button(action: {
bleManager.disconect()
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("bike_discovery_view_bike_pairing_cancel_button_title")
.fontWeight(.bold)
.foregroundColor(Color("grey55"))
})
.padding()
Spacer()
}
.navigationBarColor(backgroundColor: .white, tintColor: .black)
.navigationBarTitleDisplayMode(.inline)
}
func getSizeFromSerial() -> String {
if let serial = bleManager.model?.bikeInfo?.bikeSerialNumber {
if serial.contains("XXS"){
return "XXS"
}else if serial.contains("XSM") {
return "XS"
}else if serial.contains("SML"){
return "S"
}else if serial.contains("MED"){
return "M"
}else if serial.contains("LAR"){
return "L"
}
}
return "N/A"
}
}
This is one possible solution - using CurrentValueSubject in order to trigger dismiss and keep info about the choice made on the presented screen. Then, if registration is needed, you trigger it when sheet is dismissed.
struct MainView: View {
private let shouldShowRegistration = CurrentValueSubject<Bool, Never>(false)
#State var isSheetPresented = false
#State var isRegistrationPresented = false
var body: some View {
VStack {
// this part is if you want to push registration screen, you will need to have MainView inside NavigationView for it
NavigationLink(
destination: RegistrationView(),
isActive: $isRegistrationPresented
) {
EmptyView()
}
// ----------------------------------------------------
Button {
isSheetPresented = true
} label: {
Text("Present sheet")
}
.sheet(
isPresented: $isSheetPresented,
onDismiss: {
if shouldShowRegistration.value {
isRegistrationPresented = true
}},
content: {
ChoiceView(shouldShowRegistration: shouldShowRegistration)
})
.onReceive(shouldShowRegistration) { shouldShowRegistration in
isSheetPresented = false
}
// this part is if you want to present registration screen as sheet
// .sheet(
// isPresented: $isRegistrationPresented,
// content: {
// RegistrationView()
// })
}
}
}
struct ChoiceView: View {
let shouldShowRegistration: CurrentValueSubject<Bool, Never>
var body: some View {
VStack{
Button {
shouldShowRegistration.send(false)
} label: {
Text("Dismiss")
}
Button {
shouldShowRegistration.send(true)
} label: {
Text("Register")
}
}
}
}
struct RegistrationView: View {
var body: some View {
Text("Registration")
}
}

How to save that my button has already been tapped in SwiftUI

I have the following code that, when tapped it will add the number of likes to a post, it will only allow the user to tap it once, works well but when I reload the app I can like it again, been trying to workout the best way to save that it has been tapped already. I have added the button state as false:
#State var buttonTapped = false
Button(action:
{
self.buttonTapped.toggle() //only allow one tap
let like = Int.init(post.likes)!
ref.collection("Posts").document(post.id).updateData(["likes": "\(like
+ 1)"]) { (err) in
if err != nil{
print((err!.localizedDescription))
return
}
// postData.getAllPosts()
print("updated...")
}
}
) {
Image(systemName: "flame")
.resizable()
.frame(width: 20, height: 20)
}.disabled(buttonTapped)
Any pointers in the right direction would be greatly appreciated
You can use UserDefaults to store the value
struct ContentView : View {
#State var buttonTapped : Bool = UserDefaults.standard.bool(forKey: "buttonTapped")
var body : some View {
Button(action: {
UserDefaults.standard.set(true, forKey: "buttonTapped")
buttonTapped.toggle()
}) {
Image(systemName: "flame")
.resizable()
.frame(width: 20, height: 20)
}.disabled(buttonTapped)
}
}

SwiftUI: popover to persist (not be dismissed when tapped outside)

I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}

SwiftUI - how to save settings selections

import SwiftUI
struct CardTheme: View {
//#State private var theme = 0
#State private var theme = UserDefaults.standard.integer(forKey: "Card Theme")
var body: some View {
List {
HStack {
Text("Mono")
//.font(.system(size: 12))
.onTapGesture {
self.setTheme(i: 0)
}
Spacer()
if(theme == 0) {
Image(systemName: "checkmark")
.foregroundColor(Color.green)
}
}
HStack {
Text("Cool")
// .font(.system(size: 12))
.onTapGesture {
self.setTheme(i: 1)
}
Spacer()
if(theme == 1) {
Image(systemName: "checkmark")
.foregroundColor(Color.green)
}
}
HStack {
Text("Cute")
// .font(.system(size: 12))
.onTapGesture {
self.setTheme(i: 2)
}
Spacer()
if(theme == 2) {
Image(systemName: "checkmark")
.foregroundColor(Color.green)
}
}
}
.navigationBarTitle(Text(verbatim: "Card Theme"))
}
func setTheme(i: Int) {
theme = i
UserDefaults.standard.set(i, forKey: "Card Theme")
}
}
I have a settings menu where the user picks a theme, the default value is set to a global variable, globalVarTheme, which is 0. But after they make a selection, exit that menu, and re-enter the menu it goes back to 0 (the first item) even if they have chosen one of the other items. How do I save their selection?
Also, what is the best way to save user selections beyond the current app session? Should I write all their selections to a plist file or is there a conventional way?
#State is used for the changes within a given view. It is not meant to persist across the views. Instead use #Environment property wrapper. WWDC 2019 talks about that when to use what.
#State isn't the right PropertyWrapper.
If you want to use your settings in multiple views than use #EnvironmentObject as the PropertyWrapper.
You can read about the different PropertyWrappers here:
https://medium.com/#alex.hsieh/state-objectbinding-and-environmentobject-in-swiftui-783588b60671
If you want to save the Settings beyond the current app session you can use UserDefauls.
How do I use UserDefaults with SwiftUI?