SwiftUI - Fast sequences of explicit animation produces strange animations - ios

The following code contains a list of text and a button to add more texts. When an item is added, the scroll view should scroll to the second to last (for simplicity in this example) item.
import SwiftUI
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
createView()
}
private func createView() -> some View {
return VStack {
ScrollView {
ScrollViewReader { scrollProxy in
VStack {
ForEach(viewModel.ids, id: \.self) { id in
Text(viewModel.texts[id]!)
.padding()
}
}.onAppear {
viewModel.scrollProxy = scrollProxy
}
}
}
Button("Add") {
viewModel.onAdd()
}.padding()
}
}
}
class ViewModel: ObservableObject {
#Published var ids: [UUID]
#Published var texts: [UUID: String]
#Published var responderID: UUID?
var scrollProxy: ScrollViewProxy?
init() {
let id = UUID()
self.ids = [id]
self.texts = [id: "0"]
}
func onAdd() {
let lastID = ids[ids.count - 1]
// create new block
let newID = UUID()
ids.append(newID)
self.texts[newID] = String(ids.count)
withAnimation {
scrollProxy?.scrollTo(lastID)
}
}
}
When tapping the button slowly, the scroll animation works fine.
But when the button is tapped quickly, the animation is ... broken? The animation slows to a crawl, taking a long time to complete.
So my question is, is there another way to animation the scrolling? I've tried using CADisplayLink, but the result is also pretty bad...

Related

Problems with SwiftUI not redrawing views

Working on my first SwiftUI project, and as I started moving some of my more complex views into their own view structs I started getting problems with the views not being redrawn.
As an example, I have the following superview:
struct ContainerView: View {
#State var myDataObject: MyDataObject?
var body: some View {
if let myDataObject = myDataObject {
TheSmallerView(myDataObject: myDataObject)
.padding(.vertical, 10)
.frame(idealHeight: 10)
.padding(.horizontal, 8)
.onAppear {
findRandomData()
}
}
else {
Text("No random data found!")
.onAppear {
findRandomData()
}
}
}
private func findRandomData() {
myDataObject = DataManager.shared.randomData
}
}
Now when this first gets drawn I get the Text view on screen as the myDataObject var is nil, but the .onAppear from that gets called, and myDataStruct gets set with an actual struct. I've added breakpoints in the body variable, and I see that when this happens it gets called again and it goes into the first if clause and fetches the "TheSmallerView" view, but nothing gets redrawn on screen. It still shows the Text view from before.
What am I missing here?
EDIT: Here's the relevant parts of TheSmallerView:
struct TheSmallerView: View {
#ObservedObject var myDataObject: MyDataObject
EDIT2: Fixed the code to better reflect my actual code.
Try declaring #Binding var myDataStruct: MyDataStruct inside the TheSmallerView view and pass it like this: TheSmallerView(myDataStruct: $myDataStruct) from ContainerView
You are using #ObservedObject in the subview, but that property wrapper is only for classes (and your data is a struct).
You can use #State instead (b/c the data is a struct).
Edit:
The data isn't a struct.
Because it is a class, you should use #StateObject instead of #State.
In lack of complete code I created this simple example based on OPs code, which works fine the way it is expected to. So the problem seems to be somewhere else.
class MyDataObject: ObservableObject {
#Published var number: Int
init() {
number = Int.random(in: 0...1000)
}
}
struct ContentView: View {
#State var myDataObject: MyDataObject?
var body: some View {
if let myDataObject = myDataObject {
TheSmallerView(myDataObject: myDataObject)
.onAppear {
findRandomData()
}
}
else {
Text("No random data found!")
.onAppear {
findRandomData()
}
}
}
private func findRandomData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
myDataObject = MyDataObject()
}
}
}
struct TheSmallerView: View {
#ObservedObject var myDataObject: MyDataObject
var body: some View {
Text("The number is: \(myDataObject.number)")
}
}

SwiftUI Combine: TabView is not updating on selection when property stored in different viewmodel

