Refresh Data in SwiftUI View pulled from HTTP Request - rest

I've been looking for a solution to this problem but none of the ones I've found seem to fix my specific situation. Hopefully, with these details, someone can help me out. I'm new to Swift so bear with me. I'm working with a REST Api and HTTP Request from our help desk ticket system. I'm trying to find a way to refresh the data automatically when something changes and also have a way to manually refresh i.e. a pull down or actual refresh button. If I can figure this out for the two scenarios below, I think I can apply it to the rest of my scenarios.
Scenario #1 - I submit a new note for a ticket but the detail view doesn't change to reflect that new note. I have to go back and reopen the view to see the change.
Fetch Ticket Details
struct TicketDetails: Codable, Identifiable {
var id: Int
var type: String
var location: Location
var detail: String
var notes: [Notes]
var statustype: StatusType
}
struct Location: Codable {
let locationName: String
}
struct Notes: Codable {
var prettyUpdatedString: String?
var mobileNoteText: String?
}
struct StatusType: Codable {
var id: Int
var statusTypeName: String
}
class FetchTick: ObservableObject {
func getTicket(id: Int, userApi: String, completion: #escaping (TicketDetails) -> ()) {
guard let url = URL(string: "URL FOR TICKET DATA") else { return }
URLSession.shared.dataTask(with: url) {(data, _, _) in
let ticket = try! JSONDecoder().decode(TicketDetails.self, from: data!)
DispatchQueue.main.async {
completion(ticket)
}
}
.resume()
}
}
Create New Note on Ticket
class CreateTicketNote: ObservableObject {
func CreateNoteAction(ticketId: Int, userApi: String, techNote: String) {
let ticketUrl = URL(string:
"URLFORTICKET")!
var request = URLRequest(url: ticketUrl)
request.httpMethod = "POST"
let json: [String: Any] = [
"noteText": techNote,
"jobticket": [
"id": ticketId,
"type": "Ticket"
]
]
let data = try! JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
URLSession.shared.uploadTask(with: request, from: data) { (responseData, response, error) in
if let error = error {
print("Error making POST request: \(error.localizedDescription)")
return
}
if let responseCode = (response as? HTTPURLResponse)?.statusCode, let responseData = responseData {
guard responseCode == 200 else {
print("Invalid response code: \(responseCode)")
return
}
if let responseJSONData = try? JSONSerialization.jsonObject(with: responseData, options: .allowFragments) {
print("Response JSON data = \(responseJSONData)")
}
}
}.resume()
}
}
Detail View
struct DetailsView: View {
#ObservedObject var ticketStatusAction = TicketStatusAction()
#ObservedObject var createTicketNote = CreateTicketNote()
#State var ticket: [TicketDetails] = []
#State private var showingNoteAlert = false
#State private var showingOpenAlert = false
#State private var showingPendingAlert = false
#State private var showingDepotAlert = false
#State private var showingCloseAlert = false
#State private var note: String = ""
var id: Int
var displayClient: String
#Binding var userApi: String
var body: some View {
ScrollView(.vertical){
VStack(alignment: .leading){
if !ticket.isEmpty {
Text(self.ticket.first?.location.locationName ?? "")
.fontWeight(.bold)
.padding()
}
Text("\(displayClient) - \(id)")
.fontWeight(.bold)
.font(.system(size:20))
.padding()
Divider()
Text("Status")
.fontWeight(.bold)
.padding()
if !ticket.isEmpty {
Text(self.ticket.first?.statustype.statusTypeName ?? "")
.padding()
}
Text("Details")
.fontWeight(.bold)
.padding()
if !ticket.isEmpty {
Text(clearMarkdown(on:self.ticket.first?.detail ?? ""))
.padding()
.fixedSize(horizontal: false, vertical: true)
}
Divider()
Text("Most Recent Note")
.fontWeight(.bold)
.padding()
if !ticket.isEmpty {
Text(clearMarkdown(on: self.ticket.first?.notes.first?.prettyUpdatedString ?? ""))
.padding()
Text(clearMarkdown(on: self.ticket.first?.notes.first?.mobileNoteText ?? ""))
.padding()
.fixedSize(horizontal: false, vertical: true)
}
}
.onAppear {
FetchTick().getTicket(id: self.id, userApi: self.userApi) { (ticketDetails) in
self.ticket = [ticketDetails]
}}
Divider()
Section(header: Text("Create New Note")
.fontWeight(.bold)
.padding()
.padding(10)
.frame(maxWidth: .infinity)) {
TextField("Enter your note", text: $note)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 350)
.padding(15)
Button(action: {
self.showingNoteAlert = true
}) {
Text("Submit Note")
.frame(width: 300)
.padding(15)
.foregroundColor(Color.white)
.background(Color.orange)
.cornerRadius(5)
}.buttonStyle(BorderlessButtonStyle()
).actionSheet(isPresented:self.$showingNoteAlert) {
ActionSheet(
title: Text("Are you sure you want to add this note to \(displayClient)'s ticket?"),
message: Text("\(self.note)"),
buttons: [
.default(Text("Submit"))
{
self.createTicketNote.CreateNoteAction(ticketId: self.id, userApi: self.userApi, techNote: self.note);
self.note = ""
},
.cancel(){
self.note = ""
}])
}
}
Divider()
Section(header: Text("Change Ticket Status")
.fontWeight(.bold)
.padding()
.padding(10)
.frame(maxWidth: .infinity)) {
Button(action: {
self.showingOpenAlert = true
}) {
Text("Open")
.frame(width: 300)
.padding(15)
.foregroundColor(Color.white)
.background(Color.green)
.cornerRadius(5)
}.buttonStyle(BorderlessButtonStyle()).alert(isPresented:self.$showingOpenAlert) {
Alert(
title: Text("Are you sure you want change \(displayClient)'s ticket to Open?"),
primaryButton: .default(Text("Open"))
{
self.ticketStatusAction.TicketAction(ticketId: self.id, userApi: self.userApi, desiredStatus: 1)
},
secondaryButton: .cancel())
}
Spacer()
Button(action: {
self.showingPendingAlert = true
}) {
Text("Pending")
.frame(width: 300)
.padding(15)
.foregroundColor(Color.white)
.background(Color.yellow)
.cornerRadius(5)
}.buttonStyle(BorderlessButtonStyle()).alert(isPresented:self.$showingPendingAlert) {
Alert(
title: Text("Are you sure you want to set \(displayClient)'s ticket to Pending?"),
primaryButton: .default(Text("Pending"))
{
self.ticketStatusAction.TicketAction(ticketId: self.id, userApi: self.userApi, desiredStatus: 2)
},
secondaryButton: .cancel())
}
Spacer()
Button(action: {
self.showingDepotAlert = true
}) {
Text("Depot")
.frame(width: 300)
.padding(15)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}.buttonStyle(BorderlessButtonStyle()).alert(isPresented:self.$showingDepotAlert) {
Alert(
title: Text("Are you sure you want to depot \(displayClient)'s ticket?"),
primaryButton: .default(Text("Depot"))
{
self.ticketStatusAction.TicketAction(ticketId: self.id, userApi: self.userApi,
desiredStatus: 6)
},
secondaryButton: .cancel())
}
Spacer()
Button(action: {
self.showingCloseAlert = true
}) {
Text("Close")
.frame(width: 300)
.padding(15)
.foregroundColor(Color.white)
.background(Color.red)
.cornerRadius(5)
}.buttonStyle(BorderlessButtonStyle()).alert(isPresented:self.$showingCloseAlert) {
Alert(
title: Text("Are you sure you want to close \(displayClient)'s ticket?"),
primaryButton: .destructive(Text("Close"))
{
self.ticketStatusAction.TicketAction(ticketId: self.id, userApi: self.userApi, desiredStatus: 3)
},
secondaryButton: .cancel())
}
Spacer()
}
}
}
}
Scenario #2 - I'm viewing a list of tickets and want to make sure no additional tickets have opened. I would like to pull to refresh the list and show any new tickets.
Fet Tickets By Location
struct TicksByLocation: Codable, Identifiable {
public var id: Int
public var type: String
public var displayClient: String
public var shortDetail: String
}
class FetchTicksByLocation: ObservableObject {
func getTicksByLocation(ticketLocation: String, userApi: String, completion: #escaping (([TicksByLocation]) -> ())){
guard let url = URL(string: "URLFORTICKETS") else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let tickData = data {
let decodedData = try JSONDecoder().decode([TicksByLocation].self, from: tickData)
DispatchQueue.main.async {
completion(decodedData)
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
Show Tickets By Location
struct ShowLocationView: View {
#Binding var ticketLocation: String
#Binding var userApi: String
#Binding var tickets: [TicksByLocation]
#State var openTickets: [TicksByStatusAndLocation] = []
#State var green = Color.green
#State var yellow = Color.yellow
#State var blue = Color.blue
#State var purple = Color.purple
var body: some View {
NavigationView{
List(tickets) { tick in
VStack(alignment: .leading, spacing: 10) {
Text("\(tick.id)")
.font(.system(size: 11))
.cornerRadius(5)
Text(tick.displayClient)
Text(tick.shortDetail)
.font(.system(size: 11))
.foregroundColor(Color.gray)
NavigationLink(destination: DetailsView(
id: tick.id,
displayClient: tick.displayClient,
userApi: self.$userApi
)) {
Text("See Details")
.foregroundColor(Color.blue)
}
Divider()
}
}
.navigationBarTitle(
Text("\(ticketLocation) - All (\(tickets.count))"),
displayMode: .inline
)
.navigationBarItems(
trailing:
Text("Filter")
.contextMenu {
NavigationLink(
destination: ShowLocationAndStatusView(
ticketLocation: $ticketLocation,
userApi: $userApi,
status: "Open",
color: green
)
) {
Text("Open")
}
NavigationLink(
destination: ShowLocationAndStatusView(
ticketLocation: $ticketLocation,
userApi: $userApi,
status: "Pending",
color: yellow
)
) {
Text("Pending")
}
NavigationLink(
destination: ShowLocationAndStatusView(
ticketLocation: $ticketLocation,
userApi: $userApi,
status: "Depot",
color: blue
)
) {
Text("Depot")
}
NavigationLink(
destination: ShowLocationAndStatusView(
ticketLocation: $ticketLocation,
userApi: $userApi,
status: "Agi",
color: purple
)
) {
Text("Agi")
}
}
)
}
}
}

Solution #1
Section(header: Text("Create New Note")
.fontWeight(.bold)
.padding()
.padding(10)
.frame(maxWidth: .infinity)) {
TextField("Enter your note", text: $note)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 350)
.padding(15)
Button(action: {
self.showingNoteAlert = true
}) {
Text("Submit Note")
.frame(width: 300)
.padding(15)
.foregroundColor(Color.white)
.background(Color.orange)
.cornerRadius(5)
}.buttonStyle(BorderlessButtonStyle()
).actionSheet(isPresented:self.$showingNoteAlert) {
ActionSheet(
title: Text("Are you sure you want to add this note to \(displayClient)'s ticket?"),
message: Text("\(self.note)"),
buttons: [
.default(Text("Submit"))
{
self.createTicketNote.CreateNoteAction(ticketId: self.id, userApi: self.userApi, techNote: self.note);
self.note = "";
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
FetchTick().getTicket(id: self.id, userApi: self.userApi) { (ticketDetails) in
self.ticket = [ticketDetails]
}
}
},
.cancel(){
self.note = ""
}])
}
}
Solution #2
Using https://github.com/phuhuynh2411/SwiftUI-PullToRefresh
import SwiftUI
import UIKit
struct ShowSelectedStatus: View {
#State private var isShowing = false
#Binding var userApi: String
#Binding var tickets: [TicksByStatus]
var ticketStatus: String
var color: Color
var body: some View {
NavigationView{
List(tickets) { tick in
VStack(alignment: .leading, spacing: 10) {
Text(tick.displayClient)
.padding(10)
.background(self.color)
.cornerRadius(5)
Text(tick.shortDetail)
.font(.system(size: 11))
.foregroundColor(Color.gray)
NavigationLink(destination: DetailsView(
id: tick.id,
displayClient: tick.displayClient,
userApi: self.$userApi
)) {
Text("See Details")
.foregroundColor(Color.blue)
}
Divider()
}
}
.pullToRefresh(isShowing: $isShowing) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isShowing = false
}
FetchTicksByStatus().getTicksByStatus(
ticketStatus: self.ticketStatus,
userApi: self.userApi
) {
(ticks) in self.tickets = ticks
}
}
.navigationBarTitle(
Text("\(ticketStatus) (\(tickets.count))"),
displayMode: .inline
)
}
}
}

