React on drag over view - swift

I have a view that I want to change whenever long press or drag occurs over it. It can start outside of the view, but also inside.
struct ContentView: View {
#State var active: Bool = false
var body: some View {
Rectangle()
.fill(self.active ? Color.red : Color.secondary)
}
}
An example of this behaviour is the iPhone keyboard: when you long press on a key it pops up (active = true). When you move outside it, it pops down (active = false) but the next key is then active.
I have tried using LongPressGesture but cannot figure out how to make it behave as I want.

I made example for you in playgrounds to display how to use a LongPressGestureRecognizer.
You add the gesture recognizer to the view you want long pressed, with target being the parent controller handling the gesture recognition (ContentView in your case) and action being what happens with a long press.
In my implementation, a long press to the view bodyView changes its color from clear to red. This happens inside the didSet of the showBody property, which calls showBodyToggled(). I'm checking the state of the gesture, because the gesture recognizer will send messages for each state (I'm only performing the action if the state is .began).
Let me know if you have any questions:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
var bodyView: UIView!
var showBody = false {
didSet {
showBodyToggled()
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureSubviews()
configureLongPressRecognizer()
}
func configureSubviews() {
bodyView = UIView()
bodyView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bodyView)
NSLayoutConstraint.activate([
bodyView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
bodyView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
bodyView.heightAnchor.constraint(equalToConstant: 200),
bodyView.widthAnchor.constraint(equalToConstant: 300)
])
}
func configureLongPressRecognizer() {
bodyView.addGestureRecognizer(
UILongPressGestureRecognizer(
target: self,
action: #selector(longPressed(_:))
)
)
}
#objc func longPressed(_ sender: UILongPressGestureRecognizer) {
// the way I'm doing it: only acts when the long press first happens.
// delete this state check if you'd prefer the default long press implementation
switch sender.state {
case .began:
showBody = !showBody
default:
break
}
}
func showBodyToggled() {
UIView.animate(withDuration: 0.4) { [weak self] in
guard let self = self else { return }
self.bodyView.backgroundColor = self.showBody ? .red : .clear
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
EDIT:
Here's an example in SwiftUI where a long press of 0.5 seconds toggles the color of a circle between red and black
struct ContentView: View {
#State var active = false
var longPress: some Gesture {
LongPressGesture()
.onEnded { _ in
withAnimation(.easeIn(duration: 0.4)) {
self.active = !self.active
}
}
}
var body: some View {
Circle()
.fill(self.active ? Color.red : Color.black)
.frame(width: 100, height: 100, alignment: .center)
.gesture(longPress)
}
}

Related

How can I use native macOS windows in ZStack form in SwiftUI?

I want be able to use macOS native windows like we can use views in SwiftUI, with this condition that the first window has isMovableByWindowBackground = false and the second one has isMovableByWindowBackground = true and with moving the movable window the unmovable window following the movable one.
The current code make the all window movable, which is not my goal, I want just the red view be movable, and with movement of this red view the yellow window is following the red window.
struct ContentView: View {
var body: some View {
VStack {
Button("show window") {
ZStack {
myView()
myView2()
}
.openInWindow(sender: nil)
}
}
.frame(width: 200, height: 200)
}
func myView() -> some View {
return Color.yellow.frame(width: 400, height: 400)
}
func myView2() -> some View {
return Color.red.frame(width: 25, height: 25).overlay(Image(systemName: "arrow.up.and.down.and.arrow.left.and.right"))
}
}
extension View {
#discardableResult
func openInWindow(sender: Any?) -> NSWindow {
let controller = NSHostingController(rootView: self)
let window = NSWindow(contentViewController: controller)
window.contentViewController = controller
window.makeKeyAndOrderFront(sender)
window.titleVisibility = .hidden
window.toolbar = nil
window.styleMask = .fullSizeContentView
window.isMovableByWindowBackground = true
return window
}
}

Press SwiftUI button and go to the next screen (next view) when server callback

I stuck at the pretty simple step when I want to press on the button show loading indicator and if server returns success response then show new view
It's pretty straightforward in UIKit, but with SwiftUI I stuck to do this.
I need to know how to init/add activity indicator I found some cool examples here. Can I just store it as a let variable in my view sruct?
Then by pressing button Unhide/Animate indicator
Make a server request via my rest api service
Wait some time and show new view on success callback or error message.
Nothing super hard, but I stuck here is a button which is a part of my NavigationView. Please help me push to new screen.
Button(action: {
// show indicator or animate
// call rest api service
// wait for callback and show next view or error alert
})
I found some link but not sure how to use it right.
Not sure I need PresentationButton or NavigationLink at all as I already have a simple Button and want to kind of push new view controller.
Very similar question to this one but I have not find it useful as I don't know how to use step by step how to "Create hidden NavigationLink and bind to that state"
EDITED:
I also found this video answer looks like I figure out how to do navigation. But still need to figure out how to show activity indicator when button pressed.
To show anything you need at some point in SwiftUI, simply use a #State variable.
You can use as many of these Bool as needed. You can toggle a new view, animation...
Example
#State var showNextView = false
#State var showLoadingAnimation = false
Button(action: {
self.showLoadingAnimation.toggle()
self.makeApiCall()
}) {
Text("Show next view on api call success")
}
// Method that handle your api call
func makeApiCall() {
// Your api call
if success {
showLoadingAnimation = false
showNextView = true
}
}
As for the animation, I would suggest the use the Lottie framework. You can find some really cool animations:
https://github.com/airbnb/lottie-ios
You can find many animations here:
https://lottiefiles.com
And you can create a class to implement your Lottie animation via a JSON file that you dropped in your project:
import SwiftUI
import Lottie
struct LottieRepresentable: UIViewRepresentable {
let named: String // name of your lottie file
let loop: Bool
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: .zero)
let animationView = AnimationView()
let animation = Animation.named(named)
animationView.animation = animation
animationView.contentMode = .scaleAspectFit
if loop { animationView.loopMode = .loop }
animationView.play()
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
animationView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
return view
}
func updateUIView(_ uiView: UIView, context: Context) { }
}
Create a SwiftUI file to use your lottie animation in your code:
// MARK: - Show LottieRespresentable as view
struct LottieView: View {
let named: String
let loop: Bool
let size: CGFloat
var body: some View {
VStack {
LottieRepresentable(named: named, loop: loop)
.frame(width: size, height: size)
}
}
}
So the final code would look like this with a NavigationLink, and you will have your loader starting at the beginning of your api call, and ending when api call succeeds:
import SwiftUI
//MARK: - Content view
struct ContentView: View {
#State var showMessageView = false
#State var loopAnimation = false
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: MessageView(),
isActive: $showMessageView) {
Text("")
VStack {
Button(action: {
self.loopAnimation.toggle()
self.makeApiCall()
}) {
if self.loopAnimation {
Text("")
}
else {
Text("Submit")
}
}
}
if self.loopAnimation {
LottieView(named: "Your lottie json file name",
loop: self.loopAnimation,
size: 50)
}
}
.navigationBarTitle("Content View")
}
}
}
func makeApiCall() {
// your api call
if success {
loopAnimation = false
showMessageView = true
}
}
}

