SwiftUI - switch toolbar states - swiftui

Goal: a toolbar that switches states, according to object selected (image, text, shape)
What I did:
iimport SwiftUI
struct ToolbarMaster : View {
#State var showtoolbar = false
var toolbarmaster: [ToolbarBezier] = []
var body: some View {
HStack {
Spacer()
VStack {
Button(action: {self.showtoolbar.toggle() }) {
Image(systemName: "gear")
}
.padding(.leading)
Image("dog")
Text("Im a text")
.font(.largeTitle)
.color(.black)
Path(ellipseIn: CGRect(x: 0, y: 0, width: 100, height: 100))
.fill(Color.black)
}
NavigationView {
ZStack {
ToolbarBezier()
ToolbarArtwork()
}
.navigationBarTitle(Text("Toolbar Name"), displayMode: .inline)
}
.frame(width: 320.0)
}
}
}
My result:
How can I change the state, while selecting different objects?
I need to do in a dynamic way (not hardcoded), so that when any object that is an image, it will display Image Toolbar, and so on.

I would do it like this :
Add a #State boolean variable for each state of your toolbar
#State private var textSelected = false
Add an .onTap event to each of the "active" views on your scene :
Text("Tap me!")
.tapAction {
self.textSelected = true
}
Make the appearance of you toolbar change based on the "state" variable(s)
if self.textSelected {
self.showTextToolbarContent()
}
Of course, showTextToolbarContent() is a local function returning a some View with the toolbar content tailored for text selection.
This is obviously only a bare-bone example. In your actual implementation you'll probably use an array of bools for the "selection" state and another state var for the view currently selected.

Related

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

Radiobuttons in List swiftUI

Im displaying data from server and need to display the options in radiobuttons. But default none of the button should be selected. I was able to display radiobuttons. But when a particular button is clicked, only its image should be changed. Though with my code all the other button images too change. I have gone through some references SwiftUI - How to change the button's image on click?, but couldn't get it work. As am a newbie to SwiftUI, strucked here.
struct ListView: View {
#State var imageName: String = "radio-off"
var body: some View {
List(vwModel.OpnChoice.ItemList) { opn in
VStack(alignment:.leading) {
ForEach(opn.Choices.indices, id: \.self) { row in
Button(action: {
print("\(opn.Choices[row].ChoiceId)")
self.imageName = "radio-On"
}) {
Image(imageName)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
}
Text(opn.Choices[row].ChoiceName)
}
}
}
}
}
As you are working with indices anyway, add a #Published property to your view model which contains the selected index.
Then set the index in the button action. The redraw of the view sets the button at the selected index to the on-state and the others to the off-state.
As I don't know your environment this is a simplified stand-alone view model and view with SF Symbols images
class VMModel : ObservableObject {
#Published var selectedOption = -1
var numberOfOptions = 5
}
struct ListView: View {
#StateObject private var vwModel = VMModel()
var body: some View {
VStack(alignment:.leading) {
ForEach(0..<vwModel.numberOfOptions, id: \.self) { opn in
Button(action: {
vwModel.selectedOption = opn
print("index \(opn) selected")
}) {
Image(systemName: opn == vwModel.selectedOption ? "largecircle.fill.circle" : "circle")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
}
}
}
}
}
Update:
Meanwhile you can use a Picker with the .radioGroup modifier
enum Choice {
case one, two, three
}
struct ListView: View {
#State private var choice : Choice = .one
var body: some View {
Picker(selection: $choice, label: Text("Select an option:")) {
Text("One").tag(Choice.one)
Text("Two").tag(Choice.two)
Text("Three").tag(Choice.three)
}.pickerStyle(.radioGroup)
}
}
At the moment you are storing a single #State property "imageName" for the entire list. And then when any of the buttons in the list are tapped you are changing that single property. Which will affect all the buttons.
I'd suggest removing this property and putting something into the viewModel.
You appear to have a view model with an array called vwModel.opnChoice.itemList.
There are multiple ways of making this work. You could stop a boolean in the itemList to say whether each item is selected or not. But, as you want it to work like radio buttons what might be better is to have a property on the vwModel like selectedItem.
The button could then do...
vwModel.selectedItemId = opn.choices[row].choiceId
Then you button Label would be...
Image(vsModel.selectedItemId == opn.choices[row].choiceId ? "radio-on" : "radio-off")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
This would allow you to toggle the button when it is tapped and should turn the other buttons off.

Customize navigationBar in Swift UI

