SwiftUI view parameters not updating after sheet view dismissed - swiftui

I have a view that displays multiple images fetched from Firebase in a VStack. Each image is a button, the button opens a sheet and it displays the image and text that goes with it. When I dismiss the sheet and press on a different image the same data comes up. How do I refresh these paramaters
VStack View with images as buttons:
#State var dic = OrderedDictionary<String, UIImage>()
VStack (alignment: .center) {
ForEach(dic.keys, id: \.self) { dicObject in
Button {
showingSheet.toggle()
} label: {
Image(uiImage: dic[dicObject]!)
.resizable()
.frame(width: 300, height: 400)
.cornerRadius(20)
}
.sheet(isPresented: $showingSheet) {
GalleryViewSpecific(image: dic[dicObject]!, url: dicObject)
}
}
}
View specific image view used by .sheet
//#State var image: UIImage
#State var image: UIImage
#State var url: String
#Environment(\.dismiss) var dismiss
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Button("Press to dismiss") {
dismiss()
}
.font(.title)
.padding()
.background(.black)
Image(uiImage: image)
.resizable()
.frame(width: 300, height: 400)
.cornerRadius(20)
Text(url)
.foregroundColor(.white)
}
}
.background(Color.black)
}

Store the displayed item when you tap the button inside the ForEach:
import SwiftUI
struct ContentView: View {
struct Item: Identifiable {
let id = UUID()
let text: String
}
let items = [Item(text: "1"), Item(text: "2")]
#State var selectedItem: Item?
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
Button(action: {
selectedItem = item
}) {
Text(item.text)
}
}
.sheet(item: $selectedItem) { selectedItem in
Text(selectedItem.text)
}
}
.padding()
}
}

Related

how to create picker menu like drop down in SwiftUI

I am using a picker. Onclick textField, it shows the picker menu. But picker options show the top of the textField. Onclick textField toggle shows
Here is the image
Here is my code:
struct PickerView: View {
#State private var text:String = ""
#State private var options = ["Option 1", "Option 2", "Option 3"]
#State private var selectedOption = "Option 1"
#State private var showPicker: Bool = false
var body: some View {
VStack {
HStack {
TextField("", text: $selectedOption)
.disabled(true)
Image(systemName: "chevron.down")
.foregroundColor(.gray)
}
.padding()
.background(Color.gray)
.cornerRadius(5.0)
Picker("Options", selection: $selectedOption) {
ForEach(options, id: \.self) { option in
Text(option).tag(option)
}
}
.pickerStyle(.automatic)
.padding()
.background(Color.white)
.cornerRadius(5.0)
.shadow(radius: 5)
.offset(y: -100)
.opacity(showPicker ? 1 : 0)
.animation(.default)
}
.onTapGesture {
self.showPicker.toggle()
}
}
}
How to show the picker onClick textField directly like drop down?
Please help me..
This might be what you wanted
var body: some View {
VStack(alignment: .leading) {
HStack {
Picker("Options", selection: $selectedOption) {
ForEach(options, id: \.self) { option in
Text(option).tag(option)
}
}
.tint(.black)
.pickerStyle(.menu)
Spacer()
}
.background(.gray)
.cornerRadius(5.0)
}
}

Why the scrollview doesn't get updated with new data from array?

