Utilize buttons as pickers to pick between map views - swift

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

Related

Keeping text on TextEditor element while switching views SwiftUI

Well, I'm working on a little app where I have a TextEditor element to type whatever we want. The case is, I want to keep the text on the TextEditor while switching other views, but I can't.
TextEditor before switching the view :
TextEditor after switching the view :
The code is the next one:
struct VistaDatos: View {
#State private var opinion: String = ""
var progreso : Double {
Double(opinion.count)
}
var body: some View {
VStack{
//SOME CODE HERE ...
HStack{
Text("Mi opinión...")
.font(.headline)
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.headline)
}
VStack{
TextEditor(text: $opinion)
.background(.green)
.frame(width: 350, height: 250)
.background().colorMultiply(.green)
.overlay(Rectangle().stroke(Color.black, lineWidth:2))
.disableAutocorrection(true)
.onChange(of: self.opinion) { value in
if Int(opinion.count) > 150 {
self.opinion = String(value.prefix(150))
}
}
Text("Número de palabras: \(Int(progreso))/150").foregroundColor(Int(progreso) >= 100 ? .red : .white)
ProgressView(,value: progreso, total: 150) {
}.frame(width: 350, alignment: .center)
}
}.background(Color.green)
Spacer()
}
}
I have to use .onDisappear event to make it work (it seems to be on the first level stack ), but it isn't working...
How can I make it work?
Thanks in advance.
Since you say you have multiple views, that I assume may need the opinion text,
try this example code. It keeps your text in a ObservableObject,
that you can use throughout your app.
For you to do, is to code the save and retrieve
from wherever you want. In this example it is using the UserDefaults.
class StoreService: ObservableObject {
// your text
#Published var opinion = ""
// ... todo code to store your data when you are finished
func save() {
// save your data
UserDefaults.standard.set(opinion, forKey: "opinion")
}
// ... todo code to retrieve your data when the app starts again
init() {
// get your data
opinion = UserDefaults.standard.string(forKey: "opinion") ?? ""
}
}
struct ContentView: View {
#StateObject var store = StoreService() // <-- here
var body: some View {
NavigationStack {
VStack (spacing: 50) {
Text("\(store.opinion.count) characters typed")
NavigationLink("go to VistaDatos", value: "editor")
.navigationDestination(for: String.self) { str in
VistaDatos()
}
}
}.environmentObject(store) // <-- here
}
}
struct VistaDatos: View {
#EnvironmentObject var store: StoreService // <-- here
var progreso : Double {
Double(store.opinion.count)
}
var body: some View {
VStack{
//SOME CODE HERE ...
HStack{
Text("Mi opinión...").font(.headline)
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.headline)
}
VStack{
TextEditor(text: $store.opinion) // <-- here
.background(.green)
.frame(width: 350, height: 250)
.background().colorMultiply(.green)
.overlay(Rectangle().stroke(Color.black, lineWidth:2))
.disableAutocorrection(true)
.onChange(of: store.opinion) { value in
if store.opinion.count > 150 {
store.opinion = String(value.prefix(150))
}
}
Text("Número de palabras: \(Int(progreso))/150").foregroundColor(Int(progreso) >= 100 ? .red : .white)
ProgressView(value: progreso, total: 150).frame(width: 350, alignment: .center)
Button("Save me") { // <-- here
store.save()
}
}
}.background(Color.green)
Spacer()
}
}
Alternatively, you could use the simple #AppStorage, like this:
struct ContentView: View {
#AppStorage("opinion") var opinion = "" // <-- here
var body: some View {
NavigationStack {
VStack (spacing: 50) {
Text("\(opinion.count) characters typed")
NavigationLink("go to VistaDatos", value: "editor")
.navigationDestination(for: String.self) { str in
VistaDatos()
}
}
}
}
}
struct VistaDatos: View {
#AppStorage("opinion") var opinion = "" // <-- here
var progreso : Double {
Double(opinion.count)
}
var body: some View {
VStack{
//SOME CODE HERE ...
HStack{
Text("Mi opinión...").font(.headline)
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.headline)
}
VStack{
TextEditor(text: $opinion) // <-- here
.background(.green)
.frame(width: 350, height: 250)
.background().colorMultiply(.green)
.overlay(Rectangle().stroke(Color.black, lineWidth:2))
.disableAutocorrection(true)
.onChange(of: opinion) { value in
if opinion.count > 150 {
opinion = String(value.prefix(150))
}
}
Text("Número de palabras: \(Int(progreso))/150").foregroundColor(Int(progreso) >= 100 ? .red : .white)
ProgressView(value: progreso, total: 150).frame(width: 350, alignment: .center)
}
}.background(Color.green)
Spacer()
}
}

Swift/SwiftUI: Using AppStorage to hold list of IDs

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

SwiftUI Textfield not actually making updates to data