I am new in swift ui. I want to put image button on the side of the NavigationBar title.
I want to be able to click the user image and navigate to another view. How?
You need to use navigationBarItems for putting image to navigation bar and you should add NavigationLink to that image. For center the title you need to set navigation bar title's displayMode to .inline or you can use new Toolbar api
struct ContentView: View {
var body: some View {
NavigationView {
Text("Welcome to Stack Overflow")
.navigationBarTitle("Header", displayMode: .inline)
.navigationBarItems(leading: NavigationLink(destination: Text("Destination")) {
Image(systemName: "person.crop.circle.fill")
.font(.title)
})
}
}
}
Screenshot
Another way using toolbar item.
I am adding TapGesture on image icon, and keeping it out of navigation link, as the image is not getting circular inside the NavigationLink in ToolbarItemGroup.
By leveraging isActive property of NavigationLink which monitors onTap state we can determine either we want to push our view or not.
import SwiftUI
struct WeatherView: View {
#State var onTap = false
var borderColor: Color = Color("Black")
var addProjectToolbarItem: some ToolbarContent {
ToolbarItemGroup(placement: .navigationBarLeading) {
NavigationLink(destination:Text("Welcome"), isActive: self.$onTap) {
EmptyView()
}
Image("yourImage")
.resizable()
.frame(width: 32, height: 32)
.clipShape(Circle())
.onTapGesture {
onTap.toggle()
}
}
}
var body: some View {
NavigationView {
VStack {
Text("First view")
}
.toolbar{
addProjectToolbarItem
}
.navigationTitle("Header")
.navigationBarTitleDisplayMode(.inline)
}
}
}

SwiftUI: popover to persist (not be dismissed when tapped outside)

I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}

In SwiftUI how can I animate a button offset when displayed

In SwiftUI, I want a button to appear from off screen by dropping in from the top into a final position when the view is initially displayed, I'm not asking for animation when the button is pressed.
I have tried:
Button(action: {}) {
Text("Button")
}.offset(x: 0.0, y: 100.0).animation(.basic(duration: 5))
but no joy.
If you would like to play with offset, this can get you started.
struct ContentView : View {
#State private var offset: Length = 0
var body: some View {
Button(action: {}) { Text("Button") }
.offset(x: 0.0, y: offset)
.onAppear {
withAnimation(.basic(duration: 5)) { self.offset = 100.0 }
}
}
}
I first suggested a .transition(.move(.top)), but I am updating my answer. Unless your button is on the border of the screen, it may not be a good fit. The move is limited to the size of the moved view. So you may need to use offset after all!
Note that to make it start way out of the screen, the initial value of offset can be negative.
First of all you need to create a transition. You could create an extension for AnyTransition or just create a variable. Use the move() modifier to tell the transition to move the view in from a specific edge
let transition = AnyTransition.move(edge: .top);
This alone only works if the view is at the edge of the screen. If your view is more towards the center you can use the combined() modifier to combine another transition such as offset() to add additional offset
let transition = AnyTransition
.move(edge: .top)
.combined(with:
.offset(
.init(width: 0, height: 100)
)
);
This transition will be for both showing and removing a view although you can use AnyTransition.asymmetric() to use different transitions for showing and removing a view
Next create a showButton bool (name this whatever) which will handle showing the button. This will use the #State property wrapper so SwiftUI will refresh the UI when changed.
#State var showButton: Bool = false;
Next you need to add the transition to your button and wrap your button within an if statement checking if the showButton bool is true
if (self.showButton == true) {
Button(action: { }) {
Text("Button")
}
.transition(transition);
}
Finally you can update the showButton bool to true or false within an animation block to animate the button transition. toggle() just reverses the state of the bool
withAnimation {
self.showButton.toggle();
}
You can put your code in onAppear() and set the bool to true so the button is shown when the view appears. You can call onAppear() on most things like a VStack
.onAppear {
withAnimation {
self.showButton = true;
}
}
Check the Apple docs to see what is available for AnyTransition https://developer.apple.com/documentation/swiftui/anytransition
Presents a message box on top with animation:
import SwiftUI
struct MessageView: View {
#State private var offset: CGFloat = -200.0
var body: some View {
VStack {
HStack(alignment: .center) {
Spacer()
Text("Some message")
.foregroundColor(Color.white)
.font(Font.system(.headline).bold())
Spacer()
}.frame(height: 100)
.background(Color.gray.opacity(0.3))
.offset(x: 0.0, y: self.offset)
.onAppear {
withAnimation(.easeOut(duration: 1.5)) { self.offset = 000.0
}
}
Spacer()
}
}
}
For those that do want to start from a Button that moves when you tap on it, try this:
import SwiftUI
struct ContentView : View {
#State private var xLoc: CGFloat = 0
var body: some View {
Button("Tap me") {
withAnimation(.linear(duration: 2)) { self.xLoc+=50.0 }
}.offset(x: xLoc, y: 0.0)
}
}
Or alternatively (can replace Text with anything):
Button(action: {
withAnimation(.linear(duration: 2)) { self.xLoc+=50.0 }
} )
{ Text("Tap me") }.offset(x: xLoc, y: 0.0)