How can I play my lottie animation with an OnTapGesture event?

I followed MengTo's example of how to get a Lottie animation to play within SwiftUI. (https://www.youtube.com/watch?v=fVehE3Jf7K0) However, I was wondering if anyone could help me understand how to initially present the first frame of the animation, but only have the animation play once a user taps on it in a button format.
My current LottieButton file is as follows:
import SwiftUI
import Lottie
struct LottieButton: UIViewRepresentable {
/// Create a button.
let animationButton = AnimatedButton()
var filename = "LottieLogo2"
func makeUIView(context: UIViewRepresentableContext<LottieButton>) -> UIView {
let view = UIView()
let animation = Animation.named(filename)
animationButton.animation = animation
animationButton.contentMode = .scaleAspectFit
animationButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationButton)
animationButton.clipsToBounds = false
/// Set animation play ranges for touch states
animationButton.setPlayRange(fromMarker: "touchDownStart", toMarker: "touchDownEnd", event: .touchUpInside)
animationButton.setPlayRange(fromMarker: "touchDownEnd", toMarker: "touchUpCancel", event: .touchUpOutside)
animationButton.setPlayRange(fromMarker: "touchDownEnd", toMarker: "touchUpEnd", event: .touchUpInside)
NSLayoutConstraint.activate([
animationButton.heightAnchor.constraint(equalTo: view.heightAnchor),
animationButton.widthAnchor.constraint(equalTo: view.widthAnchor),
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieButton>) {
}
}
And then I just have a simple view that is displaying the animation:
struct InboxView: View {
var body: some View {
VStack {
Button(action: {}) {
LottieButton(filename: "inbox-notification")
.frame(width: 100)
}
}
}
}
If you want to show lottie file only when button pressed, you can set #State toggle and show your Lottie when the variable in toggled.
Sample code:
#State var toggleValue = false
var body: some View {
VStack {
Button(action: {
self.toggleValue.toggle()
}) {
VStack {
if toggleValue {
LottieView(filename: "inbox-notification")
.frame(width: 100)
}
Text("Button")
}
}
}
}
After digging deeper into the Lottie documentation, I saw that there was commented code that showed how to implement an animated button. I edited the code above in LottieButton to set up the button and then I was able to place it into a view and get it to animate on tap. Here is the working code:
struct InboxView: View {
var body: some View {
VStack {
LottieButton(filename: "inbox-notification")
.frame(width: 100)
}
}
}

SwiftUI with SceneKit: How to use button action from view to manipulate underlying scene

I’m trying to implement a very basic SwiftUI app with an underlying SceneKit scene/view. The buttons in the SwiftUI view should manipulate the content of the scene and vice versa the content of the scene should determine if the buttons are active or inactive.
Yes, I’ve read and watched the Apple developer sessions 226: Data Flow through SwiftUI and 231: Integrating SwiftUI. And I worked my way through the Apple tutorials. But I’m little lost here. Any hints and directions are appreciated.
Here is the code:
I have a MainView, which uses a SceneKit SCNView with a HUDView on top:
import SwiftUI
struct MainView: View {
var body: some View {
ZStack {
SceneView()
HUDView()
}
}
}
The SceneView integrates a SCNView via the UIViewRepresentable protocol. The scene has two functions to add and remove the box from the scene:
import SwiftUI
import SceneKit
struct SceneView: UIViewRepresentable {
let scene = SCNScene()
func makeUIView(context: Context) -> SCNView {
// create a box
scene.rootNode.addChildNode(createBox())
// code for creating the camera and setting up lights is omitted for simplicity
// …
// retrieve the SCNView
let scnView = SCNView()
scnView.scene = scene
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// configure the view
scnView.backgroundColor = UIColor.gray
// show statistics such as fps and timing information
scnView.showsStatistics = true
scnView.debugOptions = .showWireframe
}
func createBox() -> SCNNode {
let boxGeometry = SCNBox(width: 20, height: 24, length: 40, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
return box
}
func removeBox() {
// check if box exists and remove it from the scene
guard let box = scene.rootNode.childNode(withName: "box", recursively: true) else { return }
box.removeFromParentNode()
}
func addBox() {
// check if box is already present, no need to add one
if scene.rootNode.childNode(withName: "box", recursively: true) != nil {
return
}
scene.rootNode.addChildNode(createBox())
}
}
The HUDView bundles the buttons with actions to add and remove the box from the underlying scene. If the box object is in the scene, the add button should be inactive, only the remove button should be active:
struct HUDView: View {
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .center, spacing: 2) {
Spacer()
ButtonView(action: {}, icon: "plus.square.fill", isActive: false)
ButtonView(action: {}, icon: "minus.square.fill")
ButtonView(action: {}) // just a dummy button
}
.background(Color.white.opacity(0.2))
Spacer()
}
}
}
The buttons are fairly simple as well, they take an action and optionally an icon as well as their initial active/inactive state:
struct ButtonView: View {
let action: () -> Void
var icon: String = "square"
#State var isActive: Bool = true
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title)
.accentColor(isActive ? Color.white : Color.white.opacity(0.5))
}
.frame(width: 44, height: 44)
}
}
The resulting app is pretty simple:
The SceneKit scene is rendered correctly and I am able to manipulate the camera on screen.
But how do I connect the button actions to the corresponding scene functions? How do I let the scene inform the HUDView about it’s content (box is present or not), so it can set the active/inactive state of the buttons?
What is the best approach to this task? Should I implement a Coordinator in SceneView? Should I use a separate SceneViewController implemented via the UIViewControllerRepresentable protocol?
I found a solution using #EnvironmentalObject but I am not completely sure, if this is the right approach. So comments on this are appreciated.
First, I moved the SCNScene into it’s own class and made it an OberservableObject:
class Scene: ObservableObject {
#Published var scene: SCNScene
init(_ scene: SCNScene = SCNScene()) {
self.scene = scene
self.scene = setup(scene: self.scene)
}
// code omitted which deals with setting the scene up and adding/removing the box
// method used to determine if the box node is present in the scene -> used later on
func boxIsPresent() -> Bool {
return scene.rootNode.childNode(withName: "box", recursively: true) != nil
}
}
I inject this Scene into the app as an .environmentalObject(), so it is available to all views:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let sceneKit = Scene()
let mainView = MainView().environmentObject(sceneKit)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: mainView)
self.window = window
window.makeKeyAndVisible()
}
}
}
MainView is slightly altered to call SceneView (a UIViewRepresentable) with the separate Scene for the environment:
struct MainView: View {
#EnvironmentObject var scene: Scene
var body: some View {
ZStack {
SceneView(scene: self.scene.scene)
HUDView()
}
}
}
Then I make the Scene available to the HUDView as an #EnvironmentalObject, so I can reference the scene and its methods and call them from the Button action. Another effect is, I can query the Scene helper method to determine, if a Button should be active or not:
struct HUDView: View {
#EnvironmentObject var scene: Scene
#State private var canAddBox: Bool = false
#State private var canRemoveBox: Bool = true
var body: some View {
VStack {
HStack(alignment: .center, spacing: 0) {
Spacer ()
ButtonView(
action: {
self.scene.addBox()
if self.scene.boxIsPresent() {
self.canAddBox = false
self.canRemoveBox = true
}
},
icon: "plus.square.fill",
isActive: $canAddBox
)
ButtonView(
action: {
self.scene.removeBox()
if !self.scene.boxIsPresent() {
self.canRemoveBox = false
self.canAddBox = true
}
},
icon: "minus.square.fill",
isActive: $canRemoveBox
)
}
.background(Color.white.opacity(0.2))
Spacer()
}
}
}
Here is the ButtonView code, which used a #Bindingto set its active state (not sure about the correct order for this with the#State property inHUDView`):
struct ButtonView: View {
let action: () -> Void
var icon: String = "square"
#Binding var isActive: Bool
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.title)
.accentColor(self.isActive ? Color.white : Color.white.opacity(0.5))
}
.frame(width: 44, height: 44)
.disabled(self.isActive ? false: true)
}
}
Anyway, the code works now. Any thoughts on this?
I answered a very similar question here. The idea is that you create a PassthroughSubject that your parent view (the one that owns the buttons) will send events down and your UIViewRespresentable will subscribe to those events.
There are many ways to do this stuff and you've obviously solved it - congrats!
You might check out this post 58103566 on game design layout. I'm sure there will be experts that disagree which is fine, and I don't want to cause a debate, just thought it might save you a few steps in the future.
From multiple VC's, I manipulate game objects and buttons often, usually without muddying up the water too much.
Hope it helps.