Related

Swift: reload view after midnight or when a widget refreshes?

My app uses CoreData to store one quote for each day. There's a widget, the timeline of which is set to update after midnight and fetch the quote for that particular date. This works fine. However, I noticed that when I click on the widget with the new day's quote, the app - which has remained in the background - still shows the previous day's quote. Removing the app from the list of open ones and reopening it shows the correct information.
Is there a way to force a refresh of the DailyQuoteView and make sure that there isn't leftover data?
EDIT: Here's the daily quote struct that I'd like to always show the relevant data when brought from the background.
import SwiftUI
struct DailyQuoteView: View {
#Environment(\.scenePhase) var scenePhase
#State var id: UUID = .init()
#EnvironmentObject var unsplashModel: UnsplashModel
let managedObjectContext = CoreDataStack.shared.managedObjectContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)],
predicate: NSPredicate(
format: "date >= %# AND date <= %#",
argumentArray: [
Calendar.current.startOfDay(for: Date()),
Calendar.current.date(byAdding: .minute, value: 1439, to: Calendar.current.startOfDay(for: Date()))!
]
)
) var quote: FetchedResults<QuoteEntity>
#FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
) var quotes: FetchedResults<QuoteEntity>
#State var shouldPresentSheet = false
#State var animateCard = false
#Binding var cardOffset: Double
#Binding var coverImageSize: Double
#State var showAboutScreen = false
// MARK: - ELEMENTS
var coverImage: some View {
Image(uiImage: UIImage(data: quote.first?.image ?? Data()) ?? UIImage())
.resizable()
.scaledToFill()
}
var aboutButton: some View {
Button {
showAboutScreen.toggle()
} label: {
Image(systemName: "info")
.foregroundColor(.primary)
.padding(10)
.background(Circle().fill(Color("background")))
.font(.title3)
}
.padding()
.shadow(radius: 5)
}
var titleView: some View {
Text(quote.first?.title ?? "")
.font(.largeTitle)
.multilineTextAlignment(.center)
.bold()
.minimumScaleFactor(0.5)
}
var contentsBox: some View {
ScrollView {
VStack (alignment: .leading, spacing: 10) {
Text(quote.first?.summary ?? "")
.font(.body)
.multilineTextAlignment(.leading)
.padding(.bottom, 20)
if let username = quote.first?.imageUserName {
HStack(spacing: 0) {
Text("Photo: ")
Link(
destination: URL(string: "https://unsplash.com/#\(username)?utm_source=quote_of_the_day&utm_medium=referral")!,
label: {
Text((quote.first?.imageUser)!)
.underline()
})
Text(" on ")
Link(
destination: URL(string: "https://unsplash.com/")!,
label: {
Text("Unsplash")
.underline()
})
Spacer()
}
.font(.caption)
.italic()
.foregroundColor(.gray)
.padding(.vertical, 3)
}
}
.font(.subheadline)
}
}
var buttonRow: some View {
HStack {
Menu(
content: {
Button {
shouldPresentSheet.toggle()
} label: {
Text("as image")
Image(systemName: "photo")
}
ShareLink(item: quote.first!.title, message: Text(quote.first!.summary)) {
Text("as text")
Image(systemName: "text.justify.left")
}
},
label: {
HStack {
Image(systemName: "square.and.arrow.up")
Text("Share")
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).fill(Color("whitesand")))
.tint(Color(.secondaryLabel))
}
)
Spacer()
Link(destination: URL(string: quote.first!.link)!) {
Image(systemName: "book")
Text("Read more")
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).fill(Color("vpurple")))
.tint(Color(.white))
}
.frame(height: 60)
.sheet(isPresented: $shouldPresentSheet) {
QuoteShare(quote: quote.first!)
}
}
var cardView: some View {
Rectangle()
.fill((Color("background")))
.cornerRadius(50, corners: [.topLeft, .topRight])
.overlay{
VStack{
titleView
.padding(.top, 30)
contentsBox
Spacer()
buttonRow
}
.padding(.horizontal, 20)
.frame(maxWidth: 600)
}
}
// MARK: - BODY
var body: some View {
ZStack{
GeometryReader{ proxy in
coverImage
.ignoresSafeArea()
.frame(width: UIScreen.main.bounds.width, height: proxy.size.height * (animateCard ? 0.30 : 1.0))
.animation(.easeInOut(duration: 1), value: animateCard)
}
GeometryReader { proxy in
aboutButton
.frame(width: 30, height: 30)
.position(x: proxy.frame(in: .global).maxX - 30, y: proxy.frame(in: .global).minY)
}
GeometryReader{ proxy in
VStack(spacing: 0){
Spacer()
cardView
.frame(height: proxy.size.height * 0.8)
.offset(y: animateCard ? 0 : cardOffset)
.animation(.spring().delay(0.2), value: animateCard)
}
}
}
.id(id)
.blur(radius: shouldPresentSheet ? 5.0 : 0.0)
.brightness(shouldPresentSheet ? -0.2 : 0.0)
.animation(.easeIn, value: shouldPresentSheet)
.onAppear {
if !imagesLoaded {
unsplashModel.loadData(imageCount: quotes.count)
imagesLoaded = true
}
animateCard = true
}
.onChange(of: unsplashModel.dailyImageData) { _ in
if unsplashModel.dailyImageData.count == quotes.count {
updateDailyImages()
}
}
.onChange(of: scenePhase, perform: { newValue in
if newValue == .active {
id = .init()
}
})
.task {
NotificationManager.shared.scheduleNotifications(notificationText: quote.first?.title ?? "")
}
}
}
EDIT 2: Code updated with lorem ipsum's suggestion below (.id and onChange of scenePhase). However, this seems to interfere with the card animation in .onAppear { withAnimation { animateCard = true } }

