How to use Picker with data model - swiftui

I am trying to make a year selector, with information that comes from an array, but whenever I select an option the selector always returns automatically to the first position, how could I save the selected year in my $ i.anio?
Thank you
//---------------MODEL-----------------
struct SysNoAntpatologicosModel {
var anio: Int
var descripcion: String
var idantnopat: Int
var nombre: String
var presente: Bool
}
//-------------ARRAY----------------
[{
anio = 2001;
descripcion = "test1";
idantnopat = 38;
nombre = Accidente;
presente = 0;
},
{
anio = 2002;
descripcion = "test2";
idantnopat = 42;
nombre = Inmunizacion;
presente = 0;
}
]
#State var dataSys : [SysNoAntpatologicosModel] = []
ForEach($dataSys, id: \.idantnopat) { $i in
HStack{
Picker("", selection: $i.anio) {
ForEach(2000...2021, id: \.self) {
Text($0)
}
}
.pickerStyle(InlinePickerStyle())
.onChange(of: i.anio) { tag in
print("year: \(tag)")
}
}
}

With your edits, you're very close -- you just need to add "" around the Text input so that it will compile:
struct SysNoAntpatologicosModel {
var anio: Int
var descripcion: String
var idantnopat: Int
var nombre: String
var presente: Bool
}
struct ContentView : View {
#State var dataSys : [SysNoAntpatologicosModel] =
[.init(anio: 2001, descripcion: "test1", idantnopat: 38, nombre: "Accidente", presente: false),
.init(anio: 2002, descripcion: "test2", idantnopat: 42, nombre: "Inmunizacion", presente: false),
]
var body: some View {
ForEach($dataSys, id: \.idantnopat) { $i in
HStack{
Picker("", selection: $i.anio) {
ForEach(2000...2021, id: \.self) {
Text("\($0)") //<-- Here
}
}
.pickerStyle(InlinePickerStyle())
.onChange(of: i.anio) { tag in
print("year: \(tag)")
}
}
}
}
}

Related

Picker view is not changing when you try to select a different option in swiftui

I am using in swiftUI. When select picker, it is not changing. Here is code..
Here is datamodel:
struct SourceAccountModel:Codable,Identifiable{
var id: Int
let accountNumber: String
let accountTitle: String
let priaryAccount: String
init(id:Int=0,accountNumber: String, accountTitle: String, priaryAccount: String) {
self.id = id
self.accountNumber = accountNumber
self.accountTitle = accountTitle
self.priaryAccount = priaryAccount
}
}
Here is my code
struct Test2: View {
#State private var selectedOption = "Option 1"
#State private var sourceAccountList = [SourceAccountModel]()
var body: some View {
VStack{
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color.white)
.shadow(radius: 2)
Picker(selection: $selectedOption,label: EmptyView()) {
ForEach (0..<sourceAccountList.count,id: \.self) {
Text(sourceAccountList[$0].accountNumber)
}
}
.padding(8)
}
.frame(maxWidth: .infinity)
}.onAppear{
intitializeValue()
}
}
func intitializeValue(){
self.sourceAccountList.append(SourceAccountModel(id:1,accountNumber: "Option 1", accountTitle: "", priaryAccount: ""))
self.sourceAccountList.append(SourceAccountModel(id:2,accountNumber: "Option 2", accountTitle: "", priaryAccount: ""))
}
}
Always select first value. What is the wrong with my code?
selectedOption is a String, but your ForEach iterates over Range<Int>.
You can fix this by changing selectedOption to Int, e.g.
#State private var selectedOption = 0
You might find it easier to store the actual object in selectedOption: SourceAccountModel, iterate over the sourceAccountList, and tag each row:
struct SourceAccountModel: Identifiable, Hashable {
let id: Int
let accountNumber: String
init(id: Int, accountNumber: String) {
self.id = id
self.accountNumber = accountNumber
}
}
struct ContentView: View {
init() {
let sourceAccountList = [SourceAccountModel(id: 1, accountNumber: "Option 1"),
SourceAccountModel(id: 2, accountNumber: "Option 2")]
_sourceAccountList = State(wrappedValue: sourceAccountList)
_selectedOption = State(wrappedValue: sourceAccountList[0])
}
#State private var selectedOption: SourceAccountModel
#State private var sourceAccountList = [SourceAccountModel]()
var body: some View {
VStack {
Picker("Select", selection: $selectedOption) {
ForEach(sourceAccountList) { model in
Text(model.accountNumber).tag(model)
}
}
}
}
}

Struct not changing with Binding

