Swift/SwiftUI: Using AppStorage to hold list of IDs - swift

I am trying to use AppStorage to hold a list of id's for my Country struct. I want to persist this list of id's as favorites for a user and if it's a favorite, have the heart next to each country filled.
If they tap the heart to unfavorite it, it will remove the id from the favLists and if they tap it to favorite it, it will add it to the list.
This is not working as I expect and I am not sure what I am doing wrong here.
struct Number: Identifiable, Codable {
var id: Int
}
struct ContentView: View {
#Binding var countries: [Country]
#Namespace var namespace;
#AppStorage("favLists") var favLists: [Number] = [];
var body: some View {
GeometryReader { bounds in
NavigationView {
ScrollView {
ForEach(Array(filteredCountries.enumerated()), id: \.1.id) { (index,country) in
LazyVStack {
ZStack(alignment: .bottom) {
HStack {
NavigationLink(
destination: CountryView(country: country),
label: {
HStack {
Image(country.image)
.resizable()
.frame(width: 50, height: 50)
Text(country.display_name)
.foregroundColor(Color.black)
.padding(.leading)
Spacer()
}
.padding(.top, 12.0)
}
).buttonStyle(FlatLinkStyle())
if (favLists.filter{$0.id == country.id}.count > 0) {
Image(systemName: "heart.fill").foregroundColor(.red).onTapGesture {
let index = favLists.firstIndex{ $0.id == country.id}
if let index = index {
favLists.remove(at: index)
}
}
.padding(.top, 12)
} else {
Image(systemName: "heart").foregroundColor(.red).onTapGesture {
favLists.append(Number(id: country.id))
}
.padding(.top, 12)
}
}
.padding(.horizontal, 16.0)
}
}
}
}
.frame(maxWidth: bounds.size.width)
.navigationTitle("Countries")
.font(Font.custom("Avenir-Book", size: 28))
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}

Related

Can't solve scroll view in SwiftUI

Could someone please explain to me how the ScrollView works, how can I make that when I receive a message it scrolls to the last message ( item )?
I tried to make it with a, ScrollViewReader { proxy in } but I still can't make this code work.
I tried also to make it ' .onchange(of: ) { } ' (with ScrollViewReader)
struct ContentView: View{
#ObservedObject var viewModel = ViewModel()
#State var text = ""
#State var models = [String]()
var body: some View {
VStack(alignment: .leading) {
ScrollViewReader { proxy in
ScrollView (showsIndicators: false){
ForEach(models, id: \.self){ string in
Text(string)
.font(.headline)
.padding()
.background(Color("Text"))
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(.blue, style: .init(lineWidth: 1))
)
Divider()
}
.onChange(of: models){
proxy.scrollTo(models.last)
}
}
}
Spacer()
HStack{
TextField("Type here...", text: $text)
.padding(15.0)
.padding(.horizontal)
Button(){
send()
} label: {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 40))
.foregroundColor(Color("Button"))
}
.padding(10.0)
}
.overlay(
RoundedRectangle(cornerRadius: 100)
.stroke(Color("Stroke"), lineWidth: 1)
)
}
.onAppear{
viewModel.setup()
}
.padding()
.background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color("Background")/*#END_MENU_TOKEN#*/)
}
func send() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
models.append("Me: \(self.text)")
viewModel.send(text: text) { response in
DispatchQueue.main.async {
self.models.append("Answer: "+response)
self.text = ""
}
}
}
}
..............................
use a different overload
proxy.scrollTo(models.last,anchor:.top)

Utilize buttons as pickers to pick between map views