How can i make an individual button for each user so when its pressed it toggles the isFollowingUser Bool and changes to following?

Currently since its in a ForEach loop and i only have one State var, when i press follow they all change to following. I used a ternary operator to make the button color and text switch if the isFollowing is toggled. How would i go about making it toggle only for a specified user when the button is clicked. Would i just need to make 3 buttons outside the loop? When i used user.isFollowingUser.toggle in the button it tells me that I cant mutate user.
import SwiftUI
struct InstagramModel: Identifiable{
let id: String = UUID().uuidString
let username: String
let userImage: String
let followers: Int
let isVerified: Bool
let isFollowingUser: Bool
}
struct ModelPractice: View {
#State private var users: [InstagramModel] = [
InstagramModel(username: "aleedagenie", userImage: "AliImage", followers: 490, isVerified: true, isFollowingUser: false),
InstagramModel(username: "nicole29", userImage: "volcano2", followers: 1090, isVerified: true, isFollowingUser: false),
InstagramModel(username: "shamat81", userImage: "crashedCar", followers: 290, isVerified: false, isFollowingUser: false)
]
#State private var isFollowing: Bool = false
#State private var isShowDialog: Bool = false
#State private var background: Color = .mint
var body: some View {
NavigationView{
VStack{
List{
Section {
ForEach(users) { user in
VStack(alignment: .leading) {
HStack {
Image(user.userImage)
.resizable()
.frame(width: 35, height: 35)
.clipShape(Circle())
VStack(alignment: .leading) {
Text(user.username)
.font(.headline)
HStack {
Text("Followers:")
.font(.caption)
Text("\(user.followers)")
.font(.caption)
}
}
Spacer()
if user.isVerified{
Image(systemName: "checkmark.seal.fill")
.foregroundColor(.blue)
}
}
Button {
isFollowing.toggle()
} label: {
Text(isFollowing ? "Following" : "Follow")
.foregroundColor(isFollowing ? .black: .white)
.frame(maxWidth: 90)
.background(isFollowing ? .white: .blue)
.cornerRadius(12)
}
.padding(.horizontal, 44)
}
}
} header: {
Text("Instagram Users")
}
.listRowBackground(background)
}
Button {
isShowDialog.toggle()
} label: {
Text("Change Page Style")
.bold()
.frame(maxWidth: 140)
.background(.orange)
.cornerRadius(20)
}
.confirmationDialog("Text", isPresented: $isShowDialog, actions: {
Button {
background = .yellow
} label: {
Text("Option 1")
}
Button {
background = .gray
} label: {
Text("Option 2")
}
Button {
background = .green
} label: {
Text("Option 3")
}
})
.navigationTitle("Instagram")
}
}
}
}
struct ModelPractice_Previews: PreviewProvider {
static var previews: some View {
ModelPractice()
}
}
The problem here is you are trying to mutate the variable of closure called user. user is a temporary variable which is not linked with your users, so you can't mutate it.
Instead you should mutate the users.
Here is my demo, try it out. Code is below the image:
struct UserModel: Identifiable {
var id = UUID()
var name: String = ""
var following = false
}
struct DemoView: View {
#State var listUser = [
UserModel(name: "Lamb Chop", following: false),
UserModel(name: "Steak", following: false)
]
var body: some View {
List {
ForEach(listUser.indices) { index in
HStack {
Text(listUser[index].name)
Button {
listUser[index].following.toggle()
} label: {
Text(listUser[index].following ? "following" : "follow")
}
.padding(5)
.background(.black)
.cornerRadius(15)
}
}
}
}
}

