How we can safely unwrap data in SwiftUI? - swiftui

I want safely unwrap data(with guard or any other way) for rendering my views in SwiftUI, here is my code:
I do not want use "??" and "Empty", I want safely unwrap my
dataModel.items[index].name
, if there is something then show otherwise do nothing, as you can see in screenshoot we will got 4 views with this code, but it should be 2 views for "Data 1" and "Data 2"
PS: I am smart enough to know that I can use "" instead of "Empty", But "" is even a view, and I do not need it.
struct Data: Identifiable
{
let id = UUID()
var name: String?
}
let Data1 = Data(name: "Data 1")
let Data2 = Data(name: "Data 2")
let Data3 = Data()
let Data4 = Data()
class DataModel: ObservableObject
{
#Published var items: [Data] = [Data1, Data2, Data3, Data4]
}
struct ContentView: View
{
#StateObject var dataModel = DataModel()
var body: some View
{
ForEach(dataModel.items.indices, id: \.self) { index in
Text(dataModel.items[index].name ?? "Empty")
.font(.title2)
.padding()
}
}
}

like my answer of: Can you specify a "Where" in a SwiftUI ForEach statement?
you could try something like this:
if let theName = dataModel.items[index].name {
Text(theName).font(.title2).padding()
}

Related

Realm in SwiftUI: Live Text in UI for Object Only Saved Occasionally

