I am trying to integrate UIGestureRecognizer into my swift playground so that wherever I tap in my playground, my sprite goes to that point. I've tried everything! Watching countless youtube videos, and going on stack overflow for answers but everyone starts with making a class called view that is a UIView and in their code they keep referring to "self". I instead made a variable named "view" that was a SKView. I try to get parts of their code, put it in mine, and change it, but it just doesn't work. Here's the work that I got so far.
view.addGestureRecognizer(UIGestureRecognizer(target: view, action: #selector(handleTap(sender:))))
func handleTap(sender: UITapGestureRecognizer) {
player.position = sender.location(in: view)
}
My playground keeps telling me that i'm using an unresolved identifier 'handleTap(sender:)'
UIGestureRecognizer example with different states of Gesture recogniser in Playground
swift3+
import UIKit
import PlaygroundSupport
class ViewController : UIViewController {
var yellowView: UIView!
var redView: UIView!
var yellowViewOrigin: CGPoint!
override func loadView() {
// UI
let view = UIView()
view.backgroundColor = .white
yellowView = UIView()
yellowView.backgroundColor = .yellow
view.addSubview(yellowView)
redView = UIView()
redView.backgroundColor = .red
view.addSubview(redView)
// Layout
redView.translatesAutoresizingMaskIntoConstraints = false
yellowView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
yellowView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20.0),
yellowView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0),
yellowView.heightAnchor.constraint(equalToConstant: 80.0),
yellowView.widthAnchor.constraint(equalToConstant: 80.0),
redView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20.0),
redView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0),
redView.heightAnchor.constraint(equalToConstant: 80.0),
redView.widthAnchor.constraint(equalToConstant: 80.0)
])
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
let pan = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(_:)))
yellowView.addGestureRecognizer(pan)
yellowViewOrigin = yellowView.frame.origin
view.bringSubview(toFront: yellowView)
}
#objc func handlePanGesture(_ sender: UIPanGestureRecognizer) {
let targetView = sender.view!
let translation = sender.translation(in: view)
switch sender.state {
case .began,.changed:
targetView.center = CGPoint(x: targetView.center.x + translation.x
,y: targetView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: view)
case .ended:
if targetView.frame.intersects(redView.frame){
UIView.animate(withDuration: 0.3) {
targetView.alpha = 0.0
}
}
else{
UIView.animate(withDuration: 0.3) {
targetView.frame.origin = self.yellowViewOrigin
}
}
break
default:
break
}
}
}
PlaygroundPage.current.liveView = ViewController()
I think you need to add the #objc mark in front of the function declaration, so that handleTap will be visible to #selector.
#objc func handleTap(sender: UITapGestureRecognizer) {
player.position = sender.location(in: view)
}
func handleTap has to be called from your view type.
Please try to add it inside an SKView extension:
extension SKView {
func handleTap(sender: UITapGestureRecognizer) {
player.position = sender.location(in: view)
}
}
Related
I am trying to make a WYSIWYG editor by interfacing between SwiftUI and UIKit via a UIViewRepresentable. I am primarily using SwiftUI but am using UIKit here as it seems SwiftUI does not currently support the functionality needed.
My problem is, when I set the NSMutableAttributedString to be already containing a string with attributes, if I then select that text in the UIViewRepresentable before typing any new text and press the underline button in the UIToolBar to add the attribute, the attribute is added to the NSMutableAttributedString but the UIView does not update to show the updated NSMutableAttributedString. However, if I type a single character and then select the text and add the underline attribute, the UIView updates.
Could someone explain why this is and maybe point me towards a solution? Any help would be greatly appreciated.
Below is the code:
import SwiftUI
import UIKit
struct ContentView: View {
#State private var mutableAttributedString: NSMutableAttributedString = NSMutableAttributedString(
string: "this is the string before typing anything new",
attributes: [.foregroundColor: UIColor.blue])
var body: some View {
EditorExample(outerMutableString: $mutableAttributedString)
}
}
struct EditorExample: UIViewRepresentable {
#Binding var outerMutableString: NSMutableAttributedString
#State private var outerSelectedRange: NSRange = NSRange()
func makeUIView(context: Context) -> some UITextView {
// make UITextView
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: 30.0)
textView.delegate = context.coordinator
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: context.coordinator,
action: #selector(context.coordinator.underline))
toolBar.items = [underlineButton]
textView.inputAccessoryView = toolBar
return textView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.attributedText = outerMutableString
}
func makeCoordinator() -> Coordinator {
Coordinator(innerMutableString: $outerMutableString, selectedRange: $outerSelectedRange)
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var innerMutableString: NSMutableAttributedString
#Binding var selectedRange: NSRange
init(innerMutableString: Binding<NSMutableAttributedString>, selectedRange: Binding<NSRange>) {
self._innerMutableString = innerMutableString
self._selectedRange = selectedRange
}
func textViewDidChange(_ textView: UITextView) {
innerMutableString = textView.textStorage
}
func textViewDidChangeSelection(_ textView: UITextView) {
selectedRange = textView.selectedRange
}
#objc func underline() {
if (selectedRange.length > 0) {
innerMutableString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: selectedRange)
}
}
}
}
It's not working because NSAttributedString is a class and #State is for value types like structs. This means the dependency tracking is broken and things won't update correctly.
Also your UIViewRepresentable and Coordinator design is non-standard so I thought I would share an example of the correct way to do it. The binding is change to a string, which is a value type so it's working (minus the underline feature obviously).
struct ContentView: View {
//#State private var mutableAttributedString: NSMutableAttributedString = NSMutableAttributedString(
// string: "this is the string before typing anything new",
// attributes: [.foregroundColor: UIColor.blue])
#State var string = "this is the string before typing anything new"
var body: some View {
VStack {
// EditorExample(outerMutableString: $mutableAttributedString)
// EditorExample(outerMutableString: $mutableAttributedString) // a second to test bindings are working\
//Text(mutableAttributedString.string)
EditorExample(outerMutableString2: $string)
EditorExample(outerMutableString2: $string)
}
}
}
struct EditorExample: UIViewRepresentable {
//#Binding var outerMutableString: NSMutableAttributedString
#Binding var outerMutableString2: String
// this is called first
func makeCoordinator() -> Coordinator {
// we can't pass in any values to the Coordinator because they will be out of date when update is called the second time.
Coordinator()
}
// this is called second
func makeUIView(context: Context) -> UITextView {
context.coordinator.textView
}
// this is called third and then repeatedly every time a let or `#Binding var` that is passed to this struct's init has changed from last time.
func updateUIView(_ uiView: UITextView, context: Context) {
//uiView.attributedText = outerMutableString
uiView.text = outerMutableString2
// we don't usually pass bindings in to the coordinator and instead use closures.
// we have to set a new closure because the binding might be different.
context.coordinator.stringDidChange2 = { string in
outerMutableString2 = string
}
}
class Coordinator: NSObject, UITextViewDelegate {
lazy var textView: UITextView = {
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: 30.0)
textView.delegate = self
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: self,
action: #selector(underline))
toolBar.items = [underlineButton]
textView.inputAccessoryView = toolBar
return textView
}()
//var stringDidChange: ((NSMutableAttributedString) -> ())?
var stringDidChange2: ((String) -> ())?
func textViewDidChange(_ textView: UITextView) {
//innerMutableString = textView.textStorage
//stringDidChange?(textView.textStorage)
stringDidChange2?(textView.text)
}
func textViewDidChangeSelection(_ textView: UITextView) {
// selectedRange = textView.selectedRange
}
#objc func underline() {
let range = textView.selectedRange
if (range.length > 0) {
textView.textStorage.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range)
// stringDidChange?(textView.textStorage)
}
}
}
}
Does anyone have working Swiftui code that will produce a search bar in the navigation bar that is inline with the back button? As if it is a toolbar item.
Currently I have code that will produce a search bar below the navigation back button but would like it in line like the picture attached shows (where the "hi" is):
I am using code that I found in an example:
var body: some View {
let shopList = genShopList(receiptList: allReceipts)
VStack{
}
.navigationBarSearch(self.$searchInput)
}
public extension View {
public func navigationBarSearch(_ searchText: Binding<String>) -> some View {
return overlay(SearchBar(text: searchText)
.frame(width: 0, height: 0))
}
}
fileprivate struct SearchBar: UIViewControllerRepresentable {
#Binding
var text: String
init(text: Binding<String>) {
self._text = text
}
func makeUIViewController(context: Context) -> SearchBarWrapperController {
return SearchBarWrapperController()
}
func updateUIViewController(_ controller: SearchBarWrapperController, context: Context) {
controller.searchController = context.coordinator.searchController
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UISearchResultsUpdating {
#Binding
var text: String
let searchController: UISearchController
private var subscription: AnyCancellable?
init(text: Binding<String>) {
self._text = text
self.searchController = UISearchController(searchResultsController: nil)
super.init()
searchController.searchResultsUpdater = self
searchController.hidesNavigationBarDuringPresentation = true
searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchBar.text = self.text
self.subscription = self.text.publisher.sink { _ in
self.searchController.searchBar.text = self.text
}
}
deinit {
self.subscription?.cancel()
}
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
self.text = text
}
}
class SearchBarWrapperController: UIViewController {
var searchController: UISearchController? {
didSet {
self.parent?.navigationItem.searchController = searchController
}
}
override func viewWillAppear(_ animated: Bool) {
self.parent?.navigationItem.searchController = searchController
}
override func viewDidAppear(_ animated: Bool) {
self.parent?.navigationItem.searchController = searchController
}
}
}
If anyone has a solution to this problem that would be greatly appreciated! I know that in IoS 15 they are bringing out .searchable but looking for something that will work for earlier versions too.
You can put any control in the position you want by using the .toolbar modifier (iOS 14+) and an item with .principal placement, e.g.:
var body: some View {
VStack {
// rest of view
}
.toolbar {
ToolbarItem(placement: .principal) {
MySearchField(text: $searchText)
}
}
}
A couple of things to note:
The principal position overrides an inline navigation title, either when it's set with .navigationBarTitleDisplayMode(.inline) or when you have a large title and scroll up the page.
It's possible that your custom view expands horizontally so much that the back button loses any text component. Here, I used a TextField to illustrate the point:
You might be able to mitigate for that by assigning a maximum width with .frame(maxWidth:), but at the very least it's something to be aware of.
I want to open a 3D model and make its background transparent, so that I can see the UI behind the SceneView. I've tried this code, but sceneView becomes white, not transparent.
struct ModelView: View {
var body: some View {
ZStack {
Text("Behind Text Behind Text Behind Text")
SceneView(
scene: { () -> SCNScene in
let scene = SCNScene()
scene.background.contents = UIColor.clear
return scene
}(),
pointOfView: { () -> SCNNode in
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
return cameraNode
}(),
options: [
.allowsCameraControl,
.temporalAntialiasingEnabled,
]
)
}
}
}
I use XCode 12.5 and IPhone 8.
EDIT 1:
Thanks to the comments below, I decided to try new approaches but they still don't work.
Approach #1
First, I tried to create a MySceneView using SCNView through UIViewRepresentable:
struct MySceneView: UIViewRepresentable {
typealias UIViewType = SCNView
typealias Context = UIViewRepresentableContext<MySceneView>
func updateUIView(_ uiView: UIViewType, context: Context) {}
func makeUIView(context: Context) -> UIViewType {
let view = SCNView()
view.allowsCameraControl = true
view.isTemporalAntialiasingEnabled = true
view.autoenablesDefaultLighting = true
view.scene = MySceneView.scene
return view
}
static let scene: SCNScene = {
let scene = SCNScene(named: "art.scnassets/man.obj")!
scene.background.contents = UIColor.clear
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
scene.rootNode.addChildNode(cameraNode)
return scene
}()
}
Approach #2
I tried using SpriteView, here is the code:
ZStack {
Text("Behind Text Behind Text Behind Text")
SpriteView(scene: { () -> SKScene in
let scene = SKScene()
scene.backgroundColor = UIColor.clear
let model = SK3DNode(viewportSize: .init(width: 200, height: 200))
model.scnScene = MySceneView.scene
scene.addChild(model)
return scene
}(), options: [.allowsTransparency])
}
Update:
A much simpler solution is to use UIViewRepresentable, create SCNView and set backgroundColor to clear
Old:
Thanks George_E, your idea with SpriteKit worked perfectly. Here is the code:
SpriteView(scene: { () -> SKScene in
let scene = SKScene()
scene.backgroundColor = UIColor.clear
let model = SK3DNode(viewportSize: .init(width: 200, height: 200))
model.scnScene = {
let scene = SCNScene(named: "art.scnassets/man.obj")!
scene.background.contents = UIColor.clear
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
scene.rootNode.addChildNode(cameraNode)
return scene
}()
scene.addChild(model)
return scene
}(), options: [.allowsTransparency])
I wasn't able to find a fully working snippet here, but thanks to the answers from Arutyun I managed to compile a working solution without the need for SpriteKit.
import SceneKit
import SwiftUI
struct MySceneView: UIViewRepresentable {
typealias UIViewType = SCNView
typealias Context = UIViewRepresentableContext<MySceneView>
func updateUIView(_ uiView: UIViewType, context: Context) {}
func makeUIView(context: Context) -> UIViewType {
let view = SCNView()
view.backgroundColor = UIColor.clear // this is key!
view.allowsCameraControl = true
view.autoenablesDefaultLighting = true
// load the object here, could load a .scn file too
view.scene = SCNScene(named: "model.obj")!
return view
}
}
And use it just like a regular view:
import SwiftUI
struct MySceneView: View {
var body: some View {
ZStack{
// => background views here
MySceneView()
.frame( // set frame as required
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .center
)
}
}
}
struct MySceneView_Previews: PreviewProvider {
static var previews: some View {
MySceneView()
}
}
With SwiftUI:
It is slightly different in SwiftUI using a SpriteView.
To implement a transparent SpriteView in SwiftUI, you have to use the 'options' parameter:
Configure your SKScene with clear background (view AND scene)
Configure your SpriteView with the correct option
// 1. configure your scene in 'didMove'
override func didMove(to view: SKView) {
self.backgroundColor = .clear
view.backgroundColor = .clear
}
and most importantly:
// 2. configure your SpriteView with 'allowsTranspanency'
SpriteView(scene: YourSKScene(), options: [.allowsTransparency])
I didn't like using SpriteKit to make a SceneKit scenes background contents transparent because you completely loose access to the SCNView. Here is what I believe to be the correct approach.
Create a Scene Kit Scene file named GameScene.scn
Drop your 3D object into the GameScene
Use the code below
/// The SCNView view
struct GameSceneView: UIViewRepresentable {
#ObservedObject var viewModel: GameSceneViewModel
func makeUIView(context: UIViewRepresentableContext<GameSceneView>) -> SCNView {
let view = SCNView()
view.backgroundColor = viewModel.backgroundColor
view.allowsCameraControl = viewModel.allowsCameraControls
view.autoenablesDefaultLighting = viewModel.autoenablesDefaultLighting
view.scene = viewModel.scene
return view
}
func updateUIView(_ uiView: SCNView, context: UIViewRepresentableContext<GameSceneView>) {}
}
/// The view model supplying the SCNScene and its properties
class GameSceneViewModel: ObservableObject {
#Published var scene: SCNScene?
#Published var backgroundColor: UIColor
#Published var allowsCameraControls: Bool
#Published var autoenablesDefaultLighting: Bool
init(
sceneName: String = "GameScene.scn",
cameraName: String = "camera",
backgroundColor: UIColor = .clear,
allowsCameraControls: Bool = true,
autoenablesDefaultLighting: Bool = true
) {
self.scene = SCNScene(named: sceneName)
self.backgroundColor = backgroundColor
self.allowsCameraControls = allowsCameraControls
self.autoenablesDefaultLighting = autoenablesDefaultLighting
scene?.background.contents = backgroundColor
scene?.rootNode.childNode(withName: cameraName, recursively: false)
}
}
/// Usage
struct ContentView: View {
var body: some View {
VStack {
GameSceneView(viewModel: GameSceneViewModel())
}
.background(Color.blue)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
With the help of the following, I was able to follow the button on the keyboard display.
However, animation cannot be applied well.
How to show complete List when keyboard is showing up in SwiftUI
import SwiftUI
import Combine
import UIKit
class KeyboardResponder: ObservableObject {
let willset = PassthroughSubject<CGFloat, Never>()
private var _center: NotificationCenter
#Published var currentHeight: CGFloat = 0
var keyboardDuration: TimeInterval = 0
init(center: NotificationCenter = .default) {
_center = center
_center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
_center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
deinit {
_center.removeObserver(self)
}
#objc func keyBoardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
currentHeight = keyboardSize.height
guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
keyboardDuration = duration
}
}
#objc func keyBoardWillHide(notification: Notification) {
currentHeight = 0
}
}
import SwiftUI
struct Content: View {
#ObservedObject var keyboard = KeyboardResponder()
var body: some View {
VStack {
Text("text")
Spacer()
NavigationLink(destination: SubContentView()) {
Text("Done")
}
}
.padding(.bottom, keyboard.currentHeight)
animation(Animation.easeInOut(duration: keyboard.keyboardDuration))
}
}
enter image description here
Your main problem, is that you are using an implicit animation. Not only it may be animating things you may not want to animate, but also, you should never apply .animation() on containers. Of the few warnings in SwiftUI's documentation, this is one of them:
Use this modifier on leaf views rather than container views. The
animation applies to all child views within this view; calling
animation(_:) on a container view can lead to unbounded scope.
Source: https://developer.apple.com/documentation/swiftui/view/3278508-animation
The modified code removes the implicit .animation() call and replaces it with two implicit withAnimation closures.
I also replaced keyboardFrameEndUserInfoKey with keyboardFrameEndUserInfoKey, second calls were giving useless geometry.
class KeyboardResponder: ObservableObject {
let willset = PassthroughSubject<CGFloat, Never>()
private var _center: NotificationCenter
#Published var currentHeight: CGFloat = 0
var keyboardDuration: TimeInterval = 0
init(center: NotificationCenter = .default) {
_center = center
_center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
_center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
deinit {
_center.removeObserver(self)
}
#objc func keyBoardWillShow(notification: Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
keyboardDuration = duration
withAnimation(.easeInOut(duration: duration)) {
self.currentHeight = keyboardSize.height
}
}
}
#objc func keyBoardWillHide(notification: Notification) {
guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
withAnimation(.easeInOut(duration: duration)) {
currentHeight = 0
}
}
}
struct ContentView: View {
#ObservedObject var keyboard = KeyboardResponder()
var body: some View {
return VStack {
Text("text \(keyboard.currentHeight)")
TextField("Enter text", text: .constant(""))
Spacer()
NavigationLink(destination: Text("SubContentView()")) {
Text("Done")
}
}
.padding(.bottom, keyboard.currentHeight)
// .animation(Animation.easeInOut(duration: keyboard.keyboardDuration))
}
}
I have been struggling with this issue for a few hours. I can't seem to understand the procedure of using a UITapGestureRecognizer. Any help would be appreciated.
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
let textInView = "This is my text."
textView.text = textInView
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapResponse(_:)))
tapGesture.numberOfTapsRequired = 1
textView.addGestureRecognizer(tapGesture)
func tapResponse(sender: UITapGestureRecognizer) {
var location: CGPoint = sender.location(in: textView)
location.x = textView.textContainerInset.left
location.y = textView.textContainerInset.top
print(location.x)
print(location.y)
}
}
You have a function (tapResponse) inside a function (viewDidLoad).
Put it outside of viewDidLoad. Then reference it like so:
override func viewDidLoad() {
super.viewDidLoad() // Remember to always call super.
let tapGesture = UITapGestureRecognizer(
target: self,
action: #selector(ViewController.tapResponse(sender:)) // Referencing.
)
tapGesture.numberOfTapsRequired = 1
textView.addGestureRecognizer(tapGesture)
}
// tapResponse is now outside of viewDidLoad:
func tapResponse(sender: UITapGestureRecognizer) {
var location: CGPoint = sender.location(in: imageView)
location.x = textView.textContainerInset.left
location.y = textView.textContainerInset.top
print(location.x)
print(location.y)
}
Done: