I need to pass some bindings to a sheet that can be written to. What I've come up with works but seems a really inefficient way of doing it.
I have recreated a very simplified version of my code to use as an example.
I have a custom LocationStruct...
struct LocationStruct {
var id = UUID()
var name: String
var location: CLLocationCoordinate2D?
var haveVisited = false
}
I then have the parent view that displays a number of the LocationStruct's - the origin, an array of waypoints and the destination...
struct ContentView: View {
#State var origin = LocationStruct(name: "Paris")
#State var waypoints = [LocationStruct(name: "Berlin"), LocationStruct(name: "Milan")]
#State var destination = LocationStruct(name: "Venice")
#State var selectedLocation: Int?
#State var showSheet = false
var body: some View {
VStack{
HStack{
Text("Origin:")
Spacer()
Text(origin.name)
}
.onTapGesture{
selectedLocation = 1000
showSheet = true
}
ForEach(waypoints.indices, id:\.self){ i in
HStack{
Text("Waypoint \(i + 1):")
Spacer()
Text(waypoints[i].name)
}
.onTapGesture{
selectedLocation = i
showSheet = true
}
}
HStack{
Text("Destination:")
Spacer()
Text(destination.name)
}
.onTapGesture{
selectedLocation = 2000
showSheet = true
}
}
.padding()
.sheet(isPresented: $showSheet){
LocationSheet(origin: $origin, waypoints: $waypoints, destination: $destination, selectedLocation: $selectedLocation)
}
}
}
I then need to read and write to the location object that was tapped on the ContentView. I'm setting selectedLocation value to 1000 or 2000 for origin and destination otherwise its set to the waypoint array index (waypoints are limited in number so will not reach 1000).
I'm having to repeating "if let selectedLocation = ..." in quite a few places. Is there a better way of doing this, maybe some sort of computed binding or something?
struct LocationSheet: View {
#Binding var origin: LocationStruct
#Binding var waypoints: [LocationStruct]
#Binding var destination: LocationStruct
#Binding var selectedLocation: Int?
var body: some View {
VStack{
if let selectedLocation = selectedLocation {
switch selectedLocation {
case 1000:
TextField("", text: $origin.name).textFieldStyle(.roundedBorder)
case 2000:
TextField("", text: $destination.name).textFieldStyle(.roundedBorder)
default:
TextField("", text: $waypoints[selectedLocation].name).textFieldStyle(.roundedBorder)
}
}
Button(action: { markAsVisted() }){
Text("Visited")
}
}
.padding()
}
func markAsVisted(){
if let selectedLocation = selectedLocation {
switch selectedLocation {
case 1000:
origin.haveVisited = true
case 2000:
destination.haveVisited = true
default:
waypoints[selectedLocation].haveVisited = true
}
}
}
}
Thanks in advance
The trick is to use a non-optional custom state struct that holds both isPresented and the data the sheet needs, which is only valid when the sheet is showing.
Apple provide an example of this in Data Essentials in SwiftUI WWDC 2020 at 4:18.
EditorConfig can maintain invariants on its properties and be tested
independently. And because EditorConfig is a value type, any change to
a property of EditorConfig, like its progress, is visible as a change
to EditorConfig itself.
In your case it would be done as follows:
struct LocationSheetConfig {
var selectedLocation: Int = 0 // this is usually a struct, e.g. Location, or an id, e.g. UUID.
var showSheet = false
mutating func selectLocation(id: Int){
selectedLocation = id
showSheet = true
}
// usually there is another mutating func for closing the sheet.
}
#State var config = LocationSheetConfig()
.onTapGesture{
config.selectLocation(id: 1000)
}
.sheet(isPresented: $config.showSheet){
LocationSheet(origin: $origin, waypoints: $waypoints, destination: $destination, config: $config)
}
Also I noticed your Location struct is missing Identifiable protocol conformance. And you must not use give indices to the ForEach View, it has to be an array of Identifiable structs, otherwise it'll crash when there is a change (because it cannot track indices, only IDs).
For your question, instead of binding a lots variables like that you can create an EnvironmentObject to store and just pass screen with only that EnvironmentObject will make your view be simpler.
Making an enum to store location type where you tap instead of making a number const like 1000, 2000 like that - this will make you code not clean and not easily extendable.
Code of enum and ObservableObject will be like this
enum LocationType {
case origin
case waypoint
case destination
}
struct LocationStruct {
var id = UUID()
var name: String
var location: CLLocationCoordinate2D?
var haveVisited = false
}
class Location : ObservableObject {
#Published var origin = LocationStruct(name: "Paris")
#Published var waypoints = [LocationStruct(name: "Berlin"), LocationStruct(name: "Milan")]
#Published var destination = LocationStruct(name: "Venice")
#Published var tapLocationType : LocationType = .origin
#Published var tapIndex : Int?
func didTapLocaiton(type: LocationType, tapIndex: Int? = nil) {
self.tapLocationType = type
self.tapIndex = tapIndex
}
func didVisit() {
switch tapLocationType {
case .origin:
origin.haveVisited = true
case .waypoint:
waypoints[tapIndex ?? 0].haveVisited = true
case .destination:
destination.haveVisited = true
}
}
func getName() -> String {
switch tapLocationType {
case .origin:
return origin.name
case .waypoint:
return waypoints[tapIndex ?? 0].name
case .destination:
return destination.name
}
}
}
And then, come to your view just take the variable from Location and appear and when you need to change any variables you should call from Location object. Will make your code easier to handle
struct LocationSheet: View {
#ObservedObject var location : Location
var body: some View {
VStack{
Text(location.getName()).textFieldStyle(.roundedBorder)
Button(action: { markAsVisted() }){
Text("Visited")
}
}
.padding()
}
func markAsVisted(){
location.didVisit()
}
}
struct ContentView: View {
// make location state for storing
#StateObject var location = Location()
#State var showSheet = false
var body: some View {
VStack{
HStack{
Text("Origin:")
Spacer()
Text(location.origin.name)
Spacer()
Text(verbatim: "Visit: \(location.origin.haveVisited)")
}
.onTapGesture{
location.didTapLocaiton(type: .origin)
showSheet = true
}
ForEach(location.waypoints.indices, id:\.self){ i in
HStack{
Text("Waypoint \(i + 1):")
Spacer()
Text(location.waypoints[i].name)
Spacer()
Text(verbatim: "Visit: \(location.waypoints[i].haveVisited)")
}
.onTapGesture{
location.didTapLocaiton(type: .waypoint, tapIndex: i)
showSheet = true
}
}
HStack{
Text("Destination:")
Spacer()
Text(location.destination.name)
Spacer()
Text(verbatim: "Visit: \(location.destination.haveVisited)")
}
.onTapGesture{
location.didTapLocaiton(type: .destination)
showSheet = true
}
}
.padding()
.sheet(isPresented: $showSheet){
LocationSheet(location: location)
}
}
}
The result
Related
I have a picker that updates an variable bmr.
the picker in not updating the value. I put a test Text on the second screen to see if I can call the new value, its always showing the default.
import SwiftUI
struct ProfileView: View {
#AppStorage("ProfileView") var profileView:Bool = false
#AppStorage("ContentView") var contentView:Bool = false
#AppStorage("TimerView") var timerView:Bool = false
#AppStorage("Name") var userName: String?
#AppStorage("BMR") var bmr: Int?
var body: some View {
VStack {
Picker(selection: $bmr, label: Text("Enter your BMR:")) {
Text("50").tag(50)
Text("60").tag(60)
Text("70").tag(70)
Text("80").tag(80)
}
NavigationLink(destination: ContentView()) {
Text("Save")
}
}
}
}
struct ContentView: View {
#AppStorage("ProfileView") var profileView:Bool = false
#AppStorage("ContentView") var contentView:Bool = false
#AppStorage("TimerView") var timerView:Bool = false
#AppStorage("Name") var userName: String?
#AppStorage("BMR") var bmr: Int?
var body: some View {
VStack {
Text("Your BMR: \(bmr ?? 50)")
}
}
}
I am learning, so I don't know what to really try. I tried binding and unbinding, its not really working.
I tried to store variables and pass through views previously to test if it would update, and it worked fine. But passing info through views doesn't work with my app as I need this data to to be stored even if I exit.
The issue is bmr is an Int? while your tags are Int. Since they are not the same thing, the selection won't update it. The trick is to cast your tag as an Int? like this:
struct ProfileView: View {
#AppStorage("ProfileView") var profileView:Bool = false
#AppStorage("ContentView") var contentView:Bool = false
#AppStorage("TimerView") var timerView:Bool = false
#AppStorage("Name") var userName: String?
#AppStorage("BMR") var bmr: Int?
var body: some View {
VStack {
Picker(selection: $bmr, label: Text("Enter your BMR:")) {
Text("50").tag(50 as Int?)
Text("60").tag(60 as Int?)
Text("70").tag(70 as Int?)
Text("80").tag(80 as Int?)
}
NavigationLink(destination: ContentView()) {
Text("Save")
}
}
}
}
The tag and the selection types must EXACTLY match.
I am trying to get data from one view to another.
I can not figure out how to get values from the fourth view array into the Third view.
I am not using storyboards. I tried using #EnvironmentObject but can not make it work. New to coding. In xcode I am using watchos without app.
I tried to strip out most of the code and leave just the important stuff that can be tested. I used NavigationLink(destination: )to transfer between views.
enter code here
class viewFromEveryWhere: ObservableObject {
#Published var testname2: String = "testTTname"
}
struct secondView: View {
var body: some View {
Text("second view")
List(1..<7) {
Text("\($0)")
}
}
}
struct thirdView: View {
#EnvironmentObject var testname2: viewFromEveryWhere
#EnvironmentObject var testSixTestArray: viewFromEveryWhere
#State var sixTestArray:[String] = ["ww","GS","DW","CD","TS","JW",]
var body: some View {
List(sixTestArray, id:\.self) {
Text($0)
}
}
}
struct fourthView: View {
#StateObject var testname2 = viewFromEveryWhere()
#State private var name: String = ""
#State var testSixTestArray:[String] = []
func collectName () {
print("collectName triggered")
if testSixTestArray.count < 5 {
// testSixTestArray.append(name)
print(name)
print(testSixTestArray)
}
// .enviromentObject(testSixTestArray)
}
var body: some View {
VStack(alignment: . leading) {
Text("Type a name")
TextField("Enter your name", text: $name)
Text("Click to add, \(name)!")
// Button("click this if \(name) is correct") {}
Button(action:{
print("Button Tapped")
collectName()
print(testSixTestArray.count)
name = ""
}) {
Text("Add \(name) to list")
}
// .buttonStyle(GrowingButton1())
}
Text("forth view")
// testSixTestArray.append(name)
.environmentObject(testname2)
}
}
/*func presentTextInputControllerWithSuggestions(forLanguage suggestionsHandler:
((String)-> [Any]?)?,
allowedInputMode inputMode:
WKTextInputMode,
completion: #escaping ([Any]?) -> Void) {}
*/
struct ContentView: View {
#State var sixNameArray:[String] = ["","","","","","",]
#State var messageTextBox: String = "Start"
#State var button1: String = "Button 1"
#State var button2: String = "Button 2"
#State var button3: String = "Button 3"
var body: some View {
NavigationView {
VStack{
Text(messageTextBox)
.frame(width: 120, height: 15, alignment: .center)
.truncationMode(.tail)
.padding()
NavigationLink(destination: secondView(),
label:{
Text(button1)
})
.navigationBarTitle("Main Page")
NavigationLink(destination: thirdView(),
label:{
Text(button2)
})
NavigationLink(destination: fourthView(),
label:{
Text(button3)
})
}
}
}
}
enter code here
When I select one of the items inside my list from my lists it only deletes the selected item.
But when I list all the lists and their reminders inside the AllView it deletes all of the reminders inside the list.
How can I overcome that problem?
To tell my problem clearly I have two videos that show both cases.
First Case
Second case
it is my delete button inside ReminderCell view
struct ReminderCell: View {
#Environment(\.managedObjectContext) private var viewContext
var reminder: CDReminder
#State var isSelected: Bool
Button(action: {
self.isSelected = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1){
deleteReminder(at: Int(reminder.index))
}
and again inside the ReminderCell I have deleteReminder func
func deleteReminder(at offsets: Int) {
viewContext.delete(reminder)
PersistenceController.shared.saveContext()
}
Inside the AllView I am calling listDetailCell as
struct AllView: View {
#State var title = ""
#State var note = ""
#State var releaseDate = Date()
#ObservedObject var list : CDListModel
#State var selectedList = CDListModel()
#Environment(\.presentationMode) var mode
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest var lists: FetchedResults<CDListModel>
init(){
list = CDListModel()
let request: NSFetchRequest<CDListModel> = CDListModel.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \CDListModel.name, ascending: true)]
request.entity = CDListModel.entity()
_lists = FetchRequest(fetchRequest: request)
}
var body: some View {
List{
ForEach(lists, id: \.self) { list in
ListDetailCell(list: list)
}
}
}
My ListDetailCell
struct ListDetailCell: View {
#State var title = ""
#ObservedObject var list : CDListModel
#State var selectedList: CDListModel!
#State var isAddReminderTapped = false
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
VStack(alignment: .leading){
Text(list.text ?? "")
ForEach((list.reminders?.allObjects as! [CDReminder]).indices , id: \.self) { reminderIndex in
ReminderCell(reminder: (list.reminders?.allObjects[reminderIndex]) as! CDReminder, isSelected: false, selectedList: $selectedList, onComplete: {})
}
}
}
}
Your delete function is wrong.
Here you are passing an offsets: Int. But you are never using that offsets inside the function. You are just deleting the whole reminder.
func deleteReminder(at offsets: Int) {
viewContext.delete(reminder)
PersistenceController.shared.saveContext()
}
Somehow using ForEach inside the List was causing this problem in AllView.
When I change the body of the AllView like below my problem disappeared.
NavigationView {
ScrollView {
VStack{
HStack{
Text("Tumu")
.font(.system(size: 40, weight: .bold, design: .rounded))
.foregroundColor(.gray)
Spacer()
}
.padding(.leading)
ForEach(lists, id: \.self) { list in
ListDetailCell(list: list)
}
I created a custom alert. I want the product to be added to the basket when the Ok button on Alert is clicked on the first screen. When the Ok button is pressed on the second screen, the purchase of the product is requested. I called the same alert on 2 pages and I want it to take different actions. I couldn't do that with #Escaping.
AlertView
struct AlertView: View {
#Binding var openShowAlert: Bool
#State var closeShowAlert: Bool = false
#State var openState: CGFloat = -UIScreen.main.bounds.height
#State var closeState: CGFloat = UIScreen.main.bounds.height
var title: String = ""
var message: String = ""
var okButtonText: String = ""
var cancelButtonText: String = ""
var body: some View {
VStack {
Text(title)
.michromaFont(size: 20)
.padding(.top)
Spacer()
Text(message)
.michromaFont(size: 18)
Spacer()
HStack {
Button(action: {
self.openShowAlert = false
openState = -UIScreen.main.bounds.height
closeState = UIScreen.main.bounds.height
}) {
Text(cancelButtonText)
.foregroundColor(.red)
}
Spacer()
Button(action: {}) {
Text(okButtonText)
}
}
.michromaFont(size: 18)
.padding([.horizontal, .bottom])
}
.neumorphisimBackground(width: 300, height: 200)
.offset(y: self.openShowAlert ? self.openState : self.closeState)
.animation(.easeInOut)
.onChange(of: self.openShowAlert, perform: { value in
if value {
self.openState = .zero
}
})
}
}
DetailView
On this screen, click Alert presentation to add the product to the cart.
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode
var device = UIDevice.current.userInterfaceIdiom
#State var width: CGFloat = 300
#State var height: CGFloat = 450
#Binding var text: String
#State var showAlert: Bool = false
var body: some View {
ZStack() {
......
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
CartView Click I am providing an alert on this screen to purchase the product.
struct CartView: View {
#State var cartList = [1,2,3,4,5,6,7,8,9]
#Environment(\.presentationMode) var presentationMode
#State var showAlert: Bool = false
var body: some View {
ZStack(alignment: .top) {
.....
AlertView(openShowAlert: self.$showAlert)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
How can I send two different actions in the same alert.
Hmm, I don't see why it shouldn't work with a closure. Have you tried passing over a closure like so?
struct AlertView: View {
...
var okButtonAction: () -> ()
var body: some View {
...
Button(action: okButtonAction) {
Text(okButtonText)
}
}
}
Usage
AlertView(openShowAlert: self.$showAlert) {
// Your custom code
}
Alternative Idea
You could work with Combine and create a publisher with a specific key to identify the sender screen. Then you can put your custom code inside .onReceive().
I'm having trouble with usage of a count of the number of entries in a view. I especially need to know when there are no entries in the view. I have placed debug code in the view below and the view count currants.curItem.countis updating as expected. The count status in checkForUpdates() doesn't follow the view above.
If I recall correctly I should be using #EnvironmentObject or #ObservedObject only in a view. I really need some kind of global variable that I can pass to the method checkForUpdates. It is crashing when count in checkForUpdates() is nonzero when in the view it is actually zero. It also crashes in checkForUpdates() with the error Fatal error: No ObservableObject of type Currencies found. A View.environmentObject(_:) for Currencies may be missing as an ancestor of this view.
struct manCurView: View {
#EnvironmentObject var currants: Currants
var body: some View {
List {
ForEach(currants.curItem, id: \.id) { item in
HStack {
Text(item.curCode)
.frame(width: 100, alignment: .center)
Text(item.cunName)
}
.font(.subheadline)
}
.onDelete(perform: removeItems)
}
.navigationBarTitle(Text("Manage Working Blocks"), displayMode: .inline)
HStack {
NavigationLink(destination: addCurView()) {Text("Add Working Blocks").fontWeight(.bold)}
.font(.title2)
.disabled(currants.curItem.count > 7)
Here is how the data is stored for the view above
struct CurItem: Codable, Identifiable {
var id = UUID()
var cunName: String
var curName: String
var curCode: String
var curSymbol: String
var curRate: Double
}
class Currants: ObservableObject {
#Published var curItem: [CurItem]
}
And here is the class and method where I would like to use count from the view manCurView
class BlockStatus: ObservableObject {
#EnvironmentObject var globalCur : Currants
#ObservedObject var netStatus : TestNetStatus = TestNetStatus()
func checkForUpdates() -> (Bool) {
if netStatus.connected == true {
if globalCur.curItem.count > 0 {
Without a minimal reproducible example it is very difficult to give you exact code but you can try something like the code below in your manCurView
#StateObject var blockStatus: BlockStatus = BlockStatus()
.onChange(of: currants.curItem.count, perform: { value in
print("send value from here")
blockStatus.arrayCount = value
})
And adding the code below to BlockStatus
#Published var arrayCount: Int = 0{
didSet{
//Call your method here
}
}
Look at the code below.
import SwiftUI
import Combine
struct CurItem: Codable, Identifiable {
var id = UUID()
}
class Currants: ObservableObject {
#Published var curItem: [CurItem] = [CurItem(), CurItem(), CurItem(), CurItem()]
}
class TestNetStatus: ObservableObject {
static let sharedInstance = TestNetStatus()
#Published var connected: Bool = false
init() {
//Simulate changes in connection
Timer.scheduledTimer(withTimeInterval: 10, repeats: true){ timer in
self.connected.toggle()
}
}
}
class BlockStatus: ObservableObject {
#Published var arrayCount: Int = 0{
didSet{
checkForUpdates()
}
}
#Published var checkedForUpdates: Bool = false
var netStatus : TestNetStatus = TestNetStatus.sharedInstance
//private var cancellable: AnyCancellable?
init() {
//Maybe? if you want to check upon init.
//checkForUpdates()
//Something like the code below is also possible but with 2 observed objects the other variable could be outdated
// cancellable = netStatus.objectWillChange.sink { [weak self] in
// self?.checkForUpdates()
// }
}
func checkForUpdates() {
if netStatus.connected == true {
if arrayCount > 0 {
checkedForUpdates = true
}else{
checkedForUpdates = false
}
}else{
checkedForUpdates = false
}
}
}
struct ManCurView: View {
#StateObject var currants: Currants = Currants()
#StateObject var blockStatus: BlockStatus = BlockStatus()
#StateObject var testNetStatus: TestNetStatus = TestNetStatus.sharedInstance
var body: some View {
List {
Text("checkedForUpdates = " + blockStatus.checkedForUpdates.description).foregroundColor(blockStatus.checkedForUpdates ? Color.green : Color.red)
Text("connected = " + blockStatus.netStatus.connected.description).foregroundColor(blockStatus.netStatus.connected ? Color.green : Color.red)
ForEach(currants.curItem, id: \.id) { item in
HStack {
Text(item.id.uuidString)
.frame(width: 100, alignment: .center)
Text(item.id.uuidString)
}
.font(.subheadline)
}
//Replaced with toolbar button for sample
//.onDelete(perform: removeItems)
//When the array count changes
.onChange(of: currants.curItem.count, perform: { value in
blockStatus.arrayCount = value
})
//Check when the networkStatus changes
.onChange(of: testNetStatus.connected, perform: { value in
//Check arrayCount
if blockStatus.arrayCount != currants.curItem.count{
blockStatus.arrayCount = currants.curItem.count
}else{
blockStatus.checkForUpdates()
}
})
}
.navigationBarTitle(Text("Manage Working Blocks"), displayMode: .inline)
//Replaced addCurView call with toolbar button for sample
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing, content: {
Button("add-currant", action: {
currants.curItem.append(CurItem())
})
})
ToolbarItem(placement: .navigationBarLeading, content: {
Button("delete-currant", action: {
if currants.curItem.count > 0{
currants.curItem.removeFirst()
}
})
})
})
}
}
Here is ContentView: Notice in the menu that because this is a view I can use count directly to disable entry input. Down in getData() notice that I'm calling blockStatus.checkForUpdates() to determine if is OK to call the API. A fault will occur if currants.curItem.count = 0
I just realized that technically getData() is part of the ContentView so I can change the call below to if blockStatus.checkForUpdates() == true && currants.curItem.count != 0 {
I'm going to spend some time studying your suggestions above to see if I could use this in the future.
So thanks for all the help by looking into this. I wasn't aware of the suggestions on code displayed on Stackoverflow. I'll be sure to follow those guidelines in the future. Galen
import SwiftUI
import CoreData
import Combine
struct ContentView: View {
#EnvironmentObject var userData: UserData
#EnvironmentObject var currants: Currants
#EnvironmentObject var blockStatus: BlockStatus
var body: some View {
NavigationView {
VStack (alignment: .center) {
Text("Title")
.font(.title)
.fontWeight(.bold)
Spacer()
Group {
NavigationLink(destination: entryView()) {Text("Entry")}
.disabled(currants.curItem.count == 0)
Spacer()
NavigationLink(destination: totalView()) {Text("View Totals")}
Spacer()
NavigationLink(destination: listView()) {Text("View Entries")}
Spacer()
NavigationLink(destination: xchView()) {Text("View Dates")}
}
Rectangle()
.frame(height: 130)
.foregroundColor(Color.white)
}
.font(.title2)
.navigationBarItems(leading: NavigationLink (destination: settingsView()) {
Image(systemName: "gear")
.foregroundColor(.gray)
.font(.system(.title3))
}, trailing: NavigationLink( destination: aboutView()) {
Text("About")
})
.onAppear(perform: getData)
}
}
func getData() {
// check criteria for updating data once daily
if blockStatus.checkForUpdates() == true {
print(" doing update")
---- API HERE -----
}.resume()
}
}
}