SwiftUI - Strange Animation - animation

I have an image for a button. Intended result is so that when I click on the button/image, a list of items appear below it. However, for some reason the image itself shrinks upon clicking. How would I prevent the image from shrinking and just having the items populate below the image?
struct HabitGroups: View {
var imageName: String
var textOverlay: String
var listOfHabits: [String]
#Binding var selectedHabitList: [String]
#Binding var enoughHabits: Bool
#State var showHabits = false
var body: some View {
VStack {
Button(action: {
withAnimation {
self.showHabits.toggle()
}
}) {
Image(self.imageName)
.resizable()
.scaledToFit()
.frame(minWidth: 300, minHeight: 100)
.overlay(ImageOverlay(textOverlay: self.textOverlay), alignment: .bottom)
}
.buttonStyle(PlainButtonStyle())
if self.showHabits {
ForEach(self.listOfHabits, id: \.self) { habit in
AddHabitButtons(habit: habit, selectedHabitList: self.$selectedHabitList, enoughHabits: self.$enoughHabits)
}
}
}
}
}
Before Animation
After Animation

You have to many items below image to fit without resizing everything on screen (as you placed everything in on VStack), so image shrinks as it is .resizalbe.
Possible solution would be to embed habits into scroll view, like below
if self.showHabits {
ScrollView { // << here !!
ForEach(self.listOfHabits, id: \.self) { habit in
AddHabitButtons(habit: habit, selectedHabitList: self.$selectedHabitList, enoughHabits: self.$enoughHabits)
}
}
}
another alternate is to make image fixed by height, but in this case shown habits will push it up off screen. However intended behavior is not clear, anyway here it is
.buttonStyle(PlainButtonStyle())
.fixedSize(horizontal: false, vertical: true) // << here !!

Related

How to get the drop target background effect in sidebars on macOS using SwiftUI?