Changing the color of a Button in SwiftUI on tvOS

I am trying to change the color of a SwiftUI Button on tvOS.
Modifying the background almost works, except that you can see that the underlying UIButton is actually using a rounded, translucent image over the top of the background. This results in a different colour at the corners where the rectangular background lies outside the rounded image.
Adding .padding emphasises this issue:
struct ContentView: View {
#State
var selected = false
var body: some View {
Button(action: {
self.selected.toggle()
}) {
Text($selected.wrappedValue ? "On":"Off")
.foregroundColor(.white)
}.padding(.all)
.background(self.selected ? Color.green : Color.blue)
}
}
}
A related issue is the changing the color of the "focus" view, as I suspect that this is the same view as the button itself, transformed win size and color.
The typical technique in tvOS with a UIButton is to change the button image, but there doesn't seem to be any way to access the image of the underlying UIButton.
Again, I have only checked it on iOS, but this should help:
struct ContentView: View {
#State var selected = false
var body: some View {
Button(action: { self.selected.toggle() }) {
Text($selected.wrappedValue ? "On":"Off")
.foregroundColor(.white)
} .padding(.all)
.background(RoundedRectangle(cornerRadius: 5.0)
.fill(self.selected ? Color.green : Color.blue))
}
}
You can pass any view into .background() modifier, so you might also want to try placing an Image() in there.
This code seems to work fine on iOS but, as you've shown, in tvOS the underlying UIButton is visible. I'm not sure how to get at the underlying button or its image, but it seems you can hack it for now (until Apple either fixes the issue or provides a way to get at that button.)
First, move the padding up to modify the Text so that it will properly affect the underlying button.
Second, (and this is the hack) clip the view after the background modifier with a cornerRadius. As long as the radius is equal to or greater than that of the underlying button, it will clip off the excess background. (Of course, what you see is not the green Color, but the color resulting from the green color superimposed on the gray of the translucent button image. At least that's how it's shown in Xcode.)
struct ContentView: View {
#State var selected = false
var body: some View {
Button(action: {
self.selected.toggle()
}) {
Text($selected.wrappedValue ? "On":"Off")
.foregroundColor(.white)
.padding(.all)
}
.background(self.selected ? Color.green : Color.blue)
.cornerRadius(5)
}
}
Here is a working solution for tvOS on Swift 5.5
struct TVButton: View {
// MARK: - Variables
#FocusState private var isFocused: Bool
#State private var buttonScale: CGFloat = 1.0
var title: String?
var bgColor: Color = .white
var action: () -> Void
// MARK: - Body
var body: some View {
TVRepresentableButton(title: title, color: UIColor(bgColor)) {
action()
}
.focused($isFocused)
.onChange(of: isFocused) { newValue in
withAnimation(Animation.linear(duration: 0.2)) {
buttonScale = newValue ? 1.1 : 1.0
}
}
.scaleEffect(buttonScale)
}
}
struct TVRepresentableButton: UIViewRepresentable {
// MARK: - Typealias
typealias UIViewType = UIButton
// MARK: - Variables
var title: String?
var color: UIColor
var action: () -> Void
func makeUIView(context: Context) -> UIButton {
let button = UIButton(type: .system)
button.setBackgroundImage(UIImage(color: color), for: .normal)
button.setBackgroundImage(UIImage(color: color.withAlphaComponent(0.85)), for: .focused)
button.setBackgroundImage(UIImage(color: color), for: .highlighted)
button.setTitle(title, for: .normal)
button.addAction(.init(handler: { _ in action() }), for: .allEvents)
return button
}
func updateUIView(_ uiView: UIButton, context: Context) {}
}
extension UIImage {
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
}
now you are able to change Title, background color, scale and handle tap action on tvOS
Try:
button.setBackgroundImage(UIImage.imageWithColor(.lightGray), for: .normal)
You can st it for .focused and .highlighted