SwiftUI each item displays it own detailView when it is selected in a list - swift

I'm trying to make each item display it own detailView in a list using SwiftUI. But for now, I got stuck because it only display the same detailView for any item. Would anyone know how to do that?
This is the code I have for now:
struct PastryListView: View {
#State private var isShowingDetailView = false
#State private var selectedPastry : Pastry?
#State private var selection: Int? = nil
var body: some View {
ZStack {
NavigationView {
List(MockData.pastries) { Pastry in
HStack {
Image(Pastry.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 180, height: 200)
VStack {
Text(Pastry.name)
.font(Font.custom("DancingScript-Regular", size: 30))
.fontWeight(.medium)
}
.padding(.leading)
}
.onTapGesture {
selectedPastry = Pastry
isShowingDetailView = true
}
}
.navigationTitle("🥐 Pastries")
}
if isShowingDetailView { Pastry2DetailView(isShowingDetailView2: $isShowingDetailView, pastry: MockData.samplePastry2)
}
}
} }

there are many ways to achieve what you want, this is just one way:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
PastryListView()
}
}
}
struct Pastry: Identifiable {
var id: String = UUID().uuidString
var name: String
var image: UIImage
}
struct PastryListView: View {
#State private var pastries: [Pastry] = [
Pastry(name: "pastry1", image: UIImage(systemName: "globe")!),
Pastry(name: "pastry2", image: UIImage(systemName: "info")!)]
var body: some View {
NavigationView {
List(pastries) { pastry in
NavigationLink(destination: PastryDetailView(pastry: pastry)) {
HStack {
Image(uiImage: pastry.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 80)
VStack {
Text(pastry.name)
.font(Font.custom("DancingScript-Regular", size: 30))
.fontWeight(.medium)
}
.padding(.leading)
}
}
}.navigationTitle("🥐 Pastries")
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct PastryDetailView: View {
#State var pastry: Pastry
var body: some View {
Text("🥐🥐🥐🥐 " + pastry.name + " 🥐🥐🥐🥐")
}
}

Related

Having trouble with showing another view

I'm currently trying to input another listview in my contentView file to test if it'll show, but for some reason it isn't showing the list. I'm having a bit of trouble understanding why this is happening as I am not receiving any error message.
This is the code for the list file
import SwiftUI
extension Image{
func anotherImgModifier() -> some View{
self
.resizable()
.scaledToFill()
.frame( width: 75, height: 75)
.cornerRadius(9)
}
}
struct PokeListView: View {
#State var imgURL: String = ""
#EnvironmentObject var pokeWebService: PokeWebService
//functions
// func loadImage() async -> [Image]{
// for
// }
var body: some View {
NavigationView {
List( pokeWebService.pokeList?.results ?? [], id: \.id){ pokemon in
NavigationLink(destination: PokeDetailsView(urlString: pokemon.url, counter: 4, name: pokemon.name)) {
AsyncImage(url:URL(string: "https://play.pokemonshowdown.com/sprites/bw/\(pokemon.name).png")){ image in
image.anotherImgModifier()
}
placeholder: {
Image(systemName: "photo.circle.fill").iconModifer()
}.padding(40)
Text(pokemon.name.uppercased()).font(.system(size: 15, weight: .heavy, design: .rounded))
.foregroundColor(.gray)
.task{
do{
try await pokeWebService.getPokemonFromPokemonList(from: pokemon.url)
} catch{
print("---> task error: \(error)")
}
}
}
}
}
.task {
do{
try await pokeWebService.getPokemonList()
} catch{
print("---> task error: \(error)")
}
}
}
}
struct PokeListView_Previews: PreviewProvider {
static var previews: some View {
PokeListView()
.previewLayout(.sizeThatFits)
.padding()
.environmentObject(PokeWebService())
}
}
This is the code for the ContentView where I was trying to input the list file.
import SwiftUI
struct ContentView: View {
#StateObject var newsWebService = NewsWebService()
#StateObject var pokeWebService = PokeWebService()
let gbImg = Image("pokeball").resizable()
#State private var gridLayout: [GridItem] = [ GridItem(.flexible()), GridItem(.flexible())]
#State private var gridColumn: Int = 2
#State var selection: Int? = nil
var body: some View {
NavigationView{
ScrollView(.vertical, showsIndicators: false, content: {
VStack(alignment: .center, spacing: 15, content: {
Spacer()
NewsCapsule()
//GRID
//BERRIES, POKEMON, GAMES
GroupBox(label: Label{
Text("PokéStuff")
} icon: {
Image("pokeball").resizable().scaledToFit().frame(width: 30, height: 30, alignment: .leading)
}
, content: {
PokeListView()
}).padding(.horizontal, 20).foregroundColor(.red)
})//:VSTACK
})//:SCROLLVIEW
.navigationBarTitle("Pokemon",displayMode: .large)
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing, content: {
Image(systemName: "moon.circle")
.resizable()
.scaledToFit()
.font(.title2)
.foregroundColor(.red)
})
})
}//:NAVIGATIONBAR
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(NewsWebService())
.environmentObject(PokeWebService())
}
}
How would I get to fix this?
EDIT-1:
with further tests, this is what worked for me:
in ContentView, add .frame(height: 666) to the VStack {...}.
This is the reason why you do not see anything. You need a frame height.
Also in ContentView, add .environmentObject(pokeWebService) to the NavigationView,
and just use PokeListView(). This is to pass the pokeWebService
to that view. After that, all works for me. You may want to experiment
with different frame sizes and such likes. You should also remove the NavigationView from your PokeListView, there is no need for it.

