How do I make SwiftUI show entire vertical content on iPad? - swiftui

I have a SwiftUI application that was laid out using an iPhone. Now when I run it on an iPad, it appears to fill the entire width of the screen, but much of the view content is cutoff on the top and bottom. The top level view contains a container (which can hold any number of different views, based on navigation) and a splash view, which times out after the animation. Is there a way to tell it to honor the size required to fit all of the vertical views, and auto-size the width?
This is the top level view. I can post more, but that is a lot of code.
import SwiftUI
struct ContentView: View {
#State var showSplash = true
var body: some View {
ZStack() {
ContainerView()
SplashView()
.opacity(showSplash ? 1 : 0)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
withAnimation() {
self.showSplash = false
splashDidFinish()
}
}
}
}.onAppear {
NSLog(".onAppear()")
}
}
func splashDidFinish() {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "checkApplicationReady"), object: nil)
}
}

I was able to fix it using:
.aspectRatio(contentMode: .fit)

Related

Why is Text onTapGesture not working for widened frame in SwiftUI?

I have this testing file that is not working how I expect it to.
import SwiftUI
struct SwiftUIView: View {
#State var boolTest = false
var nums = ["1","2","3","4","5","6","7"]
var body: some View {
VStack {
ForEach(nums, id: \.self) { num in
Text("\(num)")
.frame(width: 400)
.font(.system(size: 70))
.foregroundColor(boolTest ? .red : .green)
.onTapGesture {
boolTest.toggle()
}
}
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
When I tap on a number, the foreground color changes as expected. However, I want to be able to tap on the areas left and right of the Text("\(num)") so I expanded the frame modifier to test. However the color only changes when I tap directly on the number or text.
How do I tap on the space left or right of the number and have it change colors instead of it doing nothing?
The system treats the "invisible" (ie doesn't have visible drawn content) part of the view as unresponsive unless you set a contentShape on it.
//...
.frame(width: 400)
.contentShape(Rectangle())
//...

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

View resizing when hiding tab bar with Introspect in SwiftUI

I'm using Introspect to hide the tab bar on child navigation link pages. However, I've noticed some odd behavior when the app is backgrounded and then brought back to the foreground.
It seems like initially, the hidden tab bar is still taking up some space, but this disappears when cycling the app back to the foreground. I'm not sure if this is SwiftUI behavior or has to do with how I'm using Introspect / UIKit.
It's causing layout issues in my app, so I'd like to make the spacing consistent if possible.
Here's a minimal example that shows the behavior:
import SwiftUI
import Introspect
struct ContentView: View {
var body: some View {
TabView {
VStack {
Spacer()
Text("Hello, world!")
}
}
.border(Color.red)
.introspectTabBarController { tabBarController in
tabBarController.tabBar.isHidden = true
}
}
}
Here is the late answer. Basically add tabbar height to current view frame. And onDissappear restore view frame size
import SwiftUI
import Introspect
#State var uiTabarController: UITabBarController?
#State var tabBarFrame: CGRect?
struct ContentView: View {
var body: some View {
TabView {
VStack {
Spacer()
Text("Hello, world!")
}
}
.border(Color.red)
.introspectTabBarController { (UITabBarController) in
uiTabarController = UITabBarController
self.tabBarFrame = uiTabarController?.view.frame
uiTabarController?.tabBar.isHidden = true
uiTabarController?.view.frame = CGRect(x:0, y:0, width:tabBarFrame!.width, height:tabBarFrame!.height+UITabBarController.tabBar.frame.height);
}
.onDisappear {
if let frame = self.tabBarFrame {
self.uiTabarController?.tabBar.isHidden = false
uiTabarController?.view.frame = frame
}
}
}
}

SwiftUI - animating a new image inside the current view

I have a View where I use a Picture(image) subview to display an image, which can come in different height and width formats.
The reference to the image is extracted from an array, which allows me to display different images in my View, by varying the reference. SwiftUI rearrange the content of view for each new image
I would like an animation on this image, say a scale effect, when the image is displayed
1) I need a first .animation(nil) to avoid animating the former image (otherwise I have an ugly fade out and aspect ratio deformation). Seems the good fix
2) But then I have a problem with the scaleEffect modifier (even if I put it to scale = 1, where it should do nothing)
The animation moves from image 1 to image 2 by imposing that the top left corner of image 2 starts from the position of top left corner of image 1, which, with different widths and heights, provokes a unwanted translation of the image center
This is reproduced in the code below where for demo purposes I'm using system images (which are not prone to bug 1))
How can I avoid that ?
3) In the demo code below, I trigger the new image with a button, which allows me to use an action and handle "scale" modification and achieve explicitly the desired effect. However in my real code, the image modification is triggered by another change in another view.
Swift knows that, hence I can use an implicit .animation modifier.
However, I can't figure out how to impose a reset of "scale" for any new image and perform my desired effect.
If I use onAppear(my code), it only works for the first image displayed, and not the following ones.
In the real code, I have a Picture(image) view, and Picture(image.animation()) does not compile.
Any idea how to achieve the action in the below code in the Button on an implicit animation ?
Thanks
import SwiftUI
let portrait = Image(systemName: "square.fill")
let landscape = Image(systemName: "square.fill")
struct ContentView: View {
#State var modified = false
#State var scale: CGFloat = 1
var body: some View {
return VStack(alignment: .center) {
Pictureclip(bool: $modified)
.animation(nil)
.scaleEffect(scale)
.animation(.easeInOut(duration: 1))
Button(action: {
self.modified.toggle()
self.scale = 1.1
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
{self.scale = 1}
}) {
Text("Tap here")
.animation(.linear)
}
}
}
}
struct Pictureclip: View {
#Binding var bool: Bool
var body: some View {
if bool == true {
return portrait
.resizable()
.frame(width: 100, height: 150)
.foregroundColor(.green)
} else {
return landscape
.resizable()
.frame(width: 150, height: 100)
.foregroundColor(.red)
}
}
}
I have a semi answer to my question, namely points 1 & 2 (here with reference to two jpeg images in the asset catalog)
import SwiftUI
let portrait = Image("head")
let landscape = Image("sea")
struct ContentView: View {
#State var modified = false
#State var scale: CGFloat = 0.95
var body: some View {
VStack(){
GeometryReader { geo in
VStack {
Picture(bool: self.modified)
.frame(width: geo.size.width * self.scale)
}
}
Spacer()
Button(action: {
self.scale = 0.95
self.modified.toggle()
withAnimation(.easeInOut(duration: 0.5)){
self.scale = 1
}
}) {
Text("Tap here")
}
}
}
}
struct Picture: View {
var bool: Bool
var body: some View {
if bool == true {
return portrait
.resizable().aspectRatio(contentMode: .fit)
.padding(.all,6.0)
} else {
return landscape
.resizable().aspectRatio(contentMode: .fit)
.padding(.all,6.0)
}
}
}
This solution enables scaling without distorting the aspect ratio of the new image during the animation. But It does not work in a code where the image update is triggered in another view. I guess I have to restructure my code, either to solve my problem or to expose it more clearly.
Edit: a quick and dirty solution is to put the triggering code (here the action code in the button) in the other view. Namely, put in view B the code that animates view A, with a state variable passed to it (here, "scale"). I'm sure there are cleaner ways, but at least this works.
I am not sure about it, but maybe it can be helpful for you.
Use DataBinding structure. I use it like this:
let binding = Binding<String>(get: {
self.storage
}, set: { newValue in
self.textOfPrimeNumber = ""
self.storage = newValue
let _ = primeFactorization(n: Int(self.storage)!, k: 2, changeable: &self.textOfPrimeNumber)
})

How to disable / modify a button using 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
}
}
}