I am trying to make an application where the user can see a list of foods and recipes and make changes etc. In the DetailView for each food, there is an EditView where the values can be changed. I am trying to use a double binding on the value by using #State to define the value and $food.name for example to make the changes happen.
When I click 'Done' and exit this view back to the DetailView, the changes are not made at all, leaving me confused?
Any help on this problem would be greatly appreciated, thank you :)
My EditView.swift:
import SwiftUI
struct EditView: View {
#State var food: Food
var body: some View {
List {
Section(header: Text("Name")) {
TextField("Name", text: $food.name)
}
Section(header: Text("Image")) {
TextField("Image", text: $food.image)
}
Section(header: Text("Desc")) {
TextField("Desc", text: $food.desc)
}
Section(header: Text("Story")) {
TextField("Story", text: $food.story)
}
}.listStyle(InsetGroupedListStyle())
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(food: cottonCandy)
}
}
My FoodDetail.swift:
import SwiftUI
struct FoodDetail: View {
#State var food: Food
#State private var isPresented = false
var body: some View {
VStack {
Image(food.image)
.resizable()
.frame(width: 300.0,height:300.0)
.aspectRatio(contentMode: .fill)
.shadow(radius: 6)
.padding(.bottom)
ScrollView {
VStack(alignment: .leading) {
Text(food.name)
.font(.largeTitle)
.fontWeight(.bold)
.padding(.leading)
.multilineTextAlignment(.leading)
Text(food.desc)
.italic()
.fontWeight(.ultraLight)
.padding(.horizontal)
.multilineTextAlignment(.leading)
Text(food.story)
.padding(.horizontal)
.padding(.top)
Text("Ingredients")
.bold()
.padding(.horizontal)
.padding(.vertical)
ForEach(food.ingredients, id: \.self) { ingredient in
Text(ingredient)
Divider()
}.padding(.horizontal)
Text("Recipe")
.bold()
.padding(.horizontal)
.padding(.vertical)
ForEach(food.recipe, id: \.self) { step in
Text(step)
Divider()
}.padding(.horizontal)
}.frame(maxWidth: .infinity)
}.frame(minWidth: 0,
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .center
)
}
.navigationBarItems(trailing: Button("Edit") {
isPresented = true
})
.fullScreenCover(isPresented: $isPresented) {
NavigationView {
EditView(food: food)
.navigationTitle(food.name)
.navigationBarItems(leading: Button("Cancel") {
isPresented = false
}, trailing: Button("Done") {
isPresented = false
})
}
}
}
}
struct FoodDetail_Previews: PreviewProvider {
static var previews: some View {
Group {
FoodDetail(food: cottonCandy)
}
}
}
Inside EditView, you want to have a Binding. Replace
#State var food: Food
with
#Binding var food: Food
... then, you'll need to pass it in:
.fullScreenCover(isPresented: $isPresented) {
NavigationView {
EditView(food: $food) /// make sure to have dollar sign
.navigationTitle(food.name)
.navigationBarItems(leading: Button("Cancel") {
isPresented = false
}, trailing: Button("Done") {
isPresented = false
})
}
}

List Sections do not work correctly if .navigationBarItems is present

How to have both a button in navigation bar and a list with sections?
Here is a code with .navigationBarItems:
struct AllMatchesView: View {
#Environment(\.managedObjectContext) var moc
#State var events = EventData()
var body: some View {
NavigationView {
List{
ForEach(events.sections) { section in
Section(header: Text(section.title)) {
ForEach(section.matches) { match in
Text("Row")
}
}
.navigationBarTitle("Title")
.navigationBarItems(trailing:
NavigationLink(destination: AddMatchView().environment(\.managedObjectContext, self.moc)) {
Image(systemName: "plus")
.resizable()
.frame(width: 22, height: 22)
.padding(.horizontal)
}
.padding(.leading)
)
}
}.onAppear(){
self.events = EventData()
}
}
}
}
Without .navigationBarItems:
struct AllMatchesView: View {
#Environment(\.managedObjectContext) var moc
#State var events = EventData()
var body: some View {
NavigationView {
List{
ForEach(events.sections) { section in
Section(header: Text(section.title)) {
ForEach(section.matches) { match in
Text("Row")
}
}
.navigationBarTitle("Title")
}
}.onAppear(){
self.events = EventData()
}
}
}
}
Result with .navigationBarItems:
Result without .navigationBarItems:
Just move those modifiers out of dynamic content, otherwise you try to include duplicated bar items for every section, that seems makes SwiftUI engine crazy.
var body: some View {
NavigationView {
List{
ForEach(events.sections) { section in
Section(header: Text(section.title)) {
ForEach(section.matches) { match in
Text("Row")
}
}
}
}
.navigationBarTitle("Title")
.navigationBarItems(trailing:
NavigationLink(destination: AddMatchView().environment(\.managedObjectContext, self.moc)) {
Image(systemName: "plus")
.resizable()
.frame(width: 22, height: 22)
.padding(.horizontal)
}
.padding(.leading)
)
.onAppear(){
self.events = EventData()
}
}
}