I'm working on Tabview with page style and I want to scroll tabview on button actions. Buttons are added inside NavigationMenu.
NavigationMenu view and NavigationModel(ViewModel) are separated from a parent.
Selection handling is done inside NavigationModel.
On tab page swipe I'm able to see the change in NavigationMenu which is fine.
But if I tap on buttons the tabview page is not swiping. Even I receive change event on method onReceive.
Code:
import SwiftUI
import Combine
final class NavigationModel: ObservableObject {
#Published var selectedItem = ""
#Published var items: [String] = [
"Button 1", "Button 2", "Button 3"
]
}
struct NavigationMenu: View {
#ObservedObject var viewModel: NavigationModel
var body: some View {
HStack {
ForEach(0..<3, id: \.self) { index in
let title = viewModel.items[index]
Button {
viewModel.selectedItem = title
} label: {
Text(title)
.font(.system(.body))
.padding()
.foregroundColor(
viewModel.selectedItem == title ? .white : .black
)
.background(viewModel.selectedItem == title ? .black : .yellow)
}
}
}
}
}
final class TabViewModel: ObservableObject {
var navModel = NavigationModel()
}
struct TabviewWithMenuView: View {
#ObservedObject var viewModel = TabViewModel()
var body: some View {
parentView
}
private var parentView: some View {
VStack(spacing: 0) {
Spacer()
NavigationMenu(viewModel: viewModel.navModel)
pageView
}
.onReceive(viewModel.navModel.$selectedItem) { output in
print("Button tapped:", output)
}
}
private var pageView: some View {
TabView(selection: $viewModel.navModel.selectedItem) {
ForEach(0..<3, id: \.self) { index in
let tag = viewModel.navModel.items[index]
item(tag: tag)
.tag(tag)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.transition(.slide)
}
private func item(tag: String) -> some View {
VStack {
Text("PAGE: " + tag)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
}
}
Image:
ObservableObject inside ObservableObject is not observed, we need to observe explicitly the instance which is changed.
A possible solution in this case is to separate PageView and inject navigation view model to it so it would be observed.
Tested with Xcode 13.3 / iOS 15.4
Here is main part:
NavigationMenu(viewModel: viewModel.navModel)
PageView(navModel: viewModel.navModel)
...
struct PageView: View {
#ObservedObject var navModel: NavigationModel
var body: some View {
pageView
}
// ....
}
Test module in project is here

SwiftUI Infinite Subview Hierarchy and Breadcrumbs

what I am trying to achieve is creating a hierarchical view. I understand that iOS simply doesn't like to use breadcrumbs but I need to navigate from a main view in to deeper subviews. they need to be nested and infinite.
you can see what I've done so far in the code and gif below. As I'm a beginner developer I'm not sure if this is the right way to achieve this kind of structure (infinite sub-views nested inside sub-views). Also when I navigate back in views, added buttons(struct A) disappears. What seems to be the problem?
Thanks in advance!
code in action gif
import SwiftUI
struct A: View, Identifiable {
#EnvironmentObject var documentB: classB
var id: Int
var text: String
var destinationLink: B?
var body: some View {
NavigationLink(destination: self.destinationLink) {
VStack{
Rectangle()
.frame(width: 35, height:25)
.background(Color.red)
Text("\(text)")
}
}
}
}
struct B: View, Identifiable {
#EnvironmentObject var documentB: classB
#State var arrayA: [A] = []
var id: Int
var text: String
var mainText: String = "Placeholder"
var body: some View {
NavigationView {
VStack {
Spacer()
ForEach(arrayA){ item in
item
}
Spacer()
Button(action: {
let newB = B(id:self.documentB.arrayB.count+1, text:"B \(self.documentB.arrayB.count+1)")
self.documentB.arrayB.append(newB)
self.arrayA.append(A(id:self.arrayA.count+1, text:"AA \(self.arrayA.count+1)", destinationLink: newB))
}) {
Text("Add A \(self.arrayA.count), B Count: \(self.documentB.arrayB.count)")
}
}
.navigationBarTitle(text)
}
}
}
class classB: ObservableObject {
#Published var arrayB: [B] = [B(id:1, text:"MainView")]
}
struct ContentView: View {
#ObservedObject var documentB = classB()
var body: some View {
VStack {
documentB.arrayB[0]
}
.environmentObject(documentB)
}
}
You just need to move NavigationView into ContentView, because the only one is needed on one view hierarchy, so
struct ContentView: View {
#ObservedObject var documentB = classB()
var body: some View {
NavigationView { // << move it here from B
VStack {
documentB.arrayB[0]
}
}
.environmentObject(documentB)
}
}

Reload view when #Published is changed

I want to be able to scan barcodes, then show the barcode at the bottom of the screen in a sheet, or a separate view.
When I update the 'barcode' variable which is #Published and accessed in other methods with #ObserveableObject, it doesn't update the view with the data from the barcode.
Content View
class ScannedCode: ObservableObject {
#Published var barcode = ""
}
struct ContentView: View {
#ObservedObject var barcode = ScannedCode()
var body: some View {
ZStack {
ScannerView()
FoundItemSheet()
}
}
}
Scanner View - scannedCode is linked to the #Published variable, so when this changes, I want it to reload FoundItemSheet() as a barcode has been found by the scanner
class Coordinator: BarcodeScannerCodeDelegate, BarcodeScannerErrorDelegate {
#ObservedObject var scannedCode = ScannedCode()
private var scannerView: ScannerView
init(_ scannerView: ScannerView) {
self.scannerView = scannerView
}
func scanner(_ controller: BarcodeScannerViewController, didCaptureCode code: String, type: String) {
self.scannedCode.barcode = code
controller.resetWithError(message: "Error message")
}
func scanner(_ controller: BarcodeScannerViewController, didReceiveError error: Error) {
print(error)
}
}
FoundItemSheet() Loads BottomSheetView which is shown at the bottom of the screen in a box over the camera. I want this to update with the barcode data when it's found.
struct FoundItemSheet: View {
#State private var bottomSheetShown = false
var body: some View {
GeometryReader { geometry in
BottomSheetView(
isOpen: self.$bottomSheetShown,
maxHeight: geometry.size.height * 0.7
) {
Color.blue
}
}.edgesIgnoringSafeArea(.all)
}
}
BottomSheetView - I have declared the #Published barcode variable in here, so when it changes, I want contentsInSlide to reload with the new barcode.
struct BottomSheetView<Content: View>: View {
#Binding var isOpen: Bool
#State var showingDetail = false
#ObservedObject var scannedCode = ScannedCode()
....
private var contentInSlide: some View {
VStack {
Text("Value is: \(scannedCode.barcode)") //Doesn't show any value for the barcode
.foregroundColor(ColorManager.beautytruthGreen)
.font(.system(size: 22, weight: .medium))
Button(action: {
self.showingDetail.toggle()
}
}
The problem is that all of your views create their own ScannedCode instances. You need to create it only on the type that creates all other views and inject the same instance into each one of them.
From your code snippets it isn't really clear which view is the parent of which, so it's hard to give you a definitive answer with a code example, but in general, you should never create an #ObservableObject in a view itself, since that object will be recreated as soon as the view is reloaded. Instead, you should be creating your #ObservableObject on the view model or the parent view and injecting it into the child view that needs reloading.
Using below code, whenever scannedCode on the ParentView is updated, it reloads its ChildView with the updated ScannedCode.
struct ParentView: View {
#Published var scannedCode = ScannedCode()
var body: some View {
ChildView(scannedCode: scannedCode)
}
}
struct ChildView: View {
#ObservedObject var scannedCode: ScannedCode
var body: some View {
Text(scannedCode.barCode)
}
}

Content inside Picker overlaps when the font size increases in SwiftUI

struct ContentView: View {
#State private var selectedNumber = 0
// var numbersArray - This will be the array
var body: some View {
VStack {
Picker("Number Picker", selection: $selectedNumber) {
ForEach(0..<Int(numbersArray.count)) {
Text("\($0 + 1)").font(.system(size: 60))
}
}
}
}
}
I am creating a Picker for selecting numbers in Watchkit. When I try to increase the font size the numbers are overlapping. How to make the Picker content resize automatically so that the contents does not overlap.
You could add something like Spacer():
struct TestSwiftUIView: View {
#State private var selectedNumber = 0
var numbersArray = [1,2,3,4,5,6]
var body: some View {
VStack {
Picker("Number Picker", selection: $selectedNumber) {
ForEach(0..<Int(numbersArray.count)) {
Spacer()
Text("\($0 + 1)").font(.system(size: 60))
}
}
}
}
}
It's just my first Idea, probably not the best solution.

Resources