if with ScrollTo in SwiftUI - swift

Sorry for the newbie question, but I'm trying to implement a scrollview with a logical text based on a preference value relating to an .id passed to the view. The code is something like this, but I keep getting a Type () cannot conform to 'View' error:
ScrollViewReader {proxy in
ScrollView (.vertical) {
if (preferences.scrollPosition != 0) {
withAnimation {
proxy.scrollTo(preferences.scrollPosition)
}
}

Updating my post: I was incorrect with the ordering
I'm just trying to put together a more concrete example for reference, is this similar to what you want/have?
import SwiftUI
class Preferences: ObservableObject {
#Published var scrollPosition: UnitPoint = .center
}
struct TestView: View {
#StateObject var preferences = Preferences()
var body: some View {
ScrollViewReader {proxy in
ScrollView (.vertical) {
}
.onAppear(perform: {
if (preferences.scrollPosition != .zero) {
withAnimation {
proxy.scrollTo(preferences.scrollPosition)
}
}
})
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}

Related

Changing scroll position from another view not working

I am working on a project where I need to reset a TabView's root view controllers (NavigationViews with Lists inside) when the TabViews selected item changes. This is pretty simple in UIKit, however in SwiftUI it doesn't seem that easy.
Let's say I have the following code:
class AppState: ObservableObject {
let objectWillChange = PassthroughSubject<AppState, Never>()
#Published var theScrollPosition: Int64? {
didSet {
print("Did set scroll position")
objectWillChange.send(self)
}
}
#Published var selectedTab: Tabs
{
didSet {
print("Tab switched, switching back to root view")
selectedItemID = nil
selectedRow = nil
theScrollPosition = -1
}
}
#Published var selectedItemID: Int64? {
didSet {
objectWillChange.send(self)
}
}
#Published var selectedRow: Int64? {
didSet {
objectWillChange.send(self)
}
}
}
struct ContentView: View {
#EnvironmentObject var state: AppState
var body: some View {
TabView {
View1().id(Tabs.Tab1)
View2().id(Tabs.Tab2)
View3().id(Tabs.Tab3)
}
}
}
struct View1: View {
#EnvironmentObject var state: AppState
#ObservedObject var viewModel: ListViewModel()
var body: some View {
ScrollViewReader { proxy in
List(viewModel.items) { item in
Section {
NavigationLink(destination:ListItemDetailView(item), tag: item.id, selection: state.selectedItemID) {
ListItemView(item)
}
}
}.onChange(of: self.state.selectedItemID) { newValue in
print("Scrolling to top")
proxy.scrollTo(0)
}
}
}
There may be some typos in the code as this is not the actual production code however the flow is this:
Selection state of TabView and NavigationLink are stored in a global EnvironmentObject. When the TabView selection changes, View1 should scroll back up to the top.
However, the onChange method is never called.
Next time, provide something runnable or at least a start...
But here is a example where view 1 will always scroll to the top, you are missing something like onAppear()
And you don't need to have AppState.theScrollPosition as a published, instead change to the right tab and have that view read the position or tag in the model.
import SwiftUI
struct ContentView: View {
var view: some View {
ScrollViewReader { proxy in
ScrollView {
VStack {
Text("TOP").id("topID")
Divider()
Spacer()
.frame(height:1000)
Text("BOTTOM").id("bottomID")
}
}
.onAppear {
proxy.scrollTo("topID")
}
}
}
var view2: some View {
VStack {
Text("View 2")
}
}
var body: some View {
TabView {
view.tag(0)
.tabItem {
Text("View 1")
}
view2.tag(1)
.tabItem {
Text("View 2")
}
}
}
}
Thanks, for some reason I was totally missing onAppear.

SwiftUI polymorphic behaviour not working for View

protocol BackgroundContent: View{
}
struct BlueDivider: BackgroundContent {
var body: some View {
Divider()
.frame(minHeight: 1)
.background(.blue)
}
}
struct RedDivider: BackgroundContent {
var body: some View {
Divider()
.frame(minHeight: 1)
.background(.red)
}
}
var p: BackgroundContent = BlueDivider()
// Use of protocol 'BackgroundContent' as a type must be written 'any BackgroundContent'
p = RedDivider()
This always ask me to use
var p: any BackgroundContent = BlueDivider()
Is there any way to use generic type which accept any kind view?
Actually, I want to use view as a state like #State private var bgView: BackgroundContent = BlueDivider() which i want to change at runtime like bgView = RedDivider()
I have made my custome view to place some other view at runtime by using this state.
For your specific problem you can do something like this here:
struct SwiftUIView: View {
#State var isRed = false
var body: some View {
Devider()
.frame(height: 1)
.background(isRed ? Color.red : Color.blue)
}
}
It is complicated but i have found a solution of this problem. First thing i have done with ObservableObject. Here is my example.
protocol BaseBackgroundContent {
var color: Color { get set }
}
class BlueContent: BaseBackgroundContent {
var color: Color = .blue
}
class RedContent: BaseBackgroundContent {
var color: Color = .red
}
And i created a custom view for Divider in this case.
struct CustomDivider: View {
var backgroundContent: any BaseBackgroundContent
var body: some View {
Divider()
.background(backgroundContent.color)
}
}
And now i used a viewModel which can be observable, and the protocol has to be Published.
class ExampleViewModel: ObservableObject {
#Published var backgroundContent: any BaseBackgroundContent = RedContent()
func change() {
backgroundContent = BlueContent()
}
}
Final step is the view. This is a exampleView. If you click the button you will see the BlueContent which was RedContent
struct Example: View {
#ObservedObject var viewModel = ExampleViewModel()
init() {
}
var body: some View {
VStack {
Text("Test")
CustomDivider(backgroundContent: viewModel.backgroundContent)
Button("Change") {
viewModel.change()
}
}
}
}

I'm trying to implement a view stack in swiftui and my #State objects are being reset for reasons that are unclear to me

I'm new to swiftui and doing an experiment with pushing and popping views with a stack. When I pop a view off the stack, the #State variable of the prior view has been reset and I don't understand why.
This demo code was tested on macos.
import SwiftUI
typealias Push = (AnyView) -> ()
typealias Pop = () -> ()
struct PushKey: EnvironmentKey {
static let defaultValue: Push = { _ in }
}
struct PopKey: EnvironmentKey {
static let defaultValue: Pop = {() in }
}
extension EnvironmentValues {
var push: Push {
get { self[PushKey.self] }
set { self[PushKey.self] = newValue }
}
var pop: Pop {
get { self[PopKey.self] }
set { self[PopKey.self] = newValue }
}
}
struct ContentView: View {
#State private var stack: [AnyView]
var body: some View {
currentView()
.environment(\.push, push)
.environment(\.pop, pop)
.frame(width: 600.0, height: 400.0)
}
public init() {
_stack = State(initialValue: [AnyView(AAA())])
}
private func currentView() -> AnyView {
if stack.count == 0 {
return AnyView(Text("stack empty"))
}
return stack.last!
}
public func push(_ content: AnyView) {
stack.append(content)
}
public func pop() {
stack.removeLast()
}
}
struct AAA : View {
#State private var data = "default text"
#Environment(\.push) var push
var body: some View {
VStack {
TextEditor(text: $data)
Button("Push") {
self.push(AnyView(BBB()))
}
}
}
}
struct BBB : View {
#Environment(\.pop) var pop
var body: some View {
VStack {
Button("Pop") {
self.pop()
}
}
}
}
If I type some text into the editor then hit Push, then Pop out of that view, I was expecting the text editor to maintain my changes but it reverts to the default text.
What am I missing?
Edit:
I guess this is really a question of how are NavigationView and NavigationLink implemented. This simple code does the what I'm trying to do:
import SwiftUI
struct MyView: View {
#State var text = "default text"
var body: some View {
VStack {
TextEditor(text: $text)
NavigationLink(destination: MyView()) {
Text("Push")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyView()
}
}
}
run that on iOS so you get a nav stack. edit the text, then push. Edit again if you want, then go back and see state is retained.
My code is trying to do the same thing in principle.
I'll share this attempt maybe it will help you create your version of this.
This all started with an attempt to create something like NavigationView and NavigationLink but being able to back track to a random View in the stack
I have a protocol where an object returns a View. Usually it is an enum. The view() references a View with a switch that provides the correct child View. The ContentView/MainView works almost like a storyboard and just presents whatever is designated in the current or path variables.
//To make the View options generic
protocol ViewOptionsProtocol: Equatable {
associatedtype V = View
#ViewBuilder func view() -> V
}
This is the basic navigation router that keep track of the main view and the NavigationLink/path. Which looks similar to what you want to do.
//A generic Navigation Router
class ViewNavigationRouter<T: ViewOptionsProtocol>: ObservableObject{
//MARK: Variables
var home: T
//Keep track of your current screen
#Published private (set) var current: T
//Keep track of the path
#Published private (set) var path: [T] = []
//MARK: init
init(home: T, current: T){
self.home = home
self.current = current
}
//MARK: Functions
//Control how you get to the screen
///Navigates to the nextScreen adding to the path/cookie crumb
func push(nextScreen: T){
//This is a basic setup just going forward
path.append(nextScreen)
}
///Goes back one step in the path/cookie crumb
func pop(){
//Use the stored path to go back
_ = path.popLast()
}
///clears the path/cookie crumb and goes to the home screen
func goHome(){
path.removeAll()
current = home
}
///Clears the path/cookie crumb array
///sets the current View to the desired screen
func show(nextScreen: T){
goHome()
current = nextScreen
}
///Searches in the path/cookie crumb for the desired View in the latest position
///Removes the later Views
///sets the nextScreen
func dismissTo(nextScreen: T){
while !path.isEmpty && path.last != nextScreen{
pop()
}
if path.isEmpty{
show(nextScreen: nextScreen)
}
}
}
It isn't an #Environment but it can easily be an #EnvrionmentObject and all the views have to be in the enum so the views are not completely unknown but it is the only way I have been able to circumvent AnyView and keep views in an #ViewBuilder.
I use something like this as the main portion in the main view body
router.path.last?.view() ?? router.current.view()
Here is a simple implementation of your sample
import SwiftUI
class MyViewModel: ViewNavigationRouter<MyViewModel.ViewOptions> {
//In some view router concepts the data that is /preserved/shared among the views is preserved in the router itself.
#Published var preservedData: String = "preserved"
init(){
super.init(home: .aaa ,current: .aaa)
}
enum ViewOptions: String, ViewOptionsProtocol, CaseIterable{
case aaa
case bbb
#ViewBuilder func view() -> some View{
ViewOptionsView(option: self)
}
}
struct ViewOptionsView: View{
let option: ViewOptions
var body: some View{
switch option {
case .aaa:
AAA()
case .bbb:
BBB()
}
}
}
}
struct MyView: View {
#StateObject var router: MyViewModel = .init()
var body: some View {
NavigationView{
ScrollView {
router.path.last?.view() ?? router.current.view()
}
.toolbar(content: {
//Custom back button
ToolbarItem(placement: .navigationBarLeading, content: {
if !router.path.isEmpty {
Button(action: {
router.pop()
}, label: {
HStack(alignment: .center, spacing: 2, content: {
Image(systemName: "chevron.backward")
if router.path.count >= 2{
Text(router.path[router.path.count - 2].rawValue)
}else{
Text(router.current.rawValue)
}
})
})
}
})
})
.navigationTitle(router.path.last?.rawValue ?? router.current.rawValue)
}.environmentObject(router)
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
struct AAA : View {
//This will reset because the view is cosmetic. the data needs to be preserved somehow via either persistence or in the router for sharing with other views.
#State private var data = "default text"
#EnvironmentObject var vm: MyViewModel
var body: some View {
VStack {
TextEditor(text: $data)
TextEditor(text: $vm.preservedData)
Button("Push") {
vm.push(nextScreen: .bbb)
}
}
}
}
struct BBB : View {
#EnvironmentObject var vm: MyViewModel
var body: some View {
VStack {
Button("Pop") {
vm.pop()
}
}
}
}

Passing data from extension in SwiftUI

I am building a complex interface in SwiftUI that I need to break into multiple extensions in order to be able to compile the code, but I can't figure out how to pass data between the extension and the body structure.
I made a simple code to explain it :
class Search: ObservableObject {
#Published var angle: Int = 10
}
struct ContentView: View {
#ObservedObject static var search = Search()
var body: some View {
VStack {
Text("\(ContentView.self.search.angle)")
aTest()
}
}
}
extension ContentView {
struct aTest: View {
var body: some View {
ZStack {
Button(action: { ContentView.search.angle = 11}) { Text("Button")}
}
}
}
}
When I press the button the text does not update, which is my issue. I really appreciate any help you can provide.
You can try the following:
struct ContentView: View {
#ObservedObject var search = Search()
var body: some View {
VStack {
Text("\(ContentView.self.search.angle)")
aTest // call as a computed property
}
}
}
extension ContentView {
var aTest: some View { // not a separate `struct` anymore
ZStack {
Button(action: { self.search.angle = 11 }) { Text("Button")}
}
}
}

SwiftUI Reorder list dynamic sections from another view

I have a simple List with sections that are stored inside an ObservableObject. I'd like to reorder them from another view.
This is my code:
class ViewModel: ObservableObject {
#Published var sections = ["S1", "S2", "S3", "S4"]
func move(from source: IndexSet, to destination: Int) {
sections.move(fromOffsets: source, toOffset: destination)
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#State var showOrderingView = false
var body: some View {
VStack {
Button("Reorder sections") {
self.showOrderingView = true
}
list
}
.sheet(isPresented: $showOrderingView) {
OrderingView(viewModel: self.viewModel)
}
}
var list: some View {
List {
ForEach(viewModel.sections, id: \.self) { section in
Section(header: Text(section)) {
ForEach(0 ..< 3, id: \.self) { _ in
Text("Item")
}
}
}
}
}
}
struct OrderingView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
NavigationView {
List {
ForEach(viewModel.sections, id: \.self) { section in
Text(section)
}
.onMove(perform: viewModel.move)
}
.navigationBarItems(trailing: EditButton())
}
}
}
But in the OrderingView when trying to move sections I'm getting this error: "Attempt to create two animations for cell". Likely it's because the order of the sections has changed.
How can I change the order of the sections?
The problem of this scenario is recreated many times ViewModel, so modifications made in sheet just lost. (The strange thing is that in SwiftUI 2.0 with StateObject these changes also lost and EditButton does not work at all.)
Anyway. It looks like here is a found workaround. The idea is to break interview dependency (binding) and work with pure data passing them explicitly into sheet and return them back explicitly from it.
Tested & worked with Xcode 12 / iOS 14, but I tried to avoid using SwiftUI 2.0 features.
class ViewModel: ObservableObject {
#Published var sections = ["S1", "S2", "S3", "S4"]
func move(from source: IndexSet, to destination: Int) {
sections.move(fromOffsets: source, toOffset: destination)
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#State var showOrderingView = false
var body: some View {
VStack {
Button("Reorder sections") {
self.showOrderingView = true
}
list
}
.sheet(isPresented: $showOrderingView) {
OrderingView(sections: viewModel.sections) {
self.viewModel.sections = $0
}
}
}
var list: some View {
List {
ForEach(viewModel.sections, id: \.self) { section in
Section(header: Text(section)) {
ForEach(0 ..< 3, id: \.self) { _ in
Text("Item")
}
}
}
}
}
}
struct OrderingView: View {
#State private var sections: [String]
let callback: ([String]) -> ()
init(sections: [String], callback: #escaping ([String]) -> ())
{
self._sections = State(initialValue: sections)
self.callback = callback
}
var body: some View {
NavigationView {
List {
ForEach(sections, id: \.self) { section in
Text(section)
}
.onMove {
self.sections.move(fromOffsets: $0, toOffset: $1)
}
}
.navigationBarItems(trailing: EditButton())
}
.onDisappear {
self.callback(self.sections)
}
}
}
A possible workaround solution for SwiftUI 1.0
I found a workaround to disable animations for the List by adding .id(UUID()):
var list: some View {
List {
...
}
.id(UUID())
}
This, however, messes the transition animations for NavigationLinks created with NavigationLink(destination:tag:selection:): Transition animation gone when presenting a NavigationLink in SwiftUI.
And all other animations (like onDelete) are missing as well.
The even more hacky solution is to disable list animations conditionally:
class ViewModel: ObservableObject {
...
#Published var isReorderingSections = false
...
}
struct OrderingView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
NavigationView {
...
}
.onAppear {
self.viewModel.isReorderingSections = true
}
.onDisappear {
self.viewModel.isReorderingSections = false
}
}
}
struct ContentView: View {
...
var list: some View {
List {
...
}
.id(viewModel.isReorderingSections ? UUID().hashValue : 1)
}
}