Best way to trigger a button events using #State and #Binding

Essentially I would like to trigger the MainButton in ContentView to Open SheetView and then use NavCloseButton to Close the SheetView to Return Back to ContentView. I've been trying to do this using #State and #Binding. While getting SheetView presented using .sheet(isPresented: is simple I'm having trouble dismissing it when the buttons are extracted out.
Can someone please show example how these actions would be performed?
ContentView:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Spacer()
Image(systemName: "hand.thumbsup.circle.fill")
.resizable()
.frame(width: 200, height: 200)
.symbolRenderingMode(.hierarchical)
Spacer()
MainButton(color: .blue, title: "Tap to Open", image: "lock.open.laptopcomputer")
}
.navigationTitle("Page One")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}
SheetView:
struct SheetView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
}
.navigationTitle("Sheet View")
.navigationBarItems(trailing: NavCloseButton(color: .red,
title: "Close",
image: ""))
}
}
}
struct SheetView_Previews: PreviewProvider {
static var previews: some View {
SheetView()
.preferredColorScheme(.dark)
}
}
AppButtons:
struct NavCloseButton: View {
var color: Color
var title: String
var image: String
var body: some View {
Button {
print("Closing")
} label: {
Text(title)
.padding()
.frame(width: 100, height: 40)
.background(color)
.foregroundColor(.white)
.cornerRadius(10)
.font(.system(.body))
}
}
}
struct MainButton: View {
var color: Color
var title: String
var image: String
var body: some View {
Button {
print("Opened")
} label: {
Label {
Text(title)
} icon: {
Image(systemName: image)
}
.padding()
.frame(width: 350, height: 60)
.background(color)
.foregroundColor(.white)
.cornerRadius(15)
.font(.title2)
}
}
}
Following your code this is how you would do it.
Changes are commented.
struct ContentView: View {
#State private var showSheet = false // define state
var body: some View {
NavigationView {
VStack {
Spacer()
Image(systemName: "hand.thumbsup.circle.fill")
.resizable()
.frame(width: 200, height: 200)
.symbolRenderingMode(.hierarchical)
Spacer()
MainButton(color: .blue, title: "Tap to Open", image: "lock.open.laptopcomputer", showSheet: $showSheet) // pass binding
}
.navigationTitle("Page One")
.sheet(isPresented: $showSheet) { // present sheet
SheetView(showSheet: $showSheet) // pass binding
}
}
}
}
struct SheetView: View {
#Binding var showSheet: Bool // pass binding here
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
}
.navigationTitle("Sheet View")
.navigationBarItems(
trailing:
NavCloseButton(color: .red,
title: "Close",
image: "",
showSheet: $showSheet)) // pass binding
}
}
}
struct NavCloseButton: View {
var color: Color
var title: String
var image: String
#Binding var showSheet: Bool // pass binding here
var body: some View {
Button {
print("Closing")
showSheet = false // set binding to dismiss sheet
} label: {
Text(title)
.padding()
.frame(width: 100, height: 40)
.background(color)
.foregroundColor(.white)
.cornerRadius(10)
.font(.system(.body))
}
}
}
struct MainButton: View {
var color: Color
var title: String
var image: String
#Binding var showSheet: Bool // pass binding here
var body: some View {
Button {
print("Opened")
showSheet = true // set binding to show sheet
} label: {
Label {
Text(title)
} icon: {
Image(systemName: image)
}
.padding()
.frame(width: 350, height: 60)
.background(color)
.foregroundColor(.white)
.cornerRadius(15)
.font(.title2)
}
}
}
But it might be easier to change the buttons to vars or define custom Button styles.
Here is another variant with one CutomButton that is taking a button action as a closure, reducing the Binding passing around:
struct ContentView: View {
#State private var showSheet = false // define state
var body: some View {
NavigationView {
VStack {
Spacer()
Image(systemName: "hand.thumbsup.circle.fill")
.resizable()
.frame(width: 200, height: 200)
.symbolRenderingMode(.hierarchical)
Spacer()
CustomButton(color: .blue, title: "Tap to Open", image: "lock.open.laptopcomputer") {
showSheet = true // passed button action
}
.font(.title2)
}
.navigationTitle("Page One")
.sheet(isPresented: $showSheet) { // present sheet
SheetView(showSheet: $showSheet) // pass binding
}
}
}
}
struct SheetView: View {
#Binding var showSheet: Bool // pass binding here
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
}
.navigationTitle("Sheet View")
.navigationBarItems(
trailing:
CustomButton(color: .red, title: "Close") {
showSheet = false // passed button action
}
)
}
}
}
struct CustomButton: View {
let color: Color
let title: String
var image: String? = nil
let action: () -> Void // button action (as trailing closure)
var body: some View {
Button {
action() // run the passed action
} label: {
Group {
if let image {
Label(title, systemImage: image)
} else {
Text(title)
}
}
.padding()
.background(color)
.foregroundColor(.white)
.cornerRadius(15)
}
}
}