So I want to be able to utilize a picker so that I can choose between different views, but the only option that I found was a picker, which outputs the following output:
I want to have a custom solution where I can have three buttons, and choose between those similar to how a picker is chosen, here is the look that I'm attempting to achieve:
The code below renders the buttons perfectly, but the Picker don't render the contents of the button nor the modifiers, how would I be able to achieve clicking on each individual button similar to how a picker is selected?
The Picker also have a .tag(0), .tag(1) that can be utilized, how would this work on buttons?
Here is the code:
struct MapDisplaySheetView: View {
#ObservedObject var mapSettings = MapSettings()
#State var mapType = 0
#State var allItems: [String] = [
"Standard",
"Hybrid",
"Image",
]
var body: some View {
VStack(spacing: 0) {
// MARK: Map Type
HStack {
Picker(
"Map Type",
selection: $mapType,
content: {
ForEach(allItems, id: \.self) { item in
VStack {
HStack {
VStack {
Text(item)
.font(.footnote.weight(.bold))
.frame(maxWidth: .infinity, alignment: .leading)
.clipped()
.foregroundColor(.primary)
}
}
.padding(8)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
}
.border(.red)
.background {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.foregroundColor(Color(.systemFill))
}
.foregroundColor(Color(.quaternaryLabel))
.cornerRadius(8)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
}
})
.pickerStyle(SegmentedPickerStyle())
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.clipped()
.padding(.bottom, 4)
}
.padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
}
}
Here is what I attempted, but it's not working, I am not getting a log.info() output when clicking buttons:
struct MapDisplaySheetView: View {
#ObservedObject var mapSettings = MapSettings()
#State var mapType = 0
#State var allItems: [String] = [
"Standard",
"Hybrid",
"Image",
]
var body: some View {
VStack(spacing: 0) {
HStack {
ForEach(allItems, id: \.self) { item in
VStack {
HStack {
VStack {
Button(action: {
// Action
}, label: {
Text(item)
})
.tag(mapSettings.mapType)
.onChange(of: mapType) { newValue in
mapSettings.mapType = newValue
log.info("We have selected \(newValue)")
}
}
}
}
}
}
}
}
}
you could try something like this:
struct MapDisplaySheetView: View {
#ObservedObject var mapSettings = MapSettings()
#State var allItems: [String] = ["Standard", "Hybrid", "Image"]
var body: some View {
HStack {
ForEach(allItems, id: \.self) { item in
Button(action: {
mapSettings.mapType = item
print("We have selected \(item)")
}, label: {
Text(item)
})
.buttonStyle(.bordered)
}
}
}
}
EDIT-1:
if the mapType is an Int, then use:
struct MapDisplaySheetView: View {
#ObservedObject var mapSettings = MapSettings()
#State var allItems: [String] = ["Standard", "Hybrid", "Image"]
var body: some View {
HStack {
ForEach(allItems, id: \.self) { item in
Button(action: {
switch item {
case "Standard": mapSettings.mapType = 0
case "Hybrid": mapSettings.mapType = 1
case "Image": mapSettings.mapType = 2
default: mapSettings.mapType = 0
}
print("We have selected \(item) mapSettings: \(mapSettings.mapType) ")
}, label: {
Text(item)
})
.buttonStyle(.bordered)
}
}
}
}

How to save the answers of a specific user to his profile in the questionnaire app (SwiftUI)?