I have this struct
struct MyObject {
var name:String
var color:String
var date:Date
init(name:String = "", color: String = "", date:Date = Date()) {
self.name = name
self.color = color
self.date = date
}
Then I have this on ContentView
#State private var temporaryObject = MyObject()
I send this to a view, like
var body: some View {
DoSomething($temporaryObject)
}
This is DoSomething
struct DoSomething: View {
#Binding var temporaryObject:MyObject
init(_ temporaryObject:Binding<MyObject>) {
self._temporaryObject = temporaryObject
}
var body: some View {
Button(action: {
// here is the problem
temporaryObject.name = "kkkk"
print(temporaryObject.name) // is equal to ""
}, label: {
Text("click me")
})
When I click the button, temporaryObject.name, in theory, is changed to "kkkk" but the print line shows it is still equals to empty.
why?
this example code works well for me. Does this code (taken from your question) not work for you?
struct ContentView: View {
#State private var temporaryObject = MyObject()
var body: some View {
VStack {
DoSomething(temporaryObject: $temporaryObject)
Text(temporaryObject.name) // <-- for testing
}
}
}
struct MyObject {
var name:String
var color:String
var date:Date
init(name:String = "", color: String = "", date:Date = Date()) {
self.name = name
self.color = color
self.date = date
}
}
struct DoSomething: View {
#Binding var temporaryObject:MyObject
var body: some View {
Button(action: {
temporaryObject.name = "kkkk"
print(temporaryObject.name) // is equal to "kkkk"
}, label: {
Text("click me")
})
}
}

Property Wrapper doesn't affect TextField

I wrote MaxCount propertyWrapper to limit String count in TextField. However, while Text view shows trimmed String, TextField shows full String.
I can achieve expected behavior via below ViewModifier, but this doesn't seem a good practice to me, I would like to achieve that behaviour via #propertyWrapper.
TextField("Type here...", text: $text)
.onChange(of: text) { newText in
// Check if newText has more characters than maxCount, if so trim it.
guard maxCount < newText.count else { text = newText; return }
text = String(newText.prefix(maxCount))
}
MaxCount.swift
#propertyWrapper struct MaxCount<T: RangeReplaceableCollection>: DynamicProperty {
// MARK: Properties
private var count: Int = 0
#State private var value: T = .init()
var wrappedValue: T {
get { value }
nonmutating set {
value = limitValue(newValue, count: count)
}
}
var projectedValue: Binding<T> {
Binding(
get: { value },
set: { wrappedValue = $0 }
)
}
// MARK: Initilizations
init(wrappedValue: T, _ count: Int) {
self.count = count
self._value = State(wrappedValue: limitValue(wrappedValue, count: count))
}
// MARK: Functions
private func limitValue(_ value: T, count: Int) -> T {
guard value.count > count else { return value }
let lastIndex = value.index(value.startIndex, offsetBy: count - 1)
let firstIndex = value.startIndex
return T(value[firstIndex...lastIndex])
}
}
ContentView.swift
struct ContentView: View {
#MaxCount(5) private var text = "This is a test text"
var body: some View {
VStack {
Text(text)
TextField("Type here...", text: $text)
}
}
}
I ended up building a new TextField as below.
Drawback: It doesn't support initialization with formatters which exists in TextField
struct FilteredTextField<Label: View>: View {
// MARK: Properties
private let label: Label
private var bindingText: Binding<String>
private let prompt: Text?
private let filter: (String) -> Bool
#State private var stateText: String
#State private var lastValidText: String = ""
// MARK: Initializations
init(text: Binding<String>, prompt: Text? = nil, label: () -> Label, filter: ((String) -> Bool)? = nil) {
self.label = label()
self.bindingText = text
self.prompt = prompt
self.filter = filter ?? { _ in true }
self._stateText = State(initialValue: text.wrappedValue)
}
init(_ titleKey: LocalizedStringKey, text: Binding<String>, prompt: Text? = nil, filter: ((String) -> Bool)? = nil) where Label == Text {
self.label = Text(titleKey)
self.bindingText = text
self.prompt = prompt
self.filter = filter ?? { _ in true }
self._stateText = State(initialValue: text.wrappedValue)
}
init(_ title: String, text: Binding<String>, prompt: Text? = nil, filter: ((String) -> Bool)? = nil) where Label == Text {
self.label = Text(title)
self.bindingText = text
self.prompt = prompt
self.filter = filter ?? { _ in true }
self._stateText = State(initialValue: text.wrappedValue)
}
// MARK: View
var body: some View {
TextField(text: $stateText, prompt: prompt, label: { label })
.onChange(of: stateText) { newValue in
guard newValue != bindingText.wrappedValue else { return }
guard filter(newValue) else { stateText = lastValidText; return }
bindingText.wrappedValue = newValue
}
.onChange(of: bindingText.wrappedValue) { newValue in
if filter(newValue) { lastValidText = newValue }
stateText = newValue
}
}
}
Usage
struct ContentView: View {
#State var test: String = ""
var body: some View {
VStack {
HStack {
Text("Default TextField")
TextField(text: $test, label: { Text("Type here...") })
}
HStack {
Text("FilteredTextField")
FilteredTextField(text: $test, label: { Text("Type here...") }) { inputString in inputString.count <= 5 }
}
}
}
}

Sorting data by Newest and Oldest

I want to sort my custom array by the last tapped(Newest). I can sort by name or by id, but I have trouble sorting by time. I couldn't find similar issues on the internet, and it gives me a lot of headaches. As you can see in my code below, I want to sort the favorite list by the last tapped. Thanks in advance for your help.
import Foundation
import SwiftUI
struct DataArray: Identifiable {
let id: Int
let cities: String
let name1: String
let name2: String
let isFavorite: Bool
var date = Date()
}
public struct ListDataArray {
static let dot = [
DataArray(id: 1,
cities: "Baltimore"
name1: "John",
name2: "Mike",
isFavorite: False),
DataArray(id: 2,
cities: "Frederick"),
name1: "Joe",
name2: "Swift",
isFavorite: False),
DataArray(id: 3,
cities: "Catonsville"
name1: "Susan",
name2: "Oliver",
isFavorite: False),
// There will be a lot of data
]
}
class Prospect: ObservableObject {
#Published var datas: [DataArray] = []
init() {
fetchDataArrays()
}
private func fetchDataArrays() {
let items = ListDataArray.dot
datas = items
}
// To View Favorite
#Published var showFavorite: Bool = false
}
struct Home: View {
#EnvironmentObject var items: Prospect
var body: some View {
ScrollView {
LazyVStack {
ForEach(items.datas) { data in
VStack {
ButtonView(data: data)
.environmentObject(items)
Text("\(data.id)")
.font(.title3)
Text(data.cities)
.font(.subheadline)
Text(data.name1)
.font(.subheadline)
Text(data.name2)
.font(.subheadline)
}
padding()
}
.padding()
}
}
}
}
struct ButtonView: View {
#EnvironmentObject var items: Prospect
let data: DataArray
var index: Int {
items.datas.firstIndex(where: { $0.id == data.id }) ?? 0
}
var body: some View {
HStack {
Button(action: {
self.items.datas[self.index].isFavorite.toggle()
} label: {
Image(systemName: self.items.datas[self.index].isFavorite ? "suit.heart.fill" : "suit.heart").padding()
})
Spacer()
Button(action: {
items.showFavorite.toggle()
} label: {
Image(systemName: "person").padding()
})
.sheet(isPresented: $items.showFavorite) {
FavoriteView()
.environmentObject(items)
}
}
}
}
struct FavoriteView: View {
#EnvironmentObject var items: Prospect
#State var selection = 0
let sortingNames = ["Newest", "Oldest", "A to Z", "Z to A"]
var body: some View {
VStack {
HStack {
ForEach(0...<4) { num in
Button(action: {
selection = num
}, label: {
Spacer()
Text(sortingNames[num])
.forgroundColor(selection == num ? Color.blue: Color.black)
Spacer()
})
}
}
List {
ForEach(sorting()) { data in
if data.isFavorite {
VStack(spacing: 10) {
Text(data.cities)
Text(data.name1)
Text(data.name2)
}
.font(.body)
}
}
.padding()
}
Spacer()
}
}
private func sorting() -> [DataArray] {
switch selection {
case 0:
// Here I want to sort by the Newest...
return items.datas.sorted(by: { $0.date < $1.date })
case 1:
// Here I want to sort by the Oldest...
return items.datas.sorted(by: { $0.date < $1.date })
case 2:
return items.datas.sorted(by: { $0.cities < $1.cities })
case 3:
return items.datas.sorted(by: { $0.cities > $1.cities })
}
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
.environmentObject(Prospect())
}
}
Focusing only on the sorting as per the question, the following works well for me.
The only significant change, was the sort by Oldest. Here is my test code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct DataArray: Identifiable {
let id: Int
let cities: String
let name1: String
let name2: String
let isFavorite: Bool
var date = Date()
}
struct ContentView: View {
let dataArr = [DataArray(id: 1,cities: "Baltimore", name1: "John", name2: "Mike", isFavorite: true,
date: Date()),
DataArray(id: 2,cities: "Frederick" ,name1: "Joe", name2: "Swift", isFavorite: true,
date: .now+1),
DataArray(id: 3,cities: "Catonsville", name1: "Susan", name2: "Oliver", isFavorite: true,
date: .now+2)
]
#State var selection = 0
let sortingNames = ["Newest", "Oldest", "A to Z", "Z to A"]
var body: some View {
VStack {
HStack {
ForEach(0..<4) { num in
Button(action: { selection = num}) {
Text(sortingNames[num])
.foregroundColor(selection == num ? Color.blue: Color.black)
}
}
}
List {
ForEach(sorting()) { data in
if data.isFavorite {
VStack(spacing: 10) {
Text("\(data.id)").foregroundColor(.red) // <-- for testing
Text(data.cities)
Text(data.name1)
Text(data.name2)
}
}
}
}
}
}
func sorting() -> [DataArray] {
switch selection {
case 0:
// Here I want to sort by the Newest...
return dataArr.sorted(by: { $0.date < $1.date })
case 1:
// Here I want to sort by the Oldest...
return dataArr.sorted(by: { $0.date > $1.date }) // <--- here
case 2:
return dataArr.sorted(by: { $0.cities < $1.cities })
case 3:
return dataArr.sorted(by: { $0.cities > $1.cities })
default:
return dataArr // <--- here need a default
}
}
}
EDIT1:
Since I don't know what you really want to do with your app,
I just modified your (test) code to show you an approach for getting your data sorted.
The main emphasis here is to have
one source of truth of the data, starting in ContenView #StateObject var items = Prospect().
Also made sorting() sorting the data in-place, so that it appear sorted everywhere.
Moved sorting() into your Prospect model.
I also renamed the badly named DataArray to CityData.
struct ContentView: View {
#StateObject var items = Prospect()
var body: some View {
Home().environmentObject(items)
}
}
struct Home: View {
#EnvironmentObject var items: Prospect
var body: some View {
ScrollView {
LazyVStack {
ForEach(items.datas) { data in
VStack {
ButtonView(data: data)
Text("\(data.id)").font(.title3)
Text(data.cities).font(.subheadline)
Text(data.name1).font(.subheadline)
Text(data.name2).font(.subheadline)
}.padding()
}.padding()
}
}
}
}
struct ButtonView: View {
#EnvironmentObject var items: Prospect
#State var data: CityData // <--- here
#State var index: Int = 0 // <--- here
var body: some View {
HStack {
Button(action: { items.datas[index].isFavorite.toggle() }) {
Image(systemName: items.datas[index].isFavorite ? "suit.heart.fill" : "suit.heart").padding()
}
Spacer()
Button(action: { items.showFavorite.toggle() }) {
Image(systemName: "person").padding()
}
}
.onAppear {
if let ndx = items.datas.firstIndex(where: { $0.id == data.id }) {
index = ndx
}
}
.sheet(isPresented: $items.showFavorite) {
FavoriteView().environmentObject(items)
}
}
}
struct FavoriteView: View {
#Environment(\.dismiss) var dismiss
#EnvironmentObject var items: Prospect
#State var selection = 0
let sortingNames = ["Newest", "Oldest", "A to Z", "Z to A"]
var body: some View {
VStack {
Button(action: {dismiss()}) {
Text("Done").foregroundColor(.blue) // <-- for testing on macos
}
HStack {
ForEach(0..<4) { num in
Button(action: {
selection = num
items.sorting(selection) // <--- here
}) {
Text(sortingNames[num])
.foregroundColor(selection == num ? Color.blue: Color.black)
}
}
}
List {
ForEach(items.datas) { data in // <--- here
if data.isFavorite {
VStack(spacing: 10) {
Text("\(data.id)").foregroundColor(.red) // <-- for testing
Text(data.cities)
Text(data.name1)
Text(data.name2)
}
}
}
}
}
}
}
class Prospect: ObservableObject {
#Published var datas: [CityData] = []
#Published var showFavorite: Bool = false
init() {
fetchCityDatas()
}
private func fetchCityDatas() {
datas = ListCityData.dot
}
// --- sort in place note sort() not sorted() ---
func sorting(_ selection: Int) {
switch selection {
case 0:
// Here I want to sort by the Newest...
datas.sort(by: { $0.date < $1.date })
case 1:
// Here I want to sort by the Oldest...
datas.sort(by: { $0.date > $1.date }) // <--- here
case 2:
datas.sort(by: { $0.cities < $1.cities })
case 3:
datas.sort(by: { $0.cities > $1.cities })
default:
break
}
}
}
public struct ListCityData {
static let dot = [CityData(id: 1,cities: "Baltimore", name1: "John", name2: "Mike", isFavorite: true,
date: .now),
CityData(id: 2,cities: "Frederick" ,name1: "Joe", name2: "Swift", isFavorite: true,
date: .now+10),
CityData(id: 3,cities: "Catonsville", name1: "Susan", name2: "Oliver", isFavorite: true,
date: .now+20)
]
}
// --- here renamed and using var ---
struct CityData: Identifiable {
let id: Int
var cities: String
var name1: String
var name2: String
var isFavorite: Bool
var date: Date
}

(SwiftUI) How to refresh the count down

I want the count down to restart when I enter next quiz. Now the count down just continues. Do you know how to fix this problem? Thank you. Following is the reproducible code.
TemplateView.swift
Since the header and the footer is same for all quizes. I am using the template view to avoid redundancy.
import SwiftUI
struct TemplateView: View {
#ObservedObject var model = TemplateViewModel()
var body: some View {
VStack {
InfoView(model: model)
Text(model.getContent(idx: model.currentIdx))
Image(systemName: "arrowshape.turn.up.right.fill")
.onTapGesture {
if model.currentIdx >= model.data.count-1 {
model.currentIdx = 0
} else {
model.currentIdx += 1
}
}
}
}
}
TemplateViewModel
import Foundation
import SwiftUI
class TemplateViewModel: ObservableObject {
#Published var currentIdx: Int = 0
var data: [QuizModel] = [
QuizModel(prepareSeconds: 10, content: "Answer the short question"),
QuizModel(prepareSeconds: 20, content: "Listen to the audio"),
QuizModel(prepareSeconds: 30, content: "Read the article"),
]
var time: Int {
return data[currentIdx].prepareSeconds
}
func getTime(idx: Int) -> Int {
return data[idx].prepareSeconds
}
func getContent(idx: Int) -> String {
return data[idx].content
}
}
InfoView
import SwiftUI
struct InfoView: View {
#ObservedObject var model: TemplateViewModel
#State var prepareSeconds: Int = 0
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
init(model: TemplateViewModel) {
self.model = model
self._prepareSeconds = State(initialValue: model.getTime(idx: model.currentIdx))
}
var body: some View {
Text(prepareSeconds.description)
.onReceive(timer, perform: { _ in
if prepareSeconds > 0 {
prepareSeconds -= 1
}
})
}
}
QuizModel.swift
This the data model, prepareSeconds means how many seconds the participant can prepare for this quiz. content is the quiz content.
struct QuizModel {
var prepareSeconds: Int
var content: String
}
The simplest way is to just add an id to InfoView so that it is forced to reset its state when the ID changes:
InfoView(model: model).id(model.currentIdx)
However, architecturally, I'm not sure that makes the most sense. I'd store the timer in your ObservableObject:
struct TemplateView: View {
#ObservedObject var model = TemplateViewModel()
var body: some View {
VStack {
InfoView(model: model)
Text(model.getContent(idx: model.currentIdx))
Image(systemName: "arrowshape.turn.up.right.fill")
.onTapGesture {
if model.currentIdx >= model.data.count-1 {
model.currentIdx = 0
} else {
model.currentIdx += 1
}
model.countDownFrom(seconds: model.getTime(idx: model.currentIdx))
}
}.onAppear {
model.countDownFrom(seconds: model.getTime(idx: model.currentIdx))
}
}
}
class TemplateViewModel: ObservableObject {
#Published var currentIdx: Int = 0
#Published var prepareSeconds: Int = 0
private var cancellable : AnyCancellable?
var data: [QuizModel] = [
QuizModel(prepareSeconds: 10, content: "Answer the short question"),
QuizModel(prepareSeconds: 20, content: "Listen to the audio"),
QuizModel(prepareSeconds: 30, content: "Read the article"),
]
var time: Int {
return data[currentIdx].prepareSeconds
}
func getTime(idx: Int) -> Int {
return data[idx].prepareSeconds
}
func getContent(idx: Int) -> String {
return data[idx].content
}
func countDownFrom(seconds: Int) {
prepareSeconds = seconds
cancellable = Timer.publish(every: 1, on: .main, in: .common).autoconnect().sink(receiveValue: { (_) in
if self.prepareSeconds > 0 {
self.prepareSeconds -= 1
}
})
}
}
struct InfoView: View {
#ObservedObject var model: TemplateViewModel
var body: some View {
Text(model.prepareSeconds.description)
}
}
struct QuizModel {
var prepareSeconds: Int
var content: String
}
More refactoring that could be done, but that'll get you started.