I'm trying to send and then display them in the scrollview realtime. But nothing shows up. How to solve it? So, basically when the user types the message into a textbox then it will be saved in array and then it will be populated to the crollView in realtime so the user can view all the messages.
Error: No errors, it just isn't visible.
import SwiftUI
struct SingleMessageBubbleModel: Identifiable {
let id = UUID()
var text: String
var received: Bool
var timeStamp: Date
}
var messagesDBArray : [SingleMessageBubbleModel] = []
struct ContentView: View {
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String
var body: some View {
if (showOnTheSpotMessaging) {
VStack {
HStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messagesDBArray, id: \.id) { message in
MessageBubble(message: message)
}
}
}
.padding(.top, 10)
.background(.gray)
.onChange(of: messagesDBArray.count) { id in
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .bottomLeading)
}
HStack () {
TextEditor (text: $textTyped)
.frame(width: 200, height: 200, alignment: .leading)
Button ("Send", action: {
messagesDBArray.append(SingleMessageBubbleModel(text: textTyped, received: true, timeStamp: Date()))
})
}
}
}
}
}
struct MessageBubble: View {
var message: SingleMessageBubbleModel
#State private var showTime = false
var body: some View {
VStack(alignment: message.received ? .leading : .trailing) {
HStack {
Text(message.text)
.padding()
.background(message.received ? Color.gray : Color.blue)
.cornerRadius(30)
}
.frame(maxWidth: 300, alignment: message.received ? .leading : .trailing)
.onTapGesture {
withAnimation {
showTime.toggle()
}
}
if showTime {
Text("\(message.timeStamp.formatted(.dateTime.hour().minute()))")
.font(.caption2)
.foregroundColor(.gray)
.padding(message.received ? .leading : .trailing, 25)
}
}
.frame(maxWidth: .infinity, alignment: message.received ? .leading : .trailing)
.padding(message.received ? .leading : .trailing)
.padding(.horizontal, 4)
}
}
Basically, when the button is pressed, your property messagesDBArray is well and truly append with the new value.
However, and it's really important to understand this point in swiftUI, nothing triggers the refresh of the view.
I suggest you two solutions:
If you don't need messagesDBArray to be outside of ContentView:
You just have to add messagesDBArray as a state in ContentView like following
struct ContentView: View {
#State var messagesDBArray : [SingleMessageBubbleModel] = []
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String = ""
var body: some View {
if (showOnTheSpotMessaging) {
VStack {
HStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messagesDBArray, id: \.id) { message in
MessageBubble(message: message)
}
}
}
.padding(.top, 10)
.background(.gray)
.onChange(of: messagesDBArray.count) { id in
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .bottomLeading)
}
HStack () {
TextEditor (text: $textTyped)
.frame(width: 200, height: 200, alignment: .leading)
Button ("Send", action: {
messagesDBArray.append(SingleMessageBubbleModel(text: textTyped, received: true, timeStamp: Date()))
})
}
}
}
}
}
If you need messagesDBArray to be outside of ContentView:
1- Create a class (ViewModel or Service or whatever you wan to call it) with messagesDBArray as a #Published property
final class ViewModel: ObservableObject {
#Published var messagesDBArray : [SingleMessageBubbleModel] = []
}
2- Observe this class in ContentView in order to append and receive the update
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String = ""
var body: some View {
if (showOnTheSpotMessaging) {
VStack {
HStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(viewModel.messagesDBArray, id: \.id) { message in
MessageBubble(message: message)
}
}
}
.padding(.top, 10)
.background(.gray)
.onChange(of: viewModel.messagesDBArray.count) { id in
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .bottomLeading)
}
HStack () {
TextEditor (text: $textTyped)
.frame(width: 200, height: 200, alignment: .leading)
Button ("Send", action: {
viewModel.messagesDBArray.append(SingleMessageBubbleModel(text: textTyped, received: true, timeStamp: Date()))
})
}
}
}
}
}
I hope that this is clear to you and that it has been useful 😉

SwiftUI: Updating a View to include a Custom Subview based on a user action is a separate view