#State variable in onTapGesture not getting updated

with the code below I was expecting when the image in VStack was tapped, it shows another image in the full screen cover but the imageName variable does not seem to get set to jugg as in the new full screen it has only a gray background
struct TestView: View {
#State var imageName = ""
#State var showFullscreen = false
var body: some View {
VStack {
Image("drow")
.resizable()
.scaledToFit()
.frame(width: 100)
.onTapGesture {
self.imageName = "jugg"
self.showFullscreen = true
}
}
.fullScreenCover(isPresented: $showFullscreen) {
ZStack {
Color.gray.ignoresSafeArea()
Image(imageName)
.resizable()
.scaledToFit()
.frame(width: 380)
}
}
}
}
as mentioned in the comments, use the .fullScreenCover(item: ..) version
of the fullScreenCover, such as:
struct ImageName: Identifiable {
let id = UUID()
var name = ""
}
struct TestView: View {
#State var imageName: ImageName?
var body: some View {
VStack {
Image("drow").resizable().scaledToFit().frame(width: 100)
.onTapGesture {
imageName = ImageName(name: "drow")
}
}
.fullScreenCover(item: $imageName) { img in
ZStack {
Color.gray.ignoresSafeArea()
Image(img.name).resizable().scaledToFit().frame(width: 380)
}
}
}
}
Seems to only work if you create a separate SwiftUI view and pass in the imageName as a #Binding variable
struct TestView: View {
#State var imageName = ""
#State var showFullscreen = false
var body: some View {
VStack {
Image("drow")
.resizable()
.scaledToFit()
.frame(width: 100)
.onTapGesture {
imageName = "jugg"
showFullscreen = true
}
}
.fullScreenCover(isPresented: $showFullscreen) {
CoverView(imageName: $imageName)
}
}
}
struct CoverView: View {
#Binding var imageName: String
var body: some View {
ZStack {
Color.gray.ignoresSafeArea()
Image(imageName)
.resizable()
.scaledToFit()
.frame(width: 380)
}
}
}

SwiftUI - Display selected value with Single Selected

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

How to setup NavigationLink in SwiftUI sheet to redirect to new view

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()
}
}
}