I am attempting the configure the text field and button in my openweathermap app to be in its own view other than the main content view. In TextFieldView, the action of the button is set up to call an API response. Then, the weather data from the response is populated on a sheet-based DetailView, which is triggered by the button in TextFieldView. I configured the ForEach method in the sheet to return the last city added to the WeatherModel array (which would technically be the most recent city entered into the text field), then populate the sheet-based DetailView with weather data for that city. Previously, When I had the HStack containing the text field, button, and sheet control set up in the ContentView, the Sheet would properly display weather for the city that had just entered into the text field. After moving those items to a separate TextFieldView, the ForEach method appears to have stopped working. Instead, the weather info returned after entering a city name into the text field is displayed on the wrong count. For instance, if I were to enter "London" in the text field, the DetailView in the sheet is completely blank. If I then enter "Rome" as the next entry, the DetailView in the sheet shows weather info for the previous "London" entry. Entering "Paris" in the textfield displays weather info for "Rome", and so on...
To summarize, the ForEach method in the sheet stopped working properly after I moved the whole textfield and button feature to a separate view. Any idea why the issue I described is happening?
Here is my code:
ContentView
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
var body: some View {
NavigationView {
VStack(alignment: .leading) {
List {
ForEach(viewModel.cityNameList.reversed()) { city in
NavigationLink(destination: DetailView(detail: city), label: {
Text(city.name).font(.system(size: 18))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°")
.font(.system(size: 18))
})
}
.onDelete { indexSet in
let reversed = Array(viewModel.cityNameList.reversed())
let items = Set(indexSet.map { reversed[$0].id })
viewModel.cityNameList.removeAll { items.contains($0.id) }
}
}
.refreshable {
viewModel.updatedAll()
}
TextFieldView(viewModel: viewModel)
}.navigationBarTitle("Weather", displayMode: .inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
TextFieldView
struct TextFieldView: View {
#State private var cityName = ""
#State private var showingDetail = false
#FocusState var isInputActive: Bool
var viewModel: WeatherViewModel
var body: some View {
HStack {
TextField("Enter City Name", text: $cityName)
.focused($isInputActive)
Spacer()
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Done") {
isInputActive = false
}
}
}
if isInputActive == false {
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
Image(systemName: "plus")
.font(.largeTitle)
.frame(width: 75, height: 75)
.foregroundColor(Color.white)
.background(Color(.systemBlue))
.clipShape(Circle())
}
.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
.frame(minWidth: 100, idealWidth: 150, maxWidth: 500, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
.padding(.leading, 16)
.padding(.trailing, 16)
}
}
struct TextFieldView_Previews: PreviewProvider {
static var previews: some View {
TextFieldView(viewModel: WeatherViewModel())
}
}
DetailView
struct DetailView: View {
#State private var cityName = ""
#State var selection: Int? = nil
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
ViewModel
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName.escaped())&units=imperial&appid=<YourAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.addToList(model)
}
}
catch {
print(error)
}
}
task.resume()
}
func updatedAll() {
// keep a copy of all the cities names
let listOfNames = cityNameList.map{$0.name}
// fetch the up-to-date weather info
for city in listOfNames {
fetchWeather(for: city)
}
}
func addToList( _ city: WeatherModel) {
// if already have this city, just update
if let ndx = cityNameList.firstIndex(where: {$0.name == city.name}) {
cityNameList[ndx].main = city.main
cityNameList[ndx].weather = city.weather
} else {
// add a new city
cityNameList.append(city)
}
}
}
Model
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
var humidity = 0
}
struct WeatherInfo: Codable {
var description: String = ""
}
You need to use an ObservedObject in your TextFieldView to use your
original (single source of truth) #StateObject var viewModel that you create in ContentView and observe any change to it.
So use this:
struct TextFieldView: View {
#ObservedObject var viewModel: WeatherViewModel
...
}
Related
I am utilizing a search bar from a Kavsoft Tutorial here: "https://www.youtube.com/watch?v=nuag1PILxCA&t=14s", I'm wondering on how to add navigation links to each of the items, I decided on embedding the itemView inside a navigation link with an array of views to loop through but it seems that it doesn't accept the index as a parameter giving "Cannot convert value of type 'item' to expected argument type 'Int'", instead I incremented the subscript on appear in the navigation link, although that updates the variable, but it doesn't seem to work for the different views themselves only navigating to the first view.
I've linked all the code needed to reproduce the problem but due to my incredibly limited experience in reproducing the problem in as less code as possible, I am not able to do so. Below the main issue of concern is the block starting from the VStack. Starting the program can be done by just adding Search_Bar() to content view body.
struct Home: View {
let views : [AnyView] = [ AnyView(untitled_Skull()), AnyView(dogs()), AnyView(cats()) ]
#Binding var filteredItems : [item]
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
var i = 0
VStack(spacing: 15){
ForEach(filteredItems){index in
NavigationLink(destination: views[i]
) {
itemView(item: index)
}.onAppear() {
i = i + 1
}
}
}
.padding()
}
}
}
func add(value: Int) -> Int {
let value = value + 1
return value
}
struct itemView: View {
var item: item
#State var show = false
var body: some View {
HStack(spacing: 15){
VStack {
let colorArray: [Color] = [.yellowLichtenstien, .redHaring, .orangeBasquiat, .pinkWarhol]
HStack {
Text(item.name)
.foregroundColor(.white)
.bold()
.padding(.leading)
Spacer()
}
HStack {
Text(item.subText)
.bold()
.foregroundColor (.white)
.font(.subheadline)
.padding(.leading)
Circle()
.frame(width: 5, height: 5)
.foregroundColor(colorArray[item.color])
Text(item.subText2)
.bold()
.foregroundColor (.white)
.font(.subheadline)
Spacer()
}
Spacer()
}
}
.padding(.horizontal)
}
}
struct item: Identifiable {
var id = UUID().uuidString
// both Image And Name Are Same....
var name: String
// since all Are Apple Native Apps...
var color: Int
var subText: String
var subText2: String
}
var searchItems = [
item(name: "Untitled (Skull)", color: 0, subText: "1983", subText2: "yay"),
item(name: "Dogs", color: 1, subText: "1972", subText2: "wow"),
item(name: "Cats", color: 2, subText: "1968", subText2: "oof")
]
struct Search_Bar: View {
#State var filteredItems = searchItems
var body: some View {
CustomNavigationView(view: AnyView(Home(filteredItems: $filteredItems)), placeHolder: "Museums, Art or anything else.", largeTitle: true, title: "Search",
onSearch: { (txt) in
if txt != ""{
self.filteredItems = searchItems.filter{$0.name.lowercased().contains(txt.lowercased())}
}
else{
self.filteredItems = searchItems
}
}, onCancel: {
// Do Your Own Code When Search And Canceled....
self.filteredItems = searchItems
})
.ignoresSafeArea()
}
}
struct Search_Bar_Previews: PreviewProvider {
static var previews: some View {
Search_Bar()
}
}
import SwiftUI
struct CustomNavigationView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return CustomNavigationView.Coordinator(parent: self)
}
// Just Change Your View That Requires Search Bar...
var view: AnyView
// Ease Of Use.....
var largeTitle: Bool
var title: String
var placeHolder: String
// onSearch And OnCancel Closures....
var onSearch: (String)->()
var onCancel: ()->()
// requre closure on Call...
init(view: AnyView,placeHolder: String? = "Search",largeTitle: Bool? = true,title: String,onSearch: #escaping (String)->(),onCancel: #escaping ()->()) {
self.title = title
self.largeTitle = largeTitle!
self.placeHolder = placeHolder!
self.view = view
self.onSearch = onSearch
self.onCancel = onCancel
}
// Integrating UIKit Navigation Controller With SwiftUI View...
func makeUIViewController(context: Context) -> UINavigationController {
// requires SwiftUI View...
let childView = UIHostingController(rootView: view)
let controller = UINavigationController(rootViewController: childView)
// Nav Bar Data...
controller.navigationBar.topItem?.title = title
controller.navigationBar.prefersLargeTitles = largeTitle
// search Bar....
let searchController = UISearchController()
searchController.searchBar.placeholder = placeHolder
// setting delegate...
searchController.searchBar.delegate = context.coordinator
// setting Search Bar In NavBar...
// disabling hide on scroll...
// disabling dim bg..
searchController.obscuresBackgroundDuringPresentation = false
controller.navigationBar.topItem?.hidesSearchBarWhenScrolling = false
controller.navigationBar.topItem?.searchController = searchController
return controller
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
// Updating Real Time...
uiViewController.navigationBar.topItem?.title = title
uiViewController.navigationBar.topItem?.searchController?.searchBar.placeholder = placeHolder
uiViewController.navigationBar.prefersLargeTitles = largeTitle
}
// search Bar Delegate...
class Coordinator: NSObject,UISearchBarDelegate{
var parent: CustomNavigationView
init(parent: CustomNavigationView) {
self.parent = parent
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// when text changes....
self.parent.onSearch(searchText)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
// when cancel button is clicked...
self.parent.onCancel()
}
}
}
Letting the random view below for the array being for example:
import SwiftUI
struct cats: View {
var body: some View {
Text("cats") //replacing this with dogs or untitled skull as an example.
}
}
struct cats_Previews: PreviewProvider {
static var previews: some View {
cats()
}
}
You can use ForEach getting the item and its index in the closure :
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: views[index]){
Text(item.name)
}
}
For example :
struct ListItem: Identifiable {
let id = UUID()
let name: String
}
struct SwiftUIView17: View {
#State private var filteredItems = ["John", "Bob", "Maria"].map(ListItem.init)
let views = [AnyView(Text("John destination")), AnyView(Text("Bob destination")), AnyView(Text("Maria destination"))]
var body: some View {
ScrollView {
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: views[index]){
Text(item.name)
}
}
}
}
}
But it would be better not to use AnyView but a ViewBuilder :
struct SwiftUIView17: View {
#State private var filteredItems = ["John", "Bob", "Maria"].map(ListItem.init)
#ViewBuilder func destination(for itemIndex: Int) -> some View {
switch itemIndex {
case 0: Text("John destination")
case 1: Text("Bob destination").foregroundColor(.red)
case 2: Rectangle()
default: Text("error")
}
}
var body: some View {
ScrollView {
ForEach(Array(filteredItems.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: destination(for: index)){
Text(item.name)
}
}
}
}
}
UIKit used to support TableView Cell that enabled a Blue info/disclosure button. The following was generated in SwiftUI, however getting the underlying functionality to work is proving a challenge for a beginner to SwiftUI.
Generated by the following code:
struct Session: Identifiable {
let date: Date
let dir: String
let instrument: String
let description: String
var id: Date { date }
}
final class SessionsData: ObservableObject {
#Published var sessions: [Session]
init() {
sessions = [Session(date: SessionsData.dateFromString(stringDate: "2016-04-14T10:44:00+0000"),dir:"Rhubarb", instrument:"LCproT", description: "brief Description"),
Session(date: SessionsData.dateFromString(stringDate: "2017-04-14T10:44:00+0001"),dir:"Custard", instrument:"LCproU", description: "briefer Description"),
Session(date: SessionsData.dateFromString(stringDate: "2018-04-14T10:44:00+0002"),dir:"Jelly", instrument:"LCproV", description: " Description")
]
}
static func dateFromString(stringDate: String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return dateFormatter.date(from:stringDate)!
}
}
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(session: session )
}
}
.navigationTitle("Session data")
}
// without this style modification we get all sorts of UIKit warnings
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SessionRow: View {
var session: Session
#State private var presentDescription = false
var body: some View {
HStack(alignment: .center){
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
Spacer()
// SessionGraph is a place holder for the Graph data.
NavigationLink(destination: SessionGraph()) {
// if this isn't an EmptyView then we get a disclosure indicator
EmptyView()
}
// Note: without setting the NavigationLink hidden
// width to 0 the List width is split 50/50 between the
// SessionRow and the NavigationLink. Making the NavigationLink
// width 0 means that SessionRow gets all the space. Howeveer
// NavigationLink still works
.hidden().frame(width: 0)
Button(action: { presentDescription = true
print("\(session.dir):\(presentDescription)")
}) {
Image(systemName: "info.circle")
}
.buttonStyle(BorderlessButtonStyle())
NavigationLink(destination: SessionDescription(),
isActive: $presentDescription) {
EmptyView()
}
.hidden().frame(width: 0)
}
.padding(.vertical, 4)
}
}
struct SessionGraph: View {
var body: some View {
Text("SessionGraph")
}
}
struct SessionDescription: View {
var body: some View {
Text("SessionDescription")
}
}
The issue comes in the behaviour of the NavigationLinks for the SessionGraph. Selecting the SessionGraph, which is the main body of the row, propagates to the SessionDescription! hence Views start flying about in an un-controlled manor.
I've seen several stated solutions to this issue, however none have worked using XCode 12.3 & iOS 14.3
Any ideas?
When you put a NavigationLink in the background of List row, the NavigationLink can still be activated on tap. Even with .buttonStyle(BorderlessButtonStyle()) (which looks like a bug to me).
A possible solution is to move all NavigationLinks outside the List and then activate them from inside the List row. For this we need #State variables holding the activation state. Then, we need to pass them to the subviews as #Binding and activate them on button tap.
Here is a possible example:
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
// create state variables for activating NavigationLinks
#State private var presentGraph: Session?
#State private var presentDescription: Session?
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(
session: session,
presentGraph: $presentGraph,
presentDescription: $presentDescription
)
}
}
.navigationTitle("Session data")
// put NavigationLinks outside the List
.background(
VStack {
presentGraphLink
presentDescriptionLink
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var presentGraphLink: some View {
// custom binding to activate a NavigationLink - basically when `presentGraph` is set
let binding = Binding<Bool>(
get: { presentGraph != nil },
set: { if !$0 { presentGraph = nil } }
)
// activate the `NavigationLink` when the `binding` is `true`
NavigationLink("", destination: SessionGraph(), isActive: binding)
}
#ViewBuilder
var presentDescriptionLink: some View {
let binding = Binding<Bool>(
get: { presentDescription != nil },
set: { if !$0 { presentDescription = nil } }
)
NavigationLink("", destination: SessionDescription(), isActive: binding)
}
}
struct SessionRow: View {
var session: Session
// pass variables as `#Binding`...
#Binding var presentGraph: Session?
#Binding var presentDescription: Session?
var body: some View {
HStack {
Button {
presentGraph = session // ...and activate them manually
} label: {
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button {
presentDescription = session
print("\(session.dir):\(presentDescription)")
} label: {
Image(systemName: "info.circle")
}
.buttonStyle(PlainButtonStyle())
}
.padding(.vertical, 4)
}
}
I'm trying to create a List and allow only one item to be selected at a time. How would I do so in a ForEach loop? I can select multiple items just fine, but the end goal is to have only one checkmark in the selected item in the List. It may not even be the proper way to handle what I'm attempting.
struct ContentView: View {
var body: some View {
NavigationView {
List((1 ..< 4).indices, id: \.self) { index in
CheckmarkView(index: index)
.padding(.all, 3)
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
//.environment(\.editMode, .constant(.active))
}
}
}
struct CheckmarkView: View {
let index: Int
#State var check: Bool = false
var body: some View {
Button(action: {
check.toggle()
}) {
HStack {
Image("Image-\(index)")
.resizable()
.frame(width: 70, height: 70)
.cornerRadius(13.5)
Text("Example-\(index)")
Spacer()
if check {
Image(systemName: "checkmark")
.resizable()
.frame(width: 12, height: 12)
}
}
}
}
}
You'll need something to store all of the states instead of storing it per-checkmark view, because of the requirement to just have one thing checked at a time. I made a little example where the logic is handled in an ObservableObject and passed to the checkmark views through a custom Binding that handles checking/unchecking states:
struct CheckmarkModel {
var id = UUID()
var state = false
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.checkmarks[index].state
} set: { (newValue) in
self.checkmarks = self.checkmarks.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.state = newValue
} else {
//not the same index
if newValue {
itemCopy.state = false
}
}
return itemCopy
}
}
}
}
struct ContentView: View {
#ObservedObject var state = StateManager()
var body: some View {
NavigationView {
List(Array(state.checkmarks.enumerated()), id: \.1.id) { (index, item) in //<-- here
CheckmarkView(index: index + 1, check: state.singularBinding(forIndex: index))
.padding(.all, 3)
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct CheckmarkView: View {
let index: Int
#Binding var check: Bool //<-- Here
var body: some View {
Button(action: {
check.toggle()
}) {
HStack {
Image("Image-\(index)")
.resizable()
.frame(width: 70, height: 70)
.cornerRadius(13.5)
Text("Example-\(index)")
Spacer()
if check {
Image(systemName: "checkmark")
.resizable()
.frame(width: 12, height: 12)
}
}
}
}
}
What's happening:
There's a CheckmarkModel that has an ID for each checkbox, and the state of that box
StateManager keeps an array of those models. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the checkbox array. Any time a checkbox is set, it unchecks all of the other boxes. I also kept your original behavior of allowing nothing to be checked
The List now gets an enumeration of the state.checkmarks -- using enumerated lets me keep your previous behavior of being able to pass an index number to the checkbox view
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
List {
ForEach(0 ..< RemindTimeType.allCases.count) {
index in CheckmarkView(title:getListTitle(index), index: index, markIndex: $markIndex)
.padding(.all, 3)
}.listRowBackground(Color.clear)
}
struct CheckmarkView: View {
let title: String
let index: Int
#Binding var markIndex: Int
var body: some View {
Button(action: {
markIndex = index
}) {
HStack {
Text(title)
.foregroundColor(Color.white)
.font(.custom(FontEnum.Regular.fontName, size: 14))
Spacer()
if index == markIndex {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(hex: 0xe6c27c))
}
}
}
}
}
We can benefit from binding collections of Swift 5.5.
import SwiftUI
struct CheckmarkModel: Identifiable, Hashable {
var id = UUID()
var state = false
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]
}
struct SingleSelectionList<Content: View>: View {
#Binding var items: [CheckmarkModel]
#Binding var selectedItem: CheckmarkModel?
var rowContent: (CheckmarkModel) -> Content
#State var previouslySelectedItemNdx: Int?
var body: some View {
List(Array($items.enumerated()), id: \.1.id) { (ndx, $item) in
rowContent(item)
.modifier(CheckmarkModifier(checked: item.id == self.selectedItem?.id))
.contentShape(Rectangle())
.onTapGesture {
if let prevIndex = previouslySelectedItemNdx {
items[prevIndex].state = false
}
self.selectedItem = item
item.state = true
previouslySelectedItemNdx = ndx
}
}
}
}
struct CheckmarkModifier: ViewModifier {
var checked: Bool = false
func body(content: Content) -> some View {
Group {
if checked {
ZStack(alignment: .trailing) {
content
Image(systemName: "checkmark")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.green)
.shadow(radius: 1)
}
} else {
content
}
}
}
}
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selectedItem: CheckmarkModel?
var body: some View {
VStack {
Text("Selected Item: \(selectedItem?.id.description ?? "Select one")")
Divider()
SingleSelectionList(items: $state.checkmarks, selectedItem: $selectedItem) { item in
HStack {
Text(item.id.description + " " + item.state.description)
Spacer()
}
}
}
}
}
A bit simplified version
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selection: CheckmarkModel.ID?
var body: some View {
List {
ForEach($state.checkmarks) { $item in
SelectionCell(item: $item, selectedItem: $selection)
.onTapGesture {
if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
state.checkmarks[ndx].state = false
}
selection = item.id
item.state = true
}
}
}
.listStyle(.plain)
}
}
struct SelectionCell: View {
#Binding var item: CheckmarkModel
#Binding var selectedItem: CheckmarkModel.ID?
var body: some View {
HStack {
Text(item.id.description + " " + item.state.description)
Spacer()
if item.id == selectedItem {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
}
A version that uses internal List's selected mark and selection:
import SwiftUI
struct CheckmarkModel: Identifiable, Hashable {
var name: String
var state: Bool = false
var id = UUID()
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(name: "Name1"), CheckmarkModel(name: "Name2"), CheckmarkModel(name: "Name3"), CheckmarkModel(name: "Name4")]
}
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selection: CheckmarkModel.ID?
#State private var selectedItems = [CheckmarkModel]()
var body: some View {
VStack {
Text("Items")
List($state.checkmarks, selection: $selection) { $item in
Text(item.name + " " + item.state.description)
}
.onChange(of: selection) { s in
for index in state.checkmarks.indices {
if state.checkmarks[index].state == true {
state.checkmarks[index].state = false
}
}
selectedItems = []
if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
state.checkmarks[ndx].state = true
selectedItems = [state.checkmarks[ndx]]
print(selectedItems)
}
}
.environment(\.editMode, .constant(.active))
Divider()
List(selectedItems) {
Text($0.name + " " + $0.state.description)
}
}
Text("\(selectedItems.count) selections")
}
}
I have a code that makes a http Request, gets an array with filenames from that, displays them each with an image and the filename below. Everything works fine.
Now I made each image a button that opens a detail page.
That works but at the top it should say the matching filename from the page before.
But I am not able to hand over the filename (name) from ContentView4 to the next page (ts).
The language is SwiftUi
Could you please help me?
Thanks
Nikias
Here is my code:
import SwiftUI
struct ContentView4: View {
#State var showingDetail = false
#State var username: String = "."
#State var password: String = "."
#State private var name = String("Nikias2")
#State private var t = String()
#State private var x = -1
#State var dateien = ["word.png"]
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
$showingDetail) {
ts(name: t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
doHttpRequest()
}
}
}
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
ContentView4()
}
}
struct ts: View {
#State var hin = false
#State var um = false
#State var datname: String = ""
var name: String
var body: some View {
NavigationView {
VStack {
Text(name)
.font(.system(size: 60))
.foregroundColor(.black)
.padding(50)
Button(action: {
self.hin.toggle()
}) {
Text("+")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.yellow)
.cornerRadius(35.0)
}
.padding()
if hin {
HStack {
Text("Datei auswählen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei hochladen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei herunterladen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.blue)
.cornerRadius(35.0)
Button(action: {
self.um.toggle()
}) {
Text("Datei umbenennen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.green)
.cornerRadius(35.0)
}
.padding()
if um {
HStack {
TextField(name, text: $datname)
.font(.headline)
.frame(width: 150, height: 70)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei umbenennen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.green)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei löschen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.red)
.cornerRadius(35.0)
}
}
}
}
I believe your issue is a result of using #State variables to store all of the attributes. #State variables are not consistent and get refreshed in the background by SwiftUI depending on your views visibility.
The piece that you are missing is a view controller class stored in an #EnviornmentObject variable. This class gets Initiated in your main contentView and is used to keep track and alter of all your attributes.
Each ContentView should reference the single #EnviornmentObject and pull data from that class.
Another solution which may work would be to replace all your #State variables with #StateObject vars. #StateObject vars are basically #State vars but get initiated before the struct get loaded and the value is kept consistent regardless of the view state of the parent struct.
Here is a rough implementation of #EnvironmentObject within your project.
Basically use the #EnvironmentObject to pass values to child views
ContentView4.swift
struct ContentView4: View {
#EnvironmentObject cv4Controller: ContentView4Controller
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< cv4Controller.dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = cv4Controller.dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(cv4Controller.t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
cv4Controller.$showingDetail) {
ts(name: cv4Controller.t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
cv4Controller.doHttpRequest()
}
}
}
ContentView4Controller.swift
class ContentView4Controller: ObservableObject {
#Published var showingDetail = false
#Published var username: String = "."
#Published var password: String = "."
#Published private var name = String("Nikias2")
#Published private var t = String()
#Published private var x = -1
#Published private var t = String()
#Published private var x = -1
#Published var dateien = ["word.png"]
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
Example of main ContentView.swift
struct ContentView: View {
var cv4Controller: ContentView4Controller = ContentView4Controller()
var body: some view {
// your main page output
GeometryReader { geo in
// just a guess for what you have in your main contentView
switch(page) {
case .main:
ContentView2()
default:
ContentView4()
break
}
}.environmentObject(cv4Controller) // this will make cv4Controller available to all child view structs
}
}
Add #Binding wrapper to the "name" variable in your ts view. And pass the t variable as a binding by adding a "$". This will keep your ts name variable updated to whatever is value it has in the parent view.
Also why do you use a NavigationView in your ts View?
struct ContentView4: View {
...
#State private var t = String()
...
var body: some View {
...
ZStack{
...
}
.sheet(isPresented: $showingDetail) {
ts(name: $t)
}
...
}
func doHttpRequest() {
...
}
}
struct ts: View {
...
#Binding var name: String
var body: some View {
...
}
}
My starting code works, but It's just displaying the Filenames in a row and if I tap a random image, the name won't fit, only if I'm going down in the row and tap them. The problem is, that I don't know how to set the variable to the id, not to pass them to the next view. Has anyone got and idea how I can pass the right filename into a variable in the for loop and read it in the next view?
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()
}
}
}