I trying to build an app that having a list that contained some information. I tried to make information partly tappable while the information contained a mobile number or maybe URL? I want to know may it be possible to do so?
For example the URL and mobile in the list
struct ContentView: View {
#State var itemList : [Item] = [Item]()
var body: some View {
VStack{
List(itemList){ item in
Text(item.info)
}
Text("Hello, World!")
}.onAppear(perform: {
print("test")
itemList = [Item(info: "testing"), Item(info: "Contact me 0123456789"), Item(info: "Check our website https://google.com")]
})
}
}
struct Item: Identifiable {
var info : String
let id = UUID()
func load() {
}
}
Related
I'm trying to have a component that basically starts with a single TextField for editing your home phone number, and then you can hit the add button to add different types of phone numbers to your account. for right now I just want to allow the user to edit the phone number, but in the future I'll probably make it so that there are actually two TextFields for each PhoneNumberListItem. 1 field for the editable name and 1 field for the editable phone itself. I'm coming from Android/Compose which is maybe where my line of thinking is stuck. appreciate any pointers.
func ListOfMyPhoneNumbers() -> some View {
#State var listOfMyPhones = [PhoneNumberListItem(name: "home", phone: "123")]
return VStack {
ForEach(listOfMyPhones) { i in
TextField(i.name, text: i.$phone).textFieldStyle(.roundedBorder)
}
Button("Add Phone") {
listOfMyPhones.append(PhoneNumberListItem(name: "other", phone: ""))
}.buttonStyle(.borderless)
Spacer()
}
.padding()
}
struct PhoneNumberListItem: Identifiable {
let id = UUID()
let name: String
#State var phone: String
}
Do not use #State var phone: String in your PhoneNumberListItem.
#State is only for use in a View. Just use a var.
Also use a struct for your view. With the following example code you will be able to edit the TextField in a ForEach, like this:
struct PhoneNumberListItem: Identifiable {
let id = UUID()
var name: String // <--- here
var phone: String // <--- here
}
struct ContentView: View {
var body: some View {
PhoneView() // <--- here
}
}
struct PhoneView: View { // <--- here
#State var listOfMyPhones = [PhoneNumberListItem(name: "home", phone: "123")]
var body: some View {
VStack {
// --- here
ForEach($listOfMyPhones) { $item in
TextField("phone", text: $item.phone)
TextField("name", text: $item.name)
}.textFieldStyle(.roundedBorder)
Button("Add Phone") {
listOfMyPhones.append(PhoneNumberListItem(name: "other", phone: ""))
}.buttonStyle(.borderless)
Spacer()
}
.padding()
}
}
As you progress with learning SwiftUI, you will want to use ObservableObject class to manage your data. Have a look at this link, it gives you some good examples of how to manage data in your app :
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
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!
I have following code in SwiftUI
struct AuthorView: View {
let authors = ["a","b","c"]
#State private var selectedAuthor = ""
var body: some View {
VStack {
Text("Authors").font(.title)
HStack {
List(authors, id: \.self, selection: $selectedAuthor) {author in
Text(author).tag(author)
}
Spacer()
}
}
}
but I can't compile due to error "Generic parameter 'SelectionValue' could not be inferred". What I'm doing wrong here? My aim is to get selected author for later use.
Thanks.
List selection by interface contract should be optional, so here is fixed part
struct AuthorView: View {
let authors = ["a","b","c"]
#State private var selectedAuthor: String? // << !!
// ... other code
}
I am looking for some guidance with SwiftUI please.
I have a view showing a simple list with each row displaying a "name" string. You can add items to the array/list by clicking on the trailing navigation bar button. This works fine. I would now like to use NavigationLink to present a new "DetailView" in which I can edit the row's "name" string. I'm struggling with how to use a binding in the detailview to update the name.
I've found plenty of tutorials online on how to present data in the new view, but nothing on how to edit the data.
Thanks in advance.
ContentView:
struct ListItem: Identifiable {
let id = UUID()
let name: String
}
class MyListClass: ObservableObject {
#Published var items = [ListItem]()
}
struct ContentView: View {
#ObservedObject var myList = MyListClass()
var body: some View {
NavigationView {
List {
ForEach(myList.items) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item.name)
}
}
}
.navigationBarItems(trailing:
Button(action: {
let item = ListItem(name: "Test")
self.myList.items.append(item)
}) {
Image(systemName: "plus")
}
)
}
}
}
DetailView
struct DetailView: View {
var item: ListItem
var body: some View {
TextField("", text: item.name)
}
}
The main idea that you pass in DetailsView not item, which is copied, because it is a value, but binding to the corresponding item in your view model.
Here is a demo with your code snapshot modified to fulfil the requested behavior:
struct ListItem: Identifiable, Equatable {
var id = UUID()
var name: String
}
class MyListClass: ObservableObject {
#Published var items = [ListItem]()
}
struct ContentView: View {
#ObservedObject var myList = MyListClass()
var body: some View {
NavigationView {
List {
ForEach(myList.items) { item in
// Pass binding to item into DetailsView
NavigationLink(destination: DetailView(item: self.$myList.items[self.myList.items.firstIndex(of: item)!])) {
Text(item.name)
}
}
}
.navigationBarItems(trailing:
Button(action: {
let item = ListItem(name: "Test")
self.myList.items.append(item)
}) {
Image(systemName: "plus")
}
)
}
}
}
struct DetailView: View {
#Binding var item: ListItem
var body: some View {
TextField("", text: self.$item.name)
}
}
I wanted to create quiet a simple app on watchOS 6, but after Apple has changed the ObjectBindig in Xcode 11 beta 5 my App does not run anymore. I simply want to synchronize data between two Views.
So I have rewritten my App with the new #Published, but I can't really set it up:
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen()) {
Text("Next View")
}
}
}
}
struct secondScreen: View {
#ObservedObject var input = UserInput()
var body: some View {
VStack {
Text("Button has been pushed \(input.score)")
Button(action: {self.input.score += 1
}) {
Text("Adder")
}
}
}
}
Your code has a couple of errors:
1) You didn't put your ContentView in a NavigationView, so the navigation between the two views never happened.
2) You used data binding in a wrong way. If you need the second view to rely on some state belonging to the first view you need to pass a binding to that state to the second view. Both in your first view and in your second view you had an #ObservedObject created inline:
#ObservedObject var input = UserInput()
so, the first view and the second one worked with two totally different objects. Instead, you are interested in sharing the score between the views. Let the first view own the UserInput object and just pass a binding to the score integer to the second view. This way both the views will work on the same value (you can copy paste the code below and try yourself).
import SwiftUI
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen(score: self.$input.score)) {
Text("Next View")
}
}
}
}
}
struct secondScreen: View {
#Binding var score: Int
var body: some View {
VStack {
Text("Button has been pushed \(score)")
Button(action: {self.score += 1
}) {
Text("Adder")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
If you really need it you can even pass the entire UserInput object to the second view:
import SwiftUI
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput() //please, note the difference between this...
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen(input: self.input)) {
Text("Next View")
}
}
}
}
}
struct secondScreen: View {
#ObservedObject var input: UserInput //... and this!
var body: some View {
VStack {
Text("Button has been pushed \(input.score)")
Button(action: {self.input.score += 1
}) {
Text("Adder")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
I tried a lot of different approaches on how to pass data from one view to another and came up with a solution that fits for simple and complex views / view models.
Version
Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)
This solution works with iOS 14.0 upwards, because you need the .onChange() view modifier. The example is written in Swift Playgrounds. If you need an onChange like modifier for lower versions, you should write your own modifier.
Main View
The main view has a #StateObject viewModel handling all of the views logic, like the button tap and the "data" (testingID: String) -> Check the ViewModel
struct TestMainView: View {
#StateObject var viewModel: ViewModel = .init()
var body: some View {
VStack {
Button(action: { self.viewModel.didTapButton() }) {
Text("TAP")
}
Spacer()
SubView(text: $viewModel.testingID)
}.frame(width: 300, height: 400)
}
}
Main View Model (ViewModel)
The viewModel publishes a testID: String?. This testID can be any kind of object (e.g. configuration object a.s.o, you name it), for this example it is just a string also needed in the sub view.
final class ViewModel: ObservableObject {
#Published var testingID: String?
func didTapButton() {
self.testingID = UUID().uuidString
}
}
So by tapping the button, our ViewModel will update the testID. We also want this testID in our SubView and if it changes, we also want our SubView to recognize and handle these changes. Through the ViewModel #Published var testingID we are able to publish changes to our view. Now let's take a look at our SubView and SubViewModel.
SubView
So the SubView has its own #StateObject to handle its own logic. It is completely separated from other views and ViewModels. In this example the SubView only presents the testID from its MainView. But remember, it can be any kind of object like presets and configurations for a database request.
struct SubView: View {
#StateObject var viewModel: SubviewModel = .init()
#Binding var test: String?
init(text: Binding<String?>) {
self._test = text
}
var body: some View {
Text(self.viewModel.subViewText ?? "no text")
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
.onAppear(perform: { self.viewModel.updateText(text: test) })
}
}
To "connect" our testingID published by our MainViewModel we initialize our SubView with a #Binding. So now we have the same testingID in our SubView. But we don't want to use it in the view directly, instead we need to pass the data into our SubViewModel, remember our SubViewModel is a #StateObject to handle all the logic. And we can't pass the value into our #StateObject during view initialization. Also if the data (testingID: String) changes in our MainViewModel, our SubViewModel should recognize and handle these changes.
Therefore we are using two ViewModifiers.
onChange
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
The onChange modifier subscribes to changes in our #Binding property. So if it changes, these changes get passed to our SubViewModel. Note that your property needs to be Equatable. If you pass a more complex object, like a Struct, make sure to implement this protocol in your Struct.
onAppear
We need onAppear to handle the "first initial data" because onChange doesn't fire the first time your view gets initialized. It is only for changes.
.onAppear(perform: { self.viewModel.updateText(text: test) })
Ok and here is the SubViewModel, nothing more to explain to this one I guess.
class SubviewModel: ObservableObject {
#Published var subViewText: String?
func updateText(text: String?) {
self.subViewText = text
}
}
Now your data is in sync between your MainViewModel and SubViewModel and this approach works for large views with many subviews and subviews of these subviews and so on. It also keeps your views and corresponding viewModels enclosed with high reusability.
Working Example
Playground on GitHub:
https://github.com/luca251117/PassingDataBetweenViewModels
Additional Notes
Why I use onAppear and onChange instead of only onReceive: It appears that replacing these two modifiers with onReceive leads to a continuous data stream firing the SubViewModel updateText multiple times. If you need to stream data for presentation, it could be fine but if you want to handle network calls for example, this can lead to problems. That's why I prefer the "two modifier approach".
Personal Note: Please don't modify the StateObject outside the corresponding view's scope. Even if it is somehow possible, it is not what its meant for.
My question is still related to how to pass data between two views but I have a more complicated JSON data set and I am running into problems both with the passing the data and with it's initialization. I have something that works but I am sure it is not correct. Here is the code. Help!!!!
/ File: simpleContentView.swift
import SwiftUI
// Following is the more complicated #ObservedObject (Buddy and class Buddies)
struct Buddy : Codable, Identifiable, Hashable {
var id = UUID()
var TheirNames: TheirNames
var dob: String = ""
var school: String = ""
enum CodingKeys1: String, CodingKey {
case id = "id"
case Names = "Names"
case dob = "dob"
case school = "school"
}
}
struct TheirNames : Codable, Identifiable, Hashable {
var id = UUID()
var first: String = ""
var middle: String = ""
var last: String = ""
enum CodingKeys2: String, CodingKey {
case id = "id"
case first = "first"
case last = "last"
}
}
class Buddies: ObservableObject {
#Published var items: [Buddy] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {UserDefaults.standard.set(encoded, forKey: "Items")}
}
}
#Published var buddy: Buddy
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([Buddy].self, from: items) {
self.items = decoded
// ??? How to initialize here
self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
return
}
}
// ??? How to initialize here
self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
self.items = []
}
}
struct simpleContentView: View {
#Environment(\.presentationMode) var presentationMode
#State private var showingSheet = true
#ObservedObject var buddies = Buddies()
var body: some View {
VStack {
Text("Simple View")
Button(action: {self.showingSheet.toggle()}) {Image(systemName: "triangle")
}.sheet(isPresented: $showingSheet) {
simpleDetailView(buddies: self.buddies, item: self.buddies.buddy)}
}
}
}
struct simpleContentView_Previews: PreviewProvider {
static var previews: some View {
simpleContentView()
}
}
// End of File: simpleContentView.swift
// This is in a separate file: simpleDetailView.swift
import SwiftUI
struct simpleDetailView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var buddies = Buddies()
var item: Buddy
var body: some View {
VStack {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Text("First Name = \(item.TheirNames.first)")
Button(action: {self.presentationMode.wrappedValue.dismiss()}){ Text("return"); Image(systemName: "gobackward")}
}
}
}
// ??? Correct way to make preview call
struct simpleDetailView_Previews: PreviewProvider {
static var previews: some View {
// ??? Correct way to call here
simpleDetailView(item: Buddy(TheirNames: TheirNames(first: "", middle: "", last: ""), dob: "", school: "") )
}
}
// end of: simpleDetailView.swift
Using directly #State variable will help you to achieve this, but if you want to sync that variable for both the screens using view model or #Published, this is what you can do. As the #State won't be binded to the #Published property. To achieve this follow these steps.
Step1: - Create a delegate to bind the value on pop or disappearing.
protocol BindingDelegate {
func updateOnPop(value : Int)
}
Step 2:- Follow the code base for Content View
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View , BindingDelegate {
#ObservedObject var input = UserInput()
#State var navIndex : Int? = nil
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1}) {
Text("Adder")
}
ZStack {
NavigationLink(destination: secondScreen(score: self.$input.score,
del: self, navIndex: $navIndex),
tag: 1, selection: $navIndex) {
EmptyView()
}
Button(action: {
self.navIndex = 1
}) {
Text("Next View")
}
}
}
}
}
func updateOnPop(value: Int) {
self.input.score = value
}
}
Step 3: Follow these steps for secondScreen
final class ViewModel : ObservableObject {
#Published var score : Int
init(_ value : Int) {
self.score = value
}
}
struct secondScreen: View {
#Binding var score: Int
#Binding var navIndex : Int?
#ObservedObject private var vm : ViewModel
var delegate : BindingDelegate?
init(score : Binding<Int>, del : BindingDelegate, navIndex : Binding<Int?>) {
self._score = score
self._navIndex = navIndex
self.delegate = del
self.vm = ViewModel(score.wrappedValue)
}
private var btnBack : some View { Button(action: {
self.delegate?.updateOnPop(value: self.vm.score)
self.navIndex = nil
}) {
HStack {
Text("Back")
}
}
}
var body: some View {
VStack {
Text("Button has been pushed \(vm.score)")
Button(action: {
self.vm.score += 1
}) {
Text("Adder")
}
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}