Start SwiftUI implementation of UIScrollView at specific scroll and zoom - swiftui

I have some experience in SwiftUI, but am new to UIKit.
I'd like to import the zoom and position from one instance of an UIViewRepresentable UIKit ScrollView to another. So, basically, the user scrolls and zooms and later, in another branch of the view hierarchy, I want to start zoomed in at that zoom and position. I can't get it to work though, even after many attempts.
Below is my makeUIView function where I try to set the position and zoom that I want (after some initial setup).
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator
scrollView.bouncesZoom = true
scrollView.delaysContentTouches = false
scrollView.maximumZoomScale = 0.85 * screenScale * 10
scrollView.minimumZoomScale = 0.85 * screenScale
// create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
scrollView.addSubview(hostedView)
/*
Here I add the zoom and position
*/
scrollView.zoomScale = 0.85 * screenscale
// add zoom and content offset
if let zoomScale = zoomScale, let contentOffset = contentOffset {
scrollView.contentOffset = contentOffset
// make sure it is within the bounds
var newZoomScale = zoomScale
if zoomScale < scrollView.minimumZoomScale {
print("too small")
newZoomScale = scrollView.minimumZoomScale
} else if zoomScale > scrollView.maximumZoomScale {
print("too large")
newZoomScale = scrollView.maximumZoomScale
}
scrollView.setContentOffset(contentOffset, animated: true)
scrollView.setZoomScale(newZoomScale, animated: true)
}
return scrollView
}
The way I get the zoom and contentOffset in the first place is to grab the values from the Coordinator in first ScrollView instance using the below code. As far as I can tell this works well and I get updates with sensible values after zooming or scrolling. The first code snippet contains the makeCoordinator function where I initiate the coordinator with methods from an environmentObject (which then updates said object). The second snippet contains the Coordinator.
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content),
userScrolledAction: drawingModel.userScrollAction,
userZoomedAction: drawingModel.userZoomAction)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
let userScrolledAction: (CGPoint) -> Void
let userZoomedAction: (CGFloat) -> Void
init(hostingController: UIHostingController<Content>, userScrolledAction: #escaping (CGPoint) -> Void, userZoomedAction: #escaping (CGFloat) -> Void) {
self.hostingController = hostingController
self.userScrolledAction = userScrolledAction
self.userZoomedAction = userZoomedAction
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
userScrolledAction(scrollView.contentOffset)
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
userZoomedAction(scrollView.zoomScale)
}
}

Related

Is there an easy way to pinch to zoom and drag any View in SwiftUI?

I have been looking for a short, reusable piece of code that allows to zoom and drag any view in SwiftUI, and also to change the scale independently.
This would be the answer.
The interesting part that I add is that the scale of the zoomed View can be controled from outside via a binding property. So we don't need to depend just on the pinching gesture, but can add a double tap to get the maximum scale, return to the normal scale, or have a slider (for instance) that changes the scale as we please.
I owe the bulk of this code to jtbandes in his answer to this question.
Here you have in a single file the code of the Zoomable and Scrollable view and a Test View to show how it works:
`
import SwiftUI
let maxAllowedScale = 4.0
struct TestZoomableScrollView: View {
#State private var scale: CGFloat = 1.0
var doubleTapGesture: some Gesture {
TapGesture(count: 2).onEnded {
if scale < maxAllowedScale / 2 {
scale = maxAllowedScale
} else {
scale = 1.0
}
}
}
var body: some View {
VStack(alignment: .center) {
Spacer()
ZoomableScrollView(scale: $scale) {
Image("foto_producto")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
}
.frame(width: 300, height: 300)
.border(.black)
.gesture(doubleTapGesture)
Spacer()
Text("Change the scale")
Slider(value: $scale, in: 0.5...maxAllowedScale + 0.5)
.padding(.horizontal)
Spacer()
}
}
}
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
#Binding private var scale: CGFloat
init(scale: Binding<CGFloat>, #ViewBuilder content: () -> Content) {
self._scale = scale
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = maxAllowedScale
scrollView.minimumZoomScale = 1
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.bouncesZoom = true
// Create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content), scale: $scale)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
uiView.zoomScale = scale
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
#Binding var scale: CGFloat
init(hostingController: UIHostingController<Content>, scale: Binding<CGFloat>) {
self.hostingController = hostingController
self._scale = scale
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
self.scale = scale
}
}
}
`
I think it's the shortest, easiest way to get the desired behaviour. Also, it works perfectly, something that I haven't found in other solutions offered here. For example, the zooming out is smooth and usually it can be jerky if you don't use this approach.
The slider hast that range to show how the minimun and maximum values are respected, in a real app the range would be 1...maxAllowedScale.
As for the double tap, the behaviour can be changed very easily depending pm what you prefer.
I attach video to show everything at once:
I hope this helps anyone who's looking for this feature.

SwiftUI - MKMapView - Display map center coordinate in a #EnvironmentObject variable

can someone please help me with my problem?
I have a SwiftUI Project and wrote a MKMapView File. I would like to store the Center coordinates in a #EnvironmentObject variable.
Unfortunately, I can't manage to pack the center coordinates into the EnvironmentObject variable.
my code looks like this:
struct MapView: UIViewRepresentable {
#EnvironmentObject var messpunkt_manager: Messpunkt_Manager
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.showsUserLocation = true // zeigt den aktuellen Standort des Users
map.delegate = context.coordinator // ermöglich die Bedienung
// shows annotations already in array
for location in messpunkt_manager.messpunkte {
let annotation = MKPointAnnotation()
annotation.title = location.title
annotation.coordinate = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)
map.addAnnotation(annotation)
}
return map
}
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapView>) {
// start coordinates
let coordinate = CLLocationCoordinate2D(latitude: messpunkt_manager.centerCoordinate.latitude, longitude: messpunkt_manager.centerCoordinate.longitude)
let span = MKCoordinateSpan(latitudeDelta: messpunkt_manager.centerZoom.latitudeDelta, longitudeDelta: messpunkt_manager.centerZoom.longitudeDelta)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
// Adds annotation to the map
for location in messpunkt_manager.messpunkte {
let annotation = MKPointAnnotation()
annotation.title = location.title
annotation.coordinate = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)
uiView.addAnnotation(annotation)
}
if messpunkt_manager.karteAusgeaehlt == 0 {
uiView.mapType = .standard
}
if messpunkt_manager.karteAusgeaehlt == 1 {
uiView.mapType = .hybrid
}
if messpunkt_manager.karteAusgeaehlt == 2 {
uiView.mapType = .satellite
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
the center coordinates should then be put into this #EnvironmentObject variable
#Published var position = CLLocationCoordinate2D()
thanks for your help!
And sorry for my naming. For me its easier to name the variables, functions ... in my native language German. I hope its not a big deal.
In your class Coordinator you need to set the value. Assuming your coordinator is set up like this:
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
...
}
Then you would add this function:
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
parent. messpunkt_manager.position = mapView.region
}
It is really helpful to post a Minimal Reproducible Example (MRE). As you can see, you left out an important piece

ZoomableScrollView no longer works in SwiftUI