I'm doing a comparison of Core Data and Realm in a SwiftUI app, and Core Data does something that I'm hoping to figure out how to do in Realm.
Core Data lets you mutate objects whenever you want, and when they are ObservableObject in SwiftUI, your UI instantly updates. You then save the context whenever you want to persist the changes.
In Realm, the objects in the UI are live, but you can't change them unless you are in a write transaction. I'm trying to get my UI to reflect live/instant changes from the user when the actual write is only performed occasionally. Below is a sample app.
Here's my Realm model:
import RealmSwift
class Item: Object, ObjectKeyIdentifiable{
#objc dynamic var recordName = ""
#objc dynamic var text = ""
override class func primaryKey() -> String? {
return "recordName"
}
}
Here is my view model that also includes my save() function that only saves every 3 seconds. In my actual app, this is because it's an expensive operation and doing it as the user types brings the app to a crawl.
class ViewModel: ObservableObject{
static let shared = ViewModel()
#Published var items: Results<Item>!
#Published var selectedItem: Item?
var token: NotificationToken? = nil
init(){
//Add dummy data
let realm = try! Realm()
realm.beginWrite()
let item1 = Item()
item1.recordName = "one"
item1.text = "One"
realm.add(item1, update: .all)
let item2 = Item()
item2.recordName = "two"
item2.text = "Two"
realm.add(item2, update: .all)
try! realm.commitWrite()
self.fetch()
//Notifications
token = realm.objects(Item.self).observe{ [weak self] _ in
self?.fetch()
}
}
//Get Data
func fetch(){
let realm = try! Realm()
self.items = realm.objects(Item.self)
}
//Save Data
var saveTimer: Timer?
func save(item: Item, text: String){
//Save occasionally
saveTimer?.invalidate()
saveTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false){ _ in
let realm = try! Realm()
try? realm.write({
item.text = text
})
}
}
}
Last of all, here is the UI. It's pretty basic and reflects the general structure of my app where I'm trying to pull this off.
struct ContentView: View {
#StateObject var model = ViewModel.shared
var body: some View {
VStack{
ForEach(model.items){ item in
HStack{
Button(item.text){
model.selectedItem = item
}
Divider()
ItemDetail(item: item)
}
}
}
}
}
...and the ItemDetail view:
struct ItemDetail: View{
#ObservedObject var item: Item
#StateObject var model = ViewModel.shared
init(item: Item){
self.item = item
}
var body: some View{
//Binding
let text = Binding<String>(
get: { item.text },
set: { model.save(item: item, text: $0) }
)
TextField("Text...", text: text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
When I type in the TextField, how do I get the Button text to reflect what I have typed in real time considering that my realm.write only happens every 3 seconds? My Button updates after a write, but I want the UI to respond live--independent of the write.
Based on the suggested documentation from Jay, I got the following to work which is quite a bit simpler:
My main view adds the #ObservedResults property wrapper like this:
struct ContentView: View {
#StateObject var model = ViewModel.shared
#ObservedResults(Item.self) var items
var body: some View {
VStack{
ForEach(items){ item in
HStack{
Button(item.text){
model.selectedItem = item
}
Divider()
ItemDetail(item: item)
}
}
}
}
}
...and then the ItemDetail view simply uses an #ObservedRealmObject property wrapper that binds to the value in Realm and manages the writes automatically:
struct ItemDetail: View{
#ObservedRealmObject var item: Item
var body: some View{
TextField("Text...", text: $item.text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
This is essentially how Core Data does it (in terms view code) except Realm saves to the store automatically. Thank you, Jay!

How we can force ForEach use UUID instead of (id: \.self)?

I have down code which makes some Text in a ForEach loop, this loop use (id: .self) which is Int, but in this case our generated ( id = UUID() ) would ignored! And ForEach would work with some internal Int numbers! How we can force ForEach to take our built UUID as id?
struct Data: Identifiable
{
let id = UUID()
var name: String
}
let Data1 = Data(name: "Data 1")
let Data2 = Data(name: "Data 2")
let Data3 = Data(name: "Data 3")
let Data4 = Data(name: "Data 4")
class DataModel: ObservableObject
{
#Published var items: [Data] = [Data1, Data2, Data3, Data4]
}
struct ContentView: View
{
#StateObject var dataModel = DataModel()
var body: some View
{
ForEach(dataModel.items.indices, id: \.self) { index in // ← Here: I want UUID
Text(dataModel.items[index].name)
.font(.title2)
.padding()
}
}
}
You can use zip as mentioned in How do you use .enumerated() with ForEach in SwiftUI?
ForEach(Array(zip(dataModel.items.indices, dataModel.items)), id: \.1) { index, item in
// index and item are both safe to use here
}
you could try this:
ForEach(dataModel.items.map{ $0.id }, id: \.self) { uuid in // ← Here: I want UUID

Easiest way to make a dynamic, editable list of simple objects in SwiftUI?

I want a dynamic array of mutable strings to be presented by a mother view with a list of child views, each presenting one of the strings, editable. Also, the mother view will show a concatenation of the strings which will update whenever one of the strings are updated in the child views.
Can't use (1) ForEach(self.model.strings.indices) since set of indices may change and can't use (2) ForEach(self.model.strings) { string in since the sub views wants to edit the strings but string will be immutable.
The only way I have found to make this work is to make use of an #EnvironmentObject that is passed around along with the parameter. This is really clunky and borders on offensive.
However, I am new to swiftui and I am sure there a much better way to go about this, please let know!
Here's what I have right now:
import SwiftUI
struct SimpleModel : Identifiable { var id = UUID(); var name: String }
let simpleData: [SimpleModel] = [SimpleModel(name: "text0"), SimpleModel(name: "text1")]
final class UserData: ObservableObject { #Published var simple = simpleData }
struct SimpleRowView: View {
#EnvironmentObject private var userData: UserData
var simple: SimpleModel
var simpleIndex: Int { userData.simple.firstIndex(where: { $0.id == simple.id })! }
var body: some View {
TextField("title", text: self.$userData.simple[simpleIndex].name)
}
}
struct SimpleView: View {
#EnvironmentObject private var userData: UserData
var body: some View {
let summary_binding = Binding<String>(
get: {
var arr: String = ""
self.userData.simple.forEach { sim in arr += sim.name }
return arr;
},
set: { _ = $0 }
)
return VStack() {
TextField("summary", text: summary_binding)
ForEach(userData.simple) { tmp in
SimpleRowView(simple: tmp).environmentObject(self.userData)
}
Button(action: { self.userData.simple.append(SimpleModel(name: "new text"))}) {
Text("Add text")
}
}
}
}
Where the EnironmentObject is created and passed as SimpleView().environmentObject(UserData()) from AppDelegate.
EDIT:
For reference, should someone find this, below is the full solution as suggested by #pawello2222, using ObservedObject instead of EnvironmentObject:
import SwiftUI
class SimpleModel : ObservableObject, Identifiable {
let id = UUID(); #Published var name: String
init(name: String) { self.name = name }
}
class SimpleArrayModel : ObservableObject, Identifiable {
let id = UUID(); #Published var simpleArray: [SimpleModel]
init(simpleArray: [SimpleModel]) { self.simpleArray = simpleArray }
}
let simpleArrayData: SimpleArrayModel = SimpleArrayModel(simpleArray: [SimpleModel(name: "text0"), SimpleModel(name: "text1")])
struct SimpleRowView: View {
#ObservedObject var simple: SimpleModel
var body: some View {
TextField("title", text: $simple.name)
}
}
struct SimpleView: View {
#ObservedObject var simpleArrayModel: SimpleArrayModel
var body: some View {
let summary_binding = Binding<String>(
get: { return self.simpleArrayModel.simpleArray.reduce("") { $0 + $1.name } },
set: { _ = $0 }
)
return VStack() {
TextField("summary", text: summary_binding)
ForEach(simpleArrayModel.simpleArray) { simple in
SimpleRowView(simple: simple).onReceive(simple.objectWillChange) {_ in self.simpleArrayModel.objectWillChange.send()}
}
Button(action: { self.simpleArrayModel.simpleArray.append(SimpleModel(name: "new text"))}) {
Text("Add text")
}
}
}
}
You don't actually need an #EnvironmentObject (it will be available globally for all views in your environment).
You may want to use #ObservedObject instead (or #StateObject if using SwiftUI 2.0):
...
return VStack {
TextField("summary", text: summary_binding)
ForEach(userData.simple, id:\.id) { tmp in
SimpleRowView(userData: self.userData, simple: tmp) // <- pass userData to child views
}
Button(action: { self.userData.simple.append(SimpleModel(name: "new text")) }) {
Text("Add text")
}
}
struct SimpleRowView: View {
#ObservedObject var userData: UserData
var simple: SimpleModel
...
}
Note that if your data is not constant you should use a dynamic ForEach loop (with an explicit id parameter):
ForEach(userData.simple, id:\.id) { ...
However, the best results you can achieve when you make your SimpleModel a class and ObservableObject. Here is a better solution how do do it properly:
SwiftUI update data for parent NavigationView
Also, you can simplify your summary_binding using reduce:
let summary_binding = Binding<String>(
get: { self.userData.simple.reduce("") { $0 + $1.name } },
set: { _ = $0 }
)

*SwiftUI Beta 7* How to give item in ForEach Loop an active state

I have a data model of the type:
struct fruit: Identifiable{
var id = UUID()
var a: String
var b: String
var isActive: Bool
}
and an array:
let fruitData=[
Model(a:"appleImg", b:"Apple", isActive: true),
Model(a:"pearImg", b:"Pear", isActive: false),
Model(a:"bananaImg", b:"Banana", isActive: false),
]
There's a RowView that looks like this:
struct RowView{
var a = "appleImg"
var b = "Apple"
var isActive = true
var body: some View{
HStack(spacing:8){
Image(a)
Text(b)
Spacer()
}
}
}
I then created a view to use ModelArray in and looped that in a ForEach in the main view. So something like:
let fruits = fruitData
ForEach(fruits){fruitPiece in
RowView(a:fruitPiece.a, b:fruitPiece.b, isActive:
fruitPiece.isActive)
}
I want to change the isActive based on the user tapping on the selected row - trick is it should be a single select, so only 1 active state at a time. Still new to SwiftUI so any help is super appreciated :)
The code below does what you asked. But some comments on your code first:
By convention, variable names should never begin with an uppercase (such as in your let ModelArray). It makes it harder to read. If not to you, to others. When sharing code (such as in stackoverflow), try to follow proper conventions.
You are calling ForEach(models), but you have not defined a models array (you did create ModelArray though).
Inside your ForEach, you are calling Model(...). Inside ForEach you need a view to display your data. But Model, in your code, is a data type. Does not make sense.
Here's the code that I think, does what you asked. When you tap the view, its isActive flag toggles. To show that it works, the text color changes accordingly.
struct Model: Identifiable {
var id = UUID()
var a: String
var b: String
var c: String
var isActive: Bool
}
struct ContentView: View {
#State private var modelArray = [
Model(a:"hi", b:"I like", c: "potatoes", isActive: true),
Model(a:"hi", b:"I like", c: "potatoes", isActive: false),
Model(a:"hi", b:"I like", c: "potatoes", isActive: false),
]
var body: some View {
ForEach(modelArray.indices, id: \.self){ idx in
Text("\(self.modelArray[idx].a) \(self.modelArray[idx].b) \(self.modelArray[idx].c)")
.foregroundColor(self.modelArray[idx].isActive ? .red : .green)
.onTapGesture {
self.modelArray[idx].isActive = true
for i in self.modelArray.indices {
if i != idx { self.modelArray[i].isActive = false }
}
}
}
}
}

SwiftUI #State var initialization issue

I would like to initialise the value of a #State var in SwiftUI through the init() method of a Struct, so it can take the proper text from a prepared dictionary for manipulation purposes in a TextField.
The source code looks like this:
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
TextField($fullText)
}
}
Unfortunately the execution fails with the error Thread 1: Fatal error: Accessing State<String> outside View.body
How can I resolve the situation? Thank you very much in advance!
SwiftUI doesn't allow you to change #State in the initializer but you can initialize it.
Remove the default value and use _fullText to set #State directly instead of going through the property wrapper accessor.
#State var fullText: String // No default value of ""
init(letter: String) {
_fullText = State(initialValue: list[letter]!)
}
I would try to initialise it in onAppear.
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
var body: some View {
TextField($fullText)
.onAppear {
self.fullText = list[letter]!
}
}
}
Or, even better, use a model object (a BindableObject linked to your view) and do all the initialisation and business logic there. Your view will update to reflect the changes automatically.
Update: BindableObject is now called ObservableObject.
The top answer is incorrect. One should never use State(initialValue:) or State(wrappedValue:) to initialize state in a View's init. In fact, State should only be initialized inline, like so:
#State private var fullText: String = "The value"
If that's not feasible, use #Binding, #ObservedObject, a combination between #Binding and #State or even a custom DynamicProperty
In your specific case, #Bindable + #State + onAppear + onChange should do the trick.
More about this and in general how DynamicPropertys work, here.
It's not an issue nowadays to set a default value of the #State variables inside the init method. But you MUST just get rid of the default value which you gave to the state and it will work as desired:
,,,
#State var fullText: String // <- No default value here
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
TextField("", text: $fullText)
}
}
Depending on the case, you can initialize the State in different ways:
// With default value
#State var fullText: String = "XXX"
// Not optional value and without default value
#State var fullText: String
init(x: String) {
fullText = x
}
// Optional value and without default value
#State var fullText: String
init(x: String) {
_fullText = State(initialValue: x)
}
The answer of Bogdan Farca is right for this case but we can't say this is the solution for the asked question because I found there is the issue with the Textfield in the asked question. Still we can use the init for the same code So look into the below code it shows the exact solution for asked question.
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
VStack {
Text("\(self.fullText)")
TextField("Enter some text", text: $fullText)
}
}
}
And use this by simply calling inside your view
struct ContentView: View {
var body: some View {
StateFromOutside(letter: "a")
}
}
You can create a view model and initiate the same as well :
class LetterViewModel: ObservableObject {
var fullText: String
let listTemp = [
"a": "Letter A",
"b": "Letter B",
// ...
]
init(initialLetter: String) {
fullText = listTemp[initialLetter] ?? ""
}
}
struct LetterView: View {
#State var viewmodel: LetterViewModel
var body: some View {
TextField("Enter text", text: $viewmodel.fullText)
}
}
And then call the view like this:
struct ContentView: View {
var body: some View {
LetterView(viewmodel: LetterViewModel(initialLetter: "a"))
}
}
By this you would also not have to call the State instantiate method.
See the .id(count) in the example come below.
import SwiftUI
import MapKit
struct ContentView: View {
#State private var count = 0
var body: some View {
Button("Tap me") {
self.count += 1
print(count)
}
Spacer()
testView(count: count).id(count) // <------ THIS IS IMPORTANT. Without this "id" the initializer setting affects the testView only once and calling testView again won't change it (not desirable, of course)
}
}
struct testView: View {
var count2: Int
#State private var region: MKCoordinateRegion
init(count: Int) {
count2 = 2*count
print("in testView: \(count)")
let lon = -0.1246402 + Double(count) / 100.0
let lat = 51.50007773 + Double(count) / 100.0
let myRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lon) , span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
_region = State(initialValue: myRegion)
}
var body: some View {
Map(coordinateRegion: $region, interactionModes: MapInteractionModes.all)
Text("\(count2)")
}
}