I have a LazyVStack which I would like to only update one view and not have all others on screen reload. With more complex cells this causes a big performance hit. I have included sample code
import SwiftUI
struct ContentView: View {
#State var items = [String]()
var body: some View {
ScrollView {
LazyVStack {
ForEach(self.items, id: \.self) { item in
Button {
if let index = self.items.firstIndex(where: {$0 == item}) {
self.items[index] = "changed \(index)"
}
} label: {
cell(text: item)
}
}
}
}
.onAppear {
for _ in 0...200 {
self.items.append(NSUUID().uuidString)
}
}
}
}
struct cell: View {
let text: String
init(text: String) {
self.text = text
print("init cell", text)
}
var body: some View {
Text(text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As you can see even when only changing 1 cell the init gets called for every cell. Is there anyway to avoid this?
Here is a working code, there is some points to mention, View in SwiftUI would get initialized here and there or anytime SwiftUI thinks it needed! But the body of View would get computed if really some value in body changed. It is planed to work like that, there is some exceptions as well. Like body get computed even the values that used in the body were as before with no change, I do not want inter to that topic! But in your example and in your issue, we want SwiftUI renders only the changed View, for this goal the down code works well without issue, but as you can see I used VStack, if we change VStack to LazyVStack, SwiftUI would renders some extra view due its undercover codes, and if you scroll to down and then to up, it would forget all rendered view and data in memory and it will try to render the old rendered views, so it is the nature of LazyVStack, we cannot do much about it. Apple want LazyVStack be Lazy. But you can see that LazyVStack would not render all views, but some of them that needs to works. we cannot say or know how much views get rendered in Lazy way, but for sure not all of them.
let initializingArray: () -> [String] = {
var items: [String] = [String]()
for _ in 0...200 { items.append(UUID().uuidString) }
return items
}
struct ContentView: View {
#State var items: [String] = initializingArray()
var body: some View {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
Button(action: {
if let index = self.items.firstIndex(where: { $0 == item }) {
items[index] = "changed \(index)"
}
}, label: {
ItemView(item: item)
})
}
}
}
}
}
struct ItemView: View {
let item: String
var body: some View {
print("Rendering row for:", item)
return Text(item)
}
}
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!)
}
Goal: To use a common header View containing a shared title Text().
Scenario: I have multiple Views that share a common tab space within the one container tab View that contains a struct Header that is to be shared.
👉 This is a (many : 1) scenario.
Note: I don't want to use a NavigationView because it screws up landscape mode. A simple small header View is fine. I just need to populate the shared Title space amongst the member Views.
I don't want to merely add duplicate headers (having exactly the same layout) for each member View.
Several ideas: I need the header to respond to the 'change of title' event so I can see the new title.
So I believe I could use 1) #Binder(each member View) --> #State (shared Header View) or 2) #Environment.
I don't know how I could fit #1 into this particular scenario.
So I'm playing with #2: Environment Object.
DesignPattern: Main Header View's title set by multiple Views so the Header View is not aware of the multiple Views:
I'm not getting the EnvironmentObject paradigm to work.
Here's the codes...
MainView:
import SwiftUI
// Need to finish this.
class NYTEnvironment {
var title = "Title"
var msg = "Mother had a feeling..."
}
class NYTSettings: ObservableObject {
#Published var environment: NYTEnvironment
init() {
self.environment = NYTEnvironment()
}
}
struct NYTView: View {
var nytSettings = NYTSettings()
#State var selectionDataSegmentIndex = 0
var bindingDataSourceSegment: Binding<Int> {
.init(get: {
selectionDataSegmentIndex
}, set: {
selectionDataSegmentIndex = $0
})
}
var body: some View {
let county = 0; let state = 1; let states = 2
VStack {
NYTHeaderView()
SegmentAndDataPickerVStack(spacing: 10) {
if let segments = Source.NYT.dataSegments {
Picker("NYT Picker", selection: bindingDataSourceSegment) {
ForEach(segments.indices, id: \.self) { (index: Int) in
Text(segments[index])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
if selectionDataSegmentIndex == county {
NYTCountyView()
} else if selectionDataSegmentIndex == state {
NYTStateView()
} else if selectionDataSegmentIndex == states {
NYTStatesView()
}
Spacer()
}.environmentObject(nytSettings)
}
struct TrailingItem: View {
var body: some View {
Button(action: {
print("Info")
}, label: {
Image(systemName: "info.circle")
})
}
}
}
// ====================================================================================
struct NYTHeaderView: View {
#EnvironmentObject var nytSettings: NYTSettings
var body: some View {
ZStack {
Color.yellow
Text(nytSettings.environment.title)
}.frame(height: Header.navigationBarHeight)
}
}
Revision: I've added EnvironmentObject modifiers to the memberViews():
if selectionDataSegmentIndex == county {
NYTCountyView().environmentObject(NYTSettings())
} else if selectionDataSegmentIndex == state {
NYTStateView().environmentObject(NYTSettings())
} else if selectionDataSegmentIndex == states {
NYTStatesView().environmentObject(NYTSettings())
}
...
One of the member Views that's within the Main Container/Tab View (per above):
struct NYTCountyView: View {
#ObservedObject var dataSource = NYTCountyModel()
#EnvironmentObject var nytSettings: NYTSettings
...
...
}.onAppear {
nytSettings.environment.title = "Selected Counties"
if dataSource.revisedCountyElementListAndDuration == nil {
dataSource.getData()
}
}
Spacer()
...
}
Here's the compile-time error:
Modus Operandi: Set the title w/in header per member View upon .onAppear().
Problem: I'm not getting any title; just the default "Title" value.
Question: Am I on the right track? If so, what am I missing?
or... is there an alternative?
The whole problem boils down to a 'Many : 1' paradigm.
I got this revelation via taking a break and going for a walk.
So this is the proverbial 'round peg in a square hole' scenario.
What I needed was a lightly coupled relationship where the origin of the title value isn't required. Hence the use of the Notification paradigm.
The header view's title is the receiver and hence I used the .onReceive modifier:
struct NYTHeaderView: View {
#State private var title: String = ""
var body: some View {
ZStack {
Color.yellow
Text(title).onReceive(NotificationCenter.default.publisher(for: .headerTitle)) {note in
title = note.object as? String ?? "New York Times"
}
}.frame(height: Header.navigationBarHeight)
}
}
This sounds like what SwiftUI preferences was built to solve. The preferences are values collected and reduced from children for some distant ancestor to use. One notable example of this is how NavigationView gets its title - the title is set on the child, not on the NavigationView itself:
NavigationView {
Text("I am a simple view")
.navigationTitle("Title")
}
So, in your case you have some kind of title (simplified to String for brevity) that each child view might want to set. So you'd define a TitlePreferenceKey like so:
struct TitlePreferenceKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
Here, the reduce function is simply applying the last value it sees from descendants, but since you'd only ever have one child view selected it should work.
Then, to use it, you'd have something like this:
struct NYTView: View {
#State var title = ""
#State var selection = 0
var body: some View {
VStack {
Text(title)
Picker("", selection: $selection) {
Text("SegmentA").tag(0)
Text("SegmentB").tag(1)
}
switch selection {
case 0: NYTCountyView()
case 1: NYTStateView()
.preference(key: TitlePreferenceKey.self, value: "State view")
default: EmptyView()
}
}
.onPreferenceChange(TitlePreferenceKey.self) {
self.title = $0
}
}
struct NYTCountyView: View {
#State var selectedCounty = "..."
var body: some View {
VStack {
//...
}
.preference(key: TitlePreferenceKey.self, value: selectedCounty)
}
}
So, a preference can be set by the parent of, as in the example of NYTStateView, or by the child with the value being dynamic, as in the example of NYTCountyView
I have a very simple schoolbook example of a SwiftUI List view that renders items from data in an array. Data in the array is Identifiable. But, when I change the the data in the array, add or remove a item then all rows in the list view are recreated. Is that correct? My understanding was that Identifiable should make sure that only the view in the list that are changed are recreated.
My list is inside a navigation view and each row links to a detail view. The problem is that since all items in the list are removed and recreated every time the data is changed then if that that happens when Im in a detail view (it's triggered by a notification) then Im thrown out back to the list.
What am I missing?
Edit: Added code example
This is my data struct:
struct Item: Identifiable {
let id: UUID
let name: String
init(name: String) {
self.id = UUID()
self.name = name
}
}
This is my ItemView
struct ItemView: View {
var item: Item
init(item: Item) {
self.item = item
print("ItemView created \(self.item.id)")
}
var body: some View {
Text(self.item.name)
}
}
An finally my list view:
struct KeyList: View {
#State var items = [Item(name: "123"), Item(name: "456"), Item(name: "789")]
var body: some View {
VStack {
List(self.items) { item in
ItemView(item: item)
}
Button(action: {
self.items.append(Item(name: "New"))
}) {
Text("Add")
}
}
}
}
When I press add it will print "ItemView created" 4 times. My understanding is that it should only do it 1 time?
Here is an example of how this could work. Tested and working on iOS 13.5
The List doesn't get recreated again when only one item is being removed. So this was accomplished.
About the poping of the View this has already been answered here:
SwiftUI ForEach refresh makes view pop
I have here a small workaround for this problem. Add the items you want to remove to an array. Then when going back, remove these items (Which will make the view pop) or go back programmatically and nothing gets removed
struct ContentView: View {
#State var text:Array<String> = ["a", "b", "c"]
var body: some View {
NavigationView() {
VStack() {
List() {
ForEach(self.text, id: \.self){ item in
NavigationLink(destination: SecondView(textItem: item, text: self.$text)) {
Text(item)
}
}
}
Button(action: {
self.text.remove(at: 0)
}){
Text("Remove \(self.text[0])")
}
}
}
}
}
struct SecondView: View {
#State var textItem: String
#Binding var text: Array<String>
#State var tmpArray: Array<String> = []
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack() {
Text(self.textItem)
Button(action: {
//Append to a tmp array which will later be used to determine what to remove
self.tmpArray.append(self.text[0])
}){
Text("Remove \(self.text[0])")
}
Button(action: {
if self.tmpArray.count > 0 {
//remove your stuff which will automatically pop the view
self.text.remove(at: 0)
} else {
// programmatically go back as nothing has been deleted
self.presentationMode.wrappedValue.dismiss()
}
}){
Text("Go Back")
}
}
}
}
I'm using a ForEach to parse a list of models and create a view for each of them, Each view contains a Button and a Text, the Button toggles a visibility state which should hide the text and change the Button's title (Invisible/Visible).
struct ContentView: View {
var colors: [MyColor] = [MyColor(val: "Blue"), MyColor(val: "Yellow"), MyColor(val: "Red")]
var body: some View {
ForEach(colors, id: \.uuid) { color in
ButtonColorView(color: color.val)
}
}
}
struct ButtonColorView: View {
var color: String
#State var visible = true
var body: some View {
if visible {
return AnyView( HStack {
Button("Invisible") {
self.visible.toggle()
}
Text(color)
})
} else {
return AnyView(
Button("Visible") {
self.visible.toggle()
}
)
}
}
}
class MyColor: Identifiable {
let uuid = UUID()
let val: String
init(val: String) {
self.val = val
}
}
Unfortunately it's not working, the views inside the ForEach do not change when the Button is pressed. I replaced the Foreach with ButtonColorView(color: colors[0].val) and it seems to work, so I'd say the problem is at ForEach.
I also tried breakpoints in ButtonColorView and it seems the view is called when the Button is triggered returning the right view, anyways the view does not update on screen.
So, am I using the ForEach in a wrong way ?
This problem occurs in a more complex app, but I tried to extract it in this small example. To summarize it: I need ButtonColorView to return different Views depending of its state (visibility in this case)
PS: I'm using Xcode 11 Beta 6
You are using ForEach correctly. I think it's the if statement within ButtonColorView's body that's causing problems. Try this:
struct ButtonColorView: View {
var color: String
#State var visible = true
var body: some View {
HStack {
Button(visible ? "Invisible" : "Visible") {
self.visible.toggle()
}
if visible {
Text(color)
}
}
}
}
You can also try something like this:
struct ButtonColorView: View {
var color: String
#State var visible = true
var body: some View {
HStack {
if visible {
HStack {
Button("Invisible") {
self.visible.toggle()
}
Text(color)
}
} else {
Button("Visible") {
self.visible.toggle()
}
}
}
}
}
I can do a static List like
List {
View1()
View2()
}
But how do i make a dynamic list of elements from an array?
I tried the following but got error: Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
let elements: [Any] = [View1.self, View2.self]
List {
ForEach(0..<elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
Is there any work around for this?
What I am trying to accomplish is a List contaning dynamic set of elements that are not statically entered.
Looks like the answer was related to wrapping my view inside of AnyView
struct ContentView : View {
var myTypes: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<myTypes.count) { index in
self.buildView(types: self.myTypes, index: index)
}
}
}
func buildView(types: [Any], index: Int) -> AnyView {
switch types[index].self {
case is View1.Type: return AnyView( View1() )
case is View2.Type: return AnyView( View2() )
default: return AnyView(EmptyView())
}
}
}
With this, i can now get view-data from a server and compose them. Also, they are only instanced when needed.
if/let flow control statement cannot be used in a #ViewBuilder block.
Flow control statements inside those special blocks are translated to structs.
e.g.
if (someBool) {
View1()
} else {
View2()
}
is translated to a ConditionalValue<View1, View2>.
Not all flow control statements are available inside those blocks, i.e. switch, but this may change in the future.
More about this in the function builder evolution proposal.
In your specific example you can rewrite the code as follows:
struct ContentView : View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<elements.count) { index in
if self.elements[index] is View1 {
View1()
} else {
View2()
}
}
}
}
}
You can use dynamic list of subviews, but you need to be careful with the types and the instantiation. For reference, this is a demo a dynamic 'hamburger' here, github/swiftui_hamburger.
// Pages View to select current page
/// This could be refactored into the top level
struct Pages: View {
#Binding var currentPage: Int
var pageArray: [AnyView]
var body: AnyView {
return pageArray[currentPage]
}
}
// Top Level View
/// Create two sub-views which, critially, need to be cast to AnyView() structs
/// Pages View then dynamically presents the subviews, based on currentPage state
struct ContentView: View {
#State var currentPage: Int = 0
let page0 = AnyView(
NavigationView {
VStack {
Text("Page Menu").color(.black)
List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Page"), displayMode: .large)
}
}
)
let page1 = AnyView(
NavigationView {
VStack {
Text("Another Page Menu").color(.black)
List(["A", "B", "C", "D", "E"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Second Page"), displayMode: .large)
}
}
)
var body: some View {
let pageArray: [AnyView] = [page0, page1]
return Pages(currentPage: self.$currentPage, pageArray: pageArray)
}
}
You can do this by polymorphism:
struct View1: View {
var body: some View {
Text("View1")
}
}
struct View2: View {
var body: some View {
Text("View2")
}
}
class ViewBase: Identifiable {
func showView() -> AnyView {
AnyView(EmptyView())
}
}
class AnyView1: ViewBase {
override func showView() -> AnyView {
AnyView(View1())
}
}
class AnyView2: ViewBase {
override func showView() -> AnyView {
AnyView(View2())
}
}
struct ContentView: View {
let views: [ViewBase] = [
AnyView1(),
AnyView2()
]
var body: some View {
List(self.views) { view in
view.showView()
}
}
}
I found a little easier way than the answers above.
Create your custom view.
Make sure that your view is Identifiable
(It tells SwiftUI it can distinguish between views inside the ForEach by looking at their id property)
For example, lets say you are just adding images to a HStack, you could create a custom SwiftUI View like:
struct MyImageView: View, Identifiable {
// Conform to Identifiable:
var id = UUID()
// Name of the image:
var imageName: String
var body: some View {
Image(imageName)
.resizable()
.frame(width: 50, height: 50)
}
}
Then in your HStack:
// Images:
HStack(spacing: 10) {
ForEach(images, id: \.self) { imageName in
MyImageView(imageName: imageName)
}
Spacer()
}
SwiftUI 2
You can now use control flow statements directly in #ViewBuilder blocks, which means the following code is perfectly valid:
struct ContentView: View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0 ..< elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
}
}
SwiftUI 1
In addition to the accepted answer you can use #ViewBuilder and avoid AnyView completely:
#ViewBuilder
func buildView(types: [Any], index: Int) -> some View {
switch types[index].self {
case is View1.Type: View1()
case is View2.Type: View2()
default: EmptyView()
}
}
Is it possible to return different Views based on needs?
In short: Sort of
As it's fully described in swift.org, It is IMPOSSIIBLE to have multiple Types returning as opaque type
If a function with an opaque return type returns from multiple places, all of the possible return values must have the same type. For a generic function, that return type can use the function’s generic type parameters, but it must still be a single type.
So how List can do that when statically passed some different views?
List is not returning different types, it returns EmptyView filled with some content view. The builder is able to build a wrapper around any type of view you pass to it, but when you use more and more views, it's not even going to compile at all! (try to pass more than 10 views for example and see what happens)
As you can see, List contents are some kind of ListCoreCellHost containing a subset of views that proves it's just a container of what it represents.
What if I have a lot of data, (like contacts) and want to fill a list for that?
You can conform to Identifiable or use identified(by:) function as described here.
What if any contact could have a different view?
As you call them contact, it means they are same thing! You should consider OOP to make them same and use inheritance advantages. But unlike UIKit, the SwiftUI is based on structs. They can not inherit each other.
So what is the solution?
You MUST wrap all kind of views you want to display into the single View type. The documentation for EmptyView is not enough to take advantage of that (for now). BUT!!! luckily, you can use UIKit
How can I take advantage of UIKit for this
Implement View1 and View2 on top of UIKit.
Define a ContainerView with of UIKit.
Implement the ContainerView the way that takes argument and represent View1 or View2 and size to fit.
Conform to UIViewRepresentable and implement it's requirements.
Make your SwiftUI List to show a list of ContainerView
So now it's a single type that can represent multiple views
Swift 5
this seems to work for me.
struct AMZ1: View {
var body: some View {
Text("Text")
}
}
struct PageView: View {
let elements: [Any] = [AMZ1(), AMZ2(), AMZ3()]
var body: some View {
TabView {
ForEach(0..<elements.count) { index in
if self.elements[index] is AMZ1 {
AMZ1()
} else if self.elements[index] is AMZ2 {
AMZ2()
} else {
AMZ3()
}
}
}
import SwiftUI
struct ContentView: View {
var animationList: [Any] = [
AnimationDemo.self, WithAnimationDemo.self, TransitionDemo.self
]
var body: some View {
NavigationView {
List {
ForEach(0..<animationList.count) { index in
NavigationLink(
destination: animationIndex(types: animationList, index: index),
label: {
listTitle(index: index)
})
}
}
.navigationBarTitle("Animations")
}
}
#ViewBuilder
func listTitle(index: Int) -> some View {
switch index {
case 0:
Text("AnimationDemo").font(.title2).bold()
case 1:
Text("WithAnimationDemo").font(.title2).bold()
case 2:
Text("TransitionDemo").font(.title2).bold()
default:
EmptyView()
}
}
#ViewBuilder
func animationIndex(types: [Any], index: Int) -> some View {
switch types[index].self {
case is AnimationDemo.Type:
AnimationDemo()
case is WithAnimationDemo.Type:
WithAnimationDemo()
case is TransitionDemo.Type:
TransitionDemo()
default:
EmptyView()
}
}
}
enter image description here