so I am trying to have a view update to display a custom view based on a user selection from another view. This is a simple task app project I started to get a better understanding of SwiftUI and have hit my first major roadblock. The custom view is generated from a Tag object from Core Data, so it would be this information that is passed from View 2 to View 1.
I've marked where the update would take place as well as where the action is performed with TODOs. Hopefully I did a good job at explaining what I am hoping to accomplish, nothing I have tried seems to work. I am sure it's something simple but the solution is evading me.
View 1: View that needs to be updated when user returns
View 2: View where selection is made
The View that needs to be updated and its ViewModel.
struct AddTaskView: View {
//MARK: Variables
#Environment(\.managedObjectContext) var coreDataHandler
#Environment(\.presentationMode) var presentationMode
#StateObject var viewModel = AddTaskViewModel()
#StateObject var taskListViewModel = TaskListViewModel()
#State private var title: String = ""
#State private var info: String = ""
#State private var dueDate = Date()
var screenWidth = UIScreen.main.bounds.size.width
var screenHeight = UIScreen.main.bounds.size.height
var body: some View {
VStack(spacing: 20) {
Text("Add a New Task")
.font(.title)
.fontWeight(.bold)
//MARK: Task.title Field
TextField("Task", text: $title)
.font(.headline)
.padding(.leading)
.frame(height: 55)
//TODO: Update to a specific color
.background(Color(red: 0.9, green: 0.9, blue: 0.9))
.cornerRadius(10)
//MARK: Task.tag Field
HStack {
Text("Tag")
Spacer()
//TODO: UPDATE TO DISPLAY TAG IF SELECTED OTHERWISE DISPLAY ADDTAGBUTTONVIEW
NavigationLink(
destination: TagListView(),
label: {
AddTagButtonView()
}
)
.accentColor(.black)
}
//MARK: Task.info Field
TextEditor(text: $info)
.frame(width: screenWidth - 40, height: screenHeight/4, alignment: .center)
.autocapitalization(.sentences)
.multilineTextAlignment(.leading)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black, lineWidth: 0.5)
)
//MARK: Task.dateDue Field
DatePicker(
"Due Date",
selection: $dueDate,
in: Date()...
)
.accentColor(.black)
.font(.headline)
Spacer()
Button(action: {
viewModel.addTask(taskTitle: title, taskInfo: info, taskDueDate: dueDate)
//Dismiss View if successful
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Add Task")
.frame(width: 150, height: 60)
.font(.headline)
.foregroundColor(.black)
.background(Color.yellow)
.cornerRadius(30)
})
}
.padding()
.navigationBarTitleDisplayMode(.inline)
}
}
final class AddTaskViewModel : ObservableObject {
var coreDataHandler = CoreDataHandler.shared
#Published var tag : Tag?
func addTask(taskTitle: String, taskInfo: String, taskDueDate: Date) {
let newTask = Task(context: coreDataHandler.container.viewContext)
newTask.title = taskTitle
newTask.info = taskInfo
newTask.dateCreated = Date()
newTask.dateDue = taskDueDate
newTask.completed = false
newTask.archived = false
coreDataHandler.save()
}
}
The View where the selection is made and its ViewModel
struct TagListView: View {
#FetchRequest(entity: Tag.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Tag.title, ascending: true)]) var tagList : FetchedResults<Tag>
#Environment(\.presentationMode) var presentationMode
#StateObject var viewModel = TagListViewModel()
var body: some View {
VStack {
HStack {
Text("Create a Tag")
.font(.system(size: 20))
.fontWeight(.medium)
Spacer()
NavigationLink(
destination: CreateTagView(),
label: {
Image(systemName: "plus.circle")
.font(.system(size: 25))
})
}
Divider()
.padding(.bottom, 10)
ScrollView(.vertical, showsIndicators: false, content: {
if tagList.count != 0 {
LazyVStack(spacing: 20) {
ForEach(tagList, id: \.self) { tag in
let tagColour = Color(red: tag.colourR, green: tag.colourG, blue: tag.colourB, opacity: tag.colourA)
Button {
//TODO: UPDATE ADDTASKVIEW TO DISPLAY THE SELECTED TAG
//Dismiss view
self.presentationMode.wrappedValue.dismiss()
} label: {
TagView(title: tag.title ?? "Tag", color: tagColour, darkText: false)
}
}
}
} else {
Text("Add your first tag.")
}
})
}
.padding()
}
}
final class TagListViewModel : ObservableObject {
}

Making a chat app in SwiftUI: How to make ScrollView keep its place when the keyboard shows up?