I have a SwiftUI List that's a sidebar on macOS. For its items I have added the dropDesternation modifier like this:
.dropDestination(for: URL.self) { urls, _ in
for url in urls {
//... adding urls to destination
}
}
return true
} isTargeted: { inDropArea in
if inDropArea {
highlightedItem = item
} else {
highlightedItem = nil
}
}
By default if the cursor is above the item I get no effect, but I want the same effect like using NSOutlineView in AppKit. Here's an example from the Finder:
As you can see I have implemented highlightedItem in the code above. I can use it to check if an item is targeted and draw a background:
.background {
if item == highlightedItem {
RoundedRectangle(cornerRadius: 5)
.fill(Color.blue)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
But that does not look quite the same:
Interestingly the effect I want is the same you get if you use a selection for the sidebar list like: List(selection: $selectedItem)
There must be a native way to do this so I don't have to fake it and get something that does not look quite right.
Having the List elements be selectable and triggering that selection temporarily seems to work quite nicely.
For example ...
let Bands: Array<String> = [
"Beatles", "Rolling Stones", "Animals", "Beach Boys", "Doors",
]
struct ContentView: View {
#State var selection: String? = nil
#State var dropSelectionCache: String? = nil
var body: some View {
VStack {
Text("Drag text on onto band to trigger highlighting")
List(Bands, id: \.self, selection: $selection) { band in
Text(band)
.dropDestination(for: String.self) { _, _ in
true
} isTargeted: { (isTarget: Bool) in
if isTarget {
dropSelectionCache = selection
withAnimation {
selection = band
}
} else {
withAnimation {
selection = dropSelectionCache
}
}
}
}
}
}
}
Here's a gif illustrating the code in operation on Ventura (apologies for slightly shakey screen recording - that's me an and not the code :-/ )
shufflingb's workaround is nice but can have performance side effects and I've found a simpler solution.
I've made a view for each list item and used blistRowBackground() for the background effect, not just .background:
struct SidebarItemView: View {
#ObservedObject var item: Item
#State private var isTargeted = false
var body: some View {
NavigationLink(value: item) {
Label {
Text(item.title)
} icon: {
Image(systemName: "folder")
}
.onDrop(of: [.image], isTargeted: $isTargeted, perform: { itemProviders in
...
return true
})
}
.listRowBackground(
Color.secondary
.cornerRadius(5)
.opacity(isTargeted ? 1.0 : 0.0)
.padding(.horizontal, 10)
)
}
}

SwiftUI, CloudKit and Images

I'm really stumped by something I think that should be relatively easy, so i need a little bump in the right direction. I've searched in a lot of places and I get either the wrong information, or outdated information (a lot!).
I am working with Core Data and CloudKit to sync data between the user's devices. Images I save as CKAsset attached to a CKRecord. That works well. The problem is with retrieving the images. I need the images for each unique enitity (Game) in a list. So I wrote a method on my viewModel that retrieves the record with the CKAsset. This works (verified), but I have no idea how to get the image out and assign that to a SwiftUI Image() View. My current method returns a closure with a UIImage, how do I set that image to an Image() within a foreach. Or any other solution is appreciated. Musn't be that hard to get the image?
/// Returns the saved UIImage from CloudKit for the game or the default Image!
func getGameImageFromCloud(for game: Game, completion: #escaping (UIImage) -> Void ) {
// Every game should always have an id (uuid)!
if let imageURL = game.iconImageURL {
let recordID = CKRecord.ID(recordName: imageURL)
var assetURL = ""
CKContainer.default().privateCloudDatabase.fetch(withRecordID: recordID) { record, error in
if let error = error {
print(error.getCloudKitError())
return
} else {
if let record = record {
if let asset = record["iconimage"] as? CKAsset {
assetURL = asset.fileURL?.path ?? ""
DispatchQueue.main.async {
completion(UIImage(contentsOfFile: assetURL) ?? AppImages.gameDefaultImage)
}
}
}
}
}
} else {
completion(AppImages.gameDefaultImage)
}
}
This is the ForEach I want to show the Image for each game (but this needed in multiple places:
//Background Tab View
TabView(selection: $gamesViewModel.currentIndex) {
ForEach(gamesViewModel.games.indices, id: \.self) { index in
GeometryReader { proxy in
Image(uiImage: gamesViewModel.getGameImageFromCloud(for: gamesViewModel.games[index], completion: { image in
}))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: proxy.size.width, height: proxy.size.height)
.cornerRadius(1)
}
.ignoresSafeArea()
.offset(y: -100)
}
.onAppear(perform: loadImage)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.animation(.easeInOut, value: gamesViewModel.currentIndex)
.overlay(
LinearGradient(colors: [
Color.clear,
Color.black.opacity(0.2),
Color.white.opacity(0.4),
Color.white,
Color.systemPurple,
Color.systemPurple
], startPoint: .top, endPoint: .bottom)
)
.ignoresSafeArea()
TIA!
So, let's go... extract ForEach image dependent internals into subview, like (of course it is not testable, just idea):
ForEach(gamesViewModel.games.indices, id: \.self) { index in
GeometryReader { proxy in
GameImageView(model: gamesViewModel, index: index) // << here !!
.frame(width: proxy.size.width, height: proxy.size.height)
.cornerRadius(1)
//.onDisappear { // if you think about cancelling
// gamesViewModel.cancelLoad(for: index)
//}
}
.ignoresSafeArea()
.offset(y: -100)
}
.onAppear(perform: loadImage)
and now subview itself
struct GameImageView: View {
var model: Your_model_type_here
var index: Int
#State private var image: UIImage? // << here !!
var body: some View {
Group {
if let loadedImage = image {
Image(uiImage: loadedImage) // << here !!
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Text("Loading...")
}
}.onAppear {
model.getGameImageFromCloud(for: model.games[index]) { image in
self.image = image
}
}
}
}
For completion's sake, my own version:
struct GameImage: View {
var game: Game
#EnvironmentObject var gamesViewModel: GamesView.ViewModel
#State private var gameImage: UIImage?
var body: some View {
Group {
if let gameImage = gameImage {
Image(uiImage: gameImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
ZStack(alignment: .center) {
Image(uiImage: AppImages.gameDefaultImage)
.resizable()
.aspectRatio(contentMode: .fill)
ProgressView()
.foregroundColor(.orange)
.font(.title)
}
}
}.onAppear {
gamesViewModel.getGameImageFromCloud(for: game) { image in
self.gameImage = image
}
}
}
}

macOS SwiftUI Navigation for a Single View

I'm attempting to create a settings view for my macOS SwiftUI status bar app. My implementation so far has been using a NavigationView, and NavigationLink, but this solution produces a half view as the settings view pushes the parent view to the side. Screenshot and code example below.
Navigation Sidebar
struct ContentView: View {
var body: some View {
VStack{
NavigationView{
NavigationLink(destination: SecondView()){
Text("Go to next view")
}}
}.frame(width: 800, height: 600, alignment: .center)}
}
struct SecondView: View {
var body: some View {
VStack{
Text("This is the second view")
}.frame(width: 800, height: 600, alignment: .center)
}
}
The little information I can find suggests that this is unavoidable using SwiftUI on macOS, because the 'full screen' NavigationView on iOS (StackNavigationViewStyle) is not available on macOS.
Is there a simple or even complex way of implementing a transition to a settings view that takes up the whole frame in SwiftUI for macOS? And if not, is it possible to use AppKit to call a View object written in SwiftUI?
Also a Swift newbie - please be gentle.
Here is a simple demo of possible approach for custom navigation-like solution. Tested with Xcode 11.4 / macOS 10.15.4
Note: background colors are used for better visibility.
struct ContentView: View {
#State private var show = false
var body: some View {
VStack{
if !show {
RootView(show: $show)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.transition(AnyTransition.move(edge: .leading)).animation(.default)
}
if show {
NextView(show: $show)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
.transition(AnyTransition.move(edge: .trailing)).animation(.default)
}
}
}
}
struct RootView: View {
#Binding var show: Bool
var body: some View {
VStack{
Button("Next") { self.show = true }
Text("This is the first view")
}
}
}
struct NextView: View {
#Binding var show: Bool
var body: some View {
VStack{
Button("Back") { self.show = false }
Text("This is the second view")
}
}
}
I've expanded upon Asperi's great suggestion and created a generic, reusable StackNavigationView for macOS (or even iOS, if you want). Some highlights:
It supports any number of subviews (in any layout).
It automatically adds a 'Back' button for each subview (just text for now, but you can swap in an icon if using macOS 11+).
Swift v5.2:
struct StackNavigationView<RootContent, SubviewContent>: View where RootContent: View, SubviewContent: View {
#Binding var currentSubviewIndex: Int
#Binding var showingSubview: Bool
let subviewByIndex: (Int) -> SubviewContent
let rootView: () -> RootContent
var body: some View {
VStack {
VStack{
if !showingSubview { // Root view
rootView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(AnyTransition.move(edge: .leading)).animation(.default)
}
if showingSubview { // Correct subview for current index
StackNavigationSubview(isVisible: self.$showingSubview) {
self.subviewByIndex(self.currentSubviewIndex)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(AnyTransition.move(edge: .trailing)).animation(.default)
}
}
}
}
init(currentSubviewIndex: Binding<Int>, showingSubview: Binding<Bool>, #ViewBuilder subviewByIndex: #escaping (Int) -> SubviewContent, #ViewBuilder rootView: #escaping () -> RootContent) {
self._currentSubviewIndex = currentSubviewIndex
self._showingSubview = showingSubview
self.subviewByIndex = subviewByIndex
self.rootView = rootView
}
private struct StackNavigationSubview<Content>: View where Content: View {
#Binding var isVisible: Bool
let contentView: () -> Content
var body: some View {
VStack {
HStack { // Back button
Button(action: {
self.isVisible = false
}) {
Text("< Back")
}.buttonStyle(BorderlessButtonStyle())
Spacer()
}
.padding(.horizontal).padding(.vertical, 4)
contentView() // Main view content
}
}
}
}
More info on #ViewBuilder and generics used can be found here.
Here's a basic example of it in use. The parent view tracks current selection and display status (using #State), allowing anything inside its subviews to trigger state changes.
struct ExampleView: View {
#State private var currentSubviewIndex = 0
#State private var showingSubview = false
var body: some View {
StackNavigationView(
currentSubviewIndex: self.$currentSubviewIndex,
showingSubview: self.$showingSubview,
subviewByIndex: { index in
self.subView(forIndex: index)
}
) {
VStack {
Button(action: { self.showSubview(withIndex: 0) }) {
Text("Show View 1")
}
Button(action: { self.showSubview(withIndex: 1) }) {
Text("Show View 2")
}
Button(action: { self.showSubview(withIndex: 2) }) {
Text("Show View 3")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
}
}
private func subView(forIndex index: Int) -> AnyView {
switch index {
case 0: return AnyView(Text("I'm View One").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.green))
case 1: return AnyView(Text("I'm View Two").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.yellow))
case 2: return AnyView(VStack {
Text("And I'm...")
Text("View Three")
}.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.orange))
default: return AnyView(Text("Inavlid Selection").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.red))
}
}
private func showSubview(withIndex index: Int) {
currentSubviewIndex = index
showingSubview = true
}
}
Note: Generics like this require all subviews to be of the same type. If that's not so, you can wrap them in AnyView, like I've done here. The AnyView wrapper isn't required if you're using a consistent type for all subviews (the root view’s type doesn’t need to match).
Heyo, so a problem I had is that I wanted to have multiple navigationView-layers, I'm not sure if that's also your attempt, but if it is: MacOS DOES NOT inherit the NavigationView.
Meaning, you need to provide your DetailView (or SecondView in your case) with it's own NavigationView. So, just embedding like [...], destination: NavigationView { SecondView() }) [...] should do the trick.
But, careful! Doing the same for iOS targets will result in unexpected behaviour. So, if you target both make sure you use #if os(macOS)!
However, when making a settings view, I'd recommend you also look into the Settings Scene provided by Apple.
Seems this didn't get fixed in Xcode 13.
Tested on Xcode 13 Big Sur, not on Monterrey though...
You can get full screen navigation with
.navigationViewStyle(StackNavigationViewStyle())

SwiftUI not very responsive to hover events

I'm trying to implement a list where I have 200 or 300 elements, and I want to change the color of the text on a hover event. But the app starts to show a delay on the hover events. Check the example code below:
struct ContentView: View {
var body: some View {
VStack {
ForEach(0...1000, id:\.self) {index in
Element()
}
}
}
}
struct Element: View {
#State private var hover = false
var body: some View {
Text("Not a fast hover!")
.foregroundColor(hover ? Color.blue : Color.white)
.onHover {_ in self.hover.toggle()}
}
}
UPDATE:
This seems to improve the responsiveness. Also if I change the background instead of the foreground color, the code is also more responsive.
struct Element: View {
#State private var hover = false
var body: some View {
ZStack {
Text("Not a fast hover!").foregroundColor(Color.blue)
Text("Not a fast hover!").opacity(hover ? 0 : 1).foregroundColor(Color.white)
}
.frame(width: 200)
.onHover {_ in self.hover.toggle()}
}
}
The solution was to use a List component instead of a VStack.

Animation in ScrollView is not working ? Using Xcode 11 beta 6

I was trying to to do transition animation Scrollview but found out that how things work differently in scrollView. But still unable to do animation in it. I am providing code, please have a look. Using Xcode 11 beta 6
import SwiftUI
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
NavigationView {
ScrollView{
VStack {
Button(action: {
// withAnimation {
self.isButtonVisible.toggle()
// }
}) {
Text("Press me")
}
if isButtonVisible {
Text("sss")
.frame(height: true ? 50 : 0, alignment: .center)
.background(Color.red)
.animation(.linear(duration: 2))
// .transition(.move(edge: .top))
}
}
}
}
}}
This must be a bug, and I suggest you file a bug report with Apple. I find a workaround (see code below), but it unfortunately uncovers another bug!
In order to make the animation inside the ScrollView to work, you can encapsulate the contents in a custom view. That will fix that problem.
This will uncover a new issue, which is made evident by the borders I added to your code: when the Text view is added, it shifts parts of the contents outside the ScrollView.
You will see that this is not correct. Try starting your app with a default value isButtonVisible = true, and you will see it renders it differently.
struct ContentView : View {
var body: some View {
NavigationView {
ScrollView {
EncapsulatedView().border(Color.green)
}.border(Color.blue)
}
}
}
struct EncapsulatedView: View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Text("Filler")
Button(action: {
withAnimation(.easeInOut(duration: 2.0)) {
self.isButtonVisible.toggle()
}
}) {
Text("Press me")
}
if isButtonVisible {
Text("sss")
.frame(height: true ? 50 : 0, alignment: .center)
.transition(.opacity)
.background(Color.red)
}
Spacer()
}.border(Color.red)
}
}

Resources