I have two pages in my app TodayPage and CalendarList page.
I use EnvironmentObject wrapper to pass data between these two pages.
When TodayPage appears on onAppear modifier I call a function to generate days of calendar for me till now everything works fine when I add text to the list of TodayPage then go to the calendarList page and come back again to TodayPage all of the text that I addd to list are gone.I find out I can avoid lost of data by adding simple if to onAppear but I'm not sure this solution is right.
I have to upload lots of code ,Thanks for your help
( DataModel ) :
import SwiftUI
import Foundation
import Combine
struct Day : Identifiable {
var id = UUID()
var name : String
var date : String
var month : String
var List : [Text1?]
}
struct Text1 : Identifiable , Hashable{
var id = UUID()
var name: String
var color: Color
}
class AppState : ObservableObject {
#Published var dataLoaded = false
#Published var allDays : [Day] = [.init(name : "",date: "",month: "",List : [])]
func getDays(number: Int) -> [Day] {
let today = Date()
let formatter = DateFormatter()
return (0..<number).map { index -> Day in
let date = Calendar.current.date(byAdding: .day, value: index, to: today) ?? Date()
return Day(name: date.dayOfWeek(withFormatter: formatter) ?? "", date: "\(Calendar.current.component(.day, from: date))", month: date.nameOfMonth(withFormatter: formatter) ?? "", List: [])
}
}
}
extension Date {
func dayOfWeek(withFormatter dateFormatter: DateFormatter) -> String? {
dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: self).capitalized
}
func nameOfMonth(withFormatter dateFormatter: DateFormatter) -> String? {
dateFormatter.dateFormat = "LLLL"
return dateFormatter.string(from: self).capitalized
}
}
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(name: "", color: .clear)] //start with one empty item
func saveToAppState(appState: AppState) {
appState.allDays[0].List.append(contentsOf: textItemsToAdd.filter {
!$0.name.isEmpty })
}
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.name ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, name: newValue, color: .clear)
}
}
}
}
List view :
struct ListView: View {
#State private var showAddListView = false
#EnvironmentObject var appState : AppState
#Binding var dayList : [Text1?]
var title : String
var body: some View {
NavigationView {
VStack {
ZStack {
List(dayList, id : \.self){ text in
Text(text?.name ?? "")
}
if showAddListView {
AddListView(showAddListView: $showAddListView)
.offset(y:-100)
}
}
}
.navigationTitle(title)
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
pop up menu View(for adding text into the list)
struct AddListView: View {
#Binding var showAddListView : Bool
#EnvironmentObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(name: "", color: .clear)) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id: \.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id))
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
TodayPage View :
struct TodayPage: View {
#EnvironmentObject var appState : AppState
var body: some View {
ListView(dayList: $appState.allDays[0].List, title: "Today")
.onAppear {
// To avoid data lost , we can use simple if below but I'm not sure it's a right solution
// if appState.dataLoaded == false {
appState.allDays = appState.getDays(number: 365)
// appState.dataLoaded = true
// }
}
}
}
CalendarListPage :
struct CalendarList: View {
#EnvironmentObject var appState : AppState
var body: some View {
NavigationView {
List {
ForEach(appState.allDays.indices, id:\.self) { index in
NavigationLink(destination: ListView(appState: _appState, dayList: $appState.allDays[index].List, title: appState.allDays[index].name).navigationBarTitleDisplayMode(.inline) ) {
HStack(alignment: .top) {
RoundedRectangle(cornerRadius: 23)
.frame(width: 74, height: 74)
.foregroundColor(Color.blue)
.overlay(
VStack {
Text(appState.allDays[index].date)
.font(.system(size: 35, weight: .regular))
.foregroundColor(.white)
Text(appState.allDays[index].month)
.foregroundColor(.white)
}
)
.padding(.trailing ,4)
VStack(alignment: .leading, spacing: 5) {
Text(appState.allDays[index].name)
.font(.system(size: 20, weight: .semibold))
}
}
.padding(.vertical ,6)
}
}
}
.navigationTitle("Calendar")
}.onAppear {
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
and finally TabBar :
struct TabBar: View {
var body: some View {
let appState = AppState()
TabView {
TodayPage().tabItem {
Image(systemName: "info.circle")
Text("Today")
}
CalendarList().tabItem {
Image(systemName: "square.fill.text.grid.1x2")
Text("Calendar")
}
}
.environmentObject(appState)
}
}
Right now, because your let appState is inside the body of TabBar, it gets recreated every time TabBar is rendered. Instead, store it as a #StateObject (or #ObservedObject if you are pre iOS 14):
struct TabBar: View {
#StateObject var appState = AppState()
var body: some View {
TabView {
TodayPage().tabItem {
Image(systemName: "info.circle")
Text("Today")
}
CalendarList().tabItem {
Image(systemName: "square.fill.text.grid.1x2")
Text("Calendar")
}
}
.onAppear {
appState.allDays = appState.getDays(number: 365)
}
.environmentObject(appState)
}
}
Then, remove your other onAppear on TodayPage
Related
I just started to learn Swift programing language and have a question.
I'm trying to create a simple one-page application where you can add movies to a favorite list. Movies must have 2 properties: title (string, mandatory) and year (integer, mandatory). But I have a problem, I don't know how to put it in one row.
And also, how to ignore duplicate movies?
import SwiftUI
struct Movie: Identifiable {
let id = UUID()
var title: String
}
class Model: ObservableObject {
#Published var movies: [Movie] = []
}
struct DynamicList: View {
#StateObject var model = Model()
#State var text = ""
#State var year = ""
var body: some View {
VStack {
Section() {
TextField("Title", text: $text)
.padding()
.border(.gray)
TextField("Year", text: $year)
.padding()
.border(.gray)
.keyboardType(.numberPad)
Button(action: {
self.addToList()
}, label: {
Text("Add")
.frame(width: 80, height: 40, alignment: .center)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
})
.padding()
}
List {
ForEach(model.movies) { movie in
MovieRow(title: movie.title)
}
}
}
.padding()
}
func addToList() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
guard !year.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
let newMovie = Movie(title: text)
model.movies.append(newMovie)
text = ""
let newYear = Movie(title: year)
model.movies.append(newYear)
year = ""
}
}
struct MovieRow: View {
let title: String
var body: some View {
Label (
title: { Text(title)},
icon: { Image(systemName: "film") }
)
}
}
struct DynamicList_Previews: PreviewProvider {
static var previews: some View {
DynamicList()
}
}
Here is the solution. It will show the data in one row and also how to ignore duplicate movies to show into the list. Check the below code:
import SwiftUI
struct Movie: Identifiable {
var id = UUID()
let title: String
let year: String
}
class MoviesViewModel: ObservableObject {
#Published var movies: [Movie] = []
}
struct ContentView: View {
#State var boolValue = false
#StateObject var viewModel = MoviesViewModel()
#State var text = ""
#State var year = ""
var body: some View {
VStack {
Section() {
TextField("Title", text: $text)
.padding()
.border(.gray)
TextField("Year", text: $year)
.padding()
.border(.gray)
.keyboardType(.numberPad)
Button(action: {
self.addToList()
}, label: {
Text("Add")
.frame(width: 80, height: 40, alignment: .center)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
})
.padding()
}
// Show the data in list form
List {
ForEach(viewModel.movies) { movie in
MovieRow(title: movie.title, year: movie.year)
}
}
}
.padding()
}
func addToList() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
guard !year.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
// Condition to check whether the data is already exit or not
boolValue = false
let newMovie = Movie(title: text, year: year)
for movie in viewModel.movies{
if ((movie.title.contains(text)) && (movie.year.contains(year))){
boolValue = true
}
}
// check if boolValue is false so the data will store into the array.
if boolValue == false{
viewModel.movies.append(newMovie)
text = ""
year = ""
}
}
}
struct MovieRow: View {
let title: String
let year: String
var body: some View {
// Show the data insert into the textfield
HStack{
Label (
title: { Text(title)},
icon: { Image(systemName: "film") }
)
Spacer()
Label (
title: { Text(year)},
icon: { Image(systemName: "calendar") }
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Maybe someone will need a similar solution, here is my result:
import SwiftUI
struct Movie: Identifiable {
let id = UUID()
var title: String
var year: String
}
class Model: ObservableObject {
#Published var movies: [Movie] = []
}
struct DynamicList: View {
#StateObject var model = Model()
#State var text = ""
#State var year = ""
var body: some View {
VStack {
Section() {
TextField("Title", text: $text)
.padding()
.border(.gray)
TextField("Year", text: $year)
.padding()
.border(.gray)
.keyboardType(.numberPad)
Button(action: {
self.addToList()
}, label: {
Text("Add")
.frame(width: 80, height: 40, alignment: .center)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
})
.padding()
}
List {
ForEach(model.movies) { movie in
MovieRow(title: movie.title, year: movie.year)
}
}
}
.padding()
}
func addToList() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
guard !year.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
let newMovie = Movie(title: text, year: year)
model.movies.append(newMovie)
text = ""
year = ""
}
}
struct MovieRow: View {
let title: String
let year: String
var body: some View {
Label (
title: { Text(title + " " + year)},
icon: { Image(systemName: "film") }
)
}
}
struct DynamicList_Previews: PreviewProvider {
static var previews: some View {
DynamicList()
}
}
Purpose
I want to update the data in the tabview automatically when I return to the RootView after I rename the tag name in the tagManagemenView.
Current Status
When I do delete operation in the TagManagementView, the RootView is able to update automatically.
However, If the Tag name is modified, the RootView display will not be updated, but clicking into the ItemDetailsView is able to display the latest modified name. Only the display of the RootView is not updated.
Background and code
Item and Tag are created using coredata and have a one-to-many relationship, where one Item corresponds to multiple Tags
// RootView
struct RootView: View {
#State private var selection = 0
var body: some View {
NavigationView {
TabView(selection: $selection) {
ItemListView()
.tabItem {
Label ("Items",systemImage: "shippingbox")
}
.tag(0)
Settings()
.tabItem{
Label("Settings", systemImage: "gearshape")
}
.tag(1)
}
.navigationTitle(selection == 0 ? "Items" : "Settings")
}
.navigationViewStyle(.stack)
}
}
// ItemListView
struct ItemListView: View {
#FetchRequest var items: FetchedResults<Item>
#State private var itemDetailViewIsShow: Bool = false
#State private var selectedItem: Item? = nil
init() {
var predicate = NSPredicate(format: "TRUEPREDICATE"))
_items = FetchRequest(fetchRequest: Item.fetchRequest(predicate))
}
var body: some View {
ForEach(items) { item in
Button(action: {
self.selectedItem = item
self.itemDetailViewIsShow = true
}, label: {
ItemCellView(item: item)
})
}
if selectedItem != nil {
NavigationLink (
destination: ItemDetailView(item: selectedItem!, detailViewIsShow: $itemDetailViewIsShow),
isActive: $itemDetailViewIsShow
) {
EmptyView()
}
.isDetailLink(false)
}
}
}
// TagManagementView
struct TagManagementView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: Tag.entity(), sortDescriptors: []) var allTags: FetchedResults<Tag>
#State var isShowDeleteAlert = false
#State var showModel = false
#State var selected: Tag?
var body: some View {
ZStack {
List {
ForEach(allTags) { tag in
TagCellView(tag: tag)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive, action: {
isShowDeleteAlert = true
selected = tag
}, label: {
Label("Delete", systemImage: "trash")
.foregroundColor(.white)
})
Button(action: {
showModel = true
selected = tag
}, label: {
Label("Edit", systemImage: "square.and.pencil")
.foregroundColor(.white)
})
}
}
.confirmationDialog("Delete confirm", isPresented: self.$isShowDeleteAlert, titleVisibility: .visible) {
Button("Delete", role: .destructive) {
if self.selected != nil {
self.selected!.delete(context: context)
}
}
Button(role: .cancel, action: {
isShowDeleteAlert = false
}, label: {
Text("Cancel")
.font(.system(size: 17, weight: .medium))
})
}
}
if self.showModel {
// background...
Color("mask").edgesIgnoringSafeArea(.all)
TagEditorModal(selected: self.$selected, isShowing: self.$showModel)
}
}
}
}
// TagEditorModal
struct TagEditorModal: View {
#Environment(\.managedObjectContext) var context
#State var tagName: String = ""
#Binding var isShowing: Bool
#Binding var selector: Tag?
init (selected: Binding<Tag?>, isShowing: Binding<Bool>) {
_isShowing = isShowing
_selector = selected
_tagName = .init(wrappedValue: selected.wrappedValue!.name)
}
var body: some View {
VStack{
TextField("Tag name", text: self.$tagName)
HStack {
Button(action: {
self.isShowing = false
}) {
Text("Cancel")
}
Button(action: {
self.selector!.update(name: self.tagName, context: context)
self.isShowing = false
}, label: {
Text("Submit")
})
}
}
}
}
// update tagName func
extension Tag {
func update(name: String, context: NSManagedObjectContext) {
self.name = name
self.updatedAt = Date()
self.objectWillChange.send()
try? context.save()
}
}
// ItemCellView
struct ItemCellView: View {
#Environment(\.managedObjectContext) var context
#ObservedObject var item: Item
var body: some View {
VStack {
Text(item.name)
TagListView(tags: .constant(item.tags))
}
}
}
// tagListView
struct TagListView: View {
#Binding var tags: [Tag]
#State private var totalHeight = CGFloat.zero
var body: some View {
VStack {
GeometryReader { geo in
VStack(alignment: .leading,spacing: 10) {
ForEach (getRows(screenWidth: geo.size.width), id: \.self) {rows in
HStack(spacing: 4) {
ForEach (rows) { tag in
Text(tag.name)
.font(.system(size: 10))
.fontWeight(.medium)
.lineLimit(1)
.cornerRadius(40)
}
}
}
}
.frame(width: geo.size.width, alignment: .leading)
.background(viewHeightReader($totalHeight))
}
}
.frame(height: totalHeight)
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geo -> Color in
let rect = geo.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
func getRows(screenWidth: CGFloat) -> [[Tag]] {
var rows: [[Tag]] = []
var currentRow: [Tag] = []
var totalWidth: CGFloat = 0
self.tags.forEach{ tag in
totalWidth += (tag.size + 24)
if totalWidth > (screenWidth) {
totalWidth = (!currentRow.isEmpty || rows.isEmpty ? (tag.size + 24) : 0)
rows.append(currentRow)
currentRow.removeAll()
currentRow.append(tag)
} else {
currentRow.append(tag)
}
}
if !currentRow.isEmpty {
rows.append(currentRow)
currentRow.removeAll()
}
return rows
}
}
I added a TagCellView and then used an #ObservedObject for the tag
struct TagChipsView: View {
#ObservedObject var tag: Tag
let verticalPadding: CGFloat = 2.0
let horizontalPadding: CGFloat = 8.0
var body: some View {
Text(tag.name)
}
}
I want when I finish selecting the language and click the Save button it will return the ContentView page and display the language I have selected. And when I click again, it has to checkmark the language I selected before.
I have successfully displayed the data, but I don't know how to save it when I click the Save button
Here is all my code currently
ContentView
struct ContentView: View {
var body: some View {
NavigationView {
HStack {
NavigationLink(destination:LanguageView() ) {
Text("Language")
Spacer()
Text("I want to show the language here ")
}
}
}
}
}
LanguageView
struct LanguageView: View {
var body: some View {
VStack {
CustomLanguageView()
Button(action: {
})
{
Text("Save")
.foregroundColor(.black)
}
.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#State var selectedLanguage: String? = nil
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage)
.padding(.trailing,40)
Rectangle().fill(Color.gray)
.frame( height: 1,alignment: .bottom)
}
.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark")
.resizable()
.frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
There are multiple ways to "Save" something but if you are just trying to get it back to the other view you could do something like this that I quickly setup.
struct ContentView: View {
#State var language: String? = ""
var body: some View {
NavigationView {
HStack {
NavigationLink(destination:LanguageView(language: $language)) {
Text("Language")
.padding()
Spacer()
Text(language!)
.padding()
}
}
}
}
}
struct LanguageView: View {
#Binding var language: String?
#State var selectedLanguage: String? = ""
var body: some View {
VStack {
CustomLanguageView(selectedLanguage: $selectedLanguage)
Button(action: {
language = selectedLanguage
})
{
Text("Save")
.foregroundColor(.black)
}
.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#Binding var selectedLanguage: String?
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage)
.padding(.trailing,40)
Rectangle().fill(Color.gray)
.frame( height: 1,alignment: .bottom)
}
.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark")
.resizable()
.frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
Or if you are actually trying to save it to the device for later use you could use
UserDefaults.standard.setValue(selectedLanguage, forKey: "language")
Then to Retrieve it later do
UserDefaults.standard.value(forKey: "language") as! String
I am attempting to build a multifaceted openweathermap app. My app is designed to prompt the user to input a city name on a WelcomeView, in order to get weather data for that city. After clicking search, the user is redirected to a sheet with destination: DetailView, which displays weather details about that requested city. My goal is to disable dismissal of the sheet in WelcomeView and instead add a navigationlink to the sheet that redirects to the ContentView. The ContentView in turn is set up to display a list of the user's recent searches (also in the form of navigation links).
My issues are the following:
The navigationLink in the WelcomeView sheet does not work. It appears to be disabled. How can I configure the navigationLink to segue to destination: ContentView() ?
After clicking the navigationLink and redirecting to ContentView, I want to ensure that the city name entered in the WelcomeView textfield is rendered as a list item in the ContentView. For that to work, would it be necessary to set up an action in NavigationLink to call viewModel.fetchWeather(for: cityName)?
Here is my code:
WelcomeView
struct WelcomeView: View {
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State private var showingDetail: Bool = false
#State private var linkActive: Bool = true
#State private var acceptedTerms = false
var body: some View {
Section {
HStack {
TextField("Search Weather by City", text: $cityName)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10.0).strokeBorder(Color.gray, style: StrokeStyle(lineWidth: 1.0)))
.padding()
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}
.sheet(isPresented: $showingDetail) {
VStack {
NavigationLink(destination: ContentView()){
Text("Return to Search")
}
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}.interactiveDismissDisabled(!acceptedTerms)
}
}
}.padding()
}
}
}
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
WelcomeView()
}
}
ContentView
let coloredToolbarAppearance = UIToolbarAppearance()
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State var showingDetail = false
init() {
// toolbar attributes
coloredToolbarAppearance.configureWithOpaqueBackground()
coloredToolbarAppearance.backgroundColor = .systemGray5
UIToolbar.appearance().standardAppearance = coloredToolbarAppearance
UIToolbar.appearance().scrollEdgeAppearance = coloredToolbarAppearance
}
var body: some View {
NavigationView {
VStack() {
List () {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°").font(.system(size: 32))
}
}
}.onDelete { index in
self.viewModel.cityNameList.remove(atOffsets: index)
}
}.onAppear() {
viewModel.fetchWeather(for: cityName)
}
}.navigationTitle("Weather")
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
TextField("Enter City Name", text: $cityName)
.frame(minWidth: 100, idealWidth: 150, maxWidth: 240, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DetailView
struct DetailView: View {
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)&units=imperial&appid=<MyAPIKey>") 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.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}
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
}
struct WeatherInfo: Codable {
var description: String = ""
}
DemoApp
#main
struct SwftUIMVVMWeatherDemoApp: App {
var body: some Scene {
WindowGroup {
// ContentView()
WelcomeView()
}
}
}
I am developing MVVM app and I have two views and view model for one of them. One view for forEach cell, and another for forEach. So i want to implement like button in "cell" view, and i have no idea how to do it, because i cant edit data from cellView. Here is my code:
TownsListView.swift
struct TownsListView: View {
#StateObject private var viewModel = TownsViewModel()
var body: some View {
ScrollView() {
ForEach(viewModel.towns) { town in
TownView(name: town.name)
.padding(.bottom)
.frame(width: 300, height: 40, alignment: .center)
.background(Color.yellow)
}
}
.onAppear {
viewModel.getTowns()
}
}
}
TownView.swift
struct TownView : View {
var name: String
#StateObject var viewModel = TownViewModel()
var body : some View {
HStack {
Text(self.name)
Spacer()
Button(action: {/* viewModel.setLike */}) {
Image(systemName: "heart")
}
}
}
}
TownsViewModel.swift
class TownsViewModel : ObservableObject {
#Published var towns = [Town]()
func getTowns() {
for town in townsArray {
towns.append(town)
}
}
}
TownViewModel.swift
class TownViewModel : ObservableObject {
func setLike() {
//Here i want to implement like function
}
}
and Model.swift
struct Town : Identifiable {
var id : String = UUID().uuidString
var name : String
var liked : Bool
}
var paris = Town(name: "Paris", liked: false)
var london = Town(name: "London", liked: false)
var barcelona = Town(name: "Barcelona", liked: false)
var tokyo = Town(name: "Tokyo", liked: false)
let townsArray = [paris,london,barcelona,tokyo]
How to implement like function for toggle liked property of Town?
Here's how I would handle this:
struct TownsListView: View {
#StateObject private var viewModel = TownsViewModel()
var body: some View {
ScrollView() {
ForEach(viewModel.towns) { town in
TownView(town: viewModel.bindingForTown(id: town.id))
.padding(.bottom)
.frame(width: 300, height: 40, alignment: .center)
.background(Color.yellow)
}
}
.onAppear {
viewModel.getTowns()
}
}
}
struct TownView : View {
#Binding var town: Town
var body : some View {
HStack {
Text(town.name)
Spacer()
Button(action: {
town.liked.toggle()
}) {
Image(systemName: "heart")
}.foregroundColor(town.liked ? .red : .black)
}
}
}
class TownsViewModel : ObservableObject {
#Published var towns : [Town] = []
func bindingForTown(id: String) -> Binding<Town> {
.init {
self.towns.first { $0.id == id }!
} set: { newValue in
self.towns = self.towns.map { $0.id == newValue.id ? newValue : $0 }
}
}
func getTowns() {
towns = townsArray
}
}
This uses a custom binding to pass down to the TownView. If you can use Swift 5.5, this gets even easier and you can use ForEach with an automatic binding:
struct TownsListView: View {
#StateObject private var viewModel = TownsViewModel()
var body: some View {
ScrollView() {
ForEach($viewModel.towns) { $town in
TownView(town: $town)
.padding(.bottom)
.frame(width: 300, height: 40, alignment: .center)
.background(Color.yellow)
}
}
.onAppear {
viewModel.getTowns()
}
}
}
You were overthinking this. Your model needs to provide the state of the app, not a function to change the state. Your views take that state and update them. I simply pulled your function stub out of class TownViewModel and added a bool like. In your view, all you need to do is have change the bool in the TownView button and you will change the state and have a record of it.
Model:
class TownViewModel : ObservableObject {
#Published var like: Bool = false
}
View:
struct TownView: View {
var name: String
#StateObject var viewModel = TownViewModel()
var body : some View {
HStack {
Text(self.name)
Spacer()
Button(action: {
viewModel.like.toggle() // Changes the "like" state
}) {
Image(systemName: (viewModel.like ? "heart.fill" : "heart")) // This is a simple
// tertiary operator that takes the current value of the "like" and changes the heart to match.
}
}
}
}