Refreshable not working or showing any errors

I am trying to use refreshable on my List from this tutorial
struct HomeTab: View {
#StateObject var getMsgs = GetMessages()
#State private var receivedMessages = [Timestamp(event: "nil", imgURL: "nil", read: false, readableDate: "nil", temp: "nil", time: "nil")]
var body: some View {
VStack(spacing: 0) {
greeting.edgesIgnoringSafeArea(.top)
messages
Spacer()
}
.onAppear {
getMsgs.fetchMessages()
}
.refreshable {
do {
let BASE_URL = "url.com"
guard let url = URL(string: BASE_URL) else { return }
let (data, _) = try await URLSession.shared.data(from:url)
receivedMessages = try JSONDecoder().decode([String: Timestamp].self, from: data).values.sorted{ $0.readableDate < $1.readableDate }
} catch {
receivedMessages = []
}
}
}
I am trying to implement it for this list:
private var messages: some View {
List(receivedMessages.filter{$0.read == false}) {msg in
NavigationLink(destination: MsgDetailView(message: msg)) {
HStack {
Spacer()
VStack {
Text(msg.event)
.font(.system(size: 17))
.bold()
.frame(maxWidth: .infinity, alignment: .center)
}
}.padding(.leading)
.padding(.top)
.padding(.trailing)
Divider()
.frame(height: 1)
.background(Color.gray)
.opacity(0.3)
.navigationBarHidden(true)
}
}
My data is being downloaded and decoded fine, it's just that the data doesn't refresh when I pull

How to append something to a list?

I'm new to Swift at the moment and I'm having trouble appending to a to-do list. I am not receiving an error, so I don't know what's wrong. I hope this isn't too much code to go through, but I don't even know where the problem is.
There is a sheet modal that opens and closes fine. The only issue is that when I press the save button, the information I typed in doesn't add to the list.
I've add the ViewModel, NewTaskView(sheet), TaskView(customizes list), and a bit of code of the DetailView where the list should be.
import Foundation
import SwiftUI
class TaskViewModel : Identifiable , ObservableObject {
#Published var tasks : [Task] = [
Task(taskName: "Lab", taskDate: Date(), taskCompleted: false),
Task(taskName: "Assignment 4.02", taskDate: Date(), taskCompleted: false)
]
#Published var sortType : SortType = .alphabetical
#Published var isPresented = false
#Published var searched = ""
func addTask(task : Task){
tasks.append(task)
}
func removeTask(indexAt : IndexSet){
tasks.remove(atOffsets: indexAt)
}
func sort(){
switch sortType {
case .alphabetical :
tasks.sort(by: { $0.taskName < $1.taskName })
case .date :
tasks.sort(by: { $0.taskDate > $1.taskDate })
}
}
}
struct Task : Identifiable , Equatable {
var id : String = UUID().uuidString
let taskName : String
let taskDate : Date
var taskCompleted: Bool
}
enum SortType : String , Identifiable , CaseIterable {
var id : String { rawValue }
case alphabetical
case date
}
struct NewTaskView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var taskVM : TaskViewModel
#State var taskName = ""
#State var taskCompleted = Bool()
#State var taskDate = Date()
var body: some View {
NavigationView {
VStack(spacing: 14) {
Spacer()
TextField("Assignment Name",text: $taskName)
.padding()
.background(Color("tan"))
.cornerRadius(5)
.font(Font.custom(FontNameManager.Montserrat.semibold, size: 15))
.padding(.bottom, 20)
HStack{
Text("Due Date")
.font(Font.custom(FontNameManager.Montserrat.semibold, size: 15))
Image(systemName: "bell.badge")
.foregroundColor(.gray)
.frame(width: 30, height: 30, alignment: .leading)
VStack{
DatePicker("Select Date", selection: $taskDate, displayedComponents: .date)
.labelsHidden()
}
}
HStack{
Text("Mark as Completed")
.font(Font.custom(FontNameManager.Montserrat.semibold, size: 15))
Button(action : {
taskCompleted.toggle()
}){
if taskCompleted == true {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color("orange"))
.font(.title2)
}
else {
Image(systemName: "circle")
.foregroundColor(Color("lightorange"))
.font(.title2)
}
}
}
Spacer()
Button(action:{taskVM.addTask(task: .init(taskName: taskName, taskDate: taskDate, taskCompleted: taskCompleted))
presentationMode.wrappedValue.dismiss()},
label:{
Text("Save")
})
}.padding()
.navigationBarItems(trailing: Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack{
Image(systemName: "xmark")
.foregroundColor(Color("orange"))
}
})
}
}
}
struct NewTaskView_Previews: PreviewProvider {
static var previews: some View {
NewTaskView(taskVM: TaskViewModel())
}
}
struct TaskView: View {
var task : Task
#Environment(\.managedObjectContext) var moc
#State var currentDate: Date = Date()
#State var taskCompleted = false
func getDateFormatString(date:Date) -> String
{
var dateString = ""
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM. dd"
dateString = dateFormatter.string(from: date)
return dateString
}
var body: some View {
HStack {
Button(action : {
self.taskCompleted.toggle()
try? self.moc.save()
}){
if self.taskCompleted {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color("orange"))
.font(.title2)
}
else {
Image(systemName: "circle")
.foregroundColor(Color("lightorange"))
.font(.title2)
}
}.padding()
VStack (alignment: .leading, spacing: 2){
Text("\(task.taskName)")
.font(Font.custom(FontNameManager.Montserrat.bold, size: 18))
.foregroundColor(Color("dark"))
.padding([.leading, .trailing], 1)
if self.taskCompleted {
Text("Completed")
.font(Font.custom(FontNameManager.Montserrat.bold, size: 12))
.foregroundColor(Color("burntorange"))
.frame(width: 95, height: 20)
.background(Color("lightorange"))
.cornerRadius(4)
}
else if currentDate > task.taskDate {
Text("Late")
.font(Font.custom(FontNameManager.Montserrat.semibold, size: 12))
.foregroundColor(.white)
.frame(width: 95, height: 18)
.background(Color("lightbrown"))
.cornerRadius(4)
}
else {
let diffs = Calendar.current.dateComponents([.day], from: currentDate, to: task.taskDate )
Text("\(diffs.day!) days left")
.font(Font.custom(FontNameManager.Montserrat.semibold, size: 12))
.foregroundColor(Color("burntorange"))
.frame(width: 95, height: 20)
.background(Color("lightorange"))
.cornerRadius(4)
}
}
Spacer()
Text(getDateFormatString(date: task.taskDate ))
.font(Font.custom(FontNameManager.Montserrat.medium, size: 12))
.padding()
}
.padding(10)
.cornerRadius(10)
.background(
RoundedRectangle(cornerRadius: 10 , style: .continuous)
.foregroundColor(.white))
}
}
struct TaskView_Previews: PreviewProvider {
static var previews: some View {
TaskView(task: Task(id: "", taskName: "Task Name", taskDate: Date(), taskCompleted: false))
}
}
struct DetailsView: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var developers : Developer
#ObservedObject var taskVM : TaskViewModel
#Environment(\.presentationMode) var presen
#Environment(\.editMode) var editButton
#State var taskCompleted = false
#State var show = false
#State var showSearch = false
#State var txt = ""
#State var currentDate: Date = Date()
#State var image : Data = .init(count: 0)
#State var indices : [Int] = []
func getDateFormatString(date:Date) -> String
{
var dateString = ""
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM. dd"
dateString = dateFormatter.string(from: date)
return dateString
}
var body: some View {
NavigationView {
ZStack {
ZStack{
VStack{
Color("lightorange")
.clipShape(CustomCorners(corners: [.bottomLeft, .bottomRight], size: 70))
.ignoresSafeArea(.all, edges: .top)
.frame(width: 420, height: 175)
Spacer()
}
VStack{
HStack {
if !self.showSearch{
Button(action: {
withAnimation(.spring()){
self.presen.wrappedValue.dismiss()
}
}) {
Image(systemName: "chevron.left")
.font(.title2)
.foregroundColor(Color("dark"))
.padding(10)
}
}
Spacer(minLength: 0)
HStack {
if self.showSearch{
Image(systemName: "magnifyingglass")
.font(.title2)
.foregroundColor(Color("dark"))
.padding(.horizontal, 8)
TextField("Search", text: $taskVM.searched , onEditingChanged: { (isBegin) in
if isBegin {
showSearch = true
} else {
showSearch = false
}
})
Button(action: {
withAnimation{
self.showSearch.toggle()
}
}) {
Image(systemName: "xmark").foregroundColor(.black).padding(.horizontal, 8)
}
}
else {
Button(action: {
withAnimation {
self.showSearch.toggle()
}
}) {
Image(systemName: "magnifyingglass")
.font(.title2)
.foregroundColor(Color("dark"))
.padding(10)
}
}
}.padding(self.showSearch ? 10 : 0)
.background(Color("lightorange"))
.cornerRadius(20)
}
ZStack{
RoundedRectangle(cornerRadius: 25)
.foregroundColor(Color("tan"))
.frame(width: 335, height: 130)
HStack {
VStack (alignment: .leading){
// Image(uiImage: UIImage(data: developers.imageD ?? self.image)!)
// .resizable()
// .frame(width: 70, height: 70)
// .clipShape(RoundedRectangle(cornerRadius: 20))
Text("\(developers.username ?? "")")
.font(Font.custom(FontNameManager.Montserrat.bold, size: 24))
.foregroundColor(Color("dark"))
Text("\(developers.descriptions ?? "")")
.font(Font.custom(FontNameManager.Montserrat.semibold, size: 18))
.foregroundColor(.gray)
}
.padding([.leading, .trailing], 70)
.padding(.bottom, 80)
Spacer()
}
}
HStack{
Text("Assignments")
.font(Font.custom(FontNameManager.Montserrat.bold, size: 20))
Spacer(minLength: 0)
EditButton()
}
.padding([.leading, .trailing], 20)
Spacer()
List {
ForEach (taskVM.tasks.filter {
self.taskVM.searched.isEmpty ? true : $0.taskName.localizedCapitalized.contains(self.taskVM.searched)} ){ task in
TaskView(task: task)
.listRowSeparator(.hidden)
}
.onDelete(perform: {
taskVM.removeTask(indexAt: $0)
})
}
.listStyle(InsetListStyle())
}
}
VStack {
Spacer()
HStack {
Spacer()
ExpandableFAB(taskVM: TaskViewModel(), show: $show)
}
}
}.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
}
struct DetailsView_Previews: PreviewProvider {
static let persistenceController = PersistenceController.shared
static var developers: Developer = {
let context = persistenceController.container.viewContext
let developers = Developer(context: context)
developers.username = "Math"
developers.descriptions = "Calculus"
return developers
}()
static var previews: some View {
DetailsView(developers: developers, taskVM: TaskViewModel()).environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
struct ExpandableFAB: View {
#Environment(\.managedObjectContext) var moc
#ObservedObject var taskVM : TaskViewModel
#Binding var show: Bool
var body: some View{
VStack(spacing: 20){
if self.show{
Button(action: {
taskVM.isPresented.toggle()
}) {
Image(systemName: "list.bullet.rectangle.portrait").resizable().frame(width: 25, height: 25).padding()
}
.foregroundColor(.white)
.background(Color("orange"))
.clipShape(Circle())
Button(action: {
self.show.toggle()
}) {
Image(systemName: "doc.badge.plus").resizable().frame(width: 25, height: 25).padding()
}.foregroundColor(.white)
.background(Color("orange"))
.clipShape(Circle())
}
Button(action: {
self.show.toggle()
}) {
Image(systemName: "plus").resizable().frame(width: 25, height: 25).padding()
}.foregroundColor(.white)
.background(Color("orange"))
.clipShape(RoundedRectangle(cornerRadius: 20))
.padding()
.rotationEffect(.init(degrees: self.show ? 180 : 0))
}.fullScreenCover(isPresented: $taskVM.isPresented, content: {
NewTaskView(taskVM: taskVM)
})
}
}
#burnsi I don't think I have that anywhere. Where should there be one?
Of course you have one and you need one:
ExpandableFAB(taskVM: TaskViewModel(), show: $show)
This line is causing the issue.
You are somehow injecting a TaskViewModel in your DetailsView (You are not showing where), but not using it. Try:
ExpandableFAB(taskVM: taskVM, show: $show)
And the place you create your ViewModel ('TaskViewModel()') should have a #StateObject wrapper.
Explanation:
As you add something to your ViewModel the views depending on the ViewModel get rebuild. So your TaskViewModel ends up beeing recreated and you still have your 2 initial values in your array.

#Environment(\.presentationMode) var mode: Binding<PresentationMode> Messing other views

I have a MailView()
import Foundation
import SwiftUI
import UIKit
import MessageUI
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
let newSubject : String
let newMsgBody : String
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
vc.setToRecipients(["hello#email.co.uk"])
vc.setSubject(newSubject)
vc.setMessageBody(newMsgBody, isHTML: false)
return vc
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
And In my SettingViews its called like so:
import SwiftUI
import URLImage
import UIKit
import MessageUI
struct SettingsView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#State private var showMailSheet = false
#State var result: Result<MFMailComposeResult, Error>? = nil
#State private var subject: String = ""
#State private var emailBody: String = ""
#EnvironmentObject var session: SessionStore
var body: some View {
NavigationView {
VStack(alignment: .leading) {
List {
Section(header: Text("Account")) {
NavigationLink(destination: ProfileView()) {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Edit Profile").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
NavigationLink(destination: AccountView()) {
HStack {
Image(systemName: "doc")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("View Account").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
NavigationLink(destination: PreferencesView()) {
HStack {
Image(systemName: "slider.horizontal.3")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Preferences").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
}
Section(header: Text("Support")) {
HStack {
Image(systemName: "bubble.right")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Contact Us").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
self.subject = "Hello"
self.sendEmail()
}) {
Text("Send").font(.system(size:12))
}
}
HStack {
Image(systemName: "ant")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Report An Issue").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
self.sendEmail()
self.subject = "Report Issue"
self.emailBody = "Im having the following issues:"
}) {
Text("Report").font(.system(size:12))
}
}
}
Section (header: Text("Legal")) {
HStack {
Image(systemName: "hand.raised")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Privacy Policy").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
if let url = URL(string: "http://www.mysite.co.uk/privacy.html") {
UIApplication.shared.open(url)
}
}) {
Text("View").font(.system(size:12))
}
}
HStack {
Image(systemName: "folder")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Terms and Conditions (EULA)").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
if let url = URL(string: "http://www.mysite.co.uk/eula.html") {
UIApplication.shared.open(url)
}
}) {
Text("View").font(.system(size:12))
}
}
}
}.listStyle(GroupedListStyle())
}.navigationBarTitle("Settings", displayMode: .inline)
.background(NavigationBarConfigurator())
}.sheet(isPresented: $showMailSheet) {
MailView(result: self.$result, newSubject: self.subject, newMsgBody: self.emailBody)
}
}
func sendEmail() {
if MFMailComposeViewController.canSendMail() {
self.showMailSheet = true
} else {
print("Error sending mail")
}
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
}
}
My Sheet is appearing nicely and once the email is sent, the sheet is dismissed as expected, but the following is causing an issue:
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
When I click:
NavigationLink(destination: ProfileView()) {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Edit Profile").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
There is a action sheet:
.actionSheet(isPresented: self.$profileViewModel.showActionSheet){
ActionSheet(title: Text("Add a profile image"), message: nil, buttons: [
.default(Text("Camera"), action: {
self.profileViewModel.showImagePicker = true
self.sourceType = .camera
}),
.default(Text("Photo Library"), action: {
self.profileViewModel.showImagePicker = true
self.sourceType = .photoLibrary
}),
.cancel()
])
}.sheet(isPresented: self.$profileViewModel.showImagePicker){
imagePicker(image: self.$profileViewModel.upload_image, showImagePicker: self.$profileViewModel.showImagePicker, sourceType: self.sourceType)
}
When i click this button it keeps dismissing the button and I can't click on the options presented.
Any idea how I can have the #Environment(\.presentationMode) var mode: Binding<PresentationMode> only effecting the dismissing of the email? and not interfering with anything else?
#Environment(\.presentationMode) should be used for the last child view that you want to have this custom behaviour.
Any child view from where you declared the #Environment(\.presentationMode), will also inherit the same behaviour.
If you declare it only in MailView, it should fix it.