I am making a chat app in SwiftUI. Here is the effect that I want to have: open any chat in Telegram or Whatsapp, tap on the input box. The content of the chat slides up when the keyboard slides up. So if you were looking at say the bottom message, you can still see it.
I am unable to get this effect in SwiftUI. Keyboard sliding up does not slide the content of the chat:
import SwiftUI
struct SlidingKeyboardTest: View {
#State var inputText = "Placeholder"
var body: some View {
VStack {
ScrollView {
LazyVStack {
ForEach(1...100, id: \.self) { id in
HStack {
Spacer()
Text("message \(id)")
Spacer()
}
}
}
}
TextEditor(text: $inputText)
.frame(height: 50)
}
.background(LinearGradient(gradient: Gradient(colors: [.white, .blue, .white]), startPoint: .top, endPoint: .bottom))
.onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
}
}
struct SlidingKeyboardTest_Previews: PreviewProvider {
static var previews: some View {
SlidingKeyboardTest()
}
}
Any ideas how to get this effect?
You need to use Introspect to receive access to UIScrollView and listen to keyboard height changes.
Here is a code:
import Combine
import Introspect
import SwiftUI
struct SlidingKeyboardTest: View {
#State var inputText = "Placeholder"
#State var keyboardHeight = CGFloat(0)
#State var scrollView: UIScrollView? = nil
var body: some View {
VStack {
ScrollView {
LazyVStack {
ForEach(1 ... 100, id: \.self) { id in
HStack {
Spacer()
Text("message \(id)")
Spacer()
}
}
}
}.introspectScrollView {
scrollView = $0
}
TextEditor(text: $inputText)
.frame(height: 50)
}.onReceive(Publishers.keyboardHeight) { height in
if height > 0 {
self.scrollView!.setContentOffset(CGPoint(x: 0, y: self.scrollView!.contentOffset.y + height), animated: true)
} else {
self.scrollView!.contentOffset.y = max(self.scrollView!.contentOffset.y - keyboardHeight, 0)
}
keyboardHeight = height
}
.background(LinearGradient(gradient: Gradient(colors: [.white, .blue, .white]), startPoint: .top, endPoint: .bottom))
.onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
}
}
struct SlidingKeyboardTest_Previews: PreviewProvider {
static var previews: some View {
SlidingKeyboardTest()
}
}

How do I properly use NavigationView in a ZStack?

I am trying to add some filter options to sit at the top of my view, above the NavigationView. I wrote the following code that mostly does what I want, however it disabled the ability to click on the rows to get to the detailed view. I assume this is because my filter buttons are on top of the ZStack, but I'm not sure how else to get this to work.
Here is the code I wrote:
import SwiftUI
struct BonusList: View {
var bonuses = sampleBonusData
#State var showSettings = false
#State var showBonuses = false
#State var bonusEarned = true
#State var showStatePicker = false
#State var showCategoryPicker = false
var body: some View {
ZStack {
NavigationView {
List(bonuses) { item in
NavigationLink(destination: BonusDetail(bonusName: item.bonusName, bonusCode: item.bonusCode, city: item.city, sampleImage: item.sampleImage)) {
HStack(spacing: 12.0) {
Image(item.sampleImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.background(Color.white)
.cornerRadius(15)
VStack(alignment: .leading) {
HStack {
Text(item.bonusName)
.font(.headline)
Spacer()
Image(systemName: "checkmark.shield")
.opacity(self.bonusEarned ? 100 : 0)
}
Text("\(item.city), \(item.state)")
.font(.subheadline)
.frame(height: 25.0)
HStack {
Text(item.bonusCategory)
.font(.caption)
.fontWeight(.bold)
.foregroundColor(.gray)
.padding(.top, 4)
Spacer()
Text(item.bonusCode)
.font(.caption)
.fontWeight(.bold)
.foregroundColor(.gray)
.padding(.top, 4)
}
}
}
}
}
.navigationBarTitle(Text("Bonuses"))
// .navigationBarHidden(true)
}
.saturation(self.bonusEarned ? 0 : 1)
HStack {
FilterByCategory(showCategoryPicker: $showCategoryPicker)
Spacer()
FilterByState(showStatePicker: $showStatePicker)
}
StatePicker(showStatePicker: $showStatePicker)
CategoryPicker(showCategoryPicker: $showCategoryPicker)
}
}
}
This is what it looks like when I run it:
If I'm understanding correctly, you have a view or two which sit higher in the ZStack that are off canvas and come in when those buttons are tapped?
You could consider using a modal and setting the view you want to show for each button as the view for the modal. This will keep your views off screen and still allow interaction with your list. Here's what I've done...
On the main view
import SwiftUI
struct MainView: View {
#State private var isPresented = false
var body: some View {
NavigationView {
VStack {
//...
}
//Modal
.sheet(isPresented: $isPresented, content: {
AddItem(showModal: self.$isPresented)
})
}
}
}
The modal's view
import SwiftUI
struct AddItem: View {
#Binding var showModal: Bool
var body: some View {
VStack {
Button(action: {
self.showModal = false
}, label: {
Text("Cancel")
})
}
}
}