How to disable / modify a button using SwiftUI? - swiftui

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
}
}
}

Related

GeometryReader acting weird when presenting a modal on iOS 16. Bug or new behavior?

I'm seeing a weird behavior that is affecting one of my views in SwiftUI after upgrading to iOS 16.
Just to give some context, here is the stack:
Xcode 14
Simulator or real device on iOS 15.5 and 16
Considering the minimum reproducible code below:
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
GeometryReader { reader in
VStack(spacing: 36) {
Text("Screen frame:\n\(String(describing: reader.frame(in: .global)))")
.multilineTextAlignment(.center)
Button {
isPresented.toggle()
} label: {
Text("Open modal")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.onReceive(NotificationCenter.default.appDidBecomeActive()) { _ in
print(reader.frame(in: .global))
}
.onReceive(NotificationCenter.default.appDidEnterBackground()) { _ in
print(reader.frame(in: .global))
}
}
.sheet(isPresented: $isPresented) {
modalView
}
}
private var modalView: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}
extension NotificationCenter {
func appDidBecomeActive() -> AnyPublisher<Notification, Never> {
publisher(for: UIApplication.didBecomeActiveNotification).eraseToAnyPublisher()
}
func appDidEnterBackground() -> AnyPublisher<Notification, Never> {
publisher(for: UIApplication.didEnterBackgroundNotification).eraseToAnyPublisher()
}
}
As soon the view starts, it's possible to see the frame available due to the GeometryReader. Then following the steps:
Open the modal view
Send the app to the background
Open the app again
Close the modal
It's possible to see that the frame changed, and the values match with the 3D effect when a view is presenting another view, and it's never changing again to the right values unless you send the app again to the background or switch views (e.g. using a TabView).
I don't find anything on iOS release notes talking about it, so I supposed it must be a bug (I've filled out a bug report already).
On iOS 15, the frame value keeps stable at the same value.
I have a couple of views relying on the value of a GeometryReader, and it's causing my view to deform because of this issue. Does anyone know a way to force the recalculation for the GeometryReader for this case?
Any help is appreciated.
The issue won't occur if you control the display of the sheet with the new presentationDetents method, provided you do not request to cover the entire screen.
I modified your code as follows:
.sheet(isPresented: $isPresented) {
if #available(iOS 16, *) {
modalView
.presentationDetents([.fraction(0.99)])
}
else {
modalView
}
}
The issue will remain if you request .fraction(1), i.e. covering the whole screen.

SwiftUI Is there any built in view that kind of slides in from the side and takes up 3/4 of the screen?

This is probably a custom view but in the Reddit app there's a toolbar and the top left button(3 lines) opens this kind of view from the side that moves the current view to the right so you can only see about 25% of it and a new view that takes up about 75% of the screen slides in. Is there anything like this built into SwiftUI and if there isn't how would I go about implementing something like this?
This is my custom side bar behave similarly to what you just mentioned, you can try it. (Images and Code are below)
Before click:
After clicked:
struct ContentView: View {
#State var isClicked = false
var body: some View {
HStack {
Rectangle()
.fill(.orange)
.frame(width: isClicked ? UIScreen.main.bounds.width * 0.75 : 0)
VStack {
HStack {
Button {
withAnimation {
isClicked.toggle()
}
} label: {
Image(systemName: "menucard.fill")
.padding(.leading)
}
Spacer()
}
Spacer()
}
}
}
}

SwiftUI animation not working using animation(_:value:)

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)
}
}
}
}
}

SwiftUI How to trigger action inside View when view appers / dissapears?

I have recently started dabbling in swiftUI. One of the thing that I have noticed is that now we have option to add modifier on View: .onAppear and .onDisappear, and trigger some action. But what I want to do is somewhat different, I want to trigger action inside the View when View appears / disappears.
For example I have video player and every time inside List view scrolls from the screen, I want to pause video (Video is embedded inside my view). And vice versa I want video to start playing. All logic for video playing / pausing is inside the view.
So how can I trigger action when view appears / disappears?
Here is my code example:
struct ContentView: View {
var body: some View {
VStack {
List(0..<14) { item in
VideoPreviewView(videoURLString: largeVideos[item])
.frame(height: 375)
.background(Color.red)
.onAppear {
// play video
}
.onDisappear {
// stop video
}
}
}
}
}
Now how do I pass VideoPreviewView these actions, player is inside the VideoPreviewView?
In order to run onAppear logic, when a child view appears you simply attach the .onAppear function inside the child views body.
struct VideoPreviewView: View {
let videoURLString: String
var body: some View {
Text("VideoPreviewView")
.onAppear {
playVideo()
}
.onDisappear {
stopVideo()
}
}
private func playVideo() {
print("Start playing \(videoURLString)")
}
private func stopVideo() {
print("Stop playing \(videoURLString)")
}
}
struct ContentView: View {
let videoUrls = ["1","2","3", "4","5", "6", "7"]
var body: some View {
VStack {
List(videoUrls, id: \.self) { urlString in
LazyVStack {
VideoPreviewView(videoURLString: urlString)
.frame(height: 375)
.background(Color.red)
}
}
}
}
}
Which yields the following output in the console:
Start playing 1
Start playing 2
Start playing 3
Start playing 4
Start playing 5
Start playing 6
Start playing 7
Stop playing 4
Stop playing 5
Stop playing 6
Stop playing 7
All onAppear methods of the child views are called, and right after the the onDisappear methods of the non-visible child views are called.
You can find a discussion on whats happening here: https://www.hackingwithswift.com/forums/swiftui/onappear-something-i-don-t-understand/1978
If you use a ScrollView + ForEach + LazyVStack (this one is important) combination, you get the following output:
struct ContentView: View {
let videoUrls = ["1","2","3", "4","5", "6", "7"]
var body: some View {
ScrollView {
ForEach(videoUrls, id: \.self) { urlString in
LazyVStack {
VideoPreviewView(videoURLString: urlString)
.frame(height: 375)
.background(Color.red)
}
}
}
}
}
Start playing 3
Start playing 2
Start playing 1
Now only the visible ChildViews are called. If you scroll down you shoudl see your desired output in the console. This should be applicable for your use case.
All code was tested and run with Xcode 12.4 and iOS 14.4

Initial SwiftUI Animation inside ScrollView

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