so I'm having a bit of an issue here I'm hoping is easy to fix, just can't figure it out at the moment. I'm running a loop through some CoreData info (posts) and returning a grid of images, I want to be able to click these images and open up a fullScreenCover of the DetailView with the correct info in it. With the current code, the DetailView always shows the data from the first post. If I change it from a Button to a NavigationLink NavigationLink(destination: DetailView(post: post)), as commented out in the code, it works perfectly, but doesn't give me the fullScreenCover behaviour I would like. What am I doing wrong here? Thanks in advance!
#FetchRequest(entity: Post.entity(), sortDescriptors: []) var posts: FetchedResults<Post>
enum ActiveSheet: Identifiable {
case detail, addNew
var id: Int {
hashValue
}
}
#State var activeSheet: ActiveSheet?
var body: some View {
ForEach(posts.reversed(), id: \.self) { post in
VStack {
Button(action: { activeSheet = .detail }){
//NavigationLink(destination: DetailView(post: post)){
ZStack {
Image(uiImage: UIImage(data: post.mainImage ?? self.image)!)
VStack {
Text("\(post.title)")
Text("\(post.desc)")
}
}
}
.fullScreenCover(item: $activeSheet) { item in
switch item {
case .detail:
DetailView(post: post)
case .addNew:
AddNewView()
}
}
}
}
}
I've made the array of posts static for now instead of coming from Core Data and mocked the objects/structs so that I could test easily, but the principal should stay the same:
struct ContentView : View {
//#FetchRequest(entity: Post.entity(), sortDescriptors: []) var posts: FetchedResults<Post>
var posts : [Post] = [Post(title: "1", desc: "desc1"),
Post(title: "2", desc: "desc2"),
Post(title: "3", desc: "desc3")]
enum ActiveSheet: Identifiable {
case detail(post: Post)
case addNew
var id: UUID {
switch self {
case .detail(let post):
return post.id
default:
return UUID()
}
}
}
#State var activeSheet: ActiveSheet?
var body: some View {
ForEach(posts.reversed(), id: \.self) { post in
VStack {
Button(action: { activeSheet = .detail(post: post) }){
ZStack {
//Image(uiImage: UIImage(data: post.mainImage ?? self.image)!)
VStack {
Text("\(post.title)")
Text("\(post.desc)")
}
}
}
}
.fullScreenCover(item: $activeSheet) { item in
switch item {
case .detail(let post):
DetailView(post: post)
case .addNew:
AddNewView()
}
}
}
}
}
struct DetailView : View {
var post: Post
var body : some View {
Text("Detail \(post.id)")
}
}
struct AddNewView : View {
var body : some View {
Text("add")
}
}
struct Post : Hashable {
var id = UUID()
var title : String
var desc : String
}
The basic idea is that instead of creating the fullScreenCover on first render, you should create it in based on the activeSheet so that it gets created dynamically. You were on the right track using item: and activeSheet already -- the problem was it wasn't tied to the actual post, since you were just using the button to set activeSheet = .detail.
I've added an associated property to case detail that allows you to actually tie a post to it. Then, in fullScreenCover you can see that I use that associated value when creating the DetailView.
You may have to make slight adjustments to fit your Post model, but the concept will remain the same.
Related
Here is a simple list view of "Topic" struct items. The goal is to present an editor view when a row of the list is tapped. In this code, tapping a row is expected to cause the selected topic to be stored as "tappedTopic" in an #State var and sets a Boolean #State var that causes the EditorV to be presented.
When the code as shown is run and a line is tapped, its topic name prints properly in the Print statement in the Button action, but then the app crashes because self.tappedTopic! finds tappedTopic to be nil in the EditTopicV(...) line.
If the line "tlVM.objectWillChange.send()" is uncommented, the code runs fine. Why is this needed?
And a second puzzle: in the case where the code runs fine, with the objectWillChange.send() uncommented, a print statement in the EditTopicV init() shows that it runs twice. Why?
Any help would be greatly appreciated. I am using Xcode 13.2.1 and my deployment target is set to iOS 15.1.
Topic.swift:
struct Topic: Identifiable {
var name: String = "Default"
var iconName: String = "circle"
var id: String { name }
}
TopicListV.swift:
struct TopicListV: View {
#ObservedObject var tlVM: TopicListVM
#State var tappedTopic: Topic? = nil
#State var doEditTappedTopic = false
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(tlVM.topics) { topic in
Button(action: {
tappedTopic = topic
// why is the following line needed?
tlVM.objectWillChange.send()
doEditTappedTopic = true
print("Tapped topic = \(tappedTopic!.name)")
}) {
Label(topic.name, systemImage: topic.iconName)
.padding(10)
}
}
}
Spacer()
}
.sheet(isPresented: $doEditTappedTopic) {
EditTopicV(tlVM: tlVM, originalTopic: self.tappedTopic!)
}
}
}
EditTopicV.swift (Editor View):
struct EditTopicV: View {
#ObservedObject var tlVM: TopicListVM
#Environment(\.presentationMode) var presentationMode
let originalTopic: Topic
#State private var editTopic: Topic
#State private var ic = "circle"
let iconList = ["circle", "leaf", "photo"]
init(tlVM: TopicListVM, originalTopic: Topic) {
print("DBG: EditTopicV: originalTopic = \(originalTopic)")
self.tlVM = tlVM
self.originalTopic = originalTopic
self._editTopic = .init(initialValue: originalTopic)
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save") {
editTopic.iconName = editTopic.iconName.lowercased()
tlVM.change(topic: originalTopic, to: editTopic)
presentationMode.wrappedValue.dismiss()
}
}
HStack {
Text("Name:")
TextField("name", text: $editTopic.name)
Spacer()
}
Picker("Color Theme", selection: $editTopic.iconName) {
ForEach(iconList, id: \.self) { icon in
Text(icon).tag(icon)
}
}
.pickerStyle(.segmented)
Spacer()
}
.padding()
}
}
TopicListVM.swift (Observable Object View Model):
class TopicListVM: ObservableObject {
#Published var topics = [Topic]()
func append(topic: Topic) {
topics.append(topic)
}
func change(topic: Topic, to newTopic: Topic) {
if let index = topics.firstIndex(where: { $0.name == topic.name }) {
topics[index] = newTopic
}
}
static func ex1() -> TopicListVM {
let tvm = TopicListVM()
tvm.append(topic: Topic(name: "leaves", iconName: "leaf"))
tvm.append(topic: Topic(name: "photos", iconName: "photo"))
tvm.append(topic: Topic(name: "shapes", iconName: "circle"))
return tvm
}
}
Here's what the list looks like:
Using sheet(isPresented:) has the tendency to cause issues like this because SwiftUI calculates the destination view in a sequence that doesn't always seem to make sense. In your case, using objectWillSend on the view model, even though it shouldn't have any effect, seems to delay the calculation of your force-unwrapped variable and avoids the crash.
To solve this, use the sheet(item:) form:
.sheet(item: $tappedTopic) { item in
EditTopicV(tlVM: tlVM, originalTopic: item)
}
Then, your item gets passed in the closure safely and there's no reason for a force unwrap.
You can also capture tappedTopic for a similar result, but you still have to force unwrap it, which is generally something we want to avoid:
.sheet(isPresented: $doEditTappedTopic) { [tappedTopic] in
EditTopicV(tlVM: tlVM, originalTopic: tappedTopic!)
}
I am passing a Person binding from the first view to the second view to the third view, when I update the binding value in the third view it pops back to the second view, I understand that SwiftUI updates the views that depend on the state value, but is poping the current view is the expected behavior or I am doing something wrong?
struct Person: Identifiable {
let id = UUID()
var name: String
var numbers = [1, 2]
}
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct SecondView: View {
#Binding var person: Person
var body: some View {
Form {
NavigationLink(destination: ThirdView(person: $person)) {
Text("Update Info")
}
}
}
}
struct ThirdView: View {
#Binding var person: Person
var body: some View {
Form {
Button(action: {
person.numbers.append(3)
}) {
Text("Append a new number")
}
}
}
}
When navigating twice you need to either use isDetailLink(false) or StackNavigationViewStyle, e.g.
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
.isDetailLink(false) // option 1
}
}
.navigationViewStyle(.stack) // option 2
}
}
SwiftUI works by updating the rendered views to match what you have in your state.
In this case, you first have a list that contains an element called Current Name. Using a NavigationLink you select this item.
You update the name and now that previous element no longer exists, it's been replaced by a new element called New Name.
Since Current Name no longer exists, it also cannot be selected any longer, and the view pops back to the list.
To be able to edit the name without popping back, you'll need to make sure that the item on the list is the same, even if the name has changed. You can do this by using an Identifiable struct instead of a String.
struct Person: Identifiable {
let id = UUID().uuidString
var name = "Current Name"
}
struct ParentView: View {
#State private var people = [Person()]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: ChildView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct ChildView: View {
#Binding var person: Person
var body: some View {
Button(action: {
person.name = "New Name"
}) {
Text("Update Name")
}
}
}
I have recently started my journey into iOS development learning swift and swift UI. I keep running into issues when it comes to app architecture. The problem i am trying to solve is this: Let's say I have an app where the user first selects a number and then presses next. The user selected number is supposed to represent the number of text fields that appear on the next view. For example, if the user selects 3 then 3 text fields will appear on the next view but if the user selects 5 then 5 texts fields will appear. Is the solution to just have a view for each case? Or is there some way to dynamically add objects to a view based on the user input. Can anyone explain how they would handle a case like this?
Views can get passed parameters (including in NavigationLink) that can determine what they look like. Here's a simple example with what you described:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(numberOfFields: numberOfFields)) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailView : View {
var numberOfFields : Int
var body: some View {
VStack {
ForEach(0..<numberOfFields) { index in
TextField("", text: .constant("Field \(index + 1)"))
}
}
}
}
Notice how numberOfFields is stored as #State in the parent view and then passed to the child view dynamically.
In general, it would probably be a good idea to visit some SwiftUI tutorials as this type of thing will be covered by most of them. Apple's official tutorials are here: https://developer.apple.com/tutorials/swiftui
Another very popular resource is Hacking With Swift: https://www.hackingwithswift.com/100/swiftui
Update, based on comments:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(textInputs: Array(repeating: "test", count: numberOfFields))) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct Model : Identifiable {
var id = UUID()
var text : String
}
class ViewModel : ObservableObject {
#Published var strings : [Model] = []
}
struct DetailView : View {
var textInputs : [String]
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(Array(viewModel.strings.enumerated()), id: \.1.id) { (index,text) in
TextField("", text: $viewModel.strings[index].text)
}
}.onAppear {
viewModel.strings = textInputs.map { Model(text: $0) }
}
}
}
When I use the "next article" button to jump to the article details page with index 3, I want to go directly back to the article list page instead of the article details page with index 2.I tried to search for methods to return to the specified page and destroy the page, but I didn't find them.How to achieve this effect in swiftui?Thanks.I guess the same scenario will happen in other mobile development, right?
The ArticleListView is :
struct ArticleListView: View {
#EnvironmentObject var modelData:ModelData
var body: some View {
NavigationView{
List{
ForEach(modelData.articleList){ article in
NavigationLink(destination:ArticleDetail(index:article.index)){
ArticleItem(index:article.index);
}
}
}
.listStyle(PlainListStyle())
}
}
}
The ArticleDetail is like this:
struct ArticleDetail: View {
#EnvironmentObject var modelData:ModelData
var index:Int
var body: some View {
VStack{
Text(modelData.articleList[index].htmlText)
NavigationLink(destination:ArticleDetail(index:self.index+1)){
Text("next article")
}
}
}
}
The Article/ArticleItemView/ModelData is like this:
struct Article:Identifiable{
var id = UUID()
var index:Int
var htmlText:String
}
struct ArticleItem: View {
#EnvironmentObject var modelData:ModelData
var index:Int
var body: some View {
Text(modelData.articleList[index].htmlText)
}
}
final class ModelData:ObservableObject {
#Published var articleList = [Article(index:0,htmlText: "first test text "),Article(index:1,htmlText: "second test text"),Article(index:2,htmlText: "third test text")]
}
This solution has some potential scalability issues, but it gets the basic job done:
struct Article {
var id = UUID()
}
struct ContentView: View {
var articles = [Article(), Article(), Article(), Article()]
#State private var activeId : UUID?
func activeBinding(id: UUID) -> Binding<Bool> {
.init { () -> Bool in
activeId == id
} set: { (newValue) in
activeId = newValue ? id : nil
}
}
var body: some View {
NavigationView {
VStack(alignment: .leading, spacing: 20) {
ForEach(articles, id: \.id) { article in
NavigationLink(destination: ArticleView(article: article,
articles: articles,
popToTop: { activeId = nil }),
isActive: activeBinding(id: article.id)) {
Text("Link to article: \(article.id)")
}
}
}
}
}
}
struct ArticleView : View {
var article : Article
var articles : [Article]
var popToTop: () -> Void
var body : some View {
VStack(alignment: .leading, spacing: 20) {
Text("Current: \(article.id)")
Button("Pop") {
popToTop()
}
ForEach(articles, id: \.id) { listArticle in
NavigationLink(destination: ArticleView(article: article, articles: articles, popToTop: popToTop)) {
Text("Link to article: \(listArticle.id)")
}
}
}
}
}
On the main page, the top-level article ID is stored in a #State variable. That is tied with a custom binding to an isActive property on the top-level link. Basically, when the article is active, the link is presented and when activeId is nil, the link becomes inactive, and pops to the top.
Because that's the top level view, any views lower in the stack will get popped off if that top-level NavigationLink is inactive.
popToTop is a function that gets passed down to the subsequent article views and gets called if the "Pop" button is pressed.
it's very strange issue as I cannot reproduce in isolated code, but I hope that someone may think about the reason. I have a view, let's say ContentView that has its ContentViewModel that is ObservedObject, and then there's another View ContentView2. And we have NavigationView in ContentView that wraps navigation link to ContentView2. And it's a bit weird, but when we do some changes that affect ContentViewModel, then NavigationView pops ContentView2 so that we end up in initial ContentView, but we didn't do anything like dismissing ContentView2 or tapping back button. I have a similar code to the one used in my project, but please note that in this code everything works fine:
func qrealm() -> Realm {
return try! Realm(configuration: .init( inMemoryIdentifier: "yw"))
}
class SomeRObj: Object {
#objc dynamic var name: String = ""
convenience init(name: String) {
self.init()
self.name = name
}
static var instance: SomeRObj {
return qrealm().objects(SomeRObj.self).first!
}
}
struct SomeRObjWrapped: Hashable {
var obj: SomeRObj
var prop: Int
}
class ContentViewModel: ObservableObject {
#Published var someRObj: [SomeRObjWrapped] = []
var any: Any?
init() {
let token = qrealm().objects(SomeRObj.self).observe { changes in
switch changes {
case let .initial(data), let .update(data, deletions: _, insertions: _, modifications: _):
let someObjs = data.map { SomeRObjWrapped(obj: $0, prop: Int.random(in: 1..<50)) }
self.someRObj = Array(someObjs)
default: break
}
}
self.any = token
}
}
struct ContentView: View {
#ObservedObject var model: ContentViewModel = ContentViewModel()
var body: some View {
NavigationView {
VStack {
ForEach(model.someRObj, id: \.self) { obj in
Heh(obj: obj.obj, pr: obj.prop)
}
NavigationLink(destination: ContentView2()) {
Text("Link")
}
}
}
}
}
struct Heh: View {
var obj: SomeRObj
var pr: Int
var body: some View {
Text("\(obj.name) \(pr)")
}
}
struct ContentView2: View {
var body: some View {
Button(action: { try! qrealm().write {
let elem = qrealm().objects(SomeRObj.self).randomElement()
elem?.name = "jotaro kujo"
}
}, label: { Text("Toggle") })
}
}
You can replace \.self with \.id:
ForEach(model.someRObj, id: \.id) { obj in
Heh(obj: obj.obj, pr: obj.prop)
}
Then every object will be identified by id and the ForEach loop will only refresh when the id is changed.
Thanks to pawello2222, I found the real reason behind it. I had a NavigationLink inside my List, so that each time there was a change NavigationLink is redrawn and it's state refreshed. I hope that it will be helpfull to someone, and the solution as pawello2222 wrote before is to identify view by id parameter.