Using Swift 5.5, Xcode 13.0, iOS 15.0.1,
Since Apple does not a good job on ScrollViews for SwiftUI (yet?), I had to implement my own zoomable ScrollView. See code below.
I had it successfully running for iOS13 and iOS14 - but now updating to iOS 15.0.1 I had to realise that it no longer works !
The error message is as follows:
objc[10882]: Cannot form weak reference to instance (0x1078d5540) of class _TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_5ImageVS_18_AspectRatioLayout__. It is possible that this object was over-released, or is in the process of deallocation.
dyld4 config: DYLD_LIBRARY_PATH=/usr/lib/system/introspection DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktraceRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
(lldb)
I have everything inside a SwiftUIPager, and the problem occurs on the very last page when swiping. However, I don't think it is the Pagers fault since I can replace the ZoomableScrolView by a normal ImageView and everything works. Therefore I think Apple messed something up in their UIViewRepresentable. But maybe you can tell me more ??
Here is the entire code of my ZoomableScrolView:
import SwiftUI
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
#Binding var didZoom: Bool
private var content: Content
init(didZoom: Binding<Bool>, #ViewBuilder content: () -> Content) {
_didZoom = didZoom
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
// create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .black
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content), didZoom: $didZoom)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
#Binding var didZoom: Bool
init(hostingController: UIHostingController<Content>, didZoom: Binding<Bool>) {
self.hostingController = hostingController
_didZoom = didZoom
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
didZoom = !(scrollView.zoomScale == scrollView.minimumZoomScale)
}
}
}
I finally found a workaround.
Add the following code inside the ZoomableScrollView-UIViewRepresentable class:
static func dismantleUIView(_ uiView: UIScrollView, coordinator: Coordinator) {
uiView.delegate = nil
coordinator.hostingController.view = nil
}

Simultaneous Gestures not working for a ZoomableScrollView in SwiftUI

Using Swift5.3.2, iOS14.4.1, Xcode12.4,
I am trying to use the .simultaneousGesture modifier in SwiftUI.
As far as I understood, this modifier should make sure that gestures (such as tap, longpress, magnification etc) should be able to co-exist within a View.
In my example I am using a ZoomableScrollView. And it works fine as long as I do not use the simultaneousGesture.
But as soon as I use the extra simultaneousGesture, the ZoomableScrollView is no longer "zoomable" (i.e. none of its gestures work anymore).
What can I do to make the zoom still work AND get an extra dragGesture ?
import SwiftUI
struct MediaTabView: View {
#GestureState private var dragOffset: CGFloat = -100
var body: some View {
ZoomableScrollView {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.simultaneousGesture(
DragGesture()
.updating($dragOffset) { (value, gestureState, transaction) in
let delta = value.location.x - value.startLocation.x
if delta > 10 { // << some appropriate horizontal threshold here
gestureState = delta
print(delta)
}
}
.onEnded {
if $0.translation.width > 100 {
// Go to the previous slide
print("on ended")
}
}
)
}
}
The code for the ZoomableScrollView is here :
import SwiftUI
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
// create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .black
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}

I get this error when I try to add TapGestureRecognizer to my UIImageView : Unrecognized selector sent to class

I have this DiaryCell (collectionviewcell) class, and I am trying to add a gesture recognizer in that class to call the method of a collectionViewController, which is a different class.
class DiaryCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//imageview for favorite button
let favoriteImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = #imageLiteral(resourceName: "favorite_gray")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
//imageview for menu button
let menuImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = #imageLiteral(resourceName: "menu_image")
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
}
I have tried different strategies, but I am not able to add the gesture recognizer to menuImageView. I have tried doing this, where HomeCollectionViewController is the controller where I want to handle the action when image is tapped.
imageView.addGestureRecognizer(UITapGestureRecognizer(target: HomeCollectionViewController.self, action: #selector(HomeCollectionViewController.menuBarPressed))
The problem is that while your code is legal and compiles, it's wrong.
You are setting up your gesture to be send to a type not an instance. You need to set the target as the actual instance of HomeCollectionViewController
One way to achieve this is by setting a weak reference to your controller in the cell.
class DiaryCell: UICollectionViewCell {
var tapGesture:UITapGestureRecognizer?
weak var gestureTarget: HomeCollectionViewController? {
didSet {
setupGestures()
}
}
func setupGestures() {
if let tapGesture = tapGesture {
removeGestureRecognizer(tapGesture)
self.tapGesture = nil
}
if let gestureTarget = gestureTarget {
let gesture = UITapGestureRecognizer(target: gestureTarget, action: #selector(HomeCollectionViewController.menuBarPressed))
addGestureRecognizer(gesture)
tapGesture = gesture
}
}
}
You would probably set the gestureTarget in your collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell method
While this solves your problem its not a good way to approach things and you should look up how to use delegation as a coding pattern.