I am creating an app for parents or guardians of autistic children. In the app, you can create profiles for several kids and then click on the "Start Questionnaire" button in the personal card of each child. When the questionnaire page opens, there the parent answers several questions in several categories, where the questions from each category have a different weight (1, 2, 3, 4). At the end of the questionnaire, these answers are recorded in the profile of a particular kid in order to display a graph of his progress based on the answers on his personal page.
My question is that I can't figure out how to save answers to a specific kid's profile. To store profiles of children, I use CoreData, where there are four attributes: name, birthdate, dateCreated and answers. I assigned the Binary Data type to the latter. I created a #State variable in the QuestionnairePageView file and specified its type as [[String:Int]] to store a question category as a String (1, 2, 3 or 4) and an answer as an Int (yes = 1 or no = 0). Also in that file I created a saveAnswers function that assigns questionnaire answers to that #State variable when the user taps End Questionnaire button.
However, it didn't work. Please help me to understand the issue.
My data manager:
import SwiftUI
import CoreData
class CoreDataManager {
let container: NSPersistentContainer
static let shared: CoreDataManager = CoreDataManager()
private init() {
container = NSPersistentContainer(name: "PersonModel")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Unable to initialize Core Data \(error)")
}
}
}
}
My QuestinarrieApp file:
import SwiftUI
#main
struct QuestinarrieApp: App {
let controller = CoreDataManager.shared.container
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, controller.viewContext)
}
}
}
My ContentView:
import SwiftUI
import CoreData
struct ContentView: View {
#State private var isShowingAddPersonPage: Bool = false
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(fetchRequest: Person.ownFetchRequest)
var persons: FetchedResults<Person>
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10) {
ForEach(persons) { person in
let age = "\(calculateAge(fromDate: person.birthdate ?? Date()))"
NavigationLink {
PersonalPageView(person: person, name: person.name ?? "Noname", age: age)
} label: {
HStack {
Text(person.name ?? "Noname").font(.title2.bold())
Spacer()
Text(age + " years old").font(.body.bold())
}
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
.padding(.horizontal)
}
}
}
}
.navigationTitle("Persons")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isShowingAddPersonPage = true
}) {
Image(systemName: "plus.circle")
} //: BUTTON
.sheet(isPresented: $isShowingAddPersonPage) {
AddPersonView()
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let container = CoreDataManager.shared.container
ContentView()
.environment(\.managedObjectContext, container.viewContext)
}
}
My AddPersonView file:
import SwiftUI
struct AddPersonView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) private var viewContext
#State private var name: String = ""
#State private var birthdate: Date = Date()
var body: some View {
NavigationView {
VStack(spacing: 10) {
// MARK: - NAME
HStack(spacing: 12) {
Image(systemName: "person.text.rectangle")
.resizable()
.renderingMode(.template)
.scaledToFit()
.frame(width: 25, height: 25)
TextField("Name", text: $name)
} //: HSTACK
.padding(.horizontal)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
// MARK: - BIRTH DATE
HStack(spacing: 12) {
Image(systemName: "calendar")
.resizable()
.renderingMode(.template)
.scaledToFit()
.frame(width: 25, height: 25)
DatePicker(
"Birth date",
selection: $birthdate,
in: ...Date(),
displayedComponents: .date
)
.accentColor(.black)
} //: HSTACK
.padding(.horizontal)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
Spacer()
// MARK: - SAVE BUTTON
Button {
if name != "" {
savePerson()
presentationMode.wrappedValue.dismiss()
}
} label: {
Text("Save")
.font(.body.bold())
.frame(maxWidth: .infinity)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
} //: BUTTON
}
.navigationTitle("Add Person")
.padding()
}
}
private func savePerson() {
let newPerson = Person(context: viewContext)
do {
newPerson.name = name
newPerson.birthdate = birthdate
newPerson.dateCreated = Date()
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
struct AddPersonView_Previews: PreviewProvider {
static var previews: some View {
let container = CoreDataManager.shared.container
AddPersonView()
.environment(\.managedObjectContext, container.viewContext)
}
}
My PersonalPageView:
import SwiftUI
struct PersonalPageView: View {
#Environment(\.managedObjectContext) private var viewContext
var person: Person
var name: String
var age: String
var body: some View {
VStack(spacing: 20) {
HStack {
Text(name).font(.largeTitle.bold())
Spacer()
Text(age + " years old").font(.title2.bold()).foregroundColor(.secondary)
}
RoundedRectangle(cornerRadius: 10)
.fill(.ultraThinMaterial)
.frame(height: 300)
VStack {
ForEach(person.answers) { answer in // <- This ForEach doesn't work for now
Text("\(answer)")
}
}
Spacer()
NavigationLink {
QuestinarriePageView(person: person, name: "Jack")
} label: {
Text("Start Questinarrie")
.font(.title2.bold())
.frame(maxWidth: .infinity)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
}
}
.navigationBarTitleDisplayMode(.inline)
.padding()
}
}
struct PersonalPageView_Previews: PreviewProvider {
static var previews: some View {
PersonalPageView(person: Person(), name: "Igor", age: "28")
}
}
And finally QuestinarriePageView:
import SwiftUI
struct QuestinarriePageView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var answers: [[String:Int]] = [[:]]
var person: Person
var name: String
var body: some View {
VStack(spacing: 40) {
HStack {
Spacer()
Button {} label: {
Text("Quit").font(.title2)
}
.tint(.black)
}
VStack {
Text("Does \(name) like to play with other kids?")
HStack(spacing: 100) {
Button {
answers += [["1":1]]
} label: {
Text("Yes")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
Button {
answers += [["1":0]]
} label: {
Text("No")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
}
}
VStack {
Text("Does \(name) like to speak?")
HStack(spacing: 100) {
Button {
answers += [["2":1]]
} label: {
Text("Yes")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
Button {
answers += [["2":0]]
} label: {
Text("No")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
}
}
VStack {
Text("Does \(name) like to play dance?")
HStack(spacing: 100) {
Button {
answers += [["3":1]]
} label: {
Text("Yes")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
Button {
answers += [["3":0]]
} label: {
Text("No")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
}
}
Spacer()
Button {
saveAnswers()
} label: {
Text("End Questionnaire")
.font(.body.bold())
.frame(maxWidth: .infinity)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
}
}
.toolbar(.hidden)
.padding()
}
private func saveAnswers() {
let thisPerson = person
do {
thisPerson.answers = answers as? Data
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
struct QuestinarriePageView_Previews: PreviewProvider {
static var previews: some View {
QuestinarriePageView(person: Person(), name: "Igor")
}
}
Also I have a file with some helpers:
import SwiftUI
import CoreData
func calculateAge(fromDate: Date) -> Int {
let now = Date()
let calendar = Calendar.current
let ageComponents = calendar.dateComponents([.year], from: fromDate, to: now)
let age = ageComponents.year!
return age
}
extension Person {
static var ownFetchRequest: NSFetchRequest<Person> {
let request: NSFetchRequest<Person> = Person.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "dateCreated", ascending: false)]
return request
}
}
I've been struggling with this problem for a month now. I hope this is solvable🙏

How do I fix buttons not being tappable in a ForEach loop?

I have a view which contains a button. The view with the button is created from a forEach loop. For some reason only some buttons are tappable and others are not.
The parent view contains a NavigationView, a scroll view inside the NavigationView, a lazyVStack inside of the scroll view, a forEachloop in that lazyVStack and in that for loop is the child view that contains the button.
struct ContentView: View {
let peoples:[Person] = Bundle.main.decode("data.json")
var body: some View {
let columns = [
GridItem(.flexible(minimum: 300), spacing: 10)
]
NavigationView {
ScrollView(.vertical) {
LazyVStack {
ForEach(peoples, id: \.self) { person in
PersonView(name: person.Name, age: person.Age)
}
}
.navigationTitle("A list of people")
.navigationViewStyle(DefaultNavigationViewStyle())
.padding()
}
}
}
}
The child view is bellow. I suspect the scroll view is stealing the user input, but I am not sure why or how to overcome it. Some buttons are tapeable and some are not.
struct PersonView: View {
#Environment(\.colorScheme) var colorScheme
var name: String
var age: Int
var body: some View {
VStack(alignment:.leading) {
Image("randoPerson")
.resizable()
.scaledToFill()
.frame(minWidth: nil, idealWidth: nil,
maxWidth: UIScreen.main.bounds.width, minHeight: nil,
idealHeight: nil, maxHeight: 300, alignment: .center)
.clipped()
VStack(alignment: .leading, spacing: 6) {
Text("name")
.fontWeight(.heavy)
.padding(.leading)
Text("Age \(age)")
.foregroundColor(Color.gray)
.padding([.leading,.bottom])
Button(action: { print("I was tapped") }) {
HStack {
Image(systemName: "message.fill")
.font(.title)
.foregroundColor(.white)
.padding(.leading)
Text("Message them")
.font(.subheadline)
.foregroundColor(.white)
.padding()
}
.background(Color.blue)
}
.padding()
}
.background(Color(UIColor.systemBackground).cornerRadius(15))
.shadow(color:colorScheme == .dark
? Color.white.opacity(0.2)
: Color.black.opacity(0.2),
radius: 7, x: 0, y: 2)
}
}
}
To fix the issue, you can add an id: UUID associated to a Person and iterate between the Person, inside the ForEach using their ID.
You will also noticed that I added the value as lowercases to respect the Swift convention.
struct Person {
let id: UUID // Add this value
var name: String
var age: Int
}
So here is the ContentView with the id replacing \.self:
struct ContentView: View {
let peoples: [Person] = Bundle.main.decode("data.json")
var body: some View {
NavigationView {
ScrollView(.vertical) {
LazyVStack {
ForEach(peoples, id: \.id) { person in // id replace \.self here
PersonView(name: person.name, age: person.age) // removed uppercase
}
}
.navigationTitle("A list of people")
.navigationViewStyle(DefaultNavigationViewStyle())
.padding()
}
}
}
}

How to Change Item's Order in a List View?

I want to allow users to change the order of photos in the list. Tinder has something like that. I'm trying to add a drag gesture to the item but nothing happened.
import SwiftUI
struct Row: Identifiable {
let id = UUID()
let cells: [Cell]
}
struct Cell: Identifiable {
let id = UUID()
let imageURL: String
#State var offset = CGSize.zero
}
struct PhotosView: View {
#State var rows = [Row(cells: [Cell(imageURL: "gigi"), Cell(imageURL: "hadid"), Cell(imageURL: "gigihadid")]), Row(cells: [Cell(imageURL: "gigi"), Cell(imageURL: "hadid")])]
var body: some View {
List {
ForEach(rows) { row in
HStack(alignment: .center, spacing:15) {
ForEach(row.cells) { cell in
Image(cell.imageURL)
.resizable()
.scaledToFill()
.frame(maxWidth: UIScreen.main.bounds.width / 3.6)
.clipped()
.cornerRadius(10)
.offset(x: cell.offset.width, y: cell.offset.height)
.gesture(
DragGesture()
.onChanged { gesture in
cell.offset = gesture.translation
}
.onEnded { _ in
if abs(cell.offset.width) > 100 {
// remove the card
} else {
cell.offset = .zero
}
}
)
}
if row.cells.count < 3 {
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color(UIColor.systemGray6))
Button(action: {}, label: {
Image(systemName: "person.crop.circle.fill.badge.plus")
.font(.title)
.foregroundColor(Color(UIColor.systemGray3))
})
}
}
}.padding(.vertical)
}.buttonStyle(PlainButtonStyle())
}.padding(.top, UIScreen.main.bounds.height / 8)
.navigationBarHidden(true)
}
}
What view looks like:
I only want users can change the order of photos via drag gesture.