How to add click event to whole view in SwiftUI

I'm developing SwiftUI test app and I added my custom DropDown menu here.
Dropdown works fine but I want to dismiss dropdown menu when user click dropdown menu outside area.
Here's my dropdown menu.
import SwiftUI
var dropdownCornerRadius:CGFloat = 3.0
struct DropdownOption: Hashable {
public static func == (lhs: DropdownOption, rhs: DropdownOption) -> Bool {
return lhs.key == rhs.key
}
var key: String
var val: String
}
struct DropdownOptionElement: View {
var dropdownWidth:CGFloat = 150
var val: String
var key: String
#Binding var selectedKey: String
#Binding var shouldShowDropdown: Bool
#Binding var displayText: String
var body: some View {
Button(action: {
self.shouldShowDropdown = false
self.displayText = self.val
self.selectedKey = self.key
}) {
VStack {
Text(self.val)
Divider()
}
}.frame(width: dropdownWidth, height: 30)
.padding(.top, 15).background(Color.gray)
}
}
struct Dropdown: View {
var dropdownWidth:CGFloat = 150
var options: [DropdownOption]
#Binding var selectedKey: String
#Binding var shouldShowDropdown: Bool
#Binding var displayText: String
var body: some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(self.options, id: \.self) { option in
DropdownOptionElement(dropdownWidth: self.dropdownWidth, val: option.val, key: option.key, selectedKey: self.$selectedKey, shouldShowDropdown: self.$shouldShowDropdown, displayText: self.$displayText)
}
}
.background(Color.white)
.cornerRadius(dropdownCornerRadius)
.overlay(
RoundedRectangle(cornerRadius: dropdownCornerRadius)
.stroke(Color.primary, lineWidth: 1)
)
}
}
struct DropdownButton: View {
var dropdownWidth:CGFloat = 300
#State var shouldShowDropdown = false
#State var displayText: String
#Binding var selectedKey: String
var options: [DropdownOption]
let buttonHeight: CGFloat = 30
var body: some View {
Button(action: {
self.shouldShowDropdown.toggle()
}) {
HStack {
Text(displayText)
Spacer()
Image(systemName: self.shouldShowDropdown ? "chevron.up" : "chevron.down")
}
}
.padding(.horizontal)
.cornerRadius(dropdownCornerRadius)
.frame(width: self.dropdownWidth, height: self.buttonHeight)
.overlay(
RoundedRectangle(cornerRadius: dropdownCornerRadius)
.stroke(Color.primary, lineWidth: 1)
)
.overlay(
VStack {
if self.shouldShowDropdown {
Spacer(minLength: buttonHeight)
Dropdown(dropdownWidth: dropdownWidth, options: self.options, selectedKey: self.$selectedKey, shouldShowDropdown: $shouldShowDropdown, displayText: $displayText)
}
}, alignment: .topLeading
)
.background(
RoundedRectangle(cornerRadius: dropdownCornerRadius).fill(Color.white)
)
}
}
struct DropdownButton_Previews: PreviewProvider {
static let options = [
DropdownOption(key: "week", val: "This week"), DropdownOption(key: "month", val: "This month"), DropdownOption(key: "year", val: "This year")
]
static var previews: some View {
Group {
VStack(alignment: .leading) {
DropdownButton(displayText: "This month", selectedKey: .constant("Test"), options: options)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
.foregroundColor(Color.primary)
VStack(alignment: .leading) {
DropdownButton(shouldShowDropdown: true, displayText: "This month", selectedKey: .constant("Test"), options: options)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
.foregroundColor(Color.primary)
}
}
}
I think I can achieve this by adding click event to whole body view and set dropdown show State flag variable to false.
But I'm not sure how to add click event to whole view.
Can anyone please help me about this issue?
Thanks.
You can try like the following in your window ContentView
struct ContentView: View {
var body: some View {
GeometryReader { gp in // << consumes all safe space
// all content here
}
.onTapGesture {
// change state closing any dropdown here
}
}
// .edgesIgnoringSafeArea(.all) // uncomment if needed entire screen
}
can be done adding .contentShape(Rectangle()) to HStack/VStack before .onTapGesture
for example
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
VStack(alignment:.leading, spacing: 8) {
CustomText(text: model?.id ?? "Today", fontSize: 12, fontColor: Color("Black50"))
CustomText(text: model?.title ?? "This Day..", fontSize: 14)
.lineLimit(2)
.padding(.trailing, 8)
}
Spacer()
Image("user_dummy")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60)
.cornerRadius(8)
}
CustomText(text: model?.document ?? "", fontSize: 12, fontColor: Color("Black50"))
.lineLimit(4)
}
.contentShape(Rectangle())
.onTapGesture {
debugPrint("Whole view as touch")
}
}