This question already has answers here:
How to only disable scroll in ScrollView but not content view?
(9 answers)
Closed 9 months ago.
I'm currently working on a swiftUI project where I'm supposed to be able to interact with the content of a scrollView. The problem is that I want to scroll only using buttons. It works but I can't figure a way to disable the scrolling without disabling the content of my ScrollView
Here's my code. I know that disabled(true) won't work but it's what I got so far
ScrollViewReader { scrolling in
ZStack{
HStack{
ScrollView(.horizontal, showsIndicators: false){
HStack{
FirstStepView().id(0).padding(.horizontal,20).frame(width: UIScreen.main.bounds.width)
SecondStepView().id(1).frame(width: UIScreen.main.bounds.width)
ThirdStepView().id(2).frame(width: UIScreen.main.bounds.width)
FourthStepView().id(3).frame(width: UIScreen.main.bounds.width)
}
}.disabled(true)
.animation(
Animation.easeOut(duration: 1)
)
}
.onChange(of: myId, perform: { value in
withAnimation{scrolling.scrollTo(myId)}
})
}
}
Don't forget to provide a minimum reproducible example something we can copy and paste to ensure that the integrity if what you want is kept.
If your *StepView.count is static the code below might work for you
struct ButtonScroll: View {
#State var myId: Int = 0
init() {
//Add this
//It does NOT work with ScrollView
UIScrollView.appearance().isScrollEnabled = false
}
var body: some View {
VStack{
//Simulates myId changing
Button("change-position", action: {
myId = Int.random(in: 0...3)
print(myId.description)
})
ZStack{
//TabView does not allow for changes in Tab/Page count nicely
TabView(selection: $myId){
//Used Button to test interaction
Button("FirstStepView", action: {
print("FirstStepView")
}).tag(0)
Button("SecondStepView", action: {
print("SecondStepView")
}).tag(1)
Button("ThirdStepView", action: {
print("ThirdStepView")
}).tag(2)
Button("FourthStepView", action: {
print("FourthStepView")
}).tag(3)
}.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.animation(
Animation.easeOut(duration: 1)
)
}
}
}
}
Related
I have a CameraView in my app that I'd like to bring up whenever a button is to be presssed. It's a custom view that looks like this
// The CameraView
struct Camera: View {
#StateObject var model = CameraViewModel()
#State var currentZoomFactor: CGFloat = 1.0
#Binding var showCameraView: Bool
// MARK: [main body starts here]
var body: some View {
GeometryReader { reader in
ZStack {
// This black background lies behind everything.
Color.black.edgesIgnoringSafeArea(.all)
CameraViewfinder(session: model.session)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.scaledToFill()
.ignoresSafeArea()
.frame(width: reader.size.width,height: reader.size.height )
// Buttons and controls on top of the CameraViewfinder
VStack {
HStack {
Button {
//
} label: {
Image(systemName: "xmark")
.resizable()
.frame(width: 20, height: 20)
.tint(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
Spacer()
flashButton
}
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
Spacer()
flipCameraButton
}
.padding([.horizontal, .bottom], 20)
.frame(maxHeight: .infinity, alignment: .bottom)
}
} // [ZStack Ends Here]
} // [Geometry Reader Ends here]
} // [Main Body Ends here]
// More view component code goes here but I've excluded it all for brevity (they don't add anything substantial to the question being asked.
} // [End of CameraView]
It contains a CameraViewfinder View which conforms to the UIViewRepresentable Protocol:
struct CameraViewfinder: UIViewRepresentable {
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
let session: AVCaptureSession
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.backgroundColor = .black
view.videoPreviewLayer.cornerRadius = 0
view.videoPreviewLayer.session = session
view.videoPreviewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
}
}
I wish to add a binding property to this camera view that allows me to toggle this view in and out of my screen like any other social media app would allow. Here's an example
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView(showCamera: $showCamera)
}
}
I understand that the code to achieve this must be written inside the updateUIView() method. Now, although I'm quite familiar with SwiftUI, I'm relatively inexperienced with UIKit, so any help on this and any helpful resources that could help me better code situations similar to this would be greatly appreciated.
Thank you.
EDIT: Made it clear that the first block of code is my CameraView.
EDIT2: Added Example of how I'd like to use the CameraView in my App.
Judging by the way you would like to use it in the app, the issue seems to not be with the CameraViewFinder but rather with the way in which you want to present it.
A proper SwiftUI way to achieve this would be to use a sheet like this:
#State var showCamera: Bool = false
var body: some View {
mainTabView
.sheet(isPresented: $showCamera) {
CameraView()
.interactiveDismissDisabled() // Disables swipe to dismiss
}
}
If you don't want to use the sheet presentation and would like to cover the whole screen instead, then you should use the .fullScreenCover() modifier like this.
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView()
.fullScreenCover(isPresented: $showCamera)
}
}
Either way you would need to somehow pass the state to your CameraView to allow the presented screen to set the state to false and therefore dismiss itself, e.g. with a button press.
In SwiftUI, I've managed to make a Button animate right when the view is first drawn to the screen, using the animation(_:) modifier, that was deprecated in macOS 12.
I've tried to replace this with the new animation(_:value:) modifier, but this time nothing happens:
So this is not working:
struct ContentView: View {
#State var isOn = false
var body: some View {
Button("Press me") {
isOn.toggle()
}
.animation(.easeIn, value: isOn)
.frame(width: 300, height: 400)
}
}
But then this is working. Why?
struct ContentView: View {
var body: some View {
Button("Press me") {
}
.animation(.easeIn)
.frame(width: 300, height: 400)
}
}
The second example animates the button just as the view displays, while the first one does nothing
The difference between animation(_:) and animation(_:value:) is straightforward. The former is implicit, and the latter explicit. The implicit nature of animation(_:) meant that anytime ANYTHING changed, it would react. The other issue it had was trying to guess what you wanted to animate. As a result, this could be erratic and unexpected. There were some other issues, so Apple has simply deprecated it.
animation(_:value:) is an explicit animation. It will only trigger when the value you give it changes. This means you can't just stick it on a view and expect the view to animate when it appears. You need to change the value in an .onAppear() or use some value that naturally changes when a view appears to trigger the animation. You also need to have some modifier specifically react to the changed value.
struct ContentView: View {
#State var isOn = false
//The better route is to have a separate variable to control the animations
// This prevents unpleasant side-effects.
#State private var animate = false
var body: some View {
VStack {
Text("I don't change.")
.padding()
Button("Press me, I do change") {
isOn.toggle()
animate = false
// Because .opacity is animated, we need to switch it
// back so the button shows.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
animate = true
}
}
// In this case I chose to animate .opacity
.opacity(animate ? 1 : 0)
.animation(.easeIn, value: animate)
.frame(width: 300, height: 400)
// If you want the button to animate when the view appears, you need to change the value
.onAppear { animate = true }
}
}
}
Follow up question: animating based on a property of an object is working on the view itself, but when I'm passing that view its data through a ForEach in the parent view, an animation modifier on that object in the parent view is not working. It won't even compile. The objects happen to be NSManagedObjects but I'm wondering if that's not the issue, it's that the modifier works directly on the child view but not on the passed version in the parent view. Any insight would be greatly appreciated
// child view
struct TileView: View {
#ObservedObject var tile: Tile
var body: some View {
Rectangle()
.fill(tile.fillColor)
.cornerRadius(7)
.overlay(
Text(tile.word)
.bold()
.font(.title3)
.foregroundColor(tile.fillColor == .myWhite ? .darkBlue : .myWhite)
)
// .animation(.easeInOut(duration: 0.75), value: tile.arrayPos)
// this modifier worked here
}
}
struct GridView: View {
#ObservedObject var game: Game
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 4)
var body: some View {
GeometryReader { geo in
LazyVGrid(columns: columns) {
ForEach(game.tilesArray, id: \.self) { tile in
Button(action: {
tile.toggleSelectedStatus()
moveTiles() <- this changes their array position (arrayPos), and
the change in position should be animated
}) {
TileView(tile: tile)
.frame(height: geo.size.height * 0.23)
}
.disabled(tile.status == .solved || tile.status == .locked)
.animation(.easeInOut(duration: 0.75), value: arrayPos)
.zIndex(tile.status == .locked ? 1 : 0)
}
}
}
}
}
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 1 year ago.
Improve this question
I'm clearly not understanding this in SwiftUI.
I have a task struct with several properties one of which - 'isHotList' is a Boolean that I want to be able to toggle on and off.
I created a row view for each task and then use this to populate a list view for all the tasks loaded from JSON - no problems there. Moved on and added a detail view (which takes a task object passed in from the list view) with a toggle button for the isHotList the problem is that the toggle works correctly in the detail view but doesn't carry back to the list view .... ?
I suspect I've created two seperate instances of the task but I'm not sure. I've used #State to declare the task but I can't get #Binding to work correctly for me.
Toggle in the detail view, task is declared with #State in this view file ...
Button(action: {
task.isHotList.toggle()
print(task.isHotList)
}){
Image(systemName: "flame.fill")
.foregroundColor(task.isHotList ? Color.orange : Color.gray)
}
The list view (separate file)
ForEach(filteredHotTasks) { task in
NavigationLink(destination: TaskDetail(task: task)) {
TaskRow(task: task)
}
}
I'm really trying to learn here but this has me confused as to what is / isn't going on
As requested the full detail view file
struct TaskDetail: View {
#EnvironmentObject var modelData: ModelData
#State var task: Task
var taskIndex: Int {
modelData.tasks.firstIndex(where: {$0.id == task.id})!
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(task.title)
.font(.title2)
.padding(.bottom, 10)
Spacer()
Button(action: {
task.isHotList.toggle()
print(task.isHotList)
}){
Image(systemName: "flame.fill")
.foregroundColor(task.isHotList ? Color.orange : Color.gray)
}
}
HStack {
Text(task.category)
.font(.subheadline)
Spacer()
HStack {
Image(systemName: "calendar.badge.clock")
Text(task.dueDate)
.font(.subheadline)
}
}
}
.padding()
}
}
}
Remove #State
struct TaskDetail: View {
#EnvironmentObject var modelData: ModelData
var task: Task /* NEW */
var taskIndex: Int {
modelData.tasks.firstIndex(where: {$0.id == task.id})!
}
}
And use
Button(action: {
/* filteredHotTasks is the array that contains tasks. */
modelData.filteredHotTasks[taskIndex].isHotList.toggle()
print(task.isHotList)
})
Remember to pass modelData using .environmentObject(modelData) somwhere in one of your root views.
If you do not do, use #ObservedObject instead of #EnvironmentObject.
struct TaskDetail: View {
#ObservedObject var modelData: ModelData /* NEW */
var task: Task /* NEW */
var taskIndex: Int {
modelData.tasks.firstIndex(where: {$0.id == task.id})!
}
}
And
NavigationLink(destination: TaskDetail(modelData: modelData, task: task)) {
TaskRow(task: task)
}
Have a look at my answer to this question
Declaring TextField and Toggle in a SwiftUI dynamic form
For more info, read this article.
Managing Model Data in Your App
🎁
Confirm Task to Equatable in addition to the rest of the protocols you confirm to.
struct Task: Codable, Identifiable, Equatable {
}
Doing so you avoid using task.id whenever you want to know if two tasks are equal as in the firstIndex
var taskIndex: Int {
modelData.tasks.firstIndex(where: {$0 == task})!
}
In my app I have a ScrollView that holds a VStack. The VStack has an animation modifier. It also has one conditional view with a transition modifier. The transition works. However, when the view first appears, the content scales up with a starting width of 0. You can see this on the green border in the video below. This only happens if the VStack is inside the ScrollView or the VStack has the animation modifier.
I have tried different other things like setting a fixedSize or setting animation(nil) but I cannot get it to work.
I don't care if there is an animation at the beginning or if the view transitions onto the screen. But I definitely do not want the bad animation at the beginning. When I click on the button, the Text and the Button should both animate.
I also tested this behaviour with the following simplified code.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
ScrollView {
VStack(spacing: 32) {
if self.viewModel.shouldShowText {
Text("Hello, World! This is a some text.")
.transition(
AnyTransition
.move(edge: .top)
.combined(
with:
.offset(
.init(width: 0, height: -100)
)
)
)
}
Button(
action: {
self.viewModel.didSelectButton()
},
label: {
Text("Button")
.font(.largeTitle)
}
)
}
.border(Color.green)
.animation(.easeInOut(duration: 5))
.border(Color.red)
HStack { Spacer() }
}
.border(Color.blue)
}
}
class ViewModel: ObservableObject {
#Published private var number: Int = 0
var shouldShowText: Bool {
return self.number % 2 == 0
}
func didSelectButton() {
self.number += 1
}
}
It works fine with Xcode 12 / iOS 14 (and stand-alone and in NavigationView), so I assume it is SwiftUI bug in previous versions.
Anyway, try to make animation be activated by value as shown below (tested & works)
// ... other code
}
.border(Color.green)
.animation(.easeInOut(duration: 5), value: viewModel.number) // << here !!
.border(Color.red)
and for this work it needs to make available published property for observation
class BlockViewModel: ObservableObject {
#Published var number: Int = 0 // << here !!
// ... other code
I want to create a POC using SwiftUI and CoreML. I use a simple button to call some function (called test here). This function is pretty heavy as it performs a CoreML inference, and it can take up to a minute to compute.
I have several problems:
The button is still active even when the computation is ongoing. In other words, if I click the button several times before the processing of the first click is finished, the processing will be performed several times. I want to disable the button as long as the processing is ongoing.
I tried to modify the button's appearance to signify the user that the processing is ongoing. In the example bellow, I change the button color to red before calling the test function, and I change it back to blue when the processing is over. But it doesn't work.
In the code bellow, the test function is just sleeping for 5 seconds to simulate the CoreML inference.
func test() -> Void {
print("Start processing")
sleep(5)
print("End processing")
}
struct ContentView: View {
#State private var buttonColor : Color = .blue
var body: some View {
VStack {
Button(action: {
self.buttonColor = .red
test()
self.buttonColor = .blue
}) {
Text("Start")
.font(.title)
.padding(.horizontal, 40)
.padding(.vertical, 5)
.background(self.buttonColor)
.foregroundColor(.white)
}
}
}
}
I guess this problem is very straight forward for most of you. I just can't find the correct search keywords to solve it by myself. Thanks for your help.
Here is possible approach (see also comments in code). Tested & works with Xcode 11.2 / iOS 13.2.
struct ContentView: View {
#State private var buttonColor : Color = .blue
var body: some View {
VStack {
Button(action: {
self.buttonColor = .red
DispatchQueue.global(qos: .background).async { // do in background
test()
DispatchQueue.main.async {
self.buttonColor = .blue // work with UI only in main
}
}
}) {
Text("Start")
.font(.title)
.padding(.horizontal, 40)
.padding(.vertical, 5)
.background(self.buttonColor)
.foregroundColor(.white)
}
.disabled(self.buttonColor == .red) // disabled while calculating
}
}
}